use crate::input::CommandRegistry;
use crate::models::Project;
use crate::ui::dialogs::DialogType;
use crate::ui::layout::SplitNode;
use anyhow::Result;
use std::collections::HashMap;
use std::time::Instant;
fn log_debug(msg: String) {
use std::fs::OpenOptions;
use std::io::Write;
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open("/tmp/kanban_debug.log")
{
let _ = writeln!(
file,
"[{}] {}",
chrono::Local::now().format("%H:%M:%S"),
msg
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum NotificationLevel {
Info,
Success,
Warning,
Error,
}
#[derive(Debug, Clone)]
pub struct Notification {
pub message: String,
pub level: NotificationLevel,
pub created_at: Instant,
}
impl Notification {
pub fn is_expired(&self) -> bool {
self.created_at.elapsed().as_secs() >= 3
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum Mode {
Normal,
TaskSelect,
Dialog,
Help,
SpaceMenu,
Preview,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MenuState {
Main,
Project,
Window,
Task,
Status,
}
pub struct App {
pub projects: Vec<Project>,
pub split_tree: SplitNode,
pub focused_pane: usize,
pub mode: Mode,
pub key_buffer: Vec<char>,
pub selected_task_index: HashMap<usize, usize>,
pub selected_column: HashMap<usize, usize>,
pub command_input: String,
#[allow(dead_code)]
pub completion_selected_index: Option<usize>,
pub next_pane_id: usize,
#[allow(dead_code)]
pub should_quit: bool,
pub dialog: Option<DialogType>,
pub menu_state: Option<MenuState>,
pub menu_selected_index: Option<usize>,
pub pending_editor_file: Option<String>,
pub is_new_task_file: bool,
pub pending_preview_file: Option<String>,
pub preview_content: String,
pub preview_scroll: u16,
pub command_registry: CommandRegistry,
pub config: crate::config::Config,
pub show_welcome_dialog: bool,
pub saved_layout: Option<SplitNode>,
pub notification: Option<Notification>,
pub last_column_resize_time: Option<std::time::Instant>,
pub list_states: HashMap<usize, ratatui::widgets::ListState>,
}
impl App {
pub fn new() -> Result<Self> {
let (config, is_first_run) = crate::config::check_first_run()?;
let projects = crate::fs::load_all_projects()?;
let mut split_tree = SplitNode::new_leaf(0);
if !projects.is_empty()
&& let Some(SplitNode::Leaf { project_id, .. }) = split_tree.find_pane_mut(0) {
*project_id = Some(projects[0].name.clone());
}
let mut app = Self {
projects,
split_tree,
focused_pane: 0,
mode: Mode::Normal,
key_buffer: Vec::new(),
selected_task_index: HashMap::new(),
selected_column: HashMap::new(),
command_input: String::new(),
completion_selected_index: None,
next_pane_id: 1,
should_quit: false,
dialog: None,
menu_state: None,
menu_selected_index: None,
pending_editor_file: None,
is_new_task_file: false,
pending_preview_file: None,
preview_content: String::new(),
preview_scroll: 0,
command_registry: CommandRegistry::new(),
config,
show_welcome_dialog: is_first_run,
saved_layout: None,
notification: None,
last_column_resize_time: None,
list_states: HashMap::new(),
};
log_debug(format!(
"App初始化: focused_pane={}, next_pane_id={}, pane_ids={:?}",
app.focused_pane,
app.next_pane_id,
app.split_tree.collect_pane_ids()
));
if let Ok(state) = crate::state::load_state() {
crate::state::apply_state(&mut app, state);
log_debug(format!(
"加载状态后: focused_pane={}, next_pane_id={}, pane_ids={:?}",
app.focused_pane,
app.next_pane_id,
app.split_tree.collect_pane_ids()
));
}
Ok(app)
}
pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
use crate::input::handle_key_input;
handle_key_input(self, key)
}
pub fn get_focused_project(&self) -> Option<&Project> {
if let Some(SplitNode::Leaf { project_id, .. }) =
self.split_tree.find_pane(self.focused_pane)
&& let Some(pid) = project_id {
return self.projects.iter().find(|p| &p.name == pid);
}
None
}
#[allow(dead_code)]
pub fn get_focused_project_mut(&mut self) -> Option<&mut Project> {
if let Some(SplitNode::Leaf { project_id, .. }) =
self.split_tree.find_pane(self.focused_pane)
&& let Some(pid) = project_id.clone() {
return self.projects.iter_mut().find(|p| p.name == pid);
}
None
}
pub fn set_focused_project(&mut self, project_name: String) {
let projects_dir = crate::fs::get_projects_dir();
let project_path = projects_dir.join(&project_name);
if let Ok(updated_project) = crate::fs::load_project(&project_path) {
if let Some(project) = self.projects.iter_mut().find(|p| p.name == project_name) {
*project = updated_project;
} else {
self.projects.push(updated_project);
}
}
if let Some(SplitNode::Leaf { project_id, .. }) =
self.split_tree.find_pane_mut(self.focused_pane)
{
*project_id = Some(project_name);
self.selected_task_index.insert(self.focused_pane, 0);
self.selected_column.insert(self.focused_pane, 0);
let state = crate::state::extract_state(self);
let _ = crate::state::save_state(&state);
}
}
pub fn reload_current_project(&mut self) -> Result<()> {
if let Some(SplitNode::Leaf { project_id, .. }) =
self.split_tree.find_pane(self.focused_pane)
&& let Some(pid) = project_id {
if let Some(project) = self.projects.iter().find(|p| &p.name == pid) {
let project_path = project.path.clone();
let project_type = project.project_type;
if let Ok(updated_project) =
crate::fs::load_project_with_type(&project_path, project_type)
&& let Some(project) = self.projects.iter_mut().find(|p| &p.name == pid) {
*project = updated_project;
}
}
}
Ok(())
}
pub fn toggle_maximize(&mut self) {
if let Some(saved) = self.saved_layout.take() {
self.split_tree = saved;
let state = crate::state::extract_state(self);
let _ = crate::state::save_state(&state);
} else {
if self.split_tree.collect_pane_ids().len() > 1 {
self.saved_layout = Some(self.split_tree.clone());
if let Some(SplitNode::Leaf { project_id, id }) =
self.split_tree.find_pane(self.focused_pane)
{
let project_id = project_id.clone();
let pane_id = *id;
self.split_tree = SplitNode::Leaf {
project_id,
id: pane_id,
};
let state = crate::state::extract_state(self);
let _ = crate::state::save_state(&state);
}
}
}
}
pub fn show_notification(&mut self, message: String, level: NotificationLevel) {
self.notification = Some(Notification {
message,
level,
created_at: Instant::now(),
});
}
pub fn clear_expired_notification(&mut self) {
if let Some(ref notification) = self.notification
&& notification.is_expired() {
self.notification = None;
}
}
pub fn get_status_name_by_column(&self, column: usize) -> Option<String> {
self.get_focused_project()?
.statuses
.get(column)
.map(|s| s.name.clone())
}
pub fn get_status_count(&self) -> usize {
self.get_focused_project()
.map(|p| p.statuses.len())
.unwrap_or(3)
}
}