commit e06f0d7f84b4476b0eda68287ca02cbda54b14ab Author: mvbingham Date: Thu Jun 4 08:48:13 2026 -0400 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b6cb81 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Python TUI Checklist + +A minimal terminal checklist app with no dependencies — just the Python standard library. + +## Usage + +Run with the built-in sample tasks: + +```bash +python checklist.py +``` + +Or pass your own items as arguments: + +```bash +python checklist.py "Task one" "Task two" "Task three" +``` + +## Controls + +| Key | Action | +|-----|--------| +| `↑` / `↓` | Move cursor | +| `Space` | Toggle checkbox | +| `+` or `i` | Add a new task | +| `A` | Check all | +| `N` | Uncheck all | +| `q` | Quit and show final state | + +## Requirements + +- Python 3.6+ +- No third-party packages — works out of the box on Windows, macOS, and Linux diff --git a/checklist.py b/checklist.py new file mode 100644 index 0000000..47877e1 --- /dev/null +++ b/checklist.py @@ -0,0 +1,131 @@ +import sys +import os + +CHECKED = "[x]" +UNCHECKED = "[ ]" + +if os.name == "nt": + import msvcrt + + def getch(): + ch = msvcrt.getwch() + if ch in ("\x00", "\xe0"): # special key prefix + return "\x1b" + msvcrt.getwch() + return ch +else: + import tty, termios + + def getch(): + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(1) + if ch == "\x1b": + sys.stdin.read(1) # [ + return "\x1b" + sys.stdin.read(1) + return ch + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + + +def clear(): + os.system("cls" if os.name == "nt" else "clear") + + +def render(items, checked, cursor): + clear() + print(" Checklist (↑↓ move, Space toggle, + add, A all, N none, q quit)\n") + for i, item in enumerate(items): + mark = CHECKED if checked[i] else UNCHECKED + pointer = ">" if i == cursor else " " + print(f" {pointer} {mark} {item}") + done = sum(checked) + print(f"\n {done}/{len(items)} done") + + +def prompt_new_task(): + print("\n New task (Enter to confirm, Esc to cancel): ", end="", flush=True) + buf = [] + while True: + if os.name == "nt": + ch = msvcrt.getwch() + else: + import tty, termios + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + + if ch in ("\r", "\n"): + print() + return "".join(buf).strip() or None + elif ch == "\x1b": + print() + return None + elif ch in ("\x08", "\x7f"): # backspace + if buf: + buf.pop() + sys.stdout.write("\b \b") + sys.stdout.flush() + elif ch == "\x03": + raise KeyboardInterrupt + elif ch >= " ": + buf.append(ch) + sys.stdout.write(ch) + sys.stdout.flush() + + +def run(items): + items = list(items) + checked = [False] * len(items) + cursor = 0 + + # Windows arrow key codes via msvcrt: up=H, down=P + UP = "\x1bH" if os.name == "nt" else "\x1bA" + DOWN = "\x1bP" if os.name == "nt" else "\x1bB" + + while True: + render(items, checked, cursor) + ch = getch() + + if ch == UP: + cursor = (cursor - 1) % len(items) + elif ch == DOWN: + cursor = (cursor + 1) % len(items) + elif ch == " ": + checked[cursor] = not checked[cursor] + elif ch in ("+", "i"): + task = prompt_new_task() + if task: + items.append(task) + checked.append(False) + cursor = len(items) - 1 + elif ch == "A": + checked = [True] * len(items) + elif ch == "N": + checked = [False] * len(items) + elif ch in ("q", "\x03", "\x1b"): + break + + clear() + print("Final checklist:") + for i, item in enumerate(items): + mark = CHECKED if checked[i] else UNCHECKED + print(f" {mark} {item}") + + +DEFAULT_ITEMS = [ + "Buy groceries", + "Write unit tests", + "Review pull request", + "Update documentation", + "Deploy to staging", +] + +if __name__ == "__main__": + items = sys.argv[1:] if len(sys.argv) > 1 else DEFAULT_ITEMS + run(items)