From 1b185ac23e2b9814978f88fa29c7568801364741 Mon Sep 17 00:00:00 2001 From: david-ajax Date: Fri, 14 Nov 2025 13:39:43 +0800 Subject: [PATCH] init --- .directory | 2 + .gitignore | 106 +++++ README.md | 31 ++ __pycache__/dynanote_shell.cpython-313.pyc | Bin 0 -> 20685 bytes data.toml | 74 +++ dynanote.py | 481 +++++++++++++++++++ dynanote_tui.py | 511 +++++++++++++++++++++ hasher.py | 8 + requirements.txt | 3 + sm2.py | 80 ++++ timer.py | 11 + 11 files changed, 1307 insertions(+) create mode 100644 .directory create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __pycache__/dynanote_shell.cpython-313.pyc create mode 100644 data.toml create mode 100755 dynanote.py create mode 100644 dynanote_tui.py create mode 100644 hasher.py create mode 100644 requirements.txt create mode 100644 sm2.py create mode 100644 timer.py diff --git a/.directory b/.directory new file mode 100644 index 0000000..8173739 --- /dev/null +++ b/.directory @@ -0,0 +1,2 @@ +[Desktop Entry] +Icon=folder-red diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..415cde5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,106 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Production builds +dist/ +build/ +.out/ +.next/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.temp + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2d0ca4 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# DynaNote - 简单系统 + +## 命令列表 + +- `list [all|整数]` - 列出记忆单元 + - `list all` - 显示所有单元 + - `list 5` - 显示今天需要复习≤5次的单元 + - `list` - 等同于 `list 5` + +- `select ` - 选择记忆单元 +- `attach <字符串>` - 为选中的单元附加字符串 +- `new <名称>` - 创建新记忆单元 +- `del ` - 删除记忆单元 +- `mark <评分>` - 使用评分(0-5)更新SM-2算法 +- `show` - 显示选中单元的详细信息 +- `edit` - 使用nano编辑器编辑选中单元 +- `clear` - 清屏 +- `help` - 显示帮助和SM-2评分标准 +- `exit` - 退出shell + +## 数据结构 + +记忆单元以以下结构存储在`data.toml`中: + +```toml +[unit_id] +name = "单元名称" +attachments = [["附件文本", 时间戳]] +created_time = 日期戳 +algodata = {"SM-2" = {"efactor" = 2.5, "real_rept" = 0, ...}} +``` \ No newline at end of file diff --git a/__pycache__/dynanote_shell.cpython-313.pyc b/__pycache__/dynanote_shell.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7bf9ec63ead30735dd0e00c9e500a300a4e329e GIT binary patch literal 20685 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 ldFDPx Dict: + return { + 'name': self.name, + 'attachments': list(self.attachments), + 'created_time': self.created_time, + 'algodata': self.algodata + } + + @classmethod + def from_dict(cls, unit_id: str, data: Dict) -> 'MemoryUnit': + unit = cls(data['name'], unit_id) + unit.attachments = set(tuple(att) for att in data['attachments']) + unit.created_time = data['created_time'] + unit.algodata = data['algodata'] + return unit + + +class DynaNoteShell(cmd.Cmd): + + intro = f'欢迎使用 DynaNote Shell. 输入 help 或 ? 查看命令列表. \n当前 UNIX 日时间戳: {timer.get_daystamp()}\n' + prompt = '(dynanote) $ ' + + def __init__(self): + super().__init__() + self.data_file = './data.toml' + self.memory_units: Dict[str, MemoryUnit] = {} + self.selected_unit: Optional[MemoryUnit] = None + self.load_data() + + def load_data(self) -> None: + """从 data.toml 文件加载记忆单元""" + if os.path.exists(self.data_file): + try: + with open(self.data_file, 'r', encoding='utf-8') as f: + data = toml.load(f) + nested_data = dict() + for key, value in data.items(): + if '.' in key: + parts = key.split('.') + current = nested_data + for part in parts[:-1]: + if part not in current: + current[part] = {} + current = current[part] + current[parts[-1]] = value + else: + nested_data[key] = value + data = nested_data + #print(data) + for unit_id, unit_data in data.items(): + self.memory_units[unit_id] = MemoryUnit.from_dict(unit_id, unit_data) + + print(f"已加载 {len(self.memory_units)} 个记忆单元") + except Exception as e: + print(f"加载数据时出错: {e}") + else: + print("未找到数据文件. 从空数据库开始. ") + + def save_data(self) -> None: + """将记忆单元保存到 data.toml 文件""" + try: + data = {} + for unit_id, unit in self.memory_units.items(): + data[unit_id] = unit.to_dict() + + with open(self.data_file, 'w', encoding='utf-8') as f: + toml.dump(data, f) + + print("数据保存成功") + except Exception as e: + print(f"保存数据时出错: {e}") + + def find_unit_by_prefix(self, prefix: str) -> Optional[str]: + matches = [unit_id for unit_id in self.memory_units.keys() + if unit_id.startswith(prefix)] + + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + print(f"多个单元匹配前缀 '{prefix}': {', '.join(matches)}") + return None + else: + # 尝试通过名称查找 + name_matches = [unit_id for unit_id, unit in self.memory_units.items() + if unit.name == prefix] + if len(name_matches) == 1: + return name_matches[0] + elif len(name_matches) > 1: + print(f"多个单元具有名称 '{prefix}'") + return None + + return None + + def do_list(self, arg: str) -> None: + """列出记忆单元 + 用法: list [all|整数] + - list all: 显示所有记忆单元 + - list 5: 显示复习次数<=5且今天到期的单元 + - list: 同 'list 5' + """ + if not arg: + print("默认列出显示复习次数 <= 5, 且今天到期的单元") + arg = "5" + + if arg.lower() == "all" or arg.lower() == "-a": + units_to_show = list(self.memory_units.values()) + else: + try: + max_reviews = int(arg) + today = timer.get_daystamp() + units_to_show = [ + unit for unit in self.memory_units.values() + if unit.algodata[sm2.SM2Algorithm.algo_name]['real_rept'] <= max_reviews + and unit.algodata[sm2.SM2Algorithm.algo_name]['next_date'] <= today + ] + except ValueError: + print("无效参数. 使用 'all' 或整数. ") + return + + if not units_to_show: + print("没有符合条件的记忆单元") + return + + # 准备 tabulate 数据 + table_data = [] + for unit in units_to_show: + algodata = unit.algodata[sm2.SM2Algorithm.algo_name] + table_data.append([ + unit.unit_id[:8] + "...", + unit.name, + algodata['real_rept'], + algodata['efactor'], + algodata['next_date'], + len(unit.attachments) + ]) + + headers = ["ID", "名称", "复习次数", "EF", "下次复习时间", "附件"] + print(tabulate(table_data, headers=headers)) + + def do_select(self, arg: str) -> None: + """通过 ID、前缀或名称选择记忆单元 + 用法: select <单元ID|前缀|名称> + """ + if not arg: + print("请提供单元 ID、前缀或名称") + return + + unit_id = self.find_unit_by_prefix(arg) + if unit_id: + self.selected_unit = self.memory_units[unit_id] + print(f"已选择单元: {self.selected_unit.name} ({unit_id[:8]}...)") + self.prompt = f"({unit_id[:6]}..) $ " + else: + print(f"未找到唯一匹配 '{arg}' 的单元") + + def do_attach(self, arg: str) -> None: + """将字符串附加到选定的记忆单元 + 用法: attach <字符串> + """ + if not self.selected_unit: + print("未选择单元. 请先使用 'select'. ") + return + + if not arg: + print("请提供要附加的字符串") + return + + attachment = (arg, str(timer.get_timestamp())) + self.selected_unit.attachments.add(attachment) + print(f"附件已添加到 {self.selected_unit.name}") + + def do_new(self, arg: str) -> None: + """创建新的记忆单元 + 用法: new <名称> + """ + if not arg: + print("请提供新单元的名称") + return + + unit = MemoryUnit(arg) + self.memory_units[unit.unit_id] = unit + self.selected_unit = unit + print(f"已创建新单元: {unit.name} ({unit.unit_id[:8]}...)") + + def do_del(self, arg: str) -> None: + """删除记忆单元 + 用法: del <单元ID|前缀|名称> + """ + if not arg: + print("请提供单元 ID、前缀或名称") + return + + unit_id = self.find_unit_by_prefix(arg) + if unit_id: + unit_name = self.memory_units[unit_id].name + del self.memory_units[unit_id] + + # 如果删除的是当前选中的单元,则清除选择 + if self.selected_unit and self.selected_unit.unit_id == unit_id: + self.selected_unit = None + + print(f"已删除单元: {unit_name}") + else: + print(f"未找到唯一匹配 '{arg}' 的单元") + + def do_mark(self, arg: str) -> None: + """使用评分(0-5)更新 SM-2 算法 + 用法: mark <评分> + """ + if not self.selected_unit: + print("未选择单元. 请先使用 'select'. ") + return + + try: + rating = int(arg) + if rating < 0 or rating > 5: + print("评分必须在 0 到 5 之间") + return + except ValueError: + print("请提供有效的整数评分(0-5)") + return + + # 显示评分标准以确认 + print("\nSM-2 评分标准:") + print(" 5 - 完美回答") + print(" 4 - 犹豫后正确回答") + print(" 3 - 经过严重困难后回忆起正确答案") + print(" 2 - 错误回答; 但记得正确答案") + print(" 1 - 错误回答; 但正确答案似乎熟悉") + print(" 0 - 完全遗忘") + if self.selected_unit.algodata["SM-2"]["is_activated"] == 0: + print("此次为初次激活, 可以使用 mark 5") + # 请求确认 + response = input(f"\n确认对 '{self.selected_unit.name}' 评分 {rating}?(y/N): ") + if response.lower() not in ['y', 'yes']: + print("评分已取消") + return + if self.selected_unit.algodata["SM-2"]["is_activated"] == 0: + sm2.SM2Algorithm.revisor(self.selected_unit.algodata, rating, is_new_activation=1) + self.selected_unit.algodata["SM-2"]["is_activated"] = 1 + else: + sm2.SM2Algorithm.revisor(self.selected_unit.algodata, rating) + print(f"已使用评分 {rating} 更新 {self.selected_unit.name} 的 SM-2 参数") + + def do_show(self, arg: str) -> None: + """显示选定记忆单元的详细信息 + 用法: show + """ + if not self.selected_unit: + print("未选择单元. 请先使用 'select'. ") + return + + unit = self.selected_unit + algodata = unit.algodata[sm2.SM2Algorithm.algo_name] + + print(f"\n记忆单元: {unit.name}") + print(f"ID: {unit.unit_id}") + print(f"创建时间: {unit.created_time}") + print(f"\nSM-2 算法数据:") + print(f" EFactor: {algodata['efactor']}") + print(f" 实际复习次数: {algodata['real_rept']}") + print(f" 当前重复次数: {algodata['rept']}") + print(f" 间隔: {algodata['interval']} 天") + print(f" 上次复习: {algodata['last_date']}") + print(f" 下次复习: {algodata['next_date']}") + print(f" 是否激活: {algodata['is_activated']}") + + if unit.attachments: + print(f"\n附件 ({len(unit.attachments)}):") + # 按时间戳从最老到最新排序 + sorted_attachments = sorted(unit.attachments, key=lambda x: float(x[1])) + for i, (text, timestamp) in enumerate(sorted_attachments, 1): + print(f" {i}. {text[:80]}{'...' if len(text) > 80 else ''} (于 {timestamp})") + else: + print("\n无附件") + + def do_edit(self, arg: str) -> None: + """使用 nano 编辑器编辑选定的记忆单元 + 用法: edit + """ + if not self.selected_unit: + print("未选择单元. 请先使用 'select'. ") + return + + # 创建包含单元数据的临时文件 + with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f: + temp_file = f.name + toml.dump({self.selected_unit.unit_id: self.selected_unit.to_dict()}, f) + + try: + # 启动 nano 编辑器 + subprocess.run(['nano', temp_file], check=True) + + # 读取编辑后的文件 + with open(temp_file, 'r', encoding='utf-8') as f: + edited_data = toml.load(f) + + # 更新记忆单元 + if self.selected_unit.unit_id in edited_data: + updated_data = edited_data[self.selected_unit.unit_id] + self.memory_units[self.selected_unit.unit_id] = MemoryUnit.from_dict( + self.selected_unit.unit_id, updated_data + ) + self.selected_unit = self.memory_units[self.selected_unit.unit_id] + print("单元更新成功") + else: + print("错误: 在编辑后的文件中未找到单元 ID") + + except subprocess.CalledProcessError: + print("编辑器已关闭但未保存") + except Exception as e: + print(f"编辑单元时出错: {e}") + finally: + # 清理临时文件 + if os.path.exists(temp_file): + os.unlink(temp_file) + + def do_reset(self, arg: str) -> None: + """重置选定单元的记忆数据 + 用法: reset + """ + if not self.selected_unit: + print("未选择单元. 请先使用 'select'. ") + return + + # 确认重置操作 + response = input(f"确认重置单元 '{self.selected_unit.name}' 的记忆数据?(y/N): ") + if response.lower() not in ['y', 'yes']: + print("重置已取消") + return + + # 重置记忆数据 + unit = self.selected_unit + unit.algodata = { + sm2.SM2Algorithm.algo_name: { + 'efactor': 2.5, + 'real_rept': 0, + 'rept': 0, + 'interval': 0, + 'last_date': 0, + 'next_date': 0, + 'is_activated': 0, + 'last_modify': timer.get_timestamp() + } + } + + print(f"已重置单元 '{unit.name}' 的记忆数据") + + def do_clear(self, arg: str) -> None: + """清屏 + 用法: clear + """ + os.system('clear') + print(f"当前 UNIX 日时间戳: {timer.get_daystamp()}") + + def do_help(self, arg: str) -> None: + """显示帮助和 SM-2 评分标准 + 用法: help + """ + print("\nDynaNote Shell 命令:") + print(" list [all|整数] - 列出记忆单元") + print(" select - 选择记忆单元") + print(" attach <字符串> - 将字符串附加到选定单元") + print(" new <名称> - 创建新记忆单元") + print(" del - 删除记忆单元") + print(" mark <评分> - 使用评分(0-5)更新 SM-2") + print(" show - 显示选定单元详情") + print(" edit - 使用 nano 编辑选定单元") + print(" clear - 清屏") + print(" help - 显示此帮助") + print(" exit - 退出 shell") + + print("\nSM-2 评分标准:") + print(" 5 - 完美回答") + print(" 4 - 犹豫后正确回答") + print(" 3 - 经过严重困难后回忆起正确答案") + print(" 2 - 错误回答; 但记得正确答案") + print(" 1 - 错误回答; 但正确答案似乎熟悉") + print(" 0 - 完全遗忘") + + def do_exit(self, arg: str) -> bool: + """退出 shell + 用法: exit + """ + self.save_data() + return True + + def do_quit(self, arg: str) -> bool: + """退出 shell(exit 的别名) + 用法: quit + """ + return self.do_exit(arg) + + def do_time(self, arg: str) -> bool: + print(f"UNIX 日时间戳: {timer.get_daystamp()}") + + def do_dev(self, arg: str) -> bool: + os.system(f"nano {__file__}") + + def do_save(self, arg: str) -> bool: + self.save_data() + + # 别名 + def do_sel(self, arg: str) -> None: + """select 命令的别名""" + self.do_select(arg) + + def do_add(self, arg: str) -> None: + """attach 命令的别名""" + self.do_attach(arg) + + def do_nano(self, arg: str) -> None: + """edit 命令的别名""" + self.do_edit(arg) + + def do_ls(self, arg: str) -> None: + """list 命令的别名""" + self.do_list(arg) + + def do_la(self, arg: str) -> None: + """list -a 命令的别名""" + self.do_list("all") + + def do_sh(self, arg: str) -> None: + """show 命令的别名""" + self.do_show(arg) + + def do_rm(self, arg: str) -> None: + """del 命令的别名""" + self.do_del(arg) + + +def main(): + """主入口点""" + try: + DynaNoteShell().cmdloop() + except KeyboardInterrupt: + print("\n用户中断") + except Exception as e: + print(f"错误: {e}") + + +if __name__ == '__main__': + main() diff --git a/dynanote_tui.py b/dynanote_tui.py new file mode 100644 index 0000000..c1cc5ad --- /dev/null +++ b/dynanote_tui.py @@ -0,0 +1,511 @@ +#!/usr/bin/env python3 +""" +DynaNote TUI - 使用 SM-2 算法的间隔重复闪卡系统 (Textual 版本) +""" + +import os +import sys +import toml +import tempfile +import subprocess +from typing import Dict, List, Optional, Tuple +from datetime import datetime + +# 导入现有模块 +import hasher +import timer +import sm2 +import pathlib + +from textual import on +from textual.app import App, ComposeResult +from textual.containers import Container, Horizontal, Vertical +from textual.widgets import ( + Header, Footer, Button, Static, Input, + DataTable, Label, Select, TextArea, + TabbedContent, TabPane, ContentSwitcher +) +from textual.screen import Screen, ModalScreen +from textual.reactive import reactive + +os.chdir(pathlib.Path(__file__).resolve().parent) + +class MemoryUnit: + """表示单个记忆单元(闪卡)""" + + def __init__(self, name: str, unit_id: Optional[str] = None): + self.name = name + self.unit_id = unit_id or hasher.get_md5(name) + self.attachments = set() # 元组集合 (字符串, 创建时间戳) + self.created_time = timer.get_daystamp() + self.algodata = { + sm2.SM2Algorithm.algo_name: { + 'efactor': 2.5, + 'real_rept': 0, + 'rept': 0, + 'interval': 0, + 'last_date': 0, + 'next_date': 0, + 'is_activated': 0, + 'last_modify': timer.get_timestamp() + } + } + + def to_dict(self) -> Dict: + """将记忆单元转换为字典以便 TOML 序列化""" + return { + 'name': self.name, + 'attachments': list(self.attachments), + 'created_time': self.created_time, + 'algodata': self.algodata + } + + @classmethod + def from_dict(cls, unit_id: str, data: Dict) -> 'MemoryUnit': + """从字典创建记忆单元""" + unit = cls(data['name'], unit_id) + unit.attachments = set(tuple(att) for att in data['attachments']) + unit.created_time = data['created_time'] + unit.algodata = data['algodata'] + return unit + + +class NewUnitScreen(ModalScreen): + """创建新记忆单元的模态屏幕""" + + def compose(self) -> ComposeResult: + with Container(): + yield Label("创建新记忆单元", classes="modal-title") + yield Input(placeholder="输入单元名称...", id="unit-name") + with Horizontal(): + yield Button("创建", variant="primary", id="create-btn") + yield Button("取消", variant="default", id="cancel-btn") + + @on(Button.Pressed, "#create-btn") + def create_unit(self) -> None: + name_input = self.query_one("#unit-name", Input) + name = name_input.value.strip() + if name: + self.dismiss(name) + + @on(Button.Pressed, "#cancel-btn") + def cancel(self) -> None: + self.dismiss(None) + + +class MarkRatingScreen(ModalScreen): + """评分记忆单元的模态屏幕""" + + def __init__(self, unit_name: str): + super().__init__() + self.unit_name = unit_name + + def compose(self) -> ComposeResult: + with Container(): + yield Label(f"为 '{self.unit_name}' 评分", classes="modal-title") + yield Label("\nSM-2 评分标准:", classes="rating-title") + yield Label("5 - 完美回答", classes="rating-item") + yield Label("4 - 犹豫后正确回答", classes="rating-item") + yield Label("3 - 经过严重困难后回忆起正确答案", classes="rating-item") + yield Label("2 - 错误回答; 但记得正确答案", classes="rating-item") + yield Label("1 - 错误回答; 但正确答案似乎熟悉", classes="rating-item") + yield Label("0 - 完全遗忘", classes="rating-item") + + with Horizontal(): + for rating in [5, 4, 3, 2, 1, 0]: + yield Button(str(rating), id=f"rating-{rating}") + yield Button("取消", variant="default", id="cancel-btn") + + @on(Button.Pressed) + def handle_rating(self, event: Button.Pressed) -> None: + if event.button.id and event.button.id.startswith("rating-"): + rating = int(event.button.id.split("-")[1]) + self.dismiss(rating) + elif event.button.id == "cancel-btn": + self.dismiss(None) + + +class AttachmentScreen(ModalScreen): + """添加附件的模态屏幕""" + + def compose(self) -> ComposeResult: + with Container(): + yield Label("添加附件", classes="modal-title") + yield TextArea(id="attachment-text", language="markdown") + with Horizontal(): + yield Button("添加", variant="primary", id="add-btn") + yield Button("取消", variant="default", id="cancel-btn") + + @on(Button.Pressed, "#add-btn") + def add_attachment(self) -> None: + text_area = self.query_one("#attachment-text", TextArea) + text = text_area.text.strip() + if text: + self.dismiss(text) + + @on(Button.Pressed, "#cancel-btn") + def cancel(self) -> None: + self.dismiss(None) + + +class MainScreen(Screen): + """主屏幕""" + + selected_unit = reactive(None) + + def __init__(self): + super().__init__() + self.data_file = './data.toml' + self.memory_units: Dict[str, MemoryUnit] = {} + self.load_data() + + def load_data(self) -> None: + """从 data.toml 文件加载记忆单元""" + if os.path.exists(self.data_file): + try: + with open(self.data_file, 'r', encoding='utf-8') as f: + data = toml.load(f) + # 处理嵌套数据 + nested_data = dict() + for key, value in data.items(): + if '.' in key: + parts = key.split('.') + current = nested_data + for part in parts[:-1]: + if part not in current: + current[part] = {} + current = current[part] + current[parts[-1]] = value + else: + nested_data[key] = value + data = nested_data + + for unit_id, unit_data in data.items(): + self.memory_units[unit_id] = MemoryUnit.from_dict(unit_id, unit_data) + + self.notify(f"已加载 {len(self.memory_units)} 个记忆单元", severity="information") + except Exception as e: + self.notify(f"加载数据时出错: {e}", severity="error") + else: + self.notify("未找到数据文件. 从空数据库开始.", severity="warning") + + def save_data(self) -> None: + """将记忆单元保存到 data.toml 文件""" + try: + data = {} + for unit_id, unit in self.memory_units.items(): + data[unit_id] = unit.to_dict() + + with open(self.data_file, 'w', encoding='utf-8') as f: + toml.dump(data, f) + + self.notify("数据保存成功", severity="information") + except Exception as e: + self.notify(f"保存数据时出错: {e}", severity="error") + + def compose(self) -> ComposeResult: + yield Header() + with TabbedContent(): + with TabPane("记忆单元列表", id="list-tab"): + with Vertical(): + yield Label("记忆单元列表", classes="section-title") + with Horizontal(): + yield Button("刷新", id="refresh-btn") + yield Button("新建", id="new-btn", variant="primary") + yield Button("删除", id="delete-btn", variant="error") + yield DataTable(id="units-table") + + with TabPane("单元详情", id="detail-tab"): + with Vertical(): + yield Label("记忆单元详情", classes="section-title") + yield Static("未选择任何单元", id="unit-info") + with Horizontal(): + yield Button("评分", id="mark-btn", variant="success") + yield Button("添加附件", id="attach-btn") + yield Button("编辑", id="edit-btn") + yield Static("", id="attachments-info") + + yield Footer() + + def on_mount(self) -> None: + """挂载时初始化""" + self.setup_table() + self.update_unit_info() + + def on_resume(self) -> None: + """从模态屏幕返回时刷新数据""" + self.refresh_table() + + def setup_table(self) -> None: + """设置数据表格""" + table = self.query_one("#units-table", DataTable) + + # 只在表格为空时添加列头,避免重复添加 + if not table.columns: + table.add_columns( + "ID", "名称", "复习次数", "EF", "下次复习时间", "附件数" + ) + + # 清除现有行数据 + table.clear() + + today = timer.get_daystamp() + for unit in self.memory_units.values(): + algodata = unit.algodata[sm2.SM2Algorithm.algo_name] + table.add_row( + unit.unit_id[:8] + "...", + unit.name, + str(algodata['real_rept']), + f"{algodata['efactor']:.2f}", + str(algodata['next_date']), + str(len(unit.attachments)) + ) + + def update_unit_info(self) -> None: + """更新单元详情显示""" + info_widget = self.query_one("#unit-info", Static) + attachments_widget = self.query_one("#attachments-info", Static) + + if self.selected_unit: + unit = self.selected_unit + algodata = unit.algodata[sm2.SM2Algorithm.algo_name] + + info_text = f""" +记忆单元: {unit.name} +ID: {unit.unit_id} +创建时间: {unit.created_time} + +SM-2 算法数据: + EFactor: {algodata['efactor']} + 实际复习次数: {algodata['real_rept']} + 当前重复次数: {algodata['rept']} + 间隔: {algodata['interval']} 天 + 上次复习: {algodata['last_date']} + 下次复习: {algodata['next_date']} + 是否激活: {algodata['is_activated']} +""" + info_widget.update(info_text) + + if unit.attachments: + attachments_text = f"\n附件 ({len(unit.attachments)}):\n" + sorted_attachments = sorted(unit.attachments, key=lambda x: float(x[1])) + for i, (text, timestamp) in enumerate(sorted_attachments, 1): + attachments_text += f" {i}. {text[:80]}{'...' if len(text) > 80 else ''} (于 {timestamp})\n" + attachments_widget.update(attachments_text) + else: + attachments_widget.update("\n无附件") + else: + info_widget.update("未选择任何单元") + attachments_widget.update("") + + @on(DataTable.RowSelected, "#units-table") + def on_row_selected(self, event: DataTable.RowSelected) -> None: + """处理表格行选择""" + if event.row_key: + unit_id_prefix = event.row_key.value + for unit_id, unit in self.memory_units.items(): + if unit_id.startswith(unit_id_prefix): + self.selected_unit = unit + self.update_unit_info() + self.notify(f"已选择单元: {unit.name}", severity="information") + break + + @on(Button.Pressed, "#refresh-btn") + def refresh_table(self) -> None: + """刷新表格""" + self.load_data() # 重新加载数据 + self.setup_table() + self.update_unit_info() + self.notify("表格已刷新", severity="information") + + @on(Button.Pressed, "#new-btn") + async def new_unit(self) -> None: + """创建新单元""" + name = await self.app.push_screen_wait(NewUnitScreen()) + if name: + unit = MemoryUnit(name) + self.memory_units[unit.unit_id] = unit + self.selected_unit = unit + self.setup_table() + self.update_unit_info() + self.save_data() + self.notify(f"已创建新单元: {unit.name}", severity="success") + + @on(Button.Pressed, "#delete-btn") + def delete_unit(self) -> None: + """删除选中的单元""" + if self.selected_unit: + unit_name = self.selected_unit.name + unit_id = self.selected_unit.unit_id + del self.memory_units[unit_id] + self.selected_unit = None + self.setup_table() + self.update_unit_info() + self.save_data() + self.notify(f"已删除单元: {unit_name}", severity="warning") + else: + self.notify("请先选择一个单元", severity="error") + + @on(Button.Pressed, "#mark-btn") + async def mark_unit(self) -> None: + """为选中的单元评分""" + if not self.selected_unit: + self.notify("请先选择一个单元", severity="error") + return + + rating = await self.app.push_screen_wait( + MarkRatingScreen(self.selected_unit.name) + ) + + if rating is not None: + algodata = self.selected_unit.algodata + if algodata[sm2.SM2Algorithm.algo_name]['is_activated'] == 0: + sm2.SM2Algorithm.revisor(algodata, rating, is_new_activation=1) + algodata[sm2.SM2Algorithm.algo_name]['is_activated'] = 1 + else: + sm2.SM2Algorithm.revisor(algodata, rating) + + self.update_unit_info() + self.save_data() + self.notify(f"已使用评分 {rating} 更新 {self.selected_unit.name} 的 SM-2 参数", + severity="success") + + @on(Button.Pressed, "#attach-btn") + async def add_attachment(self) -> None: + """为选中的单元添加附件""" + if not self.selected_unit: + self.notify("请先选择一个单元", severity="error") + return + + text = await self.app.push_screen_wait(AttachmentScreen()) + if text: + attachment = (text, str(timer.get_timestamp())) + self.selected_unit.attachments.add(attachment) + self.update_unit_info() + self.save_data() + self.notify(f"附件已添加到 {self.selected_unit.name}", severity="success") + + @on(Button.Pressed, "#edit-btn") + def edit_unit(self) -> None: + """编辑选中的单元""" + if not self.selected_unit: + self.notify("请先选择一个单元", severity="error") + return + + # 创建包含单元数据的临时文件 + with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f: + temp_file = f.name + toml.dump({self.selected_unit.unit_id: self.selected_unit.to_dict()}, f) + + try: + # 启动 nano 编辑器 + subprocess.run(['nano', temp_file], check=True) + + # 读取编辑后的文件 + with open(temp_file, 'r', encoding='utf-8') as f: + edited_data = toml.load(f) + + # 更新记忆单元 + if self.selected_unit.unit_id in edited_data: + updated_data = edited_data[self.selected_unit.unit_id] + self.memory_units[self.selected_unit.unit_id] = MemoryUnit.from_dict( + self.selected_unit.unit_id, updated_data + ) + self.selected_unit = self.memory_units[self.selected_unit.unit_id] + self.update_unit_info() + self.save_data() + self.notify("单元更新成功", severity="success") + else: + self.notify("错误: 在编辑后的文件中未找到单元 ID", severity="error") + + except subprocess.CalledProcessError: + self.notify("编辑器已关闭但未保存", severity="warning") + except Exception as e: + self.notify(f"编辑单元时出错: {e}", severity="error") + finally: + # 清理临时文件 + if os.path.exists(temp_file): + os.unlink(temp_file) + + +class DynaNoteTUI(App): + """DynaNote TUI 应用""" + + CSS = """ + .modal-title { + text-align: center; + text-style: bold; + margin: 1; + } + + .section-title { + text-align: center; + text-style: bold; + margin: 1; + } + + .rating-title { + text-style: bold; + margin: 1 0; + } + + .rating-item { + margin: 0 0 0 2; + } + + #unit-info { + margin: 1; + border: solid $primary; + padding: 1; + } + + #attachments-info { + margin: 1; + border: solid $accent; + padding: 1; + } + + DataTable { + height: 1fr; + } + """ + + BINDINGS = [ + ("q", "quit", "退出"), + ("s", "save", "保存数据"), + ("r", "refresh", "刷新"), + ("n", "new", "新建单元"), + ] + + def on_mount(self) -> None: + """挂载时切换到主屏幕""" + self.push_screen(MainScreen()) + + def action_quit(self) -> None: + """退出应用""" + self.exit() + + def action_save(self) -> None: + """保存数据""" + if hasattr(self.screen, 'save_data'): + self.screen.save_data() + + def action_refresh(self) -> None: + """刷新""" + if hasattr(self.screen, 'refresh_table'): + self.screen.refresh_table() + + def action_new(self) -> None: + """新建单元""" + if hasattr(self.screen, 'new_unit'): + self.screen.new_unit() + + +def main(): + """主入口点""" + app = DynaNoteTUI() + app.run() + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/hasher.py b/hasher.py new file mode 100644 index 0000000..82229a0 --- /dev/null +++ b/hasher.py @@ -0,0 +1,8 @@ +# 哈希服务 +import hashlib + +def get_md5(text): + return hashlib.md5(text.encode('utf-8')).hexdigest() + +def hash(text): + return hashlib.md5(text.encode('utf-8')).hexdigest() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5764523 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +toml +cmd2 +tabulate \ No newline at end of file diff --git a/sm2.py b/sm2.py new file mode 100644 index 0000000..aac280b --- /dev/null +++ b/sm2.py @@ -0,0 +1,80 @@ +import timer +from typing import TypedDict + +class SM2Algorithm(): + algo_name = "SM-2" + + class AlgodataDict(TypedDict): + efactor: float + real_rept: int + rept: int + interval: int + last_date: int + next_date: int + is_activated: int + last_modify: float + + defaults = { + 'efactor': 2.5, + 'real_rept': 0, + 'rept': 0, + 'interval': 0, + 'last_date': 0, + 'next_date': 0, + 'is_activated': 0, + 'last_modify': timer.get_timestamp() + } + + @classmethod + def revisor(cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False): + """SM-2 算法迭代决策机制实现 + 根据 quality(0 ~ 5) 进行参数迭代最佳间隔 + quality 由主程序评估 + + Args: + quality (int): 记忆保留率量化参数 + """ + if feedback == -1: + return + + algodata[cls.algo_name]['efactor'] = algodata[cls.algo_name]['efactor'] + ( + 0.1 - (5 - feedback) * (0.08 + (5 - feedback) * 0.02) + ) + algodata[cls.algo_name]['efactor'] = max(1.3, algodata[cls.algo_name]['efactor']) + + if feedback < 3: + algodata[cls.algo_name]['rept'] = 0 + algodata[cls.algo_name]['interval'] = 0 + else: + algodata[cls.algo_name]['rept'] += 1 + + algodata[cls.algo_name]['real_rept'] += 1 + + if is_new_activation: + algodata[cls.algo_name]['rept'] = 0 + algodata[cls.algo_name]['efactor'] = 2.5 + + if algodata[cls.algo_name]['rept'] == 0: + algodata[cls.algo_name]['interval'] = 1 + elif algodata[cls.algo_name]['rept'] == 1: + algodata[cls.algo_name]['interval'] = 6 + else: + algodata[cls.algo_name]['interval'] = round( + algodata[cls.algo_name]['interval'] * algodata[cls.algo_name]['efactor'] + ) + + algodata[cls.algo_name]['last_date'] = timer.get_daystamp() + algodata[cls.algo_name]['next_date'] = timer.get_daystamp() + algodata[cls.algo_name]['interval'] + algodata[cls.algo_name]['last_modify'] = timer.get_timestamp() + + @classmethod + def is_due(cls, algodata): + return (algodata[cls.algo_name]['next_date'] <= timer.get_daystamp()) + + @classmethod + def rate(cls, algodata): + return str(algodata[cls.algo_name]['efactor']) + + @classmethod + def nextdate(cls, algodata): + return algodata[cls.algo_name]['next_date'] diff --git a/timer.py b/timer.py new file mode 100644 index 0000000..f16e272 --- /dev/null +++ b/timer.py @@ -0,0 +1,11 @@ +import time + +def get_daystamp() -> int: + """获取当前日戳(以天为单位的整数时间戳)""" + + return int((time.time() + 8 * 3600) // (24 * 3600)) + +def get_timestamp() -> float: + """获取 UNIX 时间戳""" + # 搞这个类的原因是要支持可复现操作 + return time.time()