pluto-shelf 0.1.0__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.
- pluto_shelf-0.1.0/LICENSE +21 -0
- pluto_shelf-0.1.0/PKG-INFO +103 -0
- pluto_shelf-0.1.0/README.md +82 -0
- pluto_shelf-0.1.0/pyproject.toml +48 -0
- pluto_shelf-0.1.0/pyproject.toml.orig +40 -0
- pluto_shelf-0.1.0/src/pluto/__init__.py +58 -0
- pluto_shelf-0.1.0/src/pluto/app.py +129 -0
- pluto_shelf-0.1.0/src/pluto/config.py +119 -0
- pluto_shelf-0.1.0/src/pluto/drawer.py +842 -0
- pluto_shelf-0.1.0/src/pluto/ingest.py +240 -0
- pluto_shelf-0.1.0/src/pluto/install.py +56 -0
- pluto_shelf-0.1.0/src/pluto/shake.py +130 -0
- pluto_shelf-0.1.0/src/pluto/store.py +243 -0
- pluto_shelf-0.1.0/src/pluto/style.py +266 -0
- pluto_shelf-0.1.0/src/pluto/tray.py +324 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HACKE-RC
|
|
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.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pluto-shelf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pluto: a drop shelf for Wayland. Drag anything to the screen edge, keep it, drag it back out later.
|
|
5
|
+
Keywords: wayland,hyprland,drag-and-drop,shelf,dropover,layer-shell
|
|
6
|
+
Author: HACKE-RC
|
|
7
|
+
Author-email: HACKE-RC <60568652+HACKE-RC@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Environment :: X11 Applications :: GTK
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Desktop Environment
|
|
14
|
+
Requires-Dist: pycairo>=1.26
|
|
15
|
+
Requires-Dist: pygobject>=3.50
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Project-URL: Homepage, https://github.com/HACKE-RC/pluto
|
|
18
|
+
Project-URL: Repository, https://github.com/HACKE-RC/pluto
|
|
19
|
+
Project-URL: Issues, https://github.com/HACKE-RC/pluto/issues
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# pluto
|
|
23
|
+
|
|
24
|
+
Pluto is a drop shelf for Wayland. Drag something to the edge of the screen, drop it on
|
|
25
|
+
the shelf that slides out, go find where it needs to go, then drag it back out.
|
|
26
|
+
Works with files, folders, text, links and images from the browser.
|
|
27
|
+
|
|
28
|
+
Built for Hyprland. Runs on any compositor that has `wlr-layer-shell` and a
|
|
29
|
+
tray that speaks StatusNotifierItem (waybar, quickshell and the like).
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
The shelf is invisible until you need it: a 3 px strip on the right edge of the
|
|
36
|
+
screen. Drag anything onto that strip and a panel slides in at your pointer.
|
|
37
|
+
Drop, and the item is parked there. Files and folders are kept by reference,
|
|
38
|
+
so nothing is copied or moved until you drag it somewhere. Images dragged out
|
|
39
|
+
of a browser are saved locally (the pixels the browser hands over, or a
|
|
40
|
+
download of the image URL when it only hands over a link), so they survive
|
|
41
|
+
closing the tab, and copying one puts the actual image on the clipboard.
|
|
42
|
+
|
|
43
|
+
You can keep several shelves. Starting a new one puts the current one behind
|
|
44
|
+
the tray icon with everything still on it, and you can switch back whenever.
|
|
45
|
+
Shelves are saved to disk and come back after a restart.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
uv tool install pluto-shelf # from PyPI
|
|
51
|
+
uv tool install git+https://github.com/HACKE-RC/pluto # or straight from GitHub
|
|
52
|
+
pluto install && hyprctl reload && pluto # Hyprland rules, keybind, autostart
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
You need GTK 4, gtk4-layer-shell, gobject-introspection and cairo on the
|
|
56
|
+
system; see [docs/install.md](docs/install.md) for the details and for other
|
|
57
|
+
compositors.
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
Open the panel in one of three ways:
|
|
62
|
+
|
|
63
|
+
- drag something to the right edge of the screen
|
|
64
|
+
- shake the mouse left and right while holding something
|
|
65
|
+
- press `Super+Shift+Z` (this also works mid-drag)
|
|
66
|
+
|
|
67
|
+
Drag items out one at a time, or select several with `Ctrl` or `Shift` and
|
|
68
|
+
drag them together. The "drag all" handle at the bottom takes everything at
|
|
69
|
+
once. Drag-outs copy by default, so the original file and the shelf item both
|
|
70
|
+
stay where they are.
|
|
71
|
+
|
|
72
|
+
Keys while the panel is open:
|
|
73
|
+
|
|
74
|
+
| key | |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `Ctrl+V` | paste the clipboard onto the shelf |
|
|
77
|
+
| `Ctrl+C` | copy the selected items |
|
|
78
|
+
| `Enter` | open the selected items |
|
|
79
|
+
| `Delete` | remove them from the shelf |
|
|
80
|
+
| `Esc` | close the panel |
|
|
81
|
+
|
|
82
|
+
Right-click for the menu: rename, new shelf, switch shelf, clear, delete.
|
|
83
|
+
Double-click the shelf name to rename it. The `x` in the corner closes the panel.
|
|
84
|
+
|
|
85
|
+
From the command line:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
pluto start the daemon
|
|
89
|
+
pluto toggle open or close the panel
|
|
90
|
+
pluto new start a new shelf, keep the current one in the tray
|
|
91
|
+
pluto add FILE... put files, folders, URLs or text on the shelf
|
|
92
|
+
pluto quit stop the daemon
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## More
|
|
96
|
+
|
|
97
|
+
- [Installing](docs/install.md)
|
|
98
|
+
- [Configuration](docs/configuration.md)
|
|
99
|
+
- [How it works, and what Wayland and Hyprland make awkward](docs/internals.md)
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# pluto
|
|
2
|
+
|
|
3
|
+
Pluto is a drop shelf for Wayland. Drag something to the edge of the screen, drop it on
|
|
4
|
+
the shelf that slides out, go find where it needs to go, then drag it back out.
|
|
5
|
+
Works with files, folders, text, links and images from the browser.
|
|
6
|
+
|
|
7
|
+
Built for Hyprland. Runs on any compositor that has `wlr-layer-shell` and a
|
|
8
|
+
tray that speaks StatusNotifierItem (waybar, quickshell and the like).
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## What it does
|
|
13
|
+
|
|
14
|
+
The shelf is invisible until you need it: a 3 px strip on the right edge of the
|
|
15
|
+
screen. Drag anything onto that strip and a panel slides in at your pointer.
|
|
16
|
+
Drop, and the item is parked there. Files and folders are kept by reference,
|
|
17
|
+
so nothing is copied or moved until you drag it somewhere. Images dragged out
|
|
18
|
+
of a browser are saved locally (the pixels the browser hands over, or a
|
|
19
|
+
download of the image URL when it only hands over a link), so they survive
|
|
20
|
+
closing the tab, and copying one puts the actual image on the clipboard.
|
|
21
|
+
|
|
22
|
+
You can keep several shelves. Starting a new one puts the current one behind
|
|
23
|
+
the tray icon with everything still on it, and you can switch back whenever.
|
|
24
|
+
Shelves are saved to disk and come back after a restart.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
uv tool install pluto-shelf # from PyPI
|
|
30
|
+
uv tool install git+https://github.com/HACKE-RC/pluto # or straight from GitHub
|
|
31
|
+
pluto install && hyprctl reload && pluto # Hyprland rules, keybind, autostart
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
You need GTK 4, gtk4-layer-shell, gobject-introspection and cairo on the
|
|
35
|
+
system; see [docs/install.md](docs/install.md) for the details and for other
|
|
36
|
+
compositors.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Open the panel in one of three ways:
|
|
41
|
+
|
|
42
|
+
- drag something to the right edge of the screen
|
|
43
|
+
- shake the mouse left and right while holding something
|
|
44
|
+
- press `Super+Shift+Z` (this also works mid-drag)
|
|
45
|
+
|
|
46
|
+
Drag items out one at a time, or select several with `Ctrl` or `Shift` and
|
|
47
|
+
drag them together. The "drag all" handle at the bottom takes everything at
|
|
48
|
+
once. Drag-outs copy by default, so the original file and the shelf item both
|
|
49
|
+
stay where they are.
|
|
50
|
+
|
|
51
|
+
Keys while the panel is open:
|
|
52
|
+
|
|
53
|
+
| key | |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `Ctrl+V` | paste the clipboard onto the shelf |
|
|
56
|
+
| `Ctrl+C` | copy the selected items |
|
|
57
|
+
| `Enter` | open the selected items |
|
|
58
|
+
| `Delete` | remove them from the shelf |
|
|
59
|
+
| `Esc` | close the panel |
|
|
60
|
+
|
|
61
|
+
Right-click for the menu: rename, new shelf, switch shelf, clear, delete.
|
|
62
|
+
Double-click the shelf name to rename it. The `x` in the corner closes the panel.
|
|
63
|
+
|
|
64
|
+
From the command line:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
pluto start the daemon
|
|
68
|
+
pluto toggle open or close the panel
|
|
69
|
+
pluto new start a new shelf, keep the current one in the tray
|
|
70
|
+
pluto add FILE... put files, folders, URLs or text on the shelf
|
|
71
|
+
pluto quit stop the daemon
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## More
|
|
75
|
+
|
|
76
|
+
- [Installing](docs/install.md)
|
|
77
|
+
- [Configuration](docs/configuration.md)
|
|
78
|
+
- [How it works, and what Wayland and Hyprland make awkward](docs/internals.md)
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pluto-shelf"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Pluto: a drop shelf for Wayland. Drag anything to the screen edge, keep it, drag it back out later."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE*"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
keywords = [
|
|
10
|
+
"wayland",
|
|
11
|
+
"hyprland",
|
|
12
|
+
"drag-and-drop",
|
|
13
|
+
"shelf",
|
|
14
|
+
"dropover",
|
|
15
|
+
"layer-shell",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Environment :: X11 Applications :: GTK",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Desktop Environment",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pycairo>=1.26",
|
|
25
|
+
"pygobject>=3.50",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[project.authors]]
|
|
29
|
+
name = "HACKE-RC"
|
|
30
|
+
email = "60568652+HACKE-RC@users.noreply.github.com"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/HACKE-RC/pluto"
|
|
34
|
+
Repository = "https://github.com/HACKE-RC/pluto"
|
|
35
|
+
Issues = "https://github.com/HACKE-RC/pluto/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
pluto = "pluto:main"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["uv_build>=0.12.9,<0.13.0"]
|
|
42
|
+
build-backend = "uv_build"
|
|
43
|
+
|
|
44
|
+
[tool.uv.build-backend]
|
|
45
|
+
module-name = "pluto"
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 140
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pluto-shelf"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Pluto: a drop shelf for Wayland. Drag anything to the screen edge, keep it, drag it back out later."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE*"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "HACKE-RC", email = "60568652+HACKE-RC@users.noreply.github.com" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
keywords = ["wayland", "hyprland", "drag-and-drop", "shelf", "dropover", "layer-shell"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Environment :: X11 Applications :: GTK",
|
|
15
|
+
"Operating System :: POSIX :: Linux",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Desktop Environment",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"pycairo>=1.26",
|
|
21
|
+
"pygobject>=3.50",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/HACKE-RC/pluto"
|
|
26
|
+
Repository = "https://github.com/HACKE-RC/pluto"
|
|
27
|
+
Issues = "https://github.com/HACKE-RC/pluto/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
pluto = "pluto:main"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.12.9,<0.13.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
|
|
36
|
+
[tool.uv.build-backend]
|
|
37
|
+
module-name = "pluto"
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 140
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""pluto — a drop shelf for Wayland."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _ensure_layer_shell_preloaded() -> None:
|
|
11
|
+
"""gtk4-layer-shell must interpose libwayland-client before GTK loads it. dlopen(RTLD_GLOBAL) from Python is not
|
|
12
|
+
reliable for that (some hooks are missed and the layer surface ends up in a resize loop), so re-exec with LD_PRELOAD."""
|
|
13
|
+
import ctypes.util
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
if os.environ.get("PLUTO_PRELOADED") == "1":
|
|
17
|
+
return
|
|
18
|
+
lib = ctypes.util.find_library("gtk4-layer-shell")
|
|
19
|
+
if not lib:
|
|
20
|
+
sys.stderr.write("pluto: gtk4-layer-shell is not installed (libgtk4-layer-shell.so not found)\n")
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
env = dict(os.environ)
|
|
23
|
+
env["LD_PRELOAD"] = " ".join(filter(None, [lib, env.get("LD_PRELOAD")]))
|
|
24
|
+
env["PLUTO_PRELOADED"] = "1"
|
|
25
|
+
os.execve(sys.executable, [sys.executable, *sys.argv], env)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> int:
|
|
29
|
+
args = sys.argv[1:]
|
|
30
|
+
if args[:1] == ["install"]:
|
|
31
|
+
from .install import run
|
|
32
|
+
|
|
33
|
+
return run(args[1:])
|
|
34
|
+
if args[:1] == ["config"]:
|
|
35
|
+
from .config import write_default
|
|
36
|
+
|
|
37
|
+
print(write_default())
|
|
38
|
+
return 0
|
|
39
|
+
if args[:1] in (["-h"], ["--help"], ["help"]):
|
|
40
|
+
from .app import USAGE
|
|
41
|
+
|
|
42
|
+
print(USAGE, end="")
|
|
43
|
+
return 0
|
|
44
|
+
if args[:1] in (["-V"], ["--version"]):
|
|
45
|
+
print(f"pluto {__version__}")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
_ensure_layer_shell_preloaded()
|
|
49
|
+
import gi
|
|
50
|
+
|
|
51
|
+
gi.require_version("Gtk", "4.0")
|
|
52
|
+
gi.require_version("Gdk", "4.0")
|
|
53
|
+
gi.require_version("GdkPixbuf", "2.0")
|
|
54
|
+
gi.require_version("Gtk4LayerShell", "1.0")
|
|
55
|
+
|
|
56
|
+
from .app import ShelfApp
|
|
57
|
+
|
|
58
|
+
return ShelfApp().run(sys.argv)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from gi.repository import Gio, GLib, Gtk
|
|
6
|
+
|
|
7
|
+
from . import config, ingest, style
|
|
8
|
+
from .config import APP_ID
|
|
9
|
+
from .drawer import Drawer
|
|
10
|
+
from .store import Item, Store
|
|
11
|
+
from .shake import ShakeDetector
|
|
12
|
+
from .tray import Tray
|
|
13
|
+
|
|
14
|
+
VERBS = ("toggle", "show", "hide", "new", "quit")
|
|
15
|
+
USAGE = """usage: pluto [command]
|
|
16
|
+
|
|
17
|
+
(none) start the daemon (or report that it is already running)
|
|
18
|
+
toggle open / close the drawer
|
|
19
|
+
show open the drawer
|
|
20
|
+
hide close the drawer
|
|
21
|
+
new start a new shelf (the current one is kept in the tray)
|
|
22
|
+
add ITEM... put files, folders, URLs or text on the active shelf
|
|
23
|
+
quit stop the daemon
|
|
24
|
+
install write Hyprland rules, keybind and autostart
|
|
25
|
+
config create ~/.config/pluto/config.toml with the defaults
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ShelfApp(Gtk.Application):
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
|
|
32
|
+
self.cfg: config.Config | None = None
|
|
33
|
+
self.store: Store | None = None
|
|
34
|
+
self.drawer: Drawer | None = None
|
|
35
|
+
self.tray: Tray | None = None
|
|
36
|
+
self.shake: ShakeDetector | None = None
|
|
37
|
+
|
|
38
|
+
def do_startup(self) -> None:
|
|
39
|
+
Gtk.Application.do_startup(self)
|
|
40
|
+
self.cfg = config.load()
|
|
41
|
+
style.install(self.cfg)
|
|
42
|
+
self.store = Store()
|
|
43
|
+
self.drawer = Drawer(self, self.store, self.cfg)
|
|
44
|
+
self.tray = Tray(
|
|
45
|
+
self.store,
|
|
46
|
+
on_activate=self.drawer.toggle,
|
|
47
|
+
on_select_shelf=self._select_shelf,
|
|
48
|
+
on_new=self._new_shelf,
|
|
49
|
+
on_clear=self.store.clear,
|
|
50
|
+
on_delete=lambda: self.store.delete_shelf(self.store.active_id),
|
|
51
|
+
on_quit=self.quit_safely,
|
|
52
|
+
icon_color=self.cfg.palette.fg,
|
|
53
|
+
)
|
|
54
|
+
for verb in VERBS:
|
|
55
|
+
action = Gio.SimpleAction.new(verb, None)
|
|
56
|
+
action.connect("activate", self._on_verb, verb)
|
|
57
|
+
self.add_action(action)
|
|
58
|
+
self.drawer.present()
|
|
59
|
+
if self.cfg.shake:
|
|
60
|
+
self.shake = ShakeDetector(self._on_shake, window_ms=self.cfg.shake_window_ms, travel=self.cfg.shake_travel, reversals=self.cfg.shake_reversals, requires_grab=self.cfg.shake_requires_grab)
|
|
61
|
+
self.shake.start()
|
|
62
|
+
|
|
63
|
+
def _on_shake(self, x: float, y: float) -> bool:
|
|
64
|
+
if not self.drawer.expanded:
|
|
65
|
+
self.drawer.expand()
|
|
66
|
+
self.drawer._schedule_collapse(3500)
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def do_activate(self) -> None:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def do_command_line(self, cmdline: Gio.ApplicationCommandLine) -> int:
|
|
73
|
+
args = cmdline.get_arguments()[1:]
|
|
74
|
+
if not args:
|
|
75
|
+
if cmdline.get_is_remote():
|
|
76
|
+
cmdline.printerr_literal("pluto: already running\n")
|
|
77
|
+
return 0
|
|
78
|
+
verb = args[0]
|
|
79
|
+
if verb in VERBS:
|
|
80
|
+
self.activate_action(verb, None)
|
|
81
|
+
return 0
|
|
82
|
+
if verb == "add":
|
|
83
|
+
cwd = cmdline.get_cwd() or "/"
|
|
84
|
+
items = [self._item_from_arg(arg, cwd) for arg in args[1:]]
|
|
85
|
+
self.drawer.add_items(items)
|
|
86
|
+
self.drawer.expand()
|
|
87
|
+
self.drawer._schedule_collapse(4000)
|
|
88
|
+
return 0
|
|
89
|
+
cmdline.printerr_literal(USAGE)
|
|
90
|
+
return 2
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _item_from_arg(arg: str, cwd: str) -> Item:
|
|
94
|
+
if ingest.URL_RE.match(arg):
|
|
95
|
+
return Item.link(arg)
|
|
96
|
+
path = arg if os.path.isabs(arg) else os.path.join(cwd, arg)
|
|
97
|
+
if os.path.exists(path):
|
|
98
|
+
return Item.file(os.path.normpath(path))
|
|
99
|
+
return Item.text_snippet(arg)
|
|
100
|
+
|
|
101
|
+
def _on_verb(self, action, param, verb: str) -> None:
|
|
102
|
+
if verb == "toggle":
|
|
103
|
+
self.drawer.toggle()
|
|
104
|
+
elif verb == "show":
|
|
105
|
+
self.drawer.expand()
|
|
106
|
+
elif verb == "hide":
|
|
107
|
+
self.drawer.collapse(force=True)
|
|
108
|
+
elif verb == "new":
|
|
109
|
+
self._new_shelf()
|
|
110
|
+
elif verb == "quit":
|
|
111
|
+
self.quit_safely()
|
|
112
|
+
|
|
113
|
+
def _select_shelf(self, shelf_id: str) -> None:
|
|
114
|
+
self.store.set_active(shelf_id)
|
|
115
|
+
self.drawer.expand()
|
|
116
|
+
self.drawer._schedule_collapse(4000)
|
|
117
|
+
|
|
118
|
+
def _new_shelf(self) -> None:
|
|
119
|
+
self.store.new_shelf()
|
|
120
|
+
self.drawer.expand()
|
|
121
|
+
self.drawer._schedule_collapse(4000)
|
|
122
|
+
|
|
123
|
+
def quit_safely(self) -> None:
|
|
124
|
+
# Quitting mid-drag leaves the compositor's drag grab dangling; wait for it to end.
|
|
125
|
+
if self.drawer.dnd_active or self.drawer.drag_out_active:
|
|
126
|
+
GLib.timeout_add(500, lambda: (self.quit_safely(), False)[1])
|
|
127
|
+
return
|
|
128
|
+
self.store.save_now()
|
|
129
|
+
self.quit()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
from dataclasses import dataclass, field, fields
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
XDG_CONFIG = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
9
|
+
XDG_DATA = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
10
|
+
|
|
11
|
+
CONFIG_DIR = XDG_CONFIG / "pluto"
|
|
12
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
13
|
+
DATA_DIR = XDG_DATA / "pluto"
|
|
14
|
+
STATE_FILE = DATA_DIR / "state.json"
|
|
15
|
+
BLOB_DIR = DATA_DIR / "blobs"
|
|
16
|
+
BUTTON_FILE = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "pluto-button"
|
|
17
|
+
|
|
18
|
+
APP_ID = "io.pluto.Pluto"
|
|
19
|
+
NAMESPACE = "pluto"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Palette:
|
|
24
|
+
bg: str = "#1e1e2e"
|
|
25
|
+
surface: str = "#313244"
|
|
26
|
+
fg: str = "#cdd6f4"
|
|
27
|
+
muted: str = "#6c7086"
|
|
28
|
+
dim: str = "#585b70"
|
|
29
|
+
accent: str = "#89b4fa"
|
|
30
|
+
danger: str = "#f38ba8"
|
|
31
|
+
border: str = "#45475a"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Config:
|
|
36
|
+
edge: str = "right"
|
|
37
|
+
width: int = 320
|
|
38
|
+
max_height: float = 0.7
|
|
39
|
+
strip_width: int = 3
|
|
40
|
+
opacity: float = 0.8
|
|
41
|
+
radius: int = 6
|
|
42
|
+
font: str = "JetBrainsMono Nerd Font, JetBrains Mono, monospace"
|
|
43
|
+
font_size: int = 12
|
|
44
|
+
auto_collapse_ms: int = 1200
|
|
45
|
+
linger_after_drop_ms: int = 2500
|
|
46
|
+
remove_on_drag_out: bool = False
|
|
47
|
+
drag_out_action: str = "copy"
|
|
48
|
+
slide_ms: int = 180
|
|
49
|
+
shake: bool = True
|
|
50
|
+
shake_requires_grab: bool = True
|
|
51
|
+
shake_reversals: int = 4
|
|
52
|
+
shake_travel: int = 40
|
|
53
|
+
shake_window_ms: int = 600
|
|
54
|
+
palette: Palette = field(default_factory=Palette)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
DEFAULT_CONFIG_TOML = """\
|
|
58
|
+
# pluto configuration — every key is optional; these are the defaults.
|
|
59
|
+
|
|
60
|
+
edge = "right" # screen edge the drawer lives on: "right" or "left"
|
|
61
|
+
width = 320 # drawer width in logical pixels
|
|
62
|
+
max_height = 0.7 # drawer grows with content up to this fraction of the screen
|
|
63
|
+
strip_width = 3 # width of the invisible edge strip that catches drags
|
|
64
|
+
opacity = 0.8 # panel background alpha (pairs with Hyprland blur)
|
|
65
|
+
radius = 6 # corner radius on the screen-facing side
|
|
66
|
+
font = "JetBrainsMono Nerd Font, JetBrains Mono, monospace"
|
|
67
|
+
font_size = 12
|
|
68
|
+
auto_collapse_ms = 1200 # collapse this long after the pointer leaves
|
|
69
|
+
linger_after_drop_ms = 2500
|
|
70
|
+
remove_on_drag_out = false # remove an item from the shelf after dragging it out (Ctrl-drag keeps it)
|
|
71
|
+
slide_ms = 180 # open/close animation duration
|
|
72
|
+
drag_out_action = "copy" # "copy": targets always copy; "move": the target may move the file (shelf item goes stale)
|
|
73
|
+
shake = true # shake the pointer left-right to summon the panel (Hyprland only)
|
|
74
|
+
shake_requires_grab = true # only while the left button is held (needs the binds from `pluto install`)
|
|
75
|
+
shake_reversals = 4 # direction changes needed within shake_window_ms
|
|
76
|
+
shake_travel = 40 # minimum pixels per leg of the shake
|
|
77
|
+
shake_window_ms = 600
|
|
78
|
+
|
|
79
|
+
[palette]
|
|
80
|
+
bg = "#1e1e2e"
|
|
81
|
+
surface = "#313244"
|
|
82
|
+
fg = "#cdd6f4"
|
|
83
|
+
muted = "#6c7086"
|
|
84
|
+
dim = "#585b70"
|
|
85
|
+
accent = "#89b4fa"
|
|
86
|
+
danger = "#f38ba8"
|
|
87
|
+
border = "#45475a"
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _apply(obj, data: dict) -> None:
|
|
92
|
+
names = {f.name: f.type for f in fields(obj)}
|
|
93
|
+
for key, value in data.items():
|
|
94
|
+
if key in names and key != "palette":
|
|
95
|
+
setattr(obj, key, value)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load() -> Config:
|
|
99
|
+
cfg = Config()
|
|
100
|
+
if not CONFIG_FILE.exists():
|
|
101
|
+
return cfg
|
|
102
|
+
with CONFIG_FILE.open("rb") as fh:
|
|
103
|
+
data = tomllib.load(fh)
|
|
104
|
+
_apply(cfg, data)
|
|
105
|
+
_apply(cfg.palette, data.get("palette", {}))
|
|
106
|
+
if cfg.edge not in ("right", "left"):
|
|
107
|
+
cfg.edge = "right"
|
|
108
|
+
if cfg.drag_out_action not in ("copy", "move"):
|
|
109
|
+
cfg.drag_out_action = "copy"
|
|
110
|
+
cfg.strip_width = max(1, int(cfg.strip_width))
|
|
111
|
+
cfg.opacity = min(1.0, max(0.0, float(cfg.opacity)))
|
|
112
|
+
return cfg
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def write_default() -> Path:
|
|
116
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
if not CONFIG_FILE.exists():
|
|
118
|
+
CONFIG_FILE.write_text(DEFAULT_CONFIG_TOML)
|
|
119
|
+
return CONFIG_FILE
|