Compare commits

..

11 Commits

Author SHA1 Message Date
arabianq 930857312d change version to 1.9.0 2026-05-15 22:18:21 +03:00
Denis e884993dba feat: add Kazakh and Hebrew translations for various UI elements (#106) 2026-05-15 22:05:48 +03:00
arabianq 05dd4319cc refactor: remove selected file handling from FileAction and related input logic 2026-05-15 22:05:30 +03:00
arabianq e320c85a6f ci: fix regular builds 2026-05-15 21:48:00 +03:00
arabianq f02bbc1e1c ci: remove --release flag from regular builds 2026-05-15 21:47:30 +03:00
arabianq 02f1116076 fix: incorrect string for dirs.open 2026-05-15 21:42:08 +03:00
Tarasov Aleksandr 8155cceac8 feat: recursively show directories in files list (#105) 2026-05-15 21:41:12 +03:00
arabianq d974a93c04 refactor: cargo fmt 2026-05-15 21:06:20 +03:00
Tarasov Aleksandr c6d9f2d6e7 feat/localization (#104)
* initial i18n setup for PWSP-GUI

* add Russian locale

* add missing entries

* add Spanish locale

* add French locale

* add Chinese locale

* add Arabic locale

* update cargo-sources.json
2026-05-15 20:29:39 +03:00
Tarasov Aleksandr dc1ecc81ea refactor: replace all rust Result with anyhow::Result (#103) 2026-05-15 19:32:26 +03:00
Tarasov Aleksandr 9b70bcd69d Update copyright year and owner in LICENSE file 2026-05-15 18:50:20 +03:00
27 changed files with 1256 additions and 357 deletions
+3 -3
View File
@@ -47,8 +47,8 @@ jobs:
echo "$BIN_NAMES" >> $GITHUB_OUTPUT echo "$BIN_NAMES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
- name: Build all release binaries - name: Build all binaries
run: cargo build --release --locked run: cargo build --locked
- name: Package all binaries into one archive - name: Package all binaries into one archive
shell: bash shell: bash
@@ -61,7 +61,7 @@ jobs:
FILES=() FILES=()
while IFS= read -r BIN; do while IFS= read -r BIN; do
[ -z "$BIN" ] && continue [ -z "$BIN" ] && continue
FILES+=("target/release/$BIN") FILES+=("target/debug/$BIN")
done <<< "${{ steps.cargo-meta.outputs.bin_names }}" done <<< "${{ steps.cargo-meta.outputs.bin_names }}"
if [ "${#FILES[@]}" -eq 0 ]; then if [ "${#FILES[@]}" -eq 0 ]; then
Generated
+226 -13
View File
@@ -140,6 +140,15 @@ dependencies = [
"x11rb", "x11rb",
] ]
[[package]]
name = "arc-swap"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@@ -309,6 +318,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base62"
version = "2.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b"
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.72.1" version = "0.72.1"
@@ -1514,6 +1529,30 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "globset"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "globwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
"bitflags 1.3.2",
"ignore",
"walkdir",
]
[[package]] [[package]]
name = "glow" name = "glow"
version = "0.17.0" version = "0.17.0"
@@ -1773,6 +1812,22 @@ dependencies = [
"icu_properties", "icu_properties",
] ]
[[package]]
name = "ignore"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.10" version = "0.25.10"
@@ -1832,6 +1887,15 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.13.0"
@@ -3053,7 +3117,7 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [ dependencies = [
"toml_edit", "toml_edit 0.25.11+spec-1.1.0",
] ]
[[package]] [[package]]
@@ -3095,8 +3159,9 @@ checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
[[package]] [[package]]
name = "pwsp" name = "pwsp"
version = "1.8.1" version = "1.9.0"
dependencies = [ dependencies = [
"anyhow",
"async-trait", "async-trait",
"clap", "clap",
"dirs", "dirs",
@@ -3111,8 +3176,10 @@ dependencies = [
"pipewire", "pipewire",
"rfd", "rfd",
"rodio", "rodio",
"rust-i18n",
"serde", "serde",
"serde_json", "serde_json",
"sys-locale",
"system-fonts", "system-fonts",
"tokio", "tokio",
] ]
@@ -3338,6 +3405,57 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rust-i18n"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21031bf5e6f2c0ae745d831791c403608e99a8bd3776c7e5e5535acd70c3b7ba"
dependencies = [
"globwalk",
"regex",
"rust-i18n-macro",
"rust-i18n-support",
"smallvec",
]
[[package]]
name = "rust-i18n-macro"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51fe5295763b358606f7ca26a564e20f4469775a57ec1f09431249a33849ff52"
dependencies = [
"glob",
"proc-macro2",
"quote",
"rust-i18n-support",
"serde",
"serde_json",
"serde_yaml",
"syn",
]
[[package]]
name = "rust-i18n-support"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69bcc115c8eea2803aa3d85362e339776f4988a0349f2f475af572e497443f6f"
dependencies = [
"arc-swap",
"base62",
"globwalk",
"itertools 0.11.0",
"lazy_static",
"normpath",
"proc-macro2",
"regex",
"serde",
"serde_json",
"serde_yaml",
"siphasher",
"toml 0.8.23",
"triomphe",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"
@@ -3391,6 +3509,12 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@@ -3478,6 +3602,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.1.1" version = "1.1.1"
@@ -3487,6 +3620,19 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -3914,7 +4060,7 @@ dependencies = [
"cfg-expr", "cfg-expr",
"heck", "heck",
"pkg-config", "pkg-config",
"toml", "toml 1.1.2+spec-1.1.0",
"version-compare", "version-compare",
] ]
@@ -4060,6 +4206,18 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_edit 0.22.27",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "1.1.2+spec-1.1.0" version = "1.1.2+spec-1.1.0"
@@ -4068,11 +4226,20 @@ checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde_core", "serde_core",
"serde_spanned", "serde_spanned 1.1.1",
"toml_datetime", "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser", "toml_parser",
"toml_writer", "toml_writer",
"winnow", "winnow 1.0.2",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
] ]
[[package]] [[package]]
@@ -4084,6 +4251,20 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.15",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.25.11+spec-1.1.0" version = "0.25.11+spec-1.1.0"
@@ -4091,9 +4272,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser", "toml_parser",
"winnow", "winnow 1.0.2",
] ]
[[package]] [[package]]
@@ -4102,9 +4283,15 @@ version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [ dependencies = [
"winnow", "winnow 1.0.2",
] ]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "toml_writer" name = "toml_writer"
version = "1.1.1+spec-1.1.0" version = "1.1.1+spec-1.1.0"
@@ -4143,6 +4330,17 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "triomphe"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39"
dependencies = [
"arc-swap",
"serde",
"stable_deref_trait",
]
[[package]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.25.1" version = "0.25.1"
@@ -4202,6 +4400,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@@ -5084,6 +5288,15 @@ dependencies = [
"xkbcommon-dl", "xkbcommon-dl",
] ]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.2" version = "1.0.2"
@@ -5317,7 +5530,7 @@ dependencies = [
"uds_windows", "uds_windows",
"uuid", "uuid",
"windows-sys 0.61.2", "windows-sys 0.61.2",
"winnow", "winnow 1.0.2",
"zbus_macros", "zbus_macros",
"zbus_names", "zbus_names",
"zvariant", "zvariant",
@@ -5345,7 +5558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
dependencies = [ dependencies = [
"serde", "serde",
"winnow", "winnow 1.0.2",
"zvariant", "zvariant",
] ]
@@ -5482,7 +5695,7 @@ dependencies = [
"enumflags2", "enumflags2",
"serde", "serde",
"url", "url",
"winnow", "winnow 1.0.2",
"zvariant_derive", "zvariant_derive",
"zvariant_utils", "zvariant_utils",
] ]
@@ -5510,5 +5723,5 @@ dependencies = [
"quote", "quote",
"serde", "serde",
"syn", "syn",
"winnow", "winnow 1.0.2",
] ]
+14 -7
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pwsp" name = "pwsp"
version = "1.8.1" version = "1.9.0"
edition = "2024" edition = "2024"
authors = ["arabian"] authors = ["arabian"]
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients." description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
@@ -26,8 +26,21 @@ clap = { version = "4.6.1", default-features = false, features = [
"error-context", "error-context",
"derive", "derive",
] } ] }
dirs = "6.0.0" dirs = "6.0.0"
itertools = "0.14.0" itertools = "0.14.0"
evdev = { version = "0.13.2", features = ["tokio"] }
rfd = { version = "0.17.2", default-features = false, features = [
"xdg-portal",
] }
opener = { version = "0.8.4", features = ["reveal"] }
system-fonts = "0.1.0"
anyhow = "1.0.102"
rust-i18n = "4.0.0"
sys-locale = "0.3.2"
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "1a08f281c352622bd82b87b8731585245802d9cf", default-features = false, features = [ rodio = { git = "https://github.com/arabianq/rodio.git", rev = "1a08f281c352622bd82b87b8731585245802d9cf", default-features = false, features = [
"symphonia-all", "symphonia-all",
@@ -35,12 +48,6 @@ rodio = { git = "https://github.com/arabianq/rodio.git", rev = "1a08f281c352622b
"playback", "playback",
] } ] }
pipewire = "0.9.2" pipewire = "0.9.2"
evdev = { version = "0.13.2", features = ["tokio"] }
rfd = { version = "0.17.2", default-features = false, features = [
"xdg-portal",
] }
opener = { version = "0.8.4", features = ["reveal"] }
system-fonts = "0.1.0"
egui = { version = "0.34.2", default-features = false, features = [ egui = { version = "0.34.2", default-features = false, features = [
"default_fonts", "default_fonts",
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 Tarasov Alexander Copyright (c) 2026 arabianq
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+3 -5
View File
@@ -99,15 +99,13 @@ pwsp-cli --help # View all commands
| Action | Keyboard | Mouse | | Action | Keyboard | Mouse |
| :----------------------------------- | :--------------------- | :------------------- | | :----------------------------------- | :--------------------- | :------------------- |
| **Play Track** (Stops others) | `Enter` | `Left Click` | | **Play Track** (Stops others) | | `Left Click` |
| **Add Track** (Plays simultaneously) | `Ctrl + Enter` | `Ctrl + Left Click` | | **Add Track** (Plays simultaneously) | | `Ctrl + Left Click` |
| **Replace Last Track** | `Shift + Enter` | `Shift + Left Click` | | **Replace Last Track** | | `Shift + Left Click` |
| **Pause / Resume** | `Space` | | | **Pause / Resume** | `Space` | |
| **Stop All Tracks** | `Backspace` | | | **Stop All Tracks** | `Backspace` | |
| **Open / Close Settings** | `I` | | | **Open / Close Settings** | `I` | |
| **Search** | `/` | | | **Search** | `/` | |
| **Navigate Files** | `Ctrl + ↑ / ↓` | |
| **Navigate Directories** | `Ctrl + Shift + ↑ / ↓` | |
--- ---
+343
View File
@@ -0,0 +1,343 @@
_version = 2
# ----------------
# Main page
# ----------------
[gui.play_file_button]
en = "Play file"
ru = "Выбрать файл"
es = "Reproducir archivo"
fr = "Lire le fichier"
zh = "播放文件"
ar = "تشغيل الملف"
kz = "Файлды ойнату"
he = "נגן קובץ"
[gui.choose_mic_select]
en = "Select microphone"
ru = "Выбрать микрофон"
es = "Seleccionar micrófono"
fr = "Sélectionner le microphone"
zh = "选择麦克风"
ar = "اختر الميكروفون"
kz = "Микрофонды таңдау"
he = "בחר מיקרופון"
[gui.search_placeholder]
en = "Search files..."
ru = "Поиск файлов..."
es = "Buscar archivos..."
fr = "Rechercher des fichiers..."
zh = "搜索文件..."
ar = "البحث عن ملفات..."
kz = "Файлдарды іздеу..."
he = "חפש קבצים..."
[gui.context.dirs.open]
en = "Open"
ru = "Открыть"
es = "Abrir"
fr = "Ouvrir"
zh = "打开"
ar = "فتح"
kz = "Ашу"
he = "פתח"
[gui.context.dirs.open_in_fm]
en = "Open in File Manager"
ru = "Открыть в менеджере файлов"
es = "Abrir en el gestor de archivos"
fr = "Ouvrir dans le gestionnaire de fichiers"
zh = "在文件管理器中打开"
ar = "فتح في مدير الملفات"
kz = "Файл менеджерінде ашу"
he = "פתח במנהל הקבצים"
[gui.context.dirs.remove]
en = "Remove"
ru = "Удалить"
es = "Eliminar"
fr = "Supprimer"
zh = "移除"
ar = "إزالة"
kz = "Жою"
he = "הסר"
[gui.context.files.play_solo]
en = "Play Solo"
ru = "Играть"
es = "Reproducir solo"
fr = "Jouer en solo"
zh = "单独播放"
ar = "تشغيل منفرد"
kz = "Жалғыз ойнату"
he = "נגן סולו"
[gui.context.files.add_new]
en = "Add New"
ru = "Добавить"
es = "Añadir nuevo"
fr = "Ajouter un nouveau"
zh = "添加新项"
ar = "إضافة جديد"
kz = "Жаңасын қосу"
he = "הוסף חדש"
[gui.context.files.replace_last]
en = "Replace Last"
ru = "Заменить Последний"
es = "Reemplazar último"
fr = "Remplacer le dernier"
zh = "替换上一个"
ar = "استبدال الأخير"
kz = "Соңғысын ауыстыру"
he = "החלף אחרון"
[gui.context.files.show_in_fm]
en = "Show in File Manager"
ru = "Открыть в менеджере файлов"
es = "Mostrar en el gestor de archivos"
fr = "Afficher dans le gestionnaire de fichiers"
zh = "在文件管理器中显示"
ar = "عرض في مدير الملفات"
kz = "Файл менеджерінде көрсету"
he = "הצג במנהל הקבצים"
[gui.context.files.asign_hotkey]
en = "Asign Hotkey"
ru = "Назначить Горячую Клавишу"
es = "Asignar atajo"
fr = "Assigner un raccourci"
zh = "分配快捷键"
ar = "تعيين مفتاح اختصار"
kz = "Ыстық пернені тағайындау"
he = "הקצה מקש קיצור"
# ----------------
# Settings
# ----------------
[gui.settings.header]
en = "Settings"
ru = "Настройки"
es = "Ajustes"
fr = "Paramètres"
zh = "设置"
ar = "الإعدادات"
kz = "Баптаулар"
he = "הגדרות"
[gui.settings.remember_volume]
en = "Always remember volume"
ru = "Всегда запоминать громкость"
es = "Recordar siempre el volumen"
fr = "Toujours se souvenir du volume"
zh = "始终记住音量"
ar = "تذكر مستوى الصوت دائمًا"
kz = "Әрқашан дыбыс деңгейін есте сақтау"
he = "זכור תמיד עוצמת קול"
[gui.settings.remember_mic]
en = "Always remember microphone"
ru = "Всегда запоминать микрофон"
es = "Recordar siempre el micrófono"
fr = "Toujours se souvenir du microphone"
zh = "始终记住麦克风"
ar = "تذكر الميكروفون دائمًا"
kz = "Әрқашан микрофонды есте сақтау"
he = "זכור תמיד מיקרופון"
[gui.settings.remember_ui_scale]
en = "Always remember UI scale factor"
ru = "Всегда запоминать масштаб интерфейса"
es = "Recordar siempre la escala de la interfaz"
fr = "Toujours se souvenir de l'échelle de l'interface"
zh = "始终记住界面缩放比例"
ar = "تذكر عامل تكبير الواجهة دائمًا"
kz = "Әрқашан интерфейс масштабын есте сақтау"
he = "זכור תמיד קנה מידה של ממשק משתמש"
[gui.settings.pause_on_window_close]
en = "Pause audio playback when the window is closed"
ru = "Останавливать воспроизведение при закрытии окна"
es = "Pausar la reproducción de audio al cerrar la ventana"
fr = "Mettre en pause la lecture audio à la fermeture de la fenêtre"
zh = "关闭窗口时暂停音频播放"
ar = "إيقاف الصوت مؤقتًا عند إغلاق النافذة"
kz = "Терезе жабылған кезде дыбысты ойнатуды кідірту"
he = "השהה השמעת שמע כאשר החלון נסגר"
[gui.settings.version]
en = "GUI version: %{version}"
ru = "Версия GUI: %{version}"
es = "Versión de la GUI: %{version}"
fr = "Version de l'interface : %{version}"
zh = "GUI 版本: %{version}"
ar = "إصدار الواجهة: %{version}"
kz = "GUI нұсқасы: %{version}"
he = "גרסת ממשק משתמש: %{version}"
# ----------------
# Hotkeys
# ----------------
[gui.hotkeys.header]
en = "Hotkeys"
ru = "Горячие клавиши"
es = "Atajos de teclado"
fr = "Raccourcis clavier"
zh = "快捷键"
ar = "اختصارات لوحة المفاتيح"
kz = "Ыстық пернелер"
he = "מקשי קיצור"
[gui.hotkeys.search_placeholder]
en = "Search hotkeys..."
ru = "Поиск горячих клавиш..."
es = "Buscar atajos..."
fr = "Rechercher des raccourcis..."
zh = "搜索快捷键..."
ar = "البحث عن الاختصارات..."
kz = "Ыстық пернелерді іздеу..."
he = "חפש מקשי קיצור..."
[gui.hotkeys.add_command_select]
en = "Add Command"
ru = "Добавить команду"
es = "Añadir comando"
fr = "Ajouter une commande"
zh = "添加命令"
ar = "إضافة أمر"
kz = "Команда қосу"
he = "הוסף פקודה"
[gui.hotkeys.toggle_pause_command]
en = "Toggle Pause"
ru = "Переключить паузу"
es = "Alternar pausa"
fr = "Basculer la pause"
zh = "切换暂停"
ar = "تبديل الإيقاف المؤقت"
kz = "Кідіртуді ауыстыру"
he = "הפעל/השהה"
[gui.hotkeys.stop_playback_command]
en = "Stop Playback"
ru = "Остановить воспроизведение"
es = "Detener reproducción"
fr = "Arrêter la lecture"
zh = "停止播放"
ar = "إيقاف التشغيل"
kz = "Ойнатуды тоқтату"
he = "עצור השמעה"
[gui.hotkeys.pause_playback_command]
en = "Pause Playback"
ru = "Поставить воспроизведение на паузу"
es = "Pausar reproducción"
fr = "Mettre en pause la lecture"
zh = "暂停播放"
ar = "إيقاف التشغيل مؤقتاً"
kz = "Ойнатуды кідірту"
he = "השהה השמעה"
[gui.hotkeys.resume_playback_command]
en = "Resume Playback"
ru = "Продолжить воспроизведение"
es = "Reanudar reproducción"
fr = "Reprendre la lecture"
zh = "恢复播放"
ar = "استئناف التشغيل"
kz = "Ойнатуды жалғастыру"
he = "המשך השמעה"
[gui.hotkeys.toggle_loop_command]
en = "Toggle Loop"
ru = "Переключить зацикливание"
es = "Alternar bucle"
fr = "Basculer la boucle"
zh = "切换循环"
ar = "تبديل التكرار"
kz = "Қайталауды ауыстыру"
he = "הפעל/כבה לולאה"
[gui.hotkeys.column_slot]
en = "Slot"
ru = "Слот"
es = "Ranura"
fr = "Emplacement"
zh = "插槽"
ar = "الخانة"
kz = "Ұяшық"
he = "משבצת"
[gui.hotkeys.column_sound]
en = "Sound"
ru = "Звук"
es = "Sonido"
fr = "Son"
zh = "声音"
ar = "الصوت"
kz = "Дыбыс"
he = "צליל"
[gui.hotkeys.column_key_chord]
en = "Key Chord"
ru = "Клавиша"
es = "Combinación de teclas"
fr = "Combinaison de touches"
zh = "组合键"
ar = "تركيبة المفاتيح"
kz = "Пернелер тіркесімі"
he = "צירוף מקשים"
[gui.hotkeys.column_actions]
en = "Actions"
ru = "Действия"
es = "Acciones"
fr = "Actions"
zh = "操作"
ar = "الإجراءات"
kz = "Әрекеттер"
he = "פעולות"
[gui.hotkeys.no_hotkeys_configured]
en = "No hotkeys configured"
ru = "Горячие клавиши не настроены"
es = "No hay atajos configurados"
fr = "Aucun raccourci configuré"
zh = "未配置快捷键"
ar = "لا توجد اختصارات معينة"
kz = "Ыстық пернелер бапталмаған"
he = "לא הוגדרו מקשי קיצור"
[gui.hotkeys.capture.header]
en = "Press a key combination (e.g. Ctrl+Alt+1)"
ru = "Нажмите сочетание клавиш (например, Ctrl+Alt+1)"
es = "Presione una combinación de teclas (ej. Ctrl+Alt+1)"
fr = "Appuyez sur une combinaison de touches (ex. Ctrl+Alt+1)"
zh = "按下一个组合键 (例如 Ctrl+Alt+1)"
ar = "اضغط على تركيبة مفاتيح (مثلاً Ctrl+Alt+1)"
kz = "Пернелер тіркесімін басыңыз (мысалы, Ctrl+Alt+1)"
he = "לחץ על צירוף מקשים (למשל Ctrl+Alt+1)"
[gui.hotkeys.capture.for]
en = "for"
ru = "для"
es = "para"
fr = "pour"
zh = "用于"
ar = "لـ"
kz = "үшін"
he = "עבור"
[gui.hotkeys.capture.cancel]
en = "Press Escape to canel"
ru = "Нажмите Escape для отмены"
es = "Presione Escape para cancelar"
fr = "Appuyez sur Échap pour annuler"
zh = "按 Escape 取消"
ar = "اضغط Esc للإلغاء"
kz = "Болдырмау үшін Escape пернесін басыңыз"
he = "לחץ על Escape לביטול"
+3 -3
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp-bin pkgbase = pwsp-bin
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries) pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
pkgver = 1.8.1 pkgver = 1.9.0
pkgrel = 1 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64 arch = x86_64
@@ -9,8 +9,8 @@ depends = pipewire
depends = alsa-lib depends = alsa-lib
provides = pwsp provides = pwsp
conflicts = pwsp conflicts = pwsp
source = pwsp-bin-1.8.1.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.8.1/pwsp-v1.8.1-linux-x64.zip source = pwsp-bin-1.9.0.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.9.0/pwsp-v1.9.0-linux-x64.zip
source = pipewire-soundpad-1.8.1.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.8.1.tar.gz source = pipewire-soundpad-1.9.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.9.0.tar.gz
sha256sums = SKIP sha256sums = SKIP
sha256sums = SKIP sha256sums = SKIP
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgname=pwsp-bin pkgname=pwsp-bin
_pkgname=pipewire-soundpad _pkgname=pipewire-soundpad
pkgver=1.8.1 pkgver=1.9.0
pkgrel=1 pkgrel=1
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)" pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
arch=('x86_64') arch=('x86_64')
+2 -2
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp pkgbase = pwsp
pkgdesc = Lets you play audio files through your microphone pkgdesc = Lets you play audio files through your microphone
pkgver = 1.8.1 pkgver = 1.9.0
pkgrel = 1 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = any arch = any
@@ -11,7 +11,7 @@ pkgbase = pwsp
makedepends = cmake makedepends = cmake
makedepends = pipewire makedepends = pipewire
makedepends = alsa-lib makedepends = alsa-lib
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.8.1.tar.gz source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.9.0.tar.gz
sha256sums = SKIP sha256sums = SKIP
pkgname = pwsp pkgname = pwsp
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgsubn=pwsp pkgsubn=pwsp
pkgname=pwsp pkgname=pwsp
pkgver=1.8.1 pkgver=1.9.0
pkgrel=1 pkgrel=1
pkgdesc="Lets you play audio files through your microphone" pkgdesc="Lets you play audio files through your microphone"
arch=('any') arch=('any')
+247
View File
@@ -193,6 +193,19 @@
"dest": "cargo/vendor/arboard-3.6.1", "dest": "cargo/vendor/arboard-3.6.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/arc-swap/arc-swap-1.9.1.crate",
"sha256": "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207",
"dest": "cargo/vendor/arc-swap-1.9.1"
},
{
"type": "inline",
"contents": "{\"package\": \"6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207\", \"files\": {}}",
"dest": "cargo/vendor/arc-swap-1.9.1",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -388,6 +401,19 @@
"dest": "cargo/vendor/autocfg-1.5.0", "dest": "cargo/vendor/autocfg-1.5.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/base62/base62-2.2.4.crate",
"sha256": "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b",
"dest": "cargo/vendor/base62-2.2.4"
},
{
"type": "inline",
"contents": "{\"package\": \"cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b\", \"files\": {}}",
"dest": "cargo/vendor/base62-2.2.4",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -1966,6 +1992,32 @@
"dest": "cargo/vendor/glob-0.3.3", "dest": "cargo/vendor/glob-0.3.3",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/globset/globset-0.4.18.crate",
"sha256": "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3",
"dest": "cargo/vendor/globset-0.4.18"
},
{
"type": "inline",
"contents": "{\"package\": \"52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3\", \"files\": {}}",
"dest": "cargo/vendor/globset-0.4.18",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/globwalk/globwalk-0.8.1.crate",
"sha256": "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc",
"dest": "cargo/vendor/globwalk-0.8.1"
},
{
"type": "inline",
"contents": "{\"package\": \"93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc\", \"files\": {}}",
"dest": "cargo/vendor/globwalk-0.8.1",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -2291,6 +2343,19 @@
"dest": "cargo/vendor/idna_adapter-1.2.2", "dest": "cargo/vendor/idna_adapter-1.2.2",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/ignore/ignore-0.4.25.crate",
"sha256": "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a",
"dest": "cargo/vendor/ignore-0.4.25"
},
{
"type": "inline",
"contents": "{\"package\": \"d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a\", \"files\": {}}",
"dest": "cargo/vendor/ignore-0.4.25",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -2356,6 +2421,19 @@
"dest": "cargo/vendor/indexmap-2.14.0", "dest": "cargo/vendor/indexmap-2.14.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/itertools/itertools-0.11.0.crate",
"sha256": "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57",
"dest": "cargo/vendor/itertools-0.11.0"
},
{
"type": "inline",
"contents": "{\"package\": \"b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57\", \"files\": {}}",
"dest": "cargo/vendor/itertools-0.11.0",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -4298,6 +4376,45 @@
"dest": "cargo/vendor/roxmltree-0.20.0", "dest": "cargo/vendor/roxmltree-0.20.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/rust-i18n/rust-i18n-4.0.0.crate",
"sha256": "21031bf5e6f2c0ae745d831791c403608e99a8bd3776c7e5e5535acd70c3b7ba",
"dest": "cargo/vendor/rust-i18n-4.0.0"
},
{
"type": "inline",
"contents": "{\"package\": \"21031bf5e6f2c0ae745d831791c403608e99a8bd3776c7e5e5535acd70c3b7ba\", \"files\": {}}",
"dest": "cargo/vendor/rust-i18n-4.0.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/rust-i18n-macro/rust-i18n-macro-4.0.0.crate",
"sha256": "51fe5295763b358606f7ca26a564e20f4469775a57ec1f09431249a33849ff52",
"dest": "cargo/vendor/rust-i18n-macro-4.0.0"
},
{
"type": "inline",
"contents": "{\"package\": \"51fe5295763b358606f7ca26a564e20f4469775a57ec1f09431249a33849ff52\", \"files\": {}}",
"dest": "cargo/vendor/rust-i18n-macro-4.0.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/rust-i18n-support/rust-i18n-support-4.0.0.crate",
"sha256": "69bcc115c8eea2803aa3d85362e339776f4988a0349f2f475af572e497443f6f",
"dest": "cargo/vendor/rust-i18n-support-4.0.0"
},
{
"type": "inline",
"contents": "{\"package\": \"69bcc115c8eea2803aa3d85362e339776f4988a0349f2f475af572e497443f6f\", \"files\": {}}",
"dest": "cargo/vendor/rust-i18n-support-4.0.0",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -4376,6 +4493,19 @@
"dest": "cargo/vendor/rustversion-1.0.22", "dest": "cargo/vendor/rustversion-1.0.22",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/ryu/ryu-1.0.23.crate",
"sha256": "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f",
"dest": "cargo/vendor/ryu-1.0.23"
},
{
"type": "inline",
"contents": "{\"package\": \"9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f\", \"files\": {}}",
"dest": "cargo/vendor/ryu-1.0.23",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -4506,6 +4636,19 @@
"dest": "cargo/vendor/serde_repr-0.1.20", "dest": "cargo/vendor/serde_repr-0.1.20",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/serde_spanned/serde_spanned-0.6.9.crate",
"sha256": "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3",
"dest": "cargo/vendor/serde_spanned-0.6.9"
},
{
"type": "inline",
"contents": "{\"package\": \"bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3\", \"files\": {}}",
"dest": "cargo/vendor/serde_spanned-0.6.9",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -4519,6 +4662,19 @@
"dest": "cargo/vendor/serde_spanned-1.1.1", "dest": "cargo/vendor/serde_spanned-1.1.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/serde_yaml/serde_yaml-0.9.34+deprecated.crate",
"sha256": "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47",
"dest": "cargo/vendor/serde_yaml-0.9.34+deprecated"
},
{
"type": "inline",
"contents": "{\"package\": \"6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47\", \"files\": {}}",
"dest": "cargo/vendor/serde_yaml-0.9.34+deprecated",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -5221,6 +5377,19 @@
"dest": "cargo/vendor/tokio-macros-2.7.0", "dest": "cargo/vendor/tokio-macros-2.7.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/toml/toml-0.8.23.crate",
"sha256": "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362",
"dest": "cargo/vendor/toml-0.8.23"
},
{
"type": "inline",
"contents": "{\"package\": \"dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362\", \"files\": {}}",
"dest": "cargo/vendor/toml-0.8.23",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -5234,6 +5403,19 @@
"dest": "cargo/vendor/toml-1.1.2+spec-1.1.0", "dest": "cargo/vendor/toml-1.1.2+spec-1.1.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/toml_datetime/toml_datetime-0.6.11.crate",
"sha256": "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c",
"dest": "cargo/vendor/toml_datetime-0.6.11"
},
{
"type": "inline",
"contents": "{\"package\": \"22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c\", \"files\": {}}",
"dest": "cargo/vendor/toml_datetime-0.6.11",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -5247,6 +5429,19 @@
"dest": "cargo/vendor/toml_datetime-1.1.1+spec-1.1.0", "dest": "cargo/vendor/toml_datetime-1.1.1+spec-1.1.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/toml_edit/toml_edit-0.22.27.crate",
"sha256": "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a",
"dest": "cargo/vendor/toml_edit-0.22.27"
},
{
"type": "inline",
"contents": "{\"package\": \"41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a\", \"files\": {}}",
"dest": "cargo/vendor/toml_edit-0.22.27",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -5273,6 +5468,19 @@
"dest": "cargo/vendor/toml_parser-1.1.2+spec-1.1.0", "dest": "cargo/vendor/toml_parser-1.1.2+spec-1.1.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/toml_write/toml_write-0.1.2.crate",
"sha256": "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801",
"dest": "cargo/vendor/toml_write-0.1.2"
},
{
"type": "inline",
"contents": "{\"package\": \"5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801\", \"files\": {}}",
"dest": "cargo/vendor/toml_write-0.1.2",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -5325,6 +5533,19 @@
"dest": "cargo/vendor/tracing-core-0.1.36", "dest": "cargo/vendor/tracing-core-0.1.36",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/triomphe/triomphe-0.1.15.crate",
"sha256": "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39",
"dest": "cargo/vendor/triomphe-0.1.15"
},
{
"type": "inline",
"contents": "{\"package\": \"dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39\", \"files\": {}}",
"dest": "cargo/vendor/triomphe-0.1.15",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -5429,6 +5650,19 @@
"dest": "cargo/vendor/unicode-xid-0.2.6", "dest": "cargo/vendor/unicode-xid-0.2.6",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/unsafe-libyaml/unsafe-libyaml-0.2.11.crate",
"sha256": "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861",
"dest": "cargo/vendor/unsafe-libyaml-0.2.11"
},
{
"type": "inline",
"contents": "{\"package\": \"673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861\", \"files\": {}}",
"dest": "cargo/vendor/unsafe-libyaml-0.2.11",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -6521,6 +6755,19 @@
"dest": "cargo/vendor/winit-0.30.13", "dest": "cargo/vendor/winit-0.30.13",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/winnow/winnow-0.7.15.crate",
"sha256": "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945",
"dest": "cargo/vendor/winnow-0.7.15"
},
{
"type": "inline",
"contents": "{\"package\": \"df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945\", \"files\": {}}",
"dest": "cargo/vendor/winnow-0.7.15",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -25,7 +25,7 @@
<name>arabian</name> <name>arabian</name>
</developer> </developer>
<releases> <releases>
<release version="1.8.1" date="2026-05-15" /> <release version="1.9.0" date="2026-05-15" />
</releases> </releases>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
</component> </component>
+1 -1
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0 %global cargo_install_lib 0
Name: pwsp Name: pwsp
Version: 1.8.1 Version: 1.9.0
Release: %autorelease Release: %autorelease
Summary: Lets you play audio files through your microphone Summary: Lets you play audio files through your microphone
+4 -5
View File
@@ -1,9 +1,10 @@
use anyhow::{Result, anyhow};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use pwsp::{ use pwsp::{
types::socket::Request, types::socket::Request,
utils::daemon::{make_request, wait_for_daemon}, utils::daemon::{make_request, wait_for_daemon},
}; };
use std::{error::Error, path::PathBuf}; use std::path::PathBuf;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@@ -146,7 +147,7 @@ enum SetCommands {
} }
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
wait_for_daemon().await?; wait_for_daemon().await?;
@@ -204,9 +205,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
}, },
}; };
let response = make_request(request) let response = make_request(request).await.map_err(|e| anyhow!(e))?;
.await
.map_err(|e| e as Box<dyn Error>)?;
println!("{} : {}", response.status, response.message); println!("{} : {}", response.status, response.message);
Ok(()) Ok(())
+5 -4
View File
@@ -1,3 +1,4 @@
use anyhow::{Result, anyhow};
use pwsp::{ use pwsp::{
types::socket::{MAX_MESSAGE_SIZE, Request, Response}, types::socket::{MAX_MESSAGE_SIZE, Request, Response},
utils::{ utils::{
@@ -11,7 +12,7 @@ use pwsp::{
}, },
}; };
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::{error::Error, fs, time::Duration}; use std::{fs, time::Duration};
use tokio::{ use tokio::{
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
net::UnixListener, net::UnixListener,
@@ -19,11 +20,11 @@ use tokio::{
}; };
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<()> {
create_runtime_dir()?; create_runtime_dir()?;
if is_daemon_running()? { if is_daemon_running()? {
return Err("Another instance is already running.".into()); return Err(anyhow!("Another instance is already running."));
} }
get_daemon_config(); // Initialize daemon config get_daemon_config(); // Initialize daemon config
@@ -76,7 +77,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
async fn commands_loop(listener: UnixListener) -> Result<(), Box<dyn Error>> { async fn commands_loop(listener: UnixListener) -> Result<()> {
loop { loop {
let (mut stream, _addr) = listener.accept().await?; let (mut stream, _addr) = listener.accept().await?;
+231 -89
View File
@@ -6,10 +6,16 @@ use egui::{
use egui_dnd::dnd; use egui_dnd::dnd;
use egui_extras::{Column, TableBuilder}; use egui_extras::{Column, TableBuilder};
use egui_material_icons::icons::*; use egui_material_icons::icons::*;
use pwsp::types::gui::AudioPlayerState;
use pwsp::types::socket::Request; use pwsp::types::socket::Request;
use pwsp::types::{audio_player::TrackInfo, gui::AppState}; use pwsp::types::{audio_player::TrackInfo, gui::AppState};
use pwsp::utils::gui::{format_time_pair, make_request_async}; use pwsp::utils::gui::{format_time_pair, make_request_async};
use std::{path::Path, time::Instant}; use rust_i18n::t;
use std::{
cmp::Ordering,
path::{Path, PathBuf},
time::Instant,
};
enum TrackAction { enum TrackAction {
Pause(u32), Pause(u32),
@@ -25,6 +31,12 @@ enum HotkeyAction {
Play(String), Play(String),
} }
enum FileAction {
Play(PathBuf, bool),
StopAndPlay(u32, PathBuf, bool),
AssignHotkey(PathBuf),
}
impl SoundpadGui { impl SoundpadGui {
fn get_volume_icon(volume: f32) -> &'static str { fn get_volume_icon(volume: f32) -> &'static str {
if volume > 0.7 { if volume > 0.7 {
@@ -59,17 +71,18 @@ impl SoundpadGui {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add_space(ui.available_height() / 3.0); ui.add_space(ui.available_height() / 3.0);
ui.label( ui.label(
RichText::new("Press a key combination (e.g. Ctrl+Alt+1)") RichText::new(t!("gui.hotkeys.capture.header"))
.size(18.0) .size(18.0)
.color(Color32::YELLOW) .color(Color32::YELLOW)
.monospace(), .monospace(),
); );
ui.add_space(10.0); ui.add_space(10.0);
let target = if let Some(slot) = &self.app_state.assigning_hotkey_slot { let target = if let Some(slot) = &self.app_state.assigning_hotkey_slot {
format!("for slot '{}'", slot) format!("{} '{}'", t!("gui.hotkeys.capture.for"), slot)
} else if let Some(path) = &self.app_state.assigning_hotkey_for_file { } else if let Some(path) = &self.app_state.assigning_hotkey_for_file {
format!( format!(
"for '{}'", "{} '{}'",
t!("gui.hotkeys.capture.for"),
path.file_name().unwrap_or_default().to_string_lossy() path.file_name().unwrap_or_default().to_string_lossy()
) )
} else { } else {
@@ -77,7 +90,7 @@ impl SoundpadGui {
}; };
ui.label(RichText::new(target).size(16.0)); ui.label(RichText::new(target).size(16.0));
ui.add_space(10.0); ui.add_space(10.0);
ui.label("Press Escape to cancel"); ui.label(t!("gui.hotkeys.capture.cancel"));
}); });
} }
@@ -94,7 +107,11 @@ impl SoundpadGui {
ui.add_space(ui.available_width() / 2.0 - 40.0); ui.add_space(ui.available_width() / 2.0 - 40.0);
ui.label(RichText::new("Settings").color(Color32::WHITE).monospace()); ui.label(
RichText::new(t!("gui.settings.header"))
.color(Color32::WHITE)
.monospace(),
);
}); });
// -------------------------------- // --------------------------------
@@ -102,17 +119,19 @@ impl SoundpadGui {
ui.add_space(20.0); ui.add_space(20.0);
// --------- Checkboxes ---------- // --------- Checkboxes ----------
let save_volume_response = let save_volume_response = ui.checkbox(
ui.checkbox(&mut self.config.save_volume, "Always remember volume"); &mut self.config.save_volume,
t!("gui.settings.remember_volume"),
);
let save_input_response = let save_input_response =
ui.checkbox(&mut self.config.save_input, "Always remember microphone"); ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic"));
let save_scale_response = ui.checkbox( let save_scale_response = ui.checkbox(
&mut self.config.save_scale_factor, &mut self.config.save_scale_factor,
"Always remember UI scale factor", t!("gui.settings.remember_ui_scale"),
); );
let pause_on_exit_response = ui.checkbox( let pause_on_exit_response = ui.checkbox(
&mut self.config.pause_on_exit, &mut self.config.pause_on_exit,
"Pause audio playback when the window is closed", t!("gui.settings.pause_on_window_close"),
); );
if save_volume_response.changed() if save_volume_response.changed()
@@ -125,7 +144,10 @@ impl SoundpadGui {
// -------------------------------- // --------------------------------
ui.with_layout(Layout::bottom_up(Align::Min), |ui| { ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
ui.label(format!("GUI version: {}", env!("CARGO_PKG_VERSION"))); ui.label(t!(
"gui.settings.version",
version = env!("CARGO_PKG_VERSION")
));
}); });
}); });
} }
@@ -157,28 +179,44 @@ impl SoundpadGui {
} }
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label(RichText::new("Hotkeys").color(Color32::WHITE).monospace()); ui.label(
RichText::new(t!("gui.hotkeys.header"))
.color(Color32::WHITE)
.monospace(),
);
}); });
}); });
} }
fn draw_hotkeys_search(&mut self, ui: &mut Ui) { fn draw_hotkeys_search(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| { ui.menu_button(
format!(
"{} {}",
ICON_ADD.codepoint,
t!("gui.hotkeys.add_command_select")
),
|ui| {
let mut selected_cmd = None; let mut selected_cmd = None;
if ui.button("Toggle Pause").clicked() { if ui.button(t!("gui.hotkeys.toggle_pause_command")).clicked() {
selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None))); selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None)));
} }
if ui.button("Stop Playback").clicked() { if ui.button(t!("gui.hotkeys.stop_playback_command")).clicked() {
selected_cmd = Some(("cmd_stop", Request::stop(None))); selected_cmd = Some(("cmd_stop", Request::stop(None)));
} }
if ui.button("Pause Playback").clicked() { if ui
.button(t!("gui.hotkeys.pause_playback_command"))
.clicked()
{
selected_cmd = Some(("cmd_pause", Request::pause(None))); selected_cmd = Some(("cmd_pause", Request::pause(None)));
} }
if ui.button("Resume Playback").clicked() { if ui
.button(t!("gui.hotkeys.resume_playback_command"))
.clicked()
{
selected_cmd = Some(("cmd_resume", Request::resume(None))); selected_cmd = Some(("cmd_resume", Request::resume(None)));
} }
if ui.button("Toggle Loop").clicked() { if ui.button(t!("gui.hotkeys.toggle_loop_command")).clicked() {
selected_cmd = Some(("cmd_toggle_loop", Request::toggle_loop(None))); selected_cmd = Some(("cmd_toggle_loop", Request::toggle_loop(None)));
} }
@@ -191,13 +229,14 @@ impl SoundpadGui {
self.app_state.hotkey_capture_active = true; self.app_state.hotkey_capture_active = true;
ui.close(); ui.close();
} }
}); },
);
ui.add_space(10.0); ui.add_space(10.0);
ui.add( ui.add(
TextEdit::singleline(&mut self.app_state.hotkey_search_query) TextEdit::singleline(&mut self.app_state.hotkey_search_query)
.hint_text("Search hotkeys...") .hint_text(t!("gui.hotkeys.search_placeholder"))
.desired_width(f32::INFINITY), .desired_width(f32::INFINITY),
); );
}); });
@@ -242,7 +281,7 @@ impl SoundpadGui {
.header(30.0, |mut header| { .header(30.0, |mut header| {
header.col(|ui| { header.col(|ui| {
ui.label( ui.label(
RichText::new("Slot") RichText::new(t!("gui.hotkeys.column_slot"))
.strong() .strong()
.monospace() .monospace()
.color(Color32::LIGHT_GRAY), .color(Color32::LIGHT_GRAY),
@@ -250,7 +289,7 @@ impl SoundpadGui {
}); });
header.col(|ui| { header.col(|ui| {
ui.label( ui.label(
RichText::new("Sound") RichText::new(t!("gui.hotkeys.column_sound"))
.strong() .strong()
.monospace() .monospace()
.color(Color32::LIGHT_GRAY), .color(Color32::LIGHT_GRAY),
@@ -258,7 +297,7 @@ impl SoundpadGui {
}); });
header.col(|ui| { header.col(|ui| {
ui.label( ui.label(
RichText::new("Key Chord") RichText::new(t!("gui.hotkeys.column_key_chord"))
.strong() .strong()
.monospace() .monospace()
.color(Color32::LIGHT_GRAY), .color(Color32::LIGHT_GRAY),
@@ -266,7 +305,7 @@ impl SoundpadGui {
}); });
header.col(|ui| { header.col(|ui| {
ui.label( ui.label(
RichText::new("Actions") RichText::new(t!("gui.hotkeys.column_actions"))
.strong() .strong()
.monospace() .monospace()
.color(Color32::LIGHT_GRAY), .color(Color32::LIGHT_GRAY),
@@ -279,7 +318,8 @@ impl SoundpadGui {
row.col(|_| {}); row.col(|_| {});
row.col(|ui| { row.col(|ui| {
ui.label( ui.label(
RichText::new("No hotkey slots configured.").color(Color32::GRAY), RichText::new(t!("gui.hotkeys.no_hotkeys_configured"))
.color(Color32::GRAY),
); );
}); });
row.col(|_| {}); row.col(|_| {});
@@ -671,7 +711,11 @@ impl SoundpadGui {
// Context menu // Context menu
dir_button_response.context_menu(|ui| { dir_button_response.context_menu(|ui| {
if ui if ui
.button(format!("{} {}", ICON_OPEN_IN_NEW.codepoint, "Show")) .button(format!(
"{} {}",
ICON_OPEN_IN_NEW.codepoint,
t!("gui.context.dirs.open")
))
.clicked() .clicked()
{ {
self.open_dir(&path); self.open_dir(&path);
@@ -680,7 +724,8 @@ impl SoundpadGui {
if ui if ui
.button(format!( .button(format!(
"{} {}", "{} {}",
ICON_OPEN_IN_BROWSER.codepoint, "Open in File Manager" ICON_OPEN_IN_BROWSER.codepoint,
t!("gui.context.dirs.open_in_fm")
)) ))
.clicked() .clicked()
&& let Err(e) = opener::open(&path) && let Err(e) = opener::open(&path)
@@ -691,7 +736,11 @@ impl SoundpadGui {
ui.separator(); ui.separator();
if ui if ui
.button(format!("{} {}", ICON_DELETE.codepoint, "Remove")) .button(format!(
"{} {}",
ICON_DELETE.codepoint,
t!("gui.context.dirs.remove")
))
.clicked() .clicked()
{ {
self.app_state.dirs_to_remove.insert(path.clone()); self.app_state.dirs_to_remove.insert(path.clone());
@@ -710,7 +759,7 @@ impl SoundpadGui {
}); });
ui.with_layout(Layout::bottom_up(Align::Min), |ui| { ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
let play_file_button = Button::new("Play file"); let play_file_button = Button::new(t!("gui.play_file_button"));
let play_file_button_response = ui.add(play_file_button); let play_file_button_response = ui.add(play_file_button);
if play_file_button_response.clicked() { if play_file_button_response.clicked() {
self.open_file(); self.open_file();
@@ -725,7 +774,8 @@ impl SoundpadGui {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let search_field_response = ui.add_sized( let search_field_response = ui.add_sized(
[ui.available_width(), 22.0], [ui.available_width(), 22.0],
TextEdit::singleline(&mut self.app_state.search_query).hint_text("Search..."), TextEdit::singleline(&mut self.app_state.search_query)
.hint_text(t!("gui.search_placeholder")),
); );
if self.app_state.force_focus_search { if self.app_state.force_focus_search {
@@ -743,10 +793,106 @@ impl SoundpadGui {
ui.set_min_height(area_size.y); ui.set_min_height(area_size.y);
ui.vertical(|ui| { ui.vertical(|ui| {
let mut actions = Vec::new();
let files = self.get_filtered_files(); let files = self.get_filtered_files();
for entry_path in files { for entry_path in files {
let file_name = entry_path Self::draw_tree_node(
ui,
entry_path,
&mut self.app_state,
&self.audio_player_state,
&mut actions,
);
}
for action in actions {
match action {
FileAction::Play(path, concurrent) => self.play_file(&path, concurrent),
FileAction::StopAndPlay(id, path, concurrent) => {
self.stop(Some(id));
self.play_file(&path, concurrent);
}
FileAction::AssignHotkey(path) => {
self.app_state.assigning_hotkey_for_file = Some(path);
self.app_state.hotkey_capture_active = true;
}
}
}
});
});
});
}
fn draw_tree_node(
ui: &mut Ui,
path: std::path::PathBuf,
app_state: &mut AppState,
audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>,
) {
if path.is_dir() {
let dir_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
CollapsingHeader::new(dir_name)
.id_salt(&path)
.show(ui, |ui| {
let children = if let Some(cached) = app_state.dir_cache.get(&path) {
cached.clone()
} else {
let mut read = Vec::new();
if let Ok(entries) = std::fs::read_dir(&path) {
for entry in entries.filter_map(|e| e.ok()) {
read.push(entry.path());
}
}
read.sort_by(|a, b| {
let a_is_dir = a.is_dir();
let b_is_dir = b.is_dir();
if a_is_dir && !b_is_dir {
Ordering::Less
} else if !a_is_dir && b_is_dir {
Ordering::Greater
} else {
a.cmp(b)
}
});
app_state.dir_cache.insert(path.clone(), read.clone());
read
};
let search_query = app_state.search_query.to_lowercase();
let search_query = search_query.trim();
for child in children {
if !child.is_dir() {
if !crate::gui::SUPPORTED_EXTENSIONS.contains(
&child
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
) {
continue;
}
if !search_query.is_empty() {
let file_name = child
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if !file_name.to_lowercase().contains(search_query) {
continue;
}
}
}
Self::draw_tree_node(ui, child, app_state, audio_player_state, actions);
}
});
} else {
let file_name = path
.file_name() .file_name()
.unwrap_or_default() .unwrap_or_default()
.to_string_lossy() .to_string_lossy()
@@ -754,7 +900,21 @@ impl SoundpadGui {
ui.horizontal(|ui| { ui.horizontal(|ui| {
// Hotkey badge // Hotkey badge
let hotkey_badge = self.get_hotkey_badge(&entry_path); let mut hotkey_badge = None;
for slot in &app_state.hotkey_config.slots {
if slot.action.name == "play"
&& let Some(file_path_str) = slot.action.args.get("file_path")
&& Path::new(file_path_str) == path
{
if let Some(chord) = &slot.key_chord {
hotkey_badge = Some(format!("[{}]", chord));
} else {
hotkey_badge = Some(format!("[{}]", slot.slot));
}
break;
}
}
if let Some(badge) = &hotkey_badge { if let Some(badge) = &hotkey_badge {
ui.label( ui.label(
RichText::new(badge) RichText::new(badge)
@@ -764,61 +924,62 @@ impl SoundpadGui {
); );
} }
let mut file_button_text = RichText::new(&file_name); let file_button_text = RichText::new(&file_name);
if let Some(current_file) = &self.app_state.selected_file
&& current_file.eq(&entry_path)
{
file_button_text = file_button_text.color(Color32::WHITE);
}
let file_button = Button::new(file_button_text).frame(false).truncate(); let file_button = Button::new(file_button_text).frame(false).truncate();
let file_button_response = ui.add(file_button); let file_button_response = ui.add(file_button);
if file_button_response.clicked() { if file_button_response.clicked() {
ui.input(|i| { ui.input(|i| {
if i.modifiers.ctrl { if i.modifiers.ctrl {
self.play_file(&entry_path, true); actions.push(FileAction::Play(path.clone(), true));
} else if i.modifiers.shift } else if i.modifiers.shift
&& let Some(last_track) = && let Some(last_track) = audio_player_state.tracks.last()
self.audio_player_state.tracks.last()
{ {
self.stop(Some(last_track.id)); actions.push(FileAction::StopAndPlay(
self.play_file(&entry_path, true); last_track.id,
path.clone(),
true,
));
} else { } else {
self.play_file(&entry_path, false); actions.push(FileAction::Play(path.clone(), false));
} }
}); });
self.app_state.selected_file = Some(entry_path.clone());
} }
// Context menu // Context menu
file_button_response.context_menu(|ui| { file_button_response.context_menu(|ui| {
if ui if ui
.button(format!("{} {}", ICON_BOLT.codepoint, "Play Solo")) .button(format!(
"{} {}",
ICON_BOLT.codepoint,
t!("gui.context.files.play_solo")
))
.clicked() .clicked()
{ {
self.play_file(&entry_path, false); actions.push(FileAction::Play(path.clone(), false));
self.app_state.selected_file = Some(entry_path.clone());
}
if ui
.button(format!("{} {}", ICON_ADD.codepoint, "Add New"))
.clicked()
{
self.play_file(&entry_path, true);
self.app_state.selected_file = Some(entry_path.clone());
} }
if ui if ui
.button(format!( .button(format!(
"{} {}", "{} {}",
ICON_SWAP_HORIZ.codepoint, "Replace Last" ICON_ADD.codepoint,
t!("gui.context.files.add_new")
)) ))
.clicked() .clicked()
&& let Some(last_track) = self.audio_player_state.tracks.last()
{ {
self.stop(Some(last_track.id)); actions.push(FileAction::Play(path.clone(), true));
self.play_file(&entry_path, true); }
self.app_state.selected_file = Some(entry_path.clone());
if ui
.button(format!(
"{} {}",
ICON_SWAP_HORIZ.codepoint,
t!("gui.context.files.replace_last")
))
.clicked()
&& let Some(last_track) = audio_player_state.tracks.last()
{
actions.push(FileAction::StopAndPlay(last_track.id, path.clone(), true));
} }
ui.separator(); ui.separator();
@@ -826,10 +987,11 @@ impl SoundpadGui {
if ui if ui
.button(format!( .button(format!(
"{} {}", "{} {}",
ICON_OPEN_IN_BROWSER.codepoint, "Show in File Manager" ICON_OPEN_IN_BROWSER.codepoint,
t!("gui.context.files.show_in_fm")
)) ))
.clicked() .clicked()
&& let Err(e) = opener::reveal(&entry_path) && let Err(e) = opener::reveal(&path)
{ {
eprintln!("Failed to open file manager: {}", e); eprintln!("Failed to open file manager: {}", e);
} }
@@ -839,37 +1001,17 @@ impl SoundpadGui {
if ui if ui
.button(format!( .button(format!(
"{} {}", "{} {}",
ICON_KEYBOARD.codepoint, "Assign Hotkey" ICON_KEYBOARD.codepoint,
t!("gui.context.files.asign_hotkey")
)) ))
.clicked() .clicked()
{ {
self.app_state.assigning_hotkey_for_file = actions.push(FileAction::AssignHotkey(path.clone()));
Some(entry_path.clone());
self.app_state.hotkey_capture_active = true;
ui.close(); ui.close();
} }
}); });
}); });
} }
});
});
});
}
fn get_hotkey_badge(&self, path: &Path) -> Option<String> {
for slot in &self.app_state.hotkey_config.slots {
if slot.action.name == "play"
&& let Some(file_path_str) = slot.action.args.get("file_path")
&& Path::new(file_path_str) == path
{
if let Some(chord) = &slot.key_chord {
return Some(format!("[{}]", chord));
} else {
return Some(format!("[{}]", slot.slot));
}
}
}
None
} }
fn draw_footer(&mut self, ui: &mut Ui) { fn draw_footer(&mut self, ui: &mut Ui) {
@@ -880,7 +1022,7 @@ impl SoundpadGui {
let mut selected_input = self.audio_player_state.current_input.to_owned(); let mut selected_input = self.audio_player_state.current_input.to_owned();
let prev_input = selected_input.to_owned(); let prev_input = selected_input.to_owned();
ComboBox::from_label("Choose microphone") ComboBox::from_label(t!("gui.choose_mic_select"))
.height(30.0) .height(30.0)
.selected_text( .selected_text(
self.audio_player_state self.audio_player_state
+1 -71
View File
@@ -3,8 +3,6 @@ use egui::{Context, Id, Key, Modifiers};
use pwsp::types::socket::Request; use pwsp::types::socket::Request;
use pwsp::utils::gui::make_request_async; use pwsp::utils::gui::make_request_async;
use std::path::PathBuf;
/// Convert an egui Key + Modifiers to a normalized chord string like "Ctrl+Shift+A". /// Convert an egui Key + Modifiers to a normalized chord string like "Ctrl+Shift+A".
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();
@@ -94,7 +92,7 @@ impl SoundpadGui {
} }
pub fn handle_input(&mut self, ctx: &Context) { pub fn handle_input(&mut self, ctx: &Context) {
let modifiers = self.modifiers(ctx); let _modifiers = self.modifiers(ctx);
let search_focused = { let search_focused = {
if let Some(focused_id) = self.get_focused(ctx) if let Some(focused_id) = self.get_focused(ctx)
&& let Some(search_id) = self.app_state.search_field_id && let Some(search_id) = self.app_state.search_field_id
@@ -197,74 +195,6 @@ impl SoundpadGui {
} }
} }
// Play selected file on Enter
if self.key_pressed(ctx, Key::Enter)
&& let Some(path) = self.app_state.selected_file.clone()
{
if modifiers.ctrl {
self.play_file(&path, true);
} else if modifiers.shift
&& let Some(last_track) = self.audio_player_state.tracks.last()
{
self.stop(Some(last_track.id));
self.play_file(&path, true);
} else {
self.play_file(&path, false);
}
}
// Iterate through dirs and files with Ctrl + Up/Down
let arrow_up_pressed = self.key_pressed(ctx, Key::ArrowUp);
let arrow_down_pressed = self.key_pressed(ctx, Key::ArrowDown);
if modifiers.ctrl && (arrow_up_pressed || arrow_down_pressed) {
if modifiers.shift && !self.app_state.dirs.is_empty() {
let mut dirs: Vec<PathBuf> = self.app_state.dirs.to_vec();
dirs.sort();
let current_dir_index = self
.app_state
.current_dir
.as_ref()
.and_then(|cd| dirs.iter().position(|x| x == cd));
let new_dir_index =
match (current_dir_index, arrow_up_pressed, arrow_down_pressed) {
(Some(i), true, false) => (i + dirs.len() - 1) % dirs.len(),
(Some(i), false, true) => (i + 1) % dirs.len(),
(Some(i), true, true) => i,
(None, true, _) => dirs.len() - 1,
(None, false, true) => 0,
_ => return,
};
self.open_dir(&dirs[new_dir_index]);
} else if self.app_state.current_dir.is_some() {
let files = self.get_filtered_files();
if files.is_empty() {
return;
}
let current_files_index = self
.app_state
.selected_file
.as_ref()
.and_then(|f| files.iter().position(|x| x == f));
let new_files_index =
match (current_files_index, arrow_up_pressed, arrow_down_pressed) {
(Some(i), true, false) => (i + files.len() - 1) % files.len(),
(Some(i), false, true) => (i + 1) % files.len(),
(Some(i), true, true) => i,
(None, true, _) => files.len() - 1,
(None, false, true) => 0,
_ => return,
};
self.app_state.selected_file = Some(files[new_files_index].clone());
}
}
// Check for hotkey chord triggers // Check for hotkey chord triggers
let slots_to_play: Vec<String> = ctx.input(|i| { let slots_to_play: Vec<String> = ctx.input(|i| {
let mut result = vec![]; let mut result = vec![];
+23 -15
View File
@@ -2,6 +2,7 @@ mod draw;
mod input; mod input;
mod update; mod update;
use anyhow::{Result, anyhow};
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native}; use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
use egui::{Context, FontData, FontDefinitions, FontFamily, FontTweak, Vec2, ViewportBuilder}; use egui::{Context, FontData, FontDefinitions, FontFamily, FontTweak, Vec2, ViewportBuilder};
use itertools::Itertools; use itertools::Itertools;
@@ -20,7 +21,7 @@ use pwsp::{
}; };
use rfd::FileDialog; use rfd::FileDialog;
use std::{ use std::{
error::Error, cmp::Ordering,
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
@@ -110,14 +111,14 @@ impl SoundpadGui {
self.app_state.current_dir = Some(path.clone()); self.app_state.current_dir = Some(path.clone());
match path.read_dir() { match path.read_dir() {
Ok(read_dir) => { Ok(read_dir) => {
self.app_state.files = read_dir self.app_state.listed_files = read_dir
.filter_map(|res| res.ok()) .filter_map(|res| res.ok())
.map(|entry| entry.path()) .map(|entry| entry.path())
.collect(); .collect();
} }
Err(e) => { Err(e) => {
eprintln!("Failed to read directory {:?}: {}", path, e); eprintln!("Failed to read directory {:?}: {}", path, e);
self.app_state.files.clear(); self.app_state.listed_files.clear();
} }
} }
} }
@@ -157,8 +158,18 @@ impl SoundpadGui {
} }
pub fn get_filtered_files(&self) -> Vec<PathBuf> { pub fn get_filtered_files(&self) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = self.app_state.files.iter().cloned().collect(); let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
files.sort(); files.sort_by(|a, b| {
let a_is_dir = a.is_dir();
let b_is_dir = b.is_dir();
if a_is_dir && !b_is_dir {
Ordering::Less
} else if !a_is_dir && b_is_dir {
Ordering::Greater
} else {
a.cmp(b)
}
});
let search_query = self.app_state.search_query.to_lowercase(); let search_query = self.app_state.search_query.to_lowercase();
let search_query = search_query.trim(); let search_query = search_query.trim();
@@ -167,7 +178,7 @@ impl SoundpadGui {
.into_iter() .into_iter()
.filter(|entry_path| { .filter(|entry_path| {
if entry_path.is_dir() { if entry_path.is_dir() {
return false; return true;
} }
if !SUPPORTED_EXTENSIONS.contains( if !SUPPORTED_EXTENSIONS.contains(
@@ -198,11 +209,7 @@ impl SoundpadGui {
} }
} }
fn add_font( fn add_font(font_name: &str, font_bytes: &[u8], fonts: &mut FontDefinitions) -> Result<()> {
font_name: &str,
font_bytes: &[u8],
fonts: &mut FontDefinitions,
) -> Result<(), Box<dyn Error>> {
let font_data = FontData::from_owned(font_bytes.to_vec()).tweak(FontTweak { let font_data = FontData::from_owned(font_bytes.to_vec()).tweak(FontTweak {
scale: 1.0, scale: 1.0,
hinting_override: Some(true), hinting_override: Some(true),
@@ -227,12 +234,13 @@ fn add_font(
Ok(()) Ok(())
} }
fn load_system_fonts(fonts: &mut FontDefinitions) -> Result<(), Box<dyn Error>> { fn load_system_fonts(fonts: &mut FontDefinitions) -> Result<()> {
let (_, en_sans) = find_for_locale("en", FontStyle::Sans); let (_, en_sans) = find_for_locale("en", FontStyle::Sans);
let (_, en_serif) = find_for_locale("en", FontStyle::Serif); let (_, en_serif) = find_for_locale("en", FontStyle::Serif);
let (_, ja_sans) = find_for_locale("ja", FontStyle::Sans); let (_, ja_sans) = find_for_locale("ja", FontStyle::Sans);
let (_, ar_sans) = find_for_locale("ar", FontStyle::Sans);
let system_fonts = [en_sans, en_serif, ja_sans].concat(); let system_fonts = [en_sans, en_serif, ja_sans, ar_sans].concat();
for font in system_fonts.iter().rev() { for font in system_fonts.iter().rev() {
let font_bytes = match &font.source { let font_bytes = match &font.source {
@@ -246,7 +254,7 @@ fn load_system_fonts(fonts: &mut FontDefinitions) -> Result<(), Box<dyn Error>>
Ok(()) Ok(())
} }
pub async fn run() -> Result<(), Box<dyn Error>> { pub async fn run() -> Result<()> {
const ICON: &[u8] = include_bytes!("../../assets/icon.png"); const ICON: &[u8] = include_bytes!("../../assets/icon.png");
let options = NativeOptions { let options = NativeOptions {
@@ -283,6 +291,6 @@ pub async fn run() -> Result<(), Box<dyn Error>> {
} }
Ok(()) Ok(())
} }
Err(e) => Err(e.into()), Err(e) => Err(anyhow!(e.to_string())),
} }
} }
+1 -1
View File
@@ -16,7 +16,7 @@ impl App for SoundpadGui {
&& current_dir == &path && current_dir == &path
{ {
self.app_state.current_dir = None; self.app_state.current_dir = None;
self.app_state.files.clear(); self.app_state.listed_files.clear();
} }
} }
+8 -2
View File
@@ -1,8 +1,14 @@
mod gui; mod gui;
use std::error::Error; use anyhow::Result;
use rust_i18n::i18n;
i18n!("locales", fallback = "en");
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<()> {
let locale = sys_locale::get_locale().unwrap_or(String::from("en-US"));
rust_i18n::set_locale(&locale);
gui::run().await gui::run().await
} }
+14 -17
View File
@@ -5,6 +5,7 @@ use crate::{
pipewire::{create_link, get_device, link_player_to_virtual_mic}, pipewire::{create_link, get_device, link_player_to_virtual_mic},
}, },
}; };
use anyhow::{Result, anyhow};
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source}; use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@@ -65,7 +66,7 @@ pub struct AudioPlayer {
} }
impl AudioPlayer { impl AudioPlayer {
pub async fn new() -> Result<Self, Box<dyn Error>> { pub async fn new() -> Result<Self> {
let daemon_config = get_daemon_config(); let daemon_config = get_daemon_config();
let default_volume = daemon_config.default_volume.unwrap_or(1.0); let default_volume = daemon_config.default_volume.unwrap_or(1.0);
@@ -88,7 +89,7 @@ impl AudioPlayer {
Ok(audio_player) Ok(audio_player)
} }
fn ensure_stream(&mut self) -> Result<&MixerDeviceSink, Box<dyn Error>> { fn ensure_stream(&mut self) -> Result<&MixerDeviceSink> {
if self.stream_handle.is_none() { if self.stream_handle.is_none() {
let mut sink = DeviceSinkBuilder::open_default_sink()?; let mut sink = DeviceSinkBuilder::open_default_sink()?;
sink.log_on_drop(false); sink.log_on_drop(false);
@@ -126,7 +127,7 @@ impl AudioPlayer {
} }
} }
async fn link_player(&mut self) -> Result<(), Box<dyn Error>> { async fn link_player(&mut self) -> Result<()> {
if self.player_link_sender.is_some() { if self.player_link_sender.is_some() {
return Ok(()); return Ok(());
} }
@@ -140,7 +141,7 @@ impl AudioPlayer {
} }
} }
async fn link_devices(&mut self) -> Result<(), Box<dyn Error>> { async fn link_devices(&mut self) -> Result<()> {
self.abort_link_thread(); self.abort_link_thread();
let input_device; let input_device;
@@ -289,7 +290,7 @@ impl AudioPlayer {
0.0 0.0
} }
pub fn seek(&mut self, position: f32, id: Option<u32>) -> Result<(), Box<dyn Error>> { pub fn seek(&mut self, position: f32, id: Option<u32>) -> Result<()> {
let position = if position < 0.0 { 0.0 } else { position }; let position = if position < 0.0 { 0.0 } else { position };
if let Some(id) = id { if let Some(id) = id {
@@ -305,22 +306,18 @@ impl AudioPlayer {
Ok(()) Ok(())
} }
pub fn get_duration(&mut self, id: Option<u32>) -> Result<f32, Box<dyn Error>> { pub fn get_duration(&mut self, id: Option<u32>) -> Result<f32> {
if let Some(id) = id { if let Some(id) = id {
if let Some(sound) = self.tracks.get(&id) { if let Some(sound) = self.tracks.get(&id) {
return sound.duration.ok_or("Unknown duration".into()); return sound.duration.ok_or(anyhow!("Unknown duration"));
} }
} else if let Some(sound) = self.tracks.values().last() { } else if let Some(sound) = self.tracks.values().last() {
return sound.duration.ok_or("Unknown duration".into()); return sound.duration.ok_or(anyhow!("Unknown duration"));
} }
Err("No track playing".into()) Err(anyhow!("No track playing"))
} }
pub async fn play( pub async fn play(&mut self, file_path: &Path, concurrent: bool) -> Result<u32> {
&mut self,
file_path: &Path,
concurrent: bool,
) -> Result<u32, Box<dyn Error>> {
let path_buf = file_path.to_path_buf(); let path_buf = file_path.to_path_buf();
let decoder_result = let decoder_result =
@@ -369,7 +366,7 @@ impl AudioPlayer {
Ok(id) Ok(id)
} }
Err(err) => Err(err as Box<dyn Error>), Err(err) => Err(anyhow!(err)),
} }
} }
@@ -472,11 +469,11 @@ impl AudioPlayer {
} }
} }
pub async fn set_current_input_device(&mut self, name: &str) -> Result<(), Box<dyn Error>> { pub async fn set_current_input_device(&mut self, name: &str) -> Result<()> {
let input_device = get_device(name).await?; let input_device = get_device(name).await?;
if input_device.device_type != DeviceType::Input { if input_device.device_type != DeviceType::Input {
return Err("Selected device is not an input device".into()); return Err(anyhow!("Selected device is not an input device"));
} }
self.input_device_name = Some(name.to_string()); self.input_device_name = Some(name.to_string());
+9 -8
View File
@@ -1,6 +1,7 @@
use crate::{types::socket::Request, utils::config::get_config_path}; use crate::{types::socket::Request, utils::config::get_config_path};
use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, error::Error, fs, path::PathBuf}; use std::{collections::HashMap, fs, path::PathBuf};
#[derive(Default, Clone, Serialize, Deserialize)] #[derive(Default, Clone, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
@@ -10,7 +11,7 @@ pub struct DaemonConfig {
} }
impl DaemonConfig { impl DaemonConfig {
pub fn save_to_file(&self) -> Result<(), Box<dyn Error>> { pub fn save_to_file(&self) -> Result<()> {
let config_path = get_config_path()?.join("daemon.json"); let config_path = get_config_path()?.join("daemon.json");
if let Some(config_dir) = config_path.parent() if let Some(config_dir) = config_path.parent()
@@ -24,7 +25,7 @@ impl DaemonConfig {
Ok(()) Ok(())
} }
pub fn load_from_file() -> Result<DaemonConfig, Box<dyn Error>> { pub fn load_from_file() -> Result<DaemonConfig> {
let config_path = get_config_path()?.join("daemon.json"); let config_path = get_config_path()?.join("daemon.json");
let bytes = fs::read(config_path)?; let bytes = fs::read(config_path)?;
match serde_json::from_slice::<DaemonConfig>(&bytes) { match serde_json::from_slice::<DaemonConfig>(&bytes) {
@@ -65,7 +66,7 @@ impl Default for GuiConfig {
} }
impl GuiConfig { impl GuiConfig {
pub fn save_to_file(&mut self) -> Result<(), Box<dyn Error>> { pub fn save_to_file(&mut self) -> Result<()> {
let config_path = get_config_path()?.join("gui.json"); let config_path = get_config_path()?.join("gui.json");
if let Some(config_dir) = config_path.parent() if let Some(config_dir) = config_path.parent()
@@ -84,7 +85,7 @@ impl GuiConfig {
Ok(()) Ok(())
} }
pub fn load_from_file() -> Result<GuiConfig, Box<dyn Error>> { pub fn load_from_file() -> Result<GuiConfig> {
let config_path = get_config_path()?.join("gui.json"); let config_path = get_config_path()?.join("gui.json");
let bytes = fs::read(config_path)?; let bytes = fs::read(config_path)?;
match serde_json::from_slice::<GuiConfig>(&bytes) { match serde_json::from_slice::<GuiConfig>(&bytes) {
@@ -108,11 +109,11 @@ pub struct HotkeyConfig {
} }
impl HotkeyConfig { impl HotkeyConfig {
pub fn config_path() -> Result<PathBuf, Box<dyn Error>> { pub fn config_path() -> Result<PathBuf> {
Ok(get_config_path()?.join("hotkeys.json")) Ok(get_config_path()?.join("hotkeys.json"))
} }
pub fn load() -> Result<HotkeyConfig, Box<dyn Error>> { pub fn load() -> Result<HotkeyConfig> {
let path = Self::config_path()?; let path = Self::config_path()?;
if !path.exists() { if !path.exists() {
return Ok(HotkeyConfig::default()); return Ok(HotkeyConfig::default());
@@ -124,7 +125,7 @@ impl HotkeyConfig {
} }
} }
pub fn save(&self) -> Result<(), Box<dyn Error>> { pub fn save(&self) -> Result<()> {
let path = Self::config_path()?; let path = Self::config_path()?;
if let Some(dir) = path.parent() if let Some(dir) = path.parent()
&& !dir.exists() && !dir.exists()
+6 -3
View File
@@ -43,15 +43,18 @@ pub struct AppState {
pub dirs: Vec<PathBuf>, pub dirs: Vec<PathBuf>,
pub dirs_to_remove: HashSet<PathBuf>, pub dirs_to_remove: HashSet<PathBuf>,
pub selected_file: Option<PathBuf>, pub listed_files: HashSet<PathBuf>,
pub files: HashSet<PathBuf>, pub listed_dirs: HashSet<PathBuf>,
pub dir_cache: HashMap<PathBuf, Vec<PathBuf>>,
pub show_hotkeys: bool, pub show_hotkeys: bool,
pub hotkey_capture_active: bool,
pub hotkey_config: HotkeyConfig, pub hotkey_config: HotkeyConfig,
pub hotkey_search_query: String, pub hotkey_search_query: String,
pub assigning_hotkey_slot: Option<String>, pub assigning_hotkey_slot: Option<String>,
pub assigning_hotkey_for_file: Option<PathBuf>, pub assigning_hotkey_for_file: Option<PathBuf>,
pub hotkey_capture_active: bool,
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
+3 -2
View File
@@ -1,6 +1,7 @@
use std::{error::Error, path::PathBuf}; use anyhow::Result;
use std::path::PathBuf;
pub fn get_config_path() -> Result<PathBuf, Box<dyn Error>> { pub fn get_config_path() -> Result<PathBuf> {
let config_path = dirs::config_dir().expect("Failed to obtain config dir"); let config_path = dirs::config_dir().expect("Failed to obtain config dir");
Ok(config_path.join("pwsp")) Ok(config_path.join("pwsp"))
} }
+4 -3
View File
@@ -4,6 +4,7 @@ use crate::types::{
socket::{MAX_MESSAGE_SIZE, Request, Response}, socket::{MAX_MESSAGE_SIZE, Request, Response},
}; };
use anyhow::Result;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf; use std::path::PathBuf;
use std::{error::Error, fs}; use std::{error::Error, fs};
@@ -40,7 +41,7 @@ pub fn get_runtime_dir() -> PathBuf {
dirs::runtime_dir().unwrap_or(PathBuf::from("/run/pwsp")) dirs::runtime_dir().unwrap_or(PathBuf::from("/run/pwsp"))
} }
pub fn create_runtime_dir() -> Result<(), Box<dyn Error>> { pub fn create_runtime_dir() -> Result<()> {
let runtime_dir = get_runtime_dir(); let runtime_dir = get_runtime_dir();
if !runtime_dir.exists() { if !runtime_dir.exists() {
fs::create_dir_all(&runtime_dir)?; fs::create_dir_all(&runtime_dir)?;
@@ -50,7 +51,7 @@ pub fn create_runtime_dir() -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
pub fn is_daemon_running() -> Result<bool, Box<dyn Error>> { pub fn is_daemon_running() -> Result<bool> {
let lock_file = fs::File::create(get_runtime_dir().join("daemon.lock"))?; let lock_file = fs::File::create(get_runtime_dir().join("daemon.lock"))?;
match lock_file.try_lock() { match lock_file.try_lock() {
Ok(_) => Ok(false), Ok(_) => Ok(false),
@@ -58,7 +59,7 @@ pub fn is_daemon_running() -> Result<bool, Box<dyn Error>> {
} }
} }
pub async fn wait_for_daemon() -> Result<(), Box<dyn Error>> { pub async fn wait_for_daemon() -> Result<()> {
if is_daemon_running()? { if is_daemon_running()? {
return Ok(()); return Ok(());
} }
+3 -3
View File
@@ -7,8 +7,8 @@ use crate::{
}, },
utils::daemon::{is_daemon_running, make_request}, utils::daemon::{is_daemon_running, make_request},
}; };
use anyhow::{Result, anyhow};
use std::{ use std::{
error::Error,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Instant, time::Instant,
}; };
@@ -22,11 +22,11 @@ pub fn get_gui_config() -> GuiConfig {
}) })
} }
pub fn make_request_sync(request: Request) -> Result<Response, Box<dyn Error>> { pub fn make_request_sync(request: Request) -> Result<Response> {
tokio::task::block_in_place(|| { tokio::task::block_in_place(|| {
tokio::runtime::Handle::current() tokio::runtime::Handle::current()
.block_on(make_request(request)) .block_on(make_request(request))
.map_err(|e| e as Box<dyn Error>) .map_err(|e| anyhow!(e))
}) })
} }
+21 -19
View File
@@ -1,9 +1,10 @@
use crate::types::pipewire::{AudioDevice, DeviceType, Port, Terminate}; use crate::types::pipewire::{AudioDevice, DeviceType, Port, Terminate};
use anyhow::{Result, anyhow};
use pipewire::{ use pipewire::{
context::ContextRc, link::Link, main_loop::MainLoopRc, properties::properties, context::ContextRc, link::Link, main_loop::MainLoopRc, properties::properties,
registry::GlobalObject, spa::utils::dict::DictRef, registry::GlobalObject, spa::utils::dict::DictRef,
}; };
use std::{collections::HashMap, error::Error, thread}; use std::{collections::HashMap, thread};
use tokio::{ use tokio::{
sync::mpsc, sync::mpsc,
time::{Duration, timeout}, time::{Duration, timeout},
@@ -142,7 +143,7 @@ async fn pw_get_global_objects_thread(
main_loop.run(); main_loop.run();
} }
pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), Box<dyn Error>> { pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>)> {
// 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();
@@ -155,7 +156,7 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
// Wait for initialization to complete // Wait for initialization to complete
if let Err(e) = init_receiver.recv()? { if let Err(e) = init_receiver.recv()? {
return Err(e.into()); return Err(anyhow!(e));
} }
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new(); let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
@@ -238,7 +239,7 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
} }
} }
pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>> { pub async fn get_device(device_name: &str) -> Result<AudioDevice> {
let (input_devices, output_devices) = get_all_devices().await?; let (input_devices, output_devices) = get_all_devices().await?;
input_devices input_devices
@@ -250,10 +251,10 @@ pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>
|| device.name.contains(device_name) || device.name.contains(device_name)
|| device.nick.contains(device_name) || device.nick.contains(device_name)
}) })
.ok_or_else(|| "Device not found".into()) .ok_or_else(|| anyhow!("Device not found"))
} }
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> { pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>> {
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 (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
@@ -305,45 +306,46 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
}); });
if let Err(e) = init_receiver.recv()? { if let Err(e) = init_receiver.recv()? {
return Err(e.into()); return Err(anyhow!(e));
} }
Ok(pw_sender) Ok(pw_sender)
} }
pub async fn link_player_to_virtual_mic() pub async fn link_player_to_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>> {
-> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
let pwsp_daemon_output = match get_device("pwsp-daemon").await { let pwsp_daemon_output = match get_device("pwsp-daemon").await {
Ok(device) => device, Ok(device) => device,
Err(_) => { Err(_) => {
return Err( return Err(anyhow!(
"Could not find alsa_playback.pwsp-daemon device, skipping device linking".into(), "Could not find alsa_playback.pwsp-daemon device, skipping device linking"
); ));
} }
}; };
let pwsp_daemon_input = match get_device("pwsp-virtual-mic").await { let pwsp_daemon_input = match get_device("pwsp-virtual-mic").await {
Ok(device) => device, Ok(device) => device,
Err(_) => { Err(_) => {
return Err("Could not find pwsp-virtual-mic device, skipping device linking".into()); return Err(anyhow!(
"Could not find pwsp-virtual-mic device, skipping device linking"
));
} }
}; };
let output_fl = match pwsp_daemon_output.output_fl { let output_fl = match pwsp_daemon_output.output_fl {
Some(port) => port, Some(port) => port,
None => return Err("Failed to get pwsp-daemon output_fl".into()), None => return Err(anyhow!("Failed to get pwsp-daemon output_fl")),
}; };
let output_fr = match pwsp_daemon_output.output_fr { let output_fr = match pwsp_daemon_output.output_fr {
Some(port) => port, Some(port) => port,
None => return Err("Failed to get pwsp-daemon output_fr".into()), None => return Err(anyhow!("Failed to get pwsp-daemon output_fr")),
}; };
let input_fl = match pwsp_daemon_input.input_fl { let input_fl = match pwsp_daemon_input.input_fl {
Some(port) => port, Some(port) => port,
None => return Err("Failed to get pwsp-virtual-mic input_fl".into()), None => return Err(anyhow!("Failed to get pwsp-virtual-mic input_fl")),
}; };
let input_fr = match pwsp_daemon_input.input_fr { let input_fr = match pwsp_daemon_input.input_fr {
Some(port) => port, Some(port) => port,
None => return Err("Failed to get pwsp-virtual-mic input_fr".into()), None => return Err(anyhow!("Failed to get pwsp-virtual-mic input_fr")),
}; };
create_link(output_fl, output_fr, input_fl, input_fr) create_link(output_fl, output_fr, input_fl, input_fr)
@@ -354,7 +356,7 @@ pub fn create_link(
output_fr: Port, output_fr: Port,
input_fl: Port, input_fl: Port,
input_fr: Port, input_fr: Port,
) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> { ) -> Result<pipewire::channel::Sender<Terminate>> {
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 (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
@@ -419,7 +421,7 @@ pub fn create_link(
}); });
if let Err(e) = init_receiver.recv()? { if let Err(e) = init_receiver.recv()? {
return Err(e.into()); return Err(anyhow!(e));
} }
Ok(pw_sender) Ok(pw_sender)