builauncher 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.3
2
+ Name: builauncher
3
+ Version: 0.1.1
4
+ Summary: Pie-menu launcher with type-to-search, on BUI
5
+ Requires-Dist: blenderui>=0.4.1
6
+ Requires-Dist: numpy
7
+ Requires-Dist: rapidfuzz>=3
8
+ Requires-Dist: jeepney>=0.8 ; sys_platform == 'linux'
9
+ Requires-Dist: pyside6>=6.6
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+
13
+ # builauncher
14
+
15
+ A pie-menu launcher drawn with [BUI](https://github.com/ppmpreetham/BUI), Blender's UI as a
16
+ library, in a Qt window through PySide6 (Qt for Python; PyQt is not used). Press a hotkey,
17
+ flick towards an item, or type to search every command and installed app.
18
+
19
+ ![The pie, and a search for "p"](assets/screenshot.png)
20
+
21
+ ## Install
22
+
23
+ uv tool install builauncher # or: pipx install builauncher, or uvx builauncher to try it
24
+ builauncher install # start the daemon at every login
25
+ builauncher daemon & # start it now, for this session
26
+
27
+ Then press Ctrl+Alt+Space. `builauncher uninstall` removes the login entries. Run `install`
28
+ from an installed copy, not from `uvx`: the environment `uvx` makes is temporary.
29
+
30
+ ## Use
31
+
32
+ | Input | Result |
33
+ | --- | --- |
34
+ | Hotkey | Open the pie. The hotkey again closes it. |
35
+ | Hold the hotkey, move the pointer, release | Run the item in that direction. |
36
+ | Drag from the empty middle | Run the item in the drag direction. |
37
+ | Click an item | Run it, or open its sub-pie. |
38
+ | Click the pie centre | Back to the parent pie. |
39
+ | Search item, or start typing | Search menu commands and installed apps. |
40
+ | Up, Down, Return | Pick a result and run it. |
41
+ | Backspace on an empty search, or Tab | Back to the pie. |
42
+ | Escape, Alt+F4, a click outside | Close. |
43
+
44
+ ## Commands
45
+
46
+ | Command | Does |
47
+ | --- | --- |
48
+ | `builauncher` or `builauncher open` | Open the menu. With no daemon running, it opens once and exits. |
49
+ | `builauncher daemon` | Stay running and bind the global shortcut. |
50
+ | `builauncher stop` | Stop the running daemon. |
51
+ | `builauncher install` | Add the login entry and the app entry (XDG autostart). |
52
+ | `builauncher uninstall` | Remove them. |
53
+
54
+ `open` and `daemon` take `--config FILE`.
55
+
56
+ ## Config
57
+
58
+ Copy `builauncher/default.toml` to `~/.config/builauncher/menu.toml` and edit it. On Windows,
59
+ copy `builauncher/default-windows.toml` to `%APPDATA%\builauncher\menu.toml`.
60
+
61
+ shortcut = "CTRL+ALT+space" # asked for at the first start
62
+ scale = 1.0 # size of the pie and the search
63
+ apps = true # also search installed apps
64
+
65
+ [[item]]
66
+ name = "Terminal"
67
+ icon = "CONSOLE" # a Blender icon name
68
+ exec = "konsole" # run a command
69
+
70
+ [[item]]
71
+ name = "Media"
72
+ icon = "SOUND"
73
+ pie = "media" # open the sub-pie named media
74
+
75
+ [[pie.media]]
76
+ name = "Next"
77
+ icon = "FF"
78
+ exec = "playerctl next"
79
+
80
+ An item has either `exec` or `pie`. Each `[[pie.NAME]]` table is one item of that
81
+ sub-pie, and its items can open further sub-pies. `pie = "places"` is built in: Home,
82
+ Documents, Downloads, Music, Pictures, Videos and the Trash. A `[[pie.places]]` of your
83
+ own replaces it.
84
+
85
+ The top pie holds 7 items, because Search takes one slot. A sub-pie also holds 7,
86
+ because Back takes one. A pie that opens itself, directly or through another, is rejected
87
+ at load.
88
+
89
+ ## Desktop support
90
+ ### Linux
91
+ The daemon asks the desktop for its shortcut through the
92
+ [XDG GlobalShortcuts portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html),
93
+ which KDE Plasma, GNOME 48 and later, and Hyprland provide. After the first start, change
94
+ the shortcut in the desktop's shortcut settings. On a desktop without the portal, bind the
95
+ `builauncher` command to a key yourself.
96
+
97
+ The menu opens under the pointer. On Wayland an app cannot place its window or read the
98
+ pointer, so builauncher covers the screen with a transparent window and draws the menu
99
+ where the pointer enters it. That window is a layer-shell overlay when Qt has the
100
+ layer-shell plugin, and a full-screen window otherwise, so it works on any compositor,
101
+ GNOME included. On X11 the window moves to the pointer.
102
+
103
+ `install` writes an XDG autostart entry. Sway and Hyprland do not run those, so add
104
+ `builauncher daemon` to their startup config.
105
+
106
+ ### Windows
107
+
108
+ The commands and the config are the same. Underneath:
109
+
110
+ | Linux | Windows |
111
+ | --- | --- |
112
+ | Shortcut from the XDG portal | `RegisterHotKey`; a timer polls `GetAsyncKeyState` for the release, so hold and release still works. If another app holds the shortcut, the daemon says so. |
113
+ | `install` writes an XDG autostart entry | `install` adds a value under `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` that starts the daemon with `pythonw`, so no console opens. |
114
+ | Unix socket in `$XDG_RUNTIME_DIR` | Named pipe `\\.\pipe\builauncher` |
115
+ | Apps from `.desktop` files | Apps from Start Menu shortcuts |
116
+ | `xdg-open`, `trash:///` | `explorer`, `shell:RecycleBinFolder` |
117
+ | Transparent full-screen window, menu under the pointer | Frameless, always-on-top window at the pointer |
118
+
119
+ The Windows default menu uses Windows Terminal (`wt`), sends the media keys through
120
+ PowerShell, and locks, sleeps, restarts and shuts down with `rundll32` and `shutdown`.
121
+ Sleep hibernates instead when hibernation is on.
122
+
123
+ ## Development
124
+
125
+ uv venv --system-site-packages
126
+ uv sync
127
+ uv run pytest
128
+ uv build
129
+
130
+ ## Thanks
131
+
132
+ - [Blender](https://www.blender.org), whose UI and pie menus BUI follows.
133
+ - [BUI](https://github.com/ppmpreetham/BUI), which draws the menu.
134
+ - [Kando](https://github.com/kando-menu/kando), for the gestures, timings and fades.
135
+ - [PySide6](https://doc.qt.io/qtforpython-6/) (Qt for Python), for the window.
136
+ - [RapidFuzz](https://github.com/rapidfuzz/RapidFuzz), for the search.
137
+ - [jeepney](https://gitlab.com/takluyver/jeepney), for D-Bus.
138
+ - [numpy](https://numpy.org), for the transparency.
@@ -0,0 +1,126 @@
1
+ # builauncher
2
+
3
+ A pie-menu launcher drawn with [BUI](https://github.com/ppmpreetham/BUI), Blender's UI as a
4
+ library, in a Qt window through PySide6 (Qt for Python; PyQt is not used). Press a hotkey,
5
+ flick towards an item, or type to search every command and installed app.
6
+
7
+ ![The pie, and a search for "p"](assets/screenshot.png)
8
+
9
+ ## Install
10
+
11
+ uv tool install builauncher # or: pipx install builauncher, or uvx builauncher to try it
12
+ builauncher install # start the daemon at every login
13
+ builauncher daemon & # start it now, for this session
14
+
15
+ Then press Ctrl+Alt+Space. `builauncher uninstall` removes the login entries. Run `install`
16
+ from an installed copy, not from `uvx`: the environment `uvx` makes is temporary.
17
+
18
+ ## Use
19
+
20
+ | Input | Result |
21
+ | --- | --- |
22
+ | Hotkey | Open the pie. The hotkey again closes it. |
23
+ | Hold the hotkey, move the pointer, release | Run the item in that direction. |
24
+ | Drag from the empty middle | Run the item in the drag direction. |
25
+ | Click an item | Run it, or open its sub-pie. |
26
+ | Click the pie centre | Back to the parent pie. |
27
+ | Search item, or start typing | Search menu commands and installed apps. |
28
+ | Up, Down, Return | Pick a result and run it. |
29
+ | Backspace on an empty search, or Tab | Back to the pie. |
30
+ | Escape, Alt+F4, a click outside | Close. |
31
+
32
+ ## Commands
33
+
34
+ | Command | Does |
35
+ | --- | --- |
36
+ | `builauncher` or `builauncher open` | Open the menu. With no daemon running, it opens once and exits. |
37
+ | `builauncher daemon` | Stay running and bind the global shortcut. |
38
+ | `builauncher stop` | Stop the running daemon. |
39
+ | `builauncher install` | Add the login entry and the app entry (XDG autostart). |
40
+ | `builauncher uninstall` | Remove them. |
41
+
42
+ `open` and `daemon` take `--config FILE`.
43
+
44
+ ## Config
45
+
46
+ Copy `builauncher/default.toml` to `~/.config/builauncher/menu.toml` and edit it. On Windows,
47
+ copy `builauncher/default-windows.toml` to `%APPDATA%\builauncher\menu.toml`.
48
+
49
+ shortcut = "CTRL+ALT+space" # asked for at the first start
50
+ scale = 1.0 # size of the pie and the search
51
+ apps = true # also search installed apps
52
+
53
+ [[item]]
54
+ name = "Terminal"
55
+ icon = "CONSOLE" # a Blender icon name
56
+ exec = "konsole" # run a command
57
+
58
+ [[item]]
59
+ name = "Media"
60
+ icon = "SOUND"
61
+ pie = "media" # open the sub-pie named media
62
+
63
+ [[pie.media]]
64
+ name = "Next"
65
+ icon = "FF"
66
+ exec = "playerctl next"
67
+
68
+ An item has either `exec` or `pie`. Each `[[pie.NAME]]` table is one item of that
69
+ sub-pie, and its items can open further sub-pies. `pie = "places"` is built in: Home,
70
+ Documents, Downloads, Music, Pictures, Videos and the Trash. A `[[pie.places]]` of your
71
+ own replaces it.
72
+
73
+ The top pie holds 7 items, because Search takes one slot. A sub-pie also holds 7,
74
+ because Back takes one. A pie that opens itself, directly or through another, is rejected
75
+ at load.
76
+
77
+ ## Desktop support
78
+ ### Linux
79
+ The daemon asks the desktop for its shortcut through the
80
+ [XDG GlobalShortcuts portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html),
81
+ which KDE Plasma, GNOME 48 and later, and Hyprland provide. After the first start, change
82
+ the shortcut in the desktop's shortcut settings. On a desktop without the portal, bind the
83
+ `builauncher` command to a key yourself.
84
+
85
+ The menu opens under the pointer. On Wayland an app cannot place its window or read the
86
+ pointer, so builauncher covers the screen with a transparent window and draws the menu
87
+ where the pointer enters it. That window is a layer-shell overlay when Qt has the
88
+ layer-shell plugin, and a full-screen window otherwise, so it works on any compositor,
89
+ GNOME included. On X11 the window moves to the pointer.
90
+
91
+ `install` writes an XDG autostart entry. Sway and Hyprland do not run those, so add
92
+ `builauncher daemon` to their startup config.
93
+
94
+ ### Windows
95
+
96
+ The commands and the config are the same. Underneath:
97
+
98
+ | Linux | Windows |
99
+ | --- | --- |
100
+ | Shortcut from the XDG portal | `RegisterHotKey`; a timer polls `GetAsyncKeyState` for the release, so hold and release still works. If another app holds the shortcut, the daemon says so. |
101
+ | `install` writes an XDG autostart entry | `install` adds a value under `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` that starts the daemon with `pythonw`, so no console opens. |
102
+ | Unix socket in `$XDG_RUNTIME_DIR` | Named pipe `\\.\pipe\builauncher` |
103
+ | Apps from `.desktop` files | Apps from Start Menu shortcuts |
104
+ | `xdg-open`, `trash:///` | `explorer`, `shell:RecycleBinFolder` |
105
+ | Transparent full-screen window, menu under the pointer | Frameless, always-on-top window at the pointer |
106
+
107
+ The Windows default menu uses Windows Terminal (`wt`), sends the media keys through
108
+ PowerShell, and locks, sleeps, restarts and shuts down with `rundll32` and `shutdown`.
109
+ Sleep hibernates instead when hibernation is on.
110
+
111
+ ## Development
112
+
113
+ uv venv --system-site-packages
114
+ uv sync
115
+ uv run pytest
116
+ uv build
117
+
118
+ ## Thanks
119
+
120
+ - [Blender](https://www.blender.org), whose UI and pie menus BUI follows.
121
+ - [BUI](https://github.com/ppmpreetham/BUI), which draws the menu.
122
+ - [Kando](https://github.com/kando-menu/kando), for the gestures, timings and fades.
123
+ - [PySide6](https://doc.qt.io/qtforpython-6/) (Qt for Python), for the window.
124
+ - [RapidFuzz](https://github.com/rapidfuzz/RapidFuzz), for the search.
125
+ - [jeepney](https://gitlab.com/takluyver/jeepney), for D-Bus.
126
+ - [numpy](https://numpy.org), for the transparency.
File without changes
@@ -0,0 +1,49 @@
1
+ """The `builauncher` command. Qt loads only in the process that shows the menu."""
2
+ import argparse
3
+ import os
4
+ import subprocess
5
+ import sys
6
+
7
+ from .install import install, uninstall
8
+ from .system import WINDOWS
9
+ from .trigger import STOP, TRIGGER, send
10
+
11
+
12
+ def parser() -> argparse.ArgumentParser:
13
+ cli = argparse.ArgumentParser(prog="builauncher", description="Pie-menu launcher on BUI.")
14
+ sub = cli.add_subparsers(dest="command", metavar="command")
15
+ sub.add_parser("open", help="open the menu: trigger the daemon, or open it once (the default)")
16
+ sub.add_parser("daemon", help="stay running, bind the global shortcut, wait for triggers")
17
+ sub.add_parser("stop", help="stop the running daemon")
18
+ sub.add_parser("install", help="add the autostart entry and the app entry")
19
+ sub.add_parser("uninstall", help="remove them")
20
+ for name in ("open", "daemon"):
21
+ sub.choices[name].add_argument("--config", metavar="TOML", help="menu file (default: ~/.config/builauncher/menu.toml)")
22
+ return cli
23
+
24
+
25
+ def main() -> int:
26
+ args = parser().parse_args()
27
+ match args.command:
28
+ case "install":
29
+ install()
30
+ case "uninstall":
31
+ uninstall()
32
+ case "stop":
33
+ if not send(STOP):
34
+ print("builauncher: no daemon runs", file=sys.stderr)
35
+ return 1
36
+ case "daemon" | "open" | None:
37
+ daemon = args.command == "daemon"
38
+ if not daemon and send(TRIGGER):
39
+ return 0
40
+ config = [args.config] if getattr(args, "config", None) else []
41
+ argv = [sys.executable, "-m", "builauncher.app", *(["--daemon"] if daemon else []), *config]
42
+ if WINDOWS: # Windows execv does not quote arguments with spaces
43
+ return subprocess.call(argv)
44
+ os.execv(sys.executable, argv)
45
+ return 0
46
+
47
+
48
+ if __name__ == "__main__":
49
+ sys.exit(main())
@@ -0,0 +1,48 @@
1
+ """The launcher process: `--daemon` waits for triggers, else it opens the menu once."""
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import buipy
7
+ from PySide6.QtWidgets import QApplication
8
+
9
+ from .items.apps import apps
10
+ from .config import load
11
+ from .daemon import serve
12
+ from .launcher import Launcher
13
+ from .items.menu import actions
14
+ from .shortcut import bind
15
+ from .window import Overlay, layer_shell
16
+
17
+ SHELL_VAR = "QT_WAYLAND_SHELL_INTEGRATION"
18
+
19
+
20
+ def main() -> int:
21
+ daemon = "--daemon" in sys.argv[1:]
22
+ paths = [Path(a) for a in sys.argv[1:] if a != "--daemon"]
23
+ # wayland: a layer surface has no frame, no tiling, and stays above windows
24
+ shell = os.environ.get(SHELL_VAR)
25
+ if layer_shell():
26
+ os.environ[SHELL_VAR] = "layer-shell"
27
+ qt = QApplication(sys.argv[:1])
28
+ qt.setQuitOnLastWindowClosed(not daemon)
29
+ cfg = load(paths[0] if paths else None)
30
+ buipy.kit_set_scale(cfg.scale) # before any size is taken from the kit unit
31
+ entries = [*actions(cfg.menu), *(("App", a) for a in (apps() if cfg.apps else ()))]
32
+ launcher = Launcher(cfg.menu, entries, lambda: window.dismiss())
33
+ window = Overlay(launcher, daemon)
34
+ # the window has its shell now; launched apps must not inherit layer-shell (they open full screen)
35
+ os.environ.pop(SHELL_VAR, None) if shell is None else os.environ.update({SHELL_VAR: shell})
36
+ if not daemon:
37
+ window.summon()
38
+ return qt.exec()
39
+ server = serve(window.trigger) # noqa: F841 (kept alive)
40
+ try:
41
+ portal = bind(cfg.shortcut, window.trigger, window.release) # noqa: F841 (kept alive)
42
+ except (OSError, ValueError) as error:
43
+ print(f"builauncher: no global shortcut ({error}); bind `builauncher` in the desktop", file=sys.stderr)
44
+ return qt.exec()
45
+
46
+
47
+ if __name__ == "__main__":
48
+ sys.exit(main())
@@ -0,0 +1,37 @@
1
+ """Load the menu from ~/.config/builauncher/menu.toml (or the bundled default)."""
2
+ import tomllib
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from .items.menu import PIE_SLOTS, Menu, parse
7
+ from .items.places import places
8
+ from .system import WINDOWS, config_dir
9
+
10
+ DEFAULT = Path(__file__).with_name("default-windows.toml" if WINDOWS else "default.toml")
11
+ SHORTCUT = "CTRL+ALT+space" # XDG shortcuts format
12
+ SCALE = 1.0 # BUI ui scale; 1 is Blender's default
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class Config:
17
+ menu: Menu
18
+ apps: bool = True
19
+ shortcut: str = SHORTCUT # asked for at the first bind; then the desktop's settings own it
20
+ scale: float = SCALE
21
+
22
+
23
+ def user_path() -> Path:
24
+ return config_dir() / "menu.toml"
25
+
26
+
27
+ def load(path: Path | None = None) -> Config:
28
+ path = path or (user_path() if user_path().exists() else DEFAULT)
29
+ raw = tomllib.loads(path.read_text())
30
+ pies = {"places": places(), **raw.get("pie", {})} # built-in pies; a [[pie.places]] replaces one
31
+ items = tuple(parse(item, pies) for item in raw.get("item", []))
32
+ if not 0 < len(items) < PIE_SLOTS:
33
+ raise ValueError(f"{path}: the top menu needs 1 to {PIE_SLOTS - 1} items (one slot is Search)")
34
+ scale = float(raw.get("scale", SCALE))
35
+ if not 0.5 <= scale <= 4:
36
+ raise ValueError(f"{path}: scale must be 0.5 to 4")
37
+ return Config(Menu("builauncher", items), raw.get("apps", True), raw.get("shortcut", SHORTCUT), scale)
@@ -0,0 +1,31 @@
1
+ """One running launcher: listen on a Unix socket (a named pipe on Windows) for triggers and stop."""
2
+ from collections.abc import Callable
3
+
4
+ from PySide6.QtCore import QCoreApplication
5
+ from PySide6.QtNetwork import QLocalServer
6
+
7
+ from .system import server_name
8
+ from .trigger import STOP, TRIGGER
9
+
10
+
11
+ def serve(on_trigger: Callable[[], None]) -> QLocalServer:
12
+ """Listen for triggers. Keep the result alive."""
13
+ path = server_name()
14
+ QLocalServer.removeServer(path) # a socket left by a crash
15
+ server = QLocalServer()
16
+ if not server.listen(path):
17
+ raise OSError(f"{path}: {server.errorString()}")
18
+
19
+ handlers = {TRIGGER: on_trigger, STOP: QCoreApplication.quit}
20
+
21
+ def read(message: bytes) -> None:
22
+ if handler := handlers.get(message):
23
+ handler()
24
+
25
+ def accept() -> None:
26
+ while conn := server.nextPendingConnection():
27
+ conn.readyRead.connect(lambda c=conn: read(c.readAll().data()))
28
+ conn.disconnected.connect(conn.deleteLater)
29
+
30
+ server.newConnection.connect(accept)
31
+ return server
@@ -0,0 +1,75 @@
1
+ # Copy to %APPDATA%\builauncher\menu.toml and edit.
2
+ # The same format as the Linux default.toml: an item runs a command (`exec`) or opens a
3
+ # sub-pie (`pie = "name"`); `pie = "places"` is built in. `icon` is a Blender icon name.
4
+ apps = true # also search Start Menu shortcuts
5
+ shortcut = "CTRL+ALT+space" # RegisterHotKey; another app may already hold it
6
+ scale = 1.0 # size of the pie and the search (BUI ui scale)
7
+
8
+ # the top pie: 1 to 7 items, as Search takes a slot
9
+
10
+ [[item]]
11
+ name = "Terminal"
12
+ icon = "CONSOLE"
13
+ exec = "wt"
14
+
15
+ [[item]]
16
+ name = "Browser"
17
+ icon = "WORLD"
18
+ exec = 'explorer "https://"'
19
+
20
+ [[item]]
21
+ name = "Files"
22
+ icon = "FILE_FOLDER"
23
+ pie = "places"
24
+
25
+ [[item]]
26
+ name = "Media"
27
+ icon = "SOUND"
28
+ pie = "media"
29
+
30
+ [[item]]
31
+ name = "System"
32
+ icon = "PREFERENCES"
33
+ pie = "system"
34
+
35
+ # sub-pies. Windows has no media command, so WScript sends the media keys (179, 176, 177).
36
+
37
+ [[pie.media]]
38
+ name = "Play / Pause"
39
+ icon = "PLAY"
40
+ exec = 'powershell -NoProfile -WindowStyle Hidden -Command "(New-Object -ComObject WScript.Shell).SendKeys([char]179)"'
41
+
42
+ [[pie.media]]
43
+ name = "Next"
44
+ icon = "FF"
45
+ exec = 'powershell -NoProfile -WindowStyle Hidden -Command "(New-Object -ComObject WScript.Shell).SendKeys([char]176)"'
46
+
47
+ [[pie.media]]
48
+ name = "Previous"
49
+ icon = "REW"
50
+ exec = 'powershell -NoProfile -WindowStyle Hidden -Command "(New-Object -ComObject WScript.Shell).SendKeys([char]177)"'
51
+
52
+ [[pie.system]]
53
+ name = "Lock"
54
+ icon = "LOCKED"
55
+ exec = "rundll32.exe user32.dll,LockWorkStation"
56
+
57
+ [[pie.system]]
58
+ name = "Sleep"
59
+ icon = "SORTTIME"
60
+ exec = "rundll32.exe powrprof.dll,SetSuspendState 0,1,0" # hibernates if hibernation is on
61
+
62
+ [[pie.system]]
63
+ name = "Power"
64
+ icon = "QUIT"
65
+ pie = "power"
66
+
67
+ [[pie.power]]
68
+ name = "Restart"
69
+ icon = "FILE_REFRESH"
70
+ exec = "shutdown /r /t 0"
71
+
72
+ [[pie.power]]
73
+ name = "Shut down"
74
+ icon = "QUIT"
75
+ exec = "shutdown /s /t 0"
@@ -0,0 +1,79 @@
1
+ # Copy to ~/.config/builauncher/menu.toml and edit.
2
+ # An item runs a command (`exec`) or opens a sub-pie (`pie = "name"`).
3
+ # `[[pie.name]]` tables are that sub-pie's items: 1 to 7, as Back takes a slot.
4
+ # `pie = "places"` is built in: home, documents, downloads, ... and the trash.
5
+ # `icon` is a Blender icon name (CONSOLE, FILE_FOLDER, PLAY, ...).
6
+ apps = true # also search installed .desktop apps
7
+ # global shortcut, asked of the desktop at the first daemon start (XDG GlobalShortcuts portal).
8
+ # after that, change it in the desktop's shortcut settings.
9
+ shortcut = "CTRL+ALT+space"
10
+ scale = 1.0 # size of the pie and the search (BUI ui scale)
11
+
12
+ # the top pie: 1 to 7 items, as Search takes a slot
13
+
14
+ [[item]]
15
+ name = "Terminal"
16
+ icon = "CONSOLE"
17
+ exec = "konsole"
18
+
19
+ [[item]]
20
+ name = "Browser"
21
+ icon = "WORLD"
22
+ exec = "xdg-open https://"
23
+
24
+ [[item]]
25
+ name = "Files"
26
+ icon = "FILE_FOLDER"
27
+ pie = "places"
28
+
29
+ [[item]]
30
+ name = "Media"
31
+ icon = "SOUND"
32
+ pie = "media"
33
+
34
+ [[item]]
35
+ name = "System"
36
+ icon = "PREFERENCES"
37
+ pie = "system"
38
+
39
+ # sub-pies
40
+
41
+ [[pie.media]]
42
+ name = "Play / Pause"
43
+ icon = "PLAY"
44
+ exec = "playerctl play-pause"
45
+
46
+ [[pie.media]]
47
+ name = "Next"
48
+ icon = "FF"
49
+ exec = "playerctl next"
50
+
51
+ [[pie.media]]
52
+ name = "Previous"
53
+ icon = "REW"
54
+ exec = "playerctl previous"
55
+
56
+ [[pie.system]]
57
+ name = "Lock"
58
+ icon = "LOCKED"
59
+ exec = "loginctl lock-session"
60
+
61
+ [[pie.system]]
62
+ name = "Suspend"
63
+ icon = "SORTTIME"
64
+ exec = "systemctl suspend"
65
+
66
+ [[pie.system]]
67
+ name = "Power"
68
+ icon = "QUIT"
69
+ pie = "power"
70
+
71
+ [[pie.power]]
72
+ name = "Reboot"
73
+ icon = "FILE_REFRESH"
74
+ exec = "systemctl reboot"
75
+
76
+ [[pie.power]]
77
+ name = "Shut down"
78
+ icon = "QUIT"
79
+ exec = "systemctl poweroff"
@@ -0,0 +1,13 @@
1
+ """Fuzzy search with RapidFuzz."""
2
+ from collections.abc import Callable, Sequence
3
+
4
+ from rapidfuzz import fuzz, process, utils
5
+
6
+ CUTOFF = 50 # lowest WRatio score (0-100) that shows
7
+
8
+
9
+ def search[T](query: str, items: Sequence[T], key: Callable[[T], str], limit: int = 8) -> list[T]:
10
+ """Return the best `limit` items for query, best first."""
11
+ found = process.extract(query, [key(i) for i in items], scorer=fuzz.WRatio,
12
+ processor=utils.default_process, limit=limit, score_cutoff=CUTOFF)
13
+ return [items[index] for _, _, index in found]
@@ -0,0 +1,82 @@
1
+ """`builauncher install`: start the daemon at login.
2
+
3
+ Linux: an XDG autostart entry, and the .desktop file the shortcut portal knows the app by.
4
+ Both are freedesktop.org standards, so KDE, GNOME, Xfce, Cinnamon and others read them.
5
+ Windows: a value under the user's Run registry key, with pythonw so no console opens.
6
+ """
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from .system import APP_ID, WINDOWS
14
+
15
+ if WINDOWS:
16
+ import winreg
17
+
18
+ RUN_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
19
+ ENTRY = """[Desktop Entry]
20
+ Type=Application
21
+ Name=builauncher
22
+ Comment=Pie menu launcher
23
+ Exec={exec}
24
+ Icon=system-run
25
+ Terminal=false
26
+ NoDisplay=true
27
+ X-GNOME-Autostart-enabled=true
28
+ """
29
+
30
+
31
+ def paths() -> tuple[Path, Path]:
32
+ """The app entry (portal app id) and the autostart entry."""
33
+ data = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local/share")
34
+ config = Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config")
35
+ return data / "applications" / f"{APP_ID}.desktop", config / "autostart" / f"{APP_ID}.desktop"
36
+
37
+
38
+ def command() -> str:
39
+ """The installed `builauncher` script, else this Python with -m."""
40
+ return shutil.which("builauncher") or f"{sys.executable} -m builauncher"
41
+
42
+
43
+ def install() -> None:
44
+ install_windows() if WINDOWS else install_xdg()
45
+
46
+
47
+ def uninstall() -> None:
48
+ uninstall_windows() if WINDOWS else uninstall_xdg()
49
+
50
+
51
+ def install_windows() -> None:
52
+ pythonw = Path(sys.executable).with_name("pythonw.exe")
53
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY, 0, winreg.KEY_SET_VALUE) as key:
54
+ winreg.SetValueEx(key, APP_ID, 0, winreg.REG_SZ, f'"{pythonw}" -m builauncher daemon')
55
+ print(f"added {APP_ID} to HKCU\\{RUN_KEY}")
56
+ print("start it now with: builauncher daemon (it starts by itself at the next sign-in)")
57
+
58
+
59
+ def uninstall_windows() -> None:
60
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY, 0, winreg.KEY_SET_VALUE) as key:
61
+ try:
62
+ winreg.DeleteValue(key, APP_ID)
63
+ except FileNotFoundError:
64
+ pass # not installed
65
+ print(f"removed {APP_ID} from HKCU\\{RUN_KEY}")
66
+
67
+
68
+ def install_xdg() -> None:
69
+ app, autostart = paths()
70
+ for path, args in ((app, ""), (autostart, " daemon")):
71
+ path.parent.mkdir(parents=True, exist_ok=True)
72
+ path.write_text(ENTRY.format(exec=command() + args))
73
+ print(f"wrote {path}")
74
+ if tool := shutil.which("update-desktop-database"):
75
+ subprocess.run([tool, str(app.parent)], check=False)
76
+ print("start it now with: builauncher daemon & (it starts by itself at the next login)")
77
+
78
+
79
+ def uninstall_xdg() -> None:
80
+ for path in paths():
81
+ path.unlink(missing_ok=True)
82
+ print(f"removed {path}")