notification sounds
This commit is contained in:
@@ -4,7 +4,13 @@ use dbus::{
|
|||||||
message::MatchRule,
|
message::MatchRule,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::{mpsc, Arc};
|
use std::{
|
||||||
|
sync::{
|
||||||
|
mpsc::{self},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{overlays::toast::Toast, state::AppState};
|
use crate::{overlays::toast::Toast, state::AppState};
|
||||||
|
|
||||||
@@ -25,6 +31,10 @@ impl NotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn submit_pending(&self, app: &mut AppState) {
|
pub fn submit_pending(&self, app: &mut AppState) {
|
||||||
|
if let Some((c, _)) = &self.dbus_data {
|
||||||
|
let _ = c.process(Duration::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
self.rx_toast.try_iter().for_each(|toast| {
|
self.rx_toast.try_iter().for_each(|toast| {
|
||||||
toast.submit(app);
|
toast.submit(app);
|
||||||
});
|
});
|
||||||
@@ -44,9 +54,7 @@ impl NotificationManager {
|
|||||||
|
|
||||||
let sender = self.tx_toast.clone();
|
let sender = self.tx_toast.clone();
|
||||||
|
|
||||||
let token = c.start_receive(
|
let Ok(token) = c.add_match(rule, move |_: (), _, msg| {
|
||||||
rule,
|
|
||||||
Box::new(move |msg, _| {
|
|
||||||
if let Ok(toast) = parse_dbus(&msg) {
|
if let Ok(toast) = parse_dbus(&msg) {
|
||||||
match sender.try_send(toast) {
|
match sender.try_send(toast) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
@@ -56,8 +64,10 @@ impl NotificationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}),
|
}) else {
|
||||||
);
|
log::error!("Failed to add dbus match. Desktop notifications will not work.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
self.dbus_data = Some((c, token));
|
self.dbus_data = Some((c, token));
|
||||||
}
|
}
|
||||||
@@ -73,7 +83,7 @@ impl NotificationManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut buf = [0u8; 1500];
|
let mut buf = [0u8; 1024 * 16]; // vrcx embeds icons as b64
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Ok((num_bytes, _)) = socket.recv_from(&mut buf) {
|
if let Ok((num_bytes, _)) = socket.recv_from(&mut buf) {
|
||||||
@@ -84,6 +94,7 @@ impl NotificationManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
log::info!("Received notification message: {}", json_str);
|
||||||
let msg = match serde_json::from_str::<XsoMessage>(json_str) {
|
let msg = match serde_json::from_str::<XsoMessage>(json_str) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -92,10 +103,13 @@ impl NotificationManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if msg.messageType != 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let toast = Toast::new(msg.title, msg.content.unwrap_or_else(|| "".into()))
|
let toast = Toast::new(msg.title, msg.content.unwrap_or_else(|| "".into()))
|
||||||
.with_timeout(msg.timeout)
|
.with_timeout(msg.timeout.unwrap_or(5.))
|
||||||
.with_volume(msg.volume)
|
.with_sound(msg.volume.unwrap_or(0.) > 0.1);
|
||||||
.with_opacity(msg.opacity);
|
|
||||||
|
|
||||||
match sender.try_send(toast) {
|
match sender.try_send(toast) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
@@ -131,23 +145,26 @@ fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
|
|||||||
summary
|
summary
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Toast::new(title.into(), body.into()))
|
Ok(Toast::new(title.into(), body.into())
|
||||||
|
.with_sound(true)
|
||||||
|
.with_timeout(5.0)
|
||||||
|
.with_opacity(1.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct XsoMessage {
|
struct XsoMessage {
|
||||||
messageType: i32,
|
messageType: i32,
|
||||||
index: i32,
|
index: Option<i32>,
|
||||||
volume: f32,
|
volume: Option<f32>,
|
||||||
audioPath: Arc<str>,
|
audioPath: Option<Arc<str>>,
|
||||||
timeout: f32,
|
timeout: Option<f32>,
|
||||||
title: Arc<str>,
|
title: Arc<str>,
|
||||||
content: Option<Arc<str>>,
|
content: Option<Arc<str>>,
|
||||||
icon: Option<Arc<str>>,
|
icon: Option<Arc<str>>,
|
||||||
height: f32,
|
height: Option<f32>,
|
||||||
opacity: f32,
|
opacity: Option<f32>,
|
||||||
useBase64Icon: bool,
|
useBase64Icon: Option<bool>,
|
||||||
sourceApp: Option<Arc<str>>,
|
sourceApp: Option<Arc<str>>,
|
||||||
alwaysShow: bool,
|
alwaysShow: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ use crate::{
|
|||||||
config,
|
config,
|
||||||
gui::{color_parse, CanvasBuilder, Control},
|
gui::{color_parse, CanvasBuilder, Control},
|
||||||
hid::{KeyModifier, VirtualKey, ALT, CTRL, KEYS_TO_MODS, META, SHIFT, SUPER},
|
hid::{KeyModifier, VirtualKey, ALT, CTRL, KEYS_TO_MODS, META, SHIFT, SUPER},
|
||||||
state::{AppSession, AppState},
|
state::AppState,
|
||||||
};
|
};
|
||||||
use glam::{vec2, vec3a, Affine2, Vec4};
|
use glam::{vec2, vec3a, Affine2, Vec4};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rodio::{Decoder, OutputStream, OutputStreamHandle, Source};
|
use rodio::{Decoder, Source};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
const PIXELS_PER_UNIT: f32 = 80.;
|
const PIXELS_PER_UNIT: f32 = 80.;
|
||||||
@@ -39,9 +39,6 @@ where
|
|||||||
let data = KeyboardData {
|
let data = KeyboardData {
|
||||||
modifiers: 0,
|
modifiers: 0,
|
||||||
processes: vec![],
|
processes: vec![],
|
||||||
audio_stream: None,
|
|
||||||
first_try: true,
|
|
||||||
audio_handle: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut canvas = CanvasBuilder::new(
|
let mut canvas = CanvasBuilder::new(
|
||||||
@@ -142,7 +139,7 @@ fn key_press(
|
|||||||
) {
|
) {
|
||||||
match control.state.as_mut() {
|
match control.state.as_mut() {
|
||||||
Some(KeyButtonData::Key { vk, pressed }) => {
|
Some(KeyButtonData::Key { vk, pressed }) => {
|
||||||
data.key_click(&app.session);
|
data.key_click(app);
|
||||||
|
|
||||||
if let PointerMode::Right = mode {
|
if let PointerMode::Right = mode {
|
||||||
data.modifiers |= SHIFT;
|
data.modifiers |= SHIFT;
|
||||||
@@ -155,11 +152,11 @@ fn key_press(
|
|||||||
Some(KeyButtonData::Modifier { modifier, sticky }) => {
|
Some(KeyButtonData::Modifier { modifier, sticky }) => {
|
||||||
*sticky = data.modifiers & *modifier == 0;
|
*sticky = data.modifiers & *modifier == 0;
|
||||||
data.modifiers |= *modifier;
|
data.modifiers |= *modifier;
|
||||||
data.key_click(&app.session);
|
data.key_click(app);
|
||||||
app.hid_provider.set_modifiers(data.modifiers);
|
app.hid_provider.set_modifiers(data.modifiers);
|
||||||
}
|
}
|
||||||
Some(KeyButtonData::Macro { verbs }) => {
|
Some(KeyButtonData::Macro { verbs }) => {
|
||||||
data.key_click(&app.session);
|
data.key_click(app);
|
||||||
for (vk, press) in verbs {
|
for (vk, press) in verbs {
|
||||||
app.hid_provider.send_key(*vk as _, *press);
|
app.hid_provider.send_key(*vk as _, *press);
|
||||||
}
|
}
|
||||||
@@ -169,7 +166,7 @@ fn key_press(
|
|||||||
data.processes
|
data.processes
|
||||||
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
|
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
|
||||||
|
|
||||||
data.key_click(&app.session);
|
data.key_click(app);
|
||||||
if let Ok(child) = Command::new(program).args(args).spawn() {
|
if let Ok(child) = Command::new(program).args(args).spawn() {
|
||||||
data.processes.push(child);
|
data.processes.push(child);
|
||||||
}
|
}
|
||||||
@@ -228,28 +225,16 @@ fn test_highlight(
|
|||||||
struct KeyboardData {
|
struct KeyboardData {
|
||||||
modifiers: KeyModifier,
|
modifiers: KeyModifier,
|
||||||
processes: Vec<Child>,
|
processes: Vec<Child>,
|
||||||
audio_stream: Option<OutputStream>,
|
|
||||||
audio_handle: Option<OutputStreamHandle>,
|
|
||||||
first_try: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyboardData {
|
impl KeyboardData {
|
||||||
fn key_click(&mut self, session: &AppSession) {
|
fn key_click(&mut self, app: &mut AppState) {
|
||||||
if !session.config.keyboard_sound_enabled {
|
if !app.session.config.keyboard_sound_enabled {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.audio_stream.is_none() && self.first_try {
|
if let Some(handle) = app.audio.get_handle() {
|
||||||
self.first_try = false;
|
// https://freesound.org/people/UberBosser/sounds/421581/
|
||||||
if let Ok((stream, handle)) = OutputStream::try_default() {
|
|
||||||
self.audio_stream = Some(stream);
|
|
||||||
self.audio_handle = Some(handle);
|
|
||||||
} else {
|
|
||||||
log::error!("Failed to open audio stream");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(handle) = &self.audio_handle {
|
|
||||||
let wav = include_bytes!("../res/421581.wav");
|
let wav = include_bytes!("../res/421581.wav");
|
||||||
let cursor = Cursor::new(wav);
|
let cursor = Cursor::new(wav);
|
||||||
let source = Decoder::new_wav(cursor).unwrap();
|
let source = Decoder::new_wav(cursor).unwrap();
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
io::Cursor,
|
||||||
ops::Add,
|
ops::Add,
|
||||||
sync::{atomic::AtomicUsize, Arc},
|
sync::{atomic::AtomicUsize, Arc},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use rodio::{Decoder, Source};
|
||||||
|
|
||||||
use glam::vec3a;
|
use glam::vec3a;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -24,8 +27,8 @@ pub struct Toast {
|
|||||||
pub title: Arc<str>,
|
pub title: Arc<str>,
|
||||||
pub body: Arc<str>,
|
pub body: Arc<str>,
|
||||||
pub opacity: f32,
|
pub opacity: f32,
|
||||||
pub volume: f32,
|
|
||||||
pub timeout: f32,
|
pub timeout: f32,
|
||||||
|
pub sound: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -36,7 +39,7 @@ impl Toast {
|
|||||||
body,
|
body,
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
timeout: 3.0,
|
timeout: 3.0,
|
||||||
volume: 0.0,
|
sound: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn with_timeout(mut self, timeout: f32) -> Self {
|
pub fn with_timeout(mut self, timeout: f32) -> Self {
|
||||||
@@ -47,8 +50,8 @@ impl Toast {
|
|||||||
self.opacity = opacity;
|
self.opacity = opacity;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn with_volume(mut self, volume: f32) -> Self {
|
pub fn with_sound(mut self, sound: bool) -> Self {
|
||||||
self.volume = volume;
|
self.sound = sound;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn submit(self, app: &mut AppState) {
|
pub fn submit(self, app: &mut AppState) {
|
||||||
@@ -59,6 +62,8 @@ impl Toast {
|
|||||||
let destroy_at =
|
let destroy_at =
|
||||||
std::time::Instant::now().add(std::time::Duration::from_secs_f32(self.timeout));
|
std::time::Instant::now().add(std::time::Duration::from_secs_f32(self.timeout));
|
||||||
|
|
||||||
|
let has_sound = self.sound;
|
||||||
|
|
||||||
app.tasks.enqueue(TaskType::CreateOverlay(
|
app.tasks.enqueue(TaskType::CreateOverlay(
|
||||||
selector.clone(),
|
selector.clone(),
|
||||||
Box::new(move |app| new_toast(self, name, app)),
|
Box::new(move |app| new_toast(self, name, app)),
|
||||||
@@ -66,6 +71,15 @@ impl Toast {
|
|||||||
|
|
||||||
app.tasks
|
app.tasks
|
||||||
.enqueue_at(TaskType::DropOverlay(selector), destroy_at);
|
.enqueue_at(TaskType::DropOverlay(selector), destroy_at);
|
||||||
|
|
||||||
|
if has_sound {
|
||||||
|
if let Some(handle) = app.audio.get_handle() {
|
||||||
|
let wav = include_bytes!("../res/557297.wav");
|
||||||
|
let cursor = Cursor::new(wav);
|
||||||
|
let source = Decoder::new_wav(cursor).unwrap();
|
||||||
|
let _ = handle.play_raw(source.convert_samples());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +105,11 @@ fn new_toast(
|
|||||||
.ok()?;
|
.ok()?;
|
||||||
(w0.max(w1), h1 + 50.)
|
(w0.max(w1), h1 + 50.)
|
||||||
} else {
|
} else {
|
||||||
app.fc
|
let (w, h) = app
|
||||||
|
.fc
|
||||||
.get_text_size(&title, FONT_SIZE, app.graphics.clone())
|
.get_text_size(&title, FONT_SIZE, app.graphics.clone())
|
||||||
.ok()?
|
.ok()?;
|
||||||
|
(w, h + 20.)
|
||||||
};
|
};
|
||||||
|
|
||||||
let og_width = size.0;
|
let og_width = size.0;
|
||||||
@@ -123,7 +139,7 @@ fn new_toast(
|
|||||||
canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., title);
|
canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., title);
|
||||||
} else {
|
} else {
|
||||||
log::info!("Toast: {}", title);
|
log::info!("Toast: {}", title);
|
||||||
canvas.label(0., 0., size.0, size.1, title);
|
canvas.label_centered(PADDING.0, 0., og_width, size.1, title);
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = OverlayState {
|
let state = OverlayState {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
f32::consts::PI,
|
f32::consts::PI,
|
||||||
io::Read,
|
io::{Cursor, Read},
|
||||||
process::{self, Stdio},
|
process::{self, Stdio},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Instant,
|
time::Instant,
|
||||||
@@ -9,6 +9,7 @@ use std::{
|
|||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use glam::{Quat, Vec3, Vec3A};
|
use glam::{Quat, Vec3, Vec3A};
|
||||||
|
use rodio::{Decoder, Source};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -395,6 +396,15 @@ fn btn_mirror_dn(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn audio_thump(app: &mut AppState) {
|
||||||
|
if let Some(handle) = app.audio.get_handle() {
|
||||||
|
let wav = include_bytes!("../res/380885.wav");
|
||||||
|
let cursor = Cursor::new(wav);
|
||||||
|
let source = Decoder::new_wav(cursor).unwrap();
|
||||||
|
let _ = handle.play_raw(source.convert_samples());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn btn_func_dn(
|
fn btn_func_dn(
|
||||||
control: &mut Control<(), ElemState>,
|
control: &mut Control<(), ElemState>,
|
||||||
_: &mut (),
|
_: &mut (),
|
||||||
@@ -434,6 +444,7 @@ fn btn_func_dn(
|
|||||||
.submit(app);
|
.submit(app);
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
audio_thump(app);
|
||||||
}
|
}
|
||||||
ButtonFunc::SwitchWatchHand => {
|
ButtonFunc::SwitchWatchHand => {
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
@@ -456,6 +467,7 @@ fn btn_func_dn(
|
|||||||
.submit(app);
|
.submit(app);
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
audio_thump(app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -736,6 +748,7 @@ fn overlay_button_up(control: &mut Control<(), ElemState>, _: &mut (), app: &mut
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
audio_thump(app);
|
||||||
}
|
}
|
||||||
PointerMode::Middle => {
|
PointerMode::Middle => {
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
@@ -751,6 +764,7 @@ fn overlay_button_up(control: &mut Control<(), ElemState>, _: &mut (), app: &mut
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
audio_thump(app);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/res/380885.wav
Normal file
BIN
src/res/380885.wav
Normal file
Binary file not shown.
BIN
src/res/557297.wav
Normal file
BIN
src/res/557297.wav
Normal file
Binary file not shown.
30
src/state.rs
30
src/state.rs
@@ -2,6 +2,7 @@ use std::{path::PathBuf, sync::Arc};
|
|||||||
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use glam::{Quat, Vec3};
|
use glam::{Quat, Vec3};
|
||||||
|
use rodio::{OutputStream, OutputStreamHandle};
|
||||||
use vulkano::format::Format;
|
use vulkano::format::Format;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -25,6 +26,7 @@ pub struct AppState {
|
|||||||
pub format: vulkano::format::Format,
|
pub format: vulkano::format::Format,
|
||||||
pub input_state: InputState,
|
pub input_state: InputState,
|
||||||
pub hid_provider: Box<dyn HidProvider>,
|
pub hid_provider: Box<dyn HidProvider>,
|
||||||
|
pub audio: AudioOutput,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@@ -59,6 +61,7 @@ impl AppState {
|
|||||||
format: Format::R8G8B8A8_UNORM,
|
format: Format::R8G8B8A8_UNORM,
|
||||||
input_state: InputState::new(),
|
input_state: InputState::new(),
|
||||||
hid_provider: crate::hid::initialize(),
|
hid_provider: crate::hid::initialize(),
|
||||||
|
audio: AudioOutput::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,3 +118,30 @@ impl AppSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AudioOutput {
|
||||||
|
audio_stream: Option<(OutputStream, OutputStreamHandle)>,
|
||||||
|
first_try: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioOutput {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
AudioOutput {
|
||||||
|
audio_stream: None,
|
||||||
|
first_try: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_handle(&mut self) -> Option<&OutputStreamHandle> {
|
||||||
|
if self.audio_stream.is_none() && self.first_try {
|
||||||
|
self.first_try = false;
|
||||||
|
if let Ok((stream, handle)) = OutputStream::try_default() {
|
||||||
|
self.audio_stream = Some((stream, handle));
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to open audio stream");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.audio_stream.as_ref().map(|(_, h)| h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user