Reorganize repository

This commit is contained in:
2026-01-23 20:29:44 +01:00
parent 1433e07066
commit a1ddb4a170
40 changed files with 372 additions and 257084 deletions

View File

@@ -0,0 +1,7 @@
[package]
name = "cagire-ratatui"
version = "0.1.0"
edition = "2021"
[dependencies]
ratatui = "0.29"

View File

@@ -0,0 +1,60 @@
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use super::ModalFrame;
pub struct ConfirmModal<'a> {
title: &'a str,
message: &'a str,
selected: bool,
}
impl<'a> ConfirmModal<'a> {
pub fn new(title: &'a str, message: &'a str, selected: bool) -> Self {
Self {
title,
message,
selected,
}
}
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
let inner = ModalFrame::new(self.title)
.width(30)
.height(5)
.border_color(Color::Yellow)
.render_centered(frame, term);
let rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
frame.render_widget(
Paragraph::new(self.message).alignment(Alignment::Center),
rows[0],
);
let yes_style = if self.selected {
Style::new().fg(Color::Black).bg(Color::Yellow)
} else {
Style::default()
};
let no_style = if !self.selected {
Style::new().fg(Color::Black).bg(Color::Yellow)
} else {
Style::default()
};
let buttons = Line::from(vec![
Span::styled(" Yes ", yes_style),
Span::raw(" "),
Span::styled(" No ", no_style),
]);
frame.render_widget(
Paragraph::new(buttons).alignment(Alignment::Center),
rows[1],
);
}
}

13
crates/ratatui/src/lib.rs Normal file
View File

@@ -0,0 +1,13 @@
mod confirm;
mod modal;
mod scope;
mod spectrum;
mod text_input;
mod vu_meter;
pub use confirm::ConfirmModal;
pub use modal::ModalFrame;
pub use scope::{Orientation, Scope};
pub use spectrum::Spectrum;
pub use text_input::TextInputModal;
pub use vu_meter::VuMeter;

View File

@@ -0,0 +1,58 @@
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Borders, Clear};
use ratatui::Frame;
pub struct ModalFrame<'a> {
title: &'a str,
width: u16,
height: u16,
border_color: Color,
}
impl<'a> ModalFrame<'a> {
pub fn new(title: &'a str) -> Self {
Self {
title,
width: 40,
height: 5,
border_color: Color::White,
}
}
pub fn width(mut self, w: u16) -> Self {
self.width = w;
self
}
pub fn height(mut self, h: u16) -> Self {
self.height = h;
self
}
pub fn border_color(mut self, c: Color) -> Self {
self.border_color = c;
self
}
pub fn render_centered(&self, frame: &mut Frame, term: Rect) -> Rect {
let width = self.width.min(term.width.saturating_sub(4));
let height = self.height.min(term.height.saturating_sub(4));
let x = term.x + (term.width.saturating_sub(width)) / 2;
let y = term.y + (term.height.saturating_sub(height)) / 2;
let area = Rect::new(x, y, width, height);
frame.render_widget(Clear, area);
let block = Block::default()
.borders(Borders::ALL)
.title(self.title)
.border_style(Style::new().fg(self.border_color));
let inner = block.inner(area);
frame.render_widget(block, area);
inner
}
}

149
crates/ratatui/src/scope.rs Normal file
View File

@@ -0,0 +1,149 @@
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::Widget;
#[allow(dead_code)]
pub enum Orientation {
Horizontal,
Vertical,
}
pub struct Scope<'a> {
data: &'a [f32],
orientation: Orientation,
color: Color,
gain: f32,
}
impl<'a> Scope<'a> {
pub fn new(data: &'a [f32]) -> Self {
Self {
data,
orientation: Orientation::Horizontal,
color: Color::Green,
gain: 1.0,
}
}
pub fn orientation(mut self, o: Orientation) -> Self {
self.orientation = o;
self
}
pub fn color(mut self, c: Color) -> Self {
self.color = c;
self
}
}
impl Widget for Scope<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 || self.data.is_empty() {
return;
}
match self.orientation {
Orientation::Horizontal => {
render_horizontal(self.data, area, buf, self.color, self.gain)
}
Orientation::Vertical => render_vertical(self.data, area, buf, self.color, self.gain),
}
}
}
fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
let width = area.width as usize;
let height = area.height as usize;
let fine_width = width * 2;
let fine_height = height * 4;
let mut patterns = vec![0u8; width * height];
for fine_x in 0..fine_width {
let sample_idx = (fine_x * data.len()) / fine_width;
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize;
let fine_y = fine_y.min(fine_height - 1);
let char_x = fine_x / 2;
let char_y = fine_y / 4;
let dot_x = fine_x % 2;
let dot_y = fine_y % 4;
let bit = match (dot_x, dot_y) {
(0, 0) => 0x01,
(0, 1) => 0x02,
(0, 2) => 0x04,
(0, 3) => 0x40,
(1, 0) => 0x08,
(1, 1) => 0x10,
(1, 2) => 0x20,
(1, 3) => 0x80,
_ => unreachable!(),
};
patterns[char_y * width + char_x] |= bit;
}
for cy in 0..height {
for cx in 0..width {
let pattern = patterns[cy * width + cx];
if pattern != 0 {
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
buf[(area.x + cx as u16, area.y + cy as u16)]
.set_char(ch)
.set_fg(color);
}
}
}
}
fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
let width = area.width as usize;
let height = area.height as usize;
let fine_width = width * 2;
let fine_height = height * 4;
let mut patterns = vec![0u8; width * height];
for fine_y in 0..fine_height {
let sample_idx = (fine_y * data.len()) / fine_height;
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
let fine_x = fine_x.min(fine_width - 1);
let char_x = fine_x / 2;
let char_y = fine_y / 4;
let dot_x = fine_x % 2;
let dot_y = fine_y % 4;
let bit = match (dot_x, dot_y) {
(0, 0) => 0x01,
(0, 1) => 0x02,
(0, 2) => 0x04,
(0, 3) => 0x40,
(1, 0) => 0x08,
(1, 1) => 0x10,
(1, 2) => 0x20,
(1, 3) => 0x80,
_ => unreachable!(),
};
patterns[char_y * width + char_x] |= bit;
}
for cy in 0..height {
for cx in 0..width {
let pattern = patterns[cy * width + cx];
if pattern != 0 {
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
buf[(area.x + cx as u16, area.y + cy as u16)]
.set_char(ch)
.set_fg(color);
}
}
}
}

View File

@@ -0,0 +1,62 @@
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::Widget;
const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}'];
pub struct Spectrum<'a> {
data: &'a [f32; 32],
}
impl<'a> Spectrum<'a> {
pub fn new(data: &'a [f32; 32]) -> Self {
Self { data }
}
}
impl Widget for Spectrum<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 {
return;
}
let height = area.height as f32;
let band_width = area.width as usize / 32;
if band_width == 0 {
return;
}
for (band, &mag) in self.data.iter().enumerate() {
let bar_height = mag * height;
let full_cells = bar_height as usize;
let frac = bar_height - full_cells as f32;
let frac_idx = (frac * 8.0) as usize;
let x_start = area.x + (band * band_width) as u16;
for row in 0..area.height as usize {
let y = area.y + area.height - 1 - row as u16;
let ratio = row as f32 / area.height as f32;
let color = if ratio < 0.33 {
Color::Rgb(40, 180, 80)
} else if ratio < 0.66 {
Color::Rgb(220, 180, 40)
} else {
Color::Rgb(220, 60, 40)
};
for dx in 0..band_width as u16 {
let x = x_start + dx;
if x >= area.x + area.width {
break;
}
if row < full_cells {
buf[(x, y)].set_char(BLOCKS[7]).set_fg(color);
} else if row == full_cells && frac_idx > 0 {
buf[(x, y)].set_char(BLOCKS[frac_idx - 1]).set_fg(color);
}
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use super::ModalFrame;
pub struct TextInputModal<'a> {
title: &'a str,
input: &'a str,
hint: Option<&'a str>,
border_color: Color,
width: u16,
}
impl<'a> TextInputModal<'a> {
pub fn new(title: &'a str, input: &'a str) -> Self {
Self {
title,
input,
hint: None,
border_color: Color::White,
width: 50,
}
}
pub fn hint(mut self, h: &'a str) -> Self {
self.hint = Some(h);
self
}
pub fn border_color(mut self, c: Color) -> Self {
self.border_color = c;
self
}
pub fn width(mut self, w: u16) -> Self {
self.width = w;
self
}
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
let height = if self.hint.is_some() { 6 } else { 5 };
let inner = ModalFrame::new(self.title)
.width(self.width)
.height(height)
.border_color(self.border_color)
.render_centered(frame, term);
if self.hint.is_some() {
let rows =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::raw("> "),
Span::styled(self.input, Style::new().fg(Color::Cyan)),
Span::styled("", Style::new().fg(Color::White)),
])),
rows[0],
);
if let Some(hint) = self.hint {
frame.render_widget(
Paragraph::new(Span::styled(hint, Style::new().fg(Color::DarkGray))),
rows[1],
);
}
} else {
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::raw("> "),
Span::styled(self.input, Style::new().fg(Color::Cyan)),
Span::styled("", Style::new().fg(Color::White)),
])),
inner,
);
}
}
}

View File

@@ -0,0 +1,81 @@
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::Widget;
const DB_MIN: f32 = -48.0;
const DB_MAX: f32 = 3.0;
const DB_RANGE: f32 = DB_MAX - DB_MIN;
pub struct VuMeter {
left: f32,
right: f32,
}
impl VuMeter {
pub fn new(left: f32, right: f32) -> Self {
Self { left, right }
}
fn amplitude_to_db(amp: f32) -> f32 {
if amp <= 0.0 {
DB_MIN
} else {
(20.0 * amp.log10()).clamp(DB_MIN, DB_MAX)
}
}
fn db_to_normalized(db: f32) -> f32 {
(db - DB_MIN) / DB_RANGE
}
fn row_to_color(row_position: f32) -> Color {
if row_position > 0.9 {
Color::Red
} else if row_position > 0.75 {
Color::Yellow
} else {
Color::Green
}
}
}
impl Widget for VuMeter {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width < 3 || area.height == 0 {
return;
}
let height = area.height as usize;
let half_width = area.width / 2;
let gap = 1u16;
let left_db = Self::amplitude_to_db(self.left);
let right_db = Self::amplitude_to_db(self.right);
let left_norm = Self::db_to_normalized(left_db);
let right_norm = Self::db_to_normalized(right_db);
let left_rows = (left_norm * height as f32).round() as usize;
let right_rows = (right_norm * height as f32).round() as usize;
for row in 0..height {
let y = area.y + area.height - 1 - row as u16;
let row_position = (row as f32 + 0.5) / height as f32;
let color = Self::row_to_color(row_position);
for col in 0..half_width.saturating_sub(gap) {
let x = area.x + col;
if row < left_rows {
buf[(x, y)].set_char(' ').set_bg(color);
}
}
for col in 0..half_width.saturating_sub(gap) {
let x = area.x + half_width + gap + col;
if x < area.x + area.width && row < right_rows {
buf[(x, y)].set_char(' ').set_bg(color);
}
}
}
}
}