Sound applet rewrite (#28)
* Reset cosmic-applet-audio * save point with rwlock to talk to pulse * almost making pulse work with a subscription, but awating results causes a panic * maybe threaded * Working pulse connection * working async pulse audio listener * cargo fmt * working communication * make worky ChannelVolumes * more fixy * working control for speaker volume * fix changing volume on input * Initial port to iced-sctk * Fix revealer return types * fix: build * feat: more applet updates Co-authored-by: Ashley Wulber <ashley@system76.com>
This commit is contained in:
parent
f4b3ddcafc
commit
f3b5713ff5
25 changed files with 7825 additions and 1564 deletions
269
Cargo.lock
generated
269
Cargo.lock
generated
|
|
@ -229,7 +229,7 @@ source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b20
|
|||
dependencies = [
|
||||
"bitflags",
|
||||
"cairo-sys-rs",
|
||||
"glib 0.16.0",
|
||||
"glib",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
|
|
@ -240,7 +240,7 @@ name = "cairo-sys-rs"
|
|||
version = "0.16.0"
|
||||
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
|
||||
dependencies = [
|
||||
"glib-sys 0.16.0",
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
|
@ -348,29 +348,6 @@ dependencies = [
|
|||
"xdg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-applet-audio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"freedesktop-desktop-entry",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"gtk4",
|
||||
"libadwaita",
|
||||
"libcosmic",
|
||||
"libcosmic-applet",
|
||||
"libpulse-binding",
|
||||
"libpulse-glib-binding",
|
||||
"mpris2-zbus",
|
||||
"once_cell",
|
||||
"relm4",
|
||||
"relm4-macros",
|
||||
"tokio",
|
||||
"tracker",
|
||||
"zbus 2.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-applet-network"
|
||||
version = "0.1.0"
|
||||
|
|
@ -625,15 +602,6 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "4.0.0"
|
||||
|
|
@ -806,19 +774,6 @@ version = "1.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "freedesktop-desktop-entry"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45157175a725e81f3f594382430b6b78af5f8f72db9bd51b94f0785f80fc6d29"
|
||||
dependencies = [
|
||||
"dirs 3.0.2",
|
||||
"gettext-rs",
|
||||
"memchr",
|
||||
"thiserror",
|
||||
"xdg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.23"
|
||||
|
|
@ -940,7 +895,7 @@ dependencies = [
|
|||
"bitflags",
|
||||
"gdk-pixbuf-sys",
|
||||
"gio",
|
||||
"glib 0.16.0",
|
||||
"glib",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
@ -950,8 +905,8 @@ version = "0.16.0"
|
|||
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
|
||||
dependencies = [
|
||||
"gio-sys",
|
||||
"glib-sys 0.16.0",
|
||||
"gobject-sys 0.16.0",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
|
@ -966,7 +921,7 @@ dependencies = [
|
|||
"gdk-pixbuf",
|
||||
"gdk4-sys",
|
||||
"gio",
|
||||
"glib 0.16.0",
|
||||
"glib",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
|
@ -979,8 +934,8 @@ dependencies = [
|
|||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gio-sys",
|
||||
"glib-sys 0.16.0",
|
||||
"gobject-sys 0.16.0",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"pkg-config",
|
||||
|
|
@ -1010,26 +965,6 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gettext-rs"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364"
|
||||
dependencies = [
|
||||
"gettext-sys",
|
||||
"locale_config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gettext-sys"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"temp-dir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.16.0"
|
||||
|
|
@ -1041,7 +976,7 @@ dependencies = [
|
|||
"futures-io",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib 0.16.0",
|
||||
"glib",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
|
|
@ -1052,33 +987,13 @@ name = "gio-sys"
|
|||
version = "0.16.0"
|
||||
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
|
||||
dependencies = [
|
||||
"glib-sys 0.16.0",
|
||||
"gobject-sys 0.16.0",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.15.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-task",
|
||||
"glib-macros 0.15.11",
|
||||
"glib-sys 0.15.10",
|
||||
"gobject-sys 0.15.10",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.16.0"
|
||||
|
|
@ -1090,9 +1005,9 @@ dependencies = [
|
|||
"futures-executor",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"glib-macros 0.16.0",
|
||||
"glib-sys 0.16.0",
|
||||
"gobject-sys 0.16.0",
|
||||
"glib-macros",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"smallvec",
|
||||
|
|
@ -1104,21 +1019,6 @@ name = "glib-build-tools"
|
|||
version = "0.1.0"
|
||||
source = "git+https://github.com/gtk-rs/gtk-rs-core#4ca23ca05bd95655717605eaee349bbb5abc29b7"
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.15.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"proc-macro-crate",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.16.0"
|
||||
|
|
@ -1133,16 +1033,6 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.15.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.16.0"
|
||||
|
|
@ -1152,23 +1042,12 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.15.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
|
||||
dependencies = [
|
||||
"glib-sys 0.15.10",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.16.0"
|
||||
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
|
||||
dependencies = [
|
||||
"glib-sys 0.16.0",
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
|
@ -1178,7 +1057,7 @@ name = "graphene-rs"
|
|||
version = "0.16.0"
|
||||
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
|
||||
dependencies = [
|
||||
"glib 0.16.0",
|
||||
"glib",
|
||||
"graphene-sys",
|
||||
"libc",
|
||||
]
|
||||
|
|
@ -1188,7 +1067,7 @@ name = "graphene-sys"
|
|||
version = "0.16.0"
|
||||
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
|
||||
dependencies = [
|
||||
"glib-sys 0.16.0",
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"system-deps",
|
||||
|
|
@ -1202,7 +1081,7 @@ dependencies = [
|
|||
"bitflags",
|
||||
"cairo-rs",
|
||||
"gdk4",
|
||||
"glib 0.16.0",
|
||||
"glib",
|
||||
"graphene-rs",
|
||||
"gsk4-sys",
|
||||
"libc",
|
||||
|
|
@ -1216,8 +1095,8 @@ source = "git+https://github.com/gtk-rs/gtk4-rs#e4178e68237503c93ca98193e7832b7e
|
|||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk4-sys",
|
||||
"glib-sys 0.16.0",
|
||||
"gobject-sys 0.16.0",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"graphene-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
|
|
@ -1236,7 +1115,7 @@ dependencies = [
|
|||
"gdk-pixbuf",
|
||||
"gdk4",
|
||||
"gio",
|
||||
"glib 0.16.0",
|
||||
"glib",
|
||||
"graphene-rs",
|
||||
"gsk4",
|
||||
"gtk4-macros",
|
||||
|
|
@ -1268,8 +1147,8 @@ dependencies = [
|
|||
"gdk-pixbuf-sys",
|
||||
"gdk4-sys",
|
||||
"gio-sys",
|
||||
"glib-sys 0.16.0",
|
||||
"gobject-sys 0.16.0",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"graphene-sys",
|
||||
"gsk4-sys",
|
||||
"libc",
|
||||
|
|
@ -1479,7 +1358,7 @@ dependencies = [
|
|||
"gdk-pixbuf",
|
||||
"gdk4",
|
||||
"gio",
|
||||
"glib 0.16.0",
|
||||
"glib",
|
||||
"gtk4",
|
||||
"libadwaita-sys",
|
||||
"libc",
|
||||
|
|
@ -1494,8 +1373,8 @@ source = "git+https://gitlab.gnome.org/World/Rust/libadwaita-rs#01881b0c9f67ed5a
|
|||
dependencies = [
|
||||
"gdk4-sys",
|
||||
"gio-sys",
|
||||
"glib-sys 0.16.0",
|
||||
"gobject-sys 0.16.0",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk4-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
|
|
@ -1551,56 +1430,6 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libpulse-binding"
|
||||
version = "2.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17be42160017e0ae993c03bfdab4ecb6f82ce3f8d515bd8da8fdf18d10703663"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"libpulse-sys",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libpulse-glib-binding"
|
||||
version = "2.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df0e7a964c9f7e95d4f073affc19adfda009fa0d55e8831dbb66c78be1d0e6e5"
|
||||
dependencies = [
|
||||
"glib 0.15.12",
|
||||
"glib-sys 0.15.10",
|
||||
"libpulse-binding",
|
||||
"libpulse-mainloop-glib-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libpulse-mainloop-glib-sys"
|
||||
version = "1.19.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36f61c4064926cc77ea14bb206a21ce1d5a06e175e5c0ce078804bb6c4527b28"
|
||||
dependencies = [
|
||||
"glib-sys 0.15.10",
|
||||
"libpulse-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libpulse-sys"
|
||||
version = "1.19.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "991e6bd0efe2a36e6534e136e7996925e4c1a8e35b7807fe533f2beffff27c30"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"pkg-config",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "locale_config"
|
||||
version = "0.3.0"
|
||||
|
|
@ -1680,18 +1509,6 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpris2-zbus"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/mpris2-zbus#4e853c5a62a8a89ce58af11daddb62427523bc07"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
"time 0.3.13",
|
||||
"zbus 2.3.2",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanorand"
|
||||
version = "0.7.0"
|
||||
|
|
@ -1740,17 +1557,6 @@ dependencies = [
|
|||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
|
|
@ -1872,7 +1678,7 @@ source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b20
|
|||
dependencies = [
|
||||
"bitflags",
|
||||
"gio",
|
||||
"glib 0.16.0",
|
||||
"glib",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pango-sys",
|
||||
|
|
@ -1883,8 +1689,8 @@ name = "pango-sys"
|
|||
version = "0.16.0"
|
||||
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
|
||||
dependencies = [
|
||||
"glib-sys 0.16.0",
|
||||
"gobject-sys 0.16.0",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
|
@ -2188,7 +1994,7 @@ checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
|
|||
[[package]]
|
||||
name = "relm4"
|
||||
version = "0.5.0-beta.1"
|
||||
source = "git+https://github.com/relm4/relm4?branch=next#55231e300ce9e2b51d66f078b7759eba0759537c"
|
||||
source = "git+https://github.com/Relm4/Relm4.git?branch=next#55231e300ce9e2b51d66f078b7759eba0759537c"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-oneshot",
|
||||
|
|
@ -2197,7 +2003,6 @@ dependencies = [
|
|||
"gtk4",
|
||||
"log",
|
||||
"once_cell",
|
||||
"relm4-macros",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
|
@ -2205,7 +2010,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "relm4-macros"
|
||||
version = "0.5.0-beta.1"
|
||||
source = "git+https://github.com/relm4/relm4?branch=next#55231e300ce9e2b51d66f078b7759eba0759537c"
|
||||
source = "git+https://github.com/Relm4/Relm4.git?branch=next#55231e300ce9e2b51d66f078b7759eba0759537c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -2513,12 +2318,6 @@ dependencies = [
|
|||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "temp-dir"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.3.0"
|
||||
|
|
@ -3001,7 +2800,7 @@ version = "2.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6"
|
||||
dependencies = [
|
||||
"dirs 4.0.0",
|
||||
"dirs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3029,7 +2828,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"byteorder",
|
||||
"derivative",
|
||||
"dirs 4.0.0",
|
||||
"dirs",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
|
|
@ -3069,7 +2868,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"byteorder",
|
||||
"derivative",
|
||||
"dirs 4.0.0",
|
||||
"dirs",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
|
|
|
|||
11
Cargo.toml
11
Cargo.toml
|
|
@ -1,6 +1,5 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"applets/cosmic-applet-audio",
|
||||
"applets/cosmic-applet-network",
|
||||
"applets/cosmic-applet-notifications",
|
||||
"applets/cosmic-applet-power",
|
||||
|
|
@ -14,13 +13,13 @@ exclude = [
|
|||
"applets/cosmic-applet-graphics",
|
||||
"applets/cosmic-applet-workspaces",
|
||||
"applets/cosmic-applet-battery",
|
||||
|
||||
"applets/cosmic-applet-audio",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
wayland-protocols = { git = "https://github.com/smithay/wayland-rs", version = "0.30.0-beta.9"}
|
||||
wayland-protocols-wlr = { git = "https://github.com/smithay/wayland-rs", version = "0.1.0-beta.9"}
|
||||
wayland-sys = { git = "https://github.com/smithay/wayland-rs", version = "0.30.0-beta.13"}
|
||||
wayland-backend = { git = "https://github.com/smithay/wayland-rs", version = "0.1.0-beta.13"}
|
||||
wayland-scanner = { git = "https://github.com/smithay/wayland-rs", version = "0.30.0-beta.13"}
|
||||
wayland-client = { git = "https://github.com/smithay/wayland-rs", version = "0.30.0-beta.13"}
|
||||
wayland-sys = { git = "https://github.com/smithay/wayland-rs", version = "0.30.0-beta.9"}
|
||||
wayland-backend = { git = "https://github.com/smithay/wayland-rs", version = "0.1.0-beta.9"}
|
||||
wayland-scanner = { git = "https://github.com/smithay/wayland-rs", version = "0.30.0-beta.9"}
|
||||
wayland-client = { git = "https://github.com/smithay/wayland-rs", version = "0.30.0-beta.9"}
|
||||
|
|
|
|||
3165
applets/cosmic-applet-audio/Cargo.lock
generated
Normal file
3165
applets/cosmic-applet-audio/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,25 +2,32 @@
|
|||
name = "cosmic-applet-audio"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.21"
|
||||
futures-util = "0.3.21"
|
||||
libcosmic-applet = { path = "../../libcosmic-applet" }
|
||||
icon-loader = { version = "0.3.6", features = ["gtk"] }
|
||||
libpulse-binding = "2.26.0"
|
||||
libpulse-glib-binding = "2.25.0"
|
||||
tracker = "0.1.1"
|
||||
freedesktop-desktop-entry = "0.5.0"
|
||||
mpris2-zbus = { git = "https://github.com/pop-os/mpris2-zbus" }
|
||||
zbus = "2.1.1"
|
||||
tokio = { version = "1.17.0", features = ["full"] }
|
||||
relm4 = { git = "https://github.com/relm4/relm4", branch = "next", features = ["macros"] }
|
||||
relm4-macros = { git = "https://github.com/relm4/relm4", branch = "next" }
|
||||
once_cell = "1.10.0"
|
||||
gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs", features = ["v4_2"] }
|
||||
adw = { git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", package = "libadwaita"}
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = ["widgets"] }
|
||||
async-io = "1.6.0"
|
||||
tokio = { version = "1.20.1", features=["full"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet"] }
|
||||
iced_sctk = { git = "https://github.com/pop-os/iced-sctk" }
|
||||
sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", version = "0.16" }
|
||||
|
||||
[features]
|
||||
[workspace]
|
||||
resolved = "2"
|
||||
|
||||
[dependencies.iced]
|
||||
git = "https://github.com/pop-os/iced.git"
|
||||
branch = "sctk-cosmic"
|
||||
# path = "../iced"
|
||||
default-features = false
|
||||
features = ["image", "svg", "tokio", "wayland"]
|
||||
|
||||
[dependencies.iced_native]
|
||||
git = "https://github.com/pop-os/iced.git"
|
||||
branch = "sctk-cosmic"
|
||||
|
||||
[dependencies.iced_futures]
|
||||
git = "https://github.com/pop-os/iced.git"
|
||||
branch = "sctk-cosmic"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[Desktop Entry]
|
||||
Name=Cosmic Dock App List
|
||||
Name=Cosmic Applet Audio
|
||||
Comment=Write a GTK + Rust application
|
||||
Type=Application
|
||||
Exec=cosmic-applet-audio
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/com/System76/CosmicDockAppList/">
|
||||
<gresource prefix="/com/System76/CosmicAppletAudio/">
|
||||
<!-- see https://gtk-rs.org/gtk4-rs/git/docs/gtk4/struct.Application.html#automatic-resources -->
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
|
|
|||
|
|
@ -1,451 +0,0 @@
|
|||
use crate::icons::{parse_desktop_icons, DesktopApplication};
|
||||
use futures_util::StreamExt;
|
||||
use libcosmic_widgets::LabeledItem;
|
||||
use libpulse_binding::{
|
||||
context::{subscribe::{Facility, InterestMaskSet, Operation}, State},
|
||||
volume::Volume,
|
||||
};
|
||||
use mpris2_zbus::media_player::MediaPlayer;
|
||||
use relm4::{
|
||||
component,
|
||||
gtk::{
|
||||
self,
|
||||
glib::{self, clone},
|
||||
prelude::*,
|
||||
Align, Box as GtkBox, Button, Image, Label, ListBox, Orientation, PositionType, Revealer,
|
||||
RevealerTransitionType, Scale, Separator, Window,
|
||||
},
|
||||
view, ComponentParts, ComponentSender, RelmContainerExt, Sender, SimpleComponent, send,
|
||||
};
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
use tracker::track;
|
||||
use zbus::Connection;
|
||||
|
||||
use crate::pa::{DeviceInfo, PA};
|
||||
|
||||
pub enum AppInput {
|
||||
Inputs,
|
||||
Outputs,
|
||||
InputVolume,
|
||||
OutputVolume,
|
||||
NowPlaying,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct NowPlayingInfo {
|
||||
title: String,
|
||||
artist: String,
|
||||
album: String,
|
||||
art_url: String,
|
||||
}
|
||||
|
||||
#[track]
|
||||
pub struct App {
|
||||
#[no_eq]
|
||||
default_input: Option<DeviceInfo>,
|
||||
#[no_eq]
|
||||
inputs: Vec<DeviceInfo>,
|
||||
#[no_eq]
|
||||
default_output: Option<DeviceInfo>,
|
||||
#[no_eq]
|
||||
outputs: Vec<DeviceInfo>,
|
||||
#[no_eq]
|
||||
now_playing: Vec<NowPlayingInfo>,
|
||||
#[do_not_track]
|
||||
desktop_icons: HashMap<DesktopApplication, String>,
|
||||
#[do_not_track]
|
||||
pa: PA,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
let mut input_controller =
|
||||
SourceController::create().expect("failed to create input controller");
|
||||
let default_input = input_controller.get_default_device().ok();
|
||||
let inputs = input_controller.list_devices().unwrap_or_default();
|
||||
let mut output_controller =
|
||||
SinkController::create().expect("failed to create output controller");
|
||||
let default_output = output_controller.get_default_device().ok();
|
||||
let outputs = output_controller.list_devices().unwrap_or_default();
|
||||
let now_playing = Vec::new();
|
||||
let desktop_icons = parse_desktop_icons();
|
||||
// XXX handle no pulseaudio daemon?
|
||||
let pa = PA::new().unwrap();
|
||||
Self {
|
||||
default_input,
|
||||
inputs,
|
||||
default_output,
|
||||
outputs,
|
||||
now_playing,
|
||||
desktop_icons,
|
||||
pa,
|
||||
tracker: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn get_default_input_name(&self) -> &str {
|
||||
match &self.default_input {
|
||||
Some(input) => match &input.description {
|
||||
Some(name) => name.as_str(),
|
||||
None => "Input Device",
|
||||
},
|
||||
None => "No Input Device",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_default_output_name(&self) -> &str {
|
||||
match &self.default_output {
|
||||
Some(output) => match &output.description {
|
||||
Some(name) => name.as_str(),
|
||||
None => "Output Device",
|
||||
},
|
||||
None => "No Output Device",
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_default_input(&mut self) {
|
||||
let mut input_controller =
|
||||
SourceController::create().expect("failed to create input controller");
|
||||
self.default_input = match self.default_input.as_ref() {
|
||||
Some(input) => match &input.name {
|
||||
Some(name) => input_controller.get_device_by_name(name.as_str()).ok(),
|
||||
None => input_controller.get_device_by_index(input.index).ok(),
|
||||
},
|
||||
None => return,
|
||||
};
|
||||
}
|
||||
|
||||
fn refresh_default_output(&mut self) {
|
||||
let mut output_controller =
|
||||
SinkController::create().expect("failed to create output controller");
|
||||
self.default_output = match self.default_output.as_ref() {
|
||||
Some(output) => match &output.name {
|
||||
Some(name) => output_controller.get_device_by_name(name.as_str()).ok(),
|
||||
None => output_controller.get_device_by_index(output.index).ok(),
|
||||
},
|
||||
None => return,
|
||||
};
|
||||
}
|
||||
|
||||
fn subscribe_for_updates(&self, input: &Sender<AppInput>) {
|
||||
let input_clone = input.clone();
|
||||
self.pa.set_subscribe_callback(move |facility, operation, _idx| {
|
||||
if !matches!(operation, Some(Operation::Changed)) {
|
||||
return;
|
||||
}
|
||||
match facility {
|
||||
Some(Facility::Sink) => {
|
||||
input_clone.send(AppInput::OutputVolume);
|
||||
}
|
||||
Some(Facility::Source) => {
|
||||
input_clone.send(AppInput::InputVolume);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
self.pa.set_state_callback(move |pa, state| {
|
||||
if state == State::Ready {
|
||||
pa.subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn refresh_input_list(&mut self) {
|
||||
let mut input_controller =
|
||||
SourceController::create().expect("failed to create input controller");
|
||||
self.set_inputs(input_controller.list_devices().unwrap_or_default());
|
||||
}
|
||||
|
||||
fn refresh_input_widgets(&self, widgets: &AppWidgets) {
|
||||
while let Some(row) = widgets.inputs.row_at_index(0) {
|
||||
widgets.inputs.remove(&row);
|
||||
}
|
||||
for input in self.get_inputs() {
|
||||
let input = Rc::new(input.clone());
|
||||
let name = match &input.name {
|
||||
Some(name) => name.to_owned(),
|
||||
None => continue, // Why doesn't this have a name? Whatever, it's invalid.
|
||||
};
|
||||
view! {
|
||||
item = LabeledItem {
|
||||
set_title: input.description
|
||||
.as_ref()
|
||||
.unwrap_or(&name),
|
||||
set_child: set_current_input_device = &Button {
|
||||
set_label: "Switch",
|
||||
connect_clicked: move |_| {
|
||||
SourceController::create()
|
||||
.expect("failed to create input controller")
|
||||
.set_default_device(&name)
|
||||
.expect("failed to set default device");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
widgets.inputs.container_add(&item);
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_output_list(&mut self) {
|
||||
let mut output_controller =
|
||||
SinkController::create().expect("failed to create output controller");
|
||||
self.set_outputs(output_controller.list_devices().unwrap_or_default());
|
||||
}
|
||||
|
||||
fn refresh_output_widgets(&self, widgets: &AppWidgets) {
|
||||
while let Some(row) = widgets.outputs.row_at_index(0) {
|
||||
widgets.outputs.remove(&row);
|
||||
}
|
||||
for output in self.get_outputs() {
|
||||
let output = Rc::new(output.clone());
|
||||
let name = match &output.name {
|
||||
Some(name) => name.to_owned(),
|
||||
None => continue, // Why doesn't this have a name? Whatever, it's invalid.
|
||||
};
|
||||
view! {
|
||||
item = LabeledItem {
|
||||
set_title: output.description
|
||||
.as_ref()
|
||||
.unwrap_or(&name),
|
||||
set_child: set_current_output_device = &Button {
|
||||
set_label: "Switch",
|
||||
connect_clicked: move |_| {
|
||||
SinkController::create()
|
||||
.expect("failed to create output controller")
|
||||
.set_default_device(&name)
|
||||
.expect("failed to set default device");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
widgets.outputs.container_add(&item);
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_now_playing(&mut self) {
|
||||
let mut output_controller =
|
||||
SinkController::create().expect("failed to create output controller");
|
||||
self.set_now_playing(output_controller.list_applications().unwrap_or_default());
|
||||
}
|
||||
|
||||
fn refresh_now_playing_widgets(&self, widgets: &AppWidgets) {
|
||||
while let Some(row) = widgets.playing_apps.row_at_index(0) {
|
||||
widgets.playing_apps.remove(&row);
|
||||
}
|
||||
for app in self.get_now_playing() {
|
||||
let index = app.index;
|
||||
let muted = app.mute;
|
||||
let icon_name = app
|
||||
.proplist
|
||||
.get_str("application.icon_name")
|
||||
.or_else(|| {
|
||||
app.proplist
|
||||
.get_str("application.name")
|
||||
.and_then(|name| self.desktop_icons.get(&DesktopApplication::Name(name)))
|
||||
.cloned()
|
||||
})
|
||||
.or_else(|| {
|
||||
app.proplist
|
||||
.get_str("application.process.binary")
|
||||
.and_then(|name| self.desktop_icons.get(&DesktopApplication::Binary(name)))
|
||||
.cloned()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let name = app.name.clone().unwrap_or_default();
|
||||
view! {
|
||||
item = GtkBox {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
append: icon = &Image {
|
||||
set_icon_name: Some(&icon_name),
|
||||
set_pixel_size: 24,
|
||||
},
|
||||
append: title = &Label {
|
||||
set_label: &name,
|
||||
},
|
||||
append: media_buttons = &GtkBox {
|
||||
set_halign: Align::End,
|
||||
append: pause_button = &Button {
|
||||
#[wrap(Some)]
|
||||
set_child: pause_button_img = &Image {
|
||||
set_icon_name: Some("media-playback-pause-symbolic"),
|
||||
set_pixel_size: 24,
|
||||
},
|
||||
connect_clicked: move |_| {
|
||||
SinkController::create()
|
||||
.expect("failed to create output controller")
|
||||
.set_app_mute(index, !muted)
|
||||
.expect("failed to (un)mute application");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
widgets.playing_apps.container_add(&item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component(pub)]
|
||||
impl SimpleComponent for App {
|
||||
type Widgets = AppWidgets;
|
||||
type InitParams = ();
|
||||
type Input = AppInput;
|
||||
type Output = ();
|
||||
|
||||
view! {
|
||||
Window {
|
||||
set_title: Some("COSMIC Network Applet"),
|
||||
set_default_width: 400,
|
||||
set_default_height: 300,
|
||||
|
||||
GtkBox {
|
||||
set_orientation: Orientation::Vertical,
|
||||
set_spacing: 24,
|
||||
GtkBox {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
set_spacing: 16,
|
||||
Image {
|
||||
set_icon_name: Some("audio-speakers-symbolic"),
|
||||
},
|
||||
append: output_volume = &Scale::with_range(Orientation::Horizontal, 0., 100., 1.) {
|
||||
set_format_value_func: |_, value| {
|
||||
format!("{:.0}%", value)
|
||||
},
|
||||
#[watch]
|
||||
set_value: model.default_output.as_ref().map(|info| (info.volume.avg().0 as f64 / Volume::NORMAL.0 as f64) * 100.).unwrap_or(0.),
|
||||
set_value_pos: PositionType::Right,
|
||||
set_hexpand: true
|
||||
}
|
||||
},
|
||||
GtkBox {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
set_spacing: 16,
|
||||
Image {
|
||||
set_icon_name: Some("audio-input-microphone-symbolic"),
|
||||
},
|
||||
append: input_volume = &Scale::with_range(Orientation::Horizontal, 0., 100., 1.) {
|
||||
set_format_value_func: |_, value| {
|
||||
format!("{:.0}%", value)
|
||||
},
|
||||
#[watch]
|
||||
set_value: model.default_input
|
||||
.as_ref()
|
||||
.map(|info| (info.volume.avg().0 as f64 / Volume::NORMAL.0 as f64) * 100.)
|
||||
.unwrap_or(0.),
|
||||
set_value_pos: PositionType::Right,
|
||||
set_hexpand: true
|
||||
}
|
||||
},
|
||||
Separator {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
},
|
||||
GtkBox {
|
||||
set_orientation: Orientation::Vertical,
|
||||
Button {
|
||||
#[wrap(Some)]
|
||||
set_child: current_output = &Label {
|
||||
#[watch]
|
||||
set_text: model.get_default_output_name()
|
||||
},
|
||||
connect_clicked[sender, outputs_revealer] => move |_| {
|
||||
sender.input(AppInput::Outputs);
|
||||
outputs_revealer.set_reveal_child(!outputs_revealer.reveals_child());
|
||||
}
|
||||
},
|
||||
append: outputs_revealer = &Revealer {
|
||||
set_transition_type: RevealerTransitionType::SlideDown,
|
||||
#[wrap(Some)]
|
||||
set_child: outputs = &ListBox {
|
||||
set_selection_mode: gtk::SelectionMode::None,
|
||||
set_activate_on_single_click: true
|
||||
}
|
||||
}
|
||||
},
|
||||
Separator {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
},
|
||||
GtkBox {
|
||||
set_orientation: Orientation::Vertical,
|
||||
Button {
|
||||
#[wrap(Some)]
|
||||
set_child: current_input = &Label {
|
||||
#[watch]
|
||||
set_text: model.get_default_input_name()
|
||||
},
|
||||
connect_clicked[sender, inputs_revealer] => move |_| {
|
||||
sender.input(AppInput::Inputs);
|
||||
inputs_revealer.set_reveal_child(!inputs_revealer.reveals_child());
|
||||
}
|
||||
},
|
||||
append: inputs_revealer = &Revealer {
|
||||
set_transition_type: RevealerTransitionType::SlideDown,
|
||||
#[wrap(Some)]
|
||||
set_child: inputs = &ListBox {
|
||||
set_selection_mode: gtk::SelectionMode::None,
|
||||
set_activate_on_single_click: true
|
||||
}
|
||||
}
|
||||
},
|
||||
Separator {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
},
|
||||
append: playing_apps = &ListBox {
|
||||
set_selection_mode: gtk::SelectionMode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
_init_params: Self::InitParams,
|
||||
root: &Self::Root,
|
||||
sender: &ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let model = App::default();
|
||||
let widgets = view_output!();
|
||||
model.subscribe_for_updates(&sender.input);
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
msg: Self::Input,
|
||||
_sender: &ComponentSender<Self>,
|
||||
) {
|
||||
self.reset();
|
||||
match msg {
|
||||
AppInput::Outputs => {
|
||||
self.refresh_output_list();
|
||||
}
|
||||
AppInput::Inputs => {
|
||||
self.refresh_input_list();
|
||||
}
|
||||
AppInput::InputVolume => {
|
||||
self.refresh_default_input();
|
||||
}
|
||||
AppInput::OutputVolume => {
|
||||
self.refresh_default_output();
|
||||
}
|
||||
AppInput::NowPlaying => {
|
||||
self.refresh_now_playing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_view() {
|
||||
if model.changed(App::outputs()) {
|
||||
model.refresh_output_widgets(widgets);
|
||||
}
|
||||
if model.changed(App::inputs()) {
|
||||
model.refresh_input_widgets(widgets);
|
||||
}
|
||||
if model.changed(App::now_playing()) {
|
||||
model.refresh_now_playing_widgets(widgets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use freedesktop_desktop_entry::{default_paths, DesktopEntry, Iter};
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub enum DesktopApplication {
|
||||
Name(String),
|
||||
Binary(String),
|
||||
}
|
||||
|
||||
pub fn parse_desktop_icons() -> HashMap<DesktopApplication, String> {
|
||||
let mut out = HashMap::new();
|
||||
for path in Iter::new(default_paths()) {
|
||||
let file = match std::fs::read_to_string(&path) {
|
||||
Ok(data) => data,
|
||||
_ => continue,
|
||||
};
|
||||
let entry = match DesktopEntry::decode(&path, &file) {
|
||||
Ok(entry) => entry,
|
||||
_ => continue,
|
||||
};
|
||||
let icon = match entry.icon() {
|
||||
Some(icon) => icon,
|
||||
None => continue,
|
||||
};
|
||||
if let Some(name) = entry.name(None) {
|
||||
out.insert(DesktopApplication::Name(name.into_owned()), icon.to_owned());
|
||||
};
|
||||
if let Some(exec) = entry
|
||||
.exec()
|
||||
.and_then(|entry| entry.split_whitespace().next())
|
||||
{
|
||||
out.insert(DesktopApplication::Binary(exec.to_owned()), icon.to_owned());
|
||||
};
|
||||
}
|
||||
out
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
use gtk4::{glib::clone, prelude::*, Button, Label, ListBox};
|
||||
use libcosmic::widgets::{relm4::RelmContainerExt, LabeledItem};
|
||||
|
||||
use crate::pa::{DeviceInfo, PA};
|
||||
|
||||
pub async fn get_inputs(pa: &PA) -> Vec<DeviceInfo> {
|
||||
// XXX handle error
|
||||
pa.get_source_info_list()
|
||||
.await
|
||||
.expect("failed to list input devices")
|
||||
}
|
||||
|
||||
pub async fn refresh_default_input(pa: &PA, label: &Label) -> DeviceInfo {
|
||||
// XXX handle error
|
||||
let default_input = pa
|
||||
.get_default_source()
|
||||
.await
|
||||
.expect("failed to get default input");
|
||||
label.set_text(match &default_input.description {
|
||||
Some(name) => name.as_str(),
|
||||
None => "Input Device",
|
||||
});
|
||||
default_input
|
||||
}
|
||||
|
||||
pub async fn refresh_input_widgets(pa: &PA, inputs: &ListBox) {
|
||||
while let Some(row) = inputs.row_at_index(0) {
|
||||
inputs.remove(&row);
|
||||
}
|
||||
for input in get_inputs(pa).await {
|
||||
let name = match &input.name {
|
||||
Some(name) => name.to_owned(),
|
||||
None => continue, // Why doesn't this have a name? Whatever, it's invalid.
|
||||
};
|
||||
view! {
|
||||
item = LabeledItem {
|
||||
set_title: input.description
|
||||
.as_ref()
|
||||
.unwrap_or(&name),
|
||||
set_child: set_current_input_device = &Button {
|
||||
set_label: "Switch",
|
||||
connect_clicked: clone!(@strong pa => move |_| {
|
||||
pa.set_default_source(&name);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
inputs.container_add(&item);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,208 +1,372 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use iced::widget::Space;
|
||||
|
||||
#[macro_use]
|
||||
extern crate relm4_macros;
|
||||
use cosmic::widget::{icon, toggler, horizontal_rule};
|
||||
use cosmic::applet::CosmicAppletHelper;
|
||||
use cosmic::Renderer;
|
||||
|
||||
mod icons;
|
||||
mod input;
|
||||
mod now_playing;
|
||||
mod output;
|
||||
mod pa;
|
||||
use pa::PA;
|
||||
mod task;
|
||||
mod volume;
|
||||
mod volume_scale;
|
||||
use volume_scale::VolumeScale;
|
||||
|
||||
use futures::{channel::mpsc, stream::StreamExt};
|
||||
use gtk4::{
|
||||
gio::ApplicationFlags,
|
||||
glib::{self, clone, MainContext, PRIORITY_DEFAULT},
|
||||
prelude::*,
|
||||
Align, Application, ApplicationWindow, Box as GtkBox, Button, Image, Label, ListBox,
|
||||
Orientation, PositionType, Revealer, RevealerTransitionType, Scale, SelectionMode, Separator,
|
||||
use cosmic::iced_native::window::Settings;
|
||||
use cosmic::iced_style::application::{self, Appearance};
|
||||
use cosmic::iced_style::svg;
|
||||
use cosmic::theme::{self, Svg};
|
||||
use cosmic::{iced_style, settings, Element, Theme};
|
||||
use cosmic::iced::{
|
||||
executor,
|
||||
widget::{button, column, row, text, slider},
|
||||
window, Alignment, Application, Command, Length, Subscription,
|
||||
};
|
||||
use libpulse_binding::{
|
||||
context::{
|
||||
subscribe::{Facility, InterestMaskSet, Operation},
|
||||
FlagSet, State,
|
||||
},
|
||||
volume::Volume,
|
||||
};
|
||||
use mpris2_zbus::metadata::Metadata;
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("failed to build tokio runtime"));
|
||||
use iced_sctk::application::SurfaceIdWrapper;
|
||||
use iced_sctk::command::platform_specific::wayland::window::SctkWindowSettings;
|
||||
use iced_sctk::commands::popup::{destroy_popup, get_popup};
|
||||
use iced_sctk::settings::InitialSurface;
|
||||
use iced_sctk::Color;
|
||||
use iced_sctk::widget::container;
|
||||
|
||||
fn main() {
|
||||
let _monitors = libcosmic::init();
|
||||
let application = Application::new(None, ApplicationFlags::default());
|
||||
application.connect_activate(app);
|
||||
application.run();
|
||||
mod pulse;
|
||||
use crate::pulse::DeviceInfo;
|
||||
use libpulse_binding::volume::{Volume, VolumeLinear};
|
||||
|
||||
pub fn main() -> cosmic::iced::Result {
|
||||
let helper = CosmicAppletHelper::default();
|
||||
Audio::run(helper.window_settings())
|
||||
}
|
||||
|
||||
fn app(application: &Application) {
|
||||
// XXX handle no pulseaudio daemon?
|
||||
let pa = PA::new().unwrap();
|
||||
let (refresh_output_tx, mut refresh_output_rx) = mpsc::unbounded();
|
||||
let (refresh_input_tx, mut refresh_input_rx) = mpsc::unbounded();
|
||||
let (now_playing_tx, mut now_playing_rx) = mpsc::unbounded::<Vec<Metadata>>();
|
||||
pa
|
||||
.set_subscribe_callback(clone!(@strong refresh_output_tx, @strong refresh_input_tx => move |facility, operation, _idx| {
|
||||
if !matches!(operation, Some(Operation::Changed)) {
|
||||
return;
|
||||
}
|
||||
match facility {
|
||||
Some(Facility::Sink) => {
|
||||
refresh_output_tx.unbounded_send(()).expect("failed to send output refresh message");
|
||||
}
|
||||
Some(Facility::Source) => {
|
||||
refresh_input_tx.unbounded_send(()).expect("failed to send output refresh message");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}));
|
||||
pa.set_state_callback(move |pa, state| {
|
||||
if state == State::Ready {
|
||||
pa.subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE);
|
||||
refresh_output_tx
|
||||
.unbounded_send(())
|
||||
.expect("failed to send output refresh message");
|
||||
refresh_input_tx
|
||||
.unbounded_send(())
|
||||
.expect("failed to send output refresh message");
|
||||
}
|
||||
});
|
||||
pa.connect().unwrap(); // XXX unwrap
|
||||
view! {
|
||||
window = libcosmic_applet::AppletWindow {
|
||||
set_application: Some(application),
|
||||
set_title: Some("COSMIC Network Applet"),
|
||||
#[wrap(Some)]
|
||||
set_child: button = &libcosmic_applet::AppletButton {
|
||||
set_button_icon_name: "audio-volume-medium-symbolic",
|
||||
#[wrap(Some)]
|
||||
set_popover_child: window_box = &GtkBox {
|
||||
set_orientation: Orientation::Vertical,
|
||||
set_spacing: 24,
|
||||
append: output_box = &GtkBox {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
set_spacing: 16,
|
||||
append: output_icon = &Image {
|
||||
set_icon_name: Some("audio-speakers-symbolic"),
|
||||
},
|
||||
append: output_volume = &VolumeScale::new(pa.clone(), true) {
|
||||
set_format_value_func: |_, value| {
|
||||
format!("{:.0}%", value)
|
||||
},
|
||||
set_value_pos: PositionType::Right,
|
||||
set_hexpand: true
|
||||
}
|
||||
},
|
||||
append: input_box = &GtkBox {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
set_spacing: 16,
|
||||
append: input_icon = &Image {
|
||||
set_icon_name: Some("audio-input-microphone-symbolic"),
|
||||
},
|
||||
append: input_volume = &VolumeScale::new(pa.clone(), false) {
|
||||
set_format_value_func: |_, value| {
|
||||
format!("{:.0}%", value)
|
||||
},
|
||||
set_value_pos: PositionType::Right,
|
||||
set_hexpand: true
|
||||
}
|
||||
},
|
||||
append = &Separator {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
},
|
||||
append: output_list_box = &GtkBox {
|
||||
set_orientation: Orientation::Vertical,
|
||||
append: current_output_button = &Button {
|
||||
#[wrap(Some)]
|
||||
set_child: current_output = &Label {},
|
||||
connect_clicked[outputs_revealer] => move |_| {
|
||||
outputs_revealer.set_reveal_child(!outputs_revealer.reveals_child());
|
||||
}
|
||||
},
|
||||
append: outputs_revealer = &Revealer {
|
||||
set_transition_type: RevealerTransitionType::SlideDown,
|
||||
#[wrap(Some)]
|
||||
set_child: outputs = &ListBox {
|
||||
set_selection_mode: SelectionMode::None,
|
||||
set_activate_on_single_click: true
|
||||
}
|
||||
}
|
||||
},
|
||||
append = &Separator {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
},
|
||||
append: input_list_box = &GtkBox {
|
||||
set_orientation: Orientation::Vertical,
|
||||
append: current_input_button = &Button {
|
||||
#[wrap(Some)]
|
||||
set_child: current_input = &Label {},
|
||||
connect_clicked[inputs_revealer] => move |_| {
|
||||
inputs_revealer.set_reveal_child(!inputs_revealer.reveals_child());
|
||||
}
|
||||
},
|
||||
append: inputs_revealer = &Revealer {
|
||||
set_transition_type: RevealerTransitionType::SlideDown,
|
||||
#[wrap(Some)]
|
||||
set_child: inputs = &ListBox {
|
||||
set_selection_mode: SelectionMode::None,
|
||||
set_activate_on_single_click: true
|
||||
}
|
||||
}
|
||||
},
|
||||
append = &Separator {
|
||||
set_orientation: Orientation::Horizontal,
|
||||
},
|
||||
append: playing_apps = &ListBox {
|
||||
set_selection_mode: SelectionMode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct Audio {
|
||||
is_open: IsOpen,
|
||||
current_output: Option<DeviceInfo>,
|
||||
current_input: Option<DeviceInfo>,
|
||||
outputs: Vec<DeviceInfo>,
|
||||
inputs: Vec<DeviceInfo>,
|
||||
pulse_state: PulseState,
|
||||
applet_helper: CosmicAppletHelper,
|
||||
icon_name: String,
|
||||
theme: Theme,
|
||||
popup: Option<window::Id>,
|
||||
id_ctr: u32,
|
||||
}
|
||||
|
||||
glib::MainContext::default().spawn_local(
|
||||
clone!(@weak inputs, @weak current_input, @weak input_volume, @strong pa => async move {
|
||||
while let Some(()) = refresh_input_rx.next().await {
|
||||
input::refresh_input_widgets(&pa, &inputs).await;
|
||||
let default_input = input::refresh_default_input(&pa, ¤t_input).await;
|
||||
volume::update_volume(&default_input, &input_volume);
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum IsOpen {
|
||||
None,
|
||||
Output,
|
||||
Input,
|
||||
}
|
||||
}),
|
||||
);
|
||||
glib::MainContext::default().spawn_local(
|
||||
clone!(@weak outputs, @weak current_output, @weak output_volume, @strong pa, @strong button, => async move {
|
||||
while let Some(()) = refresh_output_rx.next().await {
|
||||
output::refresh_output_widgets(&pa, &outputs);
|
||||
let default_output = output::refresh_default_output(&pa, ¤t_output).await;
|
||||
volume::update_volume(&default_output, &output_volume);
|
||||
button.set_button_icon_name({
|
||||
let volume = default_output.volume.avg().0 as f64 / Volume::NORMAL.0 as f64;
|
||||
// XXX correct cutoffs?
|
||||
if default_output.mute {
|
||||
"audio-volume-muted"
|
||||
} else if volume > 1.0 {
|
||||
"audio-volume-overamplified-symbolic"
|
||||
} else if volume > 0.66 {
|
||||
"audio-volume-high-symbolic"
|
||||
} else if volume > 0.33 {
|
||||
"audio-volume-medium-symbolic"
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
SetOutputVolume(f64),
|
||||
SetInputVolume(f64),
|
||||
OutputToggle,
|
||||
InputToggle,
|
||||
OutputChanged(String),
|
||||
InputChanged(String),
|
||||
Pulse(pulse::Event),
|
||||
Ignore,
|
||||
TogglePopup,
|
||||
}
|
||||
|
||||
impl Application for Audio {
|
||||
type Message = Message;
|
||||
type Theme = Theme;
|
||||
type Executor = executor::Default;
|
||||
type Flags = ();
|
||||
|
||||
fn new(_flags: ()) -> (Audio, Command<Message>) {
|
||||
(
|
||||
Audio {
|
||||
is_open: IsOpen::None,
|
||||
current_output: None,
|
||||
current_input: None,
|
||||
outputs: vec![],
|
||||
inputs: vec![],
|
||||
pulse_state: PulseState::Disconnected,
|
||||
icon_name: "audio-volume-high-symbolic".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Audio")
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
}
|
||||
|
||||
fn close_requested(&self, _id: iced_sctk::application::SurfaceIdWrapper) -> Self::Message {
|
||||
Message::Ignore
|
||||
}
|
||||
|
||||
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
|
||||
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
|
||||
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
text_color: theme.cosmic().on_bg_color().into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::TogglePopup => {
|
||||
if let Some(p) = self.popup.take() {
|
||||
return destroy_popup(p);
|
||||
} else {
|
||||
"audio-volume-low-symbolic"
|
||||
self.id_ctr += 1;
|
||||
let new_id = window::Id::new(self.id_ctr);
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let popup_settings =
|
||||
self.applet_helper.get_popup_settings(window::Id::new(0), new_id, (400, 300), None, None);
|
||||
return get_popup(popup_settings);
|
||||
}
|
||||
}
|
||||
Message::SetOutputVolume(vol) => {
|
||||
self.current_output.as_mut().map(|o| {
|
||||
o.volume
|
||||
.set(o.volume.len(), VolumeLinear(vol / 100.0).into())
|
||||
});
|
||||
if let PulseState::Connected(connection) = &mut self.pulse_state {
|
||||
if let Some(device) = &self.current_output {
|
||||
if let Some(name) = &device.name {
|
||||
connection.send(pulse::Message::SetSinkVolumeByName(
|
||||
name.clone().to_string(),
|
||||
device.volume,
|
||||
))
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SetInputVolume(vol) => {
|
||||
self.current_input.as_mut().map(|i| {
|
||||
i.volume
|
||||
.set(i.volume.len(), VolumeLinear(vol / 100.0).into())
|
||||
});
|
||||
if let PulseState::Connected(connection) = &mut self.pulse_state {
|
||||
if let Some(device) = &self.current_input {
|
||||
if let Some(name) = &device.name {
|
||||
println!("increasing volume of {}", name);
|
||||
connection.send(pulse::Message::SetSourceVolumeByName(
|
||||
name.clone().to_string(),
|
||||
device.volume,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::OutputChanged(val) => println!("changed output {}", val),
|
||||
Message::InputChanged(val) => println!("changed input {}", val),
|
||||
Message::OutputToggle => {
|
||||
self.is_open = if self.is_open == IsOpen::Output {
|
||||
IsOpen::None
|
||||
} else {
|
||||
IsOpen::Output
|
||||
}
|
||||
}
|
||||
Message::InputToggle => {
|
||||
self.is_open = if self.is_open == IsOpen::Input {
|
||||
IsOpen::None
|
||||
} else {
|
||||
IsOpen::Input
|
||||
}
|
||||
}
|
||||
Message::Pulse(event) => match event {
|
||||
pulse::Event::Connected(mut connection) => {
|
||||
connection.send(pulse::Message::GetSinks);
|
||||
connection.send(pulse::Message::GetSources);
|
||||
connection.send(pulse::Message::GetDefaultSink);
|
||||
connection.send(pulse::Message::GetDefaultSource);
|
||||
self.pulse_state = PulseState::Connected(connection);
|
||||
}
|
||||
pulse::Event::MessageReceived(msg) => {
|
||||
match msg {
|
||||
// This is where we match messages from the subscription to app state
|
||||
pulse::Message::SetSinks(sinks) => self.outputs = sinks,
|
||||
pulse::Message::SetSources(sources) => {
|
||||
self.inputs = sources
|
||||
.into_iter()
|
||||
.filter(|source| {
|
||||
!source
|
||||
.name
|
||||
.as_ref()
|
||||
.unwrap_or(&String::from("Generic"))
|
||||
.contains("monitor")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
pulse::Message::SetDefaultSink(sink) => {
|
||||
self.current_output = Some(sink);
|
||||
}
|
||||
pulse::Message::SetDefaultSource(source) => {
|
||||
self.current_input = Some(source)
|
||||
}
|
||||
pulse::Message::Disconnected => {
|
||||
panic!("Subscriton error handling is bad. This should never happen.")
|
||||
}
|
||||
_ => {
|
||||
println!("Received misc message")
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: view() should gray out buttons/slider when state is disconnected
|
||||
pulse::Event::Disconnected => {
|
||||
println!("setting state to disconnected");
|
||||
self.pulse_state = PulseState::Disconnected
|
||||
}
|
||||
},
|
||||
Message::Ignore => {},
|
||||
};
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
pulse::connect().map(Message::Pulse)
|
||||
}
|
||||
|
||||
fn view(&self, id: SurfaceIdWrapper) -> Element<Message> {
|
||||
match id {
|
||||
SurfaceIdWrapper::LayerSurface(_) => unimplemented!(),
|
||||
SurfaceIdWrapper::Window(_) => self.applet_helper.icon_button(
|
||||
&self.icon_name,
|
||||
)
|
||||
.on_press(Message::TogglePopup)
|
||||
.into(),
|
||||
SurfaceIdWrapper::Popup(_) => {
|
||||
let out_f64 = VolumeLinear::from(
|
||||
self.current_output
|
||||
.as_ref()
|
||||
.map(|o| o.volume.avg())
|
||||
.unwrap_or(Volume::default()),
|
||||
)
|
||||
.0 * 100.0;
|
||||
let in_f64 = VolumeLinear::from(
|
||||
self.current_input
|
||||
.as_ref()
|
||||
.map(|o| o.volume.avg())
|
||||
.unwrap_or(Volume::default()),
|
||||
)
|
||||
.0 * 100.0;
|
||||
|
||||
let sink = row![
|
||||
icon("status/audio-volume-high-symbolic", 24),
|
||||
slider(0.0..=100.0, out_f64, Message::SetOutputVolume),
|
||||
text(format!("{}%", out_f64.round()))
|
||||
]
|
||||
.spacing(10)
|
||||
.padding(10);
|
||||
let source = row![
|
||||
icon("devices/audio-input-microphone-symbolic", 24),
|
||||
slider(0.0..=100.0, in_f64, Message::SetInputVolume),
|
||||
text(format!("{}%", in_f64.round()))
|
||||
]
|
||||
.spacing(10)
|
||||
.padding(10);
|
||||
|
||||
// TODO change these from helper functions to iced components for improved reusability
|
||||
let output_drop = revealer(
|
||||
self.is_open == IsOpen::Output,
|
||||
"Output",
|
||||
match &self.current_output {
|
||||
Some(output) => pretty_name(output.description.clone()),
|
||||
None => String::from("No device selected"),
|
||||
},
|
||||
self.outputs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|output| pretty_name(output.description))
|
||||
.collect(),
|
||||
Message::OutputToggle,
|
||||
Message::OutputChanged(String::from("test")),
|
||||
);
|
||||
glib::MainContext::default().spawn_local(clone!(@weak playing_apps => async move {
|
||||
while let Some(all_metadata) = now_playing_rx.next().await {
|
||||
let input_drop = revealer(
|
||||
self.is_open == IsOpen::Input,
|
||||
"Input",
|
||||
match &self.current_input {
|
||||
Some(input) => pretty_name(input.description.clone()),
|
||||
None => String::from("No device selected"),
|
||||
},
|
||||
self.inputs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|input| pretty_name(input.description))
|
||||
.collect(),
|
||||
Message::InputToggle,
|
||||
Message::InputChanged(String::from("test")),
|
||||
);
|
||||
|
||||
let content = column![]
|
||||
.align_items(Alignment::Start)
|
||||
.spacing(20)
|
||||
.push(sink)
|
||||
.push(source)
|
||||
.push(spacer())
|
||||
.push(output_drop)
|
||||
.push(input_drop);
|
||||
|
||||
self.applet_helper.popup_container(
|
||||
container(content)
|
||||
).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make this a themeable widget like the mock-ups
|
||||
fn spacer() -> iced::widget::Space {
|
||||
Space::with_width(Length::Fill)
|
||||
}
|
||||
|
||||
fn revealer<'a>(
|
||||
open: bool,
|
||||
title: &'a str,
|
||||
selected: String,
|
||||
options: Vec<String>,
|
||||
toggle: Message,
|
||||
_change: Message,
|
||||
) -> iced_sctk::widget::Column<'a, Message, Renderer> {
|
||||
if open {
|
||||
options.iter().fold(
|
||||
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|
||||
|col, device| col.push(text(device)),
|
||||
)
|
||||
} else {
|
||||
column![revealer_head(open, title, selected, toggle)]
|
||||
}
|
||||
}
|
||||
|
||||
fn revealer_head<'a>(
|
||||
_open: bool,
|
||||
title: &'a str,
|
||||
selected: String,
|
||||
toggle: Message,
|
||||
) -> iced_sctk::widget::Button<Message, Renderer> {
|
||||
button(row![row![title].width(Length::Fill), text(selected)])
|
||||
.width(Length::Fill)
|
||||
.on_press(toggle)
|
||||
}
|
||||
|
||||
fn pretty_name(name: Option<String>) -> String {
|
||||
match name {
|
||||
Some(n) => n,
|
||||
None => String::from("Generic"),
|
||||
}
|
||||
}
|
||||
|
||||
enum PulseState {
|
||||
Disconnected,
|
||||
Connected(pulse::Connection),
|
||||
}
|
||||
|
||||
impl Default for PulseState {
|
||||
fn default() -> Self {
|
||||
Self::Disconnected
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IsOpen {
|
||||
fn default() -> Self {
|
||||
IsOpen::None
|
||||
}
|
||||
}));
|
||||
window.show();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
use gtk4::{glib::Sender, prelude::*, Button, Image, Label, ListBox};
|
||||
use mpris2_zbus::{media_player::MediaPlayer, metadata::Metadata};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use zbus::Connection;
|
||||
|
||||
pub async fn metadata_update(tx: Sender<Vec<Metadata>>) {
|
||||
let connection = Connection::session()
|
||||
.await
|
||||
.expect("failed to connect to zbus");
|
||||
loop {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
let media_players = MediaPlayer::new_all(&connection)
|
||||
.await
|
||||
.expect("failed to get media players");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
use gtk4::{glib::clone, prelude::*, Button, Label, ListBox};
|
||||
use libcosmic::widgets::{relm4::RelmContainerExt, LabeledItem};
|
||||
|
||||
use crate::pa::{DeviceInfo, PA};
|
||||
|
||||
pub async fn get_outputs(pa: &PA) -> Vec<DeviceInfo> {
|
||||
// XXX handle error
|
||||
pa.get_sink_info_list()
|
||||
.await
|
||||
.expect("failed to list output devices")
|
||||
}
|
||||
|
||||
pub async fn refresh_default_output(pa: &PA, label: &Label) -> DeviceInfo {
|
||||
// XXX handle error
|
||||
let default_output = pa
|
||||
.get_default_sink()
|
||||
.await
|
||||
.expect("failed to get default output");
|
||||
label.set_text(match &default_output.description {
|
||||
Some(name) => name.as_str(),
|
||||
None => "Output Device",
|
||||
});
|
||||
default_output
|
||||
}
|
||||
|
||||
pub async fn refresh_output_widgets(pa: &PA, outputs: &ListBox) {
|
||||
while let Some(row) = outputs.row_at_index(0) {
|
||||
outputs.remove(&row);
|
||||
}
|
||||
for output in get_outputs(pa).await {
|
||||
let name = match &output.name {
|
||||
Some(name) => name.to_owned(),
|
||||
None => continue, // Why doesn't this have a name? Whatever, it's invalid.
|
||||
};
|
||||
view! {
|
||||
item = LabeledItem {
|
||||
set_title: output.description
|
||||
.as_ref()
|
||||
.unwrap_or(&name),
|
||||
set_child: set_current_input_device = &Button {
|
||||
set_label: "Switch",
|
||||
connect_clicked: clone!(@strong pa => move |_| {
|
||||
pa.set_default_sink(&name);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
outputs.container_add(&item);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
use libpulse_binding::operation::Operation;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
future::{self, Future},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
task::{self, Poll, Waker},
|
||||
};
|
||||
|
||||
struct PAFutInner<T> {
|
||||
res: Option<T>,
|
||||
waker: Option<Waker>,
|
||||
}
|
||||
|
||||
pub struct PAFutWaker<T>(Rc<RefCell<PAFutInner<T>>>);
|
||||
|
||||
impl<T> PAFutWaker<T> {
|
||||
pub fn wake(&self, res: T) {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
inner.res = Some(res);
|
||||
if let Some(waker) = inner.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PAFut<T, F: ?Sized> {
|
||||
inner: Rc<RefCell<PAFutInner<T>>>,
|
||||
operation: Operation<F>,
|
||||
}
|
||||
|
||||
impl<T, F: ?Sized> PAFut<T, F> {
|
||||
pub fn new(cb: impl FnOnce(PAFutWaker<T>) -> Operation<F>) -> Self {
|
||||
let inner = Rc::new(RefCell::new(PAFutInner {
|
||||
res: None,
|
||||
waker: None,
|
||||
}));
|
||||
let operation = cb(PAFutWaker(inner.clone()));
|
||||
Self { inner, operation }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, F: ?Sized> Future for PAFut<T, F> {
|
||||
type Output = T;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> Poll<Self::Output> {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if let Some(res) = inner.res.take() {
|
||||
Poll::Ready(res)
|
||||
} else {
|
||||
inner.waker = Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, F: ?Sized> Drop for PAFut<T, F> {
|
||||
fn drop(&mut self) {
|
||||
self.operation.cancel();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
use gtk4::glib;
|
||||
use libpulse_binding::{
|
||||
callbacks::ListResult,
|
||||
context::{
|
||||
introspect::{Introspector, SinkInfo, SourceInfo},
|
||||
subscribe::{Facility, InterestMaskSet, Operation},
|
||||
Context, FlagSet, State,
|
||||
},
|
||||
error::PAErr,
|
||||
volume::ChannelVolumes,
|
||||
};
|
||||
use libpulse_glib_binding::Mainloop;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
mod future;
|
||||
use future::{PAFut, PAFutWaker};
|
||||
|
||||
pub struct DeviceInfo {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub volume: ChannelVolumes,
|
||||
pub mute: bool,
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
impl<'a> From<&SinkInfo<'a>> for DeviceInfo {
|
||||
fn from(info: &SinkInfo<'a>) -> Self {
|
||||
Self {
|
||||
name: info.name.clone().map(|x| x.into_owned()),
|
||||
description: info.description.clone().map(|x| x.into_owned()),
|
||||
volume: info.volume,
|
||||
mute: info.mute,
|
||||
index: info.index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&SourceInfo<'a>> for DeviceInfo {
|
||||
fn from(info: &SourceInfo<'a>) -> Self {
|
||||
Self {
|
||||
name: info.name.clone().map(|x| x.into_owned()),
|
||||
description: info.description.clone().map(|x| x.into_owned()),
|
||||
volume: info.volume,
|
||||
mute: info.mute,
|
||||
index: info.index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerInfo {
|
||||
pub default_sink_name: Option<String>,
|
||||
pub default_source_name: Option<String>,
|
||||
}
|
||||
|
||||
struct PAInner {
|
||||
main_loop: Mainloop,
|
||||
pub context: RefCell<Context>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PA(Rc<PAInner>);
|
||||
|
||||
impl PA {
|
||||
pub fn new() -> Option<Self> {
|
||||
let main_loop = Mainloop::new(None)?;
|
||||
let context = Context::new(&main_loop, "com.system76.cosmic.applets.audio")?;
|
||||
Some(Self(Rc::new(PAInner {
|
||||
main_loop,
|
||||
context: RefCell::new(context),
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn set_state_callback<F: Fn(&Self, State) + 'static>(&self, cb: F) {
|
||||
let pa = self.clone(); // TODO: weak ref?
|
||||
let cb = Rc::new(cb);
|
||||
self.0
|
||||
.context
|
||||
.borrow_mut()
|
||||
.set_state_callback(Some(Box::new(move || {
|
||||
let pa = pa.clone();
|
||||
let cb = cb.clone();
|
||||
glib::source::idle_add_local_once(move || {
|
||||
let state = pa.0.context.borrow().get_state();
|
||||
cb(&pa, state);
|
||||
});
|
||||
})));
|
||||
}
|
||||
|
||||
// TODO: builder pattern?
|
||||
pub fn set_subscribe_callback<F: FnMut(Option<Facility>, Option<Operation>, u32) + 'static>(
|
||||
&self,
|
||||
cb: F,
|
||||
) {
|
||||
self.0
|
||||
.context
|
||||
.borrow_mut()
|
||||
.set_subscribe_callback(Some(Box::new(cb)));
|
||||
}
|
||||
|
||||
pub fn subscribe(&self, mask: InterestMaskSet) {
|
||||
// XXX cb; operation; async
|
||||
self.0.context.borrow_mut().subscribe(mask, |_| {});
|
||||
}
|
||||
|
||||
pub fn connect(&self) -> Result<(), PAErr> {
|
||||
self.0
|
||||
.context
|
||||
.borrow_mut()
|
||||
.connect(None, FlagSet::empty(), None)
|
||||
}
|
||||
|
||||
fn introspect(&self) -> Introspector {
|
||||
self.0.context.borrow().introspect()
|
||||
}
|
||||
|
||||
pub async fn get_server_info(&self) -> ServerInfo {
|
||||
PAFut::new(|waker| {
|
||||
self.introspect().get_server_info(move |info| {
|
||||
waker.wake(ServerInfo {
|
||||
default_sink_name: info.default_sink_name.clone().map(|x| x.into_owned()),
|
||||
default_source_name: info.default_source_name.clone().map(|x| x.into_owned()),
|
||||
});
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_sink_info_list(&self) -> Result<Vec<DeviceInfo>, ()> {
|
||||
let mut items = Some(Vec::new());
|
||||
PAFut::new(|waker| {
|
||||
self.introspect()
|
||||
.get_sink_info_list(move |result| match result {
|
||||
ListResult::Item(item) => items.as_mut().unwrap().push(DeviceInfo::from(item)),
|
||||
ListResult::End => waker.wake(Ok(items.take().unwrap())),
|
||||
ListResult::Error => waker.wake(Err(())),
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_default_sink(&self) -> Result<DeviceInfo, ()> {
|
||||
let name = match self.get_server_info().await.default_sink_name {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
let mut sink = None;
|
||||
PAFut::new(|waker| {
|
||||
self.introspect()
|
||||
.get_sink_info_by_name(&name, move |result| match result {
|
||||
ListResult::Item(item) => {
|
||||
sink = Some(DeviceInfo::from(item));
|
||||
}
|
||||
ListResult::End => waker.wake(sink.take().ok_or(())),
|
||||
ListResult::Error => waker.wake(Err(())),
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
// XXX async wait and handle error
|
||||
pub fn set_default_sink(&self, name: &str) {
|
||||
self.0.context.borrow_mut().set_default_sink(name, |_| {});
|
||||
}
|
||||
|
||||
pub fn set_default_source(&self, name: &str) {
|
||||
self.0.context.borrow_mut().set_default_source(name, |_| {});
|
||||
}
|
||||
|
||||
pub async fn get_source_info_list(&self) -> Result<Vec<DeviceInfo>, ()> {
|
||||
let mut items = Some(Vec::new());
|
||||
PAFut::new(|waker| {
|
||||
self.introspect()
|
||||
.get_source_info_list(move |result| match result {
|
||||
ListResult::Item(item) => items.as_mut().unwrap().push(DeviceInfo::from(item)),
|
||||
ListResult::End => waker.wake(Ok(items.take().unwrap())),
|
||||
ListResult::Error => waker.wake(Err(())),
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_default_source(&self) -> Result<DeviceInfo, ()> {
|
||||
let name = match self.get_server_info().await.default_source_name {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
let mut source = None;
|
||||
PAFut::new(|waker| {
|
||||
self.introspect()
|
||||
.get_source_info_by_name(&name, move |result| match result {
|
||||
ListResult::Item(item) => {
|
||||
source = Some(DeviceInfo::from(item));
|
||||
}
|
||||
ListResult::End => waker.wake(source.take().ok_or(())),
|
||||
ListResult::Error => waker.wake(Err(())),
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn set_sink_volume_by_name(
|
||||
&self,
|
||||
name: &str,
|
||||
volume: &ChannelVolumes,
|
||||
) -> PAFut<bool, impl ?Sized> {
|
||||
PAFut::new(|waker| {
|
||||
self.introspect().set_sink_volume_by_name(
|
||||
name,
|
||||
volume,
|
||||
Some(Box::new(move |success| waker.wake(success))),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_source_volume_by_name(
|
||||
&self,
|
||||
name: &str,
|
||||
volume: &ChannelVolumes,
|
||||
) -> PAFut<bool, impl ?Sized> {
|
||||
PAFut::new(|waker| {
|
||||
self.introspect().set_source_volume_by_name(
|
||||
name,
|
||||
volume,
|
||||
Some(Box::new(move |success| waker.wake(success))),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
521
applets/cosmic-applet-audio/src/pulse.rs
Normal file
521
applets/cosmic-applet-audio/src/pulse.rs
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
use iced_native::subscription::{self, Subscription};
|
||||
use std::cell::RefCell;
|
||||
use std::{rc::Rc, thread};
|
||||
|
||||
extern crate libpulse_binding as pulse;
|
||||
//use futures::channel::mpsc;
|
||||
use libpulse_binding::{
|
||||
callbacks::ListResult,
|
||||
context::{
|
||||
introspect::{Introspector, SinkInfo, SourceInfo},
|
||||
subscribe::{Facility, InterestMaskSet, Operation},
|
||||
Context,
|
||||
},
|
||||
error::PAErr,
|
||||
mainloop::standard::{IterateResult, Mainloop},
|
||||
proplist::Proplist,
|
||||
volume::ChannelVolumes,
|
||||
};
|
||||
pub fn connect() -> Subscription<Event> {
|
||||
struct Connect;
|
||||
|
||||
subscription::unfold(
|
||||
std::any::TypeId::of::<Connect>(),
|
||||
State::Disconnected,
|
||||
|state| async move {
|
||||
match state {
|
||||
// if app just started, or we are re-trying match here. Returns coenncting
|
||||
// message. We should store this in our app's state, but it isn't safe to
|
||||
// send messages until we get a conencted message. Which will be received
|
||||
// by the `State::Connecting` message below
|
||||
State::Disconnected => match PulseHandle::create() {
|
||||
Ok(pulse_handle) => (None, State::Connecting(pulse_handle)),
|
||||
Err(_) => (Some(Event::Disconnected), State::Disconnected),
|
||||
},
|
||||
// Just a buffer to make sure the GUI doesn't send messages until pulse is ready
|
||||
// The GUI doesn't have to monitor this state, as it is never sent to the GUI
|
||||
State::Connecting(mut pulse_handle) => {
|
||||
match pulse_handle.from_pulse.recv().await {
|
||||
Some(Message::Connected) => {(
|
||||
Some(Event::Connected(Connection(pulse_handle.to_pulse))),
|
||||
State::Connected(pulse_handle.from_pulse),
|
||||
)}
|
||||
Some(Message::Disconnected) => (Some(Event::Disconnected), State::Disconnected),
|
||||
_ => panic!("Pulse subscription logic is faulty as the PulseServer shouldn't send unique messages until connection is successful")
|
||||
}
|
||||
},
|
||||
State::Connected(mut from_pulse) => {
|
||||
// This is where we match messages from the pulse server to pass to the gui
|
||||
match from_pulse.recv().await {
|
||||
Some(Message::SetSinks(sinks)) => (Some(Event::MessageReceived(Message::SetSinks(sinks))), State::Connected(from_pulse)),
|
||||
Some(Message::SetSources(sources)) => (Some(Event::MessageReceived(Message::SetSources(sources))), State::Connected(from_pulse)),
|
||||
Some(Message::SetDefaultSink(sink)) => (Some(Event::MessageReceived(Message::SetDefaultSink(sink))), State::Connected(from_pulse)),
|
||||
Some(Message::SetDefaultSource(source)) => (Some(Event::MessageReceived(Message::SetDefaultSource(source))), State::Connected(from_pulse)),
|
||||
Some(Message::Disconnected) => (Some(Event::Disconnected), State::Disconnected),
|
||||
None => (Some(Event::Disconnected), State::Disconnected),
|
||||
_ => (None, State::Connected(from_pulse)),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// #[derive(Debug)]
|
||||
enum State {
|
||||
Disconnected,
|
||||
Connecting(PulseHandle),
|
||||
Connected(tokio::sync::mpsc::Receiver<Message>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
Connected(Connection),
|
||||
Disconnected,
|
||||
MessageReceived(Message),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Connection(tokio::sync::mpsc::Sender<Message>);
|
||||
|
||||
impl Connection {
|
||||
pub fn send(&mut self, message: Message) {
|
||||
let _ = self
|
||||
.0
|
||||
.try_send(message)
|
||||
.expect("Send message to PulseAudio server");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Message {
|
||||
Connected,
|
||||
Disconnected,
|
||||
GetSinks,
|
||||
GetSources,
|
||||
SetSinks(Vec<DeviceInfo>),
|
||||
SetSources(Vec<DeviceInfo>),
|
||||
GetDefaultSink,
|
||||
GetDefaultSource,
|
||||
SetDefaultSink(DeviceInfo),
|
||||
SetDefaultSource(DeviceInfo),
|
||||
SetSinkVolumeByName(String, ChannelVolumes),
|
||||
SetSourceVolumeByName(String, ChannelVolumes),
|
||||
}
|
||||
|
||||
struct PulseHandle {
|
||||
to_pulse: tokio::sync::mpsc::Sender<Message>,
|
||||
from_pulse: tokio::sync::mpsc::Receiver<Message>,
|
||||
}
|
||||
|
||||
impl PulseHandle {
|
||||
// Create pulse server thread, and bidirectional comms
|
||||
pub fn create() -> Result<PulseHandle, PAErr> {
|
||||
let (to_pulse, mut to_pulse_recv) = tokio::sync::mpsc::channel(10);
|
||||
let (mut from_pulse_send, from_pulse) = tokio::sync::mpsc::channel(10);
|
||||
//let from_pulse = Arc::new(Mutex::new(vec![]));
|
||||
//let mut from_pulse2 = from_pulse.clone();
|
||||
// this thread should complete by pushing a completed message,
|
||||
// or fail message. This should never complete/fail without pushing
|
||||
// a message. This lets the iced subscription go to sleep while init
|
||||
// finishes. TLDR: be very careful with error handling
|
||||
thread::spawn(move || {
|
||||
if let Ok(mut server) = PulseServer::connect().and_then(|server| server.init()) {
|
||||
PulseHandle::blocking_send_connected(&mut from_pulse_send);
|
||||
|
||||
// take `PulseServer` and handle reciver into async context
|
||||
// to listen for messages that need to be passed to the pulseserver
|
||||
// this lets us put the thread to sleep, but keep hold a single
|
||||
// thread, because pulse audio's API is not multithreaded... at all
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
loop {
|
||||
// This is where the we match messages from the GUI to pass to the pulse server
|
||||
if let Some(msg) = to_pulse_recv.recv().await {
|
||||
match msg {
|
||||
Message::GetDefaultSink => match server.get_default_sink() {
|
||||
Ok(sink) => from_pulse_send
|
||||
.send(Message::SetDefaultSink(sink))
|
||||
.await
|
||||
.unwrap(),
|
||||
Err(_) => {
|
||||
PulseHandle::send_disconnected(&mut from_pulse_send).await
|
||||
}
|
||||
},
|
||||
Message::GetDefaultSource => match server.get_default_source() {
|
||||
Ok(source) => from_pulse_send
|
||||
.send(Message::SetDefaultSource(source))
|
||||
.await
|
||||
.unwrap(),
|
||||
Err(e) => {
|
||||
println!("ERROR! {:?}", e);
|
||||
PulseHandle::send_disconnected(&mut from_pulse_send).await;
|
||||
}
|
||||
},
|
||||
Message::GetSinks => match server.get_sinks() {
|
||||
Ok(sinks) => from_pulse_send
|
||||
.send(Message::SetSinks(sinks))
|
||||
.await
|
||||
.unwrap(),
|
||||
Err(_) => {
|
||||
PulseHandle::send_disconnected(&mut from_pulse_send).await
|
||||
}
|
||||
},
|
||||
Message::GetSources => match server.get_sources() {
|
||||
Ok(sinks) => from_pulse_send
|
||||
.send(Message::SetSources(sinks))
|
||||
.await
|
||||
.unwrap(),
|
||||
Err(_) => {
|
||||
PulseHandle::send_disconnected(&mut from_pulse_send).await
|
||||
}
|
||||
},
|
||||
Message::SetSinkVolumeByName(name, channel_volumes) => {
|
||||
server.set_sink_volume_by_name(&name, &channel_volumes)
|
||||
}
|
||||
Message::SetSourceVolumeByName(name, channel_volumes) => {
|
||||
server.set_source_volume_by_name(&name, &channel_volumes)
|
||||
}
|
||||
_ => {
|
||||
println!("message doesn't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Always report that server is disconnected
|
||||
PulseHandle::blocking_send_disconnected(&mut from_pulse_send);
|
||||
});
|
||||
Ok(PulseHandle {
|
||||
to_pulse,
|
||||
from_pulse,
|
||||
})
|
||||
}
|
||||
|
||||
fn blocking_send_disconnected(sender: &mut tokio::sync::mpsc::Sender<Message>) {
|
||||
sender.blocking_send(Message::Disconnected);
|
||||
}
|
||||
|
||||
fn blocking_send_connected(sender: &mut tokio::sync::mpsc::Sender<Message>) {
|
||||
sender.blocking_send(Message::Connected).unwrap()
|
||||
}
|
||||
|
||||
async fn send_disconnected(sender: &mut tokio::sync::mpsc::Sender<Message>) {
|
||||
sender.send(Message::Disconnected).await.unwrap()
|
||||
}
|
||||
|
||||
async fn send_connected(sender: &mut tokio::sync::mpsc::Sender<Message>) {
|
||||
sender.send(Message::Connected).await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
struct PulseServer {
|
||||
mainloop: Rc<RefCell<Mainloop>>,
|
||||
context: Rc<RefCell<Context>>,
|
||||
introspector: Introspector,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum PulseServerError<'a> {
|
||||
IterateErr(IterateResult),
|
||||
ContextErr(pulse::context::State),
|
||||
OperationErr(pulse::operation::State),
|
||||
PAErr(PAErr),
|
||||
Connect,
|
||||
Misc(&'a str),
|
||||
}
|
||||
|
||||
// `PulseServer` code is heavily inspired by Dave Patrick Caberto's pulsectl-rs (SeaDve)
|
||||
// https://crates.io/crates/pulsectl-rs
|
||||
impl PulseServer {
|
||||
// connect() requires init() to be run after
|
||||
pub fn connect() -> Result<PulseServer, PulseServerError<'static>> {
|
||||
// TODO: fix app name, should be variable
|
||||
let mut proplist = Proplist::new().unwrap();
|
||||
proplist
|
||||
.set_str(
|
||||
pulse::proplist::properties::APPLICATION_NAME,
|
||||
"com.system76",
|
||||
)
|
||||
.or(Err(PulseServerError::Connect))?;
|
||||
|
||||
let mainloop = Rc::new(RefCell::new(
|
||||
pulse::mainloop::standard::Mainloop::new().ok_or(PulseServerError::Connect)?,
|
||||
));
|
||||
|
||||
let context = Rc::new(RefCell::new(
|
||||
Context::new_with_proplist(&*mainloop.borrow(), "MainConn", &proplist)
|
||||
.ok_or(PulseServerError::Connect)?,
|
||||
));
|
||||
|
||||
let introspector = context.borrow_mut().introspect();
|
||||
|
||||
context
|
||||
.borrow_mut()
|
||||
.connect(None, pulse::context::FlagSet::NOFLAGS, None)
|
||||
.map_err(|e| PulseServerError::PAErr(e))?;
|
||||
|
||||
Ok(PulseServer {
|
||||
mainloop,
|
||||
context,
|
||||
introspector,
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for pulse audio connection to complete
|
||||
pub fn init(self) -> Result<Self, PulseServerError<'static>> {
|
||||
loop {
|
||||
match self.mainloop.borrow_mut().iterate(false) {
|
||||
IterateResult::Success(_) => {}
|
||||
IterateResult::Err(e) => {
|
||||
return Err(PulseServerError::IterateErr(IterateResult::Err(e)))
|
||||
}
|
||||
IterateResult::Quit(e) => {
|
||||
return Err(PulseServerError::IterateErr(IterateResult::Quit(e)))
|
||||
}
|
||||
}
|
||||
|
||||
match self.context.borrow().get_state() {
|
||||
pulse::context::State::Ready => break,
|
||||
pulse::context::State::Failed => {
|
||||
return Err(PulseServerError::ContextErr(pulse::context::State::Failed))
|
||||
}
|
||||
pulse::context::State::Terminated => {
|
||||
return Err(PulseServerError::ContextErr(
|
||||
pulse::context::State::Terminated,
|
||||
))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
// Get a list of output devices
|
||||
pub fn get_sinks(&self) -> Result<Vec<DeviceInfo>, PulseServerError> {
|
||||
let list: Rc<RefCell<Option<Vec<DeviceInfo>>>> = Rc::new(RefCell::new(Some(Vec::new())));
|
||||
let list_ref = list.clone();
|
||||
|
||||
let operation = self.introspector.get_sink_info_list(
|
||||
move |sink_list: ListResult<&pulse::context::introspect::SinkInfo>| {
|
||||
if let ListResult::Item(item) = sink_list {
|
||||
list_ref.borrow_mut().as_mut().unwrap().push(item.into());
|
||||
}
|
||||
},
|
||||
);
|
||||
self.wait_for_result(operation)
|
||||
.and_then(|_| {
|
||||
list.borrow_mut().take().ok_or(PulseServerError::Misc(
|
||||
"get_sinks(): failed to wait for operation",
|
||||
))
|
||||
})
|
||||
.and_then(|result| Ok(result))
|
||||
}
|
||||
|
||||
// Get a list of input devices
|
||||
pub fn get_sources(&self) -> Result<Vec<DeviceInfo>, PulseServerError> {
|
||||
let list: Rc<RefCell<Option<Vec<DeviceInfo>>>> = Rc::new(RefCell::new(Some(Vec::new())));
|
||||
let list_ref = list.clone();
|
||||
|
||||
let operation = self.introspector.get_source_info_list(
|
||||
move |sink_list: ListResult<&pulse::context::introspect::SourceInfo>| {
|
||||
if let ListResult::Item(item) = sink_list {
|
||||
list_ref.borrow_mut().as_mut().unwrap().push(item.into());
|
||||
}
|
||||
},
|
||||
);
|
||||
self.wait_for_result(operation)
|
||||
.and_then(|_| {
|
||||
list.borrow_mut().take().ok_or(PulseServerError::Misc(
|
||||
"get_sources(): Failed to wait for operation",
|
||||
))
|
||||
})
|
||||
.and_then(|result| Ok(result))
|
||||
}
|
||||
|
||||
pub fn get_server_info(&mut self) -> Result<ServerInfo, PulseServerError> {
|
||||
let info = Rc::new(RefCell::new(Some(None)));
|
||||
let info_ref = info.clone();
|
||||
|
||||
let op = self.introspector.get_server_info(move |res| {
|
||||
info_ref.borrow_mut().as_mut().unwrap().replace(res.into());
|
||||
});
|
||||
self.wait_for_result(op)?;
|
||||
info.take()
|
||||
.flatten()
|
||||
.ok_or(PulseServerError::Misc("get_server_info(): failed"))
|
||||
}
|
||||
|
||||
fn get_default_sink(&mut self) -> Result<DeviceInfo, PulseServerError> {
|
||||
let server_info = self.get_server_info();
|
||||
match server_info {
|
||||
Ok(info) => {
|
||||
let name = &info.default_sink_name.unwrap_or(String::new());
|
||||
let device = Rc::new(RefCell::new(Some(None)));
|
||||
let dev_ref = device.clone();
|
||||
let op = self.introspector.get_sink_info_by_name(
|
||||
name,
|
||||
move |sink_list: ListResult<&SinkInfo>| {
|
||||
if let ListResult::Item(item) = sink_list {
|
||||
dev_ref.borrow_mut().as_mut().unwrap().replace(item.into());
|
||||
}
|
||||
},
|
||||
);
|
||||
self.wait_for_result(op)?;
|
||||
let mut result = device.borrow_mut();
|
||||
result.take().unwrap().ok_or_else(|| {
|
||||
PulseServerError::Misc("get_default_sink(): Error getting requested device")
|
||||
})
|
||||
}
|
||||
Err(_) => Err(PulseServerError::Misc("get_default_sink() failed")),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_default_source(&mut self) -> Result<DeviceInfo, PulseServerError> {
|
||||
let server_info = self.get_server_info();
|
||||
match server_info {
|
||||
Ok(info) => {
|
||||
let name = &info.default_source_name.unwrap_or(String::new());
|
||||
let device = Rc::new(RefCell::new(Some(None)));
|
||||
let dev_ref = device.clone();
|
||||
let op = self.introspector.get_source_info_by_name(
|
||||
name,
|
||||
move |sink_list: ListResult<&SourceInfo>| {
|
||||
if let ListResult::Item(item) = sink_list {
|
||||
dev_ref.borrow_mut().as_mut().unwrap().replace(item.into());
|
||||
}
|
||||
},
|
||||
);
|
||||
self.wait_for_result(op)?;
|
||||
let mut result = device.borrow_mut();
|
||||
result.take().unwrap().ok_or_else(|| {
|
||||
PulseServerError::Misc("get_default_source(): Error getting requested device")
|
||||
})
|
||||
}
|
||||
Err(_) => Err(PulseServerError::Misc("get_default_source() failed")),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sink_volume_by_name(&mut self, name: &str, volume: &ChannelVolumes) {
|
||||
let op = self
|
||||
.introspector
|
||||
.set_sink_volume_by_name(name, volume, None);
|
||||
self.wait_for_result(op).ok();
|
||||
}
|
||||
|
||||
fn set_source_volume_by_name(&mut self, name: &str, volume: &ChannelVolumes) {
|
||||
let op = self
|
||||
.introspector
|
||||
.set_source_volume_by_name(name, volume, None);
|
||||
self.wait_for_result(op).ok();
|
||||
}
|
||||
|
||||
// after building an operation such as get_devices() we need to keep polling
|
||||
// the pulse audio server to "wait" for the operation to complete
|
||||
fn wait_for_result<G: ?Sized>(
|
||||
&self,
|
||||
operation: pulse::operation::Operation<G>,
|
||||
) -> Result<(), PulseServerError> {
|
||||
// TODO: make this loop async. It is already in an async context, so
|
||||
// we could make this thread sleep while waiting for the pulse server's
|
||||
// response.
|
||||
loop {
|
||||
match self.mainloop.borrow_mut().iterate(false) {
|
||||
IterateResult::Err(e) => {
|
||||
return Err(PulseServerError::IterateErr(IterateResult::Err(e)))
|
||||
}
|
||||
IterateResult::Quit(e) => {
|
||||
return Err(PulseServerError::IterateErr(IterateResult::Quit(e)))
|
||||
}
|
||||
IterateResult::Success(_) => {}
|
||||
}
|
||||
match operation.get_state() {
|
||||
pulse::operation::State::Done => return Ok(()),
|
||||
pulse::operation::State::Running => {}
|
||||
pulse::operation::State::Cancelled => {
|
||||
return Err(PulseServerError::OperationErr(
|
||||
pulse::operation::State::Cancelled,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DeviceInfo {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub volume: ChannelVolumes,
|
||||
pub mute: bool,
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
impl<'a> From<&SinkInfo<'a>> for DeviceInfo {
|
||||
fn from(info: &SinkInfo<'a>) -> Self {
|
||||
Self {
|
||||
name: info.name.clone().map(|x| x.into_owned()),
|
||||
description: info.description.clone().map(|x| x.into_owned()),
|
||||
volume: info.volume,
|
||||
mute: info.mute,
|
||||
index: info.index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&SourceInfo<'a>> for DeviceInfo {
|
||||
fn from(info: &SourceInfo<'a>) -> Self {
|
||||
Self {
|
||||
name: info.name.clone().map(|x| x.into_owned()),
|
||||
description: info.description.clone().map(|x| x.into_owned()),
|
||||
volume: info.volume,
|
||||
mute: info.mute,
|
||||
index: info.index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for DeviceInfo {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServerInfo {
|
||||
/// User name of the daemon process.
|
||||
pub user_name: Option<String>,
|
||||
/// Host name the daemon is running on.
|
||||
pub host_name: Option<String>,
|
||||
/// Version string of the daemon.
|
||||
pub server_version: Option<String>,
|
||||
/// Server package name (usually “pulseaudio”).
|
||||
pub server_name: Option<String>,
|
||||
// Default sample specification.
|
||||
//pub sample_spec: sample::Spec,
|
||||
/// Name of default sink.
|
||||
pub default_sink_name: Option<String>,
|
||||
/// Name of default source.
|
||||
pub default_source_name: Option<String>,
|
||||
/// A random cookie for identifying this instance of PulseAudio.
|
||||
pub cookie: u32,
|
||||
// Default channel map.
|
||||
//pub channel_map: channelmap::Map,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a pulse::context::introspect::ServerInfo<'a>> for ServerInfo {
|
||||
fn from(info: &'a pulse::context::introspect::ServerInfo<'a>) -> Self {
|
||||
ServerInfo {
|
||||
user_name: info.user_name.as_ref().map(|cow| cow.to_string()),
|
||||
host_name: info.host_name.as_ref().map(|cow| cow.to_string()),
|
||||
server_version: info.server_version.as_ref().map(|cow| cow.to_string()),
|
||||
server_name: info.server_name.as_ref().map(|cow| cow.to_string()),
|
||||
//sample_spec: info.sample_spec,
|
||||
default_sink_name: info.default_sink_name.as_ref().map(|cow| cow.to_string()),
|
||||
default_source_name: info.default_source_name.as_ref().map(|cow| cow.to_string()),
|
||||
cookie: info.cookie,
|
||||
//channel_map: info.channel_map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use std::future::Future;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub fn spawn<O, F>(future: F) -> tokio::task::JoinHandle<O>
|
||||
where
|
||||
F: Future<Output = O> + Send + 'static,
|
||||
O: Send + 'static,
|
||||
{
|
||||
crate::RT.spawn(future)
|
||||
}
|
||||
|
||||
pub fn block_on<O, F>(future: F) -> O
|
||||
where
|
||||
F: Future<Output = O> + Send + 'static,
|
||||
O: Send + 'static,
|
||||
{
|
||||
crate::RT.block_on(future)
|
||||
}
|
||||
|
||||
pub fn spawn_local<F: Future<Output = ()> + 'static>(future: F) {
|
||||
gtk4::glib::MainContext::default().spawn_local(future);
|
||||
}
|
||||
|
||||
pub async fn wait_for_local<O, F>(future: F) -> Option<O>
|
||||
where
|
||||
O: Send + 'static,
|
||||
F: Future<Output = O> + Send + 'static,
|
||||
{
|
||||
let (tx, rx) = oneshot::channel::<O>();
|
||||
gtk4::glib::MainContext::default().spawn_local(async move {
|
||||
std::mem::drop(tx.send(future.await));
|
||||
});
|
||||
rx.await.ok()
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
use gtk4::prelude::*;
|
||||
use libpulse_binding::volume::Volume;
|
||||
|
||||
use crate::{pa::DeviceInfo, volume_scale::VolumeScale};
|
||||
|
||||
pub fn update_volume(device: &DeviceInfo, scale: &VolumeScale) {
|
||||
scale.set_name(device.name.clone());
|
||||
scale.set_volume(&device.volume);
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
// TODO: Use `Volume::ui_max()`?
|
||||
// * Make sure volumes greater than this are handled properly.
|
||||
|
||||
use gtk4::{glib, prelude::*, subclass::prelude::*};
|
||||
use libpulse_binding::volume::{ChannelVolumes, Volume};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::PA;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct VolumeScaleImp {
|
||||
name: Rc<RefCell<Option<String>>>,
|
||||
in_drag: Cell<bool>,
|
||||
volume_to_set: Cell<Option<f64>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for VolumeScaleImp {
|
||||
const NAME: &'static str = "VolumeScale";
|
||||
type Type = VolumeScale;
|
||||
type ParentType = gtk4::Scale;
|
||||
}
|
||||
|
||||
impl ObjectImpl for VolumeScaleImp {
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
obj.set_range(0., 100.);
|
||||
|
||||
let gesture_drag = gtk4::GestureDrag::new();
|
||||
gesture_drag.connect_drag_begin(glib::clone!(@weak obj => move |_, _, _| {
|
||||
obj.imp().in_drag.set(true);
|
||||
}));
|
||||
gesture_drag.connect_drag_end(glib::clone!(@weak obj => move |_, _, _| {
|
||||
obj.imp().in_drag.set(false);
|
||||
if let Some(volume) = obj.imp().volume_to_set.take() {
|
||||
obj.set_value(volume);
|
||||
}
|
||||
}));
|
||||
obj.add_controller(&gesture_drag);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for VolumeScaleImp {}
|
||||
impl RangeImpl for VolumeScaleImp {}
|
||||
impl ScaleImpl for VolumeScaleImp {}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct VolumeScale(ObjectSubclass<VolumeScaleImp>)
|
||||
@extends gtk4::Scale, gtk4::Range, gtk4::Widget,
|
||||
@implements gtk4::Accessible, gtk4::Orientable;
|
||||
}
|
||||
|
||||
impl VolumeScale {
|
||||
pub fn new(pa: PA, sink: bool) -> Self {
|
||||
let scale: VolumeScale = glib::Object::new(&[]).unwrap();
|
||||
let name = scale.imp().name.clone();
|
||||
let updater = Updater::new(move |value: f64| {
|
||||
let name = name.clone();
|
||||
let pa = pa.clone();
|
||||
async move {
|
||||
let mut volumes = ChannelVolumes::default();
|
||||
let volume = value * (Volume::NORMAL.0 as f64) / 100.;
|
||||
volumes.set(1, Volume(volume as _)); // XXX ?
|
||||
|
||||
let name_ref = name.borrow();
|
||||
if let Some(name) = name_ref.as_deref() {
|
||||
if sink {
|
||||
let fut = pa.set_sink_volume_by_name(name, &volumes);
|
||||
drop(name_ref);
|
||||
fut.await;
|
||||
} else {
|
||||
let fut = pa.set_source_volume_by_name(name, &volumes);
|
||||
drop(name_ref);
|
||||
fut.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
scale.connect_change_value(move |_scale, _scroll, value| {
|
||||
updater.update(value);
|
||||
gtk4::Inhibit(false)
|
||||
});
|
||||
scale
|
||||
}
|
||||
|
||||
pub fn set_volume(&self, volume: &ChannelVolumes) {
|
||||
let value = volume.avg().0 as f64 / (Volume::NORMAL.0 as f64) * 100.;
|
||||
if self.imp().in_drag.get() {
|
||||
// Don't set value of scale while it is being moved
|
||||
self.imp().volume_to_set.set(Some(value));
|
||||
} else {
|
||||
self.set_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_name(&self, name: Option<String>) {
|
||||
*self.imp().name.borrow_mut() = name;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform an asynchronous update operation without queuing more than one set.
|
||||
struct Updater<T: 'static> {
|
||||
updating: Rc<Cell<bool>>,
|
||||
value: Rc<Cell<Option<T>>>,
|
||||
update_fn: Rc<dyn Fn(T) -> Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||
}
|
||||
|
||||
impl<T: 'static> Updater<T> {
|
||||
fn new<Fut: Future<Output = ()> + 'static, F: Fn(T) -> Fut + 'static>(f: F) -> Self {
|
||||
let value = Rc::new(Cell::new(None));
|
||||
let updating = Rc::new(Cell::new(false));
|
||||
let update_fn =
|
||||
Rc::new(move |value| Box::pin(f(value)) as Pin<Box<dyn Future<Output = ()>>>);
|
||||
Self {
|
||||
updating,
|
||||
value,
|
||||
update_fn,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&self, value: T) {
|
||||
self.value.set(Some(value));
|
||||
if self.updating.replace(true) == false {
|
||||
let value = self.value.clone();
|
||||
let updating = self.updating.clone();
|
||||
let update_fn = self.update_fn.clone();
|
||||
glib::MainContext::default().spawn_local(async move {
|
||||
while let Some(value) = value.take() {
|
||||
update_fn(value).await;
|
||||
}
|
||||
updating.set(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
2
applets/cosmic-applet-battery/Cargo.lock
generated
2
applets/cosmic-applet-battery/Cargo.lock
generated
|
|
@ -1730,7 +1730,7 @@ checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
|
|||
[[package]]
|
||||
name = "libcosmic"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic/?branch=master#5dbb050d48576a0f8a2986b24a58b7eb63e57191"
|
||||
source = "git+https://github.com/pop-os/libcosmic/?branch=master#91e826d8eacd03d8e7ef5b1a767b2164f8719ed0"
|
||||
dependencies = [
|
||||
"apply",
|
||||
"cosmic-panel-config",
|
||||
|
|
|
|||
|
|
@ -46,19 +46,8 @@ fn format_duration(duration: Duration) -> String {
|
|||
}
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
let mut settings = settings();
|
||||
let helper = CosmicAppletHelper::default();
|
||||
let pixels = helper.suggested_icon_size() as u32;
|
||||
settings.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings {
|
||||
iced_settings: Settings {
|
||||
size: (pixels + 16, pixels + 16),
|
||||
min_size: Some((pixels + 16, pixels + 16)),
|
||||
max_size: Some((pixels + 16, pixels + 16)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
CosmicBatteryApplet::run(settings)
|
||||
CosmicBatteryApplet::run(helper.window_settings())
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
|
|
|||
2
applets/cosmic-applet-graphics/Cargo.lock
generated
2
applets/cosmic-applet-graphics/Cargo.lock
generated
|
|
@ -1531,7 +1531,7 @@ checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
|
|||
[[package]]
|
||||
name = "libcosmic"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic/?branch=master#5dbb050d48576a0f8a2986b24a58b7eb63e57191"
|
||||
source = "git+https://github.com/pop-os/libcosmic/?branch=master#91e826d8eacd03d8e7ef5b1a767b2164f8719ed0"
|
||||
dependencies = [
|
||||
"apply",
|
||||
"cosmic-panel-config",
|
||||
|
|
|
|||
|
|
@ -12,17 +12,6 @@ use cosmic_panel_config::PanelSize;
|
|||
use window::*;
|
||||
|
||||
pub fn main() -> cosmic::iced::Result {
|
||||
let mut settings = settings();
|
||||
let helper = CosmicAppletHelper::default();
|
||||
let pixels = helper.suggested_icon_size() as u32;
|
||||
settings.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings {
|
||||
iced_settings: Settings {
|
||||
size: (pixels + 16, pixels + 16),
|
||||
min_size: Some((pixels + 16, pixels + 16)),
|
||||
max_size: Some((pixels + 16, pixels + 16)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
Window::run(settings)
|
||||
Window::run(helper.window_settings())
|
||||
}
|
||||
|
|
|
|||
3698
applets/cosmic-applet-network/Cargo.lock
generated
Normal file
3698
applets/cosmic-applet-network/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
7
debian/rules
vendored
7
debian/rules
vendored
|
|
@ -43,6 +43,13 @@ override_dh_auto_clean:
|
|||
echo 'directory = "vendor"' >> .cargo/config; \
|
||||
tar pcf vendor.tar vendor; \
|
||||
rm -rf vendor; \
|
||||
cd ../..; \
|
||||
cd applets/cosmic-applet-audio/; \
|
||||
mkdir -p .cargo; \
|
||||
cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config; \
|
||||
echo 'directory = "vendor"' >> .cargo/config; \
|
||||
tar pcf vendor.tar vendor; \
|
||||
rm -rf vendor; \
|
||||
fi
|
||||
|
||||
override_dh_auto_build:
|
||||
|
|
|
|||
14
justfile
14
justfile
|
|
@ -28,6 +28,9 @@ workspaces_button_id := 'com.system76.CosmicPanelWorkspacesButton'
|
|||
|
||||
build: _extract_vendor
|
||||
#!/usr/bin/env bash
|
||||
pushd applets/cosmic-applet-audio/
|
||||
cargo build {{cargo_args}}
|
||||
popd
|
||||
pushd applets/cosmic-applet-graphics/
|
||||
cargo build {{cargo_args}}
|
||||
popd
|
||||
|
|
@ -41,6 +44,11 @@ build: _extract_vendor
|
|||
|
||||
# Installs files into the system
|
||||
install:
|
||||
# audio
|
||||
install -Dm0644 applets/cosmic-applet-audio/data/icons/{{audio_id}}.svg {{iconsdir}}/{{audio_id}}.svg
|
||||
install -Dm0644 applets/cosmic-applet-audio/data/{{audio_id}}.desktop {{sharedir}}/applications/{{audio_id}}.desktop
|
||||
install -Dm0755 applets/cosmic-applet-audio/target/release/cosmic-applet-audio {{bindir}}/cosmic-applet-audio
|
||||
|
||||
# app list
|
||||
install -Dm0644 applets/cosmic-app-list/data/icons/{{app_list_id}}-symbolic.svg {{iconsdir}}/{{app_list_id}}-symbolic.svg
|
||||
install -Dm0644 applets/cosmic-app-list/data/icons/{{app_list_id}}.Devel.svg {{iconsdir}}/{{app_list_id}}.Devel.svg
|
||||
|
|
@ -48,11 +56,6 @@ install:
|
|||
install -Dm0644 applets/cosmic-app-list/data/{{app_list_id}}.desktop {{sharedir}}/applications/{{app_list_id}}.desktop
|
||||
install -Dm0755 target/release/cosmic-app-list {{bindir}}/cosmic-app-list
|
||||
|
||||
# audio
|
||||
install -Dm0644 applets/cosmic-applet-audio/data/icons/{{audio_id}}.svg {{iconsdir}}/{{audio_id}}.svg
|
||||
install -Dm0644 applets/cosmic-applet-audio/data/{{audio_id}}.desktop {{sharedir}}/applications/{{audio_id}}.desktop
|
||||
install -Dm0755 target/release/cosmic-applet-audio {{bindir}}/cosmic-applet-audio
|
||||
|
||||
# network
|
||||
install -Dm0644 applets/cosmic-applet-network/data/icons/{{network_id}}.svg {{iconsdir}}/{{network_id}}.svg
|
||||
install -Dm0644 applets/cosmic-applet-network/data/{{network_id}}.desktop {{sharedir}}/applications/{{network_id}}.desktop
|
||||
|
|
@ -112,4 +115,5 @@ _extract_vendor:
|
|||
rm -rf applets/cosmic-applet-graphics/vendor; tar xf applets/cosmic-applet-graphics/vendor.tar --directory applets/cosmic-applet-graphics
|
||||
rm -rf applets/cosmic-applet-workspaces/vendor; tar xf applets/cosmic-applet-workspaces/vendor.tar --directory applets/cosmic-applet-workspaces
|
||||
rm -rf applets/cosmic-applet-battery/vendor; tar xf applets/cosmic-applet-battery/vendor.tar --directory applets/cosmic-applet-battery
|
||||
rm -rf applets/cosmic-applet-audio/vendor; tar xf applets/cosmic-applet-audio/vendor.tar --directory applets/cosmic-applet-audio
|
||||
fi
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue