Skip to main content

clankerdiff_core/
commands.rs

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}