Skip to main content

max / makenotwork

5.6 KB · 185 lines History Blame Raw
1 //! License key management screen — list, generate, revoke keys.
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, Rect};
7 use ratatui::style::{Modifier, Style};
8 use ratatui::text::{Line, Span};
9 use ratatui::widgets::{Block, Borders, Paragraph, TableState};
10
11 use super::App;
12
13 /// The license key columns. The key code is the row; whether it still works is
14 /// the next thing asked of it, and the activation count and the date after
15 /// that.
16 const KEY_COLUMNS: [Column<'static>; 4] = [
17 Column {
18 name: "Key",
19 width: Width::Fill,
20 priority: Priority::Essential,
21 sortable: false,
22 sorted: None,
23 },
24 Column {
25 name: "Status",
26 width: Width::Fixed,
27 priority: Priority::Secondary,
28 sortable: false,
29 sorted: None,
30 },
31 Column {
32 name: "Activations",
33 width: Width::Fixed,
34 priority: Priority::Optional,
35 sortable: false,
36 sorted: None,
37 },
38 Column {
39 name: "Created",
40 width: Width::Fixed,
41 priority: Priority::Optional,
42 sortable: false,
43 sorted: None,
44 },
45 ];
46
47 /// The tracks the hand-written `Constraint`s carried.
48 const KEY_SIZING: Sizing<'static> = Sizing {
49 lengths: &[
50 ("Key", 24),
51 ("Status", 10),
52 ("Activations", 12),
53 ("Created", 12),
54 ],
55 fallback: 12,
56 };
57
58 pub(crate) fn render(frame: &mut Frame, app: &App) {
59 let area = frame.area();
60
61 let item_title = app.keys_item_title.as_deref().unwrap_or("License Keys");
62
63 let title = Line::from(vec![
64 Span::styled(
65 " Makenot.work ",
66 Style::default().add_modifier(Modifier::BOLD),
67 ),
68 Span::raw(" -- "),
69 Span::styled(item_title, Style::default().add_modifier(Modifier::BOLD)),
70 Span::raw(" -- Keys "),
71 ]);
72
73 let block = Block::default()
74 .title(title)
75 .borders(Borders::ALL)
76 .border_style(Style::default().fg(app.theme.line_border));
77
78 let inner = block.inner(area);
79 frame.render_widget(block, area);
80
81 let chunks = Layout::vertical([
82 Constraint::Length(1), // spacer
83 Constraint::Length(1), // section header
84 Constraint::Min(3), // key list
85 Constraint::Length(1), // status line
86 Constraint::Length(1), // keybindings
87 ])
88 .split(inner);
89
90 // Section header
91 let count = app.license_keys.len();
92 let header = Paragraph::new(Line::from(vec![
93 Span::raw(" "),
94 Span::styled("Keys", Style::default().add_modifier(Modifier::BOLD)),
95 if count == 0 {
96 Span::raw("")
97 } else {
98 Span::raw(format!(" ({count})"))
99 },
100 ]));
101 frame.render_widget(header, chunks[1]);
102
103 // Key list
104 if app.loading {
105 let loading = Paragraph::new(" Loading...");
106 frame.render_widget(loading, chunks[2]);
107 } else if app.license_keys.is_empty() {
108 let empty = Paragraph::new(" No license keys. Press [g] to generate one.");
109 frame.render_widget(empty, chunks[2]);
110 } else {
111 render_key_table(frame, app, chunks[2]);
112 }
113
114 // Status line
115 if let Some(ref status) = app.keys_status {
116 let style = if status.starts_with("Error") {
117 Style::default().fg(app.theme.status_danger)
118 } else {
119 Style::default().fg(app.theme.status_success)
120 };
121 let status_line = Paragraph::new(Line::from(vec![
122 Span::raw(" "),
123 Span::styled(status.as_str(), style),
124 ]));
125 frame.render_widget(status_line, chunks[3]);
126 }
127
128 // Keybindings
129 let mut key_spans = vec![
130 Span::raw(" "),
131 Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)),
132 Span::raw(" Nav "),
133 Span::styled("[g]", Style::default().add_modifier(Modifier::BOLD)),
134 Span::raw(" Generate "),
135 ];
136
137 if !app.license_keys.is_empty() {
138 key_spans.extend([
139 Span::styled("[x]", Style::default().add_modifier(Modifier::BOLD)),
140 Span::raw(" Revoke "),
141 ]);
142 }
143
144 key_spans.extend([
145 Span::styled("[r]", Style::default().add_modifier(Modifier::BOLD)),
146 Span::raw(" Refresh "),
147 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
148 Span::raw(" Back"),
149 ]);
150
151 let keys =
152 Paragraph::new(Line::from(key_spans)).style(Style::default().fg(app.theme.content_muted));
153 frame.render_widget(keys, chunks[4]);
154 }
155
156 fn render_key_table(frame: &mut Frame, app: &App, area: Rect) {
157 let area = super::indent(area, 2);
158
159 let rows: Vec<Vec<Cell>> = app
160 .license_keys
161 .iter()
162 .map(|key| {
163 let activations = match key.max_activations {
164 Some(max) => format!("{}/{}", key.activation_count, max),
165 None => key.activation_count.to_string(),
166 };
167
168 vec![
169 Cell::new("Key", key.key_code.as_str()),
170 Cell::new("Status", if key.is_revoked { "Revoked" } else { "Active" }),
171 Cell::new("Activations", activations),
172 Cell::new(
173 "Created",
174 key.created_at.get(..10).unwrap_or(&key.created_at),
175 ),
176 ]
177 })
178 .collect();
179
180 let style = TableStyle::from_theme(&app.theme);
181 let table = table::table(&KEY_COLUMNS, &rows, &KEY_SIZING, &style, area.width);
182 let mut state = TableState::default().with_selected(Some(app.selected_index));
183 frame.render_stateful_widget(table, area, &mut state);
184 }
185