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