rusticity_term/aws/
profile.rs1use crate::common::SortDirection;
2use crate::ui::active_border;
3use crate::ui::table::{render_table, Column as TableColumn, TableConfig};
4use ratatui::{prelude::*, widgets::*};
5
6pub struct Profile {
7 pub name: String,
8 pub region: Option<String>,
9 pub account: Option<String>,
10 pub role_arn: Option<String>,
11 pub source_profile: Option<String>,
12}
13
14impl Profile {
15 pub fn load_all() -> Vec<Self> {
16 let mut profiles = Vec::new();
17 let home = std::env::var("HOME").unwrap_or_default();
18 let config_path = format!("{}/.aws/config", home);
19 let credentials_path = format!("{}/.aws/credentials", home);
20
21 if let Ok(content) = std::fs::read_to_string(&config_path) {
23 let mut current_profile: Option<String> = None;
24 let mut current_region: Option<String> = None;
25 let mut current_role: Option<String> = None;
26 let mut current_source: Option<String> = None;
27
28 for line in content.lines() {
29 let line = line.trim();
30 if line.starts_with('[') && line.ends_with(']') {
31 if let Some(name) = current_profile.take() {
32 profiles.push(Profile {
33 name,
34 region: current_region.take(),
35 account: None,
36 role_arn: current_role.take(),
37 source_profile: current_source.take(),
38 });
39 }
40 let profile_name = line
41 .trim_start_matches('[')
42 .trim_end_matches(']')
43 .trim_start_matches("profile ")
44 .to_string();
45 current_profile = Some(profile_name);
46 } else if let Some(key_value) = line.split_once('=') {
47 let key = key_value.0.trim();
48 let value = key_value.1.trim().to_string();
49 match key {
50 "region" => current_region = Some(value),
51 "role_arn" => current_role = Some(value),
52 "source_profile" => current_source = Some(value),
53 _ => {}
54 }
55 }
56 }
57 if let Some(name) = current_profile {
58 profiles.push(Profile {
59 name,
60 region: current_region,
61 account: None,
62 role_arn: current_role,
63 source_profile: current_source,
64 });
65 }
66 }
67
68 if let Ok(content) = std::fs::read_to_string(&credentials_path) {
70 for line in content.lines() {
71 let line = line.trim();
72 if line.starts_with('[') && line.ends_with(']') {
73 let profile_name = line
74 .trim_start_matches('[')
75 .trim_end_matches(']')
76 .to_string();
77 if !profiles.iter().any(|p| p.name == profile_name) {
78 profiles.push(Profile {
79 name: profile_name,
80 region: None,
81 account: None,
82 role_arn: None,
83 source_profile: None,
84 });
85 }
86 }
87 }
88 }
89
90 profiles
91 }
92}
93
94pub fn render_profile_picker(
95 frame: &mut ratatui::Frame,
96 app: &crate::app::App,
97 area: ratatui::prelude::Rect,
98 centered_rect: fn(u16, u16, ratatui::prelude::Rect) -> ratatui::prelude::Rect,
99) {
100 let popup_area = centered_rect(80, 70, area);
101
102 let chunks = Layout::default()
103 .direction(Direction::Vertical)
104 .constraints([Constraint::Length(3), Constraint::Min(0)])
105 .split(popup_area);
106
107 let cursor = "█";
108 let filter = Paragraph::new(Line::from(vec![
109 Span::raw(&app.profile_filter),
110 Span::styled(cursor, Style::default().fg(Color::Green)),
111 ]))
112 .block(
113 Block::default()
114 .title(" 🔍 ")
115 .borders(Borders::ALL)
116 .border_style(active_border()),
117 )
118 .style(Style::default());
119
120 frame.render_widget(Clear, popup_area);
121 frame.render_widget(filter, chunks[0]);
122
123 struct ProfileNameColumn;
124 impl TableColumn<Profile> for ProfileNameColumn {
125 fn name(&self) -> &str {
126 "Profile"
127 }
128 fn width(&self) -> u16 {
129 25
130 }
131 fn render(&self, item: &Profile) -> (String, Style) {
132 (item.name.clone(), Style::default())
133 }
134 }
135
136 struct ProfileAccountColumn;
137 impl TableColumn<Profile> for ProfileAccountColumn {
138 fn name(&self) -> &str {
139 "Account"
140 }
141 fn width(&self) -> u16 {
142 15
143 }
144 fn render(&self, item: &Profile) -> (String, Style) {
145 (item.account.clone().unwrap_or_default(), Style::default())
146 }
147 }
148
149 struct ProfileRegionColumn;
150 impl TableColumn<Profile> for ProfileRegionColumn {
151 fn name(&self) -> &str {
152 "Region"
153 }
154 fn width(&self) -> u16 {
155 15
156 }
157 fn render(&self, item: &Profile) -> (String, Style) {
158 (item.region.clone().unwrap_or_default(), Style::default())
159 }
160 }
161
162 struct ProfileRoleColumn;
163 impl TableColumn<Profile> for ProfileRoleColumn {
164 fn name(&self) -> &str {
165 "Role/User"
166 }
167 fn width(&self) -> u16 {
168 30
169 }
170 fn render(&self, item: &Profile) -> (String, Style) {
171 if let Some(ref role) = item.role_arn {
172 if role.contains(":role/") {
173 let role_name = role.split('/').next_back().unwrap_or(role);
174 (format!("role/{}", role_name), Style::default())
175 } else if role.contains(":user/") {
176 let user_name = role.split('/').next_back().unwrap_or(role);
177 (format!("user/{}", user_name), Style::default())
178 } else {
179 (role.clone(), Style::default())
180 }
181 } else {
182 (String::new(), Style::default())
183 }
184 }
185 }
186
187 struct ProfileSourceColumn;
188 impl TableColumn<Profile> for ProfileSourceColumn {
189 fn name(&self) -> &str {
190 "Source"
191 }
192 fn width(&self) -> u16 {
193 20
194 }
195 fn render(&self, item: &Profile) -> (String, Style) {
196 (
197 item.source_profile.clone().unwrap_or_default(),
198 Style::default(),
199 )
200 }
201 }
202
203 let columns: Vec<Box<dyn TableColumn<Profile>>> = vec![
204 Box::new(ProfileNameColumn),
205 Box::new(ProfileAccountColumn),
206 Box::new(ProfileRegionColumn),
207 Box::new(ProfileRoleColumn),
208 Box::new(ProfileSourceColumn),
209 ];
210
211 let filtered = app.get_filtered_profiles();
212 let config = TableConfig {
213 items: filtered,
214 selected_index: app.profile_picker_selected,
215 expanded_index: None,
216 columns: &columns,
217 sort_column: "Profile",
218 sort_direction: SortDirection::Asc,
219 title: " Profiles (^R to fetch accounts) ".to_string(),
220 area: chunks[1],
221 get_expanded_content: None,
222 is_active: true,
223 };
224
225 render_table(frame, config);
226}
227
228pub fn filter_profiles<'a>(profiles: &'a [Profile], filter: &str) -> Vec<&'a Profile> {
229 let mut filtered: Vec<&Profile> = if filter.is_empty() {
230 profiles.iter().collect()
231 } else {
232 let filter_lower = filter.to_lowercase();
233 profiles
234 .iter()
235 .filter(|p| {
236 p.name.to_lowercase().contains(&filter_lower)
237 || p.region
238 .as_ref()
239 .is_some_and(|r| r.to_lowercase().contains(&filter_lower))
240 || p.account
241 .as_ref()
242 .is_some_and(|a| a.to_lowercase().contains(&filter_lower))
243 || p.role_arn
244 .as_ref()
245 .is_some_and(|r| r.to_lowercase().contains(&filter_lower))
246 || p.source_profile
247 .as_ref()
248 .is_some_and(|s| s.to_lowercase().contains(&filter_lower))
249 })
250 .collect()
251 };
252 filtered.sort_by(|a, b| a.name.cmp(&b.name));
253 filtered
254}