Keep config items in an external file; add a parser for that file
1 files changed, 120 insertions(+), 5 deletions(-)

M check.py
M check.py +120 -5
@@ 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()