init
This commit is contained in:
2
.directory
Normal file
2
.directory
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Icon=folder-red
|
||||||
106
.gitignore
vendored
Normal file
106
.gitignore
vendored
Normal 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
31
README.md
Normal 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, ...}}
|
||||||
|
```
|
||||||
BIN
__pycache__/dynanote_shell.cpython-313.pyc
Normal file
BIN
__pycache__/dynanote_shell.cpython-313.pyc
Normal file
Binary file not shown.
74
data.toml
Normal file
74
data.toml
Normal 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
481
dynanote.py
Executable 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:
|
||||||
|
"""退出 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()
|
||||||
511
dynanote_tui.py
Normal file
511
dynanote_tui.py
Normal 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
8
hasher.py
Normal 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
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
toml
|
||||||
|
cmd2
|
||||||
|
tabulate
|
||||||
80
sm2.py
Normal file
80
sm2.py
Normal 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']
|
||||||
Reference in New Issue
Block a user