use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeKind {
None,
Added,
Deleted,
Modified,
Run,
Unknown(char),
}
impl ChangeKind {
pub fn from_status_char(c: char) -> Self {
match c {
' ' => Self::None,
'A' => Self::Added,
'D' => Self::Deleted,
'M' => Self::Modified,
'R' => Self::Run,
other => Self::Unknown(other),
}
}
pub fn as_symbol(self) -> char {
match self {
Self::None => ' ',
Self::Added => 'A',
Self::Deleted => 'D',
Self::Modified => 'M',
Self::Run => 'R',
Self::Unknown(c) => c,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusEntry {
pub path: PathBuf,
pub actual_vs_state: ChangeKind,
pub actual_vs_target: ChangeKind,
}
impl fmt::Display for StatusEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{} {}",
self.actual_vs_state.as_symbol(),
self.actual_vs_target.as_symbol(),
self.path.display()
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffText {
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Apply,
Doctor,
Data,
OpenSourceDir,
ExternalDiff,
DebugContext,
Update,
EditConfig,
EditConfigTemplate,
EditIgnore,
ReAdd,
Merge,
MergeAll,
Add,
Ignore,
Edit,
Forget,
Chattr,
Destroy,
Purge,
}
impl Action {
pub const ALL: [Action; 20] = [
Action::Apply,
Action::Doctor,
Action::Data,
Action::OpenSourceDir,
Action::ExternalDiff,
Action::DebugContext,
Action::Update,
Action::EditConfig,
Action::EditConfigTemplate,
Action::EditIgnore,
Action::ReAdd,
Action::Merge,
Action::MergeAll,
Action::Add,
Action::Ignore,
Action::Edit,
Action::Forget,
Action::Chattr,
Action::Destroy,
Action::Purge,
];
pub fn label(self) -> &'static str {
match self {
Action::Apply => "apply",
Action::Doctor => "doctor",
Action::Data => "data",
Action::OpenSourceDir => "open-source-dir",
Action::ExternalDiff => "external-diff",
Action::DebugContext => "debug-context",
Action::Update => "update",
Action::EditConfig => "edit-config",
Action::EditConfigTemplate => "edit-config-template",
Action::EditIgnore => "edit-ignore",
Action::ReAdd => "re-add",
Action::Merge => "merge",
Action::MergeAll => "merge-all",
Action::Add => "add",
Action::Ignore => "ignore",
Action::Edit => "edit",
Action::Forget => "forget",
Action::Chattr => "chattr",
Action::Destroy => "destroy",
Action::Purge => "purge",
}
}
pub fn description(self) -> &'static str {
match self {
Action::Apply => "apply target state to destination",
Action::Doctor => "run chezmoi diagnostics",
Action::Data => "show template data as JSON",
Action::OpenSourceDir => "open an interactive shell in the source directory",
Action::ExternalDiff => "open full diff in configured external diff tool",
Action::DebugContext => "show current app/view/debug context",
Action::Update => "update source and apply changes",
Action::EditConfig => "edit chezmoi config file",
Action::EditConfigTemplate => "edit chezmoi config template",
Action::EditIgnore => "edit .chezmoiignore",
Action::ReAdd => "re-import selected modified file",
Action::Merge => "run 3-way merge",
Action::MergeAll => "run 3-way merge for all changes",
Action::Add => "add existing file to managed set",
Action::Ignore => "append target to .chezmoiignore",
Action::Edit => "edit source state in external editor",
Action::Forget => "remove from managed set",
Action::Chattr => "change source attributes",
Action::Destroy => "delete from source/destination/state",
Action::Purge => "remove chezmoi config and data",
}
}
pub fn is_dangerous(self) -> bool {
matches!(self, Action::Destroy | Action::Purge)
}
pub fn requires_confirmation(self) -> bool {
matches!(
self,
Action::Apply
| Action::Update
| Action::MergeAll
| Action::Forget
| Action::Chattr
| Action::Destroy
| Action::Purge
)
}
pub fn confirm_phrase(self) -> Option<&'static str> {
match self {
Action::Destroy => Some("DESTROY"),
Action::Purge => Some("PURGE"),
_ => None,
}
}
pub fn needs_target(self) -> bool {
matches!(
self,
Action::ReAdd
| Action::Merge
| Action::Add
| Action::Ignore
| Action::Edit
| Action::Forget
| Action::Chattr
| Action::Destroy
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActionRequest {
pub action: Action,
pub target: Option<PathBuf>,
pub chattr_attrs: Option<String>,
}
impl ActionRequest {
pub fn requires_strict_confirmation(&self) -> bool {
matches!(self.action, Action::Destroy | Action::Purge)
}
pub fn confirmation_phrase(&self) -> Option<String> {
let base = self.action.confirm_phrase()?;
match self.action {
Action::Destroy => self
.target
.as_ref()
.map(|target| format!("{base} {}", target.display())),
Action::Purge => Some(format!("{base} ALL")),
_ => Some(base.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandResult {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListView {
Status,
Managed,
Unmanaged,
Source,
}
impl ListView {
pub fn title(self) -> &'static str {
match self {
ListView::Status => "Status",
ListView::Managed => "Managed",
ListView::Unmanaged => "Unmanaged",
ListView::Source => "Source",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn action_request_confirmation_phrase_includes_target_for_destroy() {
let req = ActionRequest {
action: Action::Destroy,
target: Some(PathBuf::from("/tmp/demo.txt")),
chattr_attrs: None,
};
assert_eq!(
req.confirmation_phrase(),
Some("DESTROY /tmp/demo.txt".to_string())
);
}
#[test]
fn action_request_confirmation_phrase_is_all_for_purge() {
let req = ActionRequest {
action: Action::Purge,
target: None,
chattr_attrs: None,
};
assert_eq!(req.confirmation_phrase(), Some("PURGE ALL".to_string()));
}
#[test]
fn readd_action_requires_target() {
assert!(Action::ReAdd.needs_target());
assert_eq!(
Action::ReAdd.description(),
"re-import selected modified file"
);
}
}