diff --git a/Cargo.lock b/Cargo.lock index b11e8eb..45534c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,6 +75,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -1425,6 +1475,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "command-fds" version = "0.3.2" @@ -2094,6 +2150,29 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equator" version = "0.4.2" @@ -3649,6 +3728,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -3673,6 +3758,30 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +[[package]] +name = "jiff" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4427,6 +4536,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oo7" version = "0.5.0" @@ -4734,6 +4849,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" +[[package]] +name = "portable-atomic" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postage" version = "0.5.0" @@ -6370,7 +6500,9 @@ version = "0.1.0" dependencies = [ "chrono", "dirs 6.0.0", + "env_logger", "gpui", + "log", "serde", "serde_json", "taskchampion", @@ -7031,6 +7163,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.19.0" diff --git a/Cargo.toml b/Cargo.toml index 5cbaff6..64cc33e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,9 @@ license = "MIT OR Apache-2.0" [dependencies] chrono = { version = "0.4.42", features = ["serde"] } dirs = "6.0.0" +env_logger = "0.11.0" gpui = "0.2.2" +log = "0.4.22" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.147" taskchampion = "2.0.3" diff --git a/src/app.rs b/src/app.rs index 883f3b2..49cb846 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,7 @@ use gpui::prelude::*; use crate::models::{FilterState, ProjectTree}; +use crate::task::{TaskOverview, TaskService}; use crate::theme::ActiveTheme; use crate::view::sidebar::{Sidebar, TagItem}; use crate::view::status_bar::StatusBar; @@ -10,6 +11,7 @@ pub(crate) struct App { sidebar: gpui::Entity, filter_state: gpui::Entity, status_bar: gpui::Entity, + task_service: TaskService, } impl gpui::Render for App { @@ -60,56 +62,83 @@ impl App { app.new(|cx: &mut gpui::Context<'_, App>| { let filter_state = cx.new(|_cx| FilterState::new()); + log::info!("Initializing TaskService"); + let mut task_service = match TaskService::new() { + Ok(service) => { + log::info!("TaskService initialized successfully"); + service + } + Err(e) => { + log::error!("Failed to initialize TaskService: {:?}", e); + panic!("Cannot continue without TaskService"); + } + }; + + log::info!("Loading overview (tasks, projects, tags)"); + let overview = task_service.get_overview().unwrap_or_else(|e| { + log::error!("Failed to get overview: {:?}", e); + TaskOverview { + tasks: vec![], + projects: vec![], + tags: vec![], + total_tasks: 0, + pending_tasks: 0, + completed_tasks: 0, + } + }); + + log::info!( + "Loaded {} tasks, {} projects, {} tags", + overview.total_tasks, + overview.projects.len(), + overview.tags.len() + ); + + for (i, task) in overview.tasks.iter().take(5).enumerate() { + log::debug!( + "Task {}: {} (project: {:?}, tags: {:?})", + i + 1, + task.description, + task.project, + task.tags + ); + } + if overview.tasks.len() > 5 { + log::debug!("... and {} more tasks", overview.tasks.len() - 5); + } + let mut project_tree = ProjectTree::new(); - project_tree.build_from_projects(&[ - ("Work.Backend.API".to_string(), 5), - ("Work.Backend.DB".to_string(), 7), - ("Work.Frontend.React".to_string(), 3), - ("Work.Frontend.Styling".to_string(), 2), - ("Home.Kitchen".to_string(), 4), - ("Home.Garden".to_string(), 2), - ("ignis.v0.1.phase0".to_string(), 15), - ("free-ai".to_string(), 2), - ]); + project_tree.build_from_projects(&overview.projects); - project_tree.expand_path("Work"); - project_tree.expand_path("Work.Backend"); + let tags: Vec = overview + .tags + .into_iter() + .map(|(name, task_count)| { + log::debug!("Tag: {} ({} tasks)", name, task_count); + TagItem { name, task_count } + }) + .collect(); - let tags = vec![ - TagItem { - name: "parser".to_string(), - task_count: 8, - }, - TagItem { - name: "cli".to_string(), - task_count: 5, - }, - TagItem { - name: "testing".to_string(), - task_count: 6, - }, - TagItem { - name: "diagnostics".to_string(), - task_count: 7, - }, - TagItem { - name: "analyzer".to_string(), - task_count: 3, - }, - ]; + log::info!("Projects and tags loaded successfully"); let status_bar = cx.new(|cx| StatusBar::new(cx)); let sidebar = cx.new(|cx| { Sidebar::new(project_tree, tags, filter_state.clone(), cx) .on_filter_change(|filter, _window, _cx| { - println!("Filter changed:"); + log::info!("Filter changed"); if let Some(ref project) = filter.selected_project { - println!(" Project: {}", project); + log::debug!("Selected project: {}", project); } else { - println!(" Project: All"); + log::debug!("Selected project: All"); + } + if filter.active_tags.is_empty() { + log::debug!("Active tags: None"); + } else { + let tags_list: Vec<&str> = + filter.active_tags.iter().map(|s| s.as_str()).collect(); + log::debug!("Active tags: {}", tags_list.join(", ")); } - println!(" Active tags: {:?}", filter.active_tags); }) }); @@ -117,6 +146,7 @@ impl App { sidebar, filter_state, status_bar, + task_service, } }) }, diff --git a/src/main.rs b/src/main.rs index bf4d342..444243b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,5 +8,10 @@ mod theme; mod view; fn main() { + env_logger::Builder::from_default_env() + .filter_level(log::LevelFilter::Debug) + .init(); + + log::info!("Starting Task Warrior GPUI"); App::run(); } diff --git a/src/task/error.rs b/src/task/error.rs index 1d8201d..8e52dd0 100644 --- a/src/task/error.rs +++ b/src/task/error.rs @@ -1,7 +1,9 @@ use std::fmt; +#[derive(Debug)] pub enum TaskError { Storage(String), + Config(String), NotFound(uuid::Uuid), InvalidTag(String), InvalidProject(String), @@ -16,6 +18,7 @@ impl fmt::Display for TaskError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { TaskError::Storage(msg) => write!(f, "Storage error: {}", msg), + TaskError::Config(msg) => write!(f, "Configuration error: {}", msg), TaskError::NotFound(id) => write!(f, "Task not found: {}", id), TaskError::InvalidTag(tag) => write!(f, "Invalid tag: {}", tag), TaskError::InvalidProject(project) => write!(f, "Invalid project: {}", project), diff --git a/src/task/mod.rs b/src/task/mod.rs index a6f1414..e0187cc 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -5,5 +5,5 @@ pub mod service; pub use error::{TaskError, TaskResult}; pub use filter::{DueDateFilter, TagsFilterMode, TaskFilter}; -pub use model::{Task, TaskAnnotation, TaskPriority, TaskStatus, TaskUpdate}; +pub use model::{Task, TaskAnnotation, TaskOverview, TaskPriority, TaskStatus, TaskUpdate}; pub use service::{SyncResult, TaskService}; diff --git a/src/task/model.rs b/src/task/model.rs index 9bcbc04..177b507 100644 --- a/src/task/model.rs +++ b/src/task/model.rs @@ -214,3 +214,13 @@ pub struct TaskUpdate { pub annotations: Option>, pub dependencies: Option>, } + +#[derive(Debug, Clone)] +pub struct TaskOverview { + pub tasks: Vec, + pub projects: Vec<(String, usize)>, + pub tags: Vec<(String, usize)>, + pub total_tasks: usize, + pub pending_tasks: usize, + pub completed_tasks: usize, +} diff --git a/src/task/service.rs b/src/task/service.rs index 8e494ae..967785c 100644 --- a/src/task/service.rs +++ b/src/task/service.rs @@ -9,34 +9,85 @@ use uuid::Uuid; use super::error::{TaskError, TaskResult}; use super::filter::TaskFilter; -use super::model::{Task, TaskPriority, TaskStatus}; +use super::model::{Task, TaskOverview, TaskStatus}; pub struct TaskService { replica: Replica, taskdb_dir: PathBuf, } +fn read_taskrc_config() -> TaskResult { + if let Ok(taskdata) = std::env::var("TASKDATA") { + log::info!("Using TASKDATA env var: {}", taskdata); + return Ok(PathBuf::from(taskdata)); + } + + let taskrc_path = if let Ok(taskrc) = std::env::var("TASKRC") { + PathBuf::from(taskrc) + } else { + dirs::home_dir() + .ok_or_else(|| TaskError::Config("Cannot find home directory".into()))? + .join(".config/task/taskrc") + }; + + log::debug!("Looking for taskrc at: {:?}", taskrc_path); + + if taskrc_path.exists() { + let content = std::fs::read_to_string(&taskrc_path) + .map_err(|e| TaskError::Config(format!("Failed to read taskrc: {}", e)))?; + + for line in content.lines() { + let line = line.trim(); + if line.starts_with("data.location=") { + let path = line.strip_prefix("data.location=").unwrap().trim(); + + let expanded = if path.starts_with("~") { + let home = dirs::home_dir() + .ok_or_else(|| TaskError::Config("Cannot expand ~ in path".into()))?; + PathBuf::from(path.replacen("~", home.to_str().unwrap(), 1)) + } else { + PathBuf::from(path) + }; + + log::info!("Found data.location in taskrc: {:?}", expanded); + return Ok(expanded); + } + } + } + + log::warn!("No taskrc found or data.location not set, using default ~/.task"); + let default = dirs::home_dir() + .ok_or_else(|| TaskError::Config("Cannot find home directory".into()))? + .join(".task"); + Ok(default) +} + impl TaskService { pub fn new() -> TaskResult { - let taskdb_dir = dirs::home_dir() - .ok_or_else(|| TaskError::Storage("Cannot find home directory".into()))? - .join(".task"); + let taskdb_dir = read_taskrc_config()?; + + log::debug!("TaskService: Using taskdb_dir: {:?}", taskdb_dir); + log::debug!("TaskService: Directory exists: {}", taskdb_dir.exists()); Self::with_path(taskdb_dir) } pub fn with_path(taskdb_dir: PathBuf) -> TaskResult { + log::debug!("TaskService: Initializing with path: {:?}", taskdb_dir); + let storage = StorageConfig::OnDisk { taskdb_dir: taskdb_dir.clone(), create_if_missing: true, access_mode: AccessMode::ReadWrite, }; - let replica = Replica::new( - storage - .into_storage() - .map_err(|e| TaskError::Storage(e.to_string()))?, - ); + log::debug!("TaskService: Creating storage..."); + let replica = Replica::new(storage.into_storage().map_err(|e| { + log::error!("TaskService: Storage creation failed: {}", e); + TaskError::Storage(e.to_string()) + })?); + + log::debug!("TaskService: Replica created successfully"); Ok(Self { replica, @@ -92,10 +143,17 @@ impl TaskService { } pub fn get_all_tasks(&mut self) -> TaskResult> { - let all = self - .replica - .all_tasks() - .map_err(|e| TaskError::Storage(e.to_string()))?; + log::debug!("TaskService::get_all_tasks: Fetching all tasks from replica"); + let all = self.replica.all_tasks().map_err(|e| { + log::error!("TaskService::get_all_tasks: Failed to get all tasks: {}", e); + TaskError::Storage(e.to_string()) + })?; + + log::debug!( + "TaskService::get_all_tasks: Found {} tasks in storage", + all.len() + ); + let working_set = self .replica .working_set() @@ -110,9 +168,70 @@ impl TaskService { }) .collect(); + log::debug!( + "TaskService::get_all_tasks: Converted to {} Task objects", + tasks.len() + ); Ok(tasks) } + pub fn get_overview(&mut self) -> TaskResult { + log::info!("TaskService::get_overview: Fetching complete overview"); + + let tasks = self.get_all_tasks()?; + + log::debug!("Processing {} tasks for overview", tasks.len()); + + let mut project_counts: HashMap = HashMap::new(); + let mut tag_counts: HashMap = HashMap::new(); + let mut pending_count = 0; + let mut completed_count = 0; + + for task in &tasks { + match task.status { + TaskStatus::Pending => pending_count += 1, + TaskStatus::Completed => completed_count += 1, + _ => {} + } + + if matches!(task.status, TaskStatus::Pending) { + if let Some(project) = &task.project { + *project_counts.entry(project.clone()).or_insert(0) += 1; + } + + for tag in &task.tags { + *tag_counts.entry(tag.clone()).or_insert(0) += 1; + } + } + } + + let mut projects: Vec<(String, usize)> = project_counts.into_iter().collect(); + projects.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase())); + + let mut tags: Vec<(String, usize)> = tag_counts.into_iter().collect(); + tags.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase())); + + let total_tasks = tasks.len(); + + log::info!( + "Overview: {} tasks ({} pending, {} completed), {} projects, {} tags", + total_tasks, + pending_count, + completed_count, + projects.len(), + tags.len() + ); + + Ok(TaskOverview { + tasks, + projects, + tags, + total_tasks, + pending_tasks: pending_count, + completed_tasks: completed_count, + }) + } + pub fn get_filtered_tasks(&mut self, filter: &TaskFilter) -> TaskResult> { let all = self.get_all_tasks()?; Ok(filter.apply(&all)) @@ -338,7 +457,13 @@ impl TaskService { } pub fn list_tags(&mut self) -> TaskResult> { + log::debug!("TaskService::list_tags: Getting all tasks"); let all_tasks = self.get_all_tasks()?; + log::debug!( + "TaskService::list_tags: Processing {} tasks for tags", + all_tasks.len() + ); + let mut tag_counts: HashMap = HashMap::new(); for task in all_tasks { @@ -349,6 +474,11 @@ impl TaskService { } } + log::debug!( + "TaskService::list_tags: Found {} unique tags", + tag_counts.len() + ); + let mut stats: Vec<(String, usize)> = tag_counts.into_iter().collect(); stats.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase())); @@ -357,8 +487,14 @@ impl TaskService { } pub fn list_projects(&mut self) -> TaskResult> { + log::debug!("TaskService::list_projects: Getting all tasks"); let all_tasks = self.get_all_tasks()?; - let mut project_counts: HashMap = HashMap::new(); // (total, pending) + log::debug!( + "TaskService::list_projects: Processing {} tasks for projects", + all_tasks.len() + ); + + let mut project_counts: HashMap = HashMap::new(); for task in all_tasks { if let Some(project) = &task.project { @@ -370,6 +506,11 @@ impl TaskService { } } + log::debug!( + "TaskService::list_projects: Found {} unique projects", + project_counts.len() + ); + let stats: Vec<(String, usize, usize)> = project_counts .into_iter() .map(|(name, (task_count, pending_count))| (name, task_count, pending_count)) @@ -379,11 +520,17 @@ impl TaskService { } pub fn get_projects_for_tree(&mut self) -> TaskResult> { + log::debug!("TaskService::get_projects_for_tree: Getting project stats"); let stats = self.list_projects()?; - Ok(stats + let result: Vec<(String, usize)> = stats .into_iter() .map(|(name, _total, pending)| (name, pending)) - .collect()) + .collect(); + log::debug!( + "TaskService::get_projects_for_tree: Returning {} projects", + result.len() + ); + Ok(result) } pub fn add_annotation(&mut self, uuid: Uuid, description: String) -> TaskResult { diff --git a/src/view/sidebar.rs b/src/view/sidebar.rs index dffa736..48b19ac 100644 --- a/src/view/sidebar.rs +++ b/src/view/sidebar.rs @@ -288,57 +288,74 @@ impl Render for Sidebar { .bg(theme.background) .child( div() - .px_3() - .py_2() - .border_b_1() - .border_color(theme.border) + .flex() + .flex_col() + .h(gpui::relative(0.5)) + .overflow_hidden() .child( div() - .text_sm() - .font_weight(gpui::FontWeight::BOLD) - .text_color(theme.foreground) - .child("PROJECTS"), + .px_3() + .py_2() + .border_b_1() + .border_color(theme.border) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::BOLD) + .text_color(theme.foreground) + .child("PROJECTS"), + ), + ) + .child( + div() + .id("sidebar-projects") + .flex() + .flex_col() + .flex_1() + .min_h_0() + .py_2() + .overflow_y_scroll() + .scrollbar_width(gpui::px(6.0)) + .children(projects), ), ) .child( div() - .id("sidebar-projects") - .flex() - .flex_col() - .flex_1() - .py_2() - .overflow_y_scroll() - .scrollbar_width(gpui::px(6.0)) - .children(projects), + .h_px() + .bg(theme.border), ) - .child(div().px_3().py_2().child(Divider::new( - theme.border, - crate::components::divider::DividerDirection::Horizontal, - ))) .child( div() - .px_3() - .py_2() - .border_b_1() - .border_color(theme.border) + .flex() + .flex_col() + .h(gpui::relative(0.5)) + .overflow_hidden() .child( div() - .text_sm() - .font_weight(gpui::FontWeight::BOLD) - .text_color(theme.foreground) - .child("TAGS"), + .px_3() + .py_2() + .border_b_1() + .border_color(theme.border) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::BOLD) + .text_color(theme.foreground) + .child("TAGS"), + ), + ) + .child( + div() + .id("sidebar-tags") + .flex() + .flex_col() + .flex_1() + .min_h_0() + .py_2() + .overflow_y_scroll() + .scrollbar_width(gpui::px(6.0)) + .children(tags), ), - ) - .child( - div() - .id("sidebar-tags") - .flex() - .flex_col() - .flex_1() - .py_2() - .overflow_y_scroll() - .scrollbar_width(gpui::px(6.0)) - .children(tags), ), ) }