checklist / check.py

#!/home/sietse/anaconda3/bin/python3.6

import curses
from pprint import pformat

TASKS = [
    'Workrave',
    'Bril',
    'Methylfenidaat',
    'Check diary today',
    'Check diary 5 days',
    'Read todo.txt',
]

def color_pair(id, fg, bg):
    """Combine defining a colour pair and returning its reference"""
    curses.init_pair(id, fg, bg)
    return curses.color_pair(id)


class TerminalAndStyles:
    """Context manager: initialize terminal for curses and reset on exit"""
    def __init__(self):
        pass

    def __enter__(self):
        self.main_t = curses.initscr()
        self.t = curses.newwin(curses.LINES - 3, curses.COLS - 8, 3, 8)
        curses.start_color()
        curses.noecho()
        curses.cbreak()
        curses.curs_set(False)
        self.t.keypad(True)

        self.styles = {
            'plain': curses.color_pair(0),
            'selected': color_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE),
        }

        return self.t, self.styles

    def __exit__(self, type, value, traceback):
        curses.endwin()
        return False  # reraise if needed


def update(state, key):
    OS, NS = state, state.copy()  # Old state and New State
    NS = state.copy()
    if key == 'KEY_UP':
        NS['current_task_id'] = (OS['current_task_id'] - 1) % len(state['tasks'])
    elif key == 'KEY_DOWN':
        NS['current_task_id'] = (OS['current_task_id'] + 1) % len(state['tasks'])
    elif key == ' ':
        checked, desc = OS['tasks'][OS['current_task_id']]
        NS['tasks'][OS['current_task_id']] = not checked, desc
    elif key == 'q':
        NS['running'] = False
    return NS


def checkbox(checked):
    return '[x]' if checked else '[ ]'


def view(state, t, styles) -> None:
    curses.update_lines_cols()
    for i, (checked, task) in enumerate(state['tasks']):
        if i == state['current_task_id']:
            style = styles['selected']
        else:
            style = styles['plain']
        t.addstr(i, 0, f'- {checkbox(checked)} {task}', style)

    t.addstr(i + 2, 0, pformat(state), styles['plain'])


def run(t, styles):
    state = {
        'tasks': [(False, t) for t in TASKS],
        'current_task_id': 0,
        'running': True,
    }
    view(state, t, styles)
    while True:
        key = t.getkey()
        state = update(state, key)
        view(state, t, styles)
        if not state['running']:
            break

def main():
    with TerminalAndStyles() as (t, styles):
        run(t, styles)

if __name__ == '__main__':
    main()