Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
924 changes: 848 additions & 76 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ edition = "2024"

[dependencies]
chrono = "0.4.42"
crossterm = "0.29.0"
crossterm = "0.28.1"
dirs = "6.0.0"
ratatui = "0.29.0"
tui-input = "0.14.0"
ratatui = "0.30.0"
tui-input = "0.15.0"
uuid = { version = "1.19.0", features = ["v4"] }
rusqlite = { version = "0.38.0", features = ["bundled"] }
tui-tree-widget = "0.24.0"
47 changes: 47 additions & 0 deletions src/actions/archive_workspace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use chrono::Utc;
use uuid::Uuid;

use crate::{
app::App,
db::repositories::{WorkspaceRepository, TaskRepository},
};

pub fn archive_workspace(app: &mut App, option_idx: Option<usize>, workspace_id: Uuid) {
if option_idx != Some(0) {
return;
}

let workspace = match app.workspaces.iter_mut().find(|s| s.id == workspace_id) {
Some(s) => s,
None => return,
};

workspace.archived = !workspace.archived;
workspace.archived_at = if workspace.archived {
Some(Utc::now())
} else {
None
};
workspace.updated_at = Some(Utc::now());

if let Err(e) = WorkspaceRepository::update(&app.db.connection, workspace) {
app.error = Some(e.to_string());
return;
}

let is_archived = workspace.archived;

for task in app.tasks.iter_mut().filter(|t| t.workspace_id == Some(workspace_id)) {
task.archived = is_archived;
task.archived_at = if is_archived { Some(Utc::now()) } else { None };
task.updated_at = Some(Utc::now());

if let Err(e) = TaskRepository::update(&app.db.connection, task) {
app.error = Some(e.to_string());
return;
}
}

app.selected_tasks.clear();
app.state.workspaces_tree_state.select_first();
}
6 changes: 4 additions & 2 deletions src/actions/create_task.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use uuid::Uuid;

use crate::{app::App, db::repositories::TaskRepository, models};

pub fn create_task(app: &mut App, title: String) {
let new_task = models::Task::new(title);
pub fn create_task(app: &mut App, title: String, workspace_id: Option<Uuid>) {
let new_task = models::Task::new(title, workspace_id);

if let Err(e) = TaskRepository::create(&app.db.connection, &new_task) {
app.error = Some(e.to_string());
Expand Down
18 changes: 18 additions & 0 deletions src/actions/create_workspace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use crate::{app::App, db::repositories::WorkspaceRepository, models};

pub fn create_workspace(app: &mut App, title: String) {
let new_workspace = models::Workspace::new(title);
let workspace_id = new_workspace.id.to_string();

if let Err(e) = WorkspaceRepository::create(&app.db.connection, &new_workspace) {
app.error = Some(e.to_string());
return;
};

let is_first_workspace = app.workspaces.is_empty();
app.workspaces.push(new_workspace);

if is_first_workspace {
app.state.workspaces_tree_state.select(vec![workspace_id]);
}
}
19 changes: 19 additions & 0 deletions src/actions/delete_workspace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use uuid::Uuid;

use crate::{app::App, db::repositories::WorkspaceRepository};

pub fn delete_workspace(app: &mut App, option_idx: Option<usize>, workspace_id: Uuid) {
if option_idx != Some(0) {
return;
}

if let Err(e) = WorkspaceRepository::delete(&app.db.connection, &workspace_id) {
app.error = Some(e.to_string());
return;
}

app.tasks.retain(|t| t.workspace_id != Some(workspace_id));
app.workspaces.retain(|s| s.id != workspace_id);
app.selected_tasks.clear();
app.state.workspaces_tree_state.select_first();
}
58 changes: 32 additions & 26 deletions src/actions/edit_task.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
use chrono::Utc;
use ratatui::DefaultTerminal;
use uuid::Uuid;

use crate::{app::App, db::repositories::TaskRepository, editor, state::PanelState};

pub fn edit_task(app: &mut App, terminal: &mut DefaultTerminal) {
if app.state.active_panel == PanelState::ActiveTasks {
let task_id = app
.state
.get_selected_panel_state()
.and_then(|s| s.selected())
.and_then(|idx| app.get_current_tasks().get(idx).map(|t| t.id));

if let Some(task_id) = task_id
&& let Some(task_ref) = app.tasks.iter().find(|t| t.id == task_id)
{
let update = editor::open_in_editor(task_ref, terminal);

// Only apply changes if title is not empty
if !update.title.is_empty()
&& let Some(task) = app.tasks.iter_mut().find(|t| t.id == task_id)
{
task.title = update.title;
task.description = Some(update.description);
task.updated_at = Some(Utc::now());

if let Err(e) = TaskRepository::update(&app.db.connection, task) {
app.error = Some(e.to_string());

return;
};
}
if app.state.active_panel != PanelState::ActiveTasks {
return;
}

let selected = app.state.workspaces_tree_state.selected();
if selected.is_empty() {
return;
}

let selected_id = selected.last().unwrap();
let task_id = match Uuid::parse_str(selected_id) {
Ok(uuid) => uuid,
Err(_) => return,
};

let task_ref = match app.tasks.iter().find(|t| t.id == task_id) {
Some(t) => t,
None => return,
};

let update = editor::open_in_editor(task_ref, terminal);

if !update.title.is_empty() {
if let Some(task) = app.tasks.iter_mut().find(|t| t.id == task_id) {
task.title = update.title;
task.description = Some(update.description);
task.updated_at = Some(Utc::now());

if let Err(e) = TaskRepository::update(&app.db.connection, task) {
app.error = Some(e.to_string());
};
}
}
}
20 changes: 18 additions & 2 deletions src/actions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
pub mod archive_workspace;
pub mod clean_err_msg;
pub mod close_modal;
pub mod create_task;
pub mod create_workspace;
pub mod delete_task;
pub mod delete_workspace;
pub mod edit_priority;
pub mod edit_task;
pub mod edit_title;
pub mod move_task;
pub mod open_archive_modal;
pub mod open_create_modal;
pub mod open_archive_workspace_modal;
pub mod open_create_task_modal;
pub mod open_create_workspace_modal;
pub mod open_delete_modal;
pub mod open_delete_workspace_modal;
pub mod open_edit_title_modal;
pub mod open_move_task_modal;
pub mod open_priority_modal;
pub mod quit;
pub mod select_next_task;
Expand All @@ -18,17 +26,25 @@ pub mod toggle_archive_task;
pub mod toggle_task_completion;
pub mod toggle_task_selection;

pub use archive_workspace::archive_workspace;
pub use clean_err_msg::clean_err_msg;
pub use close_modal::close_modal;
pub use create_task::create_task;
pub use create_workspace::create_workspace;
pub use delete_task::delete_task;
pub use delete_workspace::delete_workspace;
pub use edit_priority::edit_priority;
pub use edit_task::edit_task;
pub use edit_title::edit_title;
pub use move_task::move_task;
pub use open_archive_modal::open_archive_modal;
pub use open_create_modal::open_create_modal;
pub use open_archive_workspace_modal::open_archive_workspace_modal;
pub use open_create_task_modal::open_create_task_modal;
pub use open_create_workspace_modal::open_create_workspace_modal;
pub use open_delete_modal::open_delete_modal;
pub use open_delete_workspace_modal::open_delete_workspace_modal;
pub use open_edit_title_modal::open_edit_title_modal;
pub use open_move_task_modal::open_move_task_modal;
pub use open_priority_modal::open_priority_modal;
pub use quit::quit;
pub use select_next_task::select_next_task;
Expand Down
29 changes: 29 additions & 0 deletions src/actions/move_task.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use chrono::Utc;
use uuid::Uuid;

use crate::{app::App, db::repositories::TaskRepository, models::Workspace};

pub fn move_task(app: &mut App, option_idx: Option<usize>, task_id: Uuid, workspaces: &[Workspace]) {
let idx = match option_idx {
Some(i) => i,
None => return,
};

let new_workspace_id = if idx >= workspaces.len() {
None
} else {
workspaces.get(idx).map(|w| w.id)
};

let task = match app.tasks.iter_mut().find(|t| t.id == task_id) {
Some(t) => t,
None => return,
};

task.workspace_id = new_workspace_id;
task.updated_at = Some(Utc::now());

if let Err(e) = TaskRepository::update(&app.db.connection, task) {
app.error = Some(e.to_string());
}
}
31 changes: 25 additions & 6 deletions src/actions/open_archive_modal.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
use uuid::Uuid;

use crate::{app::App, state::PanelState};

pub fn open_archive_modal(app: &mut App) {
let is_archived = app.state.active_panel == PanelState::ArchivedTasks;

if app.selected_tasks.is_empty() {
if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) {
let task_id = app.get_current_tasks()[task_index].id;
app.state.open_archived_task(vec![task_id], is_archived)
}
} else {
if !app.selected_tasks.is_empty() {
app.state
.open_archived_task(app.selected_tasks.clone(), is_archived);
return;
}

if app.state.active_panel == PanelState::ActiveTasks {
let selected = app.state.workspaces_tree_state.selected();
if selected.is_empty() {
return;
}

let selected_id = selected.last().unwrap();
if let Ok(uuid) = Uuid::parse_str(selected_id) {
if app.tasks.iter().any(|t| t.id == uuid) {
app.state.open_archived_task(vec![uuid], is_archived);
}
}
} else if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected())
{
let tasks = app.get_current_tasks();
if task_index < tasks.len() {
app.state
.open_archived_task(vec![tasks[task_index].id], is_archived);
}
}
}
22 changes: 22 additions & 0 deletions src/actions/open_archive_workspace_modal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use uuid::Uuid;

use crate::{app::App, state::PanelState};

pub fn open_archive_workspace_modal(app: &mut App) {
if app.state.active_panel != PanelState::ActiveTasks {
return;
}

let selected = app.state.workspaces_tree_state.selected();
if selected.len() != 1 {
return;
}

let selected_id = &selected[0];

if let Ok(uuid) = Uuid::parse_str(selected_id) {
if let Some(workspace) = app.workspaces.iter().find(|s| s.id == uuid) {
app.state.open_archive_workspace(uuid, workspace.archived);
}
}
}
21 changes: 21 additions & 0 deletions src/actions/open_create_task_modal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use uuid::Uuid;

use crate::{app::App, state::PanelState};

pub fn open_create_task_modal(app: &mut App) {
if app.state.active_panel != PanelState::ActiveTasks {
return;
}

let selected = app.state.workspaces_tree_state.selected();

let workspace_id = if selected.is_empty() {
None
} else {
Uuid::parse_str(&selected[0]).ok().filter(|uuid| {
app.workspaces.iter().any(|w| w.id == *uuid)
})
};

app.state.open_create_task(workspace_id)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{app::App, state::PanelState};

pub fn open_create_modal(app: &mut App) {
pub fn open_create_workspace_modal(app: &mut App) {
if app.state.active_panel == PanelState::ActiveTasks {
app.state.open_create_task()
app.state.open_create_workspace()
}
}
32 changes: 25 additions & 7 deletions src/actions/open_delete_modal.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
use crate::app::App;
use uuid::Uuid;

use crate::{app::App, state::PanelState};

pub fn open_delete_modal(app: &mut App) {
if app.selected_tasks.is_empty() {
if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) {
let task_id = app.get_current_tasks()[task_index].id;
app.state.open_delete_task(vec![task_id]);
}
} else {
if !app.selected_tasks.is_empty() {
app.state.open_delete_task(app.selected_tasks.clone());
return;
}

if app.state.active_panel == PanelState::ActiveTasks {
let selected = app.state.workspaces_tree_state.selected();
if selected.is_empty() {
return;
}

let selected_id = selected.last().unwrap();
if let Ok(uuid) = Uuid::parse_str(selected_id) {
if app.tasks.iter().any(|t| t.id == uuid) {
app.state.open_delete_task(vec![uuid]);
}
}
} else if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected())
{
let tasks = app.get_current_tasks();
if task_index < tasks.len() {
app.state.open_delete_task(vec![tasks[task_index].id]);
}
}
}
Loading