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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@

简单terminal**界面**

## Textual 界面

新增 `ebhs_tui.py`,使用 [Textual](https://github.com/Textualize/textual) 提供基于终端的图形界面。运行:

```bash
pip install textual rich
python ebhs_tui.py
```

数据将保存到 `ebhs_data.json` 文件中,界面中可以录入学习内容、查看待复习条目并完成复习。

**注意** mysql必须多预留一列log*

---------------------------------------------------
Expand Down
131 changes: 131 additions & 0 deletions ebhs_tui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import json
import os
import datetime
from typing import List, Dict

from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Button, Static, Input, DataTable
from textual.containers import Horizontal

DATA_FILE = "ebhs_data.json"

# Interval minutes for each stage of Ebbinghaus curve
STAGE_MINUTES = [
0,
5,
30,
12 * 60,
24 * 60,
2 * 24 * 60,
4 * 24 * 60,
7 * 24 * 60,
15 * 24 * 60,
]


def load_data() -> List[Dict]:
if not os.path.exists(DATA_FILE):
return []
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)


def save_data(data: List[Dict]):
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)


def add_content(content: str):
data = load_data()
new_id = max([item["id"] for item in data], default=0) + 1
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
data.append({"id": new_id, "content": content, "stage": 0, "logs": [now]})
save_data(data)


def get_due_items() -> List[Dict]:
data = load_data()
now = datetime.datetime.now()
due = []
for item in data:
stage = int(item.get("stage", 0))
if stage >= len(STAGE_MINUTES):
stage = len(STAGE_MINUTES) - 1
last_time = datetime.datetime.strptime(item["logs"][-1], "%Y-%m-%d %H:%M:%S")
delta_minutes = (now - last_time).total_seconds() / 60
if delta_minutes >= STAGE_MINUTES[stage]:
due.append(item)
return due


def mark_reviewed(item_id: int):
data = load_data()
for item in data:
if item["id"] == item_id:
item["stage"] = int(item.get("stage", 0)) + 1
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
item.setdefault("logs", []).append(now)
break
save_data(data)


class Menu(Static):
def compose(self) -> ComposeResult:
yield Button("Add New", id="add")
yield Button("Show Due", id="show")
yield Button("Review First Due", id="review")
yield Button("Quit", id="quit")


class EbhsApp(App):
CSS_PATH = None

def compose(self) -> ComposeResult:
yield Header()
self.menu = Menu()
yield self.menu
self.output = DataTable(id="output")
yield self.output
yield Footer()

async def on_button_pressed(self, event: Button.Pressed) -> None:
button_id = event.button.id
if button_id == "add":
await self.action_add_new()
elif button_id == "show":
await self.action_show_due()
elif button_id == "review":
await self.action_review_first()
elif button_id == "quit":
self.exit()

async def action_add_new(self):
"""Prompt user for new content and add it to the data store."""
self.input = Input(placeholder="New content", id="input")
await self.mount(self.input)
await self.input.focus()

async def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "input":
content = event.value.strip()
await event.input.remove()
if content:
add_content(content)
await self.action_show_due()

async def action_show_due(self):
self.output.clear(columns=True)
self.output.add_column("ID")
self.output.add_column("Content")
for item in get_due_items():
self.output.add_row(str(item["id"]), item["content"])

async def action_review_first(self):
due = get_due_items()
if due:
mark_reviewed(due[0]["id"])
await self.action_show_due()


if __name__ == "__main__":
EbhsApp().run()