1use iced::Task;
8
9use crate::message::Message;
10use crate::state::GitKraft;
11
12impl GitKraft {
13 pub fn update(&mut self, message: Message) -> Task<Message> {
17 match &message {
18 Message::SwitchTab(index) => {
20 let index = *index;
21 if index < self.tabs.len() {
22 self.active_tab = index;
23 }
24 Task::none()
25 }
26
27 Message::NewTab => {
28 self.tabs.push(crate::state::RepoTab::new_empty());
29 self.active_tab = self.tabs.len() - 1;
30 crate::features::repo::commands::load_recent_repos_async()
32 }
33
34 Message::CloseTab(index) => {
35 let index = *index;
36 if self.tabs.len() > 1 && index < self.tabs.len() {
37 self.tabs.remove(index);
38 if self.active_tab >= self.tabs.len() {
40 self.active_tab = self.tabs.len() - 1;
41 } else if self.active_tab > index {
42 self.active_tab -= 1;
43 }
44 }
45 let open_tabs = self.open_tab_paths();
46 let active = self.active_tab;
47 crate::features::repo::commands::save_session_async(open_tabs, active)
48 }
49
50 Message::OpenRepo
52 | Message::InitRepo
53 | Message::RepoSelected(_)
54 | Message::RepoInitSelected(_)
55 | Message::RepoOpened(_)
56 | Message::RefreshRepo
57 | Message::RepoRefreshed(_)
58 | Message::OpenRecentRepo(_)
59 | Message::CloseRepo
60 | Message::RepoRecorded(_)
61 | Message::RepoRestoredAt(_, _)
62 | Message::MoreCommitsLoaded(_)
63 | Message::SettingsLoaded(_)
64 | Message::GitOperationResult(_) => {
65 crate::features::repo::update::update(self, message)
66 }
67
68 Message::CheckoutBranch(_)
70 | Message::BranchCheckedOut(_)
71 | Message::CreateBranch
72 | Message::NewBranchNameChanged(_)
73 | Message::BranchCreated(_)
74 | Message::DeleteBranch(_)
75 | Message::BranchDeleted(_)
76 | Message::ToggleBranchCreate
77 | Message::ToggleLocalBranches
78 | Message::ToggleRemoteBranches => {
79 crate::features::branches::update::update(self, message)
80 }
81
82 Message::SelectCommit(_)
84 | Message::CommitFileListLoaded(_)
85 | Message::SingleFileDiffLoaded(_)
86 | Message::DiffFileWithWorkingTree(_, _)
87 | Message::DiffWithWorkingTreeLoaded(_)
88 | Message::DiffMultiWithWorkingTree(_, _)
89 | Message::CheckoutFileAtCommit(_, _)
90 | Message::CheckoutMultiFilesAtCommit(_, _)
91 | Message::CommitRangeDiffLoaded(_) => {
92 crate::features::commits::update::update(self, message)
95 }
96
97 Message::CommitMessageChanged(_)
98 | Message::CreateCommit
99 | Message::CommitCreated(_) => crate::features::commits::update::update(self, message),
100
101 Message::CommitLogScrolled(abs_y, rel_y) => {
102 const COMMITS_PAGE_SIZE: usize = 200;
106 const LOAD_TRIGGER_RELATIVE: f32 = 0.85;
109
110 self.active_tab_mut().commit_scroll_offset = *abs_y;
111
112 let tab = self.active_tab();
113 if *rel_y >= LOAD_TRIGGER_RELATIVE
114 && tab.has_more_commits
115 && !tab.is_loading_more_commits
116 {
117 if let Some(path) = tab.repo_path.clone() {
118 let current = tab.commits.len();
119 self.active_tab_mut().is_loading_more_commits = true;
120 return crate::features::repo::commands::load_more_commits(
121 path,
122 current,
123 COMMITS_PAGE_SIZE,
124 );
125 }
126 }
127 Task::none()
128 }
129
130 Message::DiffViewScrolled(abs_y) => {
131 self.active_tab_mut().diff_scroll_offset = *abs_y;
132 Task::none()
133 }
134
135 Message::StageFile(_)
137 | Message::UnstageFile(_)
138 | Message::StageAll
139 | Message::UnstageAll
140 | Message::DiscardFile(_)
141 | Message::ConfirmDiscard(_)
142 | Message::CancelDiscard
143 | Message::StagingUpdated(_)
144 | Message::ToggleSelectUnstaged(_)
145 | Message::ToggleSelectStaged(_)
146 | Message::StageSelected
147 | Message::UnstageSelected
148 | Message::DiscardSelected
149 | Message::DiscardStagedFile(_) => {
150 crate::features::staging::update::update(self, message)
151 }
152
153 Message::StashSave
155 | Message::StashPop(_)
156 | Message::StashDrop(_)
157 | Message::StashUpdated(_)
158 | Message::StashMessageChanged(_)
159 | Message::StashApply(_)
160 | Message::ViewStashDiff(_)
161 | Message::StashDiffLoaded(_) => crate::features::stash::update::update(self, message),
162
163 Message::Fetch | Message::FetchCompleted(_) => {
165 crate::features::remotes::update::update(self, message)
166 }
167
168 Message::SelectDiff(_)
170 | Message::SelectDiffByIndex(_)
171 | Message::CommitMultiDiffLoaded(_) => {
172 crate::features::diff::update::update(self, message)
173 }
174
175 Message::ModifiersChanged(mods) => {
176 self.keyboard_modifiers = *mods;
177 Task::none()
178 }
179
180 Message::DismissError => {
181 self.active_tab_mut().error_message = None;
182 Task::none()
183 }
184
185 Message::ZoomIn => {
186 self.ui_scale = (self.ui_scale + 0.1).min(2.0);
187 crate::features::repo::commands::save_layout_async(self.current_layout())
188 }
189
190 Message::ZoomOut => {
191 self.ui_scale = (self.ui_scale - 0.1).max(0.5);
192 crate::features::repo::commands::save_layout_async(self.current_layout())
193 }
194
195 Message::ZoomReset => {
196 self.ui_scale = 1.0;
197 crate::features::repo::commands::save_layout_async(self.current_layout())
198 }
199
200 Message::ToggleSidebar => {
201 self.sidebar_expanded = !self.sidebar_expanded;
202 crate::features::repo::commands::save_layout_async(self.current_layout())
203 }
204
205 Message::PaneDragStart(target, _x) => {
207 self.dragging = Some(*target);
208 self.drag_initialized = false;
212 Task::none()
213 }
214
215 Message::PaneDragStartH(target, _y) => {
216 self.dragging_h = Some(*target);
217 self.drag_initialized_h = false;
218 Task::none()
219 }
220
221 Message::PaneDragMove(x, y) => {
222 use crate::state::{DragTarget, DragTargetH};
223
224 self.cursor_pos = iced::Point::new(*x, *y);
226
227 if let Some(target) = self.dragging {
228 if !self.drag_initialized {
229 self.drag_start_x = *x;
231 self.drag_initialized = true;
232 } else {
233 let dx = *x - self.drag_start_x;
234 self.drag_start_x = *x;
235
236 match target {
237 DragTarget::SidebarRight => {
238 self.sidebar_width = (self.sidebar_width + dx).clamp(120.0, 500.0);
239 }
240 DragTarget::CommitLogRight => {
241 self.commit_log_width =
242 (self.commit_log_width + dx).clamp(200.0, 1200.0);
243 }
244 DragTarget::DiffFileListRight => {
245 self.diff_file_list_width =
246 (self.diff_file_list_width + dx).clamp(100.0, 400.0);
247 }
248 }
249 }
250 }
251
252 if let Some(target_h) = self.dragging_h {
253 if !self.drag_initialized_h {
254 self.drag_start_y = *y;
255 self.drag_initialized_h = true;
256 } else {
257 let dy = *y - self.drag_start_y;
258 self.drag_start_y = *y;
259
260 match target_h {
261 DragTargetH::StagingTop => {
262 self.staging_height =
264 (self.staging_height - dy).clamp(100.0, 600.0);
265 }
266 }
267 }
268 }
269
270 Task::none()
271 }
272
273 Message::PaneDragEnd => {
274 self.dragging = None;
275 self.dragging_h = None;
276 self.drag_initialized = false;
277 self.drag_initialized_h = false;
278 crate::features::repo::commands::save_layout_async(self.current_layout())
279 }
280
281 Message::OpenBranchContextMenu(name, local_index, is_current) => {
283 let pos = (self.cursor_pos.x, self.cursor_pos.y);
284 let tab = self.active_tab_mut();
285 tab.context_menu_pos = pos;
286 tab.context_menu = Some(crate::state::ContextMenu::Branch {
287 name: name.clone(),
288 is_current: *is_current,
289 local_index: *local_index,
290 });
291 Task::none()
292 }
293
294 Message::OpenRemoteBranchContextMenu(name) => {
295 let pos = (self.cursor_pos.x, self.cursor_pos.y);
296 let tab = self.active_tab_mut();
297 tab.context_menu_pos = pos;
298 tab.context_menu =
299 Some(crate::state::ContextMenu::RemoteBranch { name: name.clone() });
300 Task::none()
301 }
302
303 Message::OpenCommitFileContextMenu(oid, file_path) => {
304 let pos = (self.cursor_pos.x, self.cursor_pos.y);
305 let tab = self.active_tab_mut();
306 tab.context_menu_pos = pos;
307 tab.context_menu = Some(crate::state::ContextMenu::CommitFile {
308 oid: oid.clone(),
309 file_path: file_path.clone(),
310 });
311 Task::none()
312 }
313
314 Message::OpenStashContextMenu(index) => {
315 let index = *index;
316 let pos = (self.cursor_pos.x, self.cursor_pos.y);
317 let tab = self.active_tab_mut();
318 tab.context_menu_pos = pos;
319 tab.context_menu = Some(crate::state::ContextMenu::Stash { index });
320 Task::none()
321 }
322
323 Message::OpenUnstagedFileContextMenu(path) => {
324 let pos = (self.cursor_pos.x, self.cursor_pos.y);
325 let tab = self.active_tab_mut();
326 tab.context_menu_pos = pos;
327 tab.context_menu =
328 Some(crate::state::ContextMenu::UnstagedFile { path: path.clone() });
329 Task::none()
330 }
331
332 Message::OpenStagedFileContextMenu(path) => {
333 let pos = (self.cursor_pos.x, self.cursor_pos.y);
334 let tab = self.active_tab_mut();
335 tab.context_menu_pos = pos;
336 tab.context_menu =
337 Some(crate::state::ContextMenu::StagedFile { path: path.clone() });
338 Task::none()
339 }
340
341 Message::OpenCommitContextMenu(idx) => {
342 let oid = self.active_tab().commits.get(*idx).map(|c| c.oid.clone());
343 let pos = (self.cursor_pos.x, self.cursor_pos.y);
344 if let Some(oid) = oid {
345 let tab = self.active_tab_mut();
346 tab.context_menu_pos = pos;
347 tab.context_menu = Some(crate::state::ContextMenu::Commit { index: *idx, oid });
348 }
349 Task::none()
350 }
351
352 Message::OpenSearchResultContextMenu(idx) => {
353 if let Some(commit) = self.search_results.get(*idx) {
354 let oid = commit.oid.clone();
355 let pos = (self.cursor_pos.x, self.cursor_pos.y);
356 let tab = self.active_tab_mut();
357 tab.context_menu_pos = pos;
358 tab.context_menu = Some(crate::state::ContextMenu::Commit { index: *idx, oid });
359 }
360 Task::none()
361 }
362
363 Message::CloseContextMenu => {
364 self.active_tab_mut().context_menu = None;
365 Task::none()
366 }
367
368 Message::BeginRenameBranch(name) => {
370 let tab = self.active_tab_mut();
371 tab.context_menu = None;
372 tab.rename_branch_input = name.clone();
373 tab.rename_branch_target = Some(name.clone());
374 Task::none()
375 }
376
377 Message::RenameBranchInputChanged(s) => {
378 self.active_tab_mut().rename_branch_input = s.clone();
379 Task::none()
380 }
381
382 Message::CancelRename => {
383 let tab = self.active_tab_mut();
384 tab.rename_branch_target = None;
385 tab.rename_branch_input.clear();
386 Task::none()
387 }
388
389 Message::ConfirmRenameBranch => {
390 let (original, new_name, path) = {
391 let tab = self.active_tab();
392 (
393 tab.rename_branch_target.clone(),
394 tab.rename_branch_input.trim().to_string(),
395 tab.repo_path.clone(),
396 )
397 };
398 if let (Some(orig), false) = (&original, new_name.is_empty()) {
399 if *orig != new_name {
400 if let Some(path) = path {
401 let orig = orig.clone();
402 {
403 let tab = self.active_tab_mut();
404 tab.rename_branch_target = None;
405 tab.rename_branch_input.clear();
406 tab.is_loading = true;
407 tab.status_message =
408 Some(format!("Renaming '{orig}' → '{new_name}'…"));
409 }
410 return crate::features::repo::commands::rename_branch_async(
411 path, orig, new_name,
412 );
413 }
414 }
415 }
416 self.active_tab_mut().rename_branch_target = None;
417 Task::none()
418 }
419
420 Message::PushBranch(name) => {
422 let name = name.clone();
423 let remote = self
424 .active_tab()
425 .remotes
426 .first()
427 .map(|r| r.name.clone())
428 .unwrap_or_else(|| "origin".to_string());
429 self.active_tab_mut().context_menu = None;
430 with_repo!(
431 self,
432 loading,
433 format!("Pushing '{name}' to {remote}…"),
434 |path| crate::features::repo::commands::push_branch_async(path, name, remote)
435 )
436 }
437
438 Message::PullBranch(_name) => {
439 let remote = self
440 .active_tab()
441 .remotes
442 .first()
443 .map(|r| r.name.clone())
444 .unwrap_or_else(|| "origin".to_string());
445 self.active_tab_mut().context_menu = None;
446 with_repo!(
447 self,
448 loading,
449 format!("Pulling from {remote} (rebase)…"),
450 |path| crate::features::repo::commands::pull_rebase_async(path, remote)
451 )
452 }
453
454 Message::RebaseOnto(target) => {
455 let target = target.clone();
456 self.active_tab_mut().context_menu = None;
457 with_repo!(
458 self,
459 loading,
460 format!("Rebasing onto '{target}'…"),
461 |path| crate::features::repo::commands::rebase_onto_async(path, target)
462 )
463 }
464
465 Message::MergeBranch(name) => {
466 let name = name.clone();
467 self.active_tab_mut().context_menu = None;
468 with_repo!(
469 self,
470 loading,
471 format!("Merging '{name}' into current branch…"),
472 |path| crate::features::repo::commands::merge_branch_async(path, name)
473 )
474 }
475
476 Message::CheckoutRemoteBranch(name) => {
477 let name = name.clone();
478 self.active_tab_mut().context_menu = None;
479 with_repo!(self, loading, format!("Checking out '{name}'…"), |path| {
480 crate::features::repo::commands::checkout_remote_branch_async(path, name)
481 })
482 }
483
484 Message::DeleteRemoteBranch(name) => {
485 let name = name.clone();
486 self.active_tab_mut().context_menu = None;
487 with_repo!(
488 self,
489 loading,
490 format!("Deleting remote branch '{name}'…"),
491 |path| crate::features::repo::commands::delete_remote_branch_async(path, name)
492 )
493 }
494
495 Message::BeginCreateTag(oid, annotated) => {
496 let tab = self.active_tab_mut();
497 tab.context_menu = None;
498 tab.create_tag_target_oid = Some(oid.clone());
499 tab.create_tag_annotated = *annotated;
500 tab.create_tag_name.clear();
501 tab.create_tag_message.clear();
502 Task::none()
503 }
504
505 Message::TagNameChanged(s) => {
506 self.active_tab_mut().create_tag_name = s.clone();
507 Task::none()
508 }
509
510 Message::TagMessageChanged(s) => {
511 self.active_tab_mut().create_tag_message = s.clone();
512 Task::none()
513 }
514
515 Message::ConfirmCreateTag => {
516 let (oid, name, message, annotated, path) = {
517 let tab = self.active_tab();
518 (
519 tab.create_tag_target_oid.clone(),
520 tab.create_tag_name.trim().to_string(),
521 tab.create_tag_message.trim().to_string(),
522 tab.create_tag_annotated,
523 tab.repo_path.clone(),
524 )
525 };
526 if let (Some(oid), false) = (&oid, name.is_empty()) {
527 if let Some(path) = path {
528 let oid = oid.clone();
529 {
530 let tab = self.active_tab_mut();
531 tab.create_tag_target_oid = None;
532 tab.create_tag_name.clear();
533 tab.create_tag_message.clear();
534 tab.is_loading = true;
535 tab.status_message = Some(format!("Creating tag '{name}'…"));
536 }
537 return if annotated {
538 crate::features::repo::commands::create_annotated_tag_async(
539 path, name, message, oid,
540 )
541 } else {
542 crate::features::repo::commands::create_tag_async(path, name, oid)
543 };
544 }
545 }
546 Task::none()
547 }
548
549 Message::CancelCreateTag => {
550 let tab = self.active_tab_mut();
551 tab.create_tag_target_oid = None;
552 tab.create_tag_name.clear();
553 tab.create_tag_message.clear();
554 Task::none()
555 }
556
557 Message::CherryPickCommit(oid) => {
558 let oid = oid.clone();
559 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
560 self.active_tab_mut().context_menu = None;
561 with_repo!(self, loading, format!("Cherry-picking {short}…"), |path| {
562 crate::features::repo::commands::cherry_pick_async(path, oid)
563 })
564 }
565
566 Message::BeginCreateBranchAtCommit(oid) => {
567 let tab = self.active_tab_mut();
568 tab.context_menu = None;
569 tab.create_branch_at_oid = Some(oid.clone());
570 tab.new_branch_name.clear();
571 Task::none()
572 }
573
574 Message::ConfirmCreateBranchAtCommit => {
575 let (oid, name, path) = {
576 let tab = self.active_tab();
577 (
578 tab.create_branch_at_oid.clone(),
579 tab.new_branch_name.trim().to_string(),
580 tab.repo_path.clone(),
581 )
582 };
583 if let (Some(oid), false) = (&oid, name.is_empty()) {
584 if let Some(path) = path {
585 let oid = oid.clone();
586 {
587 let tab = self.active_tab_mut();
588 tab.create_branch_at_oid = None;
589 tab.new_branch_name.clear();
590 tab.is_loading = true;
591 tab.status_message = Some(format!("Creating branch '{name}'…"));
592 }
593 return crate::features::repo::commands::create_branch_at_commit_async(
594 path, name, oid,
595 );
596 }
597 }
598 Task::none()
599 }
600
601 Message::CancelCreateBranchAtCommit => {
602 let tab = self.active_tab_mut();
603 tab.create_branch_at_oid = None;
604 tab.new_branch_name.clear();
605 Task::none()
606 }
607
608 Message::ExecuteCommitAction(oid, action) => {
610 let oid = oid.clone();
611 let action = action.clone();
612 let label = action.label();
613 self.active_tab_mut().context_menu = None;
614 with_repo!(self, loading, format!("{label}…"), |path| {
615 crate::features::repo::commands::execute_commit_action_async(path, oid, action)
616 })
617 }
618
619 Message::CheckoutCommitDetached(oid) => {
620 let oid = oid.clone();
621 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
622 self.active_tab_mut().context_menu = None;
623 with_repo!(self, loading, format!("Checking out {short}…"), |path| {
624 crate::features::repo::commands::checkout_commit_async(path, oid)
625 })
626 }
627
628 Message::RebaseOntoCommit(oid) => {
629 let oid = oid.clone();
630 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
631 self.active_tab_mut().context_menu = None;
632 with_repo!(self, loading, format!("Rebasing onto {short}…"), |path| {
633 crate::features::repo::commands::rebase_onto_async(path, oid)
634 })
635 }
636
637 Message::RevertCommit(oid) => {
638 let oid = oid.clone();
639 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
640 self.active_tab_mut().context_menu = None;
641 with_repo!(self, loading, format!("Reverting {short}…"), |path| {
642 crate::features::repo::commands::revert_commit_async(path, oid)
643 })
644 }
645
646 Message::ResetSoft(ref oid)
647 | Message::ResetMixed(ref oid)
648 | Message::ResetHard(ref oid) => {
649 let mode = match &message {
650 Message::ResetSoft(_) => "soft",
651 Message::ResetMixed(_) => "mixed",
652 Message::ResetHard(_) => "hard",
653 _ => unreachable!(),
654 };
655 let oid = oid.clone();
656 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
657 self.active_tab_mut().context_menu = None;
658 with_repo!(
659 self,
660 loading,
661 format!("Resetting ({mode}) to {short}…"),
662 |path| crate::features::repo::commands::reset_to_commit_async(
663 path,
664 oid,
665 mode.to_string()
666 )
667 )
668 }
669
670 Message::CopyText(text) => {
672 self.active_tab_mut().context_menu = None;
673 iced::clipboard::write(text.clone())
674 }
675
676 Message::ThemeChanged(index) => {
678 self.current_theme_index = *index;
679 let name = gitkraft_core::THEME_NAMES
681 .get(*index)
682 .copied()
683 .unwrap_or("Default");
684 crate::features::repo::commands::save_theme_async(name.to_string())
685 }
686
687 Message::ThemeSaved(_result) => {
688 Task::none()
690 }
691
692 Message::EditorChanged(editor) => {
693 self.editor = editor.clone();
694 self.active_tab_mut().status_message =
695 Some(format!("Editor set to {}", self.editor));
696 let name = self.editor.display_name().to_string();
698 crate::features::repo::commands::save_editor_async(name)
699 }
700
701 Message::EditorSaved(_result) => {
702 Task::none()
704 }
705
706 Message::LayoutSaved(_result) => {
707 Task::none()
709 }
710
711 Message::SessionSaved(_) => {
712 Task::none()
714 }
715
716 Message::LayoutLoaded(result) => {
717 if let Ok(Some(layout)) = result {
718 if let Some(w) = layout.sidebar_width {
719 self.sidebar_width = w;
720 }
721 if let Some(w) = layout.commit_log_width {
722 self.commit_log_width = w;
723 }
724 if let Some(h) = layout.staging_height {
725 self.staging_height = h;
726 }
727 if let Some(w) = layout.diff_file_list_width {
728 self.diff_file_list_width = w;
729 }
730 if let Some(expanded) = layout.sidebar_expanded {
731 self.sidebar_expanded = expanded;
732 }
733 if let Some(scale) = layout.ui_scale {
734 self.ui_scale = scale.clamp(0.5, 2.0);
735 }
736 }
737 Task::none()
738 }
739
740 Message::ToggleSearch => {
742 self.search_visible = !self.search_visible;
743 if !self.search_visible {
744 self.search_query.clear();
745 self.search_results.clear();
746 self.search_selected = None;
747 self.search_diff_files.clear();
748 self.search_diff_selected.clear();
749 self.search_diff_content.clear();
750 self.search_diff_oid = None;
751 Task::none()
752 } else {
753 iced::widget::operation::focus_next()
754 }
755 }
756
757 Message::SearchQueryChanged(query) => {
758 let query = query.clone();
759 self.search_query = query.clone();
760 if query.trim().len() >= 2 {
761 if let Some(path) = self.active_tab().repo_path.clone() {
762 return crate::features::commits::commands::search_commits(path, query);
763 }
764 } else {
765 self.search_results.clear();
766 self.search_selected = None;
767 }
768 Task::none()
769 }
770
771 Message::SearchResultsLoaded(result) => {
772 match result {
773 Ok(results) => {
774 self.search_results = results.clone();
775 self.search_selected = if self.search_results.is_empty() {
776 None
777 } else {
778 Some(0)
779 };
780 }
781 Err(e) => {
782 self.search_results.clear();
783 self.active_tab_mut().error_message = Some(format!("Search failed: {e}"));
784 }
785 }
786 Task::none()
787 }
788
789 Message::SelectSearchResult(index) => {
790 let index = *index;
791 if index < self.search_results.len() {
792 self.search_selected = Some(index);
793 }
794 Task::none()
795 }
796
797 Message::ConfirmSearchResult => {
798 if let Some(idx) = self.search_selected {
799 if let Some(commit) = self.search_results.get(idx).cloned() {
800 let oid = commit.oid.clone();
801 self.search_diff_oid = Some(oid.clone());
803 self.search_diff_files.clear();
804 self.search_diff_selected.clear();
805 self.search_diff_content.clear();
806
807 if let Some(path) = self.active_tab().repo_path.clone() {
808 return crate::features::commits::commands::search_diff_file_list(
809 path, oid,
810 );
811 }
812 }
813 }
814 Task::none()
815 }
816
817 Message::SearchDiffFilesLoaded(result) => {
818 match result {
819 Ok(files) => {
820 self.search_diff_files = files.clone();
821 self.search_diff_selected.clear();
822 self.search_diff_content.clear();
823 }
824 Err(e) => {
825 self.active_tab_mut().error_message =
826 Some(format!("Failed to load diff files: {e}"));
827 }
828 }
829 Task::none()
830 }
831
832 Message::ToggleSearchDiffFile(index) => {
833 let index = *index;
834 if self.search_diff_selected.contains(&index) {
835 self.search_diff_selected.remove(&index);
836 } else {
837 self.search_diff_selected.insert(index);
838 }
839 Task::none()
840 }
841
842 Message::ToggleSearchDiffSelectAll => {
843 if self.search_diff_selected.len() == self.search_diff_files.len() {
844 self.search_diff_selected.clear();
845 } else {
846 self.search_diff_selected = (0..self.search_diff_files.len()).collect();
847 }
848 Task::none()
849 }
850
851 Message::ViewSearchDiffFile(index) => {
852 let index = *index;
853 if let Some(file) = self.search_diff_files.get(index) {
854 let file_path = file.display_path().to_string();
855 if let (Some(oid), Some(repo_path)) = (
856 self.search_diff_oid.clone(),
857 self.active_tab().repo_path.clone(),
858 ) {
859 return crate::features::commits::commands::search_diff_file(
860 repo_path, oid, file_path,
861 );
862 }
863 }
864 Task::none()
865 }
866
867 Message::SearchFileDiffLoaded(result) => {
868 match result {
869 Ok(diff) => {
870 self.search_diff_content = vec![diff.clone()];
871 }
872 Err(e) => {
873 self.active_tab_mut().error_message =
874 Some(format!("Failed to load file diff: {e}"));
875 }
876 }
877 Task::none()
878 }
879
880 Message::DiffSelectedFiles => {
881 if self.search_diff_selected.is_empty() {
882 return Task::none();
883 }
884 let file_paths: Vec<String> = self
885 .search_diff_selected
886 .iter()
887 .filter_map(|&i| self.search_diff_files.get(i))
888 .map(|f| f.display_path().to_string())
889 .collect();
890 if let (Some(oid), Some(repo_path)) = (
891 self.search_diff_oid.clone(),
892 self.active_tab().repo_path.clone(),
893 ) {
894 return crate::features::commits::commands::search_diff_multi_files(
895 repo_path, oid, file_paths,
896 );
897 }
898 Task::none()
899 }
900
901 Message::SearchMultiDiffLoaded(result) => {
902 match result {
903 Ok(diffs) => {
904 self.search_diff_content = diffs.clone();
905 }
906 Err(e) => {
907 self.active_tab_mut().error_message =
908 Some(format!("Failed to load diffs: {e}"));
909 }
910 }
911 Task::none()
912 }
913
914 Message::SearchDiffBack => {
915 self.search_diff_content.clear();
916 Task::none()
917 }
918
919 Message::FileSystemChanged => {
920 if self.has_repo() && !self.active_tab().is_loading {
921 if let Some(path) = self.active_tab().repo_path.clone() {
922 return crate::features::repo::commands::refresh_staging_only(path);
923 }
924 }
925 Task::none()
926 }
927
928 Message::OpenInEditor(path) => {
929 self.active_tab_mut().context_menu = None;
930 if matches!(self.editor, gitkraft_core::Editor::None) {
931 self.active_tab_mut().status_message = Some(
932 "No editor configured — select one from the editor dropdown in the toolbar"
933 .into(),
934 );
935 return Task::none();
936 }
937 if let Some(repo_path) = self.active_tab().repo_path.as_ref() {
938 let full_path = repo_path.join(path);
939 match self.editor.open_file(&full_path) {
940 Ok(()) => {
941 self.active_tab_mut().status_message =
942 Some(format!("Opened in {}", self.editor));
943 }
944 Err(e) => {
945 self.active_tab_mut().error_message =
946 Some(format!("Failed to open editor: {e}"));
947 }
948 }
949 }
950 Task::none()
951 }
952
953 Message::OpenInDefaultProgram(path) => {
954 self.active_tab_mut().context_menu = None;
955 if let Some(repo_path) = self.active_tab().repo_path.as_ref() {
956 let full_path = repo_path.join(path);
957 if let Err(e) = gitkraft_core::open_file_default(&full_path) {
958 self.active_tab_mut().error_message =
959 Some(format!("Failed to open file: {e}"));
960 }
961 }
962 Task::none()
963 }
964
965 Message::ShowInFolder(path) => {
966 self.active_tab_mut().context_menu = None;
967 if let Some(repo_path) = self.active_tab().repo_path.as_ref() {
968 let full_path = repo_path.join(path);
969 if let Err(e) = gitkraft_core::show_in_folder(&full_path) {
970 self.active_tab_mut().error_message =
971 Some(format!("Failed to show in folder: {e}"));
972 }
973 }
974 Task::none()
975 }
976
977 Message::Noop => Task::none(),
978
979 Message::CherryPickCommits(oids) => {
980 let oids = oids.clone();
981 self.active_tab_mut().context_menu = None;
982 with_repo!(
983 self,
984 loading,
985 format!("Cherry-picking {} commit(s)…", oids.len()),
986 |path| crate::features::repo::commands::cherry_pick_commits_async(path, oids)
987 )
988 }
989
990 Message::RevertCommits(oids) => {
991 let oids = oids.clone();
992 self.active_tab_mut().context_menu = None;
993 with_repo!(
994 self,
995 loading,
996 format!("Reverting {} commit(s)…", oids.len()),
997 |path| crate::features::repo::commands::revert_commits_async(path, oids)
998 )
999 }
1000 }
1001 }
1002}