132 lines
3.4 KiB
Python
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)
|