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