diff --git a/Cargo.lock b/Cargo.lock index 976b53f..19c3b12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1824,8 +1824,7 @@ dependencies = [ [[package]] name = "doux" -version = "0.0.15" -source = "git+https://github.com/sova-org/doux?tag=v0.0.15#29d8f055612f6141d7546d72b91e60026937b0fd" +version = "0.0.18" dependencies = [ "arc-swap", "clap", diff --git a/Cargo.toml b/Cargo.toml index 2d7f1bb..75f51a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ cagire-forth = { path = "crates/forth" } cagire-markdown = { path = "crates/markdown" } cagire-project = { path = "crates/project" } cagire-ratatui = { path = "crates/ratatui" } -doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.15", features = ["native", "soundfont"] } +doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.18", features = ["native", "soundfont"] } rusty_link = "0.4" ratatui = "0.30" crossterm = "0.29" @@ -105,6 +105,9 @@ egui-baseview = { path = "plugins/egui-baseview" } [patch."https://github.com/RustAudio/baseview.git"] baseview = { path = "plugins/baseview" } +[patch.'https://github.com/sova-org/doux'] +doux = { path = "/Users/bubo/doux" } + [package.metadata.bundle.bin.cagire-desktop] name = "Cagire" identifier = "com.sova.cagire" diff --git a/plugins/cagire-plugins/Cargo.toml b/plugins/cagire-plugins/Cargo.toml index a3615e4..583d6c3 100644 --- a/plugins/cagire-plugins/Cargo.toml +++ b/plugins/cagire-plugins/Cargo.toml @@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer cagire-forth = { path = "../../crates/forth" } cagire-project = { path = "../../crates/project" } cagire-ratatui = { path = "../../crates/ratatui" } -doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.15", features = ["native", "soundfont"] } +doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.18", features = ["native", "soundfont"] } nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] } nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" } egui_ratatui = "2.1" diff --git a/src/app/dispatch.rs b/src/app/dispatch.rs index 1159bf3..067f1b7 100644 --- a/src/app/dispatch.rs +++ b/src/app/dispatch.rs @@ -375,7 +375,11 @@ impl App { AppCommand::AudioSettingPrev => self.audio.prev_setting(self.plugin_mode), 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::SetDevicesFocus(focus) => self.audio.devices_focus = focus, + AppCommand::CycleHost { right } => { + self.audio.cycle_host(right); + self.audio.trigger_restart(); + } AppCommand::AdjustAudioSetting { setting, delta } => { use crate::state::SettingKind; match setting { diff --git a/src/app/persistence.rs b/src/app/persistence.rs index ff3722e..1718f6f 100644 --- a/src/app/persistence.rs +++ b/src/app/persistence.rs @@ -16,6 +16,7 @@ impl App { pub fn save_settings(&self, link: &LinkState) { let settings = Settings { audio: crate::settings::AudioSettings { + host: self.audio.config.selected_host.clone(), output_device: self.audio.config.output_device.clone(), input_device: self.audio.config.input_device.clone(), channels: self.audio.config.channels, diff --git a/src/bin/desktop/main.rs b/src/bin/desktop/main.rs index 7741609..4dbd665 100644 --- a/src/bin/desktop/main.rs +++ b/src/bin/desktop/main.rs @@ -131,6 +131,7 @@ impl CagireDesktop { let new_audio_rx = sequencer.swap_audio_channel(); let new_config = AudioStreamConfig { + host: self.app.audio.config.selected_host.clone(), output_device: self.app.audio.config.output_device.clone(), input_device: self.app.audio.config.input_device.clone(), channels: self.app.audio.config.channels, diff --git a/src/commands.rs b/src/commands.rs index c0b72a0..f525fda 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use crate::model::{FollowUp, LaunchQuantization, PatternSpeed}; use crate::page::Page; -use crate::state::{ColorScheme, DeviceKind, Modal, OptionsFocus, PatternField, ScriptField, SettingKind}; +use crate::state::{ColorScheme, DevicesFocus, Modal, OptionsFocus, PatternField, ScriptField, SettingKind}; pub enum AppCommand { // Undo/Redo @@ -252,7 +252,8 @@ pub enum AppCommand { AudioSettingPrev, SetOutputDevice(String), SetInputDevice(String), - SetDeviceKind(DeviceKind), + SetDevicesFocus(DevicesFocus), + CycleHost { right: bool }, AdjustAudioSetting { setting: SettingKind, delta: i32, diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 7b99bbf..2bb98ac 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -279,6 +279,7 @@ use super::AudioCommand; #[cfg(feature = "cli")] pub struct AudioStreamConfig { + pub host: Option, pub output_device: Option, pub input_device: Option, pub channels: u16, @@ -317,10 +318,16 @@ pub fn build_stream( sample_paths: &[std::path::PathBuf], device_lost: Arc, ) -> Result { + let selection = match &config.host { + Some(name) => doux::audio::HostSelection::Named(name.to_lowercase()), + None => doux::audio::HostSelection::Auto, + }; + let host = doux::audio::get_host(selection).map_err(|e| format!("{e}"))?; + let device = match &config.output_device { - Some(name) => doux::audio::find_output_device(name) + Some(name) => doux::audio::find_output_device_for(&host, name) .ok_or_else(|| format!("Device not found: {name}"))?, - None => doux::audio::default_output_device().ok_or("No default output device")?, + None => doux::audio::default_output_device_for(&host).ok_or("No default output device")?, }; let default_config = device.default_output_config().map_err(|e| e.to_string())?; @@ -329,10 +336,10 @@ pub fn build_stream( let max_channels = doux::audio::max_output_channels(&device); let channels = config.channels.min(max_channels).max(2); - let host_name = doux::audio::preferred_host().id().name().to_string(); - let is_jack = host_name.to_lowercase().contains("jack"); + let host_name = host.id().name().to_string(); + let host_managed_buffer = doux::audio::host_controls_buffer_size(&host); - let buffer_size = if config.buffer_size > 0 && !is_jack { + let buffer_size = if config.buffer_size > 0 && !host_managed_buffer { cpal::BufferSize::Fixed(config.buffer_size) } else { cpal::BufferSize::Default @@ -370,7 +377,7 @@ pub fn build_stream( .input_device .as_ref() .and_then(|name| { - let dev = doux::audio::find_input_device(name); + let dev = doux::audio::find_input_device_for(&host, name); if dev.is_none() { eprintln!("input device not found: {name}"); } diff --git a/src/init.rs b/src/init.rs index 24a02d7..4e0ac9a 100644 --- a/src/init.rs +++ b/src/init.rs @@ -99,6 +99,18 @@ pub fn init(args: InitArgs) -> Init { }); } + app.audio.config.selected_host = settings.audio.host.clone(); + if let Some(ref host_name) = app.audio.config.selected_host { + if let Some(idx) = app + .audio + .available_hosts + .iter() + .position(|h| &h.name == host_name) + { + app.audio.host_index = idx; + app.audio.refresh_devices_for_host(); + } + } app.audio.config.output_device = args.output.or(settings.audio.output_device.clone()); app.audio.config.input_device = args.input.or(settings.audio.input_device.clone()); app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels); @@ -210,6 +222,7 @@ pub fn init(args: InitArgs) -> Init { let (stream_error_tx, stream_error_rx) = crossbeam_channel::bounded(16); let stream_config = AudioStreamConfig { + host: app.audio.config.selected_host.clone(), output_device: app.audio.config.output_device.clone(), input_device: app.audio.config.input_device.clone(), channels: app.audio.config.channels, diff --git a/src/input/engine_page.rs b/src/input/engine_page.rs index dba4504..1c2a83b 100644 --- a/src/input/engine_page.rs +++ b/src/input/engine_page.rs @@ -4,7 +4,7 @@ use std::sync::atomic::Ordering; use super::{InputContext, InputResult}; use crate::commands::AppCommand; use crate::engine::{AudioCommand, SeqCommand}; -use crate::state::{ConfirmAction, DeviceKind, EngineSection, LinkSetting, Modal, SettingKind}; +use crate::state::{ConfirmAction, DevicesFocus, EngineSection, LinkSetting, Modal, SettingKind}; pub(crate) fn cycle_engine_setting(ctx: &mut InputContext, right: bool) { let sign = if right { 1 } else { -1 }; @@ -13,10 +13,14 @@ pub(crate) fn cycle_engine_setting(ctx: &mut InputContext, right: bool) { setting: SettingKind::Channels, delta: sign, }), - SettingKind::BufferSize => ctx.dispatch(AppCommand::AdjustAudioSetting { - setting: SettingKind::BufferSize, - delta: sign * 64, - }), + SettingKind::BufferSize => { + if !ctx.app.audio.host_controls_buffer() { + ctx.dispatch(AppCommand::AdjustAudioSetting { + setting: SettingKind::BufferSize, + delta: sign * 64, + }); + } + } SettingKind::Polyphony => ctx.dispatch(AppCommand::AdjustAudioSetting { setting: SettingKind::Polyphony, delta: sign, @@ -146,9 +150,22 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input KeyCode::Tab => ctx.dispatch(AppCommand::AudioNextSection), KeyCode::BackTab => ctx.dispatch(AppCommand::AudioPrevSection), KeyCode::Up => match ctx.app.audio.section { - EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.device_kind { - DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputListUp), - DeviceKind::Input => ctx.dispatch(AppCommand::AudioInputListUp), + EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.devices_focus { + DevicesFocus::Host => {} + DevicesFocus::Output => { + if ctx.app.audio.output_list.cursor == 0 { + ctx.dispatch(AppCommand::SetDevicesFocus(DevicesFocus::Host)); + } else { + ctx.dispatch(AppCommand::AudioOutputListUp); + } + } + DevicesFocus::Input => { + if ctx.app.audio.input_list.cursor == 0 { + ctx.dispatch(AppCommand::SetDevicesFocus(DevicesFocus::Host)); + } else { + ctx.dispatch(AppCommand::AudioInputListUp); + } + } }, EngineSection::Settings => { ctx.dispatch(AppCommand::AudioSettingPrev); @@ -168,12 +185,15 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input _ => {} }, KeyCode::Down => match ctx.app.audio.section { - EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.device_kind { - DeviceKind::Output => { + EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.devices_focus { + DevicesFocus::Host => { + ctx.dispatch(AppCommand::SetDevicesFocus(DevicesFocus::Output)); + } + DevicesFocus::Output => { let count = ctx.app.audio.output_devices.len(); ctx.dispatch(AppCommand::AudioOutputListDown(count)); } - DeviceKind::Input => { + DevicesFocus::Input => { let count = ctx.app.audio.input_devices.len(); ctx.dispatch(AppCommand::AudioInputListDown(count)); } @@ -198,20 +218,22 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input }, KeyCode::PageUp => { if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices { - match ctx.app.audio.device_kind { - DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputPageUp), - DeviceKind::Input => ctx.app.audio.input_list.page_up(), + match ctx.app.audio.devices_focus { + DevicesFocus::Host => {} + DevicesFocus::Output => ctx.dispatch(AppCommand::AudioOutputPageUp), + DevicesFocus::Input => ctx.app.audio.input_list.page_up(), } } } KeyCode::PageDown => { if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices { - match ctx.app.audio.device_kind { - DeviceKind::Output => { + match ctx.app.audio.devices_focus { + DevicesFocus::Host => {} + DevicesFocus::Output => { let count = ctx.app.audio.output_devices.len(); ctx.dispatch(AppCommand::AudioOutputPageDown(count)); } - DeviceKind::Input => { + DevicesFocus::Input => { let count = ctx.app.audio.input_devices.len(); ctx.dispatch(AppCommand::AudioInputPageDown(count)); } @@ -220,8 +242,9 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input } KeyCode::Enter => { if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices { - match ctx.app.audio.device_kind { - DeviceKind::Output => { + match ctx.app.audio.devices_focus { + DevicesFocus::Host => {} + DevicesFocus::Output => { let cursor = ctx.app.audio.output_list.cursor; if cursor < ctx.app.audio.output_devices.len() { let index = ctx.app.audio.output_devices[cursor].index; @@ -229,7 +252,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input ctx.app.save_settings(ctx.link); } } - DeviceKind::Input => { + DevicesFocus::Input => { let cursor = ctx.app.audio.input_list.cursor; if cursor < ctx.app.audio.input_devices.len() { let index = ctx.app.audio.input_devices[cursor].index; @@ -241,9 +264,16 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input } } KeyCode::Left => match ctx.app.audio.section { - EngineSection::Devices if !ctx.app.plugin_mode => { - ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Output)); - } + EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.devices_focus { + DevicesFocus::Host => { + ctx.dispatch(AppCommand::CycleHost { right: false }); + ctx.app.save_settings(ctx.link); + } + DevicesFocus::Output => {} + DevicesFocus::Input => { + ctx.dispatch(AppCommand::SetDevicesFocus(DevicesFocus::Output)); + } + }, EngineSection::Settings => cycle_engine_setting(ctx, false), EngineSection::Link => cycle_link_setting(ctx, false), EngineSection::MidiOutput => cycle_midi_output(ctx, false), @@ -251,9 +281,16 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input _ => {} }, KeyCode::Right => match ctx.app.audio.section { - EngineSection::Devices if !ctx.app.plugin_mode => { - ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Input)); - } + EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.devices_focus { + DevicesFocus::Host => { + ctx.dispatch(AppCommand::CycleHost { right: true }); + ctx.app.save_settings(ctx.link); + } + DevicesFocus::Output => { + ctx.dispatch(AppCommand::SetDevicesFocus(DevicesFocus::Input)); + } + DevicesFocus::Input => {} + }, EngineSection::Settings => cycle_engine_setting(ctx, true), EngineSection::Link => cycle_link_setting(ctx, true), EngineSection::MidiOutput => cycle_midi_output(ctx, true), diff --git a/src/main.rs b/src/main.rs index 3a81b4d..2528755 100644 --- a/src/main.rs +++ b/src/main.rs @@ -126,6 +126,7 @@ fn main() -> io::Result<()> { let new_audio_rx = sequencer.swap_audio_channel(); let new_config = AudioStreamConfig { + host: app.audio.config.selected_host.clone(), output_device: app.audio.config.output_device.clone(), input_device: app.audio.config.input_device.clone(), channels: app.audio.config.channels, diff --git a/src/settings.rs b/src/settings.rs index 7bff67d..c9d92e0 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -26,6 +26,8 @@ pub struct Settings { #[derive(Debug, Serialize, Deserialize)] pub struct AudioSettings { + #[serde(default)] + pub host: Option, pub output_device: Option, pub input_device: Option, pub channels: u16, @@ -96,6 +98,7 @@ pub struct LinkSettings { impl Default for AudioSettings { fn default() -> Self { Self { + host: None, output_device: None, input_device: None, channels: 2, diff --git a/src/state/audio.rs b/src/state/audio.rs index 3c4f9c8..231c131 100644 --- a/src/state/audio.rs +++ b/src/state/audio.rs @@ -105,6 +105,7 @@ impl RefreshRate { #[derive(Clone)] pub struct AudioConfig { + pub selected_host: Option, pub output_device: Option, pub input_device: Option, pub channels: u16, @@ -133,6 +134,7 @@ pub struct AudioConfig { impl Default for AudioConfig { fn default() -> Self { Self { + selected_host: None, output_device: None, input_device: None, channels: 2, @@ -234,7 +236,8 @@ impl CyclicEnum for LinkSetting { } #[derive(Clone, Copy, PartialEq, Eq, Default)] -pub enum DeviceKind { +pub enum DevicesFocus { + Host, #[default] Output, Input, @@ -293,11 +296,13 @@ impl Default for Metrics { pub struct AudioSettings { pub config: AudioConfig, pub section: EngineSection, - pub device_kind: DeviceKind, + pub devices_focus: DevicesFocus, pub setting_kind: SettingKind, pub link_setting: LinkSetting, pub midi_output_slot: usize, pub midi_input_slot: usize, + pub available_hosts: Vec, + pub host_index: usize, pub output_devices: Vec, pub input_devices: Vec, pub output_list: ListSelectState, @@ -310,16 +315,25 @@ pub struct AudioSettings { impl Default for AudioSettings { fn default() -> Self { + let hosts = doux::audio::list_hosts(); + let preferred = doux::audio::preferred_host(); + let preferred_name = preferred.id().name().to_string(); + let host_index = hosts + .iter() + .position(|h| h.name == preferred_name) + .unwrap_or(0); Self { config: AudioConfig::default(), section: EngineSection::default(), - device_kind: DeviceKind::default(), + devices_focus: DevicesFocus::default(), setting_kind: SettingKind::default(), link_setting: LinkSetting::default(), midi_output_slot: 0, midi_input_slot: 0, - output_devices: doux::audio::list_output_devices(), - input_devices: doux::audio::list_input_devices(), + available_hosts: hosts, + host_index, + output_devices: doux::audio::list_output_devices_for(&preferred), + input_devices: doux::audio::list_input_devices_for(&preferred), output_list: ListSelectState { cursor: 0, scroll_offset: 0, @@ -344,11 +358,13 @@ impl AudioSettings { Self { config: AudioConfig::default(), section: EngineSection::Settings, - device_kind: DeviceKind::default(), + devices_focus: DevicesFocus::default(), setting_kind: SettingKind::Polyphony, link_setting: LinkSetting::default(), midi_output_slot: 0, midi_input_slot: 0, + available_hosts: Vec::new(), + host_index: 0, output_devices: Vec::new(), input_devices: Vec::new(), output_list: ListSelectState { @@ -370,8 +386,75 @@ impl AudioSettings { } pub fn refresh_devices(&mut self) { - self.output_devices = doux::audio::list_output_devices(); - self.input_devices = doux::audio::list_input_devices(); + self.refresh_devices_for_host(); + } + + pub fn refresh_devices_for_host(&mut self) { + let host_name = self + .available_hosts + .get(self.host_index) + .map(|h| h.name.clone()); + let selection = match host_name { + Some(name) => doux::audio::HostSelection::Named(name.to_lowercase()), + None => doux::audio::HostSelection::Auto, + }; + if let Ok(host) = doux::audio::get_host(selection) { + self.output_devices = doux::audio::list_output_devices_for(&host); + self.input_devices = doux::audio::list_input_devices_for(&host); + } else { + self.output_devices = Vec::new(); + self.input_devices = Vec::new(); + } + self.output_list.cursor = 0; + self.output_list.scroll_offset = 0; + self.input_list.cursor = 0; + self.input_list.scroll_offset = 0; + } + + pub fn cycle_host(&mut self, right: bool) { + let available: Vec = self + .available_hosts + .iter() + .enumerate() + .filter(|(_, h)| h.available) + .map(|(i, _)| i) + .collect(); + if available.len() <= 1 { + return; + } + let current_pos = available + .iter() + .position(|&i| i == self.host_index) + .unwrap_or(0); + let new_pos = if right { + (current_pos + 1) % available.len() + } else if current_pos == 0 { + available.len() - 1 + } else { + current_pos - 1 + }; + self.host_index = available[new_pos]; + let name = self.available_hosts[self.host_index].name.clone(); + self.config.selected_host = Some(name); + self.config.output_device = None; + self.config.input_device = None; + self.refresh_devices_for_host(); + } + + pub fn host_controls_buffer(&self) -> bool { + let host_name = self + .available_hosts + .get(self.host_index) + .map(|h| h.name.clone()); + let selection = match host_name { + Some(name) => doux::audio::HostSelection::Named(name.to_lowercase()), + None => doux::audio::HostSelection::Auto, + }; + if let Ok(host) = doux::audio::get_host(selection) { + doux::audio::host_controls_buffer_size(&host) + } else { + false + } } pub fn next_section(&mut self, plugin_mode: bool) { diff --git a/src/state/mod.rs b/src/state/mod.rs index 2e096e0..616eb28 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -28,7 +28,7 @@ pub mod sample_browser; pub mod undo; pub mod ui; -pub use audio::{AudioSettings, DeviceKind, EngineSection, LinkSetting, MainLayout, Metrics, ScopeMode, SettingKind, SpectrumMode}; +pub use audio::{AudioSettings, DevicesFocus, EngineSection, LinkSetting, MainLayout, Metrics, ScopeMode, SettingKind, SpectrumMode}; pub use color_scheme::ColorScheme; pub use editor::{ CopiedStepData, CopiedSteps, EditorContext, EditorTarget, EuclideanField, PatternField, diff --git a/src/views/engine_view.rs b/src/views/engine_view.rs index 8366243..635eda8 100644 --- a/src/views/engine_view.rs +++ b/src/views/engine_view.rs @@ -8,7 +8,7 @@ use ratatui::Frame; use crate::app::App; use crate::engine::LinkState; use crate::midi; -use crate::state::{DeviceKind, EngineSection, LinkSetting, SettingKind}; +use crate::state::{DevicesFocus, EngineSection, LinkSetting, SettingKind}; use crate::theme; use crate::widgets::{ render_scroll_indicators, render_section_header, IndicatorAlign, Scope, @@ -607,18 +607,46 @@ pub fn list_height(item_count: usize) -> u16 { pub fn devices_section_height(app: &App) -> u16 { let output_h = list_height(app.audio.output_devices.len()); let input_h = list_height(app.audio.input_devices.len()); - 3 + output_h.max(input_h) + 4 + output_h.max(input_h) } fn render_devices(frame: &mut Frame, app: &App, area: Rect) { let theme = theme::get(); let section_focused = app.audio.section == EngineSection::Devices; - let [header_area, content_area] = - Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).areas(area); + let [header_area, host_area, content_area] = + Layout::vertical([Constraint::Length(2), Constraint::Length(1), Constraint::Min(1)]) + .areas(area); render_section_header(frame, "DEVICES", section_focused, header_area); + // Host row + let host_focused = section_focused && app.audio.devices_focus == DevicesFocus::Host; + let host_name = app + .audio + .available_hosts + .get(app.audio.host_index) + .map(|h| h.name.as_str()) + .unwrap_or("-"); + let highlight = Style::new() + .fg(theme.engine.focused) + .add_modifier(Modifier::BOLD); + let normal = Style::new().fg(theme.engine.normal); + let label_style = Style::new().fg(theme.engine.label); + + let host_line = Line::from(vec![ + Span::styled( + if host_focused { + "> Driver " + } else { + " Driver " + }, + label_style, + ), + render_selector(host_name, host_focused, highlight, normal), + ]); + frame.render_widget(Paragraph::new(host_line), host_area); + let [output_col, separator, input_col] = Layout::horizontal([ Constraint::Percentage(48), Constraint::Length(3), @@ -626,8 +654,8 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) { ]) .areas(content_area); - let output_focused = section_focused && app.audio.device_kind == DeviceKind::Output; - let input_focused = section_focused && app.audio.device_kind == DeviceKind::Input; + let output_focused = section_focused && app.audio.devices_focus == DevicesFocus::Output; + let input_focused = section_focused && app.audio.devices_focus == DevicesFocus::Input; render_device_column( frame, @@ -758,8 +786,8 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) { label_style, ), render_selector( - &if app.audio.config.host_name.to_lowercase().contains("jack") { - "JACK managed".to_string() + &if app.audio.host_controls_buffer() { + "Host managed".to_string() } else { format!("{}", app.audio.config.buffer_size) },