From f5b34aa89c16493bbff8567e7b2d2ee35f2563ee Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 29 Jun 2024 17:12:13 -0400 Subject: [PATCH] New menu system for I38 that no longer relies on sgtk-menu. --- README.md | 6 +- i38.sh | 4 +- scripts/menu.py | 167 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 5 deletions(-) create mode 100755 scripts/menu.py diff --git a/README.md b/README.md index a3cf1bb..d9b239c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Accessibility setup script for the i3 window manager. ## i38.sh -Released under the terms of the WTFPL License: http://www.wtfpl.net +Released under the terms of the GPL License Version 3: http://www.wtfpl.net This is a Stormux project: https://stormux.org @@ -25,9 +25,9 @@ An uppercase I looks like a 1, 3 from i3, and 8 because the song [We Are 138](ht - ocrdesktop: For getting contents of the current window with OCR. - pamixer: for the mute-unmute script - playerctl: music controls +- python-gobject: for applications menu. - python-i3ipc: for sounds etc. - remind: [optional]For reminder notifications, Requires notify-daemon and notify-send for automatic reminders. -- sgtk-menu: for applications menu - sox: for sounds. - transfersh: [optional] for file sharing GUI - udiskie: [optional] for automatically mounting removable storage @@ -70,7 +70,7 @@ You can apply the same configuration to GTK2 appss. Create or edit ~/.gtkrc-2.0 ## Usage: - With no arguments, create the i3 configuration. -- -h: This help screen. +- -h: Help screen. - -u: Copy over the latest version of scripts. - -x: Generate ~/.xinitrc and ~/.xprofile. - -X: Generate ~/.xprofile only. diff --git a/i38.sh b/i38.sh index a0777b7..cf452a8 100755 --- a/i38.sh +++ b/i38.sh @@ -19,7 +19,7 @@ sensibleTerminal="i3-sensible-terminal" export DIALOGOPTS='--no-lines --visit-items' # Check to make sure minimum requirements are installed. -for i in dialog jq sgtk-menu yad ; do +for i in dialog jq yad ; do if ! command -v "$i" &> /dev/null ; then missing+=("$i") fi @@ -463,7 +463,7 @@ bindsym \$mod+Return exec $sensibleTerminal bindsym \$mod+F4 kill # Applications menu -bindsym \$mod+F1 exec --no-startup-id sgtk-menu -f +bindsym \$mod+F1 exec --no-startup-id "${i3Path}/scripts/menu.py" # Desktop icons bindsym \$mod+Control+d exec --no-startup-id ${i3Path}/scripts/desktop.sh diff --git a/scripts/menu.py b/scripts/menu.py new file mode 100755 index 0000000..6863b6b --- /dev/null +++ b/scripts/menu.py @@ -0,0 +1,167 @@ +#!/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() +