Compare commits

..

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

3 changed files with 182 additions and 252 deletions

373
dotman.py
View File

@ -1,308 +1,241 @@
#!/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
# the local .config directory. When the backup command is executed, it # the local .config directory. When the backup command is executed, it
# copies the files and directories in the deploy_list to the # copies the files and directories in the deploy_list to the
# local repository. git is used to keep track of changes. # local repository. git is used to keep track of changes.
# #
# Deploying a configuration is possible by either directly calling it, or # Deploying a configuration is possible by either directly calling it, or
# by specifying a git tag. The tag is used to checkout the repository to a # by specifying a git tag. The tag is used to checkout the repository to a
# specific commit, and then deploy the configuration on that time. # specific commit, and then deploy the configuration on that time.
# #
# Similar to deploying, backing up a configuration is possible by specifying # Similar to deploying, backing up a configuration is possible by specifying
# 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 Keys:
-d, --deploy [optional] deploy configuration -b, --backup Backup your dotfiles. Doesn't require user assistance but errors may occur.
-d, --deploy Deploy your dotfiles. Doesn't require user assistance but errors may occur.
=> backup Backups configuration to managed_repo (pushes to remote) -v, --version Shows the version and quits
=> deploy Deploys configuration in place from managed_repo (pulls remote) -h, --help Shows this message and quits
=> help Prints this message.
=> version Prints the version.
''' '''
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('%', ''))
line = join(dir_config, line[1:]) if line.startswith('%') else join(dir_home, line)
if os.path.isfile(line):
if ignore_it: l_i.append(line)
else: l_d.append(line)
else: else:
if ignore_it: l_i.extend(u_get_files(line)) line = os.path.join(dir_home, line)
else: l_d.extend(u_get_files(line))
for element in l_i: if os.path.isfile(line):
if element in l_d: if ignore_it: list_ignore.append(line)
l_d.remove(element) else: list_deploy.append(line)
else:
with open(join(dir_state, 'dotman', 'dotman.log'), 'w') as f: if ignore_it: list_ignore.extend(util_get_all_files(line))
f.writelines(map(lambda x: x + '\n', l_d)) else: list_deploy.extend(util_get_all_files(line))
return l_d
for element in list_ignore:
if element in list_deploy:
list_deploy.remove(element)
with open(f'{dir_home}/dotman.log', 'w') as f:
f.writelines(map(lambda x: x + '\n', list_deploy))
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')
if repo.index.diff(None) or repo.untracked_files: repo.remotes.origin.push()
repo.git.commit('-m', 'committed by dotman')
repo.remotes.origin.push('main') if tag != '':
return True 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 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.
Optinally, a tag can be specified to deploy a specific configuration.
if len(l_deploy) == 0: Args:
print('[W] dotman: deploy_list is not created or empty. nothing will be deployed.') tag (str, optional): Git tag for a specific configuration. Defaults to ''.
return False """
if tag != '':
for file in l_deploy: repo = Repo(params['managed_repo'])
file_in_repo = u_path('deploy', file) 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():
if len(sys.argv) == 1: if len(sys.argv) == 1:
print(help_message) print(help_message)
sys.exit(0) sys.exit(0)
t_init() task_init()
task_config()
task_repo()
task_list()
c = Crispy() c = Crispy()
c.add_subcommand('init', 'Initialize dotman for use') c.add_variable('backup', bool)
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']:
util_errout('[ERR] can\'t do both, sorry :(', 11)
match subcommand: elif args['backup']:
case 'init': backup(args['tag'] if 'tag' in args.keys() else '')
if args['url'] and type(args['url']) == str: elif args['deploy']:
t_set_params('repo_url', str(args['url'])) deploy(args['tag'])
if not args['local']:
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
# 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