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 ) :
2024-06-29 20:49:18 -04:00
desktopEntries = [ ]
2024-06-29 17:12:13 -04:00
for path in paths :
for file in Path ( path ) . rglob ( ' *.desktop ' ) :
config = configparser . ConfigParser ( interpolation = None )
config . read ( file )
2024-06-29 20:49:18 -04:00
desktopEntries . append ( config )
return desktopEntries
2024-06-29 17:12:13 -04:00
2024-06-29 20:49:18 -04:00
userApplicationsPath = Path . home ( ) / ' .local/share/applications '
systemApplicationsPath = Path ( ' /usr/share/applications ' )
2024-06-29 17:12:13 -04:00
2024-06-29 20:49:18 -04:00
desktopEntries = read_desktop_files ( [ userApplicationsPath , systemApplicationsPath ] )
2024-06-29 17:12:13 -04:00
# Combine some of the categories
2024-06-29 20:49:18 -04:00
categoryMap = {
2024-06-29 17:12:13 -04:00
" 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 )
2024-06-29 20:49:18 -04:00
for entry in desktopEntries :
2024-06-29 17:12:13 -04:00
try :
2024-06-29 20:49:18 -04:00
entryCategories = entry . get ( ' Desktop Entry ' , ' Categories ' ) . split ( ' ; ' )
for category in entryCategories :
combinedCategory = categoryMap . get ( category , category )
2024-06-29 17:12:13 -04:00
name = entry . get ( ' Desktop Entry ' , ' Name ' )
2024-06-29 20:49:18 -04:00
execCommand = entry . get ( ' Desktop Entry ' , ' Exec ' )
# Use a tuple of name and execCommand as a unique identifier
if ( name , execCommand ) not in categories [ combinedCategory ] :
categories [ combinedCategory ] . add ( ( name , execCommand ) )
2024-06-29 17:12:13 -04:00
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 20:49:18 -04:00
sortedCategories = sorted ( categories . items ( ) ) # Sort categories alphabetically
2024-06-29 19:00:36 -04:00
2024-06-29 20:49:18 -04:00
for category , entries in sortedCategories :
2024-06-29 17:12:13 -04:00
if category == " " :
continue
2024-06-29 20:49:18 -04:00
categoryIter = self . store . append ( parent = None , row = [ category , None ] )
sortedEntries = sorted ( entries , key = lambda e : e [ 0 ] ) # Sort entries by name
for name , execCommand in sortedEntries :
self . store . append ( parent = categoryIter , row = [ name , execCommand ] )
2024-06-29 17:12:13 -04:00
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 )
2024-06-29 20:49:18 -04:00
execCommand = model . get_value ( iter , 1 )
if execCommand :
os . system ( execCommand )
2024-06-29 17:12:13 -04:00
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 ( )
2024-06-29 20:49:18 -04:00
elif keyval == Gdk . KEY_Left :
2024-06-29 17:12:13 -04:00
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 20:49:18 -04:00
elif keyval == Gdk . KEY_Right :
path = self . treeview . get_cursor ( ) [ 0 ]
if path :
if not self . treeview . row_expanded ( path ) :
self . treeview . expand_row ( path , open_all = False )
2024-06-29 17:12:13 -04:00
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 ( )