This commit is contained in:
2025-11-14 13:39:43 +08:00
commit 1b185ac23e
11 changed files with 1307 additions and 0 deletions

2
.directory Normal file
View File

@@ -0,0 +1,2 @@
[Desktop Entry]
Icon=folder-red

106
.gitignore vendored Normal file
View File

@@ -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

31
README.md Normal file
View File

@@ -0,0 +1,31 @@
# DynaNote - 简单系统
## 命令列表
- `list [all|整数]` - 列出记忆单元
- `list all` - 显示所有单元
- `list 5` - 显示今天需要复习≤5次的单元
- `list` - 等同于 `list 5`
- `select <id|前缀|名称>` - 选择记忆单元
- `attach <字符串>` - 为选中的单元附加字符串
- `new <名称>` - 创建新记忆单元
- `del <id|前缀|名称>` - 删除记忆单元
- `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, ...}}
```

Binary file not shown.

74
data.toml Normal file
View File

@@ -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

481
dynanote.py Executable file
View File

@@ -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 <id|前缀|名称> - 选择记忆单元")
print(" attach <字符串> - 将字符串附加到选定单元")
print(" new <名称> - 创建新记忆单元")
print(" del <id|前缀|名称> - 删除记忆单元")
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:
"""退出 shellexit 的别名)
用法: 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()

511
dynanote_tui.py Normal file
View File

@@ -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()

8
hasher.py Normal file
View File

@@ -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()

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
toml
cmd2
tabulate

80
sm2.py Normal file
View File

@@ -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']

11
timer.py Normal file
View File

@@ -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()