From 8ad042139ee0d1591496fc201ea163d950839fa2 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Sun, 12 Jul 2026 12:43:50 +0700 Subject: [PATCH] feat: add todo management feature; implement todo content tracking and display in the UI --- Cargo.lock | 34 ++++++++++++++++++++++++ Cargo.toml | 1 + src/app/runtime/actions/mod.rs | 14 ++++++++++ src/app/state/misc.rs | 2 ++ src/main.rs | 10 +++---- src/view/mod.rs | 48 ++++++++++++++++++++++++++++++---- 6 files changed, 99 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6d3969..468de41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,6 +812,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1638,6 +1647,19 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +dependencies = [ + "bitflags 1.3.2", + "fluent-uri", + "serde", + "serde_json", + "serde_repr", +] + [[package]] name = "mac_address" version = "1.1.8" @@ -2805,6 +2827,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4288,6 +4321,7 @@ dependencies = [ "include_dir", "infer", "libc", + "lsp-types", "percent-encoding", "pulldown-cmark", "ratatui", diff --git a/Cargo.toml b/Cargo.toml index e336519..ec8052a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ tracing = "0.1" chrono = { version = "0.4", features = ["serde"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } webbrowser = "1" +lsp-types = "0.97" [[bin]] name = "zesdex" diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index d246718..294879e 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -303,6 +303,20 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { state.misc.tick_count = state.misc.tick_count.wrapping_add(1); let now_ms = chrono::Utc::now().timestamp_millis(); state.misc.drain_expired_toasts(now_ms); + + if state.misc.tick_count.is_multiple_of(10) { + let todo_path = state.session_dir.join("todo.md"); + if let Ok(content) = std::fs::read_to_string(&todo_path) { + if content != state.misc.todo_content { + state.misc.todo_content = content; + state.dirty = true; + } + } else if !state.misc.todo_content.is_empty() { + state.misc.todo_content.clear(); + state.dirty = true; + } + } + // Background API connectivity check — runs on a background thread // every ~1s while disconnected, every ~30s while connected, so the // status bar reflects real API availability without user input. diff --git a/src/app/state/misc.rs b/src/app/state/misc.rs index ab6aec4..0fc4edc 100644 --- a/src/app/state/misc.rs +++ b/src/app/state/misc.rs @@ -278,6 +278,7 @@ pub struct MiscState { pub editor: Option, pub api_connected: bool, pub tick_count: u64, + pub todo_content: String, } impl MiscState { @@ -294,6 +295,7 @@ impl MiscState { editor: None, api_connected: false, tick_count: 0, + todo_content: String::new(), } } diff --git a/src/main.rs b/src/main.rs index 20fd126..5fd4d30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -116,7 +116,7 @@ fn run_single_process() -> Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, crossterm::event::EnableMouseCapture)?; + execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.clear()?; @@ -124,7 +124,7 @@ fn run_single_process() -> Result<()> { let run_result = run_loop(&mut state, &mut terminal); let mut restore_stdout = io::stdout(); - let _ = execute!(restore_stdout, LeaveAlternateScreen, crossterm::event::DisableMouseCapture); + let _ = execute!(restore_stdout, LeaveAlternateScreen); let _ = disable_raw_mode(); if let Err(e) = run_result { @@ -460,7 +460,7 @@ fn run_attach(session_id: &str) -> Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, crossterm::event::EnableMouseCapture)?; + execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.clear()?; @@ -544,7 +544,7 @@ fn run_attach(session_id: &str) -> Result<()> { })?; } - let _ = execute!(io::stdout(), LeaveAlternateScreen, crossterm::event::DisableMouseCapture); + let _ = execute!(io::stdout(), LeaveAlternateScreen); let _ = disable_raw_mode(); let _ = client_state.settings.save(); @@ -570,7 +570,7 @@ fn run_loop( let _ = terminal.clear(); let _ = disable_raw_mode(); - let _ = execute!(io::stdout(), LeaveAlternateScreen, crossterm::event::DisableMouseCapture); + let _ = execute!(io::stdout(), LeaveAlternateScreen); } result } diff --git a/src/view/mod.rs b/src/view/mod.rs index b748600..125c7b1 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -29,6 +29,20 @@ use theme::Theme; pub fn draw(frame: &mut Frame, state: &crate::app::state::rest::AppStateRest) { let area = frame.area(); + let show_todo = !state.misc.todo_content.is_empty() || state.misc.overlay == crate::app::state::types::Overlay::Todo; + let (main_area, todo_area) = if show_todo && area.width > 60 { + let h_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Min(40), + Constraint::Length(30), + ]) + .split(area); + (h_chunks[0], Some(h_chunks[1])) + } else { + (area, None) + }; + let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -36,26 +50,50 @@ pub fn draw(frame: &mut Frame, state: &crate::app::state::rest::AppStateRest) { Constraint::Length(3), Constraint::Length(1), ]) - .split(area); + .split(main_area); - let main_area = chunks[0]; + let chat_area = chunks[0]; let input_area = chunks[1]; let status_area = chunks[2]; - if state.misc.overlay.is_active() { + if state.misc.overlay.is_active() && state.misc.overlay != crate::app::state::types::Overlay::Todo { let overlay = state.misc.overlay; - render_overlay(frame, main_area, overlay, state); + render_overlay(frame, chat_area, overlay, state); } else { - render_main_panel(frame, main_area, state); + render_main_panel(frame, chat_area, state); } render_input_bar(frame, input_area, state); status::draw_status_bar(frame, status_area, state); + if let Some(todo_rect) = todo_area { + render_todo_panel(frame, todo_rect, state); + } + // Toast notifications at top-right (like Hyprland) render_toasts(frame, state); } +fn render_todo_panel(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) { + let block = Block::default() + .title(" Todo ") + .borders(Borders::ALL) + .border_style(Style::default().fg(Theme::PRIMARY)) + .style(Style::default().bg(Theme::BG)); + + let content = if state.misc.todo_content.is_empty() { + "No tasks yet." + } else { + &state.misc.todo_content + }; + + let paragraph = Paragraph::new(content) + .block(block) + .wrap(Wrap { trim: false }); + + frame.render_widget(paragraph, area); +} + fn render_main_panel(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) { chat::draw_chat(frame, area, state); }