feat: Integrate TaskWarrior with config reading and optimized data

loading

- Read data.location from ~/.config/task/taskrc (supports
  TASKDATA/TASKRC env vars)
- Add get_overview() method to fetch tasks, projects, tags in one call
  (3→1 requests)
- Replace mock data with real TaskWarrior data
- Add logging with env_logger
- Improve tag logging output
This commit is contained in:
Ignacio Perez
2025-12-25 16:28:29 -03:00
parent 33769a4686
commit 8bfb0467fb
9 changed files with 446 additions and 94 deletions
+68 -38
View File
@@ -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<Sidebar>,
filter_state: gpui::Entity<FilterState>,
status_bar: gpui::Entity<StatusBar>,
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<TagItem> = 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,
}
})
},
+5
View File
@@ -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();
}
+3
View File
@@ -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),
+1 -1
View File
@@ -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};
+10
View File
@@ -214,3 +214,13 @@ pub struct TaskUpdate {
pub annotations: Option<Vec<String>>,
pub dependencies: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct TaskOverview {
pub tasks: Vec<Task>,
pub projects: Vec<(String, usize)>,
pub tags: Vec<(String, usize)>,
pub total_tasks: usize,
pub pending_tasks: usize,
pub completed_tasks: usize,
}
+163 -16
View File
@@ -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<PathBuf> {
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<Self> {
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<Self> {
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<Vec<Task>> {
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<TaskOverview> {
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<String, usize> = HashMap::new();
let mut tag_counts: HashMap<String, usize> = 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<Vec<Task>> {
let all = self.get_all_tasks()?;
Ok(filter.apply(&all))
@@ -338,7 +457,13 @@ impl TaskService {
}
pub fn list_tags(&mut self) -> TaskResult<Vec<(String, usize)>> {
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<String, usize> = 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<Vec<(String, usize, usize)>> {
log::debug!("TaskService::list_projects: Getting all tasks");
let all_tasks = self.get_all_tasks()?;
let mut project_counts: HashMap<String, (usize, usize)> = HashMap::new(); // (total, pending)
log::debug!(
"TaskService::list_projects: Processing {} tasks for projects",
all_tasks.len()
);
let mut project_counts: HashMap<String, (usize, usize)> = 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<Vec<(String, usize)>> {
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<Task> {
+56 -39
View File
@@ -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),
),
)
}