use crate::gui::message::Message;
use crate::gui::state::{Focus, GuiApp, SidebarMode};
use crate::gui::subscription::ACTIVE_FOCUS;
use crate::gui::update::common;
use crate::model::AppIntent;
use crate::storage::LOCAL_TRASH_HREF;
use chrono::NaiveTime;
use iced::Task;
use iced::widget::text_editor;
fn description_caret_link_message(app: &GuiApp, line: &str, column: usize) -> Option<Message> {
use crate::model::parser::{SyntaxType, strip_quotes, tokenize_smart_input};
let col = column.min(line.len());
let tokens = tokenize_smart_input(line, false);
let token = tokens.iter().find(|t| col >= t.start && col <= t.end)?;
let raw = &line[token.start..token.end];
match token.kind {
SyntaxType::WikiLink => {
let clean = strip_quotes(raw.trim_start_matches("[[").trim_end_matches("]]"));
let target = clean.split('|').next()?.trim();
if target.is_empty() {
return None;
}
let ctx = app
.editing_tree_uid
.clone()
.or(app.editing_uid.clone())
.or(app.journal_editing_uid.clone());
Some(Message::OpenWikiLink(target.to_string(), ctx))
}
SyntaxType::Url => {
let stripped = raw.trim_start_matches("[[").trim_end_matches("]]");
let clean = strip_quotes(stripped.trim_start_matches("url:"));
if clean.is_empty() {
return None;
}
Some(Message::OpenUrl(clean.to_string()))
}
_ => None,
}
}
fn dispatch_and_maintain_selection(app: &mut GuiApp, intent: AppIntent, focus_uid: &str) {
let was_selected = app.selected_uid.as_deref() == Some(focus_uid);
let old_idx = app.find_task_index_by_uid(focus_uid);
common::dispatch_intent(app, intent);
if was_selected
&& let Some(idx) = old_idx
&& app.find_task_index_by_uid(focus_uid).is_none()
{
let new_idx = idx.min(app.tasks.len().saturating_sub(1));
let mut fallback = None;
for i in new_idx..app.tasks.len() {
if let Some(t) = app.get_task_at_index(i) {
fallback = Some(t.uid.clone());
break;
}
}
if fallback.is_none() {
for i in (0..new_idx).rev() {
if let Some(t) = app.get_task_at_index(i) {
fallback = Some(t.uid.clone());
break;
}
}
}
if fallback.is_some() {
app.selected_uid = fallback;
}
}
}
fn dispatch_and_select_next_row(app: &mut GuiApp, intent: AppIntent, uid: String) {
let was_selected = app.selected_uid.as_ref() == Some(&uid);
let next_uid = if was_selected {
app.find_task_index_by_uid(&uid)
.and_then(|idx| {
app.get_task_at_index(idx + 1).or_else(|| {
if idx > 0 {
app.get_task_at_index(idx - 1)
} else {
None
}
})
})
.map(|task| task.uid.clone())
} else {
None
};
dispatch_and_maintain_selection(app, intent, &uid);
if was_selected
&& let Some(next_uid) = next_uid
&& app.find_task_index_by_uid(&next_uid).is_some()
{
app.selected_uid = Some(next_uid);
}
}
fn handle_gui_text_undo(app: &mut GuiApp) {
let is_desc = app.last_edited_field == 1 || app.input_history.undo.is_empty();
if is_desc {
if let Some(prev) = app.desc_history.pop_undo(app.description_value.text()) {
app.description_value = text_editor::Content::with_text(&prev);
app.description_value
.perform(text_editor::Action::Move(text_editor::Motion::DocumentEnd));
}
} else {
if let Some(prev) = app.input_history.pop_undo(app.input_value.text()) {
app.input_value = text_editor::Content::with_text(&prev);
app.input_value
.perform(text_editor::Action::Move(text_editor::Motion::DocumentEnd));
}
}
}
fn handle_gui_text_redo(app: &mut GuiApp) {
let is_desc = app.last_edited_field == 1 || app.input_history.redo.is_empty();
if is_desc {
if let Some(next) = app.desc_history.pop_redo(app.description_value.text()) {
app.description_value = text_editor::Content::with_text(&next);
app.description_value
.perform(text_editor::Action::Move(text_editor::Motion::DocumentEnd));
}
} else {
if let Some(next) = app.input_history.pop_redo(app.input_value.text()) {
app.input_value = text_editor::Content::with_text(&next);
app.input_value
.perform(text_editor::Action::Move(text_editor::Motion::DocumentEnd));
}
}
}
pub fn handle(app: &mut GuiApp, message: Message) -> Task<Message> {
match message {
Message::Undo => {
if app.active_focus == Focus::AddTaskInput {
handle_gui_text_undo(app);
return Task::none();
}
if let Some(record) = app.undo_history.pop_undo() {
app.store.apply_actions(&record.reverse);
app.undo_history.push_redo(record.clone());
app.selected_uid = record.primary_uid.clone();
common::refresh_filtered_tasks(app);
if let Some(tx) = &app.bg_tx {
let _ =
tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(record.reverse));
}
app.info_msg = Some(
rust_i18n::t!("task_action_undone", desc = record.description).to_string(),
);
app.info_msg_version = app.info_msg_version.wrapping_add(1);
app.error_msg = None;
let version = app.info_msg_version;
return Task::batch(vec![
common::scroll_to_selected_delayed(app, false),
Task::perform(
async move {
tokio::time::sleep(std::time::Duration::from_secs(4)).await;
version
},
Message::DismissInfo,
),
]);
}
Task::none()
}
Message::Redo => {
if app.active_focus == Focus::AddTaskInput {
handle_gui_text_redo(app);
return Task::none();
}
if let Some(record) = app.undo_history.pop_redo() {
app.store.apply_actions(&record.forward);
app.undo_history.push_undo(record.clone());
app.selected_uid = record.primary_uid.clone();
common::refresh_filtered_tasks(app);
if let Some(tx) = &app.bg_tx {
let _ =
tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(record.forward));
}
app.info_msg = Some(
rust_i18n::t!("task_action_redone", desc = record.description).to_string(),
);
app.info_msg_version = app.info_msg_version.wrapping_add(1);
app.error_msg = None;
let version = app.info_msg_version;
return Task::batch(vec![
common::scroll_to_selected_delayed(app, false),
Task::perform(
async move {
tokio::time::sleep(std::time::Duration::from_secs(4)).await;
version
},
Message::DismissInfo,
),
]);
}
Task::none()
}
Message::ApplySuggestion(range, text) => {
if app.sidebar_mode == SidebarMode::Journal {
let current = app.journal_editor_content.text();
let mut new_text = current[..range.start].to_string();
new_text.push_str(&text);
if range.end < current.len() {
new_text.push_str(¤t[range.end..]);
} else {
new_text.push(' ');
}
app.journal_editor_content = text_editor::Content::with_text(&new_text);
app.journal_editor_content
.perform(text_editor::Action::Move(text_editor::Motion::DocumentEnd));
app.journal_debounce_version = app.journal_debounce_version.wrapping_add(1);
let version = app.journal_debounce_version;
return Task::batch(vec![
iced::widget::operation::focus(iced::widget::Id::new("journal_editor")),
Task::perform(
async move {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
version
},
Message::SaveJournal,
),
]);
}
app.active_focus = Focus::AddTaskInput;
if let Ok(mut focus) = ACTIVE_FOCUS.write() {
*focus = Focus::AddTaskInput;
}
let is_desc = app.last_edited_field == 1 || app.editing_tree_uid.is_some();
if is_desc {
let current = app.description_value.text();
app.desc_history.push(current.clone());
let mut new_text = current[..range.start].to_string();
new_text.push_str(&text);
if range.end < current.len() {
new_text.push_str(¤t[range.end..]);
} else {
new_text.push(' ');
}
app.description_value = text_editor::Content::with_text(&new_text);
app.description_value
.perform(text_editor::Action::Move(text_editor::Motion::DocumentEnd));
iced::widget::operation::focus(iced::widget::Id::new("description_input"))
} else {
let current = app.input_value.text();
app.input_history.push(current.clone());
let mut new_text = current[..range.start].to_string();
new_text.push_str(&text);
if range.end < current.len() {
new_text.push_str(¤t[range.end..]);
} else {
new_text.push(' ');
}
app.input_value = text_editor::Content::with_text(&new_text);
app.input_value
.perform(text_editor::Action::Move(text_editor::Motion::DocumentEnd));
iced::widget::operation::focus(iced::widget::Id::new("main_input"))
}
}
Message::InputChanged(action) => {
app.active_focus = Focus::AddTaskInput;
if let Ok(mut focus) = ACTIVE_FOCUS.write() {
*focus = Focus::AddTaskInput;
}
if let text_editor::Action::Edit(text_editor::Edit::Enter) = action {
return handle_submit(app, false);
}
if let text_editor::Action::Edit(text_editor::Edit::Insert('\t')) = action {
return Task::done(Message::TabPressed(true));
}
let old_text = app.input_value.text();
app.input_value.perform(action);
if old_text != app.input_value.text() {
app.input_history.push(old_text);
app.last_edited_field = 0;
}
Task::none()
}
Message::DescriptionChanged(action) => {
if let text_editor::Action::Edit(text_editor::Edit::Insert('\t')) = action {
return Task::done(Message::TabPressed(true));
}
if let text_editor::Action::Click(_) = action
&& crate::gui::subscription::cmd_held()
{
app.description_value.perform(action);
let cursor_pos = app.description_value.cursor().position;
if let Some(line) = app.description_value.line(cursor_pos.line)
&& let Some(msg) =
description_caret_link_message(app, &line.text, cursor_pos.column)
{
return Task::done(msg);
}
return Task::none();
}
let is_enter = matches!(action, text_editor::Action::Edit(text_editor::Edit::Enter));
let old_text = app.description_value.text();
app.description_value.perform(action);
if is_enter {
let cursor_pos = app.description_value.cursor().position;
let line_idx = cursor_pos.line;
let text = app.description_value.text();
if line_idx > 0 {
let lines: Vec<&str> = text.split('\n').collect();
if line_idx - 1 < lines.len() {
let prev_line = lines[line_idx - 1];
let prefix = crate::model::extractor::extract_list_prefix(prev_line);
if !prefix.is_empty() {
if prev_line.trim() == prefix.trim() {
app.description_value.perform(text_editor::Action::Edit(
text_editor::Edit::Backspace,
));
for _ in 0..prefix.chars().count() {
app.description_value.perform(text_editor::Action::Edit(
text_editor::Edit::Backspace,
));
}
} else {
for c in prefix.chars() {
app.description_value.perform(text_editor::Action::Edit(
text_editor::Edit::Insert(c),
));
}
}
}
}
}
}
if old_text != app.description_value.text() {
app.desc_history.push(old_text);
app.last_edited_field = 1;
}
Task::none()
}
Message::StartCreateChild(parent_uid) => {
app.creating_child_of = Some(parent_uid.clone());
app.selected_uid = Some(parent_uid.clone());
app.creating_with_desc = false;
let mut initial_input = String::new();
if let Some(parent) = app.store.get_task_ref(&parent_uid) {
if parent.is_journal {
initial_input.push_str("is:page ");
}
for cat in &parent.categories {
initial_input
.push_str(&format!("#{} ", crate::model::parser::quote_value(cat)));
}
for loc in &parent.locations {
initial_input
.push_str(&format!("@@{} ", crate::model::parser::quote_value(loc)));
}
}
app.input_value = text_editor::Content::with_text(&initial_input);
app.input_value
.perform(text_editor::Action::Move(text_editor::Motion::DocumentEnd));
app.input_history.clear();
app.desc_history.clear();
app.last_edited_field = 0;
app.active_focus = Focus::AddTaskInput;
if let Ok(mut focus) = ACTIVE_FOCUS.write() {
*focus = Focus::AddTaskInput;
}
iced::widget::operation::focus(iced::widget::Id::new("main_input"))
}
Message::StartCreateWithDescription => {
app.creating_with_desc = true;
app.active_focus = Focus::AddTaskInput;
if let Ok(mut focus) = ACTIVE_FOCUS.write() {
*focus = Focus::AddTaskInput;
}
if app.input_value.text().trim().is_empty() {
iced::widget::operation::focus(iced::widget::Id::new("main_input"))
} else {
iced::widget::operation::focus(iced::widget::Id::new("description_input"))
}
}
Message::SubmitTask => handle_submit(app, false),
Message::SaveTaskKeepEditing => handle_submit(app, true),
Message::SaveAndSwitchEditor => {
let to_tree = app.editing_tree_uid.clone();
let to_desc = app.editing_uid.clone();
let task = handle_submit(app, false);
if let Some(uid) = to_tree {
if let Some(idx) = app.find_task_index_by_uid(&uid) {
return Task::batch(vec![task, handle(app, Message::EditTaskStart(idx))]);
}
} else if let Some(uid) = to_desc {
return Task::batch(vec![task, handle(app, Message::EditTaskTree(uid))]);
}
task
}
Message::EditTaskStart(index) => {
let data = app
.get_task_at_index(index)
.map(|t| (t.uid.clone(), t.to_smart_string(), t.description.clone()));
if let Some((task_uid, task_summary, task_description)) = data {
app.input_value = text_editor::Content::with_text(&task_summary);
app.input_value
.perform(text_editor::Action::Move(text_editor::Motion::DocumentEnd));
app.description_value = text_editor::Content::with_text(&task_description);
app.input_history.clear();
app.desc_history.clear();
app.last_edited_field = 0;
app.editing_uid = Some(task_uid.clone());
app.selected_uid = Some(task_uid);
app.active_focus = Focus::AddTaskInput;
if let Ok(mut focus) = ACTIVE_FOCUS.write() {
*focus = Focus::AddTaskInput;
}
return iced::widget::operation::focus(iced::widget::Id::new("main_input"));
}
Task::none()
}
Message::EditTaskTree(uid) => {
if let Some(idx) = app.find_task_index_by_uid(&uid) {
let data = app.get_task_at_index(idx).map(|t| t.uid.clone());
if let Some(task_uid) = data {
app.input_value = text_editor::Content::new();
let tree_md = crate::model::extractor::serialize_task_tree(
&app.store,
&task_uid,
&app.calendars,
false,
);
app.description_value = text_editor::Content::with_text(&tree_md);
app.input_history.clear();
app.desc_history.clear();
app.last_edited_field = 1;
app.editing_tree_uid = Some(task_uid.clone());
app.selected_uid = Some(task_uid);
app.active_focus = Focus::AddTaskInput;
if let Ok(mut focus) = ACTIVE_FOCUS.write() {
*focus = Focus::AddTaskInput;
}
return iced::widget::operation::focus(iced::widget::Id::new(
"description_input",
));
}
}
Task::none()
}
Message::KeyboardEditTree => {
let mut target_uid = None;
if app.active_focus == Focus::Sidebar && app.sidebar_mode == SidebarMode::Journal {
if let Some(page) = app.cached_journal_pages.get(app.sidebar_selection_idx)
&& page.is_task
{
target_uid = Some(page.key.clone());
}
} else if app.sidebar_mode == SidebarMode::Journal {
target_uid = app.journal_editing_uid.clone().or_else(|| {
let target_href = app
.active_cal_href
.clone()
.unwrap_or_else(|| crate::storage::LOCAL_CALENDAR_HREF.to_string());
app.store
.get_journal_entry(&target_href, app.journal_date)
.map(|t| t.uid.clone())
});
} else if let Some(selected_uid) = app.selected_uid.clone() {
target_uid = Some(selected_uid);
}
if let Some(uid) = target_uid {
return handle(app, Message::EditTaskTree(uid));
}
Task::none()
}
Message::CancelEdit => {
app.input_history.clear();
app.desc_history.clear();
app.input_value = text_editor::Content::new();
app.description_value = text_editor::Content::new();
app.editing_uid = None;
app.editing_tree_uid = None;
app.creating_child_of = None;
app.child_lock_active = false;
app.creating_with_desc = false;
app.editor_maximized = false;
common::scroll_to_selected(app, true)
}
Message::ToggleTaskShift(uid) => {
dispatch_and_select_next_row(app, AppIntent::ToggleTaskShift { uid: uid.clone() }, uid);
Task::none()
}
Message::CompleteTree(uid) => {
dispatch_and_select_next_row(app, AppIntent::CompleteTree { uid: uid.clone() }, uid);
Task::none()
}
Message::ShiftSpaceSelected => {
if let Some(uid) = app.selected_uid.clone()
&& let Some(idx) = app.find_task_index_by_uid(&uid)
&& let Some(t) = app.get_task_at_index(idx)
{
if t.is_note {
return Task::none();
}
let intent = if t.rrule.is_some() {
AppIntent::ToggleTaskShift { uid: uid.clone() }
} else if t.has_subtasks {
AppIntent::CompleteTree { uid: uid.clone() }
} else {
AppIntent::ToggleTask { uid: uid.clone() }
};
dispatch_and_select_next_row(app, intent, uid);
}
Task::none()
}
Message::ToggleTask(index, _) => {
let data = app.get_task_at_index(index).map(|t| {
(
t.uid.clone(),
t.etag == "pending_refresh",
t.status.is_done(),
)
});
if let Some((uid, is_pending, is_done)) = data {
if is_pending {
return Task::none();
}
if is_done {
dispatch_and_maintain_selection(
app,
AppIntent::ToggleTask { uid: uid.clone() },
&uid,
);
} else {
dispatch_and_select_next_row(
app,
AppIntent::ToggleTask { uid: uid.clone() },
uid,
);
}
}
Task::none()
}
Message::ToggleDoneGroup(key) => {
common::dispatch_intent(app, crate::model::AppIntent::ToggleDoneGroup { key });
common::save_config(app);
Task::none()
}
Message::ToggleTreeCollapse(uid) => {
common::dispatch_intent(app, AppIntent::ToggleTreeCollapse { uid });
Task::none()
}
Message::SetTreeCollapse(uid, collapsed) => {
common::dispatch_intent(app, AppIntent::SetTreeCollapse { uid, collapsed });
Task::none()
}
Message::ToggleHelpSection(title) => {
if app.help_expanded_sections.contains(&title) {
app.help_expanded_sections.remove(&title);
} else {
app.help_expanded_sections.insert(title);
}
Task::none()
}
Message::DeleteTask(index) => {
if let Some(uid) = app.get_task_at_index(index).map(|t| t.uid.clone()) {
app.selected_uid = Some(uid.clone());
dispatch_and_maintain_selection(
app,
AppIntent::DeleteTask { uid: uid.clone() },
&uid,
);
}
Task::none()
}
Message::EditSelectedDescription => {
if let Some(uid) = &app.selected_uid
&& let Some(idx) = app.find_task_index_by_uid(uid)
{
return Task::batch(vec![
handle(app, Message::EditTaskStart(idx)),
iced::widget::operation::focus(iced::widget::Id::new("description_input")),
]);
}
Task::none()
}
Message::PromoteSelected => {
if let Some(uid) = app.selected_uid.clone() {
common::dispatch_intent(app, AppIntent::RemoveParent { uid });
}
Task::none()
}
Message::DemoteSelected => {
if let Some(uid) = app.selected_uid.clone()
&& let Some(idx) = app.find_task_index_by_uid(&uid)
&& idx > 0
{
let parent_candidate_uid = app.get_task_at_index(idx - 1).unwrap().uid.clone();
if parent_candidate_uid != uid {
app.yanked_uid = Some(parent_candidate_uid);
return handle(app, Message::MakeChild(uid));
}
}
Task::none()
}
Message::YankSelected => {
if let Some(uid) = app.selected_uid.clone() {
app.yanked_uid = Some(uid.clone());
let mut tasks = vec![common::scroll_to_selected(app, false)];
if let Some(idx) = app.find_task_index_by_uid(&uid)
&& let Some(t) = app.get_task_at_index(idx)
{
let text = if t.description.is_empty() {
t.to_smart_string()
} else {
format!("{}\n\n{}", t.to_smart_string(), t.description)
};
tasks.push(iced::clipboard::write(text));
}
return Task::batch(tasks);
}
Task::none()
}
Message::KeyboardLinkChild => {
if let Some(parent_uid) = app.yanked_uid.clone()
&& let Some(selected_uid) = app.selected_uid.clone()
&& parent_uid != selected_uid
{
dispatch_and_maintain_selection(
app,
AppIntent::MakeChild {
uid: selected_uid.clone(),
parent_uid,
},
&selected_uid,
);
}
Task::none()
}
Message::KeyboardCreateChild => {
let mut target_uid = None;
if app.active_focus == Focus::Sidebar && app.sidebar_mode == SidebarMode::Journal {
if let Some(page) = app.cached_journal_pages.get(app.sidebar_selection_idx)
&& page.is_task
{
target_uid = Some(page.key.clone());
}
} else if app.sidebar_mode == SidebarMode::Journal {
target_uid = app.journal_editing_uid.clone().or_else(|| {
let target_href = app
.active_cal_href
.clone()
.unwrap_or_else(|| crate::storage::LOCAL_CALENDAR_HREF.to_string());
app.store
.get_journal_entry(&target_href, app.journal_date)
.map(|t| t.uid.clone())
});
} else if let Some(selected_uid) = app.selected_uid.clone() {
target_uid = Some(selected_uid);
}
if let Some(uid) = target_uid {
return handle(app, Message::StartCreateChild(uid));
}
Task::none()
}
Message::KeyboardAddDependency => {
if let Some(blocker_uid) = app.yanked_uid.clone()
&& let Some(uid) = app.selected_uid.clone()
{
common::dispatch_intent(app, AppIntent::AddDependency { uid, blocker_uid });
}
Task::none()
}
Message::KeyboardAddRelation => {
if let Some(related_uid) = app.yanked_uid.clone()
&& let Some(uid) = app.selected_uid.clone()
{
common::dispatch_intent(app, AppIntent::AddRelatedTo { uid, related_uid });
}
Task::none()
}
Message::KeyboardOpenContextMenu => {
if let Some(uid) = app.selected_uid.clone() {
return crate::gui::update::view::handle(app, Message::OpenContextMenu(uid, true));
}
Task::none()
}
Message::KeyboardToggleDetails => {
if let Some(uid) = app.selected_uid.clone() {
return crate::gui::update::view::handle(app, Message::ToggleDetails(uid));
}
Task::none()
}
Message::KeyboardDuplicateTask => {
if let Some(selected) = app.selected_uid.clone() {
return handle(app, Message::DuplicateTask(selected));
}
Task::none()
}
Message::KeyboardToggleTreeCollapse => {
if app.active_focus == crate::gui::state::Focus::Sidebar {
match app.sidebar_mode {
SidebarMode::Calendars => {}
SidebarMode::Categories => {
let cats = &app.cached_categories;
if let Some(cat) = cats.get(app.sidebar_selection_idx)
&& cat.has_children
{
return crate::gui::update::view::handle(
app,
Message::ToggleTagCollapse(cat.full_key.clone()),
);
}
}
SidebarMode::Locations => {
let locs = &app.cached_locations;
if let Some(loc) = locs.get(app.sidebar_selection_idx)
&& loc.has_children
{
return crate::gui::update::view::handle(
app,
Message::ToggleLocationCollapse(loc.full_key.clone()),
);
}
}
SidebarMode::Journal => {
if let Some(page) = app.cached_journal_pages.get(app.sidebar_selection_idx)
&& page.has_children
{
if page.is_task {
return crate::gui::update::tasks::handle(
app,
Message::ToggleTreeCollapse(page.key.clone()),
);
} else {
return crate::gui::update::view::handle(
app,
Message::ToggleTagCollapse(page.key.clone()),
);
}
}
}
SidebarMode::Goals => {}
}
return Task::none();
}
if let Some(uid) = app.selected_uid.clone()
&& let Some(idx) = app.find_task_index_by_uid(&uid)
&& let Some(task) = app.get_task_at_index(idx)
{
return handle(app, Message::SetTreeCollapse(uid, !task.collapsed));
}
Task::none()
}
Message::DuplicateTask(uid) => {
app.yanked_uid = None;
app.yank_lock_active = false;
common::dispatch_intent(app, AppIntent::DuplicateTaskTree { uid });
Task::none()
}
Message::KeyboardOpenLocations => {
if let Some(uid) = app.selected_uid.clone()
&& let Some(task) = app.store.get_task_ref(&uid)
{
let count = task.tree_location_count;
if count > 1 {
return crate::gui::update::view::handle(app, Message::OpenLocations(uid));
} else if count == 1 {
return crate::gui::update::view::handle(app, Message::OpenCoordinates(uid));
}
}
Task::none()
}
Message::KeyboardOpenUrl => {
if let Some(uid) = app.selected_uid.clone()
&& let Some(task) = app.store.get_task_ref(&uid)
&& let Some(url) = &task.url
{
return crate::gui::update::view::handle(app, Message::OpenUrl(url.clone()));
}
Task::none()
}
Message::KeyboardDeleteTaskTree => {
if let Some(uid) = app.selected_uid.clone() {
return handle(app, Message::DeleteTaskTree(uid));
}
if app.sidebar_mode == SidebarMode::Journal
&& let Some(uid) = app.journal_editing_uid.clone()
{
return handle(app, Message::DeleteTaskTree(uid));
}
Task::none()
}
Message::DeleteTaskTree(uid) => {
app.yanked_uid = None;
app.yank_lock_active = false;
if app.journal_editing_uid.as_ref() == Some(&uid) {
app.journal_editing_uid = None;
}
dispatch_and_maintain_selection(
app,
AppIntent::DeleteTaskTree { uid: uid.clone() },
&uid,
);
Task::none()
}
Message::ToggleActiveSelected => {
if let Some(uid) = app.selected_uid.clone()
&& let Some(idx) = app.find_task_index_by_uid(&uid)
&& let Some(t) = app.get_task_at_index(idx)
{
if t.status == crate::model::TaskStatus::InProcess {
common::dispatch_intent(app, AppIntent::PauseTask { uid });
} else {
common::dispatch_intent(app, AppIntent::StartTask { uid });
}
}
Task::none()
}
Message::StopSelected => {
if let Some(uid) = app.selected_uid.clone() {
common::dispatch_intent(app, AppIntent::StopTask { uid });
}
Task::none()
}
Message::CancelSelected => {
if let Some(uid) = app.selected_uid.clone() {
dispatch_and_select_next_row(app, AppIntent::CancelTask { uid: uid.clone() }, uid);
}
Task::none()
}
Message::ChangePrioritySelected(delta) => {
if let Some(uid) = app.selected_uid.clone() {
common::dispatch_intent(app, AppIntent::ChangePriority { uid, delta });
}
Task::none()
}
Message::ChangePriority(index, delta) => {
if let Some(uid) = app.get_task_at_index(index).map(|t| t.uid.clone()) {
app.selected_uid = Some(uid.clone());
common::dispatch_intent(app, AppIntent::ChangePriority { uid, delta });
}
Task::none()
}
Message::SetTaskStatus(index, new_status) => {
if let Some(uid) = app.get_task_at_index(index).map(|t| t.uid.clone()) {
app.selected_uid = Some(uid.clone());
if new_status == crate::model::TaskStatus::Cancelled {
dispatch_and_select_next_row(
app,
AppIntent::CancelTask { uid: uid.clone() },
uid,
);
} else if new_status.is_done() {
dispatch_and_select_next_row(
app,
AppIntent::ToggleTask { uid: uid.clone() },
uid,
);
}
}
Task::none()
}
Message::MoveTask(uid, target_href) => {
app.selected_uid = Some(uid.clone());
app.moving_task_uid = None;
let intent = if app.moving_task_is_tree {
AppIntent::MoveTaskTree {
uid: uid.clone(),
target_href: target_href.clone(),
}
} else {
AppIntent::MoveTask {
uid: uid.clone(),
target_href: target_href.clone(),
}
};
dispatch_and_maintain_selection(app, intent, &uid);
if app.sidebar_mode == SidebarMode::Journal {
app.journal_editing_href = Some(target_href.clone());
app.active_cal_href = Some(target_href);
crate::gui::update::common::refresh_filtered_tasks(app);
}
Task::none()
}
Message::StartTask(uid) => {
common::dispatch_intent(app, AppIntent::StartTask { uid });
Task::none()
}
Message::PauseTask(uid) => {
common::dispatch_intent(app, AppIntent::PauseTask { uid });
Task::none()
}
Message::StopTask(uid) => {
common::dispatch_intent(app, AppIntent::StopTask { uid });
Task::none()
}
Message::YankTask(uid) => {
app.yanked_uid = Some(uid.clone());
app.selected_uid = Some(uid.clone());
let mut tasks = vec![common::scroll_to_selected(app, false)];
if let Some(idx) = app.find_task_index_by_uid(&uid)
&& let Some(t) = app.get_task_at_index(idx)
{
let text = if t.description.is_empty() {
t.to_smart_string()
} else {
format!("{}\n\n{}", t.to_smart_string(), t.description)
};
tasks.push(iced::clipboard::write(text));
}
Task::batch(tasks)
}
Message::CopyToClipboard(text) => Task::batch(vec![iced::clipboard::write(text)]),
Message::TogglePin(uid) => {
common::dispatch_intent(app, AppIntent::TogglePin { uid });
Task::none()
}
Message::ClearYank => {
app.yanked_uid = None;
app.yank_lock_active = false;
Task::none()
}
Message::EscCaptured => {
app.active_focus = Focus::MainList;
if let Ok(mut focus) = ACTIVE_FOCUS.write() {
*focus = Focus::MainList;
}
app.info_msg = None;
if app.editing_uid.is_some()
|| app.editing_tree_uid.is_some()
|| app.creating_child_of.is_some()
|| app.creating_with_desc
{
app.input_value = text_editor::Content::new();
app.description_value = text_editor::Content::new();
app.editing_uid = None;
app.editing_tree_uid = None;
app.creating_child_of = None;
app.creating_with_desc = false;
app.editor_maximized = false;
}
common::scroll_to_selected_delayed(app, false)
}
Message::EscapePressed => {
app.active_focus = Focus::MainList;
if let Ok(mut focus) = ACTIVE_FOCUS.write() {
*focus = Focus::MainList;
}
let mut needs_refresh = false;
let mut captured_action = false;
if app.info_msg.is_some() {
app.info_msg = None;
captured_action = true;
} else if app.moving_task_uid.is_some() {
app.moving_task_uid = None;
captured_action = true;
} else if app.ics_import_dialog_open {
app.ics_import_dialog_open = false;
app.ics_import_file_path = None;
app.ics_import_content = None;
app.ics_import_selected_calendar = None;
app.ics_import_task_count = None;
captured_action = true;
} else if app.editing_uid.is_some()
|| app.editing_tree_uid.is_some()
|| app.creating_child_of.is_some()
|| app.creating_with_desc
{
app.input_value = text_editor::Content::new();
app.description_value = text_editor::Content::new();
app.editing_uid = None;
app.editing_tree_uid = None;
app.creating_child_of = None;
app.child_lock_active = false;
app.creating_with_desc = false;
app.editor_maximized = false;
captured_action = true;
} else if app.yanked_uid.is_some() {
app.yanked_uid = None;
app.yank_lock_active = false;
captured_action = true;
} else if !app.input_value.text().is_empty() {
app.input_value = text_editor::Content::new();
captured_action = true;
} else if !app.search_value.text().is_empty() {
app.search_value = text_editor::Content::new();
needs_refresh = true;
captured_action = true;
} else if app.session.focused_task_uid.is_some() {
app.session.focused_task_uid = None;
needs_refresh = true;
captured_action = true;
} else if !app.session.selected_categories.is_empty() {
app.session.selected_categories.clear();
needs_refresh = true;
captured_action = true;
} else if !app.session.selected_locations.is_empty() {
app.session.selected_locations.clear();
needs_refresh = true;
captured_action = true;
}
if needs_refresh {
common::refresh_filtered_tasks(app);
}
if captured_action || (app.editing_uid.is_none() && app.editing_tree_uid.is_none()) {
return common::scroll_to_selected_delayed(app, true);
}
Task::none()
}
Message::MakeChild(target_uid) => {
if let Some(parent_uid) = app.yanked_uid.clone()
&& let Some(_orig) = app.store.get_task_ref(&target_uid)
{
if !app.yank_lock_active {
app.yanked_uid = None;
}
dispatch_and_maintain_selection(
app,
AppIntent::MakeChild {
uid: target_uid.clone(),
parent_uid,
},
&target_uid,
);
}
Task::none()
}
Message::RemoveParent(child_uid) => {
dispatch_and_maintain_selection(
app,
AppIntent::RemoveParent {
uid: child_uid.clone(),
},
&child_uid,
);
Task::none()
}
Message::RemoveDependency(uid, blocker_uid) => {
dispatch_and_maintain_selection(
app,
AppIntent::RemoveDependency {
uid: uid.clone(),
blocker_uid,
},
&uid,
);
Task::none()
}
Message::RemoveRelatedTo(uid, related_uid) => {
dispatch_and_maintain_selection(
app,
AppIntent::RemoveRelatedTo {
uid: uid.clone(),
related_uid,
},
&uid,
);
Task::none()
}
Message::AddDependency(target_uid) => {
if let Some(blocker_uid) = app.yanked_uid.clone() {
if !app.yank_lock_active {
app.yanked_uid = None;
}
dispatch_and_maintain_selection(
app,
AppIntent::AddDependency {
uid: target_uid.clone(),
blocker_uid,
},
&target_uid,
);
}
Task::none()
}
Message::AddRelatedTo(target_uid) => {
if let Some(related_uid) = app.yanked_uid.clone() {
if !app.yank_lock_active {
app.yanked_uid = None;
}
dispatch_and_maintain_selection(
app,
AppIntent::AddRelatedTo {
uid: target_uid.clone(),
related_uid,
},
&target_uid,
);
}
Task::none()
}
Message::StartMoveTask(uid) => {
app.moving_task_uid = Some(uid.clone());
app.move_target_idx = 0;
app.active_context_menu = None; if let Some(task) = app.store.get_task_ref(&uid) {
let calendar_href = task.calendar_href.clone();
let has_subtasks = task.has_subtasks;
app.moving_task_is_tree = has_subtasks;
let targets = app.get_move_targets(&calendar_href, has_subtasks);
if app.move_target_idx >= targets.len() {
app.move_target_idx = targets.len().saturating_sub(1);
}
} else {
app.moving_task_is_tree = false;
}
Task::none()
}
Message::ToggleMoveTree(is_tree) => {
app.moving_task_is_tree = is_tree;
if let Some(uid) = &app.moving_task_uid
&& let Some(task) = app.store.get_task_ref(uid)
{
let targets = app.get_move_targets(&task.calendar_href, is_tree);
if app.move_target_idx >= targets.len() {
app.move_target_idx = targets.len().saturating_sub(1);
}
}
Task::none()
}
Message::CancelMoveTask => {
app.moving_task_uid = None;
Task::none()
}
Message::MigrateLocalTo(source_href, target_href) => {
if let Some(local_map) = app.store.calendars.get(&source_href) {
let tasks_to_move: Vec<_> = local_map.values().cloned().collect();
if tasks_to_move.is_empty() {
return Task::none();
}
app.loading = true;
if let Some(client) = &app.client {
return Task::perform(
crate::gui::async_ops::async_migrate_wrapper(
client.clone(),
tasks_to_move,
target_href,
),
|res| Message::MigrationComplete(res.map_err(|e| e.to_string())),
);
} else {
app.error_msg = Some(rust_i18n::t!("error_cannot_export_offline").to_string());
app.loading = false;
}
}
Task::none()
}
Message::SnoozeCustomInput(val) => {
app.snooze_custom_input = val;
Task::none()
}
Message::SnoozeCustomSubmit(t_uid, a_uid) => {
app.ringing_tasks
.retain(|(t, a)| !(t.uid == t_uid && a.uid == a_uid));
let mins = if let Ok(n) = app.snooze_custom_input.parse::<u32>() {
n
} else {
crate::model::parser::parse_duration(&app.snooze_custom_input).unwrap_or(10)
};
app.snooze_custom_input.clear();
handle(app, Message::SnoozeAlarm(t_uid, a_uid, mins))
}
Message::CompleteTaskFromAlarm(t_uid, a_uid) => {
app.ringing_tasks
.retain(|(t, a)| !(t.uid == t_uid && a.uid == a_uid));
if let Some(idx) = app.find_task_index_by_uid(&t_uid)
&& let Some(task) = app.get_task_at_index(idx)
&& !task.status.is_done()
{
if task.etag == "pending_refresh" {
return Task::none();
}
dispatch_and_select_next_row(
app,
AppIntent::ToggleTask { uid: t_uid.clone() },
t_uid.clone(),
);
}
Task::none()
}
Message::CancelTaskFromAlarm(t_uid, a_uid) => {
app.ringing_tasks
.retain(|(t, a)| !(t.uid == t_uid && a.uid == a_uid));
if app.find_task_index_by_uid(&t_uid).is_some() {
dispatch_and_select_next_row(
app,
AppIntent::CancelTask { uid: t_uid.clone() },
t_uid.clone(),
);
}
Task::none()
}
Message::SnoozeAlarm(t_uid, a_uid, mins) => {
if let Some((task, _)) = app.store.get_task_mut(&t_uid)
&& task.handle_snooze(&a_uid, mins)
{
task.sequence += 1;
let cloned = task.clone();
common::refresh_filtered_tasks(app);
if let Some(tx) = &app.bg_tx {
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(vec![
crate::journal::Action::Update(cloned),
]));
}
}
Task::none()
}
Message::DismissAlarm(t_uid, a_uid) => {
if let Some((task, _)) = app.store.get_task_mut(&t_uid)
&& task.handle_dismiss(&a_uid)
{
task.sequence += 1;
let cloned = task.clone();
common::refresh_filtered_tasks(app);
if let Some(tx) = &app.bg_tx {
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(vec![
crate::journal::Action::Update(cloned),
]));
}
}
Task::none()
}
Message::StartAddSession(uid) => {
app.adding_session_uid = Some(uid.clone());
app.editing_session_idx = None;
app.session_input = iced::widget::text_editor::Content::new();
app.expanded_tasks.insert(uid.clone());
app.active_focus = Focus::AddTaskInput;
if let Ok(mut focus) = ACTIVE_FOCUS.write() {
*focus = Focus::AddTaskInput;
}
iced::widget::operation::focus(iced::widget::Id::from(format!("session_input_{}", uid)))
}
Message::SessionInputChanged(action) => {
if let iced::widget::text_editor::Action::Edit(iced::widget::text_editor::Edit::Enter) =
action
{
return handle(app, Message::SubmitSession);
}
if let iced::widget::text_editor::Action::Edit(
iced::widget::text_editor::Edit::Insert('\t'),
) = action
{
return Task::none();
}
app.session_input.perform(action);
Task::none()
}
Message::StartEditSession(uid, idx) => {
app.adding_session_uid = Some(uid.clone());
app.editing_session_idx = Some(idx);
app.expanded_tasks.insert(uid.clone());
if let Some(task) = app.store.get_task_ref(&uid)
&& let Some(session) = task.sessions.get(idx)
{
let s_dt = chrono::DateTime::from_timestamp(session.start, 0)
.unwrap()
.with_timezone(&chrono::Local);
let e_dt = chrono::DateTime::from_timestamp(session.end, 0)
.unwrap()
.with_timezone(&chrono::Local);
let prefill = format!(
"{} {}-{}",
s_dt.format("%Y-%m-%d"),
s_dt.format("%H:%M"),
e_dt.format("%H:%M")
);
app.session_input = iced::widget::text_editor::Content::with_text(&prefill);
app.session_input
.perform(iced::widget::text_editor::Action::Move(
iced::widget::text_editor::Motion::DocumentEnd,
));
}
app.active_focus = Focus::AddTaskInput;
if let Ok(mut focus) = ACTIVE_FOCUS.write() {
*focus = Focus::AddTaskInput;
}
iced::widget::operation::focus(iced::widget::Id::from(format!("session_input_{}", uid)))
}
Message::CancelAddSession => {
app.adding_session_uid = None;
app.editing_session_idx = None;
app.session_input = iced::widget::text_editor::Content::new();
Task::none()
}
Message::SubmitSession => {
if let Some(uid) = app.adding_session_uid.clone() {
let input_text = app.session_input.text();
if let Some(session) = crate::model::parser::parse_session_input(&input_text)
&& let Some((t_mut, _)) = app.store.get_task_mut(&uid)
{
if let Some(idx) = app.editing_session_idx {
t_mut.remove_session(idx);
}
t_mut.add_session(session);
t_mut.sequence += 1;
let cloned = t_mut.clone();
app.adding_session_uid = None;
app.editing_session_idx = None;
app.session_input = iced::widget::text_editor::Content::new();
common::refresh_filtered_tasks(app);
if let Some(tx) = &app.bg_tx {
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(vec![
crate::journal::Action::Update(cloned),
]));
}
}
}
Task::none()
}
Message::DeleteSession(uid, idx) => {
if let Some((t_mut, _)) = app.store.get_task_mut(&uid) {
t_mut.remove_session(idx);
t_mut.sequence += 1;
let cloned = t_mut.clone();
common::refresh_filtered_tasks(app);
if let Some(tx) = &app.bg_tx {
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(vec![
crate::journal::Action::Update(cloned),
]));
}
}
Task::none()
}
Message::ToggleShowAllSessions(uid) => {
app.expanded_tasks.insert(uid.clone());
if app.show_all_sessions.contains(&uid) {
app.show_all_sessions.remove(&uid);
} else {
app.show_all_sessions.insert(uid);
}
Task::none()
}
Message::KeyboardAddSession => {
if let Some(selected_uid) = app.selected_uid.clone() {
return handle(app, Message::StartAddSession(selected_uid));
}
Task::none()
}
Message::KeyboardToggleSessions => {
if let Some(selected_uid) = app.selected_uid.clone() {
return handle(app, Message::ToggleDetails(selected_uid));
}
Task::none()
}
_ => Task::none(),
}
}
fn handle_submit(app: &mut GuiApp, keep_editing: bool) -> Task<Message> {
use crate::gui::update::common::{
apply_alias_retroactively, refresh_filtered_tasks, save_config,
};
use crate::model::{Task as TodoTask, extract_inline_aliases};
app.input_history.clear();
app.desc_history.clear();
let raw_text = app.input_value.text();
let text_to_submit = raw_text.trim().to_string();
if text_to_submit.is_empty() && app.editing_tree_uid.is_none() {
if !keep_editing {
app.input_value = text_editor::Content::new();
}
return Task::none();
}
if text_to_submit.starts_with(':') && !text_to_submit.contains(' ') {
match text_to_submit.to_lowercase().as_str() {
":undo" => {
app.input_value = text_editor::Content::new();
return handle(app, Message::Undo);
}
":redo" => {
app.input_value = text_editor::Content::new();
return handle(app, Message::Redo);
}
":empty-trash" => {
app.input_value = text_editor::Content::new();
let ctrl = app.controller.clone();
return Task::perform(
async move {
let _ = ctrl.empty_trash().await;
},
|_| Message::Refresh,
);
}
_ => {}
}
}
let (clean_input_1, new_goals) = crate::model::parser::extract_inline_goals(&text_to_submit);
let (clean_input, new_aliases) = extract_inline_aliases(&clean_input_1);
let mut retroactive_sync_batch = Vec::new();
let mut config_changed = false;
if !new_goals.is_empty() {
let old_config = app.core_config.clone();
for (key, goal) in new_goals {
app.core_config.goals.insert(key, goal);
}
app.core_config
.update_sync_timestamp_if_changed(&old_config);
if app.core_config.settings_updated_at != old_config.settings_updated_at
&& let Some(tx) = &app.bg_tx
{
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::SyncNow);
}
config_changed = true;
}
if !new_aliases.is_empty() {
for (key, tags) in new_aliases {
app.tag_aliases.insert(key.clone(), tags.clone());
retroactive_sync_batch.extend(apply_alias_retroactively(app, &key, &tags));
}
config_changed = true;
}
if config_changed {
save_config(app);
}
let trimmed = clean_input.trim();
let is_alias_only = !trimmed.contains(' ')
&& (trimmed.contains(":=") || trimmed.to_lowercase().starts_with("loc:"));
if (trimmed.is_empty() || is_alias_only)
&& app.editing_uid.is_none()
&& app.editing_tree_uid.is_none()
{
if !keep_editing {
app.input_value = text_editor::Content::new();
app.editor_maximized = false;
}
refresh_filtered_tasks(app);
if !retroactive_sync_batch.is_empty() {
let actions: Vec<_> = retroactive_sync_batch
.into_iter()
.map(crate::journal::Action::Update)
.collect();
if let Some(tx) = &app.bg_tx {
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(actions));
}
}
return Task::none();
}
if clean_input.starts_with('#')
&& !clean_input.trim().contains(' ')
&& app.editing_uid.is_none()
&& app.editing_tree_uid.is_none()
{
let tag = clean_input.trim().trim_start_matches('#').to_string();
if !tag.is_empty() && !text_to_submit.contains(":=") {
let tags =
crate::model::parser::resolve_selection_aliases(&tag, false, &app.tag_aliases);
app.sidebar_mode = SidebarMode::Categories;
app.session.selected_categories.clear();
for t in tags {
app.session.selected_categories.push(t);
}
if !keep_editing {
app.input_value = text_editor::Content::new();
}
refresh_filtered_tasks(app);
if !retroactive_sync_batch.is_empty() {
let mut actions = Vec::new();
for t in retroactive_sync_batch {
actions.push(crate::journal::Action::Update(t));
}
if !actions.is_empty()
&& let Some(tx) = &app.bg_tx
{
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(actions));
}
}
return Task::none();
}
}
let is_loc_jump = clean_input.starts_with("@@") || clean_input.starts_with("loc:");
if is_loc_jump
&& !clean_input.trim().contains(' ')
&& app.editing_uid.is_none()
&& app.editing_tree_uid.is_none()
{
let loc = crate::model::parser::strip_quotes(
clean_input
.trim_start_matches("@@")
.trim_start_matches("loc:"),
);
if !loc.is_empty() {
let locs =
crate::model::parser::resolve_selection_aliases(&loc, true, &app.tag_aliases);
app.sidebar_mode = SidebarMode::Locations;
app.session.selected_locations.clear();
for l in locs {
app.session.selected_locations.push(l);
}
if !keep_editing {
app.input_value = text_editor::Content::new();
}
refresh_filtered_tasks(app);
if !retroactive_sync_batch.is_empty() {
let mut actions = Vec::new();
for t in retroactive_sync_batch {
actions.push(crate::journal::Action::Update(t));
}
if !actions.is_empty()
&& let Some(tx) = &app.bg_tx
{
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(actions));
}
}
return Task::none();
}
}
let config_time = NaiveTime::parse_from_str(&app.default_reminder_time, "%H:%M").ok();
let desc_text = app.description_value.text();
let is_journal = app
.editing_tree_uid
.as_ref()
.or(app.editing_uid.as_ref())
.and_then(|uid| app.store.get_task_ref(uid))
.map(|t| t.is_journal)
.unwrap_or(false);
let (cleaned_desc, extracted_subtasks) =
crate::model::extractor::extract_markdown_tasks(&desc_text, is_journal);
if let Some(tree_uid) = &app.editing_tree_uid {
let sync_options = crate::store::SyncTreeOptions {
aliases: &app.tag_aliases,
default_reminder_time: config_time,
trash_retention_days: app.core_config.trash_retention_days,
calendars: &app.calendars,
};
let (mut actions, warnings) =
match app
.store
.sync_tree_from_markdown(tree_uid, &desc_text, &sync_options, is_journal)
{
Ok(res) => res,
Err(e) => {
app.error_msg = Some(e);
return Task::none();
}
};
if !warnings.is_empty() {
for w in warnings {
log::warn!("Dependency resolution: {}", w);
}
}
app.selected_uid = Some(tree_uid.clone());
if !keep_editing {
app.input_value = text_editor::Content::new();
app.description_value = text_editor::Content::new();
app.editing_tree_uid = None;
app.editor_maximized = false;
} else {
let tree_md = crate::model::extractor::serialize_task_tree(
&app.store,
tree_uid,
&app.calendars,
false,
);
app.description_value = text_editor::Content::with_text(&tree_md);
}
refresh_filtered_tasks(app);
actions.extend(
retroactive_sync_batch
.into_iter()
.map(crate::journal::Action::Update),
);
if !actions.is_empty()
&& let Some(tx) = &app.bg_tx
{
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(actions));
}
return Task::none();
} else if let Some(edit_uid) = app.editing_uid.clone() {
if let Some(task_ref) = app.store.get_task_ref(&edit_uid) {
let old_href = task_ref.calendar_href.clone();
let mut task = task_ref.clone();
task.description = cleaned_desc.clone();
task.apply_smart_input(&clean_input, &app.tag_aliases, config_time);
let warnings = app.store.resolve_dependencies(&mut task);
if !warnings.is_empty() {
for w in warnings {
log::warn!("Dependency resolution: {}", w);
}
}
if let Some(target) = task.target_collection.take() {
task.calendar_href =
crate::model::resolve_collection(&target, &app.calendars, &old_href);
}
let new_href = task.calendar_href.clone();
task.sequence += 1;
let task_copy = task.clone();
app.store.update_or_add_task(task);
if !keep_editing {
app.input_value = text_editor::Content::new();
app.description_value = text_editor::Content::new();
app.editing_uid = None;
app.editor_maximized = false;
} else {
app.input_value = text_editor::Content::with_text(&clean_input);
app.description_value = text_editor::Content::with_text(&cleaned_desc);
}
app.selected_uid = Some(task_copy.uid.clone());
let mut actions = Vec::new();
if old_href != new_href {
actions.push(crate::journal::Action::Move(
task_copy.clone(),
new_href.clone(),
));
}
actions.push(crate::journal::Action::Update(task_copy.clone()));
let mut resolved_props = std::collections::HashMap::new();
resolved_props.insert(
edit_uid.clone(),
(
task_copy.categories.clone(),
task_copy.locations.clone(),
task_copy.priority,
),
);
for ext in extracted_subtasks {
let mut sub = TodoTask::new(&ext.raw_text, &app.tag_aliases, config_time);
sub.uid = ext.uid;
let p_uid_str = ext.parent_uid.clone().unwrap_or_else(|| edit_uid.clone());
if let Some((p_cats, p_loc, p_prio)) = resolved_props.get(&p_uid_str) {
sub.inherit_properties(p_cats, p_loc, *p_prio);
}
resolved_props.insert(
sub.uid.clone(),
(sub.categories.clone(), sub.locations.clone(), sub.priority),
);
let warnings = app.store.resolve_dependencies(&mut sub);
if !warnings.is_empty() {
for w in warnings {
log::warn!("Dependency resolution: {}", w);
}
}
if !ext.description.is_empty() {
if sub.description.is_empty() {
sub.description = ext.description;
} else {
sub.description
.push_str(&format!("\n\n{}", ext.description));
}
}
sub.apply_extracted_status(ext.status);
sub.parent_uid = Some(ext.parent_uid.unwrap_or(edit_uid.clone()));
sub.dependencies = ext.dependencies;
sub.calendar_href = new_href.clone();
if let Some(pc) = ext.percent_complete {
sub.percent_complete = Some(pc);
}
sub.is_note = ext.is_note;
if is_journal && ext.is_note {
sub.is_journal = true;
}
app.store.add_task(sub.clone());
actions.push(crate::journal::Action::Create(sub));
}
refresh_filtered_tasks(app);
for t in retroactive_sync_batch {
actions.push(crate::journal::Action::Update(t));
}
if !actions.is_empty()
&& let Some(tx) = &app.bg_tx
{
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(actions));
}
return Task::none();
}
} else if !clean_input.is_empty() {
let mut new_task = TodoTask::new(&clean_input, &app.tag_aliases, config_time);
let warnings = app.store.resolve_dependencies(&mut new_task);
if !warnings.is_empty() {
for w in warnings {
log::warn!("Dependency resolution: {}", w);
}
}
if new_task.summary.trim().is_empty() && cleaned_desc.is_empty() {
if !keep_editing {
app.input_value = text_editor::Content::new();
app.description_value = text_editor::Content::new();
app.creating_with_desc = false;
app.editor_maximized = false;
}
return Task::none();
}
if !cleaned_desc.is_empty() {
if new_task.description.is_empty() {
new_task.description = cleaned_desc.clone();
} else {
new_task
.description
.push_str(&format!("\n\n{}", cleaned_desc));
}
}
let parent = if let Some(parent_uid) = &app.creating_child_of {
app.store.get_task_ref(parent_uid).cloned()
} else {
None
};
if let Some(parent_ref) = &parent {
new_task.parent_uid = Some(parent_ref.uid.clone());
if !app.child_lock_active {
app.creating_child_of = None;
}
}
let target_href = parent
.as_ref()
.map(|p| p.calendar_href.clone())
.or_else(|| app.active_cal_href.clone())
.or_else(|| {
app.calendars
.iter()
.find(|c| {
!app.hidden_calendars.contains(&c.href)
&& !app.disabled_calendars.contains(&c.href)
&& c.href != LOCAL_TRASH_HREF
&& c.href != "local://recovery"
})
.map(|c| c.href.clone())
})
.unwrap_or_default();
if !target_href.is_empty() {
new_task.calendar_href = target_href.clone();
if let Some(target) = new_task.target_collection.take() {
new_task.calendar_href = crate::model::resolve_collection(
&target,
&app.calendars,
&new_task.calendar_href,
);
}
let inherited_href = new_task.calendar_href.clone();
let parent_uid = new_task.uid.clone();
let parent_is_journal = new_task.is_journal;
let mut resolved_props = std::collections::HashMap::new();
resolved_props.insert(
parent_uid.clone(),
(
new_task.categories.clone(),
new_task.locations.clone(),
new_task.priority,
),
);
let mut tasks_to_create = vec![new_task];
for ext in extracted_subtasks {
let mut sub = TodoTask::new(&ext.raw_text, &app.tag_aliases, config_time);
sub.uid = ext.uid;
let p_uid_str = ext.parent_uid.clone().unwrap_or_else(|| parent_uid.clone());
if let Some((p_cats, p_loc, p_prio)) = resolved_props.get(&p_uid_str) {
sub.inherit_properties(p_cats, p_loc, *p_prio);
}
resolved_props.insert(
sub.uid.clone(),
(sub.categories.clone(), sub.locations.clone(), sub.priority),
);
let warnings = app.store.resolve_dependencies(&mut sub);
if !warnings.is_empty() {
for w in warnings {
log::warn!("Dependency resolution: {}", w);
}
}
if !ext.description.is_empty() {
if sub.description.is_empty() {
sub.description = ext.description;
} else {
sub.description
.push_str(&format!("\n\n{}", ext.description));
}
}
sub.apply_extracted_status(ext.status);
sub.parent_uid = Some(ext.parent_uid.unwrap_or(parent_uid.clone()));
sub.dependencies.extend(ext.dependencies);
sub.dependencies.sort();
sub.dependencies.dedup();
if let Some(target) = sub.target_collection.take() {
sub.calendar_href =
crate::model::resolve_collection(&target, &app.calendars, &inherited_href);
} else {
sub.calendar_href = inherited_href.clone();
}
if let Some(pc) = ext.percent_complete {
sub.percent_complete = Some(pc);
}
sub.is_note = ext.is_note;
if parent_is_journal && ext.is_note {
sub.is_journal = true;
}
tasks_to_create.push(sub);
}
for t in &mut tasks_to_create {
let warnings = app.store.resolve_dependencies(t);
if !warnings.is_empty() {
for w in warnings {
log::warn!("Dependency resolution: {}", w);
}
}
}
app.task_ids
.entry(parent_uid.clone())
.or_insert_with(iced::widget::Id::unique);
for t in &tasks_to_create {
app.store.add_task(t.clone());
}
app.selected_uid = Some(parent_uid.clone());
refresh_filtered_tasks(app);
if !keep_editing {
app.input_value = text_editor::Content::new();
app.description_value = text_editor::Content::new();
app.creating_with_desc = false;
app.editor_maximized = false;
} else {
app.creating_with_desc = false;
app.editing_uid = Some(parent_uid.clone());
app.input_value = text_editor::Content::with_text(&clean_input);
app.description_value = text_editor::Content::with_text(&cleaned_desc);
}
let scroll_cmd = common::scroll_to_selected_delayed(app, false);
let focus_cmd = iced::widget::operation::focus(iced::widget::Id::new("main_input"));
let mut actions = Vec::new();
for t in tasks_to_create {
actions.push(crate::journal::Action::Create(t));
}
if !retroactive_sync_batch.is_empty() {
for t in retroactive_sync_batch {
actions.push(crate::journal::Action::Update(t));
}
}
if !actions.is_empty()
&& let Some(tx) = &app.bg_tx
{
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(actions));
}
return Task::batch(vec![scroll_cmd, focus_cmd]);
}
}
if !retroactive_sync_batch.is_empty() {
let mut actions = Vec::new();
for t in retroactive_sync_batch {
actions.push(crate::journal::Action::Update(t));
}
if !actions.is_empty()
&& let Some(tx) = &app.bg_tx
{
let _ = tx.try_send(crate::gui::async_ops::WorkerCommand::Batch(actions));
}
}
Task::none()
}