1use crate::{DiffScope, DiffSide, RepositoryAction, RevealAmount, ViewMode};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum FocusPane {
7 #[default]
8 Files,
9 Diff,
10}
11
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13pub enum InteractionPhase {
14 #[default]
15 Browse,
16 Draft,
17 Help,
18 ThemePicker,
19 RepositoryPrompt,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24#[expect(
25 clippy::struct_excessive_bools,
26 reason = "Capabilities are independent flags"
27)]
28pub struct ReviewCapabilities {
29 pub repository: bool,
30 pub refresh: bool,
31 pub scope: bool,
32 pub submit: bool,
33 pub clipboard: bool,
34}
35
36impl Default for ReviewCapabilities {
37 fn default() -> Self {
38 Self {
39 repository: true,
40 refresh: true,
41 scope: true,
42 submit: true,
43 clipboard: true,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[expect(
50 clippy::struct_excessive_bools,
51 reason = "Command conditions are independent flags"
52)]
53pub struct CommandContext {
54 pub phase: InteractionPhase,
55 pub capabilities: ReviewCapabilities,
56 pub repository_pending: bool,
57 pub document_ready: bool,
58 pub source_view: bool,
59 pub navigation_available: bool,
60 pub themes_available: bool,
61}
62
63impl Default for CommandContext {
64 fn default() -> Self {
65 Self {
66 phase: InteractionPhase::Browse,
67 capabilities: ReviewCapabilities::default(),
68 repository_pending: false,
69 document_ready: true,
70 source_view: false,
71 navigation_available: true,
72 themes_available: false,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum ReviewCommand {
80 BeginComment,
81 EditComment,
82 DeleteComment,
83 UndoComment,
84 SubmitComment,
85 Cancel,
86 ShowHelp,
87 ScrollHelp(isize),
88 OpenThemePicker,
89 SelectTheme(usize),
90 MoveTheme(isize),
91 CommitTheme,
92}
93
94impl ReviewCommand {
95 #[must_use]
96 pub fn enabled(self, context: &CommandContext) -> bool {
97 let phase = context.phase;
98 match self {
99 Self::Cancel => true,
100 Self::ShowHelp => phase == InteractionPhase::Browse,
101 Self::ScrollHelp(_) => phase == InteractionPhase::Help,
102 Self::OpenThemePicker => phase == InteractionPhase::Browse && context.themes_available,
103 Self::SelectTheme(_) | Self::MoveTheme(_) | Self::CommitTheme => {
104 phase == InteractionPhase::ThemePicker
105 }
106 Self::SubmitComment => phase == InteractionPhase::Draft && context.document_ready,
107 Self::BeginComment | Self::EditComment | Self::DeleteComment | Self::UndoComment => {
108 phase == InteractionPhase::Browse && context.document_ready
109 }
110 }
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum DiffReviewCommand {
117 Review(ReviewCommand),
118 Focus(FocusPane),
119 ToggleFocus,
120 SelectFile(usize),
121 SelectRow(usize),
122 SelectSide(DiffSide),
123 MoveSelection(isize),
124 Scroll { pane: FocusPane, lines: isize },
125 Page(isize),
126 First,
127 Last,
128 OpenSelected,
129 CollapseSelected,
130 RevealGap(RevealAmount),
131 ToggleFullFile,
132 ToggleSourceView,
133 SetViewMode(ViewMode),
134 CycleViewMode,
135 SubmitReview,
136 CopyReview,
137 SetScope(DiffScope),
138 CycleScope,
139 Refresh,
140 ToggleStage,
141 StageAll,
142 UnstageAll,
143 BeginCommit,
144 BeginDiscard,
145 RepositoryAction(RepositoryAction),
146}
147
148impl From<ReviewCommand> for DiffReviewCommand {
149 fn from(command: ReviewCommand) -> Self {
150 Self::Review(command)
151 }
152}
153
154impl DiffReviewCommand {
155 #[must_use]
156 pub fn enabled(&self, context: &CommandContext) -> bool {
157 if context.source_view
158 && matches!(
159 self,
160 Self::RevealGap(_)
161 | Self::ToggleFullFile
162 | Self::SetViewMode(_)
163 | Self::CycleViewMode
164 | Self::SelectSide(_)
165 | Self::Review(
166 ReviewCommand::BeginComment
167 | ReviewCommand::EditComment
168 | ReviewCommand::DeleteComment
169 | ReviewCommand::UndoComment
170 )
171 )
172 {
173 return false;
174 }
175 if let Self::Review(command) = self {
176 return command.enabled(context);
177 }
178 if context.phase != InteractionPhase::Browse {
179 return false;
180 }
181 let capability = match self {
182 Self::SetScope(_) | Self::CycleScope => {
183 context.capabilities.scope && !context.repository_pending
184 }
185 Self::Refresh => context.capabilities.refresh && !context.repository_pending,
186 Self::ToggleStage
187 | Self::StageAll
188 | Self::UnstageAll
189 | Self::BeginCommit
190 | Self::BeginDiscard
191 | Self::RepositoryAction(_) => {
192 context.capabilities.repository && !context.repository_pending
193 }
194 Self::SubmitReview => context.capabilities.submit,
195 Self::CopyReview => context.capabilities.clipboard,
196 Self::Focus(FocusPane::Files) | Self::ToggleFocus => context.navigation_available,
197 _ => true,
198 };
199 capability
200 && (context.document_ready
201 || matches!(self, Self::SetScope(_) | Self::CycleScope | Self::Refresh))
202 }
203}