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.
- omarchy_dms-0.1.0/LICENSE +21 -0
- omarchy_dms-0.1.0/PKG-INFO +106 -0
- omarchy_dms-0.1.0/README.md +86 -0
- omarchy_dms-0.1.0/pyproject.toml +51 -0
- omarchy_dms-0.1.0/setup.cfg +4 -0
- omarchy_dms-0.1.0/src/omarchy_dms/__init__.py +24 -0
- omarchy_dms-0.1.0/src/omarchy_dms/__main__.py +5 -0
- omarchy_dms-0.1.0/src/omarchy_dms/cli.py +92 -0
- omarchy_dms-0.1.0/src/omarchy_dms/dms.py +132 -0
- omarchy_dms-0.1.0/src/omarchy_dms/ops.py +178 -0
- omarchy_dms-0.1.0/src/omarchy_dms/store.py +131 -0
- omarchy_dms-0.1.0/src/omarchy_dms/theme.py +156 -0
- omarchy_dms-0.1.0/src/omarchy_dms.egg-info/PKG-INFO +106 -0
- omarchy_dms-0.1.0/src/omarchy_dms.egg-info/SOURCES.txt +16 -0
- omarchy_dms-0.1.0/src/omarchy_dms.egg-info/dependency_links.txt +1 -0
- omarchy_dms-0.1.0/src/omarchy_dms.egg-info/entry_points.txt +2 -0
- omarchy_dms-0.1.0/src/omarchy_dms.egg-info/top_level.txt +1 -0
- omarchy_dms-0.1.0/tests/test_omarchy_dms.py +315 -0
|
@@ -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,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,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
|
+
)
|