1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! Navigation / view-state commands — `:region`, `:profile`,
//! `:account`, `:sort`, `:group`, `:redact`. These manipulate App's
//! navigation state (active region, profile, sort key) and either
//! trigger a `spawn_refresh` / `spawn_rebuild` (region / profile /
//! account) or just rebuild the in-memory view (sort / group).
//!
//! Sixth slice of the `execute_command` split. Same parent-module
//! visibility as the other `cmd_*` sub-modules.
use super::{parse_toggle, App, PickerKind, SortKey};
impl App {
/// `:region <name> | all | off | r <name>`.
/// - `all` → multi-region fan-out across `extra_regions ∪ current`.
/// - `off` / `single` → return to single-region mode.
/// - `<name>` → switch to that region (rebuilds the AWS client).
pub(crate) fn cmd_region(&mut self, rest: &[&str]) {
match rest.first().copied() {
Some("all") => {
let mut regions = self.extra_regions.clone();
if !regions.iter().any(|r| r == &self.context.region) {
regions.push(self.context.region.clone());
}
if regions.is_empty() {
self.error_message =
Some("no regions configured — set extra_regions in config.toml".into());
return;
}
regions.sort();
regions.dedup();
self.multi_regions = regions.clone();
self.status_message = Some(format!(
"multi-region: fanning across {} regions ({})",
regions.len(),
regions.join(", ")
));
self.spawn_refresh();
}
Some("off") | Some("single") => {
self.multi_regions.clear();
self.status_message = Some("multi-region off".into());
self.spawn_refresh();
}
Some(r) => self.apply_picker_choice(PickerKind::Region, r.to_string()),
None => self.error_message = Some("usage: :region <name> | all | off".into()),
}
}
/// `:account NAME`. Two paths:
/// 1. `accounts.NAME.role_arn = …` configured in config.toml →
/// AssumeRole flow via `spawn_assume_role_switch`.
/// 2. Otherwise legacy aliasing to `:profile NAME` (operators
/// who use one profile per account via the `role_arn` chain
/// in `~/.aws/config`).
pub(crate) fn cmd_account(&mut self, rest: &[&str]) {
match rest.first() {
Some(name) => {
let name = (*name).to_string();
if self.accounts.contains_key(&name) {
self.spawn_assume_role_switch(name);
} else {
self.apply_picker_choice(PickerKind::Profile, name);
}
}
None => {
self.error_message = Some(
"usage: :account <name> (resolves accounts.NAME from config.toml, falls back to :profile)"
.into(),
);
}
}
}
pub(crate) fn cmd_profile(&mut self, rest: &[&str]) {
match rest.first() {
Some(p) => {
// Validate against the parsed profile list before kicking
// a rebuild. A typo like `:profile prod-readonl` (missing
// y) would otherwise reach the SDK and surface as a
// flattened error from somewhere deep — far less
// actionable than naming the profile that doesn't exist
// and pointing at the picker.
let known = crate::profiles::load_profiles();
if !known.iter().any(|known| known == *p) {
self.error_message = Some(format!(
"profile '{p}' not in ~/.aws/{{config,credentials}} — press `p` to pick from the list, or add it with `aws configure --profile {p}`"
));
return;
}
self.apply_picker_choice(PickerKind::Profile, (*p).to_string());
}
None => self.error_message = Some("usage: :profile <name>".into()),
}
}
/// `:sort <key> [asc|desc]` — keys: name app status health version age.
pub(crate) fn cmd_sort(&mut self, rest: &[&str]) {
let Some(key) = rest.first() else {
self.error_message = Some(
"usage: :sort <key> [asc|desc] — keys: name app status health version age".into(),
);
return;
};
match SortKey::parse(key) {
Some(k) => {
self.sort_key = k;
self.sort_desc = matches!(rest.get(1), Some(&"desc"));
self.resort_envs();
self.status_message = Some(format!(
"sort: {} ({})",
self.sort_key.label(),
if self.sort_desc { "desc" } else { "asc" }
));
}
None => self.error_message = Some(format!("unknown sort key: {key}")),
}
}
pub(crate) fn cmd_group(&mut self, rest: &[&str]) {
self.grouped = parse_toggle(rest.first().copied(), self.grouped);
self.rebuild_view();
self.status_message = Some(if self.grouped {
"grouped by application".into()
} else {
"ungrouped".into()
});
}
pub(crate) fn cmd_redact(&mut self, rest: &[&str]) {
self.redact = parse_toggle(rest.first().copied(), self.redact);
self.status_message = Some(if self.redact {
"redact mode ON".into()
} else {
"redact mode off".into()
});
}
}