You've already forked HeurAMS-Classic
268 lines
10 KiB
Python
268 lines
10 KiB
Python
"""仓库编辑器, 使用TextArea控件等实现仓库配置编辑"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import toml
|
|
from textual.app import ComposeResult
|
|
from textual.containers import Container, Horizontal, ScrollableContainer, Vertical
|
|
from textual.reactive import reactive
|
|
from textual.screen import Screen
|
|
from textual.widgets import (
|
|
Button,
|
|
Footer,
|
|
Header,
|
|
Label,
|
|
ListItem,
|
|
ListView,
|
|
Static,
|
|
TextArea,
|
|
)
|
|
|
|
from heurams.context import config_var
|
|
from heurams.kernel.repolib import Repo
|
|
from heurams.services.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class RepoEditorScreen(Screen):
|
|
"""仓库编辑器屏幕"""
|
|
|
|
SUB_TITLE = "仓库编辑器"
|
|
|
|
BINDINGS = [
|
|
("q", "go_back", "返回"),
|
|
("s", "save_file", "保存"),
|
|
("r", "reload_file", "重载"),
|
|
("d", "toggle_dark", ""),
|
|
]
|
|
|
|
# 当前选择的仓库路径
|
|
selected_repo_path: reactive[Optional[Path]] = reactive(None)
|
|
# 当前选择的文件名
|
|
selected_filename: reactive[Optional[str]] = reactive(None)
|
|
# 文件内容
|
|
file_content: reactive[str] = reactive("")
|
|
|
|
def __init__(
|
|
self,
|
|
repo: Optional[Repo] = None,
|
|
name: str | None = None,
|
|
id: str | None = None,
|
|
classes: str | None = None,
|
|
) -> None:
|
|
super().__init__(name, id, classes)
|
|
self.repo = repo
|
|
self.repo_dir: Optional[Path] = None
|
|
self.file_list = []
|
|
if repo is not None and repo.source is not None:
|
|
self.repo_dir = repo.source
|
|
self._load_file_list()
|
|
# selected_repo_path 将在 on_mount 中设置,避免触发watch时组件未就绪
|
|
|
|
def _load_file_list(self) -> None:
|
|
"""加载仓库目录下的文件列表"""
|
|
if self.repo_dir is None:
|
|
return
|
|
self.file_list = []
|
|
for fname in Repo.file_mapping.values():
|
|
fpath = self.repo_dir / fname
|
|
if fpath.exists():
|
|
self.file_list.append(fname)
|
|
# 也可能存在其他文件,但暂时只支持标准文件
|
|
self.file_list.sort()
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""组合界面组件"""
|
|
yield Header(show_clock=True)
|
|
with Container(id="main_container"):
|
|
with Horizontal(id="top_panel"):
|
|
# 左侧: 仓库选择
|
|
with Vertical(id="repo_selector", classes="panel"):
|
|
yield Label("仓库列表", classes="panel-title")
|
|
yield ListView(
|
|
*[
|
|
ListItem(Label(repo_dir.name))
|
|
for repo_dir in self._get_repo_dirs()
|
|
],
|
|
id="repo_list",
|
|
classes="list-view",
|
|
)
|
|
# 中间: 文件列表
|
|
with Vertical(id="file_selector", classes="panel"):
|
|
yield Label("文件列表", classes="panel-title")
|
|
yield ListView(
|
|
*[ListItem(Label(fname)) for fname in self.file_list],
|
|
id="file_list",
|
|
classes="list-view",
|
|
)
|
|
# 右侧: 编辑区域
|
|
with Vertical(id="editor_panel", classes="panel"):
|
|
yield Label("编辑文件", classes="panel-title")
|
|
yield TextArea(
|
|
id="text_editor",
|
|
language="plaintext",
|
|
classes="text-editor",
|
|
)
|
|
with Horizontal(id="button_bar"):
|
|
yield Button("保存", id="save_button", variant="primary")
|
|
yield Button("重载", id="reload_button", variant="default")
|
|
yield Button("返回", id="back_button", variant="error")
|
|
yield Footer()
|
|
|
|
def _get_repo_dirs(self) -> list[Path]:
|
|
"""获取data/repo/下所有有效仓库目录"""
|
|
repo_root = Path(config_var.get()["paths"]["data"]) / "repo"
|
|
repo_dirs = []
|
|
if repo_root.exists():
|
|
for entry in repo_root.iterdir():
|
|
if entry.is_dir():
|
|
# 检查是否存在 manifest.toml
|
|
if (entry / "manifest.toml").exists():
|
|
repo_dirs.append(entry)
|
|
return repo_dirs
|
|
|
|
def on_mount(self) -> None:
|
|
"""挂载组件时初始化"""
|
|
# 如果已有仓库,设置 selected_repo_path 以触发watch(此时组件已就绪)
|
|
if self.repo_dir is not None:
|
|
self.selected_repo_path = self.repo_dir
|
|
# 焦点放在仓库列表
|
|
self.query_one("#repo_list", ListView).focus()
|
|
|
|
def watch_selected_repo_path(
|
|
self, old_path: Optional[Path], new_path: Optional[Path]
|
|
) -> None:
|
|
"""当选择的仓库路径变化时,加载文件列表"""
|
|
if new_path is None:
|
|
self.file_list = []
|
|
self.selected_filename = None
|
|
self.file_content = ""
|
|
return
|
|
self.repo_dir = new_path
|
|
self._load_file_list()
|
|
# 如果组件已挂载,更新UI
|
|
if self.is_mounted:
|
|
file_list_view = self.query_one("#file_list", ListView)
|
|
file_list_view.clear()
|
|
for fname in self.file_list:
|
|
file_list_view.append(ListItem(Label(fname)))
|
|
# 清空编辑器
|
|
self.query_one("#text_editor", TextArea).text = ""
|
|
self.selected_filename = None
|
|
|
|
def watch_selected_filename(
|
|
self, old_name: Optional[str], new_name: Optional[str]
|
|
) -> None:
|
|
"""当选择的文件名变化时,加载文件内容"""
|
|
if new_name is None or self.repo_dir is None:
|
|
self.file_content = ""
|
|
return
|
|
file_path = self.repo_dir / new_name
|
|
if not file_path.exists():
|
|
self.notify(f"文件不存在: {new_name}", severity="error")
|
|
return
|
|
try:
|
|
content = file_path.read_text(encoding="utf-8")
|
|
self.file_content = content
|
|
# 如果组件已挂载,更新编辑器
|
|
if self.is_mounted:
|
|
editor = self.query_one("#text_editor", TextArea)
|
|
editor.text = content
|
|
# 根据文件后缀设置语言
|
|
if new_name.endswith(".toml"):
|
|
editor.language = "toml"
|
|
elif new_name.endswith(".json"):
|
|
editor.language = "json"
|
|
else:
|
|
editor.language = "plaintext"
|
|
except Exception as e:
|
|
logger.error(f"读取文件失败: {e}")
|
|
self.notify(f"读取文件失败: {e}", severity="error")
|
|
|
|
def watch_file_content(self, old_content: str, new_content: str) -> None:
|
|
"""当文件内容变化时更新编辑器(仅当外部改变时)"""
|
|
# 目前不需要做任何事情,因为编辑器内容已绑定
|
|
pass
|
|
|
|
def on_list_view_selected(self, event) -> None:
|
|
"""处理列表项选择事件"""
|
|
if not isinstance(event.item, ListItem):
|
|
return
|
|
list_id = event.list_view.id
|
|
selected_label = event.item.query_one(Label)
|
|
selected_text = str(selected_label.render())
|
|
|
|
if list_id == "repo_list":
|
|
# 用户选择了仓库
|
|
repo_root = Path(config_var.get()["paths"]["data"]) / "repo"
|
|
selected_dir = repo_root / selected_text
|
|
if selected_dir.exists():
|
|
self.selected_repo_path = selected_dir
|
|
elif list_id == "file_list":
|
|
# 用户选择了文件
|
|
if self.repo_dir is None:
|
|
self.notify("请先选择仓库", severity="warning")
|
|
return
|
|
self.selected_filename = selected_text
|
|
|
|
def on_button_pressed(self, event) -> None:
|
|
"""处理按钮点击事件"""
|
|
event.stop()
|
|
if event.button.id == "save_button":
|
|
self.action_save_file()
|
|
elif event.button.id == "reload_button":
|
|
self.action_reload_file()
|
|
elif event.button.id == "back_button":
|
|
self.action_go_back()
|
|
|
|
def action_save_file(self) -> None:
|
|
"""保存当前编辑的文件"""
|
|
if self.repo_dir is None or self.selected_filename is None:
|
|
self.notify("未选择仓库或文件", severity="warning")
|
|
return
|
|
file_path = self.repo_dir / self.selected_filename
|
|
editor = self.query_one("#text_editor", TextArea)
|
|
new_content = editor.text
|
|
# 验证格式
|
|
try:
|
|
if self.selected_filename.endswith(".toml"):
|
|
toml.loads(new_content) # 验证TOML
|
|
elif self.selected_filename.endswith(".json"):
|
|
json.loads(new_content) # 验证JSON
|
|
except Exception as e:
|
|
self.notify(f"格式错误: {e}", severity="error")
|
|
return
|
|
# 写入文件
|
|
try:
|
|
file_path.write_text(new_content, encoding="utf-8")
|
|
self.notify("保存成功", severity="information")
|
|
except Exception as e:
|
|
logger.error(f"保存文件失败: {e}")
|
|
self.notify(f"保存文件失败: {e}", severity="error")
|
|
|
|
def action_reload_file(self) -> None:
|
|
"""重新加载当前文件(放弃修改)"""
|
|
if self.repo_dir is None or self.selected_filename is None:
|
|
self.notify("未选择仓库或文件", severity="warning")
|
|
return
|
|
file_path = self.repo_dir / self.selected_filename
|
|
try:
|
|
content = file_path.read_text(encoding="utf-8")
|
|
editor = self.query_one("#text_editor", TextArea)
|
|
editor.text = content
|
|
self.notify("已重载", severity="information")
|
|
except Exception as e:
|
|
logger.error(f"重载文件失败: {e}")
|
|
self.notify(f"重载文件失败: {e}", severity="error")
|
|
|
|
def action_go_back(self) -> None:
|
|
"""返回上一屏幕"""
|
|
self.app.pop_screen()
|
|
|
|
def action_toggle_dark(self) -> None:
|
|
"""切换暗色模式"""
|
|
self.app.dark = not self.app.dark
|