use anyhow::Result;
use crossterm::{
event::{self, Event},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io;
use std::path::Path;
mod app;
mod cli;
mod config;
mod core;
mod fs;
mod input;
mod mcp;
mod models;
mod state;
mod ui;
use app::App;
fn handle_new_task_from_file(app: &mut App, temp_file_path: &str) -> Result<()> {
use crate::models::Task;
use crate::ui::layout::SplitNode;
let content = std::fs::read_to_string(temp_file_path)?;
if content.trim().is_empty() {
return Ok(());
}
let title = content
.lines()
.next()
.unwrap_or("未命名任务")
.trim_start_matches('#')
.trim()
.to_string();
if title.is_empty() || title == "任务标题" {
return Ok(());
}
let project_name = if let Some(SplitNode::Leaf { project_id, .. }) =
app.split_tree.find_pane(app.focused_pane)
{
if let Some(name) = project_id {
name.clone()
} else {
anyhow::bail!("当前面板没有项目");
}
} else {
anyhow::bail!("找不到当前面板");
};
let project_path = if let Some(project) = app.projects.iter().find(|p| p.name == project_name)
{
project.path.clone()
} else {
anyhow::bail!("在项目列表中找不到项目");
};
let next_id = crate::fs::get_next_task_id(&project_path).map_err(|e| anyhow::anyhow!(e))?;
let column = app
.selected_column
.get(&app.focused_pane)
.copied()
.unwrap_or(0);
let status = app
.get_status_name_by_column(column)
.unwrap_or_else(|| "todo".to_string());
let mut task = Task::new(next_id, title.clone(), status.clone());
let task_dir = project_path.join(&status);
std::fs::create_dir_all(&task_dir)?;
let task_file = task_dir.join(format!("{:03}.md", next_id));
std::fs::write(&task_file, &content)?;
task.file_path = task_file;
match crate::fs::load_project(&project_path) {
Ok(updated_project) => {
if let Some(project) = app.projects.iter_mut().find(|p| p.name == project_name) {
*project = updated_project;
let new_task_idx = project
.tasks
.iter()
.filter(|t| t.status == status)
.count()
.saturating_sub(1);
app.selected_task_index
.insert(app.focused_pane, new_task_idx);
}
}
Err(e) => {
anyhow::bail!("重新加载项目失败: {}", e);
}
}
Ok(())
}
fn main() -> Result<()> {
let should_run_tui = cli::handle_cli()?;
if !should_run_tui {
return Ok(());
}
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let _ = fs::ensure_global_ai_config();
let _ = fs::ensure_global_claude_md();
let mut app = App::new()?;
let res = run_app(&mut terminal, &mut app);
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
if let Err(err) = res {
eprintln!("Error: {:?}", err);
}
Ok(())
}
pub fn suspend_terminal<B>(terminal: &mut Terminal<B>) -> Result<()>
where
B: ratatui::backend::Backend + std::io::Write,
{
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}
pub fn resume_terminal<B>(terminal: &mut Terminal<B>) -> Result<()>
where
B: ratatui::backend::Backend + std::io::Write,
{
enable_raw_mode()?;
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
terminal.clear()?;
Ok(())
}
fn run_app<B>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()>
where
B: ratatui::backend::Backend + std::io::Write,
{
loop {
app.clear_expired_notification();
terminal.draw(|f| ui::render(f, app))?;
if let Some(file_path) = app.pending_editor_file.take() {
let is_new_task = app.is_new_task_file;
app.is_new_task_file = false;
suspend_terminal(terminal)?;
if let Err(e) = open_external_editor(&file_path, &app.config.editor) {
app.show_notification(
format!("打开编辑器失败: {}", e),
app::NotificationLevel::Error,
);
}
resume_terminal(terminal)?;
if is_new_task {
match handle_new_task_from_file(app, &file_path) {
Ok(_) => {
let _ = std::fs::remove_file(&file_path);
}
Err(e) => {
app.show_notification(
format!("创建任务失败: {}. 临时文件保留在: {}", e, file_path),
app::NotificationLevel::Error,
);
}
}
} else {
if let Err(e) = app.reload_current_project() {
app.show_notification(
format!("重新加载项目失败: {}", e),
app::NotificationLevel::Error,
);
}
}
}
if let Some(file_path) = app.pending_preview_file.take() {
suspend_terminal(terminal)?;
if let Err(e) = open_external_previewer(&file_path, &app.config.markdown_viewer) {
app.show_notification(
format!("打开预览工具失败: {}", e),
app::NotificationLevel::Error,
);
}
resume_terminal(terminal)?;
}
if event::poll(std::time::Duration::from_millis(100))?
&& let Event::Key(key) = event::read()?
&& !app.handle_key(key) {
let state = state::extract_state(app);
if let Err(e) = state::save_state(&state) {
eprintln!("保存状态失败: {}", e);
}
return Ok(()); }
}
}
fn open_external_editor(file_path: &str, editor_cmd: &str) -> Result<()> {
let parts: Vec<&str> = editor_cmd.split_whitespace().collect();
let (editor, args) = if parts.is_empty() {
("vim", vec![])
} else {
(parts[0], parts[1..].to_vec())
};
let mut cmd = std::process::Command::new(editor);
for arg in args {
cmd.arg(arg);
}
cmd.arg(file_path);
let status = cmd.status()?;
if !status.success() {
anyhow::bail!("编辑器退出异常: {}", status);
}
Ok(())
}
fn open_external_previewer(file_path: &str, viewer_cmd: &str) -> Result<()> {
let parts: Vec<&str> = viewer_cmd.split_whitespace().collect();
let (viewer, args) = if parts.is_empty() {
("cat", vec![])
} else {
(parts[0], parts[1..].to_vec())
};
let mut cmd = std::process::Command::new(viewer);
for arg in args {
cmd.arg(arg);
}
cmd.arg(file_path);
let status = cmd.status()?;
if !status.success() {
anyhow::bail!("预览工具退出异常: {}", status);
}
println!("\n按任意键返回...");
std::io::stdin().read_line(&mut String::new())?;
Ok(())
}