Skip to main content

purple_ssh/app/
pickers.rs

1//! Overlay picker lifecycle. Implements `impl App` continuation with
2//! open/close domain actions for every `.open`-flag-based picker overlay
3//! (password, key, proxyjump, vault_role, region). Screen-based pickers
4//! (TagPicker, ThemePicker, SnippetPicker, etc.) live in `selection.rs`.
5
6use ratatui::widgets::ListState;
7
8use crate::app::App;
9use crate::app::ui_state::{AwsProfileNote, AwsProfileRow};
10
11impl App {
12    /// Close the password picker overlay.
13    pub fn close_password_picker(&mut self) {
14        log::debug!("[purple] close_password_picker");
15        self.ui.password_picker.open = false;
16    }
17
18    /// Close the key picker overlay.
19    pub fn close_key_picker(&mut self) {
20        log::debug!("[purple] close_key_picker");
21        self.ui.key_picker.open = false;
22    }
23
24    /// Close the ProxyJump picker overlay.
25    pub fn close_proxyjump_picker(&mut self) {
26        log::debug!("[purple] close_proxyjump_picker");
27        self.ui.proxyjump_picker.open = false;
28    }
29
30    /// Close the Vault SSH role picker overlay.
31    pub fn close_vault_role_picker(&mut self) {
32        log::debug!("[purple] close_vault_role_picker");
33        self.ui.vault_role_picker.open = false;
34    }
35
36    /// Close the provider region picker overlay.
37    pub fn close_region_picker(&mut self) {
38        log::debug!("[purple] close_region_picker");
39        self.ui.region_picker.open = false;
40    }
41
42    /// Open the password picker overlay focused on the first source.
43    pub fn open_password_picker(&mut self) {
44        log::debug!("[purple] open_password_picker");
45        self.ui.password_picker.open = true;
46        self.ui.password_picker.list = ListState::default();
47        self.ui.password_picker.list.select(Some(0));
48    }
49
50    /// The AWS profile picker's rows: every profile found in `~/.aws/config`
51    /// and `~/.aws/credentials`, sorted, each with what purple will do with
52    /// it. A profile with nothing worth saying carries no note.
53    ///
54    /// Resolving the chain per profile is what makes the refusals visible. The
55    /// shape that decides whether purple can use a profile lives in the file
56    /// rather than in the name, so otherwise it only surfaces on the next
57    /// sync.
58    fn aws_profile_rows(&self) -> Vec<AwsProfileRow> {
59        let env = self.env();
60        let profiles = crate::providers::aws_profile::AwsProfiles::load(
61            env.aws_config_file().as_deref(),
62            env.aws_credentials_file().as_deref(),
63        );
64        profiles
65            .names()
66            .iter()
67            .map(|name| {
68                let note = match profiles.resolve_chain(name) {
69                    Ok(chain) if !chain.roles.is_empty() => Some(AwsProfileNote {
70                        text: crate::messages::PROFILE_ASSUMES_ROLE,
71                        usable: true,
72                    }),
73                    Ok(_) => None,
74                    Err(e) => Some(AwsProfileNote {
75                        text: crate::providers::aws::chain_error_note(&e),
76                        usable: false,
77                    }),
78                };
79                AwsProfileRow {
80                    name: (*name).to_string(),
81                    note,
82                    region: profiles
83                        .get(name)
84                        .map(|p| p.region.trim().to_string())
85                        .unwrap_or_default(),
86                }
87            })
88            .collect()
89    }
90
91    /// Open the AWS profile picker overlay, reading `~/.aws` once. Returns
92    /// false without opening when there is nothing to pick, so the caller can
93    /// say why instead of showing an empty list.
94    pub fn open_profile_picker(&mut self) -> bool {
95        let rows = self.aws_profile_rows();
96        if rows.is_empty() {
97            return false;
98        }
99        log::debug!("[purple] open_profile_picker: {} profiles", rows.len());
100        self.ui.aws_profile_rows = rows;
101        self.ui.profile_picker.open = true;
102        self.ui.profile_picker.list = ListState::default();
103        self.ui.profile_picker.list.select(Some(0));
104        true
105    }
106
107    /// Open the key picker overlay. Rescans `~/.ssh` first so the list
108    /// reflects keys added since the form was opened, then selects the
109    /// first key when at least one was discovered.
110    pub fn open_key_picker(&mut self) {
111        log::debug!("[purple] open_key_picker");
112        self.scan_keys();
113        self.ui.key_picker.open = true;
114        self.ui.key_picker.list = ListState::default();
115        if !self.keys.list.is_empty() {
116            self.ui.key_picker.list.select(Some(0));
117        }
118    }
119
120    /// Open the ProxyJump picker overlay. The opening cursor lands on the
121    /// first host row rather than the first list entry, so separator/header
122    /// rows above the host list do not steal initial focus.
123    pub fn open_proxyjump_picker(&mut self) {
124        log::debug!("[purple] open_proxyjump_picker");
125        self.ui.proxyjump_picker.open = true;
126        self.ui.proxyjump_picker.list = ListState::default();
127        if let Some(idx) = self.proxyjump_first_host_index() {
128            self.ui.proxyjump_picker.list.select(Some(idx));
129        }
130    }
131
132    /// Open the Vault SSH role picker. The caller is responsible for
133    /// guarding against an empty candidate list; this method assumes at
134    /// least one role and selects the first.
135    pub fn open_vault_role_picker(&mut self) {
136        log::debug!("[purple] open_vault_role_picker");
137        self.ui.vault_role_picker.open = true;
138        self.ui.vault_role_picker.list = ListState::default();
139        self.ui.vault_role_picker.list.select(Some(0));
140    }
141
142    /// Open the provider region picker overlay with the cursor on the
143    /// first row. Region picker uses a `cursor: usize` rather than a
144    /// ratatui `ListState` because its rows are a synthetic flat array.
145    pub fn open_region_picker(&mut self) {
146        log::debug!("[purple] open_region_picker");
147        self.ui.region_picker.open = true;
148        self.ui.region_picker.cursor = 0;
149    }
150}