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:
Brock 2022-12-08 11:58:39 -07:00 committed by GitHub
parent f4b3ddcafc
commit f3b5713ff5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 7825 additions and 1564 deletions

269
Cargo.lock generated
View file

@ -229,7 +229,7 @@ source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b20
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cairo-sys-rs", "cairo-sys-rs",
"glib 0.16.0", "glib",
"libc", "libc",
"once_cell", "once_cell",
"thiserror", "thiserror",
@ -240,7 +240,7 @@ name = "cairo-sys-rs"
version = "0.16.0" version = "0.16.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee" source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
dependencies = [ dependencies = [
"glib-sys 0.16.0", "glib-sys",
"libc", "libc",
"system-deps", "system-deps",
] ]
@ -348,29 +348,6 @@ dependencies = [
"xdg", "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]] [[package]]
name = "cosmic-applet-network" name = "cosmic-applet-network"
version = "0.1.0" version = "0.1.0"
@ -625,15 +602,6 @@ dependencies = [
"generic-array", "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]] [[package]]
name = "dirs" name = "dirs"
version = "4.0.0" version = "4.0.0"
@ -806,19 +774,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 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]] [[package]]
name = "futures" name = "futures"
version = "0.3.23" version = "0.3.23"
@ -940,7 +895,7 @@ dependencies = [
"bitflags", "bitflags",
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
"gio", "gio",
"glib 0.16.0", "glib",
"libc", "libc",
] ]
@ -950,8 +905,8 @@ version = "0.16.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee" source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
dependencies = [ dependencies = [
"gio-sys", "gio-sys",
"glib-sys 0.16.0", "glib-sys",
"gobject-sys 0.16.0", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
] ]
@ -966,7 +921,7 @@ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4-sys", "gdk4-sys",
"gio", "gio",
"glib 0.16.0", "glib",
"libc", "libc",
"pango", "pango",
] ]
@ -979,8 +934,8 @@ dependencies = [
"cairo-sys-rs", "cairo-sys-rs",
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
"gio-sys", "gio-sys",
"glib-sys 0.16.0", "glib-sys",
"gobject-sys 0.16.0", "gobject-sys",
"libc", "libc",
"pango-sys", "pango-sys",
"pkg-config", "pkg-config",
@ -1010,26 +965,6 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "gio" name = "gio"
version = "0.16.0" version = "0.16.0"
@ -1041,7 +976,7 @@ dependencies = [
"futures-io", "futures-io",
"futures-util", "futures-util",
"gio-sys", "gio-sys",
"glib 0.16.0", "glib",
"libc", "libc",
"once_cell", "once_cell",
"thiserror", "thiserror",
@ -1052,33 +987,13 @@ name = "gio-sys"
version = "0.16.0" version = "0.16.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee" source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
dependencies = [ dependencies = [
"glib-sys 0.16.0", "glib-sys",
"gobject-sys 0.16.0", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
"winapi", "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]] [[package]]
name = "glib" name = "glib"
version = "0.16.0" version = "0.16.0"
@ -1090,9 +1005,9 @@ dependencies = [
"futures-executor", "futures-executor",
"futures-task", "futures-task",
"futures-util", "futures-util",
"glib-macros 0.16.0", "glib-macros",
"glib-sys 0.16.0", "glib-sys",
"gobject-sys 0.16.0", "gobject-sys",
"libc", "libc",
"once_cell", "once_cell",
"smallvec", "smallvec",
@ -1104,21 +1019,6 @@ name = "glib-build-tools"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core#4ca23ca05bd95655717605eaee349bbb5abc29b7" 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]] [[package]]
name = "glib-macros" name = "glib-macros"
version = "0.16.0" version = "0.16.0"
@ -1133,16 +1033,6 @@ dependencies = [
"syn", "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]] [[package]]
name = "glib-sys" name = "glib-sys"
version = "0.16.0" version = "0.16.0"
@ -1152,23 +1042,12 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "gobject-sys" name = "gobject-sys"
version = "0.16.0" version = "0.16.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee" source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
dependencies = [ dependencies = [
"glib-sys 0.16.0", "glib-sys",
"libc", "libc",
"system-deps", "system-deps",
] ]
@ -1178,7 +1057,7 @@ name = "graphene-rs"
version = "0.16.0" version = "0.16.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee" source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
dependencies = [ dependencies = [
"glib 0.16.0", "glib",
"graphene-sys", "graphene-sys",
"libc", "libc",
] ]
@ -1188,7 +1067,7 @@ name = "graphene-sys"
version = "0.16.0" version = "0.16.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee" source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
dependencies = [ dependencies = [
"glib-sys 0.16.0", "glib-sys",
"libc", "libc",
"pkg-config", "pkg-config",
"system-deps", "system-deps",
@ -1202,7 +1081,7 @@ dependencies = [
"bitflags", "bitflags",
"cairo-rs", "cairo-rs",
"gdk4", "gdk4",
"glib 0.16.0", "glib",
"graphene-rs", "graphene-rs",
"gsk4-sys", "gsk4-sys",
"libc", "libc",
@ -1216,8 +1095,8 @@ source = "git+https://github.com/gtk-rs/gtk4-rs#e4178e68237503c93ca98193e7832b7e
dependencies = [ dependencies = [
"cairo-sys-rs", "cairo-sys-rs",
"gdk4-sys", "gdk4-sys",
"glib-sys 0.16.0", "glib-sys",
"gobject-sys 0.16.0", "gobject-sys",
"graphene-sys", "graphene-sys",
"libc", "libc",
"pango-sys", "pango-sys",
@ -1236,7 +1115,7 @@ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
"gio", "gio",
"glib 0.16.0", "glib",
"graphene-rs", "graphene-rs",
"gsk4", "gsk4",
"gtk4-macros", "gtk4-macros",
@ -1268,8 +1147,8 @@ dependencies = [
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
"gdk4-sys", "gdk4-sys",
"gio-sys", "gio-sys",
"glib-sys 0.16.0", "glib-sys",
"gobject-sys 0.16.0", "gobject-sys",
"graphene-sys", "graphene-sys",
"gsk4-sys", "gsk4-sys",
"libc", "libc",
@ -1479,7 +1358,7 @@ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
"gio", "gio",
"glib 0.16.0", "glib",
"gtk4", "gtk4",
"libadwaita-sys", "libadwaita-sys",
"libc", "libc",
@ -1494,8 +1373,8 @@ source = "git+https://gitlab.gnome.org/World/Rust/libadwaita-rs#01881b0c9f67ed5a
dependencies = [ dependencies = [
"gdk4-sys", "gdk4-sys",
"gio-sys", "gio-sys",
"glib-sys 0.16.0", "glib-sys",
"gobject-sys 0.16.0", "gobject-sys",
"gtk4-sys", "gtk4-sys",
"libc", "libc",
"system-deps", "system-deps",
@ -1551,56 +1430,6 @@ dependencies = [
"winapi", "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]] [[package]]
name = "locale_config" name = "locale_config"
version = "0.3.0" version = "0.3.0"
@ -1680,18 +1509,6 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "nanorand" name = "nanorand"
version = "0.7.0" version = "0.7.0"
@ -1740,17 +1557,6 @@ dependencies = [
"pin-utils", "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]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -1872,7 +1678,7 @@ source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b20
dependencies = [ dependencies = [
"bitflags", "bitflags",
"gio", "gio",
"glib 0.16.0", "glib",
"libc", "libc",
"once_cell", "once_cell",
"pango-sys", "pango-sys",
@ -1883,8 +1689,8 @@ name = "pango-sys"
version = "0.16.0" version = "0.16.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee" source = "git+https://github.com/gtk-rs/gtk-rs-core#49d9100919c51b248bc176763b203bce23efe0ee"
dependencies = [ dependencies = [
"glib-sys 0.16.0", "glib-sys",
"gobject-sys 0.16.0", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
] ]
@ -2188,7 +1994,7 @@ checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]] [[package]]
name = "relm4" name = "relm4"
version = "0.5.0-beta.1" 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 = [ dependencies = [
"async-broadcast", "async-broadcast",
"async-oneshot", "async-oneshot",
@ -2197,7 +2003,6 @@ dependencies = [
"gtk4", "gtk4",
"log", "log",
"once_cell", "once_cell",
"relm4-macros",
"tokio", "tokio",
"tracing", "tracing",
] ]
@ -2205,7 +2010,7 @@ dependencies = [
[[package]] [[package]]
name = "relm4-macros" name = "relm4-macros"
version = "0.5.0-beta.1" 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 = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2513,12 +2318,6 @@ dependencies = [
"version-compare", "version-compare",
] ]
[[package]]
name = "temp-dir"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.3.0" version = "3.3.0"
@ -3001,7 +2800,7 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6"
dependencies = [ dependencies = [
"dirs 4.0.0", "dirs",
] ]
[[package]] [[package]]
@ -3029,7 +2828,7 @@ dependencies = [
"async-trait", "async-trait",
"byteorder", "byteorder",
"derivative", "derivative",
"dirs 4.0.0", "dirs",
"enumflags2", "enumflags2",
"event-listener", "event-listener",
"futures-core", "futures-core",
@ -3069,7 +2868,7 @@ dependencies = [
"async-trait", "async-trait",
"byteorder", "byteorder",
"derivative", "derivative",
"dirs 4.0.0", "dirs",
"enumflags2", "enumflags2",
"event-listener", "event-listener",
"futures-core", "futures-core",

View file

@ -1,6 +1,5 @@
[workspace] [workspace]
members = [ members = [
"applets/cosmic-applet-audio",
"applets/cosmic-applet-network", "applets/cosmic-applet-network",
"applets/cosmic-applet-notifications", "applets/cosmic-applet-notifications",
"applets/cosmic-applet-power", "applets/cosmic-applet-power",
@ -14,13 +13,13 @@ exclude = [
"applets/cosmic-applet-graphics", "applets/cosmic-applet-graphics",
"applets/cosmic-applet-workspaces", "applets/cosmic-applet-workspaces",
"applets/cosmic-applet-battery", "applets/cosmic-applet-battery",
"applets/cosmic-applet-audio",
] ]
[patch.crates-io] [patch.crates-io]
wayland-protocols = { git = "https://github.com/smithay/wayland-rs", version = "0.30.0-beta.9"} 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-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-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.13"} 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.13"} 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.13"} 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

File diff suppressed because it is too large Load diff

View file

@ -2,25 +2,32 @@
name = "cosmic-applet-audio" name = "cosmic-applet-audio"
version = "0.1.0" version = "0.1.0"
edition = "2021" 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] [dependencies]
futures = "0.3.21" icon-loader = { version = "0.3.6", features = ["gtk"] }
futures-util = "0.3.21"
libcosmic-applet = { path = "../../libcosmic-applet" }
libpulse-binding = "2.26.0" libpulse-binding = "2.26.0"
libpulse-glib-binding = "2.25.0" libpulse-glib-binding = "2.25.0"
tracker = "0.1.1" tokio = { version = "1.20.1", features=["full"] }
freedesktop-desktop-entry = "0.5.0" libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet"] }
mpris2-zbus = { git = "https://github.com/pop-os/mpris2-zbus" } iced_sctk = { git = "https://github.com/pop-os/iced-sctk" }
zbus = "2.1.1" sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", version = "0.16" }
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"
[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"

View file

@ -1,5 +1,5 @@
[Desktop Entry] [Desktop Entry]
Name=Cosmic Dock App List Name=Cosmic Applet Audio
Comment=Write a GTK + Rust application Comment=Write a GTK + Rust application
Type=Application Type=Application
Exec=cosmic-applet-audio Exec=cosmic-applet-audio

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<gresources> <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 --> <!-- see https://gtk-rs.org/gtk4-rs/git/docs/gtk4/struct.Application.html#automatic-resources -->
</gresource> </gresource>
</gresources> </gresources>

View file

@ -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);
}
}
}

View file

@ -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
}

View file

@ -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);
}
}

View file

@ -1,208 +1,372 @@
// SPDX-License-Identifier: GPL-3.0-or-later use iced::widget::Space;
#[macro_use] use cosmic::widget::{icon, toggler, horizontal_rule};
extern crate relm4_macros; use cosmic::applet::CosmicAppletHelper;
use cosmic::Renderer;
mod icons; use cosmic::iced_native::window::Settings;
mod input; use cosmic::iced_style::application::{self, Appearance};
mod now_playing; use cosmic::iced_style::svg;
mod output; use cosmic::theme::{self, Svg};
mod pa; use cosmic::{iced_style, settings, Element, Theme};
use pa::PA; use cosmic::iced::{
mod task; executor,
mod volume; widget::{button, column, row, text, slider},
mod volume_scale; window, Alignment, Application, Command, Length, Subscription,
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 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() { mod pulse;
let _monitors = libcosmic::init(); use crate::pulse::DeviceInfo;
let application = Application::new(None, ApplicationFlags::default()); use libpulse_binding::volume::{Volume, VolumeLinear};
application.connect_activate(app);
application.run(); pub fn main() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default();
Audio::run(helper.window_settings())
} }
fn app(application: &Application) { #[derive(Default)]
// XXX handle no pulseaudio daemon? struct Audio {
let pa = PA::new().unwrap(); is_open: IsOpen,
let (refresh_output_tx, mut refresh_output_rx) = mpsc::unbounded(); current_output: Option<DeviceInfo>,
let (refresh_input_tx, mut refresh_input_rx) = mpsc::unbounded(); current_input: Option<DeviceInfo>,
let (now_playing_tx, mut now_playing_rx) = mpsc::unbounded::<Vec<Metadata>>(); outputs: Vec<DeviceInfo>,
pa inputs: Vec<DeviceInfo>,
.set_subscribe_callback(clone!(@strong refresh_output_tx, @strong refresh_input_tx => move |facility, operation, _idx| { pulse_state: PulseState,
if !matches!(operation, Some(Operation::Changed)) { applet_helper: CosmicAppletHelper,
return; icon_name: String,
} theme: Theme,
match facility { popup: Option<window::Id>,
Some(Facility::Sink) => { id_ctr: u32,
refresh_output_tx.unbounded_send(()).expect("failed to send output refresh message"); }
}
Some(Facility::Source) => { #[derive(Debug, PartialEq, Eq)]
refresh_input_tx.unbounded_send(()).expect("failed to send output refresh message"); enum IsOpen {
} None,
_ => {} Output,
} Input,
})); }
pa.set_state_callback(move |pa, state| {
if state == State::Ready { #[derive(Debug, Clone)]
pa.subscribe(InterestMaskSet::SINK | InterestMaskSet::SOURCE); enum Message {
refresh_output_tx SetOutputVolume(f64),
.unbounded_send(()) SetInputVolume(f64),
.expect("failed to send output refresh message"); OutputToggle,
refresh_input_tx InputToggle,
.unbounded_send(()) OutputChanged(String),
.expect("failed to send output refresh message"); InputChanged(String),
} Pulse(pulse::Event),
}); Ignore,
pa.connect().unwrap(); // XXX unwrap TogglePopup,
view! { }
window = libcosmic_applet::AppletWindow {
set_application: Some(application), impl Application for Audio {
set_title: Some("COSMIC Network Applet"), type Message = Message;
#[wrap(Some)] type Theme = Theme;
set_child: button = &libcosmic_applet::AppletButton { type Executor = executor::Default;
set_button_icon_name: "audio-volume-medium-symbolic", type Flags = ();
#[wrap(Some)]
set_popover_child: window_box = &GtkBox { fn new(_flags: ()) -> (Audio, Command<Message>) {
set_orientation: Orientation::Vertical, (
set_spacing: 24, Audio {
append: output_box = &GtkBox { is_open: IsOpen::None,
set_orientation: Orientation::Horizontal, current_output: None,
set_spacing: 16, current_input: None,
append: output_icon = &Image { outputs: vec![],
set_icon_name: Some("audio-speakers-symbolic"), inputs: vec![],
pulse_state: PulseState::Disconnected,
icon_name: "audio-volume-high-symbolic".to_string(),
..Default::default()
}, },
append: output_volume = &VolumeScale::new(pa.clone(), true) { Command::none(),
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,
}
}
}
}
} }
glib::MainContext::default().spawn_local( fn title(&self) -> String {
clone!(@weak inputs, @weak current_input, @weak input_volume, @strong pa => async move { String::from("Audio")
while let Some(()) = refresh_input_rx.next().await {
input::refresh_input_widgets(&pa, &inputs).await;
let default_input = input::refresh_default_input(&pa, &current_input).await;
volume::update_volume(&default_input, &input_volume);
} }
}),
); fn theme(&self) -> Theme {
glib::MainContext::default().spawn_local( self.theme
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); fn close_requested(&self, _id: iced_sctk::application::SurfaceIdWrapper) -> Self::Message {
let default_output = output::refresh_default_output(&pa, &current_output).await; Message::Ignore
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; fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
// XXX correct cutoffs? <Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
if default_output.mute { background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
"audio-volume-muted" text_color: theme.cosmic().on_bg_color().into(),
} else if volume > 1.0 { })
"audio-volume-overamplified-symbolic" }
} else if volume > 0.66 {
"audio-volume-high-symbolic" fn update(&mut self, message: Message) -> Command<Message> {
} else if volume > 0.33 { match message {
"audio-volume-medium-symbolic" Message::TogglePopup => {
if let Some(p) = self.popup.take() {
return destroy_popup(p);
} else { } 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 { let input_drop = revealer(
while let Some(all_metadata) = now_playing_rx.next().await { 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();
} }

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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))),
)
})
}
}

View 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,
}
}
}

View file

@ -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()
}

View file

@ -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);
}

View file

@ -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);
});
}
}
}

View file

@ -1730,7 +1730,7 @@ checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
[[package]] [[package]]
name = "libcosmic" name = "libcosmic"
version = "0.1.0" 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 = [ dependencies = [
"apply", "apply",
"cosmic-panel-config", "cosmic-panel-config",

View file

@ -46,19 +46,8 @@ fn format_duration(duration: Duration) -> String {
} }
pub fn run() -> cosmic::iced::Result { pub fn run() -> cosmic::iced::Result {
let mut settings = settings();
let helper = CosmicAppletHelper::default(); let helper = CosmicAppletHelper::default();
let pixels = helper.suggested_icon_size() as u32; CosmicBatteryApplet::run(helper.window_settings())
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)
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]

View file

@ -1531,7 +1531,7 @@ checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
[[package]] [[package]]
name = "libcosmic" name = "libcosmic"
version = "0.1.0" 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 = [ dependencies = [
"apply", "apply",
"cosmic-panel-config", "cosmic-panel-config",

View file

@ -12,17 +12,6 @@ use cosmic_panel_config::PanelSize;
use window::*; use window::*;
pub fn main() -> cosmic::iced::Result { pub fn main() -> cosmic::iced::Result {
let mut settings = settings();
let helper = CosmicAppletHelper::default(); let helper = CosmicAppletHelper::default();
let pixels = helper.suggested_icon_size() as u32; Window::run(helper.window_settings())
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)
} }

3698
applets/cosmic-applet-network/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

7
debian/rules vendored
View file

@ -43,6 +43,13 @@ override_dh_auto_clean:
echo 'directory = "vendor"' >> .cargo/config; \ echo 'directory = "vendor"' >> .cargo/config; \
tar pcf vendor.tar vendor; \ tar pcf vendor.tar vendor; \
rm -rf 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 fi
override_dh_auto_build: override_dh_auto_build:

View file

@ -28,6 +28,9 @@ workspaces_button_id := 'com.system76.CosmicPanelWorkspacesButton'
build: _extract_vendor build: _extract_vendor
#!/usr/bin/env bash #!/usr/bin/env bash
pushd applets/cosmic-applet-audio/
cargo build {{cargo_args}}
popd
pushd applets/cosmic-applet-graphics/ pushd applets/cosmic-applet-graphics/
cargo build {{cargo_args}} cargo build {{cargo_args}}
popd popd
@ -41,6 +44,11 @@ build: _extract_vendor
# Installs files into the system # Installs files into the system
install: 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 # 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}}-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 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 -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 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 # network
install -Dm0644 applets/cosmic-applet-network/data/icons/{{network_id}}.svg {{iconsdir}}/{{network_id}}.svg 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 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-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-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-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 fi