Compare commits

...

3 Commits

Author SHA1 Message Date
3a5f070efa
redesigned logic, UX, etc. v2 2024-09-15 23:24:06 +03:00
a2724ef837
make func call async 2024-09-09 00:22:41 +03:00
15bda21067
rm nowplaying from vc 2024-09-08 23:01:52 +03:00
3 changed files with 254 additions and 184 deletions

339
dotman.py
View File

@ -1,9 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# #
# Ferit Yiğit BALABAN <fyb@fybx.dev>, 2024 # Yiğid BALABAN <fyb@fybx.dev>, 2024
# #
# Description # dotman
#
# [S] Description
# dotman is a simple dotfiles manager that can be used to backup and deploy dotfiles. # dotman is a simple dotfiles manager that can be used to backup and deploy dotfiles.
# #
# It manages a git repository to deploy a list of files and directories to # It manages a git repository to deploy a list of files and directories to
@ -19,177 +21,207 @@
# a tag name. The tag is created after the files and directories are copied, # a tag name. The tag is created after the files and directories are copied,
# essentially creating a snapshot of the configuration at that time. # essentially creating a snapshot of the configuration at that time.
# Details # [S] Details
# * The configuration file for dotman is searched in $HOME/dotman/config # * The configuration file for dotman is locatd in $HOME/.config/dotman/config
# * The repository managed by dotman is searched in $HOME/dotman/managed_repo # * The deploy list for selecting what and what not $HOME/.config/dotman/deploy_list
# * The deploy list for selecting what and what not # to backup/deploy is searched in
# to backup/deploy is searched in $HOME/dotman/deploy_list # * The repository managed by dotman is located in $HOME/.local/state/dotman/managed_repo
import os import os
import shutil import shutil
import tomllib
import sys import sys
from git.repo import Repo from git.repo import Repo
from crispy.crispy import Crispy from crispy.crispy import Crispy
from tomlkit import dumps as toml_dumps
from tomlkit import parse as toml_parse
from tomlkit.items import String as toml_String
from tomlkit.items import Item as toml_Item
from os.path import join, relpath
VER = 'v2'
VER = 'v1.8'
help_message = f''' help_message = f'''
dotman {VER} dotfiles manager by ferityigitbalaban dotman {VER} dotfiles/home backup helper by yigid balaban
Unrecognized keys are ignored. If every key supplied is unrecognized, Commands:
this have the same effect as calling dotman without any key. => init Initialize dotman installation.
-u, --url [required] the URL of remote
-l, --local [optional] create repository locally
-d, --deploy [optional] deploy configuration
Keys: => backup Backups configuration to managed_repo (pushes to remote)
-b, --backup Backup your dotfiles. Doesn't require user assistance but errors may occur. => deploy Deploys configuration in place from managed_repo (pulls remote)
-d, --deploy Deploy your dotfiles. Doesn't require user assistance but errors may occur. => help Prints this message.
-v, --version Shows the version and quits => version Prints the version.
-h, --help Shows this message and quits
''' '''
INITIALIZED = False
dir_home = os.path.expandvars('$HOME') dir_home = os.path.expandvars('$HOME')
dir_config = os.path.join(dir_home, '.config') dir_config = join(dir_home, '.config')
dir_state = join(dir_home, '.local', 'state')
params = {} params = {}
list_ignore = []
list_deploy = []
def util_get_all_files(directory: str) -> list[str]: def u_path(mode, path):
if mode == 'deploy':
return join(params['managed_repo'], relpath(path, dir_home))
elif mode == 'backup':
return join(params['managed_repo'], relpath(path, dir_home))
else:
raise ValueError(mode)
def u_get_files(directory: str) -> list[str]:
if not os.path.exists(directory): if not os.path.exists(directory):
return [] return []
files = [] files = []
for root, _, filenames in os.walk(directory): for root, _, filenames in os.walk(directory):
files.extend(os.path.join(root, filename) for filename in filenames) files.extend(join(root, filename) for filename in filenames)
return files return files
def util_errout(msg: str, code: int): def t_init():
print(msg) global INITIALIZED
sys.exit(code) if INITIALIZED:
return
INITIALIZED = True
def task_init():
global params global params
params = { params = {
'managed_repo': f'{dir_config}/dotman/managed_repo', 'managed_repo': f'{dir_state}/dotman/managed_repo',
'deploy_list': f'{dir_config}/dotman/deploy_list', 'deploy_list': f'{dir_config}/dotman/deploy_list',
'config_file': f'{dir_config}/dotman/config', 'config_file': f'{dir_config}/dotman/config',
'repo_url': '', 'repo_url': '',
} }
if os.path.exists(params['config_file']):
def task_config(): with open(params['config_file'], 'r') as f:
"""Reads and parses the configuration file data = toml_parse(f.read())
""" params.update({k: str(v) if isinstance(v, (toml_String, toml_Item)) else v for k, v in data.items()})
with open(params['config_file'], 'rb') as f:
conf = tomllib.load(f)
if 'repo_url' not in conf.keys():
util_errout(f'[ERR] expected "repo_url" in {params["config_file"]}', 1)
params['repo_url'] = conf['repo_url']
def task_repo():
is_repo = os.path.exists(f"{params['managed_repo']}/.git")
if not is_repo:
Repo.clone_from(params['repo_url'], params['managed_repo'])
else: else:
repo = Repo(params['managed_repo']) with open(params['config_file'], 'w') as f:
repo.remotes.origin.pull() f.write(toml_dumps(params))
def task_list(): is_local_repo_created = lambda: os.path.exists(f"{params['managed_repo']}/.git")
with open(params['deploy_list'], 'r') as f:
def t_make_repo(from_url: str, local = False, check = True):
"""
Create the local repository either by cloning from a remote, or by initializing it.
:param str from_url: URL of the remote
:param bool local: Whether to create locally (default = False)
:param bool check: Whether to check if local repository exists (default = True)
"""
if check and is_local_repo_created():
print('[W] dotman: a managed repository was initialized. overriding contents')
if local:
r = Repo.init(params['managed_repo'])
r.create_remote('origin', url=from_url)
r.git.checkout('-b', 'main')
return
print(f'[I] dotman: cloning from remote {params['repo_url']}')
Repo.clone_from(from_url, params['managed_repo'])
def t_pull_repo(overwrite: bool):
try:
# clone the repo from remote if local doesn't exist
# or if we are allowed to overwrite existing local
p_local_exists = is_local_repo_created()
if not p_local_exists or overwrite:
if p_local_exists:
shutil.rmtree(params['managed_repo'])
t_make_repo(params['repo_url'], check=False)
else:
# repo exists and it's forbidden to overwrite
repo = Repo(params['managed_repo'])
repo.remotes.origin.pull()
except Exception as e:
print(f'[E] dotman: unhandled error in \'t_pull_repo\': {e}')
return False
return True
def t_set_params(param_key: str, param_value: str):
params[param_key] = param_value
with open(params['config_file'], 'w') as f:
f.write(toml_dumps(params))
def t_list(p_list: str) -> list[str]:
l_i, l_d = [], []
with open(p_list, 'r') as f:
lines = f.readlines() lines = f.readlines()
lines = [l.strip() for l in lines] lines = [l.strip() for l in lines]
lines = [l for l in lines if l] lines = [l for l in lines if l]
for line in lines: for line in lines:
ignore_it = False ignore_it = False
if line.startswith('#'): if line.startswith('#'):
continue continue
if line.startswith('!'): if line.startswith('!'):
ignore_it = True ignore_it = True
line = line.removeprefix('!') line = line.removeprefix('!')
if line.startswith('%'): elif '.git' in line:
line = os.path.join(dir_config, line.replace('%', '')) ignore_it = True
else:
line = os.path.join(dir_home, line)
line = join(dir_config, line[1:]) if line.startswith('%') else join(dir_home, line)
if os.path.isfile(line): if os.path.isfile(line):
if ignore_it: list_ignore.append(line) if ignore_it: l_i.append(line)
else: list_deploy.append(line) else: l_d.append(line)
else: else:
if ignore_it: list_ignore.extend(util_get_all_files(line)) if ignore_it: l_i.extend(u_get_files(line))
else: list_deploy.extend(util_get_all_files(line)) else: l_d.extend(u_get_files(line))
for element in list_ignore: for element in l_i:
if element in list_deploy: if element in l_d:
list_deploy.remove(element) l_d.remove(element)
with open(f'{dir_home}/dotman.log', 'w') as f: with open(join(dir_state, 'dotman', 'dotman.log'), 'w') as f:
f.writelines(map(lambda x: x + '\n', list_deploy)) f.writelines(map(lambda x: x + '\n', l_d))
return l_d
def backup(tag=''): def a_backup():
"""Copies files and directories denoted in deploy_list from their source to l_deploy = t_list(params['deploy_list'])
managed_repo directory.
Args: if len(l_deploy) == 0:
tag (str, optional): Git tag to publish for the commit. Defaults to ''. print('[W] dotman: deploy_list is not created or empty. nothing will be backed up.')
""" return False
for file in list_deploy:
file_in_repo = util_path('backup', file) for file in l_deploy:
print(file_in_repo) file_in_repo = u_path('backup', file)
os.makedirs(os.path.dirname(file_in_repo), exist_ok=True) os.makedirs(os.path.dirname(file_in_repo), exist_ok=True)
shutil.copy(file, file_in_repo) shutil.copy(file, file_in_repo)
repo = Repo(params['managed_repo']) repo = Repo(params['managed_repo'])
repo.git.add(all=True) repo.git.add(all=True)
repo.git.commit('-m', 'committed by dotman')
repo.remotes.origin.push()
if tag != '': if repo.index.diff(None) or repo.untracked_files:
if tag in map(lambda x: x.replace('refs/tags/', ''), repo.tags): repo.git.commit('-m', 'committed by dotman')
return repo.remotes.origin.push('main')
created_tag = repo.create_tag(tag) return True
repo.remotes.origin.push(created_tag.name)
def deploy(tag=''): def a_deploy(use_deploy_list_in_managed_repo = False):
"""Copies files and directories in managed Git repository to l_deploy = t_list(os.path.join(params['managed_repo'], 'dotman', 'deploy_list')) if use_deploy_list_in_managed_repo else t_list(params['deploy_list'])
local .config directory, if they are present in the deploy list.
Optinally, a tag can be specified to deploy a specific configuration. if len(l_deploy) == 0:
print('[W] dotman: deploy_list is not created or empty. nothing will be deployed.')
return False
Args: for file in l_deploy:
tag (str, optional): Git tag for a specific configuration. Defaults to ''. file_in_repo = u_path('deploy', file)
"""
if tag != '':
repo = Repo(params['managed_repo'])
repo.git.checkout(tag)
task_list()
for file in list_deploy:
file_in_repo = util_path('deploy', file)
os.makedirs(os.path.dirname(file), exist_ok=True) os.makedirs(os.path.dirname(file), exist_ok=True)
shutil.copy(file_in_repo, file) shutil.copy(file_in_repo, file)
return True
def util_path(mode, path):
if mode == 'deploy':
return os.path.join(params['managed_repo'], os.path.relpath(path, dir_home))
elif mode == 'backup':
return os.path.join(params['managed_repo'], os.path.relpath(path, dir_home))
else:
raise ValueError(mode)
def main(): def main():
@ -197,45 +229,80 @@ def main():
print(help_message) print(help_message)
sys.exit(0) sys.exit(0)
task_init() t_init()
task_config()
task_repo()
task_list()
c = Crispy() c = Crispy()
c.add_variable('backup', bool) c.add_subcommand('init', 'Initialize dotman for use')
c.add_subcommand('config', 'Configure dotman')
c.add_variable('url', str)
c.add_variable('local', bool)
c.add_variable('deploy', bool) c.add_variable('deploy', bool)
c.add_subcommand('deploy', 'Deploy a configuration to place')
c.add_subcommand('backup', 'Backup current state following a deploy list')
c.add_variable('tag', str) c.add_variable('tag', str)
args = c.parse_arguments(sys.argv[1:])[1] subcommand, args = c.parse_arguments(sys.argv[1:])
if args['backup'] and args['deploy']: match subcommand:
util_errout('[ERR] can\'t do both, sorry :(', 11) case 'init':
elif args['backup']: if args['url'] and type(args['url']) == str:
backup(args['tag'] if 'tag' in args.keys() else '') t_set_params('repo_url', str(args['url']))
elif args['deploy']: if not args['local']:
deploy(args['tag']) s_pull = t_pull_repo(overwrite=True)
if args['deploy'] and s_pull:
a_deploy(True)
else:
t_make_repo(args['url'], True)
case 'config':
if args['url']: t_set_params('repo_url', str(args['url']))
case 'deploy':
if t_pull_repo(False):
a_deploy()
case 'backup':
a_backup()
case 'help':
print(help_message)
case 'version':
print(VER)
case _:
print(help_message)
if __name__ == '__main__': if __name__ == '__main__':
main() main()
# Tasks
# 1. expand dir_config
# 2. read configuration file
# 3. check managed_repo status
# 1. create if necessary
# 2. or pull changes
# 4. read deploy_list
# command is deploy (tag?) # def backup(tag=''):
# 1. if tag is specified, checkout to that tag # """Copies files and directories denoted in deploy_list from their source to
# 2. copy files and directories in deploy_list (ask for using the same deploy_list) to local .config directory # managed_repo directory.
#
# Args:
# tag (str, optional): Git tag to publish for the commit. Defaults to ''.
# """
# if tag != '':
# if tag in map(lambda x: x.replace('refs/tags/', ''), repo.tags):
# return
# created_tag = repo.create_tag(tag)
# repo.remotes.origin.push(created_tag.name)
# def deploy(tag=''):
# """Copies files and directories in managed Git repository to
# local .config directory, if they are present in the deploy list.
#
# Optinally, a tag can be specified to deploy a specific configuration.
#
# Args:
# tag (str, optional): Git tag for a specific configuration. Defaults to ''.
# """
# if tag != '':
# repo = Repo(params['managed_repo'])
# repo.git.checkout(tag)
# t_list()
#
# for file in list_deploy:
# file_in_repo = util_path('deploy', file)
# os.makedirs(os.path.dirname(file), exist_ok=True)
# shutil.copy(file_in_repo, file)
# command is backup (tag?)
# 1. copy using deploy_list from sources to repository directory
# 2. create a new commit and push
# 3. if tag is specified, check if tag exists
# 1. if tag does not exist, create tag and push
# 2. if tag exists, warn and exit

Binary file not shown.

View File

@ -1,42 +1,45 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# Yigid BALABAN, <fyb@fybx.dev> # Yigid BALABAN, <fyb@fybx.dev>
# wireless.sh
# #
# description
# toggle wifi and bluetooth on or off quickly
control() { control() {
local device="$1" local device="$1"
local subcommand="$2" local subcommand="$2"
case "$subcommand" in case "$subcommand" in
off) off)
rfkill block "$device" && rfkill block "$device" &&
case $device in
bluetooth) bluetoothctl power off ;;
wifi) nmcli radio wifi off ;;
esac
;;
on)
case $device in rfkill unblock "$device" && sleep 1 &&
bluetooth) bluetoothctl power off ;; case $device in
wifi) nmcli radio wifi off ;; bluetooth) bluetoothctl power on ;;
esac wifi) nmcli radio wifi on ;;
;; esac
on) ;;
*)
rfkill unblock "$device" && sleep 1 && # shellcheck disable=SC2154
echo "$command: subcommand '$subcommand' is not a valid argument." >&2
case $device in return 1
bluetooth) bluetoothctl power on ;; ;;
wifi) nmcli radio wifi on ;; esac
esac
;;
*)
# shellcheck disable=SC2154
echo "$command: subcommand '$subcommand' is not a valid argument." >&2
return 1
esac
} }
if [[ $# -ne 2 ]]; then if [[ $# -ne 2 ]]; then
echo "Usage: $0 <device> <subcommand>" >&2 echo "Usage: $0 <device> <subcommand>" >&2
echo "Valid devices: bluetooth, wifi" >&2 echo "Valid devices: bluetooth, wifi" >&2
echo "Valid subcommands: on, off" >&2 echo "Valid subcommands: on, off" >&2
exit 1 exit 1
fi fi
control "$1" "$2" control "$1" "$2" &
disown