From 4691713040154efc0f58a7de9279c91ef9aab1dc Mon Sep 17 00:00:00 2001 From: Romain Bertrand Date: Mon, 7 Sep 2026 12:01:17 +0200 Subject: [PATCH] refactor: remove dead code across UI, sync, and backend layers Drop uncalled helpers, redundant state, and speculative abstractions: - Replace IconService theme machinery with plain icon consts - Remove TaskResult enum; results already flow over the action channel - Collapse EventHandler struct into a free next_event() function - Derive task dialog project index from stored UUID (single source of truth) - Merge create/edit dialog renderers into one function per dialog - Remove unused BackendRegistry lifecycle methods and repository helpers - Keep label badge plumbing (labels not yet loaded from DB, see TODOs) --- src/backend/mod.rs | 6 - src/backend/todoist.rs | 4 - src/backend_registry.rs | 145 +-------- src/entities/mod.rs | 7 - src/icons.rs | 298 +----------------- src/logger.rs | 7 - src/repositories/backend.rs | 23 -- src/repositories/section.rs | 12 - src/sync/mod.rs | 13 - src/sync/sections.rs | 7 - src/sync/tasks.rs | 9 - src/ui/app_component/actions.rs | 10 +- src/ui/app_component/mod.rs | 11 +- src/ui/components/badge.rs | 3 +- src/ui/components/dialog_component.rs | 128 +++----- src/ui/components/dialogs/label_dialogs.rs | 26 +- src/ui/components/dialogs/project_dialogs.rs | 3 - src/ui/components/dialogs/system_dialogs.rs | 18 +- src/ui/components/dialogs/task_dialogs.rs | 51 --- src/ui/components/mod.rs | 4 +- src/ui/components/sidebar_component.rs | 5 +- src/ui/components/sidebar_item_component.rs | 16 +- src/ui/components/task_list_component.rs | 37 +-- src/ui/components/task_list_item_component.rs | 56 +--- src/ui/core/actions.rs | 3 - src/ui/core/event_handler.rs | 56 +--- src/ui/core/mod.rs | 7 +- src/ui/core/task_manager.rs | 108 ++----- src/ui/mod.rs | 4 +- src/ui/renderer.rs | 30 +- tests/icons_test.rs | 46 --- tests/logger.rs | 10 - tests/ui/components/sidebar_component.rs | 2 +- 33 files changed, 154 insertions(+), 1011 deletions(-) delete mode 100644 tests/icons_test.rs diff --git a/src/backend/mod.rs b/src/backend/mod.rs index f18daa7..4dd65ce 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -122,9 +122,6 @@ pub struct UpdateProjectArgs { pub struct UpdateTaskArgs { pub content: Option, pub description: Option, - pub project_remote_id: Option, - pub section_remote_id: Option, - pub parent_remote_id: Option, pub priority: Option, pub due_date: Option, pub due_datetime: Option, @@ -145,9 +142,6 @@ pub struct UpdateLabelArgs { /// task management services (Todoist, TickTick, GitHub, etc.). #[async_trait] pub trait Backend: Send + Sync { - /// Returns the backend type identifier (e.g., "todoist", "ticktick"). - fn backend_type(&self) -> &str; - // Sync operations - fetch all data async fn fetch_projects(&self) -> Result, BackendError>; async fn fetch_tasks(&self) -> Result, BackendError>; diff --git a/src/backend/todoist.rs b/src/backend/todoist.rs index 4d15ac9..8bee0c0 100644 --- a/src/backend/todoist.rs +++ b/src/backend/todoist.rs @@ -93,10 +93,6 @@ impl TodoistBackend { #[async_trait] impl Backend for TodoistBackend { - fn backend_type(&self) -> &str { - "todoist" - } - async fn fetch_projects(&self) -> Result, BackendError> { let mut all_projects = Vec::new(); let mut cursor: Option = None; diff --git a/src/backend_registry.rs b/src/backend_registry.rs index 361ae78..35fb0f2 100644 --- a/src/backend_registry.rs +++ b/src/backend_registry.rs @@ -1,11 +1,11 @@ -//! Backend registry for managing multiple backend instances. +//! Backend registry for managing backend instances. //! -//! This module provides the `BackendRegistry` which manages the lifecycle of -//! backend instances, including loading from database, creating instances, -//! and coordinating sync operations across multiple backends. +//! This module provides the `BackendRegistry`, which holds the app's configured +//! backends: it persists their configuration in the database and keeps the +//! in-memory backend instance the sync service operates on. use anyhow::Result; -use log::{error, info}; +use log::info; use sea_orm::{ActiveValue, IntoActiveModel}; use std::collections::HashMap; use std::sync::Arc; @@ -23,10 +23,8 @@ type BackendMap = HashMap>>; /// Registry for managing backend instances and their configurations. /// /// The `BackendRegistry` is responsible for: -/// - Loading backend configurations from the database /// - Creating and caching backend instances -/// - Managing backend lifecycle (add/remove/enable/disable) -/// - Coordinating sync operations across multiple backends +/// - Persisting backend configuration to the database pub struct BackendRegistry { storage: Arc>, backends: Arc>, @@ -47,43 +45,6 @@ impl BackendRegistry { } } - /// Load all backends from the database and create their instances. - /// - /// This should be called once during application initialization. - /// - /// # Errors - /// Returns error if database access fails or backend creation fails - pub async fn load_backends(&self) -> Result<()> { - let storage = self.storage.lock().await; - let backend_models = BackendRepository::get_all(&storage.conn).await?; - - info!("Loading {} backend(s) from database", backend_models.len()); - - let mut backends = self.backends.lock().await; - - for backend_model in backend_models { - match Self::create_backend_instance(&backend_model) { - Ok(backend_instance) => { - info!( - "✅ Loaded backend: {} ({})", - backend_model.name, backend_model.backend_type - ); - backends.insert(backend_model.uuid, Arc::new(backend_instance)); - } - Err(e) => { - error!( - "❌ Failed to load backend {} ({}): {}", - backend_model.name, backend_model.backend_type, e - ); - // Continue loading other backends - } - } - } - - info!("Loaded {} backend instance(s)", backends.len()); - Ok(()) - } - /// Get a backend instance by UUID. /// /// # Arguments @@ -102,15 +63,6 @@ impl BackendRegistry { .ok_or_else(|| anyhow::anyhow!("Backend not found: {}", uuid)) } - /// Get all backend instances. - /// - /// # Returns - /// Vector of all backend instances - pub async fn get_all_backends(&self) -> Vec>> { - let backends = self.backends.lock().await; - backends.values().cloned().collect() - } - /// List all backend configurations from the database. /// /// # Returns @@ -123,18 +75,6 @@ impl BackendRegistry { BackendRepository::get_all(&storage.conn).await } - /// List all enabled backend configurations from the database. - /// - /// # Returns - /// Vector of enabled backend models - /// - /// # Errors - /// Returns error if database access fails - pub async fn list_enabled_backends(&self) -> Result> { - let storage = self.storage.lock().await; - BackendRepository::get_enabled(&storage.conn).await - } - /// Add a new backend. /// /// # Arguments @@ -230,79 +170,6 @@ impl BackendRegistry { Ok(()) } - /// Remove a backend. - /// - /// # Arguments - /// * `uuid` - Backend UUID - /// - /// # Errors - /// Returns error if deletion fails - pub async fn remove_backend(&self, uuid: &Uuid) -> Result<()> { - let storage = self.storage.lock().await; - BackendRepository::delete(&storage.conn, uuid).await?; - - // Remove from in-memory cache - let mut backends = self.backends.lock().await; - backends.remove(uuid); - - info!("✅ Removed backend: {}", uuid); - Ok(()) - } - - /// Enable a backend. - /// - /// # Arguments - /// * `uuid` - Backend UUID - /// - /// # Errors - /// Returns error if backend not found or update fails - pub async fn enable_backend(&self, uuid: &Uuid) -> Result<()> { - self.set_enabled_status(uuid, true).await - } - - /// Disable a backend. - /// - /// # Arguments - /// * `uuid` - Backend UUID - /// - /// # Errors - /// Returns error if backend not found or update fails - pub async fn disable_backend(&self, uuid: &Uuid) -> Result<()> { - self.set_enabled_status(uuid, false).await - } - - /// Helper to set enabled status. - async fn set_enabled_status(&self, uuid: &Uuid, enabled: bool) -> Result<()> { - let storage = self.storage.lock().await; - - let backend_model = BackendRepository::get_by_uuid(&storage.conn, uuid) - .await? - .ok_or_else(|| anyhow::anyhow!("Backend not found: {}", uuid))?; - - let mut active_model = backend_model.into_active_model(); - active_model.is_enabled = ActiveValue::Set(enabled); - - BackendRepository::update(&storage.conn, active_model).await?; - - let status = if enabled { "enabled" } else { "disabled" }; - info!("✅ Backend {} {}", uuid, status); - Ok(()) - } - - /// Create a backend instance from a backend model. - /// - /// # Arguments - /// * `model` - Backend model from database - /// - /// # Returns - /// Boxed backend instance - /// - /// # Errors - /// Returns error if backend creation fails - fn create_backend_instance(model: &backend::Model) -> Result> { - factory::create_backend(&model.backend_type, &model.credentials) - } - /// Get the storage instance (for creating SyncService instances). pub fn storage(&self) -> Arc> { self.storage.clone() diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 51b7f0e..99d661e 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -4,10 +4,3 @@ pub mod project; pub mod section; pub mod task; pub(crate) mod task_label; - -pub use backend::Entity as Backend; -pub use label::Entity as Label; -pub use project::Entity as Project; -pub use section::Entity as Section; -pub use task::Entity as Task; -pub use task_label::Entity as TaskLabel; diff --git a/src/icons.rs b/src/icons.rs index 87a8ad9..6fd294e 100644 --- a/src/icons.rs +++ b/src/icons.rs @@ -1,285 +1,13 @@ -//! Icon service for managing different icon themes -//! -//! This module provides a centralized way to manage icons throughout the application, -//! supporting different themes like emoji, Unicode, and ASCII fallbacks. -//! -//! This is not really used right now as we were doing more important refactoring, but this is -//! definitely something we will pick up in the future. - -use serde::{Deserialize, Serialize}; - -/// Icon theme variants -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -pub enum IconTheme { - /// Emoji icons (colorful, modern look) - Emoji, - /// Unicode symbols (clean, native look) - #[default] - Unicode, - /// ASCII characters (maximum compatibility) - Ascii, -} - -/// Task status icons -#[derive(Debug, Clone)] -pub struct TaskStatusIcons { - pub pending: &'static str, - pub completed: &'static str, - pub deleted: &'static str, -} - -/// UI element icons -#[derive(Debug, Clone)] -pub struct UiIcons { - pub tasks_title: &'static str, - pub projects_title: &'static str, - pub error: &'static str, - pub info: &'static str, - pub warning: &'static str, - pub success: &'static str, -} - -/// Priority indicators -#[derive(Debug, Clone)] -pub struct PriorityIcons { - pub urgent: &'static str, - pub high: &'static str, - pub medium: &'static str, - pub low: &'static str, -} - -/// Status and metadata icons -#[derive(Debug, Clone)] -pub struct StatusIcons { - pub recurring: &'static str, - pub due_date: &'static str, - pub duration: &'static str, - pub sync_in_progress: &'static str, - pub sync_success: &'static str, - pub sync_error: &'static str, -} - -/// Complete icon set for a specific theme -#[derive(Debug, Clone)] -pub struct IconSet { - pub task_status: TaskStatusIcons, - pub ui: UiIcons, - pub priority: PriorityIcons, - pub status: StatusIcons, -} - -/// Icon service for managing themes and providing icons -#[derive(Debug, Clone)] -pub struct IconService { - current_theme: IconTheme, -} - -impl Default for IconService { - fn default() -> Self { - Self::new(IconTheme::default()) - } -} - -impl IconService { - /// Create a new icon service with the specified theme - #[must_use] - pub fn new(theme: IconTheme) -> Self { - Self { current_theme: theme } - } - - /// Get the current theme - #[must_use] - pub fn theme(&self) -> IconTheme { - self.current_theme - } - - /// Get the complete icon set for the current theme - #[must_use] - pub fn icons(&self) -> IconSet { - match self.current_theme { - IconTheme::Emoji => Self::emoji_icons(), - IconTheme::Unicode => Self::unicode_icons(), - IconTheme::Ascii => Self::ascii_icons(), - } - } - - /// Get emoji icon set - fn emoji_icons() -> IconSet { - IconSet { - task_status: TaskStatusIcons { - pending: "🔳", - completed: "✅", - deleted: "❌", - }, - ui: UiIcons { - tasks_title: "📝", - projects_title: "📁", - error: "❌", - info: "💡", - warning: "⚠️", - success: "✅", - }, - priority: PriorityIcons { - urgent: "🔴", - high: "🟡", - medium: "🟢", - low: "🔵", - }, - status: StatusIcons { - recurring: "🔄", - due_date: "📅", - duration: "⏱️", - sync_in_progress: "🔄", - sync_success: "✅", - sync_error: "❌", - }, - } - } - - /// Get Unicode icon set - fn unicode_icons() -> IconSet { - IconSet { - task_status: TaskStatusIcons { - pending: "☐", - completed: "☒", - deleted: "✗", - }, - ui: UiIcons { - tasks_title: "▶", - projects_title: "◆", - error: "✗", - info: "ⓘ", - warning: "⚠", - success: "✓", - }, - priority: PriorityIcons { - urgent: "●", - high: "◉", - medium: "○", - low: "◯", - }, - status: StatusIcons { - recurring: "↻", - due_date: "◷", - duration: "⧖", - sync_in_progress: "⟳", - sync_success: "✓", - sync_error: "✗", - }, - } - } - - /// Get ASCII icon set - fn ascii_icons() -> IconSet { - IconSet { - task_status: TaskStatusIcons { - pending: "[ ]", - completed: "[X]", - deleted: "[D]", - }, - ui: UiIcons { - tasks_title: ">", - projects_title: "#", - error: "X", - info: "i", - warning: "!", - success: "+", - }, - priority: PriorityIcons { - urgent: "!!", - high: "!", - medium: "+", - low: "-", - }, - status: StatusIcons { - recurring: "~", - due_date: "@", - duration: "T", - sync_in_progress: "...", - sync_success: "+", - sync_error: "X", - }, - } - } - - /// Convenience methods for commonly used icons - #[must_use] - pub fn task_pending(&self) -> &'static str { - self.icons().task_status.pending - } - - #[must_use] - pub fn task_completed(&self) -> &'static str { - self.icons().task_status.completed - } - - #[must_use] - pub fn task_deleted(&self) -> &'static str { - self.icons().task_status.deleted - } - - #[must_use] - pub fn info(&self) -> &'static str { - self.icons().ui.info - } - - #[must_use] - pub fn warning(&self) -> &'static str { - self.icons().ui.warning - } - - /// Convenience methods for project and label icons - #[must_use] - pub fn project_regular(&self) -> &'static str { - match self.current_theme { - IconTheme::Emoji => "📁", - IconTheme::Unicode => "#", - IconTheme::Ascii => "#", - } - } - - #[must_use] - pub fn project_favorite(&self) -> &'static str { - match self.current_theme { - IconTheme::Emoji => "⭐", - IconTheme::Unicode => "★", - IconTheme::Ascii => "*", - } - } - - #[must_use] - pub fn label(&self) -> &'static str { - match self.current_theme { - IconTheme::Emoji => "🏷️", - IconTheme::Unicode => "◉", - IconTheme::Ascii => "@", - } - } - - #[must_use] - pub fn today(&self) -> &'static str { - match self.current_theme { - IconTheme::Emoji => "📅", - IconTheme::Unicode => "◷", - IconTheme::Ascii => "@", - } - } - - #[must_use] - pub fn tomorrow(&self) -> &'static str { - match self.current_theme { - IconTheme::Emoji => "🗓️", - IconTheme::Unicode => "◶", - IconTheme::Ascii => "+", - } - } - - #[must_use] - pub fn upcoming(&self) -> &'static str { - match self.current_theme { - IconTheme::Emoji => "📊", - IconTheme::Unicode => "◎", - IconTheme::Ascii => ">", - } - } -} +//! Icon glyphs rendered in the UI. + +pub const TASK_PENDING: &str = "☐"; +pub const TASK_COMPLETED: &str = "☒"; +pub const TASK_DELETED: &str = "✗"; +pub const INFO: &str = "ⓘ"; +pub const WARNING: &str = "⚠"; +pub const PROJECT_REGULAR: &str = "#"; +pub const PROJECT_FAVORITE: &str = "★"; +pub const LABEL: &str = "◉"; +pub const TODAY: &str = "◷"; +pub const TOMORROW: &str = "◶"; +pub const UPCOMING: &str = "◎"; diff --git a/src/logger.rs b/src/logger.rs index a1c2129..6d93843 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -63,13 +63,6 @@ pub fn get_memory_logs() -> Vec { } } -/// Clear all logs from memory -pub fn clear_memory_logs() { - if let Ok(mut logs) = MEMORY_LOGS.lock() { - logs.clear(); - } -} - /// Custom logger that stores logs in memory for UI display struct MemoryLogger; diff --git a/src/repositories/backend.rs b/src/repositories/backend.rs index 848fdaa..5e6a2d5 100644 --- a/src/repositories/backend.rs +++ b/src/repositories/backend.rs @@ -29,17 +29,6 @@ impl BackendRepository { Ok(backend::Entity::find().all(conn).await?) } - /// Get all enabled backends. - pub async fn get_enabled(conn: &C) -> Result> - where - C: ConnectionTrait, - { - Ok(backend::Entity::find() - .filter(backend::Column::IsEnabled.eq(true)) - .all(conn) - .await?) - } - /// Create a new backend. pub async fn create(conn: &C, backend: backend::ActiveModel) -> Result where @@ -55,16 +44,4 @@ impl BackendRepository { { Ok(backend.update(conn).await?) } - - /// Delete a backend by UUID. - pub async fn delete(conn: &C, uuid: &Uuid) -> Result<()> - where - C: ConnectionTrait, - { - if let Some(backend) = Self::get_by_uuid(conn, uuid).await? { - use sea_orm::ModelTrait; - backend.delete(conn).await?; - } - Ok(()) - } } diff --git a/src/repositories/section.rs b/src/repositories/section.rs index 8fba376..cedf0b2 100644 --- a/src/repositories/section.rs +++ b/src/repositories/section.rs @@ -21,18 +21,6 @@ impl SectionRepository { .await?) } - /// Get sections for a specific project. - pub async fn get_for_project(conn: &C, project_uuid: &Uuid) -> Result> - where - C: ConnectionTrait, - { - Ok(section::Entity::find() - .filter(section::Column::ProjectUuid.eq(*project_uuid)) - .order_by_asc(section::Column::OrderIndex) - .all(conn) - .await?) - } - /// Get a single section by remote_id and backend_uuid. pub async fn get_by_remote_id(conn: &C, backend_uuid: &Uuid, remote_id: &str) -> Result> where diff --git a/src/sync/mod.rs b/src/sync/mod.rs index 65c672b..f8778a9 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -81,8 +81,6 @@ pub struct SyncService { /// allowing for proper status indicators and error handling. #[derive(Debug, Clone)] pub enum SyncStatus { - /// Sync service is not currently performing any operations - Idle, /// A sync operation is currently in progress InProgress, /// The last sync operation completed successfully @@ -152,17 +150,6 @@ impl SyncService { self.debug_mode } - /// Checks if a synchronization operation is currently in progress. - /// - /// This method is useful for UI components to show loading indicators - /// and prevent concurrent sync operations. - /// - /// # Returns - /// `true` if sync is in progress, `false` otherwise - pub async fn is_syncing(&self) -> bool { - *self.sync_in_progress.lock().await - } - /// Performs a full synchronization with the remote backend. /// /// This method fetches all projects, tasks, labels, and sections from the remote backend diff --git a/src/sync/sections.rs b/src/sync/sections.rs index c5f4177..217dadd 100644 --- a/src/sync/sections.rs +++ b/src/sync/sections.rs @@ -2,7 +2,6 @@ use crate::entities::section; use crate::repositories::SectionRepository; use crate::sync::SyncService; use anyhow::Result; -use uuid::Uuid; impl SyncService { /// Get all sections from local storage (fast) @@ -10,10 +9,4 @@ impl SyncService { let storage = self.storage.lock().await; SectionRepository::get_all(&storage.conn).await } - - /// Get sections for a project from local storage (fast) - pub async fn get_sections_for_project(&self, project_uuid: &Uuid) -> Result> { - let storage = self.storage.lock().await; - SectionRepository::get_for_project(&storage.conn, project_uuid).await - } } diff --git a/src/sync/tasks.rs b/src/sync/tasks.rs index 99df12b..7ff0c3d 100644 --- a/src/sync/tasks.rs +++ b/src/sync/tasks.rs @@ -226,9 +226,6 @@ impl SyncService { let task_args = crate::backend::UpdateTaskArgs { content: Some(content.to_string()), description: None, - project_remote_id: None, - section_remote_id: None, - parent_remote_id: None, priority: None, due_date: None, due_datetime: None, @@ -258,9 +255,6 @@ impl SyncService { let task_args = crate::backend::UpdateTaskArgs { content: None, description: None, - project_remote_id: None, - section_remote_id: None, - parent_remote_id: None, priority: None, due_date: due_date.map(std::string::ToString::to_string), due_datetime: None, @@ -290,9 +284,6 @@ impl SyncService { let task_args = crate::backend::UpdateTaskArgs { content: None, description: None, - project_remote_id: None, - section_remote_id: None, - parent_remote_id: None, priority: Some(priority), due_date: None, due_datetime: None, diff --git a/src/ui/app_component/actions.rs b/src/ui/app_component/actions.rs index 2ee5bbc..f7f7bbc 100644 --- a/src/ui/app_component/actions.rs +++ b/src/ui/app_component/actions.rs @@ -56,7 +56,7 @@ impl AppComponent { self.toast = Some(Toast::error(&message, &self.config.theme)); Action::None } - SyncStatus::Idle | SyncStatus::InProgress => Action::None, + SyncStatus::InProgress => Action::None, } } Action::SyncFailed(error) => { @@ -269,14 +269,6 @@ impl AppComponent { self.dialog.update_search_results(&query, results); Action::None } - Action::NextTask => { - info!("Navigation: Next task (j/down)"); - action - } - Action::PreviousTask => { - info!("Navigation: Previous task (k/up)"); - action - } Action::RefreshData => { info!("Data: Refreshing UI data after task operation"); // Schedule a data fetch to reload current view with updated data diff --git a/src/ui/app_component/mod.rs b/src/ui/app_component/mod.rs index 68dcc61..cf30214 100644 --- a/src/ui/app_component/mod.rs +++ b/src/ui/app_component/mod.rs @@ -287,10 +287,9 @@ impl AppComponent { } // Clean up finished tasks - let completed_tasks = self.task_manager.cleanup_finished_tasks(); - if !completed_tasks.is_empty() { - let count = completed_tasks.len(); - info!("Background: Cleaned up {} finished tasks", count); + let cleaned = self.task_manager.cleanup_finished_tasks(); + if cleaned > 0 { + info!("Background: Cleaned up {cleaned} finished tasks"); } actions @@ -350,10 +349,6 @@ impl AppComponent { // Periodic updates Action::None } - EventType::Render => { - // Render updates - Action::None - } EventType::Other => Action::None, }; diff --git a/src/ui/components/badge.rs b/src/ui/components/badge.rs index bb1c685..7cbd182 100644 --- a/src/ui/components/badge.rs +++ b/src/ui/components/badge.rs @@ -22,14 +22,13 @@ pub fn create_paren_badge(text: &str) -> Span<'static> { pub fn create_label_badge(name: &str, theme: &Theme) -> Span<'static> { let style = Style::default().fg(theme.label).add_modifier(Modifier::BOLD); - Span::styled(format!("@{}", name), style) + Span::styled(format!("@{name}"), style) } /// Create task badges optimized for terminal compatibility #[must_use] pub fn create_task_badges( is_recurring: bool, - _has_deadline: bool, duration: Option<&str>, labels: &[label::Model], theme: &Theme, diff --git a/src/ui/components/dialog_component.rs b/src/ui/components/dialog_component.rs index 118d5cf..1b1ddea 100644 --- a/src/ui/components/dialog_component.rs +++ b/src/ui/components/dialog_component.rs @@ -6,7 +6,6 @@ use crate::config::DisplayConfig; use crate::entities::{label, project, task}; -use crate::icons::IconService; use crate::sync::SyncService; use crate::theme::Theme; use crate::ui::components::task_list_item_component::{ListItem as TaskListItem, TaskItem}; @@ -50,10 +49,8 @@ pub struct DialogComponent { pub tasks: Vec, pub selected_project_index: usize, pub selected_parent_project_index: Option, // For project creation parent selection - pub selected_task_project_index: Option, // For task creation project selection (None = no project/inbox) - pub selected_task_project_uuid: Option, // Store the actual UUID to avoid index issues + pub selected_task_project_uuid: Option, // Selected project in the task dialog (None = Inbox) pub task_project_explicitly_selected: bool, // Track if user explicitly selected a project via Tab - pub icons: IconService, // Scrolling support for long content dialogs pub scroll_offset: usize, pub scrollbar_state: ScrollbarState, @@ -64,12 +61,6 @@ pub struct DialogComponent { pub theme: Theme, } -impl Default for DialogComponent { - fn default() -> Self { - Self::new() - } -} - impl DialogComponent { pub fn new() -> Self { Self { @@ -81,10 +72,8 @@ impl DialogComponent { tasks: Vec::new(), selected_project_index: 0, selected_parent_project_index: None, - selected_task_project_index: None, // Default to "None" for tasks (no project) - selected_task_project_uuid: None, // No project selected initially + selected_task_project_uuid: None, // No project selected initially (Inbox) task_project_explicitly_selected: false, // User hasn't used Tab yet - icons: IconService::default(), scroll_offset: 0, scrollbar_state: ScrollbarState::new(0), search_results: Vec::new(), @@ -102,11 +91,6 @@ impl DialogComponent { self.theme = theme; } - pub fn update_data(&mut self, projects: Vec, labels: Vec) { - self.projects = projects; - self.labels = labels; - } - pub fn update_data_with_tasks( &mut self, projects: Vec, @@ -284,8 +268,7 @@ impl DialogComponent { self.cursor_position = 0; self.selected_project_index = 0; self.selected_parent_project_index = None; - self.selected_task_project_index = None; // Reset to "None" for task creation - self.selected_task_project_uuid = None; // Reset stored UUID + self.selected_task_project_uuid = None; // Reset to Inbox for task creation self.task_project_explicitly_selected = false; // Reset selection flag self.scroll_offset = 0; self.scrollbar_state = ScrollbarState::new(0); @@ -318,24 +301,29 @@ impl DialogComponent { fn render_task_creation_dialog(&self, f: &mut Frame, area: Rect) { let task_projects = self.get_task_projects(); - task_dialogs::render_task_creation_dialog( + task_dialogs::render_task_dialog( f, area, - &self.icons, &self.input_buffer, self.cursor_position, &task_projects, - self.selected_task_project_index, + self.selected_task_project_index(), + false, // is_editing &self.theme, ); } + /// Index of the selected task project within `get_task_projects()` (None = Inbox). + fn selected_task_project_index(&self) -> Option { + self.selected_task_project_uuid + .and_then(|uuid| self.get_task_projects().iter().position(|p| p.uuid == uuid)) + } + fn render_project_creation_dialog(&self, f: &mut Frame, area: Rect) { let root_projects = self.get_root_projects(); project_dialogs::render_project_creation_dialog( f, area, - &self.icons, &self.input_buffer, self.cursor_position, &root_projects, @@ -345,34 +333,27 @@ impl DialogComponent { } fn render_project_edit_dialog(&self, f: &mut Frame, area: Rect) { - project_dialogs::render_project_edit_dialog( - f, - area, - &self.icons, - &self.input_buffer, - self.cursor_position, - &self.theme, - ); + project_dialogs::render_project_edit_dialog(f, area, &self.input_buffer, self.cursor_position, &self.theme); } fn render_label_creation_dialog(&self, f: &mut Frame, area: Rect) { - label_dialogs::render_label_creation_dialog( + label_dialogs::render_label_dialog( f, area, - &self.icons, &self.input_buffer, self.cursor_position, + false, // is_editing &self.theme, ); } fn render_label_edit_dialog(&self, f: &mut Frame, area: Rect) { - label_dialogs::render_label_edit_dialog( + label_dialogs::render_label_dialog( f, area, - &self.icons, &self.input_buffer, self.cursor_position, + true, // is_editing &self.theme, ); } @@ -387,27 +368,26 @@ impl DialogComponent { None }; - task_dialogs::render_task_edit_dialog( + task_dialogs::render_task_dialog( f, area, - &self.icons, &self.input_buffer, self.cursor_position, &task_projects, current_project_index, + true, // is_editing &self.theme, ); } fn render_delete_confirmation_dialog(&self, f: &mut Frame, area: Rect, item_type: &str) { - system_dialogs::render_delete_confirmation_dialog(f, area, &self.icons, item_type, &self.theme); + system_dialogs::render_delete_confirmation_dialog(f, area, item_type, &self.theme); } fn render_info_dialog(&mut self, f: &mut Frame, area: Rect, message: &str) { system_dialogs::render_info_dialog( f, area, - &self.icons, message, self.scroll_offset, &mut self.scrollbar_state, @@ -419,7 +399,6 @@ impl DialogComponent { system_dialogs::render_error_dialog( f, area, - &self.icons, message, self.scroll_offset, &mut self.scrollbar_state, @@ -497,17 +476,21 @@ impl DialogComponent { .search_results .iter() .map(|task| { - // TODO: Load task-label relationships from database - let task_labels = Vec::new(); + // Resolve the task's project name once, at build time + let project_name = self + .projects + .iter() + .find(|p| p.uuid == task.project_uuid) + .map(|p| p.name.clone()); // Create TaskItem with the same formatting as main task list + // TODO: Load task-label relationships from database let task_item = TaskItem::new( task.clone(), 0, // depth: 0 for search results (no indentation) 0, // child_count: 0 for search results - self.icons.clone(), - self.projects.clone(), - task_labels, + project_name, + Vec::new(), ); // Use the same render method as main task list @@ -768,36 +751,24 @@ impl Component for DialogComponent { // Mark that user has explicitly selected a project via Tab self.task_project_explicitly_selected = true; - self.selected_task_project_index = match self.selected_task_project_index { - None => { - // First tab: select first project - self.selected_task_project_uuid = Some(projects_data[0].0); - log::info!( - "Tab: Selected project {} ({})", - projects_data[0].1, - projects_data[0].0 - ); - Some(0) - } - Some(index) => { - let next_index = (index + 1) % (projects_data.len() + 1); - if next_index == projects_data.len() { - // Cycle back to "None" option (inbox) - self.selected_task_project_uuid = None; - log::info!("Tab: Selected inbox (no project)"); - None - } else { - // Select the project at next_index - self.selected_task_project_uuid = Some(projects_data[next_index].0); - log::info!( - "Tab: Selected project {} ({})", - projects_data[next_index].1, - projects_data[next_index].0 - ); - Some(next_index) - } - } - }; + let current_index = self + .selected_task_project_uuid + .and_then(|uuid| projects_data.iter().position(|(id, _)| *id == uuid)); + let next_index = + current_index.map(|index| (index + 1) % (projects_data.len() + 1)).unwrap_or(0); + + if next_index == projects_data.len() { + // Cycle back to the "None" option (inbox) + self.selected_task_project_uuid = None; + log::info!("Tab: Selected inbox (no project)"); + } else { + self.selected_task_project_uuid = Some(projects_data[next_index].0); + log::info!( + "Tab: Selected project {} ({})", + projects_data[next_index].1, + projects_data[next_index].0 + ); + } } } else if matches!(self.dialog_type, Some(DialogType::ProjectCreation)) { let root_projects = self.get_root_projects(); @@ -846,11 +817,10 @@ impl Component for DialogComponent { DialogType::TaskCreation { default_project_uuid } => { self.input_buffer.clear(); self.cursor_position = 0; - // Set the selected task project index and UUID if a default project is provided + // Preselect the default project when one is provided if let Some(project_uuid) = default_project_uuid { let task_projects = self.get_task_projects(); - if let Some(index) = task_projects.iter().position(|p| &p.uuid == project_uuid) { - self.selected_task_project_index = Some(index); + if task_projects.iter().any(|p| &p.uuid == project_uuid) { self.selected_task_project_uuid = Some(*project_uuid); let proj_name = self .projects diff --git a/src/ui/components/dialogs/label_dialogs.rs b/src/ui/components/dialogs/label_dialogs.rs index 01d5586..6fe76f2 100644 --- a/src/ui/components/dialogs/label_dialogs.rs +++ b/src/ui/components/dialogs/label_dialogs.rs @@ -1,5 +1,4 @@ use super::common::{self, shortcuts}; -use crate::icons::IconService; use crate::theme::Theme; use crate::ui::layout::LayoutManager; use ratatui::{ @@ -8,10 +7,9 @@ use ratatui::{ Frame, }; -fn render_label_dialog( +pub fn render_label_dialog( f: &mut Frame, area: Rect, - _icons: &IconService, input_buffer: &str, cursor_position: usize, is_editing: bool, @@ -59,25 +57,3 @@ fn render_label_dialog( let final_y = chunks[0].y.saturating_add(1); f.set_cursor_position((final_x, final_y)); } - -pub fn render_label_creation_dialog( - f: &mut Frame, - area: Rect, - icons: &IconService, - input_buffer: &str, - cursor_position: usize, - theme: &Theme, -) { - render_label_dialog(f, area, icons, input_buffer, cursor_position, false, theme); -} - -pub fn render_label_edit_dialog( - f: &mut Frame, - area: Rect, - icons: &IconService, - input_buffer: &str, - cursor_position: usize, - theme: &Theme, -) { - render_label_dialog(f, area, icons, input_buffer, cursor_position, true, theme); -} diff --git a/src/ui/components/dialogs/project_dialogs.rs b/src/ui/components/dialogs/project_dialogs.rs index 22b02b2..d89e6f5 100644 --- a/src/ui/components/dialogs/project_dialogs.rs +++ b/src/ui/components/dialogs/project_dialogs.rs @@ -1,5 +1,4 @@ use super::common::{self, shortcuts}; -use crate::icons::IconService; use crate::theme::Theme; use crate::ui::layout::LayoutManager; use ratatui::{ @@ -12,7 +11,6 @@ use ratatui::{ pub fn render_project_creation_dialog( f: &mut Frame, area: Rect, - _icons: &IconService, input_buffer: &str, cursor_position: usize, root_projects: &[&crate::entities::project::Model], @@ -80,7 +78,6 @@ pub fn render_project_creation_dialog( pub fn render_project_edit_dialog( f: &mut Frame, area: Rect, - _icons: &IconService, input_buffer: &str, cursor_position: usize, theme: &Theme, diff --git a/src/ui/components/dialogs/system_dialogs.rs b/src/ui/components/dialogs/system_dialogs.rs index 9d65e6e..73ff636 100644 --- a/src/ui/components/dialogs/system_dialogs.rs +++ b/src/ui/components/dialogs/system_dialogs.rs @@ -1,4 +1,4 @@ -use crate::icons::IconService; +use crate::icons::{INFO, WARNING}; use crate::logger; use crate::theme::Theme; use crate::ui::layout::LayoutManager; @@ -96,18 +96,12 @@ fn render_scrollable_message_dialog( } } -pub fn render_delete_confirmation_dialog( - f: &mut Frame, - area: Rect, - icons: &IconService, - item_type: &str, - theme: &Theme, -) { +pub fn render_delete_confirmation_dialog(f: &mut Frame, area: Rect, item_type: &str, theme: &Theme) { let dialog_area = LayoutManager::centered_rect_lines(60, 8, area); f.render_widget(Clear, dialog_area); // Main dialog block with rounded borders and danger theme (appropriate for deletion) - let title = format!("{} Confirm Delete", icons.warning()); + let title = format!("{WARNING} Confirm Delete"); let main_block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) @@ -161,14 +155,13 @@ pub fn render_delete_confirmation_dialog( pub fn render_info_dialog( f: &mut Frame, area: Rect, - icons: &IconService, message: &str, scroll_offset: usize, scrollbar_state: &mut ScrollbarState, theme: &Theme, ) { let config = ScrollableDialogConfig { - title: format!("{} Info", icons.info()), + title: format!("{INFO} Info"), color: theme.info_dialog, width_percent: 60, height_lines: 10, @@ -179,14 +172,13 @@ pub fn render_info_dialog( pub fn render_error_dialog( f: &mut Frame, area: Rect, - icons: &IconService, message: &str, scroll_offset: usize, scrollbar_state: &mut ScrollbarState, theme: &Theme, ) { let config = ScrollableDialogConfig { - title: format!("{} Error", icons.warning()), + title: format!("{WARNING} Error"), color: theme.danger, width_percent: 70, height_lines: 12, diff --git a/src/ui/components/dialogs/task_dialogs.rs b/src/ui/components/dialogs/task_dialogs.rs index 9f557da..8e875a2 100644 --- a/src/ui/components/dialogs/task_dialogs.rs +++ b/src/ui/components/dialogs/task_dialogs.rs @@ -1,6 +1,5 @@ use super::common::{self, shortcuts}; use crate::entities::project; -use crate::icons::IconService; use crate::theme::Theme; use crate::ui::layout::LayoutManager; use ratatui::{ @@ -13,7 +12,6 @@ use ratatui::{ pub fn render_task_dialog( f: &mut Frame, area: Rect, - _icons: &IconService, input_buffer: &str, cursor_position: usize, task_projects: &[&project::Model], @@ -82,52 +80,3 @@ pub fn render_task_dialog( // Set terminal cursor position f.set_cursor_position((chunks[0].x + 1 + cursor_position as u16, chunks[0].y + 1)); } - -// Legacy wrapper functions for backward compatibility -#[allow(clippy::too_many_arguments)] -pub fn render_task_creation_dialog( - f: &mut Frame, - area: Rect, - icons: &IconService, - input_buffer: &str, - cursor_position: usize, - task_projects: &[&project::Model], - selected_task_project_index: Option, - theme: &Theme, -) { - render_task_dialog( - f, - area, - icons, - input_buffer, - cursor_position, - task_projects, - selected_task_project_index, - false, // is_editing = false for creation - theme, - ); -} - -#[allow(clippy::too_many_arguments)] -pub fn render_task_edit_dialog( - f: &mut Frame, - area: Rect, - icons: &IconService, - input_buffer: &str, - cursor_position: usize, - task_projects: &[&project::Model], - selected_task_project_index: Option, - theme: &Theme, -) { - render_task_dialog( - f, - area, - icons, - input_buffer, - cursor_position, - task_projects, - selected_task_project_index, - true, // is_editing = true for editing - theme, - ); -} diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index a41b098..5dda1cf 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -49,7 +49,7 @@ pub(crate) mod sidebar_item_component; pub(crate) mod task_list_component; pub mod task_list_item_component; -// Public exports for external use -pub use dialog_component::DialogComponent; +// Re-exports for use across the crate +pub(crate) use dialog_component::DialogComponent; pub use sidebar_component::SidebarComponent; pub use task_list_component::TaskListComponent; diff --git a/src/ui/components/sidebar_component.rs b/src/ui/components/sidebar_component.rs index 7bf1b2a..a712748 100644 --- a/src/ui/components/sidebar_component.rs +++ b/src/ui/components/sidebar_component.rs @@ -5,7 +5,6 @@ //! It handles keyboard and mouse navigation with proper visual feedback. use crate::entities::{label, project}; -use crate::icons::IconService; use crate::theme::Theme; use crate::ui::components::scrollbar_helper::ScrollbarHelper; use crate::ui::components::sidebar_item_component::{SidebarItem, SidebarItemType}; @@ -38,7 +37,6 @@ pub struct SidebarComponent { pub selection: SidebarSelection, pub projects: Vec, pub labels: Vec, - pub icons: IconService, items: Vec, folder_states: HashMap, list_state: ListState, @@ -61,7 +59,6 @@ impl SidebarComponent { selection: SidebarSelection::Today, projects: Vec::new(), labels: Vec::new(), - icons: IconService::default(), items: Vec::new(), folder_states: HashMap::new(), list_state, @@ -461,7 +458,7 @@ impl Component for SidebarComponent { let all_items: Vec = self .items .iter() - .map(|item| item.render(&self.icons, &self.selection, false, &self.theme)) + .map(|item| item.render(&self.selection, false, &self.theme)) .collect(); // Calculate areas for list and scrollbar using helper diff --git a/src/ui/components/sidebar_item_component.rs b/src/ui/components/sidebar_item_component.rs index 9330ff3..0d9341b 100644 --- a/src/ui/components/sidebar_item_component.rs +++ b/src/ui/components/sidebar_item_component.rs @@ -4,7 +4,7 @@ //! enabling foldable account folders and hierarchical display of projects and labels. use crate::entities::{label, project}; -use crate::icons::IconService; +use crate::icons::{LABEL, PROJECT_FAVORITE, PROJECT_REGULAR, TODAY, TOMORROW, UPCOMING}; use crate::theme::Theme; use crate::ui::core::SidebarSelection; use ratatui::{ @@ -35,7 +35,6 @@ pub trait SidebarItem { /// Render the item as a ListItem with appropriate styling fn render<'a>( &'a self, - icons: &'a IconService, current_selection: &'a SidebarSelection, is_selected: bool, theme: &'a Theme, @@ -54,7 +53,6 @@ pub trait SidebarItem { impl SidebarItem for SidebarItemType { fn render<'a>( &'a self, - icons: &'a IconService, current_selection: &'a SidebarSelection, _is_selected: bool, theme: &'a Theme, @@ -69,9 +67,9 @@ impl SidebarItem for SidebarItemType { }; let icon = match selection { - SidebarSelection::Today => icons.today(), - SidebarSelection::Tomorrow => icons.tomorrow(), - SidebarSelection::Upcoming => icons.upcoming(), + SidebarSelection::Today => TODAY, + SidebarSelection::Tomorrow => TOMORROW, + SidebarSelection::Upcoming => UPCOMING, _ => "", }; @@ -107,9 +105,9 @@ impl SidebarItem for SidebarItemType { }; let icon = if project.is_favorite { - icons.project_favorite() + PROJECT_FAVORITE } else { - icons.project_regular() + PROJECT_REGULAR }; let mut spans = vec![]; @@ -141,7 +139,7 @@ impl SidebarItem for SidebarItemType { }; ListItem::new(Line::from(vec![ - Span::styled(icons.label().to_string(), style), + Span::styled(LABEL, style), Span::styled(label.name.clone(), style), ])) } diff --git a/src/ui/components/task_list_component.rs b/src/ui/components/task_list_component.rs index 3251d82..6b04927 100644 --- a/src/ui/components/task_list_component.rs +++ b/src/ui/components/task_list_component.rs @@ -7,7 +7,6 @@ use crate::config::DisplayConfig; use crate::constants::{HEADER_OVERDUE, HEADER_TODAY, HEADER_TOMORROW}; use crate::entities::{label, project, section, task}; -use crate::icons::IconService; use crate::theme::Theme; use crate::ui::components::scrollbar_helper::ScrollbarHelper; use crate::ui::components::task_list_item_component::{ListItem, TaskItem, TaskListItemType}; @@ -46,7 +45,6 @@ pub struct TaskListComponent { pub sections: Vec, pub projects: Vec, pub labels: Vec, - pub icons: IconService, // Keep raw task data for building items pub tasks: Vec, pub display_config: DisplayConfig, @@ -71,7 +69,6 @@ impl TaskListComponent { sections: Vec::new(), projects: Vec::new(), labels: Vec::new(), - icons: IconService::default(), display_config: DisplayConfig::default(), scrollbar_helper: ScrollbarHelper::new(), theme: Theme::default(), @@ -344,19 +341,16 @@ impl TaskListComponent { // Calculate child count let child_count = self.get_child_task_count(&task.uuid); - // TODO: Load task-label relationships from database to populate labels - // For now, we pass an empty vec - labels need to be loaded via task_labels join - let task_labels = Vec::new(); + // Resolve the task's project name once, at build time + let project_name = self + .projects + .iter() + .find(|p| p.uuid == task.project_uuid) + .map(|p| p.name.clone()); // Create and add the task item - let task_item = TaskItem::new( - task.clone(), - depth, - child_count, - self.icons.clone(), - self.projects.clone(), - task_labels, - ); + // TODO: Load task-label relationships from database + let task_item = TaskItem::new(task.clone(), depth, child_count, project_name, Vec::new()); self.items.push(TaskListItemType::Task(Box::new(task_item))); // Find and add children @@ -612,22 +606,7 @@ impl Component for TaskListComponent { } } - fn update(&mut self, action: Action) -> Action { - match action { - Action::NextTask => { - self.next_task(); - Action::None - } - Action::PreviousTask => { - self.previous_task(); - Action::None - } - _ => action, - } - } - fn render(&mut self, f: &mut Frame, rect: Rect) { - // Calculate areas for list and scrollbar using helper let total_items = self.items.len(); // Calculate areas for list and scrollbar using helper diff --git a/src/ui/components/task_list_item_component.rs b/src/ui/components/task_list_item_component.rs index db3b66e..f90ae34 100644 --- a/src/ui/components/task_list_item_component.rs +++ b/src/ui/components/task_list_item_component.rs @@ -1,6 +1,6 @@ use crate::config::DisplayConfig; -use crate::entities::{project, task}; -use crate::icons::IconService; +use crate::entities::task; +use crate::icons::{TASK_COMPLETED, TASK_DELETED, TASK_PENDING}; use crate::theme::Theme; use crate::ui::components::badge::{create_priority_badge, create_task_badges}; use crate::utils::datetime::{format_human_date, format_human_datetime}; @@ -20,9 +20,6 @@ pub trait ListItem { /// Whether this item can be selected by the user fn is_selectable(&self) -> bool; - - /// Indentation level for hierarchical display (0 = root level) - fn indent_level(&self) -> usize; } /// Enum representing different types of items that can appear in the task list @@ -49,14 +46,6 @@ impl ListItem for TaskListItemType { Self::Separator(item) => item.is_selectable(), } } - - fn indent_level(&self) -> usize { - match self { - Self::Task(item) => item.indent_level(), - Self::Header(item) => item.indent_level(), - Self::Separator(item) => item.indent_level(), - } - } } /// A task item component @@ -65,8 +54,7 @@ pub struct TaskItem { pub task: task::Model, pub depth: usize, pub child_count: usize, - pub icons: IconService, - pub projects: Vec, + pub project_name: Option, pub labels: Vec, } @@ -75,16 +63,14 @@ impl TaskItem { task: task::Model, depth: usize, child_count: usize, - icons: IconService, - projects: Vec, + project_name: Option, labels: Vec, ) -> Self { Self { task, depth, child_count, - icons, - projects, + project_name, labels, } } @@ -104,11 +90,11 @@ impl ListItem for TaskItem { fn render(&self, selected: bool, display_config: &DisplayConfig, theme: &Theme) -> RatatuiListItem<'static> { // Choose the appropriate icon based on task state let status_icon = if self.task.is_deleted { - self.icons.task_deleted() + TASK_DELETED } else if self.task.is_completed { - self.icons.task_completed() + TASK_COMPLETED } else { - self.icons.task_pending() + TASK_PENDING }; let mut line_spans = Vec::new(); @@ -171,16 +157,10 @@ impl ListItem for TaskItem { line_spans.push(Span::styled(progress_text, progress_style)); } - // Project display (with optional colors) - if let Some(project) = self.projects.iter().find(|p| p.uuid == self.task.project_uuid) { + // Project tag (resolved at item build time) + if let Some(name) = &self.project_name { line_spans.push(Span::raw(" ")); - let project_style = if display_config.show_project_colors { - // Use project color if available, otherwise the theme's project tag color - Style::default().fg(theme.project_tag) - } else { - Style::default().fg(theme.project_tag) - }; - line_spans.push(Span::styled(format!("#{}", project.name), project_style)); + line_spans.push(Span::styled(format!("#{name}"), Style::default().fg(theme.project_tag))); } // Due date/datetime display @@ -201,7 +181,6 @@ impl ListItem for TaskItem { if display_config.show_durations || display_config.show_labels { let metadata_badges = create_task_badges( self.task.is_recurring, - self.task.due_date.is_some() || self.task.deadline.is_some(), if display_config.show_durations { self.task.duration.as_deref() } else { @@ -217,7 +196,6 @@ impl ListItem for TaskItem { } } - // Add description excerpt if available and configured to show // Add description excerpt if available and configured to show if display_config.show_descriptions { if let Some(desc) = &self.task.description { @@ -241,10 +219,6 @@ impl ListItem for TaskItem { fn is_selectable(&self) -> bool { true } - - fn indent_level(&self) -> usize { - self.depth - } } /// A header item component (for sections, projects, etc.) @@ -272,10 +246,6 @@ impl ListItem for HeaderItem { fn is_selectable(&self) -> bool { false } - - fn indent_level(&self) -> usize { - self.indent - } } /// A separator item component @@ -304,8 +274,4 @@ impl ListItem for SeparatorItem { fn is_selectable(&self) -> bool { false } - - fn indent_level(&self) -> usize { - self.indent - } } diff --git a/src/ui/core/actions.rs b/src/ui/core/actions.rs index e7a6e0b..ad3bb81 100644 --- a/src/ui/core/actions.rs +++ b/src/ui/core/actions.rs @@ -31,8 +31,6 @@ pub enum LoadKind { pub enum Action { // Navigation NavigateToSidebar(SidebarSelection), - NextTask, - PreviousTask, // Task operations CompleteTask(Uuid), @@ -97,7 +95,6 @@ pub enum Action { // UI operations ToggleSidebar, ShowHelp(bool), - ShowDebug(bool), ShowDialog(DialogType), HideDialog, HelpScrollUp, diff --git a/src/ui/core/event_handler.rs b/src/ui/core/event_handler.rs index 60ca4a6..9bb8af0 100644 --- a/src/ui/core/event_handler.rs +++ b/src/ui/core/event_handler.rs @@ -1,40 +1,25 @@ use crossterm::event::{poll, Event, KeyEvent, MouseEvent}; -use tokio::time::{interval, Duration}; +use tokio::time::Duration; -pub struct EventHandler { - #[allow(dead_code)] - tick_interval: tokio::time::Interval, - #[allow(dead_code)] - render_interval: tokio::time::Interval, -} - -impl EventHandler { - pub fn new() -> Self { - Self { - tick_interval: interval(Duration::from_millis(100)), // 10 Hz for application ticks - render_interval: interval(Duration::from_millis(16)), // ~60 FPS render rate - } - } - - pub async fn next_event(&mut self) -> anyhow::Result { - // Check for terminal events without blocking first - if poll(Duration::from_millis(0))? { - match crossterm::event::read()? { - Event::Key(key) => { - return Ok(EventType::Key(key)); - } - Event::Mouse(mouse) => { - return Ok(EventType::Mouse(mouse)); - } - Event::Resize(w, h) => return Ok(EventType::Resize(w, h)), - _ => return Ok(EventType::Other), +/// Wait for the next terminal event, or return a `Tick` when the terminal is idle. +pub async fn next_event() -> anyhow::Result { + // Check for terminal events without blocking first + if poll(Duration::from_millis(0))? { + match crossterm::event::read()? { + Event::Key(key) => { + return Ok(EventType::Key(key)); + } + Event::Mouse(mouse) => { + return Ok(EventType::Mouse(mouse)); } + Event::Resize(w, h) => return Ok(EventType::Resize(w, h)), + _ => return Ok(EventType::Other), } - - // If no immediate event, wait a bit and return tick - tokio::time::sleep(Duration::from_millis(100)).await; - Ok(EventType::Tick) } + + // If no immediate event, wait a bit and return tick + tokio::time::sleep(Duration::from_millis(100)).await; + Ok(EventType::Tick) } #[derive(Debug, Clone)] @@ -43,12 +28,5 @@ pub enum EventType { Mouse(MouseEvent), Resize(u16, u16), Tick, - Render, Other, } - -impl Default for EventHandler { - fn default() -> Self { - Self::new() - } -} diff --git a/src/ui/core/mod.rs b/src/ui/core/mod.rs index a8f6f47..d7d7457 100644 --- a/src/ui/core/mod.rs +++ b/src/ui/core/mod.rs @@ -18,7 +18,7 @@ //! //! 1. **Components** implement the [`Component`] trait for consistent rendering //! 2. **Actions** define state transitions and user interactions -//! 3. **Events** are processed through the [`EventHandler`] system +//! 3. **Events** are read by the event loop and dispatched as actions //! 4. **Tasks** are managed asynchronously via the [`TaskManager`] //! //! This architecture ensures clean separation of concerns and makes the codebase @@ -32,7 +32,6 @@ pub mod operations; pub mod task_manager; // Re-export core types for easier access from other modules -pub use actions::{Action, DialogType, LoadKind, SidebarSelection}; +pub use actions::{LoadKind, SidebarSelection}; pub use component::Component; -pub use event_handler::{EventHandler, EventType}; -pub use task_manager::{TaskId, TaskKind, TaskManager, TaskResult}; +pub use event_handler::EventType; diff --git a/src/ui/core/task_manager.rs b/src/ui/core/task_manager.rs index 68aa3cb..5daf7cf 100644 --- a/src/ui/core/task_manager.rs +++ b/src/ui/core/task_manager.rs @@ -1,5 +1,5 @@ use super::actions::{Action, LoadKind, SidebarSelection}; -use crate::sync::{SyncService, SyncStatus}; +use crate::sync::SyncService; use std::collections::HashMap; use tokio::sync::mpsc; use tokio::task::JoinHandle; @@ -19,29 +19,10 @@ pub enum TaskKind { #[derive(Debug)] pub struct BackgroundTask { - pub id: TaskId, - pub handle: JoinHandle>, + pub handle: JoinHandle<()>, pub kind: TaskKind, } -#[derive(Debug, Clone)] -pub enum TaskResult { - SyncCompleted(SyncStatus), - SyncFailed(String), - TaskOperationCompleted(String), - DataLoadCompleted { - projects: Vec, - labels: Vec, - sections: Vec, - tasks: Vec, - }, - SearchCompleted { - query: String, - results: Vec, - }, - Other(String), -} - pub struct TaskManager { tasks: HashMap, next_task_id: TaskId, @@ -73,19 +54,10 @@ impl TaskManager { // Send sync started notification let _ = action_sender.send(Action::StartSync); - match sync_service.force_sync().await { - Ok(status) => { - let result = TaskResult::SyncCompleted(status.clone()); - let _ = action_sender.send(Action::SyncCompleted(status)); - Ok(result) - } - Err(e) => { - let error_msg = e.to_string(); - let result = TaskResult::SyncFailed(error_msg.clone()); - let _ = action_sender.send(Action::SyncFailed(error_msg)); - Ok(result) - } - } + let _ = match sync_service.force_sync().await { + Ok(status) => action_sender.send(Action::SyncCompleted(status)), + Err(e) => action_sender.send(Action::SyncFailed(e.to_string())), + }; }); self.insert(task_id, handle, TaskKind::Sync); @@ -107,24 +79,18 @@ impl TaskManager { let handle = tokio::spawn(async move { match operation().await { - Ok(message) => { - let result = TaskResult::TaskOperationCompleted(message.clone()); + Ok(_message) => { // Send refresh action to update UI with latest data from database let _ = action_sender.send(Action::RefreshData); if let Some(action) = on_success { let _ = action_sender.send(action); } - - Ok(result) } Err(e) => { - let error_msg = format!("Operation failed: {}", e); - let result = TaskResult::Other(error_msg.clone()); let _ = action_sender.send(Action::ShowDialog(crate::ui::core::actions::DialogType::Error( - error_msg, + format!("Operation failed: {e}"), ))); - Ok(result) } } }); @@ -133,27 +99,12 @@ impl TaskManager { task_id } - /// Check for completed tasks and clean them up - pub fn cleanup_finished_tasks(&mut self) -> Vec<(TaskId, anyhow::Result)> { - let mut completed = Vec::new(); - let mut to_remove = Vec::new(); - - for (task_id, task) in &mut self.tasks { - if task.handle.is_finished() { - to_remove.push(*task_id); - } - } - - for task_id in to_remove { - if let Some(_task) = self.tasks.remove(&task_id) { - // Since the task is finished, we'll just mark it as completed - // The actual result was already sent via the action channel - let result = Ok(TaskResult::Other("Task completed".to_string())); - completed.push((task_id, result)); - } - } - - completed + /// Drop the bookkeeping for tasks that have finished, returning how many went. + /// Results travel over the action channel, so there is nothing here to collect. + pub fn cleanup_finished_tasks(&mut self) -> usize { + let before = self.tasks.len(); + self.tasks.retain(|_, task| !task.handle.is_finished()); + before - self.tasks.len() } /// Check if any sync tasks are currently running @@ -200,13 +151,6 @@ impl TaskManager { } }; - let result = TaskResult::DataLoadCompleted { - projects: projects.clone(), - labels: labels.clone(), - sections: sections.clone(), - tasks: tasks.clone(), - }; - let _ = action_sender.send(Action::DataLoaded { kind, projects, @@ -214,15 +158,11 @@ impl TaskManager { sections, tasks, }); - - Ok(result) } (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => { - let error_msg = format!("Failed to load data: {}", e); let _ = action_sender.send(Action::ShowDialog(crate::ui::core::actions::DialogType::Error( - error_msg.clone(), + format!("Failed to load data: {e}"), ))); - Ok(TaskResult::Other(error_msg)) } } }); @@ -241,20 +181,10 @@ impl TaskManager { let handle = tokio::spawn(async move { match sync_service.search_tasks(&query).await { Ok(results) => { - let result = TaskResult::SearchCompleted { - query: query.clone(), - results: results.clone(), - }; - let _ = action_sender.send(Action::SearchResultsLoaded { query, results }); - - Ok(result) - } - Err(e) => { - let error_msg = format!("Failed to search tasks: {}", e); - // Don't show error dialog for search failures, just log silently - Ok(TaskResult::Other(error_msg)) } + // Search failures stay silent: no dialog, no toast. + Err(e) => log::warn!("Failed to search tasks: {e}"), } }); @@ -262,8 +192,8 @@ impl TaskManager { task_id } - fn insert(&mut self, id: TaskId, handle: JoinHandle>, kind: TaskKind) { - self.tasks.insert(id, BackgroundTask { id, handle, kind }); + fn insert(&mut self, id: TaskId, handle: JoinHandle<()>, kind: TaskKind) { + self.tasks.insert(id, BackgroundTask { handle, kind }); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ab1f00f..a41cb17 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -54,7 +54,5 @@ pub mod core; pub mod layout; pub(crate) mod renderer; -// Re-export main UI types for external use -pub use app_component::AppComponent; -pub use layout::LayoutManager; +// `renderer` is crate-private, so this is how main reaches the event loop. pub use renderer::run_app; diff --git a/src/ui/renderer.rs b/src/ui/renderer.rs index 5570dc6..f15468b 100644 --- a/src/ui/renderer.rs +++ b/src/ui/renderer.rs @@ -2,7 +2,8 @@ use crate::config::Config; use crate::sync::SyncService; use crate::theme::ThemeWarning; use crate::ui::app_component::AppComponent; -use crate::ui::core::{Component, EventHandler, EventType}; +use crate::ui::core::event_handler::next_event; +use crate::ui::core::{Component, EventType}; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture}, execute, @@ -13,7 +14,6 @@ use ratatui::{ Terminal, }; use std::io; -use tokio::time::{interval, Duration}; /// Enhanced async event loop with proper background task support pub async fn run_app( @@ -39,22 +39,11 @@ pub async fn run_app( // Initialize application components let mut app = AppComponent::new(sync_service, config.clone(), theme_warnings); - let mut event_handler = EventHandler::new(); // Start initial sync automatically app.trigger_initial_sync(); - // Create intervals for periodic tasks - let mut cleanup_interval = interval(Duration::from_secs(5)); // Clean up finished tasks every 5 seconds - let mut render_interval = interval(Duration::from_millis(16)); // ~60 FPS rendering - let result = run_app_loop( - &mut terminal, - &mut app, - &mut event_handler, - &mut cleanup_interval, - &mut render_interval, - ) - .await; + let result = run_app_loop(&mut terminal, &mut app).await; // Restore terminal disable_raw_mode()?; @@ -70,13 +59,7 @@ pub async fn run_app( result } -async fn run_app_loop( - terminal: &mut Terminal, - app: &mut AppComponent, - event_handler: &mut EventHandler, - _cleanup_interval: &mut tokio::time::Interval, - _render_interval: &mut tokio::time::Interval, -) -> anyhow::Result<()> +async fn run_app_loop(terminal: &mut Terminal, app: &mut AppComponent) -> anyhow::Result<()> where B::Error: std::error::Error + Send + Sync + 'static, { @@ -90,7 +73,7 @@ where } // Simplified event loop to avoid deadlocks - let event_result = event_handler.next_event().await?; + let event_result = next_event().await?; match event_result { EventType::Key(_) | EventType::Mouse(_) | EventType::Resize(_, _) => { @@ -120,9 +103,6 @@ where needs_render = true; } } - EventType::Render => { - needs_render = true; - } EventType::Other => { // Handle other event types if needed } diff --git a/tests/icons_test.rs b/tests/icons_test.rs deleted file mode 100644 index adc5c56..0000000 --- a/tests/icons_test.rs +++ /dev/null @@ -1,46 +0,0 @@ -use terminalist::icons::*; - -#[test] -fn test_default_theme() { - let service = IconService::default(); - assert_eq!(service.theme(), IconTheme::Unicode); -} - -#[test] -fn test_emoji_icons() { - let service = IconService::new(IconTheme::Emoji); - assert_eq!(service.task_pending(), "🔳"); - assert_eq!(service.task_completed(), "✅"); - assert_eq!(service.task_deleted(), "❌"); -} - -#[test] -fn test_unicode_icons() { - let service = IconService::new(IconTheme::Unicode); - assert_eq!(service.task_pending(), "☐"); - assert_eq!(service.task_completed(), "☒"); - assert_eq!(service.task_deleted(), "✗"); -} - -#[test] -fn test_ascii_icons() { - let service = IconService::new(IconTheme::Ascii); - assert_eq!(service.task_pending(), "[ ]"); - assert_eq!(service.task_completed(), "[X]"); - assert_eq!(service.task_deleted(), "[D]"); -} - -#[test] -fn test_today_tomorrow_icons() { - let emoji_service = IconService::new(IconTheme::Emoji); - assert_eq!(emoji_service.today(), "📅"); - assert_eq!(emoji_service.tomorrow(), "🗓️"); - - let unicode_service = IconService::new(IconTheme::Unicode); - assert_eq!(unicode_service.today(), "◷"); - assert_eq!(unicode_service.tomorrow(), "◶"); - - let ascii_service = IconService::new(IconTheme::Ascii); - assert_eq!(ascii_service.today(), "@"); - assert_eq!(ascii_service.tomorrow(), "+"); -} diff --git a/tests/logger.rs b/tests/logger.rs index 8c154fe..5303fd5 100644 --- a/tests/logger.rs +++ b/tests/logger.rs @@ -1,15 +1,5 @@ use terminalist::logger; -#[test] -fn test_memory_logs() { - // Clear any existing logs - logger::clear_memory_logs(); - - // Test that we can get empty logs - let logs = logger::get_memory_logs(); - assert!(logs.is_empty()); -} - #[test] fn test_log_file_path() { // Test that we can get the log file path diff --git a/tests/ui/components/sidebar_component.rs b/tests/ui/components/sidebar_component.rs index 8f3f51b..6d798ab 100644 --- a/tests/ui/components/sidebar_component.rs +++ b/tests/ui/components/sidebar_component.rs @@ -1,7 +1,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use terminalist::entities::project; use terminalist::ui::components::SidebarComponent; -use terminalist::ui::core::{Action, Component, SidebarSelection}; +use terminalist::ui::core::{actions::Action, Component, SidebarSelection}; use uuid::Uuid; fn project_model(name: &str) -> project::Model {