188 lines
6.4 KiB
Python
Executable file
188 lines
6.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
from functools import partial
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
import mako.lookup
|
|
import mako.template
|
|
import requests
|
|
import toml
|
|
import yaml
|
|
|
|
BASE16_TEMPLATES_URL = 'https://raw.githubusercontent.com/chriskempson/base16-templates-source/master/list.yaml'
|
|
BASE16_TEMPLATES = yaml.safe_load(requests.get(BASE16_TEMPLATES_URL).text)
|
|
|
|
# Pending https://github.com/chriskempson/base16-templates-source/pull/106
|
|
BASE16_TEMPLATES['wofi-colors'] = 'https://github.com/agausmann/base16-wofi-colors'
|
|
|
|
|
|
def get_base16(scheme, app, template='default'):
|
|
base_url = BASE16_TEMPLATES[app]
|
|
if 'github.com' in base_url:
|
|
base_url = base_url.replace('github.com', 'raw.githubusercontent.com') + '/master/'
|
|
else:
|
|
base_url += '/blob/master/'
|
|
config = yaml.safe_load(requests.get(base_url + 'templates/config.yaml').text)
|
|
output = config[template]['output']
|
|
extension = config[template]['extension']
|
|
return requests.get(base_url + output + '/base16-' + scheme + extension).text
|
|
|
|
|
|
def is_outdated(src: List[Path], dst: Path) -> bool:
|
|
if not dst.exists():
|
|
return True
|
|
|
|
dst_modified = dst.stat().st_mtime
|
|
return any(
|
|
a_src.stat().st_mtime > dst_modified
|
|
for a_src in src
|
|
if a_src.exists()
|
|
)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Generates and installs dotfiles for this host.',
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
'-d', '--dotfiles',
|
|
help='The base directory of the dotfiles repository.',
|
|
type=Path,
|
|
default=Path(sys.argv[0]).parent,
|
|
)
|
|
parser.add_argument(
|
|
'-n', '--hostname',
|
|
help='The hostname or other identifying name of this system that will'
|
|
' be used to retrieve the host-specific configuration.',
|
|
default=os.environ.get('HOSTNAME') or socket.gethostname(),
|
|
)
|
|
parser.add_argument(
|
|
'-o', '--home',
|
|
help='The home directory where generated dotfiles will be installed.',
|
|
type=Path,
|
|
default=os.environ.get('HOME') or Path.home(),
|
|
)
|
|
parser.add_argument(
|
|
'-f', '--force',
|
|
help='Force overwrite all files even if they are not considered outdated.',
|
|
action='store_true'
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
raw_dir = args.dotfiles / 'raw'
|
|
templates_dir = args.dotfiles / 'templates'
|
|
include_dir = args.dotfiles / 'include'
|
|
default_host_filename = args.dotfiles / 'hosts' / 'default.toml'
|
|
host_filename = args.dotfiles / 'hosts' / '{}.toml'.format(args.hostname)
|
|
|
|
with open(default_host_filename) as host_file:
|
|
host_config = toml.load(host_file)
|
|
|
|
if host_filename.exists():
|
|
with open(host_filename) as host_file:
|
|
host_config.update(toml.load(host_file))
|
|
|
|
host_config['name'] = args.hostname
|
|
|
|
# Preprocess output configs for sway
|
|
for input in host_config['inputs']:
|
|
# Generate config lines for sway template
|
|
lines = []
|
|
for key in input:
|
|
if key == 'match':
|
|
continue
|
|
if isinstance(input[key], list):
|
|
val = ' '.join(repr(elem) for elem in input[key])
|
|
else:
|
|
val = repr(input[key])
|
|
lines.append(f'{key} {val}')
|
|
|
|
input['sway-lines'] = lines
|
|
|
|
for output in host_config['outputs']:
|
|
# Generate config lines for sway template
|
|
lines = []
|
|
for key in output:
|
|
if key == 'match':
|
|
continue
|
|
if isinstance(output[key], list):
|
|
val = ' '.join(repr(elem) for elem in output[key])
|
|
else:
|
|
val = repr(output[key])
|
|
lines.append(f'{key} {val}')
|
|
|
|
output['sway-lines'] = lines
|
|
|
|
# Attempt to resolve device names for swaylock template
|
|
# (Workaround https://github.com/swaywm/swaylock/issues/114)
|
|
#
|
|
# This will only work if this is run on the target host
|
|
# and if sway is running, but that is usually the case...
|
|
if output['match'] != '*':
|
|
try:
|
|
get_outputs = subprocess.check_output(
|
|
['swaymsg', '-t', 'get_outputs', '-p'],
|
|
).decode('utf-8')
|
|
for line in get_outputs.splitlines():
|
|
# Line format: Output <device> '<match identifier>'
|
|
if line.startswith('Output') and output['match'] in line:
|
|
output['device'] = line.split()[1]
|
|
break
|
|
except subprocess.CalledProcessError:
|
|
print('Could not contact sway to retrieve output names.')
|
|
print('Please re-run in sway to finish configuring swaylock.')
|
|
|
|
lookup = mako.lookup.TemplateLookup(
|
|
directories=[
|
|
str(templates_dir),
|
|
str(include_dir),
|
|
],
|
|
)
|
|
|
|
for raw_path in raw_dir.glob('**/*'):
|
|
if not raw_path.is_file():
|
|
continue
|
|
rel_path = raw_path.relative_to(raw_dir)
|
|
output_path = args.home / rel_path
|
|
|
|
if args.force or is_outdated([raw_path], output_path):
|
|
print(rel_path)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy(raw_path, output_path)
|
|
|
|
for template_path in templates_dir.glob('**/*'):
|
|
if not template_path.is_file():
|
|
continue
|
|
rel_path = template_path.relative_to(templates_dir)
|
|
output_path = args.home / template_path.relative_to(templates_dir)
|
|
|
|
if args.force or is_outdated([template_path, host_filename], output_path):
|
|
print(rel_path)
|
|
template = mako.template.Template(
|
|
filename=str(template_path),
|
|
strict_undefined=True,
|
|
lookup=lookup,
|
|
)
|
|
output = template.render(
|
|
host=host_config,
|
|
home=args.home,
|
|
get_base16=partial(get_base16, host_config.get('base16-scheme', 'default-dark')),
|
|
)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(output_path, 'w+') as output_file:
|
|
output_file.write(output)
|
|
|
|
# Copy permissions from original file
|
|
output_path.chmod(template_path.stat().st_mode & 0o777)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|