use super::{Notebook, NotebookCell, CellId, CellType, ExecutionEngine, NotebookError, NotebookResult};
use std::io::{self, Write};
use std::time::Duration;
#[derive(Debug, Clone)]
pub enum UIEvent {
KeyPress(KeyEvent),
Mouse(MouseEvent),
Resize(u16, u16),
Quit,
}
#[derive(Debug, Clone)]
pub struct KeyEvent {
pub key: Key,
pub modifiers: KeyModifiers,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Key {
Char(char),
Function(u8),
Arrow(ArrowKey),
Special(SpecialKey),
}
#[derive(Debug, Clone, PartialEq)]
pub enum ArrowKey {
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SpecialKey {
Enter,
Tab,
Backspace,
Delete,
Home,
End,
PageUp,
PageDown,
Escape,
}
#[derive(Debug, Clone, PartialEq)]
pub struct KeyModifiers {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
}
impl KeyModifiers {
pub fn none() -> Self {
Self {
ctrl: false,
alt: false,
shift: false,
}
}
pub fn ctrl() -> Self {
Self {
ctrl: true,
alt: false,
shift: false,
}
}
pub fn shift() -> Self {
Self {
ctrl: false,
alt: false,
shift: true,
}
}
}
#[derive(Debug, Clone)]
pub struct MouseEvent {
pub x: u16,
pub y: u16,
pub button: MouseButton,
pub action: MouseAction,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MouseButton {
Left,
Right,
Middle,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MouseAction {
Press,
Release,
Move,
Scroll(ScrollDirection),
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScrollDirection {
Up,
Down,
}
#[derive(Debug, Clone)]
pub enum UICommand {
CreateCell(CellType),
DeleteCell(CellId),
MoveCell(CellId, isize),
ExecuteCell(CellId),
ExecuteAll,
ConvertCell(CellId, CellType),
Save,
SaveAs(String),
Open(String),
Export(String, super::ExportFormat),
ShowHelp,
Quit,
}
#[derive(Debug, Clone)]
pub struct KeyBinding {
pub key_event: KeyEvent,
pub command: UICommand,
pub description: String,
}
impl KeyBinding {
pub fn new(key: Key, modifiers: KeyModifiers, command: UICommand, description: &str) -> Self {
Self {
key_event: KeyEvent { key, modifiers },
command,
description: description.to_string(),
}
}
pub fn matches(&self, event: &KeyEvent) -> bool {
self.key_event.key == event.key && self.key_event.modifiers == event.modifiers
}
}
pub struct NotebookUI {
notebook: Option<Notebook>,
execution_engine: ExecutionEngine,
current_cell: Option<usize>,
edit_mode: bool,
key_bindings: Vec<KeyBinding>,
terminal_size: (u16, u16),
scroll_offset: usize,
status_message: Option<String>,
status_timeout: Option<std::time::Instant>,
}
impl NotebookUI {
pub fn new() -> Self {
let mut ui = Self {
notebook: None,
execution_engine: ExecutionEngine::new(),
current_cell: None,
edit_mode: false,
key_bindings: Vec::new(),
terminal_size: (80, 24),
scroll_offset: 0,
status_message: None,
status_timeout: None,
};
ui.setup_default_key_bindings();
ui
}
pub fn with_notebook(notebook: Notebook) -> Self {
let mut ui = Self::new();
ui.set_notebook(notebook);
ui
}
pub fn set_notebook(&mut self, notebook: Notebook) {
self.notebook = Some(notebook);
self.current_cell = None;
self.edit_mode = false;
self.scroll_offset = 0;
}
fn setup_default_key_bindings(&mut self) {
self.key_bindings = vec![
KeyBinding::new(
Key::Char('n'),
KeyModifiers::ctrl(),
UICommand::CreateCell(CellType::Code),
"创建新代码单元格"
),
KeyBinding::new(
Key::Char('m'),
KeyModifiers::ctrl(),
UICommand::CreateCell(CellType::Markdown),
"创建新 Markdown 单元格"
),
KeyBinding::new(
Key::Char('d'),
KeyModifiers::ctrl(),
UICommand::DeleteCell(uuid::Uuid::nil()), "删除当前单元格"
),
KeyBinding::new(
Key::Special(SpecialKey::Enter),
KeyModifiers::ctrl(),
UICommand::ExecuteCell(uuid::Uuid::nil()),
"执行当前单元格"
),
KeyBinding::new(
Key::Special(SpecialKey::Enter),
KeyModifiers::shift(),
UICommand::ExecuteCell(uuid::Uuid::nil()),
"执行当前单元格并创建新单元格"
),
KeyBinding::new(
Key::Char('r'),
KeyModifiers::ctrl(),
UICommand::ExecuteAll,
"执行所有单元格"
),
KeyBinding::new(
Key::Char('s'),
KeyModifiers::ctrl(),
UICommand::Save,
"保存笔记本"
),
KeyBinding::new(
Key::Char('o'),
KeyModifiers::ctrl(),
UICommand::Open("".to_string()),
"打开笔记本"
),
KeyBinding::new(
Key::Arrow(ArrowKey::Up),
KeyModifiers::none(),
UICommand::ShowHelp, "向上移动"
),
KeyBinding::new(
Key::Arrow(ArrowKey::Down),
KeyModifiers::none(),
UICommand::ShowHelp, "向下移动"
),
KeyBinding::new(
Key::Char('h'),
KeyModifiers::ctrl(),
UICommand::ShowHelp,
"显示帮助"
),
KeyBinding::new(
Key::Char('q'),
KeyModifiers::ctrl(),
UICommand::Quit,
"退出"
),
];
}
pub fn get_notebook(&self) -> Option<&Notebook> {
self.notebook.as_ref()
}
pub fn get_notebook_mut(&mut self) -> Option<&mut Notebook> {
self.notebook.as_mut()
}
pub fn run(&mut self) -> NotebookResult<()> {
self.initialize_terminal()?;
loop {
self.render()?;
match self.read_event()? {
UIEvent::KeyPress(key_event) => {
if let Some(command) = self.handle_key_event(&key_event) {
if matches!(command, UICommand::Quit) {
break;
}
self.execute_command(command)?;
}
}
UIEvent::Resize(width, height) => {
self.terminal_size = (width, height);
}
UIEvent::Quit => break,
_ => {}
}
}
self.cleanup_terminal()?;
Ok(())
}
fn initialize_terminal(&mut self) -> NotebookResult<()> {
print!("\x1b[?1049h"); print!("\x1b[?25l"); io::stdout().flush().map_err(|e| NotebookError::Io(e))?;
self.terminal_size = self.get_terminal_size();
Ok(())
}
fn cleanup_terminal(&mut self) -> NotebookResult<()> {
print!("\x1b[?25h"); print!("\x1b[?1049l"); io::stdout().flush().map_err(|e| NotebookError::Io(e))?;
Ok(())
}
fn get_terminal_size(&self) -> (u16, u16) {
(80, 24)
}
fn read_event(&self) -> NotebookResult<UIEvent> {
Ok(UIEvent::Quit)
}
fn handle_key_event(&mut self, event: &KeyEvent) -> Option<UICommand> {
if let Some(command) = self.handle_navigation(event) {
return Some(command);
}
for binding in &self.key_bindings {
if binding.matches(event) {
let mut command = binding.command.clone();
command = self.replace_placeholders(command);
return Some(command);
}
}
None
}
fn handle_navigation(&mut self, event: &KeyEvent) -> Option<UICommand> {
if self.edit_mode {
return None; }
match &event.key {
Key::Arrow(ArrowKey::Up) => {
if let Some(current) = self.current_cell {
if current > 0 {
self.current_cell = Some(current - 1);
self.ensure_cell_visible();
}
}
}
Key::Arrow(ArrowKey::Down) => {
if let Some(notebook) = &self.notebook {
if let Some(current) = self.current_cell {
if current < notebook.cell_count() - 1 {
self.current_cell = Some(current + 1);
self.ensure_cell_visible();
}
}
}
}
Key::Special(SpecialKey::Enter) => {
self.edit_mode = true;
}
Key::Special(SpecialKey::Escape) => {
self.edit_mode = false;
}
_ => return None,
}
None
}
fn ensure_cell_visible(&mut self) {
if let Some(current) = self.current_cell {
let visible_height = self.terminal_size.1 as usize - 3;
if current < self.scroll_offset {
self.scroll_offset = current;
} else if current >= self.scroll_offset + visible_height {
self.scroll_offset = current - visible_height + 1;
}
}
}
fn replace_placeholders(&self, mut command: UICommand) -> UICommand {
match &mut command {
UICommand::DeleteCell(id) | UICommand::ExecuteCell(id) => {
if let Some(current) = self.current_cell {
if let Some(notebook) = &self.notebook {
if let Some(cell) = notebook.get_cell(current) {
*id = cell.id;
}
}
}
}
UICommand::ConvertCell(id, _) => {
if let Some(current) = self.current_cell {
if let Some(notebook) = &self.notebook {
if let Some(cell) = notebook.get_cell(current) {
*id = cell.id;
}
}
}
}
_ => {}
}
command
}
fn execute_command(&mut self, command: UICommand) -> NotebookResult<()> {
match command {
UICommand::CreateCell(cell_type) => {
self.create_cell(cell_type)?;
}
UICommand::DeleteCell(cell_id) => {
self.delete_cell(cell_id)?;
}
UICommand::ExecuteCell(cell_id) => {
self.execute_cell(cell_id)?;
}
UICommand::ExecuteAll => {
self.execute_all_cells()?;
}
UICommand::Save => {
self.save_notebook()?;
}
UICommand::ShowHelp => {
self.show_help();
}
_ => {
self.set_status_message("命令尚未实现".to_string());
}
}
Ok(())
}
fn create_cell(&mut self, cell_type: CellType) -> NotebookResult<()> {
if let Some(notebook) = &mut self.notebook {
let cell = match cell_type {
CellType::Code => NotebookCell::new_code("".to_string()),
CellType::Text => NotebookCell::new_text("".to_string()),
CellType::Markdown => NotebookCell::new_markdown("".to_string()),
CellType::Output => return Err(NotebookError::Cell("不能手动创建输出单元格".to_string())),
};
let insert_index = self.current_cell.map(|i| i + 1).unwrap_or(0);
notebook.insert_cell(insert_index, cell)?;
self.current_cell = Some(insert_index);
self.edit_mode = true;
self.set_status_message(format!("创建了新的{}单元格", cell_type.display_name()));
}
Ok(())
}
fn delete_cell(&mut self, cell_id: CellId) -> NotebookResult<()> {
if let Some(notebook) = &mut self.notebook {
if let Some((index, _)) = notebook.find_cell(&cell_id) {
notebook.remove_cell(index)?;
if let Some(current) = self.current_cell {
if current >= notebook.cell_count() && notebook.cell_count() > 0 {
self.current_cell = Some(notebook.cell_count() - 1);
} else if notebook.cell_count() == 0 {
self.current_cell = None;
}
}
self.set_status_message("删除了单元格".to_string());
}
}
Ok(())
}
fn execute_cell(&mut self, cell_id: CellId) -> NotebookResult<()> {
if let Some(notebook) = &mut self.notebook {
if let Some((_, cell)) = notebook.find_cell_mut(&cell_id) {
let result = self.execution_engine.execute_cell(cell)?;
match result {
super::ExecutionResult::Success { execution_time, .. } => {
self.set_status_message(format!("执行成功 ({:.2}ms)", execution_time.as_millis()));
}
super::ExecutionResult::Error { error, .. } => {
self.set_status_message(format!("执行错误: {}", error));
}
super::ExecutionResult::Skipped => {
self.set_status_message("跳过执行(非代码单元格)".to_string());
}
super::ExecutionResult::Cancelled => {
self.set_status_message("执行被取消".to_string());
}
}
}
}
Ok(())
}
fn execute_all_cells(&mut self) -> NotebookResult<()> {
let cell_ids: Vec<_> = if let Some(notebook) = &self.notebook {
notebook.get_code_cells().into_iter().map(|(_, cell)| cell.id).collect()
} else {
Vec::new()
};
let total = cell_ids.len();
let mut executed = 0;
for cell_id in cell_ids {
self.execute_cell(cell_id)?;
executed += 1;
self.set_status_message(format!("执行进度: {}/{}", executed, total));
}
self.set_status_message(format!("执行完成: {}/{} 个单元格", executed, total));
Ok(())
}
fn save_notebook(&mut self) -> NotebookResult<()> {
if let Some(notebook) = &mut self.notebook {
if let Some(path) = notebook.get_file_path().cloned() {
super::NotebookSerializer::save_to_file(notebook, path)?;
self.set_status_message("笔记本已保存".to_string());
} else {
self.set_status_message("请先指定文件路径".to_string());
}
}
Ok(())
}
fn show_help(&mut self) {
let help_text = self.generate_help_text();
self.set_status_message(help_text);
}
fn generate_help_text(&self) -> String {
let mut help = String::from("快捷键帮助:\n");
for binding in &self.key_bindings {
let key_desc = self.format_key_event(&binding.key_event);
help.push_str(&format!(" {} - {}\n", key_desc, binding.description));
}
help
}
fn format_key_event(&self, event: &KeyEvent) -> String {
let mut parts = Vec::new();
if event.modifiers.ctrl {
parts.push("Ctrl");
}
if event.modifiers.alt {
parts.push("Alt");
}
if event.modifiers.shift {
parts.push("Shift");
}
let key_str = match &event.key {
Key::Char(c) => c.to_string(),
Key::Function(n) => format!("F{}", n),
Key::Arrow(arrow) => match arrow {
ArrowKey::Up => "↑".to_string(),
ArrowKey::Down => "↓".to_string(),
ArrowKey::Left => "←".to_string(),
ArrowKey::Right => "→".to_string(),
},
Key::Special(special) => match special {
SpecialKey::Enter => "Enter".to_string(),
SpecialKey::Tab => "Tab".to_string(),
SpecialKey::Backspace => "Backspace".to_string(),
SpecialKey::Delete => "Delete".to_string(),
SpecialKey::Home => "Home".to_string(),
SpecialKey::End => "End".to_string(),
SpecialKey::PageUp => "PageUp".to_string(),
SpecialKey::PageDown => "PageDown".to_string(),
SpecialKey::Escape => "Escape".to_string(),
},
};
parts.push(&key_str);
parts.join("+")
}
fn set_status_message(&mut self, message: String) {
self.status_message = Some(message);
self.status_timeout = Some(std::time::Instant::now() + Duration::from_secs(3));
}
fn render(&mut self) -> NotebookResult<()> {
print!("\x1b[2J\x1b[H");
self.render_title_bar()?;
self.render_cells()?;
self.render_status_bar()?;
io::stdout().flush().map_err(|e| NotebookError::Io(e))?;
Ok(())
}
fn render_title_bar(&self) -> NotebookResult<()> {
let title = if let Some(notebook) = &self.notebook {
format!("Yufmath Notebook - {}", notebook.metadata.title)
} else {
"Yufmath Notebook".to_string()
};
let width = self.terminal_size.0 as usize;
let padding = if title.len() < width {
" ".repeat(width - title.len())
} else {
String::new()
};
println!("\x1b[7m{}{}\x1b[0m", title, padding);
Ok(())
}
fn render_cells(&self) -> NotebookResult<()> {
if let Some(notebook) = &self.notebook {
let visible_height = self.terminal_size.1 as usize - 3;
let end_index = std::cmp::min(
self.scroll_offset + visible_height,
notebook.cell_count()
);
for i in self.scroll_offset..end_index {
if let Some(cell) = notebook.get_cell(i) {
let is_current = Some(i) == self.current_cell;
self.render_cell(cell, is_current, i)?;
}
}
} else {
println!("没有打开的笔记本");
}
Ok(())
}
fn render_cell(&self, cell: &NotebookCell, is_current: bool, index: usize) -> NotebookResult<()> {
let prefix = if is_current { ">" } else { " " };
let cell_type = cell.cell_type.display_name();
println!("{} [{}] {}: {}",
prefix,
index + 1,
cell_type,
cell.get_text().lines().next().unwrap_or(""));
if let Some(output) = cell.get_output() {
println!(" 输出: {}", output.get_text());
}
Ok(())
}
fn render_status_bar(&mut self) -> NotebookResult<()> {
if let Some(timeout) = self.status_timeout {
if std::time::Instant::now() > timeout {
self.status_message = None;
self.status_timeout = None;
}
}
let status = if let Some(message) = &self.status_message {
message.clone()
} else {
let mode = if self.edit_mode { "编辑" } else { "命令" };
let cell_info = if let Some(current) = self.current_cell {
format!("单元格 {}", current + 1)
} else {
"无单元格".to_string()
};
format!("{} 模式 | {} | Ctrl+H 显示帮助", mode, cell_info)
};
let width = self.terminal_size.0 as usize;
let padding = if status.len() < width {
" ".repeat(width - status.len())
} else {
String::new()
};
println!("\x1b[7m{}{}\x1b[0m", status, padding);
Ok(())
}
}
impl Default for NotebookUI {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_modifiers() {
let none = KeyModifiers::none();
assert!(!none.ctrl && !none.alt && !none.shift);
let ctrl = KeyModifiers::ctrl();
assert!(ctrl.ctrl && !ctrl.alt && !ctrl.shift);
let shift = KeyModifiers::shift();
assert!(!shift.ctrl && !shift.alt && shift.shift);
}
#[test]
fn test_key_binding() {
let binding = KeyBinding::new(
Key::Char('s'),
KeyModifiers::ctrl(),
UICommand::Save,
"保存文件"
);
let matching_event = KeyEvent {
key: Key::Char('s'),
modifiers: KeyModifiers::ctrl(),
};
let non_matching_event = KeyEvent {
key: Key::Char('s'),
modifiers: KeyModifiers::none(),
};
assert!(binding.matches(&matching_event));
assert!(!binding.matches(&non_matching_event));
}
#[test]
fn test_notebook_ui_creation() {
let ui = NotebookUI::new();
assert!(ui.notebook.is_none());
assert!(ui.current_cell.is_none());
assert!(!ui.edit_mode);
assert!(!ui.key_bindings.is_empty());
}
#[test]
fn test_set_notebook() {
let mut ui = NotebookUI::new();
let mut notebook = Notebook::with_title("测试笔记本".to_string());
notebook.add_cell(NotebookCell::new_code("test".to_string()));
ui.set_notebook(notebook);
assert!(ui.notebook.is_some());
assert_eq!(ui.current_cell, Some(0));
assert_eq!(ui.scroll_offset, 0);
}
#[test]
fn test_status_message() {
let mut ui = NotebookUI::new();
ui.set_status_message("测试消息".to_string());
assert_eq!(ui.status_message, Some("测试消息".to_string()));
assert!(ui.status_timeout.is_some());
}
}