feat: upd docs, rm save command

This commit is contained in:
2025-12-21 21:05:10 +03:00
parent f5c0480f88
commit 089f2ef9ee
4 changed files with 181 additions and 109 deletions

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 dmitry Copyright (c) 2025 Dmitry Fedotov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including associated documentation files (the "Software"), to deal in the Software without restriction, including

215
README.md
View File

@@ -1,45 +1,180 @@
# themer # userctx
Apply visual themes to various apps with ease. Manage you configurations and themes with ease.
## Kitty ## Installation
Themer symlinks ~/.config/kitty/current-theme.conf to the You just need to copy the file to a directory
file found within theme directory. in your $PATH (for example: /usr/loca/bin) and
So: copy/create a config file.
1. touch ~/.config/kitty/current-theme.conf ```bash
2. echo 'include current-theme.conf' >> ~/.config/kitty/kitty.conf sudo install userctx.py /usr/local/bin/userctx
If you even called kitten theme 'Some Theme' then mkdir -p ~/.config/userctx
your setup is fine and ready for Themer. cp config.toml ~/.config/userctx
## Niri
If ~/.config/niri/configs/ dir exists then
config is assembled from this dir + theme.
Else niri.kdl from theme directory is considered the whole config and
Themer symlinks ~/.config/niri/config.kdl to you niri.kdl inside
theme directory.
## Sway
Replaces either ~/.config/sway/theme.conf or whole config.
## Swaybg
If using swaybg in other compositor besides sway
then it must be a systemd unit for Themer to manage it.
Put the following to ~/.config/systemd/user/swaybg.service
```
[Unit]
PartOf=graphical-session.target
After=graphical-session.target
Requisite=graphical-session.target
[Service]
ExecStart=/usr/bin/swaybg -m fill -i "%h/.config/swaybg/wallpaper"
Restart=on-failure
``` ```
1. mkdir ~/.config/swaybg ## userctx basics
2. ln -s /path/to/any/wallpaper ~/.config/swaybg/wallpaper **userctx** manages configuration *contexts*. A context is a directory somewhere
3. systemctl --user daemon-reload in your system which stores actual configuration files for your apps
4. systemctl --user add-wants niri.service swaybg.service similar to what resides in ~/.config.
Typically a context directory looks like this.
```bash
/home/dmitry/.config/userctx/Goldfish
├── sway
│   └── theme.conf
└── wofi
└── style.css
```
In the above example, context "Goldfish" contains configs for apps "sway" and "wofi".
These configs will be applied when you apply context "Goldfish".
When a context is applied the app configs from context directory are
symlinked you ~/.config/ folder for each managed app. Goldfish/wofi/style.css symlinks to
\~/.config/wofi/style.css, Goldfish/sway/theme.conf symlinks to \~/.config/sway/theme.conf etc.
This default behaviour can be changed and specific actions or customizations
can be configured on per-app basis by editing **userctx** config file.
The default location for the config is ~/.config/userctx/config.toml.
**userctx** can also be configured to run scripts, trigger your apps to
reload configs etc. For a detailed overview of all config options see below.
There are two commands **userctx** understands: *list* and *apply*.
- *list* lists available contexts in a manner suitable for dmenu-like menu apps
- *apply* \<context\> applies named context
To test your configuration run:
```bash
userctx --nop apply <context_name>
```
The program will then only output what it will do without changing
anything in your home directory.
For a quickstart and simple examples jump to "Examples" section below.
## Configuring userctx
TODO: full description of config options.
## Examples
### Basic usecase: we only need symlinks
Let's add configuration for "foot" terminal emulator, which
will be applied when we apply context "Goldfish" assuming
we would like to switch foot's visual theme when swithching context.
1. First we need to create a separate file for visuals config in out
context directory.
```bash
mkdir ~/.config/userctx/Goldfish/foot/
vim ~/.config/userctx/Goldfish/foot/theme.ini
```
And put the awesome "Tempus Day" into theme.ini:
```
# -*- conf -*-
# theme: Tempus Day
# author: Protesilaos Stavrou (https://protesilaos.com)
# description: Light theme with warm colours (WCAG AA compliant)
[colors]
foreground = 464340
# original background
# background = f8f2e5
background = ffffff
regular0 = 464340
regular1 = c81000
regular2 = 107410
regular3 = 806000
regular4 = 385dc4
regular5 = b63052
regular6 = 007070
regular7 = e7e3d7
bright0 = 68607d
bright1 = b24000
bright2 = 427040
bright3 = 6f6600
bright4 = 0f64c4
bright5 = 8050a7
bright6 = 336c87
bright7 = f8f2e5
```
You could as well symlink any file (pre-existing theme for foot)
to ~/.config/userctx/Goldfish/foot/theme.ini
2. To include this theme file from main foot config add the following line
to $HOME/.config/foot/foot.ini (edit to match your user's homedir):
```
include=/home/dmitry/.config/foot/theme.ini
```
Do not forget to remove style settings from foot.ini so they do not
conflict with separate theme.ini.
3. Finally, tell userctx to manage foot config for you.
Edit ~/.config/userctx/config.toml and add "foot" to "apps"
array in "general" section of the config.
```
[general]
apps = [
"foot",
"sway",
"wofi",
]
```
4. Test you configuration
```bash
userctx --nop apply Goldfish
```
The **--nop** (no-op) flag tells **userctx** to perform a dry-run. It will just output
what it is going to do when you actually apply context.
If all looks good - that's it. When you issue **userctx apply Goldfish**
a symlink will be created in your homedir:
~/.config/foot/theme.ini -> ~/.config/userctx/Goldfish/foot/theme.ini
### More advanced usecase: run command and hot-reload
Let's configure **userctx** to apply theme to helix editor.
1. Similar to the above section, create ~/.config/userctx/Goldfish/helix/helix.toml
with the following contents:
```toml
inherits = "github_light"
"ui.background" = {}
```
Now edit ~/.config/userctx/config.toml.
Add the "helix" to "general" section.
```toml
[general]
apps = [
"foot",
"sway",
"helix",
"wofi",
]
```
2. Add "apps.helix" section:
```toml
[apps.helix]
symlink."*" = "themes/current_theme.toml"
exec = """sed -i -E 's/^theme = (.+)/theme = "current_theme"/' ~/.config/helix/config.toml"""
reload = "pkill -USR1 hx"
```
Here we instruct **userctx** to symlink any (single) file it finds in "helix" subdirectory of context folder
to ~/.config/helix/themes/current_theme.toml
Then we run sed to change the config file. This part is not really necessare if helix is
configured to use theme named "current_theme" and you're sure that config won't change.
We could just replace the file and issue USR1. The sed part if for the case when config
is changed by user or other app.
Finally we set the reload command which
will tell helix to reload config.
3. Test your configuration,
```bash
userctx --nop apply Goldfish
```
See also template "config.toml" with numerous app settings.
https://github.com/YaLTeR/niri/wiki/Example-systemd-Setup

58
TODO.md
View File

@@ -1,58 +0,0 @@
How this must work.
1. On first launch userctx reads ~/.config/userctx/config and aquires list of apps it should manage
2. userctx backs up whole config directories to ~/.config/userctx so further manipulations are safe
Default name is userctx/original, but user can provide a different name from command line.
example:
userctx backup [--name <name>] [--apps TODO]
default name is "original"
default apps list is read from config. command line overrides config completely, no merging.
Backup algorithm:
- If directory ~/.config/sway does not exist or is empty will do nothing
- if directory exists AND not empty then create directory ~/.config/userctx/original/sway
- copy contents of all files AND direcotries in it to ~/.config/userctx/original/sway
- if any file failed to copy - rollback and remove ~/.config/userctx/original/sway
- remove original files (and directories) from ~/.config/sway
- create symlinks to copied files in ~/.config/sway
3. Swithing algorithm:
User requests theme change with:
userctx apply <theme>
userctx tries to apply configs as follows:
- looks up a list of apps it should manage; if list is empty - return non-zero
- looks up requested theme name in ~/.config/userctx/<name>; if not found - return non-zero
- looks up config dir ~/.config/userctx/<theme>/<app>; if none found - return non-zero
- for each <filename> in ~/.config/userctx/<theme>/<app> userctx does
```
mkdir -p ~/.config/<app> # creates app config directory if it does not exist (else no-op)
rm -f ~/.config/<app>/<filename> # tries to delete the config if it exists (else no-op)
ln -s ~/.config/userctx/<theme>/<app>/<filename> ~/.config/<app>/<filename> # symlinks the file from requested theme user dir
```
- after creating symlinks userctx tries to let the app know that config changed
- if app has hot-reload (niri, alacritty, hyprland) - do nothing
- if app re-reads configs on start (fuzzel, wofi) - do nothing
- if app obeys USR1/URS2 - issue the signal
- if app is a systemd service - exec systemctl --user restart <app>
This way a single copy of config files are kept in ~/.config/userctx.
This way user can choose which files to manage themselves and which userctx
should manage.
4. cli interface
userctx list - returns newline-separated list (suitable for dmenu)
userctx apply $(userctx list | fuzzel -d) - a usable way to select theme with dmenu-like apps
TODO:
0. Update README.md, make app usable for a random person on the internet.
1. implement backup
2. parse cli args in a decent way
3. implement behaviour discussed above
4. Think of config options: configure ability to merge several files; map contents of theme dir to specific files
Wider TODO:
1. Think of this app as a user context switcher, not only theme manager.

View File

@@ -2,10 +2,9 @@
import os, sys, subprocess, tomllib, argparse import os, sys, subprocess, tomllib, argparse
COMMAND_LIST = 'list' COMMAND_LIST = 'list'
COMMAND_LOAD = 'load' COMMAND_APPLY = 'apply'
COMMAND_SAVE = 'save'
COMMAND_CTX_NAME = 'context_name' COMMAND_CTX_NAME = 'context_name'
COMMANDS_ALL = [COMMAND_LIST, COMMAND_LOAD, COMMAND_SAVE] COMMANDS_ALL = [COMMAND_LIST, COMMAND_APPLY]
SECTION_GENERAL = 'general' SECTION_GENERAL = 'general'
KEY_APPS = 'apps' KEY_APPS = 'apps'
@@ -269,11 +268,9 @@ class Runner(object):
p.add_argument('-n', '--nop', action='store_true', default=False, p.add_argument('-n', '--nop', action='store_true', default=False,
help='do nothing, just print out what userctx will do') help='do nothing, just print out what userctx will do')
subp = p.add_subparsers(help='command to execute', dest='command') subp = p.add_subparsers(help='command to execute', dest='command')
loadp = subp.add_parser(COMMAND_LOAD, help='load context (provide name of context)') applyp = subp.add_parser(COMMAND_APPLY, help='load context (provide name of context)')
loadp.add_argument(COMMAND_CTX_NAME, help='name of context to load') applyp.add_argument(COMMAND_CTX_NAME, help='name of context to load')
listp = subp.add_parser(COMMAND_LIST, help='list available contexts') listp = subp.add_parser(COMMAND_LIST, help='list available contexts')
savep = subp.add_parser(COMMAND_SAVE, help='save current context (provide name of context)')
savep.add_argument(COMMAND_CTX_NAME, help='name of context to save')
try: # just in case try: # just in case
args = p.parse_args() args = p.parse_args()
except Exception as e: except Exception as e:
@@ -288,10 +285,8 @@ class Runner(object):
ctxmgr = ContextManager(config) ctxmgr = ContextManager(config)
if args.command == COMMAND_LIST: if args.command == COMMAND_LIST:
ctxmgr.print_contexts_list() ctxmgr.print_contexts_list()
elif args.command == COMMAND_LOAD: elif args.command == COMMAND_APPLY:
ctxmgr.load_context(args.__dict__[COMMAND_CTX_NAME]) # guaranteed to be present by argparse ctxmgr.load_context(args.__dict__[COMMAND_CTX_NAME]) # guaranteed to be present by argparse
elif args.command == COMMAND_SAVE:
raise Exception('"save" not implemented')
except Exception as e: except Exception as e:
print(f'error executing command:', e) print(f'error executing command:', e)
return 3 return 3