├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── actions.md ├── clippy.toml ├── rustfmt.toml ├── special_keys.md └── src ├── action_handlers ├── mod.rs ├── pulseaudio_info.rs ├── pulseaudio_status.rs ├── user_action.rs ├── user_input.rs └── volume_input_edit.rs ├── actor_system ├── actor │ ├── actor_type.rs │ ├── instance.rs │ ├── item.rs │ ├── message_queue.rs │ ├── mod.rs │ ├── registered_actors.rs │ └── status.rs ├── context.rs ├── messages.rs ├── mod.rs ├── prelude.rs ├── retry_strategy.rs └── worker.rs ├── actors ├── event_loop_actor.rs ├── input_actor.rs ├── mod.rs └── pa_actor.rs ├── cli_options.rs ├── config ├── actions.rs ├── colors.rs ├── default.rs ├── errors.rs ├── keys_mouse.rs ├── mod.rs └── variables.rs ├── help.rs ├── main.rs ├── models ├── actions.rs ├── context_menus.rs ├── entry │ ├── card_entry.rs │ ├── entries.rs │ ├── entry_type.rs │ ├── identifier.rs │ ├── mod.rs │ └── play_entry.rs ├── input_event.rs ├── mod.rs ├── page_entries.rs ├── page_type.rs ├── redraw.rs ├── state │ ├── mod.rs │ └── page_entries.rs ├── style.rs └── ui_mode.rs ├── multimap.rs ├── pa ├── callbacks.rs ├── common.rs ├── errors.rs ├── mod.rs ├── monitor.rs ├── pa_actions.rs └── pa_interface.rs ├── prelude.rs ├── ui ├── buffer.rs ├── errors.rs ├── mod.rs ├── rect.rs ├── scrollable.rs ├── util.rs └── widgets │ ├── block.rs │ ├── context_menu.rs │ ├── entry.rs │ ├── help.rs │ ├── mod.rs │ ├── tool_window.rs │ ├── volume.rs │ ├── volume_input.rs │ └── warning_text.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /log 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsmixer" 3 | version = "0.5.5" 4 | license = "MIT" 5 | homepage = "https://github.com/jantap/rsmixer" 6 | repository = "https://github.com/jantap/rsmixer" 7 | description = "PulseAudio volume mixer written in rust" 8 | readme = "README.md" 9 | categories = ["command-line-utilities"] 10 | keywords = ["pulseaudio", "mixer", "volume", "tui", "cli"] 11 | authors = ["Jan Wojcicki "] 12 | edition = "2018" 13 | 14 | [features] 15 | default = [] 16 | pa_v13 = ["libpulse-binding/pa_v13"] 17 | 18 | [dependencies] 19 | 20 | # logging 21 | log = "0.4.8" 22 | simple-logging = "2.0.2" 23 | env_logger = "0.7.1" 24 | 25 | # general 26 | linked-hash-map = { version = "0.5.3", features = ["serde_impl"] } 27 | lazy_static = "1.4.0" 28 | unicode-width = "0.1.8" 29 | state = { version = "0.4.1", features = ["tls"] } 30 | crossterm = { version = "0.19.0", features = ["serde", "event-stream"] } 31 | crossbeam-channel = "0.4.2" 32 | 33 | # error handling 34 | thiserror = "1.0.20" 35 | 36 | # config and cli options 37 | serde = { version = "=1.0.114", features = ["derive"] } 38 | toml = "0.5.6" 39 | confy = "0.4.0" 40 | gumdrop = "0.8.0" 41 | 42 | # async 43 | tokio = { version = "1.3.0", features = ["full"] } 44 | tokio-stream = { version = "0.1.4", features = ["sync"] } 45 | futures = "0.3.5" 46 | 47 | # pulseaudio 48 | libpulse-binding = { version = "2.21.0", default-features = false } 49 | semver = "0.11.0" 50 | itertools = "0.10.0" 51 | async-trait = "0.1.45" 52 | anyhow = "1.0.38" 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 jantap 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RsMixer 2 | 3 | ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/jantap/rsmixer) 4 | ![Crates.io tag](https://img.shields.io/crates/v/rsmixer) 5 | 6 | RsMixer is a PulseAudio volume mixer written in rust 7 | 8 | ## Features 9 | 10 | - monitors displaying current volume 11 | - applications using outputs displayed in a nested tree structure for easier viewing 12 | - changing card settings 13 | - all the basic stuff you expect a volume mixer to do 14 | 15 | ## Installation 16 | 17 | You can install RsMixer through cargo: 18 | 19 | ``` 20 | cargo install rsmixer 21 | ``` 22 | 23 | or by manually building it: 24 | 25 | ``` 26 | git clone https://github.com/jantap/rsmixer.git 27 | cargo install --path ./rsmixer 28 | ``` 29 | 30 | or if you're an Arch user, you can install it from AUR: 31 | 32 | ``` 33 | git clone https://aur.archlinux.org/rsmixer.git 34 | cd rsmixer 35 | makepkg -si 36 | ``` 37 | 38 | ## Usage 39 | 40 | Application screen is divided into 3 pages: Output, Input and Cards. Output combines PulseAudio sinks and sink inputs (if you don't know much about pulseaudio - basically sinks/sources are speakers/microphones, sink inputs/source outputs are audio streams from applications, for outputing and inputing sound respectively) into one tree-like view, that makes it easy to see which device every app uses. 41 | 42 | All keybindings are configurable through `~/.config/rsmixer/rsmixer.toml`. [Changing keybindings][changing keybindings] for more info. 43 | 44 | Default keybindings: 45 | 46 | - j,k - move between entries 47 | - h, l, H, L - change volume 48 | - 1, 2, 3 - open outputs, inputs, and cards respectively 49 | - enter - open context menu 50 | 51 | ## Changing keybindings 52 | 53 | In `~/.config/rsmixer/rsmixer.toml` you will find a section `[bindings]`. There you will find a list of default keybindings. 54 | 55 | All keybindings look one of these: 56 | 57 | ``` 58 | q = ['exit'] 59 | "shift+tab" = ['cycle_pages_backward'] 60 | right = ['raise_volume(5)'] 61 | ``` 62 | 63 | Basically `key = ArrayOf(action)`. Key is either: 64 | 65 | - a char 66 | - a special key. [Special keys supported](special_keys.md) (if anything is missing just create an issue) 67 | - a key combination, with plus signs between keys (one or more of shift, ctrl, alt and and a char/special key, seperated by plus signs) 68 | 69 | In the same way you can set behavior on right and middle clicks on entries 70 | 71 | ``` 72 | mouse_right = ['mute'] 73 | mouse_middle = ['hide'] 74 | ``` 75 | 76 | (left mouse click is automatically assigned to selecting entries and opening context menu when entry is already selected) 77 | 78 | When that key/key combination gets pressed rsmixer performs an action assigned to that keybinding. [Possible actions](actions.md) 79 | 80 | ## License 81 | 82 | [MIT](https://choosealicense.com/licenses/mit/) 83 | -------------------------------------------------------------------------------- /actions.md: -------------------------------------------------------------------------------- 1 | Actions have on of two formats: 2 | 3 | ``` 4 | mute 5 | raise_volume(5) 6 | ``` 7 | 8 | Most actions are just words seperated by underscores, however some take a parameter in form of a number. 9 | 10 | | name | description | argument | 11 | | -------------------- | ------------------------------------------------------- | ---------------------------- | 12 | | up(arg) | select an option higher than the currently selected one | number of places to move | 13 | | down(arg) | select an option lower than the currently selected one | number of places to move | 14 | | lower_volume(arg) | lower the volume of the currently selected entry | how much to lower the volume | 15 | | raise_volume(arg) | raise the volume of the currently selected entry | how much to raise the volume | 16 | | mute | mute the currently selected entry | | 17 | | hide | hide sink inputs/source outputs of current sink/source | | 18 | | show_output | show output tab | | 19 | | show_input | show input tab | | 20 | | show_cards | show cards tab | | 21 | | cycle_pages_forward | cycle to the next tab | | 22 | | cycle_pages_backward | cycle to the previous tab | | 23 | | context_menu | open context menu of the currently selected entry | | 24 | | close_context_menu | close the currently open context menu | | 25 | | confirm | confirm selection in currently open context menu | | 26 | | help | show help screen | | 27 | | exit | close rsmixer | | 28 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | too-many-arguments-threshold = 10 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | imports_granularity = "crate" 3 | group_imports = "StdExternalCrate" 4 | -------------------------------------------------------------------------------- /special_keys.md: -------------------------------------------------------------------------------- 1 | - backspace => Backspace, 2 | - enter => Enter 3 | - left => Left 4 | - right => Right 5 | - up => Up 6 | - down => Down 7 | - home => Home 8 | - end => End 9 | - pageup => PageUp 10 | - pagedown => PageDown 11 | - tab => Tab 12 | - delete => Delete 13 | - insert => Insert 14 | - null => Null 15 | - esc => Esc 16 | - F1-F12 17 | -------------------------------------------------------------------------------- /src/action_handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pulseaudio_info; 2 | pub mod pulseaudio_status; 3 | pub mod user_action; 4 | pub mod user_input; 5 | pub mod volume_input_edit; 6 | -------------------------------------------------------------------------------- /src/action_handlers/pulseaudio_info.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use crate::models::{EntryUpdate, RSState}; 4 | 5 | pub fn handle(msg: &EntryUpdate, state: &mut RSState) { 6 | match msg { 7 | EntryUpdate::EntryUpdate(ident, entry) => { 8 | state.update_entry(ident, entry.deref().to_owned()); 9 | } 10 | EntryUpdate::EntryRemoved(ident) => { 11 | state.remove_entry(&ident); 12 | } 13 | EntryUpdate::PeakVolumeUpdate(ident, peak) => { 14 | state.update_peak_volume(ident, peak); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/action_handlers/pulseaudio_status.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{PAStatus, RSState, UIMode}; 2 | 3 | pub fn handle(msg: &PAStatus, state: &mut RSState) { 4 | match msg { 5 | PAStatus::PulseAudioDisconnected => { 6 | state.reset(); 7 | } 8 | PAStatus::RetryIn(time) => { 9 | state.change_ui_mode(UIMode::RetryIn(*time)); 10 | } 11 | PAStatus::ConnectToPulseAudio => { 12 | state.change_ui_mode(UIMode::Normal); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/action_handlers/user_action.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | actor_system::Ctx, 3 | models::{PageType, PulseAudioAction, RSState, UIMode, UserAction}, 4 | }; 5 | 6 | pub fn handle(msg: &UserAction, state: &mut RSState, ctx: &Ctx) { 7 | match msg { 8 | UserAction::MoveUp(how_much) => { 9 | state.move_up(*how_much as usize); 10 | } 11 | UserAction::MoveDown(how_much) => { 12 | state.move_down(*how_much as usize); 13 | } 14 | UserAction::MoveLeft => { 15 | state.move_left(); 16 | } 17 | UserAction::MoveRight => { 18 | state.move_right(); 19 | } 20 | UserAction::SetSelected(index) => { 21 | state.set_selected(*index as usize); 22 | } 23 | UserAction::ChangePage(page) => { 24 | state.change_page(*page); 25 | } 26 | UserAction::CyclePages(which_way) => { 27 | ctx.send_to( 28 | "event_loop", 29 | UserAction::ChangePage(PageType::from(i8::from(state.current_page) + which_way)), 30 | ); 31 | } 32 | UserAction::RequestMute(ident) => { 33 | if state.ui_mode != UIMode::Normal || state.current_page == PageType::Cards { 34 | return; 35 | } 36 | 37 | state.request_mute(ident); 38 | } 39 | UserAction::RequstChangeVolume(how_much, ident) => { 40 | if state.ui_mode != UIMode::Normal || state.current_page == PageType::Cards { 41 | return; 42 | } 43 | 44 | state.request_change_volume(*how_much, ident); 45 | } 46 | UserAction::OpenContextMenu(ident) => { 47 | if state.ui_mode == UIMode::Normal { 48 | state.open_context_menu(ident); 49 | } 50 | } 51 | UserAction::CloseContextMenu => { 52 | if let UIMode::ContextMenu | UIMode::Help | UIMode::InputVolumeValue = state.ui_mode { 53 | state.change_ui_mode(UIMode::Normal); 54 | } 55 | } 56 | UserAction::Confirm => match state.ui_mode { 57 | UIMode::ContextMenu => { 58 | state.confirm_context_menu(); 59 | } 60 | UIMode::MoveEntry(ident, parent) => { 61 | state.change_ui_mode(UIMode::Normal); 62 | ctx.send_to( 63 | "pulseaudio", 64 | PulseAudioAction::MoveEntryToParent(ident, parent), 65 | ); 66 | } 67 | UIMode::InputVolumeValue => { 68 | state.confirm_input_volume(); 69 | state.change_ui_mode(UIMode::Normal); 70 | } 71 | _ => {} 72 | }, 73 | UserAction::Hide(ident) => { 74 | if UIMode::Normal == state.ui_mode { 75 | state.hide_entry(ident); 76 | } 77 | } 78 | UserAction::ShowHelp => { 79 | if UIMode::Normal == state.ui_mode { 80 | state.change_ui_mode(UIMode::Help); 81 | } 82 | } 83 | UserAction::RequestQuit => { 84 | ctx.shutdown(); 85 | } 86 | UserAction::InputVolumeValue => { 87 | if UIMode::Normal == state.ui_mode && state.current_page != PageType::Cards { 88 | state.setup_volume_input(); 89 | state.change_ui_mode(UIMode::InputVolumeValue); 90 | } 91 | } 92 | UserAction::ChangeVolumeInputValue(value, cursor) => { 93 | state.set_volume_input_value(value.clone(), *cursor); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/action_handlers/user_input.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use anyhow::Result; 4 | use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind}; 5 | 6 | use super::volume_input_edit; 7 | use crate::{ 8 | actor_system::Ctx, 9 | entry::{EntryIdentifier, EntryKind}, 10 | models::{InputEvent, PageType, RSState, UIMode, UserAction, UserInput}, 11 | ui::{Rect, Scrollable}, 12 | BINDINGS, 13 | }; 14 | 15 | pub fn handle(input: &UserInput, state: &RSState, ctx: &Ctx) -> Result<()> { 16 | let input_event = InputEvent::try_from(input.event)?; 17 | let mut actions; 18 | 19 | if let Some(bindings) = (*BINDINGS).get().get_vec(&input_event) { 20 | actions = bindings.clone(); 21 | 22 | handle_conflicting_bindings(&mut actions, state); 23 | 24 | if let Event::Mouse(mouse_event) = input.event { 25 | handle_mouse_bindings(&mut actions, mouse_event, state); 26 | } 27 | } else { 28 | actions = Vec::new(); 29 | 30 | if let Event::Mouse(mouse_event) = input.event { 31 | handle_unbindable_mouse_actions(&mut actions, mouse_event, state); 32 | } 33 | } 34 | 35 | if state.ui_mode == UIMode::InputVolumeValue { 36 | if let Event::Key(key_event) = input.event { 37 | volume_input_edit::handle(&mut actions, &key_event, state)?; 38 | } 39 | } 40 | 41 | for action in actions { 42 | ctx.send_to("event_loop", action); 43 | } 44 | 45 | Ok(()) 46 | } 47 | 48 | fn handle_unbindable_mouse_actions( 49 | actions: &mut Vec, 50 | mouse_event: MouseEvent, 51 | state: &RSState, 52 | ) { 53 | let mouse_pos = Rect::new(mouse_event.column, mouse_event.row, 1, 1); 54 | match (&state.ui_mode, mouse_event.kind) { 55 | (UIMode::Help, MouseEventKind::Up(_)) => { 56 | if !mouse_pos.intersects(&state.help.window.area) { 57 | actions.push(UserAction::CloseContextMenu); 58 | } 59 | } 60 | (UIMode::Normal, MouseEventKind::Up(MouseButton::Left)) => { 61 | let (ident, page_type) = find_collisions(mouse_event, state); 62 | 63 | if let Some(pt) = page_type { 64 | actions.push(UserAction::ChangePage(pt)); 65 | } 66 | if let Some(ident) = ident { 67 | let new_selected = state 68 | .page_entries 69 | .iter_entries() 70 | .position(|i| *i == ident) 71 | .unwrap_or_else(|| state.page_entries.selected()); 72 | 73 | if state.page_entries.selected() == new_selected { 74 | actions.push(UserAction::OpenContextMenu(None)); 75 | } else { 76 | actions.push(UserAction::SetSelected(new_selected)); 77 | } 78 | } 79 | } 80 | (UIMode::Normal, MouseEventKind::ScrollUp) => { 81 | if mouse_pos.y == 0 { 82 | actions.push(UserAction::CyclePages(-1)); 83 | } 84 | } 85 | (UIMode::Normal, MouseEventKind::ScrollDown) => { 86 | if mouse_pos.y == 0 { 87 | actions.push(UserAction::CyclePages(1)); 88 | } 89 | } 90 | (UIMode::ContextMenu, MouseEventKind::Up(MouseButton::Left)) => { 91 | if state.context_menu.area.intersects(&mouse_pos) { 92 | if let Some(i) = state 93 | .context_menu 94 | .visible_range(state.context_menu.area.height) 95 | .enumerate() 96 | .find(|(index, _)| { 97 | mouse_event.row == state.context_menu.area.y + (*index) as u16 98 | }) 99 | .map(|(_, i)| i) 100 | { 101 | if i == state.context_menu.selected() { 102 | actions.push(UserAction::Confirm); 103 | } else { 104 | actions.push(UserAction::SetSelected(i)); 105 | } 106 | } 107 | } else if !state.context_menu.tool_window.area.intersects(&mouse_pos) { 108 | actions.push(UserAction::CloseContextMenu); 109 | } 110 | } 111 | (UIMode::ContextMenu, MouseEventKind::ScrollUp) => { 112 | if state.context_menu.area.intersects(&mouse_pos) { 113 | actions.push(UserAction::MoveUp(1)); 114 | } 115 | } 116 | (UIMode::ContextMenu, MouseEventKind::ScrollDown) => { 117 | if state.context_menu.area.intersects(&mouse_pos) { 118 | actions.push(UserAction::MoveDown(1)); 119 | } 120 | } 121 | _ => {} 122 | } 123 | } 124 | 125 | fn find_collisions( 126 | mouse_event: MouseEvent, 127 | state: &RSState, 128 | ) -> (Option, Option) { 129 | let mouse_event_rect = Rect::new(mouse_event.column, mouse_event.row, 1, 1); 130 | 131 | let mut ident = None; 132 | let mut page_type = None; 133 | 134 | if mouse_event_rect.y > 0 { 135 | for entry in state 136 | .page_entries 137 | .visible_range(state.ui.entries_area.height) 138 | .filter_map(|i| state.page_entries.get(i)) 139 | .filter_map(|ident| state.entries.get(&ident)) 140 | { 141 | let area = match &entry.entry_kind { 142 | EntryKind::CardEntry(card) => card.area, 143 | EntryKind::PlayEntry(play) => play.area, 144 | }; 145 | 146 | if area.intersects(&mouse_event_rect) { 147 | ident = Some(EntryIdentifier::new(entry.entry_type, entry.index)); 148 | break; 149 | } 150 | } 151 | } else { 152 | let mut cur_x = 1; 153 | for (i, pn) in state.ui.pages_names.iter().enumerate() { 154 | if mouse_event_rect.x > cur_x && mouse_event_rect.x < cur_x + pn.len() as u16 { 155 | page_type = Some(PageType::from(i as i8)); 156 | break; 157 | } 158 | cur_x += pn.len() as u16 + 3; 159 | } 160 | } 161 | (ident, page_type) 162 | } 163 | 164 | fn handle_mouse_bindings(actions: &mut Vec, mouse_event: MouseEvent, state: &RSState) { 165 | let (ident, _) = find_collisions(mouse_event, state); 166 | 167 | if ident.is_none() { 168 | return; 169 | } 170 | 171 | for a in actions { 172 | match a { 173 | UserAction::RequstChangeVolume(value, _) => { 174 | *a = UserAction::RequstChangeVolume(*value, ident); 175 | } 176 | UserAction::RequestMute(_) => { 177 | *a = UserAction::RequestMute(ident); 178 | } 179 | UserAction::OpenContextMenu(_) => { 180 | *a = UserAction::OpenContextMenu(ident); 181 | } 182 | UserAction::Hide(_) => { 183 | *a = UserAction::Hide(ident); 184 | } 185 | _ => {} 186 | } 187 | } 188 | } 189 | 190 | fn handle_conflicting_bindings(actions: &mut Vec, state: &RSState) { 191 | if actions.len() == 1 { 192 | return; 193 | } 194 | 195 | if actions.contains(&UserAction::RequestQuit) && actions.contains(&UserAction::CloseContextMenu) 196 | { 197 | if let UIMode::ContextMenu 198 | | UIMode::Help 199 | | UIMode::InputVolumeValue 200 | | UIMode::MoveEntry(_, _) = state.ui_mode 201 | { 202 | actions.retain(|action| *action != UserAction::RequestQuit); 203 | } else { 204 | actions.retain(|action| *action != UserAction::CloseContextMenu); 205 | } 206 | } 207 | 208 | if actions.contains(&UserAction::Confirm) 209 | && actions.contains(&UserAction::OpenContextMenu(None)) 210 | { 211 | if let UIMode::MoveEntry(_, _) | UIMode::ContextMenu | UIMode::InputVolumeValue = 212 | state.ui_mode 213 | { 214 | actions.retain(|action| *action != UserAction::OpenContextMenu(None)); 215 | } else { 216 | actions.retain(|action| *action != UserAction::Confirm); 217 | } 218 | } 219 | 220 | if actions.contains(&UserAction::MoveLeft) { 221 | if let UIMode::ContextMenu | UIMode::Help = state.ui_mode { 222 | actions.retain(|action| *action == UserAction::MoveLeft); 223 | } else { 224 | actions.retain(|action| *action != UserAction::MoveLeft); 225 | } 226 | } 227 | 228 | if actions.contains(&UserAction::MoveRight) { 229 | if let UIMode::ContextMenu | UIMode::Help = state.ui_mode { 230 | actions.retain(|action| *action == UserAction::MoveRight); 231 | } else { 232 | actions.retain(|action| *action != UserAction::MoveRight); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/action_handlers/volume_input_edit.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event::{KeyCode, KeyEvent}; 3 | 4 | use crate::models::{RSState, UserAction}; 5 | 6 | pub fn handle(actions: &mut Vec, input: &KeyEvent, state: &RSState) -> Result<()> { 7 | let confirm = actions.iter().any(|a| *a == UserAction::Confirm); 8 | let close_context_menu = actions.iter().any(|a| *a == UserAction::CloseContextMenu); 9 | 10 | if confirm { 11 | actions.clear(); 12 | actions.push(UserAction::Confirm); 13 | return Ok(()); 14 | } 15 | if close_context_menu { 16 | actions.clear(); 17 | actions.push(UserAction::CloseContextMenu); 18 | return Ok(()); 19 | } 20 | 21 | let new_input_value = match input.code { 22 | KeyCode::Char(x @ '0') 23 | | KeyCode::Char(x @ '1') 24 | | KeyCode::Char(x @ '2') 25 | | KeyCode::Char(x @ '3') 26 | | KeyCode::Char(x @ '4') 27 | | KeyCode::Char(x @ '5') 28 | | KeyCode::Char(x @ '6') 29 | | KeyCode::Char(x @ '7') 30 | | KeyCode::Char(x @ '8') 31 | | KeyCode::Char(x @ '9') => Some(add_char(x, state)), 32 | KeyCode::Backspace => Some(remove_char(state)), 33 | KeyCode::Left => Some(move_cursor(state, -1)), 34 | KeyCode::Right => Some(move_cursor(state, 1)), 35 | _ => None, 36 | }; 37 | 38 | if let Some((value, cursor)) = new_input_value { 39 | actions.clear(); 40 | actions.push(UserAction::ChangeVolumeInputValue(value, cursor)); 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | fn remove_char(state: &RSState) -> (String, u8) { 47 | let value = state.input_exact_volume.value.clone(); 48 | let cursor = state.input_exact_volume.cursor as usize; 49 | 50 | if cursor == 0 { 51 | (value, cursor as u8) 52 | } else { 53 | let value = format!("{}{}", &value[0..(cursor - 1)], &value[cursor..]); 54 | 55 | (value, cursor as u8 - 1) 56 | } 57 | } 58 | 59 | fn move_cursor(state: &RSState, val: i8) -> (String, u8) { 60 | let value = state.input_exact_volume.value.clone(); 61 | let cursor = state.input_exact_volume.cursor as i8; 62 | 63 | if cursor < -val || cursor > value.len() as i8 - val { 64 | (value, cursor as u8) 65 | } else { 66 | (value, (cursor + val) as u8) 67 | } 68 | } 69 | 70 | fn add_char(c: char, state: &RSState) -> (String, u8) { 71 | let value = state.input_exact_volume.value.clone(); 72 | let cursor = state.input_exact_volume.cursor as usize; 73 | 74 | if value.len() == 3 { 75 | (value, cursor as u8) 76 | } else { 77 | let value = format!("{}{}{}", &value[0..cursor], c, &value[cursor..]); 78 | 79 | (value, cursor as u8 + 1) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/actor_system/actor/actor_type.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Copy, Clone)] 2 | pub enum ActorType { 3 | Eventful, 4 | Continous, 5 | } 6 | -------------------------------------------------------------------------------- /src/actor_system/actor/instance.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use tokio::{ 4 | sync::{RwLock, RwLockWriteGuard}, 5 | task::{self, JoinHandle}, 6 | }; 7 | use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; 8 | 9 | use super::super::{ 10 | actor::{Actor, ActorType, BoxedResultFuture, LockedActor}, 11 | channel, 12 | context::Ctx, 13 | messages::{BoxedMessage, Shutdown}, 14 | prelude::LockedReceiver, 15 | Sender, LOGGING_MODULE, 16 | }; 17 | use crate::prelude::*; 18 | 19 | pub struct ActorInstance { 20 | actor: LockedActor, 21 | pub event_channel: Sender, 22 | events_rx: LockedReceiver, 23 | task: Option>>, 24 | } 25 | 26 | impl ActorInstance { 27 | pub fn new(actor: Actor) -> Self { 28 | let (sx, rx) = channel(); 29 | let rx = Arc::new(RwLock::new(UnboundedReceiverStream::new(rx))); 30 | 31 | Self { 32 | actor: LockedActor::new(actor), 33 | event_channel: sx, 34 | events_rx: rx, 35 | task: None, 36 | } 37 | } 38 | 39 | pub async fn stop_actor(&mut self) { 40 | let mut actor = self.actor.write().await; 41 | actor.stop().await; 42 | } 43 | 44 | pub async fn get_receiver( 45 | &mut self, 46 | ) -> RwLockWriteGuard<'_, UnboundedReceiverStream> { 47 | self.events_rx.write().await 48 | } 49 | 50 | pub async fn start_actor_task(&mut self, id: &'static str, actor_type: ActorType, ctx: Ctx) { 51 | let event_loop = match actor_type { 52 | ActorType::Continous => { 53 | let mut actor = self.actor.write().await; 54 | let actor = actor.as_continous().unwrap(); 55 | actor.run(ctx.clone(), Arc::clone(&self.events_rx)) 56 | } 57 | ActorType::Eventful => { 58 | let actor = self.actor.clone(); 59 | generate_eventful_actor_loop(actor, Arc::clone(&self.events_rx), ctx.clone()) 60 | } 61 | }; 62 | 63 | let task_future = generate_actor_result_handler(id, ctx.clone(), event_loop); 64 | 65 | let task = task::spawn(task_future); 66 | 67 | self.task = Some(task); 68 | } 69 | 70 | pub async fn join(&mut self) { 71 | if let Some(handle) = &mut self.task { 72 | let _ = handle.await; 73 | } 74 | 75 | let mut actor = self.actor.write().await; 76 | actor.stop().await; 77 | } 78 | } 79 | 80 | fn generate_actor_result_handler( 81 | id: &'static str, 82 | ctx: Ctx, 83 | f: BoxedResultFuture, 84 | ) -> BoxedResultFuture { 85 | Box::pin(async move { 86 | let result = task::spawn(f).await; 87 | 88 | match result { 89 | Ok(res) => { 90 | if res.is_err() { 91 | warn!("Actor '{}' returned Err while handling message", id); 92 | } 93 | ctx.actor_returned(id, res); 94 | } 95 | Err(_) => { 96 | warn!("Actor '{}' panicked while handling message", id); 97 | ctx.actor_panicked(id); 98 | } 99 | } 100 | 101 | Ok(()) 102 | }) 103 | } 104 | 105 | fn generate_eventful_actor_loop( 106 | actor: LockedActor, 107 | rx: LockedReceiver, 108 | ctx: Ctx, 109 | ) -> BoxedResultFuture { 110 | Box::pin(async move { 111 | let mut actor = actor.write().await; 112 | let mut rx = rx.write().await; 113 | while let Some(msg) = rx.next().await { 114 | if msg.is::() { 115 | break; 116 | } 117 | 118 | let result = match &mut *actor { 119 | Actor::Eventful(actor) => actor.handle_message(ctx.clone(), msg).await, 120 | _ => Ok(()), 121 | }; 122 | 123 | if result.is_err() { 124 | return result; 125 | } 126 | } 127 | Ok(()) 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /src/actor_system/actor/item.rs: -------------------------------------------------------------------------------- 1 | use futures::future::{AbortHandle, Abortable}; 2 | use tokio::task; 3 | use tokio_stream::StreamExt; 4 | 5 | use super::{ 6 | super::{ 7 | context::Ctx, 8 | messages::{BoxedMessage, Shutdown}, 9 | retry_strategy::{RetryStrategy, Strategy}, 10 | }, 11 | ActorFactory, ActorInstance, ActorStatus, LockedActorStatus, MessageQueue, 12 | }; 13 | use crate::prelude::*; 14 | 15 | pub struct ActorItem { 16 | pub id: &'static str, 17 | factory: ActorFactory, 18 | status: LockedActorStatus, 19 | instance: Option, 20 | message_queue: MessageQueue, 21 | retry_strategy: RetryStrategy, 22 | retry_arbitrer: Option, 23 | } 24 | 25 | impl ActorItem { 26 | pub fn new(id: &'static str, factory: ActorFactory) -> Self { 27 | Self { 28 | id, 29 | factory, 30 | status: LockedActorStatus::new(ActorStatus::Off), 31 | instance: None, 32 | message_queue: MessageQueue::default(), 33 | retry_strategy: RetryStrategy::default(), 34 | retry_arbitrer: None, 35 | } 36 | } 37 | 38 | pub fn register_and_start(self, ctx: &mut Ctx) { 39 | let id = self.id; 40 | ctx.register_actor(self); 41 | ctx.start_actor(id); 42 | } 43 | 44 | pub fn on_error)> + 'static>(mut self, r: R) -> Self { 45 | self.retry_strategy.on_error = Box::new(r); 46 | self 47 | } 48 | 49 | pub fn on_panic + 'static>(mut self, r: R) -> Self { 50 | self.retry_strategy.on_panic = Box::new(r); 51 | self 52 | } 53 | 54 | pub fn send(&mut self, msg: BoxedMessage) { 55 | self.message_queue.push(msg); 56 | 57 | if let Some(instance) = &self.instance { 58 | self.message_queue.send(&instance.event_channel); 59 | } 60 | } 61 | 62 | pub async fn stop(&mut self) { 63 | self.status.set(ActorStatus::Stopping).await; 64 | if let Some(instance) = &mut self.instance { 65 | let _ = instance.event_channel.send(Box::new(Shutdown {})); 66 | } 67 | } 68 | 69 | pub async fn restart(&mut self) { 70 | self.status.set(ActorStatus::Restarting).await; 71 | if let Some(instance) = &mut self.instance { 72 | let _ = instance.event_channel.send(Box::new(Shutdown {})); 73 | } 74 | } 75 | 76 | pub async fn actor_task_finished(&mut self, ctx: &Ctx, result: Option>) { 77 | match self.status.get().await { 78 | ActorStatus::Ready => { 79 | let strategy = match result { 80 | Some(Ok(_)) => { 81 | self.status.set(ActorStatus::Off).await; 82 | return; 83 | } 84 | Some(Err(err)) => { 85 | let strategy = &mut self.retry_strategy; 86 | strategy.retry_count += 1; 87 | 88 | (strategy.on_error)((strategy.retry_count - 1, Err(err))) 89 | } 90 | None => { 91 | let strategy = &mut self.retry_strategy; 92 | strategy.retry_count += 1; 93 | 94 | (strategy.on_panic)(strategy.retry_count - 1) 95 | } 96 | }; 97 | 98 | let id = self.id; 99 | let ctx = ctx.clone(); 100 | let status = self.status.clone(); 101 | self.status.set(ActorStatus::ArbiterRunning).await; 102 | 103 | let (handle, registration) = AbortHandle::new_pair(); 104 | self.retry_arbitrer = Some(handle); 105 | 106 | task::spawn(Abortable::new( 107 | async move { 108 | if strategy.await { 109 | ctx.start_actor(id); 110 | } else { 111 | status.set(ActorStatus::Off).await; 112 | } 113 | }, 114 | registration, 115 | )); 116 | } 117 | ActorStatus::Restarting => { 118 | let _ = self.start(ctx).await; 119 | } 120 | ActorStatus::Stopping => { 121 | self.status.set(ActorStatus::Off).await; 122 | } 123 | _ => {} 124 | } 125 | } 126 | 127 | pub async fn stop_and_cache_messages(&mut self) { 128 | if let Some(instance) = &mut self.instance { 129 | instance.stop_actor().await; 130 | 131 | let mut rx = instance.get_receiver().await; 132 | rx.close(); 133 | 134 | while let Some(msg) = rx.next().await { 135 | self.message_queue.push(msg); 136 | } 137 | } 138 | 139 | self.instance = None; 140 | } 141 | 142 | pub fn clean_up_retry_arbitrer(&mut self) { 143 | if let Some(handle) = &mut self.retry_arbitrer { 144 | handle.abort(); 145 | } 146 | self.retry_arbitrer = None; 147 | } 148 | 149 | pub fn shutdown(&mut self) { 150 | if let Some(instance) = &mut self.instance { 151 | let _ = instance.event_channel.send(Box::new(Shutdown {})); 152 | } 153 | 154 | if let Some(handle) = &mut self.retry_arbitrer { 155 | handle.abort(); 156 | } 157 | } 158 | 159 | pub async fn join(&mut self) { 160 | if let Some(instance) = &mut self.instance { 161 | instance.join().await; 162 | } 163 | } 164 | 165 | pub async fn start(&mut self, ctx: &Ctx) -> Result<()> { 166 | if !self.status.is_off().await { 167 | return Err(anyhow::anyhow!("actor is not stopped")); 168 | } 169 | 170 | self.clean_up_retry_arbitrer(); 171 | 172 | let mut actor = (self.factory)(); 173 | 174 | actor.start(ctx.clone()).await; 175 | 176 | self.status.set(ActorStatus::Ready).await; 177 | 178 | let actor_type = actor.actor_type(); 179 | 180 | self.instance = Some(ActorInstance::new(actor)); 181 | 182 | let instance = self.instance.as_mut().unwrap(); 183 | instance 184 | .start_actor_task(self.id, actor_type, ctx.clone()) 185 | .await; 186 | 187 | self.message_queue.send(&instance.event_channel); 188 | 189 | Ok(()) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/actor_system/actor/message_queue.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use super::super::{messages::BoxedMessage, Sender}; 4 | 5 | #[derive(Default)] 6 | pub struct MessageQueue { 7 | queue: VecDeque, 8 | } 9 | 10 | impl MessageQueue { 11 | pub fn send(&mut self, sx: &Sender) { 12 | while let Some(msg) = self.queue.pop_front() { 13 | if let Err(e) = sx.send(msg) { 14 | self.queue.push_back(e.0); 15 | } 16 | } 17 | } 18 | 19 | pub fn push(&mut self, msg: BoxedMessage) { 20 | self.queue.push_back(msg); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/actor_system/actor/mod.rs: -------------------------------------------------------------------------------- 1 | mod actor_type; 2 | mod instance; 3 | mod item; 4 | mod message_queue; 5 | mod registered_actors; 6 | mod status; 7 | 8 | use std::{pin::Pin, sync::Arc}; 9 | 10 | pub use actor_type::ActorType; 11 | use async_trait::async_trait; 12 | use futures::Future; 13 | pub use instance::ActorInstance; 14 | pub use item::ActorItem; 15 | pub use message_queue::MessageQueue; 16 | pub use status::{ActorStatus, LockedActorStatus}; 17 | use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; 18 | 19 | use super::{context::Ctx, messages::BoxedMessage, prelude::LockedReceiver}; 20 | use crate::prelude::*; 21 | 22 | pub type BoxedEventfulActor = Box; 23 | pub type BoxedContinousActor = Box; 24 | 25 | pub type BoxedResultFuture = Pin> + Send + Sync>>; 26 | 27 | pub type ActorFactory = &'static (dyn Fn() -> Actor + Send + Sync); 28 | 29 | #[async_trait] 30 | pub trait EventfulActor { 31 | async fn start(&mut self, ctx: Ctx); 32 | async fn stop(&mut self); 33 | fn handle_message<'a>( 34 | &'a mut self, 35 | ctx: Ctx, 36 | msg: BoxedMessage, 37 | ) -> Pin> + Send + Sync + 'a>>; 38 | } 39 | 40 | #[async_trait] 41 | pub trait ContinousActor { 42 | async fn start(&mut self, ctx: Ctx); 43 | fn run(&mut self, ctx: Ctx, events_rx: LockedReceiver) -> BoxedResultFuture; 44 | async fn stop(&mut self); 45 | } 46 | 47 | pub enum Actor { 48 | Eventful(BoxedEventfulActor), 49 | Continous(BoxedContinousActor), 50 | } 51 | 52 | impl Actor { 53 | pub fn actor_type(&self) -> ActorType { 54 | match self { 55 | Self::Eventful(_) => ActorType::Eventful, 56 | Self::Continous(_) => ActorType::Continous, 57 | } 58 | } 59 | pub async fn start(&mut self, ctx: Ctx) { 60 | match self { 61 | Self::Eventful(actor) => actor.start(ctx).await, 62 | Self::Continous(actor) => actor.start(ctx).await, 63 | } 64 | } 65 | pub async fn stop(&mut self) { 66 | match self { 67 | Self::Eventful(actor) => actor.stop().await, 68 | Self::Continous(actor) => actor.stop().await, 69 | } 70 | } 71 | pub fn as_continous(&mut self) -> Option<&mut BoxedContinousActor> { 72 | match self { 73 | Self::Continous(a) => Some(a), 74 | Self::Eventful(_) => None, 75 | } 76 | } 77 | #[allow(dead_code)] 78 | pub fn as_eventful(&mut self) -> Option<&mut BoxedEventfulActor> { 79 | match self { 80 | Self::Eventful(a) => Some(a), 81 | Self::Continous(_) => None, 82 | } 83 | } 84 | } 85 | 86 | #[derive(Clone)] 87 | pub struct LockedActor(Arc>); 88 | 89 | impl LockedActor { 90 | pub fn new(actor: Actor) -> Self { 91 | Self { 92 | 0: Arc::new(RwLock::new(actor)), 93 | } 94 | } 95 | #[allow(dead_code)] 96 | pub async fn read(&self) -> RwLockReadGuard<'_, Actor> { 97 | self.0.read().await 98 | } 99 | pub async fn write(&self) -> RwLockWriteGuard<'_, Actor> { 100 | self.0.write().await 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/actor_system/actor/registered_actors.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/actor_system/actor/status.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use tokio::sync::RwLock; 4 | 5 | #[derive(PartialEq, Copy, Clone, Debug)] 6 | pub enum ActorStatus { 7 | Off, 8 | Ready, 9 | Stopping, 10 | Restarting, 11 | ArbiterRunning, 12 | } 13 | 14 | impl ActorStatus { 15 | pub fn is_off(&self) -> bool { 16 | matches!(self, Self::ArbiterRunning | Self::Off) 17 | } 18 | } 19 | 20 | #[derive(Clone)] 21 | pub struct LockedActorStatus(Arc>); 22 | 23 | impl LockedActorStatus { 24 | pub fn new(status: ActorStatus) -> Self { 25 | Self { 26 | 0: Arc::new(RwLock::new(status)), 27 | } 28 | } 29 | pub async fn set(&self, status: ActorStatus) { 30 | let mut stat = self.0.write().await; 31 | *stat = status; 32 | } 33 | pub async fn get(&self) -> ActorStatus { 34 | let stat = self.0.read().await; 35 | *stat 36 | } 37 | pub async fn is_off(&self) -> bool { 38 | self.get().await.is_off() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/actor_system/context.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, sync::Arc}; 2 | 3 | use super::{actor::ActorItem, messages::SystemMessage, Sender}; 4 | use crate::prelude::*; 5 | 6 | #[derive(Clone)] 7 | pub struct Ctx { 8 | internal_sx: Sender>, 9 | } 10 | 11 | impl From>> for Ctx { 12 | fn from(internal_sx: Sender>) -> Self { 13 | Self { internal_sx } 14 | } 15 | } 16 | 17 | impl Ctx { 18 | pub fn send_to(&self, id: &'static str, msg: T) { 19 | let _ = self 20 | .internal_sx 21 | .send(Arc::new(SystemMessage::SendMsg(id, Box::new(msg)))); 22 | } 23 | pub fn shutdown(&self) { 24 | let _ = self.internal_sx.send(Arc::new(SystemMessage::Shutdown)); 25 | } 26 | #[allow(dead_code)] 27 | pub fn stop_actor(&self, id: &'static str) { 28 | let _ = self 29 | .internal_sx 30 | .send(Arc::new(SystemMessage::StopActor(id))); 31 | } 32 | #[allow(dead_code)] 33 | pub fn restart_actor(&self, id: &'static str) { 34 | let _ = self 35 | .internal_sx 36 | .send(Arc::new(SystemMessage::RestartActor(id))); 37 | } 38 | pub fn register_actor(&mut self, actor_item: ActorItem) { 39 | let _ = self 40 | .internal_sx 41 | .send(Arc::new(SystemMessage::RegisterActor(actor_item))); 42 | } 43 | pub fn start_actor(&self, id: &'static str) { 44 | let _ = self 45 | .internal_sx 46 | .send(Arc::new(SystemMessage::StartActor(id))); 47 | } 48 | pub fn actor_panicked(&self, id: &'static str) { 49 | let _ = self 50 | .internal_sx 51 | .send(Arc::new(SystemMessage::ActorTaskFinished(id, None))); 52 | } 53 | pub fn actor_returned(&self, id: &'static str, result: Result<()>) { 54 | let _ = self 55 | .internal_sx 56 | .send(Arc::new(SystemMessage::ActorTaskFinished(id, Some(result)))); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/actor_system/messages.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, fmt::Debug}; 2 | 3 | use super::actor::ActorItem; 4 | use crate::prelude::*; 5 | 6 | pub type BoxedMessage = Box; 7 | 8 | pub trait Message: Any + Send + Sync + Debug {} 9 | impl Message for T where T: Any + Send + Sync + Debug {} 10 | 11 | pub enum SystemMessage { 12 | RegisterActor(ActorItem), 13 | StopActor(&'static str), 14 | StartActor(&'static str), 15 | SendMsg(&'static str, BoxedMessage), 16 | RestartActor(&'static str), 17 | ActorTaskFinished(&'static str, Option>), 18 | Shutdown, 19 | // Broadcast(BoxedMessage), 20 | } 21 | 22 | pub struct Shutdown {} 23 | -------------------------------------------------------------------------------- /src/actor_system/mod.rs: -------------------------------------------------------------------------------- 1 | mod actor; 2 | mod context; 3 | mod messages; 4 | pub mod prelude; 5 | mod retry_strategy; 6 | mod worker; 7 | 8 | pub use actor::{Actor, ActorStatus, ActorType, ContinousActor, EventfulActor}; 9 | pub use context::Ctx; 10 | pub use messages::BoxedMessage; 11 | pub use retry_strategy::StrategyClosure; 12 | use tokio::sync::mpsc::{ 13 | unbounded_channel as channel, UnboundedReceiver as Receiver, UnboundedSender as Sender, 14 | }; 15 | pub use worker::Worker; 16 | 17 | static LOGGING_MODULE: &str = "ActorSystem"; 18 | 19 | pub fn new() -> (Ctx, Worker) { 20 | let (sx, rx) = channel(); 21 | 22 | (sx.clone().into(), Worker::new(sx, rx)) 23 | } 24 | -------------------------------------------------------------------------------- /src/actor_system/prelude.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | pub use async_trait::async_trait; 4 | use tokio::sync::RwLock; 5 | use tokio_stream::wrappers::UnboundedReceiverStream; 6 | 7 | pub use super::{ 8 | actor::{Actor, ActorItem, ActorStatus, BoxedResultFuture, ContinousActor, EventfulActor}, 9 | context::Ctx, 10 | messages::{BoxedMessage, Shutdown}, 11 | retry_strategy::{PinnedClosure, RetryStrategy}, 12 | worker::Worker, 13 | }; 14 | 15 | pub type LockedReceiver = Arc>>; 16 | -------------------------------------------------------------------------------- /src/actor_system/retry_strategy.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | 3 | use futures::Future; 4 | 5 | use crate::prelude::*; 6 | 7 | pub trait StrategyClosure: Future + Send + Sync {} 8 | impl StrategyClosure for X where X: Future + Send + Sync {} 9 | 10 | pub type PinnedClosure = Pin>; 11 | 12 | pub trait Strategy: Fn(T) -> PinnedClosure + Send + Sync {} 13 | impl Strategy for X where X: Fn(T) -> PinnedClosure + Send + Sync {} 14 | 15 | pub struct RetryStrategy { 16 | pub on_panic: Box>, 17 | pub on_error: Box)>>, 18 | pub retry_count: usize, 19 | } 20 | 21 | impl Default for RetryStrategy { 22 | fn default() -> Self { 23 | Self { 24 | on_panic: Box::new(|_: usize| Box::pin(async { false })), 25 | on_error: Box::new(|(_, _)| Box::pin(async { false })), 26 | retry_count: 0, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/actor_system/worker.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use tokio::task; 4 | 5 | use super::{ 6 | actor::ActorItem, 7 | context::Ctx, 8 | messages::{BoxedMessage, SystemMessage}, 9 | Receiver, Sender, LOGGING_MODULE, 10 | }; 11 | use crate::prelude::*; 12 | 13 | pub struct RegisteredActors { 14 | items: HashMap<&'static str, ActorItem>, 15 | ctx: Ctx, 16 | } 17 | 18 | impl RegisteredActors { 19 | pub fn new(ctx: Ctx) -> Self { 20 | Self { 21 | items: HashMap::new(), 22 | ctx, 23 | } 24 | } 25 | 26 | pub fn register(&mut self, item: ActorItem) { 27 | self.items.insert(item.id, item); 28 | } 29 | 30 | pub async fn stop(&mut self, id: &'static str) { 31 | if let Some(item) = self.items.get_mut(id) { 32 | item.stop().await; 33 | } 34 | } 35 | 36 | pub async fn restart(&mut self, id: &'static str) { 37 | if let Some(item) = self.items.get_mut(id) { 38 | item.restart().await; 39 | } 40 | } 41 | 42 | pub async fn stop_and_cache_messages(&mut self, id: &'static str) { 43 | if let Some(item) = self.items.get_mut(id) { 44 | item.stop_and_cache_messages().await; 45 | } 46 | } 47 | 48 | pub async fn start(&mut self, id: &'static str) -> Result<()> { 49 | if let Some(item) = self.items.get_mut(id) { 50 | item.start(&self.ctx).await 51 | } else { 52 | Err(anyhow::anyhow!("actor is not registered")) 53 | } 54 | } 55 | 56 | pub fn send(&mut self, id: &'static str, msg: BoxedMessage) { 57 | if let Some(item) = self.items.get_mut(id) { 58 | item.send(msg); 59 | } 60 | } 61 | 62 | pub async fn actor_task_finished(&mut self, id: &'static str, result: Option>) { 63 | if let Some(item) = self.items.get_mut(id) { 64 | item.actor_task_finished(&self.ctx, result).await; 65 | } 66 | } 67 | 68 | pub async fn shutdown(&mut self) { 69 | for item in self.items.values_mut() { 70 | item.shutdown(); 71 | } 72 | 73 | for item in self.items.values_mut() { 74 | item.join().await; 75 | } 76 | } 77 | } 78 | 79 | pub struct Worker { 80 | actors: RegisteredActors, 81 | internal_rx: Option>>, 82 | } 83 | impl Worker { 84 | pub fn new(sx: Sender>, rx: Receiver>) -> Self { 85 | Self { 86 | actors: RegisteredActors::new(sx.into()), 87 | internal_rx: Some(rx), 88 | } 89 | } 90 | 91 | pub fn start(mut self) -> tokio::task::JoinHandle> { 92 | let mut i_rx = self.internal_rx.take().unwrap(); 93 | task::spawn(async move { 94 | debug!("Starting worker task"); 95 | 96 | while let Some(msg) = i_rx.recv().await { 97 | let msg = match Arc::try_unwrap(msg) { 98 | Ok(x) => x, 99 | Err(_) => { 100 | error!("Failed to unwrap Arc. Skipping message. This may be very bad news"); 101 | continue; 102 | } 103 | }; 104 | match msg { 105 | SystemMessage::RegisterActor(item) => { 106 | self.actors.register(item); 107 | } 108 | SystemMessage::StartActor(id) => { 109 | if let Err(e) = self.actors.start(id).await { 110 | error!("Failed to start actor {}.\n{:#?}", id, e); 111 | } 112 | } 113 | SystemMessage::StopActor(id) => { 114 | self.actors.stop(id).await; 115 | } 116 | SystemMessage::SendMsg(id, m) => { 117 | self.actors.send(id, m); 118 | } 119 | SystemMessage::ActorTaskFinished(id, result) => { 120 | self.actors.stop_and_cache_messages(id).await; 121 | 122 | self.actors.actor_task_finished(id, result).await; 123 | } 124 | SystemMessage::RestartActor(id) => { 125 | self.actors.restart(id).await; 126 | } 127 | SystemMessage::Shutdown => { 128 | self.actors.shutdown().await; 129 | break; 130 | } 131 | }; 132 | } 133 | Ok(()) 134 | }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/actors/event_loop_actor.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Stdout, pin::Pin}; 2 | 3 | use anyhow::Result; 4 | use futures::Future; 5 | 6 | use crate::{ 7 | action_handlers::*, 8 | actor_system::prelude::*, 9 | models::{ 10 | EntryUpdate, PAStatus, PulseAudioAction, RSState, ResizeScreen, UserAction, UserInput, 11 | }, 12 | ui, STYLES, 13 | }; 14 | 15 | #[derive(Default)] 16 | pub struct EventLoopActor { 17 | stdout: Option, 18 | state: RSState, 19 | } 20 | 21 | impl EventLoopActor { 22 | pub fn factory() -> Actor { 23 | Actor::Eventful(Box::new(Self::default())) 24 | } 25 | 26 | pub fn item() -> ActorItem { 27 | ActorItem::new("event_loop", &Self::factory) 28 | .on_panic(|_| -> PinnedClosure { Box::pin(async { true }) }) 29 | .on_error(|_| -> PinnedClosure { Box::pin(async { true }) }) 30 | } 31 | } 32 | 33 | #[async_trait] 34 | impl EventfulActor for EventLoopActor { 35 | async fn start(&mut self, ctx: Ctx) { 36 | self.stdout = Some(ui::prepare_terminal().unwrap()); 37 | self.state = RSState::new(ctx.clone()); 38 | self.state.ui.buffer.set_styles((*STYLES).get().clone()); 39 | self.state.redraw.resize = true; 40 | 41 | ctx.send_to("pulseaudio", PulseAudioAction::RequestPulseAudioState); 42 | } 43 | 44 | async fn stop(&mut self) { 45 | ui::clean_terminal().unwrap(); 46 | } 47 | 48 | fn handle_message<'a>( 49 | &'a mut self, 50 | ctx: Ctx, 51 | msg: BoxedMessage, 52 | ) -> Pin> + Send + Sync + 'a>> { 53 | Box::pin(async move { 54 | if msg.is::() { 55 | let msg = msg.downcast_ref::().unwrap(); 56 | 57 | pulseaudio_info::handle(&msg, &mut self.state); 58 | } else if msg.is::() { 59 | let msg = msg.downcast_ref::().unwrap(); 60 | 61 | pulseaudio_status::handle(&msg, &mut self.state); 62 | } else if msg.is::() { 63 | let msg = msg.downcast_ref::().unwrap(); 64 | 65 | user_input::handle(&msg, &self.state, &ctx)?; 66 | } else if msg.is::() { 67 | let msg = msg.downcast_ref::().unwrap(); 68 | 69 | user_action::handle(&msg, &mut self.state, &ctx); 70 | } else if msg.is::() { 71 | self.state.redraw.resize = true; 72 | } 73 | 74 | if self.state.redraw.anything() { 75 | if let Some(stdout) = &mut self.stdout { 76 | ui::redraw(stdout, &mut self.state).await?; 77 | } 78 | 79 | self.state.redraw.reset(); 80 | } 81 | 82 | Ok(()) 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/actors/input_actor.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event::{Event, EventStream, MouseEventKind}; 3 | use tokio_stream::StreamExt; 4 | 5 | use crate::{ 6 | actor_system::prelude::*, 7 | models::{ResizeScreen, UserInput}, 8 | }; 9 | 10 | pub struct InputActor {} 11 | 12 | impl InputActor { 13 | pub fn factory() -> Actor { 14 | Actor::Continous(Box::new(Self {})) 15 | } 16 | 17 | pub fn item() -> ActorItem { 18 | ActorItem::new("input", &Self::factory) 19 | .on_panic(|_| -> PinnedClosure { Box::pin(async { true }) }) 20 | .on_error(|_| -> PinnedClosure { Box::pin(async { true }) }) 21 | } 22 | } 23 | 24 | #[async_trait] 25 | impl ContinousActor for InputActor { 26 | async fn start(&mut self, _ctx: Ctx) {} 27 | async fn stop(&mut self) {} 28 | 29 | fn run(&mut self, ctx: Ctx, events_rx: LockedReceiver) -> BoxedResultFuture { 30 | Box::pin(start(events_rx, ctx)) 31 | } 32 | } 33 | 34 | pub async fn start(rx: LockedReceiver, ctx: Ctx) -> Result<()> { 35 | let mut reader = EventStream::new(); 36 | let mut rx = rx.write().await; 37 | 38 | loop { 39 | let input_event = reader.next(); 40 | let recv_event = rx.next(); 41 | 42 | tokio::select! { 43 | ev = input_event => { 44 | let ev = if let Some(ev) = ev { ev } else { continue; }; 45 | let ev = if let Ok(ev) = ev { ev } else { continue; }; 46 | 47 | match ev { 48 | Event::Key(_) => { 49 | ctx.send_to("event_loop", UserInput::new(ev)); 50 | } 51 | Event::Mouse(me) => { 52 | if MouseEventKind::Moved != me.kind { 53 | ctx.send_to("event_loop", UserInput::new(ev)); 54 | } 55 | } 56 | Event::Resize(_, _) => { 57 | ctx.send_to("event_loop", ResizeScreen::new()); 58 | } 59 | }; 60 | } 61 | ev = recv_event => { 62 | let ev = if let Some(ev) = ev { ev } else { continue; }; 63 | if ev.is::() { 64 | return Ok(()); 65 | } 66 | } 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/actors/mod.rs: -------------------------------------------------------------------------------- 1 | mod event_loop_actor; 2 | mod input_actor; 3 | mod pa_actor; 4 | 5 | pub use event_loop_actor::EventLoopActor; 6 | pub use input_actor::InputActor; 7 | pub use pa_actor::PulseActor; 8 | -------------------------------------------------------------------------------- /src/actors/pa_actor.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use tokio::{sync::mpsc, task}; 5 | use tokio_stream::{ 6 | wrappers::{IntervalStream, UnboundedReceiverStream}, 7 | StreamExt, 8 | }; 9 | 10 | use crate::{ 11 | actor_system::prelude::*, 12 | models::{EntryUpdate, PAStatus, PulseAudioAction}, 13 | pa::{self, common::*}, 14 | VARIABLES, 15 | }; 16 | 17 | pub struct PulseActor {} 18 | 19 | impl PulseActor { 20 | pub fn factory() -> Actor { 21 | Actor::Continous(Box::new(Self {})) 22 | } 23 | pub fn item() -> ActorItem { 24 | ActorItem::new("pulseaudio", &Self::factory) 25 | .on_panic(|_| -> PinnedClosure { Box::pin(async { true }) }) 26 | .on_error(|_| -> PinnedClosure { Box::pin(async { true }) }) 27 | } 28 | } 29 | 30 | #[async_trait] 31 | impl ContinousActor for PulseActor { 32 | async fn start(&mut self, _ctx: Ctx) {} 33 | async fn stop(&mut self) {} 34 | fn run(&mut self, ctx: Ctx, events_rx: LockedReceiver) -> BoxedResultFuture { 35 | Box::pin(start_async(events_rx, ctx)) 36 | } 37 | } 38 | 39 | async fn start_async(external_rx: LockedReceiver, ctx: Ctx) -> Result<()> { 40 | let mut interval = IntervalStream::new(tokio::time::interval(Duration::from_millis(50))); 41 | 42 | let send = |ch: &cb_channel::Sender, msg: PAInternal| -> Result<()> { 43 | match ch.send(msg) { 44 | Ok(()) => Ok(()), 45 | Err(err) => Err(PAError::ChannelError(err).into()), 46 | } 47 | }; 48 | let retry_time = (*VARIABLES).get().pa_retry_time; 49 | let mut external_rx = external_rx.write().await; 50 | 51 | loop { 52 | let (info_sx, info_rx) = mpsc::unbounded_channel(); 53 | let (internal_actions_sx, internal_actions_rx) = mpsc::unbounded_channel::(); 54 | let (internal_sx, internal_rx) = cb_channel::unbounded(); 55 | let (pa_finished_sx, pa_finished_rx) = mpsc::unbounded_channel(); 56 | 57 | let sync_pa = task::spawn_blocking(move || { 58 | let res = pa::start(internal_rx, info_sx, internal_actions_sx); 59 | let _ = pa_finished_sx.send(res); 60 | }); 61 | 62 | let mut pa_finished_rx = UnboundedReceiverStream::new(pa_finished_rx); 63 | let mut internal_actions_rx = UnboundedReceiverStream::new(internal_actions_rx); 64 | let mut info_rx = UnboundedReceiverStream::new(info_rx); 65 | 66 | ctx.send_to("event_loop", PAStatus::ConnectToPulseAudio); 67 | 68 | loop { 69 | let res = external_rx.next(); 70 | let finished = pa_finished_rx.next(); 71 | let actions = internal_actions_rx.next(); 72 | let info = info_rx.next(); 73 | let timeout = interval.next(); 74 | 75 | tokio::select! { 76 | r = res => { 77 | if let Some(cmd) = r { 78 | if cmd.is::() { 79 | if let Some(cmd) = cmd.downcast_ref::() { 80 | internal_sx.send(PAInternal::Command(Box::new(cmd.clone())))?; 81 | } 82 | continue; 83 | } 84 | if cmd.downcast_ref::().is_some() { 85 | internal_sx.send(PAInternal::Command(Box::new(PulseAudioAction::Shutdown)))?; 86 | sync_pa.await.unwrap(); 87 | return Ok(()); 88 | } 89 | } 90 | } 91 | _ = finished => { 92 | break; 93 | } 94 | i = actions => { 95 | if let Some(action) = i { 96 | ctx.send_to("event_loop", action); 97 | } 98 | } 99 | i = info => { 100 | if let Some(ident) = i { 101 | send(&internal_sx, PAInternal::AskInfo(ident))?; 102 | } 103 | } 104 | _ = timeout => { 105 | send(&internal_sx, PAInternal::Tick)?; 106 | } 107 | }; 108 | } 109 | ctx.send_to("event_loop", PAStatus::PulseAudioDisconnected); 110 | for i in 0..retry_time { 111 | ctx.send_to("event_loop", PAStatus::RetryIn(retry_time - i)); 112 | 113 | let timeout_part = tokio::time::sleep(std::time::Duration::from_secs(1)); 114 | let event = external_rx.next(); 115 | tokio::select! { 116 | _ = timeout_part => {}, 117 | ev = event => { 118 | if let Some(x) = ev { 119 | if x.is::() { 120 | return Ok(()); 121 | } 122 | } 123 | } 124 | }; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/cli_options.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use gumdrop::Options; 3 | use log::LevelFilter; 4 | 5 | #[derive(Debug, Options)] 6 | pub struct CliOptions { 7 | #[options(help = "filepath to log to (if left empty the program doesn't log)")] 8 | log_file: Option, 9 | 10 | #[options(count, help = "verbosity. Once - info, twice - debug")] 11 | verbose: usize, 12 | 13 | #[options(help = "show this text")] 14 | help: bool, 15 | } 16 | 17 | impl CliOptions { 18 | pub fn check() -> Result<()> { 19 | let opts = CliOptions::parse_args_default_or_exit(); 20 | 21 | if opts.help { 22 | println!("{}", CliOptions::usage()); 23 | return Ok(()); 24 | } 25 | 26 | if let Some(file) = opts.log_file { 27 | let lvl = match opts.verbose { 28 | 2 => LevelFilter::Debug, 29 | 1 => LevelFilter::Info, 30 | _ => LevelFilter::Warn, 31 | }; 32 | simple_logging::log_to_file(file, lvl).unwrap(); 33 | } 34 | 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/config/actions.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use crate::{ 4 | config::ConfigError, 5 | models::{PageType, UserAction}, 6 | }; 7 | 8 | impl ToString for UserAction { 9 | fn to_string(&self) -> String { 10 | match self { 11 | UserAction::RequestQuit => "exit".to_string(), 12 | UserAction::RequestMute(_) => "mute".to_string(), 13 | UserAction::ChangePage(PageType::Output) => "show_output".to_string(), 14 | UserAction::ChangePage(PageType::Input) => "show_input".to_string(), 15 | UserAction::ChangePage(PageType::Cards) => "show_cards".to_string(), 16 | UserAction::OpenContextMenu(_) => "context_menu".to_string(), 17 | UserAction::ShowHelp => "help".to_string(), 18 | UserAction::RequstChangeVolume(num, _) => { 19 | if *num < 0 { 20 | format!("lower_volume({})", num) 21 | } else { 22 | format!("raise_volume({})", num) 23 | } 24 | } 25 | UserAction::MoveUp(num) => format!("up({})", num), 26 | UserAction::MoveDown(num) => format!("down({})", num), 27 | UserAction::MoveLeft => "left".to_string(), 28 | UserAction::MoveRight => "right".to_string(), 29 | UserAction::CyclePages(x) => { 30 | if *x > 0 { 31 | "cycle_pages_forward".to_string() 32 | } else { 33 | "cycle_pages_backward".to_string() 34 | } 35 | } 36 | UserAction::CloseContextMenu => "close_context_menu".to_string(), 37 | UserAction::Confirm => "confirm".to_string(), 38 | UserAction::Hide(_) => "hide".to_string(), 39 | UserAction::InputVolumeValue => "input_volume_value".to_string(), 40 | UserAction::ChangeVolumeInputValue(_, _) | UserAction::SetSelected(_) => { 41 | "unsupported".to_string() 42 | } 43 | } 44 | } 45 | } 46 | 47 | impl TryFrom for UserAction { 48 | type Error = ConfigError; 49 | 50 | fn try_from(st: String) -> Result { 51 | let mut s = &st[..]; 52 | let mut a = String::new(); 53 | 54 | if let Some(lparen) = st.chars().position(|c| c == '(') { 55 | let rparen = match st.chars().position(|c| c == ')') { 56 | Some(r) => r, 57 | None => { 58 | return Err(ConfigError::ActionBindingError(st.clone())); 59 | } 60 | }; 61 | a = st 62 | .chars() 63 | .skip(lparen + 1) 64 | .take(rparen - lparen - 1) 65 | .collect(); 66 | s = &st[0..lparen]; 67 | } 68 | 69 | let x = match s { 70 | "exit" => UserAction::RequestQuit, 71 | "mute" => UserAction::RequestMute(None), 72 | "show_output" => UserAction::ChangePage(PageType::Output), 73 | "show_input" => UserAction::ChangePage(PageType::Input), 74 | "show_cards" => UserAction::ChangePage(PageType::Cards), 75 | "context_menu" => UserAction::OpenContextMenu(None), 76 | "help" => UserAction::ShowHelp, 77 | "lower_volume" => { 78 | let a = match a.parse::() { 79 | Ok(x) => x, 80 | Err(_) => { 81 | return Err(ConfigError::ActionBindingError(st.clone())); 82 | } 83 | }; 84 | UserAction::RequstChangeVolume(-a, None) 85 | } 86 | "raise_volume" => { 87 | let a = match a.parse::() { 88 | Ok(x) => x, 89 | Err(_) => { 90 | return Err(ConfigError::ActionBindingError(st.clone())); 91 | } 92 | }; 93 | UserAction::RequstChangeVolume(a, None) 94 | } 95 | "up" => { 96 | let a = match a.parse::() { 97 | Ok(x) => x, 98 | Err(_) => { 99 | return Err(ConfigError::ActionBindingError(st.clone())); 100 | } 101 | }; 102 | UserAction::MoveUp(a) 103 | } 104 | "down" => { 105 | let a = match a.parse::() { 106 | Ok(x) => x, 107 | Err(_) => { 108 | return Err(ConfigError::ActionBindingError(st.clone())); 109 | } 110 | }; 111 | UserAction::MoveDown(a) 112 | } 113 | "left" => UserAction::MoveLeft, 114 | "right" => UserAction::MoveRight, 115 | "cycle_pages_forward" => UserAction::CyclePages(1), 116 | "cycle_pages_backward" => UserAction::CyclePages(-1), 117 | "input_volume_value" => UserAction::InputVolumeValue, 118 | "close_context_menu" => UserAction::CloseContextMenu, 119 | "confirm" => UserAction::Confirm, 120 | "hide" => UserAction::Hide(None), 121 | _ => { 122 | return Err(ConfigError::ActionBindingError(st.clone())); 123 | } 124 | }; 125 | Ok(x) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/config/colors.rs: -------------------------------------------------------------------------------- 1 | use crossterm::style::Color; 2 | 3 | pub fn str_to_color(s: &str) -> Option { 4 | if s.chars().take(1).collect::() == "#" && s.len() == 7 { 5 | Some(Color::Rgb { 6 | r: u8::from_str_radix(&s[1..3], 16).expect("error in config"), 7 | g: u8::from_str_radix(&s[3..5], 16).expect("error in config"), 8 | b: u8::from_str_radix(&s[5..7], 16).expect("error in config"), 9 | }) 10 | } else { 11 | match &s[..].parse::() { 12 | Ok(c) => Some(*c), 13 | Err(_) => None, 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/config/default.rs: -------------------------------------------------------------------------------- 1 | use super::{ConfigColor, RsMixerConfig}; 2 | use linked_hash_map::LinkedHashMap; 3 | use crate::{VERSION, multimap::MultiMap}; 4 | 5 | impl std::default::Default for RsMixerConfig { 6 | fn default() -> Self { 7 | let mut bindings = MultiMap::new(); 8 | bindings.insert("q".to_string(), "exit".to_string()); 9 | bindings.insert("ctrl+c".to_string(), "exit".to_string()); 10 | bindings.insert("?".to_string(), "help".to_string()); 11 | 12 | bindings.insert("j".to_string(), "down(1)".to_string()); 13 | bindings.insert("k".to_string(), "up(1)".to_string()); 14 | bindings.insert("h".to_string(), "left".to_string()); 15 | bindings.insert("l".to_string(), "right".to_string()); 16 | bindings.insert("down".to_string(), "down(1)".to_string()); 17 | bindings.insert("up".to_string(), "up(1)".to_string()); 18 | 19 | bindings.insert("left".to_string(), "lower_volume(1)".to_string()); 20 | bindings.insert("right".to_string(), "raise_volume(1)".to_string()); 21 | bindings.insert("h".to_string(), "lower_volume(5)".to_string()); 22 | bindings.insert("l".to_string(), "raise_volume(5)".to_string()); 23 | bindings.insert("shift+h".to_string(), "lower_volume(15)".to_string()); 24 | bindings.insert("shift+l".to_string(), "raise_volume(15)".to_string()); 25 | bindings.insert("scroll_down".to_string(), "lower_volume(5)".to_string()); 26 | bindings.insert("scroll_up".to_string(), "raise_volume(5)".to_string()); 27 | 28 | bindings.insert("m".to_string(), "mute".to_string()); 29 | bindings.insert("mouse_middle".to_string(), "mute".to_string()); 30 | bindings.insert("mouse_right".to_string(), "mute".to_string()); 31 | 32 | bindings.insert("1".to_string(), "show_output".to_string()); 33 | bindings.insert("2".to_string(), "show_input".to_string()); 34 | bindings.insert("3".to_string(), "show_cards".to_string()); 35 | bindings.insert("F1".to_string(), "show_output".to_string()); 36 | bindings.insert("F2".to_string(), "show_input".to_string()); 37 | bindings.insert("F3".to_string(), "show_cards".to_string()); 38 | bindings.insert("tab".to_string(), "cycle_pages_forward".to_string()); 39 | bindings.insert("shift+tab".to_string(), "cycle_pages_backward".to_string()); 40 | 41 | bindings.insert("enter".to_string(), "context_menu".to_string()); 42 | bindings.insert("enter".to_string(), "confirm".to_string()); 43 | bindings.insert("esc".to_string(), "close_context_menu".to_string()); 44 | bindings.insert("q".to_string(), "close_context_menu".to_string()); 45 | 46 | let mut c = LinkedHashMap::new(); 47 | c.insert( 48 | "normal".to_string(), 49 | ConfigColor { 50 | fg: Some("white".to_string()), 51 | bg: None, 52 | attributes: None, 53 | }, 54 | ); 55 | c.insert( 56 | "bold".to_string(), 57 | ConfigColor { 58 | fg: Some("white".to_string()), 59 | bg: None, 60 | attributes: Some(vec!["bold".to_string()]), 61 | }, 62 | ); 63 | c.insert( 64 | "inverted".to_string(), 65 | ConfigColor { 66 | fg: Some("black".to_string()), 67 | bg: Some("white".to_string()), 68 | attributes: None, 69 | }, 70 | ); 71 | c.insert( 72 | "muted".to_string(), 73 | ConfigColor { 74 | fg: Some("grey".to_string()), 75 | bg: None, 76 | attributes: None, 77 | }, 78 | ); 79 | c.insert( 80 | "red".to_string(), 81 | ConfigColor { 82 | fg: Some("red".to_string()), 83 | bg: None, 84 | attributes: None, 85 | }, 86 | ); 87 | c.insert( 88 | "orange".to_string(), 89 | ConfigColor { 90 | fg: Some("yellow".to_string()), 91 | bg: None, 92 | attributes: None, 93 | }, 94 | ); 95 | c.insert( 96 | "green".to_string(), 97 | ConfigColor { 98 | fg: Some("green".to_string()), 99 | bg: None, 100 | attributes: None, 101 | }, 102 | ); 103 | 104 | Self { 105 | version: Some(String::from(VERSION)), 106 | pulse_audio: None, 107 | bindings, 108 | colors: c, 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/config/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum ConfigError { 5 | #[error("Config file is incorrect")] 6 | ConfyError(#[from] confy::ConfyError), 7 | #[error("'{0}' is not a valid key binding")] 8 | KeyCodeError(String), 9 | #[error("'{0}' is not a valid key action")] 10 | ActionBindingError(String), 11 | #[error("'{0}' is not a valid key color")] 12 | InvalidColor(String), 13 | #[error("'{0}' is not a valid key version code")] 14 | InvalidVersion(String), 15 | } 16 | -------------------------------------------------------------------------------- /src/config/keys_mouse.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, KeyModifiers, MouseButton, MouseEventKind}; 2 | 3 | use crate::{config::ConfigError, models::InputEvent}; 4 | 5 | pub fn try_string_to_event(key: &str) -> Result { 6 | let s = String::from(key).to_lowercase(); 7 | let mut modifiers = KeyModifiers::empty(); 8 | 9 | let parts = s.split('+').collect::>(); 10 | 11 | for &p in parts.iter().take(parts.len() - 1) { 12 | match p { 13 | "shift" => modifiers |= KeyModifiers::SHIFT, 14 | "ctrl" => modifiers |= KeyModifiers::CONTROL, 15 | "alt" => modifiers |= KeyModifiers::ALT, 16 | _ => return Err(ConfigError::KeyCodeError(String::from(key))), 17 | }; 18 | } 19 | 20 | let code = *parts.last().unwrap(); 21 | 22 | if let Ok(kind) = try_string_to_mouseevent(code) { 23 | Ok(InputEvent::mouse(kind, modifiers)) 24 | } else { 25 | try_string_to_keyevent(key, code, modifiers) 26 | } 27 | } 28 | 29 | pub fn try_string_to_mouseevent(code: &str) -> Result { 30 | match code { 31 | "scroll_down" => Ok(MouseEventKind::ScrollDown), 32 | "scroll_up" => Ok(MouseEventKind::ScrollUp), 33 | "mouse_right" => Ok(MouseEventKind::Up(MouseButton::Right)), 34 | "mouse_middle" => Ok(MouseEventKind::Up(MouseButton::Middle)), 35 | _ => Err(ConfigError::KeyCodeError(code.to_string())), 36 | } 37 | } 38 | 39 | pub fn try_string_to_keyevent( 40 | key: &str, 41 | code: &str, 42 | mut modifiers: KeyModifiers, 43 | ) -> Result { 44 | let code = match code { 45 | "backspace" => KeyCode::Backspace, 46 | "enter" => KeyCode::Enter, 47 | "left" => KeyCode::Left, 48 | "right" => KeyCode::Right, 49 | "up" => KeyCode::Up, 50 | "down" => KeyCode::Down, 51 | "home" => KeyCode::Home, 52 | "end" => KeyCode::End, 53 | "pageup" => KeyCode::PageUp, 54 | "pagedown" => KeyCode::PageDown, 55 | "tab" => { 56 | if modifiers.contains(KeyModifiers::SHIFT) { 57 | modifiers = !modifiers ^ !KeyModifiers::SHIFT; 58 | KeyCode::BackTab 59 | } else { 60 | KeyCode::Tab 61 | } 62 | } 63 | "backtab" => KeyCode::BackTab, 64 | "delete" => KeyCode::Delete, 65 | "insert" => KeyCode::Insert, 66 | "null" => KeyCode::Null, 67 | "esc" => KeyCode::Esc, 68 | _ => match code.len() { 69 | 1 => { 70 | let big_c = code.to_uppercase().chars().next().unwrap(); 71 | let c = code.chars().next().unwrap(); 72 | if modifiers.contains(KeyModifiers::SHIFT) 73 | && KeyCode::Char(c) != KeyCode::Char(big_c) 74 | { 75 | KeyCode::Char(big_c) 76 | } else { 77 | KeyCode::Char(c) 78 | } 79 | } 80 | 2 => { 81 | if let Ok(f) = code[1..code.len()].parse::() { 82 | KeyCode::F(f) 83 | } else { 84 | return Err(ConfigError::KeyCodeError(String::from(key))); 85 | } 86 | } 87 | _ => return Err(ConfigError::KeyCodeError(String::from(key))), 88 | }, 89 | }; 90 | 91 | Ok(InputEvent::key(code, modifiers)) 92 | } 93 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | mod actions; 2 | mod colors; 3 | mod default; 4 | mod errors; 5 | pub mod keys_mouse; 6 | mod variables; 7 | 8 | use std::{collections::HashMap, convert::TryFrom}; 9 | 10 | use crossterm::style::{Attribute, ContentStyle}; 11 | pub use errors::ConfigError; 12 | use linked_hash_map::LinkedHashMap; 13 | use semver::Version; 14 | use serde::{Deserialize, Serialize}; 15 | pub use variables::Variables; 16 | 17 | use crate::{ 18 | models::{InputEvent, UserAction}, 19 | multimap::MultiMap, 20 | prelude::*, 21 | Styles, VERSION, 22 | }; 23 | 24 | #[derive(Serialize, Deserialize, Clone)] 25 | pub struct RsMixerConfig { 26 | version: Option, 27 | pulse_audio: Option, 28 | bindings: MultiMap, 29 | colors: LinkedHashMap, 30 | } 31 | 32 | #[derive(Serialize, Deserialize, Clone, Default)] 33 | pub struct PulseAudio { 34 | disable_live_volume: Option, 35 | retry_time: Option, 36 | rate: Option, 37 | frag_size: Option, 38 | } 39 | 40 | impl PulseAudio { 41 | pub fn disable_live_volume(&self) -> bool { 42 | self.disable_live_volume.unwrap_or(false) 43 | } 44 | pub fn retry_time(&self) -> u64 { 45 | self.retry_time.unwrap_or(5) 46 | } 47 | pub fn rate(&self) -> u32 { 48 | self.rate.unwrap_or(20) 49 | } 50 | pub fn frag_size(&self) -> u32 { 51 | self.frag_size.unwrap_or(48) 52 | } 53 | } 54 | 55 | #[derive(Serialize, Deserialize, Clone)] 56 | pub struct ConfigColor { 57 | fg: Option, 58 | bg: Option, 59 | attributes: Option>, 60 | } 61 | 62 | impl RsMixerConfig { 63 | pub fn load() -> Result { 64 | let config: RsMixerConfig = confy::load("rsmixer")?; 65 | Ok(config) 66 | } 67 | 68 | pub fn interpret(&mut self) -> Result<(Styles, MultiMap, Variables)> { 69 | self.compatibility_layer()?; 70 | 71 | let bindings = self.bindings()?; 72 | 73 | let mut styles: Styles = HashMap::new(); 74 | 75 | for (k, v) in &self.colors { 76 | let mut c = ContentStyle::new(); 77 | 78 | if let Some(q) = &v.fg { 79 | if let Some(color) = colors::str_to_color(q) { 80 | c = c.foreground(color); 81 | } else { 82 | return Err(ConfigError::InvalidColor(q.clone())) 83 | .context("while parsing config file"); 84 | } 85 | } 86 | if let Some(q) = &v.bg { 87 | if let Some(color) = colors::str_to_color(q) { 88 | c = c.background(color); 89 | } else { 90 | return Err(ConfigError::InvalidColor(q.clone())) 91 | .context("while parsing config file"); 92 | } 93 | } 94 | if let Some(attrs) = &v.attributes { 95 | for attr in attrs { 96 | match &attr[..] { 97 | "bold" => { 98 | c = c.attribute(Attribute::Bold); 99 | } 100 | "underlined" => { 101 | c = c.attribute(Attribute::Underlined); 102 | } 103 | "italic" => { 104 | c = c.attribute(Attribute::Italic); 105 | } 106 | "dim" => { 107 | c = c.attribute(Attribute::Dim); 108 | } 109 | _ => {} 110 | }; 111 | } 112 | } 113 | styles.insert(k.into(), c); 114 | } 115 | 116 | self.version = Some(String::from(VERSION)); 117 | 118 | confy::store("rsmixer", self.clone())?; 119 | 120 | Ok((styles, bindings, Variables::new(self))) 121 | } 122 | 123 | fn bindings(&self) -> Result> { 124 | let mut bindings: MultiMap = MultiMap::new(); 125 | 126 | for (k, cs) in self.bindings.iter_vecs() { 127 | for c in cs { 128 | bindings.insert( 129 | keys_mouse::try_string_to_event(&k)?, 130 | UserAction::try_from(c.clone())?, 131 | ); 132 | } 133 | } 134 | 135 | Ok(bindings) 136 | } 137 | 138 | fn compatibility_layer(&mut self) -> Result<()> { 139 | let current_ver = Version::parse(VERSION)?; 140 | 141 | let config_ver = match &self.version { 142 | Some(v) => v.clone(), 143 | None => "0.0.0".to_string(), 144 | }; 145 | let config_ver = Version::parse(&config_ver)?; 146 | 147 | if config_ver >= current_ver { 148 | return Ok(()); 149 | } 150 | 151 | if config_ver == Version::parse("0.3.0").unwrap() { 152 | if let Some(c) = self.colors.get(&"normal".to_string()) { 153 | let mut c = c.clone(); 154 | c.attributes = Some(vec!["bold".to_string()]); 155 | self.colors.insert("bold".to_string(), c); 156 | } 157 | return Ok(()); 158 | } 159 | 160 | let mut parsed: MultiMap = MultiMap::new(); 161 | 162 | for (k, cs) in self.bindings.iter_vecs() { 163 | for c in cs { 164 | parsed.insert( 165 | keys_mouse::try_string_to_event(&k)?, 166 | (UserAction::try_from(c.clone())?, k.clone()), 167 | ); 168 | } 169 | } 170 | 171 | if parsed 172 | .iter() 173 | .find(|(_, v)| (**v).0 == UserAction::Confirm) 174 | .is_none() 175 | { 176 | if let Some((_, (_, k))) = parsed 177 | .iter() 178 | .find(|(_, v)| (**v).0 == UserAction::OpenContextMenu(None)) 179 | { 180 | self.bindings 181 | .insert(k.clone(), UserAction::Confirm.to_string()); 182 | } 183 | } 184 | 185 | Ok(()) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/config/variables.rs: -------------------------------------------------------------------------------- 1 | use super::{PulseAudio, RsMixerConfig}; 2 | 3 | pub struct Variables { 4 | pub pa_retry_time: u64, 5 | pub pa_disable_live_volume: bool, 6 | pub pa_rate: u32, 7 | pub pa_frag_size: u32, 8 | } 9 | 10 | impl Variables { 11 | pub fn new(config: &RsMixerConfig) -> Self { 12 | let def = PulseAudio::default(); 13 | let pulse = match &config.pulse_audio { 14 | Some(p) => p, 15 | None => &def, 16 | }; 17 | 18 | Self { 19 | pa_retry_time: pulse.retry_time(), 20 | pa_rate: pulse.rate(), 21 | pa_frag_size: pulse.frag_size(), 22 | pa_disable_live_volume: pulse.disable_live_volume(), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/help.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, mem::discriminant}; 2 | 3 | use crate::{ 4 | models::{PageType, UserAction}, 5 | repeat, BINDINGS, 6 | }; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct HelpLine { 10 | pub key_events: Vec, 11 | pub category: String, 12 | } 13 | 14 | impl HelpLine { 15 | pub fn lines_needed(&self, mut w: u16) -> u16 { 16 | let mut i = 0; 17 | let mut j = 0; 18 | w -= self.category.len() as u16; 19 | self.key_events.iter().for_each(|ke| { 20 | if i + ke.len() as u16 + 1 > w { 21 | i = (ke.len() + 1) as u16; 22 | j += 1; 23 | } else { 24 | i += (ke.len() + 1) as u16; 25 | } 26 | }); 27 | 28 | j + 1 29 | } 30 | 31 | pub fn as_lines(&self, mut w: u16) -> Vec { 32 | let mut cur = "".to_string(); 33 | let mut v = Vec::new(); 34 | w -= self.category.len() as u16; 35 | self.key_events.iter().for_each(|ke| { 36 | if cur.len() + ke.len() + 1 > w as usize { 37 | v.push(cur.clone()); 38 | cur = format!("{} ", ke); 39 | } else { 40 | cur = format!("{}{} ", cur, ke); 41 | } 42 | }); 43 | 44 | v.push(cur); 45 | 46 | if !v.is_empty() { 47 | v[0] = format!( 48 | "{}{}{}", 49 | v[0], 50 | repeat!(" ", w as usize - v[0].len()), 51 | self.category 52 | ); 53 | } 54 | v 55 | } 56 | } 57 | 58 | pub enum ActionMatcher { 59 | Any(UserAction), 60 | Concrete(UserAction), 61 | } 62 | 63 | impl ActionMatcher { 64 | fn is_matching(&self, l2: &UserAction) -> bool { 65 | match self { 66 | Self::Any(l1) => discriminant(l1) == discriminant(l2), 67 | Self::Concrete(l1) => l1 == l2, 68 | } 69 | } 70 | } 71 | 72 | pub fn generate() -> Vec { 73 | let mut categories = Vec::new(); 74 | 75 | let mut volume_deltas = HashSet::new(); 76 | 77 | for (_, v) in (*BINDINGS).get().iter() { 78 | if let UserAction::RequstChangeVolume(x, _) = v { 79 | volume_deltas.insert(x.abs()); 80 | } 81 | } 82 | 83 | categories.push(( 84 | "Navigation".to_string(), 85 | vec![ 86 | ActionMatcher::Any(UserAction::MoveUp(0)), 87 | ActionMatcher::Any(UserAction::MoveDown(0)), 88 | ], 89 | )); 90 | 91 | for vd in volume_deltas { 92 | categories.push(( 93 | format!("Change volume by {}", vd), 94 | vec![ 95 | ActionMatcher::Concrete(UserAction::RequstChangeVolume(vd, None)), 96 | ActionMatcher::Concrete(UserAction::RequstChangeVolume(-vd, None)), 97 | ], 98 | )) 99 | } 100 | categories.push(( 101 | "Mute/unmute".to_string(), 102 | vec![ActionMatcher::Concrete(UserAction::RequestMute(None))], 103 | )); 104 | categories.push(( 105 | "Change page".to_string(), 106 | vec![ActionMatcher::Any(UserAction::ChangePage(PageType::Output))], 107 | )); 108 | categories.push(( 109 | "Cycle pages".to_string(), 110 | vec![ActionMatcher::Any(UserAction::CyclePages(0))], 111 | )); 112 | categories.push(( 113 | "Context menu".to_string(), 114 | vec![ActionMatcher::Any(UserAction::OpenContextMenu(None))], 115 | )); 116 | categories.push(( 117 | "Quit".to_string(), 118 | vec![ActionMatcher::Any(UserAction::RequestQuit)], 119 | )); 120 | 121 | let mut help_lines = Vec::new(); 122 | 123 | for category in categories { 124 | let mut hl = HelpLine { 125 | key_events: Vec::new(), 126 | category: category.0, 127 | }; 128 | for (k, v) in (*BINDINGS).get().iter() { 129 | for matcher in &category.1 { 130 | if matcher.is_matching(v) { 131 | hl.key_events.push(k.to_string()); 132 | } 133 | } 134 | } 135 | help_lines.push(hl); 136 | } 137 | 138 | help_lines 139 | } 140 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::upper_case_acronyms)] 2 | 3 | extern crate crossbeam_channel as cb_channel; 4 | extern crate libpulse_binding as pulse; 5 | 6 | static LOGGING_MODULE: &str = "Main"; 7 | 8 | mod action_handlers; 9 | mod actor_system; 10 | mod actors; 11 | mod cli_options; 12 | mod config; 13 | mod help; 14 | mod models; 15 | mod multimap; 16 | mod pa; 17 | mod prelude; 18 | mod ui; 19 | mod util; 20 | 21 | use std::collections::HashMap; 22 | 23 | use actors::*; 24 | use cli_options::CliOptions; 25 | use config::{RsMixerConfig, Variables}; 26 | use crossterm::style::ContentStyle; 27 | use lazy_static::lazy_static; 28 | use models::{entry, InputEvent, Style, UserAction}; 29 | use multimap::MultiMap; 30 | use prelude::*; 31 | use state::Storage; 32 | use tokio::runtime; 33 | 34 | lazy_static! { 35 | pub static ref STYLES: Storage = Storage::new(); 36 | pub static ref VARIABLES: Storage = Storage::new(); 37 | pub static ref BINDINGS: Storage> = Storage::new(); 38 | } 39 | 40 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 41 | 42 | pub type Styles = HashMap; 43 | 44 | fn load_config_and_options() -> Result<()> { 45 | info!("Checking command line options and config"); 46 | 47 | CliOptions::check()?; 48 | debug!("CLI options checked"); 49 | 50 | let mut config = RsMixerConfig::load()?; 51 | let (styles, bindings, variables) = config.interpret()?; 52 | 53 | STYLES.set(styles); 54 | BINDINGS.set(bindings); 55 | VARIABLES.set(variables); 56 | debug!("Config loaded"); 57 | 58 | Ok(()) 59 | } 60 | 61 | async fn run() -> Result<()> { 62 | load_config_and_options()?; 63 | 64 | debug!("Starting actor system"); 65 | let (mut context, worker) = actor_system::new(); 66 | 67 | let actor_system_handle = worker.start(); 68 | 69 | EventLoopActor::item().register_and_start(&mut context); 70 | PulseActor::item().register_and_start(&mut context); 71 | InputActor::item().register_and_start(&mut context); 72 | 73 | debug!("Actor system started"); 74 | actor_system_handle.await? 75 | } 76 | 77 | fn main() -> Result<()> { 78 | info!("Starting RsMixer"); 79 | 80 | let threaded_rt = runtime::Builder::new_multi_thread().enable_time().build()?; 81 | threaded_rt.block_on(async { 82 | debug!("Tokio runtime started"); 83 | 84 | if let Err(e) = run().await { 85 | println!("{:#?}", e); 86 | } 87 | }); 88 | 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /src/models/actions.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crossterm::event::Event; 4 | use pulse::volume::ChannelVolumes; 5 | 6 | use crate::{ 7 | entry::{Entry, EntryIdentifier}, 8 | models::PageType, 9 | }; 10 | 11 | #[derive(Clone, PartialEq, Debug)] 12 | pub enum PAStatus { 13 | // PulseAudio connection status 14 | RetryIn(u64), 15 | ConnectToPulseAudio, 16 | PulseAudioDisconnected, 17 | } 18 | 19 | // redraw the whole screen (called every window resize) 20 | #[derive(Clone, PartialEq, Debug)] 21 | pub struct ResizeScreen {} 22 | impl ResizeScreen { 23 | pub fn new() -> Self { 24 | Self {} 25 | } 26 | } 27 | 28 | #[derive(Clone, PartialEq, Debug)] 29 | pub enum EntryUpdate { 30 | // entry updates 31 | EntryRemoved(EntryIdentifier), 32 | EntryUpdate(EntryIdentifier, Box), 33 | PeakVolumeUpdate(EntryIdentifier, f32), 34 | } 35 | 36 | #[derive(Clone, PartialEq, Debug)] 37 | pub struct UserInput { 38 | pub event: Event, 39 | } 40 | impl UserInput { 41 | pub fn new(event: Event) -> Self { 42 | Self { event } 43 | } 44 | } 45 | 46 | #[derive(Clone, PartialEq, Debug)] 47 | pub enum UserAction { 48 | // move around the UI 49 | MoveUp(u16), 50 | MoveDown(u16), 51 | MoveLeft, 52 | MoveRight, 53 | SetSelected(usize), 54 | ChangePage(PageType), 55 | // positive - forwards, negative - backwards 56 | CyclePages(i8), 57 | 58 | // volume changes 59 | RequestMute(Option), 60 | 61 | // request volume change where the argument is a 62 | // number of percentage points it should be changed by 63 | RequstChangeVolume(i16, Option), 64 | 65 | InputVolumeValue, 66 | ChangeVolumeInputValue(String, u8), 67 | 68 | // context menus 69 | OpenContextMenu(Option), 70 | CloseContextMenu, 71 | Confirm, 72 | 73 | ShowHelp, 74 | 75 | Hide(Option), 76 | 77 | RequestQuit, 78 | } 79 | 80 | #[derive(Clone, PartialEq, Debug)] 81 | pub enum PulseAudioAction { 82 | RequestPulseAudioState, 83 | MuteEntry(EntryIdentifier, bool), 84 | MoveEntryToParent(EntryIdentifier, EntryIdentifier), 85 | ChangeCardProfile(EntryIdentifier, String), 86 | SetVolume(EntryIdentifier, ChannelVolumes), 87 | CreateMonitors(HashMap>), 88 | SetSuspend(EntryIdentifier, bool), 89 | KillEntry(EntryIdentifier), 90 | Shutdown, 91 | } 92 | -------------------------------------------------------------------------------- /src/models/context_menus.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | actor_system::Ctx, 3 | entry::{Entry, EntryIdentifier, EntryKind, EntryType}, 4 | models::{PulseAudioAction, UserAction}, 5 | scrollable, 6 | ui::{widgets::ToolWindowWidget, Rect, Scrollable}, 7 | }; 8 | 9 | #[derive(PartialEq, Clone)] 10 | pub enum ContextMenuOption { 11 | MoveToEntry(EntryIdentifier, String), 12 | ChangeCardProfile(String, String), 13 | Kill, 14 | Move, 15 | Suspend, 16 | Resume, 17 | SetAsDefault, 18 | InputExactVolume, 19 | } 20 | 21 | impl From for String { 22 | fn from(option: ContextMenuOption) -> Self { 23 | match option { 24 | ContextMenuOption::MoveToEntry(_, s) => s, 25 | ContextMenuOption::ChangeCardProfile(_, s) => s, 26 | ContextMenuOption::Kill => "Kill".into(), 27 | ContextMenuOption::Move => "Move".into(), 28 | ContextMenuOption::Suspend => "Suspend".into(), 29 | ContextMenuOption::Resume => "Resume".into(), 30 | ContextMenuOption::SetAsDefault => "Set as default".into(), 31 | ContextMenuOption::InputExactVolume => "Input exact volume value".into(), 32 | } 33 | } 34 | } 35 | 36 | pub enum ContextMenuEffect { 37 | None, 38 | MoveEntry, 39 | } 40 | 41 | scrollable!( 42 | ContextMenu, 43 | fn selected(&self) -> usize { 44 | self.selected 45 | }, 46 | fn len(&self) -> usize { 47 | self.options.len() 48 | }, 49 | fn set_selected(&mut self, selected: usize) -> bool { 50 | if selected < self.options.len() { 51 | self.selected = selected; 52 | true 53 | } else { 54 | false 55 | } 56 | }, 57 | fn element_height(&self, _index: usize) -> u16 { 58 | 1 59 | } 60 | ); 61 | 62 | pub struct ContextMenu { 63 | pub options: Vec, 64 | selected: usize, 65 | pub horizontal_scroll: usize, 66 | pub area: Rect, 67 | pub entry_ident: EntryIdentifier, 68 | pub tool_window: ToolWindowWidget, 69 | } 70 | 71 | impl ContextMenu { 72 | pub fn new(entry: &Entry) -> Self { 73 | let play = match &entry.entry_kind { 74 | EntryKind::PlayEntry(play) => Some(play), 75 | EntryKind::CardEntry(_) => None, 76 | }; 77 | let card = match &entry.entry_kind { 78 | EntryKind::PlayEntry(_) => None, 79 | EntryKind::CardEntry(card) => Some(card), 80 | }; 81 | let options: Vec = match entry.entry_type { 82 | EntryType::Source | EntryType::Sink => vec![ 83 | if play.unwrap().suspended { 84 | ContextMenuOption::Resume 85 | } else { 86 | ContextMenuOption::Suspend 87 | }, 88 | ContextMenuOption::SetAsDefault, 89 | ContextMenuOption::InputExactVolume, 90 | ], 91 | EntryType::SinkInput => vec![ 92 | ContextMenuOption::Move, 93 | ContextMenuOption::Kill, 94 | ContextMenuOption::InputExactVolume, 95 | ], 96 | EntryType::SourceOutput => vec![ContextMenuOption::InputExactVolume], 97 | EntryType::Card => card 98 | .unwrap() 99 | .profiles 100 | .iter() 101 | .map(|p| { 102 | ContextMenuOption::ChangeCardProfile(p.name.clone(), p.description.clone()) 103 | }) 104 | .collect(), 105 | }; 106 | 107 | Self { 108 | options, 109 | selected: 0, 110 | horizontal_scroll: 0, 111 | area: Rect::default(), 112 | tool_window: ToolWindowWidget::default(), 113 | entry_ident: EntryIdentifier::new(entry.entry_type, entry.index), 114 | } 115 | } 116 | 117 | pub fn resolve(&self, ident: EntryIdentifier, ctx: &Ctx) -> ContextMenuEffect { 118 | match &self.options[self.selected] { 119 | ContextMenuOption::Move => { 120 | return ContextMenuEffect::MoveEntry; 121 | } 122 | ContextMenuOption::InputExactVolume => { 123 | ctx.send_to("event_loop", UserAction::InputVolumeValue); 124 | } 125 | ContextMenuOption::MoveToEntry(entry, _) => { 126 | ctx.send_to( 127 | "pulseaudio", 128 | PulseAudioAction::MoveEntryToParent(ident, *entry), 129 | ); 130 | } 131 | ContextMenuOption::ChangeCardProfile(name, _) => { 132 | ctx.send_to( 133 | "pulseaudio", 134 | PulseAudioAction::ChangeCardProfile(ident, name.clone()), 135 | ); 136 | } 137 | ContextMenuOption::Suspend => { 138 | ctx.send_to("pulseaudio", PulseAudioAction::SetSuspend(ident, true)); 139 | } 140 | ContextMenuOption::Resume => { 141 | ctx.send_to("pulseaudio", PulseAudioAction::SetSuspend(ident, false)); 142 | } 143 | ContextMenuOption::Kill => { 144 | ctx.send_to("pulseaudio", PulseAudioAction::KillEntry(ident)); 145 | } 146 | _ => {} 147 | }; 148 | 149 | ContextMenuEffect::None 150 | } 151 | 152 | pub fn max_horizontal_scroll(&self) -> usize { 153 | let (start, end) = self.visible_start_end(self.area.height); 154 | let longest = self 155 | .options 156 | .iter() 157 | .skip(start) 158 | .take(end - start) 159 | .map(|o| String::from(o.clone()).len()) 160 | .max(); 161 | 162 | match longest { 163 | None => 0, 164 | Some(l) => l / self.area.width as usize, 165 | } 166 | } 167 | } 168 | impl Default for ContextMenu { 169 | fn default() -> Self { 170 | Self { 171 | options: Vec::new(), 172 | selected: 0, 173 | horizontal_scroll: 0, 174 | area: Rect::default(), 175 | tool_window: ToolWindowWidget::default(), 176 | entry_ident: EntryIdentifier::new(EntryType::Sink, 0), 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/models/entry/card_entry.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::Rect; 2 | 3 | #[derive(PartialEq, Clone, Debug)] 4 | pub struct CardProfile { 5 | pub name: String, 6 | pub description: String, 7 | #[cfg(any(feature = "pa_v13"))] 8 | pub available: bool, 9 | pub area: Rect, 10 | pub is_selected: bool, 11 | } 12 | impl Eq for CardProfile {} 13 | 14 | #[derive(PartialEq, Clone, Debug)] 15 | pub struct CardEntry { 16 | pub profiles: Vec, 17 | pub selected_profile: Option, 18 | pub area: Rect, 19 | pub is_selected: bool, 20 | pub name: String, 21 | } 22 | impl Eq for CardEntry {} 23 | -------------------------------------------------------------------------------- /src/models/entry/entries.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use super::{CardEntry, Entry, EntryIdentifier, EntryType, PlayEntry}; 4 | 5 | pub struct Entries(BTreeMap); 6 | 7 | impl Default for Entries { 8 | fn default() -> Self { 9 | Self(BTreeMap::new()) 10 | } 11 | } 12 | 13 | impl Entries { 14 | pub fn iter_type<'a>( 15 | &'a self, 16 | entry_type: EntryType, 17 | ) -> Box + 'a> { 18 | Box::new( 19 | self.0 20 | .iter() 21 | .filter(move |(ident, _)| ident.entry_type == entry_type), 22 | ) 23 | } 24 | pub fn iter_type_mut<'a>( 25 | &'a mut self, 26 | entry_type: EntryType, 27 | ) -> Box + 'a> { 28 | Box::new( 29 | self.0 30 | .iter_mut() 31 | .filter(move |(ident, _)| ident.entry_type == entry_type), 32 | ) 33 | } 34 | pub fn get(&self, entry_ident: &EntryIdentifier) -> Option<&Entry> { 35 | self.0.get(entry_ident) 36 | } 37 | pub fn get_mut(&mut self, ident: &EntryIdentifier) -> Option<&mut Entry> { 38 | self.0.get_mut(ident) 39 | } 40 | pub fn get_card_entry(&self, entry_ident: &EntryIdentifier) -> Option<&CardEntry> { 41 | self.0 42 | .get(entry_ident) 43 | .and_then(|e| e.entry_kind.card_entry()) 44 | } 45 | pub fn get_play_entry(&self, entry_ident: &EntryIdentifier) -> Option<&PlayEntry> { 46 | self.0 47 | .get(entry_ident) 48 | .and_then(|e| e.entry_kind.play_entry()) 49 | } 50 | pub fn get_play_entry_mut(&mut self, entry_ident: &EntryIdentifier) -> Option<&mut PlayEntry> { 51 | self.0 52 | .get_mut(entry_ident) 53 | .and_then(|e| e.entry_kind.play_entry_mut()) 54 | } 55 | pub fn position

(&mut self, predicate: P) -> Option 56 | where 57 | P: FnMut((&EntryIdentifier, &Entry)) -> bool, 58 | { 59 | self.0.iter().position(predicate) 60 | } 61 | pub fn find

(&mut self, predicate: P) -> Option<(&EntryIdentifier, &Entry)> 62 | where 63 | P: FnMut(&(&EntryIdentifier, &Entry)) -> bool, 64 | { 65 | self.0.iter().find(predicate) 66 | } 67 | pub fn remove(&mut self, ident: &EntryIdentifier) -> Option { 68 | self.0.remove(ident) 69 | } 70 | pub fn insert(&mut self, entry_ident: EntryIdentifier, val: Entry) -> Option { 71 | self.0.insert(entry_ident, val) 72 | } 73 | pub fn hide(&mut self, ident: EntryIdentifier) { 74 | let (entry_type, index, parent) = if let Some(entry) = self.0.get(&ident) { 75 | (entry.entry_type, entry.index, entry.parent()) 76 | } else { 77 | return; 78 | }; 79 | 80 | match entry_type { 81 | EntryType::Sink | EntryType::Source => { 82 | let desired = if entry_type == EntryType::Sink { 83 | EntryType::SinkInput 84 | } else { 85 | EntryType::SourceOutput 86 | }; 87 | 88 | self.0 89 | .iter_mut() 90 | .filter(|(i, e)| { 91 | (i.entry_type == desired && e.parent() == Some(index)) 92 | || (i.entry_type == entry_type && e.index == index) 93 | }) 94 | .for_each(|(_, e)| e.negate_hidden(e.entry_type)); 95 | } 96 | EntryType::SinkInput | EntryType::SourceOutput => { 97 | let (desired, parent_type) = if entry_type == EntryType::SinkInput { 98 | (EntryType::SinkInput, EntryType::Sink) 99 | } else { 100 | (EntryType::SourceOutput, EntryType::Source) 101 | }; 102 | self.0 103 | .iter_mut() 104 | .filter(|(ident, e)| { 105 | (ident.entry_type == desired && e.parent() == parent) 106 | || (ident.entry_type == parent_type && Some(e.index) == parent) 107 | }) 108 | .for_each(|(_, e)| e.negate_hidden(e.entry_type)); 109 | } 110 | _ => {} 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/models/entry/entry_type.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)] 2 | pub enum EntryType { 3 | Sink, 4 | SinkInput, 5 | Source, 6 | SourceOutput, 7 | Card, 8 | } 9 | 10 | impl From for u8 { 11 | fn from(x: EntryType) -> Self { 12 | match x { 13 | EntryType::Sink => 1, 14 | EntryType::Source => 2, 15 | EntryType::SinkInput => 3, 16 | EntryType::SourceOutput => 4, 17 | EntryType::Card => 5, 18 | } 19 | } 20 | } 21 | 22 | impl std::cmp::PartialOrd for EntryType { 23 | fn partial_cmp(&self, other: &Self) -> Option { 24 | Some(self.cmp(other)) 25 | } 26 | } 27 | 28 | impl std::cmp::Ord for EntryType { 29 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 30 | let a: u8 = (*self).into(); 31 | let b: u8 = (*other).into(); 32 | 33 | a.cmp(&b) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/models/entry/identifier.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use super::EntryType; 4 | 5 | #[derive(Clone, Copy, PartialEq, Hash, Debug)] 6 | pub struct EntryIdentifier { 7 | pub entry_type: EntryType, 8 | pub index: u32, 9 | } 10 | 11 | impl Eq for EntryIdentifier {} 12 | 13 | impl std::cmp::PartialOrd for EntryIdentifier { 14 | fn partial_cmp(&self, other: &Self) -> Option { 15 | Some(self.cmp(other)) 16 | } 17 | } 18 | 19 | impl std::cmp::Ord for EntryIdentifier { 20 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 21 | if self == other { 22 | return std::cmp::Ordering::Equal; 23 | } 24 | 25 | let result = self.entry_type.cmp(&other.entry_type); 26 | match result { 27 | Ordering::Equal => Ordering::Greater, 28 | _ => result, 29 | } 30 | } 31 | } 32 | 33 | impl EntryIdentifier { 34 | pub fn new(entry_type: EntryType, index: u32) -> Self { 35 | Self { entry_type, index } 36 | } 37 | 38 | pub fn is_card(&self) -> bool { 39 | self.entry_type == EntryType::Card 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/models/entry/mod.rs: -------------------------------------------------------------------------------- 1 | mod card_entry; 2 | mod entries; 3 | mod entry_type; 4 | mod identifier; 5 | mod play_entry; 6 | 7 | pub use card_entry::{CardEntry, CardProfile}; 8 | pub use entries::Entries; 9 | pub use entry_type::EntryType; 10 | pub use identifier::EntryIdentifier; 11 | pub use play_entry::PlayEntry; 12 | use pulse::volume::ChannelVolumes; 13 | 14 | use crate::{ 15 | ui::{widgets::VolumeWidget, Rect}, 16 | unwrap_or_return, 17 | }; 18 | 19 | #[derive(PartialEq, Copy, Clone, Debug)] 20 | pub enum EntrySpaceLvl { 21 | Empty, 22 | Parent, 23 | ParentNoChildren, 24 | MidChild, 25 | LastChild, 26 | Card, 27 | } 28 | 29 | #[derive(PartialEq, Clone, Debug)] 30 | pub enum EntryKind { 31 | CardEntry(CardEntry), 32 | PlayEntry(PlayEntry), 33 | } 34 | 35 | impl EntryKind { 36 | pub fn play_entry(&self) -> Option<&PlayEntry> { 37 | match self { 38 | Self::PlayEntry(play) => Some(play), 39 | Self::CardEntry(_) => None, 40 | } 41 | } 42 | pub fn card_entry(&self) -> Option<&CardEntry> { 43 | match self { 44 | Self::CardEntry(card) => Some(card), 45 | Self::PlayEntry(_) => None, 46 | } 47 | } 48 | pub fn play_entry_mut(&mut self) -> Option<&mut PlayEntry> { 49 | match self { 50 | Self::PlayEntry(play) => Some(play), 51 | Self::CardEntry(_) => None, 52 | } 53 | } 54 | pub fn card_entry_mut(&mut self) -> Option<&mut CardEntry> { 55 | match self { 56 | Self::CardEntry(card) => Some(card), 57 | Self::PlayEntry(_) => None, 58 | } 59 | } 60 | } 61 | 62 | #[derive(PartialEq, Clone, Debug, Copy)] 63 | pub enum HiddenStatus { 64 | Show, 65 | HiddenKids, 66 | Hidden, 67 | NoKids, 68 | } 69 | impl Eq for HiddenStatus {} 70 | 71 | impl HiddenStatus { 72 | pub fn negate(&mut self, entry_type: EntryType) { 73 | match entry_type { 74 | EntryType::Source | EntryType::Sink => match self { 75 | Self::Show => *self = Self::HiddenKids, 76 | Self::HiddenKids => *self = Self::Show, 77 | _ => {} 78 | }, 79 | EntryType::SourceOutput | EntryType::SinkInput => match self { 80 | Self::Show => *self = Self::Hidden, 81 | Self::Hidden => *self = Self::Show, 82 | _ => {} 83 | }, 84 | _ => {} 85 | } 86 | } 87 | } 88 | 89 | #[derive(PartialEq, Clone, Debug)] 90 | pub struct Entry { 91 | pub entry_type: EntryType, 92 | pub entry_ident: EntryIdentifier, 93 | pub index: u32, 94 | pub name: String, 95 | pub is_selected: bool, 96 | pub position: EntrySpaceLvl, 97 | pub entry_kind: EntryKind, 98 | } 99 | impl Eq for Entry {} 100 | 101 | impl Entry { 102 | pub fn negate_hidden(&mut self, entry_type: EntryType) { 103 | if let EntryKind::PlayEntry(play) = &mut self.entry_kind { 104 | play.hidden.negate(entry_type); 105 | } 106 | } 107 | 108 | pub fn parent(&self) -> Option { 109 | if let EntryKind::PlayEntry(play) = &self.entry_kind { 110 | play.parent 111 | } else { 112 | None 113 | } 114 | } 115 | 116 | pub fn new_play_entry( 117 | entry_type: EntryType, 118 | index: u32, 119 | name: String, 120 | parent: Option, 121 | mute: bool, 122 | volume: ChannelVolumes, 123 | monitor_source: Option, 124 | sink: Option, 125 | suspended: bool, 126 | ) -> Self { 127 | Self { 128 | entry_ident: EntryIdentifier::new(entry_type, index), 129 | entry_type, 130 | index, 131 | name: name.clone(), 132 | is_selected: false, 133 | position: EntrySpaceLvl::Empty, 134 | entry_kind: EntryKind::PlayEntry(PlayEntry { 135 | peak: 0.0, 136 | mute, 137 | parent, 138 | volume, 139 | monitor_source, 140 | sink, 141 | volume_bar: VolumeWidget::default(), 142 | peak_volume_bar: VolumeWidget::default(), 143 | suspended, 144 | area: Rect::default(), 145 | name, 146 | is_selected: false, 147 | position: EntrySpaceLvl::Empty, 148 | hidden: HiddenStatus::Show, 149 | }), 150 | } 151 | } 152 | 153 | pub fn new_card_entry( 154 | index: u32, 155 | name: String, 156 | profiles: Vec, 157 | selected_profile: Option, 158 | ) -> Self { 159 | Self { 160 | entry_ident: EntryIdentifier::new(EntryType::Card, index), 161 | entry_type: EntryType::Card, 162 | index, 163 | name: name.clone(), 164 | is_selected: false, 165 | position: EntrySpaceLvl::Card, 166 | entry_kind: EntryKind::CardEntry(CardEntry { 167 | area: Rect::default(), 168 | is_selected: false, 169 | profiles, 170 | selected_profile, 171 | name, 172 | }), 173 | } 174 | } 175 | 176 | pub fn calc_area(position: EntrySpaceLvl, mut area: Rect) -> Rect { 177 | let amount = match position { 178 | EntrySpaceLvl::Card => 1, 179 | EntrySpaceLvl::Parent => 2, 180 | EntrySpaceLvl::ParentNoChildren => 2, 181 | _ => 5, 182 | }; 183 | 184 | area.x += amount; 185 | if amount < area.width { 186 | area.width -= amount; 187 | } else { 188 | area.width = 0; 189 | } 190 | 191 | area 192 | } 193 | 194 | pub fn monitor_source(&self, entries: &Entries) -> Option { 195 | if let EntryKind::PlayEntry(play) = &self.entry_kind { 196 | match self.entry_type { 197 | EntryType::SinkInput => { 198 | if let Some(sink) = play.sink { 199 | match entries.get(&EntryIdentifier::new(EntryType::Sink, sink)) { 200 | Some(_) => play.monitor_source, 201 | None => None, 202 | } 203 | } else { 204 | None 205 | } 206 | } 207 | _ => play.monitor_source, 208 | } 209 | } else { 210 | None 211 | } 212 | } 213 | 214 | pub fn needs_redraw(&self, entries: &Entries) -> bool { 215 | match &self.entry_kind { 216 | EntryKind::CardEntry(card) => { 217 | let old_card = unwrap_or_return!(entries.get_card_entry(&self.entry_ident), true); 218 | old_card.name != card.name || old_card.selected_profile != card.selected_profile 219 | } 220 | EntryKind::PlayEntry(play) => { 221 | let old_play = unwrap_or_return!(entries.get_play_entry(&self.entry_ident), true); 222 | old_play.name != play.name 223 | || old_play.mute != play.mute 224 | || old_play.volume != play.volume 225 | || (play.peak - old_play.peak).abs() < f32::EPSILON 226 | } 227 | } 228 | } 229 | 230 | pub fn inherit_area(&mut self, entries: &Entries) { 231 | match &mut self.entry_kind { 232 | EntryKind::CardEntry(card) => { 233 | if let Some(old_card) = entries.get_card_entry(&self.entry_ident) { 234 | card.area = old_card.area; 235 | } 236 | } 237 | EntryKind::PlayEntry(play) => { 238 | if let Some(old_play) = entries.get_play_entry(&self.entry_ident) { 239 | play.area = old_play.area; 240 | play.volume_bar = old_play.volume_bar; 241 | play.peak_volume_bar = old_play.peak_volume_bar; 242 | } 243 | } 244 | }; 245 | } 246 | 247 | pub fn area(&self) -> Rect { 248 | match &self.entry_kind { 249 | EntryKind::CardEntry(card) => card.area, 250 | EntryKind::PlayEntry(play) => play.area, 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/models/entry/play_entry.rs: -------------------------------------------------------------------------------- 1 | use pulse::volume::ChannelVolumes; 2 | 3 | use super::{EntrySpaceLvl, HiddenStatus}; 4 | use crate::ui::{widgets::VolumeWidget, Rect}; 5 | 6 | #[derive(PartialEq, Clone, Debug)] 7 | pub struct PlayEntry { 8 | pub peak: f32, 9 | pub mute: bool, 10 | pub volume: ChannelVolumes, 11 | pub monitor_source: Option, 12 | pub sink: Option, 13 | pub volume_bar: VolumeWidget, 14 | pub peak_volume_bar: VolumeWidget, 15 | pub suspended: bool, 16 | pub area: Rect, 17 | pub name: String, 18 | pub is_selected: bool, 19 | pub position: EntrySpaceLvl, 20 | pub hidden: HiddenStatus, 21 | pub parent: Option, 22 | } 23 | impl Eq for PlayEntry {} 24 | -------------------------------------------------------------------------------- /src/models/input_event.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::TryFrom, 3 | fmt::{self, Display}, 4 | }; 5 | 6 | use crossterm::event::{Event, KeyCode, KeyModifiers, MouseButton, MouseEventKind}; 7 | 8 | use crate::config::ConfigError; 9 | 10 | #[derive(Clone, Copy, Debug, Hash, PartialEq)] 11 | pub enum InputEventKind { 12 | Mouse(MouseEventKind), 13 | Key(KeyCode), 14 | } 15 | impl Eq for InputEventKind {} 16 | 17 | #[derive(Clone, Copy, Debug, Hash, PartialEq)] 18 | pub struct InputEvent { 19 | pub kind: InputEventKind, 20 | pub modifiers: KeyModifiers, 21 | } 22 | impl Eq for InputEvent {} 23 | 24 | impl InputEvent { 25 | pub fn key(key_code: KeyCode, modifiers: KeyModifiers) -> Self { 26 | Self { 27 | kind: InputEventKind::Key(key_code), 28 | modifiers, 29 | } 30 | } 31 | pub fn mouse(mouse_kind: MouseEventKind, modifiers: KeyModifiers) -> Self { 32 | Self { 33 | kind: InputEventKind::Mouse(mouse_kind), 34 | modifiers, 35 | } 36 | } 37 | } 38 | 39 | impl TryFrom for InputEvent { 40 | type Error = ConfigError; 41 | fn try_from(value: Event) -> Result { 42 | match value { 43 | Event::Key(key) => Ok(InputEvent::key(key.code, key.modifiers)), 44 | Event::Mouse(mouse) => Ok(InputEvent::mouse(mouse.kind, mouse.modifiers)), 45 | _ => Err(ConfigError::KeyCodeError( 46 | "Event::Redraw cannot be converted to InputEvent".to_string(), 47 | )), 48 | } 49 | } 50 | } 51 | 52 | impl Display for InputEvent { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | let mut modifiers = self.modifiers; 55 | let mut kind = self.kind; 56 | 57 | if let InputEventKind::Key(key) = kind { 58 | if key == KeyCode::BackTab { 59 | kind = InputEventKind::Key(KeyCode::Tab); 60 | modifiers |= KeyModifiers::SHIFT; 61 | } 62 | } 63 | 64 | if modifiers.contains(KeyModifiers::CONTROL) { 65 | write!(f, "Ctrl+")?; 66 | } 67 | if modifiers.contains(KeyModifiers::SHIFT) { 68 | write!(f, "Shift+")?; 69 | } 70 | if modifiers.contains(KeyModifiers::ALT) { 71 | write!(f, "Alt+")?; 72 | } 73 | 74 | let last = match kind { 75 | InputEventKind::Key(code) => match code { 76 | KeyCode::Backspace => "Backspace".to_string(), 77 | KeyCode::Enter => "Enter".to_string(), 78 | KeyCode::Left => "Left".to_string(), 79 | KeyCode::Right => "Right".to_string(), 80 | KeyCode::Up => "Up".to_string(), 81 | KeyCode::Down => "Down".to_string(), 82 | KeyCode::Home => "Home".to_string(), 83 | KeyCode::End => "End".to_string(), 84 | KeyCode::PageUp => "PageUp".to_string(), 85 | KeyCode::PageDown => "PageDown".to_string(), 86 | KeyCode::Tab => "Tab".to_string(), 87 | KeyCode::BackTab => "Backtab".to_string(), 88 | KeyCode::Delete => "Delete".to_string(), 89 | KeyCode::Insert => "Insert".to_string(), 90 | KeyCode::Null => "Null".to_string(), 91 | KeyCode::Esc => "Esc".to_string(), 92 | KeyCode::F(i) => format!("F{}", i), 93 | KeyCode::Char(c) => format!("{}", c), 94 | }, 95 | InputEventKind::Mouse(mouse) => match mouse { 96 | MouseEventKind::Up(MouseButton::Left) => "MLeft".to_string(), 97 | MouseEventKind::Up(MouseButton::Right) => "MRight".to_string(), 98 | MouseEventKind::Up(MouseButton::Middle) => "MMiddle".to_string(), 99 | MouseEventKind::ScrollUp => "ScrollUp".to_string(), 100 | MouseEventKind::ScrollDown => "ScrollDown".to_string(), 101 | _ => "".to_string(), 102 | }, 103 | }; 104 | 105 | write!(f, "{}", last) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod actions; 2 | pub mod context_menus; 3 | pub mod entry; 4 | mod input_event; 5 | mod page_entries; 6 | mod page_type; 7 | mod redraw; 8 | mod state; 9 | mod style; 10 | mod ui_mode; 11 | 12 | pub use actions::*; 13 | pub use context_menus::{ContextMenu, ContextMenuEffect, ContextMenuOption}; 14 | pub use input_event::{InputEvent, InputEventKind}; 15 | pub use page_entries::PageEntries; 16 | pub use page_type::PageType; 17 | pub use redraw::Redraw; 18 | pub use style::Style; 19 | pub use ui_mode::UIMode; 20 | 21 | pub use self::state::RSState; 22 | -------------------------------------------------------------------------------- /src/models/page_entries.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | entry::{EntryIdentifier, EntrySpaceLvl, EntryType}, 3 | scrollable, 4 | ui::{util::entry_height, Scrollable}, 5 | }; 6 | 7 | pub struct PageEntries { 8 | pub entries: Vec, 9 | pub last_term_h: u16, 10 | pub lvls: Vec, 11 | pub visibility: Vec, 12 | selected: usize, 13 | } 14 | 15 | impl PageEntries { 16 | pub fn new() -> Self { 17 | Self { 18 | entries: Vec::new(), 19 | last_term_h: 0, 20 | lvls: Vec::new(), 21 | visibility: Vec::new(), 22 | selected: 0, 23 | } 24 | } 25 | 26 | pub fn iter_entries(&self) -> std::slice::Iter { 27 | self.entries.iter() 28 | } 29 | 30 | pub fn len(&self) -> usize { 31 | self.entries.len() 32 | } 33 | 34 | pub fn ident_position(&self, ident: EntryIdentifier) -> Option { 35 | self.iter_entries().position(|i| *i == ident) 36 | } 37 | 38 | pub fn get(&self, i: usize) -> Option { 39 | if i < self.len() { 40 | Some(self.entries[i]) 41 | } else { 42 | None 43 | } 44 | } 45 | 46 | pub fn get_selected(&self) -> Option { 47 | self.get(self.selected()) 48 | } 49 | 50 | pub fn set(&mut self, vs: Vec, parent_type: EntryType) -> bool { 51 | let ret = if vs.len() == self.len() { 52 | // check if any page entry changed identifier or level 53 | vs.iter().enumerate().find(|&(i, &e)| { 54 | e != self.get(i).unwrap() || calc_lvl(parent_type, &vs, i) != self.lvls[i] 55 | }) != None 56 | } else { 57 | true 58 | }; 59 | 60 | if ret { 61 | self.lvls = Vec::new(); 62 | 63 | for index in 0..vs.len() { 64 | self.lvls.push(calc_lvl(parent_type, &vs, index)); 65 | } 66 | 67 | self.entries = vs; 68 | } 69 | 70 | ret 71 | } 72 | } 73 | 74 | scrollable!( 75 | PageEntries, 76 | fn selected(&self) -> usize { 77 | self.selected 78 | }, 79 | fn len(&self) -> usize { 80 | self.entries.len() 81 | }, 82 | fn set_selected(&mut self, selected: usize) -> bool { 83 | if selected < self.entries.len() { 84 | self.selected = selected; 85 | true 86 | } else { 87 | false 88 | } 89 | }, 90 | fn element_height(&self, index: usize) -> u16 { 91 | if let Some(lvl) = self.lvls.get(index) { 92 | entry_height(*lvl) 93 | } else { 94 | 0 95 | } 96 | } 97 | ); 98 | 99 | fn calc_lvl(parent_type: EntryType, vs: &[EntryIdentifier], index: usize) -> EntrySpaceLvl { 100 | if parent_type == EntryType::Card { 101 | EntrySpaceLvl::Card 102 | } else if vs[index].entry_type == parent_type { 103 | if index + 1 >= vs.len() || vs[index + 1].entry_type == parent_type { 104 | EntrySpaceLvl::ParentNoChildren 105 | } else { 106 | EntrySpaceLvl::Parent 107 | } 108 | } else if index + 1 >= vs.len() || vs[index + 1].entry_type == parent_type { 109 | EntrySpaceLvl::LastChild 110 | } else { 111 | EntrySpaceLvl::MidChild 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/models/page_type.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, iter}; 2 | 3 | use super::UIMode; 4 | use crate::entry::{Entries, Entry, EntryIdentifier, EntryKind, EntryType, HiddenStatus}; 5 | 6 | #[derive(PartialEq, Clone, Hash, Copy, Debug)] 7 | pub enum PageType { 8 | Output, 9 | Input, 10 | Cards, 11 | } 12 | impl Eq for PageType {} 13 | impl Display for PageType { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | write!(f, "{}", self.as_str()) 16 | } 17 | } 18 | impl From for i8 { 19 | fn from(p: PageType) -> i8 { 20 | match p { 21 | PageType::Output => 0, 22 | PageType::Input => 1, 23 | PageType::Cards => 2, 24 | } 25 | } 26 | } 27 | impl From for PageType { 28 | fn from(p: i8) -> PageType { 29 | match p { 30 | -1 => PageType::Cards, 31 | 0 => PageType::Output, 32 | 1 => PageType::Input, 33 | 2 => PageType::Cards, 34 | _ => PageType::Output, 35 | } 36 | } 37 | } 38 | impl PageType { 39 | pub fn parent_child_types(&self) -> (EntryType, EntryType) { 40 | match self { 41 | Self::Output => (EntryType::Sink, EntryType::SinkInput), 42 | Self::Input => (EntryType::Source, EntryType::SourceOutput), 43 | Self::Cards => (EntryType::Card, EntryType::Card), 44 | } 45 | } 46 | pub fn as_str(&self) -> &'static str { 47 | match self { 48 | PageType::Output => "Output", 49 | PageType::Input => "Input", 50 | PageType::Cards => "Cards", 51 | } 52 | } 53 | pub fn as_styled_string(&self) -> String { 54 | // let styled_name = |pt: PageType| { 55 | // if pt == *self { 56 | // get_style("normal.bold").apply(pt.as_str()) 57 | // } else { 58 | // get_style("muted").apply(pt.as_str()) 59 | // } 60 | // }; 61 | 62 | // let divider = get_style("muted").apply(" / "); 63 | 64 | // format!( 65 | // "{}{}{}{}{}", 66 | // styled_name(PageType::Output), 67 | // divider.clone(), 68 | // styled_name(PageType::Input), 69 | // divider.clone(), 70 | // styled_name(PageType::Cards) 71 | // ) 72 | "".to_string() 73 | } 74 | pub fn generate_page<'a>( 75 | &'a self, 76 | entries: &'a Entries, 77 | ui_mode: &'a UIMode, 78 | ) -> Box + 'a> { 79 | if *self == PageType::Cards { 80 | return Box::new(entries.iter_type(EntryType::Card)); 81 | } 82 | 83 | let (parent, child) = self.parent_child_types(); 84 | 85 | if let UIMode::MoveEntry(ident, parent) = ui_mode { 86 | let en = entries.get(ident).unwrap(); 87 | let p = *parent; 88 | let parent_pos = entries 89 | .iter_type(parent.entry_type) 90 | .position(|(&i, _)| i == p) 91 | .unwrap(); 92 | return Box::new( 93 | entries 94 | .iter_type(parent.entry_type) 95 | .take(parent_pos + 1) 96 | .chain(iter::once((ident, en))) 97 | .chain( 98 | entries 99 | .iter_type(parent.entry_type) 100 | .skip(parent_pos + 1) 101 | .take_while(|_| true), 102 | ), 103 | ); 104 | } 105 | 106 | Box::new( 107 | entries 108 | .iter_type(parent) 109 | .map(move |(ident, entry)| { 110 | std::iter::once((ident, entry)).chain(entries.iter_type(child).filter( 111 | move |(_, e)| { 112 | e.parent() == Some(ident.index) 113 | && match &e.entry_kind { 114 | EntryKind::CardEntry(_) => true, 115 | EntryKind::PlayEntry(play) => { 116 | play.hidden != HiddenStatus::Hidden 117 | } 118 | } 119 | }, 120 | )) 121 | }) 122 | .flatten(), 123 | ) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/models/redraw.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | #[derive(PartialEq, Debug, Clone)] 4 | pub struct Redraw { 5 | pub entries: bool, 6 | pub peak_volume: Option, 7 | pub resize: bool, 8 | pub affected_entries: HashSet, 9 | pub context_menu: bool, 10 | } 11 | impl Default for Redraw { 12 | fn default() -> Self { 13 | Self { 14 | entries: false, 15 | peak_volume: None, 16 | resize: false, 17 | affected_entries: HashSet::new(), 18 | context_menu: false, 19 | } 20 | } 21 | } 22 | 23 | impl Redraw { 24 | pub fn reset(&mut self) { 25 | *self = Redraw::default(); 26 | } 27 | pub fn anything(&self) -> bool { 28 | self.entries 29 | || self.peak_volume.is_some() 30 | || self.resize 31 | || self.context_menu 32 | || !self.affected_entries.is_empty() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/models/state/mod.rs: -------------------------------------------------------------------------------- 1 | mod page_entries; 2 | 3 | use std::collections::HashMap; 4 | 5 | use super::{ 6 | ContextMenu, ContextMenuEffect, PageEntries, PageType, PulseAudioAction, Redraw, UIMode, 7 | }; 8 | use crate::{ 9 | actor_system::Ctx, 10 | entry::{Entries, Entry, EntryIdentifier, EntryKind}, 11 | ui::{ 12 | widgets::{HelpWidget, VolumeInputWidget, WarningTextWidget}, 13 | Scrollable, UI, 14 | }, 15 | util::{percent_to_volume, volume_to_percent}, 16 | }; 17 | 18 | pub struct RSState { 19 | pub current_page: PageType, 20 | pub entries: Entries, 21 | pub page_entries: PageEntries, 22 | pub context_menu: ContextMenu, 23 | pub ui_mode: UIMode, 24 | pub redraw: Redraw, 25 | pub help: HelpWidget, 26 | pub warning_text: WarningTextWidget, 27 | pub input_exact_volume: VolumeInputWidget, 28 | pub ui: UI, 29 | pub ctx: Option, 30 | } 31 | 32 | impl Default for RSState { 33 | fn default() -> Self { 34 | Self { 35 | current_page: PageType::Output, 36 | entries: Entries::default(), 37 | page_entries: PageEntries::new(), 38 | context_menu: ContextMenu::default(), 39 | ui_mode: UIMode::Normal, 40 | redraw: Redraw::default(), 41 | help: HelpWidget::default(), 42 | warning_text: WarningTextWidget { 43 | text: "".to_string(), 44 | }, 45 | input_exact_volume: VolumeInputWidget::default(), 46 | ui: UI::default(), 47 | ctx: None, 48 | } 49 | } 50 | } 51 | 52 | impl RSState { 53 | pub fn new(ctx: Ctx) -> Self { 54 | Self { 55 | current_page: PageType::Output, 56 | entries: Entries::default(), 57 | page_entries: PageEntries::new(), 58 | context_menu: ContextMenu::default(), 59 | ui_mode: UIMode::Normal, 60 | redraw: Redraw::default(), 61 | help: HelpWidget::default(), 62 | input_exact_volume: VolumeInputWidget::default(), 63 | warning_text: WarningTextWidget { 64 | text: "".to_string(), 65 | }, 66 | ui: UI::default(), 67 | ctx: Some(ctx), 68 | } 69 | } 70 | pub fn reset(&mut self) { 71 | self.ctx().send_to( 72 | "pulseaudio", 73 | PulseAudioAction::CreateMonitors(HashMap::new()), 74 | ); 75 | *self = Self::new(self.ctx.take().unwrap()); 76 | self.redraw.resize = true; 77 | } 78 | pub fn change_ui_mode(&mut self, mode: UIMode) { 79 | log::debug!("changing ui mode to {:?}", mode); 80 | self.ui_mode = mode; 81 | self.redraw.resize = true; 82 | } 83 | pub fn remove_entry(&mut self, ident: &EntryIdentifier) { 84 | self.entries.remove(ident); 85 | 86 | if self.page_entries.ident_position(*ident).is_some() { 87 | page_entries::update(self); 88 | } 89 | 90 | if self.ui_mode == UIMode::ContextMenu { 91 | self.change_ui_mode(UIMode::Normal); 92 | } 93 | } 94 | 95 | pub fn update_entry(&mut self, ident: &EntryIdentifier, mut entry: Entry) { 96 | if entry.needs_redraw(&self.entries) { 97 | if let Some(i) = self 98 | .page_entries 99 | .iter_entries() 100 | .position(|id| *id == entry.entry_ident) 101 | { 102 | self.redraw.affected_entries.insert(i); 103 | } 104 | } 105 | 106 | entry.inherit_area(&self.entries); 107 | 108 | self.entries.insert(*ident, entry); 109 | 110 | page_entries::update(self); 111 | } 112 | 113 | pub fn update_peak_volume(&mut self, ident: &EntryIdentifier, peak: &f32) { 114 | if let Some(play) = self.entries.get_play_entry_mut(ident) { 115 | if (play.peak - peak).abs() < f32::EPSILON { 116 | return; 117 | } 118 | play.peak = *peak; 119 | 120 | if let Some(i) = self.page_entries.iter_entries().position(|&i| *ident == i) { 121 | self.redraw.peak_volume = Some(i); 122 | } 123 | } 124 | } 125 | 126 | pub fn move_down(&mut self, how_much: usize) { 127 | match self.ui_mode { 128 | UIMode::Normal => { 129 | self.selected_entry_needs_redraw(); 130 | self.page_entries.down(how_much); 131 | self.selected_entry_needs_redraw(); 132 | 133 | page_entries::update(self); 134 | } 135 | UIMode::ContextMenu => { 136 | self.context_menu.down(how_much); 137 | self.context_menu.horizontal_scroll = 0; 138 | 139 | self.redraw.context_menu = true; 140 | } 141 | UIMode::Help => { 142 | self.help.down(how_much); 143 | 144 | self.redraw.context_menu = true; 145 | } 146 | UIMode::MoveEntry(_, _) => { 147 | if self.page_entries.entries.len() < 2 { 148 | return; 149 | } 150 | let l = self.page_entries.len() - 1; 151 | let selected = self.page_entries.selected() - 1; 152 | 153 | let mut j = (selected + how_much as usize) % l; 154 | 155 | if j >= selected { 156 | j += 1; 157 | } 158 | 159 | let entry_ident = self.page_entries.get_selected().unwrap(); 160 | let new_parent = self.page_entries.get(j as usize).unwrap(); 161 | self.change_ui_mode(UIMode::MoveEntry(entry_ident, new_parent)); 162 | 163 | page_entries::update(self); 164 | } 165 | _ => {} 166 | } 167 | } 168 | 169 | pub fn move_up(&mut self, how_much: usize) { 170 | match self.ui_mode { 171 | UIMode::Normal => { 172 | self.selected_entry_needs_redraw(); 173 | self.page_entries.up(how_much); 174 | self.selected_entry_needs_redraw(); 175 | 176 | page_entries::update(self); 177 | } 178 | UIMode::ContextMenu => { 179 | self.context_menu.up(how_much); 180 | self.context_menu.horizontal_scroll = 0; 181 | 182 | self.redraw.context_menu = true; 183 | } 184 | UIMode::Help => { 185 | self.help.up(how_much); 186 | 187 | self.redraw.context_menu = true; 188 | } 189 | UIMode::MoveEntry(_, _) => { 190 | if self.page_entries.entries.len() < 2 { 191 | return; 192 | } 193 | let l = (self.page_entries.len() - 1) as i32; 194 | let selected = (self.page_entries.selected() - 1) as i32; 195 | 196 | let mut j = selected - how_much as i32; 197 | 198 | if j < 0 { 199 | j = j.abs() % l; 200 | j = l - j; 201 | } 202 | 203 | if j >= selected { 204 | j += 1; 205 | } 206 | 207 | let entry_ident = self.page_entries.get_selected().unwrap(); 208 | let new_parent = self.page_entries.get(j as usize).unwrap(); 209 | self.change_ui_mode(UIMode::MoveEntry(entry_ident, new_parent)); 210 | 211 | page_entries::update(self); 212 | } 213 | _ => {} 214 | } 215 | } 216 | 217 | pub fn move_left(&mut self) { 218 | if self.context_menu.horizontal_scroll > 0 { 219 | self.context_menu.horizontal_scroll -= 1; 220 | 221 | self.redraw.context_menu = true; 222 | } 223 | } 224 | 225 | pub fn move_right(&mut self) { 226 | if self.context_menu.horizontal_scroll < self.context_menu.max_horizontal_scroll() { 227 | self.context_menu.horizontal_scroll += 1; 228 | 229 | self.redraw.context_menu = true; 230 | } 231 | } 232 | 233 | pub fn set_selected(&mut self, index: usize) { 234 | match self.ui_mode { 235 | UIMode::Normal => { 236 | self.selected_entry_needs_redraw(); 237 | self.page_entries.set_selected(index); 238 | self.selected_entry_needs_redraw(); 239 | 240 | page_entries::update(self); 241 | } 242 | UIMode::ContextMenu => { 243 | self.context_menu.set_selected(index); 244 | 245 | self.redraw.context_menu = true; 246 | } 247 | _ => {} 248 | } 249 | } 250 | 251 | pub fn request_mute(&mut self, ident: &Option) { 252 | let ident = match *ident { 253 | Some(i) => i, 254 | None => match self.page_entries.get_selected() { 255 | Some(sel) => sel, 256 | None => { 257 | return; 258 | } 259 | }, 260 | }; 261 | 262 | let mute = match self.entries.get_play_entry(&ident) { 263 | Some(p) => p.mute, 264 | None => { 265 | return; 266 | } 267 | }; 268 | self.ctx() 269 | .send_to("pulseaudio", PulseAudioAction::MuteEntry(ident, !mute)); 270 | } 271 | 272 | pub fn request_change_volume(&mut self, how_much: i16, ident: &Option) { 273 | let ident = match *ident { 274 | Some(i) => i, 275 | None => match self.page_entries.get_selected() { 276 | Some(sel) => sel, 277 | None => { 278 | return; 279 | } 280 | }, 281 | }; 282 | 283 | if let Some(play) = self.entries.get_play_entry_mut(&ident) { 284 | let mut vols = play.volume; 285 | 286 | let target_percent = volume_to_percent(vols) as i16 + how_much; 287 | 288 | let target = percent_to_volume(target_percent); 289 | 290 | for v in vols.get_mut() { 291 | v.0 = target; 292 | } 293 | 294 | self.ctx() 295 | .send_to("pulseaudio", PulseAudioAction::SetVolume(ident, vols)); 296 | } 297 | } 298 | 299 | pub fn setup_volume_input(&mut self) { 300 | let ident = match self.page_entries.get_selected() { 301 | Some(i) => i, 302 | None => { 303 | return; 304 | } 305 | }; 306 | 307 | let percent = if let Some(play) = self.entries.get_play_entry(&ident) { 308 | volume_to_percent(play.volume) 309 | } else { 310 | 0 311 | }; 312 | let percent = percent.to_string(); 313 | 314 | let cursor = percent.len(); 315 | 316 | self.set_volume_input_value(percent, cursor as u8); 317 | } 318 | 319 | pub fn set_volume_input_value(&mut self, percent: String, cursor: u8) { 320 | self.redraw.context_menu = true; 321 | 322 | self.input_exact_volume.value = percent; 323 | self.input_exact_volume.cursor = cursor; 324 | } 325 | 326 | pub fn open_context_menu(&mut self, ident: &Option) { 327 | if let Some(ident) = ident { 328 | if let Some(index) = self.page_entries.iter_entries().position(|i| *i == *ident) { 329 | self.page_entries.set_selected(index); 330 | 331 | page_entries::update(self); 332 | } 333 | } 334 | 335 | if self.page_entries.selected() < self.page_entries.len() { 336 | if let Some(entry) = self 337 | .entries 338 | .get(&self.page_entries.get(self.page_entries.selected()).unwrap()) 339 | { 340 | self.ui_mode = UIMode::ContextMenu; 341 | self.context_menu = ContextMenu::new(entry); 342 | 343 | if let EntryKind::CardEntry(card) = &entry.entry_kind { 344 | self.context_menu 345 | .set_selected(card.selected_profile.unwrap_or(0)); 346 | } 347 | 348 | self.redraw.resize = true; 349 | } 350 | } 351 | } 352 | 353 | pub fn confirm_input_volume(&mut self) { 354 | let selected = match self.page_entries.get_selected() { 355 | Some(ident) => ident, 356 | None => { 357 | return; 358 | } 359 | }; 360 | 361 | let percent = match self.input_exact_volume.value.parse::() { 362 | Ok(percent) => percent, 363 | Err(_) => { 364 | return; 365 | } 366 | }; 367 | 368 | let vol = percent_to_volume(percent as i16); 369 | 370 | if let Some(play) = self.entries.get_play_entry_mut(&selected) { 371 | let mut vols = play.volume; 372 | 373 | for v in vols.get_mut() { 374 | v.0 = vol; 375 | } 376 | 377 | self.ctx() 378 | .send_to("pulseaudio", PulseAudioAction::SetVolume(selected, vols)); 379 | } 380 | } 381 | 382 | pub fn confirm_context_menu(&mut self) { 383 | let selected = match self.page_entries.get_selected() { 384 | Some(ident) => ident, 385 | None => { 386 | return; 387 | } 388 | }; 389 | 390 | let answer = self.context_menu.resolve(selected, &self.ctx()); 391 | 392 | match answer { 393 | ContextMenuEffect::None => { 394 | self.change_ui_mode(UIMode::Normal); 395 | } 396 | ContextMenuEffect::MoveEntry => { 397 | let (parent_type, _) = self.current_page.parent_child_types(); 398 | let entry_ident = selected; 399 | 400 | if let Some(parent_id) = self.entries.get_play_entry(&entry_ident).unwrap().parent { 401 | let entry_parent = EntryIdentifier::new(parent_type, parent_id); 402 | let parent_ident = match self.entries.find(|(&i, _)| i == entry_parent) { 403 | Some((i, _)) => *i, 404 | None => EntryIdentifier::new(parent_type, 0), 405 | }; 406 | 407 | self.change_ui_mode(UIMode::MoveEntry(entry_ident, parent_ident)); 408 | 409 | page_entries::update(self); 410 | } else { 411 | self.change_ui_mode(UIMode::Normal); 412 | } 413 | } 414 | }; 415 | } 416 | 417 | pub fn hide_entry(&mut self, ident: &Option) { 418 | let ident = match *ident { 419 | Some(i) => i, 420 | None => match self.page_entries.get_selected() { 421 | Some(i) => i, 422 | None => { 423 | return; 424 | } 425 | }, 426 | }; 427 | 428 | self.entries.hide(ident); 429 | 430 | page_entries::update(self); 431 | } 432 | 433 | pub fn change_page(&mut self, page: PageType) { 434 | self.current_page = page; 435 | self.change_ui_mode(UIMode::Normal); 436 | page_entries::update(self); 437 | } 438 | 439 | pub fn ctx(&self) -> &Ctx { 440 | self.ctx.as_ref().unwrap() 441 | } 442 | fn selected_entry_needs_redraw(&mut self) { 443 | self.redraw 444 | .affected_entries 445 | .insert(self.page_entries.selected()); 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/models/state/page_entries.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use super::RSState; 4 | use crate::{ 5 | entry::{EntryIdentifier, EntryKind, EntryType, HiddenStatus}, 6 | models::{PageType, PulseAudioAction, UIMode}, 7 | ui::Scrollable, 8 | }; 9 | 10 | pub fn update(state: &mut RSState) { 11 | let last_sel = state.page_entries.get_selected(); 12 | 13 | let (p, c) = state.current_page.parent_child_types(); 14 | 15 | if p != EntryType::Card && c != EntryType::Card { 16 | let mut parents = HashSet::new(); 17 | state.entries.iter_type(c).for_each(|(_, e)| { 18 | if let EntryKind::PlayEntry(play) = &e.entry_kind { 19 | parents.insert(play.parent); 20 | } 21 | }); 22 | 23 | for (_, p_e) in state.entries.iter_type_mut(p) { 24 | if let EntryKind::PlayEntry(play) = &mut p_e.entry_kind { 25 | play.hidden = match parents.get(&Some(p_e.index)) { 26 | Some(_) => HiddenStatus::HiddenKids, 27 | None => HiddenStatus::NoKids, 28 | }; 29 | } 30 | } 31 | } 32 | 33 | let entries_changed = state.page_entries.set( 34 | state 35 | .current_page 36 | .generate_page(&state.entries, &state.ui_mode) 37 | .map(|x| *x.0) 38 | .collect::>(), 39 | p, 40 | ); 41 | 42 | match state.ui_mode { 43 | UIMode::MoveEntry(ident, _) => { 44 | if let Some(i) = state.page_entries.iter_entries().position(|&x| x == ident) { 45 | state.page_entries.set_selected(i); 46 | } 47 | } 48 | _ => { 49 | if let Some(i) = state 50 | .page_entries 51 | .iter_entries() 52 | .position(|&x| Some(x) == last_sel) 53 | { 54 | state.page_entries.set_selected(i); 55 | } 56 | } 57 | }; 58 | 59 | if entries_changed { 60 | let monitors = monitor_list(state); 61 | state 62 | .ctx() 63 | .send_to("pulseaudio", PulseAudioAction::CreateMonitors(monitors)); 64 | 65 | state.redraw.resize = true; 66 | } 67 | } 68 | 69 | fn monitor_list(state: &mut RSState) -> HashMap> { 70 | let mut monitors = HashMap::new(); 71 | 72 | if state.current_page == PageType::Cards { 73 | return monitors; 74 | } 75 | 76 | state.page_entries.iter_entries().for_each(|ident| { 77 | if let Some(entry) = state.entries.get(ident) { 78 | monitors.insert( 79 | EntryIdentifier::new(entry.entry_type, entry.index), 80 | entry.monitor_source(&state.entries), 81 | ); 82 | } 83 | }); 84 | 85 | monitors 86 | } 87 | -------------------------------------------------------------------------------- /src/models/style.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, PartialEq, Debug, Hash)] 2 | pub enum Style { 3 | Normal, 4 | Muted, 5 | Bold, 6 | Inverted, 7 | Red, 8 | Green, 9 | Orange, 10 | } 11 | impl Eq for Style {} 12 | 13 | impl From<&String> for Style { 14 | fn from(s: &String) -> Self { 15 | match &s[..] { 16 | "normal" => Style::Normal, 17 | "muted" => Style::Muted, 18 | "bold" => Style::Bold, 19 | "inverted" => Style::Inverted, 20 | "red" => Style::Red, 21 | "green" => Style::Green, 22 | "orange" => Style::Orange, 23 | _ => Style::Normal, 24 | } 25 | } 26 | } 27 | 28 | impl Default for Style { 29 | fn default() -> Self { 30 | Style::Normal 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/models/ui_mode.rs: -------------------------------------------------------------------------------- 1 | use crate::entry::EntryIdentifier; 2 | 3 | #[derive(PartialEq, Debug)] 4 | pub enum UIMode { 5 | Normal, 6 | ContextMenu, 7 | Help, 8 | MoveEntry(EntryIdentifier, EntryIdentifier), 9 | InputVolumeValue, 10 | RetryIn(u64), 11 | } 12 | -------------------------------------------------------------------------------- /src/multimap.rs: -------------------------------------------------------------------------------- 1 | use std::hash::Hash; 2 | 3 | use linked_hash_map::LinkedHashMap; 4 | use serde::{ 5 | private::de::{Content, ContentRefDeserializer}, 6 | Deserialize, Deserializer, Serialize, Serializer, 7 | }; 8 | 9 | #[derive(Clone)] 10 | enum Element { 11 | Single(Vec), 12 | Many(Vec), 13 | } 14 | 15 | impl Serialize for Element { 16 | fn serialize(&self, serializer: S) -> Result 17 | where 18 | S: Serializer, 19 | { 20 | match self { 21 | Self::Single(x) => x[0].serialize(serializer), 22 | Self::Many(xs) => xs.serialize(serializer), 23 | } 24 | } 25 | } 26 | impl<'de, T: Deserialize<'de>> Deserialize<'de> for Element { 27 | fn deserialize(deserializer: D) -> Result 28 | where 29 | D: Deserializer<'de>, 30 | { 31 | let content = Content::deserialize(deserializer)?; 32 | if let Ok(x) = T::deserialize(ContentRefDeserializer::::new(&content)) { 33 | Ok(Element::Single(vec![x])) 34 | } else { 35 | let xs = Vec::::deserialize(ContentRefDeserializer::::new(&content))?; 36 | Ok(Element::Many(xs)) 37 | } 38 | } 39 | } 40 | 41 | #[derive(Clone, Serialize, Deserialize, Default)] 42 | pub struct MultiMap(LinkedHashMap>); 43 | 44 | impl MultiMap { 45 | pub fn new() -> Self { 46 | Self { 47 | 0: LinkedHashMap::new(), 48 | } 49 | } 50 | 51 | pub fn insert(&mut self, k: K, v: V) { 52 | let to_push; 53 | match self.0.get_mut(&k) { 54 | Some(e) => { 55 | match e { 56 | Element::Single(x) => { 57 | to_push = Element::Many(vec![x.remove(0), v]); 58 | } 59 | Element::Many(xs) => { 60 | xs.push(v); 61 | return; 62 | } 63 | }; 64 | } 65 | None => { 66 | to_push = Element::Single(vec![v]); 67 | } 68 | }; 69 | self.0.insert(k, to_push); 70 | } 71 | 72 | pub fn iter(&self) -> impl Iterator { 73 | self.0.iter().flat_map(|(k, v)| { 74 | let vs = match v { 75 | Element::Single(x) => x, 76 | Element::Many(xs) => xs, 77 | }; 78 | vs.iter().map(move |s| (k, s)) 79 | }) 80 | } 81 | 82 | pub fn iter_vecs(&self) -> impl Iterator)> + '_ { 83 | self.0.iter().map(|(k, v)| match v { 84 | Element::Single(x) => (k, x), 85 | Element::Many(xs) => (k, xs), 86 | }) 87 | } 88 | 89 | pub fn get_vec(&self, k: &K) -> Option<&Vec> { 90 | match self.0.get(k) { 91 | Some(v) => match v { 92 | Element::Single(x) => Some(x), 93 | Element::Many(xs) => Some(xs), 94 | }, 95 | None => None, 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/pa/callbacks.rs: -------------------------------------------------------------------------------- 1 | use pulse::{ 2 | callbacks::ListResult, 3 | context::{ 4 | introspect::{CardInfo, SinkInfo, SinkInputInfo, SourceInfo, SourceOutputInfo}, 5 | subscribe::{InterestMaskSet, Operation}, 6 | }, 7 | def::{SinkState, SourceState}, 8 | }; 9 | 10 | use super::{common::*, pa_interface::ACTIONS_SX}; 11 | use crate::{ 12 | entry::{CardProfile, Entry}, 13 | models::EntryUpdate, 14 | ui::Rect, 15 | }; 16 | 17 | pub fn subscribe( 18 | context: &Rc>, 19 | info_sx: mpsc::UnboundedSender, 20 | ) -> Result<()> { 21 | info!("[PAInterface] Registering pulseaudio callbacks"); 22 | 23 | context.borrow_mut().subscribe( 24 | InterestMaskSet::SINK 25 | | InterestMaskSet::SINK_INPUT 26 | | InterestMaskSet::SOURCE 27 | | InterestMaskSet::CARD 28 | | InterestMaskSet::SOURCE_OUTPUT 29 | | InterestMaskSet::CLIENT 30 | | InterestMaskSet::SERVER, 31 | |success: bool| { 32 | assert!(success, "subscription failed"); 33 | }, 34 | ); 35 | 36 | context.borrow_mut().set_subscribe_callback(Some(Box::new( 37 | move |facility, operation, index| { 38 | if let Some(facility) = facility { 39 | match facility { 40 | Facility::Server | Facility::Client => { 41 | log::error!("{:?} {:?}", facility, operation); 42 | return; 43 | } 44 | _ => {} 45 | }; 46 | 47 | let entry_type: EntryType = facility.into(); 48 | match operation { 49 | Some(Operation::New) => { 50 | info!("[PAInterface] New {:?}", entry_type); 51 | 52 | info_sx 53 | .send(EntryIdentifier::new(entry_type, index)) 54 | .unwrap(); 55 | } 56 | Some(Operation::Changed) => { 57 | info!("[PAInterface] {:?} changed", entry_type); 58 | info_sx 59 | .send(EntryIdentifier::new(entry_type, index)) 60 | .unwrap(); 61 | } 62 | Some(Operation::Removed) => { 63 | info!("[PAInterface] {:?} removed", entry_type); 64 | (*ACTIONS_SX) 65 | .get() 66 | .send(EntryUpdate::EntryRemoved(EntryIdentifier::new( 67 | entry_type, index, 68 | ))) 69 | .unwrap(); 70 | } 71 | _ => {} 72 | }; 73 | }; 74 | }, 75 | ))); 76 | 77 | Ok(()) 78 | } 79 | 80 | pub fn request_current_state( 81 | context: Rc>, 82 | info_sxx: mpsc::UnboundedSender, 83 | ) -> Result<()> { 84 | info!("[PAInterface] Requesting starting state"); 85 | 86 | let introspector = context.borrow_mut().introspect(); 87 | 88 | let info_sx = info_sxx.clone(); 89 | introspector.get_sink_info_list(move |x: ListResult<&SinkInfo>| { 90 | if let ListResult::Item(e) = x { 91 | let _ = info_sx 92 | .clone() 93 | .send(EntryIdentifier::new(EntryType::Sink, e.index)); 94 | } 95 | }); 96 | 97 | let info_sx = info_sxx.clone(); 98 | introspector.get_sink_input_info_list(move |x: ListResult<&SinkInputInfo>| { 99 | if let ListResult::Item(e) = x { 100 | let _ = info_sx.send(EntryIdentifier::new(EntryType::SinkInput, e.index)); 101 | } 102 | }); 103 | 104 | let info_sx = info_sxx.clone(); 105 | introspector.get_source_info_list(move |x: ListResult<&SourceInfo>| { 106 | if let ListResult::Item(e) = x { 107 | let _ = info_sx.send(EntryIdentifier::new(EntryType::Source, e.index)); 108 | } 109 | }); 110 | 111 | let info_sx = info_sxx.clone(); 112 | introspector.get_source_output_info_list(move |x: ListResult<&SourceOutputInfo>| { 113 | if let ListResult::Item(e) = x { 114 | let _ = info_sx.send(EntryIdentifier::new(EntryType::SourceOutput, e.index)); 115 | } 116 | }); 117 | 118 | introspector.get_card_info_list(move |x: ListResult<&CardInfo>| { 119 | if let ListResult::Item(e) = x { 120 | let _ = info_sxx.send(EntryIdentifier::new(EntryType::Card, e.index)); 121 | } 122 | }); 123 | 124 | Ok(()) 125 | } 126 | 127 | pub fn request_info( 128 | ident: EntryIdentifier, 129 | context: &Rc>, 130 | info_sx: mpsc::UnboundedSender, 131 | ) { 132 | let introspector = context.borrow_mut().introspect(); 133 | debug!( 134 | "[PAInterface] Requesting info for {:?} {}", 135 | ident.entry_type, ident.index 136 | ); 137 | match ident.entry_type { 138 | EntryType::SinkInput => { 139 | introspector.get_sink_input_info(ident.index, on_sink_input_info(&info_sx)); 140 | } 141 | EntryType::Sink => { 142 | introspector.get_sink_info_by_index(ident.index, on_sink_info(&info_sx)); 143 | } 144 | EntryType::SourceOutput => { 145 | introspector.get_source_output_info(ident.index, on_source_output_info(&info_sx)); 146 | } 147 | EntryType::Source => { 148 | introspector.get_source_info_by_index(ident.index, on_source_info(&info_sx)); 149 | } 150 | EntryType::Card => { 151 | introspector.get_card_info_by_index(ident.index, on_card_info); 152 | } 153 | }; 154 | } 155 | pub fn on_card_info(res: ListResult<&CardInfo>) { 156 | if let ListResult::Item(i) = res { 157 | let n = match i 158 | .proplist 159 | .get_str(pulse::proplist::properties::DEVICE_DESCRIPTION) 160 | { 161 | Some(s) => s, 162 | None => String::from(""), 163 | }; 164 | let profiles: Vec = i 165 | .profiles 166 | .iter() 167 | .filter_map(|p| { 168 | p.name.clone().map(|n| CardProfile { 169 | area: Rect::default(), 170 | is_selected: false, 171 | name: n.to_string(), 172 | description: match &p.description { 173 | Some(s) => s.to_string(), 174 | None => n.to_string(), 175 | }, 176 | #[cfg(any(feature = "pa_v13"))] 177 | available: p.available, 178 | }) 179 | }) 180 | .collect(); 181 | 182 | let selected_profile = match &i.active_profile { 183 | Some(x) => { 184 | if let Some(n) = &x.name { 185 | profiles.iter().position(|p| p.name == *n) 186 | } else { 187 | None 188 | } 189 | } 190 | None => None, 191 | }; 192 | 193 | let ident = EntryIdentifier::new(EntryType::Card, i.index); 194 | let entry = Entry::new_card_entry(i.index, n, profiles, selected_profile); 195 | 196 | (*ACTIONS_SX) 197 | .get() 198 | .send(EntryUpdate::EntryUpdate(ident, Box::new(entry))) 199 | .unwrap(); 200 | } 201 | } 202 | 203 | pub fn on_sink_info( 204 | _sx: &mpsc::UnboundedSender, 205 | ) -> impl Fn(ListResult<&SinkInfo>) { 206 | |res: ListResult<&SinkInfo>| { 207 | if let ListResult::Item(i) = res { 208 | debug!("[PADataInterface] Update {} sink info", i.index); 209 | let name = match &i.description { 210 | Some(name) => name.to_string(), 211 | None => String::new(), 212 | }; 213 | let ident = EntryIdentifier::new(EntryType::Sink, i.index); 214 | let entry = Entry::new_play_entry( 215 | EntryType::Sink, 216 | i.index, 217 | name, 218 | None, 219 | i.mute, 220 | i.volume, 221 | Some(i.monitor_source), 222 | None, 223 | i.state == SinkState::Suspended, 224 | ); 225 | 226 | (*ACTIONS_SX) 227 | .get() 228 | .send(EntryUpdate::EntryUpdate(ident, Box::new(entry))) 229 | .unwrap(); 230 | } 231 | } 232 | } 233 | 234 | pub fn on_sink_input_info( 235 | sx: &mpsc::UnboundedSender, 236 | ) -> impl Fn(ListResult<&SinkInputInfo>) { 237 | let info_sx = sx.clone(); 238 | move |res: ListResult<&SinkInputInfo>| { 239 | if let ListResult::Item(i) = res { 240 | debug!("[PADataInterface] Update {} sink input info", i.index); 241 | let n = match i 242 | .proplist 243 | .get_str(pulse::proplist::properties::APPLICATION_NAME) 244 | { 245 | Some(s) => s, 246 | None => match &i.name { 247 | Some(s) => s.to_string(), 248 | None => String::from(""), 249 | }, 250 | }; 251 | let ident = EntryIdentifier::new(EntryType::SinkInput, i.index); 252 | 253 | let entry = Entry::new_play_entry( 254 | EntryType::SinkInput, 255 | i.index, 256 | n, 257 | Some(i.sink), 258 | i.mute, 259 | i.volume, 260 | None, 261 | Some(i.sink), 262 | false, 263 | ); 264 | 265 | (*ACTIONS_SX) 266 | .get() 267 | .send(EntryUpdate::EntryUpdate(ident, Box::new(entry))) 268 | .unwrap(); 269 | let _ = info_sx.send(EntryIdentifier::new(EntryType::Sink, i.sink)); 270 | } 271 | } 272 | } 273 | 274 | pub fn on_source_info( 275 | _sx: &mpsc::UnboundedSender, 276 | ) -> impl Fn(ListResult<&SourceInfo>) { 277 | move |res: ListResult<&SourceInfo>| { 278 | if let ListResult::Item(i) = res { 279 | debug!("[PADataInterface] Update {} source info", i.index); 280 | let name = match &i.description { 281 | Some(name) => name.to_string(), 282 | None => String::new(), 283 | }; 284 | let ident = EntryIdentifier::new(EntryType::Source, i.index); 285 | let entry = Entry::new_play_entry( 286 | EntryType::Source, 287 | i.index, 288 | name, 289 | None, 290 | i.mute, 291 | i.volume, 292 | Some(i.index), 293 | None, 294 | i.state == SourceState::Suspended, 295 | ); 296 | 297 | (*ACTIONS_SX) 298 | .get() 299 | .send(EntryUpdate::EntryUpdate(ident, Box::new(entry))) 300 | .unwrap(); 301 | } 302 | } 303 | } 304 | 305 | pub fn on_source_output_info( 306 | sx: &mpsc::UnboundedSender, 307 | ) -> impl Fn(ListResult<&SourceOutputInfo>) { 308 | let info_sx = sx.clone(); 309 | move |res: ListResult<&SourceOutputInfo>| { 310 | if let ListResult::Item(i) = res { 311 | debug!("[PADataInterface] Update {} source output info", i.index); 312 | let n = match i 313 | .proplist 314 | .get_str(pulse::proplist::properties::APPLICATION_NAME) 315 | { 316 | Some(s) => s, 317 | None => String::from(""), 318 | }; 319 | if n == "RsMixerContext" { 320 | return; 321 | } 322 | let ident = EntryIdentifier::new(EntryType::SourceOutput, i.index); 323 | let entry = Entry::new_play_entry( 324 | EntryType::SourceOutput, 325 | i.index, 326 | n, 327 | Some(i.source), 328 | i.mute, 329 | i.volume, 330 | Some(i.source), 331 | None, 332 | false, 333 | ); 334 | 335 | (*ACTIONS_SX) 336 | .get() 337 | .send(EntryUpdate::EntryUpdate(ident, Box::new(entry))) 338 | .unwrap(); 339 | let _ = info_sx.send(EntryIdentifier::new(EntryType::Source, i.index)); 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/pa/common.rs: -------------------------------------------------------------------------------- 1 | pub use std::{cell::RefCell, collections::HashMap, rc::Rc}; 2 | 3 | pub use pulse::{ 4 | context::{subscribe::Facility, Context as PAContext}, 5 | mainloop::{api::Mainloop as MainloopTrait, threaded::Mainloop}, 6 | stream::Stream, 7 | }; 8 | pub use tokio::sync::mpsc; 9 | 10 | pub use super::{errors::PAError, monitor::Monitors, PAInternal, SPEC}; 11 | pub use crate::{ 12 | entry::{EntryIdentifier, EntryType}, 13 | models::{EntryUpdate, PulseAudioAction}, 14 | prelude::*, 15 | }; 16 | 17 | pub static LOGGING_MODULE: &str = "PAInterface"; 18 | 19 | impl From for EntryType { 20 | fn from(fac: Facility) -> Self { 21 | match fac { 22 | Facility::Sink => EntryType::Sink, 23 | Facility::Source => EntryType::Source, 24 | Facility::SinkInput => EntryType::SinkInput, 25 | Facility::SourceOutput => EntryType::SourceOutput, 26 | Facility::Card => EntryType::Card, 27 | _ => EntryType::Sink, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pa/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use super::PAInternal; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum PAError { 7 | #[error("cannot create pulseaudio mainloop")] 8 | MainloopCreateError, 9 | #[error("cannot connect pulseaudio mainloop")] 10 | MainloopConnectError, 11 | #[error("cannot create pulseaudio stream")] 12 | StreamCreateError, 13 | #[error("internal channel send error")] 14 | ChannelError(#[from] cb_channel::SendError), 15 | #[error("pulseaudio disconnected")] 16 | PulseAudioDisconnected, 17 | } 18 | -------------------------------------------------------------------------------- /src/pa/mod.rs: -------------------------------------------------------------------------------- 1 | mod callbacks; 2 | pub mod common; 3 | mod errors; 4 | mod monitor; 5 | mod pa_actions; 6 | mod pa_interface; 7 | 8 | use common::*; 9 | use lazy_static::lazy_static; 10 | pub use pa_interface::start; 11 | 12 | #[derive(Debug)] 13 | pub enum PAInternal { 14 | Tick, 15 | Command(Box), 16 | AskInfo(EntryIdentifier), 17 | } 18 | 19 | lazy_static! { 20 | pub static ref SPEC: pulse::sample::Spec = pulse::sample::Spec { 21 | format: pulse::sample::Format::FLOAT32NE, 22 | channels: 1, 23 | rate: 1024, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/pa/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use pulse::stream::PeekResult; 4 | 5 | use super::{common::*, pa_interface::ACTIONS_SX}; 6 | use crate::VARIABLES; 7 | 8 | pub struct Monitor { 9 | stream: Rc>, 10 | exit_sender: cb_channel::Sender, 11 | } 12 | 13 | pub struct Monitors { 14 | monitors: HashMap, 15 | errors: HashMap, 16 | } 17 | 18 | impl Default for Monitors { 19 | fn default() -> Self { 20 | Self { 21 | monitors: HashMap::new(), 22 | errors: HashMap::new(), 23 | } 24 | } 25 | } 26 | 27 | impl Monitors { 28 | pub fn filter( 29 | &mut self, 30 | mainloop: &Rc>, 31 | context: &Rc>, 32 | targets: &HashMap>, 33 | ) { 34 | // remove failed streams 35 | // then send exit signal if stream is unwanted 36 | self.monitors.retain(|ident, monitor| { 37 | match monitor.stream.borrow_mut().get_state() { 38 | pulse::stream::State::Terminated | pulse::stream::State::Failed => { 39 | info!( 40 | "[PAInterface] Disconnecting {} sink input monitor (failed state)", 41 | ident.index 42 | ); 43 | return false; 44 | } 45 | _ => {} 46 | }; 47 | 48 | if targets.get(ident) == None { 49 | let _ = monitor.exit_sender.send(0); 50 | } 51 | 52 | true 53 | }); 54 | 55 | targets.iter().for_each(|(ident, monitor_src)| { 56 | if self.monitors.get(ident).is_none() { 57 | self.create_monitor(mainloop, context, *ident, *monitor_src); 58 | } 59 | }); 60 | } 61 | 62 | fn create_monitor( 63 | &mut self, 64 | mainloop: &Rc>, 65 | context: &Rc>, 66 | ident: EntryIdentifier, 67 | monitor_src: Option, 68 | ) { 69 | if let Some(count) = self.errors.get(&ident) { 70 | if *count >= 5 { 71 | self.errors.remove(&ident); 72 | (*ACTIONS_SX) 73 | .get() 74 | .send(EntryUpdate::EntryRemoved(ident)) 75 | .unwrap(); 76 | } 77 | } 78 | if self.monitors.contains_key(&ident) { 79 | return; 80 | } 81 | let (sx, rx) = cb_channel::unbounded(); 82 | if let Ok(stream) = create( 83 | &mainloop, 84 | &context, 85 | &pulse::sample::Spec { 86 | format: pulse::sample::Format::FLOAT32NE, 87 | channels: 1, 88 | rate: (*VARIABLES).get().pa_rate, 89 | }, 90 | ident, 91 | monitor_src, 92 | rx, 93 | ) { 94 | self.monitors.insert( 95 | ident, 96 | Monitor { 97 | stream, 98 | exit_sender: sx, 99 | }, 100 | ); 101 | self.errors.remove(&ident); 102 | } else { 103 | self.error(&ident); 104 | } 105 | } 106 | 107 | fn error(&mut self, ident: &EntryIdentifier) { 108 | let count = match self.errors.get(&ident) { 109 | Some(x) => *x, 110 | None => 0, 111 | }; 112 | self.errors.insert(*ident, count + 1); 113 | } 114 | } 115 | 116 | fn slice_to_4_bytes(slice: &[u8]) -> [u8; 4] { 117 | slice.try_into().expect("slice with incorrect length") 118 | } 119 | 120 | fn create( 121 | p_mainloop: &Rc>, 122 | p_context: &Rc>, 123 | p_spec: &pulse::sample::Spec, 124 | ident: EntryIdentifier, 125 | source_index: Option, 126 | close_rx: cb_channel::Receiver, 127 | ) -> Result>> { 128 | info!("[PADataInterface] Attempting to create new monitor stream"); 129 | 130 | let stream_index = if ident.entry_type == EntryType::SinkInput { 131 | Some(ident.index) 132 | } else { 133 | None 134 | }; 135 | 136 | let stream = Rc::new(RefCell::new( 137 | match Stream::new(&mut p_context.borrow_mut(), "RsMixer monitor", p_spec, None) { 138 | Some(stream) => stream, 139 | None => { 140 | return Err(PAError::StreamCreateError) 141 | .context("while creating stream for monitoring volume"); 142 | } 143 | }, 144 | )); 145 | 146 | // Stream state change callback 147 | { 148 | debug!("[PADataInterface] Registering stream state change callback"); 149 | let ml_ref = Rc::clone(&p_mainloop); 150 | let stream_ref = Rc::downgrade(&stream); 151 | stream 152 | .borrow_mut() 153 | .set_state_callback(Some(Box::new(move || { 154 | let state = unsafe { (*(*stream_ref.as_ptr()).as_ptr()).get_state() }; 155 | match state { 156 | pulse::stream::State::Ready 157 | | pulse::stream::State::Failed 158 | | pulse::stream::State::Terminated => { 159 | unsafe { (*ml_ref.as_ptr()).signal(false) }; 160 | } 161 | _ => {} 162 | } 163 | }))); 164 | } 165 | 166 | // for sink inputs we want to set monitor stream to sink 167 | if let Some(index) = stream_index { 168 | stream.borrow_mut().set_monitor_stream(index).unwrap(); 169 | } 170 | 171 | let x; 172 | let mut s = None; 173 | if let Some(i) = source_index { 174 | x = i.to_string(); 175 | s = Some(x.as_str()); 176 | } 177 | 178 | debug!("[PADataInterface] Connecting stream"); 179 | match stream.borrow_mut().connect_record( 180 | s, 181 | Some(&pulse::def::BufferAttr { 182 | maxlength: std::u32::MAX, 183 | tlength: std::u32::MAX, 184 | prebuf: std::u32::MAX, 185 | minreq: 0, 186 | fragsize: (*VARIABLES).get().pa_frag_size, 187 | }), 188 | pulse::stream::FlagSet::PEAK_DETECT | pulse::stream::FlagSet::ADJUST_LATENCY, 189 | ) { 190 | Ok(_) => {} 191 | Err(_) => { 192 | return Err(PAError::StreamCreateError) 193 | .context("while connecting stream for monitoring volume"); 194 | } 195 | }; 196 | 197 | debug!("[PADataInterface] Waiting for stream to be ready"); 198 | loop { 199 | match stream.borrow_mut().get_state() { 200 | pulse::stream::State::Ready => { 201 | break; 202 | } 203 | pulse::stream::State::Failed | pulse::stream::State::Terminated => { 204 | error!("[PADataInterface] Stream state failed/terminated"); 205 | return Err(PAError::StreamCreateError).context("stream terminated"); 206 | } 207 | _ => { 208 | p_mainloop.borrow_mut().wait(); 209 | } 210 | } 211 | } 212 | 213 | stream.borrow_mut().set_state_callback(None); 214 | 215 | { 216 | info!("[PADataInterface] Registering stream read callback"); 217 | let ml_ref = Rc::clone(&p_mainloop); 218 | let stream_ref = Rc::downgrade(&stream); 219 | stream.borrow_mut().set_read_callback(Some(Box::new(move |_size: usize| { 220 | let remove_failed = || { 221 | error!("[PADataInterface] Monitor failed or terminated"); 222 | }; 223 | let disconnect_stream = || { 224 | warn!("[PADataInterface] {:?} Monitor existed while the sink (input)/source (output) was already gone", ident); 225 | unsafe { 226 | (*(*stream_ref.as_ptr()).as_ptr()).disconnect().unwrap(); 227 | (*ml_ref.as_ptr()).signal(false); 228 | }; 229 | }; 230 | 231 | if close_rx.try_recv().is_ok() { 232 | disconnect_stream(); 233 | return; 234 | } 235 | 236 | match unsafe {(*(*stream_ref.as_ptr()).as_ptr()).get_state() }{ 237 | pulse::stream::State::Failed => { 238 | remove_failed(); 239 | }, 240 | pulse::stream::State::Terminated => { 241 | remove_failed(); 242 | }, 243 | pulse::stream::State::Ready => { 244 | match unsafe{ (*(*stream_ref.as_ptr()).as_ptr()).peek() } { 245 | Ok(res) => match res { 246 | PeekResult::Data(data) => { 247 | let count = data.len() / 4; 248 | let mut peak = 0.0; 249 | for c in 0..count { 250 | let data_slice = slice_to_4_bytes(&data[c * 4 .. (c + 1) * 4]); 251 | peak += f32::from_ne_bytes(data_slice).abs(); 252 | } 253 | peak = peak / count as f32; 254 | 255 | if (*ACTIONS_SX).get().send(EntryUpdate::PeakVolumeUpdate(ident, peak)).is_err() { 256 | disconnect_stream(); 257 | } 258 | 259 | unsafe { (*(*stream_ref.as_ptr()).as_ptr()).discard().unwrap(); }; 260 | }, 261 | PeekResult::Hole(_) => { 262 | unsafe { (*(*stream_ref.as_ptr()).as_ptr()).discard().unwrap(); }; 263 | }, 264 | _ => {}, 265 | }, 266 | Err(_) => { 267 | remove_failed(); 268 | }, 269 | } 270 | }, 271 | _ => {}, 272 | }; 273 | // unsafe {(*ml_ref.get_mut().unwrap()).signal(false)}; 274 | }))); 275 | } 276 | 277 | Ok(stream) 278 | } 279 | -------------------------------------------------------------------------------- /src/pa/pa_actions.rs: -------------------------------------------------------------------------------- 1 | use super::{callbacks, common::*}; 2 | 3 | pub fn handle_command( 4 | cmd: PulseAudioAction, 5 | context: &Rc>, 6 | info_sx: &mpsc::UnboundedSender, 7 | ) -> Option<()> { 8 | match cmd { 9 | PulseAudioAction::RequestPulseAudioState => { 10 | callbacks::request_current_state(Rc::clone(&context), info_sx.clone()).unwrap(); 11 | } 12 | PulseAudioAction::MuteEntry(ident, mute) => { 13 | set_mute(ident, mute, &context); 14 | } 15 | PulseAudioAction::MoveEntryToParent(ident, parent) => { 16 | move_entry_to_parent(ident, parent, &context, info_sx.clone()); 17 | } 18 | PulseAudioAction::ChangeCardProfile(ident, profile) => { 19 | change_card_profile(ident, profile, &context); 20 | } 21 | PulseAudioAction::SetVolume(ident, vol) => { 22 | set_volume(ident, vol, &context); 23 | } 24 | PulseAudioAction::SetSuspend(ident, suspend) => { 25 | set_suspend(ident, suspend, &context); 26 | } 27 | PulseAudioAction::KillEntry(ident) => { 28 | kill_entry(ident, &context); 29 | } 30 | PulseAudioAction::Shutdown => { 31 | //@TODO disconnect monitors 32 | return None; 33 | } 34 | _ => {} 35 | }; 36 | Some(()) 37 | } 38 | 39 | fn set_volume( 40 | ident: EntryIdentifier, 41 | vol: pulse::volume::ChannelVolumes, 42 | context: &Rc>, 43 | ) { 44 | let mut introspector = context.borrow_mut().introspect(); 45 | match ident.entry_type { 46 | EntryType::Sink => { 47 | introspector.set_sink_volume_by_index(ident.index, &vol, None); 48 | } 49 | EntryType::SinkInput => { 50 | introspector.set_sink_input_volume(ident.index, &vol, None); 51 | } 52 | EntryType::Source => { 53 | introspector.set_source_volume_by_index(ident.index, &vol, None); 54 | } 55 | EntryType::SourceOutput => { 56 | introspector.set_source_output_volume(ident.index, &vol, None); 57 | } 58 | _ => {} 59 | }; 60 | } 61 | 62 | fn change_card_profile(ident: EntryIdentifier, profile: String, context: &Rc>) { 63 | if ident.entry_type != EntryType::Card { 64 | return; 65 | } 66 | context 67 | .borrow_mut() 68 | .introspect() 69 | .set_card_profile_by_index(ident.index, &profile[..], None); 70 | } 71 | 72 | fn move_entry_to_parent( 73 | ident: EntryIdentifier, 74 | parent: EntryIdentifier, 75 | context: &Rc>, 76 | info_sx: mpsc::UnboundedSender, 77 | ) { 78 | let mut introspector = context.borrow_mut().introspect(); 79 | 80 | match ident.entry_type { 81 | EntryType::SinkInput => { 82 | introspector.move_sink_input_by_index( 83 | ident.index, 84 | parent.index, 85 | Some(Box::new(move |_| { 86 | info_sx.send(parent).unwrap(); 87 | info_sx.send(ident).unwrap(); 88 | })), 89 | ); 90 | } 91 | EntryType::SourceOutput => { 92 | introspector.move_source_output_by_index( 93 | ident.index, 94 | parent.index, 95 | Some(Box::new(move |_| { 96 | info_sx.send(parent).unwrap(); 97 | info_sx.send(ident).unwrap(); 98 | })), 99 | ); 100 | } 101 | _ => {} 102 | }; 103 | } 104 | 105 | fn set_suspend(ident: EntryIdentifier, suspend: bool, context: &Rc>) { 106 | let mut introspector = context.borrow_mut().introspect(); 107 | match ident.entry_type { 108 | EntryType::Sink => { 109 | introspector.suspend_sink_by_index(ident.index, suspend, None); 110 | } 111 | EntryType::Source => { 112 | introspector.suspend_source_by_index(ident.index, suspend, None); 113 | } 114 | _ => {} 115 | }; 116 | } 117 | 118 | fn kill_entry(ident: EntryIdentifier, context: &Rc>) { 119 | let mut introspector = context.borrow_mut().introspect(); 120 | match ident.entry_type { 121 | EntryType::SinkInput => { 122 | introspector.kill_sink_input(ident.index, |_| {}); 123 | } 124 | EntryType::SourceOutput => { 125 | introspector.kill_source_output(ident.index, |_| {}); 126 | } 127 | _ => {} 128 | }; 129 | } 130 | 131 | fn set_mute(ident: EntryIdentifier, mute: bool, context: &Rc>) { 132 | let mut introspector = context.borrow_mut().introspect(); 133 | match ident.entry_type { 134 | EntryType::Sink => { 135 | introspector.set_sink_mute_by_index(ident.index, mute, None); 136 | } 137 | EntryType::SinkInput => { 138 | introspector.set_sink_input_mute(ident.index, mute, None); 139 | } 140 | EntryType::Source => { 141 | introspector.set_source_mute_by_index(ident.index, mute, None); 142 | } 143 | EntryType::SourceOutput => { 144 | introspector.set_source_output_mute(ident.index, mute, None); 145 | } 146 | _ => {} 147 | }; 148 | } 149 | -------------------------------------------------------------------------------- /src/pa/pa_interface.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use lazy_static::lazy_static; 4 | use pulse::proplist::Proplist; 5 | use state::Storage; 6 | 7 | use super::{callbacks, common::*, pa_actions}; 8 | 9 | lazy_static! { 10 | pub static ref ACTIONS_SX: Storage> = Storage::new(); 11 | } 12 | 13 | pub fn start( 14 | internal_rx: cb_channel::Receiver, 15 | info_sx: mpsc::UnboundedSender, 16 | actions_sx: mpsc::UnboundedSender, 17 | ) -> Result<()> { 18 | (*ACTIONS_SX).set(actions_sx); 19 | 20 | // Create new mainloop and context 21 | let mut proplist = Proplist::new().unwrap(); 22 | proplist 23 | .set_str(pulse::proplist::properties::APPLICATION_NAME, "RsMixer") 24 | .unwrap(); 25 | 26 | debug!("[PAInterface] Creating new mainloop"); 27 | let mainloop = Rc::new(RefCell::new(match Mainloop::new() { 28 | Some(ml) => ml, 29 | None => { 30 | error!("[PAInterface] Error while creating new mainloop"); 31 | return Err(PAError::MainloopCreateError.into()); 32 | } 33 | })); 34 | 35 | debug!("[PAInterface] Creating new context"); 36 | let context = Rc::new(RefCell::new( 37 | match PAContext::new_with_proplist( 38 | mainloop.borrow_mut().deref(), 39 | "RsMixerContext", 40 | &proplist, 41 | ) { 42 | Some(ctx) => ctx, 43 | None => { 44 | error!("[PAInterface] Error while creating new context"); 45 | return Err(PAError::MainloopCreateError.into()); 46 | } 47 | }, 48 | )); 49 | 50 | // PAContext state change callback 51 | { 52 | debug!("[PAInterface] Registering state change callback"); 53 | let ml_ref = Rc::clone(&mainloop); 54 | let context_ref = Rc::clone(&context); 55 | context 56 | .borrow_mut() 57 | .set_state_callback(Some(Box::new(move || { 58 | let state = unsafe { (*context_ref.as_ptr()).get_state() }; 59 | if matches!( 60 | state, 61 | pulse::context::State::Ready 62 | | pulse::context::State::Failed 63 | | pulse::context::State::Terminated 64 | ) { 65 | unsafe { (*ml_ref.as_ptr()).signal(false) }; 66 | } 67 | }))); 68 | } 69 | 70 | // Try to connect to pulseaudio 71 | debug!("[PAInterface] Connecting context"); 72 | 73 | if context 74 | .borrow_mut() 75 | .connect(None, pulse::context::FlagSet::NOFLAGS, None) 76 | .is_err() 77 | { 78 | error!("[PAInterface] Error while connecting context"); 79 | return Err(PAError::MainloopConnectError.into()); 80 | } 81 | 82 | info!("[PAInterface] Starting mainloop"); 83 | 84 | // start mainloop 85 | mainloop.borrow_mut().lock(); 86 | if mainloop.borrow_mut().start().is_err() { 87 | return Err(PAError::MainloopConnectError.into()); 88 | } 89 | 90 | debug!("[PAInterface] Waiting for context to be ready..."); 91 | // wait for context to be ready 92 | loop { 93 | match context.borrow_mut().get_state() { 94 | pulse::context::State::Ready => { 95 | break; 96 | } 97 | pulse::context::State::Failed | pulse::context::State::Terminated => { 98 | mainloop.borrow_mut().unlock(); 99 | mainloop.borrow_mut().stop(); 100 | error!("[PAInterface] Connection failed or context terminated"); 101 | return Err(PAError::MainloopConnectError.into()); 102 | } 103 | _ => { 104 | mainloop.borrow_mut().wait(); 105 | } 106 | } 107 | } 108 | debug!("[PAInterface] PAContext ready"); 109 | 110 | context.borrow_mut().set_state_callback(None); 111 | 112 | callbacks::subscribe(&context, info_sx.clone())?; 113 | callbacks::request_current_state(context.clone(), info_sx.clone())?; 114 | 115 | mainloop.borrow_mut().unlock(); 116 | 117 | debug!("[PAInterface] Actually starting our mainloop"); 118 | 119 | let mut monitors = Monitors::default(); 120 | let mut last_targets = HashMap::new(); 121 | 122 | while let Ok(msg) = internal_rx.recv() { 123 | mainloop.borrow_mut().lock(); 124 | 125 | match context.borrow_mut().get_state() { 126 | pulse::context::State::Ready => {} 127 | _ => { 128 | mainloop.borrow_mut().unlock(); 129 | return Err(PAError::PulseAudioDisconnected).context("disconnected while working"); 130 | } 131 | } 132 | 133 | match msg { 134 | PAInternal::AskInfo(ident) => { 135 | callbacks::request_info(ident, &context, info_sx.clone()); 136 | } 137 | PAInternal::Tick => { 138 | // remove failed monitors 139 | monitors.filter(&mainloop, &context, &last_targets); 140 | } 141 | PAInternal::Command(cmd) => { 142 | let cmd = cmd.deref(); 143 | if pa_actions::handle_command(cmd.clone(), &context, &info_sx).is_none() { 144 | monitors.filter(&mainloop, &context, &HashMap::new()); 145 | mainloop.borrow_mut().unlock(); 146 | break; 147 | } 148 | 149 | if let PulseAudioAction::CreateMonitors(mons) = cmd.clone() { 150 | last_targets = mons; 151 | monitors.filter(&mainloop, &context, &last_targets); 152 | } 153 | } 154 | }; 155 | mainloop.borrow_mut().unlock(); 156 | } 157 | 158 | Ok(()) 159 | } 160 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use anyhow::{Context, Result}; 2 | 3 | pub use crate::{debug, error, info, warn}; 4 | -------------------------------------------------------------------------------- /src/ui/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, HashMap}, 3 | io::Write, 4 | iter::Iterator, 5 | }; 6 | 7 | use crossterm::{ 8 | cursor, queue, 9 | style::{self, ContentStyle}, 10 | }; 11 | 12 | use super::Rect; 13 | use crate::models::Style; 14 | 15 | #[derive(Clone, Copy, PartialEq, Debug, Default)] 16 | pub struct Pixel { 17 | pub text: Option, 18 | pub style: Style, 19 | } 20 | 21 | #[derive(Clone, PartialEq, Debug, Default)] 22 | pub struct Pixels(Vec); 23 | 24 | impl Pixels { 25 | pub fn next(mut self, style: Style, s: char) -> Self { 26 | self.0.push(Pixel { 27 | style, 28 | text: Some(s), 29 | }); 30 | self 31 | } 32 | pub fn string(mut self, style: Style, s: &str) -> Self { 33 | for c in s.chars() { 34 | self.0.push(Pixel { 35 | style, 36 | text: Some(c), 37 | }); 38 | } 39 | self 40 | } 41 | pub fn get_mut(&mut self, index: usize) -> Option<&mut Pixel> { 42 | self.0.get_mut(index) 43 | } 44 | pub fn iter_mut(&mut self) -> impl Iterator + '_ { 45 | self.0.iter_mut() 46 | } 47 | } 48 | 49 | impl From> for Pixels { 50 | fn from(s: Vec) -> Self { 51 | Self { 0: s } 52 | } 53 | } 54 | impl From for Vec { 55 | fn from(s: Pixels) -> Self { 56 | s.0 57 | } 58 | } 59 | 60 | pub struct Buffer { 61 | pub width: u16, 62 | pub height: u16, 63 | pixels: Vec, 64 | changes: BTreeMap, 65 | pub styles: HashMap, 66 | } 67 | 68 | impl Default for Buffer { 69 | fn default() -> Self { 70 | Self { 71 | width: 0, 72 | height: 0, 73 | pixels: Vec::new(), 74 | changes: BTreeMap::new(), 75 | styles: HashMap::new(), 76 | } 77 | } 78 | } 79 | 80 | impl Buffer { 81 | pub fn set_styles(&mut self, styles: HashMap) { 82 | self.styles = styles; 83 | } 84 | 85 | pub fn resize(&mut self, width: u16, height: u16) { 86 | self.width = width; 87 | self.height = height; 88 | self.pixels = (0..width * height).map(|_| Pixel::default()).collect(); 89 | } 90 | 91 | pub fn draw_changes(&mut self, stdout: &mut W) -> Result<(), crossterm::ErrorKind> { 92 | let mut last_style = None; 93 | let mut last_coord = None; 94 | let mut text = "".to_string(); 95 | 96 | for (k, v) in self.changes.iter() { 97 | if let Some(pixel) = self.pixels.get(*k) { 98 | if *pixel != *v { 99 | self.pixels[*k] = *v; 100 | } else { 101 | continue; 102 | } 103 | } else { 104 | continue; 105 | } 106 | 107 | if last_style != Some(v.style) || *k == 0 || last_coord != Some(*k - 1) { 108 | if !text.is_empty() { 109 | let style = match self.styles.get(&last_style.unwrap()) { 110 | Some(s) => *s, 111 | None => ContentStyle::default(), 112 | }; 113 | 114 | queue!(stdout, style::PrintStyledContent(style.apply(text)))?; 115 | } 116 | 117 | let (x, y) = self.coord_to_xy(*k); 118 | queue!(stdout, cursor::MoveTo(x, y))?; 119 | 120 | text = v.text.unwrap_or(' ').to_string(); 121 | last_style = Some(v.style); 122 | } else { 123 | text = format!("{}{}", text, v.text.unwrap_or(' ')); 124 | } 125 | 126 | last_coord = Some(*k); 127 | } 128 | 129 | if !text.is_empty() { 130 | let style = match self.styles.get(&last_style.unwrap()) { 131 | Some(s) => *s, 132 | None => ContentStyle::default(), 133 | }; 134 | 135 | queue!(stdout, style::PrintStyledContent(style.apply(text)))?; 136 | } 137 | 138 | self.changes.clear(); 139 | 140 | stdout.flush()?; 141 | 142 | Ok(()) 143 | } 144 | 145 | fn coord_to_xy(&self, coord: usize) -> (u16, u16) { 146 | let y = (coord as f32 / self.width as f32).floor() as usize; 147 | let x = coord - (y * self.width as usize); 148 | (x as u16, y as u16) 149 | } 150 | 151 | fn xy_to_coord(&self, x: u16, y: u16) -> usize { 152 | (y * self.width + x) as usize 153 | } 154 | 155 | pub fn rect(&mut self, rect: Rect, text: char, style: Style) { 156 | let text: String = (0..rect.width).map(|_| text).collect(); 157 | for y in 0..rect.height { 158 | self.string(rect.x, rect.y + y, text.clone(), style); 159 | } 160 | } 161 | 162 | pub fn string(&mut self, x: u16, y: u16, text: String, style: Style) { 163 | let coord = self.xy_to_coord(x, y); 164 | 165 | for (i, c) in text.chars().enumerate() { 166 | if i + coord >= self.pixels.len() { 167 | break; 168 | } 169 | 170 | self.pixel( 171 | coord + i, 172 | Pixel { 173 | text: Some(c), 174 | style, 175 | }, 176 | ); 177 | } 178 | } 179 | 180 | pub fn pixels(&mut self, x: u16, y: u16, pixels: &Pixels) { 181 | let coord = self.xy_to_coord(x, y); 182 | 183 | for (i, p) in pixels.0.iter().enumerate() { 184 | if i + coord >= self.pixels.len() { 185 | break; 186 | } 187 | 188 | self.pixel(coord + i, *p); 189 | } 190 | } 191 | 192 | pub fn pixel(&mut self, coord: usize, pixel: Pixel) { 193 | if self.pixels[coord] != pixel { 194 | self.changes.insert(coord, pixel); 195 | } else { 196 | self.changes.remove(&coord); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/ui/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum UIError { 5 | #[error("terminal window is too small")] 6 | TerminalTooSmall, 7 | #[error("crossterm terminal error")] 8 | TerminalError(#[from] crossterm::ErrorKind), 9 | #[error("terminal io error")] 10 | IoError(#[from] std::io::Error), 11 | } 12 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | mod buffer; 2 | mod errors; 3 | mod rect; 4 | mod scrollable; 5 | pub mod util; 6 | pub mod widgets; 7 | 8 | use std::io::Write; 9 | 10 | use buffer::{Buffer, Pixel, Pixels}; 11 | pub use errors::UIError; 12 | pub use rect::Rect; 13 | pub use scrollable::Scrollable; 14 | pub use util::{clean_terminal, entry_height, prepare_terminal}; 15 | use widgets::{BlockWidget, Widget}; 16 | 17 | use crate::{ 18 | models::{PageType, RSState, Style, UIMode}, 19 | prelude::*, 20 | }; 21 | 22 | pub async fn redraw(stdout: &mut W, state: &mut RSState) -> Result<()> { 23 | make_changes(state).await?; 24 | 25 | state.ui.buffer.draw_changes(stdout)?; 26 | 27 | Ok(()) 28 | } 29 | pub async fn make_changes(state: &mut RSState) -> Result<()> { 30 | if state.redraw.resize { 31 | state.ui.terminal_too_small = match resize(state) { 32 | Ok(()) => false, 33 | Err(err) => match err.source() { 34 | Some(source) => match source.downcast_ref::() { 35 | Some(UIError::TerminalTooSmall) => true, 36 | _ => { 37 | return Err(err); 38 | } 39 | }, 40 | None => { 41 | return Err(err); 42 | } 43 | }, 44 | }; 45 | } 46 | 47 | if state.ui.terminal_too_small { 48 | state.warning_text.text = "Terminal too small".to_string(); 49 | state.warning_text.render(&mut state.ui.buffer)?; 50 | 51 | return Ok(()); 52 | } 53 | 54 | if let UIMode::RetryIn(time) = state.ui_mode { 55 | state.warning_text.text = format!("PulseAudio disconnected. Retrying in {}...", time); 56 | state.warning_text.render(&mut state.ui.buffer)?; 57 | 58 | return Ok(()); 59 | } 60 | 61 | if state.redraw.resize { 62 | state.ui.border.title_pixels = Some(gen_page_names(state)); 63 | state.ui.border.render(&mut state.ui.buffer)?; 64 | } 65 | 66 | let only_affected = 67 | !state.redraw.resize && !state.redraw.entries && !state.redraw.affected_entries.is_empty(); 68 | 69 | if state.redraw.resize || state.redraw.entries || only_affected { 70 | let indexes_to_redraw = state 71 | .page_entries 72 | .visible_range(state.ui.entries_area.height) 73 | .filter(|i| !only_affected || state.redraw.affected_entries.get(i).is_some()) 74 | .collect::>(); 75 | 76 | for i in &indexes_to_redraw { 77 | let ident = state.page_entries.get(*i).unwrap(); 78 | if let Some(entry) = state.entries.get_mut(&ident) { 79 | entry.position = state.page_entries.lvls[*i]; 80 | entry.is_selected = state.page_entries.selected() == *i; 81 | 82 | entry.render(&mut state.ui.buffer)?; 83 | } 84 | } 85 | 86 | if !indexes_to_redraw.is_empty() 87 | && indexes_to_redraw.last().unwrap() + 1 == state.page_entries.len() 88 | { 89 | let last_entry_ident = state 90 | .page_entries 91 | .get(*indexes_to_redraw.last().unwrap()) 92 | .unwrap(); 93 | 94 | if let Some(entry) = state.entries.get_mut(&last_entry_ident) { 95 | let area = entry.area(); 96 | 97 | let bottom = Rect::new( 98 | area.x, 99 | area.y + area.height, 100 | area.width, 101 | state.ui.entries_area.height - area.y - area.height, 102 | ); 103 | 104 | state.ui.buffer.rect(bottom, ' ', Style::Normal); 105 | } 106 | } 107 | } 108 | 109 | if let Some(index) = state.redraw.peak_volume { 110 | if state 111 | .page_entries 112 | .visible_range(state.ui.entries_area.height) 113 | .any(|i| i == index) 114 | { 115 | if let Some(play) = state 116 | .entries 117 | .get_play_entry_mut(&state.page_entries.get(index).unwrap()) 118 | { 119 | play.peak_volume_bar = play.peak_volume_bar.volume(play.peak); 120 | play.peak_volume_bar.small_render(&mut state.ui.buffer)?; 121 | } 122 | } 123 | } 124 | 125 | match state.ui_mode { 126 | UIMode::Help => state.help.render(&mut state.ui.buffer)?, 127 | UIMode::ContextMenu => state.context_menu.render(&mut state.ui.buffer)?, 128 | UIMode::InputVolumeValue => state.input_exact_volume.render(&mut state.ui.buffer)?, 129 | _ => {} 130 | }; 131 | 132 | Ok(()) 133 | } 134 | 135 | pub struct UI { 136 | pub buffer: Buffer, 137 | pub border: BlockWidget, 138 | pub entries_area: Rect, 139 | pub terminal_too_small: bool, 140 | pub pages_names: Vec, 141 | } 142 | 143 | impl Default for UI { 144 | fn default() -> Self { 145 | Self { 146 | buffer: Buffer::default(), 147 | border: BlockWidget::default().clean_inside(true), 148 | entries_area: Rect::default(), 149 | terminal_too_small: false, 150 | pages_names: vec![ 151 | PageType::Output.to_string(), 152 | PageType::Input.to_string(), 153 | PageType::Cards.to_string(), 154 | ], 155 | } 156 | } 157 | } 158 | 159 | fn resize(state: &mut RSState) -> Result<()> { 160 | let (x, y) = crossterm::terminal::size()?; 161 | state.ui.buffer.resize(x, y); 162 | 163 | state.ui.border.resize(Rect::new( 164 | 0, 165 | 0, 166 | state.ui.buffer.width, 167 | state.ui.buffer.height, 168 | ))?; 169 | 170 | state.ui.entries_area = Rect::new(2, 2, state.ui.buffer.width - 4, state.ui.buffer.height - 4); 171 | let mut entry_area = state.ui.entries_area; 172 | 173 | for i in state 174 | .page_entries 175 | .visible_range(state.ui.entries_area.height) 176 | { 177 | let ent = match state.entries.get_mut(&state.page_entries.get(i).unwrap()) { 178 | Some(x) => x, 179 | None => { 180 | continue; 181 | } 182 | }; 183 | ent.position = state.page_entries.lvls[i]; 184 | 185 | entry_area = entry_area.h(entry_height(ent.position)); 186 | 187 | ent.resize(entry_area)?; 188 | 189 | entry_area.y += entry_height(ent.position); 190 | } 191 | 192 | state.context_menu.resize(state.ui.entries_area)?; 193 | 194 | if state.ui_mode == UIMode::InputVolumeValue { 195 | if let Some(ident) = &state.page_entries.get_selected() { 196 | if let Some(play) = state.entries.get_play_entry(ident) { 197 | state.input_exact_volume.resize(play.area)?; 198 | } 199 | } 200 | } 201 | 202 | state.help.resize(state.ui.entries_area)?; 203 | 204 | Ok(()) 205 | } 206 | 207 | fn gen_page_names(state: &mut RSState) -> Pixels { 208 | if state.ui.buffer.width as usize 209 | > 2 + state.ui.pages_names.iter().map(|p| p.len()).sum::() + 6 210 | { 211 | let style = |i: usize| { 212 | if i as i8 == state.current_page.into() { 213 | Style::Bold 214 | } else { 215 | Style::Muted 216 | } 217 | }; 218 | 219 | Pixels::default() 220 | .string(style(0), &state.ui.pages_names[0]) 221 | .string(Style::Muted, " / ") 222 | .string(style(1), &state.ui.pages_names[1]) 223 | .string(Style::Muted, " / ") 224 | .string(style(2), &state.ui.pages_names[2]) 225 | } else { 226 | Pixels::default().string( 227 | Style::Bold, 228 | &state.ui.pages_names[state.page_entries.selected()], 229 | ) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/ui/rect.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq)] 2 | pub struct Rect { 3 | pub x: u16, 4 | pub y: u16, 5 | pub width: u16, 6 | pub height: u16, 7 | } 8 | 9 | impl Rect { 10 | pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self { 11 | Self { 12 | x, 13 | y, 14 | width, 15 | height, 16 | } 17 | } 18 | pub fn default() -> Self { 19 | Self { 20 | x: 0, 21 | y: 0, 22 | width: 0, 23 | height: 0, 24 | } 25 | } 26 | pub fn x(&self, x: u16) -> Self { 27 | Self::new(x, self.y, self.width, self.height) 28 | } 29 | pub fn y(&self, y: u16) -> Self { 30 | Self::new(self.x, y, self.width, self.height) 31 | } 32 | pub fn w(&self, w: u16) -> Self { 33 | Self::new(self.x, self.y, w, self.height) 34 | } 35 | pub fn h(&self, h: u16) -> Self { 36 | Self::new(self.x, self.y, self.width, h) 37 | } 38 | pub fn intersects(&self, rect: &Rect) -> bool { 39 | self.x < rect.x + rect.width 40 | && rect.x < self.x + self.width 41 | && self.y < rect.y + rect.height 42 | && rect.y < self.y + self.height 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ui/scrollable.rs: -------------------------------------------------------------------------------- 1 | pub trait Scrollable { 2 | fn selected(&self) -> usize; 3 | fn len(&self) -> usize; 4 | fn set_selected(&mut self, selected: usize) -> bool; 5 | fn element_height(&self, index: usize) -> u16; 6 | fn visible_range(&self, height: u16) -> Box>; 7 | fn visible_start_end(&self, height: u16) -> (usize, usize); 8 | fn up(&mut self, how_much: usize); 9 | fn down(&mut self, how_much: usize); 10 | } 11 | 12 | #[macro_export] 13 | macro_rules! scrollable { 14 | ($x:ty, $($y:item),*) => { 15 | impl Scrollable for $x { 16 | fn up(&mut self, how_much: usize) { 17 | let how_much = how_much % self.len(); 18 | 19 | self.set_selected((self.len() + self.selected() - how_much) % self.len()); 20 | } 21 | fn down(&mut self, how_much: usize) { 22 | let how_much = how_much % self.len(); 23 | 24 | self.set_selected((how_much + self.selected()) % self.len()); 25 | } 26 | fn visible_range(&self, height: u16) -> Box> { 27 | let (a, b) = self.visible_start_end(height); 28 | 29 | Box::new((a..b).into_iter()) 30 | } 31 | fn visible_start_end(&self, height: u16) -> (usize, usize) { 32 | let mut last_first = 0; 33 | let mut current_height = 0; 34 | let mut i = 0; 35 | while i < self.len() { 36 | let eh = self.element_height(i); 37 | if current_height + eh > height { 38 | if i > self.selected() { 39 | break; 40 | } 41 | current_height = eh; 42 | last_first = i; 43 | } else { 44 | current_height += eh; 45 | } 46 | 47 | i += 1; 48 | } 49 | 50 | (last_first, i) 51 | } 52 | $($y)* 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ui/util.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::{cursor::Hide, execute}; 4 | 5 | use crate::{entry::EntrySpaceLvl, ui::UIError}; 6 | 7 | pub fn entry_height(lvl: EntrySpaceLvl) -> u16 { 8 | if lvl == EntrySpaceLvl::Card { 9 | 1 10 | } else if lvl == EntrySpaceLvl::ParentNoChildren || lvl == EntrySpaceLvl::LastChild { 11 | 4 12 | } else { 13 | 3 14 | } 15 | } 16 | 17 | pub fn prepare_terminal() -> Result { 18 | let mut stdout = io::stdout(); 19 | crossterm::execute!( 20 | stdout, 21 | crossterm::terminal::EnterAlternateScreen, 22 | crossterm::event::EnableMouseCapture 23 | )?; 24 | crossterm::terminal::enable_raw_mode()?; 25 | execute!(stdout, Hide)?; 26 | 27 | Ok(stdout) 28 | } 29 | 30 | pub fn clean_terminal() -> Result<(), UIError> { 31 | let mut stdout = std::io::stdout(); 32 | crossterm::execute!( 33 | stdout, 34 | crossterm::cursor::Show, 35 | crossterm::terminal::LeaveAlternateScreen, 36 | crossterm::event::DisableMouseCapture 37 | )?; 38 | crossterm::terminal::disable_raw_mode()?; 39 | 40 | Ok(()) 41 | } 42 | 43 | #[macro_export] 44 | macro_rules! repeat { 45 | ($char:expr, $times:expr) => { 46 | (0..$times).map(|_| $char).collect::() 47 | }; 48 | } 49 | 50 | #[macro_export] 51 | macro_rules! format_text { 52 | ($char:expr, $($style:expr),*) => { 53 | { 54 | let mut v = Vec::new(); 55 | let styles = vec![$($style),*]; 56 | let mut active_style = 0; 57 | let mut word = $char.chars().peekable(); 58 | while let Some(cur) = word.next() { 59 | if cur == '{' && word.peek() == Some(&'}') { 60 | active_style += 1; 61 | word.next(); 62 | continue; 63 | } 64 | 65 | v.push(Pixel { 66 | style: styles[active_style], 67 | text: Some(cur), 68 | }); 69 | } 70 | 71 | v 72 | } 73 | } 74 | } 75 | #[macro_export] 76 | macro_rules! format_text2 { 77 | ($($x:tt)*) => { 78 | let res = format_text_intern!(format_args!($($x:tt)*)); 79 | res 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/ui/widgets/block.rs: -------------------------------------------------------------------------------- 1 | use super::Widget; 2 | use crate::{ 3 | models::Style, 4 | prelude::*, 5 | repeat, 6 | ui::{Buffer, Pixels, Rect, UIError}, 7 | }; 8 | 9 | #[derive(Clone)] 10 | pub struct BlockWidget { 11 | pub area: Rect, 12 | pub title: Option, 13 | pub title_pixels: Option, 14 | pub clean_inside: bool, 15 | } 16 | 17 | impl Default for BlockWidget { 18 | fn default() -> Self { 19 | Self { 20 | title: None, 21 | title_pixels: None, 22 | clean_inside: false, 23 | area: Rect::default(), 24 | } 25 | } 26 | } 27 | 28 | impl BlockWidget { 29 | pub fn clean_inside(mut self, clean: bool) -> Self { 30 | self.clean_inside = clean; 31 | self 32 | } 33 | } 34 | 35 | impl Widget for BlockWidget { 36 | fn resize(&mut self, area: Rect) -> Result<()> { 37 | if area.width < 2 || area.height < 2 { 38 | return Err(UIError::TerminalTooSmall.into()); 39 | } 40 | 41 | self.area = area; 42 | 43 | Ok(()) 44 | } 45 | fn render(&mut self, buffer: &mut Buffer) -> Result<()> { 46 | let mut top_border = Pixels::default().string( 47 | Style::Normal, 48 | &format!("┌{}┐", repeat!("─", self.area.width - 2)), 49 | ); 50 | 51 | if let Some(title) = &self.title { 52 | for (i, c) in title.chars().enumerate() { 53 | if let Some(pixel) = top_border.get_mut(i + 1) { 54 | pixel.text = Some(c); 55 | } else { 56 | break; 57 | } 58 | } 59 | } else if let Some(title) = &mut self.title_pixels { 60 | for (i, p) in title.iter_mut().enumerate() { 61 | if let Some(pixel) = top_border.get_mut(i + 1) { 62 | *pixel = *p; 63 | } else { 64 | break; 65 | } 66 | } 67 | } 68 | buffer.pixels(self.area.x, self.area.y, &top_border); 69 | 70 | if self.clean_inside { 71 | let mut middle = Pixels::default().next(Style::Normal, '│'); 72 | for _ in 0..(self.area.width - 2) { 73 | middle = middle.next(Style::Normal, ' '); 74 | } 75 | middle = middle.next(Style::Normal, '│'); 76 | 77 | for i in 1..(self.area.height - 1) { 78 | buffer.pixels(self.area.x, self.area.y + i, &middle); 79 | } 80 | } else { 81 | for i in 1..(self.area.height - 1) { 82 | buffer.string(self.area.x, self.area.y + i, "│".to_string(), Style::Normal); 83 | buffer.string( 84 | self.area.x + self.area.width - 1, 85 | self.area.y + i, 86 | "│".to_string(), 87 | Style::Normal, 88 | ); 89 | } 90 | } 91 | 92 | let bottom_border = format!("└{}┘", repeat!("─", self.area.width - 2)); 93 | 94 | buffer.string( 95 | self.area.x, 96 | self.area.y + self.area.height - 1, 97 | bottom_border, 98 | Style::Normal, 99 | ); 100 | 101 | Ok(()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/ui/widgets/context_menu.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::max; 2 | 3 | use super::Widget; 4 | use crate::{ 5 | models::ContextMenu, 6 | prelude::*, 7 | ui::{Buffer, Rect, Scrollable, Style, UIError}, 8 | }; 9 | 10 | impl Widget for ContextMenu { 11 | fn resize(&mut self, area: Rect) -> Result<()> { 12 | let mut longest_word = 0; 13 | self.options.iter().for_each(|o| { 14 | longest_word = max(longest_word, String::from(o.clone()).len()); 15 | }); 16 | 17 | if area.height < 3 || area.width < 4 { 18 | return Err(UIError::TerminalTooSmall.into()); 19 | } 20 | self.tool_window.padding.0 = if area.width < longest_word as u16 + 6 { 21 | 1 22 | } else { 23 | 3 24 | }; 25 | self.tool_window.padding.1 = if area.height < 8 { 1 } else { 2 }; 26 | 27 | self.tool_window.inner_width = longest_word as u16; 28 | self.tool_window.inner_height = self.options.len() as u16; 29 | 30 | self.tool_window.resize(area)?; 31 | 32 | self.area = Rect::new( 33 | self.tool_window.area.x + self.tool_window.padding.0, 34 | self.tool_window.area.y + self.tool_window.padding.1, 35 | self.tool_window.area.width - self.tool_window.padding.0 * 2, 36 | self.tool_window.area.height - self.tool_window.padding.1 * 2, 37 | ); 38 | 39 | Ok(()) 40 | } 41 | fn render(&mut self, buffer: &mut Buffer) -> Result<()> { 42 | self.tool_window.render(buffer)?; 43 | 44 | for (y, i) in self.visible_range(self.area.height).enumerate() { 45 | let text: String = self.options[i].clone().into(); 46 | 47 | let text: String = text 48 | .chars() 49 | .skip(self.horizontal_scroll * self.area.width as usize) 50 | .take(self.area.width as usize) 51 | .collect(); 52 | 53 | let text_x = if self.horizontal_scroll > 0 { 54 | self.area.x 55 | } else { 56 | self.area.x + self.area.width / 2 - text.len() as u16 / 2 57 | }; 58 | buffer.string( 59 | text_x, 60 | self.area.y + y as u16, 61 | text, 62 | if self.selected() == i { 63 | Style::Inverted 64 | } else { 65 | Style::Normal 66 | }, 67 | ); 68 | } 69 | 70 | let (first, last) = self.visible_start_end(self.area.height); 71 | if last - first != self.len() { 72 | if first != 0 { 73 | buffer.string( 74 | self.area.x + self.area.width / 2, 75 | self.area.y - 1, 76 | "▲".to_string(), 77 | Style::Normal, 78 | ); 79 | } 80 | if last != self.len() { 81 | buffer.string( 82 | self.area.x + self.area.width / 2, 83 | self.area.y + self.area.height, 84 | "▼".to_string(), 85 | Style::Normal, 86 | ); 87 | } 88 | } 89 | 90 | let max_horizontal_scroll = self.max_horizontal_scroll(); 91 | if self.horizontal_scroll < max_horizontal_scroll { 92 | buffer.string( 93 | self.area.x + self.area.width, 94 | self.area.y + self.area.height / 2, 95 | "▶".to_string(), 96 | Style::Normal, 97 | ); 98 | } 99 | if self.horizontal_scroll > 0 { 100 | buffer.string( 101 | self.area.x - 1, 102 | self.area.y + self.area.height / 2, 103 | "◀".to_string(), 104 | Style::Normal, 105 | ); 106 | } 107 | 108 | Ok(()) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ui/widgets/entry.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | 3 | use pulse::volume; 4 | 5 | use crate::{ 6 | entry::{CardEntry, Entry, EntryKind, EntrySpaceLvl, HiddenStatus, PlayEntry}, 7 | prelude::*, 8 | ui::{ 9 | widgets::{VolumeWidgetBorder, Widget}, 10 | Buffer, Rect, Style, UIError, 11 | }, 12 | }; 13 | 14 | impl Widget for Entry { 15 | fn resize(&mut self, area: Rect) -> Result<()> { 16 | if area.width < 7 || area.height < 1 { 17 | return Err(UIError::TerminalTooSmall.into()); 18 | } 19 | 20 | match &mut self.entry_kind { 21 | EntryKind::PlayEntry(play) => { 22 | play.position = self.position; 23 | play.resize(area) 24 | } 25 | EntryKind::CardEntry(card) => card.resize(area), 26 | } 27 | } 28 | fn render(&mut self, buffer: &mut Buffer) -> Result<()> { 29 | match &mut self.entry_kind { 30 | EntryKind::PlayEntry(play) => { 31 | play.is_selected = self.is_selected; 32 | play.position = self.position; 33 | 34 | play.render(buffer) 35 | } 36 | EntryKind::CardEntry(card) => { 37 | card.is_selected = self.is_selected; 38 | 39 | card.render(buffer) 40 | } 41 | } 42 | } 43 | } 44 | 45 | impl CardEntry { 46 | pub fn set_is_selected(mut self, is_selected: bool) -> Self { 47 | self.is_selected = is_selected; 48 | self 49 | } 50 | } 51 | 52 | impl PlayEntry { 53 | pub fn set_is_selected(mut self, is_selected: bool) -> Self { 54 | self.is_selected = is_selected; 55 | self 56 | } 57 | pub fn position(mut self, position: EntrySpaceLvl) -> Self { 58 | self.position = position; 59 | self 60 | } 61 | 62 | fn is_volume_visible(&self) -> bool { 63 | let (_, w) = self.text_volume_widths(); 64 | w > 0 65 | } 66 | 67 | fn text_volume_widths(&self) -> (u16, u16) { 68 | let w = self.area.width - self.offset(); 69 | let text_width = if w > 100 { 70 | (w as f32 * 0.3).floor() as u16 71 | } else if w > 35 { 72 | 35 73 | } else { 74 | w 75 | }; 76 | (text_width, w - text_width) 77 | } 78 | 79 | fn play_entry_text_area(&self) -> Rect { 80 | let (w, _) = self.text_volume_widths(); 81 | Rect::new(self.area.x + self.offset(), self.area.y, w, 2) 82 | } 83 | 84 | fn offset(&self) -> u16 { 85 | match self.position { 86 | EntrySpaceLvl::Parent | EntrySpaceLvl::ParentNoChildren => 2, 87 | EntrySpaceLvl::MidChild | EntrySpaceLvl::LastChild => 5, 88 | _ => 0, 89 | } 90 | } 91 | } 92 | 93 | impl Widget for CardEntry { 94 | fn resize(&mut self, area: Rect) -> Result<()> { 95 | self.area = area; 96 | Ok(()) 97 | } 98 | 99 | fn render(&mut self, buffer: &mut Buffer) -> Result<()> { 100 | buffer.rect(self.area, ' ', Style::Normal); 101 | 102 | let style = if self.is_selected { 103 | Style::Bold 104 | } else { 105 | Style::Normal 106 | }; 107 | let name_style = if self.is_selected { 108 | Style::Inverted 109 | } else { 110 | Style::Normal 111 | }; 112 | 113 | let name_len = min(self.name.len(), (self.area.width / 2).into()); 114 | 115 | buffer.string( 116 | self.area.x, 117 | self.area.y, 118 | (&self.name[0..name_len]).to_string(), 119 | name_style, 120 | ); 121 | 122 | if let Some(index) = self.selected_profile { 123 | let profile_len = min( 124 | self.profiles[index].description.len(), 125 | (self.area.width / 2).into(), 126 | ); 127 | 128 | buffer.string( 129 | self.area.x + self.area.width - profile_len as u16, 130 | self.area.y, 131 | (&self.profiles[index].description[0..profile_len]).to_string(), 132 | style, 133 | ); 134 | } 135 | 136 | Ok(()) 137 | } 138 | } 139 | impl Widget for PlayEntry { 140 | fn resize(&mut self, area: Rect) -> Result<()> { 141 | self.area = area; 142 | let (text_width, w) = self.text_volume_widths(); 143 | 144 | if w > 0 { 145 | let volume_area = Rect::new( 146 | self.area.x + text_width + self.offset() + 1, 147 | self.area.y, 148 | w - 2, 149 | 1, 150 | ); 151 | self.volume_bar = self.volume_bar.set_area(volume_area); 152 | } 153 | 154 | let y = match self.position { 155 | EntrySpaceLvl::ParentNoChildren | EntrySpaceLvl::LastChild => { 156 | self.area.y + self.area.height - 2 157 | } 158 | _ => self.area.y + self.area.height - 1, 159 | }; 160 | 161 | self.peak_volume_bar = self.peak_volume_bar.set_area(Rect::new( 162 | self.area.x + self.offset(), 163 | y, 164 | self.area.width - self.offset() - 1, 165 | 1, 166 | )); 167 | 168 | Ok(()) 169 | } 170 | 171 | fn render(&mut self, buffer: &mut Buffer) -> Result<()> { 172 | if self.area.width < 5 || self.area.height < 2 { 173 | return Err(UIError::TerminalTooSmall.into()); 174 | } 175 | 176 | buffer.rect(self.area, ' ', Style::Normal); 177 | 178 | let style = if self.is_selected { 179 | Style::Bold 180 | } else { 181 | Style::Normal 182 | }; 183 | let name_style = if self.is_selected { 184 | Style::Inverted 185 | } else { 186 | Style::Normal 187 | }; 188 | 189 | let text_area = self.play_entry_text_area(); 190 | let short_name = self 191 | .name 192 | .chars() 193 | .take(if text_area.width > 2 { 194 | text_area.width as usize - 2 195 | } else { 196 | 0 197 | }) 198 | .collect::(); 199 | 200 | buffer.string(text_area.x, text_area.y, short_name, name_style); 201 | 202 | let avg = self.volume.avg().0; 203 | let base_delta = (volume::Volume::NORMAL.0 as f32 - volume::Volume::MUTED.0 as f32) / 100.0; 204 | let vol_percent = ((avg - volume::Volume::MUTED.0) as f32 / base_delta).round() as u32; 205 | 206 | if self.is_volume_visible() { 207 | let volume_area = self.volume_bar.area; 208 | self.volume_bar = self 209 | .volume_bar 210 | .volume(vol_percent as f32 / 150.0) 211 | .mute(self.mute) 212 | .border(VolumeWidgetBorder::Upper); 213 | 214 | self.volume_bar.render(buffer)?; 215 | 216 | self.volume_bar.area = self.volume_bar.area.y(volume_area.y + 1); 217 | 218 | self.volume_bar = self.volume_bar.border(VolumeWidgetBorder::Lower); 219 | 220 | self.volume_bar.render(buffer)?; 221 | 222 | self.volume_bar.area = self.volume_bar.area.y(volume_area.y); 223 | 224 | if self.is_selected { 225 | let c = "-".to_string(); 226 | buffer.string(volume_area.x - 1, volume_area.y, c.clone(), style); 227 | buffer.string(volume_area.x - 1, volume_area.y + 1, c.clone(), style); 228 | buffer.string( 229 | volume_area.x + volume_area.width, 230 | volume_area.y, 231 | c.clone(), 232 | style, 233 | ); 234 | buffer.string( 235 | volume_area.x + volume_area.width, 236 | volume_area.y + 1, 237 | c, 238 | style, 239 | ); 240 | } 241 | } 242 | 243 | let vol_perc = format!(" {}", vol_percent); 244 | let vol_perc = String::from(&vol_perc[vol_perc.len() - 3..vol_perc.len()]); 245 | let vol_db = self.volume.avg().print_db(); 246 | 247 | if vol_db.len() + vol_perc.len() <= text_area.width as usize + 3 { 248 | let vol_str = format!( 249 | "{}{}{}", 250 | vol_db, 251 | (0..text_area.width as usize - 3 - vol_perc.len() - vol_db.len()) 252 | .map(|_| " ") 253 | .collect::(), 254 | vol_perc 255 | ); 256 | 257 | buffer.string(text_area.x + 1, text_area.y + 1, vol_str, style); 258 | } 259 | 260 | self.peak_volume_bar.mute = self.mute; 261 | self.peak_volume_bar.render(buffer)?; 262 | 263 | match self.position { 264 | EntrySpaceLvl::Parent => { 265 | buffer.string(self.area.x, self.area.y, "▼".to_string(), style); 266 | buffer.string(self.area.x, self.area.y + 1, "│".to_string(), style); 267 | buffer.string(self.area.x, self.area.y + 2, "│".to_string(), style); 268 | } 269 | EntrySpaceLvl::ParentNoChildren => match self.hidden { 270 | HiddenStatus::HiddenKids => { 271 | buffer.string(self.area.x, self.area.y, "▲".to_string(), style); 272 | } 273 | HiddenStatus::NoKids => { 274 | buffer.string(self.area.x, self.area.y, "▶".to_string(), style); 275 | } 276 | _ => {} 277 | }, 278 | EntrySpaceLvl::MidChild => { 279 | buffer.string(self.area.x, self.area.y, "│".to_string(), style); 280 | buffer.string(self.area.x, self.area.y + 1, "│".to_string(), style); 281 | buffer.string(self.area.x, self.area.y + 2, "├───".to_string(), style); 282 | } 283 | EntrySpaceLvl::LastChild => { 284 | buffer.string(self.area.x, self.area.y, "│".to_string(), style); 285 | buffer.string(self.area.x, self.area.y + 1, "│".to_string(), style); 286 | buffer.string(self.area.x, self.area.y + 2, "└───".to_string(), style); 287 | } 288 | _ => {} 289 | }; 290 | 291 | Ok(()) 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/ui/widgets/help.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | 3 | use super::{ToolWindowWidget, Widget}; 4 | use crate::{ 5 | help::{self, HelpLine}, 6 | prelude::*, 7 | scrollable, 8 | ui::{Buffer, Rect, Scrollable, Style, UIError}, 9 | }; 10 | 11 | #[derive(Clone)] 12 | pub struct HelpWidget { 13 | pub window: ToolWindowWidget, 14 | lines: Vec, 15 | longest_line: u16, 16 | min_line: u16, 17 | max_inner_height: u16, 18 | selected: usize, 19 | } 20 | 21 | impl Default for HelpWidget { 22 | fn default() -> Self { 23 | let lines = help::generate(); 24 | let longest_line = lines 25 | .iter() 26 | .map(|hl| -> usize { 27 | hl.key_events 28 | .iter() 29 | .map(|kv| -> usize { kv.len() + 1 }) 30 | .sum::() + hl.category.len() 31 | }) 32 | .max() 33 | .unwrap_or(0); 34 | 35 | let min_line = lines 36 | .iter() 37 | .map(|hl| -> usize { 38 | hl.key_events 39 | .iter() 40 | .map(|kv| -> usize { kv.len() + 1 }) 41 | .max() 42 | .unwrap_or(0) + hl.category.len() 43 | }) 44 | .max() 45 | .unwrap_or(0); 46 | 47 | let max_inner_height = lines 48 | .iter() 49 | .map(|hl| hl.lines_needed(longest_line as u16 + 1)) 50 | .sum(); 51 | 52 | Self { 53 | window: ToolWindowWidget::default(), 54 | lines, 55 | min_line: min_line as u16, 56 | longest_line: longest_line as u16, 57 | max_inner_height, 58 | selected: 0, 59 | } 60 | } 61 | } 62 | 63 | impl Widget for HelpWidget { 64 | fn resize(&mut self, area: Rect) -> Result<()> { 65 | if area.height < 3 || area.width < self.min_line + 2 { 66 | return Err(UIError::TerminalTooSmall.into()); 67 | } 68 | self.window.padding.0 = if area.width < self.min_line + 6 { 1 } else { 3 }; 69 | self.window.padding.1 = if area.height < 8 { 1 } else { 2 }; 70 | 71 | self.window.inner_width = self.longest_line; 72 | self.window.inner_height = self.max_inner_height; 73 | 74 | self.window.resize(area)?; 75 | 76 | if self.window.area.width < 4 { 77 | self.window.inner_height = self 78 | .lines 79 | .iter() 80 | .map(|hl| hl.lines_needed(self.window.area.width - self.window.padding.0 * 2)) 81 | .sum(); 82 | 83 | self.window.resize(area)?; 84 | } 85 | 86 | Ok(()) 87 | } 88 | fn render(&mut self, buffer: &mut Buffer) -> Result<()> { 89 | self.window.render(buffer)?; 90 | 91 | let inside_height = self.window.area.height - self.window.padding.1 * 2; 92 | let inside_width = self.window.area.width - self.window.padding.0 * 2; 93 | 94 | let lines = self 95 | .lines 96 | .iter() 97 | .map(|hl| hl.as_lines(inside_width)) 98 | .concat(); 99 | 100 | let (start, end) = self.visible_start_end(inside_height); 101 | 102 | for (i, l) in lines.iter().skip(start).take(end - start).enumerate() { 103 | buffer.string( 104 | self.window.area.x + self.window.padding.0, 105 | self.window.area.y + self.window.padding.1 + i as u16, 106 | l.clone(), 107 | if start + i == self.selected() { 108 | Style::Bold 109 | } else { 110 | Style::Normal 111 | }, 112 | ); 113 | } 114 | 115 | let (first, last) = self.visible_start_end(inside_height); 116 | if last - first != self.len() { 117 | let area = Rect::new( 118 | self.window.area.x + self.window.padding.0, 119 | self.window.area.y + self.window.padding.1, 120 | self.window.area.width - self.window.padding.0 * 2, 121 | self.window.area.height - self.window.padding.1 * 2, 122 | ); 123 | if first != 0 { 124 | buffer.string( 125 | area.x + area.width / 2, 126 | area.y + 2, 127 | "▲".to_string(), 128 | Style::Normal, 129 | ); 130 | } 131 | if last != self.len() { 132 | buffer.string( 133 | area.x + area.width / 2, 134 | area.y + area.height - 2, 135 | "▲".to_string(), 136 | Style::Normal, 137 | ); 138 | } 139 | } 140 | 141 | Ok(()) 142 | } 143 | } 144 | 145 | scrollable!( 146 | HelpWidget, 147 | fn selected(&self) -> usize { 148 | self.selected 149 | }, 150 | fn len(&self) -> usize { 151 | self.lines 152 | .iter() 153 | .map(|hl| hl.lines_needed(self.window.area.width - self.window.padding.0 * 2)) 154 | .sum::() as usize 155 | }, 156 | fn set_selected(&mut self, selected: usize) -> bool { 157 | if selected < self.len() { 158 | self.selected = selected; 159 | true 160 | } else { 161 | false 162 | } 163 | }, 164 | fn element_height(&self, _index: usize) -> u16 { 165 | 1 166 | } 167 | ); 168 | -------------------------------------------------------------------------------- /src/ui/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | mod block; 2 | mod context_menu; 3 | mod entry; 4 | mod help; 5 | mod tool_window; 6 | mod volume; 7 | mod volume_input; 8 | mod warning_text; 9 | 10 | pub use block::BlockWidget; 11 | pub use help::HelpWidget; 12 | pub use tool_window::ToolWindowWidget; 13 | pub use volume::{VolumeWidget, VolumeWidgetBorder}; 14 | pub use volume_input::VolumeInputWidget; 15 | pub use warning_text::WarningTextWidget; 16 | 17 | use super::{Buffer, Rect}; 18 | use crate::prelude::*; 19 | 20 | pub trait Widget { 21 | fn render(&mut self, buffer: &mut Buffer) -> Result<()>; 22 | fn resize(&mut self, area: Rect) -> Result<()>; 23 | } 24 | -------------------------------------------------------------------------------- /src/ui/widgets/tool_window.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::{max, min}; 2 | 3 | use super::{BlockWidget, Widget}; 4 | use crate::{ 5 | prelude::*, 6 | ui::{Buffer, Rect}, 7 | }; 8 | 9 | #[derive(Clone)] 10 | 11 | pub struct ToolWindowWidget { 12 | pub area: Rect, 13 | border: BlockWidget, 14 | pub inner_width: u16, 15 | pub inner_height: u16, 16 | pub padding: (u16, u16), 17 | } 18 | 19 | impl Default for ToolWindowWidget { 20 | fn default() -> Self { 21 | Self { 22 | area: Rect::default(), 23 | border: BlockWidget::default().clean_inside(true), 24 | inner_width: 0, 25 | inner_height: 0, 26 | padding: (2, 3), 27 | } 28 | } 29 | } 30 | 31 | impl Widget for ToolWindowWidget { 32 | fn resize(&mut self, mut area: Rect) -> Result<()> { 33 | let target_h = min(self.inner_height + self.padding.1 * 2, area.height); 34 | 35 | let target_w = min( 36 | max(40, self.inner_width + self.padding.0 * 2) as u16, 37 | area.width, 38 | ); 39 | 40 | if area.width > target_w { 41 | area.x += (area.width - target_w) / 2; 42 | } 43 | if area.height > target_h { 44 | area.y += (area.height - target_h) / 2; 45 | } 46 | 47 | area.width = target_w; 48 | area.height = target_h; 49 | 50 | self.area = area; 51 | self.border.resize(area)?; 52 | 53 | Ok(()) 54 | } 55 | fn render(&mut self, buffer: &mut Buffer) -> Result<()> { 56 | self.border.render(buffer)?; 57 | 58 | Ok(()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ui/widgets/volume.rs: -------------------------------------------------------------------------------- 1 | use super::Widget; 2 | use crate::{ 3 | prelude::*, 4 | ui::{Buffer, Pixel, Rect, Style, UIError}, 5 | }; 6 | 7 | #[derive(Copy, Clone, PartialEq, Debug)] 8 | pub enum VolumeWidgetBorder { 9 | Single, 10 | Upper, 11 | Lower, 12 | None, 13 | } 14 | 15 | #[derive(Copy, Clone, PartialEq, Debug)] 16 | pub struct VolumeWidget { 17 | pub percent: f32, 18 | pub last_percent: f32, 19 | pub border: VolumeWidgetBorder, 20 | pub area: Rect, 21 | pub mute: bool, 22 | } 23 | 24 | impl VolumeWidget { 25 | pub fn default() -> Self { 26 | Self { 27 | percent: 0.0, 28 | last_percent: 0.0, 29 | border: VolumeWidgetBorder::Single, 30 | area: Rect::default(), 31 | mute: false, 32 | } 33 | } 34 | 35 | pub fn volume(mut self, percent: f32) -> Self { 36 | self.last_percent = self.percent; 37 | self.percent = percent; 38 | self 39 | } 40 | 41 | pub fn border(mut self, border: VolumeWidgetBorder) -> Self { 42 | self.border = border; 43 | self 44 | } 45 | 46 | pub fn mute(mut self, mute: bool) -> Self { 47 | self.mute = mute; 48 | self 49 | } 50 | 51 | pub fn set_area(mut self, area: Rect) -> Self { 52 | self.area = area; 53 | self 54 | } 55 | 56 | fn get_segments(&self) -> (u16, u16, u16) { 57 | let width = self.area.width; 58 | let third = (0.34 * (width - 2) as f32).floor() as u16; 59 | let last = width - 2 - third * 2; 60 | 61 | (third, third * 2, third * 2 + last) 62 | } 63 | 64 | pub fn small_render(&mut self, buffer: &mut Buffer) -> Result<()> { 65 | let filled = (self.percent * (self.area.width - 2) as f32).floor() as u16; 66 | let last_filled = (self.last_percent * (self.area.width - 2) as f32).floor() as u16; 67 | let smaller = filled.min(last_filled); 68 | let greater = filled.max(last_filled); 69 | 70 | let segments = self.get_segments(); 71 | 72 | let pixels: Vec = (smaller..greater) 73 | .map(|i| Pixel { 74 | text: if i < filled { Some('▮') } else { Some('-') }, 75 | style: if self.mute { 76 | Style::Muted 77 | } else if i < segments.0 { 78 | Style::Green 79 | } else if i < segments.1 { 80 | Style::Orange 81 | } else { 82 | Style::Red 83 | }, 84 | }) 85 | .collect(); 86 | 87 | buffer.pixels(self.area.x + 1 + smaller, self.area.y, &pixels.into()); 88 | 89 | Ok(()) 90 | } 91 | } 92 | 93 | impl Widget for VolumeWidget { 94 | fn resize(&mut self, area: Rect) -> Result<()> { 95 | if area.width < 3 || area.height < 1 { 96 | return Err(UIError::TerminalTooSmall.into()); 97 | } 98 | 99 | self.area = area; 100 | 101 | Ok(()) 102 | } 103 | fn render(&mut self, buffer: &mut Buffer) -> Result<()> { 104 | self.border.render(buffer, &self.area); 105 | 106 | let filled = (self.percent * (self.area.width - 2) as f32).floor() as u16; 107 | let segments = self.get_segments(); 108 | 109 | let pixels: Vec = (0..(self.area.width - 2)) 110 | .map(|i| Pixel { 111 | text: if i < filled { Some('▮') } else { Some('-') }, 112 | style: if self.mute { 113 | Style::Muted 114 | } else if i < segments.0 { 115 | Style::Green 116 | } else if i < segments.1 { 117 | Style::Orange 118 | } else { 119 | Style::Red 120 | }, 121 | }) 122 | .collect(); 123 | 124 | buffer.pixels(self.area.x + 1, self.area.y, &pixels.into()); 125 | 126 | Ok(()) 127 | } 128 | } 129 | 130 | impl VolumeWidgetBorder { 131 | fn render(&mut self, buffer: &mut Buffer, area: &Rect) { 132 | if *self == VolumeWidgetBorder::None { 133 | return; 134 | } 135 | 136 | let ch1 = match self { 137 | VolumeWidgetBorder::Single => "[", 138 | VolumeWidgetBorder::Upper => "┌", 139 | VolumeWidgetBorder::Lower => "└", 140 | _ => "", 141 | }; 142 | let ch2 = match self { 143 | VolumeWidgetBorder::Single => "]", 144 | VolumeWidgetBorder::Upper => "┐", 145 | VolumeWidgetBorder::Lower => "┘", 146 | _ => "", 147 | }; 148 | 149 | buffer.string(area.x, area.y, ch1.to_string(), Style::Normal); 150 | buffer.string( 151 | area.x + area.width - 1, 152 | area.y, 153 | ch2.to_string(), 154 | Style::Normal, 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/ui/widgets/volume_input.rs: -------------------------------------------------------------------------------- 1 | use super::{BlockWidget, Widget}; 2 | use crate::{ 3 | models::Style, 4 | prelude::*, 5 | ui::{Buffer, Rect}, 6 | }; 7 | 8 | #[derive(Clone)] 9 | pub struct VolumeInputWidget { 10 | pub value: String, 11 | pub cursor: u8, 12 | pub window: BlockWidget, 13 | } 14 | 15 | impl Default for VolumeInputWidget { 16 | fn default() -> Self { 17 | Self { 18 | value: "".to_string(), 19 | cursor: 0, 20 | window: BlockWidget::default().clean_inside(true), 21 | } 22 | } 23 | } 24 | 25 | impl Widget for VolumeInputWidget { 26 | fn resize(&mut self, area: Rect) -> Result<()> { 27 | let area = Rect::new(area.x + area.width / 2 - 3, area.y, 7, 3); 28 | self.window.resize(area)?; 29 | Ok(()) 30 | } 31 | 32 | fn render(&mut self, buffer: &mut Buffer) -> Result<()> { 33 | self.window.render(buffer)?; 34 | 35 | buffer.string( 36 | self.window.area.x + 3 - self.value.len() as u16 / 2, 37 | self.window.area.y + 1, 38 | self.value.clone(), 39 | Style::Normal, 40 | ); 41 | 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ui/widgets/warning_text.rs: -------------------------------------------------------------------------------- 1 | use super::Widget; 2 | use crate::{ 3 | models::Style, 4 | prelude::*, 5 | ui::{Buffer, Rect}, 6 | }; 7 | 8 | #[derive(Clone)] 9 | pub struct WarningTextWidget { 10 | pub text: String, 11 | } 12 | 13 | impl Default for WarningTextWidget { 14 | fn default() -> Self { 15 | Self { 16 | text: String::from(""), 17 | } 18 | } 19 | } 20 | 21 | impl Widget for WarningTextWidget { 22 | fn resize(&mut self, _area: Rect) -> Result<()> { 23 | Ok(()) 24 | } 25 | fn render(&mut self, buffer: &mut Buffer) -> Result<()> { 26 | buffer.rect( 27 | Rect::new(0, 0, buffer.width, buffer.height), 28 | ' ', 29 | Style::Normal, 30 | ); 31 | buffer.string(0, 0, self.text.clone(), Style::Normal); 32 | 33 | Ok(()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use pulse::volume; 2 | 3 | pub fn volume_to_percent(volume: volume::ChannelVolumes) -> u16 { 4 | let avg = volume.avg().0; 5 | 6 | let base_delta = (volume::Volume::NORMAL.0 as f32 - volume::Volume::MUTED.0 as f32) / 100.0; 7 | 8 | ((avg - volume::Volume::MUTED.0) as f32 / base_delta).round() as u16 9 | } 10 | 11 | pub fn percent_to_volume(target_percent: i16) -> u32 { 12 | let base_delta = (volume::Volume::NORMAL.0 as f32 - volume::Volume::MUTED.0 as f32) / 100.0; 13 | 14 | if target_percent < 0 { 15 | volume::Volume::MUTED.0 16 | } else if target_percent == 100 { 17 | volume::Volume::NORMAL.0 18 | } else if target_percent >= 150 { 19 | (volume::Volume::NORMAL.0 as f32 * 1.5) as u32 20 | } else if target_percent < 100 { 21 | volume::Volume::MUTED.0 + target_percent as u32 * base_delta as u32 22 | } else { 23 | volume::Volume::NORMAL.0 + (target_percent - 100) as u32 * base_delta as u32 24 | } 25 | } 26 | 27 | #[macro_export] 28 | macro_rules! unwrap_or_return { 29 | ($x:expr, $y:expr) => { 30 | match $x { 31 | Some(x) => x, 32 | None => { 33 | return $y; 34 | } 35 | } 36 | }; 37 | ($x:expr) => { 38 | unwrap_or_return!($x, ()) 39 | }; 40 | } 41 | 42 | #[macro_export] 43 | macro_rules! error { 44 | ($($x:expr),*) => { 45 | log::error!("[{}] {}", LOGGING_MODULE, format!($($x),*)); 46 | } 47 | } 48 | 49 | #[macro_export] 50 | macro_rules! debug { 51 | ($($x:expr),*) => { 52 | log::debug!("[{}] {}", LOGGING_MODULE, format!($($x),*)); 53 | } 54 | } 55 | 56 | #[macro_export] 57 | macro_rules! info { 58 | ($($x:expr),*) => { 59 | log::info!("[{}] {}", LOGGING_MODULE, format!($($x),*)); 60 | } 61 | } 62 | 63 | #[macro_export] 64 | macro_rules! warn { 65 | ($($x:expr),*) => { 66 | log::warn!("[{}] {}", LOGGING_MODULE, format!($($x),*)); 67 | } 68 | } 69 | --------------------------------------------------------------------------------