initial code scetch
This commit is contained in:
238
src/tui.rs
Normal file
238
src/tui.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Gauge, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{app::App, deck::{Deck, DeckId}};
|
||||
|
||||
pub fn draw(f: &mut Frame, app: &mut App) {
|
||||
let area = f.area();
|
||||
draw_base(f, app, area);
|
||||
if let Some(sel) = &mut app.file_selector {
|
||||
sel.render(f, area);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_base(f: &mut Frame, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(4),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
// Header
|
||||
let header = Paragraph::new(Line::from(vec![
|
||||
Span::styled(" ♫ djdeck ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
format!("OSC :{} → :{} ", crate::osc::LISTEN_PORT, crate::osc::SEND_PORT),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
),
|
||||
Span::styled(
|
||||
"PipeWire graph: each deck → rubberband filter-chain → sink",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
),
|
||||
]));
|
||||
f.render_widget(header, chunks[0]);
|
||||
|
||||
// 2×2 deck grid
|
||||
let rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(chunks[1]);
|
||||
|
||||
for row in 0..2usize {
|
||||
let cols = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(rows[row]);
|
||||
|
||||
for col in 0..2usize {
|
||||
let idx = row * 2 + col;
|
||||
if idx < app.decks.len() {
|
||||
draw_deck(f, &app.decks[idx], cols[col], app.focused_deck == idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw_help(f, chunks[2]);
|
||||
}
|
||||
|
||||
fn deck_color(id: DeckId) -> Color {
|
||||
match id {
|
||||
DeckId::A => Color::Cyan,
|
||||
DeckId::Z => Color::Magenta,
|
||||
DeckId::S => Color::Green,
|
||||
DeckId::X => Color::Yellow,
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_deck(f: &mut Frame, deck: &Deck, area: Rect, focused: bool) {
|
||||
let color = deck_color(deck.id);
|
||||
|
||||
let border_style = if focused {
|
||||
Style::default().fg(color).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
|
||||
// Truncate track name to fit
|
||||
let max_name = area.width.saturating_sub(18) as usize;
|
||||
let track_display = deck.track_name.as_deref().unwrap_or("[no track]");
|
||||
let track_display = if track_display.len() > max_name {
|
||||
format!("{}…", &track_display[..max_name.saturating_sub(1)])
|
||||
} else {
|
||||
track_display.to_string()
|
||||
};
|
||||
|
||||
// Node ID status
|
||||
let graph_status = match (deck.stream_node_id, deck.filter_node_id) {
|
||||
(Some(s), Some(f)) => format!(" [pw:{}/{}]", s, f),
|
||||
(Some(s), None) => format!(" [pw:{}/--]", s),
|
||||
_ => " [pw:--]".to_string(),
|
||||
};
|
||||
|
||||
let title = format!(
|
||||
" Deck {} {} {} {} ",
|
||||
deck.id.label(),
|
||||
deck.play_state.icon(),
|
||||
track_display,
|
||||
graph_status,
|
||||
);
|
||||
|
||||
let outer = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(title)
|
||||
.border_style(border_style)
|
||||
.style(Style::default().bg(Color::Black));
|
||||
f.render_widget(outer, area);
|
||||
|
||||
let inner = area.inner(ratatui::layout::Margin { horizontal: 1, vertical: 1 });
|
||||
|
||||
let rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1), // timecode
|
||||
Constraint::Length(1), // progress
|
||||
Constraint::Length(1), // gap
|
||||
Constraint::Length(1), // fader
|
||||
Constraint::Length(1), // pitch + tempo
|
||||
Constraint::Length(1), // gap
|
||||
Constraint::Length(1), // VU
|
||||
])
|
||||
.split(inner);
|
||||
|
||||
// Timecode
|
||||
let (em, es) = deck.elapsed();
|
||||
let (rm, rs) = deck.remaining();
|
||||
f.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::styled(format!("{:02}:{:02}", em, es), Style::default().fg(color).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(" / ", Style::default().fg(Color::DarkGray)),
|
||||
Span::styled(format!("-{:02}:{:02}", rm, rs), Style::default().fg(Color::DarkGray)),
|
||||
])),
|
||||
rows[0],
|
||||
);
|
||||
|
||||
// Progress
|
||||
f.render_widget(
|
||||
Gauge::default()
|
||||
.gauge_style(Style::default().fg(color).bg(Color::Black))
|
||||
.percent((deck.progress() * 100.0) as u16)
|
||||
.label(""),
|
||||
rows[1],
|
||||
);
|
||||
|
||||
// Fader
|
||||
let fader_pct = (deck.fader * 100.0) as u16;
|
||||
f.render_widget(
|
||||
Gauge::default()
|
||||
.gauge_style(Style::default()
|
||||
.fg(if fader_pct > 10 { Color::White } else { Color::DarkGray })
|
||||
.bg(Color::Black))
|
||||
.percent(fader_pct.min(100))
|
||||
.label(format!("VOL {:3}%", fader_pct)),
|
||||
rows[3],
|
||||
);
|
||||
|
||||
// Pitch + Tempo (both live in rubberband filter-chain node)
|
||||
f.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::styled("PITCH ", Style::default().fg(Color::DarkGray)),
|
||||
Span::styled(
|
||||
format!("{:+.1}st", deck.pitch),
|
||||
Style::default().fg(if deck.pitch.abs() > 0.05 { Color::Yellow } else { Color::White }),
|
||||
),
|
||||
Span::styled(" TEMPO ", Style::default().fg(Color::DarkGray)),
|
||||
Span::styled(
|
||||
format!("{:.2}×", deck.tempo),
|
||||
Style::default().fg(if (deck.tempo - 1.0).abs() > 0.01 { Color::Yellow } else { Color::White }),
|
||||
),
|
||||
])),
|
||||
rows[4],
|
||||
);
|
||||
|
||||
// VU
|
||||
draw_vu(f, deck.vu, rows[6], color);
|
||||
}
|
||||
|
||||
fn draw_vu(f: &mut Frame, vu: (f32, f32), area: Rect, color: Color) {
|
||||
let half = area.width / 2;
|
||||
let la = Rect { x: area.x, y: area.y, width: half.saturating_sub(1), height: 1 };
|
||||
let ra = Rect { x: area.x + half, y: area.y, width: half, height: 1 };
|
||||
|
||||
let vc = |p: u16| if p > 90 { Color::Red } else if p > 70 { Color::Yellow } else { color };
|
||||
|
||||
let lp = (vu.0 * 100.0) as u16;
|
||||
let rp = (vu.1 * 100.0) as u16;
|
||||
|
||||
f.render_widget(Gauge::default().gauge_style(Style::default().fg(vc(lp)).bg(Color::Black)).percent(lp.min(100)).label("L"), la);
|
||||
f.render_widget(Gauge::default().gauge_style(Style::default().fg(vc(rp)).bg(Color::Black)).percent(rp.min(100)).label("R"), ra);
|
||||
}
|
||||
|
||||
fn draw_help(f: &mut Frame, area: Rect) {
|
||||
let help = Paragraph::new(vec![
|
||||
Line::from(vec![
|
||||
Span::styled(" Focus: ", Style::default().fg(Color::DarkGray)),
|
||||
k("a"), k("z"), k("s"), k("x"),
|
||||
Span::styled(" Load: ", Style::default().fg(Color::DarkGray)),
|
||||
k("Shift+A/Z/S/X"),
|
||||
Span::styled(" Play/Pause: ", Style::default().fg(Color::DarkGray)),
|
||||
k("Space"),
|
||||
Span::styled(" Stop: ", Style::default().fg(Color::DarkGray)),
|
||||
k("Enter"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" Fader: ", Style::default().fg(Color::DarkGray)),
|
||||
k("↑/↓"),
|
||||
Span::styled(" Pitch: ", Style::default().fg(Color::DarkGray)),
|
||||
k("[/]"),
|
||||
Span::styled(" Tempo: ", Style::default().fg(Color::DarkGray)),
|
||||
k(",/."),
|
||||
Span::styled(" Cue: ", Style::default().fg(Color::DarkGray)),
|
||||
k("c"), Span::raw(" set "), k("v"), Span::raw(" goto"),
|
||||
Span::styled(" Seek: ", Style::default().fg(Color::DarkGray)),
|
||||
k("←/→"),
|
||||
Span::styled(" Quit: ", Style::default().fg(Color::DarkGray)),
|
||||
k("q"),
|
||||
]),
|
||||
])
|
||||
.block(Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::default().fg(Color::DarkGray)))
|
||||
.style(Style::default().bg(Color::Black));
|
||||
|
||||
f.render_widget(help, area);
|
||||
}
|
||||
|
||||
fn k(s: &str) -> Span<'_> {
|
||||
Span::styled(
|
||||
format!("[{}] ", s),
|
||||
Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user