feat: v0.1.0 #1
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
|
test
|
||||||
|
|||||||
36
TODO.md
36
TODO.md
@@ -1,49 +1,49 @@
|
|||||||
How this must work.
|
How this must work.
|
||||||
|
|
||||||
1. On first launch themere reads ~/.config/themer/config and aquires list of apps it should manage
|
1. On first launch userctx reads ~/.config/userctx/config and aquires list of apps it should manage
|
||||||
2. Themer backs up whole config directories to ~/.config/themer so further manipulations are safe
|
2. userctx backs up whole config directories to ~/.config/userctx so further manipulations are safe
|
||||||
Default name is themer/original, but user can provide a different name from command line.
|
Default name is userctx/original, but user can provide a different name from command line.
|
||||||
|
|
||||||
example:
|
example:
|
||||||
themer backup [--name <name>] [--apps TODO]
|
userctx backup [--name <name>] [--apps TODO]
|
||||||
default name is "original"
|
default name is "original"
|
||||||
default apps list is read from config. command line overrides config completely, no merging.
|
default apps list is read from config. command line overrides config completely, no merging.
|
||||||
|
|
||||||
Backup algorithm:
|
Backup algorithm:
|
||||||
- If directory ~/.config/sway does not exist or is empty will do nothing
|
- If directory ~/.config/sway does not exist or is empty will do nothing
|
||||||
- if directory exists AND not empty then create directory ~/.config/themer/original/sway
|
- if directory exists AND not empty then create directory ~/.config/userctx/original/sway
|
||||||
- copy contents of all files AND direcotries in it to ~/.config/themer/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/themer/original/sway
|
- if any file failed to copy - rollback and remove ~/.config/userctx/original/sway
|
||||||
- remove original files (and directories) from ~/.config/sway
|
- remove original files (and directories) from ~/.config/sway
|
||||||
- create symlinks to copied files in ~/.config/sway
|
- create symlinks to copied files in ~/.config/sway
|
||||||
|
|
||||||
3. Swithing algorithm:
|
3. Swithing algorithm:
|
||||||
User requests theme change with:
|
User requests theme change with:
|
||||||
themer apply <theme>
|
userctx apply <theme>
|
||||||
|
|
||||||
Themer tries to apply configs as follows:
|
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 a list of apps it should manage; if list is empty - return non-zero
|
||||||
- looks up requested theme name in ~/.config/themer/<name>; if not found - return non-zero
|
- looks up requested theme name in ~/.config/userctx/<name>; if not found - return non-zero
|
||||||
- looks up config dir ~/.config/themer/<theme>/<app>; if none found - return non-zero
|
- looks up config dir ~/.config/userctx/<theme>/<app>; if none found - return non-zero
|
||||||
- for each <filename> in ~/.config/themer/<theme>/<app> themer does
|
- 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)
|
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)
|
rm -f ~/.config/<app>/<filename> # tries to delete the config if it exists (else no-op)
|
||||||
ln -s ~/.config/themer/<theme>/<app>/<filename> ~/.config/<app>/<filename> # symlinks the file from requested theme user dir
|
ln -s ~/.config/userctx/<theme>/<app>/<filename> ~/.config/<app>/<filename> # symlinks the file from requested theme user dir
|
||||||
```
|
```
|
||||||
- after creating symlinks themer tries to let the app know that config changed
|
- 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 has hot-reload (niri, alacritty, hyprland) - do nothing
|
||||||
- if app re-reads configs on start (fuzzel, wofi) - do nothing
|
- if app re-reads configs on start (fuzzel, wofi) - do nothing
|
||||||
- if app obeys USR1/URS2 - issue the signal
|
- if app obeys USR1/URS2 - issue the signal
|
||||||
- if app is a systemd service - exec systemctl --user restart <app>
|
- if app is a systemd service - exec systemctl --user restart <app>
|
||||||
|
|
||||||
This way a single copy of config files are kept in ~/.config/themer.
|
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 themer
|
This way user can choose which files to manage themselves and which userctx
|
||||||
should manage.
|
should manage.
|
||||||
|
|
||||||
4. cli interface
|
4. cli interface
|
||||||
themer list - returns newline-separated list (suitable for dmenu)
|
userctx list - returns newline-separated list (suitable for dmenu)
|
||||||
themer apply $(themer list | fuzzel -d) - a usable way to select theme with dmenu-like apps
|
userctx apply $(userctx list | fuzzel -d) - a usable way to select theme with dmenu-like apps
|
||||||
|
|
||||||
TODO:
|
TODO:
|
||||||
|
|
||||||
|
|||||||
187
config.toml
187
config.toml
@@ -5,85 +5,87 @@
|
|||||||
# tasks
|
# tasks
|
||||||
dry_run = true
|
dry_run = true
|
||||||
|
|
||||||
# "tasks" array must not be empty
|
# "apps" array tells userctx what to do (which apps
|
||||||
# it tells userctx what to do (which apps
|
# should be actually managed). If it is empty or not
|
||||||
# should be actually managed)
|
# set, then userctx will do nothing.
|
||||||
tasks = [
|
apps = [
|
||||||
"sway",
|
#"sway",
|
||||||
"gtk",
|
#"gtk",
|
||||||
"helix",
|
#"helix",
|
||||||
"foot",
|
#"foot",
|
||||||
"wofi",
|
#"wofi",
|
||||||
"mako",
|
#"mako",
|
||||||
|
"myapp",
|
||||||
]
|
]
|
||||||
|
|
||||||
# target directory where managed configs
|
# target directory where managed configs
|
||||||
# are located
|
# are located
|
||||||
# defaults to $XDG_CONFIG_HOME if specifically set
|
# defaults to $XDG_CONFIG_HOME if specifically set
|
||||||
# or ~/.config if $XGD_CONFIG_HOME is not set
|
# or ~/.config if $XGD_CONFIG_HOME is not set
|
||||||
managed_directory = "~/.config"
|
target_path = "~/.config"
|
||||||
|
|
||||||
# directory where contexts are stored
|
# directory where contexts are stored
|
||||||
# default is "~/.config/userctx"
|
# default is "~/.config/userctx"
|
||||||
contexts_path = "~/.config/userctx"
|
source_path = "~/code/userctx/test"
|
||||||
|
|
||||||
# This section of config is intended to document
|
[apps]
|
||||||
# what userctx can do. Assume we are
|
example2.scripts = ["s1", "s2"]
|
||||||
# building a config for app "example"
|
myapp.symlink."*" = "*"
|
||||||
[task.example]
|
myapp.exec = """echo "hello world""""
|
||||||
# the default path to apply your context to is
|
myapp.actions = {}
|
||||||
|
myapp.script = []
|
||||||
|
|
||||||
|
# section that demonstrates all the available
|
||||||
|
# features of userctx
|
||||||
|
[apps.example]
|
||||||
|
# Path to appliaction configs is read from
|
||||||
|
# general.source_path. The default source path is
|
||||||
|
# ~/.config/userctx/<context_name>/example for this
|
||||||
|
# app.
|
||||||
|
# Individual path for app can be provided via
|
||||||
|
# "source_path" for particular app. Then userctx will try to lookup
|
||||||
|
# app settings in /source_path/<context_name>
|
||||||
|
# Default is general.source_path/<context_name>/<app_name>.
|
||||||
|
# For this example it will expand to
|
||||||
|
# ~/.config/userctx/<context_name/example.
|
||||||
|
# TODO: do we really need this?
|
||||||
|
source_path = "~/tmp/contexts"
|
||||||
|
|
||||||
|
# The default path to apply your context to is
|
||||||
# ~/.config/example (for app name "example")
|
# ~/.config/example (for app name "example")
|
||||||
# "target_path" overrides this.
|
# if not overridden with general.target_path.
|
||||||
# If set then $CONTEXT_DST env will hold this value and
|
# Setting "target_path" overrides this.
|
||||||
|
# $CONTEXT_DST env will hold the actual destination path.
|
||||||
# symlinks will be created relative to this specified
|
# symlinks will be created relative to this specified
|
||||||
# path.
|
# path.
|
||||||
target_path = "~/.config/example/configs"
|
target_path = "~/.config/example/configs"
|
||||||
|
|
||||||
# "actions" array defines which steps should be
|
# "symlink" map defines
|
||||||
# executed to apply content from
|
|
||||||
# context directory to users home
|
|
||||||
# directory. If actions array is empty the default
|
|
||||||
# behaviour is to just symlink everything from
|
|
||||||
# $CONTEXT_SRC to $CONTEXT_DST
|
|
||||||
actions = [
|
|
||||||
"symlinks", # do the symlinking as specified in "symlinks array below"
|
|
||||||
"code", # execute shell script from object "code" below
|
|
||||||
"commands", # run commands from "commands array below"
|
|
||||||
"scripts", # run scripts from "scripts" array below
|
|
||||||
]
|
|
||||||
|
|
||||||
# "symlinks" array of mappings (inline tables of TOML) defines
|
|
||||||
# which files will be symlinked to users home
|
# which files will be symlinked to users home
|
||||||
# directory. If "symlinks" key is present in
|
# directory.
|
||||||
# "actions" above but this "symlinks" array is omitted
|
# Names of files in thes mapping are relative to
|
||||||
# then userctx treats all files in the context directory as
|
# context directory and target directory
|
||||||
# files that must be symlinked to users home.
|
# by default source is ~/.config/userctx/<context_name>/example
|
||||||
symlinks = [
|
# and target directory is ~/.config/example
|
||||||
# names of files in these mapping are relative to
|
# for app named "example"
|
||||||
# context directory and target directory
|
#
|
||||||
# by default source is ~/.config/userctx/<context_name>/example for
|
# this will link file from context dir to destination dir
|
||||||
# task (app) named "example"
|
# $CONTEXT_SRC/context_file.conf -> $CONTEXT_DST/symlink_to_context_file.conf
|
||||||
# default target directory is ~/.config/example for
|
symlink."context_file.conf" = "symlink_to_context_file.conf"
|
||||||
# task (app) named "example"
|
|
||||||
#
|
|
||||||
# this will link file from context dir to destination dir
|
|
||||||
# $CONTEXT_SRC/context_file.conf -> $CONTEXT_DST/symlink_to_context_file.conf
|
|
||||||
{"context_file.conf" = "symlink_to_context_file.conf"},
|
|
||||||
|
|
||||||
# this will link file to subdirectory inside destination dir
|
|
||||||
# directory must be created beforehand
|
|
||||||
# $CONTEXT_SRC/other_file.conf -> $CONTEXT_DST/subdir/other_file.conf
|
|
||||||
{"other_file.conf" = "subdir/other_file.conf" },
|
|
||||||
# also possible option
|
|
||||||
{"subdir/yet_another_file.conf" = "yet_another_file.conf" },
|
|
||||||
]
|
|
||||||
|
|
||||||
# "code" object let's you write
|
# this will link file to subdirectory inside destination dir
|
||||||
# a custom script that will apply contents of
|
# Note that destination subdir must be created beforehand.
|
||||||
# your context. Note that string is a multiline string
|
# $CONTEXT_SRC/other_file.conf -> $CONTEXT_DST/subdir/other_file.conf
|
||||||
# strating and ending with three double-qoutes.
|
symlink."other_file.conf" = "subdir/other_file.conf"
|
||||||
# This string will be passed as is to $SHELL -c <string>
|
symlink."subdir/yet_another_file.conf" = "yet_another_file.conf"
|
||||||
code = """
|
|
||||||
|
|
||||||
|
# "exec" let's you write a reload command or
|
||||||
|
# even a bigger custom script that will apply contents of
|
||||||
|
# your context. Note the triple double-qoutes for multiline
|
||||||
|
# strings (see TOML spec).
|
||||||
|
# This string will be passed as is to $SHELL for execution.
|
||||||
|
exec = """
|
||||||
# this is a normal shell script which will
|
# this is a normal shell script which will
|
||||||
# be executed by user's $SHELL or 'bash' binary
|
# be executed by user's $SHELL or 'bash' binary
|
||||||
# if users $SHELL is empty
|
# if users $SHELL is empty
|
||||||
@@ -97,13 +99,6 @@ code = """
|
|||||||
echo "$CONTEXT_NAME"
|
echo "$CONTEXT_NAME"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Another way in run something when applying context
|
|
||||||
# is to define "commands" array.
|
|
||||||
commands = [
|
|
||||||
['first_command', '--arg1', 'value', '--arg2', 'value2'],
|
|
||||||
['second_command', '--arg1', 'value', '--arg2', 'value2'],
|
|
||||||
]
|
|
||||||
|
|
||||||
# scripts array will try to find
|
# scripts array will try to find
|
||||||
# named files in $CONTEXT_SRC and
|
# named files in $CONTEXT_SRC and
|
||||||
# execute them with $SHELL <script>
|
# execute them with $SHELL <script>
|
||||||
@@ -111,26 +106,23 @@ commands = [
|
|||||||
# and action "scripts" is present in
|
# and action "scripts" is present in
|
||||||
# "actions" array above then all files
|
# "actions" array above then all files
|
||||||
# in $CONTEXT_SRC will be treated as scripts
|
# in $CONTEXT_SRC will be treated as scripts
|
||||||
# (note the "task.gtk section below")
|
# (note the "apps.gtk section below")
|
||||||
scripts = [
|
scripts = [
|
||||||
"script1.sh",
|
"script1.sh",
|
||||||
"script2.sh",
|
"script2.sh",
|
||||||
]
|
]
|
||||||
|
|
||||||
# reload command (if set) will be executed after
|
reload = "pkill -USR1 example"
|
||||||
# all actions
|
|
||||||
reload = ['echo', 'this is my reload command']
|
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
# settings for some
|
# settings for some
|
||||||
# commonly used apps
|
# commonly used apps
|
||||||
#########################
|
#########################
|
||||||
|
|
||||||
[task.niri]
|
[apps.niri]
|
||||||
target_path = "~/.config/niri/configs"
|
target_path = "~/.config/niri/configs"
|
||||||
actions = [
|
actions = [
|
||||||
"symlinks",
|
"symlink",
|
||||||
"code",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# The snippet below is needed for niri 25.8 which did not
|
# The snippet below is needed for niri 25.8 which did not
|
||||||
@@ -141,50 +133,49 @@ actions = [
|
|||||||
# symlink configs found in the context dir to
|
# symlink configs found in the context dir to
|
||||||
# ~/.config/niri/configs, which then get concatenated.
|
# ~/.config/niri/configs, which then get concatenated.
|
||||||
# For niri since 25.11 you can just put your includes
|
# For niri since 25.11 you can just put your includes
|
||||||
# into the context dir and omit [task.niri] altogether.
|
# into the context dir and omit [apps.niri] altogether.
|
||||||
code = """
|
exec = """
|
||||||
cat ~/.config/niri/configs/* > ~/.config/niri/config.kdl
|
cat ~/.config/niri/configs/* > ~/.config/niri/config.kdl
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[task.helix]
|
[apps.helix]
|
||||||
actions = [
|
actions = [
|
||||||
"symlinks",
|
"exec",
|
||||||
"code",
|
"symlink",
|
||||||
]
|
]
|
||||||
# context must contain file named "helix.toml" which describes a theme.
|
# context must contain file named "helix.toml" which describes a theme.
|
||||||
symlinks = [
|
|
||||||
# $CONTEXT_SRC/helix.toml -> $CONTEXT_DST/themes/current_theme.toml
|
# $CONTEXT_SRC/helix.toml -> $CONTEXT_DST/themes/current_theme.toml
|
||||||
{"helix.toml" = "themes/current_theme.toml"}
|
symlink."helix.toml" = "themes/current_theme.toml"
|
||||||
]
|
|
||||||
code = """
|
exec = """
|
||||||
sed -i -E 's/^theme = (.+)/theme = "current_theme"/' ~/.config/helix/config.toml
|
sed -i -E 's/^theme = (.+)/theme = "current_theme"/' ~/.config/helix/config.toml
|
||||||
"""
|
"""
|
||||||
reload = ['pkill', '-USR1', 'hx']
|
reload = "pkill -USR1 hx"
|
||||||
|
|
||||||
[task.gtk]
|
[apps.gtk]
|
||||||
# To setup the look of GTK you may put a simple shell script
|
# To setup the look of GTK you may put a simple shell script
|
||||||
# into ~/.userctx/<context_name>/gtk/theme.sh with contents similar to:
|
# into ~/.userctx/<context_name>/gtk/theme.sh with contents similar to:
|
||||||
# gsettings set org.gnome.desktop.interface color-scheme 'default'
|
# gsettings set org.gnome.desktop.interface color-scheme 'default'
|
||||||
# gsettings set org.gnome.desktop.interface gtk-theme 'Yaru'
|
# gsettings set org.gnome.desktop.interface gtk-theme 'Yaru'
|
||||||
# gsettings set org.gnome.desktop.interface icon-theme 'Yaru'
|
# gsettings set org.gnome.desktop.interface icon-theme 'Yaru'
|
||||||
actions = ["scripts"]
|
actions = ["script"]
|
||||||
|
|
||||||
[task.mako]
|
[apps.mako]
|
||||||
reload = ["makoctl", "reload"]
|
reload = "makoctl reload"
|
||||||
|
|
||||||
[task.sway]
|
[apps.sway]
|
||||||
reload = ["swaymsg", "reload"]
|
reload = "swaymsg reload"
|
||||||
|
|
||||||
[task.kitty]
|
[apps.kitty]
|
||||||
reload = ['pkill', '-USR1', 'kitty']
|
reload = 'pkill -USR1 kitty'
|
||||||
|
|
||||||
[task.waybar]
|
[apps.waybar]
|
||||||
reload = ['pkill', '-USR2', 'waybar']
|
reload = 'pkill -USR2 waybar'
|
||||||
|
|
||||||
[task.swaybg]
|
[apps.swaybg]
|
||||||
# This config is for swaybg setup as s systemd service.
|
# This config is for swaybg setup as s systemd service.
|
||||||
# Unit runs "swaybg -m fill -i "%h/.config/swaybg/wallpaper".
|
# Unit runs "swaybg -m fill -i "%h/.config/swaybg/wallpaper".
|
||||||
# ~/.config/userctx/<context>/swaybg/ contains single file
|
# ~/.config/userctx/<context>/swaybg/ contains single file
|
||||||
# called "wallpaper" which gets symlinked to ~/.config/swaybg/wallpaper
|
# called "wallpaper" which gets symlinked to ~/.config/swaybg/wallpaper
|
||||||
target_path = '~/.config/swaybg'
|
target_path = '~/.config/swaybg'
|
||||||
reload = ['systemctl', '--user', 'restart', 'swaybg.service']
|
reload = 'systemctl --user restart swaybg.service'
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
[general]
|
|
||||||
# if dry run is set to true
|
|
||||||
# userctx will only validate config
|
|
||||||
# and tell you what it will do to execute
|
|
||||||
# tasks
|
|
||||||
dry_run = true
|
|
||||||
|
|
||||||
# "apps" array tells userctx what to do (which apps
|
|
||||||
# should be actually managed). If it is empty or not
|
|
||||||
# set, then userctx will do nothing.
|
|
||||||
apps = [
|
|
||||||
"sway",
|
|
||||||
"gtk",
|
|
||||||
"helix",
|
|
||||||
"foot",
|
|
||||||
"wofi",
|
|
||||||
"mako",
|
|
||||||
]
|
|
||||||
|
|
||||||
# target directory where managed configs
|
|
||||||
# are located
|
|
||||||
# defaults to $XDG_CONFIG_HOME if specifically set
|
|
||||||
# or ~/.config if $XGD_CONFIG_HOME is not set
|
|
||||||
target_path = "~/.config"
|
|
||||||
|
|
||||||
# directory where contexts are stored
|
|
||||||
# default is "~/.config/userctx"
|
|
||||||
source_path = "~/.config/userctx"
|
|
||||||
|
|
||||||
[apps]
|
|
||||||
helix.symlink."*" = "themes/theme.toml"
|
|
||||||
helix.exec = "pkill -USR1 hx"
|
|
||||||
|
|
||||||
example2.scripts = ["s1", "s2"]
|
|
||||||
|
|
||||||
[apps.example]
|
|
||||||
symlink."this.file" = "that.file"
|
|
||||||
symlink."*" = "theme.conf" # TODO: handle any file to particular name
|
|
||||||
exec = 'echo "this simulates reload command"'
|
|
||||||
|
|
||||||
[apps.niri]
|
|
||||||
exec = "this simulates code for niri restart"
|
|
||||||
|
|
||||||
[apps.niri.symlink]
|
|
||||||
"another.file" = "another.link"
|
|
||||||
|
|
||||||
# section that demonstrates all the available
|
|
||||||
# features of userctx
|
|
||||||
[apps.example]
|
|
||||||
# Path to appliaction configs is read from
|
|
||||||
# general.soucre_path. The default source path is
|
|
||||||
# ~/.config/userctx/<context_name>/example for this
|
|
||||||
# app.
|
|
||||||
# Individual path for app can be provided via
|
|
||||||
# "source_path" for particular app. Then userctx will try to lookup
|
|
||||||
# app settings in /source_path/<context_name>
|
|
||||||
# Default is general.source_path/<context_name>/<app_name>.
|
|
||||||
# For this example it will expand to
|
|
||||||
# ~/.config/userctx/<context_name/example.
|
|
||||||
# TODO: do we really need this?
|
|
||||||
source_path = "~/tmp/contexts"
|
|
||||||
|
|
||||||
# The default path to apply your context to is
|
|
||||||
# ~/.config/example (for app name "example")
|
|
||||||
# if not overridden with general.target_path.
|
|
||||||
# Setting "target_path" overrides this.
|
|
||||||
# $CONTEXT_DST env will hold the actual destination path.
|
|
||||||
# symlinks will be created relative to this specified
|
|
||||||
# path.
|
|
||||||
target_path = "~/.config/example/configs"
|
|
||||||
|
|
||||||
# "symlink" map defines
|
|
||||||
# which files will be symlinked to users home
|
|
||||||
# directory.
|
|
||||||
# Names of files in thes mapping are relative to
|
|
||||||
# context directory and target directory
|
|
||||||
# by default source is ~/.config/userctx/<context_name>/example
|
|
||||||
# and target directory is ~/.config/example
|
|
||||||
# for app named "example"
|
|
||||||
#
|
|
||||||
# this will link file from context dir to destination dir
|
|
||||||
# $CONTEXT_SRC/context_file.conf -> $CONTEXT_DST/symlink_to_context_file.conf
|
|
||||||
symlink."context_file.conf" = "symlink_to_context_file.conf"
|
|
||||||
|
|
||||||
# this will link file to subdirectory inside destination dir
|
|
||||||
# Note that destination subdir must be created beforehand.
|
|
||||||
# $CONTEXT_SRC/other_file.conf -> $CONTEXT_DST/subdir/other_file.conf
|
|
||||||
symlink."other_file.conf" = "subdir/other_file.conf"
|
|
||||||
symlink."subdir/yet_another_file.conf" = "yet_another_file.conf"
|
|
||||||
|
|
||||||
|
|
||||||
# "exec" let's you write a reload command or
|
|
||||||
# even a bigger custom script that will apply contents of
|
|
||||||
# your context. Note the triple double-qoutes for multiline
|
|
||||||
# strings (see TOML spec).
|
|
||||||
# This string will be passed as is to $SHELL for execution.
|
|
||||||
exec = """
|
|
||||||
# this is a normal shell script which will
|
|
||||||
# be executed by user's $SHELL or 'bash' binary
|
|
||||||
# if users $SHELL is empty
|
|
||||||
|
|
||||||
# users envs are available
|
|
||||||
echo "$HOME"
|
|
||||||
|
|
||||||
# additional envs are available
|
|
||||||
echo "$CONTEXT_SRC"
|
|
||||||
echo "$CONTEXT_DST"
|
|
||||||
echo "$CONTEXT_NAME"
|
|
||||||
"""
|
|
||||||
|
|
||||||
# scripts array will try to find
|
|
||||||
# named files in $CONTEXT_SRC and
|
|
||||||
# execute them with $SHELL <script>
|
|
||||||
# IMPORTANT: if array is empty
|
|
||||||
# and action "scripts" is present in
|
|
||||||
# "actions" array above then all files
|
|
||||||
# in $CONTEXT_SRC will be treated as scripts
|
|
||||||
# (note the "apps.gtk section below")
|
|
||||||
scripts = [
|
|
||||||
"script1.sh",
|
|
||||||
"script2.sh",
|
|
||||||
]
|
|
||||||
|
|
||||||
#########################
|
|
||||||
settings for some
|
|
||||||
commonly used apps
|
|
||||||
#########################
|
|
||||||
|
|
||||||
[apps.niri]
|
|
||||||
target_path = "~/.config/niri/configs"
|
|
||||||
actions = [
|
|
||||||
"symlinks",
|
|
||||||
"code",
|
|
||||||
]
|
|
||||||
|
|
||||||
# The snippet below is needed for niri 25.8 which did not
|
|
||||||
# support imports/includes in the config. I'm splitting
|
|
||||||
# config into several parts and them concatenating them
|
|
||||||
# after applying changes to theme. The other parts are left intact.
|
|
||||||
# Also note the custom target path above. It instructs userctx to
|
|
||||||
# symlink configs found in the context dir to
|
|
||||||
# ~/.config/niri/configs, which then get concatenated.
|
|
||||||
# For niri since 25.11 you can just put your includes
|
|
||||||
# into the context dir and omit [apps.niri] altogether.
|
|
||||||
exec = """
|
|
||||||
cat ~/.config/niri/configs/* > ~/.config/niri/config.kdl
|
|
||||||
"""
|
|
||||||
|
|
||||||
[apps.helix]
|
|
||||||
actions = [
|
|
||||||
"symlinks",
|
|
||||||
"code",
|
|
||||||
]
|
|
||||||
# context must contain file named "helix.toml" which describes a theme.
|
|
||||||
# $CONTEXT_SRC/helix.toml -> $CONTEXT_DST/themes/current_theme.toml
|
|
||||||
symlink."helix.toml" = "themes/current_theme.toml"
|
|
||||||
|
|
||||||
exec = """
|
|
||||||
sed -i -E 's/^theme = (.+)/theme = "current_theme"/' ~/.config/helix/config.toml
|
|
||||||
pkill -USR1 hx
|
|
||||||
"""
|
|
||||||
|
|
||||||
[apps.gtk]
|
|
||||||
# To setup the look of GTK you may put a simple shell script
|
|
||||||
# into ~/.userctx/<context_name>/gtk/theme.sh with contents similar to:
|
|
||||||
# gsettings set org.gnome.desktop.interface color-scheme 'default'
|
|
||||||
# gsettings set org.gnome.desktop.interface gtk-theme 'Yaru'
|
|
||||||
# gsettings set org.gnome.desktop.interface icon-theme 'Yaru'
|
|
||||||
actions = ["scripts"]
|
|
||||||
|
|
||||||
[apps.mako]
|
|
||||||
exec = "makoctl reload"
|
|
||||||
|
|
||||||
[apps.sway]
|
|
||||||
exec = "swaymsg reload"
|
|
||||||
|
|
||||||
[apps.kitty]
|
|
||||||
exec = 'pkill -USR1 kitty'
|
|
||||||
|
|
||||||
[apps.waybar]
|
|
||||||
exec = 'pkill -USR2 waybar'
|
|
||||||
|
|
||||||
[apps.swaybg]
|
|
||||||
# This config is for swaybg setup as s systemd service.
|
|
||||||
# Unit runs "swaybg -m fill -i "%h/.config/swaybg/wallpaper".
|
|
||||||
# ~/.config/userctx/<context>/swaybg/ contains single file
|
|
||||||
# called "wallpaper" which gets symlinked to ~/.config/swaybg/wallpaper
|
|
||||||
target_path = '~/.config/swaybg'
|
|
||||||
reload = ['systemctl', '--user', 'restart', 'swaybg.service']
|
|
||||||
206
userctx.py
206
userctx.py
@@ -8,21 +8,19 @@ COMMAND_CTX_NAME = 'context_name'
|
|||||||
COMMANDS_ALL = [COMMAND_LIST, COMMAND_LOAD, COMMAND_SAVE]
|
COMMANDS_ALL = [COMMAND_LIST, COMMAND_LOAD, COMMAND_SAVE]
|
||||||
|
|
||||||
SECTION_GENERAL = 'general'
|
SECTION_GENERAL = 'general'
|
||||||
KEY_CONTEXTS_PATH = 'contexts_path'
|
KEY_APPS = 'apps'
|
||||||
KEY_TASKS = 'tasks'
|
|
||||||
KEY_DRY_RUN = 'dry_run'
|
KEY_DRY_RUN = 'dry_run'
|
||||||
KEY_MANAGED_DIR = 'managed_directory'
|
|
||||||
|
|
||||||
SECTION_TASK = 'task'
|
SECTION_APPS = 'apps'
|
||||||
KEY_SRC_PATH = 'source_path'
|
KEY_SRC_PATH = 'source_path'
|
||||||
KEY_DST_PATH = 'target_path'
|
KEY_DST_PATH = 'target_path'
|
||||||
KEY_RELOAD = 'reload'
|
KEY_RELOAD = 'reload'
|
||||||
KEY_ACTIONS = 'actions'
|
KEY_ACTIONS = 'actions'
|
||||||
KEY_SYMLINK = 'symlinks'
|
KEY_SYMLINK = 'symlink'
|
||||||
KEY_COMMAND = 'commands'
|
KEY_SCRIPT = 'script'
|
||||||
KEY_SCRIPT = 'scripts'
|
KEY_EXEC = 'exec'
|
||||||
KEY_CODE = 'code'
|
ACTIONS_ALL = [KEY_SYMLINK, KEY_SCRIPT, KEY_EXEC, KEY_RELOAD]
|
||||||
ACTIONS_ALL = [KEY_SYMLINK, KEY_COMMAND, KEY_SCRIPT, KEY_CODE]
|
APPS_VALID_KEYS = [KEY_ACTIONS, KEY_SYMLINK, KEY_SCRIPT, KEY_EXEC, KEY_RELOAD, KEY_SRC_PATH, KEY_DST_PATH]
|
||||||
|
|
||||||
ENV_CONTEXT_NAME = 'CONTEXT_NAME'
|
ENV_CONTEXT_NAME = 'CONTEXT_NAME'
|
||||||
ENV_CONTEXT_SRC = 'CONTEXT_SRC'
|
ENV_CONTEXT_SRC = 'CONTEXT_SRC'
|
||||||
@@ -48,71 +46,93 @@ class OS(object):
|
|||||||
def exec_snippet(self, code: str) -> None:
|
def exec_snippet(self, code: str) -> None:
|
||||||
return self.exec_command([self.shell, "-c", code])
|
return self.exec_command([self.shell, "-c", code])
|
||||||
|
|
||||||
|
def scan_dir(self, path: str) -> list[str]:
|
||||||
|
out = []
|
||||||
|
def scan(dir: str, prefix: str):
|
||||||
|
entries = list(os.scandir(dir)) # extract values from iterator
|
||||||
|
out.extend([os.path.join(prefix, e.name) for e in entries if e.is_file()])
|
||||||
|
for d in [e.name for e in entries if e.is_dir()]:
|
||||||
|
scan(os.path.join(dir, d), os.path.join(prefix, d))
|
||||||
|
scan(path, '')
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
class Task(object):
|
class Task(object):
|
||||||
def __init__(self, name: str, src: str, dst: str, conf: dict, dry_run=True):
|
def __init__(self, name: str, src: str, dst: str, conf: dict, dry_run=True):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.src = conf.get(KEY_SRC_PATH) or src # default is ~/.config/userctx/<context>/<task>
|
self.src = conf.get(KEY_SRC_PATH) or src # default is ~/.config/userctx/<context>/<task>
|
||||||
self.dst = conf.get(KEY_DST_PATH) or dst # default is ~/.config/<task>
|
self.dst = conf.get(KEY_DST_PATH) or dst # default is ~/.config/<task>
|
||||||
self.actions = conf.get(KEY_ACTIONS) or [KEY_SYMLINK] # default is to just symlink all
|
|
||||||
self.symlinks = conf.get(KEY_SYMLINK)
|
self.symlinks = conf.get(KEY_SYMLINK)
|
||||||
self.commands = conf.get(KEY_COMMAND)
|
|
||||||
self.scripts = conf.get(KEY_SCRIPT)
|
self.scripts = conf.get(KEY_SCRIPT)
|
||||||
self.code = conf.get(KEY_CODE)
|
self.code = conf.get(KEY_EXEC)
|
||||||
self.reload_command = conf.get(KEY_RELOAD)
|
self.reload = conf.get(KEY_RELOAD)
|
||||||
|
self.actions = conf.get(KEY_ACTIONS)
|
||||||
|
if self.actions is None: # not set, actions = [] is a no-op conf for app
|
||||||
|
self.actions = [KEY_SYMLINK]
|
||||||
|
if self.scripts is not None: # may be an empty array
|
||||||
|
self.actions.append(KEY_SCRIPT)
|
||||||
|
if self.code is not None:
|
||||||
|
self.actions.append(KEY_EXEC)
|
||||||
|
if self.reload is not None:
|
||||||
|
self.actions.append(KEY_RELOAD)
|
||||||
self.shell = OS(env={ENV_CONTEXT_NAME: self.name, ENV_CONTEXT_SRC: self.src, ENV_CONTEXT_DST: self.dst})
|
self.shell = OS(env={ENV_CONTEXT_NAME: self.name, ENV_CONTEXT_SRC: self.src, ENV_CONTEXT_DST: self.dst})
|
||||||
self.dry_run = dry_run
|
self.dry_run = dry_run
|
||||||
|
|
||||||
def execute(self) -> None:
|
def execute(self) -> None:
|
||||||
|
if self.dry_run:
|
||||||
|
print("Configured actions:", self.actions)
|
||||||
for a in self.actions:
|
for a in self.actions:
|
||||||
if a == KEY_SYMLINK:
|
if a == KEY_SYMLINK:
|
||||||
self._symlink()
|
self._symlink()
|
||||||
elif a == KEY_COMMAND:
|
|
||||||
self._run_commands()
|
|
||||||
elif a == KEY_SCRIPT:
|
elif a == KEY_SCRIPT:
|
||||||
self._run_scripts()
|
self._run_scripts()
|
||||||
elif a == KEY_CODE:
|
elif a == KEY_EXEC:
|
||||||
self._run_code()
|
self._run_code(self.code)
|
||||||
if self.reload_command and not self.dry_run:
|
elif a == KEY_RELOAD:
|
||||||
self.shell.exec_command(self.reload_command)
|
self._run_code(self.reload)
|
||||||
|
|
||||||
def _symlink(self) -> None:
|
def _symlink(self) -> None:
|
||||||
if not self.symlinks: # will symlink everything we have then
|
entries = self.shell.scan_dir(self.src)
|
||||||
self.symlinks = [{entry.name: entry.name} for entry in os.scandir(self.src)]
|
if not entries:
|
||||||
for l in self.symlinks:
|
return
|
||||||
for k, v in l.items():
|
if self.symlinks is None: # will symlink everything we have then
|
||||||
src, dst = os.path.join(self.src, k), os.path.join(self.dst, v)
|
self.symlinks = {'*': '*'}
|
||||||
if self.dry_run:
|
not_included = [e for e in entries if e not in self.symlinks]
|
||||||
print("will remove:", dst, "will symlink:", src, "to", dst)
|
|
||||||
continue
|
if '*' in self.symlinks:
|
||||||
if os.path.lexists(dst): os.remove(dst)
|
target = self.symlinks['*']
|
||||||
os.symlink(src, dst)
|
if target == '*': # symlink."*" = "*" in config
|
||||||
|
self.symlinks.update(dict(zip(not_included, not_included)))
|
||||||
|
else: # symlink."*" = "some/path/target.file"
|
||||||
|
self.symlinks[not_included[0]] = target
|
||||||
|
del self.symlinks['*']
|
||||||
|
|
||||||
|
for k, v in self.symlinks.items():
|
||||||
|
src, dst = os.path.join(self.src, k), os.path.join(self.dst, v)
|
||||||
|
if self.dry_run:
|
||||||
|
print("will remove:", dst)
|
||||||
|
print("will symlink:", src, "to", dst)
|
||||||
|
continue
|
||||||
|
if os.path.lexists(dst): os.remove(dst)
|
||||||
|
os.symlink(src, dst)
|
||||||
|
|
||||||
def _run_scripts(self) -> None:
|
def _run_scripts(self) -> None:
|
||||||
if not self.scripts: # nothing provided explicitly
|
if not self.scripts: # nothing provided explicitly
|
||||||
self.scripts = [entry.name for entry in os.scandir(self.src) if entry.is_file()]
|
self.scripts = [e for e in self.shell.scan_dir(self.src) if e.endswith('.sh')]
|
||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
print(f'will execute scripts from {self.src}:', self.scripts)
|
print(f'will execute scripts from {self.src}:', self.scripts)
|
||||||
return
|
return
|
||||||
for s in self.scripts:
|
for s in self.scripts:
|
||||||
self.shell.exec_script(os.path.join(self.src, s))
|
self.shell.exec_script(os.path.join(self.src, s))
|
||||||
|
|
||||||
def _run_code(self) -> None:
|
def _run_code(self, snippet: str) -> None:
|
||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
print("will execute code:", self.code)
|
print("will execute script:", snippet)
|
||||||
return
|
return
|
||||||
self.shell.exec_snippet(self.code)
|
self.shell.exec_snippet(snippet)
|
||||||
|
|
||||||
def _run_commands(self) -> None:
|
|
||||||
if self.dry_run:
|
|
||||||
print("will execute:", self.commands)
|
|
||||||
return
|
|
||||||
for cmd in self.commands:
|
|
||||||
self.shell.exec_command(cmd)
|
|
||||||
|
|
||||||
|
|
||||||
class ContextManagerConfig(object):
|
class ContextManagerConfig(object):
|
||||||
def __init__(self, conf_file_path: str):
|
def __init__(self, conf_file_path: str, dry_run: bool):
|
||||||
try:
|
try:
|
||||||
with open(conf_file_path, 'rb') as f: self.config = tomllib.load(f)
|
with open(conf_file_path, 'rb') as f: self.config = tomllib.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -127,73 +147,69 @@ class ContextManagerConfig(object):
|
|||||||
def user_path(s: str) -> str:
|
def user_path(s: str) -> str:
|
||||||
return os.path.expanduser(s)
|
return os.path.expanduser(s)
|
||||||
|
|
||||||
if SECTION_GENERAL not in self.config:
|
if SECTION_GENERAL not in self.config or not isinstance(self.config[SECTION_GENERAL], dict):
|
||||||
raise ValueError(f'section "{SECTION_GENERAL}" not found in the config')
|
raise ValueError(f'section "{SECTION_GENERAL}" must be present in the config and must be a valid mapping (table)')
|
||||||
|
|
||||||
# Will extnsively check what's inside config.toml without additinal imports.
|
# Will extensively check what's inside config.toml without additinal imports.
|
||||||
# Also, fuck dynamic typing!
|
# Also, fuck dynamic typing!
|
||||||
self.tasks = self.config[SECTION_GENERAL].get(KEY_TASKS)
|
self.tasks = self.config[SECTION_GENERAL].get(KEY_APPS)
|
||||||
if not self.tasks or not is_arr_of_str(self.tasks):
|
if not self.tasks or not is_arr_of_str(self.tasks):
|
||||||
raise ValueError(f'invalid config: key "{KEY_TASKS}" in section "{SECTION_GENERAL}" must be a non-empty array of strings')
|
raise ValueError(f'invalid config: key "{KEY_APPS}" in section "{SECTION_GENERAL}" must be a non-empty array of strings')
|
||||||
|
|
||||||
if SECTION_TASK not in self.config: # valid case with all defaults
|
|
||||||
self.config[SECTION_TASK] = dict()
|
|
||||||
|
|
||||||
errors, err_count = list(), 0 # fuck it again
|
errors, err_count = list(), 0 # fuck it again
|
||||||
self.dry_run = self.config[SECTION_GENERAL].get(KEY_DRY_RUN) or False
|
self.dry_run = (self.config[SECTION_GENERAL].get(KEY_DRY_RUN) or False) or dry_run
|
||||||
if not isinstance(self.dry_run, bool):
|
if not isinstance(self.dry_run, bool):
|
||||||
err_count += 1
|
err_count += 1
|
||||||
errors.append(f'{err_count}. key "{KEY_DRY_RUN}" in section "{SECTION_GENERAL} of the config must be bool"')
|
errors.append(f'{err_count}. key "{KEY_DRY_RUN}" in section "{SECTION_GENERAL} of the config must be bool"')
|
||||||
|
|
||||||
self.home = user_path(self.config[SECTION_GENERAL].get(KEY_CONTEXTS_PATH) or os.path.join(XDG_CONFIG_HOME, 'userctx'))
|
self.home = user_path(self.config[SECTION_GENERAL].get(KEY_SRC_PATH) or os.path.join(XDG_CONFIG_HOME, 'userctx'))
|
||||||
if not os.path.isabs(self.home):
|
if not os.path.isabs(self.home):
|
||||||
err_count +=1
|
err_count += 1
|
||||||
errors.append(f'{err_count}. path "{self.home}" in key "{KEY_CONTEXTS_PATH}" of section "{SECTION_GENERAL}" must be absolute or relative to home: "~/{self.home}"')
|
errors.append(f'{err_count}. path "{self.home}" in key "{KEY_SRC_PATH}" of section "{SECTION_GENERAL}" must be absolute or relative to home: "~/{self.home}"')
|
||||||
|
|
||||||
self.managed_path = user_path(self.config[SECTION_GENERAL].get(KEY_MANAGED_DIR) or XDG_CONFIG_HOME)
|
self.managed_path = user_path(self.config[SECTION_GENERAL].get(KEY_DST_PATH) or XDG_CONFIG_HOME)
|
||||||
if not os.path.isabs(self.managed_path):
|
if not os.path.isabs(self.managed_path):
|
||||||
err_count +=1
|
err_count += 1
|
||||||
errors.append(f'{err_count}. path "{self.managed_path}" in key "{KEY_MANAGED_DIR}" of section "{SECTION_GENERAL}" must be absolute or relative to home: "~/{self.managed_path}"')
|
errors.append(f'{err_count}. path "{self.managed_path}" in key "{KEY_DST_PATH}" of section "{SECTION_GENERAL}" must be absolute or relative to home: "~/{self.managed_path}"')
|
||||||
|
|
||||||
|
if SECTION_APPS not in self.config: # valid case with all defaults
|
||||||
|
self.config[SECTION_APPS] = dict()
|
||||||
|
|
||||||
tasks = self.config.get(SECTION_TASK)
|
tasks = self.config.get(SECTION_APPS)
|
||||||
for name, t in tasks.items():
|
for name, t in tasks.items():
|
||||||
errstr = f'for task "{name}"'
|
errstr = f'for app "{name}"'
|
||||||
for k in list(t.keys()): # we might change t
|
for k in list(t.keys()): # we might change t
|
||||||
v = t[k]
|
v = t[k]
|
||||||
if k in [KEY_SRC_PATH, KEY_DST_PATH, KEY_CODE] and not isinstance(v, str):
|
if k not in APPS_VALID_KEYS:
|
||||||
err_count +=1
|
err_count += 1
|
||||||
|
errors.append(f'{err_count}. {errstr} unknown key "{k}"')
|
||||||
|
continue
|
||||||
|
if k == KEY_EXEC and not isinstance(v, str):
|
||||||
|
err_count += 1
|
||||||
errors.append(f'{err_count}. {errstr} key "{k}" must be a string')
|
errors.append(f'{err_count}. {errstr} key "{k}" must be a string')
|
||||||
continue
|
elif k == KEY_SYMLINK and not is_dict_of_str(v):
|
||||||
elif k in [KEY_COMMAND, KEY_SCRIPT, KEY_RELOAD, KEY_ACTIONS, KEY_SYMLINK] and not isinstance(v, list):
|
err_count += 1
|
||||||
err_count +=1
|
errors.append(f'{err_count}. {errstr} key "{k}" must be a table (mapping)')
|
||||||
errors.append(f'{err_count}. {errstr} key "{k}" must be an array')
|
elif k in [KEY_SCRIPT, KEY_ACTIONS] and not is_arr_of_str(v):
|
||||||
continue
|
err_count += 1
|
||||||
if k == KEY_COMMAND and not all(is_arr_of_str(elem) for elem in v):
|
errors.append(f'{err_count}. {errstr} key "{k}" must be array of strings')
|
||||||
err_count +=1
|
elif k == KEY_ACTIONS and not all(a in ACTIONS_ALL for a in v):
|
||||||
errors.append(f'{err_count}. {errstr} all elements of "{KEY_COMMAND}" must be arrays of strings')
|
err_count += 1
|
||||||
if k == KEY_SCRIPT and not is_arr_of_str(v):
|
errors.append(f'{err_count}. {errstr} invalid actions found: {[a for a in v if a not in ACTIONS_ALL]} in key "{k}", valid actions are: {ACTIONS_ALL}')
|
||||||
err_count +=1
|
elif k == KEY_RELOAD and not isinstance(v, str):
|
||||||
errors.append(f'{err_count}. {errstr} "{KEY_SCRIPT}" must be array of strings')
|
err_count += 1
|
||||||
if k == KEY_SYMLINK and not all(is_dict_of_str(elem) for elem in v):
|
errors.append(f'{err_count}. {errstr} "{KEY_RELOAD}" must ba a string')
|
||||||
err_count +=1
|
elif k in [KEY_SRC_PATH, KEY_DST_PATH] and not isinstance(v, str):
|
||||||
errors.append(f'{err_count}. {errstr} all elements of "{KEY_SYMLINK}" array must be mappings (inline tables) of strings')
|
err_count += 1
|
||||||
if k == KEY_ACTIONS and not is_arr_of_str(v):
|
errors.append(f'{err_count}. {errstr} key "{k}" must contain a string')
|
||||||
err_count +=1
|
elif k in [KEY_SRC_PATH, KEY_DST_PATH]:
|
||||||
errors.append(f'{err_count}. {errstr} "{KEY_ACTIONS}" must be array if strings')
|
|
||||||
if k == KEY_ACTIONS and not all(a in ACTIONS_ALL for a in v):
|
|
||||||
err_count +=1
|
|
||||||
errors.append(f'{err_count}. {errstr} found invalid actions: {[a for a in v if a not in ACTIONS_ALL]} in key "{KEY_ACTIONS}", valid actions are: {ACTIONS_ALL}')
|
|
||||||
if k == KEY_RELOAD and not is_arr_of_str(v):
|
|
||||||
err_count +=1
|
|
||||||
errors.append(f'{err_count}. {errstr} "{KEY_RELOAD}" must be array of strings')
|
|
||||||
if k in [KEY_SRC_PATH, KEY_DST_PATH]:
|
|
||||||
p = user_path(v)
|
p = user_path(v)
|
||||||
if not os.path.isabs(p):
|
if not os.path.isabs(p):
|
||||||
err_count +=1
|
err_count += 1
|
||||||
errors.append(f'{err_count}. {errstr} path "{v}" in key "{k}" must be either absolute or relative to home: "~/{v}"')
|
errors.append(f'{err_count}. {errstr} path "{v}" in key "{k}" must be either absolute or relative to home: "~/{v}"')
|
||||||
t[k] = p # substituting path (e.g. "~/somedir" -> "/home/user/somedir")
|
t[k] = p # substituting path (e.g. "~/somedir" -> "/home/user/somedir")
|
||||||
if errors:
|
if errors:
|
||||||
print(f'{err_count} errors found in the config ({conf_file_path}):')
|
print(f'{err_count} errors found in the config "{conf_file_path}":')
|
||||||
print('\n'.join(errors))
|
print('\n'.join(errors))
|
||||||
raise ValueError('invalid config')
|
raise ValueError('invalid config')
|
||||||
|
|
||||||
@@ -206,9 +222,9 @@ class ContextManagerConfig(object):
|
|||||||
raise ValueError(f'context directory "{context_name}" not found in "{self.home}"')
|
raise ValueError(f'context directory "{context_name}" not found in "{self.home}"')
|
||||||
out = list()
|
out = list()
|
||||||
for name in self.tasks:
|
for name in self.tasks:
|
||||||
task_path = os.path.join(ctx_path, name)
|
src_path = os.path.join(ctx_path, name)
|
||||||
target_path = os.path.join(self.managed_path, name)
|
dst_path = os.path.join(self.managed_path, name)
|
||||||
t = Task(name, task_path, target_path, self.config[SECTION_TASK].get(name) or dict(), dry_run=self.dry_run)
|
t = Task(name, src_path, dst_path, self.config[SECTION_APPS].get(name) or dict(), dry_run=self.dry_run)
|
||||||
out.append(t)
|
out.append(t)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -217,10 +233,10 @@ class ContextManager(object):
|
|||||||
def __init__(self, config: ContextManagerConfig):
|
def __init__(self, config: ContextManagerConfig):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
def print_contexts_list(self) -> str:
|
def print_contexts_list(self) -> None:
|
||||||
print('\n'.join(self.config.get_contexts_list()))
|
print('\n'.join(self.config.get_contexts_list()))
|
||||||
|
|
||||||
def apply_context(self, context_name: str) -> None:
|
def load_context(self, context_name: str) -> None:
|
||||||
print(f'loading and applying context: "{context_name}"')
|
print(f'loading and applying context: "{context_name}"')
|
||||||
try:
|
try:
|
||||||
tasks = self.config.get_tasks_for_context(context_name)
|
tasks = self.config.get_tasks_for_context(context_name)
|
||||||
@@ -250,6 +266,8 @@ class Runner(object):
|
|||||||
p = argparse.ArgumentParser(prog=APP_NAME, description=APP_DESC)
|
p = argparse.ArgumentParser(prog=APP_NAME, description=APP_DESC)
|
||||||
p.add_argument('-c', '--config', help='path to config file, default: %(default)s',
|
p.add_argument('-c', '--config', help='path to config file, default: %(default)s',
|
||||||
default=os.path.join(XDG_CONFIG_HOME, 'userctx', 'config.toml'))
|
default=os.path.join(XDG_CONFIG_HOME, 'userctx', 'config.toml'))
|
||||||
|
p.add_argument('-n', '--nop', action='store_true', default=False,
|
||||||
|
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)')
|
loadp = subp.add_parser(COMMAND_LOAD, help='load context (provide name of context)')
|
||||||
loadp.add_argument(COMMAND_CTX_NAME, help='name of context to load')
|
loadp.add_argument(COMMAND_CTX_NAME, help='name of context to load')
|
||||||
@@ -262,7 +280,7 @@ class Runner(object):
|
|||||||
print("error parsing command-line arguments:", e)
|
print("error parsing command-line arguments:", e)
|
||||||
return 1
|
return 1
|
||||||
try:
|
try:
|
||||||
config = ContextManagerConfig(args.config)
|
config = ContextManagerConfig(args.config, args.nop)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("error loading config:", e)
|
print("error loading config:", e)
|
||||||
return 2
|
return 2
|
||||||
@@ -271,7 +289,7 @@ class Runner(object):
|
|||||||
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_LOAD:
|
||||||
ctxmgr.apply_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:
|
elif args.command == COMMAND_SAVE:
|
||||||
raise Exception('"save" not implemented')
|
raise Exception('"save" not implemented')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user