Make bottom panel fixed size so content doesn't go up and down when playing new sound effects, add more apps to Linux excludes, add multiple virtual output support by using buttons(on/off) on Linux so you can select the virtual mic for multiple apps at the same time

This commit is contained in:
csd4ni3l
2026-03-05 21:24:55 +01:00
parent 8d036bca08
commit 4128566048
2 changed files with 74 additions and 61 deletions

View File

@@ -5,7 +5,8 @@ use rodio::{
use serde_json::Value;
use std::process::Command;
const APPS_TO_EXCLUDE: [&str; 1] = ["plasmashell"];
const APPS_TO_EXCLUDE: [&str; 7] = ["plasmashell", "pavucontrol", "pipewire", "wireplumber", "kwin_wayland", "kwin_x11", "obs"];
const NODE_NAMES_TO_EXCLUDE: [&str; 2] = ["VirtualMicSource", "SoundboardSink"];
fn pactl_list(sink_type: &str) -> Value {
let command_output = Command::new("pactl")
@@ -23,21 +24,35 @@ fn pactl_list(sink_type: &str) -> Value {
}
}
pub fn get_sink_by_index(sink_type: &str, index: String) -> Value {
let sinks = pactl_list(sink_type);
pub fn get_soundboard_sink_index() -> String {
let source_outputs = pactl_list("sinks");
source_outputs
.as_array()
.unwrap_or(&vec![])
.iter()
.find(|sink| sink["name"] == "SoundboardSink")
.and_then(|sink| {
Some(sink["index"].to_string())
})
.unwrap()
}
for sink in sinks.as_array().unwrap_or(&vec![]) {
if sink["index"]
.as_u64()
.expect("sink index is not a number")
.to_string()
== index
{
return sink.clone();
}
}
pub fn get_default_source() -> String {
let sources = pactl_list("sources");
return Value::Null {};
let command = Command::new("pactl")
.args(&["get-default-source"])
.output()
.unwrap();
let default_source_name = String::from_utf8_lossy(&command.stdout).trim().to_string();
sources.as_array()
.unwrap()
.iter()
.find(|sink|{ sink["name"].as_str() == Some(&default_source_name) })
.and_then(|s|{ Some(s["index"].to_string()) })
.unwrap()
}
fn find_soundboard_sinks() -> Vec<Value> {
@@ -73,10 +88,11 @@ pub fn list_outputs() -> Vec<(String, String)> {
.iter()
.filter_map(|sink| {
let app_name = sink["properties"]["application.name"].as_str()?;
let node_name = sink["properties"]["node.name"].as_str()?;
let binary = sink["properties"]["application.process.binary"]
.as_str()
.unwrap_or("Unknown");
if APPS_TO_EXCLUDE.contains(&binary) {
if APPS_TO_EXCLUDE.contains(&binary) || NODE_NAMES_TO_EXCLUDE.contains(&node_name) {
return None;
}
let index = sink["index"]
@@ -88,9 +104,9 @@ pub fn list_outputs() -> Vec<(String, String)> {
.collect();
}
pub fn move_index_to_virtualmic(index: String) {
Command::new("pactl")
.args(&["move-source-output", index.as_str(), "VirtualMicSource"]) // as_str is needed here as you cannot instantly dereference a growing String (Rust...)
pub fn move_output_to_sink(output_index: String, sink_index: String) {
let output = Command::new("pactl")
.args(&["move-source-output", output_index.as_str(), sink_index.as_str()]) // as_str is needed here as you cannot instantly dereference a growing String (Rust...)
.output()
.expect("Failed to execute process");
}

View File

@@ -57,8 +57,7 @@ struct AppState {
currently_playing: Vec<PlayingSound>,
sound_system: SoundSystem,
virt_outputs: Vec<(String, String)>,
virt_output_index_switch: String,
virt_output_index: String,
is_virt_output_used: HashMap<String, bool>,
last_virt_output_update: Instant,
current_view: String,
youtube_downloader_state: YoutubeDownloaderState
@@ -156,8 +155,7 @@ fn main() {
currently_playing: Vec::new(),
sound_system: create_virtual_mic(),
virt_outputs: Vec::new(),
virt_output_index_switch: String::from("0"),
virt_output_index: String::from("999"),
is_virt_output_used: HashMap::new(),
current_view: "main".to_string(),
last_virt_output_update: Instant::now(),
youtube_downloader_state: YoutubeDownloaderState {
@@ -181,30 +179,29 @@ fn main() {
}
fn update(mut app_state: ResMut<AppState>) {
if app_state.last_virt_output_update.elapsed().as_secs_f32() >= 3.0 {
app_state.last_virt_output_update = Instant::now();
app_state.virt_outputs = list_outputs();
}
#[cfg(target_os = "linux")] {
if app_state.last_virt_output_update.elapsed().as_secs_f32() >= 1.5 {
app_state.last_virt_output_update = Instant::now();
app_state.virt_outputs = list_outputs();
let is_virt_output_used = app_state.is_virt_output_used.clone();
if app_state.virt_outputs.is_empty() {
return;
}
for virt_output in &app_state.virt_outputs.clone() {
if !is_virt_output_used.contains_key(&virt_output.1) {
app_state.is_virt_output_used.insert(virt_output.1.clone(), false);
}
if !(app_state.virt_output_index == "999".to_string()) {
app_state.virt_output_index_switch = app_state.virt_outputs[0].1.clone();
}
if app_state.virt_output_index != app_state.virt_output_index_switch {
app_state.virt_output_index = app_state.virt_output_index_switch.clone();
#[cfg(target_os = "linux")]
linux_lib::move_index_to_virtualmic(app_state.virt_output_index_switch.clone());
if app_state.is_virt_output_used[&virt_output.1] {
linux_lib::move_output_to_sink(virt_output.1.clone(), linux_lib::get_soundboard_sink_index());
}
else {
linux_lib::move_output_to_sink(virt_output.1.clone(), linux_lib::get_default_source());
}
}
}
}
}
fn load_system(mut app_state: ResMut<AppState>) {
if !app_state.virt_outputs.is_empty() {
app_state.virt_output_index_switch = app_state.virt_outputs[0].1.clone();
}
load_data(&mut app_state);
}
@@ -309,25 +306,22 @@ fn play_sound(file_path: String, app_state: &mut AppState) {
app_state.currently_playing.push(playing_sound);
}
fn create_virtual_mic_dropdown(ui: &mut Ui, app_state: &mut ResMut<AppState>, available_width: f32, available_height: f32) {
fn create_virtual_mic_ui(ui: &mut Ui, app_state: &mut ResMut<AppState>, available_width: f32, available_height: f32) {
#[cfg(target_os = "linux")] {
let outputs = app_state.virt_outputs.clone();
let output_index = app_state.virt_output_index.clone();
let output_sink = linux_lib::get_sink_by_index("source-outputs", output_index);
if let Some(app_name) = output_sink["properties"]["application.name"].as_str() {
egui::ComboBox::from_id_salt("Virtual Mic Output")
.selected_text(app_name.to_string())
.width(available_width)
.height(available_height / 15.0)
.show_ui(ui, |ui| {
for output in &outputs {
ui.selectable_value(
&mut app_state.virt_output_index_switch,
output.1.clone(),
output.0.clone(),
);
}
});
if app_state.is_virt_output_used.len() != 0 {
let outputs = app_state.virt_outputs.clone();
for output in &outputs {
let current_value = *app_state.is_virt_output_used.get(&output.1).unwrap_or(&false);
if ui
.add_sized(
[available_width, available_height / 30.0],
egui::Button::new(format!("{} - {}", output.0.clone(), current_value)),
)
.clicked()
{
*app_state.is_virt_output_used.entry(output.1.clone()).or_insert(false) = !current_value;
}
}
}
else {
ui.add(egui::Button::new("No apps found to use.".to_string()));
@@ -350,7 +344,7 @@ fn main_ui(ctx: &Context, mut app_state: ResMut<AppState>) {
let available_width = ui.available_width();
let available_height = ui.available_height();
ui.label("Virtual Mic Output");
create_virtual_mic_dropdown(ui, &mut app_state, available_width, available_height);
create_virtual_mic_ui(ui, &mut app_state, available_width, available_height);
if ui
.add_sized(
@@ -361,7 +355,6 @@ fn main_ui(ctx: &Context, mut app_state: ResMut<AppState>) {
{
if let Some(folder) = rfd::FileDialog::new().pick_folder() {
if let Some(path_str) = folder.to_str() {
println!("Selected: {}", path_str);
app_state.json_data.tabs.push(path_str.to_string());
std::fs::write(
"data.json",
@@ -581,7 +574,11 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
}
});
egui::TopBottomPanel::bottom("currently_playing").show(ctx, |ui| {
let window_height = ctx.screen_rect().height();
egui::TopBottomPanel::bottom("currently_playing")
.exact_height(window_height * 0.1)
.show(ctx, |ui| {
ui.vertical(|ui| {
for playing_sound in &mut app_state.currently_playing {
ui.horizontal(|ui| {