commit 1b185ac23e2b9814978f88fa29c7568801364741 Author: david-ajax Date: Fri Nov 14 13:39:43 2025 +0800 init 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 0000000..f7bf9ec Binary files /dev/null and b/__pycache__/dynanote_shell.cpython-313.pyc differ diff --git a/data.toml b/data.toml new file mode 100644 index 0000000..32b2a0b --- /dev/null +++ b/data.toml @@ -0,0 +1,74 @@ +[9ec9ec1a7382207eaf27d667698e578b] +name = "若干化学疑问_11/11" +attachments = [ [ "已解决", "1762839529",], [ "扫描(1drv): 9ec9ec1a", "1762839574",],] +created_time = 20403 + +[936c6297a0a93936871d3e47106d4082] +name = "大PI键专题" +attachments = [] +created_time = 20403 + +[85865ffb4031e0bd0d6fc6c51b797bf5] +name = "三角函数的omega取值问题" +attachments = [ [ "观看视频:https://www.bilibili.com/video/BV1ha4y1k7Mj", "1762924869.3563218",], [ "观看视频:https://www.bilibili.com/video/BV1nK411X7P8 (偏简单)", "1762926375.4859126",], [ "代换法: 消去omega, 消去phi, 得到必要条件", "1762926685.2495012",], [ "必须验证必要条件", "1762926694.1452887",], [ "关于y轴对称就是波函数(sin和cos)取+-1", "1762926660.337826",],] +created_time = 20403 + +[871b195560037973fe0ecb2e1815486f] +name = "同源染色体的交叉互换比例问题" +attachments = [] +created_time = 20404 + +[dbd61489864714ad259f6a735f959e5a] +name = "导数参数问题" +attachments = [] +created_time = 20405 + +[9ec9ec1a7382207eaf27d667698e578b.algodata.SM-2] +efactor = 2.5 +real_rept = 0 +rept = 0 +interval = 0 +last_date = 0 +next_date = 0 +is_activated = 0 +last_modify = 1762839499.8240736 + +[936c6297a0a93936871d3e47106d4082.algodata.SM-2] +efactor = 2.5 +real_rept = 0 +rept = 0 +interval = 0 +last_date = 0 +next_date = 0 +is_activated = 0 +last_modify = 1762840219.70494 + +[85865ffb4031e0bd0d6fc6c51b797bf5.algodata.SM-2] +efactor = 2.5 +real_rept = 1 +rept = 0 +interval = 1 +last_date = 20404 +next_date = 20405 +is_activated = 1 +last_modify = 1762926032.3709328 + +[871b195560037973fe0ecb2e1815486f.algodata.SM-2] +efactor = 2.5 +real_rept = 0 +rept = 0 +interval = 0 +last_date = 0 +next_date = 0 +is_activated = 0 +last_modify = 1762926781.6402347 + +[dbd61489864714ad259f6a735f959e5a.algodata.SM-2] +efactor = 2.5 +real_rept = 0 +rept = 0 +interval = 0 +last_date = 0 +next_date = 0 +is_activated = 0 +last_modify = 1762965303.8027596 diff --git a/dynanote.py b/dynanote.py new file mode 100755 index 0000000..fe72e80 --- /dev/null +++ b/dynanote.py @@ -0,0 +1,481 @@ +#!/usr/bin/env python3 + +import os +import sys +import cmd +import toml +import tempfile +import subprocess +from typing import Dict, List, Optional, Tuple +from tabulate import tabulate + +import hasher +import timer +import sm2 +import pathlib + +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: + 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()