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 crate::features::commits::update::update(self, message)
91 }
92
93 Message::CommitMessageChanged(_)
94 | Message::CreateCommit
95 | Message::CommitCreated(_) => crate::features::commits::update::update(self, message),
96
97 Message::CommitLogScrolled(abs_y, rel_y) => {
98 const COMMITS_PAGE_SIZE: usize = 200;
102 const LOAD_TRIGGER_RELATIVE: f32 = 0.85;
105
106 self.active_tab_mut().commit_scroll_offset = *abs_y;
107
108 let tab = self.active_tab();
109 if *rel_y >= LOAD_TRIGGER_RELATIVE
110 && tab.has_more_commits
111 && !tab.is_loading_more_commits
112 {
113 if let Some(path) = tab.repo_path.clone() {
114 let current = tab.commits.len();
115 self.active_tab_mut().is_loading_more_commits = true;
116 return crate::features::repo::commands::load_more_commits(
117 path,
118 current,
119 COMMITS_PAGE_SIZE,
120 );
121 }
122 }
123 Task::none()
124 }
125
126 Message::DiffViewScrolled(abs_y) => {
127 self.active_tab_mut().diff_scroll_offset = *abs_y;
128 Task::none()
129 }
130
131 Message::StageFile(_)
133 | Message::UnstageFile(_)
134 | Message::StageAll
135 | Message::UnstageAll
136 | Message::DiscardFile(_)
137 | Message::ConfirmDiscard(_)
138 | Message::CancelDiscard
139 | Message::StagingUpdated(_)
140 | Message::ToggleSelectUnstaged(_)
141 | Message::ToggleSelectStaged(_)
142 | Message::StageSelected
143 | Message::UnstageSelected
144 | Message::DiscardSelected
145 | Message::DiscardStagedFile(_) => {
146 crate::features::staging::update::update(self, message)
147 }
148
149 Message::StashSave
151 | Message::StashPop(_)
152 | Message::StashDrop(_)
153 | Message::StashUpdated(_)
154 | Message::StashMessageChanged(_)
155 | Message::StashApply(_)
156 | Message::ViewStashDiff(_)
157 | Message::StashDiffLoaded(_) => crate::features::stash::update::update(self, message),
158
159 Message::Fetch | Message::FetchCompleted(_) => {
161 crate::features::remotes::update::update(self, message)
162 }
163
164 Message::SelectDiff(_) | Message::SelectDiffByIndex(_) => {
166 crate::features::diff::update::update(self, message)
167 }
168
169 Message::DismissError => {
170 self.active_tab_mut().error_message = None;
171 Task::none()
172 }
173
174 Message::ZoomIn => {
175 self.ui_scale = (self.ui_scale + 0.1).min(2.0);
176 crate::features::repo::commands::save_layout_async(self.current_layout())
177 }
178
179 Message::ZoomOut => {
180 self.ui_scale = (self.ui_scale - 0.1).max(0.5);
181 crate::features::repo::commands::save_layout_async(self.current_layout())
182 }
183
184 Message::ZoomReset => {
185 self.ui_scale = 1.0;
186 crate::features::repo::commands::save_layout_async(self.current_layout())
187 }
188
189 Message::ToggleSidebar => {
190 self.sidebar_expanded = !self.sidebar_expanded;
191 crate::features::repo::commands::save_layout_async(self.current_layout())
192 }
193
194 Message::PaneDragStart(target, _x) => {
196 self.dragging = Some(*target);
197 self.drag_initialized = false;
201 Task::none()
202 }
203
204 Message::PaneDragStartH(target, _y) => {
205 self.dragging_h = Some(*target);
206 self.drag_initialized_h = false;
207 Task::none()
208 }
209
210 Message::PaneDragMove(x, y) => {
211 use crate::state::{DragTarget, DragTargetH};
212
213 self.cursor_pos = iced::Point::new(*x, *y);
215
216 if let Some(target) = self.dragging {
217 if !self.drag_initialized {
218 self.drag_start_x = *x;
220 self.drag_initialized = true;
221 } else {
222 let dx = *x - self.drag_start_x;
223 self.drag_start_x = *x;
224
225 match target {
226 DragTarget::SidebarRight => {
227 self.sidebar_width = (self.sidebar_width + dx).clamp(120.0, 500.0);
228 }
229 DragTarget::CommitLogRight => {
230 self.commit_log_width =
231 (self.commit_log_width + dx).clamp(200.0, 1200.0);
232 }
233 DragTarget::DiffFileListRight => {
234 self.diff_file_list_width =
235 (self.diff_file_list_width + dx).clamp(100.0, 400.0);
236 }
237 }
238 }
239 }
240
241 if let Some(target_h) = self.dragging_h {
242 if !self.drag_initialized_h {
243 self.drag_start_y = *y;
244 self.drag_initialized_h = true;
245 } else {
246 let dy = *y - self.drag_start_y;
247 self.drag_start_y = *y;
248
249 match target_h {
250 DragTargetH::StagingTop => {
251 self.staging_height =
253 (self.staging_height - dy).clamp(100.0, 600.0);
254 }
255 }
256 }
257 }
258
259 Task::none()
260 }
261
262 Message::PaneDragEnd => {
263 self.dragging = None;
264 self.dragging_h = None;
265 self.drag_initialized = false;
266 self.drag_initialized_h = false;
267 crate::features::repo::commands::save_layout_async(self.current_layout())
268 }
269
270 Message::OpenBranchContextMenu(name, local_index, is_current) => {
272 let pos = (self.cursor_pos.x, self.cursor_pos.y);
273 let tab = self.active_tab_mut();
274 tab.context_menu_pos = pos;
275 tab.context_menu = Some(crate::state::ContextMenu::Branch {
276 name: name.clone(),
277 is_current: *is_current,
278 local_index: *local_index,
279 });
280 Task::none()
281 }
282
283 Message::OpenRemoteBranchContextMenu(name) => {
284 let pos = (self.cursor_pos.x, self.cursor_pos.y);
285 let tab = self.active_tab_mut();
286 tab.context_menu_pos = pos;
287 tab.context_menu =
288 Some(crate::state::ContextMenu::RemoteBranch { name: name.clone() });
289 Task::none()
290 }
291
292 Message::OpenCommitFileContextMenu(oid, file_path) => {
293 let pos = (self.cursor_pos.x, self.cursor_pos.y);
294 let tab = self.active_tab_mut();
295 tab.context_menu_pos = pos;
296 tab.context_menu = Some(crate::state::ContextMenu::CommitFile {
297 oid: oid.clone(),
298 file_path: file_path.clone(),
299 });
300 Task::none()
301 }
302
303 Message::OpenStashContextMenu(index) => {
304 let index = *index;
305 let pos = (self.cursor_pos.x, self.cursor_pos.y);
306 let tab = self.active_tab_mut();
307 tab.context_menu_pos = pos;
308 tab.context_menu = Some(crate::state::ContextMenu::Stash { index });
309 Task::none()
310 }
311
312 Message::OpenUnstagedFileContextMenu(path) => {
313 let pos = (self.cursor_pos.x, self.cursor_pos.y);
314 let tab = self.active_tab_mut();
315 tab.context_menu_pos = pos;
316 tab.context_menu =
317 Some(crate::state::ContextMenu::UnstagedFile { path: path.clone() });
318 Task::none()
319 }
320
321 Message::OpenStagedFileContextMenu(path) => {
322 let pos = (self.cursor_pos.x, self.cursor_pos.y);
323 let tab = self.active_tab_mut();
324 tab.context_menu_pos = pos;
325 tab.context_menu =
326 Some(crate::state::ContextMenu::StagedFile { path: path.clone() });
327 Task::none()
328 }
329
330 Message::OpenCommitContextMenu(idx) => {
331 let oid = self.active_tab().commits.get(*idx).map(|c| c.oid.clone());
332 let pos = (self.cursor_pos.x, self.cursor_pos.y);
333 if let Some(oid) = oid {
334 let tab = self.active_tab_mut();
335 tab.context_menu_pos = pos;
336 tab.context_menu = Some(crate::state::ContextMenu::Commit { index: *idx, oid });
337 }
338 Task::none()
339 }
340
341 Message::OpenSearchResultContextMenu(idx) => {
342 if let Some(commit) = self.search_results.get(*idx) {
343 let oid = commit.oid.clone();
344 let pos = (self.cursor_pos.x, self.cursor_pos.y);
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::CloseContextMenu => {
353 self.active_tab_mut().context_menu = None;
354 Task::none()
355 }
356
357 Message::BeginRenameBranch(name) => {
359 let tab = self.active_tab_mut();
360 tab.context_menu = None;
361 tab.rename_branch_input = name.clone();
362 tab.rename_branch_target = Some(name.clone());
363 Task::none()
364 }
365
366 Message::RenameBranchInputChanged(s) => {
367 self.active_tab_mut().rename_branch_input = s.clone();
368 Task::none()
369 }
370
371 Message::CancelRename => {
372 let tab = self.active_tab_mut();
373 tab.rename_branch_target = None;
374 tab.rename_branch_input.clear();
375 Task::none()
376 }
377
378 Message::ConfirmRenameBranch => {
379 let (original, new_name, path) = {
380 let tab = self.active_tab();
381 (
382 tab.rename_branch_target.clone(),
383 tab.rename_branch_input.trim().to_string(),
384 tab.repo_path.clone(),
385 )
386 };
387 if let (Some(orig), false) = (&original, new_name.is_empty()) {
388 if *orig != new_name {
389 if let Some(path) = path {
390 let orig = orig.clone();
391 {
392 let tab = self.active_tab_mut();
393 tab.rename_branch_target = None;
394 tab.rename_branch_input.clear();
395 tab.is_loading = true;
396 tab.status_message =
397 Some(format!("Renaming '{orig}' → '{new_name}'…"));
398 }
399 return crate::features::repo::commands::rename_branch_async(
400 path, orig, new_name,
401 );
402 }
403 }
404 }
405 self.active_tab_mut().rename_branch_target = None;
406 Task::none()
407 }
408
409 Message::PushBranch(name) => {
411 let name = name.clone();
412 let remote = self
413 .active_tab()
414 .remotes
415 .first()
416 .map(|r| r.name.clone())
417 .unwrap_or_else(|| "origin".to_string());
418 self.active_tab_mut().context_menu = None;
419 with_repo!(
420 self,
421 loading,
422 format!("Pushing '{name}' to {remote}…"),
423 |path| crate::features::repo::commands::push_branch_async(path, name, remote)
424 )
425 }
426
427 Message::PullBranch(_name) => {
428 let remote = self
429 .active_tab()
430 .remotes
431 .first()
432 .map(|r| r.name.clone())
433 .unwrap_or_else(|| "origin".to_string());
434 self.active_tab_mut().context_menu = None;
435 with_repo!(
436 self,
437 loading,
438 format!("Pulling from {remote} (rebase)…"),
439 |path| crate::features::repo::commands::pull_rebase_async(path, remote)
440 )
441 }
442
443 Message::RebaseOnto(target) => {
444 let target = target.clone();
445 self.active_tab_mut().context_menu = None;
446 with_repo!(
447 self,
448 loading,
449 format!("Rebasing onto '{target}'…"),
450 |path| crate::features::repo::commands::rebase_onto_async(path, target)
451 )
452 }
453
454 Message::MergeBranch(name) => {
455 let name = name.clone();
456 self.active_tab_mut().context_menu = None;
457 with_repo!(
458 self,
459 loading,
460 format!("Merging '{name}' into current branch…"),
461 |path| crate::features::repo::commands::merge_branch_async(path, name)
462 )
463 }
464
465 Message::CheckoutRemoteBranch(name) => {
466 let name = name.clone();
467 self.active_tab_mut().context_menu = None;
468 with_repo!(self, loading, format!("Checking out '{name}'…"), |path| {
469 crate::features::repo::commands::checkout_remote_branch_async(path, name)
470 })
471 }
472
473 Message::DeleteRemoteBranch(name) => {
474 let name = name.clone();
475 self.active_tab_mut().context_menu = None;
476 with_repo!(
477 self,
478 loading,
479 format!("Deleting remote branch '{name}'…"),
480 |path| crate::features::repo::commands::delete_remote_branch_async(path, name)
481 )
482 }
483
484 Message::BeginCreateTag(oid, annotated) => {
485 let tab = self.active_tab_mut();
486 tab.context_menu = None;
487 tab.create_tag_target_oid = Some(oid.clone());
488 tab.create_tag_annotated = *annotated;
489 tab.create_tag_name.clear();
490 tab.create_tag_message.clear();
491 Task::none()
492 }
493
494 Message::TagNameChanged(s) => {
495 self.active_tab_mut().create_tag_name = s.clone();
496 Task::none()
497 }
498
499 Message::TagMessageChanged(s) => {
500 self.active_tab_mut().create_tag_message = s.clone();
501 Task::none()
502 }
503
504 Message::ConfirmCreateTag => {
505 let (oid, name, message, annotated, path) = {
506 let tab = self.active_tab();
507 (
508 tab.create_tag_target_oid.clone(),
509 tab.create_tag_name.trim().to_string(),
510 tab.create_tag_message.trim().to_string(),
511 tab.create_tag_annotated,
512 tab.repo_path.clone(),
513 )
514 };
515 if let (Some(oid), false) = (&oid, name.is_empty()) {
516 if let Some(path) = path {
517 let oid = oid.clone();
518 {
519 let tab = self.active_tab_mut();
520 tab.create_tag_target_oid = None;
521 tab.create_tag_name.clear();
522 tab.create_tag_message.clear();
523 tab.is_loading = true;
524 tab.status_message = Some(format!("Creating tag '{name}'…"));
525 }
526 return if annotated {
527 crate::features::repo::commands::create_annotated_tag_async(
528 path, name, message, oid,
529 )
530 } else {
531 crate::features::repo::commands::create_tag_async(path, name, oid)
532 };
533 }
534 }
535 Task::none()
536 }
537
538 Message::CancelCreateTag => {
539 let tab = self.active_tab_mut();
540 tab.create_tag_target_oid = None;
541 tab.create_tag_name.clear();
542 tab.create_tag_message.clear();
543 Task::none()
544 }
545
546 Message::CheckoutCommitDetached(oid) => {
548 let oid = oid.clone();
549 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
550 self.active_tab_mut().context_menu = None;
551 with_repo!(self, loading, format!("Checking out {short}…"), |path| {
552 crate::features::repo::commands::checkout_commit_async(path, oid)
553 })
554 }
555
556 Message::RebaseOntoCommit(oid) => {
557 let oid = oid.clone();
558 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
559 self.active_tab_mut().context_menu = None;
560 with_repo!(self, loading, format!("Rebasing onto {short}…"), |path| {
561 crate::features::repo::commands::rebase_onto_async(path, oid)
562 })
563 }
564
565 Message::RevertCommit(oid) => {
566 let oid = oid.clone();
567 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
568 self.active_tab_mut().context_menu = None;
569 with_repo!(self, loading, format!("Reverting {short}…"), |path| {
570 crate::features::repo::commands::revert_commit_async(path, oid)
571 })
572 }
573
574 Message::ResetSoft(ref oid)
575 | Message::ResetMixed(ref oid)
576 | Message::ResetHard(ref oid) => {
577 let mode = match &message {
578 Message::ResetSoft(_) => "soft",
579 Message::ResetMixed(_) => "mixed",
580 Message::ResetHard(_) => "hard",
581 _ => unreachable!(),
582 };
583 let oid = oid.clone();
584 let short = gitkraft_core::utils::short_oid_str(&oid).to_string();
585 self.active_tab_mut().context_menu = None;
586 with_repo!(
587 self,
588 loading,
589 format!("Resetting ({mode}) to {short}…"),
590 |path| crate::features::repo::commands::reset_to_commit_async(
591 path,
592 oid,
593 mode.to_string()
594 )
595 )
596 }
597
598 Message::CopyText(text) => {
600 self.active_tab_mut().context_menu = None;
601 iced::clipboard::write(text.clone())
602 }
603
604 Message::ThemeChanged(index) => {
606 self.current_theme_index = *index;
607 let name = gitkraft_core::THEME_NAMES
609 .get(*index)
610 .copied()
611 .unwrap_or("Default");
612 crate::features::repo::commands::save_theme_async(name.to_string())
613 }
614
615 Message::ThemeSaved(_result) => {
616 Task::none()
618 }
619
620 Message::EditorChanged(editor) => {
621 self.editor = editor.clone();
622 self.active_tab_mut().status_message =
623 Some(format!("Editor set to {}", self.editor));
624 let name = self.editor.display_name().to_string();
626 crate::features::repo::commands::save_editor_async(name)
627 }
628
629 Message::EditorSaved(_result) => {
630 Task::none()
632 }
633
634 Message::LayoutSaved(_result) => {
635 Task::none()
637 }
638
639 Message::SessionSaved(_) => {
640 Task::none()
642 }
643
644 Message::LayoutLoaded(result) => {
645 if let Ok(Some(layout)) = result {
646 if let Some(w) = layout.sidebar_width {
647 self.sidebar_width = w;
648 }
649 if let Some(w) = layout.commit_log_width {
650 self.commit_log_width = w;
651 }
652 if let Some(h) = layout.staging_height {
653 self.staging_height = h;
654 }
655 if let Some(w) = layout.diff_file_list_width {
656 self.diff_file_list_width = w;
657 }
658 if let Some(expanded) = layout.sidebar_expanded {
659 self.sidebar_expanded = expanded;
660 }
661 if let Some(scale) = layout.ui_scale {
662 self.ui_scale = scale.clamp(0.5, 2.0);
663 }
664 }
665 Task::none()
666 }
667
668 Message::ToggleSearch => {
670 self.search_visible = !self.search_visible;
671 if !self.search_visible {
672 self.search_query.clear();
673 self.search_results.clear();
674 self.search_selected = None;
675 }
676 Task::none()
677 }
678
679 Message::SearchQueryChanged(query) => {
680 let query = query.clone();
681 self.search_query = query.clone();
682 if query.trim().len() >= 2 {
683 if let Some(path) = self.active_tab().repo_path.clone() {
684 return crate::features::commits::commands::search_commits(path, query);
685 }
686 } else {
687 self.search_results.clear();
688 self.search_selected = None;
689 }
690 Task::none()
691 }
692
693 Message::SearchResultsLoaded(result) => {
694 match result {
695 Ok(results) => {
696 self.search_results = results.clone();
697 self.search_selected = if self.search_results.is_empty() {
698 None
699 } else {
700 Some(0)
701 };
702 }
703 Err(e) => {
704 self.search_results.clear();
705 self.active_tab_mut().error_message = Some(format!("Search failed: {e}"));
706 }
707 }
708 Task::none()
709 }
710
711 Message::SelectSearchResult(index) => {
712 let index = *index;
713 if index < self.search_results.len() {
714 self.search_selected = Some(index);
715 }
716 Task::none()
717 }
718
719 Message::ConfirmSearchResult => {
720 if let Some(idx) = self.search_selected {
721 if let Some(commit) = self.search_results.get(idx).cloned() {
722 let oid = commit.oid.clone();
723 let short_oid = commit.short_oid.clone();
724 let commit_idx =
726 self.active_tab().commits.iter().position(|c| c.oid == oid);
727 if let Some(ci) = commit_idx {
728 let tab = self.active_tab_mut();
729 tab.selected_commit = Some(ci);
730 tab.show_commit_detail = true;
731 }
732 self.search_query.clear();
734 self.search_results.clear();
735 self.search_selected = None;
736
737 if let Some(path) = self.active_tab().repo_path.clone() {
738 let tab = self.active_tab_mut();
739 tab.status_message = Some(format!("Loading diff for {short_oid}…"));
740 tab.selected_commit_oid = Some(oid.clone());
741 return crate::features::commits::commands::load_commit_file_list(
742 path, oid,
743 );
744 }
745 }
746 }
747 Task::none()
748 }
749
750 Message::FileSystemChanged => {
751 if self.has_repo() && !self.active_tab().is_loading {
752 if let Some(path) = self.active_tab().repo_path.clone() {
753 return crate::features::repo::commands::refresh_staging_only(path);
754 }
755 }
756 Task::none()
757 }
758
759 Message::OpenInEditor(path) => {
760 self.active_tab_mut().context_menu = None;
761 if matches!(self.editor, gitkraft_core::Editor::None) {
762 self.active_tab_mut().status_message = Some(
763 "No editor configured — select one from the editor dropdown in the toolbar"
764 .into(),
765 );
766 return Task::none();
767 }
768 if let Some(repo_path) = self.active_tab().repo_path.as_ref() {
769 let full_path = repo_path.join(path);
770 match self.editor.open_file(&full_path) {
771 Ok(()) => {
772 self.active_tab_mut().status_message =
773 Some(format!("Opened in {}", self.editor));
774 }
775 Err(e) => {
776 self.active_tab_mut().error_message =
777 Some(format!("Failed to open editor: {e}"));
778 }
779 }
780 }
781 Task::none()
782 }
783
784 Message::OpenInDefaultProgram(path) => {
785 self.active_tab_mut().context_menu = None;
786 if let Some(repo_path) = self.active_tab().repo_path.as_ref() {
787 let full_path = repo_path.join(path);
788 if let Err(e) = gitkraft_core::open_file_default(&full_path) {
789 self.active_tab_mut().error_message =
790 Some(format!("Failed to open file: {e}"));
791 }
792 }
793 Task::none()
794 }
795
796 Message::ShowInFolder(path) => {
797 self.active_tab_mut().context_menu = None;
798 if let Some(repo_path) = self.active_tab().repo_path.as_ref() {
799 let full_path = repo_path.join(path);
800 if let Err(e) = gitkraft_core::show_in_folder(&full_path) {
801 self.active_tab_mut().error_message =
802 Some(format!("Failed to show in folder: {e}"));
803 }
804 }
805 Task::none()
806 }
807
808 Message::Noop => Task::none(),
809 }
810 }
811}