I38/scripts/menu.py

175 lines
6.9 KiB
Python
Raw Normal View History

#!/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):
desktopEntries = []
for path in paths:
for file in Path(path).rglob('*.desktop'):
config = configparser.ConfigParser(interpolation=None)
config.read(file)
desktopEntries.append(config)
return desktopEntries
userApplicationsPath = Path.home() / '.local/share/applications'
systemApplicationsPath = Path('/usr/share/applications')
userFlatpakApplicationsPath = Path.home() / '.local/share/flatpak/exports/share/applications'
systemFlatpakApplicationsPath = Path('/var/lib/flatpak/exports/share/applications')
desktopEntries = read_desktop_files([userApplicationsPath, systemApplicationsPath, userFlatpakApplicationsPath, systemFlatpakApplicationsPath])
# Combine some of the categories
categoryMap = {
"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 desktopEntries:
try:
entryCategories = entry.get('Desktop Entry', 'Categories').split(';')
for category in entryCategories:
combinedCategory = categoryMap.get(category, category)
name = entry.get('Desktop Entry', 'Name')
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))
except configparser.NoOptionError:
continue
class Xdg_Menu_Window(Gtk.Window):
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
sortedCategories = sorted(categories.items()) # Sort categories alphabetically
for category, entries in sortedCategories:
if category == "":
continue
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])
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)
execCommand = model.get_value(iter, 1)
if execCommand:
os.system(execCommand)
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 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)
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)
win = Xdg_Menu_Window()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()