├── .gitignore ├── LICENSE ├── README.md ├── co.uk.dvlv.toolbox-gui.desktop ├── co.uk.dvlv.toolbox-gui.metainfo.xml ├── co.uk.dvlv.toolbox-gui.yml ├── icons └── co.uk.dvlv.toolbox-gui.svg ├── screenshot.png ├── singlefile.sh ├── src ├── about_window.py ├── app.py ├── echo_into_file.sh ├── edit_window.py ├── help_window.py ├── info_window.py ├── main_window.py ├── run_application_window.py ├── run_software_window.py ├── toolbox_name_window.py └── utils.py ├── toolbox-gui ├── toolbox-gui-fp └── toolbox-gui.flatpak /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | __pycache__ 3 | .flatpak-builder/ 4 | build-dir 5 | **/__pycache__ 6 | run 7 | repo 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dvlv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toolbox GUI 2 | A GUI manager for your toolboxes, made with Python and GTK. 3 | 4 | ![screenshot](screenshot.png) 5 | 6 | ## Functionality 7 | - :heavy_plus_sign: - Create new Toolbox. 8 | - ⏹ - Stop Toolbox (only appears if running). 9 | - :information_source: - View Toolbox information. 10 | - :gear: - Change Toolbox Settings (name). 11 | - :computer: - Open a terminal in Toolbox 12 | - :package: 13 | - Run a Command inside Toolbox 14 | - View Applications inside Toolbox / Copy Applications to Host 15 | - Install an RPM File inside Toolbox (The flatpak only has access to `~/Downloads` by default) 16 | - Update Toolbox 17 | - :wastebasket: - Delete Toolbox 18 | 19 | (Icons may differ based on your icon theme) 20 | 21 | ## Flatpak 22 | Clone or download this repo, `cd` into the folder, then install like so: 23 | 24 | ### From Binary 25 | - `flatpak install --user toolbox-gui.flatpak` 26 | 27 | ### Build From Source 28 | - Install `flatpak-builder` (probably in a Toolbox) 29 | - `flatpak-builder --user --install --force-clean build-dir co.uk.dvlv.toolbox-gui.yml` 30 | 31 | ## Running (Standalone script) 32 | Clone this repo, then execute `./toolbox-gui`. A Silverblue / Kinoite installation should come with the necessary python dependencies out-of-the-box. 33 | 34 | ## TODO 35 | - [ ] Icon PNGs 36 | 37 | 38 | ### Future Functionality 39 | - [ ] Export / Import list of packages (for upgrading) 40 | - [ ] Dist Upgrades (sudo dnf update --releasever=36) 41 | 42 | - [ ] test 43 | 44 | -------------------------------------------------------------------------------- /co.uk.dvlv.toolbox-gui.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Toolbox GUI 3 | Exec=toolbox-gui 4 | Type=Application 5 | Icon=co.uk.dvlv.toolbox-gui 6 | Categories=Utility; 7 | Comment=A Graphical Application For Managing Toolboxes 8 | -------------------------------------------------------------------------------- /co.uk.dvlv.toolbox-gui.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | co.uk.dvlv.toolbox-gui 4 | Toolbox GUI 5 | Dvlv 6 | co.uk.dvlv.toolbox-gui.desktop 7 | Graphical Toolbox Manager 8 | 9 |

10 | A Graphical Application for managing your Toolboxes on Silverblue and Kinoite 11 |

12 |
13 | 14 | CC0-1.0 15 | MIT 16 | 17 | https://github.com/Dvlv/toolbox-gui 18 | 19 | 20 | 21 | 22 |
23 | -------------------------------------------------------------------------------- /co.uk.dvlv.toolbox-gui.yml: -------------------------------------------------------------------------------- 1 | app-id: co.uk.dvlv.toolbox-gui 2 | runtime: org.gnome.Platform 3 | runtime-version: '42' 4 | sdk: org.gnome.Sdk 5 | finish-args: 6 | - --socket=wayland 7 | - --socket=fallback-x11 8 | - --filesystem=xdg-data 9 | - --filesystem=~/.icons 10 | - --filesystem=xdg-download:ro 11 | - --talk-name=org.freedesktop.Flatpak 12 | command: toolbox-gui 13 | modules: 14 | - name: runner 15 | buildsystem: simple 16 | build-commands: 17 | - install -D toolbox-gui-fp /app/bin/toolbox-gui 18 | - cp -r src /app/src 19 | - mkdir /app/icons 20 | - cp icons/co.uk.dvlv.toolbox-gui.svg /app/icons/co.uk.dvlv.toolbox-gui.svg 21 | 22 | - mkdir -p /app/share/applications 23 | - install -D co.uk.dvlv.toolbox-gui.desktop /app/share/applications/ 24 | 25 | - install -Dp -m 644 co.uk.dvlv.toolbox-gui.metainfo.xml /app/share/metainfo/co.uk.dvlv.toolbox-gui.metainfo.xml 26 | 27 | - mkdir -p /app/share/icons/hicolor/scalable/apps 28 | - install -D icons/co.uk.dvlv.toolbox-gui.svg /app/share/icons/hicolor/scalable/apps 29 | sources: 30 | - type: file 31 | path: co.uk.dvlv.toolbox-gui.desktop 32 | 33 | - type: file 34 | path: icons/co.uk.dvlv.toolbox-gui.svg 35 | 36 | - type: file 37 | path: co.uk.dvlv.toolbox-gui.metainfo.xml 38 | 39 | - type: file 40 | path: src/echo_into_file.sh 41 | 42 | - type: file 43 | path: toolbox-gui-fp 44 | 45 | - type: dir 46 | path: icons 47 | dest: icons 48 | 49 | - type: dir 50 | path: src 51 | dest: src 52 | -------------------------------------------------------------------------------- /icons/co.uk.dvlv.toolbox-gui.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 28 | 30 | 37 | 44 | 45 | 49 | 57 | 65 | 66 | 71 | 79 | 87 | 88 | 92 | 97 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dvlv/toolbox-gui/e8d80598eb59bec0d36c9bed8e86a0824e93a132/screenshot.png -------------------------------------------------------------------------------- /singlefile.sh: -------------------------------------------------------------------------------- 1 | flatpak-builder --repo=repo --force-clean build-dir co.uk.dvlv.toolbox-gui.yml 2 | flatpak build-bundle repo toolbox-gui.flatpak co.uk.dvlv.toolbox-gui 3 | -------------------------------------------------------------------------------- /src/about_window.py: -------------------------------------------------------------------------------- 1 | import os 2 | from app import Gtk, GdkPixbuf 3 | from utils import set_icon_at_small_size 4 | 5 | 6 | class AboutWindow(Gtk.MessageDialog): 7 | def __init__(self, parent): 8 | super().__init__(title=f"About", transient_for=parent, flags=0) 9 | 10 | self.add_buttons(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) 11 | 12 | main_box = self.get_content_area() 13 | vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 14 | vbox.set_border_width(20) 15 | 16 | program_icon = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "icons", "co.uk.dvlv.toolbox-gui.svg")) 17 | 18 | img = Gtk.Image() 19 | pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(program_icon, 64, 64, True) 20 | img.set_from_pixbuf(pb) 21 | 22 | vbox.add(img) 23 | 24 | markup = """ 25 | A graphical manager for your toolboxes 26 | Released under the MIT Licence 27 | """ 28 | for line in markup.split("\n"): 29 | lbl = Gtk.Label(label=line) 30 | vbox.add(lbl) 31 | 32 | 33 | self.format_secondary_text("Toolbox GUI") 34 | 35 | main_box.add(vbox) 36 | 37 | self.show_all() -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | import gi 2 | 3 | gi.require_version("Gtk", "3.0") 4 | from gi.repository import Gtk, Gdk, Gio, GLib, GObject, GdkPixbuf 5 | 6 | 7 | if __name__ == "__main__": 8 | from main_window import MyWindow 9 | 10 | win = MyWindow() 11 | win.connect("destroy", Gtk.main_quit) 12 | win.show_all() 13 | Gtk.main() 14 | -------------------------------------------------------------------------------- /src/echo_into_file.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | find /usr/share/icons -type f -iname "*$1*" >> $2/tb_gui_icon_files.txt 3 | -------------------------------------------------------------------------------- /src/edit_window.py: -------------------------------------------------------------------------------- 1 | from app import Gtk 2 | 3 | 4 | class EditWindow(Gtk.MessageDialog): 5 | def __init__(self, parent, toolbox): 6 | super().__init__(title=f"Edit {toolbox}", transient_for=parent, flags=0) 7 | 8 | self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) 9 | 10 | save_btn = Gtk.Button(label="Save") 11 | self.add_action_widget(save_btn, Gtk.ResponseType.OK) 12 | 13 | l1 = Gtk.Label(label="Toolbox Name:") 14 | self.toolbox_name = Gtk.Entry(text=toolbox) 15 | self.toolbox_name.connect( 16 | "activate", lambda e: self.emit("response", Gtk.ResponseType.OK) 17 | ) 18 | 19 | box = self.get_content_area() 20 | 21 | spacer = Gtk.Box(spacing=10) 22 | spacer.set_border_width(10) 23 | 24 | spacer.add(l1) 25 | spacer.add(self.toolbox_name) 26 | 27 | box.add(spacer) 28 | 29 | self.show_all() 30 | 31 | def get_entered_name(self): 32 | return self.toolbox_name.get_text() 33 | -------------------------------------------------------------------------------- /src/help_window.py: -------------------------------------------------------------------------------- 1 | from app import Gtk 2 | from utils import set_icon_at_small_size 3 | 4 | 5 | class HelpWindow(Gtk.MessageDialog): 6 | def __init__(self, parent): 7 | super().__init__(title=f"Help", transient_for=parent, flags=0) 8 | 9 | self.add_buttons(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) 10 | 11 | box = self.get_content_area() 12 | 13 | spacer = Gtk.Box(spacing=10) 14 | spacer.set_border_width(20) 15 | 16 | grid = Gtk.Grid() 17 | grid.set_column_spacing(20) 18 | grid.set_row_spacing(20) 19 | 20 | help_parts = { 21 | "list-add-symbolic": "Create a new Toolbox", 22 | "media-playback-stop-symbolic": "Stop a Toolbox", 23 | "dialog-information-symbolic": "View Toolbox Information", 24 | "preferences-system-symbolic": "Edit Toolbox", 25 | "utilities-terminal-symbolic": "Open Terminal in Toolbox", 26 | "system-software-install-symbolic": "Open Application Menu", 27 | "user-trash-symbolic": "Delete Toolbox", 28 | } 29 | 30 | pos = 1 31 | for icon, desc in help_parts.items(): 32 | img = Gtk.Image() 33 | set_icon_at_small_size(icon, img) 34 | lbl = Gtk.Label(label=desc) 35 | lbl.set_xalign(0) 36 | 37 | grid.attach(img, 1, pos, 1, 1) 38 | grid.attach(lbl, 2, pos, 1, 1) 39 | 40 | pos += 1 41 | 42 | spacer.add(grid) 43 | box.add(spacer) 44 | 45 | self.show_all() 46 | -------------------------------------------------------------------------------- /src/info_window.py: -------------------------------------------------------------------------------- 1 | from app import Gtk 2 | 3 | 4 | class InfoWindow(Gtk.MessageDialog): 5 | def __init__(self, parent, toolbox: str, info: dict): 6 | super().__init__(title=f"Info for {toolbox}", transient_for=parent, flags=0) 7 | 8 | self.add_buttons(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) 9 | 10 | box = self.get_content_area() 11 | 12 | spacer = Gtk.Box(spacing=10) 13 | spacer.set_border_width(10) 14 | 15 | grid = Gtk.Grid() 16 | grid.set_column_spacing(20) 17 | 18 | labels = [] 19 | for lbl in ["ID", "Status", "Image", "Created At"]: 20 | l = Gtk.Label() 21 | l.set_markup(f"{lbl}:") 22 | labels.append(l) 23 | 24 | grid.attach(labels[0], 1, 1, 1, 1) 25 | grid.attach(Gtk.Label(label=info["container_id"]), 1, 2, 1, 1) 26 | 27 | grid.attach(labels[1], 2, 1, 1, 1) 28 | grid.attach(Gtk.Label(label=info["status"]), 2, 2, 1, 1) 29 | 30 | grid.attach(labels[2], 3, 1, 1, 1) 31 | grid.attach(Gtk.Label(label=info["image"]), 3, 2, 1, 1) 32 | 33 | grid.attach(labels[3], 4, 1, 1, 1) 34 | grid.attach(Gtk.Label(label=info["created_at"]), 4, 2, 1, 1) 35 | 36 | spacer.add(grid) 37 | box.add(spacer) 38 | 39 | self.show_all() 40 | -------------------------------------------------------------------------------- /src/main_window.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import time 4 | 5 | from functools import partial 6 | from shutil import which 7 | 8 | 9 | from about_window import AboutWindow 10 | from edit_window import EditWindow 11 | from help_window import HelpWindow 12 | from info_window import InfoWindow 13 | from run_application_window import RunApplicationWindow 14 | from run_software_window import RunSoftwareWindow 15 | from toolbox_name_window import ToolboxNameWindow 16 | 17 | from app import Gtk, GLib, Gdk 18 | from utils import ( 19 | FLATPAK_SPAWN, 20 | FLATPAK_SPAWN_ARR, 21 | get_output, 22 | get_stderr, 23 | create_toolbox_button, 24 | create_popover_button, 25 | execute_delete_toolbox, 26 | fetch_all_toolboxes, 27 | fetch_all_toolbox_names, 28 | launch_app, 29 | edit_exec_of_toolbox_desktop, 30 | is_dark_theme, 31 | is_flatpak, 32 | copy_desktop_from_toolbox_to_host, 33 | copy_icons_for_toolbox_desktop, 34 | ) 35 | 36 | fp_spawn = [] 37 | if is_flatpak(): 38 | fp_spawn = ["flatpak", "spawn", "--host"] 39 | 40 | terminal = "gnome-terminal" 41 | terminal_exec_arg = "--" 42 | err = get_stderr([*FLATPAK_SPAWN_ARR, "which", "gnome-terminal"]) 43 | if err: 44 | terminal = "konsole" 45 | terminal_exec_arg = "-e" 46 | 47 | 48 | class MyWindow(Gtk.Window): 49 | def __init__(self): 50 | super().__init__(title="Toolbox GUI") 51 | self.init_header_bar() 52 | 53 | self.toolbox_rows = {} 54 | self.icon_cache = {} 55 | 56 | self.scrolled = Gtk.ScrolledWindow() 57 | self.scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 58 | 59 | self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 60 | # self.box.set_homogeneous(False) 61 | 62 | self.scrolled.add(self.box) 63 | 64 | self.render_all_toolboxes() 65 | self.add(self.scrolled) 66 | 67 | d_width = 650 68 | calc_height = 75 * len(self.toolbox_rows) 69 | if calc_height > 900: 70 | calc_height = 900 71 | self.set_default_size(d_width, calc_height) 72 | 73 | self.apply_css() 74 | 75 | self.box.show_all() 76 | 77 | def apply_css(self): 78 | """ 79 | Applies CSS and sets dark theme 80 | """ 81 | css = self.get_css() 82 | css_provider = Gtk.CssProvider() 83 | css_provider.load_from_data(css) 84 | context = Gtk.StyleContext() 85 | screen = Gdk.Screen.get_default() 86 | context.add_provider_for_screen( 87 | screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 88 | ) 89 | 90 | settings = Gtk.Settings.get_default() 91 | settings.set_property("gtk-application-prefer-dark-theme", is_dark_theme()) 92 | 93 | def get_css(self): 94 | """ 95 | CSS for the application 96 | """ 97 | primary = "white" 98 | primary2 = "#efefef" 99 | 100 | if is_dark_theme(): 101 | primary = "#020202" 102 | primary2 = "#333333" 103 | 104 | css = f""" 105 | 106 | .tb_row {{ 107 | background: {primary}; 108 | border: none; 109 | padding: 20px; 110 | }} 111 | 112 | .tb_row:nth-child(even) {{ 113 | background: {primary2}; 114 | }} 115 | """ 116 | 117 | return css.encode() 118 | 119 | def init_header_bar(self): 120 | """ 121 | Sets Header / Title bar at top, containing add button 122 | """ 123 | header = Gtk.HeaderBar() 124 | header.set_title("Toolbox GUI") 125 | header.set_show_close_button(True) 126 | 127 | img = Gtk.Image() 128 | img.set_from_icon_name("list-add-symbolic", Gtk.IconSize.BUTTON) 129 | new_btn = Gtk.Button(image=img) 130 | new_btn.set_name("new-button") 131 | new_btn.connect("clicked", lambda s: self.create_new_toolbox()) 132 | new_btn.set_tooltip_text("Create a New Toolbox") 133 | new_btn.get_style_context().add_class("add_btn") 134 | 135 | menu_items = { 136 | "Refresh": partial(self.delayed_rerender), 137 | "Help": partial(self.show_help_window), 138 | "About": partial(self.show_about_window), 139 | } 140 | 141 | menu_btn = create_popover_button("open-menu-symbolic", "Open Menu", menu_items) 142 | 143 | header.pack_start(menu_btn) 144 | header.pack_end(new_btn) 145 | 146 | self.set_titlebar(header) 147 | 148 | def render_all_toolboxes(self): 149 | """ 150 | Clears window and renders each row for each toolbox 151 | """ 152 | self.clear_main_box() 153 | 154 | toolboxes = fetch_all_toolboxes() 155 | 156 | for tb in toolboxes: 157 | self.render_toolbox_row(tb) 158 | 159 | self.box.show_all() 160 | 161 | def clear_main_box(self): 162 | """ 163 | Removes all children from the main window 164 | """ 165 | for child in self.box.get_children(): 166 | self.box.remove(child) 167 | 168 | def render_toolbox_row(self, toolbox_info): 169 | """ 170 | Renders a row for each toolbox, containing the name and buttons 171 | """ 172 | tb_row = Gtk.Box(spacing=10) 173 | tb_row.get_style_context().add_class("tb_row") 174 | 175 | tb_frame = Gtk.Frame() 176 | tb_frame.get_style_context().add_class("tb_frame") 177 | 178 | toolbox, version, status, tb_id = ( 179 | toolbox_info[0], 180 | toolbox_info[1], 181 | toolbox_info[2], 182 | toolbox_info[3], 183 | ) 184 | 185 | lbl = Gtk.Label(label=f"(f{version}) {toolbox} ") 186 | lbl.set_xalign(0) 187 | 188 | buttons = [] 189 | 190 | application_menu_items = { 191 | "Run a Command in this Toolbox": partial(self.run_from_toolbox, toolbox), 192 | "Open an Application from this Toolbox": partial( 193 | self.open_application_in_toolbox, toolbox 194 | ), 195 | "Install an RPM in this Toolbox": partial( 196 | self.install_into_toolbox, toolbox 197 | ), 198 | "Update This Toolbox": partial(self.update_toolbox, toolbox), 199 | } 200 | 201 | if status.startswith("Up"): 202 | buttons.append( 203 | create_toolbox_button( 204 | "media-playback-stop-symbolic", 205 | "Stop this Toolbox", 206 | partial(self.stop_toolbox, toolbox), 207 | ) 208 | ) 209 | 210 | buttons.append( 211 | create_toolbox_button( 212 | "dialog-information-symbolic", 213 | "View Information about this Toolbox", 214 | partial(self.view_toolbox_info, tb_id, toolbox), 215 | ) 216 | ) 217 | buttons.append( 218 | create_toolbox_button( 219 | "preferences-system-symbolic", 220 | "Edit this Toolbox", 221 | partial(self.edit_toolbox, toolbox), 222 | ) 223 | ) 224 | buttons.append( 225 | create_toolbox_button( 226 | "utilities-terminal-symbolic", 227 | "Launch a Terminal in this Toolbox", 228 | partial(self.start_toolbox, toolbox), 229 | ) 230 | ) 231 | buttons.append( 232 | create_popover_button( 233 | "system-software-install-symbolic", 234 | "Application Options", 235 | application_menu_items, 236 | ) 237 | ) 238 | buttons.append( 239 | create_toolbox_button( 240 | "user-trash-symbolic", 241 | "Delete this Toolbox", 242 | partial(self.confirm_delete_toolbox, toolbox), 243 | ) 244 | ) 245 | 246 | tb_row.pack_start(lbl, True, True, 0) 247 | 248 | for btn in buttons: 249 | tb_row.pack_start(btn, False, False, 3) 250 | 251 | tb_row.show_all() 252 | # tb_frame.add(tb_row) 253 | 254 | self.box.pack_start(tb_row, False, False, 0) 255 | 256 | self.toolbox_rows[toolbox] = tb_row 257 | 258 | def start_toolbox(self, toolbox: str): 259 | """ 260 | Opens a terminal in a toolbox 261 | """ 262 | subprocess.Popen( 263 | [ 264 | *FLATPAK_SPAWN_ARR, 265 | terminal, 266 | terminal_exec_arg, 267 | "toolbox", 268 | "enter", 269 | toolbox, 270 | ] 271 | ) 272 | GLib.timeout_add_seconds(1, self.delayed_rerender) 273 | 274 | def edit_toolbox(self, toolbox: str): 275 | """ 276 | Opens popup to rename a toolbox 277 | """ 278 | d = EditWindow(self, toolbox) 279 | response = d.run() 280 | new_name = d.get_entered_name() 281 | if new_name and new_name != toolbox: 282 | subprocess.run([*FLATPAK_SPAWN_ARR, "podman", "rename", toolbox, new_name]) 283 | 284 | d.destroy() 285 | 286 | self.render_all_toolboxes() 287 | 288 | def install_into_toolbox(self, toolbox: str, *args): 289 | """ 290 | Passes typed information into ``dnf install`` inside the toolbox 291 | """ 292 | file_to_install = self.show_file_chooser() 293 | if file_to_install: 294 | subprocess.run( 295 | [ 296 | *FLATPAK_SPAWN_ARR, 297 | terminal, 298 | terminal_exec_arg, 299 | "toolbox", 300 | "run", 301 | "-c", 302 | toolbox, 303 | "sudo", 304 | "dnf", 305 | "install", 306 | "-y", 307 | file_to_install, 308 | ] 309 | ) 310 | 311 | def update_toolbox(self, toolbox, *args): 312 | """ 313 | Runs ``dnf update`` inside toolbox 314 | """ 315 | subprocess.Popen( 316 | [ 317 | *FLATPAK_SPAWN_ARR, 318 | terminal, 319 | terminal_exec_arg, 320 | "toolbox", 321 | "run", 322 | "-c", 323 | toolbox, 324 | "sudo", 325 | "dnf", 326 | "update", 327 | "-y", 328 | ] 329 | ) 330 | 331 | def run_from_toolbox(self, toolbox: str, *args): 332 | """ 333 | Runs command inside a toolbox 334 | """ 335 | dialog = RunSoftwareWindow(self, toolbox) 336 | response = dialog.run() 337 | cmd = None 338 | if response == Gtk.ResponseType.OK: 339 | cmd = dialog.get_entered_command() 340 | dialog.destroy() 341 | else: 342 | dialog.destroy() 343 | 344 | if cmd: 345 | subprocess.Popen( 346 | [ 347 | *FLATPAK_SPAWN_ARR, 348 | terminal, 349 | terminal_exec_arg, 350 | "toolbox", 351 | "run", 352 | "-c", 353 | toolbox, 354 | *cmd.split(" "), 355 | ] 356 | ) 357 | 358 | def open_application_in_toolbox(self, toolbox: str, *args): 359 | """ 360 | Opens a dialog containing the applications inside the toolbox, 361 | then runs one if selected 362 | """ 363 | apps = get_output( 364 | [ 365 | *FLATPAK_SPAWN_ARR, 366 | "toolbox", 367 | "run", 368 | "-c", 369 | toolbox, 370 | "ls", 371 | "/usr/share/applications", 372 | ] 373 | ) 374 | apps = apps.replace("\r\n", " ") 375 | apps = apps.replace("\t", " ") 376 | 377 | apps = [a for a in apps.split(" ") if a.endswith(".desktop")] 378 | 379 | d = RunApplicationWindow(self, toolbox, apps) 380 | response = d.run() 381 | if response == Gtk.ResponseType.CANCEL: 382 | d.destroy() 383 | 384 | chosen_app = d.get_chosen_app() 385 | d.destroy() 386 | 387 | if chosen_app: 388 | GLib.timeout_add_seconds(0.5, launch_app, toolbox, chosen_app) 389 | 390 | def create_new_toolbox(self): 391 | """ 392 | Shows a dialog to fetch a name, then makes a toolbox with that name. 393 | """ 394 | all_toolboxes = fetch_all_toolbox_names() 395 | 396 | d = ToolboxNameWindow(self, all_toolboxes) 397 | response = d.run() 398 | tb_name = None 399 | if response == Gtk.ResponseType.OK: 400 | tb_name = d.get_entered_name() 401 | 402 | while tb_name in all_toolboxes: 403 | d.show_already_exists_message() 404 | tb_name = None 405 | response = d.run() 406 | 407 | if response == Gtk.ResponseType.OK: 408 | tb_name = d.get_entered_name() 409 | 410 | if tb_name: 411 | subprocess.run([*FLATPAK_SPAWN_ARR, "toolbox", "create", tb_name, "-y"]) 412 | 413 | current_w, current_h = self.get_size() 414 | min_h = 75 * (len(self.toolbox_rows) + 1) 415 | if current_h < min_h and current_h < 900: 416 | self.resize(current_w, min_h) 417 | 418 | self.render_all_toolboxes() 419 | 420 | d.destroy() 421 | 422 | def confirm_delete_toolbox(self, toolbox: str): 423 | """ 424 | Shows a dialog to confirm deleting of a toolbox 425 | Then deletes it if confirmed 426 | """ 427 | dialog = Gtk.MessageDialog( 428 | transient_for=self, 429 | flags=0, 430 | message_type=Gtk.MessageType.QUESTION, 431 | buttons=Gtk.ButtonsType.YES_NO, 432 | text=f"Are you sure you want to delete {toolbox}?", 433 | ) 434 | 435 | dialog.format_secondary_text("This cannot be undone!") 436 | 437 | response = dialog.run() 438 | 439 | if response == Gtk.ResponseType.YES: 440 | execute_delete_toolbox(toolbox) 441 | self.render_all_toolboxes() 442 | 443 | dialog.destroy() 444 | 445 | def view_toolbox_info(self, tb_id: str, toolbox: str): 446 | """ 447 | Shows dialog with information about a toolbox 448 | """ 449 | cmd = ( 450 | f"{FLATPAK_SPAWN}podman ps -a -f id={tb_id}" 451 | + " --format={{.ID}}||{{.Image}}||{{.Status}}||{{.CreatedAt}}" 452 | ) 453 | output = get_output(cmd.split(" ")) 454 | c_id, image, status, created_at = output.split("||") 455 | 456 | info = { 457 | "container_id": c_id, 458 | "image": image, 459 | "status": status, 460 | "created_at": created_at.split(".")[0], 461 | } 462 | 463 | d = InfoWindow(self, toolbox, info) 464 | d.run() 465 | d.destroy() 466 | 467 | def stop_toolbox(self, toolbox: str): 468 | """ 469 | Runs podman stop 470 | """ 471 | subprocess.run([*FLATPAK_SPAWN_ARR, "podman", "stop", toolbox]) 472 | GLib.timeout_add_seconds(1, self.delayed_rerender) 473 | 474 | def copy_desktop_to_host(self, toolbox: str, app: str): 475 | """ 476 | Kicks off copying of .desktop file to the host 477 | """ 478 | copy_desktop_from_toolbox_to_host(toolbox, app) 479 | GLib.timeout_add_seconds(0.5, edit_exec_of_toolbox_desktop, toolbox, app) 480 | GLib.timeout_add_seconds(0.75, copy_icons_for_toolbox_desktop, toolbox, app) 481 | 482 | def show_file_chooser(self): 483 | """ 484 | Shows a file chooser dialog to pick an RPM file to install 485 | """ 486 | dialog = Gtk.FileChooserDialog( 487 | title="Please choose a file to install", 488 | parent=self, 489 | action=Gtk.FileChooserAction.OPEN, 490 | ) 491 | dialog.add_buttons( 492 | Gtk.STOCK_CANCEL, 493 | Gtk.ResponseType.CANCEL, 494 | Gtk.STOCK_OPEN, 495 | Gtk.ResponseType.OK, 496 | ) 497 | 498 | response = dialog.run() 499 | if response == Gtk.ResponseType.OK: 500 | rpm_name = dialog.get_filename() 501 | dialog.destroy() 502 | 503 | return rpm_name 504 | elif response == Gtk.ResponseType.CANCEL: 505 | dialog.destroy() 506 | 507 | return None 508 | 509 | def show_help_window(self, *args): 510 | w = HelpWindow(self) 511 | w.run() 512 | w.destroy() 513 | 514 | def show_about_window(self, *args): 515 | w = AboutWindow(self) 516 | w.run() 517 | w.destroy() 518 | 519 | def delayed_rerender(self, *args): 520 | """ 521 | Used to rerender the rows after a delay 522 | """ 523 | 524 | self.render_all_toolboxes() 525 | -------------------------------------------------------------------------------- /src/run_application_window.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import gobject 3 | from functools import partial 4 | 5 | from app import Gtk, GLib, GdkPixbuf 6 | from utils import set_icon_at_small_size 7 | 8 | 9 | class ImgFetchThread(threading.Thread): 10 | def __init__(self, master): 11 | super().__init__() 12 | self.master = master 13 | 14 | def run(self): 15 | from utils import get_icon_from_desktop 16 | 17 | for app in self.master.imgs.keys(): 18 | icon = get_icon_from_desktop(self.master.toolbox, app) 19 | if not icon: 20 | continue 21 | 22 | img = self.master.imgs[app] 23 | idx, spin = self.master.spinners[app] 24 | 25 | spin.stop() 26 | spin.hide() 27 | 28 | set_icon_at_small_size(icon, img) 29 | 30 | self.master.grid.attach(img, 1, idx, 1, 1) 31 | self.master.grid.show_all() 32 | 33 | self.master.add_icon_to_cache(app, icon) 34 | 35 | return 36 | 37 | 38 | class RunApplicationWindow(Gtk.MessageDialog): 39 | def __init__(self, parent, toolbox, apps: list): 40 | super().__init__( 41 | title=f"Run Application from {toolbox}", transient_for=parent, flags=0 42 | ) 43 | self.apps = apps 44 | self.toolbox = toolbox 45 | self.parent = parent 46 | 47 | self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) 48 | 49 | box = self.get_content_area() 50 | 51 | spacer = Gtk.Box(spacing=10, orientation=Gtk.Orientation.VERTICAL) 52 | spacer.set_border_width(10) 53 | 54 | grid = Gtk.Grid() 55 | grid.set_column_spacing(10) 56 | grid.set_row_spacing(10) 57 | 58 | self.chosen_app = None 59 | self.imgs = {} 60 | self.spinners = {} 61 | for idx, app in enumerate(self.apps): 62 | nice_app = app.replace("-", " ") 63 | nice_app = nice_app.replace("_", " ") 64 | nice_app = nice_app.replace(".desktop", "") 65 | nice_app = nice_app.title() 66 | 67 | img = Gtk.Image() 68 | img.get_style_context().add_class("app_icon") 69 | if app in self.parent.icon_cache: 70 | set_icon_at_small_size(self.parent.icon_cache[app], img) 71 | grid.attach(img, 1, idx, 1, 1) 72 | else: 73 | self.imgs[app] = img 74 | spin = Gtk.Spinner() 75 | self.spinners[app] = [idx, spin] 76 | grid.attach(spin, 1, idx, 1, 1) 77 | spin.start() 78 | 79 | lbl = Gtk.Label(label=nice_app) 80 | lbl.set_xalign(0) 81 | 82 | btn = Gtk.Button(label="Open") 83 | btn.connect("clicked", partial(self.on_app_chosen, app)) 84 | 85 | i_btn = Gtk.Button(label="Add to Menu") 86 | i_btn.connect("clicked", partial(self.add_to_menu, app)) 87 | 88 | grid.attach(lbl, 2, idx, 1, 1) 89 | grid.attach(i_btn, 3, idx, 1, 1) 90 | grid.attach(btn, 4, idx, 1, 1) 91 | 92 | spacer.add(grid) 93 | spacer.show_all() 94 | box.add(spacer) 95 | 96 | self.grid = grid 97 | 98 | self.show_all() 99 | 100 | if len(self.imgs): 101 | self.img_thread = ImgFetchThread(self) 102 | self.img_thread.start() 103 | 104 | def on_app_chosen(self, app: str, *args): 105 | self.chosen_app = app 106 | self.emit("response", Gtk.ResponseType.OK) 107 | 108 | def add_to_menu(self, app: str, *args): 109 | self.parent.copy_desktop_to_host(self.toolbox, app) 110 | self.format_secondary_text(f"{app} copied to host!") 111 | 112 | def set_chosen_app(self, app: str): 113 | self.chosen_app = app 114 | 115 | def get_chosen_app(self): 116 | return self.chosen_app 117 | 118 | def add_icon_to_cache(self, app: str, icon: str): 119 | if app not in self.parent.icon_cache: 120 | self.parent.icon_cache[app] = icon 121 | -------------------------------------------------------------------------------- /src/run_software_window.py: -------------------------------------------------------------------------------- 1 | from app import Gtk 2 | 3 | 4 | class RunSoftwareWindow(Gtk.MessageDialog): 5 | def __init__(self, parent, toolbox): 6 | super().__init__( 7 | title=f"Run Command in {toolbox}", transient_for=parent, flags=0 8 | ) 9 | 10 | self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) 11 | 12 | save_btn = Gtk.Button(label="Run") 13 | self.add_action_widget(save_btn, Gtk.ResponseType.OK) 14 | 15 | self.format_secondary_text("Command to run:") 16 | 17 | self.command = Gtk.Entry() 18 | self.command.connect( 19 | "activate", lambda e: self.emit("response", Gtk.ResponseType.OK) 20 | ) 21 | 22 | box = self.get_content_area() 23 | 24 | spacer = Gtk.Box(spacing=10, orientation=Gtk.Orientation.VERTICAL) 25 | spacer.set_border_width(10) 26 | 27 | spacer.add(self.command) 28 | 29 | box.add(spacer) 30 | 31 | self.show_all() 32 | 33 | def get_entered_command(self): 34 | return self.command.get_text() 35 | -------------------------------------------------------------------------------- /src/toolbox_name_window.py: -------------------------------------------------------------------------------- 1 | from app import Gtk 2 | 3 | 4 | class ToolboxNameWindow(Gtk.MessageDialog): 5 | def __init__(self, parent, toolboxes): 6 | super().__init__(title="Toolbox Name", transient_for=parent, flags=0) 7 | 8 | self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) 9 | 10 | create_btn = Gtk.Button(label="Create") 11 | self.add_action_widget(create_btn, Gtk.ResponseType.OK) 12 | 13 | sec_text = "Enter the name of your new toolbox" 14 | if not len(toolboxes): 15 | sec_text += "\nNote: Downloading the container image may take some time." 16 | 17 | self.format_secondary_text(sec_text) 18 | 19 | self.toolbox_name = Gtk.Entry() 20 | self.toolbox_name.connect( 21 | "activate", lambda e: self.emit("response", Gtk.ResponseType.OK) 22 | ) 23 | 24 | box = self.get_content_area() 25 | 26 | spacer = Gtk.Box(spacing=10, orientation=Gtk.Orientation.VERTICAL) 27 | spacer.set_border_width(10) 28 | spacer.add(self.toolbox_name) 29 | box.add(spacer) 30 | 31 | self.show_all() 32 | 33 | def get_entered_name(self): 34 | return self.toolbox_name.get_text() 35 | 36 | def show_already_exists_message(self): 37 | self.format_secondary_text( 38 | f"Enter the name of your new toolbox\n\nError: A toolbox called {self.get_entered_name()} already exists!" 39 | ) 40 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | import subprocess 4 | import shutil 5 | from functools import partial 6 | 7 | from app import Gtk, Gio, GdkPixbuf 8 | 9 | 10 | def get_output(cmd: list): 11 | """ 12 | Runs command and returns stdout 13 | """ 14 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) 15 | 16 | return proc.stdout.read().decode("utf-8") 17 | 18 | 19 | def get_stderr(cmd: list): 20 | """ 21 | Runs command and returns stderr 22 | """ 23 | proc = subprocess.Popen(cmd, stderr=subprocess.PIPE) 24 | 25 | return proc.stderr.read().decode("utf-8") 26 | 27 | 28 | def create_toolbox_button(icon_name: str, tooltip: str, func): 29 | """ 30 | Makes a button containing an icon which is bound to 31 | the provided func 32 | """ 33 | icon = Gtk.Image() 34 | set_icon_at_small_size(icon_name, icon) 35 | 36 | btn = Gtk.Button(label=None, image=icon) 37 | btn.connect("clicked", lambda b: func()) 38 | btn.set_tooltip_text(tooltip) 39 | btn.get_style_context().add_class("tb_btn") 40 | 41 | return btn 42 | 43 | 44 | def set_icon_at_small_size(icon: str, img: Gtk.Image): 45 | """ 46 | Uses pixbuf to set an icon at 16x16 47 | """ 48 | thm = Gtk.IconTheme.get_default() 49 | info = thm.lookup_icon(icon, 16, 0) 50 | set_from_pb = False 51 | if info: 52 | fn = info.get_filename() 53 | if fn: 54 | pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(fn, 16, 16, True) 55 | img.set_from_pixbuf(pb) 56 | set_from_pb = True 57 | 58 | if not set_from_pb: 59 | img.set_from_icon_name(icon, Gtk.IconSize.BUTTON) 60 | 61 | return img 62 | 63 | 64 | def create_popover_button(icon_name: str, tooltip: str, menu_items: dict): 65 | """ 66 | Makes a popover button containing an icon and the provided menu items 67 | """ 68 | popover = Gtk.Popover() 69 | vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 70 | 71 | for text, func in menu_items.items(): 72 | b = Gtk.ModelButton(label=text) 73 | b.connect("clicked", partial(func)) 74 | 75 | vbox.pack_start(b, False, True, 10) 76 | 77 | vbox.show_all() 78 | popover.add(vbox) 79 | popover.set_position(Gtk.PositionType.BOTTOM) 80 | 81 | icon = Gtk.Image() 82 | set_icon_at_small_size(icon_name, icon) 83 | 84 | btn = Gtk.MenuButton(label=None, image=icon, popover=popover) 85 | btn.set_tooltip_text(tooltip) 86 | 87 | btn.get_style_context().add_class("tb_btn") 88 | 89 | return btn 90 | 91 | 92 | def execute_delete_toolbox(toolbox: str): 93 | """ 94 | Stops and removes toolbox 95 | """ 96 | subprocess.run([*FLATPAK_SPAWN_ARR, "podman", "stop", toolbox]) 97 | subprocess.run([*FLATPAK_SPAWN_ARR, "toolbox", "rm", toolbox]) 98 | 99 | 100 | def fetch_all_toolboxes(): 101 | """ 102 | Returns info about all toolboxes 103 | """ 104 | cmd = ( 105 | f"{FLATPAK_SPAWN}podman ps -a --format=" 106 | + "{{.Names}}||{{.Image}}||{{.Status}}||{{.ID}}" 107 | ) 108 | 109 | toolboxes = get_output(cmd.split(" ")) 110 | 111 | toolboxes = toolboxes.split("\n")[:-1] 112 | 113 | retval = [] 114 | 115 | for tb in toolboxes: 116 | name, image, status, tb_id = tb.split("||") 117 | image_num = image.split(":")[-1] 118 | retval.append((name, image_num, status, tb_id)) 119 | 120 | return retval 121 | 122 | 123 | def fetch_all_toolbox_names(): 124 | """ 125 | Returns names of all toolboxes 126 | """ 127 | cmd = f"{FLATPAK_SPAWN}podman ps -a --format=" + "{{.Names}}" 128 | toolboxes = get_output(cmd.split(" ")) 129 | 130 | toolboxes = toolboxes.split("\n")[:-1] 131 | 132 | retval = [] 133 | 134 | for tb in toolboxes: 135 | retval.append(tb) 136 | 137 | return retval 138 | 139 | 140 | def launch_app(toolbox: str, app: str): 141 | """ 142 | Opens provided app in the toolbox 143 | """ 144 | # gtk-launch doesnt work from toolbox run :/ 145 | app_exec_cmd = get_exec_from_desktop(toolbox, app) 146 | if app_exec_cmd: 147 | cmd = app_exec_cmd.split(" ") 148 | subprocess.Popen([*FLATPAK_SPAWN_ARR, "toolbox", "run", "-c", toolbox, *cmd]) 149 | 150 | 151 | def get_exec_from_desktop(toolbox: str, app: str): 152 | """ 153 | Returns the Exec= line from a .desktop file 154 | """ 155 | contents = get_output( 156 | f"{FLATPAK_SPAWN}toolbox run -c {toolbox} cat /usr/share/applications/{app}".split( 157 | " " 158 | ) 159 | ) 160 | exec_cmd = None 161 | for line in contents.split("\n"): 162 | if line.startswith("Exec="): 163 | exec_cmd = line.replace("Exec=", "").strip() 164 | break 165 | 166 | return exec_cmd 167 | 168 | 169 | def get_icon_from_desktop(toolbox: str, app: str): 170 | """ 171 | Returns the Icon= line from a .desktop file 172 | """ 173 | contents = get_output( 174 | f"{FLATPAK_SPAWN}toolbox run -c {toolbox} cat /usr/share/applications/{app}".split( 175 | " " 176 | ) 177 | ) 178 | icon = None 179 | for line in contents.split("\n"): 180 | if line.startswith("Icon="): 181 | icon = line.replace("Icon=", "").strip() 182 | break 183 | 184 | return icon 185 | 186 | 187 | def copy_desktop_from_toolbox_to_host(toolbox: str, app: str): 188 | """ 189 | Copies .desktop file from toolbox into host 190 | """ 191 | home = os.path.expanduser("~") 192 | local_folder = f"{home}/.local/share/applications" 193 | if not os.path.exists(local_folder): 194 | os.makedirs(local_folder) 195 | 196 | subprocess.run( 197 | [ 198 | *FLATPAK_SPAWN_ARR, 199 | "toolbox", 200 | "run", 201 | "-c", 202 | toolbox, 203 | "cp", 204 | f"/usr/share/applications/{app}", 205 | f"{home}/.local/share/applications/{app}", 206 | ] 207 | ) 208 | 209 | 210 | def edit_exec_of_toolbox_desktop(toolbox: str, app: str): 211 | """ 212 | Replaces Exec= line from a .desktop file to include the toolbox run 213 | """ 214 | home = os.path.expanduser("~") 215 | app_path = f"{home}/.local/share/applications/{app}" 216 | if not os.path.exists(app_path): 217 | time.sleep(1) 218 | 219 | if not os.path.exists(app_path): 220 | # bail 221 | return 222 | 223 | content = [] 224 | with open(app_path, "r") as f: 225 | content = f.readlines() 226 | for idx, line in enumerate(content): 227 | if line.startswith("Exec="): 228 | content[idx] = line.replace("Exec=", f"Exec=toolbox run -c {toolbox} ") 229 | 230 | with open(app_path, "w") as f: 231 | f.writelines(content) 232 | 233 | 234 | def copy_icons_for_toolbox_desktop(toolbox: str, app: str): 235 | """ 236 | Searches for icons matching the app name inside the toolbox and 237 | copies them to ~/.icons on the host 238 | """ 239 | home = os.path.expanduser("~") 240 | app_path = f"{home}/.local/share/applications/{app}" 241 | if not os.path.exists(app_path): 242 | time.sleep(1) 243 | 244 | if not os.path.exists(app_path): 245 | # bail 246 | return 247 | 248 | content = [] 249 | icon_name = "" 250 | with open(app_path, "r") as f: 251 | content = f.readlines() 252 | for line in content: 253 | if line.startswith("Icon="): 254 | icon_name = line[5:] 255 | 256 | if not icon_name: 257 | return 258 | 259 | if icon_name.endswith(".png") or icon_name.endswith(".svg"): 260 | # is a file rather than a name 261 | # try copying it over? 262 | return 263 | 264 | icon_name = icon_name.strip() 265 | 266 | if not os.path.exists(f"{home}/.icons"): 267 | os.makedirs(f"{home}/.icons") 268 | 269 | data_path = f"{home}/.icons" 270 | script_dir = os.path.join( 271 | os.path.abspath(os.path.dirname(__file__)), "echo_into_file.sh" 272 | ) 273 | if is_flatpak(): 274 | data_path = os.getenv("XDG_DATA_HOME") 275 | 276 | fp_script_path = os.path.join(os.getenv("XDG_DATA_HOME"), "echo_into_file.sh") 277 | shutil.copyfile(script_dir, fp_script_path) 278 | os.chmod(fp_script_path, 0o0744) 279 | 280 | script_dir = fp_script_path 281 | 282 | icon_results_file = f"{data_path}/tb_gui_icon_files.txt" 283 | 284 | cmd = [ 285 | *FLATPAK_SPAWN_ARR, 286 | "toolbox", 287 | "run", 288 | "-c", 289 | toolbox, 290 | script_dir, 291 | icon_name, 292 | data_path, 293 | ] 294 | subprocess.run(cmd) 295 | 296 | if not os.path.exists(icon_results_file): 297 | # bail 298 | return 299 | 300 | with open(icon_results_file, "r") as f: 301 | for line in f: 302 | line = line.strip() 303 | dest_path = line.replace("/usr/share/icons", f"{home}/.icons") 304 | 305 | if not os.path.exists(os.path.dirname(dest_path)): 306 | os.makedirs(os.path.dirname(dest_path)) 307 | 308 | subprocess.run( 309 | [ 310 | *FLATPAK_SPAWN_ARR, 311 | "toolbox", 312 | "run", 313 | "-c", 314 | toolbox, 315 | "cp", 316 | line, 317 | dest_path, 318 | ] 319 | ) 320 | 321 | os.remove(icon_results_file) 322 | 323 | 324 | def is_dark_theme(): 325 | """ 326 | Tries to find if the user has set themselves to dark mode 327 | """ 328 | try: 329 | out = subprocess.run( 330 | [ 331 | *FLATPAK_SPAWN_ARR, 332 | "gsettings", 333 | "get", 334 | "org.gnome.desktop.interface", 335 | "color-scheme", 336 | ], 337 | capture_output=True, 338 | ) 339 | stdout = out.stdout.decode() 340 | except: 341 | return False 342 | 343 | try: 344 | theme = stdout.lower().strip()[1:-1] 345 | if "-dark" in theme.lower(): 346 | return True 347 | else: 348 | return False 349 | except IndexError: 350 | return False 351 | 352 | 353 | def is_flatpak(): 354 | f = os.getenv("FLATPAK_ID") 355 | if f: 356 | return True 357 | return False 358 | 359 | 360 | FLATPAK_SPAWN = "flatpak-spawn --host " if is_flatpak() else "" 361 | FLATPAK_SPAWN_ARR = ["flatpak-spawn", "--host"] if is_flatpak() else [] 362 | -------------------------------------------------------------------------------- /toolbox-gui: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | python3 src/app.py 3 | -------------------------------------------------------------------------------- /toolbox-gui-fp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | export PYTHONPATH=/app 3 | python3 /app/src/app.py 4 | -------------------------------------------------------------------------------- /toolbox-gui.flatpak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dvlv/toolbox-gui/e8d80598eb59bec0d36c9bed8e86a0824e93a132/toolbox-gui.flatpak --------------------------------------------------------------------------------