Skip to content

Commit 243ca2e

Browse files
committed
theme support in cli
Signed-off-by: Yujong Lee <yujonglee.dev@gmail.com>
1 parent 8ce64f7 commit 243ca2e

3 files changed

Lines changed: 96 additions & 66 deletions

File tree

apps/cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod commands;
33
mod event;
44
mod frame;
55
mod runtime;
6+
mod theme;
67
mod ui;
78

89
use clap::{Parser, Subcommand};

apps/cli/src/theme.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use ratatui::style::{Color, Modifier, Style};
2+
3+
#[derive(Debug, Clone, Copy)]
4+
pub struct Theme {
5+
pub border: Style,
6+
pub border_focused: Style,
7+
pub status_active: Style,
8+
pub status_degraded: Style,
9+
pub status_inactive: Style,
10+
pub error: Style,
11+
pub muted: Style,
12+
pub waveform_normal: Style,
13+
pub waveform_hot: Style,
14+
pub waveform_silent: Style,
15+
pub transcript_final: Style,
16+
pub transcript_pending: Style,
17+
pub transcript_partial: Style,
18+
pub placeholder: Style,
19+
pub shortcut_key: Style,
20+
}
21+
22+
impl Default for Theme {
23+
fn default() -> Self {
24+
Self {
25+
border: Style::new().fg(Color::DarkGray),
26+
border_focused: Style::new().fg(Color::Cyan),
27+
status_active: Style::new().fg(Color::Green),
28+
status_degraded: Style::new().fg(Color::Yellow),
29+
status_inactive: Style::new().fg(Color::Red),
30+
error: Style::new().fg(Color::Red),
31+
muted: Style::new().fg(Color::DarkGray),
32+
waveform_normal: Style::new().fg(Color::Red),
33+
waveform_hot: Style::new().fg(Color::LightRed),
34+
waveform_silent: Style::new().fg(Color::DarkGray),
35+
transcript_final: Style::new(),
36+
transcript_pending: Style::new().fg(Color::Yellow),
37+
transcript_partial: Style::new()
38+
.fg(Color::DarkGray)
39+
.add_modifier(Modifier::ITALIC),
40+
placeholder: Style::new()
41+
.fg(Color::DarkGray)
42+
.add_modifier(Modifier::ITALIC),
43+
shortcut_key: Style::new().fg(Color::DarkGray),
44+
}
45+
}
46+
}

apps/cli/src/ui.rs

Lines changed: 49 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ use hypr_transcript::WordState;
33
use ratatui::{
44
Frame,
55
layout::{Constraint, Layout, Position, Rect},
6-
style::{Color, Modifier, Style},
76
text::{Line, Span},
87
widgets::{Block, Borders, Padding, Paragraph, Wrap},
98
};
109

11-
use crate::app::App;
10+
use crate::{app::App, theme::Theme};
1211

1312
pub fn draw(frame: &mut Frame, app: &App) {
13+
let theme = Theme::default();
1414
let [content_area, status_area] =
1515
Layout::vertical([Constraint::Min(8), Constraint::Length(1)]).areas(frame.area());
1616

@@ -20,25 +20,25 @@ pub fn draw(frame: &mut Frame, app: &App) {
2020
])
2121
.areas(content_area);
2222

23-
draw_notepad(frame, app, notepad_area);
24-
draw_sidebar(frame, app, sidebar_area);
25-
draw_status_bar(frame, app, status_area);
23+
draw_notepad(frame, app, notepad_area, &theme);
24+
draw_sidebar(frame, app, sidebar_area, &theme);
25+
draw_status_bar(frame, app, status_area, &theme);
2626
}
2727

28-
fn draw_sidebar(frame: &mut Frame, app: &App, area: Rect) {
28+
fn draw_sidebar(frame: &mut Frame, app: &App, area: Rect, theme: &Theme) {
2929
let [metadata_area, meters_area, transcript_area] = Layout::vertical([
3030
Constraint::Length(8),
3131
Constraint::Length(5),
3232
Constraint::Min(3),
3333
])
3434
.areas(area);
3535

36-
draw_sidebar_metadata(frame, app, metadata_area);
37-
draw_sidebar_meters(frame, app, meters_area);
38-
draw_transcript(frame, app, transcript_area);
36+
draw_sidebar_metadata(frame, app, metadata_area, theme);
37+
draw_sidebar_meters(frame, app, meters_area, theme);
38+
draw_transcript(frame, app, transcript_area, theme);
3939
}
4040

41-
fn draw_sidebar_metadata(frame: &mut Frame, app: &App, area: Rect) {
41+
fn draw_sidebar_metadata(frame: &mut Frame, app: &App, area: Rect, theme: &Theme) {
4242
let elapsed = app.elapsed();
4343
let secs = elapsed.as_secs();
4444
let time_str = format!(
@@ -49,10 +49,10 @@ fn draw_sidebar_metadata(frame: &mut Frame, app: &App, area: Rect) {
4949
);
5050

5151
let state_style = match app.state {
52-
State::Active if app.degraded.is_some() => Style::new().fg(Color::Yellow),
53-
State::Active => Style::new().fg(Color::Green),
54-
State::Finalizing => Style::new().fg(Color::Yellow),
55-
State::Inactive => Style::new().fg(Color::Red),
52+
State::Active if app.degraded.is_some() => theme.status_degraded,
53+
State::Active => theme.status_active,
54+
State::Finalizing => theme.status_degraded,
55+
State::Inactive => theme.status_inactive,
5656
};
5757

5858
let mut lines = vec![
@@ -66,29 +66,23 @@ fn draw_sidebar_metadata(frame: &mut Frame, app: &App, area: Rect) {
6666

6767
if let Some(err) = app.errors.last() {
6868
lines.push(Line::default());
69-
lines.push(Line::from(vec![Span::styled(
70-
"Last error",
71-
Style::new().fg(Color::Red),
72-
)]));
73-
lines.push(Line::from(vec![Span::styled(
74-
err,
75-
Style::new().fg(Color::DarkGray),
76-
)]));
69+
lines.push(Line::from(vec![Span::styled("Last error", theme.error)]));
70+
lines.push(Line::from(vec![Span::styled(err, theme.muted)]));
7771
}
7872

7973
let block = Block::new()
8074
.borders(Borders::ALL)
81-
.border_style(Style::new().fg(Color::DarkGray))
75+
.border_style(theme.border)
8276
.title(" Session ")
8377
.padding(Padding::new(1, 1, 0, 0));
8478

8579
frame.render_widget(Paragraph::new(lines).block(block), area);
8680
}
8781

88-
fn draw_sidebar_meters(frame: &mut Frame, app: &App, area: Rect) {
82+
fn draw_sidebar_meters(frame: &mut Frame, app: &App, area: Rect, theme: &Theme) {
8983
let block = Block::new()
9084
.borders(Borders::ALL)
91-
.border_style(Style::new().fg(Color::DarkGray))
85+
.border_style(theme.border)
9286
.title(" Audio ")
9387
.padding(Padding::new(1, 1, 0, 0));
9488
let inner = block.inner(area);
@@ -107,7 +101,7 @@ fn draw_sidebar_meters(frame: &mut Frame, app: &App, area: Rect) {
107101
let active_width = ((waveform_width as f64) * 0.78).round() as usize;
108102
let active_width = active_width.clamp(8, waveform_width);
109103
let side_gutter = (waveform_width.saturating_sub(active_width)) / 2;
110-
let waveform = build_waveform_spans(app, active_width);
104+
let waveform = build_waveform_spans(app, active_width, theme);
111105

112106
let mut lines = (0..inner.height)
113107
.map(|_| Line::from(" ".repeat(inner.width as usize)))
@@ -125,13 +119,13 @@ fn draw_sidebar_meters(frame: &mut Frame, app: &App, area: Rect) {
125119
lines[middle_row] = Line::from(middle_spans);
126120

127121
if app.mic_muted {
128-
lines[0] = Line::from(Span::styled("mic muted", Style::new().fg(Color::DarkGray)));
122+
lines[0] = Line::from(Span::styled("mic muted", theme.muted));
129123
}
130124

131125
frame.render_widget(Paragraph::new(lines), inner);
132126
}
133127

134-
fn build_waveform_spans(app: &App, width: usize) -> Vec<Span<'static>> {
128+
fn build_waveform_spans(app: &App, width: usize, theme: &Theme) -> Vec<Span<'static>> {
135129
if width == 0 {
136130
return Vec::new();
137131
}
@@ -156,7 +150,7 @@ fn build_waveform_spans(app: &App, width: usize) -> Vec<Span<'static>> {
156150
let raw_value = column_energy(&combined, x, width) as f64;
157151
let envelope = edge_envelope(x, width);
158152
let value = (raw_value * envelope).round() as u64;
159-
let mut style = Style::new().fg(Color::Red);
153+
let mut style = theme.waveform_normal;
160154

161155
let normalized = (value as f64 / 1000.0).clamp(0.0, 1.0);
162156
let level = if value == 0 {
@@ -166,13 +160,13 @@ fn build_waveform_spans(app: &App, width: usize) -> Vec<Span<'static>> {
166160
};
167161

168162
if level == 0 || envelope < 0.22 {
169-
style = Style::new().fg(Color::DarkGray);
163+
style = theme.waveform_silent;
170164
} else if level >= 6 && envelope > 0.7 {
171-
style = Style::new().fg(Color::LightRed);
165+
style = theme.waveform_hot;
172166
}
173167

174168
if app.mic_muted {
175-
style = Style::new().fg(Color::DarkGray);
169+
style = theme.waveform_silent;
176170
}
177171

178172
spans.push(Span::styled(level_char(level).to_string(), style));
@@ -263,43 +257,35 @@ fn level_char(level: u8) -> char {
263257
}
264258
}
265259

266-
fn draw_transcript(frame: &mut Frame, app: &App, area: Rect) {
260+
fn draw_transcript(frame: &mut Frame, app: &App, area: Rect, theme: &Theme) {
267261
let mut spans: Vec<Span> = Vec::new();
268262

269263
for word in &app.words {
270264
let style = match word.state {
271-
WordState::Final => Style::new(),
272-
WordState::Pending => Style::new().fg(Color::Yellow),
265+
WordState::Final => theme.transcript_final,
266+
WordState::Pending => theme.transcript_pending,
273267
};
274268
spans.push(Span::styled(&word.text, style));
275269
spans.push(Span::raw(" "));
276270
}
277271

278272
if !app.partials.is_empty() {
279273
for partial in &app.partials {
280-
spans.push(Span::styled(
281-
&partial.text,
282-
Style::new()
283-
.fg(Color::DarkGray)
284-
.add_modifier(Modifier::ITALIC),
285-
));
274+
spans.push(Span::styled(&partial.text, theme.transcript_partial));
286275
spans.push(Span::raw(" "));
287276
}
288277
}
289278

290279
if spans.is_empty() {
291-
spans.push(Span::styled(
292-
"Waiting for speech...",
293-
Style::new().fg(Color::DarkGray).italic(),
294-
));
280+
spans.push(Span::styled("Waiting for speech...", theme.placeholder));
295281
}
296282

297283
let text = vec![Line::from(spans)];
298284

299285
let border_style = if app.transcript_focused && !app.memo_focused {
300-
Style::new().fg(Color::Cyan)
286+
theme.border_focused
301287
} else {
302-
Style::new().fg(Color::DarkGray)
288+
theme.border
303289
};
304290

305291
let block = Block::new()
@@ -316,15 +302,15 @@ fn draw_transcript(frame: &mut Frame, app: &App, area: Rect) {
316302
frame.render_widget(paragraph, area);
317303
}
318304

319-
fn draw_notepad(frame: &mut Frame, app: &App, area: Rect) {
305+
fn draw_notepad(frame: &mut Frame, app: &App, area: Rect, theme: &Theme) {
320306
if area.width < 3 || area.height < 3 {
321307
return;
322308
}
323309

324310
let border_style = if app.memo_focused {
325-
Style::new().fg(Color::Cyan)
311+
theme.border_focused
326312
} else {
327-
Style::new().fg(Color::DarkGray)
313+
theme.border
328314
};
329315

330316
let block = Block::new()
@@ -339,7 +325,7 @@ fn draw_notepad(frame: &mut Frame, app: &App, area: Rect) {
339325
let lines = if app.memo_is_empty() && !app.memo_focused {
340326
vec![Line::from(vec![Span::styled(
341327
"press [m] to start writing notes...",
342-
Style::new().fg(Color::DarkGray).italic(),
328+
theme.placeholder,
343329
)])]
344330
} else {
345331
view.lines.into_iter().map(Line::from).collect::<Vec<_>>()
@@ -356,38 +342,35 @@ fn draw_notepad(frame: &mut Frame, app: &App, area: Rect) {
356342
}
357343
}
358344

359-
fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
345+
fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect, theme: &Theme) {
360346
let word_count = app.words.len();
361347

362348
let line = if app.memo_focused {
363349
Line::from(vec![
364-
Span::styled(" [esc]", Style::new().fg(Color::DarkGray)),
350+
Span::styled(" [esc]", theme.shortcut_key),
365351
Span::raw(" transcript "),
366-
Span::styled("[tab]", Style::new().fg(Color::DarkGray)),
352+
Span::styled("[tab]", theme.shortcut_key),
367353
Span::raw(" toggle "),
368-
Span::styled("[ctrl+left/right]", Style::new().fg(Color::DarkGray)),
354+
Span::styled("[ctrl+left/right]", theme.shortcut_key),
369355
Span::raw(" panes "),
370-
Span::styled("[ctrl+u]", Style::new().fg(Color::DarkGray)),
356+
Span::styled("[ctrl+u]", theme.shortcut_key),
371357
Span::raw(" clear "),
372-
Span::styled("[ctrl+c]", Style::new().fg(Color::DarkGray)),
358+
Span::styled("[ctrl+c]", theme.shortcut_key),
373359
Span::raw(" quit "),
374360
])
375361
} else {
376362
Line::from(vec![
377-
Span::styled(" [q]", Style::new().fg(Color::DarkGray)),
363+
Span::styled(" [q]", theme.shortcut_key),
378364
Span::raw(" quit "),
379-
Span::styled("[j/k]", Style::new().fg(Color::DarkGray)),
365+
Span::styled("[j/k]", theme.shortcut_key),
380366
Span::raw(" transcript "),
381-
Span::styled("[m]", Style::new().fg(Color::DarkGray)),
367+
Span::styled("[m]", theme.shortcut_key),
382368
Span::raw(" notepad "),
383-
Span::styled("[tab]", Style::new().fg(Color::DarkGray)),
369+
Span::styled("[tab]", theme.shortcut_key),
384370
Span::raw(" toggle "),
385-
Span::styled("[ctrl+left/right]", Style::new().fg(Color::DarkGray)),
371+
Span::styled("[ctrl+left/right]", theme.shortcut_key),
386372
Span::raw(" panes "),
387-
Span::styled(
388-
format!("{word_count} words"),
389-
Style::new().fg(Color::DarkGray),
390-
),
373+
Span::styled(format!("{word_count} words"), theme.muted),
391374
])
392375
};
393376

0 commit comments

Comments
 (0)