Skip to content

Commit 9acb7b2

Browse files
committed
feat: add explicit enable/disable flags to am use
- am use -e always enables (no-op if already active) - am use -d always disables (no-op if already inactive) - am use without flags retains toggle behavior (unchanged) - Extract ProfileUse struct to share args between am use and am profile use - Add tests for enable/disable idempotency and error cases - Update shell completions for all supported shells
1 parent 98c322c commit 9acb7b2

18 files changed

Lines changed: 211 additions & 55 deletions

completions/bash/am.bash

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ _am() {
707707
return 0
708708
;;
709709
am__subcmd__profile__subcmd__use)
710-
opts="-n -i -h -V --priority --inverse --help --version [NAMES]..."
710+
opts="-e -d -n -i -h -V --enable --disable --priority --inverse --help --version [NAMES]..."
711711
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
712712
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
713713
return 0
@@ -861,7 +861,7 @@ _am() {
861861
return 0
862862
;;
863863
am__subcmd__use)
864-
opts="-n -i -h -V --priority --inverse --help --version [NAMES]..."
864+
opts="-e -d -n -i -h -V --enable --disable --priority --inverse --help --version [NAMES]..."
865865
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
866866
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
867867
return 0

completions/fish/am.fish

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subco
7070
complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s h -l help -d 'Print help'
7171
complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s V -l version -d 'Print version'
7272
complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r
73+
complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s e -l enable -d 'Enable given profile(s), does not toggle'
74+
complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s d -l disable -d 'Disable given profile(s), does not toggle'
7375
complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)'
7476
complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s h -l help -d 'Print help'
7577
complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s V -l version -d 'Print version'
@@ -90,6 +92,8 @@ complete -c am -n "__fish_am_using_subcommand init" -s V -l version -d 'Print ve
9092
complete -c am -n "__fish_am_using_subcommand setup" -s h -l help -d 'Print help'
9193
complete -c am -n "__fish_am_using_subcommand setup" -s V -l version -d 'Print version'
9294
complete -c am -n "__fish_am_using_subcommand use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r
95+
complete -c am -n "__fish_am_using_subcommand use" -s e -l enable -d 'Enable given profile(s), does not toggle'
96+
complete -c am -n "__fish_am_using_subcommand use" -s d -l disable -d 'Disable given profile(s), does not toggle'
9397
complete -c am -n "__fish_am_using_subcommand use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)'
9498
complete -c am -n "__fish_am_using_subcommand use" -s h -l help -d 'Print help'
9599
complete -c am -n "__fish_am_using_subcommand use" -s V -l version -d 'Print version'

completions/powershell/_am.ps1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock {
110110
'am;profile;use' {
111111
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Activate at specific priority position (1-based). Repositions if already active')
112112
[CompletionResult]::new('--priority', '--priority', [CompletionResultType]::ParameterName, 'Activate at specific priority position (1-based). Repositions if already active')
113+
[CompletionResult]::new('-e', '-e', [CompletionResultType]::ParameterName, 'Enable given profile(s), does not toggle')
114+
[CompletionResult]::new('--enable', '--enable', [CompletionResultType]::ParameterName, 'Enable given profile(s), does not toggle')
115+
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Disable given profile(s), does not toggle')
116+
[CompletionResult]::new('--disable', '--disable', [CompletionResultType]::ParameterName, 'Disable given profile(s), does not toggle')
113117
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'Reverse the processing order (first listed = highest priority)')
114118
[CompletionResult]::new('--inverse', '--inverse', [CompletionResultType]::ParameterName, 'Reverse the processing order (first listed = highest priority)')
115119
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help')
@@ -178,6 +182,10 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock {
178182
'am;use' {
179183
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Activate at specific priority position (1-based). Repositions if already active')
180184
[CompletionResult]::new('--priority', '--priority', [CompletionResultType]::ParameterName, 'Activate at specific priority position (1-based). Repositions if already active')
185+
[CompletionResult]::new('-e', '-e', [CompletionResultType]::ParameterName, 'Enable given profile(s), does not toggle')
186+
[CompletionResult]::new('--enable', '--enable', [CompletionResultType]::ParameterName, 'Enable given profile(s), does not toggle')
187+
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Disable given profile(s), does not toggle')
188+
[CompletionResult]::new('--disable', '--disable', [CompletionResultType]::ParameterName, 'Disable given profile(s), does not toggle')
181189
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'Reverse the processing order (first listed = highest priority)')
182190
[CompletionResult]::new('--inverse', '--inverse', [CompletionResultType]::ParameterName, 'Reverse the processing order (first listed = highest priority)')
183191
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help')

completions/zsh/_am

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ _arguments "${_arguments_options[@]}" : \
109109
_arguments "${_arguments_options[@]}" : \
110110
'(-i --inverse)-n+[Activate at specific priority position (1-based). Repositions if already active]:PRIORITY:_default' \
111111
'(-i --inverse)--priority=[Activate at specific priority position (1-based). Repositions if already active]:PRIORITY:_default' \
112+
'(-d --disable)-e[Enable given profile(s), does not toggle]' \
113+
'(-d --disable)--enable[Enable given profile(s), does not toggle]' \
114+
'(-e --enable)-d[Disable given profile(s), does not toggle]' \
115+
'(-e --enable)--disable[Disable given profile(s), does not toggle]' \
112116
'(-n --priority)-i[Reverse the processing order (first listed = highest priority)]' \
113117
'(-n --priority)--inverse[Reverse the processing order (first listed = highest priority)]' \
114118
'-h[Print help]' \
@@ -203,6 +207,10 @@ _arguments "${_arguments_options[@]}" : \
203207
_arguments "${_arguments_options[@]}" : \
204208
'(-i --inverse)-n+[Activate at specific priority position (1-based). Repositions if already active]:PRIORITY:_default' \
205209
'(-i --inverse)--priority=[Activate at specific priority position (1-based). Repositions if already active]:PRIORITY:_default' \
210+
'(-d --disable)-e[Enable given profile(s), does not toggle]' \
211+
'(-d --disable)--enable[Enable given profile(s), does not toggle]' \
212+
'(-e --enable)-d[Disable given profile(s), does not toggle]' \
213+
'(-e --enable)--disable[Disable given profile(s), does not toggle]' \
206214
'(-n --priority)-i[Reverse the processing order (first listed = highest priority)]' \
207215
'(-n --priority)--inverse[Reverse the processing order (first listed = highest priority)]' \
208216
'-h[Print help]' \

crates/am/src/bin/am.rs

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,22 @@ fn main() -> anyhow::Result<()> {
160160
println!("{}", amoxide::status::run_status());
161161
return Ok(());
162162
}
163-
Commands::Use {
164-
names,
163+
Commands::Use(ProfileUse {
164+
enable,
165+
disable,
165166
priority,
166167
inverse,
168+
names,
169+
})
170+
| Commands::Profile {
171+
action:
172+
Some(ProfileAction::Use(ProfileUse {
173+
enable,
174+
disable,
175+
priority,
176+
inverse,
177+
names,
178+
})),
167179
} => {
168180
let ordered: Vec<String> = if *inverse {
169181
names.iter().rev().cloned().collect()
@@ -172,7 +184,15 @@ fn main() -> anyhow::Result<()> {
172184
};
173185
let msg = match priority {
174186
Some(n) => Message::UseProfilesAt(ordered, *n),
175-
None => Message::ToggleProfiles(ordered),
187+
None => {
188+
if *enable {
189+
Message::EnableProfiles(ordered)
190+
} else if *disable {
191+
Message::DeactivateProfiles(ordered)
192+
} else {
193+
Message::ToggleProfiles(ordered)
194+
}
195+
}
176196
};
177197
let result = update(&mut model, msg)?;
178198
execute_effects(&mut model, result.effects)?;
@@ -189,25 +209,6 @@ fn main() -> anyhow::Result<()> {
189209
model.save_config()?;
190210
return Ok(());
191211
}
192-
ProfileAction::Use {
193-
names,
194-
priority,
195-
inverse,
196-
} => {
197-
let ordered: Vec<String> = if *inverse {
198-
names.iter().rev().cloned().collect()
199-
} else {
200-
names.clone()
201-
};
202-
let msg = match priority {
203-
Some(n) => Message::UseProfilesAt(ordered, *n),
204-
None => Message::ToggleProfiles(ordered),
205-
};
206-
let result = update(&mut model, msg)?;
207-
execute_effects(&mut model, result.effects)?;
208-
model.save_config()?;
209-
return Ok(());
210-
}
211212
ProfileAction::Remove { name, force } => {
212213
if !force {
213214
let profile = model
@@ -246,6 +247,7 @@ fn main() -> anyhow::Result<()> {
246247
return Ok(());
247248
}
248249
ProfileAction::List { used } => Message::ListProfiles { used: *used },
250+
ProfileAction::Use(_) => unreachable!("handled by outer match arm"),
249251
},
250252
Commands::Setup { shell } => {
251253
amoxide::setup::run_setup(shell)?;

crates/am/src/cli.rs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,7 @@ pub enum Commands {
9191

9292
/// Shortcut for `am profile use` — toggle one or more profiles
9393
#[command(alias = "u")]
94-
Use {
95-
/// Profile names
96-
names: Vec<String>,
97-
/// Activate at specific priority position (1-based). Repositions if already active.
98-
#[arg(short = 'n', long = "priority", conflicts_with = "inverse")]
99-
priority: Option<usize>,
100-
/// Reverse the processing order (first listed = highest priority)
101-
#[arg(short, long, conflicts_with = "priority")]
102-
inverse: bool,
103-
},
94+
Use(ProfileUse),
10495

10596
/// Launch the interactive TUI for managing aliases and profiles
10697
#[command(alias = "t")]
@@ -139,6 +130,24 @@ pub enum Commands {
139130
},
140131
}
141132

133+
#[derive(Args)]
134+
pub struct ProfileUse {
135+
/// Enable given profile(s), does not toggle
136+
#[arg(short = 'e', long = "enable", conflicts_with = "disable")]
137+
pub enable: bool,
138+
/// Disable given profile(s), does not toggle
139+
#[arg(short = 'd', long = "disable", conflicts_with = "enable")]
140+
pub disable: bool,
141+
/// Activate at specific priority position (1-based). Repositions if already active.
142+
#[arg(short = 'n', long = "priority", conflicts_with = "inverse")]
143+
pub priority: Option<usize>,
144+
/// Reverse the processing order (first listed = highest priority)
145+
#[arg(short, long, conflicts_with = "priority")]
146+
pub inverse: bool,
147+
/// Profile names
148+
pub names: Vec<String>,
149+
}
150+
142151
#[derive(Subcommand)]
143152
pub enum ProfileAction {
144153
/// Add a new profile
@@ -150,16 +159,7 @@ pub enum ProfileAction {
150159

151160
/// Toggle one or more profiles as active/inactive, optionally at a specific priority
152161
#[command(alias = "u")]
153-
Use {
154-
/// Profile names
155-
names: Vec<String>,
156-
/// Activate at specific priority position (1-based). Repositions if already active.
157-
#[arg(short = 'n', long = "priority", conflicts_with = "inverse")]
158-
priority: Option<usize>,
159-
/// Reverse the processing order (first listed = highest priority)
160-
#[arg(short, long, conflicts_with = "priority")]
161-
inverse: bool,
162-
},
162+
Use(ProfileUse),
163163

164164
/// Remove a profile
165165
#[command(alias = "r")]

crates/am/src/messages.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ pub enum Message {
3838
Sync(Shell, bool),
3939

4040
ToggleProfiles(Vec<String>),
41+
EnableProfiles(Vec<String>),
42+
DeactivateProfiles(Vec<String>),
4143
UseProfilesAt(Vec<String>, usize),
4244
RemoveProfile(String),
4345
ListProfiles {

crates/am/src/update.rs

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -755,8 +755,11 @@ pub fn update(model: &mut AppModel, message: Message) -> Result<UpdateResult, Up
755755
Ok(UpdateResult::effect(Effect::RenderSync(outcome)))
756756
}
757757
}
758-
Message::ToggleProfiles(names) => {
759-
for name in &names {
758+
Message::ToggleProfiles(ref names)
759+
| Message::EnableProfiles(ref names)
760+
| Message::DeactivateProfiles(ref names) => {
761+
// ensues they all exist, so all or nothing
762+
for name in names {
760763
model
761764
.profile_config()
762765
.get_profile_by_name(name)
@@ -765,15 +768,33 @@ pub fn update(model: &mut AppModel, message: Message) -> Result<UpdateResult, Up
765768
let project_names = project_alias_names(model);
766769
let mut effects = Vec::new();
767770
for name in names {
768-
let was_active = model.session.is_active(&name);
771+
let is_active = model.session.is_active(name);
769772
let items = model
770773
.profile_config()
771-
.get_profile_by_name(&name)
774+
.get_profile_by_name(name)
772775
.map(profile_items)
773776
.unwrap_or_default();
774-
model.session.toggle_profile(name.clone());
775-
let msg = profile_toggle_message(&name, !was_active, None, &items, &project_names);
776-
effects.push(Effect::Print(msg));
777+
if matches!(message, Message::EnableProfiles(_)) && is_active {
778+
effects.push(Effect::Print(format!(
779+
"am: profile '{name}' is already active"
780+
)));
781+
continue;
782+
} else if matches!(message, Message::DeactivateProfiles(_)) && !is_active {
783+
effects.push(Effect::Print(format!(
784+
"am: profile '{name}' was already deactivated"
785+
)));
786+
continue;
787+
} else {
788+
// in all other cases, toggle is what we want
789+
model.session.toggle_profile(name.clone());
790+
effects.push(Effect::Print(profile_toggle_message(
791+
name,
792+
!is_active,
793+
None,
794+
&items,
795+
&project_names,
796+
)));
797+
}
777798
}
778799
effects.push(Effect::SaveSession);
779800
Ok(UpdateResult::with_effects(effects))
@@ -1456,6 +1477,77 @@ mod tests {
14561477
assert!(model.session.active_profiles.is_empty());
14571478
}
14581479

1480+
#[test]
1481+
fn enable_profile_activates_inactive_profile() {
1482+
let config = Config::default();
1483+
let profile_config: ProfileConfig =
1484+
toml::from_str("[[profiles]]\nname = \"git\"\n").unwrap();
1485+
let mut model = AppModel::new(config, profile_config);
1486+
1487+
let result = update(&mut model, Message::EnableProfiles(vec!["git".into()])).unwrap();
1488+
1489+
assert!(model.session.active_profiles.contains(&"git".to_string()));
1490+
assert!(
1491+
matches!(&result.effects[0], Effect::Print(s) if s.contains("git") && s.contains("activated"))
1492+
);
1493+
}
1494+
1495+
#[test]
1496+
fn enable_profile_noop_when_already_active() {
1497+
let config = Config::default();
1498+
let profile_config: ProfileConfig =
1499+
toml::from_str("[[profiles]]\nname = \"git\"\n").unwrap();
1500+
let mut model = AppModel::new(config, profile_config);
1501+
model.session.active_profiles = vec!["git".to_string()];
1502+
1503+
let result = update(&mut model, Message::EnableProfiles(vec!["git".into()])).unwrap();
1504+
1505+
assert_eq!(model.session.active_profiles, vec!["git".to_string()]);
1506+
assert!(matches!(&result.effects[0], Effect::Print(s) if s.contains("already active")));
1507+
}
1508+
1509+
#[test]
1510+
fn deactivate_profile_removes_active_profile() {
1511+
let config = Config::default();
1512+
let profile_config: ProfileConfig =
1513+
toml::from_str("[[profiles]]\nname = \"git\"\n").unwrap();
1514+
let mut model = AppModel::new(config, profile_config);
1515+
model.session.active_profiles = vec!["git".to_string()];
1516+
1517+
let result = update(&mut model, Message::DeactivateProfiles(vec!["git".into()])).unwrap();
1518+
1519+
assert!(model.session.active_profiles.is_empty());
1520+
assert!(
1521+
matches!(&result.effects[0], Effect::Print(s) if s.contains("git") && s.contains("deactivated"))
1522+
);
1523+
}
1524+
1525+
#[test]
1526+
fn deactivate_profile_noop_when_already_inactive() {
1527+
let config = Config::default();
1528+
let profile_config: ProfileConfig =
1529+
toml::from_str("[[profiles]]\nname = \"git\"\n").unwrap();
1530+
let mut model = AppModel::new(config, profile_config);
1531+
1532+
let result = update(&mut model, Message::DeactivateProfiles(vec!["git".into()])).unwrap();
1533+
1534+
assert!(model.session.active_profiles.is_empty());
1535+
assert!(
1536+
matches!(&result.effects[0], Effect::Print(s) if s.contains("already deactivated"))
1537+
);
1538+
}
1539+
1540+
#[test]
1541+
fn enable_profile_fails_if_missing() {
1542+
let config = Config::default();
1543+
let profile_config = ProfileConfig::default();
1544+
let mut model = AppModel::new(config, profile_config);
1545+
1546+
let result = update(&mut model, Message::EnableProfiles(vec!["nope".into()]));
1547+
1548+
assert!(matches!(result, Err(UpdateError::ProfileNotFound { name }) if name == "nope"));
1549+
}
1550+
14591551
#[test]
14601552
fn use_profiles_at_inserts_sequentially() {
14611553
let config = Config::default();

crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ _am() {
753753
return 0
754754
;;
755755
am__subcmd__profile__subcmd__use)
756-
opts="-n -i -h -V --priority --inverse --help --version [NAMES]..."
756+
opts="-e -d -n -i -h -V --enable --disable --priority --inverse --help --version [NAMES]..."
757757
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
758758
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
759759
return 0
@@ -907,7 +907,7 @@ _am() {
907907
return 0
908908
;;
909909
am__subcmd__use)
910-
opts="-n -i -h -V --priority --inverse --help --version [NAMES]..."
910+
opts="-e -d -n -i -h -V --enable --disable --priority --inverse --help --version [NAMES]..."
911911
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
912912
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
913913
return 0

0 commit comments

Comments
 (0)