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::AnimationTick => {
201 self.animation_tick = self.animation_tick.wrapping_add(1);
202 Task::none()
203 }
204
205 Message::ShiftArrowDown => {
206 let file_info: Option<(usize, usize)> = {
208 let tab = self.active_tab();
209 if !tab.commit_files.is_empty() {
210 tab.selected_file_index
211 .map(|cur| (cur, tab.commit_files.len()))
212 } else {
213 None
214 }
215 };
216 if let Some((current, files_len)) = file_info {
217 let new_idx = (current + 1).min(files_len.saturating_sub(1));
218 if new_idx != current {
219 return crate::features::diff::update::update(
220 self,
221 Message::SelectDiffByIndex(new_idx),
222 );
223 }
224 return Task::none();
225 }
226 let commit_info: Option<(usize, usize)> = {
228 let tab = self.active_tab();
229 if !tab.commits.is_empty() {
230 Some((tab.selected_commit.unwrap_or(0), tab.commits.len()))
231 } else {
232 None
233 }
234 };
235 if let Some((current, commits_len)) = commit_info {
236 let new_idx = (current + 1).min(commits_len.saturating_sub(1));
237 if new_idx != current {
238 return crate::features::commits::update::update(
239 self,
240 Message::SelectCommit(new_idx),
241 );
242 }
243 }
244 Task::none()
245 }
246
247 Message::ShiftArrowUp => {
248 let file_info: Option<(usize, usize)> = {
250 let tab = self.active_tab();
251 if !tab.commit_files.is_empty() {
252 tab.selected_file_index
253 .map(|cur| (cur, tab.commit_files.len()))
254 } else {
255 None
256 }
257 };
258 if let Some((current, _files_len)) = file_info {
259 let new_idx = current.saturating_sub(1);
260 if new_idx != current {
261 return crate::features::diff::update::update(
262 self,
263 Message::SelectDiffByIndex(new_idx),
264 );
265 }
266 return Task::none();
267 }
268 let commit_info: Option<(usize, usize)> = {
270 let tab = self.active_tab();
271 if !tab.commits.is_empty() {
272 Some((tab.selected_commit.unwrap_or(0), tab.commits.len()))
273 } else {
274 None
275 }
276 };
277 if let Some((current, _commits_len)) = commit_info {
278 let new_idx = current.saturating_sub(1);
279 if new_idx != current {
280 return crate::features::commits::update::update(
281 self,
282 Message::SelectCommit(new_idx),
283 );
284 }
285 }
286 Task::none()
287 }
288
289 Message::ToggleSidebar => {
290 self.sidebar_expanded = !self.sidebar_expanded;
291 crate::features::repo::commands::save_layout_async(self.current_layout())
292 }
293
294 Message::WindowResized(w, h) => {
295 let w = *w;
296 let h = *h;
297 self.window_width = w;
298 self.window_height = h;
299 crate::features::repo::commands::save_layout_async(self.current_layout())
300 }
301
302 Message::WindowMoved(x, y) => {
303 let x = *x;
304 let y = *y;
305 self.window_x = x;
306 self.window_y = y;
307 crate::features::repo::commands::save_layout_async(self.current_layout())
308 }
309
310 Message::OpenSettingsFile => {
311 let path = match gitkraft_core::features::persistence::ops::settings_json_path() {
313 Ok(p) => p,
314 Err(e) => {
315 let msg = format!("Cannot determine settings path: {e}");
316 self.active_tab_mut().error_message = Some(msg);
317 return Task::none();
318 }
319 };
320
321 if !path.exists() {
323 let snap = gitkraft_core::features::persistence::ops::load_settings()
324 .unwrap_or_default();
325 if let Err(e) = gitkraft_core::features::persistence::ops::save_settings(&snap)
326 {
327 let msg = format!("Could not create settings file: {e}");
328 self.active_tab_mut().error_message = Some(msg);
329 return Task::none();
330 }
331 }
332
333 let path_str = path.display().to_string();
334
335 match self.editor.open_file_or_default(&path) {
340 Ok(method) => {
341 let msg = format!("Settings opened in {method} — {path_str}");
342 self.active_tab_mut().status_message = Some(msg);
343 }
344 Err(e) => {
345 let msg = format!("Could not open settings ({e}) — file is at: {path_str}");
348 self.active_tab_mut().error_message = Some(msg);
349 }
350 }
351 Task::none()
352 }
353
354 Message::PaneDragStart(target, _x) => {
356 self.dragging = Some(*target);
357 self.drag_initialized = false;
361 Task::none()
362 }
363
364 Message::PaneDragStartH(target, _y) => {
365 self.dragging_h = Some(*target);
366 self.drag_initialized_h = false;
367 Task::none()
368 }
369
370 Message::PaneDragMove(x, y) => {
371 use crate::state::{DragTarget, DragTargetH};
372
373 self.cursor_pos = iced::Point::new(*x, *y);
375
376 if let Some(target) = self.dragging {
377 if !self.drag_initialized {
378 self.drag_start_x = *x;
380 self.drag_initialized = true;
381 } else {
382 let dx = *x - self.drag_start_x;
383 self.drag_start_x = *x;
384
385 match target {
386 DragTarget::SidebarRight => {
387 self.sidebar_width = (self.sidebar_width + dx).clamp(120.0, 500.0);
388 }
389 DragTarget::CommitLogRight => {
390 self.commit_log_width =
391 (self.commit_log_width + dx).clamp(200.0, 1200.0);
392 }
393 DragTarget::DiffFileListRight => {
394 self.diff_file_list_width =
395 (self.diff_file_list_width + dx).clamp(100.0, 400.0);
396 }
397 }
398 }
399 }
400
401 if let Some(target_h) = self.dragging_h {
402 if !self.drag_initialized_h {
403 self.drag_start_y = *y;
404 self.drag_initialized_h = true;
405 } else {
406 let dy = *y - self.drag_start_y;
407 self.drag_start_y = *y;
408
409 match target_h {
410 DragTargetH::StagingTop => {
411 self.staging_height =
413 (self.staging_height - dy).clamp(100.0, 600.0);
414 }
415 }
416 }
417 }
418
419 Task::none()
420 }
421
422 Message::PaneDragEnd => {
423 self.dragging = None;
424 self.dragging_h = None;
425 self.drag_initialized = false;
426 self.drag_initialized_h = false;
427 crate::features::repo::commands::save_layout_async(self.current_layout())
428 }
429
430 Message::OpenBranchContextMenu(name, local_index, is_current) => {
432 let pos = (self.cursor_pos.x, self.cursor_pos.y);
433 let tab = self.active_tab_mut();
434 tab.context_menu_pos = pos;
435 tab.context_menu = Some(crate::state::ContextMenu::Branch {
436 name: name.clone(),
437 is_current: *is_current,
438 local_index: *local_index,
439 });
440 Task::none()
441 }
442
443 Message::OpenRemoteBranchContextMenu(name) => {
444 let pos = (self.cursor_pos.x, self.cursor_pos.y);
445 let tab = self.active_tab_mut();
446 tab.context_menu_pos = pos;
447 tab.context_menu =
448 Some(crate::state::ContextMenu::RemoteBranch { name: name.clone() });
449 Task::none()
450 }
451
452 Message::OpenCommitFileContextMenu(oid, file_path) => {
453 let pos = (self.cursor_pos.x, self.cursor_pos.y);
454 let tab = self.active_tab_mut();
455 tab.context_menu_pos = pos;
456 tab.context_menu = Some(crate::state::ContextMenu::CommitFile {
457 oid: oid.clone(),
458 file_path: file_path.clone(),
459 });
460 Task::none()
461 }
462
463 Message::OpenStashContextMenu(index) => {
464 let index = *index;
465 let pos = (self.cursor_pos.x, self.cursor_pos.y);
466 let tab = self.active_tab_mut();
467 tab.context_menu_pos = pos;
468 tab.context_menu = Some(crate::state::ContextMenu::Stash { index });
469 Task::none()
470 }
471
472 Message::OpenUnstagedFileContextMenu(path) => {
473 let pos = (self.cursor_pos.x, self.cursor_pos.y);
474 let tab = self.active_tab_mut();
475 tab.context_menu_pos = pos;
476 tab.context_menu =
477 Some(crate::state::ContextMenu::UnstagedFile { path: path.clone() });
478 Task::none()
479 }
480
481 Message::OpenStagedFileContextMenu(path) => {
482 let pos = (self.cursor_pos.x, self.cursor_pos.y);
483 let tab = self.active_tab_mut();
484 tab.context_menu_pos = pos;
485 tab.context_menu =
486 Some(crate::state::ContextMenu::StagedFile { path: path.clone() });
487 Task::none()
488 }
489
490 Message::OpenCommitContextMenu(idx) => {
491 let oid = self.active_tab().commits.get(*idx).map(|c| c.oid.clone());
492 let pos = (self.cursor_pos.x, self.cursor_pos.y);
493 if let Some(oid) = oid {
494 let tab = self.active_tab_mut();
495 tab.context_menu_pos = pos;
496 tab.context_menu = Some(crate::state::ContextMenu::Commit { index: *idx, oid });
497 }
498 Task::none()
499 }
500
501 Message::OpenSearchResultContextMenu(idx) => {
502 if let Some(commit) = self.search_results.get(*idx) {
503 let oid = commit.oid.clone();
504 let pos = (self.cursor_pos.x, self.cursor_pos.y);
505 let tab = self.active_tab_mut();
506 tab.context_menu_pos = pos;
507 tab.context_menu = Some(crate::state::ContextMenu::Commit { index: *idx, oid });
508 }
509 Task::none()
510 }
511
512 Message::CloseContextMenu => {
513 self.active_tab_mut().context_menu = None;
514 Task::none()
515 }
516
517 Message::BeginRenameBranch(name) => {
519 let tab = self.active_tab_mut();
520 tab.context_menu = None;
521 tab.rename_branch_input = name.clone();
522 tab.rename_branch_target = Some(name.clone());
523 Task::none()
524 }
525
526 Message::RenameBranchInputChanged(s) => {
527 self.active_tab_mut().rename_branch_input = s.clone();
528 Task::none()
529 }
530
531 Message::CancelRename => {
532 let tab = self.active_tab_mut();
533 tab.rename_branch_target = None;
534 tab.rename_branch_input.clear();
535 Task::none()
536 }
537
538 Message::ConfirmRenameBranch => {
539 let (original, new_name, path) = {
540 let tab = self.active_tab();
541 (
542 tab.rename_branch_target.clone(),
543 tab.rename_branch_input.trim().to_string(),
544 tab.repo_path.clone(),
545 )
546 };
547 if let (Some(orig), false) = (&original, new_name.is_empty()) {
548 if *orig != new_name {
549 if let Some(path) = path {
550 let orig = orig.clone();
551 {
552 let tab = self.active_tab_mut();
553 tab.rename_branch_target = None;
554 tab.rename_branch_input.clear();
555 tab.is_loading = true;
556 tab.status_message =
557 Some(format!("Renaming '{orig}' → '{new_name}'…"));
558 }
559 return crate::features::repo::commands::rename_branch_async(
560 path, orig, new_name,
561 );
562 }
563 }
564 }
565 self.active_tab_mut().rename_branch_target = None;
566 Task::none()
567 }
568
569 Message::PushBranch(name) => {
571 let name = name.clone();
572 let remote = self
573 .active_tab()
574 .remotes
575 .first()
576 .map(|r| r.name.clone())
577 .unwrap_or_else(|| "origin".to_string());
578 self.active_tab_mut().context_menu = None;
579 with_repo!(
580 self,
581 loading,
582 format!("Pushing '{name}' to {remote}…"),
583 |path| crate::features::repo::commands::push_branch_async(path, name, remote)
584 )
585 }
586
587 Message::PullBranch(_name) => {
588 let remote = self
589 .active_tab()
590 .remotes
591 .first()
592 .map(|r| r.name.clone())
593 .unwrap_or_else(|| "origin".to_string());
594 self.active_tab_mut().context_menu = None;
595 with_repo!(
596 self,
597 loading,
598 format!("Pulling from {remote} (rebase)…"),
599 |path| crate::features::repo::commands::pull_rebase_async(path, remote)
600 )
601 }
602
603 Message::RebaseOnto(target) => {
604 let target = target.clone();
605 self.active_tab_mut().context_menu = None;
606 with_repo!(
607 self,
608 loading,
609 format!("Rebasing onto '{target}'…"),
610 |path| crate::features::repo::commands::rebase_onto_async(path, target)
611 )
612 }
613
614 Message::MergeBranch(name) => {
615 let name = name.clone();
616 self.active_tab_mut().context_menu = None;
617 with_repo!(
618 self,
619 loading,
620 format!("Merging '{name}' into current branch…"),
621 |path| crate::features::repo::commands::merge_branch_async(path, name)
622 )
623 }
624
625 Message::CheckoutRemoteBranch(name) => {
626 let name = name.clone();
627 self.active_tab_mut().context_menu = None;
628 with_repo!(self, loading, format!("Checking out '{name}'…"), |path| {
629 crate::features::repo::commands::checkout_remote_branch_async(path, name)
630 })
631 }
632
633 Message::DeleteRemoteBranch(name) => {
634 let name = name.clone();
635 self.active_tab_mut().context_menu = None;
636 with_repo!(
637 self,
638 loading,
639 format!("Deleting remote branch '{name}'…"),
640 |path| crate::features::repo::commands::delete_remote_branch_async(path, name)
641 )
642 }
643
644 Message::BeginCreateTag(oid, annotated) => {
645 let tab = self.active_tab_mut();
646 tab.context_menu = None;
647 tab.create_tag_target_oid = Some(oid.clone());
648 tab.create_tag_annotated = *annotated;
649 tab.create_tag_name.clear();
650 tab.create_tag_message.clear();
651 Task::none()
652 }
653
654 Message::TagNameChanged(s) => {
655 self.active_tab_mut().create_tag_name = s.clone();
656 Task::none()
657 }
658
659 Message::TagMessageChanged(s) => {
660 self.active_tab_mut().create_tag_message = s.clone();
661 Task::none()
662 }
663
664 Message::ConfirmCreateTag => {
665 let (oid, name, message, annotated, path) = {
666 let tab = self.active_tab();
667 (
668 tab.create_tag_target_oid.clone(),
669 tab.create_tag_name.trim().to_string(),
670 tab.create_tag_message.trim().to_string(),
671 tab.create_tag_annotated,
672 tab.repo_path.clone(),
673 )
674 };
675 if let (Some(oid), false) = (&oid, name.is_empty()) {
676 if let Some(path) = path {
677 let oid = oid.clone();
678 {
679 let tab = self.active_tab_mut();
680 tab.create_tag_target_oid = None;
681 tab.create_tag_name.clear();
682 tab.create_tag_message.clear();
683 tab.is_loading = true;
684 tab.status_message = Some(format!("Creating tag '{name}'…"));
685 }
686 return if annotated {
687 crate::features::repo::commands::create_annotated_tag_async(
688 path, name, message, oid,
689 )
690 } else {
691 crate::features::repo::commands::create_tag_async(path, name, oid)
692 };
693 }
694 }
695 Task::none()
696 }
697
698 Message::CancelCreateTag => {
699 let tab = self.active_tab_mut();
700 tab.create_tag_target_oid = None;
701 tab.create_tag_name.clear();
702 tab.create_tag_message.clear();
703 Task::none()
704 }
705
706 Message::CherryPickCommit(oid) => {
707 let oid = oid.clone();
708 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
709 self.active_tab_mut().context_menu = None;
710 with_repo!(self, loading, format!("Cherry-picking {short}…"), |path| {
711 crate::features::repo::commands::cherry_pick_async(path, oid)
712 })
713 }
714
715 Message::BeginCreateBranchAtCommit(oid) => {
716 let tab = self.active_tab_mut();
717 tab.context_menu = None;
718 tab.create_branch_at_oid = Some(oid.clone());
719 tab.new_branch_name.clear();
720 Task::none()
721 }
722
723 Message::ConfirmCreateBranchAtCommit => {
724 let (oid, name, path) = {
725 let tab = self.active_tab();
726 (
727 tab.create_branch_at_oid.clone(),
728 tab.new_branch_name.trim().to_string(),
729 tab.repo_path.clone(),
730 )
731 };
732 if let (Some(oid), false) = (&oid, name.is_empty()) {
733 if let Some(path) = path {
734 let oid = oid.clone();
735 {
736 let tab = self.active_tab_mut();
737 tab.create_branch_at_oid = None;
738 tab.new_branch_name.clear();
739 tab.is_loading = true;
740 tab.status_message = Some(format!("Creating branch '{name}'…"));
741 }
742 return crate::features::repo::commands::create_branch_at_commit_async(
743 path, name, oid,
744 );
745 }
746 }
747 Task::none()
748 }
749
750 Message::CancelCreateBranchAtCommit => {
751 let tab = self.active_tab_mut();
752 tab.create_branch_at_oid = None;
753 tab.new_branch_name.clear();
754 Task::none()
755 }
756
757 Message::ExecuteCommitAction(oid, action) => {
759 let oid = oid.clone();
760 let action = action.clone();
761 let label = action.label();
762 self.active_tab_mut().context_menu = None;
763 with_repo!(self, loading, format!("{label}…"), |path| {
764 crate::features::repo::commands::execute_commit_action_async(path, oid, action)
765 })
766 }
767
768 Message::CheckoutCommitDetached(oid) => {
769 let oid = oid.clone();
770 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
771 self.active_tab_mut().context_menu = None;
772 with_repo!(self, loading, format!("Checking out {short}…"), |path| {
773 crate::features::repo::commands::checkout_commit_async(path, oid)
774 })
775 }
776
777 Message::RebaseOntoCommit(oid) => {
778 let oid = oid.clone();
779 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
780 self.active_tab_mut().context_menu = None;
781 with_repo!(self, loading, format!("Rebasing onto {short}…"), |path| {
782 crate::features::repo::commands::rebase_onto_async(path, oid)
783 })
784 }
785
786 Message::RevertCommit(oid) => {
787 let oid = oid.clone();
788 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
789 self.active_tab_mut().context_menu = None;
790 with_repo!(self, loading, format!("Reverting {short}…"), |path| {
791 crate::features::repo::commands::revert_commit_async(path, oid)
792 })
793 }
794
795 Message::ResetSoft(ref oid)
796 | Message::ResetMixed(ref oid)
797 | Message::ResetHard(ref oid) => {
798 let mode = match &message {
799 Message::ResetSoft(_) => "soft",
800 Message::ResetMixed(_) => "mixed",
801 Message::ResetHard(_) => "hard",
802 _ => unreachable!(),
803 };
804 let oid = oid.clone();
805 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
806 self.active_tab_mut().context_menu = None;
807 with_repo!(
808 self,
809 loading,
810 format!("Resetting ({mode}) to {short}…"),
811 |path| crate::features::repo::commands::reset_to_commit_async(
812 path,
813 oid,
814 mode.to_string()
815 )
816 )
817 }
818
819 Message::CopyText(text) => {
821 self.active_tab_mut().context_menu = None;
822 iced::clipboard::write(text.clone())
823 }
824
825 Message::ThemeChanged(index) => {
827 self.current_theme_index = *index;
828 let name = gitkraft_core::THEME_NAMES
830 .get(*index)
831 .copied()
832 .unwrap_or("Default");
833 crate::features::repo::commands::save_theme_async(name.to_string())
834 }
835
836 Message::ThemeSaved(_result) => {
837 Task::none()
839 }
840
841 Message::EditorChanged(editor) => {
842 self.editor = editor.clone();
843 self.active_tab_mut().status_message =
844 Some(format!("Editor set to {}", self.editor));
845 let name = self.editor.display_name().to_string();
847 crate::features::repo::commands::save_editor_async(name)
848 }
849
850 Message::EditorSaved(_result) => {
851 Task::none()
853 }
854
855 Message::LayoutSaved(_result) => {
856 Task::none()
858 }
859
860 Message::SessionSaved(_) => {
861 Task::none()
863 }
864
865 Message::LayoutLoaded(result) => {
866 if let Ok(Some(layout)) = result {
867 if let Some(w) = layout.sidebar_width {
868 self.sidebar_width = w;
869 }
870 if let Some(w) = layout.commit_log_width {
871 self.commit_log_width = w;
872 }
873 if let Some(h) = layout.staging_height {
874 self.staging_height = h;
875 }
876 if let Some(w) = layout.diff_file_list_width {
877 self.diff_file_list_width = w;
878 }
879 if let Some(expanded) = layout.sidebar_expanded {
880 self.sidebar_expanded = expanded;
881 }
882 if let Some(scale) = layout.ui_scale {
883 self.ui_scale = scale.clamp(0.5, 2.0);
884 }
885 }
886 Task::none()
887 }
888
889 Message::ToggleSearch => {
891 self.search_visible = !self.search_visible;
892 if !self.search_visible {
893 self.search_query.clear();
894 self.search_results.clear();
895 self.search_selected = None;
896 self.search_diff_files.clear();
897 self.search_diff_selected.clear();
898 self.search_diff_content.clear();
899 self.search_diff_oid = None;
900 Task::none()
901 } else {
902 iced::widget::operation::focus_next()
903 }
904 }
905
906 Message::SearchQueryChanged(query) => {
907 let query = query.clone();
908 self.search_query = query.clone();
909 if query.trim().len() >= 2 {
910 if let Some(path) = self.active_tab().repo_path.clone() {
911 return crate::features::commits::commands::search_commits(path, query);
912 }
913 } else {
914 self.search_results.clear();
915 self.search_selected = None;
916 }
917 Task::none()
918 }
919
920 Message::SearchResultsLoaded(result) => {
921 match result {
922 Ok(results) => {
923 self.search_results = results.clone();
924 self.search_selected = if self.search_results.is_empty() {
925 None
926 } else {
927 Some(0)
928 };
929 }
930 Err(e) => {
931 self.search_results.clear();
932 self.active_tab_mut().error_message = Some(format!("Search failed: {e}"));
933 }
934 }
935 Task::none()
936 }
937
938 Message::SelectSearchResult(index) => {
939 let index = *index;
940 if index < self.search_results.len() {
941 self.search_selected = Some(index);
942 }
943 Task::none()
944 }
945
946 Message::ConfirmSearchResult => {
947 if let Some(idx) = self.search_selected {
948 if let Some(commit) = self.search_results.get(idx).cloned() {
949 let oid = commit.oid.clone();
950 self.search_diff_oid = Some(oid.clone());
952 self.search_diff_files.clear();
953 self.search_diff_selected.clear();
954 self.search_diff_content.clear();
955
956 if let Some(path) = self.active_tab().repo_path.clone() {
957 return crate::features::commits::commands::search_diff_file_list(
958 path, oid,
959 );
960 }
961 }
962 }
963 Task::none()
964 }
965
966 Message::SearchDiffFilesLoaded(result) => {
967 match result {
968 Ok(files) => {
969 self.search_diff_files = files.clone();
970 self.search_diff_selected.clear();
971 self.search_diff_content.clear();
972 }
973 Err(e) => {
974 self.active_tab_mut().error_message =
975 Some(format!("Failed to load diff files: {e}"));
976 }
977 }
978 Task::none()
979 }
980
981 Message::ToggleSearchDiffFile(index) => {
982 let index = *index;
983 if self.search_diff_selected.contains(&index) {
984 self.search_diff_selected.remove(&index);
985 } else {
986 self.search_diff_selected.insert(index);
987 }
988 Task::none()
989 }
990
991 Message::ToggleSearchDiffSelectAll => {
992 if self.search_diff_selected.len() == self.search_diff_files.len() {
993 self.search_diff_selected.clear();
994 } else {
995 self.search_diff_selected = (0..self.search_diff_files.len()).collect();
996 }
997 Task::none()
998 }
999
1000 Message::ViewSearchDiffFile(index) => {
1001 let index = *index;
1002 if let Some(file) = self.search_diff_files.get(index) {
1003 let file_path = file.display_path().to_string();
1004 if let (Some(oid), Some(repo_path)) = (
1005 self.search_diff_oid.clone(),
1006 self.active_tab().repo_path.clone(),
1007 ) {
1008 return crate::features::commits::commands::search_diff_file(
1009 repo_path, oid, file_path,
1010 );
1011 }
1012 }
1013 Task::none()
1014 }
1015
1016 Message::SearchFileDiffLoaded(result) => {
1017 match result {
1018 Ok(diff) => {
1019 self.search_diff_content = vec![diff.clone()];
1020 }
1021 Err(e) => {
1022 self.active_tab_mut().error_message =
1023 Some(format!("Failed to load file diff: {e}"));
1024 }
1025 }
1026 Task::none()
1027 }
1028
1029 Message::DiffSelectedFiles => {
1030 if self.search_diff_selected.is_empty() {
1031 return Task::none();
1032 }
1033 let file_paths: Vec<String> = self
1034 .search_diff_selected
1035 .iter()
1036 .filter_map(|&i| self.search_diff_files.get(i))
1037 .map(|f| f.display_path().to_string())
1038 .collect();
1039 if let (Some(oid), Some(repo_path)) = (
1040 self.search_diff_oid.clone(),
1041 self.active_tab().repo_path.clone(),
1042 ) {
1043 return crate::features::commits::commands::search_diff_multi_files(
1044 repo_path, oid, file_paths,
1045 );
1046 }
1047 Task::none()
1048 }
1049
1050 Message::SearchMultiDiffLoaded(result) => {
1051 match result {
1052 Ok(diffs) => {
1053 self.search_diff_content = diffs.clone();
1054 }
1055 Err(e) => {
1056 self.active_tab_mut().error_message =
1057 Some(format!("Failed to load diffs: {e}"));
1058 }
1059 }
1060 Task::none()
1061 }
1062
1063 Message::SearchDiffBack => {
1064 self.search_diff_content.clear();
1065 Task::none()
1066 }
1067
1068 Message::FileSystemChanged => {
1069 if self.has_repo() && !self.active_tab().is_loading {
1070 return self.refresh_active_tab();
1071 }
1072 Task::none()
1073 }
1074
1075 Message::OpenInEditor(path) => {
1076 self.active_tab_mut().context_menu = None;
1077 if matches!(self.editor, gitkraft_core::Editor::None) {
1078 self.active_tab_mut().status_message = Some(
1079 "No editor configured — select one from the editor dropdown in the toolbar"
1080 .into(),
1081 );
1082 return Task::none();
1083 }
1084 if let Some(repo_path) = self.active_tab().repo_path.as_ref() {
1085 let full_path = repo_path.join(path);
1086 match self.editor.open_file(&full_path) {
1087 Ok(()) => {
1088 self.active_tab_mut().status_message =
1089 Some(format!("Opened in {}", self.editor));
1090 }
1091 Err(e) => {
1092 self.active_tab_mut().error_message =
1093 Some(format!("Failed to open editor: {e}"));
1094 }
1095 }
1096 }
1097 Task::none()
1098 }
1099
1100 Message::OpenInDefaultProgram(path) => {
1101 self.active_tab_mut().context_menu = None;
1102 if let Some(repo_path) = self.active_tab().repo_path.as_ref() {
1103 let full_path = repo_path.join(path);
1104 if let Err(e) = gitkraft_core::open_file_default(&full_path) {
1105 self.active_tab_mut().error_message =
1106 Some(format!("Failed to open file: {e}"));
1107 }
1108 }
1109 Task::none()
1110 }
1111
1112 Message::ShowInFolder(path) => {
1113 self.active_tab_mut().context_menu = None;
1114 if let Some(repo_path) = self.active_tab().repo_path.as_ref() {
1115 let full_path = repo_path.join(path);
1116 if let Err(e) = gitkraft_core::show_in_folder(&full_path) {
1117 self.active_tab_mut().error_message =
1118 Some(format!("Failed to show in folder: {e}"));
1119 }
1120 }
1121 Task::none()
1122 }
1123
1124 Message::ViewFileHistory(path) => {
1126 let path = path.clone();
1127 if let Some(repo_path) = self.active_tab().repo_path.clone() {
1128 let tab = self.active_tab_mut();
1129 tab.blame_path = None; tab.file_history_path = Some(path.clone());
1131 tab.file_history_commits.clear();
1132 tab.file_history_scroll = 0.0;
1133 tab.context_menu = None;
1134 tab.status_message = Some(format!(
1135 "Loading history for {}…",
1136 path.rsplit('/').next().unwrap_or(&path)
1137 ));
1138 crate::features::repo::commands::file_history_async(repo_path, path)
1139 } else {
1140 Task::none()
1141 }
1142 }
1143
1144 Message::FileHistoryLoaded(result) => {
1145 match result {
1146 Ok((path, commits)) => {
1147 let tab = self.active_tab_mut();
1148 tab.file_history_path = Some(path.clone());
1149 tab.file_history_commits = commits.clone();
1150 tab.status_message = Some(format!(
1151 "{} commits touch {}",
1152 commits.len(),
1153 path.rsplit('/').next().unwrap_or(path)
1154 ));
1155 }
1156 Err(e) => {
1157 let tab = self.active_tab_mut();
1158 tab.file_history_path = None;
1159 tab.error_message = Some(format!("File history failed: {e}"));
1160 }
1161 }
1162 Task::none()
1163 }
1164
1165 Message::CloseFileHistory => {
1166 let tab = self.active_tab_mut();
1167 tab.file_history_path = None;
1168 tab.file_history_commits.clear();
1169 tab.file_history_scroll = 0.0;
1170 Task::none()
1171 }
1172
1173 Message::FileHistoryScrolled(y) => {
1174 self.active_tab_mut().file_history_scroll = *y;
1175 Task::none()
1176 }
1177
1178 Message::SelectFileHistoryCommit(oid) => {
1179 let oid = oid.clone();
1180 let repo_path = self.active_tab().repo_path.clone();
1181 {
1182 let tab = self.active_tab_mut();
1183 tab.file_history_path = None;
1184 tab.file_history_commits.clear();
1185 tab.selected_commit_oid = Some(oid.clone());
1186 tab.commit_files.clear();
1187 tab.selected_diff = None;
1188 tab.show_commit_detail = true;
1189 }
1190 if let Some(path) = repo_path {
1191 crate::features::commits::commands::load_commit_file_list(path, oid)
1192 } else {
1193 Task::none()
1194 }
1195 }
1196
1197 Message::ViewFileBlame(path) => {
1199 let path = path.clone();
1200 if let Some(repo_path) = self.active_tab().repo_path.clone() {
1201 let tab = self.active_tab_mut();
1202 tab.file_history_path = None; tab.blame_path = Some(path.clone());
1204 tab.blame_lines.clear();
1205 tab.blame_scroll = 0.0;
1206 tab.context_menu = None;
1207 tab.status_message = Some(format!(
1208 "Loading blame for {}…",
1209 path.rsplit('/').next().unwrap_or(&path)
1210 ));
1211 crate::features::repo::commands::blame_file_async(repo_path, path)
1212 } else {
1213 Task::none()
1214 }
1215 }
1216
1217 Message::FileBlameLoaded(result) => {
1218 match result {
1219 Ok((path, lines)) => {
1220 let tab = self.active_tab_mut();
1221 tab.blame_path = Some(path.clone());
1222 tab.blame_lines = lines.clone();
1223 tab.status_message = Some(format!(
1224 "Blame: {} ({} lines)",
1225 path.rsplit('/').next().unwrap_or(path),
1226 lines.len()
1227 ));
1228 }
1229 Err(e) => {
1230 let tab = self.active_tab_mut();
1231 tab.blame_path = None;
1232 tab.error_message = Some(format!("Blame failed: {e}"));
1233 }
1234 }
1235 Task::none()
1236 }
1237
1238 Message::CloseFileBlame => {
1239 let tab = self.active_tab_mut();
1240 tab.blame_path = None;
1241 tab.blame_lines.clear();
1242 tab.blame_scroll = 0.0;
1243 Task::none()
1244 }
1245
1246 Message::BlameScrolled(y) => {
1247 self.active_tab_mut().blame_scroll = *y;
1248 Task::none()
1249 }
1250
1251 Message::DeleteFile(path) => {
1253 let tab = self.active_tab_mut();
1254 tab.context_menu = None;
1255 tab.pending_delete_file = Some(path.clone());
1256 tab.status_message = Some(format!(
1257 "Delete '{}' — press Confirm to delete permanently",
1258 path.rsplit('/').next().unwrap_or(path)
1259 ));
1260 Task::none()
1261 }
1262
1263 Message::ConfirmDeleteFile => {
1264 let path = self.active_tab().pending_delete_file.clone();
1265 let repo_path = self.active_tab().repo_path.clone();
1266 if let (Some(file_path), Some(repo_path)) = (path, repo_path) {
1267 let tab = self.active_tab_mut();
1268 tab.pending_delete_file = None;
1269 tab.is_loading = true;
1270 tab.status_message = Some(format!(
1271 "Deleting '{}'…",
1272 file_path.rsplit('/').next().unwrap_or(&file_path)
1273 ));
1274 crate::features::repo::commands::delete_file_async(repo_path, file_path)
1275 } else {
1276 Task::none()
1277 }
1278 }
1279
1280 Message::CancelDeleteFile => {
1281 let tab = self.active_tab_mut();
1282 tab.pending_delete_file = None;
1283 tab.status_message = None;
1284 Task::none()
1285 }
1286
1287 Message::Noop => Task::none(),
1288
1289 Message::CherryPickCommits(oids) => {
1290 let oids = oids.clone();
1291 self.active_tab_mut().context_menu = None;
1292 with_repo!(
1293 self,
1294 loading,
1295 format!("Cherry-picking {} commit(s)…", oids.len()),
1296 |path| crate::features::repo::commands::cherry_pick_commits_async(path, oids)
1297 )
1298 }
1299
1300 Message::RevertCommits(oids) => {
1301 let oids = oids.clone();
1302 self.active_tab_mut().context_menu = None;
1303 with_repo!(
1304 self,
1305 loading,
1306 format!("Reverting {} commit(s)…", oids.len()),
1307 |path| crate::features::repo::commands::revert_commits_async(path, oids)
1308 )
1309 }
1310 }
1311 }
1312}