Skip to main content

git_tailor/views/
move_select.rs

1// Copyright 2026 Thomas Johannesson
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Move commit target selection — key handling only; rendering is done via
16// the commit list (separator row injection + footer).
17
18use crate::app::{AppAction, AppMode, AppState, KeyCommand};
19
20/// Handle an action while in MoveSelect mode.
21///
22/// The user navigates an insertion cursor between commits. The cursor
23/// (`insert_before`) represents the position where the source commit will
24/// be placed. Arrow keys move the insertion point; Enter confirms; Esc
25/// cancels.
26pub fn handle_key(action: KeyCommand, app: &mut AppState) -> AppAction {
27    let (source_index, insert_before) = match app.mode {
28        AppMode::MoveSelect {
29            source_index,
30            insert_before,
31        } => (source_index, insert_before),
32        _ => return AppAction::Handled,
33    };
34
35    // Valid insertion positions: 0..=commits.len(), excluding source_index.
36    // Position N means "insert before commit N"; position commits.len() means
37    // "insert after the last commit" (i.e. move to HEAD).
38    let max_insert = app.commits.len();
39
40    // Both source_index and source_index + 1 are no-op positions:
41    // "insert before self" and "insert after self" both leave the commit
42    // in the same place.
43    let is_noop = |pos: usize| pos == source_index || pos == source_index + 1;
44
45    match action {
46        KeyCommand::MoveUp => {
47            let mut next = if app.reverse {
48                insert_before.saturating_add(1).min(max_insert)
49            } else {
50                insert_before.saturating_sub(1)
51            };
52            for _ in 0..2 {
53                if is_noop(next) {
54                    next = if app.reverse {
55                        next.saturating_add(1).min(max_insert)
56                    } else {
57                        next.saturating_sub(1)
58                    };
59                }
60            }
61            if is_noop(next) {
62                next = insert_before;
63            }
64            app.mode = AppMode::MoveSelect {
65                source_index,
66                insert_before: next,
67            };
68            AppAction::Handled
69        }
70        KeyCommand::MoveDown => {
71            let mut next = if app.reverse {
72                insert_before.saturating_sub(1)
73            } else {
74                insert_before.saturating_add(1).min(max_insert)
75            };
76            for _ in 0..2 {
77                if is_noop(next) {
78                    next = if app.reverse {
79                        next.saturating_sub(1)
80                    } else {
81                        next.saturating_add(1).min(max_insert)
82                    };
83                }
84            }
85            if is_noop(next) {
86                next = insert_before;
87            }
88            app.mode = AppMode::MoveSelect {
89                source_index,
90                insert_before: next,
91            };
92            AppAction::Handled
93        }
94        KeyCommand::Confirm => {
95            if is_noop(insert_before) {
96                app.set_error_message("Commit is already at this position");
97                return AppAction::Handled;
98            }
99
100            let source = &app.commits[source_index];
101            if source.oid == "staged" || source.oid == "unstaged" {
102                app.set_error_message("Cannot move staged/unstaged changes");
103                return AppAction::Handled;
104            }
105
106            let source_oid = source.oid.clone();
107
108            // insert_before is the commit-list index where the separator sits.
109            // The source should be placed *after* the commit at insert_before - 1,
110            // or after the reference point if insert_before == 0.
111            let insert_after_oid = if insert_before == 0 {
112                app.reference_oid.clone()
113            } else {
114                let idx = (insert_before - 1).min(app.commits.len().saturating_sub(1));
115                app.commits[idx].oid.clone()
116            };
117
118            app.mode = AppMode::CommitList;
119
120            AppAction::ExecuteMove {
121                source_oid,
122                insert_after_oid,
123            }
124        }
125        KeyCommand::ShowHelp => {
126            app.toggle_help();
127            AppAction::Handled
128        }
129        KeyCommand::Quit => {
130            app.cancel_move_select();
131            AppAction::Handled
132        }
133        _ => AppAction::Handled,
134    }
135}