#!/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): 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 class XdgMenuWindow(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 for category, entries in categories.items(): 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) win = XdgMenuWindow() win.connect("destroy", Gtk.main_quit) win.show_all() Gtk.main()