Audio settings: Sinks and sources control fully implemented (cards wip), port pactl_wrapper
This commit is contained in:
@@ -1,26 +1,26 @@
|
|||||||
<layout>
|
<layout>
|
||||||
<include src="../t_group_box.xml" />
|
<include src="../t_group_box.xml" />
|
||||||
|
|
||||||
<!-- device_name, device_icon -->
|
<!-- device_name, device_icon, id_checkbox -->
|
||||||
<template name="AudioDevice">
|
<template name="DeviceSlider">
|
||||||
<rectangle macro="group_box">
|
<rectangle macro="group_box">
|
||||||
<div width="100%" align_items="center" justify_content="center" gap="8">
|
<div width="100%" align_items="center" justify_content="center" gap="8">
|
||||||
<sprite src="${device_icon}" width="16" height="16" />
|
<sprite src="${device_icon}" width="16" height="16" />
|
||||||
<label translation="${device_name}" margin_right="8" size="12" weight="bold" />
|
<label text="${device_name}" margin_right="8" size="12" weight="bold" />
|
||||||
</div>
|
</div>
|
||||||
<div width="100%" align_items="center">
|
<div width="100%" align_items="center">
|
||||||
<CheckBox />
|
<CheckBox id="checkbox" />
|
||||||
<Button>
|
<Button id="btn_mute">
|
||||||
<sprite src="dashboard/volume.svg" width="20" height="20" margin="4" margin_left="8" margin_right="8" />
|
<sprite src="${volume_icon}" width="20" height="20" margin="4" margin_left="8" margin_right="8" />
|
||||||
</Button>
|
</Button>
|
||||||
<Slider flex_grow="1" height="16" min_value="0" max_value="150" margin_left="8" />
|
<Slider id="slider" flex_grow="1" height="16" min_value="0" max_value="150" margin_left="8" />
|
||||||
</div>
|
</div>
|
||||||
</rectangle>
|
</rectangle>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- src, translation -->
|
<!-- id (Button), src, translation -->
|
||||||
<template name="BottomButton">
|
<template name="BottomButton">
|
||||||
<Button flex_grow="1">
|
<Button flex_grow="1" id="${id}">
|
||||||
<sprite src="${src}"
|
<sprite src="${src}"
|
||||||
min_width="24" min_height="24" width="24" height="24" margin="4" margin_left="16" />
|
min_width="24" min_height="24" width="24" height="24" margin="4" margin_left="16" />
|
||||||
<label translation="${translation}" weight="bold" margin_right="16" />
|
<label translation="${translation}" weight="bold" margin_right="16" />
|
||||||
@@ -28,9 +28,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<elements>
|
<elements>
|
||||||
<AudioDevice device_name="Analog Stereo Output" device_icon="dashboard/minijack.svg" />
|
<div id="devices" flex_direction="column" gap="8">
|
||||||
<AudioDevice device_name="My spectacular display" device_icon="dashboard/displayport.svg" />
|
|
||||||
<AudioDevice device_name="Foobar device" device_icon="dashboard/minijack.svg" />
|
</div>
|
||||||
|
|
||||||
<!-- bottom buttons -->
|
<!-- bottom buttons -->
|
||||||
<div flex_direction="row" gap="4">
|
<div flex_direction="row" gap="4">
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
min_width="24" width="24" height="24" margin="4" />
|
min_width="24" width="24" height="24" margin="4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<BottomButton src="dashboard/volume.svg" translation="AUDIO.SPEAKERS" />
|
<BottomButton id="btn_sinks" src="dashboard/volume.svg" translation="AUDIO.SPEAKERS" />
|
||||||
<BottomButton src="dashboard/microphone.svg" translation="AUDIO.MICROPHONES" />
|
<BottomButton id="btn_sources" src="dashboard/microphone.svg" translation="AUDIO.MICROPHONES" />
|
||||||
<BottomButton src="dashboard/cpu.svg" translation="AUDIO.CARDS" />
|
<BottomButton id="btn_cards" src="dashboard/cpu.svg" translation="AUDIO.CARDS" />
|
||||||
</div>
|
</div>
|
||||||
</elements>
|
</elements>
|
||||||
</layout>
|
</layout>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
use chrono::Timelike;
|
use chrono::Timelike;
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
@@ -8,7 +8,7 @@ use wgui::{
|
|||||||
font_config::WguiFontConfig,
|
font_config::WguiFontConfig,
|
||||||
globals::WguiGlobals,
|
globals::WguiGlobals,
|
||||||
i18n::Translation,
|
i18n::Translation,
|
||||||
layout::{Layout, LayoutParams, RcLayout, WidgetID},
|
layout::{LayoutParams, RcLayout, WidgetID},
|
||||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||||
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
|
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
|
||||||
windowing::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
|
windowing::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
|
||||||
@@ -20,6 +20,7 @@ use crate::{
|
|||||||
Tab, TabParams, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses,
|
Tab, TabParams, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses,
|
||||||
settings::TabSettings,
|
settings::TabSettings,
|
||||||
},
|
},
|
||||||
|
task::Tasks,
|
||||||
util::popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
|
util::popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
|
||||||
views,
|
views,
|
||||||
};
|
};
|
||||||
@@ -29,18 +30,7 @@ pub struct FrontendWidgets {
|
|||||||
pub id_rect_content: WidgetID,
|
pub id_rect_content: WidgetID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
pub type FrontendTasks = Tasks<FrontendTask>;
|
||||||
pub struct FrontendTasks(pub Rc<RefCell<VecDeque<FrontendTask>>>);
|
|
||||||
|
|
||||||
impl FrontendTasks {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self(Rc::new(RefCell::new(VecDeque::new())))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push(&self, task: FrontendTask) {
|
|
||||||
self.0.borrow_mut().push_back(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Frontend {
|
pub struct Frontend {
|
||||||
pub layout: RcLayout,
|
pub layout: RcLayout,
|
||||||
@@ -61,6 +51,7 @@ pub struct Frontend {
|
|||||||
popup_manager: PopupManager,
|
popup_manager: PopupManager,
|
||||||
|
|
||||||
window_audio_settings: WguiWindow,
|
window_audio_settings: WguiWindow,
|
||||||
|
view_audio_settings: Option<views::audio_settings::View>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InitParams {
|
pub struct InitParams {
|
||||||
@@ -77,6 +68,7 @@ pub enum FrontendTask {
|
|||||||
MountPopup(MountPopupParams),
|
MountPopup(MountPopupParams),
|
||||||
RefreshPopupManager,
|
RefreshPopupManager,
|
||||||
ShowAudioSettings,
|
ShowAudioSettings,
|
||||||
|
UpdateAudioSettingsView,
|
||||||
RecenterPlayspace,
|
RecenterPlayspace,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +129,7 @@ impl Frontend {
|
|||||||
settings: params.settings,
|
settings: params.settings,
|
||||||
popup_manager,
|
popup_manager,
|
||||||
window_audio_settings: WguiWindow::default(),
|
window_audio_settings: WguiWindow::default(),
|
||||||
|
view_audio_settings: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// init some things first
|
// init some things first
|
||||||
@@ -151,10 +144,7 @@ impl Frontend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, rc_this: &RcFrontend, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> {
|
pub fn update(&mut self, rc_this: &RcFrontend, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> {
|
||||||
let mut tasks = {
|
let mut tasks = self.tasks.drain();
|
||||||
let mut tasks = self.tasks.0.borrow_mut();
|
|
||||||
std::mem::take(&mut *tasks)
|
|
||||||
};
|
|
||||||
|
|
||||||
while let Some(task) = tasks.pop_front() {
|
while let Some(task) = tasks.pop_front() {
|
||||||
self.process_task(rc_this, task)?;
|
self.process_task(rc_this, task)?;
|
||||||
@@ -260,6 +250,7 @@ impl Frontend {
|
|||||||
FrontendTask::MountPopup(params) => self.mount_popup(params)?,
|
FrontendTask::MountPopup(params) => self.mount_popup(params)?,
|
||||||
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
|
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
|
||||||
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
|
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
|
||||||
|
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,
|
||||||
FrontendTask::RecenterPlayspace => self.action_recenter_playspace()?,
|
FrontendTask::RecenterPlayspace => self.action_recenter_playspace()?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -390,11 +381,28 @@ impl Frontend {
|
|||||||
|
|
||||||
let content = self.window_audio_settings.get_content();
|
let content = self.window_audio_settings.get_content();
|
||||||
|
|
||||||
views::audio_settings::View::new(views::audio_settings::Params {
|
self.view_audio_settings = Some(views::audio_settings::View::new(views::audio_settings::Params {
|
||||||
globals: self.globals.clone(),
|
globals: self.globals.clone(),
|
||||||
layout: &mut layout,
|
layout: &mut layout,
|
||||||
parent_id: content.id,
|
parent_id: content.id,
|
||||||
})?;
|
on_update: {
|
||||||
|
let tasks = self.tasks.clone();
|
||||||
|
Rc::new(move || {
|
||||||
|
tasks.push(FrontendTask::UpdateAudioSettingsView);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_update_audio_settings(&mut self) -> anyhow::Result<()> {
|
||||||
|
let Some(view) = &mut self.view_audio_settings else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut layout = self.layout.borrow_mut();
|
||||||
|
view.update(&mut layout)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod assets;
|
|||||||
pub mod frontend;
|
pub mod frontend;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
mod tab;
|
mod tab;
|
||||||
|
mod task;
|
||||||
mod util;
|
mod util;
|
||||||
mod various;
|
mod various;
|
||||||
mod views;
|
mod views;
|
||||||
|
|||||||
21
dash-frontend/src/task.rs
Normal file
21
dash-frontend/src/task.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Tasks<TaskType>(Rc<RefCell<VecDeque<TaskType>>>)
|
||||||
|
where
|
||||||
|
TaskType: Clone;
|
||||||
|
|
||||||
|
impl<TaskType: Clone + 'static> Tasks<TaskType> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(Rc::new(RefCell::new(VecDeque::new())))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&self, task: TaskType) {
|
||||||
|
self.0.borrow_mut().push_back(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drain(&mut self) -> VecDeque<TaskType> {
|
||||||
|
let mut tasks = self.0.borrow_mut();
|
||||||
|
std::mem::take(&mut *tasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod desktop_finder;
|
pub mod desktop_finder;
|
||||||
|
pub mod pactl_wrapper;
|
||||||
pub mod popup_manager;
|
pub mod popup_manager;
|
||||||
|
|||||||
320
dash-frontend/src/util/pactl_wrapper.rs
Normal file
320
dash-frontend/src/util/pactl_wrapper.rs
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct VolumeChannel {
|
||||||
|
pub value: u32, // 48231
|
||||||
|
pub value_percent: String, // "80%"
|
||||||
|
pub db: String, // "-5.81 dB"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Volume {
|
||||||
|
// WiVRn and other devices
|
||||||
|
pub aux0: Option<VolumeChannel>,
|
||||||
|
pub aux1: Option<VolumeChannel>,
|
||||||
|
|
||||||
|
// Analog and HDMI devices
|
||||||
|
#[serde(rename = "front-left")]
|
||||||
|
pub front_left: Option<VolumeChannel>,
|
||||||
|
|
||||||
|
#[serde(rename = "front-right")]
|
||||||
|
pub front_right: Option<VolumeChannel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Sink {
|
||||||
|
pub index: u32, // 123
|
||||||
|
pub state: String, // "RUNNING" / "SUSPENDED"
|
||||||
|
pub name: String, // alsa_output.pci-0000_0c_00.4.analog-stereo
|
||||||
|
pub description: String, // Starship/Matisse HD Audio Controller Analog Stereo
|
||||||
|
pub mute: bool, // false
|
||||||
|
pub volume: Volume,
|
||||||
|
pub properties: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SourceProperties {
|
||||||
|
#[serde(rename = "device.name")]
|
||||||
|
pub device_name: Option<String>, // "alsa_card.pci-0000_0b_00.1"
|
||||||
|
#[serde(rename = "device.class")]
|
||||||
|
pub device_class: Option<String>, // "monitor", "sound"
|
||||||
|
#[serde(rename = "alsa.card_name")]
|
||||||
|
pub card_name: Option<String>, // "Valve VR Radio & HMD Mic"
|
||||||
|
#[serde(rename = "alsa.components")]
|
||||||
|
pub components: Option<String>, // USB28de:2102
|
||||||
|
#[serde(rename = "device.vendor_name")]
|
||||||
|
pub vendor_name: Option<String>, // Valve Software
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Source {
|
||||||
|
pub index: u32, // 123
|
||||||
|
pub state: String, // "RUNNING" / "SUSPENDED"
|
||||||
|
pub name: String, // alsa_input.pci-0000_0c_00.4.analog-stereo
|
||||||
|
pub description: String, // Valve VR Radio & HMD Mic Mono
|
||||||
|
pub mute: bool, // false
|
||||||
|
pub volume: Volume,
|
||||||
|
pub properties: SourceProperties,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CardProperties {
|
||||||
|
#[serde(rename = "device.description")]
|
||||||
|
pub device_description: String, // Starship/Matisse HD Audio Controller
|
||||||
|
|
||||||
|
#[serde(rename = "device.name")]
|
||||||
|
pub device_name: String, // alsa_card.pci-0000_0c_00.4
|
||||||
|
|
||||||
|
#[serde(rename = "device.nick")]
|
||||||
|
pub device_nick: String, // HD-Audio Generic
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CardProfile {
|
||||||
|
pub description: String, // "Digital Stereo (HDMI 2) Output", "Analog Stereo Output",
|
||||||
|
pub sinks: u32, // 1
|
||||||
|
pub sources: u32, // 0
|
||||||
|
pub priority: u32, // 6500
|
||||||
|
pub available: bool, // true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CardPort {
|
||||||
|
pub description: String, // "HDMI / DisplayPort 2"
|
||||||
|
pub r#type: String, // "HDMI"
|
||||||
|
pub profiles: Vec<String>, // "output:hdmi-stereo-extra1", "output:hdmi-surround-extra1", "output:analog-stereo", "output:analog-stereo+input:analog-stereo"
|
||||||
|
|
||||||
|
// example:
|
||||||
|
// "port.type": "hdmi"
|
||||||
|
// "device.product_name": "Index HMD"
|
||||||
|
pub properties: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Card {
|
||||||
|
pub index: u32, // 57
|
||||||
|
pub name: String, // alsa_card.pci-0000_0c_00.4
|
||||||
|
pub active_profile: String, // output:analog-stereo
|
||||||
|
pub properties: CardProperties,
|
||||||
|
pub profiles: HashMap<String, CardProfile>, // key: "output:analog-stereo"
|
||||||
|
pub ports: HashMap<String, CardPort>, // key: "analog-output-lineout"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ########################################
|
||||||
|
// ~ sinks ~
|
||||||
|
// ########################################
|
||||||
|
|
||||||
|
pub fn list_sinks() -> anyhow::Result<Vec<Sink>> {
|
||||||
|
let output = std::process::Command::new("pactl")
|
||||||
|
.arg("--format=json")
|
||||||
|
.arg("list")
|
||||||
|
.arg("sinks")
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!("pactl exit status {}", output.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_str = std::str::from_utf8(&output.stdout)?;
|
||||||
|
let sinks: Vec<Sink> = serde_json::from_str(json_str)?;
|
||||||
|
Ok(sinks)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_sink(sinks: &[Sink]) -> anyhow::Result<Option<Sink>> {
|
||||||
|
let output = std::process::Command::new("pactl").arg("get-default-sink").output()?;
|
||||||
|
|
||||||
|
let utf8_name = std::str::from_utf8(&output.stdout)?.trim();
|
||||||
|
|
||||||
|
for sink in sinks {
|
||||||
|
if sink.name == utf8_name {
|
||||||
|
return Ok(Some(sink.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_default_sink(sink_index: u32) -> anyhow::Result<()> {
|
||||||
|
std::process::Command::new("pactl")
|
||||||
|
.arg("set-default-sink")
|
||||||
|
.arg(format!("{}", sink_index))
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sink_from_index(sinks: &[Sink], index: u32) -> Option<&Sink> {
|
||||||
|
sinks.iter().find(|&sink| sink.index == index)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sink_volume(sink: &Sink) -> anyhow::Result<f32> {
|
||||||
|
let volume_channel = {
|
||||||
|
if let Some(front_left) = &sink.volume.front_left {
|
||||||
|
front_left
|
||||||
|
} else if let Some(aux0) = &sink.volume.aux0 {
|
||||||
|
aux0
|
||||||
|
} else {
|
||||||
|
return Ok(0.0); // fail silently
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(pair) = volume_channel.value_percent.split_once("%") else {
|
||||||
|
anyhow::bail!("volume percentage invalid"); // shouldn't happen
|
||||||
|
};
|
||||||
|
|
||||||
|
let percent_num: f32 = pair.0.parse().unwrap_or(0.0);
|
||||||
|
Ok(percent_num / 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sink_volume(sink_index: u32, volume: f32) -> anyhow::Result<()> {
|
||||||
|
let target_vol = (volume * 100.0).clamp(0.0, 150.0); // limit to 150%
|
||||||
|
|
||||||
|
std::process::Command::new("pactl")
|
||||||
|
.arg("set-sink-volume")
|
||||||
|
.arg(format!("{}", sink_index))
|
||||||
|
.arg(format!("{}%", target_vol))
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sink_mute(sink_index: u32, mute: bool) -> anyhow::Result<()> {
|
||||||
|
std::process::Command::new("pactl")
|
||||||
|
.arg("set-sink-mute")
|
||||||
|
.arg(format!("{}", sink_index))
|
||||||
|
.arg(format!("{}", mute as i32))
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ########################################
|
||||||
|
// ~ sources ~
|
||||||
|
// ########################################
|
||||||
|
|
||||||
|
pub fn list_sources() -> anyhow::Result<Vec<Source>> {
|
||||||
|
let output = std::process::Command::new("pactl")
|
||||||
|
.arg("--format=json")
|
||||||
|
.arg("list")
|
||||||
|
.arg("sources")
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!("pactl exit status {}", output.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_str = std::str::from_utf8(&output.stdout)?;
|
||||||
|
let mut sources: Vec<Source> = serde_json::from_str(json_str)?;
|
||||||
|
|
||||||
|
// exclude all monitor sources
|
||||||
|
sources.retain(|source| match &source.properties.device_class {
|
||||||
|
Some(c) => c != "monitor",
|
||||||
|
None => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(sources)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_source(sources: &[Source]) -> anyhow::Result<Option<Source>> {
|
||||||
|
let output = std::process::Command::new("pactl").arg("get-default-source").output()?;
|
||||||
|
|
||||||
|
let utf8_name = std::str::from_utf8(&output.stdout)?.trim();
|
||||||
|
|
||||||
|
for source in sources {
|
||||||
|
if source.name == utf8_name {
|
||||||
|
return Ok(Some(source.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_default_source(source_index: u32) -> anyhow::Result<()> {
|
||||||
|
std::process::Command::new("pactl")
|
||||||
|
.arg("set-default-source")
|
||||||
|
.arg(format!("{}", source_index))
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_source_from_index(sources: &[Source], index: u32) -> Option<&Source> {
|
||||||
|
sources.iter().find(|&source| source.index == index)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_source_volume(source: &Source) -> anyhow::Result<f32> {
|
||||||
|
let volume_channel = {
|
||||||
|
if let Some(front_left) = &source.volume.front_left {
|
||||||
|
front_left
|
||||||
|
} else if let Some(aux0) = &source.volume.aux0 {
|
||||||
|
aux0
|
||||||
|
} else {
|
||||||
|
return Ok(0.0); // fail silently
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(pair) = volume_channel.value_percent.split_once("%") else {
|
||||||
|
anyhow::bail!("volume percentage invalid"); // shouldn't happen
|
||||||
|
};
|
||||||
|
|
||||||
|
let percent_num: f32 = pair.0.parse().unwrap_or(0.0);
|
||||||
|
Ok(percent_num / 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_source_volume(source_index: u32, volume: f32) -> anyhow::Result<()> {
|
||||||
|
let target_vol = (volume * 100.0).clamp(0.0, 150.0); // limit to 150%
|
||||||
|
|
||||||
|
std::process::Command::new("pactl")
|
||||||
|
.arg("set-source-volume")
|
||||||
|
.arg(format!("{}", source_index))
|
||||||
|
.arg(format!("{}%", target_vol))
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_source_mute(source_index: u32, mute: bool) -> anyhow::Result<()> {
|
||||||
|
std::process::Command::new("pactl")
|
||||||
|
.arg("set-source-mute")
|
||||||
|
.arg(format!("{}", source_index))
|
||||||
|
.arg(format!("{}", mute as i32))
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ########################################
|
||||||
|
// ~ cards ~
|
||||||
|
// ########################################
|
||||||
|
|
||||||
|
pub fn list_cards() -> anyhow::Result<Vec<Card>> {
|
||||||
|
let output = std::process::Command::new("pactl")
|
||||||
|
.arg("--format=json")
|
||||||
|
.arg("list")
|
||||||
|
.arg("cards")
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!("pactl exit status {}", output.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_str = std::str::from_utf8(&output.stdout)?;
|
||||||
|
let mut cards: Vec<Card> = serde_json::from_str(json_str)?;
|
||||||
|
|
||||||
|
// exclude card which has "Loopback" in name
|
||||||
|
cards.retain(|card| card.properties.device_nick != "Loopback");
|
||||||
|
|
||||||
|
Ok(cards)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_card_profile(card_index: u32, profile: &str) -> anyhow::Result<()> {
|
||||||
|
std::process::Command::new("pactl")
|
||||||
|
.arg("set-card-profile")
|
||||||
|
.arg(format!("{}", card_index))
|
||||||
|
.arg(profile)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -2,35 +2,583 @@ use std::{collections::HashMap, rc::Rc};
|
|||||||
|
|
||||||
use wgui::{
|
use wgui::{
|
||||||
assets::AssetPath,
|
assets::AssetPath,
|
||||||
|
components::{
|
||||||
|
button::{ButtonClickCallback, ComponentButton},
|
||||||
|
checkbox::ComponentCheckbox,
|
||||||
|
slider::ComponentSlider,
|
||||||
|
},
|
||||||
globals::WguiGlobals,
|
globals::WguiGlobals,
|
||||||
i18n::Translation,
|
|
||||||
layout::{Layout, WidgetID},
|
layout::{Layout, WidgetID},
|
||||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||||
widget::label::WidgetLabel,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{task::Tasks, util::pactl_wrapper};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum CurrentMode {
|
||||||
|
Sinks,
|
||||||
|
Sources,
|
||||||
|
Cards,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct IndexAndVolume {
|
||||||
|
idx: u32,
|
||||||
|
volume: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum ViewTask {
|
||||||
|
Remount,
|
||||||
|
SetMode(CurrentMode),
|
||||||
|
SetSinkVolume(IndexAndVolume),
|
||||||
|
SetSourceVolume(IndexAndVolume),
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewTasks = Tasks<ViewTask>;
|
||||||
|
|
||||||
pub struct View {
|
pub struct View {
|
||||||
|
tasks: ViewTasks,
|
||||||
|
on_update: Rc<dyn Fn()>,
|
||||||
|
|
||||||
|
globals: WguiGlobals,
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub state: ParserState,
|
state: ParserState,
|
||||||
|
|
||||||
//entry: DesktopEntry,
|
//entry: DesktopEntry,
|
||||||
|
mode: CurrentMode,
|
||||||
|
|
||||||
|
id_devices: WidgetID,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Params<'a> {
|
pub struct Params<'a> {
|
||||||
pub globals: WguiGlobals,
|
pub globals: WguiGlobals,
|
||||||
pub layout: &'a mut Layout,
|
pub layout: &'a mut Layout,
|
||||||
pub parent_id: WidgetID,
|
pub parent_id: WidgetID,
|
||||||
|
pub on_update: Rc<dyn Fn()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View {
|
struct ProfileDisplayName {
|
||||||
pub fn new(params: Params) -> anyhow::Result<Self> {
|
name: String,
|
||||||
let doc_params = &ParseDocumentParams {
|
icon_path: &'static str,
|
||||||
globals: params.globals.clone(),
|
is_vr: bool,
|
||||||
path: AssetPath::BuiltIn("gui/view/audio_settings.xml"),
|
}
|
||||||
extra: Default::default(),
|
|
||||||
|
fn get_card_from_sink<'a>(
|
||||||
|
sink: &pactl_wrapper::Sink,
|
||||||
|
cards: &'a [pactl_wrapper::Card],
|
||||||
|
) -> Option<&'a pactl_wrapper::Card> {
|
||||||
|
let Some(sink_dev_name) = &sink.properties.get("device.name") else {
|
||||||
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
|
cards.iter().find(|&card| **sink_dev_name == card.name).map(|v| v as _)
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Self { state })
|
fn get_card_from_source<'a>(
|
||||||
|
source: &pactl_wrapper::Source,
|
||||||
|
cards: &'a [pactl_wrapper::Card],
|
||||||
|
) -> Option<&'a pactl_wrapper::Card> {
|
||||||
|
let Some(source_dev_name) = &source.properties.device_name else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
cards
|
||||||
|
.iter()
|
||||||
|
.find(|&card| **source_dev_name == card.name)
|
||||||
|
.map(|v| v as _)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn does_string_mention_hmd_sink(input: &str) -> bool {
|
||||||
|
let lwr = input.to_lowercase();
|
||||||
|
lwr.contains("hmd") || // generic hmd name detected
|
||||||
|
lwr.contains("index") || // Valve hardware
|
||||||
|
lwr.contains("oculus") || // Oculus
|
||||||
|
lwr.contains("rift") || // Also Oculus
|
||||||
|
lwr.contains("beyond") // Bigscreen Beyond
|
||||||
|
}
|
||||||
|
|
||||||
|
fn does_string_mention_hmd_source(input: &str) -> bool {
|
||||||
|
let lwr = input.to_lowercase();
|
||||||
|
lwr.contains("hmd") || // generic hmd name detected
|
||||||
|
lwr.contains("valve") || // Valve hardware
|
||||||
|
lwr.contains("oculus") || // Oculus
|
||||||
|
lwr.contains("beyond") // Bigscreen Beyond
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_card_mentioning_hmd(card: &pactl_wrapper::Card) -> bool {
|
||||||
|
does_string_mention_hmd_sink(&card.properties.device_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_source_mentioning_hmd(source: &pactl_wrapper::Source) -> bool {
|
||||||
|
if let Some(source_card_name) = &source.properties.card_name
|
||||||
|
&& does_string_mention_hmd_source(source_card_name)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WiVRn
|
||||||
|
if source.name == "wivrn.source" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_profile_display_name(profile_name: &str, card: &pactl_wrapper::Card) -> ProfileDisplayName {
|
||||||
|
let Some(profile) = card.profiles.get(profile_name) else {
|
||||||
|
// fallback
|
||||||
|
return ProfileDisplayName {
|
||||||
|
name: profile_name.into(),
|
||||||
|
icon_path: "dashboard/binary.svg",
|
||||||
|
is_vr: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut out_icon_path: &'static str;
|
||||||
|
let mut is_vr = false;
|
||||||
|
|
||||||
|
let prof = profile_name.to_lowercase();
|
||||||
|
if prof.contains("analog") {
|
||||||
|
out_icon_path = "dashboard/minijack.svg";
|
||||||
|
} else if prof.contains("iec" /* digital */) {
|
||||||
|
out_icon_path = "dashboard/binary.svg";
|
||||||
|
} else if prof.contains("hdmi") {
|
||||||
|
out_icon_path = "dashboard/displayport.svg";
|
||||||
|
} else if prof.contains("off") {
|
||||||
|
out_icon_path = "dashboard/sleep.svg";
|
||||||
|
} else if prof.contains("input") {
|
||||||
|
out_icon_path = "dashboard/microphone.svg";
|
||||||
|
} else {
|
||||||
|
out_icon_path = "dashboard/volume.svg"; // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// All ports are tied to this VR headset, assign all of them to the VR icon
|
||||||
|
if is_card_mentioning_hmd(card) {
|
||||||
|
if prof.contains("mic") {
|
||||||
|
// Probably microphone
|
||||||
|
out_icon_path = "dashboard/microphone.svg";
|
||||||
|
} else {
|
||||||
|
out_icon_path = "dashboard/vr.svg";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out_name: Option<String> = None;
|
||||||
|
|
||||||
|
for port in card.ports.values() {
|
||||||
|
// Find profile
|
||||||
|
for port_profile in &port.profiles {
|
||||||
|
if !port_profile.contains("stereo") {
|
||||||
|
continue; // we only want stereo, not surround or other types
|
||||||
|
}
|
||||||
|
|
||||||
|
if port_profile != profile_name {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match! Use its device name
|
||||||
|
let Some(product_name) = port.properties.get("device.product.name") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
out_name = Some(product_name.clone());
|
||||||
|
|
||||||
|
if does_string_mention_hmd_sink(product_name) {
|
||||||
|
// VR icon
|
||||||
|
out_icon_path = "dashboard/vr.svg";
|
||||||
|
is_vr = true;
|
||||||
|
} else {
|
||||||
|
// Monitor icon
|
||||||
|
out_icon_path = "dashboard/displayport.svg";
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileDisplayName {
|
||||||
|
name: if let Some(name) = out_name {
|
||||||
|
name
|
||||||
|
} else {
|
||||||
|
profile.description.clone()
|
||||||
|
},
|
||||||
|
icon_path: out_icon_path,
|
||||||
|
is_vr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
|
||||||
|
ParseDocumentParams {
|
||||||
|
globals: globals.clone(),
|
||||||
|
path: AssetPath::BuiltIn("gui/view/audio_settings.xml"),
|
||||||
|
extra: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait DeviceControl {
|
||||||
|
fn on_volume_request(&self) -> anyhow::Result<f32>;
|
||||||
|
fn on_check(&self) -> anyhow::Result<()>;
|
||||||
|
fn on_mute_toggle(&self) -> anyhow::Result<()>;
|
||||||
|
fn on_volume_change(&self, volume: f32) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ControlSink {
|
||||||
|
tasks: ViewTasks,
|
||||||
|
on_update: Rc<dyn Fn()>,
|
||||||
|
sink: pactl_wrapper::Sink,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControlSink {
|
||||||
|
fn new(tasks: ViewTasks, on_update: Rc<dyn Fn()>, sink: pactl_wrapper::Sink) -> Self {
|
||||||
|
Self { tasks, sink, on_update }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceControl for ControlSink {
|
||||||
|
fn on_volume_request(&self) -> anyhow::Result<f32> {
|
||||||
|
let volume = pactl_wrapper::get_sink_volume(&self.sink)?;
|
||||||
|
Ok(volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_check(&self) -> anyhow::Result<()> {
|
||||||
|
pactl_wrapper::set_default_sink(self.sink.index)?;
|
||||||
|
self.tasks.push(ViewTask::Remount);
|
||||||
|
(*self.on_update)();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mute_toggle(&self) -> anyhow::Result<()> {
|
||||||
|
pactl_wrapper::set_sink_mute(self.sink.index, !self.sink.mute)?;
|
||||||
|
self.tasks.push(ViewTask::Remount);
|
||||||
|
(*self.on_update)();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_volume_change(&self, volume: f32) -> anyhow::Result<()> {
|
||||||
|
self.tasks.push(ViewTask::SetSinkVolume(IndexAndVolume {
|
||||||
|
idx: self.sink.index,
|
||||||
|
volume,
|
||||||
|
}));
|
||||||
|
(*self.on_update)();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ControlSource {
|
||||||
|
tasks: ViewTasks,
|
||||||
|
on_update: Rc<dyn Fn()>,
|
||||||
|
source: pactl_wrapper::Source,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControlSource {
|
||||||
|
fn new(tasks: ViewTasks, on_update: Rc<dyn Fn()>, source: pactl_wrapper::Source) -> Self {
|
||||||
|
Self {
|
||||||
|
tasks,
|
||||||
|
source,
|
||||||
|
on_update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceControl for ControlSource {
|
||||||
|
fn on_volume_request(&self) -> anyhow::Result<f32> {
|
||||||
|
let volume = pactl_wrapper::get_source_volume(&self.source)?;
|
||||||
|
Ok(volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_check(&self) -> anyhow::Result<()> {
|
||||||
|
pactl_wrapper::set_default_source(self.source.index)?;
|
||||||
|
self.tasks.push(ViewTask::Remount);
|
||||||
|
(*self.on_update)();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mute_toggle(&self) -> anyhow::Result<()> {
|
||||||
|
pactl_wrapper::set_source_mute(self.source.index, !self.source.mute)?;
|
||||||
|
self.tasks.push(ViewTask::Remount);
|
||||||
|
(*self.on_update)();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_volume_change(&self, volume: f32) -> anyhow::Result<()> {
|
||||||
|
self.tasks.push(ViewTask::SetSourceVolume(IndexAndVolume {
|
||||||
|
idx: self.source.index,
|
||||||
|
volume,
|
||||||
|
}));
|
||||||
|
(*self.on_update)();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MountCardParams<'a> {
|
||||||
|
layout: &'a mut Layout,
|
||||||
|
card: &'a pactl_wrapper::Card,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MountDeviceSliderParams<'a> {
|
||||||
|
layout: &'a mut Layout,
|
||||||
|
control: Rc<dyn DeviceControl>,
|
||||||
|
checked: bool,
|
||||||
|
muted: bool,
|
||||||
|
disp: Option<ProfileDisplayName>,
|
||||||
|
alt_desc: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ONE_HUNDRED_PERCENT: f32 = 100.0;
|
||||||
|
const VOLUME_MULT: f32 = 1.0 / ONE_HUNDRED_PERCENT;
|
||||||
|
|
||||||
|
impl View {
|
||||||
|
fn handle_func_button_click(&self, task: ViewTask) -> ButtonClickCallback {
|
||||||
|
let tasks = self.tasks.clone();
|
||||||
|
let on_update = self.on_update.clone();
|
||||||
|
Box::new(move |_common, _evt| {
|
||||||
|
tasks.push(task.clone());
|
||||||
|
(*on_update)();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(params: Params) -> anyhow::Result<Self> {
|
||||||
|
let tasks = ViewTasks::new();
|
||||||
|
|
||||||
|
let state = wgui::parser::parse_from_assets(&doc_params(¶ms.globals), params.layout, params.parent_id)?;
|
||||||
|
|
||||||
|
let id_devices = state.get_widget_id("devices")?;
|
||||||
|
|
||||||
|
let btn_sinks = state.fetch_component_as::<ComponentButton>("btn_sinks")?;
|
||||||
|
let btn_sources = state.fetch_component_as::<ComponentButton>("btn_sources")?;
|
||||||
|
let btn_cards = state.fetch_component_as::<ComponentButton>("btn_cards")?;
|
||||||
|
|
||||||
|
let mut res = Self {
|
||||||
|
globals: params.globals,
|
||||||
|
state,
|
||||||
|
mode: CurrentMode::Sinks,
|
||||||
|
id_devices,
|
||||||
|
tasks,
|
||||||
|
on_update: params.on_update,
|
||||||
|
};
|
||||||
|
|
||||||
|
btn_sinks.on_click(res.handle_func_button_click(ViewTask::SetMode(CurrentMode::Sinks)));
|
||||||
|
btn_sources.on_click(res.handle_func_button_click(ViewTask::SetMode(CurrentMode::Sources)));
|
||||||
|
btn_cards.on_click(res.handle_func_button_click(ViewTask::SetMode(CurrentMode::Cards)));
|
||||||
|
|
||||||
|
res.init_mode_sinks(params.layout)?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_tasks(&mut self, layout: &mut Layout) -> anyhow::Result<bool> {
|
||||||
|
let tasks = self.tasks.drain();
|
||||||
|
if tasks.is_empty() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut set_sink_volume: Option<IndexAndVolume> = None;
|
||||||
|
let mut set_source_volume: Option<IndexAndVolume> = None;
|
||||||
|
|
||||||
|
for task in tasks {
|
||||||
|
match task {
|
||||||
|
ViewTask::Remount => match self.mode {
|
||||||
|
CurrentMode::Sinks => self.init_mode_sinks(layout)?,
|
||||||
|
CurrentMode::Sources => self.init_mode_sources(layout)?,
|
||||||
|
CurrentMode::Cards => self.init_mode_cards(layout)?,
|
||||||
|
},
|
||||||
|
ViewTask::SetSinkVolume(s) => {
|
||||||
|
set_sink_volume = Some(s);
|
||||||
|
}
|
||||||
|
ViewTask::SetSourceVolume(s) => {
|
||||||
|
set_source_volume = Some(s);
|
||||||
|
}
|
||||||
|
ViewTask::SetMode(current_mode) => {
|
||||||
|
self.mode = current_mode;
|
||||||
|
self.tasks.push(ViewTask::Remount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set volume only to the latest event (prevent cpu time starvation
|
||||||
|
// due to excessive input motion events)
|
||||||
|
if let Some(s) = set_sink_volume {
|
||||||
|
pactl_wrapper::set_sink_volume(s.idx, s.volume)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s) = set_source_volume {
|
||||||
|
pactl_wrapper::set_source_volume(s.idx, s.volume)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||||
|
while self.process_tasks(layout)? {}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mount_card(&mut self, params: MountCardParams) -> anyhow::Result<()> {
|
||||||
|
log::info!("mount card TODO: {}", params.card.name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mount_device_slider(&mut self, params: MountDeviceSliderParams) -> anyhow::Result<()> {
|
||||||
|
let mut par = HashMap::<Rc<str>, Rc<str>>::new();
|
||||||
|
|
||||||
|
if let Some(disp) = ¶ms.disp {
|
||||||
|
par.insert("device_name".into(), disp.name.as_str().into());
|
||||||
|
par.insert("device_icon".into(), disp.icon_path.into());
|
||||||
|
} else {
|
||||||
|
par.insert("device_name".into(), params.alt_desc.into());
|
||||||
|
par.insert("device_icon".into(), "dashboard/binary.svg".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
par.insert(
|
||||||
|
"volume_icon".into(),
|
||||||
|
if params.muted {
|
||||||
|
"dashboard/volume_off.svg".into()
|
||||||
|
} else {
|
||||||
|
"dashboard/volume.svg".into()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = self.state.parse_template(
|
||||||
|
&doc_params(&self.globals),
|
||||||
|
"DeviceSlider",
|
||||||
|
params.layout,
|
||||||
|
self.id_devices,
|
||||||
|
par,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut c = params.layout.start_common();
|
||||||
|
let mut common = c.common();
|
||||||
|
|
||||||
|
let checkbox = data.fetch_component_as::<ComponentCheckbox>("checkbox")?;
|
||||||
|
let btn_mute = data.fetch_component_as::<ComponentButton>("btn_mute")?;
|
||||||
|
let slider = data.fetch_component_as::<ComponentSlider>("slider")?;
|
||||||
|
|
||||||
|
slider.set_value(&mut common, params.control.on_volume_request()? / VOLUME_MULT);
|
||||||
|
|
||||||
|
checkbox.set_checked(&mut common, params.checked);
|
||||||
|
|
||||||
|
checkbox.on_toggle({
|
||||||
|
let control = params.control.clone();
|
||||||
|
Box::new(move |_common, _event| {
|
||||||
|
control.on_check()?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
slider.on_value_changed({
|
||||||
|
let control = params.control.clone();
|
||||||
|
Box::new(move |_common, event| {
|
||||||
|
control.on_volume_change(event.value * VOLUME_MULT)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
btn_mute.on_click({
|
||||||
|
let control = params.control.clone();
|
||||||
|
Box::new(move |_common, _event| {
|
||||||
|
control.on_mute_toggle()?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.finish()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_mode_sinks(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||||
|
log::info!("refreshing sink list");
|
||||||
|
|
||||||
|
let sinks = pactl_wrapper::list_sinks()?;
|
||||||
|
let cards = pactl_wrapper::list_cards()?;
|
||||||
|
let default_sink = pactl_wrapper::get_default_sink(&sinks)?;
|
||||||
|
|
||||||
|
layout.remove_children(self.id_devices);
|
||||||
|
|
||||||
|
for sink in sinks {
|
||||||
|
let card = get_card_from_sink(&sink, &cards);
|
||||||
|
|
||||||
|
let checked = if let Some(default_sink) = &default_sink {
|
||||||
|
sink.index == default_sink.index
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let alt_desc = sink.description.clone();
|
||||||
|
let muted = sink.mute;
|
||||||
|
|
||||||
|
let control = Rc::new(ControlSink::new(self.tasks.clone(), self.on_update.clone(), sink));
|
||||||
|
|
||||||
|
let disp = card
|
||||||
|
.as_ref()
|
||||||
|
.map(|card| get_profile_display_name(&card.active_profile, card));
|
||||||
|
|
||||||
|
self.mount_device_slider(MountDeviceSliderParams {
|
||||||
|
checked,
|
||||||
|
disp,
|
||||||
|
alt_desc,
|
||||||
|
layout,
|
||||||
|
control,
|
||||||
|
muted,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_mode_sources(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||||
|
log::info!("refreshing source list");
|
||||||
|
|
||||||
|
let sources = pactl_wrapper::list_sources()?;
|
||||||
|
let cards = pactl_wrapper::list_cards()?;
|
||||||
|
let default_source = pactl_wrapper::get_default_source(&sources)?;
|
||||||
|
|
||||||
|
layout.remove_children(self.id_devices);
|
||||||
|
|
||||||
|
for source in sources {
|
||||||
|
let card = get_card_from_source(&source, &cards);
|
||||||
|
|
||||||
|
let checked = if let Some(default_source) = &default_source {
|
||||||
|
source.index == default_source.index
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let alt_desc = source.description.clone();
|
||||||
|
let muted = source.mute;
|
||||||
|
|
||||||
|
let control = Rc::new(ControlSource::new(self.tasks.clone(), self.on_update.clone(), source));
|
||||||
|
|
||||||
|
let disp = card
|
||||||
|
.as_ref()
|
||||||
|
.map(|card| get_profile_display_name(&card.active_profile, card));
|
||||||
|
|
||||||
|
self.mount_device_slider(MountDeviceSliderParams {
|
||||||
|
checked,
|
||||||
|
disp,
|
||||||
|
alt_desc,
|
||||||
|
layout,
|
||||||
|
control,
|
||||||
|
muted,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_mode_cards(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||||
|
log::info!("refreshing card list");
|
||||||
|
|
||||||
|
let cards = pactl_wrapper::list_cards()?;
|
||||||
|
layout.remove_children(self.id_devices);
|
||||||
|
|
||||||
|
for card in cards {
|
||||||
|
self.mount_card(MountCardParams { layout, card: &card })?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ impl Profiler {
|
|||||||
self.frametime_sum_us += frametime;
|
self.frametime_sum_us += frametime;
|
||||||
|
|
||||||
if self.last_measure_us + self.interval_us < cur_micros {
|
if self.last_measure_us + self.interval_us < cur_micros {
|
||||||
log::debug!(
|
log::trace!(
|
||||||
"avg frametime: {:.3}ms",
|
"avg frametime: {:.3}ms",
|
||||||
(self.frametime_sum_us / self.measure_frames) as f32 / 1000.0
|
(self.frametime_sum_us / self.measure_frames) as f32 / 1000.0
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -595,7 +595,7 @@ impl Layout {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("re-computing layout, size {}x{}", size.x, size.y);
|
log::trace!("re-computing layout, size {}x{}", size.x, size.y);
|
||||||
self.mark_redraw();
|
self.mark_redraw();
|
||||||
self.prev_size = size;
|
self.prev_size = size;
|
||||||
|
|
||||||
@@ -603,7 +603,7 @@ impl Layout {
|
|||||||
self.refresh_recursively(self.tree_root_node, &mut to_refresh);
|
self.refresh_recursively(self.tree_root_node, &mut to_refresh);
|
||||||
|
|
||||||
if !to_refresh.is_empty() {
|
if !to_refresh.is_empty() {
|
||||||
log::debug!("refreshing {} registered components", to_refresh.len());
|
log::trace!("refreshing {} registered components", to_refresh.len());
|
||||||
for c in &to_refresh {
|
for c in &to_refresh {
|
||||||
self.components_to_refresh_once.insert(c.clone());
|
self.components_to_refresh_once.insert(c.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user