use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[cfg(feature = "web-monitoring")]
use utoipa::ToSchema;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ViewMode {
#[default]
Changes,
Worktrees,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum StopMode {
#[default]
None,
GracefulPending,
ImmediatePending,
ForceStopped,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WorkspaceDirtyState {
#[default]
Unknown,
Clean,
Dirty,
}
impl WorkspaceDirtyState {
pub fn observed(dirty: bool) -> Self {
if dirty {
WorkspaceDirtyState::Dirty
} else {
WorkspaceDirtyState::Clean
}
}
pub fn shows_dirty_badge(self) -> bool {
matches!(self, WorkspaceDirtyState::Dirty)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AppExecutionMode {
#[default]
Select,
Running,
Stopping,
Stopped,
Error,
}
impl AppExecutionMode {
pub fn operator_mode(self) -> crate::orchestration::operator_command::OperatorMode {
use crate::orchestration::operator_command::OperatorMode;
match self {
AppExecutionMode::Select => OperatorMode::Select,
AppExecutionMode::Running => OperatorMode::Running,
AppExecutionMode::Stopping => OperatorMode::Stopping,
AppExecutionMode::Stopped => OperatorMode::Stopped,
AppExecutionMode::Error => OperatorMode::Error,
}
}
pub fn from_operator_mode(mode: crate::orchestration::operator_command::OperatorMode) -> Self {
use crate::orchestration::operator_command::OperatorMode;
match mode {
OperatorMode::Select => AppExecutionMode::Select,
OperatorMode::Running => AppExecutionMode::Running,
OperatorMode::Stopping => AppExecutionMode::Stopping,
OperatorMode::Stopped => AppExecutionMode::Stopped,
OperatorMode::Error => AppExecutionMode::Error,
}
}
pub fn app_mode_token(self) -> &'static str {
match self {
AppExecutionMode::Select => "select",
AppExecutionMode::Running => "running",
AppExecutionMode::Stopping => "stopping",
AppExecutionMode::Stopped => "stopped",
AppExecutionMode::Error => "error",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModalState {
QrPopup,
ConfirmWorktreeDelete {
path: PathBuf,
branch: String,
},
ConfirmDirtyDiscard {
path: PathBuf,
identity: String,
branch: String,
head: String,
skip_teardown: bool,
},
ConfirmAheadDiscard {
path: PathBuf,
identity: String,
branch: String,
head: String,
dirty: bool,
skip_teardown: bool,
},
ConfirmForceKill {
change_id: String,
},
}
impl ModalState {
pub fn title_label(&self) -> &'static str {
match self {
ModalState::QrPopup => "QR Code",
ModalState::ConfirmWorktreeDelete { .. } => "Confirm Delete",
ModalState::ConfirmDirtyDiscard { .. } => "Discard Changes",
ModalState::ConfirmAheadDiscard { .. } => "Discard Commits",
ModalState::ConfirmForceKill { .. } => "Confirm Kill",
}
}
pub fn is_user_decision(&self) -> bool {
!matches!(self, ModalState::QrPopup)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteIntent {
pub path: PathBuf,
pub branch: String,
pub identity: Option<String>,
pub head: Option<String>,
pub skip_teardown: bool,
pub allow_known_dirty: bool,
pub allow_commits_ahead: bool,
}
impl DeleteIntent {
pub fn ordinary(path: PathBuf, branch: String, skip_teardown: bool) -> Self {
Self {
path,
branch,
identity: None,
head: None,
skip_teardown,
allow_known_dirty: false,
allow_commits_ahead: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "web-monitoring", derive(ToSchema))]
pub struct WorktreeInfo {
#[cfg_attr(feature = "web-monitoring", schema(value_type = String))]
pub path: PathBuf,
pub head: String,
pub branch: String,
pub is_detached: bool,
pub is_main: bool,
pub merge_conflict: Option<MergeConflictInfo>,
pub has_commits_ahead: bool,
pub is_merging: bool,
#[serde(default)]
pub inspection: crate::worktree_ops::InspectionState,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "web-monitoring", derive(ToSchema))]
pub struct MergeConflictInfo {
pub conflict_files: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::orchestration::operator_command::OperatorMode;
#[test]
fn execution_mode_defaults_to_select() {
assert_eq!(AppExecutionMode::default(), AppExecutionMode::Select);
}
#[test]
fn execution_mode_maps_onto_the_shared_operator_matrix() {
let pairs = [
(AppExecutionMode::Select, OperatorMode::Select),
(AppExecutionMode::Running, OperatorMode::Running),
(AppExecutionMode::Stopping, OperatorMode::Stopping),
(AppExecutionMode::Stopped, OperatorMode::Stopped),
(AppExecutionMode::Error, OperatorMode::Error),
];
for (execution, operator) in pairs {
assert_eq!(execution.operator_mode(), operator);
}
}
#[test]
fn canonical_app_mode_tokens_round_trip_through_the_shared_vocabulary() {
for mode in [
AppExecutionMode::Select,
AppExecutionMode::Running,
AppExecutionMode::Stopping,
AppExecutionMode::Stopped,
AppExecutionMode::Error,
] {
assert_eq!(
OperatorMode::from_app_mode(mode.app_mode_token()),
mode.operator_mode(),
"canonical app_mode token must stay execution-only for {:?}",
mode
);
}
}
#[test]
fn destructive_modals_carry_their_own_identity_payload() {
let worktree = ModalState::ConfirmWorktreeDelete {
path: PathBuf::from("/tmp/worktree-a"),
branch: "feature/a".to_string(),
};
let kill = ModalState::ConfirmForceKill {
change_id: "change-a".to_string(),
};
assert_ne!(
worktree,
ModalState::ConfirmWorktreeDelete {
path: PathBuf::from("/tmp/worktree-b"),
branch: "feature/a".to_string(),
}
);
assert_ne!(
worktree,
ModalState::ConfirmWorktreeDelete {
path: PathBuf::from("/tmp/worktree-a"),
branch: "feature/b".to_string(),
}
);
assert_ne!(
kill,
ModalState::ConfirmForceKill {
change_id: "change-b".to_string(),
}
);
}
#[test]
fn only_confirmations_are_user_decisions() {
assert!(!ModalState::QrPopup.is_user_decision());
assert!(ModalState::ConfirmWorktreeDelete {
path: PathBuf::from("/tmp/worktree-a"),
branch: "feature/a".to_string(),
}
.is_user_decision());
assert!(ModalState::ConfirmForceKill {
change_id: "change-a".to_string(),
}
.is_user_decision());
}
#[test]
fn modal_titles_are_presentation_labels_only() {
assert_eq!(ModalState::QrPopup.title_label(), "QR Code");
assert_eq!(
ModalState::ConfirmWorktreeDelete {
path: PathBuf::from("/tmp/worktree-a"),
branch: "feature/a".to_string(),
}
.title_label(),
"Confirm Delete"
);
assert_eq!(
ModalState::ConfirmForceKill {
change_id: "change-a".to_string(),
}
.title_label(),
"Confirm Kill"
);
}
}