feat: refactor architecture #4

Merged
Stevan merged 10 commits from stevanfreeborn/feat/refactor-architecture into main 2026-07-28 15:22:14 +00:00
Showing only changes of commit c8448c364e - Show all commits
+166
View File
@@ -0,0 +1,166 @@
use figlet_rs::Toilet;
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
widgets::{Block, Borders, Clear, Paragraph},
};
use crate::app::{Termato, TimerMode};
use crate::audio::AudioVisualizer;
// use unicode blocks for rendering
// Empty -> " "
// 1/8th height -> "▂"
// 2/8th height -> "▃"
// 3/8th height -> "▄"
// 4/8th height -> "▅"
// 5/8th height -> "▆"
// 6/8th height -> "▇"
// Full Height -> "█"
pub fn render_visualizer(f: &mut Frame, area: Rect, bar_values: &[u64]) {
const BLOCKS: [&str; 8] = ["", "", "", "", "", "", "", ""];
let max_bars = area.width as usize;
let line: String = bar_values
.iter()
.take(max_bars)
.map(|&val| {
if val == 0 {
" "
} else {
let idx = ((val as f32 / 100.0) * (BLOCKS.len() - 1) as f32)
.clamp(0.0, (BLOCKS.len() - 1) as f32) as usize;
BLOCKS[idx]
}
})
.collect();
let viz_paragraph = Paragraph::new(line)
.style(Style::default().fg(Color::Rgb(200, 184, 224)))
.alignment(Alignment::Center);
f.render_widget(viz_paragraph, area);
}
pub fn render_app(
app: &Termato,
f: &mut Frame,
font: &Toilet,
visualizer: Option<&AudioVisualizer>,
) {
let size = f.area();
let minutes = app.time_remaining_in_sec / 60;
let seconds = app.time_remaining_in_sec % 60;
let time_str = format!("{:02}:{:02}", minutes, seconds);
let time_color = if !app.is_running {
Color::Rgb(212, 200, 122)
} else {
match app.mode {
TimerMode::Work => Color::Rgb(212, 115, 115),
TimerMode::Break => Color::Rgb(126, 200, 192),
}
};
let raw_time_text = if let Some(fig) = font.convert(&time_str) {
fig.to_string()
} else {
time_str
};
let trimmed_time_text = raw_time_text.trim_matches('\n');
let text_lines: Vec<&str> = trimmed_time_text.lines().collect();
let text_height = text_lines.len() as u16;
let viz_height = if visualizer.is_some() { 1 } else { 0 };
let viz_width = 64;
let content_height = text_height + viz_height;
let outer_vertical = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(0),
Constraint::Length(content_height),
Constraint::Min(0),
])
.split(size);
let inner_vertical = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(text_height),
Constraint::Length(viz_height),
])
.split(outer_vertical[1]);
let viz_horizontal = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(0),
Constraint::Length(viz_width.min(size.width)),
Constraint::Min(0),
])
.split(inner_vertical[1]);
let timer_paragraph = Paragraph::new(trimmed_time_text)
.style(Style::default().fg(time_color))
.alignment(Alignment::Center);
f.render_widget(timer_paragraph, inner_vertical[0]);
if let Some(viz) = visualizer
&& let Ok(bar_data) = viz.bar_data.lock()
{
render_visualizer(f, viz_horizontal[1], &bar_data);
}
if app.show_help {
let popup_width = 44;
let popup_height = 7;
if size.width >= popup_width && size.height >= popup_height {
let popup_vertical = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length((size.height.saturating_sub(popup_height)) / 2),
Constraint::Length(popup_height),
Constraint::Min(1),
])
.split(size);
let popup_horizontal = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length((size.width.saturating_sub(popup_width)) / 2),
Constraint::Length(popup_width),
Constraint::Min(1),
])
.split(popup_vertical[1]);
let popup_area = popup_horizontal[1];
let help_block = Block::default()
.title(" Controls ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(58, 62, 70)));
let help_content = Paragraph::new(
"[Space] Pause / Resume\n\
[S] Switch Mode\n\
[R] Reset Timer\n\
[?] Toggle Help\n\
[Q] Quit",
)
.block(help_block)
.style(Style::default().fg(Color::Rgb(224, 228, 232)))
.alignment(Alignment::Left);
f.render_widget(Clear, popup_area);
f.render_widget(help_content, popup_area);
}
}
}