Feat: begin slight refactoring
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s

This commit is contained in:
2026-02-01 12:38:48 +01:00
parent a0585b0814
commit dd853b8e1b
39 changed files with 4699 additions and 3168 deletions

View File

@@ -5,6 +5,8 @@ mod types;
mod vm;
mod words;
pub use types::{CcMemory, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables};
pub use types::{
CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables,
};
pub use vm::Forth;
pub use words::{Word, WordCompile, WORDS};

View File

@@ -4,7 +4,11 @@ use std::sync::{Arc, Mutex};
use super::ops::Op;
pub const MAX_MIDI_DEVICES: usize = 4;
/// Trait for accessing MIDI CC values. Implement this to provide CC memory to the Forth VM.
pub trait CcAccess: Send + Sync {
/// Get the CC value for a given device, channel (0-15), and CC number (0-127).
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8;
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SourceSpan {
@@ -31,7 +35,7 @@ pub struct StepContext {
pub speed: f64,
pub fill: bool,
pub nudge_secs: f64,
pub cc_memory: Option<CcMemory>,
pub cc_access: Option<Arc<dyn CcAccess>>,
#[cfg(feature = "desktop")]
pub mouse_x: f64,
#[cfg(feature = "desktop")]
@@ -50,7 +54,6 @@ pub type Variables = Arc<Mutex<HashMap<String, Value>>>;
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
pub type Rng = Arc<Mutex<StdRng>>;
pub type Stack = Arc<Mutex<Vec<Value>>>;
pub type CcMemory = Arc<Mutex<[[[u8; 128]; 16]; MAX_MIDI_DEVICES]>>;
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(String, Value)]);
#[derive(Clone, Debug)]
@@ -134,7 +137,6 @@ pub(super) struct CmdRegister {
deltas: Vec<Value>,
}
impl CmdRegister {
pub(super) fn set_sound(&mut self, val: Value) {
self.sound = Some(val);
@@ -165,4 +167,3 @@ impl CmdRegister {
self.params.clear();
}
}

View File

@@ -817,9 +817,7 @@ impl Forth {
let chan = get_int("chan")
.map(|c| (c.clamp(1, 16) - 1) as u8)
.unwrap_or(0);
let dev = get_int("dev")
.map(|d| d.clamp(0, 3) as u8)
.unwrap_or(0);
let dev = get_int("dev").map(|d| d.clamp(0, 3) as u8).unwrap_or(0);
if let (Some(cc), Some(val)) = (get_int("ccnum"), get_int("ccout")) {
let cc = cc.clamp(0, 127) as u8;
@@ -840,51 +838,29 @@ impl Forth {
let velocity = get_int("velocity").unwrap_or(100).clamp(0, 127) as u8;
let dur = get_float("dur").unwrap_or(1.0);
let dur_secs = dur * ctx.step_duration();
outputs.push(format!("/midi/note/{note}/vel/{velocity}/chan/{chan}/dur/{dur_secs}/dev/{dev}"));
outputs.push(format!(
"/midi/note/{note}/vel/{velocity}/chan/{chan}/dur/{dur_secs}/dev/{dev}"
));
}
}
Op::MidiClock => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let dev = params
.iter()
.rev()
.find(|(k, _)| k == "dev")
.and_then(|(_, v)| v.as_int().ok())
.map(|d| d.clamp(0, 3) as u8)
.unwrap_or(0);
let dev = extract_dev_param(params);
outputs.push(format!("/midi/clock/dev/{dev}"));
}
Op::MidiStart => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let dev = params
.iter()
.rev()
.find(|(k, _)| k == "dev")
.and_then(|(_, v)| v.as_int().ok())
.map(|d| d.clamp(0, 3) as u8)
.unwrap_or(0);
let dev = extract_dev_param(params);
outputs.push(format!("/midi/start/dev/{dev}"));
}
Op::MidiStop => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let dev = params
.iter()
.rev()
.find(|(k, _)| k == "dev")
.and_then(|(_, v)| v.as_int().ok())
.map(|d| d.clamp(0, 3) as u8)
.unwrap_or(0);
let dev = extract_dev_param(params);
outputs.push(format!("/midi/stop/dev/{dev}"));
}
Op::MidiContinue => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let dev = params
.iter()
.rev()
.find(|(k, _)| k == "dev")
.and_then(|(_, v)| v.as_int().ok())
.map(|d| d.clamp(0, 3) as u8)
.unwrap_or(0);
let dev = extract_dev_param(params);
outputs.push(format!("/midi/continue/dev/{dev}"));
}
Op::GetMidiCC => {
@@ -893,18 +869,11 @@ impl Forth {
let cc_clamped = (cc.clamp(0, 127)) as usize;
let chan_clamped = (chan.clamp(1, 16) - 1) as usize;
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let dev = params
.iter()
.rev()
.find(|(k, _)| k == "dev")
.and_then(|(_, v)| v.as_int().ok())
.map(|d| d.clamp(0, 3) as usize)
.unwrap_or(0);
let dev = extract_dev_param(params) as usize;
let val = ctx
.cc_memory
.cc_access
.as_ref()
.and_then(|mem| mem.lock().ok())
.map(|mem| mem[dev][chan_clamped][cc_clamped])
.map(|cc| cc.get_cc(dev, chan_clamped, cc_clamped))
.unwrap_or(0);
stack.push(Value::Int(val as i64, None));
}
@@ -916,6 +885,16 @@ impl Forth {
}
}
fn extract_dev_param(params: &[(String, Value)]) -> u8 {
params
.iter()
.rev()
.find(|(k, _)| k == "dev")
.and_then(|(_, v)| v.as_int().ok())
.map(|d| d.clamp(0, 3) as u8)
.unwrap_or(0)
}
fn is_tempo_scaled_param(name: &str) -> bool {
matches!(
name,

View File

@@ -1,3 +1,6 @@
use std::collections::HashMap;
use std::sync::LazyLock;
use super::ops::Op;
use super::theory;
use super::types::{Dictionary, SourceSpan};
@@ -2446,6 +2449,21 @@ pub const WORDS: &[Word] = &[
},
];
static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(|| {
let mut map = HashMap::with_capacity(WORDS.len() * 2);
for word in WORDS {
map.insert(word.name, word);
for alias in word.aliases {
map.insert(alias, word);
}
}
map
});
fn lookup_word(name: &str) -> Option<&'static Word> {
WORD_MAP.get(name).copied()
}
pub(super) fn simple_op(name: &str) -> Option<Op> {
Some(match name {
"dup" => Op::Dup,
@@ -2636,8 +2654,7 @@ pub(super) fn compile_word(
return true;
}
for word in WORDS {
if word.name == name || word.aliases.contains(&name) {
if let Some(word) = lookup_word(name) {
match &word.compile {
Simple => {
if let Some(op) = simple_op(word.name) {
@@ -2653,7 +2670,6 @@ pub(super) fn compile_word(
}
return true;
}
}
// @varname - fetch variable
if let Some(var_name) = name.strip_prefix('@') {

View File

@@ -1,7 +1,6 @@
use crate::theme::{browser, input, ui};
use ratatui::style::Color;
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Style;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
@@ -14,7 +13,7 @@ pub struct FileBrowserModal<'a> {
entries: &'a [(String, bool, bool)],
selected: usize,
scroll_offset: usize,
border_color: Color,
border_color: Option<Color>,
width: u16,
height: u16,
}
@@ -27,7 +26,7 @@ impl<'a> FileBrowserModal<'a> {
entries,
selected: 0,
scroll_offset: 0,
border_color: ui::TEXT_PRIMARY,
border_color: None,
width: 60,
height: 16,
}
@@ -44,7 +43,7 @@ impl<'a> FileBrowserModal<'a> {
}
pub fn border_color(mut self, c: Color) -> Self {
self.border_color = c;
self.border_color = Some(c);
self
}
@@ -59,10 +58,13 @@ impl<'a> FileBrowserModal<'a> {
}
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
let colors = theme::get();
let border_color = self.border_color.unwrap_or(colors.ui.text_primary);
let inner = ModalFrame::new(self.title)
.width(self.width)
.height(self.height)
.border_color(self.border_color)
.border_color(border_color)
.render_centered(frame, term);
let rows = Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).split(inner);
@@ -71,8 +73,8 @@ impl<'a> FileBrowserModal<'a> {
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::raw("> "),
Span::styled(self.input, Style::new().fg(input::TEXT)),
Span::styled("", Style::new().fg(input::CURSOR)),
Span::styled(self.input, Style::new().fg(colors.input.text)),
Span::styled("", Style::new().fg(colors.input.cursor)),
])),
rows[0],
);
@@ -97,13 +99,13 @@ impl<'a> FileBrowserModal<'a> {
format!("{prefix}{name}")
};
let color = if is_selected {
browser::SELECTED
colors.browser.selected
} else if *is_dir {
browser::DIRECTORY
colors.browser.directory
} else if *is_cagire {
browser::PROJECT_FILE
colors.browser.project_file
} else {
browser::FILE
colors.browser.file
};
Line::from(Span::styled(display, Style::new().fg(color)))
})

View File

@@ -1,4 +1,4 @@
use crate::theme::{hint, ui};
use crate::theme;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
@@ -51,10 +51,11 @@ impl<'a> ListSelect<'a> {
}
pub fn render(self, frame: &mut Frame, area: Rect) {
let cursor_style = Style::new().fg(hint::KEY).add_modifier(Modifier::BOLD);
let selected_style = Style::new().fg(ui::ACCENT);
let colors = theme::get();
let cursor_style = Style::new().fg(colors.hint.key).add_modifier(Modifier::BOLD);
let selected_style = Style::new().fg(colors.ui.accent);
let normal_style = Style::default();
let indicator_style = Style::new().fg(ui::TEXT_DIM);
let indicator_style = Style::new().fg(colors.ui.text_dim);
let visible_end = (self.scroll_offset + self.visible_count).min(self.items.len());
let has_above = self.scroll_offset > 0;

View File

@@ -1,4 +1,4 @@
use crate::theme::{browser, search};
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
@@ -59,10 +59,11 @@ impl<'a> SampleBrowser<'a> {
}
pub fn render(self, frame: &mut Frame, area: Rect) {
let colors = theme::get();
let border_style = if self.focused {
Style::new().fg(browser::FOCUSED_BORDER)
Style::new().fg(colors.browser.focused_border)
} else {
Style::new().fg(browser::UNFOCUSED_BORDER)
Style::new().fg(colors.browser.unfocused_border)
};
let block = Block::default()
@@ -90,16 +91,16 @@ impl<'a> SampleBrowser<'a> {
};
if let Some(sa) = search_area {
self.render_search(frame, sa);
self.render_search(frame, sa, &colors);
}
self.render_tree(frame, list_area);
self.render_tree(frame, list_area, &colors);
}
fn render_search(&self, frame: &mut Frame, area: Rect) {
fn render_search(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
let style = if self.search_active {
Style::new().fg(search::ACTIVE)
Style::new().fg(colors.search.active)
} else {
Style::new().fg(search::INACTIVE)
Style::new().fg(colors.search.inactive)
};
let cursor = if self.search_active { "_" } else { "" };
let text = format!("/{}{}", self.search_query, cursor);
@@ -107,7 +108,7 @@ impl<'a> SampleBrowser<'a> {
frame.render_widget(Paragraph::new(vec![line]), area);
}
fn render_tree(&self, frame: &mut Frame, area: Rect) {
fn render_tree(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
let height = area.height as usize;
if self.entries.is_empty() {
let msg = if self.search_query.is_empty() {
@@ -115,7 +116,7 @@ impl<'a> SampleBrowser<'a> {
} else {
"No matches"
};
let line = Line::from(Span::styled(msg, Style::new().fg(browser::EMPTY_TEXT)));
let line = Line::from(Span::styled(msg, Style::new().fg(colors.browser.empty_text)));
frame.render_widget(Paragraph::new(vec![line]), area);
return;
}
@@ -130,23 +131,23 @@ impl<'a> SampleBrowser<'a> {
let (icon, icon_color) = match entry.kind {
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
("\u{25BC} ", browser::FOLDER_ICON)
("\u{25BC} ", colors.browser.folder_icon)
}
TreeLineKind::Root { expanded: false }
| TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", browser::FOLDER_ICON),
TreeLineKind::File => ("\u{266A} ", browser::FILE_ICON),
| TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", colors.browser.folder_icon),
TreeLineKind::File => ("\u{266A} ", colors.browser.file_icon),
};
let label_style = if is_cursor && self.focused {
Style::new().fg(browser::SELECTED).add_modifier(Modifier::BOLD)
Style::new().fg(colors.browser.selected).add_modifier(Modifier::BOLD)
} else if is_cursor {
Style::new().fg(browser::FILE)
Style::new().fg(colors.browser.file)
} else {
match entry.kind {
TreeLineKind::Root { .. } => {
Style::new().fg(browser::ROOT).add_modifier(Modifier::BOLD)
Style::new().fg(colors.browser.root).add_modifier(Modifier::BOLD)
}
TreeLineKind::Folder { .. } => Style::new().fg(browser::DIRECTORY),
TreeLineKind::Folder { .. } => Style::new().fg(colors.browser.directory),
TreeLineKind::File => Style::default(),
}
};

View File

@@ -1,4 +1,4 @@
use crate::theme::meter;
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
@@ -18,7 +18,7 @@ pub enum Orientation {
pub struct Scope<'a> {
data: &'a [f32],
orientation: Orientation,
color: Color,
color: Option<Color>,
gain: f32,
}
@@ -27,7 +27,7 @@ impl<'a> Scope<'a> {
Self {
data,
orientation: Orientation::Horizontal,
color: meter::LOW,
color: None,
gain: 1.0,
}
}
@@ -38,7 +38,7 @@ impl<'a> Scope<'a> {
}
pub fn color(mut self, c: Color) -> Self {
self.color = c;
self.color = Some(c);
self
}
}
@@ -49,11 +49,13 @@ impl Widget for Scope<'_> {
return;
}
let color = self.color.unwrap_or_else(|| theme::get().meter.low);
match self.orientation {
Orientation::Horizontal => {
render_horizontal(self.data, area, buf, self.color, self.gain)
render_horizontal(self.data, area, buf, color, self.gain)
}
Orientation::Vertical => render_vertical(self.data, area, buf, self.color, self.gain),
Orientation::Vertical => render_vertical(self.data, area, buf, color, self.gain),
}
}
}
@@ -64,7 +66,6 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
let fine_width = width * 2;
let fine_height = height * 4;
// Auto-scale: find peak amplitude and normalize to fill height
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
@@ -121,7 +122,6 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
let fine_width = width * 2;
let fine_height = height * 4;
// Auto-scale: find peak amplitude and normalize to fill width
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let auto_gain = if peak > 0.001 { gain / peak } else { gain };

View File

@@ -1,4 +1,4 @@
use crate::theme::sparkle;
use crate::theme;
use rand::Rng;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -41,8 +41,9 @@ impl Sparkles {
impl Widget for &Sparkles {
fn render(self, area: Rect, buf: &mut Buffer) {
let colors = theme::get().sparkle.colors;
for sp in &self.sparkles {
let color = sparkle::COLORS[sp.char_idx % sparkle::COLORS.len()];
let color = colors[sp.char_idx % colors.len()];
let intensity = (sp.life as f32 / 30.0).min(1.0);
let r = (color.0 as f32 * intensity) as u8;
let g = (color.1 as f32 * intensity) as u8;

View File

@@ -1,4 +1,4 @@
use crate::theme::meter;
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
@@ -22,6 +22,7 @@ impl Widget for Spectrum<'_> {
return;
}
let colors = theme::get();
let height = area.height as f32;
let band_width = area.width as usize / 32;
if band_width == 0 {
@@ -40,11 +41,11 @@ impl Widget for Spectrum<'_> {
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(meter::LOW_RGB.0, meter::LOW_RGB.1, meter::LOW_RGB.2)
Color::Rgb(colors.meter.low_rgb.0, colors.meter.low_rgb.1, colors.meter.low_rgb.2)
} else if ratio < 0.66 {
Color::Rgb(meter::MID_RGB.0, meter::MID_RGB.1, meter::MID_RGB.2)
Color::Rgb(colors.meter.mid_rgb.0, colors.meter.mid_rgb.1, colors.meter.mid_rgb.2)
} else {
Color::Rgb(meter::HIGH_RGB.0, meter::HIGH_RGB.1, meter::HIGH_RGB.2)
Color::Rgb(colors.meter.high_rgb.0, colors.meter.high_rgb.1, colors.meter.high_rgb.2)
};
for dx in 0..band_width as u16 {
let x = x_start + dx;

View File

@@ -1,4 +1,4 @@
use crate::theme::{input, ui};
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
@@ -11,7 +11,7 @@ pub struct TextInputModal<'a> {
title: &'a str,
input: &'a str,
hint: Option<&'a str>,
border_color: Color,
border_color: Option<Color>,
width: u16,
}
@@ -21,7 +21,7 @@ impl<'a> TextInputModal<'a> {
title,
input,
hint: None,
border_color: ui::TEXT_PRIMARY,
border_color: None,
width: 50,
}
}
@@ -32,7 +32,7 @@ impl<'a> TextInputModal<'a> {
}
pub fn border_color(mut self, c: Color) -> Self {
self.border_color = c;
self.border_color = Some(c);
self
}
@@ -42,12 +42,14 @@ impl<'a> TextInputModal<'a> {
}
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
let colors = theme::get();
let border_color = self.border_color.unwrap_or(colors.ui.text_primary);
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)
.border_color(border_color)
.render_centered(frame, term);
if self.hint.is_some() {
@@ -57,15 +59,15 @@ impl<'a> TextInputModal<'a> {
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::raw("> "),
Span::styled(self.input, Style::new().fg(input::TEXT)),
Span::styled("", Style::new().fg(input::CURSOR)),
Span::styled(self.input, Style::new().fg(colors.input.text)),
Span::styled("", Style::new().fg(colors.input.cursor)),
])),
rows[0],
);
if let Some(hint) = self.hint {
frame.render_widget(
Paragraph::new(Span::styled(hint, Style::new().fg(input::HINT))),
Paragraph::new(Span::styled(hint, Style::new().fg(colors.input.hint))),
rows[1],
);
}
@@ -73,8 +75,8 @@ impl<'a> TextInputModal<'a> {
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::raw("> "),
Span::styled(self.input, Style::new().fg(input::TEXT)),
Span::styled("", Style::new().fg(input::CURSOR)),
Span::styled(self.input, Style::new().fg(colors.input.text)),
Span::styled("", Style::new().fg(colors.input.cursor)),
])),
inner,
);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,282 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let crust = Color::Rgb(220, 224, 232);
let mantle = Color::Rgb(230, 233, 239);
let base = Color::Rgb(239, 241, 245);
let surface0 = Color::Rgb(204, 208, 218);
let surface1 = Color::Rgb(188, 192, 204);
let overlay0 = Color::Rgb(156, 160, 176);
let overlay1 = Color::Rgb(140, 143, 161);
let subtext0 = Color::Rgb(108, 111, 133);
let subtext1 = Color::Rgb(92, 95, 119);
let text = Color::Rgb(76, 79, 105);
let pink = Color::Rgb(234, 118, 203);
let mauve = Color::Rgb(136, 57, 239);
let red = Color::Rgb(210, 15, 57);
let maroon = Color::Rgb(230, 69, 83);
let peach = Color::Rgb(254, 100, 11);
let yellow = Color::Rgb(223, 142, 29);
let green = Color::Rgb(64, 160, 43);
let teal = Color::Rgb(23, 146, 153);
let sapphire = Color::Rgb(32, 159, 181);
let lavender = Color::Rgb(114, 135, 253);
ThemeColors {
ui: UiColors {
bg: base,
bg_rgb: (239, 241, 245),
text_primary: text,
text_muted: subtext0,
text_dim: overlay1,
border: surface1,
header: lavender,
unfocused: overlay0,
accent: mauve,
surface: surface0,
},
status: StatusColors {
playing_bg: Color::Rgb(220, 240, 225),
playing_fg: green,
stopped_bg: Color::Rgb(245, 220, 225),
stopped_fg: red,
fill_on: green,
fill_off: overlay0,
fill_bg: surface0,
},
selection: SelectionColors {
cursor_bg: mauve,
cursor_fg: base,
selected_bg: Color::Rgb(200, 200, 230),
selected_fg: lavender,
in_range_bg: Color::Rgb(210, 210, 235),
in_range_fg: subtext1,
cursor: mauve,
selected: Color::Rgb(200, 200, 230),
in_range: Color::Rgb(210, 210, 235),
},
tile: TileColors {
playing_active_bg: Color::Rgb(250, 220, 210),
playing_active_fg: peach,
playing_inactive_bg: Color::Rgb(250, 235, 200),
playing_inactive_fg: yellow,
active_bg: Color::Rgb(200, 235, 235),
active_fg: teal,
inactive_bg: surface0,
inactive_fg: subtext0,
active_selected_bg: Color::Rgb(215, 210, 240),
active_in_range_bg: Color::Rgb(210, 215, 230),
link_bright: [
(136, 57, 239),
(234, 118, 203),
(254, 100, 11),
(4, 165, 229),
(64, 160, 43),
],
link_dim: [
(210, 200, 240),
(240, 210, 230),
(250, 220, 200),
(200, 230, 240),
(210, 235, 210),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(220, 210, 240),
tempo_fg: mauve,
bank_bg: Color::Rgb(200, 230, 235),
bank_fg: sapphire,
pattern_bg: Color::Rgb(200, 230, 225),
pattern_fg: teal,
stats_bg: surface0,
stats_fg: subtext0,
},
modal: ModalColors {
border: lavender,
border_accent: mauve,
border_warn: peach,
border_dim: overlay1,
confirm: peach,
rename: mauve,
input: sapphire,
editor: lavender,
preview: overlay1,
},
flash: FlashColors {
error_bg: Color::Rgb(250, 215, 220),
error_fg: red,
success_bg: Color::Rgb(210, 240, 215),
success_fg: green,
info_bg: surface0,
info_fg: text,
event_rgb: (225, 215, 240),
},
list: ListColors {
playing_bg: Color::Rgb(210, 235, 220),
playing_fg: green,
staged_play_bg: Color::Rgb(225, 215, 245),
staged_play_fg: mauve,
staged_stop_bg: Color::Rgb(245, 215, 225),
staged_stop_fg: maroon,
edit_bg: Color::Rgb(210, 235, 235),
edit_fg: teal,
hover_bg: surface1,
hover_fg: text,
},
link_status: LinkStatusColors {
disabled: red,
connected: green,
listening: yellow,
},
syntax: SyntaxColors {
gap_bg: mantle,
executed_bg: Color::Rgb(225, 220, 240),
selected_bg: Color::Rgb(250, 235, 210),
emit: (text, Color::Rgb(250, 220, 215)),
number: (peach, Color::Rgb(252, 235, 220)),
string: (green, Color::Rgb(215, 240, 215)),
comment: (overlay1, crust),
keyword: (mauve, Color::Rgb(230, 220, 245)),
stack_op: (sapphire, Color::Rgb(215, 230, 240)),
operator: (yellow, Color::Rgb(245, 235, 210)),
sound: (teal, Color::Rgb(210, 240, 240)),
param: (lavender, Color::Rgb(220, 225, 245)),
context: (peach, Color::Rgb(252, 235, 220)),
note: (green, Color::Rgb(215, 240, 215)),
interval: (Color::Rgb(50, 140, 30), Color::Rgb(215, 240, 210)),
variable: (pink, Color::Rgb(245, 220, 240)),
vary: (yellow, Color::Rgb(245, 235, 210)),
generator: (teal, Color::Rgb(210, 240, 235)),
default: (subtext0, mantle),
},
table: TableColors {
row_even: mantle,
row_odd: base,
},
values: ValuesColors {
tempo: peach,
value: subtext0,
},
hint: HintColors {
key: peach,
text: overlay1,
},
view_badge: ViewBadgeColors { bg: text, fg: base },
nav: NavColors {
selected_bg: Color::Rgb(215, 205, 245),
selected_fg: text,
unselected_bg: surface0,
unselected_fg: overlay1,
},
editor_widget: EditorWidgetColors {
cursor_bg: text,
cursor_fg: base,
selection_bg: Color::Rgb(200, 210, 240),
completion_bg: surface0,
completion_fg: text,
completion_selected: peach,
completion_example: teal,
},
browser: BrowserColors {
directory: sapphire,
project_file: mauve,
selected: peach,
file: text,
focused_border: peach,
unfocused_border: overlay0,
root: text,
file_icon: overlay1,
folder_icon: sapphire,
empty_text: overlay1,
},
input: InputColors {
text: sapphire,
cursor: text,
hint: overlay1,
},
search: SearchColors {
active: peach,
inactive: overlay0,
match_bg: yellow,
match_fg: base,
},
markdown: MarkdownColors {
h1: sapphire,
h2: peach,
h3: mauve,
code: green,
code_border: Color::Rgb(190, 195, 205),
link: teal,
link_url: Color::Rgb(150, 150, 150),
quote: overlay1,
text,
list: text,
},
engine: EngineColors {
header: Color::Rgb(30, 120, 150),
header_focused: yellow,
divider: Color::Rgb(180, 185, 195),
scroll_indicator: Color::Rgb(160, 165, 175),
label: Color::Rgb(100, 105, 120),
label_focused: Color::Rgb(70, 75, 90),
label_dim: Color::Rgb(120, 125, 140),
value: Color::Rgb(60, 65, 80),
focused: yellow,
normal: text,
dim: Color::Rgb(160, 165, 175),
path: Color::Rgb(100, 105, 120),
border_magenta: mauve,
border_green: green,
border_cyan: sapphire,
separator: Color::Rgb(180, 185, 200),
hint_active: Color::Rgb(180, 140, 40),
hint_inactive: Color::Rgb(190, 195, 205),
},
dict: DictColors {
word_name: green,
word_bg: Color::Rgb(210, 225, 235),
alias: overlay1,
stack_sig: mauve,
description: text,
example: Color::Rgb(100, 105, 115),
category_focused: yellow,
category_selected: sapphire,
category_normal: text,
category_dimmed: Color::Rgb(160, 165, 175),
border_focused: yellow,
border_normal: Color::Rgb(180, 185, 195),
header_desc: Color::Rgb(90, 95, 110),
},
title: TitleColors {
big_title: mauve,
author: lavender,
link: teal,
license: peach,
prompt: Color::Rgb(90, 100, 115),
subtitle: text,
},
meter: MeterColors {
low: green,
mid: yellow,
high: red,
low_rgb: (50, 150, 40),
mid_rgb: (200, 140, 30),
high_rgb: (200, 40, 50),
},
sparkle: SparkleColors {
colors: [
(114, 135, 253),
(254, 100, 11),
(64, 160, 43),
(234, 118, 203),
(136, 57, 239),
],
},
confirm: ConfirmColors {
border: peach,
button_selected_bg: peach,
button_selected_fg: base,
},
}
}

View File

@@ -0,0 +1,285 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let crust = Color::Rgb(17, 17, 27);
let mantle = Color::Rgb(24, 24, 37);
let base = Color::Rgb(30, 30, 46);
let surface0 = Color::Rgb(49, 50, 68);
let surface1 = Color::Rgb(69, 71, 90);
let overlay0 = Color::Rgb(108, 112, 134);
let overlay1 = Color::Rgb(127, 132, 156);
let subtext0 = Color::Rgb(166, 173, 200);
let subtext1 = Color::Rgb(186, 194, 222);
let text = Color::Rgb(205, 214, 244);
let pink = Color::Rgb(245, 194, 231);
let mauve = Color::Rgb(203, 166, 247);
let red = Color::Rgb(243, 139, 168);
let maroon = Color::Rgb(235, 160, 172);
let peach = Color::Rgb(250, 179, 135);
let yellow = Color::Rgb(249, 226, 175);
let green = Color::Rgb(166, 227, 161);
let teal = Color::Rgb(148, 226, 213);
let sapphire = Color::Rgb(116, 199, 236);
let lavender = Color::Rgb(180, 190, 254);
ThemeColors {
ui: UiColors {
bg: base,
bg_rgb: (30, 30, 46),
text_primary: text,
text_muted: subtext0,
text_dim: overlay1,
border: surface1,
header: lavender,
unfocused: overlay0,
accent: mauve,
surface: surface0,
},
status: StatusColors {
playing_bg: Color::Rgb(30, 50, 40),
playing_fg: green,
stopped_bg: Color::Rgb(50, 30, 40),
stopped_fg: red,
fill_on: green,
fill_off: overlay0,
fill_bg: surface0,
},
selection: SelectionColors {
cursor_bg: mauve,
cursor_fg: crust,
selected_bg: Color::Rgb(60, 60, 90),
selected_fg: lavender,
in_range_bg: Color::Rgb(50, 50, 75),
in_range_fg: subtext1,
cursor: mauve,
selected: Color::Rgb(60, 60, 90),
in_range: Color::Rgb(50, 50, 75),
},
tile: TileColors {
playing_active_bg: Color::Rgb(80, 50, 60),
playing_active_fg: peach,
playing_inactive_bg: Color::Rgb(70, 55, 45),
playing_inactive_fg: yellow,
active_bg: Color::Rgb(40, 55, 55),
active_fg: teal,
inactive_bg: surface0,
inactive_fg: subtext0,
active_selected_bg: Color::Rgb(70, 60, 80),
active_in_range_bg: Color::Rgb(55, 55, 70),
link_bright: [
(203, 166, 247),
(245, 194, 231),
(250, 179, 135),
(137, 220, 235),
(166, 227, 161),
],
link_dim: [
(70, 55, 85),
(85, 65, 80),
(85, 60, 45),
(45, 75, 80),
(55, 80, 55),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(50, 40, 60),
tempo_fg: mauve,
bank_bg: Color::Rgb(35, 50, 55),
bank_fg: sapphire,
pattern_bg: Color::Rgb(40, 50, 50),
pattern_fg: teal,
stats_bg: surface0,
stats_fg: subtext0,
},
modal: ModalColors {
border: lavender,
border_accent: mauve,
border_warn: peach,
border_dim: overlay1,
confirm: peach,
rename: mauve,
input: sapphire,
editor: lavender,
preview: overlay1,
},
flash: FlashColors {
error_bg: Color::Rgb(50, 30, 40),
error_fg: red,
success_bg: Color::Rgb(30, 50, 40),
success_fg: green,
info_bg: surface0,
info_fg: text,
event_rgb: (55, 45, 70),
},
list: ListColors {
playing_bg: Color::Rgb(35, 55, 45),
playing_fg: green,
staged_play_bg: Color::Rgb(55, 45, 65),
staged_play_fg: mauve,
staged_stop_bg: Color::Rgb(60, 40, 50),
staged_stop_fg: maroon,
edit_bg: Color::Rgb(40, 55, 55),
edit_fg: teal,
hover_bg: surface1,
hover_fg: text,
},
link_status: LinkStatusColors {
disabled: red,
connected: green,
listening: yellow,
},
syntax: SyntaxColors {
gap_bg: mantle,
executed_bg: Color::Rgb(45, 40, 55),
selected_bg: Color::Rgb(70, 55, 40),
emit: (text, Color::Rgb(80, 50, 60)),
number: (peach, Color::Rgb(55, 45, 35)),
string: (green, Color::Rgb(35, 50, 40)),
comment: (overlay1, crust),
keyword: (mauve, Color::Rgb(50, 40, 60)),
stack_op: (sapphire, Color::Rgb(35, 45, 55)),
operator: (yellow, Color::Rgb(55, 50, 35)),
sound: (teal, Color::Rgb(35, 55, 55)),
param: (lavender, Color::Rgb(45, 45, 60)),
context: (peach, Color::Rgb(55, 45, 35)),
note: (green, Color::Rgb(35, 50, 40)),
interval: (Color::Rgb(180, 230, 150), Color::Rgb(40, 55, 35)),
variable: (pink, Color::Rgb(55, 40, 55)),
vary: (yellow, Color::Rgb(55, 50, 35)),
generator: (teal, Color::Rgb(35, 55, 50)),
default: (subtext0, mantle),
},
table: TableColors {
row_even: mantle,
row_odd: base,
},
values: ValuesColors {
tempo: peach,
value: subtext0,
},
hint: HintColors {
key: peach,
text: overlay1,
},
view_badge: ViewBadgeColors {
bg: text,
fg: crust,
},
nav: NavColors {
selected_bg: Color::Rgb(60, 50, 75),
selected_fg: text,
unselected_bg: surface0,
unselected_fg: overlay1,
},
editor_widget: EditorWidgetColors {
cursor_bg: text,
cursor_fg: crust,
selection_bg: Color::Rgb(50, 60, 90),
completion_bg: surface0,
completion_fg: text,
completion_selected: peach,
completion_example: teal,
},
browser: BrowserColors {
directory: sapphire,
project_file: mauve,
selected: peach,
file: text,
focused_border: peach,
unfocused_border: overlay0,
root: text,
file_icon: overlay1,
folder_icon: sapphire,
empty_text: overlay1,
},
input: InputColors {
text: sapphire,
cursor: text,
hint: overlay1,
},
search: SearchColors {
active: peach,
inactive: overlay0,
match_bg: yellow,
match_fg: crust,
},
markdown: MarkdownColors {
h1: sapphire,
h2: peach,
h3: mauve,
code: green,
code_border: Color::Rgb(60, 60, 70),
link: teal,
link_url: Color::Rgb(100, 100, 100),
quote: overlay1,
text,
list: text,
},
engine: EngineColors {
header: Color::Rgb(100, 160, 180),
header_focused: yellow,
divider: Color::Rgb(60, 65, 70),
scroll_indicator: Color::Rgb(80, 85, 95),
label: Color::Rgb(120, 125, 135),
label_focused: Color::Rgb(150, 155, 165),
label_dim: Color::Rgb(100, 105, 115),
value: Color::Rgb(180, 180, 190),
focused: yellow,
normal: text,
dim: Color::Rgb(80, 85, 95),
path: Color::Rgb(120, 125, 135),
border_magenta: mauve,
border_green: green,
border_cyan: sapphire,
separator: Color::Rgb(60, 65, 75),
hint_active: Color::Rgb(180, 180, 100),
hint_inactive: Color::Rgb(60, 60, 70),
},
dict: DictColors {
word_name: green,
word_bg: Color::Rgb(40, 50, 60),
alias: overlay1,
stack_sig: mauve,
description: text,
example: Color::Rgb(120, 130, 140),
category_focused: yellow,
category_selected: sapphire,
category_normal: text,
category_dimmed: Color::Rgb(80, 80, 90),
border_focused: yellow,
border_normal: Color::Rgb(60, 60, 70),
header_desc: Color::Rgb(140, 145, 155),
},
title: TitleColors {
big_title: mauve,
author: lavender,
link: teal,
license: peach,
prompt: Color::Rgb(140, 160, 170),
subtitle: text,
},
meter: MeterColors {
low: green,
mid: yellow,
high: red,
low_rgb: (40, 180, 80),
mid_rgb: (220, 180, 40),
high_rgb: (220, 60, 40),
},
sparkle: SparkleColors {
colors: [
(200, 220, 255),
(250, 179, 135),
(166, 227, 161),
(245, 194, 231),
(203, 166, 247),
],
},
confirm: ConfirmColors {
border: peach,
button_selected_bg: peach,
button_selected_fg: crust,
},
}
}

View File

@@ -0,0 +1,279 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let background = Color::Rgb(40, 42, 54);
let current_line = Color::Rgb(68, 71, 90);
let foreground = Color::Rgb(248, 248, 242);
let comment = Color::Rgb(98, 114, 164);
let cyan = Color::Rgb(139, 233, 253);
let green = Color::Rgb(80, 250, 123);
let orange = Color::Rgb(255, 184, 108);
let pink = Color::Rgb(255, 121, 198);
let purple = Color::Rgb(189, 147, 249);
let red = Color::Rgb(255, 85, 85);
let yellow = Color::Rgb(241, 250, 140);
let darker_bg = Color::Rgb(33, 34, 44);
let lighter_bg = Color::Rgb(55, 57, 70);
ThemeColors {
ui: UiColors {
bg: background,
bg_rgb: (40, 42, 54),
text_primary: foreground,
text_muted: comment,
text_dim: Color::Rgb(80, 85, 110),
border: current_line,
header: purple,
unfocused: comment,
accent: purple,
surface: current_line,
},
status: StatusColors {
playing_bg: Color::Rgb(40, 60, 50),
playing_fg: green,
stopped_bg: Color::Rgb(65, 45, 50),
stopped_fg: red,
fill_on: green,
fill_off: comment,
fill_bg: current_line,
},
selection: SelectionColors {
cursor_bg: purple,
cursor_fg: background,
selected_bg: Color::Rgb(80, 75, 110),
selected_fg: purple,
in_range_bg: Color::Rgb(65, 65, 90),
in_range_fg: foreground,
cursor: purple,
selected: Color::Rgb(80, 75, 110),
in_range: Color::Rgb(65, 65, 90),
},
tile: TileColors {
playing_active_bg: Color::Rgb(85, 60, 65),
playing_active_fg: orange,
playing_inactive_bg: Color::Rgb(80, 75, 55),
playing_inactive_fg: yellow,
active_bg: Color::Rgb(50, 70, 70),
active_fg: cyan,
inactive_bg: current_line,
inactive_fg: comment,
active_selected_bg: Color::Rgb(80, 70, 95),
active_in_range_bg: Color::Rgb(65, 65, 85),
link_bright: [
(189, 147, 249),
(255, 121, 198),
(255, 184, 108),
(139, 233, 253),
(80, 250, 123),
],
link_dim: [
(75, 60, 95),
(95, 55, 80),
(95, 70, 50),
(55, 90, 95),
(40, 95, 55),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(65, 50, 75),
tempo_fg: purple,
bank_bg: Color::Rgb(45, 65, 70),
bank_fg: cyan,
pattern_bg: Color::Rgb(40, 70, 60),
pattern_fg: green,
stats_bg: current_line,
stats_fg: comment,
},
modal: ModalColors {
border: purple,
border_accent: pink,
border_warn: orange,
border_dim: comment,
confirm: orange,
rename: purple,
input: cyan,
editor: purple,
preview: comment,
},
flash: FlashColors {
error_bg: Color::Rgb(70, 45, 50),
error_fg: red,
success_bg: Color::Rgb(40, 65, 50),
success_fg: green,
info_bg: current_line,
info_fg: foreground,
event_rgb: (70, 55, 85),
},
list: ListColors {
playing_bg: Color::Rgb(40, 65, 50),
playing_fg: green,
staged_play_bg: Color::Rgb(70, 55, 85),
staged_play_fg: purple,
staged_stop_bg: Color::Rgb(80, 50, 60),
staged_stop_fg: red,
edit_bg: Color::Rgb(45, 70, 70),
edit_fg: cyan,
hover_bg: lighter_bg,
hover_fg: foreground,
},
link_status: LinkStatusColors {
disabled: red,
connected: green,
listening: yellow,
},
syntax: SyntaxColors {
gap_bg: darker_bg,
executed_bg: Color::Rgb(55, 50, 70),
selected_bg: Color::Rgb(85, 70, 50),
emit: (foreground, Color::Rgb(85, 55, 65)),
number: (orange, Color::Rgb(75, 55, 45)),
string: (yellow, Color::Rgb(70, 70, 45)),
comment: (comment, darker_bg),
keyword: (pink, Color::Rgb(80, 50, 70)),
stack_op: (cyan, Color::Rgb(45, 65, 75)),
operator: (green, Color::Rgb(40, 70, 50)),
sound: (cyan, Color::Rgb(45, 70, 70)),
param: (purple, Color::Rgb(60, 50, 75)),
context: (orange, Color::Rgb(75, 55, 45)),
note: (green, Color::Rgb(40, 70, 50)),
interval: (Color::Rgb(120, 255, 150), Color::Rgb(40, 75, 50)),
variable: (pink, Color::Rgb(80, 50, 65)),
vary: (yellow, Color::Rgb(70, 70, 45)),
generator: (cyan, Color::Rgb(45, 70, 65)),
default: (comment, darker_bg),
},
table: TableColors {
row_even: darker_bg,
row_odd: background,
},
values: ValuesColors {
tempo: orange,
value: comment,
},
hint: HintColors {
key: orange,
text: comment,
},
view_badge: ViewBadgeColors {
bg: foreground,
fg: background,
},
nav: NavColors {
selected_bg: Color::Rgb(75, 65, 100),
selected_fg: foreground,
unselected_bg: current_line,
unselected_fg: comment,
},
editor_widget: EditorWidgetColors {
cursor_bg: foreground,
cursor_fg: background,
selection_bg: Color::Rgb(70, 75, 105),
completion_bg: current_line,
completion_fg: foreground,
completion_selected: orange,
completion_example: cyan,
},
browser: BrowserColors {
directory: cyan,
project_file: purple,
selected: orange,
file: foreground,
focused_border: orange,
unfocused_border: comment,
root: foreground,
file_icon: comment,
folder_icon: cyan,
empty_text: comment,
},
input: InputColors {
text: cyan,
cursor: foreground,
hint: comment,
},
search: SearchColors {
active: orange,
inactive: comment,
match_bg: yellow,
match_fg: background,
},
markdown: MarkdownColors {
h1: cyan,
h2: orange,
h3: purple,
code: green,
code_border: Color::Rgb(85, 90, 110),
link: pink,
link_url: Color::Rgb(120, 130, 150),
quote: comment,
text: foreground,
list: foreground,
},
engine: EngineColors {
header: cyan,
header_focused: yellow,
divider: Color::Rgb(80, 85, 105),
scroll_indicator: Color::Rgb(95, 100, 120),
label: Color::Rgb(140, 145, 165),
label_focused: Color::Rgb(170, 175, 195),
label_dim: Color::Rgb(110, 115, 135),
value: Color::Rgb(200, 205, 220),
focused: yellow,
normal: foreground,
dim: Color::Rgb(95, 100, 120),
path: Color::Rgb(140, 145, 165),
border_magenta: pink,
border_green: green,
border_cyan: cyan,
separator: Color::Rgb(80, 85, 105),
hint_active: Color::Rgb(220, 200, 100),
hint_inactive: Color::Rgb(80, 85, 105),
},
dict: DictColors {
word_name: green,
word_bg: Color::Rgb(55, 65, 80),
alias: comment,
stack_sig: purple,
description: foreground,
example: Color::Rgb(140, 145, 165),
category_focused: yellow,
category_selected: cyan,
category_normal: foreground,
category_dimmed: Color::Rgb(95, 100, 120),
border_focused: yellow,
border_normal: Color::Rgb(80, 85, 105),
header_desc: Color::Rgb(160, 165, 185),
},
title: TitleColors {
big_title: purple,
author: pink,
link: cyan,
license: orange,
prompt: Color::Rgb(160, 165, 185),
subtitle: foreground,
},
meter: MeterColors {
low: green,
mid: yellow,
high: red,
low_rgb: (70, 230, 110),
mid_rgb: (230, 240, 130),
high_rgb: (240, 80, 80),
},
sparkle: SparkleColors {
colors: [
(189, 147, 249),
(255, 184, 108),
(80, 250, 123),
(255, 121, 198),
(139, 233, 253),
],
},
confirm: ConfirmColors {
border: orange,
button_selected_bg: orange,
button_selected_fg: background,
},
}
}

View File

@@ -0,0 +1,278 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let bg0 = Color::Rgb(40, 40, 40);
let bg1 = Color::Rgb(60, 56, 54);
let bg2 = Color::Rgb(80, 73, 69);
let fg = Color::Rgb(235, 219, 178);
let fg2 = Color::Rgb(213, 196, 161);
let fg3 = Color::Rgb(189, 174, 147);
let fg4 = Color::Rgb(168, 153, 132);
let red = Color::Rgb(251, 73, 52);
let green = Color::Rgb(184, 187, 38);
let yellow = Color::Rgb(250, 189, 47);
let blue = Color::Rgb(131, 165, 152);
let purple = Color::Rgb(211, 134, 155);
let aqua = Color::Rgb(142, 192, 124);
let orange = Color::Rgb(254, 128, 25);
let darker_bg = Color::Rgb(29, 32, 33);
ThemeColors {
ui: UiColors {
bg: bg0,
bg_rgb: (40, 40, 40),
text_primary: fg,
text_muted: fg3,
text_dim: fg4,
border: bg2,
header: yellow,
unfocused: fg4,
accent: orange,
surface: bg1,
},
status: StatusColors {
playing_bg: Color::Rgb(50, 60, 45),
playing_fg: green,
stopped_bg: Color::Rgb(65, 45, 45),
stopped_fg: red,
fill_on: green,
fill_off: fg4,
fill_bg: bg1,
},
selection: SelectionColors {
cursor_bg: orange,
cursor_fg: bg0,
selected_bg: Color::Rgb(80, 70, 55),
selected_fg: yellow,
in_range_bg: Color::Rgb(65, 60, 50),
in_range_fg: fg2,
cursor: orange,
selected: Color::Rgb(80, 70, 55),
in_range: Color::Rgb(65, 60, 50),
},
tile: TileColors {
playing_active_bg: Color::Rgb(90, 65, 50),
playing_active_fg: orange,
playing_inactive_bg: Color::Rgb(80, 75, 45),
playing_inactive_fg: yellow,
active_bg: Color::Rgb(50, 65, 55),
active_fg: aqua,
inactive_bg: bg1,
inactive_fg: fg3,
active_selected_bg: Color::Rgb(85, 70, 60),
active_in_range_bg: Color::Rgb(70, 65, 55),
link_bright: [
(254, 128, 25),
(211, 134, 155),
(250, 189, 47),
(131, 165, 152),
(184, 187, 38),
],
link_dim: [
(85, 55, 35),
(75, 55, 65),
(80, 70, 40),
(50, 60, 60),
(60, 65, 35),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(75, 55, 40),
tempo_fg: orange,
bank_bg: Color::Rgb(50, 60, 60),
bank_fg: blue,
pattern_bg: Color::Rgb(50, 65, 50),
pattern_fg: aqua,
stats_bg: bg1,
stats_fg: fg3,
},
modal: ModalColors {
border: yellow,
border_accent: orange,
border_warn: red,
border_dim: fg4,
confirm: orange,
rename: purple,
input: blue,
editor: yellow,
preview: fg4,
},
flash: FlashColors {
error_bg: Color::Rgb(70, 45, 45),
error_fg: red,
success_bg: Color::Rgb(50, 65, 45),
success_fg: green,
info_bg: bg1,
info_fg: fg,
event_rgb: (70, 55, 45),
},
list: ListColors {
playing_bg: Color::Rgb(50, 65, 45),
playing_fg: green,
staged_play_bg: Color::Rgb(70, 55, 60),
staged_play_fg: purple,
staged_stop_bg: Color::Rgb(75, 50, 50),
staged_stop_fg: red,
edit_bg: Color::Rgb(50, 65, 55),
edit_fg: aqua,
hover_bg: bg2,
hover_fg: fg,
},
link_status: LinkStatusColors {
disabled: red,
connected: green,
listening: yellow,
},
syntax: SyntaxColors {
gap_bg: darker_bg,
executed_bg: Color::Rgb(55, 50, 45),
selected_bg: Color::Rgb(85, 70, 45),
emit: (fg, Color::Rgb(80, 55, 50)),
number: (orange, Color::Rgb(70, 50, 40)),
string: (green, Color::Rgb(50, 60, 40)),
comment: (fg4, darker_bg),
keyword: (red, Color::Rgb(70, 45, 45)),
stack_op: (blue, Color::Rgb(50, 55, 60)),
operator: (yellow, Color::Rgb(70, 65, 40)),
sound: (aqua, Color::Rgb(45, 60, 50)),
param: (purple, Color::Rgb(65, 50, 55)),
context: (orange, Color::Rgb(70, 50, 40)),
note: (green, Color::Rgb(50, 60, 40)),
interval: (Color::Rgb(170, 200, 100), Color::Rgb(55, 65, 40)),
variable: (purple, Color::Rgb(65, 50, 55)),
vary: (yellow, Color::Rgb(70, 65, 40)),
generator: (aqua, Color::Rgb(45, 60, 50)),
default: (fg3, darker_bg),
},
table: TableColors {
row_even: darker_bg,
row_odd: bg0,
},
values: ValuesColors {
tempo: orange,
value: fg3,
},
hint: HintColors {
key: orange,
text: fg4,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg0 },
nav: NavColors {
selected_bg: Color::Rgb(80, 65, 50),
selected_fg: fg,
unselected_bg: bg1,
unselected_fg: fg4,
},
editor_widget: EditorWidgetColors {
cursor_bg: fg,
cursor_fg: bg0,
selection_bg: Color::Rgb(70, 65, 55),
completion_bg: bg1,
completion_fg: fg,
completion_selected: orange,
completion_example: aqua,
},
browser: BrowserColors {
directory: blue,
project_file: purple,
selected: orange,
file: fg,
focused_border: orange,
unfocused_border: fg4,
root: fg,
file_icon: fg4,
folder_icon: blue,
empty_text: fg4,
},
input: InputColors {
text: blue,
cursor: fg,
hint: fg4,
},
search: SearchColors {
active: orange,
inactive: fg4,
match_bg: yellow,
match_fg: bg0,
},
markdown: MarkdownColors {
h1: blue,
h2: orange,
h3: purple,
code: green,
code_border: Color::Rgb(80, 75, 70),
link: aqua,
link_url: Color::Rgb(120, 115, 105),
quote: fg4,
text: fg,
list: fg,
},
engine: EngineColors {
header: blue,
header_focused: yellow,
divider: Color::Rgb(75, 70, 65),
scroll_indicator: Color::Rgb(90, 85, 80),
label: Color::Rgb(145, 135, 125),
label_focused: Color::Rgb(175, 165, 155),
label_dim: Color::Rgb(115, 105, 95),
value: Color::Rgb(200, 190, 175),
focused: yellow,
normal: fg,
dim: Color::Rgb(90, 85, 80),
path: Color::Rgb(145, 135, 125),
border_magenta: purple,
border_green: green,
border_cyan: aqua,
separator: Color::Rgb(75, 70, 65),
hint_active: Color::Rgb(220, 180, 80),
hint_inactive: Color::Rgb(75, 70, 65),
},
dict: DictColors {
word_name: green,
word_bg: Color::Rgb(55, 60, 55),
alias: fg4,
stack_sig: purple,
description: fg,
example: Color::Rgb(145, 135, 125),
category_focused: yellow,
category_selected: blue,
category_normal: fg,
category_dimmed: Color::Rgb(90, 85, 80),
border_focused: yellow,
border_normal: Color::Rgb(75, 70, 65),
header_desc: Color::Rgb(165, 155, 145),
},
title: TitleColors {
big_title: orange,
author: yellow,
link: aqua,
license: purple,
prompt: Color::Rgb(165, 155, 145),
subtitle: fg,
},
meter: MeterColors {
low: green,
mid: yellow,
high: red,
low_rgb: (170, 175, 35),
mid_rgb: (235, 180, 45),
high_rgb: (240, 70, 50),
},
sparkle: SparkleColors {
colors: [
(250, 189, 47),
(254, 128, 25),
(184, 187, 38),
(211, 134, 155),
(131, 165, 152),
],
},
confirm: ConfirmColors {
border: orange,
button_selected_bg: orange,
button_selected_fg: bg0,
},
}
}

View File

@@ -0,0 +1,278 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let bg = Color::Rgb(31, 31, 40);
let bg_light = Color::Rgb(43, 43, 54);
let bg_lighter = Color::Rgb(54, 54, 70);
let fg = Color::Rgb(220, 215, 186);
let fg_dim = Color::Rgb(160, 158, 140);
let comment = Color::Rgb(114, 113, 105);
let crystal_blue = Color::Rgb(126, 156, 216);
let oni_violet = Color::Rgb(149, 127, 184);
let autumn_green = Color::Rgb(118, 148, 106);
let autumn_red = Color::Rgb(195, 64, 67);
let carp_yellow = Color::Rgb(230, 195, 132);
let spring_blue = Color::Rgb(127, 180, 202);
let wave_red = Color::Rgb(226, 109, 115);
let sakura_pink = Color::Rgb(212, 140, 149);
let darker_bg = Color::Rgb(26, 26, 34);
ThemeColors {
ui: UiColors {
bg,
bg_rgb: (31, 31, 40),
text_primary: fg,
text_muted: fg_dim,
text_dim: comment,
border: bg_lighter,
header: crystal_blue,
unfocused: comment,
accent: sakura_pink,
surface: bg_light,
},
status: StatusColors {
playing_bg: Color::Rgb(40, 55, 45),
playing_fg: autumn_green,
stopped_bg: Color::Rgb(60, 40, 45),
stopped_fg: autumn_red,
fill_on: autumn_green,
fill_off: comment,
fill_bg: bg_light,
},
selection: SelectionColors {
cursor_bg: sakura_pink,
cursor_fg: bg,
selected_bg: Color::Rgb(65, 55, 70),
selected_fg: sakura_pink,
in_range_bg: Color::Rgb(50, 50, 60),
in_range_fg: fg,
cursor: sakura_pink,
selected: Color::Rgb(65, 55, 70),
in_range: Color::Rgb(50, 50, 60),
},
tile: TileColors {
playing_active_bg: Color::Rgb(65, 60, 50),
playing_active_fg: carp_yellow,
playing_inactive_bg: Color::Rgb(55, 55, 50),
playing_inactive_fg: fg_dim,
active_bg: Color::Rgb(45, 55, 70),
active_fg: crystal_blue,
inactive_bg: bg_light,
inactive_fg: fg_dim,
active_selected_bg: Color::Rgb(65, 55, 70),
active_in_range_bg: Color::Rgb(50, 50, 60),
link_bright: [
(226, 109, 115),
(149, 127, 184),
(230, 195, 132),
(127, 180, 202),
(118, 148, 106),
],
link_dim: [
(75, 45, 50),
(55, 50, 70),
(70, 60, 50),
(45, 60, 70),
(45, 55, 45),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(55, 50, 65),
tempo_fg: oni_violet,
bank_bg: Color::Rgb(45, 55, 70),
bank_fg: crystal_blue,
pattern_bg: Color::Rgb(45, 55, 45),
pattern_fg: autumn_green,
stats_bg: bg_light,
stats_fg: fg_dim,
},
modal: ModalColors {
border: crystal_blue,
border_accent: sakura_pink,
border_warn: carp_yellow,
border_dim: comment,
confirm: carp_yellow,
rename: oni_violet,
input: crystal_blue,
editor: crystal_blue,
preview: comment,
},
flash: FlashColors {
error_bg: Color::Rgb(60, 40, 45),
error_fg: wave_red,
success_bg: Color::Rgb(40, 55, 45),
success_fg: autumn_green,
info_bg: bg_light,
info_fg: fg,
event_rgb: (50, 50, 60),
},
list: ListColors {
playing_bg: Color::Rgb(40, 55, 45),
playing_fg: autumn_green,
staged_play_bg: Color::Rgb(55, 50, 70),
staged_play_fg: oni_violet,
staged_stop_bg: Color::Rgb(65, 45, 50),
staged_stop_fg: wave_red,
edit_bg: Color::Rgb(45, 55, 70),
edit_fg: crystal_blue,
hover_bg: bg_lighter,
hover_fg: fg,
},
link_status: LinkStatusColors {
disabled: autumn_red,
connected: autumn_green,
listening: carp_yellow,
},
syntax: SyntaxColors {
gap_bg: darker_bg,
executed_bg: Color::Rgb(45, 45, 55),
selected_bg: Color::Rgb(65, 60, 50),
emit: (fg, Color::Rgb(60, 50, 60)),
number: (oni_violet, Color::Rgb(55, 50, 65)),
string: (autumn_green, Color::Rgb(45, 55, 45)),
comment: (comment, darker_bg),
keyword: (sakura_pink, Color::Rgb(60, 50, 55)),
stack_op: (spring_blue, Color::Rgb(45, 55, 65)),
operator: (wave_red, Color::Rgb(60, 45, 50)),
sound: (crystal_blue, Color::Rgb(45, 55, 70)),
param: (carp_yellow, Color::Rgb(65, 60, 50)),
context: (carp_yellow, Color::Rgb(65, 60, 50)),
note: (autumn_green, Color::Rgb(45, 55, 45)),
interval: (Color::Rgb(150, 180, 130), Color::Rgb(45, 55, 45)),
variable: (autumn_green, Color::Rgb(45, 55, 45)),
vary: (carp_yellow, Color::Rgb(65, 60, 50)),
generator: (spring_blue, Color::Rgb(45, 55, 65)),
default: (fg_dim, darker_bg),
},
table: TableColors {
row_even: darker_bg,
row_odd: bg,
},
values: ValuesColors {
tempo: carp_yellow,
value: fg_dim,
},
hint: HintColors {
key: carp_yellow,
text: comment,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg },
nav: NavColors {
selected_bg: Color::Rgb(60, 50, 70),
selected_fg: fg,
unselected_bg: bg_light,
unselected_fg: comment,
},
editor_widget: EditorWidgetColors {
cursor_bg: fg,
cursor_fg: bg,
selection_bg: Color::Rgb(55, 55, 70),
completion_bg: bg_light,
completion_fg: fg,
completion_selected: carp_yellow,
completion_example: spring_blue,
},
browser: BrowserColors {
directory: crystal_blue,
project_file: oni_violet,
selected: carp_yellow,
file: fg,
focused_border: carp_yellow,
unfocused_border: comment,
root: fg,
file_icon: comment,
folder_icon: crystal_blue,
empty_text: comment,
},
input: InputColors {
text: crystal_blue,
cursor: fg,
hint: comment,
},
search: SearchColors {
active: carp_yellow,
inactive: comment,
match_bg: carp_yellow,
match_fg: bg,
},
markdown: MarkdownColors {
h1: crystal_blue,
h2: carp_yellow,
h3: oni_violet,
code: autumn_green,
code_border: Color::Rgb(65, 65, 80),
link: sakura_pink,
link_url: Color::Rgb(100, 100, 115),
quote: comment,
text: fg,
list: fg,
},
engine: EngineColors {
header: crystal_blue,
header_focused: carp_yellow,
divider: Color::Rgb(60, 60, 75),
scroll_indicator: Color::Rgb(75, 75, 92),
label: Color::Rgb(140, 138, 125),
label_focused: Color::Rgb(170, 168, 155),
label_dim: Color::Rgb(110, 108, 100),
value: Color::Rgb(200, 195, 175),
focused: carp_yellow,
normal: fg,
dim: Color::Rgb(75, 75, 92),
path: Color::Rgb(140, 138, 125),
border_magenta: oni_violet,
border_green: autumn_green,
border_cyan: spring_blue,
separator: Color::Rgb(60, 60, 75),
hint_active: Color::Rgb(220, 185, 120),
hint_inactive: Color::Rgb(60, 60, 75),
},
dict: DictColors {
word_name: autumn_green,
word_bg: Color::Rgb(45, 50, 50),
alias: comment,
stack_sig: oni_violet,
description: fg,
example: Color::Rgb(140, 138, 125),
category_focused: carp_yellow,
category_selected: crystal_blue,
category_normal: fg,
category_dimmed: Color::Rgb(75, 75, 92),
border_focused: carp_yellow,
border_normal: Color::Rgb(60, 60, 75),
header_desc: Color::Rgb(160, 158, 145),
},
title: TitleColors {
big_title: sakura_pink,
author: crystal_blue,
link: autumn_green,
license: carp_yellow,
prompt: Color::Rgb(160, 158, 145),
subtitle: fg,
},
meter: MeterColors {
low: autumn_green,
mid: carp_yellow,
high: wave_red,
low_rgb: (118, 148, 106),
mid_rgb: (230, 195, 132),
high_rgb: (226, 109, 115),
},
sparkle: SparkleColors {
colors: [
(127, 180, 202),
(230, 195, 132),
(118, 148, 106),
(226, 109, 115),
(149, 127, 184),
],
},
confirm: ConfirmColors {
border: carp_yellow,
button_selected_bg: carp_yellow,
button_selected_fg: bg,
},
}
}

View File

@@ -0,0 +1,373 @@
//! Centralized color definitions for Cagire TUI.
//! Supports multiple color schemes with runtime switching.
mod catppuccin_latte;
mod catppuccin_mocha;
mod dracula;
mod gruvbox_dark;
mod kanagawa;
mod monochrome_black;
mod monochrome_white;
mod monokai;
mod nord;
mod pitch_black;
mod rose_pine;
mod tokyo_night;
use ratatui::style::Color;
use std::cell::RefCell;
pub struct ThemeEntry {
pub id: &'static str,
pub label: &'static str,
pub colors: fn() -> ThemeColors,
}
pub const THEMES: &[ThemeEntry] = &[
ThemeEntry { id: "CatppuccinMocha", label: "Catppuccin Mocha", colors: catppuccin_mocha::theme },
ThemeEntry { id: "CatppuccinLatte", label: "Catppuccin Latte", colors: catppuccin_latte::theme },
ThemeEntry { id: "Nord", label: "Nord", colors: nord::theme },
ThemeEntry { id: "Dracula", label: "Dracula", colors: dracula::theme },
ThemeEntry { id: "GruvboxDark", label: "Gruvbox Dark", colors: gruvbox_dark::theme },
ThemeEntry { id: "Monokai", label: "Monokai", colors: monokai::theme },
ThemeEntry { id: "MonochromeBlack", label: "Monochrome (Black)", colors: monochrome_black::theme },
ThemeEntry { id: "MonochromeWhite", label: "Monochrome (White)", colors: monochrome_white::theme },
ThemeEntry { id: "PitchBlack", label: "Pitch Black", colors: pitch_black::theme },
ThemeEntry { id: "TokyoNight", label: "Tokyo Night", colors: tokyo_night::theme },
ThemeEntry { id: "RosePine", label: "Rosé Pine", colors: rose_pine::theme },
ThemeEntry { id: "Kanagawa", label: "Kanagawa", colors: kanagawa::theme },
];
thread_local! {
static CURRENT_THEME: RefCell<ThemeColors> = RefCell::new((THEMES[0].colors)());
}
pub fn get() -> ThemeColors {
CURRENT_THEME.with(|t| t.borrow().clone())
}
pub fn set(theme: ThemeColors) {
CURRENT_THEME.with(|t| *t.borrow_mut() = theme);
}
#[derive(Clone)]
pub struct ThemeColors {
pub ui: UiColors,
pub status: StatusColors,
pub selection: SelectionColors,
pub tile: TileColors,
pub header: HeaderColors,
pub modal: ModalColors,
pub flash: FlashColors,
pub list: ListColors,
pub link_status: LinkStatusColors,
pub syntax: SyntaxColors,
pub table: TableColors,
pub values: ValuesColors,
pub hint: HintColors,
pub view_badge: ViewBadgeColors,
pub nav: NavColors,
pub editor_widget: EditorWidgetColors,
pub browser: BrowserColors,
pub input: InputColors,
pub search: SearchColors,
pub markdown: MarkdownColors,
pub engine: EngineColors,
pub dict: DictColors,
pub title: TitleColors,
pub meter: MeterColors,
pub sparkle: SparkleColors,
pub confirm: ConfirmColors,
}
#[derive(Clone)]
pub struct UiColors {
pub bg: Color,
pub bg_rgb: (u8, u8, u8),
pub text_primary: Color,
pub text_muted: Color,
pub text_dim: Color,
pub border: Color,
pub header: Color,
pub unfocused: Color,
pub accent: Color,
pub surface: Color,
}
#[derive(Clone)]
pub struct StatusColors {
pub playing_bg: Color,
pub playing_fg: Color,
pub stopped_bg: Color,
pub stopped_fg: Color,
pub fill_on: Color,
pub fill_off: Color,
pub fill_bg: Color,
}
#[derive(Clone)]
pub struct SelectionColors {
pub cursor_bg: Color,
pub cursor_fg: Color,
pub selected_bg: Color,
pub selected_fg: Color,
pub in_range_bg: Color,
pub in_range_fg: Color,
pub cursor: Color,
pub selected: Color,
pub in_range: Color,
}
#[derive(Clone)]
pub struct TileColors {
pub playing_active_bg: Color,
pub playing_active_fg: Color,
pub playing_inactive_bg: Color,
pub playing_inactive_fg: Color,
pub active_bg: Color,
pub active_fg: Color,
pub inactive_bg: Color,
pub inactive_fg: Color,
pub active_selected_bg: Color,
pub active_in_range_bg: Color,
pub link_bright: [(u8, u8, u8); 5],
pub link_dim: [(u8, u8, u8); 5],
}
#[derive(Clone)]
pub struct HeaderColors {
pub tempo_bg: Color,
pub tempo_fg: Color,
pub bank_bg: Color,
pub bank_fg: Color,
pub pattern_bg: Color,
pub pattern_fg: Color,
pub stats_bg: Color,
pub stats_fg: Color,
}
#[derive(Clone)]
pub struct ModalColors {
pub border: Color,
pub border_accent: Color,
pub border_warn: Color,
pub border_dim: Color,
pub confirm: Color,
pub rename: Color,
pub input: Color,
pub editor: Color,
pub preview: Color,
}
#[derive(Clone)]
pub struct FlashColors {
pub error_bg: Color,
pub error_fg: Color,
pub success_bg: Color,
pub success_fg: Color,
pub info_bg: Color,
pub info_fg: Color,
pub event_rgb: (u8, u8, u8),
}
#[derive(Clone)]
pub struct ListColors {
pub playing_bg: Color,
pub playing_fg: Color,
pub staged_play_bg: Color,
pub staged_play_fg: Color,
pub staged_stop_bg: Color,
pub staged_stop_fg: Color,
pub edit_bg: Color,
pub edit_fg: Color,
pub hover_bg: Color,
pub hover_fg: Color,
}
#[derive(Clone)]
pub struct LinkStatusColors {
pub disabled: Color,
pub connected: Color,
pub listening: Color,
}
#[derive(Clone)]
pub struct SyntaxColors {
pub gap_bg: Color,
pub executed_bg: Color,
pub selected_bg: Color,
pub emit: (Color, Color),
pub number: (Color, Color),
pub string: (Color, Color),
pub comment: (Color, Color),
pub keyword: (Color, Color),
pub stack_op: (Color, Color),
pub operator: (Color, Color),
pub sound: (Color, Color),
pub param: (Color, Color),
pub context: (Color, Color),
pub note: (Color, Color),
pub interval: (Color, Color),
pub variable: (Color, Color),
pub vary: (Color, Color),
pub generator: (Color, Color),
pub default: (Color, Color),
}
#[derive(Clone)]
pub struct TableColors {
pub row_even: Color,
pub row_odd: Color,
}
#[derive(Clone)]
pub struct ValuesColors {
pub tempo: Color,
pub value: Color,
}
#[derive(Clone)]
pub struct HintColors {
pub key: Color,
pub text: Color,
}
#[derive(Clone)]
pub struct ViewBadgeColors {
pub bg: Color,
pub fg: Color,
}
#[derive(Clone)]
pub struct NavColors {
pub selected_bg: Color,
pub selected_fg: Color,
pub unselected_bg: Color,
pub unselected_fg: Color,
}
#[derive(Clone)]
pub struct EditorWidgetColors {
pub cursor_bg: Color,
pub cursor_fg: Color,
pub selection_bg: Color,
pub completion_bg: Color,
pub completion_fg: Color,
pub completion_selected: Color,
pub completion_example: Color,
}
#[derive(Clone)]
pub struct BrowserColors {
pub directory: Color,
pub project_file: Color,
pub selected: Color,
pub file: Color,
pub focused_border: Color,
pub unfocused_border: Color,
pub root: Color,
pub file_icon: Color,
pub folder_icon: Color,
pub empty_text: Color,
}
#[derive(Clone)]
pub struct InputColors {
pub text: Color,
pub cursor: Color,
pub hint: Color,
}
#[derive(Clone)]
pub struct SearchColors {
pub active: Color,
pub inactive: Color,
pub match_bg: Color,
pub match_fg: Color,
}
#[derive(Clone)]
pub struct MarkdownColors {
pub h1: Color,
pub h2: Color,
pub h3: Color,
pub code: Color,
pub code_border: Color,
pub link: Color,
pub link_url: Color,
pub quote: Color,
pub text: Color,
pub list: Color,
}
#[derive(Clone)]
pub struct EngineColors {
pub header: Color,
pub header_focused: Color,
pub divider: Color,
pub scroll_indicator: Color,
pub label: Color,
pub label_focused: Color,
pub label_dim: Color,
pub value: Color,
pub focused: Color,
pub normal: Color,
pub dim: Color,
pub path: Color,
pub border_magenta: Color,
pub border_green: Color,
pub border_cyan: Color,
pub separator: Color,
pub hint_active: Color,
pub hint_inactive: Color,
}
#[derive(Clone)]
pub struct DictColors {
pub word_name: Color,
pub word_bg: Color,
pub alias: Color,
pub stack_sig: Color,
pub description: Color,
pub example: Color,
pub category_focused: Color,
pub category_selected: Color,
pub category_normal: Color,
pub category_dimmed: Color,
pub border_focused: Color,
pub border_normal: Color,
pub header_desc: Color,
}
#[derive(Clone)]
pub struct TitleColors {
pub big_title: Color,
pub author: Color,
pub link: Color,
pub license: Color,
pub prompt: Color,
pub subtitle: Color,
}
#[derive(Clone)]
pub struct MeterColors {
pub low: Color,
pub mid: Color,
pub high: Color,
pub low_rgb: (u8, u8, u8),
pub mid_rgb: (u8, u8, u8),
pub high_rgb: (u8, u8, u8),
}
#[derive(Clone)]
pub struct SparkleColors {
pub colors: [(u8, u8, u8); 5],
}
#[derive(Clone)]
pub struct ConfirmColors {
pub border: Color,
pub button_selected_bg: Color,
pub button_selected_fg: Color,
}

View File

@@ -0,0 +1,275 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let bg = Color::Rgb(0, 0, 0);
let surface = Color::Rgb(18, 18, 18);
let surface2 = Color::Rgb(30, 30, 30);
let border = Color::Rgb(60, 60, 60);
let fg = Color::Rgb(255, 255, 255);
let fg_dim = Color::Rgb(180, 180, 180);
let fg_muted = Color::Rgb(120, 120, 120);
let bright = Color::Rgb(255, 255, 255);
let medium = Color::Rgb(180, 180, 180);
let dim = Color::Rgb(120, 120, 120);
let dark = Color::Rgb(80, 80, 80);
let darker = Color::Rgb(50, 50, 50);
ThemeColors {
ui: UiColors {
bg,
bg_rgb: (0, 0, 0),
text_primary: fg,
text_muted: fg_dim,
text_dim: fg_muted,
border,
header: bright,
unfocused: fg_muted,
accent: bright,
surface,
},
status: StatusColors {
playing_bg: Color::Rgb(40, 40, 40),
playing_fg: bright,
stopped_bg: Color::Rgb(25, 25, 25),
stopped_fg: medium,
fill_on: bright,
fill_off: dark,
fill_bg: surface,
},
selection: SelectionColors {
cursor_bg: bright,
cursor_fg: bg,
selected_bg: Color::Rgb(60, 60, 60),
selected_fg: bright,
in_range_bg: Color::Rgb(40, 40, 40),
in_range_fg: fg,
cursor: bright,
selected: Color::Rgb(60, 60, 60),
in_range: Color::Rgb(40, 40, 40),
},
tile: TileColors {
playing_active_bg: Color::Rgb(70, 70, 70),
playing_active_fg: bright,
playing_inactive_bg: Color::Rgb(50, 50, 50),
playing_inactive_fg: medium,
active_bg: Color::Rgb(45, 45, 45),
active_fg: bright,
inactive_bg: surface,
inactive_fg: fg_dim,
active_selected_bg: Color::Rgb(80, 80, 80),
active_in_range_bg: Color::Rgb(55, 55, 55),
link_bright: [
(255, 255, 255),
(200, 200, 200),
(160, 160, 160),
(220, 220, 220),
(180, 180, 180),
],
link_dim: [
(60, 60, 60),
(50, 50, 50),
(45, 45, 45),
(55, 55, 55),
(48, 48, 48),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(50, 50, 50),
tempo_fg: bright,
bank_bg: Color::Rgb(40, 40, 40),
bank_fg: medium,
pattern_bg: Color::Rgb(35, 35, 35),
pattern_fg: medium,
stats_bg: surface,
stats_fg: fg_dim,
},
modal: ModalColors {
border: bright,
border_accent: medium,
border_warn: fg_dim,
border_dim: fg_muted,
confirm: medium,
rename: medium,
input: bright,
editor: bright,
preview: fg_muted,
},
flash: FlashColors {
error_bg: Color::Rgb(60, 60, 60),
error_fg: bright,
success_bg: Color::Rgb(50, 50, 50),
success_fg: bright,
info_bg: surface,
info_fg: fg,
event_rgb: (40, 40, 40),
},
list: ListColors {
playing_bg: Color::Rgb(50, 50, 50),
playing_fg: bright,
staged_play_bg: Color::Rgb(45, 45, 45),
staged_play_fg: medium,
staged_stop_bg: Color::Rgb(35, 35, 35),
staged_stop_fg: dim,
edit_bg: Color::Rgb(40, 40, 40),
edit_fg: bright,
hover_bg: surface2,
hover_fg: fg,
},
link_status: LinkStatusColors {
disabled: dim,
connected: bright,
listening: medium,
},
syntax: SyntaxColors {
gap_bg: bg,
executed_bg: Color::Rgb(35, 35, 35),
selected_bg: Color::Rgb(55, 55, 55),
emit: (bright, Color::Rgb(45, 45, 45)),
number: (medium, Color::Rgb(35, 35, 35)),
string: (bright, Color::Rgb(40, 40, 40)),
comment: (dark, bg),
keyword: (bright, Color::Rgb(50, 50, 50)),
stack_op: (medium, Color::Rgb(30, 30, 30)),
operator: (medium, Color::Rgb(35, 35, 35)),
sound: (bright, Color::Rgb(45, 45, 45)),
param: (medium, Color::Rgb(35, 35, 35)),
context: (medium, Color::Rgb(30, 30, 30)),
note: (bright, Color::Rgb(40, 40, 40)),
interval: (medium, Color::Rgb(35, 35, 35)),
variable: (medium, Color::Rgb(30, 30, 30)),
vary: (dim, Color::Rgb(25, 25, 25)),
generator: (bright, Color::Rgb(45, 45, 45)),
default: (fg_dim, bg),
},
table: TableColors {
row_even: bg,
row_odd: surface,
},
values: ValuesColors {
tempo: bright,
value: fg_dim,
},
hint: HintColors {
key: bright,
text: fg_muted,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg },
nav: NavColors {
selected_bg: Color::Rgb(60, 60, 60),
selected_fg: fg,
unselected_bg: surface,
unselected_fg: fg_muted,
},
editor_widget: EditorWidgetColors {
cursor_bg: fg,
cursor_fg: bg,
selection_bg: Color::Rgb(60, 60, 60),
completion_bg: surface,
completion_fg: fg,
completion_selected: bright,
completion_example: medium,
},
browser: BrowserColors {
directory: medium,
project_file: bright,
selected: bright,
file: fg,
focused_border: bright,
unfocused_border: fg_muted,
root: fg,
file_icon: fg_muted,
folder_icon: medium,
empty_text: fg_muted,
},
input: InputColors {
text: bright,
cursor: fg,
hint: fg_muted,
},
search: SearchColors {
active: bright,
inactive: fg_muted,
match_bg: bright,
match_fg: bg,
},
markdown: MarkdownColors {
h1: bright,
h2: medium,
h3: dim,
code: medium,
code_border: Color::Rgb(60, 60, 60),
link: bright,
link_url: dim,
quote: fg_muted,
text: fg,
list: fg,
},
engine: EngineColors {
header: bright,
header_focused: bright,
divider: Color::Rgb(50, 50, 50),
scroll_indicator: Color::Rgb(70, 70, 70),
label: dim,
label_focused: medium,
label_dim: dark,
value: fg,
focused: bright,
normal: fg,
dim: dark,
path: dim,
border_magenta: medium,
border_green: medium,
border_cyan: medium,
separator: Color::Rgb(50, 50, 50),
hint_active: bright,
hint_inactive: darker,
},
dict: DictColors {
word_name: bright,
word_bg: Color::Rgb(30, 30, 30),
alias: fg_muted,
stack_sig: medium,
description: fg,
example: dim,
category_focused: bright,
category_selected: medium,
category_normal: fg,
category_dimmed: dark,
border_focused: bright,
border_normal: darker,
header_desc: dim,
},
title: TitleColors {
big_title: bright,
author: medium,
link: medium,
license: dim,
prompt: dim,
subtitle: fg,
},
meter: MeterColors {
low: dim,
mid: medium,
high: bright,
low_rgb: (120, 120, 120),
mid_rgb: (180, 180, 180),
high_rgb: (255, 255, 255),
},
sparkle: SparkleColors {
colors: [
(255, 255, 255),
(200, 200, 200),
(160, 160, 160),
(220, 220, 220),
(180, 180, 180),
],
},
confirm: ConfirmColors {
border: bright,
button_selected_bg: bright,
button_selected_fg: bg,
},
}
}

View File

@@ -0,0 +1,275 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let bg = Color::Rgb(255, 255, 255);
let surface = Color::Rgb(240, 240, 240);
let surface2 = Color::Rgb(225, 225, 225);
let border = Color::Rgb(180, 180, 180);
let fg = Color::Rgb(0, 0, 0);
let fg_dim = Color::Rgb(80, 80, 80);
let fg_muted = Color::Rgb(140, 140, 140);
let dark = Color::Rgb(0, 0, 0);
let medium = Color::Rgb(80, 80, 80);
let dim = Color::Rgb(140, 140, 140);
let light = Color::Rgb(180, 180, 180);
let lighter = Color::Rgb(210, 210, 210);
ThemeColors {
ui: UiColors {
bg,
bg_rgb: (255, 255, 255),
text_primary: fg,
text_muted: fg_dim,
text_dim: fg_muted,
border,
header: dark,
unfocused: fg_muted,
accent: dark,
surface,
},
status: StatusColors {
playing_bg: Color::Rgb(210, 210, 210),
playing_fg: dark,
stopped_bg: Color::Rgb(230, 230, 230),
stopped_fg: medium,
fill_on: dark,
fill_off: light,
fill_bg: surface,
},
selection: SelectionColors {
cursor_bg: dark,
cursor_fg: bg,
selected_bg: Color::Rgb(200, 200, 200),
selected_fg: dark,
in_range_bg: Color::Rgb(220, 220, 220),
in_range_fg: fg,
cursor: dark,
selected: Color::Rgb(200, 200, 200),
in_range: Color::Rgb(220, 220, 220),
},
tile: TileColors {
playing_active_bg: Color::Rgb(180, 180, 180),
playing_active_fg: dark,
playing_inactive_bg: Color::Rgb(200, 200, 200),
playing_inactive_fg: medium,
active_bg: Color::Rgb(210, 210, 210),
active_fg: dark,
inactive_bg: surface,
inactive_fg: fg_dim,
active_selected_bg: Color::Rgb(170, 170, 170),
active_in_range_bg: Color::Rgb(195, 195, 195),
link_bright: [
(0, 0, 0),
(60, 60, 60),
(100, 100, 100),
(40, 40, 40),
(80, 80, 80),
],
link_dim: [
(200, 200, 200),
(210, 210, 210),
(215, 215, 215),
(205, 205, 205),
(212, 212, 212),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(200, 200, 200),
tempo_fg: dark,
bank_bg: Color::Rgb(215, 215, 215),
bank_fg: medium,
pattern_bg: Color::Rgb(220, 220, 220),
pattern_fg: medium,
stats_bg: surface,
stats_fg: fg_dim,
},
modal: ModalColors {
border: dark,
border_accent: medium,
border_warn: fg_dim,
border_dim: fg_muted,
confirm: medium,
rename: medium,
input: dark,
editor: dark,
preview: fg_muted,
},
flash: FlashColors {
error_bg: Color::Rgb(200, 200, 200),
error_fg: dark,
success_bg: Color::Rgb(210, 210, 210),
success_fg: dark,
info_bg: surface,
info_fg: fg,
event_rgb: (220, 220, 220),
},
list: ListColors {
playing_bg: Color::Rgb(200, 200, 200),
playing_fg: dark,
staged_play_bg: Color::Rgb(210, 210, 210),
staged_play_fg: medium,
staged_stop_bg: Color::Rgb(220, 220, 220),
staged_stop_fg: dim,
edit_bg: Color::Rgb(215, 215, 215),
edit_fg: dark,
hover_bg: surface2,
hover_fg: fg,
},
link_status: LinkStatusColors {
disabled: dim,
connected: dark,
listening: medium,
},
syntax: SyntaxColors {
gap_bg: bg,
executed_bg: Color::Rgb(220, 220, 220),
selected_bg: Color::Rgb(200, 200, 200),
emit: (dark, Color::Rgb(215, 215, 215)),
number: (medium, Color::Rgb(225, 225, 225)),
string: (dark, Color::Rgb(220, 220, 220)),
comment: (light, bg),
keyword: (dark, Color::Rgb(205, 205, 205)),
stack_op: (medium, Color::Rgb(230, 230, 230)),
operator: (medium, Color::Rgb(225, 225, 225)),
sound: (dark, Color::Rgb(215, 215, 215)),
param: (medium, Color::Rgb(225, 225, 225)),
context: (medium, Color::Rgb(230, 230, 230)),
note: (dark, Color::Rgb(220, 220, 220)),
interval: (medium, Color::Rgb(225, 225, 225)),
variable: (medium, Color::Rgb(230, 230, 230)),
vary: (dim, Color::Rgb(235, 235, 235)),
generator: (dark, Color::Rgb(215, 215, 215)),
default: (fg_dim, bg),
},
table: TableColors {
row_even: bg,
row_odd: surface,
},
values: ValuesColors {
tempo: dark,
value: fg_dim,
},
hint: HintColors {
key: dark,
text: fg_muted,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg },
nav: NavColors {
selected_bg: Color::Rgb(200, 200, 200),
selected_fg: fg,
unselected_bg: surface,
unselected_fg: fg_muted,
},
editor_widget: EditorWidgetColors {
cursor_bg: fg,
cursor_fg: bg,
selection_bg: Color::Rgb(200, 200, 200),
completion_bg: surface,
completion_fg: fg,
completion_selected: dark,
completion_example: medium,
},
browser: BrowserColors {
directory: medium,
project_file: dark,
selected: dark,
file: fg,
focused_border: dark,
unfocused_border: fg_muted,
root: fg,
file_icon: fg_muted,
folder_icon: medium,
empty_text: fg_muted,
},
input: InputColors {
text: dark,
cursor: fg,
hint: fg_muted,
},
search: SearchColors {
active: dark,
inactive: fg_muted,
match_bg: dark,
match_fg: bg,
},
markdown: MarkdownColors {
h1: dark,
h2: medium,
h3: dim,
code: medium,
code_border: Color::Rgb(200, 200, 200),
link: dark,
link_url: dim,
quote: fg_muted,
text: fg,
list: fg,
},
engine: EngineColors {
header: dark,
header_focused: dark,
divider: Color::Rgb(210, 210, 210),
scroll_indicator: Color::Rgb(180, 180, 180),
label: dim,
label_focused: medium,
label_dim: light,
value: fg,
focused: dark,
normal: fg,
dim: light,
path: dim,
border_magenta: medium,
border_green: medium,
border_cyan: medium,
separator: Color::Rgb(210, 210, 210),
hint_active: dark,
hint_inactive: lighter,
},
dict: DictColors {
word_name: dark,
word_bg: Color::Rgb(230, 230, 230),
alias: fg_muted,
stack_sig: medium,
description: fg,
example: dim,
category_focused: dark,
category_selected: medium,
category_normal: fg,
category_dimmed: light,
border_focused: dark,
border_normal: lighter,
header_desc: dim,
},
title: TitleColors {
big_title: dark,
author: medium,
link: medium,
license: dim,
prompt: dim,
subtitle: fg,
},
meter: MeterColors {
low: dim,
mid: medium,
high: dark,
low_rgb: (140, 140, 140),
mid_rgb: (80, 80, 80),
high_rgb: (0, 0, 0),
},
sparkle: SparkleColors {
colors: [
(0, 0, 0),
(60, 60, 60),
(100, 100, 100),
(40, 40, 40),
(80, 80, 80),
],
},
confirm: ConfirmColors {
border: dark,
button_selected_bg: dark,
button_selected_fg: bg,
},
}
}

View File

@@ -0,0 +1,276 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let bg = Color::Rgb(39, 40, 34);
let bg_light = Color::Rgb(53, 54, 47);
let bg_lighter = Color::Rgb(70, 71, 62);
let fg = Color::Rgb(248, 248, 242);
let fg_dim = Color::Rgb(190, 190, 180);
let comment = Color::Rgb(117, 113, 94);
let pink = Color::Rgb(249, 38, 114);
let green = Color::Rgb(166, 226, 46);
let yellow = Color::Rgb(230, 219, 116);
let blue = Color::Rgb(102, 217, 239);
let purple = Color::Rgb(174, 129, 255);
let orange = Color::Rgb(253, 151, 31);
let darker_bg = Color::Rgb(30, 31, 26);
ThemeColors {
ui: UiColors {
bg,
bg_rgb: (39, 40, 34),
text_primary: fg,
text_muted: fg_dim,
text_dim: comment,
border: bg_lighter,
header: blue,
unfocused: comment,
accent: pink,
surface: bg_light,
},
status: StatusColors {
playing_bg: Color::Rgb(50, 65, 40),
playing_fg: green,
stopped_bg: Color::Rgb(70, 40, 55),
stopped_fg: pink,
fill_on: green,
fill_off: comment,
fill_bg: bg_light,
},
selection: SelectionColors {
cursor_bg: pink,
cursor_fg: bg,
selected_bg: Color::Rgb(85, 70, 80),
selected_fg: pink,
in_range_bg: Color::Rgb(70, 65, 70),
in_range_fg: fg,
cursor: pink,
selected: Color::Rgb(85, 70, 80),
in_range: Color::Rgb(70, 65, 70),
},
tile: TileColors {
playing_active_bg: Color::Rgb(90, 65, 45),
playing_active_fg: orange,
playing_inactive_bg: Color::Rgb(80, 75, 50),
playing_inactive_fg: yellow,
active_bg: Color::Rgb(55, 75, 70),
active_fg: blue,
inactive_bg: bg_light,
inactive_fg: fg_dim,
active_selected_bg: Color::Rgb(85, 65, 80),
active_in_range_bg: Color::Rgb(70, 65, 70),
link_bright: [
(249, 38, 114),
(174, 129, 255),
(253, 151, 31),
(102, 217, 239),
(166, 226, 46),
],
link_dim: [
(90, 40, 60),
(70, 55, 90),
(85, 60, 35),
(50, 75, 85),
(60, 80, 40),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(75, 50, 65),
tempo_fg: pink,
bank_bg: Color::Rgb(50, 70, 75),
bank_fg: blue,
pattern_bg: Color::Rgb(55, 75, 50),
pattern_fg: green,
stats_bg: bg_light,
stats_fg: fg_dim,
},
modal: ModalColors {
border: blue,
border_accent: pink,
border_warn: orange,
border_dim: comment,
confirm: orange,
rename: purple,
input: blue,
editor: blue,
preview: comment,
},
flash: FlashColors {
error_bg: Color::Rgb(75, 40, 55),
error_fg: pink,
success_bg: Color::Rgb(50, 70, 45),
success_fg: green,
info_bg: bg_light,
info_fg: fg,
event_rgb: (70, 55, 70),
},
list: ListColors {
playing_bg: Color::Rgb(50, 70, 45),
playing_fg: green,
staged_play_bg: Color::Rgb(70, 55, 80),
staged_play_fg: purple,
staged_stop_bg: Color::Rgb(80, 45, 60),
staged_stop_fg: pink,
edit_bg: Color::Rgb(50, 70, 70),
edit_fg: blue,
hover_bg: bg_lighter,
hover_fg: fg,
},
link_status: LinkStatusColors {
disabled: pink,
connected: green,
listening: yellow,
},
syntax: SyntaxColors {
gap_bg: darker_bg,
executed_bg: Color::Rgb(55, 50, 55),
selected_bg: Color::Rgb(85, 75, 50),
emit: (fg, Color::Rgb(85, 55, 65)),
number: (purple, Color::Rgb(60, 50, 75)),
string: (yellow, Color::Rgb(70, 65, 45)),
comment: (comment, darker_bg),
keyword: (pink, Color::Rgb(80, 45, 60)),
stack_op: (blue, Color::Rgb(50, 70, 75)),
operator: (pink, Color::Rgb(80, 45, 60)),
sound: (blue, Color::Rgb(50, 70, 75)),
param: (orange, Color::Rgb(80, 60, 40)),
context: (orange, Color::Rgb(80, 60, 40)),
note: (green, Color::Rgb(55, 75, 45)),
interval: (Color::Rgb(180, 235, 80), Color::Rgb(55, 75, 40)),
variable: (green, Color::Rgb(55, 75, 45)),
vary: (yellow, Color::Rgb(70, 65, 45)),
generator: (blue, Color::Rgb(50, 70, 70)),
default: (fg_dim, darker_bg),
},
table: TableColors {
row_even: darker_bg,
row_odd: bg,
},
values: ValuesColors {
tempo: orange,
value: fg_dim,
},
hint: HintColors {
key: orange,
text: comment,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg },
nav: NavColors {
selected_bg: Color::Rgb(80, 60, 75),
selected_fg: fg,
unselected_bg: bg_light,
unselected_fg: comment,
},
editor_widget: EditorWidgetColors {
cursor_bg: fg,
cursor_fg: bg,
selection_bg: Color::Rgb(75, 70, 75),
completion_bg: bg_light,
completion_fg: fg,
completion_selected: orange,
completion_example: blue,
},
browser: BrowserColors {
directory: blue,
project_file: purple,
selected: orange,
file: fg,
focused_border: orange,
unfocused_border: comment,
root: fg,
file_icon: comment,
folder_icon: blue,
empty_text: comment,
},
input: InputColors {
text: blue,
cursor: fg,
hint: comment,
},
search: SearchColors {
active: orange,
inactive: comment,
match_bg: yellow,
match_fg: bg,
},
markdown: MarkdownColors {
h1: blue,
h2: orange,
h3: purple,
code: green,
code_border: Color::Rgb(85, 85, 75),
link: pink,
link_url: Color::Rgb(130, 125, 115),
quote: comment,
text: fg,
list: fg,
},
engine: EngineColors {
header: blue,
header_focused: yellow,
divider: Color::Rgb(80, 80, 72),
scroll_indicator: Color::Rgb(95, 95, 88),
label: Color::Rgb(150, 145, 135),
label_focused: Color::Rgb(180, 175, 165),
label_dim: Color::Rgb(120, 115, 105),
value: Color::Rgb(210, 205, 195),
focused: yellow,
normal: fg,
dim: Color::Rgb(95, 95, 88),
path: Color::Rgb(150, 145, 135),
border_magenta: pink,
border_green: green,
border_cyan: blue,
separator: Color::Rgb(80, 80, 72),
hint_active: Color::Rgb(220, 200, 100),
hint_inactive: Color::Rgb(80, 80, 72),
},
dict: DictColors {
word_name: green,
word_bg: Color::Rgb(55, 65, 60),
alias: comment,
stack_sig: purple,
description: fg,
example: Color::Rgb(150, 145, 135),
category_focused: yellow,
category_selected: blue,
category_normal: fg,
category_dimmed: Color::Rgb(95, 95, 88),
border_focused: yellow,
border_normal: Color::Rgb(80, 80, 72),
header_desc: Color::Rgb(170, 165, 155),
},
title: TitleColors {
big_title: pink,
author: blue,
link: green,
license: orange,
prompt: Color::Rgb(170, 165, 155),
subtitle: fg,
},
meter: MeterColors {
low: green,
mid: yellow,
high: pink,
low_rgb: (155, 215, 45),
mid_rgb: (220, 210, 105),
high_rgb: (240, 50, 110),
},
sparkle: SparkleColors {
colors: [
(102, 217, 239),
(253, 151, 31),
(166, 226, 46),
(249, 38, 114),
(174, 129, 255),
],
},
confirm: ConfirmColors {
border: orange,
button_selected_bg: orange,
button_selected_fg: bg,
},
}
}

View File

@@ -0,0 +1,279 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let polar_night0 = Color::Rgb(46, 52, 64);
let polar_night1 = Color::Rgb(59, 66, 82);
let polar_night2 = Color::Rgb(67, 76, 94);
let polar_night3 = Color::Rgb(76, 86, 106);
let snow_storm0 = Color::Rgb(216, 222, 233);
let snow_storm2 = Color::Rgb(236, 239, 244);
let frost0 = Color::Rgb(143, 188, 187);
let frost1 = Color::Rgb(136, 192, 208);
let frost2 = Color::Rgb(129, 161, 193);
let aurora_red = Color::Rgb(191, 97, 106);
let aurora_orange = Color::Rgb(208, 135, 112);
let aurora_yellow = Color::Rgb(235, 203, 139);
let aurora_green = Color::Rgb(163, 190, 140);
let aurora_purple = Color::Rgb(180, 142, 173);
ThemeColors {
ui: UiColors {
bg: polar_night0,
bg_rgb: (46, 52, 64),
text_primary: snow_storm2,
text_muted: snow_storm0,
text_dim: polar_night3,
border: polar_night2,
header: frost1,
unfocused: polar_night3,
accent: frost1,
surface: polar_night1,
},
status: StatusColors {
playing_bg: Color::Rgb(50, 65, 60),
playing_fg: aurora_green,
stopped_bg: Color::Rgb(65, 50, 55),
stopped_fg: aurora_red,
fill_on: aurora_green,
fill_off: polar_night3,
fill_bg: polar_night1,
},
selection: SelectionColors {
cursor_bg: frost1,
cursor_fg: polar_night0,
selected_bg: Color::Rgb(70, 85, 105),
selected_fg: frost1,
in_range_bg: Color::Rgb(60, 70, 90),
in_range_fg: snow_storm0,
cursor: frost1,
selected: Color::Rgb(70, 85, 105),
in_range: Color::Rgb(60, 70, 90),
},
tile: TileColors {
playing_active_bg: Color::Rgb(80, 70, 65),
playing_active_fg: aurora_orange,
playing_inactive_bg: Color::Rgb(75, 70, 55),
playing_inactive_fg: aurora_yellow,
active_bg: Color::Rgb(50, 65, 65),
active_fg: frost0,
inactive_bg: polar_night1,
inactive_fg: snow_storm0,
active_selected_bg: Color::Rgb(75, 75, 95),
active_in_range_bg: Color::Rgb(60, 70, 85),
link_bright: [
(136, 192, 208),
(180, 142, 173),
(208, 135, 112),
(143, 188, 187),
(163, 190, 140),
],
link_dim: [
(55, 75, 85),
(70, 60, 70),
(75, 55, 50),
(55, 75, 75),
(60, 75, 55),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(65, 55, 70),
tempo_fg: aurora_purple,
bank_bg: Color::Rgb(45, 60, 70),
bank_fg: frost2,
pattern_bg: Color::Rgb(50, 65, 65),
pattern_fg: frost0,
stats_bg: polar_night1,
stats_fg: snow_storm0,
},
modal: ModalColors {
border: frost1,
border_accent: aurora_purple,
border_warn: aurora_orange,
border_dim: polar_night3,
confirm: aurora_orange,
rename: aurora_purple,
input: frost2,
editor: frost1,
preview: polar_night3,
},
flash: FlashColors {
error_bg: Color::Rgb(65, 50, 55),
error_fg: aurora_red,
success_bg: Color::Rgb(50, 65, 55),
success_fg: aurora_green,
info_bg: polar_night1,
info_fg: snow_storm2,
event_rgb: (60, 55, 75),
},
list: ListColors {
playing_bg: Color::Rgb(50, 65, 55),
playing_fg: aurora_green,
staged_play_bg: Color::Rgb(65, 55, 70),
staged_play_fg: aurora_purple,
staged_stop_bg: Color::Rgb(70, 55, 60),
staged_stop_fg: aurora_red,
edit_bg: Color::Rgb(50, 65, 65),
edit_fg: frost0,
hover_bg: polar_night2,
hover_fg: snow_storm2,
},
link_status: LinkStatusColors {
disabled: aurora_red,
connected: aurora_green,
listening: aurora_yellow,
},
syntax: SyntaxColors {
gap_bg: polar_night1,
executed_bg: Color::Rgb(55, 55, 70),
selected_bg: Color::Rgb(80, 70, 55),
emit: (snow_storm2, Color::Rgb(75, 55, 60)),
number: (aurora_orange, Color::Rgb(65, 55, 50)),
string: (aurora_green, Color::Rgb(50, 60, 50)),
comment: (polar_night3, polar_night0),
keyword: (aurora_purple, Color::Rgb(60, 50, 65)),
stack_op: (frost2, Color::Rgb(45, 55, 70)),
operator: (aurora_yellow, Color::Rgb(65, 60, 45)),
sound: (frost0, Color::Rgb(45, 60, 60)),
param: (frost1, Color::Rgb(50, 60, 70)),
context: (aurora_orange, Color::Rgb(65, 55, 50)),
note: (aurora_green, Color::Rgb(50, 60, 50)),
interval: (Color::Rgb(170, 200, 150), Color::Rgb(50, 60, 45)),
variable: (aurora_purple, Color::Rgb(60, 50, 60)),
vary: (aurora_yellow, Color::Rgb(65, 60, 45)),
generator: (frost0, Color::Rgb(45, 60, 55)),
default: (snow_storm0, polar_night1),
},
table: TableColors {
row_even: polar_night1,
row_odd: polar_night0,
},
values: ValuesColors {
tempo: aurora_orange,
value: snow_storm0,
},
hint: HintColors {
key: aurora_orange,
text: polar_night3,
},
view_badge: ViewBadgeColors {
bg: snow_storm2,
fg: polar_night0,
},
nav: NavColors {
selected_bg: Color::Rgb(65, 75, 95),
selected_fg: snow_storm2,
unselected_bg: polar_night1,
unselected_fg: polar_night3,
},
editor_widget: EditorWidgetColors {
cursor_bg: snow_storm2,
cursor_fg: polar_night0,
selection_bg: Color::Rgb(60, 75, 100),
completion_bg: polar_night1,
completion_fg: snow_storm2,
completion_selected: aurora_orange,
completion_example: frost0,
},
browser: BrowserColors {
directory: frost2,
project_file: aurora_purple,
selected: aurora_orange,
file: snow_storm2,
focused_border: aurora_orange,
unfocused_border: polar_night3,
root: snow_storm2,
file_icon: polar_night3,
folder_icon: frost2,
empty_text: polar_night3,
},
input: InputColors {
text: frost2,
cursor: snow_storm2,
hint: polar_night3,
},
search: SearchColors {
active: aurora_orange,
inactive: polar_night3,
match_bg: aurora_yellow,
match_fg: polar_night0,
},
markdown: MarkdownColors {
h1: frost2,
h2: aurora_orange,
h3: aurora_purple,
code: aurora_green,
code_border: Color::Rgb(75, 85, 100),
link: frost0,
link_url: Color::Rgb(100, 110, 125),
quote: polar_night3,
text: snow_storm2,
list: snow_storm2,
},
engine: EngineColors {
header: frost1,
header_focused: aurora_yellow,
divider: Color::Rgb(70, 80, 95),
scroll_indicator: Color::Rgb(85, 95, 110),
label: Color::Rgb(130, 140, 155),
label_focused: Color::Rgb(160, 170, 185),
label_dim: Color::Rgb(100, 110, 125),
value: Color::Rgb(190, 200, 215),
focused: aurora_yellow,
normal: snow_storm2,
dim: Color::Rgb(85, 95, 110),
path: Color::Rgb(130, 140, 155),
border_magenta: aurora_purple,
border_green: aurora_green,
border_cyan: frost2,
separator: Color::Rgb(70, 80, 95),
hint_active: Color::Rgb(200, 180, 100),
hint_inactive: Color::Rgb(70, 80, 95),
},
dict: DictColors {
word_name: aurora_green,
word_bg: Color::Rgb(50, 60, 75),
alias: polar_night3,
stack_sig: aurora_purple,
description: snow_storm2,
example: Color::Rgb(130, 140, 155),
category_focused: aurora_yellow,
category_selected: frost2,
category_normal: snow_storm2,
category_dimmed: Color::Rgb(85, 95, 110),
border_focused: aurora_yellow,
border_normal: Color::Rgb(70, 80, 95),
header_desc: Color::Rgb(150, 160, 175),
},
title: TitleColors {
big_title: frost1,
author: frost2,
link: frost0,
license: aurora_orange,
prompt: Color::Rgb(150, 160, 175),
subtitle: snow_storm2,
},
meter: MeterColors {
low: aurora_green,
mid: aurora_yellow,
high: aurora_red,
low_rgb: (140, 180, 130),
mid_rgb: (220, 190, 120),
high_rgb: (180, 90, 100),
},
sparkle: SparkleColors {
colors: [
(136, 192, 208),
(208, 135, 112),
(163, 190, 140),
(180, 142, 173),
(235, 203, 139),
],
},
confirm: ConfirmColors {
border: aurora_orange,
button_selected_bg: aurora_orange,
button_selected_fg: polar_night0,
},
}
}

View File

@@ -0,0 +1,277 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let bg = Color::Rgb(0, 0, 0);
let surface = Color::Rgb(10, 10, 10);
let surface2 = Color::Rgb(21, 21, 21);
let border = Color::Rgb(40, 40, 40);
let fg = Color::Rgb(230, 230, 230);
let fg_dim = Color::Rgb(160, 160, 160);
let fg_muted = Color::Rgb(100, 100, 100);
let red = Color::Rgb(255, 80, 80);
let green = Color::Rgb(80, 255, 120);
let yellow = Color::Rgb(255, 230, 80);
let blue = Color::Rgb(80, 180, 255);
let purple = Color::Rgb(200, 120, 255);
let cyan = Color::Rgb(80, 230, 230);
let orange = Color::Rgb(255, 160, 60);
ThemeColors {
ui: UiColors {
bg,
bg_rgb: (0, 0, 0),
text_primary: fg,
text_muted: fg_dim,
text_dim: fg_muted,
border,
header: blue,
unfocused: fg_muted,
accent: cyan,
surface,
},
status: StatusColors {
playing_bg: Color::Rgb(15, 35, 20),
playing_fg: green,
stopped_bg: Color::Rgb(40, 15, 20),
stopped_fg: red,
fill_on: green,
fill_off: fg_muted,
fill_bg: surface,
},
selection: SelectionColors {
cursor_bg: cyan,
cursor_fg: bg,
selected_bg: Color::Rgb(40, 50, 60),
selected_fg: cyan,
in_range_bg: Color::Rgb(25, 35, 45),
in_range_fg: fg,
cursor: cyan,
selected: Color::Rgb(40, 50, 60),
in_range: Color::Rgb(25, 35, 45),
},
tile: TileColors {
playing_active_bg: Color::Rgb(50, 35, 20),
playing_active_fg: orange,
playing_inactive_bg: Color::Rgb(45, 40, 15),
playing_inactive_fg: yellow,
active_bg: Color::Rgb(15, 40, 40),
active_fg: cyan,
inactive_bg: surface,
inactive_fg: fg_dim,
active_selected_bg: Color::Rgb(45, 40, 55),
active_in_range_bg: Color::Rgb(30, 35, 45),
link_bright: [
(80, 230, 230),
(200, 120, 255),
(255, 160, 60),
(80, 180, 255),
(80, 255, 120),
],
link_dim: [
(25, 60, 60),
(50, 35, 65),
(60, 45, 20),
(25, 50, 70),
(25, 65, 35),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(50, 35, 55),
tempo_fg: purple,
bank_bg: Color::Rgb(20, 45, 60),
bank_fg: blue,
pattern_bg: Color::Rgb(20, 55, 50),
pattern_fg: cyan,
stats_bg: surface,
stats_fg: fg_dim,
},
modal: ModalColors {
border: cyan,
border_accent: purple,
border_warn: orange,
border_dim: fg_muted,
confirm: orange,
rename: purple,
input: blue,
editor: cyan,
preview: fg_muted,
},
flash: FlashColors {
error_bg: Color::Rgb(50, 15, 20),
error_fg: red,
success_bg: Color::Rgb(15, 45, 25),
success_fg: green,
info_bg: surface,
info_fg: fg,
event_rgb: (40, 30, 50),
},
list: ListColors {
playing_bg: Color::Rgb(15, 45, 25),
playing_fg: green,
staged_play_bg: Color::Rgb(45, 30, 55),
staged_play_fg: purple,
staged_stop_bg: Color::Rgb(55, 25, 30),
staged_stop_fg: red,
edit_bg: Color::Rgb(15, 45, 45),
edit_fg: cyan,
hover_bg: surface2,
hover_fg: fg,
},
link_status: LinkStatusColors {
disabled: red,
connected: green,
listening: yellow,
},
syntax: SyntaxColors {
gap_bg: bg,
executed_bg: Color::Rgb(25, 25, 35),
selected_bg: Color::Rgb(55, 45, 25),
emit: (fg, Color::Rgb(50, 30, 35)),
number: (orange, Color::Rgb(50, 35, 20)),
string: (green, Color::Rgb(20, 45, 25)),
comment: (fg_muted, bg),
keyword: (purple, Color::Rgb(40, 25, 50)),
stack_op: (blue, Color::Rgb(20, 40, 55)),
operator: (yellow, Color::Rgb(50, 45, 20)),
sound: (cyan, Color::Rgb(20, 45, 45)),
param: (purple, Color::Rgb(40, 25, 50)),
context: (orange, Color::Rgb(50, 35, 20)),
note: (green, Color::Rgb(20, 45, 25)),
interval: (Color::Rgb(130, 255, 150), Color::Rgb(25, 55, 35)),
variable: (purple, Color::Rgb(40, 25, 50)),
vary: (yellow, Color::Rgb(50, 45, 20)),
generator: (cyan, Color::Rgb(20, 45, 40)),
default: (fg_dim, bg),
},
table: TableColors {
row_even: bg,
row_odd: surface,
},
values: ValuesColors {
tempo: orange,
value: fg_dim,
},
hint: HintColors {
key: orange,
text: fg_muted,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg },
nav: NavColors {
selected_bg: Color::Rgb(40, 45, 55),
selected_fg: fg,
unselected_bg: surface,
unselected_fg: fg_muted,
},
editor_widget: EditorWidgetColors {
cursor_bg: fg,
cursor_fg: bg,
selection_bg: Color::Rgb(40, 50, 65),
completion_bg: surface,
completion_fg: fg,
completion_selected: orange,
completion_example: cyan,
},
browser: BrowserColors {
directory: blue,
project_file: purple,
selected: orange,
file: fg,
focused_border: orange,
unfocused_border: fg_muted,
root: fg,
file_icon: fg_muted,
folder_icon: blue,
empty_text: fg_muted,
},
input: InputColors {
text: blue,
cursor: fg,
hint: fg_muted,
},
search: SearchColors {
active: orange,
inactive: fg_muted,
match_bg: yellow,
match_fg: bg,
},
markdown: MarkdownColors {
h1: blue,
h2: orange,
h3: purple,
code: green,
code_border: Color::Rgb(50, 50, 50),
link: cyan,
link_url: Color::Rgb(90, 90, 90),
quote: fg_muted,
text: fg,
list: fg,
},
engine: EngineColors {
header: blue,
header_focused: yellow,
divider: Color::Rgb(45, 45, 45),
scroll_indicator: Color::Rgb(60, 60, 60),
label: Color::Rgb(130, 130, 130),
label_focused: Color::Rgb(170, 170, 170),
label_dim: Color::Rgb(90, 90, 90),
value: Color::Rgb(200, 200, 200),
focused: yellow,
normal: fg,
dim: Color::Rgb(60, 60, 60),
path: Color::Rgb(130, 130, 130),
border_magenta: purple,
border_green: green,
border_cyan: cyan,
separator: Color::Rgb(45, 45, 45),
hint_active: Color::Rgb(220, 200, 80),
hint_inactive: Color::Rgb(45, 45, 45),
},
dict: DictColors {
word_name: green,
word_bg: Color::Rgb(20, 30, 35),
alias: fg_muted,
stack_sig: purple,
description: fg,
example: Color::Rgb(130, 130, 130),
category_focused: yellow,
category_selected: blue,
category_normal: fg,
category_dimmed: Color::Rgb(60, 60, 60),
border_focused: yellow,
border_normal: Color::Rgb(45, 45, 45),
header_desc: Color::Rgb(150, 150, 150),
},
title: TitleColors {
big_title: cyan,
author: blue,
link: green,
license: orange,
prompt: Color::Rgb(150, 150, 150),
subtitle: fg,
},
meter: MeterColors {
low: green,
mid: yellow,
high: red,
low_rgb: (70, 240, 110),
mid_rgb: (245, 220, 75),
high_rgb: (245, 75, 75),
},
sparkle: SparkleColors {
colors: [
(80, 230, 230),
(255, 160, 60),
(80, 255, 120),
(200, 120, 255),
(80, 180, 255),
],
},
confirm: ConfirmColors {
border: orange,
button_selected_bg: orange,
button_selected_fg: bg,
},
}
}

View File

@@ -0,0 +1,277 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let bg = Color::Rgb(25, 23, 36);
let bg_light = Color::Rgb(33, 32, 46);
let bg_lighter = Color::Rgb(42, 39, 63);
let fg = Color::Rgb(224, 222, 244);
let fg_dim = Color::Rgb(144, 140, 170);
let muted = Color::Rgb(110, 106, 134);
let rose = Color::Rgb(235, 111, 146);
let gold = Color::Rgb(246, 193, 119);
let foam = Color::Rgb(156, 207, 216);
let iris = Color::Rgb(196, 167, 231);
let pine = Color::Rgb(49, 116, 143);
let subtle = Color::Rgb(235, 188, 186);
let love = Color::Rgb(235, 111, 146);
let darker_bg = Color::Rgb(21, 19, 30);
ThemeColors {
ui: UiColors {
bg,
bg_rgb: (25, 23, 36),
text_primary: fg,
text_muted: fg_dim,
text_dim: muted,
border: bg_lighter,
header: foam,
unfocused: muted,
accent: rose,
surface: bg_light,
},
status: StatusColors {
playing_bg: Color::Rgb(35, 50, 55),
playing_fg: foam,
stopped_bg: Color::Rgb(55, 40, 50),
stopped_fg: love,
fill_on: foam,
fill_off: muted,
fill_bg: bg_light,
},
selection: SelectionColors {
cursor_bg: rose,
cursor_fg: bg,
selected_bg: Color::Rgb(60, 50, 70),
selected_fg: rose,
in_range_bg: Color::Rgb(50, 45, 60),
in_range_fg: fg,
cursor: rose,
selected: Color::Rgb(60, 50, 70),
in_range: Color::Rgb(50, 45, 60),
},
tile: TileColors {
playing_active_bg: Color::Rgb(65, 55, 50),
playing_active_fg: gold,
playing_inactive_bg: Color::Rgb(55, 55, 55),
playing_inactive_fg: subtle,
active_bg: Color::Rgb(35, 50, 60),
active_fg: foam,
inactive_bg: bg_light,
inactive_fg: fg_dim,
active_selected_bg: Color::Rgb(60, 50, 70),
active_in_range_bg: Color::Rgb(50, 45, 60),
link_bright: [
(235, 111, 146),
(196, 167, 231),
(246, 193, 119),
(156, 207, 216),
(49, 116, 143),
],
link_dim: [
(75, 45, 55),
(60, 50, 75),
(75, 60, 45),
(50, 65, 70),
(30, 50, 55),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(60, 45, 60),
tempo_fg: iris,
bank_bg: Color::Rgb(35, 50, 60),
bank_fg: foam,
pattern_bg: Color::Rgb(35, 55, 60),
pattern_fg: pine,
stats_bg: bg_light,
stats_fg: fg_dim,
},
modal: ModalColors {
border: foam,
border_accent: rose,
border_warn: gold,
border_dim: muted,
confirm: gold,
rename: iris,
input: foam,
editor: foam,
preview: muted,
},
flash: FlashColors {
error_bg: Color::Rgb(60, 40, 50),
error_fg: love,
success_bg: Color::Rgb(35, 55, 55),
success_fg: foam,
info_bg: bg_light,
info_fg: fg,
event_rgb: (50, 45, 60),
},
list: ListColors {
playing_bg: Color::Rgb(35, 55, 55),
playing_fg: foam,
staged_play_bg: Color::Rgb(55, 50, 70),
staged_play_fg: iris,
staged_stop_bg: Color::Rgb(60, 45, 55),
staged_stop_fg: love,
edit_bg: Color::Rgb(35, 50, 60),
edit_fg: foam,
hover_bg: bg_lighter,
hover_fg: fg,
},
link_status: LinkStatusColors {
disabled: love,
connected: foam,
listening: gold,
},
syntax: SyntaxColors {
gap_bg: darker_bg,
executed_bg: Color::Rgb(40, 40, 55),
selected_bg: Color::Rgb(65, 55, 50),
emit: (fg, Color::Rgb(60, 45, 60)),
number: (iris, Color::Rgb(55, 50, 70)),
string: (gold, Color::Rgb(65, 55, 45)),
comment: (muted, darker_bg),
keyword: (rose, Color::Rgb(60, 45, 55)),
stack_op: (foam, Color::Rgb(40, 55, 60)),
operator: (love, Color::Rgb(60, 45, 55)),
sound: (foam, Color::Rgb(40, 55, 60)),
param: (gold, Color::Rgb(65, 55, 45)),
context: (gold, Color::Rgb(65, 55, 45)),
note: (pine, Color::Rgb(35, 50, 55)),
interval: (Color::Rgb(100, 160, 180), Color::Rgb(35, 55, 60)),
variable: (pine, Color::Rgb(35, 50, 55)),
vary: (subtle, Color::Rgb(60, 55, 55)),
generator: (foam, Color::Rgb(40, 55, 60)),
default: (fg_dim, darker_bg),
},
table: TableColors {
row_even: darker_bg,
row_odd: bg,
},
values: ValuesColors {
tempo: gold,
value: fg_dim,
},
hint: HintColors {
key: gold,
text: muted,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg },
nav: NavColors {
selected_bg: Color::Rgb(60, 50, 70),
selected_fg: fg,
unselected_bg: bg_light,
unselected_fg: muted,
},
editor_widget: EditorWidgetColors {
cursor_bg: fg,
cursor_fg: bg,
selection_bg: Color::Rgb(55, 50, 70),
completion_bg: bg_light,
completion_fg: fg,
completion_selected: gold,
completion_example: foam,
},
browser: BrowserColors {
directory: foam,
project_file: iris,
selected: gold,
file: fg,
focused_border: gold,
unfocused_border: muted,
root: fg,
file_icon: muted,
folder_icon: foam,
empty_text: muted,
},
input: InputColors {
text: foam,
cursor: fg,
hint: muted,
},
search: SearchColors {
active: gold,
inactive: muted,
match_bg: gold,
match_fg: bg,
},
markdown: MarkdownColors {
h1: foam,
h2: gold,
h3: iris,
code: pine,
code_border: Color::Rgb(60, 55, 75),
link: rose,
link_url: Color::Rgb(100, 95, 120),
quote: muted,
text: fg,
list: fg,
},
engine: EngineColors {
header: foam,
header_focused: gold,
divider: Color::Rgb(55, 52, 70),
scroll_indicator: Color::Rgb(70, 65, 90),
label: Color::Rgb(130, 125, 155),
label_focused: Color::Rgb(160, 155, 185),
label_dim: Color::Rgb(100, 95, 125),
value: Color::Rgb(200, 195, 220),
focused: gold,
normal: fg,
dim: Color::Rgb(70, 65, 90),
path: Color::Rgb(130, 125, 155),
border_magenta: iris,
border_green: foam,
border_cyan: pine,
separator: Color::Rgb(55, 52, 70),
hint_active: Color::Rgb(230, 180, 110),
hint_inactive: Color::Rgb(55, 52, 70),
},
dict: DictColors {
word_name: pine,
word_bg: Color::Rgb(40, 50, 55),
alias: muted,
stack_sig: iris,
description: fg,
example: Color::Rgb(130, 125, 155),
category_focused: gold,
category_selected: foam,
category_normal: fg,
category_dimmed: Color::Rgb(70, 65, 90),
border_focused: gold,
border_normal: Color::Rgb(55, 52, 70),
header_desc: Color::Rgb(150, 145, 175),
},
title: TitleColors {
big_title: rose,
author: foam,
link: pine,
license: gold,
prompt: Color::Rgb(150, 145, 175),
subtitle: fg,
},
meter: MeterColors {
low: foam,
mid: gold,
high: love,
low_rgb: (156, 207, 216),
mid_rgb: (246, 193, 119),
high_rgb: (235, 111, 146),
},
sparkle: SparkleColors {
colors: [
(156, 207, 216),
(246, 193, 119),
(49, 116, 143),
(235, 111, 146),
(196, 167, 231),
],
},
confirm: ConfirmColors {
border: gold,
button_selected_bg: gold,
button_selected_fg: bg,
},
}
}

View File

@@ -0,0 +1,277 @@
use super::*;
use ratatui::style::Color;
pub fn theme() -> ThemeColors {
let bg = Color::Rgb(26, 27, 38);
let bg_light = Color::Rgb(36, 40, 59);
let bg_lighter = Color::Rgb(52, 59, 88);
let fg = Color::Rgb(169, 177, 214);
let fg_dim = Color::Rgb(130, 140, 180);
let comment = Color::Rgb(86, 95, 137);
let blue = Color::Rgb(122, 162, 247);
let purple = Color::Rgb(187, 154, 247);
let green = Color::Rgb(158, 206, 106);
let red = Color::Rgb(247, 118, 142);
let orange = Color::Rgb(224, 175, 104);
let cyan = Color::Rgb(125, 207, 255);
let yellow = Color::Rgb(224, 175, 104);
let darker_bg = Color::Rgb(22, 23, 32);
ThemeColors {
ui: UiColors {
bg,
bg_rgb: (26, 27, 38),
text_primary: fg,
text_muted: fg_dim,
text_dim: comment,
border: bg_lighter,
header: blue,
unfocused: comment,
accent: purple,
surface: bg_light,
},
status: StatusColors {
playing_bg: Color::Rgb(45, 60, 50),
playing_fg: green,
stopped_bg: Color::Rgb(60, 40, 50),
stopped_fg: red,
fill_on: green,
fill_off: comment,
fill_bg: bg_light,
},
selection: SelectionColors {
cursor_bg: purple,
cursor_fg: bg,
selected_bg: Color::Rgb(70, 60, 90),
selected_fg: purple,
in_range_bg: Color::Rgb(55, 55, 75),
in_range_fg: fg,
cursor: purple,
selected: Color::Rgb(70, 60, 90),
in_range: Color::Rgb(55, 55, 75),
},
tile: TileColors {
playing_active_bg: Color::Rgb(70, 60, 45),
playing_active_fg: orange,
playing_inactive_bg: Color::Rgb(60, 60, 50),
playing_inactive_fg: yellow,
active_bg: Color::Rgb(45, 60, 75),
active_fg: blue,
inactive_bg: bg_light,
inactive_fg: fg_dim,
active_selected_bg: Color::Rgb(70, 55, 85),
active_in_range_bg: Color::Rgb(55, 55, 75),
link_bright: [
(247, 118, 142),
(187, 154, 247),
(224, 175, 104),
(125, 207, 255),
(158, 206, 106),
],
link_dim: [
(80, 45, 55),
(65, 55, 85),
(75, 60, 40),
(45, 70, 85),
(55, 70, 45),
],
},
header: HeaderColors {
tempo_bg: Color::Rgb(65, 50, 70),
tempo_fg: purple,
bank_bg: Color::Rgb(45, 55, 75),
bank_fg: blue,
pattern_bg: Color::Rgb(50, 65, 50),
pattern_fg: green,
stats_bg: bg_light,
stats_fg: fg_dim,
},
modal: ModalColors {
border: blue,
border_accent: purple,
border_warn: orange,
border_dim: comment,
confirm: orange,
rename: purple,
input: blue,
editor: blue,
preview: comment,
},
flash: FlashColors {
error_bg: Color::Rgb(65, 40, 50),
error_fg: red,
success_bg: Color::Rgb(45, 60, 45),
success_fg: green,
info_bg: bg_light,
info_fg: fg,
event_rgb: (55, 50, 70),
},
list: ListColors {
playing_bg: Color::Rgb(45, 60, 45),
playing_fg: green,
staged_play_bg: Color::Rgb(60, 50, 75),
staged_play_fg: purple,
staged_stop_bg: Color::Rgb(70, 45, 55),
staged_stop_fg: red,
edit_bg: Color::Rgb(45, 55, 70),
edit_fg: blue,
hover_bg: bg_lighter,
hover_fg: fg,
},
link_status: LinkStatusColors {
disabled: red,
connected: green,
listening: yellow,
},
syntax: SyntaxColors {
gap_bg: darker_bg,
executed_bg: Color::Rgb(45, 45, 60),
selected_bg: Color::Rgb(70, 60, 50),
emit: (fg, Color::Rgb(70, 50, 65)),
number: (purple, Color::Rgb(55, 50, 70)),
string: (green, Color::Rgb(50, 60, 50)),
comment: (comment, darker_bg),
keyword: (purple, Color::Rgb(60, 50, 70)),
stack_op: (cyan, Color::Rgb(45, 60, 75)),
operator: (red, Color::Rgb(65, 45, 55)),
sound: (blue, Color::Rgb(45, 55, 70)),
param: (orange, Color::Rgb(70, 55, 45)),
context: (orange, Color::Rgb(70, 55, 45)),
note: (green, Color::Rgb(50, 60, 45)),
interval: (Color::Rgb(180, 220, 130), Color::Rgb(50, 65, 45)),
variable: (green, Color::Rgb(50, 60, 45)),
vary: (yellow, Color::Rgb(70, 60, 45)),
generator: (cyan, Color::Rgb(45, 60, 75)),
default: (fg_dim, darker_bg),
},
table: TableColors {
row_even: darker_bg,
row_odd: bg,
},
values: ValuesColors {
tempo: orange,
value: fg_dim,
},
hint: HintColors {
key: orange,
text: comment,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg },
nav: NavColors {
selected_bg: Color::Rgb(65, 55, 80),
selected_fg: fg,
unselected_bg: bg_light,
unselected_fg: comment,
},
editor_widget: EditorWidgetColors {
cursor_bg: fg,
cursor_fg: bg,
selection_bg: Color::Rgb(60, 60, 80),
completion_bg: bg_light,
completion_fg: fg,
completion_selected: orange,
completion_example: cyan,
},
browser: BrowserColors {
directory: blue,
project_file: purple,
selected: orange,
file: fg,
focused_border: orange,
unfocused_border: comment,
root: fg,
file_icon: comment,
folder_icon: blue,
empty_text: comment,
},
input: InputColors {
text: blue,
cursor: fg,
hint: comment,
},
search: SearchColors {
active: orange,
inactive: comment,
match_bg: yellow,
match_fg: bg,
},
markdown: MarkdownColors {
h1: blue,
h2: orange,
h3: purple,
code: green,
code_border: Color::Rgb(70, 75, 95),
link: red,
link_url: Color::Rgb(110, 120, 160),
quote: comment,
text: fg,
list: fg,
},
engine: EngineColors {
header: blue,
header_focused: yellow,
divider: Color::Rgb(65, 70, 90),
scroll_indicator: Color::Rgb(80, 85, 110),
label: Color::Rgb(130, 140, 175),
label_focused: Color::Rgb(160, 170, 200),
label_dim: Color::Rgb(100, 110, 145),
value: Color::Rgb(190, 195, 220),
focused: yellow,
normal: fg,
dim: Color::Rgb(80, 85, 110),
path: Color::Rgb(130, 140, 175),
border_magenta: purple,
border_green: green,
border_cyan: cyan,
separator: Color::Rgb(65, 70, 90),
hint_active: Color::Rgb(210, 180, 100),
hint_inactive: Color::Rgb(65, 70, 90),
},
dict: DictColors {
word_name: green,
word_bg: Color::Rgb(45, 55, 60),
alias: comment,
stack_sig: purple,
description: fg,
example: Color::Rgb(130, 140, 175),
category_focused: yellow,
category_selected: blue,
category_normal: fg,
category_dimmed: Color::Rgb(80, 85, 110),
border_focused: yellow,
border_normal: Color::Rgb(65, 70, 90),
header_desc: Color::Rgb(150, 160, 190),
},
title: TitleColors {
big_title: purple,
author: blue,
link: green,
license: orange,
prompt: Color::Rgb(150, 160, 190),
subtitle: fg,
},
meter: MeterColors {
low: green,
mid: yellow,
high: red,
low_rgb: (158, 206, 106),
mid_rgb: (224, 175, 104),
high_rgb: (247, 118, 142),
},
sparkle: SparkleColors {
colors: [
(125, 207, 255),
(224, 175, 104),
(158, 206, 106),
(247, 118, 142),
(187, 154, 247),
],
},
confirm: ConfirmColors {
border: orange,
button_selected_bg: orange,
button_selected_fg: bg,
},
}
}

View File

@@ -1,4 +1,4 @@
use crate::theme::meter;
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
@@ -30,13 +30,13 @@ impl VuMeter {
(db - DB_MIN) / DB_RANGE
}
fn row_to_color(row_position: f32) -> Color {
fn row_to_color(row_position: f32, colors: &theme::ThemeColors) -> Color {
if row_position > 0.9 {
meter::HIGH
colors.meter.high
} else if row_position > 0.75 {
meter::MID
colors.meter.mid
} else {
meter::LOW
colors.meter.low
}
}
}
@@ -47,6 +47,7 @@ impl Widget for VuMeter {
return;
}
let colors = theme::get();
let height = area.height as usize;
let half_width = area.width / 2;
let gap = 1u16;
@@ -62,7 +63,7 @@ impl Widget for VuMeter {
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);
let color = Self::row_to_color(row_position, &colors);
for col in 0..half_width.saturating_sub(gap) {
let x = area.x + col;

View File

@@ -120,14 +120,24 @@ impl App {
midi: crate::settings::MidiSettings {
output_devices: {
let outputs = crate::midi::list_midi_outputs();
self.midi.selected_outputs.iter()
.map(|opt| opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone())).unwrap_or_default())
self.midi
.selected_outputs
.iter()
.map(|opt| {
opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone()))
.unwrap_or_default()
})
.collect()
},
input_devices: {
let inputs = crate::midi::list_midi_inputs();
self.midi.selected_inputs.iter()
.map(|opt| opt.and_then(|idx| inputs.get(idx).map(|d| d.name.clone())).unwrap_or_default())
self.midi
.selected_inputs
.iter()
.map(|opt| {
opt.and_then(|idx| inputs.get(idx).map(|d| d.name.clone()))
.unwrap_or_default()
})
.collect()
},
},
@@ -139,6 +149,21 @@ impl App {
(self.editor_ctx.bank, self.editor_ctx.pattern)
}
fn selected_steps(&self) -> Vec<usize> {
match self.editor_ctx.selection_range() {
Some(range) => range.collect(),
None => vec![self.editor_ctx.step],
}
}
fn annotate_copy_name(name: &Option<String>) -> Option<String> {
match name {
Some(n) if !n.ends_with(" (copy)") => Some(format!("{n} (copy)")),
Some(n) => Some(n.clone()),
None => Some("(copy)".to_string()),
}
}
pub fn mark_all_patterns_dirty(&mut self) {
self.project_state.mark_all_dirty();
}
@@ -216,17 +241,8 @@ impl App {
pub fn toggle_steps(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
Some(range) => range.collect(),
None => vec![self.editor_ctx.step],
};
for idx in indices {
pattern_editor::toggle_step(
&mut self.project_state.project,
bank,
pattern,
idx,
);
for idx in self.selected_steps() {
pattern_editor::toggle_step(&mut self.project_state.project, bank, pattern, idx);
}
self.project_state.mark_dirty(bank, pattern);
}
@@ -261,6 +277,37 @@ impl App {
self.project_state.mark_dirty(change.bank, change.pattern);
}
fn create_step_context(&self, step_idx: usize, link: &LinkState) -> StepContext {
let (bank, pattern) = self.current_bank_pattern();
let speed = self
.project_state
.project
.pattern_at(bank, pattern)
.speed
.multiplier();
StepContext {
step: step_idx,
beat: link.beat(),
bank,
pattern,
tempo: link.tempo(),
phase: link.phase(),
slot: 0,
runs: 0,
iter: 0,
speed,
fill: false,
nudge_secs: 0.0,
cc_access: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
}
}
fn load_step_to_editor(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
if let Some(script) = pattern_editor::get_step_script(
@@ -310,37 +357,7 @@ impl App {
link: &LinkState,
audio_tx: &arc_swap::ArcSwap<Sender<crate::engine::AudioCommand>>,
) -> Result<(), String> {
let (bank, pattern) = self.current_bank_pattern();
let step_idx = self.editor_ctx.step;
let speed = self
.project_state
.project
.pattern_at(bank, pattern)
.speed
.multiplier();
let ctx = StepContext {
step: step_idx,
beat: link.beat(),
bank,
pattern,
tempo: link.tempo(),
phase: link.phase(),
slot: 0,
runs: 0,
iter: 0,
speed,
fill: false,
nudge_secs: 0.0,
cc_memory: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
};
let ctx = self.create_step_context(self.editor_ctx.step, link);
let cmds = self.script_engine.evaluate(script, &ctx)?;
for cmd in cmds {
let _ = audio_tx
@@ -370,33 +387,7 @@ impl App {
return;
}
let speed = self
.project_state
.project
.pattern_at(bank, pattern)
.speed
.multiplier();
let ctx = StepContext {
step: step_idx,
beat: link.beat(),
bank,
pattern,
tempo: link.tempo(),
phase: link.phase(),
slot: 0,
runs: 0,
iter: 0,
speed,
fill: false,
nudge_secs: 0.0,
cc_memory: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
};
let ctx = self.create_step_context(step_idx, link);
match self.script_engine.evaluate(&script, &ctx) {
Ok(cmds) => {
@@ -454,33 +445,7 @@ impl App {
continue;
}
let speed = self
.project_state
.project
.pattern_at(bank, pattern)
.speed
.multiplier();
let ctx = StepContext {
step: step_idx,
beat: 0.0,
bank,
pattern,
tempo: link.tempo(),
phase: 0.0,
slot: 0,
runs: 0,
iter: 0,
speed,
fill: false,
nudge_secs: 0.0,
cc_memory: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
};
let ctx = self.create_step_context(step_idx, link);
if let Ok(cmds) = self.script_engine.evaluate(&script, &ctx) {
if let Some(step) = self
@@ -582,7 +547,8 @@ impl App {
self.project_state.project.tempo = link.tempo();
match model::save(&self.project_state.project, &path) {
Ok(final_path) => {
self.ui.set_status(format!("Saved: {}", final_path.display()));
self.ui
.set_status(format!("Saved: {}", final_path.display()));
self.project_state.file_path = Some(final_path);
}
Err(e) => {
@@ -715,11 +681,7 @@ impl App {
pub fn paste_pattern(&mut self, bank: usize, pattern: usize) {
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!("{name} (copy)")),
Some(name) => Some(name.clone()),
None => Some("(copy)".to_string()),
};
pat.name = Self::annotate_copy_name(&src.name);
self.project_state.project.banks[bank].patterns[pattern] = pat;
self.project_state.mark_dirty(bank, pattern);
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
@@ -738,11 +700,7 @@ impl App {
pub fn paste_bank(&mut self, bank: usize) {
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!("{name} (copy)")),
Some(name) => Some(name.clone()),
None => Some("(copy)".to_string()),
};
b.name = Self::annotate_copy_name(&src.name);
self.project_state.project.banks[bank] = b;
for pattern in 0..self.project_state.project.banks[bank].patterns.len() {
self.project_state.mark_dirty(bank, pattern);
@@ -756,10 +714,7 @@ impl App {
pub fn harden_steps(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
Some(range) => range.collect(),
None => vec![self.editor_ctx.step],
};
let indices = self.selected_steps();
let pat = self.project_state.project.pattern_at(bank, pattern);
let resolutions: Vec<(usize, String)> = indices
@@ -796,18 +751,15 @@ impl App {
if count == 1 {
self.ui.flash("Step hardened", 150, FlashKind::Success);
} else {
self.ui.flash(&format!("{count} steps hardened"), 150, FlashKind::Success);
self.ui
.flash(&format!("{count} steps hardened"), 150, FlashKind::Success);
}
}
pub fn copy_steps(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let pat = self.project_state.project.pattern_at(bank, pattern);
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
Some(range) => range.collect(),
None => vec![self.editor_ctx.step],
};
let indices = self.selected_steps();
let mut steps = Vec::new();
let mut scripts = Vec::new();
@@ -836,7 +788,8 @@ impl App {
let _ = clip.set_text(scripts.join("\n"));
}
self.ui.flash(&format!("Copied {count} steps"), 150, FlashKind::Info);
self.ui
.flash(&format!("Copied {count} steps"), 150, FlashKind::Info);
}
pub fn paste_steps(&mut self, link: &LinkState) {
@@ -855,7 +808,12 @@ impl App {
if target >= pat_len {
break;
}
if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(target)
{
let source = if same_pattern { data.source } else { None };
step.active = data.active;
step.source = source;
@@ -885,7 +843,11 @@ impl App {
}
self.editor_ctx.clear_selection();
self.ui.flash(&format!("Pasted {} steps", copied.steps.len()), 150, FlashKind::Success);
self.ui.flash(
&format!("Pasted {} steps", copied.steps.len()),
150,
FlashKind::Success,
);
}
pub fn link_paste_steps(&mut self) {
@@ -897,7 +859,8 @@ impl App {
let (bank, pattern) = self.current_bank_pattern();
if copied.bank != bank || copied.pattern != pattern {
self.ui.set_status("Can only link within same pattern".to_string());
self.ui
.set_status("Can only link within same pattern".to_string());
return;
}
@@ -918,7 +881,12 @@ impl App {
if source_idx == Some(target) {
continue;
}
if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(target)
{
step.source = source_idx;
step.script.clear();
step.command = None;
@@ -928,17 +896,18 @@ impl App {
self.project_state.mark_dirty(bank, pattern);
self.load_step_to_editor();
self.editor_ctx.clear_selection();
self.ui.flash(&format!("Linked {} steps", copied.steps.len()), 150, FlashKind::Success);
self.ui.flash(
&format!("Linked {} steps", copied.steps.len()),
150,
FlashKind::Success,
);
}
pub fn duplicate_steps(&mut self, link: &LinkState) {
let (bank, pattern) = self.current_bank_pattern();
let pat = self.project_state.project.pattern_at(bank, pattern);
let pat_len = pat.length;
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
Some(range) => range.collect(),
None => vec![self.editor_ctx.step],
};
let indices = self.selected_steps();
let count = indices.len();
let paste_at = *indices.last().unwrap() + 1;
@@ -958,7 +927,12 @@ impl App {
if target >= pat_len {
break;
}
if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(target)
{
step.active = active;
step.source = source;
if source.is_some() {
@@ -984,7 +958,11 @@ impl App {
}
self.editor_ctx.clear_selection();
self.ui.flash(&format!("Duplicated {count} steps"), 150, FlashKind::Success);
self.ui.flash(
&format!("Duplicated {count} steps"),
150,
FlashKind::Success,
);
}
pub fn open_pattern_modal(&mut self, field: PatternField) {
@@ -1139,7 +1117,9 @@ impl App {
step,
name,
} => {
if let Some(s) = self.project_state.project.banks[bank].patterns[pattern].step_mut(step) {
if let Some(s) =
self.project_state.project.banks[bank].patterns[pattern].step_mut(step)
{
s.name = name;
}
self.project_state.mark_dirty(bank, pattern);
@@ -1323,6 +1303,160 @@ impl App {
let pattern = self.patterns_nav.selected_pattern();
self.stage_pattern_toggle(bank, pattern, snapshot);
}
// UI state
AppCommand::ClearMinimap => {
self.ui.minimap_until = None;
}
AppCommand::HideTitle => {
self.ui.show_title = false;
}
AppCommand::ToggleEditorStack => {
self.editor_ctx.show_stack = !self.editor_ctx.show_stack;
}
AppCommand::SetColorScheme(scheme) => {
self.ui.color_scheme = scheme;
crate::theme::set(scheme.to_theme());
}
AppCommand::ToggleRuntimeHighlight => {
self.ui.runtime_highlight = !self.ui.runtime_highlight;
}
AppCommand::ToggleCompletion => {
self.ui.show_completion = !self.ui.show_completion;
self.editor_ctx
.editor
.set_completion_enabled(self.ui.show_completion);
}
AppCommand::AdjustFlashBrightness(delta) => {
self.ui.flash_brightness = (self.ui.flash_brightness + delta).clamp(0.0, 1.0);
}
// Live keys
AppCommand::ToggleLiveKeysFill => {
self.live_keys.flip_fill();
}
// Panel
AppCommand::ClosePanel => {
self.panel.visible = false;
self.panel.focus = crate::state::PanelFocus::Main;
}
// Selection
AppCommand::SetSelectionAnchor(step) => {
self.editor_ctx.selection_anchor = Some(step);
}
AppCommand::ClearSelectionAnchor => {
self.editor_ctx.selection_anchor = None;
}
// Audio settings (engine page)
AppCommand::AudioNextSection => {
self.audio.next_section();
}
AppCommand::AudioPrevSection => {
self.audio.prev_section();
}
AppCommand::AudioOutputListUp => {
self.audio.output_list.move_up();
}
AppCommand::AudioOutputListDown(count) => {
self.audio.output_list.move_down(count);
}
AppCommand::AudioOutputPageUp => {
self.audio.output_list.page_up();
}
AppCommand::AudioOutputPageDown(count) => {
self.audio.output_list.page_down(count);
}
AppCommand::AudioInputListUp => {
self.audio.input_list.move_up();
}
AppCommand::AudioInputListDown(count) => {
self.audio.input_list.move_down(count);
}
AppCommand::AudioInputPageDown(count) => {
self.audio.input_list.page_down(count);
}
AppCommand::AudioSettingNext => {
self.audio.setting_kind = self.audio.setting_kind.next();
}
AppCommand::AudioSettingPrev => {
self.audio.setting_kind = self.audio.setting_kind.prev();
}
AppCommand::SetOutputDevice(name) => {
self.audio.config.output_device = Some(name);
}
AppCommand::SetInputDevice(name) => {
self.audio.config.input_device = Some(name);
}
AppCommand::SetDeviceKind(kind) => {
self.audio.device_kind = kind;
}
AppCommand::AdjustAudioSetting { setting, delta } => {
use crate::state::SettingKind;
match setting {
SettingKind::Channels => self.audio.adjust_channels(delta as i16),
SettingKind::BufferSize => self.audio.adjust_buffer_size(delta),
SettingKind::Polyphony => self.audio.adjust_max_voices(delta),
SettingKind::Nudge => {
self.metrics.nudge_ms =
(self.metrics.nudge_ms + delta as f64).clamp(-50.0, 50.0);
}
SettingKind::Lookahead => self.audio.adjust_lookahead(delta),
}
}
AppCommand::AudioTriggerRestart => {
self.audio.trigger_restart();
}
AppCommand::RemoveLastSamplePath => {
self.audio.remove_last_sample_path();
}
AppCommand::AudioRefreshDevices => {
self.audio.refresh_devices();
}
// Options page
AppCommand::OptionsNextFocus => {
self.options.next_focus();
}
AppCommand::OptionsPrevFocus => {
self.options.prev_focus();
}
AppCommand::ToggleRefreshRate => {
self.audio.toggle_refresh_rate();
}
AppCommand::ToggleScope => {
self.audio.config.show_scope = !self.audio.config.show_scope;
}
AppCommand::ToggleSpectrum => {
self.audio.config.show_spectrum = !self.audio.config.show_spectrum;
}
// Metrics
AppCommand::ResetPeakVoices => {
self.metrics.peak_voices = 0;
}
// MIDI connections
AppCommand::ConnectMidiOutput { slot, port } => {
if let Err(e) = self.midi.connect_output(slot, port) {
self.ui
.flash(&format!("MIDI error: {e}"), 300, FlashKind::Error);
}
}
AppCommand::DisconnectMidiOutput(slot) => {
self.midi.disconnect_output(slot);
}
AppCommand::ConnectMidiInput { slot, port } => {
if let Err(e) = self.midi.connect_input(slot, port) {
self.ui
.flash(&format!("MIDI error: {e}"), 300, FlashKind::Error);
}
}
AppCommand::DisconnectMidiInput(slot) => {
self.midi.disconnect_input(slot);
}
}
}

View File

@@ -8,12 +8,10 @@ use eframe::NativeOptions;
use egui_ratatui::RataguiBackend;
use ratatui::Terminal;
use soft_ratatui::embedded_graphics_unicodefonts::{
mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas,
mono_8x13_atlas, mono_8x13_bold_atlas, mono_8x13_italic_atlas,
mono_9x15_atlas, mono_9x15_bold_atlas,
mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
mono_9x18_atlas, mono_9x18_bold_atlas,
mono_10x20_atlas,
};
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
@@ -22,12 +20,12 @@ use cagire::engine::{
build_stream, spawn_sequencer, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand,
ScopeBuffer, SequencerConfig, SequencerHandle, SpectrumBuffer,
};
use crossbeam_channel::Receiver;
use cagire::input::{handle_key, InputContext, InputResult};
use cagire::input_egui::convert_egui_events;
use cagire::settings::Settings;
use cagire::state::audio::RefreshRate;
use cagire::views;
use crossbeam_channel::Receiver;
#[derive(Parser)]
#[command(name = "cagire-desktop", about = "Cagire desktop application")]
@@ -214,7 +212,9 @@ impl CagireDesktop {
audio_sample_pos: Arc::clone(&audio_sample_pos),
sample_rate: Arc::clone(&sample_rate_shared),
lookahead_ms: Arc::clone(&lookahead_ms),
cc_memory: Some(Arc::clone(&app.midi.cc_memory)),
cc_access: Some(
Arc::new(app.midi.cc_memory.clone()) as Arc<dyn cagire::model::CcAccess>
),
mouse_x: Arc::clone(&mouse_x),
mouse_y: Arc::clone(&mouse_y),
mouse_down: Arc::clone(&mouse_down),
@@ -349,7 +349,11 @@ impl CagireDesktop {
self.app.metrics.active_voices =
self.metrics.active_voices.load(Ordering::Relaxed) as usize;
self.app.metrics.peak_voices = self.app.metrics.peak_voices.max(self.app.metrics.active_voices);
self.app.metrics.peak_voices = self
.app
.metrics
.peak_voices
.max(self.app.metrics.active_voices);
self.app.metrics.cpu_load = self.metrics.load.get_load();
self.app.metrics.schedule_depth =
self.metrics.schedule_depth.load(Ordering::Relaxed) as usize;
@@ -399,7 +403,11 @@ impl eframe::App for CagireDesktop {
self.mouse_x.store(nx.to_bits(), Ordering::Relaxed);
self.mouse_y.store(ny.to_bits(), Ordering::Relaxed);
}
let down = if i.pointer.primary_down() { 1.0_f32 } else { 0.0_f32 };
let down = if i.pointer.primary_down() {
1.0_f32
} else {
0.0_f32
};
self.mouse_down.store(down.to_bits(), Ordering::Relaxed);
});
@@ -427,22 +435,48 @@ impl eframe::App for CagireDesktop {
while let Ok(midi_cmd) = self.midi_rx.try_recv() {
match midi_cmd {
MidiCommand::NoteOn { device, channel, note, velocity } => {
MidiCommand::NoteOn {
device,
channel,
note,
velocity,
} => {
self.app.midi.send_note_on(device, channel, note, velocity);
}
MidiCommand::NoteOff { device, channel, note } => {
MidiCommand::NoteOff {
device,
channel,
note,
} => {
self.app.midi.send_note_off(device, channel, note);
}
MidiCommand::CC { device, channel, cc, value } => {
MidiCommand::CC {
device,
channel,
cc,
value,
} => {
self.app.midi.send_cc(device, channel, cc, value);
}
MidiCommand::PitchBend { device, channel, value } => {
MidiCommand::PitchBend {
device,
channel,
value,
} => {
self.app.midi.send_pitch_bend(device, channel, value);
}
MidiCommand::Pressure { device, channel, value } => {
MidiCommand::Pressure {
device,
channel,
value,
} => {
self.app.midi.send_pressure(device, channel, value);
}
MidiCommand::ProgramChange { device, channel, program } => {
MidiCommand::ProgramChange {
device,
channel,
program,
} => {
self.app.midi.send_program_change(device, channel, program);
}
MidiCommand::Clock { device } => self.app.midi.send_realtime(device, 0xF8),

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf;
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
use crate::state::{FlashKind, Modal, PatternField};
use crate::state::{ColorScheme, DeviceKind, FlashKind, Modal, PatternField, SettingKind};
#[allow(dead_code)]
pub enum AppCommand {
@@ -169,4 +169,68 @@ pub enum AppCommand {
PatternsEnter,
PatternsBack,
PatternsTogglePlay,
// UI state
ClearMinimap,
HideTitle,
ToggleEditorStack,
SetColorScheme(ColorScheme),
ToggleRuntimeHighlight,
ToggleCompletion,
AdjustFlashBrightness(f32),
// Live keys
ToggleLiveKeysFill,
// Panel
ClosePanel,
// Selection
SetSelectionAnchor(usize),
ClearSelectionAnchor,
// Audio settings (engine page)
AudioNextSection,
AudioPrevSection,
AudioOutputListUp,
AudioOutputListDown(usize),
AudioOutputPageUp,
AudioOutputPageDown(usize),
AudioInputListUp,
AudioInputListDown(usize),
AudioInputPageDown(usize),
AudioSettingNext,
AudioSettingPrev,
SetOutputDevice(String),
SetInputDevice(String),
SetDeviceKind(DeviceKind),
AdjustAudioSetting {
setting: SettingKind,
delta: i32,
},
AudioTriggerRestart,
RemoveLastSamplePath,
AudioRefreshDevices,
// Options page
OptionsNextFocus,
OptionsPrevFocus,
ToggleRefreshRate,
ToggleScope,
ToggleSpectrum,
// Metrics
ResetPeakVoices,
// MIDI connections
ConnectMidiOutput {
slot: usize,
port: usize,
},
DisconnectMidiOutput(usize),
ConnectMidiInput {
slot: usize,
port: usize,
},
DisconnectMidiInput(usize),
}

View File

@@ -10,7 +10,9 @@ use std::time::Duration;
use thread_priority::{set_current_thread_priority, ThreadPriority};
use super::LinkState;
use crate::model::{CcMemory, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables};
use crate::model::{
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
};
use crate::model::{LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
use crate::state::LiveKeyState;
@@ -55,16 +57,50 @@ pub enum AudioCommand {
#[derive(Clone, Debug)]
pub enum MidiCommand {
NoteOn { device: u8, channel: u8, note: u8, velocity: u8 },
NoteOff { device: u8, channel: u8, note: u8 },
CC { device: u8, channel: u8, cc: u8, value: u8 },
PitchBend { device: u8, channel: u8, value: u16 },
Pressure { device: u8, channel: u8, value: u8 },
ProgramChange { device: u8, channel: u8, program: u8 },
Clock { device: u8 },
Start { device: u8 },
Stop { device: u8 },
Continue { device: u8 },
NoteOn {
device: u8,
channel: u8,
note: u8,
velocity: u8,
},
NoteOff {
device: u8,
channel: u8,
note: u8,
},
CC {
device: u8,
channel: u8,
cc: u8,
value: u8,
},
PitchBend {
device: u8,
channel: u8,
value: u16,
},
Pressure {
device: u8,
channel: u8,
value: u8,
},
ProgramChange {
device: u8,
channel: u8,
program: u8,
},
Clock {
device: u8,
},
Start {
device: u8,
},
Stop {
device: u8,
},
Continue {
device: u8,
},
}
pub enum SeqCommand {
@@ -233,7 +269,7 @@ pub struct SequencerConfig {
pub audio_sample_pos: Arc<AtomicU64>,
pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
pub lookahead_ms: Arc<std::sync::atomic::AtomicU32>,
pub cc_memory: Option<CcMemory>,
pub cc_access: Option<Arc<dyn CcAccess>>,
#[cfg(feature = "desktop")]
pub mouse_x: Arc<AtomicU32>,
#[cfg(feature = "desktop")]
@@ -253,7 +289,11 @@ pub fn spawn_sequencer(
live_keys: Arc<LiveKeyState>,
nudge_us: Arc<AtomicI64>,
config: SequencerConfig,
) -> (SequencerHandle, Receiver<AudioCommand>, Receiver<MidiCommand>) {
) -> (
SequencerHandle,
Receiver<AudioCommand>,
Receiver<MidiCommand>,
) {
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
let (audio_tx, audio_rx) = bounded::<AudioCommand>(256);
let (midi_tx, midi_rx) = bounded::<MidiCommand>(256);
@@ -291,7 +331,7 @@ pub fn spawn_sequencer(
config.audio_sample_pos,
config.sample_rate,
config.lookahead_ms,
config.cc_memory,
config.cc_access,
#[cfg(feature = "desktop")]
mouse_x,
#[cfg(feature = "desktop")]
@@ -510,12 +550,17 @@ pub(crate) struct SequencerState {
speed_overrides: HashMap<(usize, usize), f64>,
key_cache: KeyCache,
buf_audio_commands: Vec<TimestampedCommand>,
cc_memory: Option<CcMemory>,
cc_access: Option<Arc<dyn CcAccess>>,
active_notes: HashMap<(u8, u8, u8), ActiveNote>,
}
impl SequencerState {
pub fn new(variables: Variables, dict: Dictionary, rng: Rng, cc_memory: Option<CcMemory>) -> Self {
pub fn new(
variables: Variables,
dict: Dictionary,
rng: Rng,
cc_access: Option<Arc<dyn CcAccess>>,
) -> Self {
let script_engine = ScriptEngine::new(Arc::clone(&variables), dict, rng);
Self {
audio_state: AudioState::new(),
@@ -529,7 +574,7 @@ impl SequencerState {
speed_overrides: HashMap::new(),
key_cache: KeyCache::new(),
buf_audio_commands: Vec::new(),
cc_memory,
cc_access,
active_notes: HashMap::new(),
}
}
@@ -781,7 +826,7 @@ impl SequencerState {
speed: speed_mult,
fill,
nudge_secs,
cc_memory: self.cc_memory.clone(),
cc_access: self.cc_access.clone(),
#[cfg(feature = "desktop")]
mouse_x,
#[cfg(feature = "desktop")]
@@ -943,7 +988,7 @@ fn sequencer_loop(
audio_sample_pos: Arc<AtomicU64>,
sample_rate: Arc<std::sync::atomic::AtomicU32>,
lookahead_ms: Arc<std::sync::atomic::AtomicU32>,
cc_memory: Option<CcMemory>,
cc_access: Option<Arc<dyn CcAccess>>,
#[cfg(feature = "desktop")] mouse_x: Arc<AtomicU32>,
#[cfg(feature = "desktop")] mouse_y: Arc<AtomicU32>,
#[cfg(feature = "desktop")] mouse_down: Arc<AtomicU32>,
@@ -952,7 +997,7 @@ fn sequencer_loop(
let _ = set_current_thread_priority(ThreadPriority::Max);
let mut seq_state = SequencerState::new(variables, dict, rng, cc_memory);
let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);
loop {
let mut commands = Vec::new();
@@ -1002,8 +1047,15 @@ fn sequencer_loop(
if let Some((midi_cmd, dur)) = parse_midi_command(&tsc.cmd) {
match midi_tx.load().try_send(midi_cmd.clone()) {
Ok(()) => {
if let (MidiCommand::NoteOn { device, channel, note, .. }, Some(dur_secs)) =
(&midi_cmd, dur)
if let (
MidiCommand::NoteOn {
device,
channel,
note,
..
},
Some(dur_secs),
) = (&midi_cmd, dur)
{
let dur_us = (dur_secs * 1_000_000.0) as i64;
seq_state.active_notes.insert(
@@ -1037,23 +1089,36 @@ fn sequencer_loop(
if output.flush_midi_notes {
for ((device, channel, note), _) in seq_state.active_notes.drain() {
let _ = midi_tx.load().try_send(MidiCommand::NoteOff { device, channel, note });
let _ = midi_tx.load().try_send(MidiCommand::NoteOff {
device,
channel,
note,
});
}
// Send MIDI panic (CC 123 = All Notes Off) on all 16 channels for all devices
for dev in 0..4u8 {
for chan in 0..16u8 {
let _ = midi_tx
.load()
.try_send(MidiCommand::CC { device: dev, channel: chan, cc: 123, value: 0 });
let _ = midi_tx.load().try_send(MidiCommand::CC {
device: dev,
channel: chan,
cc: 123,
value: 0,
});
}
}
} else {
seq_state.active_notes.retain(|&(device, channel, note), active| {
seq_state
.active_notes
.retain(|&(device, channel, note), active| {
let should_release = current_time_us >= active.off_time_us;
let timed_out = (current_time_us - active.start_time_us) > MAX_NOTE_DURATION_US;
if should_release || timed_out {
let _ = midi_tx.load().try_send(MidiCommand::NoteOff { device, channel, note });
let _ = midi_tx.load().try_send(MidiCommand::NoteOff {
device,
channel,
note,
});
false
} else {
true
@@ -1081,7 +1146,8 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
}
let find_param = |key: &str| -> Option<&str> {
parts.iter()
parts
.iter()
.position(|&s| s == key)
.and_then(|i| parts.get(i + 1).copied())
};
@@ -1124,19 +1190,40 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
// /midi/bend/<value>/chan/<chan>/dev/<dev>
let value: u16 = parts.get(2)?.parse().ok()?;
let chan: u8 = find_param("chan")?.parse().ok()?;
Some((MidiCommand::PitchBend { device, channel: chan, value }, None))
Some((
MidiCommand::PitchBend {
device,
channel: chan,
value,
},
None,
))
}
"pressure" => {
// /midi/pressure/<value>/chan/<chan>/dev/<dev>
let value: u8 = parts.get(2)?.parse().ok()?;
let chan: u8 = find_param("chan")?.parse().ok()?;
Some((MidiCommand::Pressure { device, channel: chan, value }, None))
Some((
MidiCommand::Pressure {
device,
channel: chan,
value,
},
None,
))
}
"program" => {
// /midi/program/<value>/chan/<chan>/dev/<dev>
let program: u8 = parts.get(2)?.parse().ok()?;
let chan: u8 = find_param("chan")?.parse().ok()?;
Some((MidiCommand::ProgramChange { device, channel: chan, program }, None))
Some((
MidiCommand::ProgramChange {
device,
channel: chan,
program,
},
None,
))
}
"clock" => Some((MidiCommand::Clock { device }, None)),
"start" => Some((MidiCommand::Start { device }, None)),

View File

@@ -52,11 +52,11 @@ pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down
);
if ctx.app.ui.minimap_until.is_some() && !(ctrl && is_arrow) {
ctx.app.ui.minimap_until = None;
ctx.dispatch(AppCommand::ClearMinimap);
}
if ctx.app.ui.show_title {
ctx.app.ui.show_title = false;
ctx.dispatch(AppCommand::HideTitle);
return InputResult::Continue;
}
@@ -73,7 +73,7 @@ fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool {
match (key.code, key.kind) {
_ if !matches!(ctx.app.ui.modal, Modal::None) => false,
(KeyCode::Char('f'), KeyEventKind::Press) => {
ctx.app.live_keys.flip_fill();
ctx.dispatch(AppCommand::ToggleLiveKeysFill);
true
}
_ => false,
@@ -506,13 +506,23 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
editor.search_prev();
}
KeyCode::Char('s') if ctrl => {
ctx.app.editor_ctx.show_stack = !ctx.app.editor_ctx.show_stack;
ctx.dispatch(AppCommand::ToggleEditorStack);
}
KeyCode::Char('r') if ctrl => {
let script = ctx.app.editor_ctx.editor.lines().join("\n");
match ctx.app.execute_script_oneshot(&script, ctx.link, ctx.audio_tx) {
Ok(()) => ctx.app.ui.flash("Executed", 100, crate::state::FlashKind::Info),
Err(e) => ctx.app.ui.flash(&format!("Error: {e}"), 200, crate::state::FlashKind::Error),
match ctx
.app
.execute_script_oneshot(&script, ctx.link, ctx.audio_tx)
{
Ok(()) => ctx
.app
.ui
.flash("Executed", 100, crate::state::FlashKind::Info),
Err(e) => ctx.app.ui.flash(
&format!("Error: {e}"),
200,
crate::state::FlashKind::Error,
),
}
}
KeyCode::Char('a') if ctrl => {
@@ -745,8 +755,7 @@ fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
KeyCode::Left => state.collapse_at_cursor(),
KeyCode::Char('/') => state.activate_search(),
KeyCode::Esc | KeyCode::Tab => {
ctx.app.panel.visible = false;
ctx.app.panel.focus = PanelFocus::Main;
ctx.dispatch(AppCommand::ClosePanel);
}
_ => {}
}
@@ -783,25 +792,25 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
}
KeyCode::Left if shift && !ctrl => {
if ctx.app.editor_ctx.selection_anchor.is_none() {
ctx.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
}
ctx.dispatch(AppCommand::PrevStep);
}
KeyCode::Right if shift && !ctrl => {
if ctx.app.editor_ctx.selection_anchor.is_none() {
ctx.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
}
ctx.dispatch(AppCommand::NextStep);
}
KeyCode::Up if shift && !ctrl => {
if ctx.app.editor_ctx.selection_anchor.is_none() {
ctx.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
}
ctx.dispatch(AppCommand::StepUp);
}
KeyCode::Down if shift && !ctrl => {
if ctx.app.editor_ctx.selection_anchor.is_none() {
ctx.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
}
ctx.dispatch(AppCommand::StepDown);
}
@@ -910,9 +919,19 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
let pattern = ctx.app.current_edit_pattern();
if let Some(script) = pattern.resolve_script(ctx.app.editor_ctx.step) {
if !script.trim().is_empty() {
match ctx.app.execute_script_oneshot(script, ctx.link, ctx.audio_tx) {
Ok(()) => ctx.app.ui.flash("Executed", 100, crate::state::FlashKind::Info),
Err(e) => ctx.app.ui.flash(&format!("Error: {e}"), 200, crate::state::FlashKind::Error),
match ctx
.app
.execute_script_oneshot(script, ctx.link, ctx.audio_tx)
{
Ok(()) => ctx
.app
.ui
.flash("Executed", 100, crate::state::FlashKind::Info),
Err(e) => ctx.app.ui.flash(
&format!("Error: {e}"),
200,
crate::state::FlashKind::Error,
),
}
}
}
@@ -923,7 +942,9 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
ctx.app.editor_ctx.pattern,
ctx.app.editor_ctx.step,
);
let current_name = ctx.app.current_edit_pattern()
let current_name = ctx
.app
.current_edit_pattern()
.step(step)
.and_then(|s| s.name.clone())
.unwrap_or_default();
@@ -1063,15 +1084,15 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
selected: false,
}));
}
KeyCode::Tab => ctx.app.audio.next_section(),
KeyCode::BackTab => ctx.app.audio.prev_section(),
KeyCode::Tab => ctx.dispatch(AppCommand::AudioNextSection),
KeyCode::BackTab => ctx.dispatch(AppCommand::AudioPrevSection),
KeyCode::Up => match ctx.app.audio.section {
EngineSection::Devices => match ctx.app.audio.device_kind {
DeviceKind::Output => ctx.app.audio.output_list.move_up(),
DeviceKind::Input => ctx.app.audio.input_list.move_up(),
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputListUp),
DeviceKind::Input => ctx.dispatch(AppCommand::AudioInputListUp),
},
EngineSection::Settings => {
ctx.app.audio.setting_kind = ctx.app.audio.setting_kind.prev();
ctx.dispatch(AppCommand::AudioSettingPrev);
}
EngineSection::Samples => {}
},
@@ -1079,22 +1100,22 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
EngineSection::Devices => match ctx.app.audio.device_kind {
DeviceKind::Output => {
let count = ctx.app.audio.output_devices.len();
ctx.app.audio.output_list.move_down(count);
ctx.dispatch(AppCommand::AudioOutputListDown(count));
}
DeviceKind::Input => {
let count = ctx.app.audio.input_devices.len();
ctx.app.audio.input_list.move_down(count);
ctx.dispatch(AppCommand::AudioInputListDown(count));
}
},
EngineSection::Settings => {
ctx.app.audio.setting_kind = ctx.app.audio.setting_kind.next();
ctx.dispatch(AppCommand::AudioSettingNext);
}
EngineSection::Samples => {}
},
KeyCode::PageUp => {
if ctx.app.audio.section == EngineSection::Devices {
match ctx.app.audio.device_kind {
DeviceKind::Output => ctx.app.audio.output_list.page_up(),
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputPageUp),
DeviceKind::Input => ctx.app.audio.input_list.page_up(),
}
}
@@ -1104,11 +1125,11 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
match ctx.app.audio.device_kind {
DeviceKind::Output => {
let count = ctx.app.audio.output_devices.len();
ctx.app.audio.output_list.page_down(count);
ctx.dispatch(AppCommand::AudioOutputPageDown(count));
}
DeviceKind::Input => {
let count = ctx.app.audio.input_devices.len();
ctx.app.audio.input_list.page_down(count);
ctx.dispatch(AppCommand::AudioInputPageDown(count));
}
}
}
@@ -1119,16 +1140,16 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
DeviceKind::Output => {
let cursor = ctx.app.audio.output_list.cursor;
if cursor < ctx.app.audio.output_devices.len() {
ctx.app.audio.config.output_device =
Some(ctx.app.audio.output_devices[cursor].name.clone());
let name = ctx.app.audio.output_devices[cursor].name.clone();
ctx.dispatch(AppCommand::SetOutputDevice(name));
ctx.app.save_settings(ctx.link);
}
}
DeviceKind::Input => {
let cursor = ctx.app.audio.input_list.cursor;
if cursor < ctx.app.audio.input_devices.len() {
ctx.app.audio.config.input_device =
Some(ctx.app.audio.input_devices[cursor].name.clone());
let name = ctx.app.audio.input_devices[cursor].name.clone();
ctx.dispatch(AppCommand::SetInputDevice(name));
ctx.app.save_settings(ctx.link);
}
}
@@ -1137,20 +1158,32 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
KeyCode::Left => match ctx.app.audio.section {
EngineSection::Devices => {
ctx.app.audio.device_kind = DeviceKind::Output;
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Output));
}
EngineSection::Settings => {
match ctx.app.audio.setting_kind {
SettingKind::Channels => ctx.app.audio.adjust_channels(-1),
SettingKind::BufferSize => ctx.app.audio.adjust_buffer_size(-64),
SettingKind::Polyphony => ctx.app.audio.adjust_max_voices(-1),
SettingKind::Channels => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Channels,
delta: -1,
}),
SettingKind::BufferSize => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::BufferSize,
delta: -64,
}),
SettingKind::Polyphony => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Polyphony,
delta: -1,
}),
SettingKind::Nudge => {
let prev = ctx.nudge_us.load(Ordering::Relaxed);
ctx.nudge_us
.store((prev - 1000).max(-100_000), Ordering::Relaxed);
}
SettingKind::Lookahead => {
ctx.app.audio.adjust_lookahead(-1);
ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Lookahead,
delta: -1,
});
ctx.lookahead_ms
.store(ctx.app.audio.config.lookahead_ms, Ordering::Relaxed);
}
@@ -1161,20 +1194,32 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
},
KeyCode::Right => match ctx.app.audio.section {
EngineSection::Devices => {
ctx.app.audio.device_kind = DeviceKind::Input;
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Input));
}
EngineSection::Settings => {
match ctx.app.audio.setting_kind {
SettingKind::Channels => ctx.app.audio.adjust_channels(1),
SettingKind::BufferSize => ctx.app.audio.adjust_buffer_size(64),
SettingKind::Polyphony => ctx.app.audio.adjust_max_voices(1),
SettingKind::Channels => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Channels,
delta: 1,
}),
SettingKind::BufferSize => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::BufferSize,
delta: 64,
}),
SettingKind::Polyphony => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Polyphony,
delta: 1,
}),
SettingKind::Nudge => {
let prev = ctx.nudge_us.load(Ordering::Relaxed);
ctx.nudge_us
.store((prev + 1000).min(100_000), Ordering::Relaxed);
}
SettingKind::Lookahead => {
ctx.app.audio.adjust_lookahead(1);
ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Lookahead,
delta: 1,
});
ctx.lookahead_ms
.store(ctx.app.audio.config.lookahead_ms, Ordering::Relaxed);
}
@@ -1183,7 +1228,7 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
EngineSection::Samples => {}
},
KeyCode::Char('R') => ctx.app.audio.trigger_restart(),
KeyCode::Char('R') => ctx.dispatch(AppCommand::AudioTriggerRestart),
KeyCode::Char('A') => {
use crate::state::file_browser::FileBrowserState;
let state = FileBrowserState::new_load(String::new());
@@ -1191,9 +1236,9 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
KeyCode::Char('D') => {
if ctx.app.audio.section == EngineSection::Samples {
ctx.app.audio.remove_last_sample_path();
ctx.dispatch(AppCommand::RemoveLastSamplePath);
} else {
ctx.app.audio.refresh_devices();
ctx.dispatch(AppCommand::AudioRefreshDevices);
let out_count = ctx.app.audio.output_devices.len();
let in_count = ctx.app.audio.input_devices.len();
ctx.dispatch(AppCommand::SetStatus(format!(
@@ -1209,7 +1254,7 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let _ = ctx.audio_tx.load().send(AudioCommand::Panic);
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
}
KeyCode::Char('r') => ctx.app.metrics.peak_voices = 0,
KeyCode::Char('r') => ctx.dispatch(AppCommand::ResetPeakVoices),
KeyCode::Char('t') => {
let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate {
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
@@ -1231,8 +1276,8 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
selected: false,
}));
}
KeyCode::Down | KeyCode::Tab => ctx.app.options.next_focus(),
KeyCode::Up | KeyCode::BackTab => ctx.app.options.prev_focus(),
KeyCode::Down | KeyCode::Tab => ctx.dispatch(AppCommand::OptionsNextFocus),
KeyCode::Up | KeyCode::BackTab => ctx.dispatch(AppCommand::OptionsPrevFocus),
KeyCode::Left | KeyCode::Right => {
match ctx.app.options.focus {
OptionsFocus::ColorScheme => {
@@ -1241,26 +1286,24 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
} else {
ctx.app.ui.color_scheme.next()
};
ctx.app.ui.color_scheme = new_scheme;
crate::theme::set(new_scheme.to_theme());
ctx.dispatch(AppCommand::SetColorScheme(new_scheme));
}
OptionsFocus::RefreshRate => ctx.app.audio.toggle_refresh_rate(),
OptionsFocus::RefreshRate => ctx.dispatch(AppCommand::ToggleRefreshRate),
OptionsFocus::RuntimeHighlight => {
ctx.app.ui.runtime_highlight = !ctx.app.ui.runtime_highlight
ctx.dispatch(AppCommand::ToggleRuntimeHighlight);
}
OptionsFocus::ShowScope => {
ctx.app.audio.config.show_scope = !ctx.app.audio.config.show_scope
ctx.dispatch(AppCommand::ToggleScope);
}
OptionsFocus::ShowSpectrum => {
ctx.app.audio.config.show_spectrum = !ctx.app.audio.config.show_spectrum
ctx.dispatch(AppCommand::ToggleSpectrum);
}
OptionsFocus::ShowCompletion => {
ctx.app.ui.show_completion = !ctx.app.ui.show_completion
ctx.dispatch(AppCommand::ToggleCompletion);
}
OptionsFocus::FlashBrightness => {
let delta = if key.code == KeyCode::Left { -0.1 } else { 0.1 };
ctx.app.ui.flash_brightness =
(ctx.app.ui.flash_brightness + delta).clamp(0.0, 1.0);
ctx.dispatch(AppCommand::AdjustFlashBrightness(delta));
}
OptionsFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
OptionsFocus::StartStopSync => ctx
@@ -1270,8 +1313,10 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let delta = if key.code == KeyCode::Left { -1.0 } else { 1.0 };
ctx.link.set_quantum(ctx.link.quantum() + delta);
}
OptionsFocus::MidiOutput0 | OptionsFocus::MidiOutput1 |
OptionsFocus::MidiOutput2 | OptionsFocus::MidiOutput3 => {
OptionsFocus::MidiOutput0
| OptionsFocus::MidiOutput1
| OptionsFocus::MidiOutput2
| OptionsFocus::MidiOutput3 => {
let slot = match ctx.app.options.focus {
OptionsFocus::MidiOutput0 => 0,
OptionsFocus::MidiOutput1 => 1,
@@ -1285,7 +1330,12 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
.enumerate()
.filter(|(idx, _)| {
ctx.app.midi.selected_outputs[slot] == Some(*idx)
|| !ctx.app.midi.selected_outputs.iter().enumerate()
|| !ctx
.app
.midi
.selected_outputs
.iter()
.enumerate()
.any(|(s, sel)| s != slot && *sel == Some(*idx))
})
.collect();
@@ -1295,7 +1345,11 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
.map(|p| p + 1)
.unwrap_or(0);
let new_pos = if key.code == KeyCode::Left {
if current_pos == 0 { total_options - 1 } else { current_pos - 1 }
if current_pos == 0 {
total_options - 1
} else {
current_pos - 1
}
} else {
(current_pos + 1) % total_options
};
@@ -1308,13 +1362,16 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let (device_idx, device) = available[new_pos - 1];
if ctx.app.midi.connect_output(slot, device_idx).is_ok() {
ctx.dispatch(AppCommand::SetStatus(format!(
"MIDI output {}: {}", slot, device.name
"MIDI output {}: {}",
slot, device.name
)));
}
}
}
OptionsFocus::MidiInput0 | OptionsFocus::MidiInput1 |
OptionsFocus::MidiInput2 | OptionsFocus::MidiInput3 => {
OptionsFocus::MidiInput0
| OptionsFocus::MidiInput1
| OptionsFocus::MidiInput2
| OptionsFocus::MidiInput3 => {
let slot = match ctx.app.options.focus {
OptionsFocus::MidiInput0 => 0,
OptionsFocus::MidiInput1 => 1,
@@ -1328,7 +1385,12 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
.enumerate()
.filter(|(idx, _)| {
ctx.app.midi.selected_inputs[slot] == Some(*idx)
|| !ctx.app.midi.selected_inputs.iter().enumerate()
|| !ctx
.app
.midi
.selected_inputs
.iter()
.enumerate()
.any(|(s, sel)| s != slot && *sel == Some(*idx))
})
.collect();
@@ -1338,7 +1400,11 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
.map(|p| p + 1)
.unwrap_or(0);
let new_pos = if key.code == KeyCode::Left {
if current_pos == 0 { total_options - 1 } else { current_pos - 1 }
if current_pos == 0 {
total_options - 1
} else {
current_pos - 1
}
} else {
(current_pos + 1) % total_options
};
@@ -1351,7 +1417,8 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let (device_idx, device) = available[new_pos - 1];
if ctx.app.midi.connect_input(slot, device_idx).is_ok() {
ctx.dispatch(AppCommand::SetStatus(format!(
"MIDI input {}: {}", slot, device.name
"MIDI input {}: {}",
slot, device.name
)));
}
}

View File

@@ -146,7 +146,7 @@ fn main() -> io::Result<()> {
audio_sample_pos: Arc::clone(&audio_sample_pos),
sample_rate: Arc::clone(&sample_rate_shared),
lookahead_ms: Arc::clone(&lookahead_ms),
cc_memory: Some(Arc::clone(&app.midi.cc_memory)),
cc_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn crate::model::CcAccess>),
#[cfg(feature = "desktop")]
mouse_x: Arc::clone(&mouse_x),
#[cfg(feature = "desktop")]
@@ -257,22 +257,48 @@ fn main() -> io::Result<()> {
// Process pending MIDI commands
while let Ok(midi_cmd) = midi_rx.try_recv() {
match midi_cmd {
engine::MidiCommand::NoteOn { device, channel, note, velocity } => {
engine::MidiCommand::NoteOn {
device,
channel,
note,
velocity,
} => {
app.midi.send_note_on(device, channel, note, velocity);
}
engine::MidiCommand::NoteOff { device, channel, note } => {
engine::MidiCommand::NoteOff {
device,
channel,
note,
} => {
app.midi.send_note_off(device, channel, note);
}
engine::MidiCommand::CC { device, channel, cc, value } => {
engine::MidiCommand::CC {
device,
channel,
cc,
value,
} => {
app.midi.send_cc(device, channel, cc, value);
}
engine::MidiCommand::PitchBend { device, channel, value } => {
engine::MidiCommand::PitchBend {
device,
channel,
value,
} => {
app.midi.send_pitch_bend(device, channel, value);
}
engine::MidiCommand::Pressure { device, channel, value } => {
engine::MidiCommand::Pressure {
device,
channel,
value,
} => {
app.midi.send_pressure(device, channel, value);
}
engine::MidiCommand::ProgramChange { device, channel, program } => {
engine::MidiCommand::ProgramChange {
device,
channel,
program,
} => {
app.midi.send_program_change(device, channel, program);
}
engine::MidiCommand::Clock { device } => app.midi.send_realtime(device, 0xF8),

View File

@@ -2,10 +2,53 @@ use std::sync::{Arc, Mutex};
use midir::{MidiInput, MidiOutput};
use crate::model::CcMemory;
use cagire_forth::CcAccess;
pub const MAX_MIDI_OUTPUTS: usize = 4;
pub const MAX_MIDI_INPUTS: usize = 4;
pub const MAX_MIDI_DEVICES: usize = 4;
/// Raw CC memory storage type
type CcMemoryInner = Arc<Mutex<[[[u8; 128]; 16]; MAX_MIDI_DEVICES]>>;
/// CC memory storage: [device][channel][cc_number] -> value
/// Wrapped in a newtype to implement CcAccess (orphan rule)
#[derive(Clone)]
pub struct CcMemory(CcMemoryInner);
impl CcMemory {
pub fn new() -> Self {
Self(Arc::new(Mutex::new([[[0u8; 128]; 16]; MAX_MIDI_DEVICES])))
}
fn inner(&self) -> &CcMemoryInner {
&self.0
}
/// Set a CC value (for testing)
#[allow(dead_code)]
pub fn set_cc(&self, device: usize, channel: usize, cc: usize, value: u8) {
if let Ok(mut mem) = self.0.lock() {
mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)] = value;
}
}
}
impl Default for CcMemory {
fn default() -> Self {
Self::new()
}
}
impl CcAccess for CcMemory {
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8 {
self.0
.lock()
.ok()
.map(|mem| mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)])
.unwrap_or(0)
}
}
#[derive(Clone, Debug)]
pub struct MidiDeviceInfo {
@@ -46,7 +89,7 @@ pub fn list_midi_inputs() -> Vec<MidiDeviceInfo> {
pub struct MidiState {
output_conns: [Option<midir::MidiOutputConnection>; MAX_MIDI_OUTPUTS],
input_conns: [Option<midir::MidiInputConnection<(CcMemory, usize)>>; MAX_MIDI_INPUTS],
input_conns: [Option<midir::MidiInputConnection<(CcMemoryInner, usize)>>; MAX_MIDI_INPUTS],
pub selected_outputs: [Option<usize>; MAX_MIDI_OUTPUTS],
pub selected_inputs: [Option<usize>; MAX_MIDI_INPUTS],
pub cc_memory: CcMemory,
@@ -65,7 +108,7 @@ impl MidiState {
input_conns: [None, None, None, None],
selected_outputs: [None; MAX_MIDI_OUTPUTS],
selected_inputs: [None; MAX_MIDI_INPUTS],
cc_memory: Arc::new(Mutex::new([[[0u8; 128]; 16]; MAX_MIDI_OUTPUTS])),
cc_memory: CcMemory::new(),
}
}
@@ -99,7 +142,7 @@ impl MidiState {
let ports = midi_in.ports();
let port = ports.get(port_index).ok_or("MIDI input port not found")?;
let cc_mem = Arc::clone(&self.cc_memory);
let cc_mem = Arc::clone(self.cc_memory.inner());
let conn = midi_in
.connect(
port,
@@ -133,60 +176,46 @@ impl MidiState {
}
}
pub fn send_note_on(&mut self, device: u8, channel: u8, note: u8, velocity: u8) {
fn send_message(&mut self, device: u8, message: &[u8]) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0x90 | (channel & 0x0F);
let _ = conn.send(&[status, note & 0x7F, velocity & 0x7F]);
let _ = conn.send(message);
}
}
pub fn send_note_on(&mut self, device: u8, channel: u8, note: u8, velocity: u8) {
let status = 0x90 | (channel & 0x0F);
self.send_message(device, &[status, note & 0x7F, velocity & 0x7F]);
}
pub fn send_note_off(&mut self, device: u8, channel: u8, note: u8) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0x80 | (channel & 0x0F);
let _ = conn.send(&[status, note & 0x7F, 0]);
}
self.send_message(device, &[status, note & 0x7F, 0]);
}
pub fn send_cc(&mut self, device: u8, channel: u8, cc: u8, value: u8) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0xB0 | (channel & 0x0F);
let _ = conn.send(&[status, cc & 0x7F, value & 0x7F]);
}
self.send_message(device, &[status, cc & 0x7F, value & 0x7F]);
}
pub fn send_pitch_bend(&mut self, device: u8, channel: u8, value: u16) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0xE0 | (channel & 0x0F);
let lsb = (value & 0x7F) as u8;
let msb = ((value >> 7) & 0x7F) as u8;
let _ = conn.send(&[status, lsb, msb]);
}
self.send_message(device, &[status, lsb, msb]);
}
pub fn send_pressure(&mut self, device: u8, channel: u8, value: u8) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0xD0 | (channel & 0x0F);
let _ = conn.send(&[status, value & 0x7F]);
}
self.send_message(device, &[status, value & 0x7F]);
}
pub fn send_program_change(&mut self, device: u8, channel: u8, program: u8) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let status = 0xC0 | (channel & 0x0F);
let _ = conn.send(&[status, program & 0x7F]);
}
self.send_message(device, &[status, program & 0x7F]);
}
pub fn send_realtime(&mut self, device: u8, msg: u8) {
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
if let Some(conn) = &mut self.output_conns[slot] {
let _ = conn.send(&[msg]);
}
self.send_message(device, &[msg]);
}
}

View File

@@ -5,4 +5,7 @@ pub use cagire_project::{
load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS,
MAX_PATTERNS,
};
pub use script::{CcMemory, Dictionary, ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Value, Variables};
pub use script::{
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Value,
Variables,
};

View File

@@ -1,6 +1,8 @@
use cagire_forth::Forth;
pub use cagire_forth::{CcMemory, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables};
pub use cagire_forth::{
CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables,
};
pub struct ScriptEngine {
forth: Forth,

View File

@@ -1,65 +1,47 @@
use serde::{Deserialize, Serialize};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use crate::theme::ThemeColors;
use crate::theme::{ThemeColors, THEMES};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ColorScheme {
#[default]
CatppuccinMocha,
CatppuccinLatte,
Nord,
Dracula,
GruvboxDark,
Monokai,
PitchBlack,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ColorScheme(usize);
impl ColorScheme {
pub fn label(self) -> &'static str {
match self {
Self::CatppuccinMocha => "Catppuccin Mocha",
Self::CatppuccinLatte => "Catppuccin Latte",
Self::Nord => "Nord",
Self::Dracula => "Dracula",
Self::GruvboxDark => "Gruvbox Dark",
Self::Monokai => "Monokai",
Self::PitchBlack => "Pitch Black",
}
THEMES[self.0].label
}
pub fn next(self) -> Self {
match self {
Self::CatppuccinMocha => Self::CatppuccinLatte,
Self::CatppuccinLatte => Self::Nord,
Self::Nord => Self::Dracula,
Self::Dracula => Self::GruvboxDark,
Self::GruvboxDark => Self::Monokai,
Self::Monokai => Self::PitchBlack,
Self::PitchBlack => Self::CatppuccinMocha,
}
Self((self.0 + 1) % THEMES.len())
}
pub fn prev(self) -> Self {
match self {
Self::CatppuccinMocha => Self::PitchBlack,
Self::CatppuccinLatte => Self::CatppuccinMocha,
Self::Nord => Self::CatppuccinLatte,
Self::Dracula => Self::Nord,
Self::GruvboxDark => Self::Dracula,
Self::Monokai => Self::GruvboxDark,
Self::PitchBlack => Self::Monokai,
}
Self((self.0 + THEMES.len() - 1) % THEMES.len())
}
pub fn to_theme(self) -> ThemeColors {
match self {
Self::CatppuccinMocha => ThemeColors::catppuccin_mocha(),
Self::CatppuccinLatte => ThemeColors::catppuccin_latte(),
Self::Nord => ThemeColors::nord(),
Self::Dracula => ThemeColors::dracula(),
Self::GruvboxDark => ThemeColors::gruvbox_dark(),
Self::Monokai => ThemeColors::monokai(),
Self::PitchBlack => ThemeColors::pitch_black(),
}
(THEMES[self.0].colors)()
}
}
impl Serialize for ColorScheme {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(THEMES[self.0].id)
}
}
impl<'de> Deserialize<'de> for ColorScheme {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
THEMES
.iter()
.position(|t| t.id == s)
.map(ColorScheme)
.ok_or_else(|| de::Error::custom(format!("unknown theme: {s}")))
}
}

View File

@@ -12,7 +12,6 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table};
use ratatui::Frame;
use cagire_forth::Forth;
use crate::app::App;
use crate::engine::{LinkState, SequencerSnapshot};
use crate::model::{SourceSpan, StepContext, Value};
@@ -23,12 +22,17 @@ use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime}
use crate::widgets::{
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
};
use cagire_forth::Forth;
use super::{
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
};
fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor, cache: &std::cell::RefCell<Option<StackCache>>) -> String {
fn compute_stack_display(
lines: &[String],
editor: &cagire_ratatui::Editor,
cache: &std::cell::RefCell<Option<StackCache>>,
) -> String {
let cursor_line = editor.cursor().0;
let mut hasher = DefaultHasher::new();
@@ -46,7 +50,11 @@ fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor, cach
}
}
let partial: Vec<&str> = lines.iter().take(cursor_line + 1).map(|s| s.as_str()).collect();
let partial: Vec<&str> = lines
.iter()
.take(cursor_line + 1)
.map(|s| s.as_str())
.collect();
let script = partial.join("\n");
let result = if script.trim().is_empty() {
@@ -70,7 +78,7 @@ fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor, cach
speed: 1.0,
fill: false,
nudge_secs: 0.0,
cc_memory: None,
cc_access: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
@@ -240,7 +248,11 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc
}
fn header_height(width: u16) -> u16 {
if width >= 80 { 1 } else { 2 }
if width >= 80 {
1
} else {
2
}
}
fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
@@ -284,11 +296,8 @@ fn render_header(
.areas(area);
(t, l, tp, b, p, s)
} else {
let [line1, line2] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
])
.areas(area);
let [line1, line2] =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
let [t, l, tp, s] = Layout::horizontal([
Constraint::Min(12),
@@ -298,11 +307,8 @@ fn render_header(
])
.areas(line1);
let [b, p] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Fill(2),
])
.areas(line2);
let [b, p] =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(2)]).areas(line2);
(t, l, tp, b, p, s)
};
@@ -323,7 +329,11 @@ fn render_header(
// Fill indicator
let fill = app.live_keys.fill();
let fill_fg = if fill { theme.status.fill_on } else { theme.status.fill_off };
let fill_fg = if fill {
theme.status.fill_on
} else {
theme.status.fill_off
};
let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg);
frame.render_widget(
Paragraph::new(if fill { "F" } else { "·" })
@@ -350,7 +360,9 @@ fn render_header(
.as_deref()
.map(|n| format!(" {n} "))
.unwrap_or_else(|| format!(" Bank {:02} ", app.editor_ctx.bank + 1));
let bank_style = Style::new().bg(theme.header.bank_bg).fg(theme.ui.text_primary);
let bank_style = Style::new()
.bg(theme.header.bank_bg)
.fg(theme.ui.text_primary);
frame.render_widget(
Paragraph::new(bank_name)
.style(bank_style)
@@ -381,7 +393,9 @@ fn render_header(
" {} · {} steps{}{}{} ",
pattern_name, pattern.length, speed_info, page_info, iter_info
);
let pattern_style = Style::new().bg(theme.header.pattern_bg).fg(theme.ui.text_primary);
let pattern_style = Style::new()
.bg(theme.header.pattern_bg)
.fg(theme.ui.text_primary);
frame.render_widget(
Paragraph::new(pattern_text)
.style(pattern_style)
@@ -394,7 +408,9 @@ fn render_header(
let peers = link.peers();
let voices = app.metrics.active_voices;
let stats_text = format!(" CPU {cpu_pct:.0}% V:{voices} L:{peers} ");
let stats_style = Style::new().bg(theme.header.stats_bg).fg(theme.header.stats_fg);
let stats_style = Style::new()
.bg(theme.header.stats_bg)
.fg(theme.header.stats_fg);
frame.render_widget(
Paragraph::new(stats_text)
.style(stats_style)
@@ -528,11 +544,12 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
ConfirmModal::new("Confirm", &format!("Delete step {}?", step + 1), *selected)
.render_centered(frame, term);
}
Modal::ConfirmDeleteSteps { steps, selected, .. } => {
Modal::ConfirmDeleteSteps {
steps, selected, ..
} => {
let nums: Vec<String> = steps.iter().map(|s| format!("{:02}", s + 1)).collect();
let label = format!("Delete steps {}?", nums.join(", "));
ConfirmModal::new("Confirm", &label, *selected)
.render_centered(frame, term);
ConfirmModal::new("Confirm", &label, *selected).render_centered(frame, term);
}
Modal::ConfirmResetPattern {
pattern, selected, ..
@@ -637,7 +654,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let step_name = step.and_then(|s| s.name.as_ref());
let title = match (source_idx, step_name) {
(Some(src), Some(name)) => format!("Step {:02}: {}{:02}", step_idx + 1, name, src + 1),
(Some(src), Some(name)) => {
format!("Step {:02}: {}{:02}", step_idx + 1, name, src + 1)
}
(None, Some(name)) => format!("Step {:02}: {}", step_idx + 1, name),
(Some(src), None) => format!("Step {:02}{:02}", step_idx + 1, src + 1),
(None, None) => format!("Step {:02}", step_idx + 1),
@@ -816,7 +835,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
]);
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
} else if app.editor_ctx.show_stack {
let stack_text = compute_stack_display(text_lines, &app.editor_ctx.editor, &app.editor_ctx.stack_cache);
let stack_text = compute_stack_display(
text_lines,
&app.editor_ctx.editor,
&app.editor_ctx.stack_cache,
);
let hint = Line::from(vec![
Span::styled("Esc", key),
Span::styled(" save ", dim),
@@ -910,7 +933,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
Style::default()
.fg(theme.hint.key)
.add_modifier(Modifier::BOLD),
Style::default().fg(theme.ui.text_primary).bg(theme.ui.surface),
Style::default()
.fg(theme.ui.text_primary)
.bg(theme.ui.surface),
)
} else {
(
@@ -962,7 +987,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
.skip(*scroll)
.take(visible_rows)
.map(|(i, (key, name, desc))| {
let bg = if i % 2 == 0 { theme.table.row_even } else { theme.table.row_odd };
let bg = if i % 2 == 0 {
theme.table.row_even
} else {
theme.table.row_odd
};
Row::new(vec![
Cell::from(*key).style(Style::default().fg(theme.modal.confirm)),
Cell::from(*name).style(Style::default().fg(theme.modal.input)),
@@ -1004,7 +1033,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
Span::styled("Esc/?", Style::default().fg(theme.hint.key)),
Span::styled(" close", Style::default().fg(theme.hint.text)),
]);
frame.render_widget(Paragraph::new(keybind_hint).alignment(Alignment::Right), hint_area);
frame.render_widget(
Paragraph::new(keybind_hint).alignment(Alignment::Right),
hint_area,
);
}
}
}

View File

@@ -18,7 +18,7 @@ pub fn default_ctx() -> StepContext {
speed: 1.0,
fill: false,
nudge_secs: 0.0,
cc_memory: None,
cc_access: None,
}
}

View File

@@ -1,6 +1,10 @@
use crate::harness::{default_ctx, expect_outputs, forth};
use cagire::forth::{CcMemory, StepContext};
use std::sync::{Arc, Mutex};
use cagire::forth::{CcAccess, StepContext};
use cagire::midi::CcMemory;
use std::sync::Arc;
#[allow(unused_imports)]
use cagire::forth::Value;
#[test]
fn test_midi_channel_set() {
@@ -42,16 +46,13 @@ fn test_ccval_returns_zero_without_cc_memory() {
#[test]
fn test_ccval_reads_from_cc_memory() {
let cc_memory: CcMemory = Arc::new(Mutex::new([[[0u8; 128]; 16]; 4]));
{
let mut mem = cc_memory.lock().unwrap();
mem[0][0][1] = 64; // device 0, channel 1 (0-indexed), CC 1, value 64
mem[0][5][74] = 127; // device 0, channel 6 (0-indexed), CC 74, value 127
}
let cc_memory = CcMemory::new();
cc_memory.set_cc(0, 0, 1, 64); // device 0, channel 1 (0-indexed), CC 1, value 64
cc_memory.set_cc(0, 5, 74, 127); // device 0, channel 6 (0-indexed), CC 74, value 127
let f = forth();
let ctx = StepContext {
cc_memory: Some(Arc::clone(&cc_memory)),
cc_access: Some(Arc::new(cc_memory.clone()) as Arc<dyn CcAccess>),
..default_ctx()
};
@@ -122,7 +123,10 @@ fn test_midi_full_defaults() {
fn test_midi_bend_center() {
let outputs = expect_outputs("0.0 bend m.", 1);
// 0.0 -> 8192 (center)
assert!(outputs[0].contains("/midi/bend/8191/chan/0") || outputs[0].contains("/midi/bend/8192/chan/0"));
assert!(
outputs[0].contains("/midi/bend/8191/chan/0")
|| outputs[0].contains("/midi/bend/8192/chan/0")
);
}
#[test]