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.
- builauncher-0.1.1/PKG-INFO +138 -0
- builauncher-0.1.1/README.md +126 -0
- builauncher-0.1.1/builauncher/__init__.py +0 -0
- builauncher-0.1.1/builauncher/__main__.py +49 -0
- builauncher-0.1.1/builauncher/app.py +48 -0
- builauncher-0.1.1/builauncher/config.py +37 -0
- builauncher-0.1.1/builauncher/daemon.py +31 -0
- builauncher-0.1.1/builauncher/default-windows.toml +75 -0
- builauncher-0.1.1/builauncher/default.toml +79 -0
- builauncher-0.1.1/builauncher/fuzzy.py +13 -0
- builauncher-0.1.1/builauncher/install.py +82 -0
- builauncher-0.1.1/builauncher/items/__init__.py +1 -0
- builauncher-0.1.1/builauncher/items/apps.py +52 -0
- builauncher-0.1.1/builauncher/items/menu.py +56 -0
- builauncher-0.1.1/builauncher/items/places.py +20 -0
- builauncher-0.1.1/builauncher/launcher.py +136 -0
- builauncher-0.1.1/builauncher/shortcut.py +9 -0
- builauncher-0.1.1/builauncher/shortcut_portal.py +87 -0
- builauncher-0.1.1/builauncher/shortcut_windows.py +59 -0
- builauncher-0.1.1/builauncher/system.py +41 -0
- builauncher-0.1.1/builauncher/trigger.py +21 -0
- builauncher-0.1.1/builauncher/window.py +286 -0
- builauncher-0.1.1/pyproject.toml +26 -0
- builauncher-0.1.1/pyproject.toml.orig +26 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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}")
|