lightdark 0.9.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,9 @@
1
+ # Credits
2
+
3
+ ## Development Lead
4
+
5
+ - Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
6
+
7
+ ## Contributors
8
+
9
+ None yet. Why not be the first?
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.9.0] - 2026-08-29
8
+
9
+ ### Changed
10
+
11
+ - Rewrote it all.
12
+
13
+ ## [0.0.0] - 1970-01-01
14
+
15
+ ### Added
16
+
17
+ - This is an example entry.
18
+ - See below for the other types of changes.
19
+
20
+ ### Changed
21
+
22
+ - Change in functionality.
23
+
24
+ ### Deprecated
25
+
26
+ - Feature that will be removed soon.
27
+
28
+ ### Removed
29
+
30
+ - Feature that is now removed.
31
+
32
+ ### Fixed
33
+
34
+ - Bug that was fixed.
35
+
36
+ ### Security
37
+
38
+ - Security vulnerability notice.
File without changes
@@ -0,0 +1,15 @@
1
+ include AUTHORS.md
2
+ include CHANGELOG.md
3
+ include LICENSE
4
+ include README.md
5
+
6
+ recursive-include tests *
7
+ recursive-exclude * __pycache__
8
+ recursive-exclude * *.py[co]
9
+
10
+ recursive-include doc *.rst conf.py Makefile make.bat *.jpg *.png *.gif
11
+ recursive-exclude doc/_build *
12
+ recursive-include doc/_templates *
13
+ recursive-include doc/_static *
14
+
15
+ recursive-exclude * .gitkeep
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: lightdark
3
+ Version: 0.9.0
4
+ Summary: Cycle between your desktop themes (such as light and dark)
5
+ Author-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
6
+ Maintainer-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
7
+ License: Apache Software License 2.0
8
+ Project-URL: Homepage, https://hydra.ecd.space/eduard/darklight/timeline
9
+ Project-URL: Changelog, https://hydra.ecd.space/eduard/darklight/file?name=CHANGELOG.md&ci=trunk
10
+ Classifier: Programming Language :: Python :: 3
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ License-File: AUTHORS.md
14
+ Requires-Dist: attrs
15
+ Requires-Dist: atomicwrites
16
+ Requires-Dist: mergedeep
17
+ Requires-Dist: platformdirs
18
+ Dynamic: license-file
19
+
20
+ # lightdark
21
+
22
+ ## Quick start guide
23
+
24
+ Copy `config.json.example` to `~/.config/lightdark/config.json` then modify it to your liking. Then you can just run
25
+
26
+ ```sh
27
+ lightdark
28
+ ```
29
+
30
+ and it will cycle through the themes for you. Try `lightdark --help` to see more options.
@@ -0,0 +1,11 @@
1
+ # lightdark
2
+
3
+ ## Quick start guide
4
+
5
+ Copy `config.json.example` to `~/.config/lightdark/config.json` then modify it to your liking. Then you can just run
6
+
7
+ ```sh
8
+ lightdark
9
+ ```
10
+
11
+ and it will cycle through the themes for you. Try `lightdark --help` to see more options.
@@ -0,0 +1,20 @@
1
+ # Minimal makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line, and also
5
+ # from the environment for the first two.
6
+ SPHINXOPTS ?=
7
+ SPHINXBUILD ?= sphinx-build
8
+ SOURCEDIR = .
9
+ BUILDDIR = _build
10
+
11
+ # Put it first so that "make" without argument is like "make help".
12
+ help:
13
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14
+
15
+ .PHONY: help Makefile
16
+
17
+ # Catch-all target: route all unknown targets to Sphinx using the new
18
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19
+ %: Makefile
20
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@@ -0,0 +1,55 @@
1
+ # Configuration file for the Sphinx documentation builder.
2
+ #
3
+ # For the full list of built-in configuration values, see the documentation:
4
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html
5
+
6
+ # -- Project information -----------------------------------------------------
7
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8
+
9
+ import datetime as _datetime
10
+ from importlib.metadata import version as _get_version
11
+
12
+
13
+ project = "lightdark"
14
+ project_import_name = "lightdark"
15
+ project_dist_name = "lightdark"
16
+ author = "Eduard Christian Dumitrescu"
17
+ copyright = "2026, " + author
18
+
19
+
20
+ try:
21
+ version = _get_version(project_dist_name)
22
+ except Exception:
23
+ version = "UNKNOWN"
24
+ release = version + "~" + _datetime.date.today().strftime("%Y-%m-%d")
25
+
26
+ language = "en"
27
+
28
+
29
+ # -- General configuration ---------------------------------------------------
30
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
31
+
32
+ extensions = [
33
+ "sphinx.ext.autodoc",
34
+ "sphinx.ext.todo",
35
+ # "sphinx.ext.imgmath",
36
+ "sphinx.ext.viewcode",
37
+ "sphinx.ext.napoleon",
38
+ "sphinx.ext.autosummary",
39
+ ]
40
+
41
+ source_suffix = ".rst"
42
+ master_doc = "index"
43
+
44
+ templates_path = ["_templates"]
45
+ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
46
+
47
+ todo_include_todos = True
48
+ pygments_style = "sphinx"
49
+
50
+ # -- Options for HTML output -------------------------------------------------
51
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
52
+
53
+ html_theme = "classic"
54
+ html_static_path = ["_static"]
55
+ htmlhelp_basename = project_dist_name + "doc"
@@ -0,0 +1,20 @@
1
+ .. documentation master file, created by
2
+ sphinx-quickstart on Mon Dec 30 17:43:13 2024.
3
+ You can adapt this file completely to your liking, but it should at least
4
+ contain the root `toctree` directive.
5
+
6
+ Welcome to lightdark's documentation!
7
+ =====================================
8
+
9
+ .. toctree::
10
+ :maxdepth: 3
11
+ :caption: Contents:
12
+
13
+
14
+
15
+ Indices and tables
16
+ ==================
17
+
18
+ * :ref:`genindex`
19
+ * :ref:`modindex`
20
+ * :ref:`search`
@@ -0,0 +1,35 @@
1
+ @ECHO OFF
2
+
3
+ pushd %~dp0
4
+
5
+ REM Command file for Sphinx documentation
6
+
7
+ if "%SPHINXBUILD%" == "" (
8
+ set SPHINXBUILD=sphinx-build
9
+ )
10
+ set SOURCEDIR=.
11
+ set BUILDDIR=_build
12
+
13
+ %SPHINXBUILD% >NUL 2>NUL
14
+ if errorlevel 9009 (
15
+ echo.
16
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17
+ echo.installed, then set the SPHINXBUILD environment variable to point
18
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
19
+ echo.may add the Sphinx directory to PATH.
20
+ echo.
21
+ echo.If you don't have Sphinx installed, grab it from
22
+ echo.https://www.sphinx-doc.org/
23
+ exit /b 1
24
+ )
25
+
26
+ if "%1" == "" goto help
27
+
28
+ %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29
+ goto end
30
+
31
+ :help
32
+ %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33
+
34
+ :end
35
+ popd
@@ -0,0 +1,32 @@
1
+ [tool.black]
2
+ line-length = 120
3
+
4
+ [project]
5
+ name = "lightdark"
6
+ version = "0.9.0"
7
+ description = "Cycle between your desktop themes (such as light and dark)"
8
+ readme = "README.md"
9
+ license = { text = "Apache Software License 2.0" }
10
+ authors = [
11
+ { name = "Eduard Christian Dumitrescu", email = "eduard.c.dumitrescu@gmail.com" },
12
+ ]
13
+ maintainers = [
14
+ { name = "Eduard Christian Dumitrescu", email = "eduard.c.dumitrescu@gmail.com" },
15
+ ]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3"
18
+ ]
19
+ dependencies = ["attrs", "atomicwrites", "mergedeep", "platformdirs"]
20
+
21
+ [project.urls]
22
+ Homepage = "https://hydra.ecd.space/eduard/darklight/timeline"
23
+ Changelog = "https://hydra.ecd.space/eduard/darklight/file?name=CHANGELOG.md&ci=trunk"
24
+
25
+ [project.scripts]
26
+ lightdark = "lightdark.main:main"
27
+
28
+ [tool.setuptools]
29
+ package-dir = {"" = "src"}
30
+
31
+ [tool.setuptools.package-data]
32
+ "*" = ["*.*"]
@@ -0,0 +1,14 @@
1
+ [flake8]
2
+ ignore = E203,E302,E305,E704,E711,E712,E731,E741,W,C901,MC0001
3
+ max-line-length = 120
4
+ max-complexity = 99
5
+
6
+ [pycodestyle]
7
+ inherit = false
8
+ ignore = E203,E302,E305,E704,E711,E712,E731,E741,W,C901,MC0001
9
+ max-line-length = 120
10
+
11
+ [egg_info]
12
+ tag_build =
13
+ tag_date = 0
14
+
@@ -0,0 +1,3 @@
1
+ from setuptools import setup
2
+
3
+ setup(name="lightdark", version="0.0.0.1")
File without changes
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ main()
@@ -0,0 +1,81 @@
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+ import logging
5
+
6
+ import atomicwrites
7
+ import mergedeep
8
+ from platformdirs import PlatformDirs
9
+ from . import modules as mods
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def main():
15
+ ap = argparse.ArgumentParser()
16
+ ap.add_argument("-t", "--theme", help="set to this theme instead of cycling")
17
+ ap.add_argument("-k", "--keep", help="keep the same theme instead of cycling", action="store_true")
18
+ ap.add_argument("-v", "--verbose", help="show errors from module configuration", action="store_true")
19
+ args = ap.parse_args()
20
+
21
+ pd = PlatformDirs(appname="lightdark")
22
+ config_dirs = [Path(d) for d in pd.iter_config_dirs()]
23
+ for d in config_dirs:
24
+ if (state_path := d / "current.txt").exists():
25
+ break
26
+ else:
27
+ state_path = config_dirs[0] / "current.txt"
28
+
29
+ config_dicts = []
30
+ config_paths = []
31
+ for d in config_dirs[::-1]:
32
+ config_paths.append(config_path := d / "config.json")
33
+ if config_path.exists():
34
+ try:
35
+ config_dicts.append(json.loads(config_path.read_bytes()))
36
+ except Exception:
37
+ logger.warning("failed to read", extra=dict(data_path=str(config_path)))
38
+
39
+ if not config_dicts:
40
+ raise RuntimeError(f"config.json not found, paths searched: {config_paths!r}")
41
+
42
+ try:
43
+ old_theme = state_path.read_bytes().decode("utf-8").strip()
44
+ except Exception:
45
+ old_theme = None
46
+
47
+ mergedeep.merge((config := {}), *config_dicts[::-1])
48
+
49
+ if args.keep:
50
+ new_theme = old_theme
51
+ elif args.theme:
52
+ new_theme = args.theme
53
+ else:
54
+ cycle = config["cycle"]
55
+ try:
56
+ i = (cycle.index(old_theme) + 1) % len(cycle)
57
+ except ValueError:
58
+ i = 0
59
+ new_theme = cycle[i]
60
+
61
+ with atomicwrites.atomic_write(state_path, mode="wt", encoding="utf-8", overwrite=True) as f:
62
+ f.write(new_theme)
63
+
64
+ theme_config = config["themes"][new_theme]
65
+
66
+ # now apply the theme:
67
+ utility = mods.Utility()
68
+ for cls in mods.Module.REGISTRY.values():
69
+ try:
70
+ module: mods.Module = cls.from_config(
71
+ config=theme_config[cls.config_key], theme_name=new_theme, utility=utility, global_config=config
72
+ )
73
+ except Exception:
74
+ # mostly likely the configuration is not set
75
+ if args.verbose:
76
+ logger.warning(f"error initializing configuration module: {cls!r}", exc_info=True)
77
+ continue
78
+ try:
79
+ module.run()
80
+ except Exception:
81
+ logger.warning(f"error applying configuration module: {module!r}", exc_info=True)
@@ -0,0 +1,134 @@
1
+ from pathlib import Path
2
+ import re
3
+ import subprocess as sbp
4
+
5
+ import attr
6
+ import atomicwrites
7
+
8
+ sbp_run_pipe_check_kw = dict(stdout=sbp.PIPE, stderr=sbp.PIPE, check=True)
9
+
10
+
11
+ class Utility:
12
+ def simple_run_command(self, args, **kw):
13
+ sbp.run(args, **(sbp_run_pipe_check_kw | kw))
14
+
15
+
16
+ @attr.s(eq=False, hash=False)
17
+ class Module:
18
+ utility: Utility = attr.ib(repr=False)
19
+
20
+ REGISTRY = {}
21
+
22
+ @classmethod
23
+ def _make_kwargs(cls, output, kw):
24
+ output["utility"] = kw["utility"]
25
+
26
+ @classmethod
27
+ def from_config(cls, **kw):
28
+ cls._make_kwargs((cls_kw := {}), kw)
29
+ return cls(**cls_kw)
30
+
31
+ def __init_subclass__(cls, **kwargs):
32
+ super().__init_subclass__(**kwargs)
33
+ cls.REGISTRY[cls.__name__] = cls
34
+
35
+
36
+ @attr.s(eq=False, hash=False)
37
+ class ModuleKitty(Module):
38
+ config_key = "kitty"
39
+ p_theme_name: str = attr.ib()
40
+
41
+ @classmethod
42
+ def _make_kwargs(cls, output, kw):
43
+ super()._make_kwargs(output, kw)
44
+ output.update(p_theme_name=kw["config"]["theme_name"])
45
+
46
+ def run(self):
47
+ self.utility.simple_run_command(["kitty", "+kitten", "themes", "--cache-age=-1", self.p_theme_name])
48
+
49
+
50
+ @attr.s(eq=False, hash=False)
51
+ class ModuleEmacsClient(Module):
52
+ config_key = "emacsclient"
53
+ p_theme_names: list[str] = attr.ib()
54
+
55
+ @classmethod
56
+ def _make_kwargs(cls, output, kw):
57
+ super()._make_kwargs(output, kw)
58
+ output.update(p_theme_names=kw["config"]["theme_names"])
59
+
60
+ def run(self):
61
+ rx_safe = re.compile("^[a-zA-Z0-9_+/-]+$")
62
+ for name in self.p_theme_names:
63
+ if not rx_safe.search(name):
64
+ raise ValueError(f"unsafe name: {name!r}")
65
+ self.utility.simple_run_command(
66
+ ["emacsclient", "-n", "-r", "--eval", f"(set-custom-enable-themes '({' '.join(self.p_theme_names)}))"]
67
+ )
68
+
69
+
70
+ @attr.s(eq=False, hash=False)
71
+ class ModuleKDE(Module):
72
+ config_key = "kde"
73
+ p_color_scheme: str = attr.ib()
74
+ p_desktop_theme: str = attr.ib()
75
+
76
+ @classmethod
77
+ def _make_kwargs(cls, output, kw):
78
+ super()._make_kwargs(output, kw)
79
+ output.update(p_color_scheme=kw["config"]["plasma_color_scheme"])
80
+ output.update(p_desktop_theme=kw["config"]["plasma_desktop_theme"])
81
+
82
+ def run(self):
83
+ self.utility.simple_run_command(["plasma-apply-desktoptheme", self.p_desktop_theme])
84
+ self.utility.simple_run_command(["plasma-apply-colorscheme", self.p_color_scheme])
85
+
86
+
87
+ @attr.s(eq=False, hash=False)
88
+ class ModuleLyx(Module):
89
+ config_key = "lyx"
90
+ p_color_section: str = attr.ib()
91
+
92
+ @classmethod
93
+ def _make_kwargs(cls, output, kw):
94
+ super()._make_kwargs(output, kw)
95
+ output.update(p_color_section=kw["config"]["preferences_color_section"])
96
+
97
+ def run(self):
98
+ p = Path.home() / ".lyx/preferences"
99
+ repl = "".join(("\n", self.p_color_section.strip(), "\n\n"))
100
+ out = re.sub(
101
+ "^(# COLOR SECTION #+\n#\n).*?(?=^#\n)",
102
+ lambda m: "".join([m.group(1), repl]),
103
+ p.read_text(encoding="utf-8"),
104
+ count=1,
105
+ flags=re.M + re.S,
106
+ )
107
+ with atomicwrites.atomic_write(str(p), mode="wt") as fh:
108
+ fh.write(out)
109
+
110
+
111
+ @attr.s(eq=False, hash=False)
112
+ class ModuleGTK(Module):
113
+ config_key = "gtk2"
114
+ p_gtk_theme: str = attr.ib()
115
+
116
+ @classmethod
117
+ def _make_kwargs(cls, output, kw):
118
+ super()._make_kwargs(output, kw)
119
+ output.update(p_theme=kw["config"]["theme_name"])
120
+
121
+ def run(self):
122
+ (Path.home() / ".gtkrc-2.0").delete()
123
+ self.utility.simple_run_command(
124
+ [
125
+ "xfconf-query" "-c",
126
+ "xsettings",
127
+ "-p",
128
+ "/Net/ThemeName",
129
+ "-t",
130
+ "string",
131
+ "-s",
132
+ self.p_theme,
133
+ ],
134
+ )
File without changes
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: lightdark
3
+ Version: 0.9.0
4
+ Summary: Cycle between your desktop themes (such as light and dark)
5
+ Author-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
6
+ Maintainer-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
7
+ License: Apache Software License 2.0
8
+ Project-URL: Homepage, https://hydra.ecd.space/eduard/darklight/timeline
9
+ Project-URL: Changelog, https://hydra.ecd.space/eduard/darklight/file?name=CHANGELOG.md&ci=trunk
10
+ Classifier: Programming Language :: Python :: 3
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ License-File: AUTHORS.md
14
+ Requires-Dist: attrs
15
+ Requires-Dist: atomicwrites
16
+ Requires-Dist: mergedeep
17
+ Requires-Dist: platformdirs
18
+ Dynamic: license-file
19
+
20
+ # lightdark
21
+
22
+ ## Quick start guide
23
+
24
+ Copy `config.json.example` to `~/.config/lightdark/config.json` then modify it to your liking. Then you can just run
25
+
26
+ ```sh
27
+ lightdark
28
+ ```
29
+
30
+ and it will cycle through the themes for you. Try `lightdark --help` to see more options.
@@ -0,0 +1,25 @@
1
+ AUTHORS.md
2
+ CHANGELOG.md
3
+ LICENSE
4
+ MANIFEST.in
5
+ README.md
6
+ pyproject.toml
7
+ setup.cfg
8
+ setup.py
9
+ doc/Makefile
10
+ doc/conf.py
11
+ doc/index.rst
12
+ doc/make.bat
13
+ src/lightdark/__init__.py
14
+ src/lightdark/__main__.py
15
+ src/lightdark/main.py
16
+ src/lightdark/modules.py
17
+ src/lightdark/py.typed
18
+ src/lightdark.egg-info/PKG-INFO
19
+ src/lightdark.egg-info/SOURCES.txt
20
+ src/lightdark.egg-info/dependency_links.txt
21
+ src/lightdark.egg-info/entry_points.txt
22
+ src/lightdark.egg-info/requires.txt
23
+ src/lightdark.egg-info/top_level.txt
24
+ tests/__init__.py
25
+ tests/conftest.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lightdark = lightdark.main:main
@@ -0,0 +1,4 @@
1
+ atomicwrites
2
+ attrs
3
+ mergedeep
4
+ platformdirs
@@ -0,0 +1 @@
1
+ lightdark
@@ -0,0 +1,4 @@
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
File without changes