Skip to main content

max / makenotwork

6.3 KB · 205 lines History Blame Raw
1 //! Settings screen — profile info, SSH keys, storage meter.
2
3 use ratatui::Frame;
4 use ratatui::layout::{Constraint, Layout};
5 use ratatui::style::{Color, Modifier, Style};
6 use ratatui::text::{Line, Span};
7 use ratatui::widgets::{Block, Borders, Gauge, Paragraph, Row};
8
9 use crate::format;
10 use crate::staging;
11
12 use super::App;
13 use super::widgets;
14
15 pub fn render(frame: &mut Frame, app: &App) {
16 let area = frame.area();
17
18 let title = Line::from(vec![
19 Span::styled(" Makenot.work ", Style::default().add_modifier(Modifier::BOLD)),
20 Span::raw(" -- "),
21 Span::styled("Settings", Style::default().add_modifier(Modifier::BOLD)),
22 Span::raw(" "),
23 ]);
24
25 let block = Block::default()
26 .title(title)
27 .borders(Borders::ALL)
28 .border_style(Style::default().fg(Color::Gray));
29
30 let inner = block.inner(area);
31 frame.render_widget(block, area);
32
33 let chunks = Layout::vertical([
34 Constraint::Length(1), // spacer
35 Constraint::Length(1), // profile header
36 Constraint::Length(3), // profile info
37 Constraint::Length(1), // spacer
38 Constraint::Length(1), // storage header
39 Constraint::Length(1), // storage bar
40 Constraint::Length(1), // spacer
41 Constraint::Length(1), // SSH keys header
42 Constraint::Min(3), // SSH keys table
43 Constraint::Length(1), // status
44 Constraint::Length(1), // keybindings
45 ])
46 .split(inner);
47
48 // Profile header
49 let profile_header = Paragraph::new(Line::from(vec![
50 Span::raw(" "),
51 Span::styled("Profile", Style::default().add_modifier(Modifier::BOLD)),
52 ]));
53 frame.render_widget(profile_header, chunks[1]);
54
55 // Profile info
56 let tier = app
57 .user
58 .creator_tier
59 .as_deref()
60 .map(format::format_tier)
61 .unwrap_or("No tier");
62
63 let display_name = app
64 .user
65 .display_name
66 .as_deref()
67 .unwrap_or("(not set)");
68
69 let profile_lines = vec![
70 Line::from(vec![
71 Span::raw(" Username: "),
72 Span::styled(
73 &app.user.username,
74 Style::default().add_modifier(Modifier::BOLD),
75 ),
76 ]),
77 Line::from(vec![
78 Span::raw(" Display: "),
79 Span::raw(display_name),
80 ]),
81 Line::from(vec![
82 Span::raw(" Tier: "),
83 Span::styled(tier, Style::default().fg(Color::Cyan)),
84 ]),
85 ];
86 let profile = Paragraph::new(profile_lines);
87 frame.render_widget(profile, chunks[2]);
88
89 // Storage header
90 let storage_header = Paragraph::new(Line::from(vec![
91 Span::raw(" "),
92 Span::styled("Storage", Style::default().add_modifier(Modifier::BOLD)),
93 ]));
94 frame.render_widget(storage_header, chunks[4]);
95
96 // Storage gauge
97 if let Some(ref info) = app.storage_info {
98 let pct = if info.max_storage_bytes > 0 {
99 (info.storage_used_bytes as f64 / info.max_storage_bytes as f64 * 100.0) as u16
100 } else {
101 0
102 };
103 let label = format!(
104 " {} / {} ({}%)",
105 staging::format_bytes(info.storage_used_bytes as u64),
106 staging::format_bytes(info.max_storage_bytes as u64),
107 pct,
108 );
109
110 let color = if pct > 90 {
111 Color::Red
112 } else if pct > 70 {
113 Color::Yellow
114 } else {
115 Color::Green
116 };
117
118 let gauge = Gauge::default()
119 .gauge_style(Style::default().fg(color))
120 .label(Span::raw(label))
121 .ratio(pct as f64 / 100.0);
122 frame.render_widget(gauge, chunks[5]);
123 } else {
124 let no_storage = Paragraph::new(" No storage data available.");
125 frame.render_widget(no_storage, chunks[5]);
126 }
127
128 // SSH keys header
129 let count = app.ssh_keys.len();
130 let keys_header = Paragraph::new(Line::from(vec![
131 Span::raw(" "),
132 Span::styled("SSH Keys", Style::default().add_modifier(Modifier::BOLD)),
133 if count == 0 {
134 Span::raw("")
135 } else {
136 Span::raw(format!(" ({})", count))
137 },
138 ]));
139 frame.render_widget(keys_header, chunks[7]);
140
141 // SSH keys table
142 if app.loading {
143 let loading = Paragraph::new(" Loading...");
144 frame.render_widget(loading, chunks[8]);
145 } else if app.ssh_keys.is_empty() {
146 let empty = Paragraph::new(" No SSH keys registered.");
147 frame.render_widget(empty, chunks[8]);
148 } else {
149 let rows: Vec<Row> = app
150 .ssh_keys
151 .iter()
152 .enumerate()
153 .map(|(i, key)| {
154 let fp = key.fingerprint.get(..20).unwrap_or(&key.fingerprint);
155 let date = key.created_at.get(..10).unwrap_or(&key.created_at);
156
157 Row::new(vec![
158 format!(" {}", key.label),
159 format!("{}...", fp),
160 date.to_string(),
161 ])
162 .style(widgets::selected_style(i, Some(app.selected_index)))
163 })
164 .collect();
165
166 let widths = [
167 Constraint::Min(20),
168 Constraint::Length(24),
169 Constraint::Length(12),
170 ];
171
172 widgets::render_table(frame, chunks[8], &[" Label", "Fingerprint", "Added"], &widths, rows);
173 }
174
175 // Status line
176 if let Some(ref status) = app.settings_status {
177 let style = if status.starts_with("Error") {
178 Style::default().fg(Color::Red)
179 } else {
180 Style::default().fg(Color::Green)
181 };
182 let status_line = Paragraph::new(Line::from(vec![
183 Span::raw(" "),
184 Span::styled(status.as_str(), style),
185 ]));
186 frame.render_widget(status_line, chunks[9]);
187 }
188
189 // Keybindings
190 let key_spans = vec![
191 Span::raw(" "),
192 Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)),
193 Span::raw(" Nav "),
194 Span::styled("[r]", Style::default().add_modifier(Modifier::BOLD)),
195 Span::raw(" Refresh "),
196 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
197 Span::raw(" Back"),
198 ];
199
200 let keys_bar =
201 Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray));
202 frame.render_widget(keys_bar, chunks[10]);
203 }
204
205