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