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