#!/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 . 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()