@@ 2,8 2,11 @@
import curses
import re
+import sys
+
+from collections import defaultdict
from enum import Enum, unique
-from pprint import pformat
+from pprint import pformat, pprint
TASKS = [
@@ 27,6 30,103 @@ TASKS = [
'Plan day: put TODOs into worklog, distribute over hours',
]
+def parse_config_list(config_list, filename):
+ """Parse a list of config file lines into a dict of sections, one list of
+ items per section.
+
+ >>> pprint(dict(parse_config_list([
+ ... '[morning]\\n',
+ ... 'one\\n',
+ ... 'two\\n',
+ ... '',
+ ... '[evening]',
+ ... 'a',
+ ... '', # Empty lines within sections are preserved
+ ... 'b',
+ ... '', # empty lines between sections are ignored
+ ... '[default]',
+ ... 'evening'
+ ... ], 'myfilename')))
+ ...
+ {'default': ['a', '', 'b'],
+ 'evening': ['a', '', 'b'],
+ 'morning': ['one', 'two']}
+ """
+
+ no_newlines = (x.rstrip('\r\n') for x in config_list)
+
+ def is_section(line: str) -> bool:
+ return 3 <= len(line) and line[0] == '[' and line[-1] == ']'
+
+ class State:
+ name = 'start'
+ section = None # Name of the current section
+ first_section = None
+ empty_lines_stack = []
+
+ result = defaultdict(list)
+ for i, line in enumerate(no_newlines):
+ if len(line.lstrip()) and line.lstrip()[0] == '#':
+ # Ignore comment lines
+ continue
+
+ elif State.name == 'start':
+ if len(line) == 0:
+ continue
+ elif is_section(line):
+ # start a new section list
+ State.section = line[1:-1]
+ State.name = 'section'
+ continue
+ else:
+ raise ValueError(f"{filename}:{i}:Every checklist in the config must have "
+ "a header like '[my checklist name]'.")
+
+ elif State.name == 'section':
+ if len(line) == 0:
+ # Save the empty lines: they are saved within a checklist, but
+ # discarded outside one.
+ State.empty_lines_stack.append(line)
+ continue
+ elif is_section(line):
+ # Start a new section
+ State.empty_lines_stack.clear()
+ State.section = line[1:-1]
+ continue
+ else:
+ # New checklist item
+ result[State.section].extend(State.empty_lines_stack)
+ State.empty_lines_stack.clear()
+ result[State.section].append(line)
+ continue
+
+ if len(result) == 0:
+ raise ValueError('Input contains no checklists')
+
+ if 'default' not in result:
+ return result
+
+ if not len(result['default']):
+ raise ValueError("[default] section contains no section name")
+
+ default_section = result['default'][-1]
+
+ if default_section not in result:
+ raise ValueError(f"Could not find specified default section "
+ "[{result['default']}].")
+
+ # replace "default: "default_section's_name"
+ # with "default": [default, section's, list]
+ result['default'] = result[default_section]
+ return result
+
+
+def parse_config_file(filename):
+ with open(filename) as f:
+ config_list = f.readlines()
+
+ return parse_config_list(config_list, filename=filename)
+
@unique
class Debug(Enum):
Show = 1
@@ 135,9 235,9 @@ def view(state, t, styles) -> None:
pass
-def run(t, styles):
+def run(t, styles, tasks):
state = {
- 'tasks': [(False, t) for t in TASKS],
+ 'tasks': [(False, t) for t in tasks],
'current_task_id': 0,
'running': True,
'debug': Debug.Hidden,
@@ 151,10 251,25 @@ def run(t, styles):
if not state['running']:
break
+def inner_main(args):
+ checklists = parse_config_file('/home/sietse/.config/checklists')
+ desired_checklist = args[1] if 2 <= len(args) else 'default'
+ if desired_checklist not in checklists:
+ print("Checklist not found: {desired_checklist}.")
+ print("Available checklists: {', '.join(checklists.keys())}")
+ sys.exit(1)
+ tasks = checklists[desired_checklist]
+ with TerminalAndStyles() as (t, styles):
+ run(t, styles, tasks)
+
def main():
- with TerminalAndStyles() as (t, styles):
- run(t, styles)
+ import doctest
+ failure_count, test_count = doctest.testmod(sys.modules[__name__])
+ if failure_count:
+ sys.exit(1)
+ inner_main(sys.argv)
+
if __name__ == '__main__':
main()