159 lines
6.9 KiB
Python
159 lines
6.9 KiB
Python
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
import tomllib
|
|
|
|
|
|
def is_flashbang(hook):
|
|
return any(re.search(r'(?:[/\\\s"\x27]|^)flashbang\.(?:ps1|sh)(?:[\s"\x27]|$)', str(hook.get(key, '')), re.I)
|
|
for key in ('command', 'command_windows'))
|
|
|
|
|
|
def filter_options(path, flashbang_enabled=True, statusline_enabled=True):
|
|
content = path.read_text(encoding='utf-8-sig')
|
|
if path.name == 'settings.json':
|
|
settings = json.loads(content)
|
|
if not flashbang_enabled:
|
|
groups = settings.get('hooks', {}).get('Stop', [])
|
|
remaining = []
|
|
for group in groups:
|
|
hooks = [hook for hook in group.get('hooks', []) if not is_flashbang(hook)]
|
|
if hooks:
|
|
remaining.append(dict(group, hooks=hooks))
|
|
if remaining:
|
|
settings['hooks']['Stop'] = remaining
|
|
elif 'Stop' in settings.get('hooks', {}):
|
|
del settings['hooks']['Stop']
|
|
if not statusline_enabled:
|
|
settings.pop('statusLine', None)
|
|
return json.dumps(settings, indent=2, ensure_ascii=False) + '\n'
|
|
if path.name != 'config.toml':
|
|
raise ValueError('Install option filtering only supports client configuration files.')
|
|
tomllib.loads(content)
|
|
result = content
|
|
if not flashbang_enabled:
|
|
sections = re.split(r'(?m)(?=^\s*\[\[?[^\r\n]+\]\]?\s*$)', result)
|
|
filtered = []
|
|
for section in sections:
|
|
if re.match(r'\s*\[\[hooks\.Stop\.hooks\]\]', section):
|
|
hook = tomllib.loads(section)['hooks']['Stop']['hooks'][0]
|
|
if is_flashbang(hook):
|
|
continue
|
|
filtered.append(section)
|
|
result = ''.join(filtered)
|
|
result = re.sub(r'(?m)^\[\[hooks\.Stop\]\][ \t]*\r?\n\s*(?=\[|\Z)(?!\[\[hooks\.Stop\.hooks\]\])', '', result)
|
|
if not statusline_enabled:
|
|
sections = re.split(r'(?m)(?=^\s*\[\[?[^\r\n]+\]\]?\s*$)', result)
|
|
for index, section in enumerate(sections):
|
|
header = re.match(r'\s*\[\[?([^\]\r\n]+)\]\]?\s*(?:#.*)?(?:\r?\n|$)', section)
|
|
if header and header.group(1).strip() == 'tui':
|
|
sections[index] = re.sub(r'(?m)^[ \t]*status_line[ \t]*=.*(?:\r?\n|$)', '', section)
|
|
break
|
|
result = ''.join(sections)
|
|
tomllib.loads(result)
|
|
return result
|
|
|
|
|
|
def filter_flashbang(path):
|
|
return filter_options(path, flashbang_enabled=False)
|
|
|
|
|
|
def merge_json(current, managed):
|
|
if not isinstance(current, dict) or not isinstance(managed, dict):
|
|
return managed
|
|
result = dict(current)
|
|
if 'statusLine' not in managed:
|
|
result.pop('statusLine', None)
|
|
for key, value in managed.items():
|
|
if key == 'hooks' and isinstance(value, dict) and isinstance(result.get(key), dict):
|
|
hooks = dict(result[key])
|
|
for event in set(value) | {'Stop'}:
|
|
groups = value.get(event, [])
|
|
old_groups = hooks.get(event, [])
|
|
if isinstance(groups, list) and isinstance(old_groups, list):
|
|
retained = [group for group in old_groups if not any(
|
|
re.search(r'(?:flashbang|statusline|record-compact|session-state-pointer)\.(?:ps1|sh)',
|
|
str(hook.get('command', '')) + str(hook.get('command_windows', '')), re.I)
|
|
for hook in group.get('hooks', []) if isinstance(hook, dict)
|
|
)]
|
|
merged = groups + [group for group in retained if group not in groups]
|
|
if merged:
|
|
hooks[event] = merged
|
|
else:
|
|
hooks.pop(event, None)
|
|
else:
|
|
hooks[event] = value
|
|
result[key] = hooks
|
|
elif isinstance(value, dict) and isinstance(result.get(key), dict):
|
|
result[key] = merge_json(result[key], value)
|
|
else:
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def merge_toml(current_text, managed_text):
|
|
tomllib.loads(current_text)
|
|
tomllib.loads(managed_text)
|
|
managed_root_keys = set(tomllib.loads(managed_text))
|
|
lines = current_text.splitlines(keepends=True)
|
|
root = []
|
|
blocks = []
|
|
active = root
|
|
for line in lines:
|
|
header = re.match(r'^\s*(\[\[?.+?\]\]?)\s*(?:#.*)?$', line.rstrip('\r\n'))
|
|
if header:
|
|
active = []
|
|
blocks.append((header.group(1), active))
|
|
if active is root:
|
|
assignment = re.match(r'^\s*([A-Za-z0-9_-]+)\s*=', line)
|
|
if assignment and assignment.group(1) in managed_root_keys:
|
|
continue
|
|
active.append(line)
|
|
|
|
managed_headers = set(re.findall(r'(?m)^\s*(\[\[?.+?\]\]?)\s*(?:#.*)?$', managed_text))
|
|
extras = []
|
|
for header, block in blocks:
|
|
plugin_table = re.match(r'^\[(?:plugins|marketplaces)(?:\.|\])', header)
|
|
array_table = header.startswith('[[')
|
|
owned_hook = any(re.search(r'(?:flashbang|statusline|record-compact|session-state-pointer)\.(?:ps1|sh)', line, re.I) for line in block)
|
|
if plugin_table and header in managed_headers:
|
|
raise ValueError('Cannot merge conflicting managed plugin tables.')
|
|
if header not in managed_headers or (array_table and not owned_hook):
|
|
extras.extend(block)
|
|
|
|
result = managed_text
|
|
if root and ''.join(root).strip():
|
|
managed_lines = managed_text.splitlines(keepends=True)
|
|
split = next((i for i, line in enumerate(managed_lines) if re.match(r'^\s*\[', line)), len(managed_lines))
|
|
result = ''.join(managed_lines[:split]).rstrip() + '\n' + ''.join(root).strip() + '\n' + ''.join(managed_lines[split:])
|
|
if extras:
|
|
result = result.rstrip() + '\n\n' + ''.join(extras).lstrip()
|
|
tomllib.loads(result)
|
|
return result
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('action', choices=('filter', 'merge-json', 'merge-toml'))
|
|
parser.add_argument('--path', type=Path)
|
|
parser.add_argument('--current', type=Path)
|
|
parser.add_argument('--flashbang', choices=('true', 'false'))
|
|
parser.add_argument('--statusline', choices=('true', 'false'), default='true')
|
|
args = parser.parse_args()
|
|
try:
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
if args.action == 'filter':
|
|
print(filter_options(args.path, args.flashbang == 'true', args.statusline == 'true'), end='')
|
|
else:
|
|
if args.action == 'merge-json':
|
|
current = json.loads(args.current.read_text(encoding='utf-8-sig'))
|
|
managed = json.load(sys.stdin)
|
|
print(json.dumps(merge_json(current, managed), indent=2, ensure_ascii=False) + '\n', end='')
|
|
else:
|
|
print(merge_toml(args.current.read_text(encoding='utf-8-sig'), sys.stdin.read()), end='')
|
|
except (OSError, ValueError, KeyError) as error:
|
|
print(f'Install options: {error}', file=sys.stderr)
|
|
sys.exit(1)
|