Compare commits

..

No commits in common. "3a5f070efadb9f8203bcd16c5f0feed66227956d" and "2559fb1eb2116864274598b256794cb6fd9d6c63" have entirely different histories.

3 changed files with 182 additions and 252 deletions

341
dotman.py
View File

@ -1,11 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# #
# Yiğid BALABAN <fyb@fybx.dev>, 2024 # Ferit Yiğit BALABAN <fyb@fybx.dev>, 2024
# #
# dotman # Description
#
# [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
@ -21,207 +19,177 @@
# 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.
# [S] Details # Details
# * The configuration file for dotman is locatd in $HOME/.config/dotman/config # * The configuration file for dotman is searched in $HOME/dotman/config
# * The deploy list for selecting what and what not $HOME/.config/dotman/deploy_list # * The repository managed by dotman is searched in $HOME/dotman/managed_repo
# to backup/deploy is searched in # * The deploy list for selecting what and what not
# * The repository managed by dotman is located in $HOME/.local/state/dotman/managed_repo # to backup/deploy is searched in $HOME/dotman/deploy_list
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/home backup helper by yigid balaban dotman {VER} dotfiles manager by ferityigitbalaban
Commands: Unrecognized keys are ignored. If every key supplied is unrecognized,
=> init Initialize dotman installation. this have the same effect as calling dotman without any key.
-u, --url [required] the URL of remote
-l, --local [optional] create repository locally
-d, --deploy [optional] deploy configuration
=> backup Backups configuration to managed_repo (pushes to remote) Keys:
=> deploy Deploys configuration in place from managed_repo (pulls remote) -b, --backup Backup your dotfiles. Doesn't require user assistance but errors may occur.
=> help Prints this message. -d, --deploy Deploy your dotfiles. Doesn't require user assistance but errors may occur.
=> version Prints the version. -v, --version Shows the version and quits
-h, --help Shows this message and quits
''' '''
INITIALIZED = False
dir_home = os.path.expandvars('$HOME') dir_home = os.path.expandvars('$HOME')
dir_config = join(dir_home, '.config') dir_config = os.path.join(dir_home, '.config')
dir_state = join(dir_home, '.local', 'state')
params = {} params = {}
list_ignore = []
list_deploy = []
def u_path(mode, path): def util_get_all_files(directory: str) -> list[str]:
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(join(root, filename) for filename in filenames) files.extend(os.path.join(root, filename) for filename in filenames)
return files return files
def t_init(): def util_errout(msg: str, code: int):
global INITIALIZED print(msg)
if INITIALIZED: sys.exit(code)
return
INITIALIZED = True
def task_init():
global params global params
params = { params = {
'managed_repo': f'{dir_state}/dotman/managed_repo', 'managed_repo': f'{dir_config}/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']):
with open(params['config_file'], 'r') as f: def task_config():
data = toml_parse(f.read()) """Reads and parses the configuration file
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:
with open(params['config_file'], 'w') as f: repo = Repo(params['managed_repo'])
f.write(toml_dumps(params)) repo.remotes.origin.pull()
is_local_repo_created = lambda: os.path.exists(f"{params['managed_repo']}/.git") def task_list():
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('!')
elif '.git' in line: if line.startswith('%'):
ignore_it = True line = os.path.join(dir_config, line.replace('%', ''))
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: l_i.append(line) if ignore_it: list_ignore.append(line)
else: l_d.append(line) else: list_deploy.append(line)
else: else:
if ignore_it: l_i.extend(u_get_files(line)) if ignore_it: list_ignore.extend(util_get_all_files(line))
else: l_d.extend(u_get_files(line)) else: list_deploy.extend(util_get_all_files(line))
for element in l_i: for element in list_ignore:
if element in l_d: if element in list_deploy:
l_d.remove(element) list_deploy.remove(element)
with open(join(dir_state, 'dotman', 'dotman.log'), 'w') as f: with open(f'{dir_home}/dotman.log', 'w') as f:
f.writelines(map(lambda x: x + '\n', l_d)) f.writelines(map(lambda x: x + '\n', list_deploy))
return l_d
def a_backup(): def backup(tag=''):
l_deploy = t_list(params['deploy_list']) """Copies files and directories denoted in deploy_list from their source to
managed_repo directory.
if len(l_deploy) == 0: Args:
print('[W] dotman: deploy_list is not created or empty. nothing will be backed up.') tag (str, optional): Git tag to publish for the commit. Defaults to ''.
return False """
for file in list_deploy:
for file in l_deploy: file_in_repo = util_path('backup', file)
file_in_repo = u_path('backup', file) print(file_in_repo)
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 repo.index.diff(None) or repo.untracked_files: if tag != '':
repo.git.commit('-m', 'committed by dotman') if tag in map(lambda x: x.replace('refs/tags/', ''), repo.tags):
repo.remotes.origin.push('main') return
return True created_tag = repo.create_tag(tag)
repo.remotes.origin.push(created_tag.name)
def a_deploy(use_deploy_list_in_managed_repo = False): def deploy(tag=''):
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']) """Copies files and directories in managed Git repository to
local .config directory, if they are present in the deploy list.
if len(l_deploy) == 0: Optinally, a tag can be specified to deploy a specific configuration.
print('[W] dotman: deploy_list is not created or empty. nothing will be deployed.')
return False
for file in l_deploy: Args:
file_in_repo = u_path('deploy', file) tag (str, optional): Git tag for a specific configuration. Defaults to ''.
"""
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():
@ -229,80 +197,45 @@ def main():
print(help_message) print(help_message)
sys.exit(0) sys.exit(0)
t_init() task_init()
c = Crispy() task_config()
c.add_subcommand('init', 'Initialize dotman for use') task_repo()
c.add_subcommand('config', 'Configure dotman') task_list()
c.add_variable('url', str)
c.add_variable('local', bool)
c.add_variable('deploy', bool)
c.add_subcommand('deploy', 'Deploy a configuration to place') c = Crispy()
c.add_subcommand('backup', 'Backup current state following a deploy list') c.add_variable('backup', bool)
c.add_variable('deploy', bool)
c.add_variable('tag', str) c.add_variable('tag', str)
subcommand, args = c.parse_arguments(sys.argv[1:]) args = c.parse_arguments(sys.argv[1:])[1]
match subcommand: if args['backup'] and args['deploy']:
case 'init': util_errout('[ERR] can\'t do both, sorry :(', 11)
if args['url'] and type(args['url']) == str: elif args['backup']:
t_set_params('repo_url', str(args['url'])) backup(args['tag'] if 'tag' in args.keys() else '')
if not args['local']: elif args['deploy']:
s_pull = t_pull_repo(overwrite=True) deploy(args['tag'])
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
# def backup(tag=''): # command is deploy (tag?)
# """Copies files and directories denoted in deploy_list from their source to # 1. if tag is specified, checkout to that tag
# managed_repo directory. # 2. copy files and directories in deploy_list (ask for using the same deploy_list) to local .config 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

BIN
nowplaying Executable file

Binary file not shown.

View File

@ -1,45 +1,42 @@
#!/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)
rfkill unblock "$device" && sleep 1 && case $device in
case $device in bluetooth) bluetoothctl power off ;;
bluetooth) bluetoothctl power on ;; wifi) nmcli radio wifi off ;;
wifi) nmcli radio wifi on ;; esac
esac ;;
;; on)
*)
# shellcheck disable=SC2154 rfkill unblock "$device" && sleep 1 &&
echo "$command: subcommand '$subcommand' is not a valid argument." >&2
return 1 case $device in
;; bluetooth) bluetoothctl power on ;;
esac wifi) nmcli radio wifi on ;;
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