diff --git a/README.md b/README.md index 185b158d..389b6b34 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ ttf-font-awesome, otf-font-awesome, ttf-material-symbols-variable-git, ttf-googl # ToDo - [x] Add bluetooth widget -- [ ] Add shrunner widget +- [x] Add shrunner widget - [ ] Add wifi widget - [ ] Dashboard - [ ] Notification panel diff --git a/config/quickbar/modules/launcher/launcher.py b/config/quickbar/modules/launcher/launcher.py index 48afec1c..5feb0fdc 100644 --- a/config/quickbar/modules/launcher/launcher.py +++ b/config/quickbar/modules/launcher/launcher.py @@ -10,6 +10,7 @@ from modules.launcher.widgets import ( Cliphist, Emoji, PowerMenu, + Sh, TodoManager, WallpaperSelector, ) @@ -31,12 +32,14 @@ class Launcher(Window): self.cliphist = Cliphist() self.todo = TodoManager() self.bluetooth = BluetoothManager() + self.sh = Sh() # Initialize viewports as None self._emoji_viewport = None self._cliphist_viewport = None self._todo_viewport = None self._bluetooth_viewport = None + self._sh_viewport = None self.stack = Stack( name="launcher-content", @@ -52,6 +55,7 @@ class Launcher(Window): self.cliphist, self.todo, self.bluetooth, + self.sh, ], ) @@ -104,19 +108,17 @@ class Launcher(Window): self.cliphist, self.todo, self.bluetooth, + self.sh, ]: widget.remove_style_class("open") - # Handle wallpaper viewport if widget == self.wallpapers: self.wallpapers.viewport.hide() self.wallpapers.viewport.set_property("name", None) - # Handle emoji viewport safely if widget == self.emoji and hasattr(widget, "viewport") and widget.viewport: widget.viewport.hide() - # Handle cliphist viewport safely if ( widget == self.cliphist and hasattr(widget, "viewport") @@ -132,6 +134,9 @@ class Launcher(Window): widget.viewport.children = [] widget.is_active = False + if widget == self.sh and widget.viewport: + widget.viewport.hide() + style_classes = [ "launcher", "wallpapers", @@ -140,6 +145,7 @@ class Launcher(Window): "cliphist", "todo", "bluetooth", + "sh", ] for style_class in style_classes: if self.stack.get_style_context().has_class(style_class): @@ -159,6 +165,7 @@ class Launcher(Window): "cliphist": self.cliphist, "todo": self.todo, "bluetooth": self.bluetooth, + "sh": self.sh, } style_classes = list(widgets.keys()) @@ -213,6 +220,11 @@ class Launcher(Window): self.bluetooth.search_entry.grab_focus() self.bluetooth.device_manager.arrange_viewport() + elif widget == "sh": + self.sh.open_launcher() + self.sh.search_entry.set_text("") + self.sh.search_entry.grab_focus() + else: if hasattr(self.wallpapers, "viewport"): self.wallpapers.viewport.hide() diff --git a/config/quickbar/modules/launcher/widgets/__init__.py b/config/quickbar/modules/launcher/widgets/__init__.py index 38276c1b..24ba7fe1 100644 --- a/config/quickbar/modules/launcher/widgets/__init__.py +++ b/config/quickbar/modules/launcher/widgets/__init__.py @@ -4,5 +4,6 @@ from .cliphist import * from .dashboard import * from .emoji import * from .power import * +from .sh import * from .todo import * from .wallpapers import * diff --git a/config/quickbar/modules/launcher/widgets/sh.py b/config/quickbar/modules/launcher/widgets/sh.py new file mode 100644 index 00000000..4ef96f84 --- /dev/null +++ b/config/quickbar/modules/launcher/widgets/sh.py @@ -0,0 +1,152 @@ +import os +import subprocess +from typing import List + +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.entry import Entry +from fabric.widgets.label import Label +from fabric.widgets.scrolledwindow import ScrolledWindow +from gi.repository import GLib +from snippets import MaterialIcon + +CACHE_DIR = os.getenv( + "XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache", "fabric") +) +BINS = os.path.join(CACHE_DIR, "binaries") + + +class Sh(Box): + def __init__(self, **kwargs): + super().__init__( + name="shell-command-launcher", + all_visible=False, + visible=False, + **kwargs, + ) + + self._arranger_handler: int = 0 + self.shell_command_manager = ShellCommandManager(self) + self.viewport = None + + self.search_entry = Entry( + name="search-entry", + placeholder="Search Commands...", + h_expand=True, + notify_text=lambda entry, *_: self.handle_search_input(entry.get_text()), + on_activate=lambda entry, *_: self.handle_search_input(entry.get_text()), + ) + + self.header_box = Box( + name="header-box", + spacing=10, + orientation="h", + children=[ + self.search_entry, + Button( + name="close-button", + child=MaterialIcon("close"), + tooltip_text="Exit", + on_clicked=lambda *_: self.close_launcher(), + ), + ], + ) + + self.launcher_box = Box( + name="shell-command-launcher-box", + spacing=10, + orientation="v", + h_expand=True, + children=[self.header_box], + ) + + self.add(self.launcher_box) + + def open_launcher(self): + if not self.viewport: + self.viewport = Box(name="viewport", spacing=10, orientation="v") + self.scrolled_window = ScrolledWindow( + name="scrolled-window", + spacing=10, + h_scrollbar_policy="never", + v_scrollbar_policy="never", + child=self.viewport, + ) + self.launcher_box.add(self.scrolled_window) + + self.viewport.children = [] + self.shell_command_manager.show_shell_commands(self.viewport, "") + + self.viewport.show() + self.search_entry.grab_focus() + + def close_launcher(self): + GLib.spawn_command_line_async("fabric-cli exec quickbar 'launcher.close()'") + + def handle_search_input(self, text: str): + self.shell_command_manager.show_shell_commands(self.viewport, text) + + +class ShellCommandManager: + def __init__(self, launcher): + self.launcher = launcher + self.binaries = set() + self.reload_binaries() + + def reload_binaries(self): + paths = os.getenv("PATH", "").split(":") + self.binaries = set() + + for path in paths: + self.binaries.update(self.list_binaries_in_path(path)) + + with open(BINS, "w") as f: + f.write("\n".join(self.binaries)) + + def list_binaries_in_path(self, path): + if not os.path.exists(path): + return [] + try: + return [ + entry + for entry in os.listdir(path) + if os.access(os.path.join(path, entry), os.X_OK) + ] + except OSError: + return [] + + def show_shell_commands(self, viewport, search_query: str): + if not search_query: + return + + viewport.children = [] + + results = self.query_binaries(search_query) + self.display_results(viewport, results) + + def query_binaries(self, filter_str: str) -> List[str]: + filter_str = filter_str.lower() + filtered_bins = (cmd for cmd in self.binaries if filter_str in cmd) + return list(dict.fromkeys(filtered_bins))[:16] + + def display_results(self, viewport, results: List[str]): + for command in results: + button = Button( + child=Label( + label=command, + v_align="center", + h_align="start", + ), + on_clicked=lambda _, cmd=command: self.execute_command(cmd), + name="sh-item", + ) + viewport.add(button) + + def execute_command(self, cmd: str): + try: + output = subprocess.check_output(cmd, shell=True, text=True) + print(output) + except subprocess.CalledProcessError as e: + print(e.output) + finally: + self.launcher.set_visible(False) diff --git a/config/quickbar/modules/launcher/widgets/shell.py b/config/quickbar/modules/launcher/widgets/shell.py deleted file mode 100644 index e69de29b..00000000