Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# .gitignore

__pycache__/
*.pyc
.venv/
tasks.json
.claude/worktrees/
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# pyproject.toml

[project]
name = "todo-app"
version = "0.1.0"
description = "A minimal terminal to-do app for practicing Git worktrees"
requires-python = ">=3.10"
dependencies = ["textual>=8.0"]

[project.scripts]
todo = "todo_app.app:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# todo_app/app.py

from textual.app import App, ComposeResult
from textual.widgets import Footer, Input, Label, ListItem, ListView

from todo_app.storage import load_tasks, save_tasks


def build_item(task):
marker = "[x]" if task["done"] else "[ ]"
item = ListItem(Label(f"{marker} {task['text']}", markup=False))
item.set_class(task["done"], "done")
return item


class TodoApp(App):
CSS_PATH = "app.tcss"
BINDINGS = [
("space", "toggle", "Toggle"),
("d", "delete", "Delete"),
("q", "quit", "Quit"),
]

def __init__(self):
super().__init__()
self.tasks = load_tasks()

def compose(self) -> ComposeResult:
yield Label("todo", id="title")
yield Input(placeholder="Add a task, then press Enter", id="new-task")
yield ListView(*(build_item(task) for task in self.tasks), id="tasks")
yield Footer()

def on_mount(self) -> None:
self.query_one("#tasks", ListView).index = 0
self.query_one("#new-task", Input).focus()

def on_input_submitted(self, event: Input.Submitted) -> None:
text = event.value.strip()
if not text:
return
task = {"text": text, "done": False}
self.tasks.append(task)
list_view = self.query_one("#tasks", ListView)
list_view.append(build_item(task))
if list_view.index is None:
list_view.index = 0
event.input.clear()
save_tasks(self.tasks)

def action_toggle(self) -> None:
list_view = self.query_one("#tasks", ListView)
item = list_view.highlighted_child
if item is None:
return
task = self.tasks[list_view.index]
task["done"] = not task["done"]
marker = "[x]" if task["done"] else "[ ]"
item.query_one(Label).update(f"{marker} {task['text']}")
item.set_class(task["done"], "done")
save_tasks(self.tasks)

def action_delete(self) -> None:
list_view = self.query_one("#tasks", ListView)
index = list_view.index
if index is None:
return
del self.tasks[index]
list_view.pop(index)
save_tasks(self.tasks)


def main():
TodoApp().run()


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* todo_app/app.tcss */

Screen {
background: #1e1e2e;
color: #cdd6f4;
}

#title {
width: 100%;
height: 1;
background: #45475a;
color: #f5e0dc;
text-style: bold;
}

#new-task {
border: round #6c7086;
background: #181825;
color: #cdd6f4;
}

#tasks {
height: 1fr;
border: round #6c7086;
background: #1e1e2e;
}

#tasks > ListItem {
padding: 0 1;
}

#tasks > ListItem.done {
color: #6c7086;
text-style: strike;
}

Footer {
background: #45475a;
color: #cdd6f4;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# todo_app/storage.py

import json
from pathlib import Path

DATA_FILE = Path("tasks.json")


def load_tasks(path=DATA_FILE):
if not path.exists():
return []
return json.loads(path.read_text(encoding="utf-8"))


def save_tasks(tasks, path=DATA_FILE):
path.write_text(json.dumps(tasks, indent=2), encoding="utf-8")
Loading