Auto docs
This commit is contained in:
@@ -1,115 +0,0 @@
|
||||
# Scripting
|
||||
|
||||
Steps are programmed using Rhai, a simple scripting language.
|
||||
|
||||
## Basic Syntax
|
||||
|
||||
Create sounds using `sound()` and chain parameters:
|
||||
|
||||
```
|
||||
sound("kick").gain(0.8)
|
||||
```
|
||||
|
||||
```
|
||||
sound("hat").freq(8000).decay(0.1)
|
||||
```
|
||||
|
||||
## Context Variables
|
||||
|
||||
These are available in every step script:
|
||||
|
||||
- `step`: Current step index (0-based)
|
||||
- `beat`: Current beat position
|
||||
- `bank`: Current bank index
|
||||
- `pattern`: Current pattern index
|
||||
- `tempo`: Current BPM
|
||||
- `phase`: Phase within the bar (0.0 to 1.0)
|
||||
- `slot`: Slot number playing this pattern
|
||||
|
||||
## Randomness
|
||||
|
||||
- `rand(min, max)`: Random float in range
|
||||
- `rrand(min, max)`: Random integer in range (inclusive)
|
||||
- `seed(n)`: Set random seed for reproducibility
|
||||
|
||||
## Variables
|
||||
|
||||
Store and retrieve values across steps:
|
||||
|
||||
- `set("name", value)`: Store a value
|
||||
- `get("name")`: Retrieve a value
|
||||
|
||||
## Sound Parameters
|
||||
|
||||
### Core
|
||||
|
||||
- `sound(name)`: Create sound command
|
||||
- `freq(hz)`: Frequency
|
||||
- `note(midi)`: MIDI note number
|
||||
- `gain(amp)`: Volume (0.0-1.0)
|
||||
- `pan(pos)`: Stereo position (-1.0 to 1.0)
|
||||
- `dur(secs)`: Duration
|
||||
- `gate(secs)`: Gate time
|
||||
|
||||
### Envelope
|
||||
|
||||
- `attack(secs)`: Attack time
|
||||
- `decay(secs)`: Decay time
|
||||
- `sustain(level)`: Sustain level
|
||||
- `release(secs)`: Release time
|
||||
|
||||
### Filter
|
||||
|
||||
- `lpf(hz)`: Lowpass frequency
|
||||
- `lpq(q)`: Lowpass resonance
|
||||
- `hpf(hz)`: Highpass frequency
|
||||
- `bpf(hz)`: Bandpass frequency
|
||||
|
||||
### Effects
|
||||
|
||||
- `delay(mix)`: Delay amount
|
||||
- `delaytime(secs)`: Delay time
|
||||
- `delayfeedback(amt)`: Delay feedback
|
||||
- `verb(mix)`: Reverb amount
|
||||
- `verbdecay(secs)`: Reverb decay
|
||||
|
||||
### Modulation
|
||||
|
||||
- `vib(hz)`: Vibrato rate
|
||||
- `vibmod(amt)`: Vibrato depth
|
||||
- `fm(hz)`: FM modulator frequency
|
||||
- `fmh(ratio)`: FM harmonic ratio
|
||||
|
||||
### Sample Playback
|
||||
|
||||
- `speed(ratio)`: Playback speed
|
||||
- `begin(pos)`: Start position (0.0-1.0)
|
||||
- `end(pos)`: End position (0.0-1.0)
|
||||
|
||||
## Examples
|
||||
|
||||
Conditional based on step:
|
||||
|
||||
```
|
||||
if step % 4 == 0 {
|
||||
sound("kick").gain(1.0)
|
||||
} else {
|
||||
sound("hat").gain(0.5)
|
||||
}
|
||||
```
|
||||
|
||||
Random variation:
|
||||
|
||||
```
|
||||
sound("synth")
|
||||
.freq(rand(200.0, 800.0))
|
||||
.gain(rand(0.3, 0.7))
|
||||
```
|
||||
|
||||
Using variables:
|
||||
|
||||
```
|
||||
let n = get("counter");
|
||||
set("counter", n + 1);
|
||||
sound("beep").note(60 + (n % 12))
|
||||
```
|
||||
@@ -549,7 +549,7 @@ impl App {
|
||||
if let Some(src) = &self.copied_pattern {
|
||||
let mut pat = src.clone();
|
||||
pat.name = match &src.name {
|
||||
Some(name) if !name.ends_with(" (copy)") => Some(format!("{} (copy)", name)),
|
||||
Some(name) if !name.ends_with(" (copy)") => Some(format!("{name} (copy)")),
|
||||
Some(name) => Some(name.clone()),
|
||||
None => Some("(copy)".to_string()),
|
||||
};
|
||||
@@ -572,7 +572,7 @@ impl App {
|
||||
if let Some(src) = &self.copied_bank {
|
||||
let mut b = src.clone();
|
||||
b.name = match &src.name {
|
||||
Some(name) if !name.ends_with(" (copy)") => Some(format!("{} (copy)", name)),
|
||||
Some(name) if !name.ends_with(" (copy)") => Some(format!("{name} (copy)")),
|
||||
Some(name) => Some(name.clone()),
|
||||
None => Some("(copy)".to_string()),
|
||||
};
|
||||
@@ -844,11 +844,13 @@ impl App {
|
||||
AppCommand::DocNextTopic => {
|
||||
self.ui.doc_topic = (self.ui.doc_topic + 1) % doc_view::topic_count();
|
||||
self.ui.doc_scroll = 0;
|
||||
self.ui.doc_category = 0;
|
||||
}
|
||||
AppCommand::DocPrevTopic => {
|
||||
let count = doc_view::topic_count();
|
||||
self.ui.doc_topic = (self.ui.doc_topic + count - 1) % count;
|
||||
self.ui.doc_scroll = 0;
|
||||
self.ui.doc_category = 0;
|
||||
}
|
||||
AppCommand::DocScrollDown(n) => {
|
||||
self.ui.doc_scroll = self.ui.doc_scroll.saturating_add(n);
|
||||
@@ -856,6 +858,16 @@ impl App {
|
||||
AppCommand::DocScrollUp(n) => {
|
||||
self.ui.doc_scroll = self.ui.doc_scroll.saturating_sub(n);
|
||||
}
|
||||
AppCommand::DocNextCategory => {
|
||||
let count = doc_view::category_count();
|
||||
self.ui.doc_category = (self.ui.doc_category + 1) % count;
|
||||
self.ui.doc_scroll = 0;
|
||||
}
|
||||
AppCommand::DocPrevCategory => {
|
||||
let count = doc_view::category_count();
|
||||
self.ui.doc_category = (self.ui.doc_category + count - 1) % count;
|
||||
self.ui.doc_scroll = 0;
|
||||
}
|
||||
|
||||
// Patterns view
|
||||
AppCommand::PatternsCursorLeft => {
|
||||
|
||||
@@ -116,6 +116,8 @@ pub enum AppCommand {
|
||||
DocPrevTopic,
|
||||
DocScrollDown(usize),
|
||||
DocScrollUp(usize),
|
||||
DocNextCategory,
|
||||
DocPrevCategory,
|
||||
|
||||
// Patterns view
|
||||
PatternsCursorLeft,
|
||||
|
||||
@@ -1,20 +1,50 @@
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use rusty_link::{AblLink, SessionState};
|
||||
|
||||
pub struct LinkState {
|
||||
link: AblLink,
|
||||
quantum: f64,
|
||||
quantum: AtomicU64,
|
||||
}
|
||||
|
||||
impl LinkState {
|
||||
pub fn new(tempo: f64, quantum: f64) -> Self {
|
||||
let link = AblLink::new(tempo);
|
||||
Self { link, quantum }
|
||||
Self {
|
||||
link,
|
||||
quantum: AtomicU64::new(quantum.to_bits()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.link.is_enabled()
|
||||
}
|
||||
|
||||
pub fn set_enabled(&self, enabled: bool) {
|
||||
self.link.enable(enabled);
|
||||
}
|
||||
|
||||
pub fn enable(&self) {
|
||||
self.link.enable(true);
|
||||
}
|
||||
|
||||
pub fn is_start_stop_sync_enabled(&self) -> bool {
|
||||
self.link.is_start_stop_sync_enabled()
|
||||
}
|
||||
|
||||
pub fn set_start_stop_sync_enabled(&self, enabled: bool) {
|
||||
self.link.enable_start_stop_sync(enabled);
|
||||
}
|
||||
|
||||
pub fn quantum(&self) -> f64 {
|
||||
f64::from_bits(self.quantum.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
pub fn set_quantum(&self, quantum: f64) {
|
||||
let clamped = quantum.clamp(1.0, 16.0);
|
||||
self.quantum.store(clamped.to_bits(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn clock_micros(&self) -> i64 {
|
||||
self.link.clock_micros()
|
||||
}
|
||||
@@ -29,14 +59,14 @@ impl LinkState {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
let time = self.link.clock_micros();
|
||||
state.beat_at_time(time, self.quantum)
|
||||
state.beat_at_time(time, self.quantum())
|
||||
}
|
||||
|
||||
pub fn phase(&self) -> f64 {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
let time = self.link.clock_micros();
|
||||
state.phase_at_time(time, self.quantum)
|
||||
state.phase_at_time(time, self.quantum())
|
||||
}
|
||||
|
||||
pub fn peers(&self) -> u64 {
|
||||
|
||||
@@ -569,6 +569,11 @@ fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
AudioFocus::Channels => ctx.app.audio.adjust_channels(-1),
|
||||
AudioFocus::BufferSize => ctx.app.audio.adjust_buffer_size(-64),
|
||||
AudioFocus::SamplePaths => ctx.app.audio.remove_last_sample_path(),
|
||||
AudioFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
|
||||
AudioFocus::StartStopSync => ctx
|
||||
.link
|
||||
.set_start_stop_sync_enabled(!ctx.link.is_start_stop_sync_enabled()),
|
||||
AudioFocus::Quantum => ctx.link.set_quantum(ctx.link.quantum() - 1.0),
|
||||
},
|
||||
KeyCode::Right => match ctx.app.audio.focus {
|
||||
AudioFocus::OutputDevice => ctx.app.audio.next_output_device(),
|
||||
@@ -576,6 +581,11 @@ fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
AudioFocus::Channels => ctx.app.audio.adjust_channels(1),
|
||||
AudioFocus::BufferSize => ctx.app.audio.adjust_buffer_size(64),
|
||||
AudioFocus::SamplePaths => {}
|
||||
AudioFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
|
||||
AudioFocus::StartStopSync => ctx
|
||||
.link
|
||||
.set_start_stop_sync_enabled(!ctx.link.is_start_stop_sync_enabled()),
|
||||
AudioFocus::Quantum => ctx.link.set_quantum(ctx.link.quantum() + 1.0),
|
||||
},
|
||||
KeyCode::Char('R') => ctx.app.audio.trigger_restart(),
|
||||
KeyCode::Char('A') => {
|
||||
@@ -613,8 +623,12 @@ fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
|
||||
fn handle_doc_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('j') | KeyCode::Down => ctx.dispatch(AppCommand::DocNextTopic),
|
||||
KeyCode::Char('k') | KeyCode::Up => ctx.dispatch(AppCommand::DocPrevTopic),
|
||||
KeyCode::Char('j') | KeyCode::Down => ctx.dispatch(AppCommand::DocScrollDown(1)),
|
||||
KeyCode::Char('k') | KeyCode::Up => ctx.dispatch(AppCommand::DocScrollUp(1)),
|
||||
KeyCode::Char('h') | KeyCode::Left => ctx.dispatch(AppCommand::DocPrevCategory),
|
||||
KeyCode::Char('l') | KeyCode::Right => ctx.dispatch(AppCommand::DocNextCategory),
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::DocNextTopic),
|
||||
KeyCode::BackTab => ctx.dispatch(AppCommand::DocPrevTopic),
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::DocScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::DocScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
mod file;
|
||||
mod forth;
|
||||
pub mod forth;
|
||||
mod project;
|
||||
mod script;
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ pub enum AudioFocus {
|
||||
Channels,
|
||||
BufferSize,
|
||||
SamplePaths,
|
||||
LinkEnabled,
|
||||
StartStopSync,
|
||||
Quantum,
|
||||
}
|
||||
|
||||
pub struct Metrics {
|
||||
@@ -94,17 +97,23 @@ impl AudioSettings {
|
||||
AudioFocus::InputDevice => AudioFocus::Channels,
|
||||
AudioFocus::Channels => AudioFocus::BufferSize,
|
||||
AudioFocus::BufferSize => AudioFocus::SamplePaths,
|
||||
AudioFocus::SamplePaths => AudioFocus::OutputDevice,
|
||||
AudioFocus::SamplePaths => AudioFocus::LinkEnabled,
|
||||
AudioFocus::LinkEnabled => AudioFocus::StartStopSync,
|
||||
AudioFocus::StartStopSync => AudioFocus::Quantum,
|
||||
AudioFocus::Quantum => AudioFocus::OutputDevice,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn prev_focus(&mut self) {
|
||||
self.focus = match self.focus {
|
||||
AudioFocus::OutputDevice => AudioFocus::SamplePaths,
|
||||
AudioFocus::OutputDevice => AudioFocus::Quantum,
|
||||
AudioFocus::InputDevice => AudioFocus::OutputDevice,
|
||||
AudioFocus::Channels => AudioFocus::InputDevice,
|
||||
AudioFocus::BufferSize => AudioFocus::Channels,
|
||||
AudioFocus::SamplePaths => AudioFocus::BufferSize,
|
||||
AudioFocus::LinkEnabled => AudioFocus::SamplePaths,
|
||||
AudioFocus::StartStopSync => AudioFocus::LinkEnabled,
|
||||
AudioFocus::Quantum => AudioFocus::StartStopSync,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ pub struct UiState {
|
||||
pub modal: Modal,
|
||||
pub doc_topic: usize,
|
||||
pub doc_scroll: usize,
|
||||
pub doc_category: usize,
|
||||
pub show_title: bool,
|
||||
}
|
||||
|
||||
@@ -19,6 +20,7 @@ impl Default for UiState {
|
||||
modal: Modal::None,
|
||||
doc_topic: 0,
|
||||
doc_scroll: 0,
|
||||
doc_category: 0,
|
||||
show_title: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Gauge, Paragraph};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Row, Table};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::LinkState;
|
||||
use crate::state::AudioFocus;
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [config_area, stats_area] =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]).areas(area);
|
||||
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let [left_col, _, right_col] = Layout::horizontal([
|
||||
Constraint::Percentage(52),
|
||||
Constraint::Length(2),
|
||||
Constraint::Percentage(48),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
render_config(frame, app, config_area);
|
||||
render_stats(frame, app, stats_area);
|
||||
render_audio_section(frame, app, left_col);
|
||||
render_link_section(frame, app, link, right_col);
|
||||
}
|
||||
|
||||
fn truncate_name(name: &str, max_len: usize) -> String {
|
||||
@@ -23,204 +28,337 @@ fn truncate_name(name: &str, max_len: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_config(frame: &mut Frame, app: &App, area: Rect) {
|
||||
fn render_audio_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Configuration")
|
||||
.title(" Audio ")
|
||||
.border_style(Style::new().fg(Color::Magenta));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let [output_area, input_area, channels_area, buffer_area, rate_area, samples_area, _, hints_area] =
|
||||
Layout::vertical([
|
||||
let padded = Rect {
|
||||
x: inner.x + 1,
|
||||
y: inner.y + 1,
|
||||
width: inner.width.saturating_sub(2),
|
||||
height: inner.height.saturating_sub(1),
|
||||
};
|
||||
|
||||
let [devices_area, _, settings_area, _, samples_area] = Layout::vertical([
|
||||
Constraint::Length(4),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(4),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(3),
|
||||
])
|
||||
.areas(inner);
|
||||
.areas(padded);
|
||||
|
||||
render_devices(frame, app, devices_area);
|
||||
render_settings(frame, app, settings_area);
|
||||
render_samples(frame, app, samples_area);
|
||||
}
|
||||
|
||||
fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let header_style = Style::new()
|
||||
.fg(Color::Rgb(100, 160, 180))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Devices").style(header_style), header_area);
|
||||
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let normal = Style::new().fg(Color::White);
|
||||
let dim = Style::new().fg(Color::DarkGray);
|
||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
|
||||
let output_name = truncate_name(app.audio.current_output_device_name(), 25);
|
||||
let output_style = if app.audio.focus == AudioFocus::OutputDevice {
|
||||
highlight
|
||||
} else {
|
||||
normal
|
||||
};
|
||||
let output_line = Line::from(vec![
|
||||
Span::styled("Output ", dim),
|
||||
Span::styled("< ", output_style),
|
||||
Span::styled(output_name, output_style),
|
||||
Span::styled(" >", output_style),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(output_line), output_area);
|
||||
let output_name = truncate_name(app.audio.current_output_device_name(), 35);
|
||||
let input_name = truncate_name(app.audio.current_input_device_name(), 35);
|
||||
|
||||
let input_name = truncate_name(app.audio.current_input_device_name(), 25);
|
||||
let input_style = if app.audio.focus == AudioFocus::InputDevice {
|
||||
highlight
|
||||
} else {
|
||||
normal
|
||||
};
|
||||
let input_line = Line::from(vec![
|
||||
Span::styled("Input ", dim),
|
||||
Span::styled("< ", input_style),
|
||||
Span::styled(input_name, input_style),
|
||||
Span::styled(" >", input_style),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(input_line), input_area);
|
||||
let output_focused = app.audio.focus == AudioFocus::OutputDevice;
|
||||
let input_focused = app.audio.focus == AudioFocus::InputDevice;
|
||||
|
||||
let channels_style = if app.audio.focus == AudioFocus::Channels {
|
||||
highlight
|
||||
} else {
|
||||
normal
|
||||
};
|
||||
let channels_line = Line::from(vec![
|
||||
Span::styled("Channels ", dim),
|
||||
Span::styled("< ", channels_style),
|
||||
Span::styled(format!("{:2}", app.audio.config.channels), channels_style),
|
||||
Span::styled(" >", channels_style),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(channels_line), channels_area);
|
||||
let rows = vec![
|
||||
Row::new(vec![
|
||||
Span::styled("Output", label_style),
|
||||
render_selector(&output_name, output_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Input", label_style),
|
||||
render_selector(&input_name, input_focused, highlight, normal),
|
||||
]),
|
||||
];
|
||||
|
||||
let buffer_style = if app.audio.focus == AudioFocus::BufferSize {
|
||||
highlight
|
||||
} else {
|
||||
normal
|
||||
};
|
||||
let buffer_line = Line::from(vec![
|
||||
Span::styled("Buffer ", dim),
|
||||
Span::styled("< ", buffer_style),
|
||||
Span::styled(format!("{:4}", app.audio.config.buffer_size), buffer_style),
|
||||
Span::styled(" >", buffer_style),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(buffer_line), buffer_area);
|
||||
let table = Table::new(rows, [Constraint::Length(8), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
}
|
||||
|
||||
let rate_line = Line::from(vec![
|
||||
Span::styled("Rate ", dim),
|
||||
Span::styled(format!("{:.0} Hz", app.audio.config.sample_rate), normal),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(rate_line), rate_area);
|
||||
fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let header_style = Style::new()
|
||||
.fg(Color::Rgb(100, 160, 180))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let samples_style = if app.audio.focus == AudioFocus::SamplePaths {
|
||||
highlight
|
||||
} else {
|
||||
normal
|
||||
};
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
let mut sample_lines = vec![Line::from(vec![
|
||||
Span::styled("Samples ", dim),
|
||||
frame.render_widget(Paragraph::new("Settings").style(header_style), header_area);
|
||||
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let normal = Style::new().fg(Color::White);
|
||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
let value_style = Style::new().fg(Color::Rgb(180, 180, 190));
|
||||
|
||||
let channels_focused = app.audio.focus == AudioFocus::Channels;
|
||||
let buffer_focused = app.audio.focus == AudioFocus::BufferSize;
|
||||
|
||||
let rows = vec![
|
||||
Row::new(vec![
|
||||
Span::styled("Channels", label_style),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.channels),
|
||||
channels_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Buffer", label_style),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.buffer_size),
|
||||
buffer_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Rate", label_style),
|
||||
Span::styled(
|
||||
format!("{:.0} Hz", app.audio.config.sample_rate),
|
||||
value_style,
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(8), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
}
|
||||
|
||||
fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let header_style = Style::new()
|
||||
.fg(Color::Rgb(100, 160, 180))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let samples_focused = app.audio.focus == AudioFocus::SamplePaths;
|
||||
|
||||
let header_text = format!(
|
||||
"Samples {} paths · {} indexed",
|
||||
app.audio.config.sample_paths.len(),
|
||||
app.audio.config.sample_count
|
||||
);
|
||||
|
||||
let header_line = if samples_focused {
|
||||
Line::from(vec![
|
||||
Span::styled("Samples ", header_style),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{} paths, {} indexed",
|
||||
"{} paths · {} indexed",
|
||||
app.audio.config.sample_paths.len(),
|
||||
app.audio.config.sample_count
|
||||
),
|
||||
samples_style,
|
||||
highlight,
|
||||
),
|
||||
])];
|
||||
])
|
||||
} else {
|
||||
Line::from(Span::styled(header_text, header_style))
|
||||
};
|
||||
frame.render_widget(Paragraph::new(header_line), header_area);
|
||||
|
||||
for (i, path) in app.audio.config.sample_paths.iter().take(2).enumerate() {
|
||||
let dim = Style::new().fg(Color::Rgb(80, 85, 95));
|
||||
let path_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
for (i, path) in app.audio.config.sample_paths.iter().take(4).enumerate() {
|
||||
let path_str = path.to_string_lossy();
|
||||
let display = truncate_name(&path_str, 35);
|
||||
sample_lines.push(Line::from(vec![
|
||||
Span::styled(" ", dim),
|
||||
Span::styled(
|
||||
format!("{}: {}", i + 1, display),
|
||||
Style::new().fg(Color::DarkGray),
|
||||
),
|
||||
let display = truncate_name(&path_str, 45);
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {} ", i + 1), dim),
|
||||
Span::styled(display, path_style),
|
||||
]));
|
||||
}
|
||||
|
||||
frame.render_widget(Paragraph::new(sample_lines), samples_area);
|
||||
if lines.is_empty() {
|
||||
lines.push(Line::from(Span::styled(
|
||||
" No sample paths configured",
|
||||
dim,
|
||||
)));
|
||||
}
|
||||
|
||||
let hints_line = Line::from(vec![
|
||||
Span::styled("[R] Restart ", Style::new().fg(Color::Cyan)),
|
||||
Span::styled("[A] Add path ", Style::new().fg(Color::DarkGray)),
|
||||
Span::styled("[D] Refresh", Style::new().fg(Color::DarkGray)),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hints_line), hints_area);
|
||||
frame.render_widget(Paragraph::new(lines), content_area);
|
||||
}
|
||||
|
||||
fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
|
||||
fn render_link_section(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Engine Stats")
|
||||
.title(" Ableton Link ")
|
||||
.border_style(Style::new().fg(Color::Cyan));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let [cpu_area, voices_area, extra_area] = Layout::vertical([
|
||||
let padded = Rect {
|
||||
x: inner.x + 1,
|
||||
y: inner.y + 1,
|
||||
width: inner.width.saturating_sub(2),
|
||||
height: inner.height.saturating_sub(1),
|
||||
};
|
||||
|
||||
let [status_area, _, config_area, _, info_area] = Layout::vertical([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
])
|
||||
.areas(inner);
|
||||
.areas(padded);
|
||||
|
||||
let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0);
|
||||
let cpu_color = if cpu_pct > 80.0 {
|
||||
Color::Red
|
||||
} else if cpu_pct > 50.0 {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::Green
|
||||
};
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("CPU"))
|
||||
.gauge_style(Style::new().fg(cpu_color).bg(Color::DarkGray))
|
||||
.percent(cpu_pct as u16)
|
||||
.label(format!("{cpu_pct:.1}%"));
|
||||
|
||||
frame.render_widget(gauge, cpu_area);
|
||||
|
||||
let voice_color = if app.metrics.active_voices > 24 {
|
||||
Color::Red
|
||||
} else if app.metrics.active_voices > 16 {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::Cyan
|
||||
};
|
||||
|
||||
let voices = Paragraph::new(Line::from(vec![
|
||||
Span::raw("Active: "),
|
||||
Span::styled(
|
||||
format!("{:3}", app.metrics.active_voices),
|
||||
Style::new().fg(voice_color).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" Peak: "),
|
||||
Span::styled(
|
||||
format!("{:3}", app.metrics.peak_voices),
|
||||
Style::new().fg(Color::Yellow),
|
||||
),
|
||||
]));
|
||||
|
||||
frame.render_widget(voices, voices_area);
|
||||
|
||||
let extra = Paragraph::new(vec![
|
||||
Line::from(vec![
|
||||
Span::raw("Schedule: "),
|
||||
Span::styled(
|
||||
format!("{}", app.metrics.schedule_depth),
|
||||
Style::new().fg(Color::White),
|
||||
),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw("Pool: "),
|
||||
Span::styled(
|
||||
format!("{:.1} MB", app.sample_pool_mb),
|
||||
Style::new().fg(Color::White),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
frame.render_widget(extra, extra_area);
|
||||
render_link_status(frame, link, status_area);
|
||||
render_link_config(frame, app, link, config_area);
|
||||
render_link_info(frame, link, info_area);
|
||||
}
|
||||
|
||||
fn render_link_status(frame: &mut Frame, link: &LinkState, area: Rect) {
|
||||
let enabled = link.is_enabled();
|
||||
let peers = link.peers();
|
||||
|
||||
let (status_text, status_color) = if !enabled {
|
||||
("DISABLED", Color::Rgb(120, 60, 60))
|
||||
} else if peers > 0 {
|
||||
("CONNECTED", Color::Rgb(60, 120, 60))
|
||||
} else {
|
||||
("LISTENING", Color::Rgb(120, 120, 60))
|
||||
};
|
||||
|
||||
let status_style = Style::new().fg(status_color).add_modifier(Modifier::BOLD);
|
||||
|
||||
let peer_text = if enabled {
|
||||
if peers == 0 {
|
||||
"No peers".to_string()
|
||||
} else if peers == 1 {
|
||||
"1 peer".to_string()
|
||||
} else {
|
||||
format!("{peers} peers")
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let lines = vec![
|
||||
Line::from(Span::styled(status_text, status_style)),
|
||||
Line::from(Span::styled(
|
||||
peer_text,
|
||||
Style::new().fg(Color::Rgb(120, 125, 135)),
|
||||
)),
|
||||
];
|
||||
|
||||
frame.render_widget(Paragraph::new(lines).alignment(Alignment::Center), area);
|
||||
}
|
||||
|
||||
fn render_link_config(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let header_style = Style::new()
|
||||
.fg(Color::Rgb(100, 160, 180))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new("Configuration").style(header_style),
|
||||
header_area,
|
||||
);
|
||||
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let normal = Style::new().fg(Color::White);
|
||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
|
||||
let enabled_focused = app.audio.focus == AudioFocus::LinkEnabled;
|
||||
let startstop_focused = app.audio.focus == AudioFocus::StartStopSync;
|
||||
let quantum_focused = app.audio.focus == AudioFocus::Quantum;
|
||||
|
||||
let enabled_text = if link.is_enabled() { "On" } else { "Off" };
|
||||
let startstop_text = if link.is_start_stop_sync_enabled() {
|
||||
"On"
|
||||
} else {
|
||||
"Off"
|
||||
};
|
||||
let quantum_text = format!("{:.0}", link.quantum());
|
||||
|
||||
let rows = vec![
|
||||
Row::new(vec![
|
||||
Span::styled("Enabled", label_style),
|
||||
render_selector(enabled_text, enabled_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Start/Stop", label_style),
|
||||
render_selector(startstop_text, startstop_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Quantum", label_style),
|
||||
render_selector(&quantum_text, quantum_focused, highlight, normal),
|
||||
]),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(10), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
}
|
||||
|
||||
fn render_link_info(frame: &mut Frame, link: &LinkState, area: Rect) {
|
||||
let header_style = Style::new()
|
||||
.fg(Color::Rgb(100, 160, 180))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Session").style(header_style), header_area);
|
||||
|
||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
let value_style = Style::new().fg(Color::Rgb(180, 180, 190));
|
||||
let tempo_style = Style::new()
|
||||
.fg(Color::Rgb(220, 180, 100))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let tempo = link.tempo();
|
||||
let beat = link.beat();
|
||||
let phase = link.phase();
|
||||
|
||||
let rows = vec![
|
||||
Row::new(vec![
|
||||
Span::styled("Tempo", label_style),
|
||||
Span::styled(format!("{tempo:.1} BPM"), tempo_style),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Beat", label_style),
|
||||
Span::styled(format!("{beat:.2}"), value_style),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Phase", label_style),
|
||||
Span::styled(format!("{phase:.2}"), value_style),
|
||||
]),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(10), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
}
|
||||
|
||||
fn render_selector(value: &str, focused: bool, highlight: Style, normal: Style) -> Span<'static> {
|
||||
let style = if focused { highlight } else { normal };
|
||||
if focused {
|
||||
Span::styled(format!("< {value} >"), style)
|
||||
} else {
|
||||
Span::styled(format!(" {value} "), style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,26 +6,49 @@ use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::model::forth::{Word, WordCompile, WORDS};
|
||||
|
||||
const DOCS: &[(&str, &str)] = &[
|
||||
const STATIC_DOCS: &[(&str, &str)] = &[
|
||||
("Keybindings", include_str!("../../docs/keybindings.md")),
|
||||
("Scripting", include_str!("../../docs/scripting.md")),
|
||||
("Sequencer", include_str!("../../docs/sequencer.md")),
|
||||
];
|
||||
|
||||
const TOPICS: &[&str] = &["Keybindings", "Forth Reference", "Sequencer"];
|
||||
|
||||
const CATEGORIES: &[&str] = &[
|
||||
"Stack",
|
||||
"Arithmetic",
|
||||
"Comparison",
|
||||
"Logic",
|
||||
"Sound",
|
||||
"Variables",
|
||||
"Randomness",
|
||||
"Probability",
|
||||
"Context",
|
||||
"Music",
|
||||
"Time",
|
||||
"Parameters",
|
||||
];
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [topics_area, content_area] =
|
||||
Layout::horizontal([Constraint::Length(18), Constraint::Fill(1)]).areas(area);
|
||||
|
||||
render_topics(frame, app, topics_area);
|
||||
render_content(frame, app, content_area);
|
||||
|
||||
let topic = TOPICS[app.ui.doc_topic];
|
||||
if topic == "Forth Reference" {
|
||||
render_forth_reference(frame, app, content_area);
|
||||
} else {
|
||||
render_markdown_content(frame, app, content_area, topic);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let items: Vec<ListItem> = DOCS
|
||||
let items: Vec<ListItem> = TOPICS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (name, _))| {
|
||||
.map(|(i, name)| {
|
||||
let style = if i == app.ui.doc_topic {
|
||||
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
@@ -40,8 +63,12 @@ fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
|
||||
frame.render_widget(list, area);
|
||||
}
|
||||
|
||||
fn render_content(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let (title, md) = DOCS[app.ui.doc_topic];
|
||||
fn render_markdown_content(frame: &mut Frame, app: &App, area: Rect, topic: &str) {
|
||||
let md = STATIC_DOCS
|
||||
.iter()
|
||||
.find(|(name, _)| *name == topic)
|
||||
.map(|(_, content)| *content)
|
||||
.unwrap_or("");
|
||||
let lines = parse_markdown(md);
|
||||
|
||||
let visible_height = area.height.saturating_sub(2) as usize;
|
||||
@@ -55,10 +82,113 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
|
||||
.take(visible_height)
|
||||
.collect();
|
||||
|
||||
let para = Paragraph::new(visible).block(Block::default().borders(Borders::ALL).title(topic));
|
||||
frame.render_widget(para, area);
|
||||
}
|
||||
|
||||
fn render_forth_reference(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [cat_area, words_area] =
|
||||
Layout::horizontal([Constraint::Length(14), Constraint::Fill(1)]).areas(area);
|
||||
|
||||
render_categories(frame, app, cat_area);
|
||||
render_words(frame, app, words_area);
|
||||
}
|
||||
|
||||
fn render_categories(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let items: Vec<ListItem> = CATEGORIES
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| {
|
||||
let style = if i == app.ui.doc_category {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::White)
|
||||
};
|
||||
let prefix = if i == app.ui.doc_category { "> " } else { " " };
|
||||
ListItem::new(format!("{prefix}{name}")).style(style)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items).block(Block::default().borders(Borders::ALL).title("Category"));
|
||||
frame.render_widget(list, area);
|
||||
}
|
||||
|
||||
fn render_words(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let category = CATEGORIES[app.ui.doc_category];
|
||||
let words: Vec<&Word> = WORDS
|
||||
.iter()
|
||||
.filter(|w| word_category(w.name, &w.compile) == category)
|
||||
.collect();
|
||||
|
||||
let word_style = Style::new().fg(Color::Green).add_modifier(Modifier::BOLD);
|
||||
let stack_style = Style::new().fg(Color::Magenta);
|
||||
let desc_style = Style::new().fg(Color::White);
|
||||
let example_style = Style::new().fg(Color::Rgb(150, 150, 150));
|
||||
|
||||
let mut lines: Vec<RLine> = Vec::new();
|
||||
|
||||
for word in &words {
|
||||
lines.push(RLine::from(vec![
|
||||
Span::styled(format!("{:<14}", word.name), word_style),
|
||||
Span::styled(format!("{:<18}", word.stack), stack_style),
|
||||
Span::styled(word.desc.to_string(), desc_style),
|
||||
]));
|
||||
lines.push(RLine::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(format!("e.g. {}", word.example), example_style),
|
||||
]));
|
||||
lines.push(RLine::from(""));
|
||||
}
|
||||
|
||||
let visible_height = area.height.saturating_sub(2) as usize;
|
||||
let total_lines = lines.len();
|
||||
let max_scroll = total_lines.saturating_sub(visible_height);
|
||||
let scroll = app.ui.doc_scroll.min(max_scroll);
|
||||
|
||||
let visible: Vec<RLine> = lines
|
||||
.into_iter()
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.collect();
|
||||
|
||||
let title = format!("{category} ({} words)", words.len());
|
||||
let para = Paragraph::new(visible).block(Block::default().borders(Borders::ALL).title(title));
|
||||
frame.render_widget(para, area);
|
||||
}
|
||||
|
||||
fn word_category(name: &str, compile: &WordCompile) -> &'static str {
|
||||
const STACK: &[&str] = &["dup", "drop", "swap", "over", "rot", "nip", "tuck"];
|
||||
const ARITH: &[&str] = &[
|
||||
"+", "-", "*", "/", "mod", "neg", "abs", "floor", "ceil", "round", "min", "max",
|
||||
];
|
||||
const CMP: &[&str] = &["=", "<>", "<", ">", "<=", ">="];
|
||||
const LOGIC: &[&str] = &["and", "or", "not"];
|
||||
const SOUND: &[&str] = &["sound", "s", "emit"];
|
||||
const VAR: &[&str] = &["get", "set"];
|
||||
const RAND: &[&str] = &["rand", "rrand", "seed", "coin", "chance", "choose", "cycle"];
|
||||
const MUSIC: &[&str] = &["mtof", "ftom"];
|
||||
const TIME: &[&str] = &[
|
||||
"at", "window", "pop", "div", "each", "tempo!", "[", "]", "?",
|
||||
];
|
||||
|
||||
match compile {
|
||||
WordCompile::Simple if STACK.contains(&name) => "Stack",
|
||||
WordCompile::Simple if ARITH.contains(&name) => "Arithmetic",
|
||||
WordCompile::Simple if CMP.contains(&name) => "Comparison",
|
||||
WordCompile::Simple if LOGIC.contains(&name) => "Logic",
|
||||
WordCompile::Simple if SOUND.contains(&name) => "Sound",
|
||||
WordCompile::Alias(_) => "Sound",
|
||||
WordCompile::Simple if VAR.contains(&name) => "Variables",
|
||||
WordCompile::Simple if RAND.contains(&name) => "Randomness",
|
||||
WordCompile::Probability(_) => "Probability",
|
||||
WordCompile::Context(_) => "Context",
|
||||
WordCompile::Simple if MUSIC.contains(&name) => "Music",
|
||||
WordCompile::Simple if TIME.contains(&name) => "Time",
|
||||
WordCompile::Param => "Parameters",
|
||||
_ => "Other",
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
|
||||
let text = minimad::Text::from(md);
|
||||
let mut lines = Vec::new();
|
||||
@@ -128,5 +258,9 @@ fn compound_to_span(compound: Compound, base: Style) -> Span<'static> {
|
||||
}
|
||||
|
||||
pub fn topic_count() -> usize {
|
||||
DOCS.len()
|
||||
TOPICS.len()
|
||||
}
|
||||
|
||||
pub fn category_count() -> usize {
|
||||
CATEGORIES.len()
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ pub fn tokenize_line(line: &str) -> Vec<Token> {
|
||||
|
||||
if c == '"' {
|
||||
let mut end = start + 1;
|
||||
while let Some((i, ch)) = chars.next() {
|
||||
for (i, ch) in chars.by_ref() {
|
||||
end = i + ch.len_utf8();
|
||||
if ch == '"' {
|
||||
break;
|
||||
|
||||
@@ -286,7 +286,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
// Column 2: length
|
||||
let length_line = Line::from(vec![
|
||||
Span::styled("Length: ", bold_style),
|
||||
Span::styled(format!("{}", length), base_style),
|
||||
Span::styled(format!("{length}"), base_style),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(length_line), length_area);
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq
|
||||
match app.page {
|
||||
Page::Main => main_view::render(frame, app, snapshot, body_area),
|
||||
Page::Patterns => patterns_view::render(frame, app, snapshot, body_area),
|
||||
Page::Audio => audio_view::render(frame, app, body_area),
|
||||
Page::Audio => audio_view::render(frame, app, link, body_area),
|
||||
Page::Doc => doc_view::render(frame, app, body_area),
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ fn render_header(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let bank_name = bank
|
||||
.name
|
||||
.as_deref()
|
||||
.map(|n| format!(" {} ", n))
|
||||
.map(|n| format!(" {n} "))
|
||||
.unwrap_or_else(|| format!(" Bank {:02} ", app.editor_ctx.bank + 1));
|
||||
let bank_style = Style::new().bg(Color::Rgb(30, 60, 70)).fg(Color::White);
|
||||
frame.render_widget(
|
||||
@@ -129,7 +129,7 @@ fn render_header(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0);
|
||||
let peers = link.peers();
|
||||
let voices = app.metrics.active_voices;
|
||||
let stats_text = format!(" CPU {:.0}% V:{} L:{} ", cpu_pct, voices, peers);
|
||||
let stats_text = format!(" CPU {cpu_pct:.0}% V:{voices} L:{peers} ");
|
||||
let stats_style = Style::new()
|
||||
.bg(Color::Rgb(35, 35, 40))
|
||||
.fg(Color::Rgb(150, 150, 160));
|
||||
|
||||
Reference in New Issue
Block a user