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.
@@ -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
+ ![pluto](https://raw.githubusercontent.com/HACKE-RC/pluto/main/docs/shelf.png)
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
+ ![pluto](https://raw.githubusercontent.com/HACKE-RC/pluto/main/docs/shelf.png)
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