Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 84 additions & 15 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::wirehose::{

use anyhow::{anyhow, Result};

use ratatui::style::Stylize;
use ratatui::{
layout::Flex,
prelude::{Buffer, Constraint, Direction, Layout, Position, Rect},
Expand Down Expand Up @@ -184,7 +185,9 @@ pub struct App<'a> {
error_message: Option<String>,
/// The main tabs
tabs: Vec<Tab>,
/// The index of the currently-visible tab
/// Indices into `tabs` for tabs that currently have visible content.
visible_tabs: Vec<usize>,
/// Index into `tabs` of the currently selected visible tab.
current_tab_index: usize,
/// Areas populated during rendering which define actions corresponding to
/// mouse activity
Expand Down Expand Up @@ -228,7 +231,9 @@ impl<'a> App<'a> {
rx: mpsc::Receiver<Event>,
config: Config,
) -> Self {
let tabs = config.tabs.iter().copied().map(Tab::from).collect();
let tabs: Vec<Tab> =
config.tabs.iter().copied().map(Tab::from).collect();
let visible_tabs = (0..tabs.len()).collect();

// Update peaks with VU-meter-style ballistics
let peak_processor = |new_peak, current_peak, samples, rate| {
Expand All @@ -248,6 +253,7 @@ impl<'a> App<'a> {
rx,
error_message: None,
tabs,
visible_tabs,
current_tab_index: config.tab,
mouse_areas: Vec::new(),
is_ready: false,
Expand Down Expand Up @@ -288,6 +294,9 @@ impl<'a> App<'a> {
&self.config.filters,
);
}
if !self.config.show_empty_tabs {
self.update_visible_tabs();
}
self.state_dirty = false;

let frame = terminal.get_frame();
Expand Down Expand Up @@ -330,6 +339,7 @@ impl<'a> App<'a> {
let mut widget_state = AppWidgetState {
mouse_areas: &mut self.mouse_areas,
tabs: &mut self.tabs,
visible_tabs: &self.visible_tabs,
help_position: &mut self.help_position,
};

Expand All @@ -341,6 +351,32 @@ impl<'a> App<'a> {
self.error_message = error_message;
}

fn update_visible_tabs(&mut self) {
self.visible_tabs = self
.tabs
.iter()
.enumerate()
.filter_map(|(index, tab)| {
if self.view.len(tab.list.list_kind) > 0 {
Some(index)
} else {
None
}
})
.collect();

if self.visible_tabs.is_empty() {
return;
}

self.current_tab_index =
match self.visible_tabs.binary_search(&self.current_tab_index) {
Ok(idx) => self.visible_tabs[idx],
Err(0) => self.visible_tabs[0],
Err(idx) => self.visible_tabs[idx - 1],
};
}

fn stop_capture(&mut self, object_id: ObjectId) {
self.capturing_objects.remove(&object_id);
self.wirehose.node_capture_stop(object_id);
Expand Down Expand Up @@ -575,8 +611,8 @@ impl Handle for Action {

match self {
Action::SelectTab(index) => {
if index < app.tabs.len() {
app.current_tab_index = index;
if let Some(&tab_index) = app.visible_tabs.get(index) {
app.current_tab_index = tab_index;
}
}
Action::MoveDown => {
Expand All @@ -586,14 +622,32 @@ impl Handle for Action {
current_list!(app).up(&app.view);
}
Action::TabLeft => {
app.current_tab_index = app
.current_tab_index
.checked_sub(1)
.unwrap_or(app.tabs.len() - 1)
if app.visible_tabs.is_empty() {
return Ok(false);
}
app.current_tab_index = match app
.visible_tabs
.binary_search(&app.current_tab_index)
{
Ok(idx) | Err(idx) => {
app.visible_tabs[idx
.checked_sub(1)
.unwrap_or(app.visible_tabs.len() - 1)]
}
};
}
Action::TabRight => {
app.current_tab_index =
(app.current_tab_index + 1) % app.tabs.len()
if app.visible_tabs.is_empty() {
return Ok(false);
}
app.current_tab_index = match app
.visible_tabs
.binary_search(&app.current_tab_index)
{
Ok(idx) | Err(idx) => {
app.visible_tabs[(idx + 1) % app.visible_tabs.len()]
}
};
}
Action::CloseDropdown => {
current_list!(app).dropdown_close();
Expand Down Expand Up @@ -737,13 +791,23 @@ pub struct AppWidget<'a, 'b> {
pub struct AppWidgetState<'a> {
mouse_areas: &'a mut Vec<MouseArea>,
tabs: &'a mut Vec<Tab>,
visible_tabs: &'a [usize],
help_position: &'a mut Option<u16>,
}

impl<'a> StatefulWidget for AppWidget<'a, '_> {
type State = AppWidgetState<'a>;

fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if state.visible_tabs.is_empty() {
let line = Line::from("All tabs are empty".red().bold()).centered();
let [area_centered] = Layout::vertical([Constraint::Length(1)])
.flex(Flex::Center)
.areas(area);
line.render(area_centered, buf);
return;
}

let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Expand All @@ -755,18 +819,22 @@ impl<'a> StatefulWidget for AppWidget<'a, '_> {
let menu_area = layout[1];

let constraints: Vec<_> = state
.tabs
.visible_tabs
.iter()
.map(|tab| Constraint::Length(tab.title.len() as u16 + 2))
.map(|&tab_index| {
let tab = &state.tabs[tab_index];
Constraint::Length(tab.title.len() as u16 + 2)
})
.collect();

let menu_areas = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(menu_area);

for (i, tab) in state.tabs.iter().enumerate() {
let title_line = if i == self.current_tab_index {
for (i, &tab_index) in state.visible_tabs.iter().enumerate() {
let tab = &state.tabs[tab_index];
let title_line = if tab_index == self.current_tab_index {
Line::from(vec![
Span::styled(
&self.config.char_set.tab_marker_left,
Expand All @@ -785,7 +853,6 @@ impl<'a> StatefulWidget for AppWidget<'a, '_> {
))
};
title_line.render(menu_areas[i], buf);

state.mouse_areas.push((
menu_areas[i],
smallvec![MouseEventKind::Down(MouseButton::Left)],
Expand Down Expand Up @@ -880,6 +947,7 @@ mod tests {
names: Default::default(),
tab: 0,
tabs: vec![TabKind::Playback],
show_empty_tabs: true,
lazy_capture: Default::default(),
filters: Default::default(),
};
Expand Down Expand Up @@ -981,6 +1049,7 @@ mod tests {
TabKind::Input,
TabKind::Configuration,
],
show_empty_tabs: true,
lazy_capture: Default::default(),
filters: Default::default(),
};
Expand Down
18 changes: 18 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub struct Config {
pub names: Names,
pub tab: usize,
pub tabs: Vec<TabKind>,
pub show_empty_tabs: bool,
pub lazy_capture: bool,
pub filters: Vec<MatchCondition>,
}
Expand Down Expand Up @@ -86,6 +87,8 @@ struct ConfigFile {
tab: Option<TabKind>,
#[serde(default = "default_tabs")]
tabs: Vec<TabKind>,
#[serde(default = "default_show_empty_tabs")]
show_empty_tabs: bool,
#[serde(default = "default_lazy_capture")]
lazy_capture: bool,
#[serde(default = "Filter::defaults", deserialize_with = "Filter::merge")]
Expand Down Expand Up @@ -250,6 +253,10 @@ fn default_tabs() -> Vec<TabKind> {
]
}

fn default_show_empty_tabs() -> bool {
false
}

fn default_char_set_name() -> String {
String::from("default")
}
Expand Down Expand Up @@ -309,6 +316,14 @@ impl ConfigFile {
self.tabs = tabs.clone();
}

if opt.show_empty_tabs {
self.show_empty_tabs = true
}

if opt.no_empty_tabs {
self.show_empty_tabs = false
}

if let Some(max_volume_percent) = &opt.max_volume_percent {
self.max_volume_percent = Some(*max_volume_percent);
}
Expand Down Expand Up @@ -395,6 +410,7 @@ impl TryFrom<ConfigFile> for Config {
names: config_file.names,
tab,
tabs: config_file.tabs,
show_empty_tabs: config_file.show_empty_tabs,
lazy_capture: config_file.lazy_capture,
filters,
})
Expand Down Expand Up @@ -480,6 +496,7 @@ pub mod strict {
themes: HashMap<String, Theme>,
tab: Option<TabKind>,
tabs: Vec<TabKind>,
show_empty_tabs: bool,
lazy_capture: bool,
filters: Vec<Filter>,
}
Expand All @@ -501,6 +518,7 @@ pub mod strict {
themes: strict.themes,
tab: strict.tab,
tabs: strict.tabs,
show_empty_tabs: strict.show_empty_tabs,
lazy_capture: strict.lazy_capture,
filters: strict.filters,
}
Expand Down
2 changes: 1 addition & 1 deletion src/object_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub struct ObjectList {
/// ID of the currently selected object
pub selected: Option<ObjectId>,
/// Which set of objects to use from the View
list_kind: ListKind,
pub list_kind: ListKind,
/// Default device type to use for defaults and node rendering
device_kind: Option<DeviceKind>,
/// Target dropdown state
Expand Down
8 changes: 8 additions & 0 deletions src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ pub struct Opt {
#[clap(short = 'T', long, num_args = 1.., value_enum)]
pub tabs: Option<Vec<TabKind>>,

/// Hide tabs that have no visible entries.
#[clap(long, conflicts_with = "show_empty_tabs")]
pub no_empty_tabs: bool,

/// Show tabs even when they have no visible entries.
#[clap(long, conflicts_with = "no_empty_tabs")]
pub show_empty_tabs: bool,

/// Maximum volume for volume sliders
#[clap(short = 'm', long, value_name = "PERCENT")]
pub max_volume_percent: Option<f32>,
Expand Down
3 changes: 3 additions & 0 deletions wiremix.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ tab = "playback"
# Which tabs are present and their order
tabs = [ "playback", "recording", "output", "input", "configuration" ]

# Whether to show tabs even when their corresponding list is empty.
show_empty_tabs = false

# Maximum percentage for volume sliders
max_volume_percent = 150.0

Expand Down
Loading