2024-06-29 17:12:13 -04:00
#!/usr/bin/env python3
# This file is part of I38.
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
import os
import configparser
from pathlib import Path
from collections import defaultdict
import gi
gi . require_version ( ' Gtk ' , ' 3.0 ' )
from gi . repository import Gtk , Gdk
def read_desktop_files ( paths ) :
desktop_entries = [ ]
for path in paths :
for file in Path ( path ) . rglob ( ' *.desktop ' ) :
config = configparser . ConfigParser ( interpolation = None )
config . read ( file )
desktop_entries . append ( config )
return desktop_entries
user_applications_path = Path . home ( ) / ' .local/share/applications '
system_applications_path = Path ( ' /usr/share/applications ' )
desktop_entries = read_desktop_files ( [ user_applications_path , system_applications_path ] )
# Combine some of the categories
category_map = {
" 2DGraphics " : " Office " ,
" Accessibility " : " Settings " ,
" Audio " : " Audio and Video " ,
" AudioVideo " : " Audio and Video " ,
" AudioVideoEditing " : " Audio and Video " ,
" ActionGame " : " Games " ,
" Building " : " Development " ,
" Calendar " : " Office " ,
" Chat " : " Communication " ,
" Database " : " Office " ,
" DesktopSettings " : " Settings " ,
" Emulator " : " Games " ,
" FlowChart " : " Office " ,
" Game " : " Games " ,
" HardwareSettings " : " Settings " ,
" IDE " : " Development " ,
" InstantMessaging " : " Communication " ,
" Math " : " Education " ,
" Midi " : " Audio and Video " ,
" Mixer " : " Audio and Video " ,
" Player " : " Audio and Video " ,
" Presentation " : " Office " ,
" Recorder " : " Audio and Video " ,
" Science " : " Education " ,
" Spreadsheet " : " Office " ,
" Telephony " : " Communication " ,
" Terminal " : " Utilities " ,
" TerminalEmulator " : " Utilities " ,
" TV " : " Audio and Video " ,
" Utility " : " Utilities " ,
" VectorGraphics " : " Office " ,
" Video " : " Audio and Video " ,
" WebDevelopment " : " Development " ,
" WordProcessor " : " Office " ,
" X-Alsa " : " Audio and Video " ,
" X-Fedora " : " Utilities " ,
" X-Jack " : " Audio and Video " ,
" X-LXDE-Settings " : " Settings " ,
" X-MATE-NetworkSettings " : " Settings " ,
" X-MATE-PersonalSettings " : " Settings " ,
" X-Red-Hat-Base " : " Utilities " ,
" X-SuSE-Core-Office " : " Office " ,
" X-XFCE " : " XFCE " ,
" X-XFCE-HardwareSettings " : " Settings " ,
" X-XFCE-PersonalSettings " : " Settings " ,
" X-XFCE-SettingsDialog " : " Settings " ,
" X-Xfce " : " XFCE " ,
" X-Xfce-Toplevel " : " XFCE " ,
}
categories = defaultdict ( set )
for entry in desktop_entries :
try :
entry_categories = entry . get ( ' Desktop Entry ' , ' Categories ' ) . split ( ' ; ' )
for category in entry_categories :
combined_category = category_map . get ( category , category )
name = entry . get ( ' Desktop Entry ' , ' Name ' )
exec_command = entry . get ( ' Desktop Entry ' , ' Exec ' )
# Use a tuple of name and exec_command as a unique identifier
if ( name , exec_command ) not in categories [ combined_category ] :
categories [ combined_category ] . add ( ( name , exec_command ) )
except configparser . NoOptionError :
continue
2024-06-29 19:00:36 -04:00
class Xdg_Menu_Window ( Gtk . Window ) :
2024-06-29 17:12:13 -04:00
def __init__ ( self ) :
super ( ) . __init__ ( title = " I38 Menu " )
self . set_default_size ( 400 , 300 )
self . set_border_width ( 10 )
self . store = Gtk . TreeStore ( str , str ) # Columns: Category/Application Name, Exec Command
2024-06-29 19:00:36 -04:00
sorted_categories = sorted ( categories . items ( ) ) # Sort categories alphabetically
for category , entries in sorted_categories :
2024-06-29 17:12:13 -04:00
if category == " " :
continue
category_iter = self . store . append ( parent = None , row = [ category , None ] )
sorted_entries = sorted ( entries , key = lambda e : e [ 0 ] ) # Sort entries by name
for name , exec_command in sorted_entries :
self . store . append ( parent = category_iter , row = [ name , exec_command ] )
self . treeview = Gtk . TreeView ( model = self . store )
renderer = Gtk . CellRendererText ( )
column = Gtk . TreeViewColumn ( " Applications " , renderer , text = 0 )
self . treeview . append_column ( column )
self . treeview . set_headers_visible ( False )
self . treeview . connect ( " row-activated " , self . on_row_activated )
self . treeview . connect ( " key-press-event " , self . on_key_press )
self . add ( self . treeview )
self . connect ( " key-press-event " , self . on_key_press )
self . treeview . connect ( " focus-out-event " , self . on_focus_out )
self . treeview . grab_focus ( )
self . show_all ( )
def on_row_activated ( self , treeview , path , column ) :
model = treeview . get_model ( )
iter = model . get_iter ( path )
exec_command = model . get_value ( iter , 1 )
if exec_command :
os . system ( exec_command )
def on_focus_out ( self , widget , event ) :
Gtk . main_quit ( )
def on_key_press ( self , widget , event ) :
keyval = event . keyval
state = event . state
if keyval == Gdk . KEY_Escape :
Gtk . main_quit ( )
elif state & Gdk . ModifierType . SHIFT_MASK and keyval == Gdk . KEY_Left :
path = self . treeview . get_cursor ( ) [ 0 ]
if path :
iter = self . store . get_iter ( path )
if self . treeview . row_expanded ( path ) :
self . treeview . collapse_row ( path )
else :
parent_iter = self . store . iter_parent ( iter )
if parent_iter :
parent_path = self . store . get_path ( parent_iter )
self . treeview . collapse_row ( parent_path )
else :
self . treeview . collapse_row ( path )
2024-06-29 19:00:36 -04:00
win = Xdg_Menu_Window ( )
2024-06-29 17:12:13 -04:00
win . connect ( " destroy " , Gtk . main_quit )
win . show_all ( )
Gtk . main ( )