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 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}