mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-04-28 06:21:23 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4b0b10393 | |||
| 11de96db58 | |||
| 7396c0aef8 | |||
| fc2cd5e2da | |||
| 1a37729cf1 |
@@ -18,6 +18,7 @@ BuildRequires: cargo
|
|||||||
BuildRequires: pipewire-devel
|
BuildRequires: pipewire-devel
|
||||||
BuildRequires: alsa-lib-devel
|
BuildRequires: alsa-lib-devel
|
||||||
BuildRequires: clang-devel
|
BuildRequires: clang-devel
|
||||||
|
BuildRequires: cmake
|
||||||
|
|
||||||
%global _description %{expand:
|
%global _description %{expand:
|
||||||
PWSP lets you play audio files through your microphone. Has both CLI and
|
PWSP lets you play audio files through your microphone. Has both CLI and
|
||||||
|
|||||||
+369
-339
@@ -137,271 +137,285 @@ impl SoundpadGui {
|
|||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.spacing_mut().item_spacing.y = 5.0;
|
ui.spacing_mut().item_spacing.y = 5.0;
|
||||||
|
|
||||||
// --- Header ---
|
self.draw_hotkeys_header(ui);
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
|
|
||||||
if ui.add(back_button).clicked() {
|
|
||||||
self.app_state.show_hotkeys = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new("Hotkeys").color(Color32::WHITE).monospace());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
// --- Search and Add Command ---
|
self.draw_hotkeys_search(ui);
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| {
|
|
||||||
let mut selected_cmd = None;
|
|
||||||
if ui.button("Toggle Pause").clicked() {
|
|
||||||
selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None)));
|
|
||||||
}
|
|
||||||
if ui.button("Stop Playback").clicked() {
|
|
||||||
selected_cmd = Some(("cmd_stop", Request::stop(None)));
|
|
||||||
}
|
|
||||||
if ui.button("Pause Playback").clicked() {
|
|
||||||
selected_cmd = Some(("cmd_pause", Request::pause(None)));
|
|
||||||
}
|
|
||||||
if ui.button("Resume Playback").clicked() {
|
|
||||||
selected_cmd = Some(("cmd_resume", Request::resume(None)));
|
|
||||||
}
|
|
||||||
if ui.button("Toggle Loop").clicked() {
|
|
||||||
selected_cmd = Some(("cmd_toggle_loop", Request::toggle_loop(None)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((slot_name, req)) = selected_cmd {
|
|
||||||
make_request_async(Request::set_hotkey_action(slot_name, &req));
|
|
||||||
self.app_state
|
|
||||||
.hotkey_config
|
|
||||||
.set_slot(slot_name.to_string(), req);
|
|
||||||
self.app_state.assigning_hotkey_slot = Some(slot_name.to_string());
|
|
||||||
self.app_state.hotkey_capture_active = true;
|
|
||||||
ui.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
ui.add(
|
|
||||||
TextEdit::singleline(&mut self.app_state.hotkey_search_query)
|
|
||||||
.hint_text("Search hotkeys...")
|
|
||||||
.desired_width(f32::INFINITY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
|
|
||||||
let conflicts = self.app_state.hotkey_config.find_conflicts();
|
let action = self.draw_hotkeys_table(ui);
|
||||||
let conflict_slots: std::collections::HashSet<&str> =
|
|
||||||
conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect();
|
|
||||||
|
|
||||||
let search = self.app_state.hotkey_search_query.to_lowercase();
|
|
||||||
let mut action: Option<HotkeyAction> = None;
|
|
||||||
|
|
||||||
let slots: Vec<_> = self
|
|
||||||
.app_state
|
|
||||||
.hotkey_config
|
|
||||||
.slots
|
|
||||||
.iter()
|
|
||||||
.filter(|s| {
|
|
||||||
if search.is_empty() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
s.slot.to_lowercase().contains(&search)
|
|
||||||
|| format!("{:?}", s.action).to_lowercase().contains(&search)
|
|
||||||
|| s.key_chord
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_lowercase()
|
|
||||||
.contains(&search)
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let available_width = ui.available_width();
|
|
||||||
let col_width = (available_width / 4.0).max(80.0);
|
|
||||||
|
|
||||||
TableBuilder::new(ui)
|
|
||||||
.striped(true)
|
|
||||||
.column(Column::exact(col_width).clip(true)) // Slot
|
|
||||||
.column(Column::exact(col_width).clip(true)) // Sound / Action name
|
|
||||||
.column(Column::exact(col_width).clip(true)) // Key Chord
|
|
||||||
.column(Column::exact(col_width).clip(true)) // Actions
|
|
||||||
.header(30.0, |mut header| {
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("Slot")
|
|
||||||
.strong()
|
|
||||||
.monospace()
|
|
||||||
.color(Color32::LIGHT_GRAY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("Sound")
|
|
||||||
.strong()
|
|
||||||
.monospace()
|
|
||||||
.color(Color32::LIGHT_GRAY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("Key Chord")
|
|
||||||
.strong()
|
|
||||||
.monospace()
|
|
||||||
.color(Color32::LIGHT_GRAY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("Actions")
|
|
||||||
.strong()
|
|
||||||
.monospace()
|
|
||||||
.color(Color32::LIGHT_GRAY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.body(|mut body| {
|
|
||||||
if slots.is_empty() {
|
|
||||||
body.row(30.0, |mut row| {
|
|
||||||
row.col(|_| {});
|
|
||||||
row.col(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("No hotkey slots configured.")
|
|
||||||
.color(Color32::GRAY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
row.col(|_| {});
|
|
||||||
row.col(|_| {});
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for slot in &slots {
|
|
||||||
body.row(30.0, |mut row| {
|
|
||||||
// Column 1: Slot
|
|
||||||
row.col(|ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if conflict_slots.contains(slot.slot.as_str()) {
|
|
||||||
ui.label(
|
|
||||||
RichText::new(ICON_WARNING.codepoint)
|
|
||||||
.color(Color32::from_rgb(255, 165, 0)),
|
|
||||||
)
|
|
||||||
.on_hover_text("Key chord conflict");
|
|
||||||
}
|
|
||||||
ui.add(
|
|
||||||
Label::new(RichText::new(&slot.slot).monospace())
|
|
||||||
.truncate(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Column 2: Sound / Action name
|
|
||||||
row.col(|ui| {
|
|
||||||
let action_name = match slot.action.name.as_str() {
|
|
||||||
"play" => {
|
|
||||||
if let Some(file_path_str) =
|
|
||||||
slot.action.args.get("file_path")
|
|
||||||
{
|
|
||||||
Path::new(file_path_str)
|
|
||||||
.file_name()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string()
|
|
||||||
} else {
|
|
||||||
"Play".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"toggle_pause" => "Toggle Pause".to_string(),
|
|
||||||
"pause" => "Pause Playback".to_string(),
|
|
||||||
"resume" => "Resume Playback".to_string(),
|
|
||||||
"stop" => "Stop Playback".to_string(),
|
|
||||||
"toggle_loop" => "Toggle Loop".to_string(),
|
|
||||||
other => other.to_string(),
|
|
||||||
};
|
|
||||||
ui.add(
|
|
||||||
Label::new(RichText::new(action_name).monospace()).truncate(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Column 3: Key Chord
|
|
||||||
row.col(|ui| {
|
|
||||||
let chord_text = slot.key_chord.as_deref().unwrap_or("(none)");
|
|
||||||
ui.add(
|
|
||||||
Label::new(RichText::new(chord_text).monospace().color(
|
|
||||||
if slot.key_chord.is_some() {
|
|
||||||
Color32::from_rgb(100, 200, 100)
|
|
||||||
} else {
|
|
||||||
Color32::GRAY
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.truncate(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Column 4: Actions
|
|
||||||
row.col(|ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui
|
|
||||||
.add(Button::new(ICON_DELETE).frame(false))
|
|
||||||
.on_hover_text("Remove slot")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
action = Some(HotkeyAction::Remove(slot.slot.clone()));
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.add(Button::new(ICON_KEYBOARD).frame(false))
|
|
||||||
.on_hover_text("Set key chord")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
action = Some(HotkeyAction::Capture(slot.slot.clone()));
|
|
||||||
}
|
|
||||||
if slot.key_chord.is_some()
|
|
||||||
&& ui
|
|
||||||
.add(Button::new(ICON_BACKSPACE).frame(false))
|
|
||||||
.on_hover_text("Clear key chord")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
action = Some(HotkeyAction::ClearChord(slot.slot.clone()));
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.add(Button::new(ICON_PLAY_ARROW).frame(false))
|
|
||||||
.on_hover_text("Play")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
action = Some(HotkeyAction::Play(slot.slot.clone()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(action) = action {
|
if let Some(action) = action {
|
||||||
match action {
|
self.handle_hotkey_action(action);
|
||||||
HotkeyAction::Remove(slot) => {
|
|
||||||
make_request_async(Request::clear_hotkey(&slot));
|
|
||||||
self.app_state.hotkey_config.remove_slot(&slot);
|
|
||||||
}
|
|
||||||
HotkeyAction::Capture(slot) => {
|
|
||||||
self.app_state.assigning_hotkey_slot = Some(slot);
|
|
||||||
self.app_state.hotkey_capture_active = true;
|
|
||||||
}
|
|
||||||
HotkeyAction::ClearChord(slot) => {
|
|
||||||
make_request_async(Request::clear_hotkey_key(&slot));
|
|
||||||
self.app_state.hotkey_config.set_key_chord(&slot, None);
|
|
||||||
}
|
|
||||||
HotkeyAction::Play(slot) => {
|
|
||||||
self.play_hotkey_slot(&slot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_hotkeys_header(&mut self, ui: &mut Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
|
||||||
|
if ui.add(back_button).clicked() {
|
||||||
|
self.app_state.show_hotkeys = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(RichText::new("Hotkeys").color(Color32::WHITE).monospace());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_hotkeys_search(&mut self, ui: &mut Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| {
|
||||||
|
let mut selected_cmd = None;
|
||||||
|
if ui.button("Toggle Pause").clicked() {
|
||||||
|
selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None)));
|
||||||
|
}
|
||||||
|
if ui.button("Stop Playback").clicked() {
|
||||||
|
selected_cmd = Some(("cmd_stop", Request::stop(None)));
|
||||||
|
}
|
||||||
|
if ui.button("Pause Playback").clicked() {
|
||||||
|
selected_cmd = Some(("cmd_pause", Request::pause(None)));
|
||||||
|
}
|
||||||
|
if ui.button("Resume Playback").clicked() {
|
||||||
|
selected_cmd = Some(("cmd_resume", Request::resume(None)));
|
||||||
|
}
|
||||||
|
if ui.button("Toggle Loop").clicked() {
|
||||||
|
selected_cmd = Some(("cmd_toggle_loop", Request::toggle_loop(None)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((slot_name, req)) = selected_cmd {
|
||||||
|
make_request_async(Request::set_hotkey_action(slot_name, &req));
|
||||||
|
self.app_state
|
||||||
|
.hotkey_config
|
||||||
|
.set_slot(slot_name.to_string(), req);
|
||||||
|
self.app_state.assigning_hotkey_slot = Some(slot_name.to_string());
|
||||||
|
self.app_state.hotkey_capture_active = true;
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
ui.add(
|
||||||
|
TextEdit::singleline(&mut self.app_state.hotkey_search_query)
|
||||||
|
.hint_text("Search hotkeys...")
|
||||||
|
.desired_width(f32::INFINITY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_hotkeys_table(&mut self, ui: &mut Ui) -> Option<HotkeyAction> {
|
||||||
|
let conflicts = self.app_state.hotkey_config.find_conflicts();
|
||||||
|
let conflict_slots: std::collections::HashSet<&str> =
|
||||||
|
conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect();
|
||||||
|
|
||||||
|
let search = self.app_state.hotkey_search_query.to_lowercase();
|
||||||
|
let mut action: Option<HotkeyAction> = None;
|
||||||
|
|
||||||
|
let slots: Vec<_> = self
|
||||||
|
.app_state
|
||||||
|
.hotkey_config
|
||||||
|
.slots
|
||||||
|
.iter()
|
||||||
|
.filter(|s| {
|
||||||
|
if search.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
s.slot.to_lowercase().contains(&search)
|
||||||
|
|| format!("{:?}", s.action).to_lowercase().contains(&search)
|
||||||
|
|| s.key_chord
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_lowercase()
|
||||||
|
.contains(&search)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let available_width = ui.available_width();
|
||||||
|
let col_width = (available_width / 4.0).max(80.0);
|
||||||
|
|
||||||
|
TableBuilder::new(ui)
|
||||||
|
.striped(true)
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Slot
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Sound / Action name
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Key Chord
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Actions
|
||||||
|
.header(30.0, |mut header| {
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Slot")
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Sound")
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Key Chord")
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Actions")
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|mut body| {
|
||||||
|
if slots.is_empty() {
|
||||||
|
body.row(30.0, |mut row| {
|
||||||
|
row.col(|_| {});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("No hotkey slots configured.")
|
||||||
|
.color(Color32::GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
row.col(|_| {});
|
||||||
|
row.col(|_| {});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for slot in &slots {
|
||||||
|
body.row(30.0, |mut row| {
|
||||||
|
// Column 1: Slot
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if conflict_slots.contains(slot.slot.as_str()) {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(ICON_WARNING.codepoint)
|
||||||
|
.color(Color32::from_rgb(255, 165, 0)),
|
||||||
|
)
|
||||||
|
.on_hover_text("Key chord conflict");
|
||||||
|
}
|
||||||
|
ui.add(
|
||||||
|
Label::new(RichText::new(&slot.slot).monospace())
|
||||||
|
.truncate(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Column 2: Sound / Action name
|
||||||
|
row.col(|ui| {
|
||||||
|
let action_name = match slot.action.name.as_str() {
|
||||||
|
"play" => {
|
||||||
|
if let Some(file_path_str) =
|
||||||
|
slot.action.args.get("file_path")
|
||||||
|
{
|
||||||
|
Path::new(file_path_str)
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
"Play".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"toggle_pause" => "Toggle Pause".to_string(),
|
||||||
|
"pause" => "Pause Playback".to_string(),
|
||||||
|
"resume" => "Resume Playback".to_string(),
|
||||||
|
"stop" => "Stop Playback".to_string(),
|
||||||
|
"toggle_loop" => "Toggle Loop".to_string(),
|
||||||
|
other => other.to_string(),
|
||||||
|
};
|
||||||
|
ui.add(
|
||||||
|
Label::new(RichText::new(action_name).monospace()).truncate(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Column 3: Key Chord
|
||||||
|
row.col(|ui| {
|
||||||
|
let chord_text = slot.key_chord.as_deref().unwrap_or("(none)");
|
||||||
|
ui.add(
|
||||||
|
Label::new(RichText::new(chord_text).monospace().color(
|
||||||
|
if slot.key_chord.is_some() {
|
||||||
|
Color32::from_rgb(100, 200, 100)
|
||||||
|
} else {
|
||||||
|
Color32::GRAY
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.truncate(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Column 4: Actions
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui
|
||||||
|
.add(Button::new(ICON_DELETE).frame(false))
|
||||||
|
.on_hover_text("Remove slot")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::Remove(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add(Button::new(ICON_KEYBOARD).frame(false))
|
||||||
|
.on_hover_text("Set key chord")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::Capture(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
if slot.key_chord.is_some()
|
||||||
|
&& ui
|
||||||
|
.add(Button::new(ICON_BACKSPACE).frame(false))
|
||||||
|
.on_hover_text("Clear key chord")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::ClearChord(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add(Button::new(ICON_PLAY_ARROW).frame(false))
|
||||||
|
.on_hover_text("Play")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::Play(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_hotkey_action(&mut self, action: HotkeyAction) {
|
||||||
|
match action {
|
||||||
|
HotkeyAction::Remove(slot) => {
|
||||||
|
make_request_async(Request::clear_hotkey(&slot));
|
||||||
|
self.app_state.hotkey_config.remove_slot(&slot);
|
||||||
|
}
|
||||||
|
HotkeyAction::Capture(slot) => {
|
||||||
|
self.app_state.assigning_hotkey_slot = Some(slot);
|
||||||
|
self.app_state.hotkey_capture_active = true;
|
||||||
|
}
|
||||||
|
HotkeyAction::ClearChord(slot) => {
|
||||||
|
make_request_async(Request::clear_hotkey_key(&slot));
|
||||||
|
self.app_state.hotkey_config.set_key_chord(&slot, None);
|
||||||
|
}
|
||||||
|
HotkeyAction::Play(slot) => {
|
||||||
|
self.play_hotkey_slot(&slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_header(&mut self, ui: &mut Ui) {
|
fn draw_header(&mut self, ui: &mut Ui) {
|
||||||
ui.vertical_centered_justified(|ui| {
|
ui.vertical_centered_justified(|ui| {
|
||||||
if self.audio_player_state.tracks.is_empty() {
|
if self.audio_player_state.tracks.is_empty() {
|
||||||
@@ -409,10 +423,9 @@ impl SoundpadGui {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tracks = self.audio_player_state.tracks.clone();
|
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
|
|
||||||
for track in tracks {
|
for track in &self.audio_player_state.tracks {
|
||||||
CollapsingHeader::new(
|
CollapsingHeader::new(
|
||||||
RichText::new(
|
RichText::new(
|
||||||
track
|
track
|
||||||
@@ -445,6 +458,99 @@ impl SoundpadGui {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_playback_controls(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
|
||||||
|
let mut action = None;
|
||||||
|
|
||||||
|
let play_button = Button::new(if track.paused {
|
||||||
|
ICON_PLAY_ARROW
|
||||||
|
} else {
|
||||||
|
ICON_PAUSE
|
||||||
|
})
|
||||||
|
.corner_radius(15.0);
|
||||||
|
|
||||||
|
if ui.add_sized([30.0, 30.0], play_button).clicked() {
|
||||||
|
action = Some(if track.paused {
|
||||||
|
TrackAction::Resume(track.id)
|
||||||
|
} else {
|
||||||
|
TrackAction::Pause(track.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let loop_button = Button::new(
|
||||||
|
RichText::new(if track.looped {
|
||||||
|
ICON_REPEAT_ONE
|
||||||
|
} else {
|
||||||
|
ICON_REPEAT
|
||||||
|
})
|
||||||
|
.size(18.0),
|
||||||
|
)
|
||||||
|
.frame(false);
|
||||||
|
|
||||||
|
if ui.add_sized([15.0, 30.0], loop_button).clicked() {
|
||||||
|
action = Some(TrackAction::ToggleLoop(track.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_position_control(
|
||||||
|
ui: &mut Ui,
|
||||||
|
ui_state: &mut pwsp::types::gui::TrackUiState,
|
||||||
|
track: &TrackInfo,
|
||||||
|
default_slider_width: f32,
|
||||||
|
) {
|
||||||
|
let duration = track.duration.unwrap_or(1.0);
|
||||||
|
let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
|
||||||
|
let position_slider_width = ui.available_width()
|
||||||
|
- (30.0 * 3.0)
|
||||||
|
- default_slider_width
|
||||||
|
- (ui.spacing().item_spacing.x * 6.0);
|
||||||
|
|
||||||
|
ui.spacing_mut().slider_width = position_slider_width;
|
||||||
|
if ui.add_sized([30.0, 30.0], position_slider).drag_stopped() {
|
||||||
|
ui_state.position_dragged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_label =
|
||||||
|
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
|
||||||
|
ui.add_sized([30.0, 30.0], time_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_volume_control(
|
||||||
|
ui: &mut Ui,
|
||||||
|
ui_state: &mut pwsp::types::gui::TrackUiState,
|
||||||
|
track: &TrackInfo,
|
||||||
|
default_slider_width: f32,
|
||||||
|
) {
|
||||||
|
let volume_icon = Self::get_volume_icon(track.volume);
|
||||||
|
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
||||||
|
ui.add_sized([30.0, 30.0], volume_label)
|
||||||
|
.on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0));
|
||||||
|
|
||||||
|
let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
|
||||||
|
ui.spacing_mut().slider_width = default_slider_width - 30.0;
|
||||||
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
|
||||||
|
if ui.add_sized([30.0, 30.0], volume_slider).drag_stopped() {
|
||||||
|
ui_state.volume_dragged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_stop_control(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
|
||||||
|
let stop_button = Button::new(ICON_CLOSE).frame(false);
|
||||||
|
if ui.add_sized([30.0, 30.0], stop_button).clicked() {
|
||||||
|
Some(TrackAction::Stop(track.id))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_track_control(
|
fn draw_track_control(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
@@ -475,93 +581,17 @@ impl SoundpadGui {
|
|||||||
let mut action = None;
|
let mut action = None;
|
||||||
|
|
||||||
ui.horizontal_top(|ui| {
|
ui.horizontal_top(|ui| {
|
||||||
// ---------- Play Button ----------
|
if let Some(act) = Self::draw_playback_controls(ui, track) {
|
||||||
let play_button = Button::new(if track.paused {
|
action = Some(act);
|
||||||
ICON_PLAY_ARROW
|
|
||||||
} else {
|
|
||||||
ICON_PAUSE
|
|
||||||
})
|
|
||||||
.corner_radius(15.0);
|
|
||||||
|
|
||||||
let play_button_response = ui.add_sized([30.0, 30.0], play_button);
|
|
||||||
if play_button_response.clicked() {
|
|
||||||
if track.paused {
|
|
||||||
action = Some(TrackAction::Resume(track.id));
|
|
||||||
} else {
|
|
||||||
action = Some(TrackAction::Pause(track.id));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Loop Button ----------
|
|
||||||
let loop_button = Button::new(
|
|
||||||
RichText::new(if track.looped {
|
|
||||||
ICON_REPEAT_ONE
|
|
||||||
} else {
|
|
||||||
ICON_REPEAT
|
|
||||||
})
|
|
||||||
.size(18.0),
|
|
||||||
)
|
|
||||||
.frame(false);
|
|
||||||
|
|
||||||
let loop_button_response = ui.add_sized([15.0, 30.0], loop_button);
|
|
||||||
if loop_button_response.clicked() {
|
|
||||||
action = Some(TrackAction::ToggleLoop(track.id));
|
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Position Slider ----------
|
|
||||||
let duration = track.duration.unwrap_or(1.0);
|
|
||||||
let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration)
|
|
||||||
.show_value(false)
|
|
||||||
.step_by(0.01);
|
|
||||||
|
|
||||||
let default_slider_width = ui.spacing().slider_width;
|
let default_slider_width = ui.spacing().slider_width;
|
||||||
let position_slider_width = ui.available_width()
|
Self::draw_position_control(ui, ui_state, track, default_slider_width);
|
||||||
- (30.0 * 3.0)
|
Self::draw_volume_control(ui, ui_state, track, default_slider_width);
|
||||||
- default_slider_width
|
|
||||||
- (ui.spacing().item_spacing.x * 6.0);
|
if let Some(act) = Self::draw_stop_control(ui, track) {
|
||||||
ui.spacing_mut().slider_width = position_slider_width;
|
action = Some(act);
|
||||||
let position_slider_response = ui.add_sized([30.0, 30.0], position_slider);
|
|
||||||
if position_slider_response.drag_stopped() {
|
|
||||||
ui_state.position_dragged = true;
|
|
||||||
}
|
}
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Time Label ----------
|
|
||||||
let time_label =
|
|
||||||
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
|
|
||||||
ui.add_sized([30.0, 30.0], time_label);
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Volume Icon ----------
|
|
||||||
let volume_icon = Self::get_volume_icon(track.volume);
|
|
||||||
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
|
||||||
ui.add_sized([30.0, 30.0], volume_label)
|
|
||||||
.on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0));
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Volume Slider ----------
|
|
||||||
let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0)
|
|
||||||
.show_value(false)
|
|
||||||
.step_by(0.01);
|
|
||||||
|
|
||||||
ui.spacing_mut().slider_width = default_slider_width - 30.0;
|
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
|
||||||
|
|
||||||
let volume_slider_response = ui.add_sized([30.0, 30.0], volume_slider);
|
|
||||||
if volume_slider_response.drag_stopped() {
|
|
||||||
ui_state.volume_dragged = true;
|
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Stop Button ---------
|
|
||||||
let stop_button = Button::new(ICON_CLOSE).frame(false);
|
|
||||||
let stop_button_response = ui.add_sized([30.0, 30.0], stop_button);
|
|
||||||
if stop_button_response.clicked() {
|
|
||||||
action = Some(TrackAction::Stop(track.id));
|
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
});
|
});
|
||||||
|
|
||||||
action
|
action
|
||||||
|
|||||||
+8
-2
@@ -9,7 +9,10 @@ use std::path::PathBuf;
|
|||||||
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
|
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
|
||||||
let key_name = key.name();
|
let key_name = key.name();
|
||||||
let is_valid = (key_name.len() == 1
|
let is_valid = (key_name.len() == 1
|
||||||
&& key_name.chars().next().is_some_and(|c| c.is_ascii_alphanumeric()))
|
&& key_name
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.is_some_and(|c| c.is_ascii_alphanumeric()))
|
||||||
|| (key_name.starts_with('F')
|
|| (key_name.starts_with('F')
|
||||||
&& key_name.len() > 1
|
&& key_name.len() > 1
|
||||||
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
|
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
|
||||||
@@ -60,7 +63,10 @@ pub fn parse_chord(chord: &str) -> Option<(Modifiers, Key)> {
|
|||||||
|
|
||||||
let key_name = parts[parts.len() - 1];
|
let key_name = parts[parts.len() - 1];
|
||||||
let is_valid = (key_name.len() == 1
|
let is_valid = (key_name.len() == 1
|
||||||
&& key_name.chars().next().is_some_and(|c| c.is_ascii_alphanumeric()))
|
&& key_name
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.is_some_and(|c| c.is_ascii_alphanumeric()))
|
||||||
|| (key_name.starts_with('F')
|
|| (key_name.starts_with('F')
|
||||||
&& key_name.len() > 1
|
&& key_name.len() > 1
|
||||||
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
|
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
|
||||||
|
|||||||
+112
-33
@@ -9,11 +9,11 @@ use tokio::{
|
|||||||
time::{Duration, timeout},
|
time::{Duration, timeout},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn setup_pipewire_context() -> (MainLoopRc, ContextRc) {
|
pub fn setup_pipewire_context() -> Result<(MainLoopRc, ContextRc), String> {
|
||||||
pipewire::init();
|
pipewire::init();
|
||||||
let main_loop = MainLoopRc::new(None).expect("Failed to initialize pipewire main loop");
|
let main_loop = MainLoopRc::new(None).map_err(|e| e.to_string())?;
|
||||||
let context = ContextRc::new(&main_loop, None).expect("Failed to create pipewire context");
|
let context = ContextRc::new(&main_loop, None).map_err(|e| e.to_string())?;
|
||||||
(main_loop, context)
|
Ok((main_loop, context))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_global_object(
|
fn parse_global_object(
|
||||||
@@ -85,8 +85,15 @@ fn parse_global_object(
|
|||||||
async fn pw_get_global_objects_thread(
|
async fn pw_get_global_objects_thread(
|
||||||
main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>,
|
main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>,
|
||||||
pw_receiver: pipewire::channel::Receiver<Terminate>,
|
pw_receiver: pipewire::channel::Receiver<Terminate>,
|
||||||
|
init_sender: std::sync::mpsc::SyncSender<Result<(), String>>,
|
||||||
) {
|
) {
|
||||||
let (main_loop, context) = setup_pipewire_context();
|
let (main_loop, context) = match setup_pipewire_context() {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = init_sender.send(Err(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Stop main loop on Terminate message
|
// Stop main loop on Terminate message
|
||||||
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
||||||
@@ -94,12 +101,24 @@ async fn pw_get_global_objects_thread(
|
|||||||
move |_| _main_loop.quit()
|
move |_| _main_loop.quit()
|
||||||
});
|
});
|
||||||
|
|
||||||
let core = context
|
let core = match context.connect(None) {
|
||||||
.connect(None)
|
Ok(core) => core,
|
||||||
.expect("Failed to connect to pipewire context");
|
Err(e) => {
|
||||||
let registry = core
|
let _ = init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
|
||||||
.get_registry()
|
return;
|
||||||
.expect("Failed to get registry from pipewire context");
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let registry = match core.get_registry() {
|
||||||
|
Ok(registry) => registry,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = init_sender.send(Err(format!(
|
||||||
|
"Failed to get registry from pipewire context: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let _listener = registry
|
let _listener = registry
|
||||||
.add_listener_local()
|
.add_listener_local()
|
||||||
@@ -115,6 +134,11 @@ async fn pw_get_global_objects_thread(
|
|||||||
})
|
})
|
||||||
.register();
|
.register();
|
||||||
|
|
||||||
|
// Signal successful initialization
|
||||||
|
if init_sender.send(Ok(())).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
main_loop.run();
|
main_loop.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +146,17 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
|
|||||||
// Channels to communicate with pipewire thread
|
// Channels to communicate with pipewire thread
|
||||||
let (main_sender, mut main_receiver) = mpsc::channel(10);
|
let (main_sender, mut main_receiver) = mpsc::channel(10);
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel();
|
let (pw_sender, pw_receiver) = pipewire::channel::channel();
|
||||||
|
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
||||||
|
|
||||||
// Spawn pipewire thread in background
|
// Spawn pipewire thread in background
|
||||||
let _pw_thread =
|
let _pw_thread = tokio::spawn(async move {
|
||||||
tokio::spawn(async move { pw_get_global_objects_thread(main_sender, pw_receiver).await });
|
pw_get_global_objects_thread(main_sender, pw_receiver, init_sender).await
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for initialization to complete
|
||||||
|
if let Err(e) = init_receiver.recv()? {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
|
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
|
||||||
let mut output_devices: HashMap<u32, AudioDevice> = HashMap::new();
|
let mut output_devices: HashMap<u32, AudioDevice> = HashMap::new();
|
||||||
@@ -150,9 +181,7 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
|
|||||||
}
|
}
|
||||||
Ok(None) | Err(_) => {
|
Ok(None) | Err(_) => {
|
||||||
// Pipewire thread is finished and we can collect our devices
|
// Pipewire thread is finished and we can collect our devices
|
||||||
pw_sender
|
let _ = pw_sender.send(Terminate {});
|
||||||
.send(Terminate {})
|
|
||||||
.expect("Failed to terminate pipewire thread");
|
|
||||||
|
|
||||||
for port in ports {
|
for port in ports {
|
||||||
let node_id = port.node_id;
|
let node_id = port.node_id;
|
||||||
@@ -226,12 +255,24 @@ pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>
|
|||||||
|
|
||||||
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
||||||
|
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
||||||
|
|
||||||
let _pw_thread = thread::spawn(move || {
|
let _pw_thread = thread::spawn(move || {
|
||||||
let (main_loop, context) = setup_pipewire_context();
|
let (main_loop, context) = match setup_pipewire_context() {
|
||||||
let core = context
|
Ok(res) => res,
|
||||||
.connect(None)
|
Err(e) => {
|
||||||
.expect("Failed to connect to pipewire context");
|
let _ = init_sender.send(Err(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let core = match context.connect(None) {
|
||||||
|
Ok(core) => core,
|
||||||
|
Err(e) => {
|
||||||
|
let _ =
|
||||||
|
init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let props = properties!(
|
let props = properties!(
|
||||||
"factory.name" => "support.null-audio-sink",
|
"factory.name" => "support.null-audio-sink",
|
||||||
@@ -243,9 +284,13 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
|
|||||||
"object.linger" => "false", // Destroy the node on app exit
|
"object.linger" => "false", // Destroy the node on app exit
|
||||||
);
|
);
|
||||||
|
|
||||||
let _node = core
|
let _node = match core.create_object::<pipewire::node::Node>("adapter", &props) {
|
||||||
.create_object::<pipewire::node::Node>("adapter", &props)
|
Ok(node) => node,
|
||||||
.expect("Failed to create virtual mic");
|
Err(e) => {
|
||||||
|
let _ = init_sender.send(Err(format!("Failed to create virtual mic: {}", e)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
||||||
let _main_loop = main_loop.clone();
|
let _main_loop = main_loop.clone();
|
||||||
@@ -253,9 +298,16 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
|
|||||||
});
|
});
|
||||||
|
|
||||||
println!("Virtual mic created");
|
println!("Virtual mic created");
|
||||||
|
if init_sender.send(Ok(())).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
main_loop.run();
|
main_loop.run();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Err(e) = init_receiver.recv()? {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(pw_sender)
|
Ok(pw_sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,12 +356,24 @@ pub fn create_link(
|
|||||||
input_fr: Port,
|
input_fr: Port,
|
||||||
) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
||||||
|
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
||||||
|
|
||||||
let _pw_thread = thread::spawn(move || {
|
let _pw_thread = thread::spawn(move || {
|
||||||
let (main_loop, context) = setup_pipewire_context();
|
let (main_loop, context) = match setup_pipewire_context() {
|
||||||
let core = context
|
Ok(res) => res,
|
||||||
.connect(None)
|
Err(e) => {
|
||||||
.expect("Failed to connect to pipewire context");
|
let _ = init_sender.send(Err(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let core = match context.connect(None) {
|
||||||
|
Ok(core) => core,
|
||||||
|
Err(e) => {
|
||||||
|
let _ =
|
||||||
|
init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let props_fl = properties! {
|
let props_fl = properties! {
|
||||||
"link.output.node" => format!("{}", output_fl.node_id).as_str(),
|
"link.output.node" => format!("{}", output_fl.node_id).as_str(),
|
||||||
@@ -324,12 +388,20 @@ pub fn create_link(
|
|||||||
"link.input.port" => format!("{}", input_fr.port_id).as_str(),
|
"link.input.port" => format!("{}", input_fr.port_id).as_str(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let _link_fl = core
|
let _link_fl = match core.create_object::<Link>("link-factory", &props_fl) {
|
||||||
.create_object::<Link>("link-factory", &props_fl)
|
Ok(link) => link,
|
||||||
.expect("Failed to create link FL");
|
Err(e) => {
|
||||||
let _link_fr = core
|
let _ = init_sender.send(Err(format!("Failed to create link FL: {}", e)));
|
||||||
.create_object::<Link>("link-factory", &props_fr)
|
return;
|
||||||
.expect("Failed to create link FR");
|
}
|
||||||
|
};
|
||||||
|
let _link_fr = match core.create_object::<Link>("link-factory", &props_fr) {
|
||||||
|
Ok(link) => link,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = init_sender.send(Err(format!("Failed to create link FR: {}", e)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
let _receiver = pw_receiver.attach(main_loop.loop_(), {
|
||||||
let _main_loop = main_loop.clone();
|
let _main_loop = main_loop.clone();
|
||||||
@@ -340,8 +412,15 @@ pub fn create_link(
|
|||||||
"Link created: FL: {}-{} FR: {}-{}",
|
"Link created: FL: {}-{} FR: {}-{}",
|
||||||
output_fl.node_id, input_fl.node_id, output_fr.node_id, input_fr.node_id
|
output_fl.node_id, input_fl.node_id, output_fr.node_id, input_fr.node_id
|
||||||
);
|
);
|
||||||
|
if init_sender.send(Ok(())).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
main_loop.run();
|
main_loop.run();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Err(e) = init_receiver.recv()? {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(pw_sender)
|
Ok(pw_sender)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user