use argonauts_core::app::{AppChoice, ChoiceKind};
use argonauts_core::events::approval::{AnswerAction, AnswerOption, FreeText};
use argonauts_core::names::ChoiceId;
use argonauts_core::settings::SettingScope;
pub(crate) use argonauts_core::app::NOT_CHANGED;
pub(crate) const JUST_ASKING: &str = "Start work on it? ยท no, just asking";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OnSetAside {
SetAside,
NotChanged,
NotNow,
NextRun,
Recommended,
Cancel,
JustThisRequest,
No,
NothingRan,
NotStarted,
}
pub(crate) fn on_set_aside(kind: ChoiceKind) -> OnSetAside {
match kind {
ChoiceKind::StartWork | ChoiceKind::RefusedCommands => OnSetAside::SetAside,
ChoiceKind::Permission | ChoiceKind::GoAhead | ChoiceKind::Fallback => OnSetAside::No,
ChoiceKind::RewindTurn | ChoiceKind::Git | ChoiceKind::Templates => OnSetAside::NotChanged,
ChoiceKind::TryWorkflow => OnSetAside::NothingRan,
ChoiceKind::ConfirmTakeOver | ChoiceKind::ConfirmRewind => OnSetAside::Cancel,
ChoiceKind::SettingScope | ChoiceKind::ConfirmSafetySetting => OnSetAside::NotChanged,
ChoiceKind::TrustProject | ChoiceKind::ConfirmConnect => OnSetAside::NotNow,
ChoiceKind::DetectedChecks => OnSetAside::NextRun,
ChoiceKind::TermsNote | ChoiceKind::Init => OnSetAside::Recommended,
ChoiceKind::KeepInstructions => OnSetAside::JustThisRequest,
ChoiceKind::Setup => OnSetAside::NotChanged,
ChoiceKind::NeededUpdate => OnSetAside::NotStarted,
ChoiceKind::RenamedSettings => OnSetAside::NotChanged,
}
}
pub(crate) fn options(kind: ChoiceKind) -> Vec<AnswerOption> {
match kind {
ChoiceKind::StartWork => vec![
option('s', "Captain", choose("captain"), false),
option('c', "Crew run", choose("crew"), false),
option('n', "No, just asking", choose("no"), false),
],
ChoiceKind::ConfirmTakeOver => vec![option('t', "Take over", AnswerAction::TakeOver, true)],
ChoiceKind::SettingScope => scope_options(SettingScope::ProjectLocal),
ChoiceKind::ConfirmSafetySetting => vec![
option('y', "Change it", AnswerAction::Approve, false),
option('n', "Keep it as it is", AnswerAction::Cancel, false),
],
ChoiceKind::TrustProject => vec![
option('y', "Trust them", AnswerAction::Approve, false),
option('n', "Not now", AnswerAction::Cancel, false),
option('d', "Show the file", AnswerAction::ShowDiff, false),
],
ChoiceKind::DetectedChecks => vec![
option('y', "Use these", AnswerAction::Approve, true),
option('e', "Edit them", AnswerAction::ApproveWithEdits, false),
option('n', "Skip", AnswerAction::Cancel, false),
],
ChoiceKind::TermsNote => vec![option('c', "Continue", AnswerAction::Approve, true)],
ChoiceKind::ConfirmRewind => vec![
option('y', "Rewind", AnswerAction::Approve, false),
option('d', "Show the diff", AnswerAction::ShowDiff, false),
],
ChoiceKind::ConfirmConnect => vec![
option('y', "Write it", AnswerAction::Approve, false),
option('n', "Not now", AnswerAction::Cancel, false),
],
ChoiceKind::KeepInstructions => keep_options(),
ChoiceKind::RefusedCommands => vec![
option('y', "Yes, and continue", choose("continue"), true),
option('n', "No", choose("no"), false),
],
ChoiceKind::GoAhead => vec![
option('y', "Yes, go ahead", choose("yes"), true),
option('n', "No, keep planning", choose("no"), false),
],
ChoiceKind::RenamedSettings => vec![
option('r', "Rename them", AnswerAction::Approve, false),
option('l', "Leave them", AnswerAction::Cancel, false),
],
ChoiceKind::TryWorkflow => vec![
option('y', "Yes, run it", AnswerAction::Approve, false),
option('n', "No", choose("no"), true),
],
ChoiceKind::Init
| ChoiceKind::Permission
| ChoiceKind::RewindTurn
| ChoiceKind::Fallback
| ChoiceKind::Setup
| ChoiceKind::Git
| ChoiceKind::Templates
| ChoiceKind::NeededUpdate => Vec::new(),
}
}
pub(crate) fn keep_options() -> Vec<AnswerOption> {
let mut options: Vec<AnswerOption> = [
('m', SettingScope::ProjectLocal),
('p', SettingScope::Project),
('e', SettingScope::Global),
]
.into_iter()
.map(|(key, scope)| {
option(
key,
scope_label(scope),
choose(scope_value(scope)),
scope == SettingScope::ProjectLocal,
)
})
.collect();
options.push(option(
'r',
"Just this request",
choose(JUST_THIS_REQUEST),
false,
));
options
}
pub(crate) fn keep_crew_options() -> Vec<AnswerOption> {
let mut save = option(
'p',
"Save for this repository",
choose(scope_value(SettingScope::Project)),
false,
);
save.detail = Some("writes [crew] in .argonauts/config.toml".to_owned());
vec![
option('r', "Just this request", choose(JUST_THIS_REQUEST), true),
option(
's',
"This conversation",
choose(scope_value(SettingScope::Session)),
false,
),
save,
]
}
pub(crate) const EDIT_IT: &str = "edit";
pub(crate) const JUST_THIS_REQUEST: &str = "request";
pub(crate) fn scope_options(recommended: SettingScope) -> Vec<AnswerOption> {
[
('s', SettingScope::Session),
('m', SettingScope::ProjectLocal),
('p', SettingScope::Project),
('e', SettingScope::Global),
]
.into_iter()
.map(|(key, scope)| {
option(
key,
scope_label(scope),
choose(scope_value(scope)),
scope == recommended,
)
})
.collect()
}
pub(crate) fn scope_label(scope: SettingScope) -> &'static str {
match scope {
SettingScope::Session => "Just this session",
SettingScope::ProjectLocal => "This project, just me",
SettingScope::Project => "This project, shared",
SettingScope::Global => "Everywhere",
}
}
pub(crate) fn scope_value(scope: SettingScope) -> &'static str {
match scope {
SettingScope::Session => "session",
SettingScope::ProjectLocal => "project_local",
SettingScope::Project => "project",
SettingScope::Global => "global",
}
}
pub(crate) fn scope_from_value(value: &str) -> Option<SettingScope> {
[
SettingScope::Session,
SettingScope::ProjectLocal,
SettingScope::Project,
SettingScope::Global,
]
.into_iter()
.find(|scope| scope_value(*scope) == value)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InitAnswer {
pub label: String,
pub value: String,
pub detail: Option<String>,
}
pub(crate) fn init_options(answers: &[InitAnswer]) -> Vec<AnswerOption> {
if answers.is_empty() {
return options(ChoiceKind::TermsNote);
}
answers
.iter()
.zip('a'..='z')
.enumerate()
.map(|(index, (answer, key))| AnswerOption {
key,
label: answer.label.clone(),
action: choose(&answer.value),
recommended: index == 0,
detail: answer.detail.clone(),
})
.collect()
}
pub(crate) fn app_choice(
id: ChoiceId,
kind: ChoiceKind,
title: impl Into<String>,
body: Vec<String>,
options: Vec<AnswerOption>,
) -> AppChoice {
let free_text = match kind {
ChoiceKind::DetectedChecks => FreeText::Edits,
_ => FreeText::None,
};
AppChoice {
id,
kind,
title: title.into(),
body,
options,
free_text,
bell: false,
}
}
pub(crate) fn option(
key: char,
label: &str,
action: AnswerAction,
recommended: bool,
) -> AnswerOption {
AnswerOption {
key,
label: label.to_owned(),
action,
recommended,
detail: None,
}
}
pub(crate) fn choose(value: &str) -> AnswerAction {
AnswerAction::Choose {
value: value.to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
const SCOPES: [SettingScope; 4] = [
SettingScope::Session,
SettingScope::ProjectLocal,
SettingScope::Project,
SettingScope::Global,
];
type Shape<'a> = (char, &'a str, AnswerAction);
fn shape(options: &[AnswerOption]) -> Vec<Shape<'_>> {
options
.iter()
.map(|option| (option.key, option.label.as_str(), option.action.clone()))
.collect()
}
#[test]
fn app_choice_options_follow_the_table() {
use AnswerAction::{Approve, ApproveWithEdits, Cancel, ShowDiff, TakeOver};
let table: Vec<(ChoiceKind, Vec<Shape>, OnSetAside)> = vec![
(
ChoiceKind::StartWork,
vec![
('s', "Captain", choose("captain")),
('c', "Crew run", choose("crew")),
('n', "No, just asking", choose("no")),
],
OnSetAside::SetAside,
),
(
ChoiceKind::ConfirmTakeOver,
vec![('t', "Take over", TakeOver)],
OnSetAside::Cancel,
),
(
ChoiceKind::SettingScope,
vec![
('s', "Just this session", choose("session")),
('m', "This project, just me", choose("project_local")),
('p', "This project, shared", choose("project")),
('e', "Everywhere", choose("global")),
],
OnSetAside::NotChanged,
),
(
ChoiceKind::ConfirmSafetySetting,
vec![
('y', "Change it", Approve),
('n', "Keep it as it is", Cancel),
],
OnSetAside::NotChanged,
),
(
ChoiceKind::TrustProject,
vec![
('y', "Trust them", Approve),
('n', "Not now", Cancel),
('d', "Show the file", ShowDiff),
],
OnSetAside::NotNow,
),
(
ChoiceKind::DetectedChecks,
vec![
('y', "Use these", Approve),
('e', "Edit them", ApproveWithEdits),
('n', "Skip", Cancel),
],
OnSetAside::NextRun,
),
(
ChoiceKind::TermsNote,
vec![('c', "Continue", Approve)],
OnSetAside::Recommended,
),
(
ChoiceKind::ConfirmRewind,
vec![('y', "Rewind", Approve), ('d', "Show the diff", ShowDiff)],
OnSetAside::Cancel,
),
(
ChoiceKind::ConfirmConnect,
vec![('y', "Write it", Approve), ('n', "Not now", Cancel)],
OnSetAside::NotNow,
),
(ChoiceKind::Init, Vec::new(), OnSetAside::Recommended),
(
ChoiceKind::KeepInstructions,
vec![
('m', "This project, just me", choose("project_local")),
('p', "This project, shared", choose("project")),
('e', "Everywhere", choose("global")),
('r', "Just this request", choose("request")),
],
OnSetAside::JustThisRequest,
),
(ChoiceKind::Permission, Vec::new(), OnSetAside::No),
(
ChoiceKind::RefusedCommands,
vec![
('y', "Yes, and continue", choose("continue")),
('n', "No", choose("no")),
],
OnSetAside::SetAside,
),
(ChoiceKind::RewindTurn, Vec::new(), OnSetAside::NotChanged),
(
ChoiceKind::GoAhead,
vec![
('y', "Yes, go ahead", choose("yes")),
('n', "No, keep planning", choose("no")),
],
OnSetAside::No,
),
(ChoiceKind::Fallback, Vec::new(), OnSetAside::No),
(
ChoiceKind::TryWorkflow,
vec![('y', "Yes, run it", Approve), ('n', "No", choose("no"))],
OnSetAside::NothingRan,
),
(ChoiceKind::Setup, Vec::new(), OnSetAside::NotChanged),
(ChoiceKind::Git, Vec::new(), OnSetAside::NotChanged),
(ChoiceKind::Templates, Vec::new(), OnSetAside::NotChanged),
(ChoiceKind::NeededUpdate, Vec::new(), OnSetAside::NotStarted),
(
ChoiceKind::RenamedSettings,
vec![
('r', "Rename them", AnswerAction::Approve),
('l', "Leave them", AnswerAction::Cancel),
],
OnSetAside::NotChanged,
),
];
for (kind, expected, esc) in &table {
assert_eq!(shape(&options(*kind)), *expected, "{kind:?}");
assert_eq!(on_set_aside(*kind), *esc, "{kind:?}");
let choice = app_choice(
ChoiceId::numbered(1),
*kind,
"t",
Vec::new(),
options(*kind),
);
let free_text = match kind {
ChoiceKind::DetectedChecks => FreeText::Edits,
_ => FreeText::None,
};
assert_eq!(choice.free_text, free_text, "{kind:?}");
}
let kinds: std::collections::HashSet<ChoiceKind> =
table.iter().map(|(kind, ..)| *kind).collect();
assert_eq!(kinds.len(), table.len());
for kind in kinds {
match kind {
ChoiceKind::StartWork
| ChoiceKind::ConfirmTakeOver
| ChoiceKind::SettingScope
| ChoiceKind::ConfirmSafetySetting
| ChoiceKind::TermsNote
| ChoiceKind::Init
| ChoiceKind::DetectedChecks
| ChoiceKind::TrustProject
| ChoiceKind::ConfirmRewind
| ChoiceKind::ConfirmConnect
| ChoiceKind::KeepInstructions
| ChoiceKind::Permission
| ChoiceKind::RefusedCommands
| ChoiceKind::RewindTurn
| ChoiceKind::GoAhead
| ChoiceKind::Fallback
| ChoiceKind::TryWorkflow
| ChoiceKind::Setup
| ChoiceKind::Git
| ChoiceKind::Templates
| ChoiceKind::NeededUpdate
| ChoiceKind::RenamedSettings => {}
}
}
for scope in SCOPES {
let options = scope_options(scope);
let picked: Vec<AnswerAction> = options
.iter()
.filter(|option| option.recommended)
.map(|option| option.action.clone())
.collect();
assert_eq!(picked, vec![choose(scope_value(scope))]);
assert_eq!(scope_from_value(scope_value(scope)), Some(scope));
let serde_name = serde_json::to_value(scope).unwrap();
assert_eq!(serde_name, scope_value(scope));
}
assert_eq!(scope_from_value("somewhere"), None);
let answers = ["one", "two", "three"].map(|value| InitAnswer {
label: value.to_owned(),
value: value.to_owned(),
detail: None,
});
let steps = init_options(&answers);
let keys: Vec<(char, bool)> = steps
.iter()
.map(|option| (option.key, option.recommended))
.collect();
assert_eq!(keys, vec![('a', true), ('b', false), ('c', false)]);
assert_eq!(steps[1].action, choose("two"));
assert_eq!(shape(&init_options(&[])), vec![('c', "Continue", Approve)]);
}
}