Files
tui-checklist/checklist.py
2026-06-04 08:48:13 -04:00

132 lines
3.4 KiB
Python

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)