omarchy-dms 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 Mikel Villota
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,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: omarchy-dms
3
+ Version: 0.1.0
4
+ Summary: Omarchy themes as Dank Material Shell (dmsProfiles) looks
5
+ Author: Mikel Villota
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/Villoh/omarchy-dms
8
+ Project-URL: Issues, https://github.com/Villoh/omarchy-dms/issues
9
+ Keywords: omarchy,dms,dankmaterialshell,theme,dmsprofiles
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Desktop Environment
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # omarchy-dms
22
+
23
+ Use [Omarchy](https://omarchy.org) themes in [Dank Material Shell](https://danklinux.com)
24
+ as [dmsProfiles](https://github.com/satoshoe-dev/dms-profiles) looks: colors, wallpapers
25
+ and light/dark mode, switchable from the profile picker.
26
+
27
+ It converts an Omarchy `colors.toml` into a DMS custom theme (with the Browse previews),
28
+ copies the theme's wallpapers, and creates one profile per theme through `dms ipc`.
29
+ Themes from the [omarchy.org/themes](https://omarchy.org/themes/) store install with one command.
30
+
31
+ ## Requirements
32
+
33
+ DMS with the `dmsProfiles` plugin enabled, Python 3.11+, `git`, and optionally `fzf`
34
+ (pickers). No other dependencies.
35
+
36
+ ## Install
37
+
38
+ ```sh
39
+ pipx install omarchy-dms
40
+ # or, from the repository
41
+ pipx install git+https://github.com/Villoh/omarchy-dms
42
+ ```
43
+
44
+ On Nix: `nix run github:Villoh/omarchy-dms`.
45
+
46
+ ## Usage
47
+
48
+ ```sh
49
+ omarchy-dms install # pick from the omarchy.org/themes store (fzf)
50
+ omarchy-dms install owner/repo # or a repo / git URL
51
+ omarchy-dms installed # every theme, its origin, and whether it has a profile
52
+ omarchy-dms update [theme...] # refetch installed themes (all, or the ones named)
53
+ omarchy-dms remove [-y] [theme...] # theme, wallpapers and profile (no names: fzf, Tab marks)
54
+ omarchy-dms list # repos listed on the store
55
+ omarchy-dms sync [--force] [theme...] # profiles for Omarchy's own themes (see below)
56
+ ```
57
+
58
+ Themes land in `~/.config/DankMaterialShell/themes/omarchy-<name>/` and wallpapers in
59
+ `~/Pictures/Wallpapers/omarchy/<name>/`. Only themes with a `colors.toml` work; alacritty-only
60
+ themes are rejected.
61
+
62
+ **Own themes.** `sync` needs Omarchy's own `themes/` directory, given by `OMARCHY_THEMES`
63
+ (for example a checkout of `basecamp/omarchy`), with the theme folders already in place
64
+ for DMS: `omarchy-dms render <colors.toml> <name> <outdir>` writes one.
65
+
66
+ **Wallpaper carousel.** dmsProfiles stores one wallpaper copy per profile. The CLI registers
67
+ itself as the profile *apply command* so that, after a switch, the wallpaper points back at the
68
+ theme's folder and a carousel lists all of its images.
69
+
70
+ ## As a library
71
+
72
+ `omarchy_dms.Omarchy(dms, store, ...)` is the API behind the CLI: it never prints or exits,
73
+ progress goes to a `log` callback and failures are exceptions (`DmsError`, `StoreError`,
74
+ `ThemeError`). Another frontend can import it instead of running the CLI.
75
+
76
+ ```python
77
+ from omarchy_dms import Dms, Omarchy, Store
78
+
79
+ omarchy = Omarchy(Dms(), Store(themes_dir, wallpapers_dir), log=print)
80
+ omarchy.install("owner/omarchy-example-theme")
81
+ ```
82
+
83
+ ## Development
84
+
85
+ ```sh
86
+ uv sync
87
+ uv run python -m unittest discover -s tests
88
+ uv run ruff check . && uv run ruff format --check .
89
+ ```
90
+
91
+ ## Releasing
92
+
93
+ Bump `version` in `pyproject.toml` (the Nix package reads it from there), commit, then tag it:
94
+
95
+ ```sh
96
+ git tag v0.1.0 && git push origin v0.1.0
97
+ ```
98
+
99
+ `.github/workflows/release.yml` does the rest: it checks the tag against the version, runs CI,
100
+ builds and smoke-tests the package, publishes it to PyPI with trusted publishing, and creates the
101
+ GitHub release. Its notes are `.github/release-notes/<tag>.md` if you wrote one, otherwise
102
+ the ones GitHub generates from merged pull requests.
103
+
104
+ ## License
105
+
106
+ [MIT](LICENSE)
@@ -0,0 +1,86 @@
1
+ # omarchy-dms
2
+
3
+ Use [Omarchy](https://omarchy.org) themes in [Dank Material Shell](https://danklinux.com)
4
+ as [dmsProfiles](https://github.com/satoshoe-dev/dms-profiles) looks: colors, wallpapers
5
+ and light/dark mode, switchable from the profile picker.
6
+
7
+ It converts an Omarchy `colors.toml` into a DMS custom theme (with the Browse previews),
8
+ copies the theme's wallpapers, and creates one profile per theme through `dms ipc`.
9
+ Themes from the [omarchy.org/themes](https://omarchy.org/themes/) store install with one command.
10
+
11
+ ## Requirements
12
+
13
+ DMS with the `dmsProfiles` plugin enabled, Python 3.11+, `git`, and optionally `fzf`
14
+ (pickers). No other dependencies.
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ pipx install omarchy-dms
20
+ # or, from the repository
21
+ pipx install git+https://github.com/Villoh/omarchy-dms
22
+ ```
23
+
24
+ On Nix: `nix run github:Villoh/omarchy-dms`.
25
+
26
+ ## Usage
27
+
28
+ ```sh
29
+ omarchy-dms install # pick from the omarchy.org/themes store (fzf)
30
+ omarchy-dms install owner/repo # or a repo / git URL
31
+ omarchy-dms installed # every theme, its origin, and whether it has a profile
32
+ omarchy-dms update [theme...] # refetch installed themes (all, or the ones named)
33
+ omarchy-dms remove [-y] [theme...] # theme, wallpapers and profile (no names: fzf, Tab marks)
34
+ omarchy-dms list # repos listed on the store
35
+ omarchy-dms sync [--force] [theme...] # profiles for Omarchy's own themes (see below)
36
+ ```
37
+
38
+ Themes land in `~/.config/DankMaterialShell/themes/omarchy-<name>/` and wallpapers in
39
+ `~/Pictures/Wallpapers/omarchy/<name>/`. Only themes with a `colors.toml` work; alacritty-only
40
+ themes are rejected.
41
+
42
+ **Own themes.** `sync` needs Omarchy's own `themes/` directory, given by `OMARCHY_THEMES`
43
+ (for example a checkout of `basecamp/omarchy`), with the theme folders already in place
44
+ for DMS: `omarchy-dms render <colors.toml> <name> <outdir>` writes one.
45
+
46
+ **Wallpaper carousel.** dmsProfiles stores one wallpaper copy per profile. The CLI registers
47
+ itself as the profile *apply command* so that, after a switch, the wallpaper points back at the
48
+ theme's folder and a carousel lists all of its images.
49
+
50
+ ## As a library
51
+
52
+ `omarchy_dms.Omarchy(dms, store, ...)` is the API behind the CLI: it never prints or exits,
53
+ progress goes to a `log` callback and failures are exceptions (`DmsError`, `StoreError`,
54
+ `ThemeError`). Another frontend can import it instead of running the CLI.
55
+
56
+ ```python
57
+ from omarchy_dms import Dms, Omarchy, Store
58
+
59
+ omarchy = Omarchy(Dms(), Store(themes_dir, wallpapers_dir), log=print)
60
+ omarchy.install("owner/omarchy-example-theme")
61
+ ```
62
+
63
+ ## Development
64
+
65
+ ```sh
66
+ uv sync
67
+ uv run python -m unittest discover -s tests
68
+ uv run ruff check . && uv run ruff format --check .
69
+ ```
70
+
71
+ ## Releasing
72
+
73
+ Bump `version` in `pyproject.toml` (the Nix package reads it from there), commit, then tag it:
74
+
75
+ ```sh
76
+ git tag v0.1.0 && git push origin v0.1.0
77
+ ```
78
+
79
+ `.github/workflows/release.yml` does the rest: it checks the tag against the version, runs CI,
80
+ builds and smoke-tests the package, publishes it to PyPI with trusted publishing, and creates the
81
+ GitHub release. Its notes are `.github/release-notes/<tag>.md` if you wrote one, otherwise
82
+ the ones GitHub generates from merged pull requests.
83
+
84
+ ## License
85
+
86
+ [MIT](LICENSE)
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "omarchy-dms"
7
+ version = "0.1.0"
8
+ description = "Omarchy themes as Dank Material Shell (dmsProfiles) looks"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ authors = [{ name = "Mikel Villota" }]
14
+ keywords = ["omarchy", "dms", "dankmaterialshell", "theme", "dmsprofiles"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Topic :: Desktop Environment",
22
+ ]
23
+
24
+ [project.urls]
25
+ Repository = "https://github.com/Villoh/omarchy-dms"
26
+ Issues = "https://github.com/Villoh/omarchy-dms/issues"
27
+
28
+ [project.scripts]
29
+ omarchy-dms = "omarchy_dms.cli:main"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.pyright]
35
+ extraPaths = ["src"]
36
+
37
+ [dependency-groups]
38
+ dev = ["ruff>=0.11.0"]
39
+
40
+ [tool.ruff]
41
+ target-version = "py311"
42
+ line-length = 120
43
+ src = ["src", "tests"]
44
+
45
+ [tool.ruff.lint]
46
+ select = ["E", "F", "I", "UP", "B", "SIM"]
47
+
48
+ # The preview SVG template and long test literals read better unwrapped.
49
+ [tool.ruff.lint.per-file-ignores]
50
+ "src/omarchy_dms/theme.py" = ["E501"]
51
+ "tests/*.py" = ["E501"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ """Omarchy themes as DMS profiles.
2
+
3
+ Library first: `ops.Omarchy` is the API (no printing, no exiting, progress goes
4
+ to a `log` callback); `cli` is one thin frontend over it. Other frontends import
5
+ this package instead of shelling out to the CLI.
6
+ """
7
+
8
+ from .dms import Dms, DmsError
9
+ from .ops import Omarchy, Row, store_repos
10
+ from .store import Store, StoreError, Theme, resolve_repo
11
+ from .theme import ThemeError
12
+
13
+ __all__ = [
14
+ "Dms",
15
+ "DmsError",
16
+ "Omarchy",
17
+ "Row",
18
+ "Store",
19
+ "StoreError",
20
+ "Theme",
21
+ "ThemeError",
22
+ "resolve_repo",
23
+ "store_repos",
24
+ ]
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,92 @@
1
+ """Command line frontend over ops.Omarchy."""
2
+
3
+ import argparse
4
+ import os
5
+ import shlex
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from . import theme
12
+ from .dms import Dms, DmsError
13
+ from .ops import Omarchy, store_repos
14
+ from .store import Store, StoreError
15
+
16
+ HOME = Path.home()
17
+ THEMES = HOME / ".config/DankMaterialShell/themes"
18
+ WALLPAPERS = HOME / "Pictures/Wallpapers/omarchy"
19
+
20
+
21
+ def _hook_command():
22
+ exe = shutil.which("omarchy-dms")
23
+ return f"{shlex.quote(exe)} hook" if exe else f"{shlex.quote(sys.executable)} -m omarchy_dms hook"
24
+
25
+
26
+ def _pick(items, prompt, multi):
27
+ if not shutil.which("fzf"):
28
+ raise StoreError("fzf not found: pass the name(s) as arguments")
29
+ cmd = ["fzf", "--prompt", prompt, *(["-m"] if multi else [])]
30
+ r = subprocess.run(cmd, input="\n".join(items), text=True, stdout=subprocess.PIPE, check=False)
31
+ return r.stdout.split()
32
+
33
+
34
+ def _parser():
35
+ p = argparse.ArgumentParser(prog="omarchy-dms", description="Omarchy themes as DMS profiles.")
36
+ sub = p.add_subparsers(dest="cmd", required=True)
37
+ s = sub.add_parser("sync", help="create profiles for Omarchy's own themes (all, or the ones named)")
38
+ s.add_argument("--force", action="store_true", help="recreate the ones that exist")
39
+ s.add_argument("themes", nargs="*")
40
+ s = sub.add_parser("install", help="install a community theme and create its profile")
41
+ s.add_argument("repo", nargs="?", help="owner/repo or git URL (none: pick from omarchy.org/themes)")
42
+ s = sub.add_parser("update", help="refresh themes installed with install (all, or the ones named)")
43
+ s.add_argument("themes", nargs="*")
44
+ s = sub.add_parser("remove", help="remove theme, wallpapers and profile (none named: pick with fzf)")
45
+ s.add_argument("-y", "--yes", action="store_true", help="do not ask")
46
+ s.add_argument("themes", nargs="*")
47
+ sub.add_parser("installed", help="every theme, where it comes from, profile or not")
48
+ sub.add_parser("list", help="repos listed on omarchy.org/themes")
49
+ s = sub.add_parser("render", help="convert a colors.toml into a DMS theme folder")
50
+ s.add_argument("colors")
51
+ s.add_argument("slug")
52
+ s.add_argument("outdir")
53
+ sub.add_parser("hook", help="dmsProfiles apply command (reads DMS_PROFILE_SLUG)")
54
+ return p
55
+
56
+
57
+ def main(argv=None):
58
+ args = _parser().parse_args(argv)
59
+ builtin = Path(os.environ["OMARCHY_THEMES"]) if "OMARCHY_THEMES" in os.environ else None
60
+ omarchy = Omarchy(Dms(), Store(THEMES, WALLPAPERS), builtin, _hook_command())
61
+ try:
62
+ match args.cmd:
63
+ case "sync":
64
+ omarchy.sync(args.themes, args.force)
65
+ case "install":
66
+ repo = args.repo or next(iter(_pick(store_repos(), "omarchy theme> ", False)), None)
67
+ if repo:
68
+ omarchy.install(repo)
69
+ case "update":
70
+ return 1 if omarchy.update(args.themes) else 0
71
+ case "remove":
72
+ names = args.themes or _pick(omarchy.store.slugs(), "remove (Tab marks)> ", True)
73
+ if names and not args.yes:
74
+ print(f"remove: {' '.join(names)}")
75
+ if input("continue? [y/N] ") != "y":
76
+ return 0
77
+ if names:
78
+ omarchy.remove(names)
79
+ case "installed":
80
+ for row in omarchy.installed():
81
+ origin = row.origin or "community (no .source: reinstall to track)"
82
+ print(f"{row.slug:<22} {'profile' if row.profile else '-':<8} {origin}")
83
+ case "list":
84
+ print("\n".join(store_repos()))
85
+ case "render":
86
+ theme.render(args.colors, args.slug, args.outdir)
87
+ case "hook":
88
+ omarchy.hook(os.environ.get("DMS_PROFILE_SLUG", ""))
89
+ except (DmsError, StoreError, theme.ThemeError) as e:
90
+ print(f"error: {e}", file=sys.stderr)
91
+ return 1
92
+ return 0
@@ -0,0 +1,132 @@
1
+ """Thin client for the DMS shell over `dms ipc`.
2
+
3
+ `runner` is the only thing that touches the process table, so a frontend or a
4
+ test can swap it. Calls the plugin finishes asynchronously (create, remove,
5
+ apply) are polled until the shell reports the result, instead of sleeping.
6
+ """
7
+
8
+ import contextlib
9
+ import json
10
+ import subprocess
11
+ import time
12
+ from collections.abc import Callable
13
+
14
+ Runner = Callable[[list[str]], str]
15
+
16
+
17
+ class DmsError(RuntimeError):
18
+ pass
19
+
20
+
21
+ def run_dms(args):
22
+ try:
23
+ p = subprocess.run(["dms", *args], capture_output=True, text=True, check=False)
24
+ except FileNotFoundError as e:
25
+ raise DmsError("dms not found in PATH") from e
26
+ if p.returncode:
27
+ raise DmsError(f"dms {' '.join(args)}: {p.stderr.strip() or p.stdout.strip()}")
28
+ return p.stdout.strip()
29
+
30
+
31
+ class Dms:
32
+ def __init__(
33
+ self,
34
+ runner: Runner = run_dms,
35
+ sleep=time.sleep,
36
+ attempts=60,
37
+ interval=0.25,
38
+ spawn: Callable[..., object] = subprocess.Popen,
39
+ ):
40
+ self._run = runner
41
+ self._sleep = sleep
42
+ self.attempts = attempts
43
+ self.interval = interval
44
+ self._spawn = spawn
45
+
46
+ def ipc(self, *args):
47
+ return self._run(["ipc", "call", *args])
48
+
49
+ def _json(self, *args):
50
+ out = self.ipc(*args)
51
+ try:
52
+ return json.loads(out or "null")
53
+ except json.JSONDecodeError as e:
54
+ raise DmsError(f"dms ipc call {' '.join(args)}: not JSON: {out[:80]!r}") from e
55
+
56
+ def _wait(self, what, ok):
57
+ for _ in range(self.attempts):
58
+ if ok():
59
+ return
60
+ self._sleep(self.interval)
61
+ raise DmsError(f"timed out waiting for {what}")
62
+
63
+ # -- reading
64
+ def profiles(self):
65
+ return self._json("profiles", "list") or []
66
+
67
+ def find_profile(self, slug, name):
68
+ return next((p for p in self.profiles() if p.get("slug") == slug or p.get("name") == name), None)
69
+
70
+ def status(self):
71
+ return self._json("profiles", "status") or {}
72
+
73
+ def active(self):
74
+ return self.status().get("active", "")
75
+
76
+ def snapshot(self):
77
+ return self._json("profiles", "snapshot") or {}
78
+
79
+ # -- profiles
80
+ def set_option(self, key, value):
81
+ self.ipc("profiles", "setOption", key, value)
82
+
83
+ @contextlib.contextmanager
84
+ def autosave_paused(self):
85
+ """Autosave would write each theme change into the previously active profile."""
86
+ self.set_option("autosave", "false")
87
+ try:
88
+ yield
89
+ finally:
90
+ self.set_option("autosave", "true")
91
+
92
+ def create_profile(self, name):
93
+ self.ipc("profiles", "create", name)
94
+ self._wait(f"profile {name!r}", lambda: any(p.get("name") == name for p in self.profiles()))
95
+
96
+ def remove_profile(self, slug):
97
+ self.ipc("profiles", "remove", slug)
98
+ self._wait(f"profile {slug!r} to go", lambda: all(p.get("slug") != slug for p in self.profiles()))
99
+
100
+ def apply_profile(self, slug):
101
+ self.ipc("profiles", "apply", slug)
102
+
103
+ def done():
104
+ s = self.status()
105
+ return s.get("active") == slug and not s.get("switching")
106
+
107
+ self._wait(f"profile {slug!r} to apply", done)
108
+
109
+ # -- the look itself
110
+ def custom_theme(self, theme_json, theme_id):
111
+ self.ipc("profiles", "customTheme", str(theme_json), theme_id, "default", "default", "default", "both")
112
+ self._wait("theme", lambda: self.snapshot().get("settings", {}).get("customThemeFile") == str(theme_json))
113
+
114
+ def set_mode(self, mode):
115
+ self.ipc("theme", mode)
116
+ self._wait(f"{mode} mode", lambda: self.snapshot().get("session", {}).get("isLightMode") == (mode == "light"))
117
+
118
+ def set_wallpaper(self, path):
119
+ out = self.ipc("wallpaper", "set", str(path))
120
+ if out.startswith("ERROR"):
121
+ raise DmsError(out)
122
+ self._wait("wallpaper", lambda: self.snapshot().get("session", {}).get("wallpaperPath") == str(path))
123
+
124
+ def set_wallpaper_later(self, path, delay=2):
125
+ """Detached, so the caller (dmsProfiles' apply command) returns at once."""
126
+ self._spawn(
127
+ ["sh", "-c", 'sleep "$1"; exec dms ipc call wallpaper set "$2"', "_", str(delay), str(path)],
128
+ start_new_session=True,
129
+ stdin=subprocess.DEVNULL,
130
+ stdout=subprocess.DEVNULL,
131
+ stderr=subprocess.DEVNULL,
132
+ )