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 navigation_available: bool,
59 pub themes_available: bool,
60}
61
62impl Default for CommandContext {
63 fn default() -> Self {
64 Self {
65 phase: InteractionPhase::Browse,
66 capabilities: ReviewCapabilities::default(),
67 repository_pending: false,
68 document_ready: true,
69 navigation_available: true,
70 themes_available: false,
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum ReviewCommand {
78 BeginComment,
79 EditComment,
80 DeleteComment,
81 UndoComment,
82 SubmitComment,
83 Cancel,
84 ShowHelp,
85 ScrollHelp(isize),
86 OpenThemePicker,
87 SelectTheme(usize),
88 MoveTheme(isize),
89 CommitTheme,
90}
91
92impl ReviewCommand {
93 #[must_use]
94 pub fn enabled(self, context: &CommandContext) -> bool {
95 let phase = context.phase;
96 match self {
97 Self::Cancel => true,
98 Self::ShowHelp => phase == InteractionPhase::Browse,
99 Self::ScrollHelp(_) => phase == InteractionPhase::Help,
100 Self::OpenThemePicker => phase == InteractionPhase::Browse && context.themes_available,
101 Self::SelectTheme(_) | Self::MoveTheme(_) | Self::CommitTheme => {
102 phase == InteractionPhase::ThemePicker
103 }
104 Self::SubmitComment => phase == InteractionPhase::Draft && context.document_ready,
105 Self::BeginComment | Self::EditComment | Self::DeleteComment | Self::UndoComment => {
106 phase == InteractionPhase::Browse && context.document_ready
107 }
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub enum DiffReviewCommand {
115 Review(ReviewCommand),
116 Focus(FocusPane),
117 ToggleFocus,
118 SelectFile(usize),
119 SelectRow(usize),
120 SelectSide(DiffSide),
121 MoveSelection(isize),
122 Scroll { pane: FocusPane, lines: isize },
123 Page(isize),
124 First,
125 Last,
126 OpenSelected,
127 CollapseSelected,
128 RevealGap(RevealAmount),
129 ToggleFullFile,
130 SetViewMode(ViewMode),
131 CycleViewMode,
132 SubmitReview,
133 CopyReview,
134 SetScope(DiffScope),
135 CycleScope,
136 Refresh,
137 ToggleStage,
138 StageAll,
139 UnstageAll,
140 BeginCommit,
141 BeginDiscard,
142 RepositoryAction(RepositoryAction),
143}
144
145impl From<ReviewCommand> for DiffReviewCommand {
146 fn from(command: ReviewCommand) -> Self {
147 Self::Review(command)
148 }
149}
150
151impl DiffReviewCommand {
152 #[must_use]
153 pub fn enabled(&self, context: &CommandContext) -> bool {
154 if let Self::Review(command) = self {
155 return command.enabled(context);
156 }
157 if context.phase != InteractionPhase::Browse {
158 return false;
159 }
160 let capability = match self {
161 Self::SetScope(_) | Self::CycleScope => {
162 context.capabilities.scope && !context.repository_pending
163 }
164 Self::Refresh => context.capabilities.refresh && !context.repository_pending,
165 Self::ToggleStage
166 | Self::StageAll
167 | Self::UnstageAll
168 | Self::BeginCommit
169 | Self::BeginDiscard
170 | Self::RepositoryAction(_) => {
171 context.capabilities.repository && !context.repository_pending
172 }
173 Self::SubmitReview => context.capabilities.submit,
174 Self::CopyReview => context.capabilities.clipboard,
175 Self::Focus(FocusPane::Files) | Self::ToggleFocus => context.navigation_available,
176 _ => true,
177 };
178 capability
179 && (context.document_ready
180 || matches!(self, Self::SetScope(_) | Self::CycleScope | Self::Refresh))
181 }
182}