Skip to main content

max / makenotwork

7.3 KB · 228 lines History Blame Raw
1 //! Settings screen — profile info, SSH keys, storage meter.
2
3 use makeover_tui::makeover_layout::{Column, Priority, Width};
4 use makeover_tui::table::{self, Cell, Sizing, TableStyle};
5 use ratatui::Frame;
6 use ratatui::layout::{Constraint, Layout};
7 use ratatui::style::{Modifier, Style};
8 use ratatui::text::{Line, Span};
9 use ratatui::widgets::{Block, Borders, Gauge, Paragraph, TableState};
10
11 use crate::format;
12 use crate::staging;
13
14 use super::App;
15
16 /// The SSH key columns. The label is what a creator recognises a key by; the
17 /// fingerprint is what identifies it to the server, so it outranks the date.
18 const SSH_KEY_COLUMNS: [Column<'static>; 3] = [
19 Column {
20 name: "Label",
21 width: Width::Fill,
22 priority: Priority::Essential,
23 sortable: false,
24 sorted: None,
25 },
26 Column {
27 name: "Fingerprint",
28 width: Width::Fixed,
29 priority: Priority::Secondary,
30 sortable: false,
31 sorted: None,
32 },
33 Column {
34 name: "Added",
35 width: Width::Fixed,
36 priority: Priority::Optional,
37 sortable: false,
38 sorted: None,
39 },
40 ];
41
42 /// The tracks the hand-written `Constraint`s carried.
43 const SSH_KEY_SIZING: Sizing<'static> = Sizing {
44 lengths: &[("Label", 20), ("Fingerprint", 24), ("Added", 12)],
45 fallback: 12,
46 };
47
48 pub(crate) fn render(frame: &mut Frame, app: &App) {
49 let area = frame.area();
50
51 let title = Line::from(vec![
52 Span::styled(
53 " Makenot.work ",
54 Style::default().add_modifier(Modifier::BOLD),
55 ),
56 Span::raw(" -- "),
57 Span::styled("Settings", Style::default().add_modifier(Modifier::BOLD)),
58 Span::raw(" "),
59 ]);
60
61 let block = Block::default()
62 .title(title)
63 .borders(Borders::ALL)
64 .border_style(Style::default().fg(app.theme.line_border));
65
66 let inner = block.inner(area);
67 frame.render_widget(block, area);
68
69 let chunks = Layout::vertical([
70 Constraint::Length(1), // spacer
71 Constraint::Length(1), // profile header
72 Constraint::Length(3), // profile info
73 Constraint::Length(1), // spacer
74 Constraint::Length(1), // storage header
75 Constraint::Length(1), // storage bar
76 Constraint::Length(1), // spacer
77 Constraint::Length(1), // SSH keys header
78 Constraint::Min(3), // SSH keys table
79 Constraint::Length(1), // status
80 Constraint::Length(1), // keybindings
81 ])
82 .split(inner);
83
84 // Profile header
85 let profile_header = Paragraph::new(Line::from(vec![
86 Span::raw(" "),
87 Span::styled("Profile", Style::default().add_modifier(Modifier::BOLD)),
88 ]));
89 frame.render_widget(profile_header, chunks[1]);
90
91 // Profile info
92 let tier = app
93 .user
94 .creator_tier
95 .as_deref()
96 .map_or("No tier", format::format_tier);
97
98 let display_name = app.user.display_name.as_deref().unwrap_or("(not set)");
99
100 let profile_lines = vec![
101 Line::from(vec![
102 Span::raw(" Username: "),
103 Span::styled(
104 &app.user.username,
105 Style::default().add_modifier(Modifier::BOLD),
106 ),
107 ]),
108 Line::from(vec![Span::raw(" Display: "), Span::raw(display_name)]),
109 Line::from(vec![
110 Span::raw(" Tier: "),
111 Span::styled(tier, Style::default().fg(app.theme.action_primary)),
112 ]),
113 ];
114 let profile = Paragraph::new(profile_lines);
115 frame.render_widget(profile, chunks[2]);
116
117 // Storage header
118 let storage_header = Paragraph::new(Line::from(vec![
119 Span::raw(" "),
120 Span::styled("Storage", Style::default().add_modifier(Modifier::BOLD)),
121 ]));
122 frame.render_widget(storage_header, chunks[4]);
123
124 // Storage gauge
125 if let Some(ref info) = app.storage_info {
126 let pct = if info.max_storage_bytes > 0 {
127 (info.storage_used_bytes as f64 / info.max_storage_bytes as f64 * 100.0) as u16
128 } else {
129 0
130 };
131 let label = format!(
132 " {} / {} ({}%)",
133 staging::format_bytes(info.storage_used_bytes as u64),
134 staging::format_bytes(info.max_storage_bytes as u64),
135 pct,
136 );
137
138 let color = if pct > 90 {
139 app.theme.status_danger
140 } else if pct > 70 {
141 app.theme.status_warning
142 } else {
143 app.theme.status_success
144 };
145
146 let gauge = Gauge::default()
147 .gauge_style(Style::default().fg(color))
148 .label(Span::raw(label))
149 .ratio(pct as f64 / 100.0);
150 frame.render_widget(gauge, chunks[5]);
151 } else {
152 let no_storage = Paragraph::new(" No storage data available.");
153 frame.render_widget(no_storage, chunks[5]);
154 }
155
156 // SSH keys header
157 let count = app.ssh_keys.len();
158 let keys_header = Paragraph::new(Line::from(vec![
159 Span::raw(" "),
160 Span::styled("SSH Keys", Style::default().add_modifier(Modifier::BOLD)),
161 if count == 0 {
162 Span::raw("")
163 } else {
164 Span::raw(format!(" ({count})"))
165 },
166 ]));
167 frame.render_widget(keys_header, chunks[7]);
168
169 // SSH keys table
170 if app.loading {
171 let loading = Paragraph::new(" Loading...");
172 frame.render_widget(loading, chunks[8]);
173 } else if app.ssh_keys.is_empty() {
174 let empty = Paragraph::new(" No SSH keys registered.");
175 frame.render_widget(empty, chunks[8]);
176 } else {
177 // Four cells rather than two: this table sits under a sub-heading.
178 let area = super::indent(chunks[8], 4);
179 let rows: Vec<Vec<Cell>> = app
180 .ssh_keys
181 .iter()
182 .map(|key| {
183 let fp = key.fingerprint.get(..20).unwrap_or(&key.fingerprint);
184
185 vec![
186 Cell::new("Label", key.label.as_str()),
187 Cell::new("Fingerprint", format!("{fp}...")),
188 Cell::new("Added", key.created_at.get(..10).unwrap_or(&key.created_at)),
189 ]
190 })
191 .collect();
192
193 let style = TableStyle::from_theme(&app.theme);
194 let table = table::table(&SSH_KEY_COLUMNS, &rows, &SSH_KEY_SIZING, &style, area.width);
195 let mut state = TableState::default().with_selected(Some(app.selected_index));
196 frame.render_stateful_widget(table, area, &mut state);
197 }
198
199 // Status line
200 if let Some(ref status) = app.settings_status {
201 let style = if status.starts_with("Error") {
202 Style::default().fg(app.theme.status_danger)
203 } else {
204 Style::default().fg(app.theme.status_success)
205 };
206 let status_line = Paragraph::new(Line::from(vec![
207 Span::raw(" "),
208 Span::styled(status.as_str(), style),
209 ]));
210 frame.render_widget(status_line, chunks[9]);
211 }
212
213 // Keybindings
214 let key_spans = vec![
215 Span::raw(" "),
216 Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)),
217 Span::raw(" Nav "),
218 Span::styled("[r]", Style::default().add_modifier(Modifier::BOLD)),
219 Span::raw(" Refresh "),
220 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
221 Span::raw(" Back"),
222 ];
223
224 let keys_bar =
225 Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted));
226 frame.render_widget(keys_bar, chunks[10]);
227 }
228