refactor: compile applets as multicall binary
This commit is contained in:
parent
4099624499
commit
3c4acdacd7
48 changed files with 2393 additions and 2256 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
|||
# Cargo
|
||||
.cargo.
|
||||
.cargo
|
||||
target/
|
||||
build/
|
||||
_build/
|
||||
|
|
|
|||
102
Cargo.lock
generated
102
Cargo.lock
generated
|
|
@ -115,9 +115,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.80"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
|
||||
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
|
||||
|
||||
[[package]]
|
||||
name = "apply"
|
||||
|
|
@ -583,9 +583,9 @@ checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
|
|||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.14.3"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
|
||||
checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
|
||||
dependencies = [
|
||||
"bytemuck_derive",
|
||||
]
|
||||
|
|
@ -854,7 +854,7 @@ dependencies = [
|
|||
"futures-util",
|
||||
"i18n-embed 0.13.9",
|
||||
"i18n-embed-fl 0.6.7",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.1",
|
||||
"libcosmic",
|
||||
"log",
|
||||
"nix 0.26.4",
|
||||
|
|
@ -937,6 +937,7 @@ dependencies = [
|
|||
"rust-embed 6.8.1",
|
||||
"slotmap",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
|
@ -981,6 +982,7 @@ dependencies = [
|
|||
"rust-embed-utils 7.8.1",
|
||||
"slotmap",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"zbus",
|
||||
|
|
@ -1027,6 +1029,9 @@ dependencies = [
|
|||
"rust-embed 6.8.1",
|
||||
"rust-embed-utils 7.8.1",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
|
|
@ -1038,6 +1043,9 @@ dependencies = [
|
|||
"libcosmic",
|
||||
"serde",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
|
|
@ -1057,6 +1065,8 @@ dependencies = [
|
|||
"rust-embed 8.3.0",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1073,6 +1083,8 @@ dependencies = [
|
|||
"rust-embed 6.8.1",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1096,6 +1108,28 @@ dependencies = [
|
|||
"xdg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-applets"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"cosmic-app-list",
|
||||
"cosmic-applet-audio",
|
||||
"cosmic-applet-battery",
|
||||
"cosmic-applet-bluetooth",
|
||||
"cosmic-applet-minimize",
|
||||
"cosmic-applet-network",
|
||||
"cosmic-applet-notifications",
|
||||
"cosmic-applet-power",
|
||||
"cosmic-applet-status-area",
|
||||
"cosmic-applet-tiling",
|
||||
"cosmic-applet-time",
|
||||
"cosmic-applet-workspaces",
|
||||
"libcosmic",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-client-toolkit"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1119,7 +1153,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-config"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"atomicwrites",
|
||||
"cosmic-config-derive",
|
||||
|
|
@ -1139,7 +1173,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-config-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
|
|
@ -1192,7 +1226,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-panel-config"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/cosmic-panel#07fcaee64f80d9aa498be53d40077bc0a510437b"
|
||||
source = "git+https://github.com/pop-os/cosmic-panel#f3fd857536bf8947a2ac765b01ed7078f452a767"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmic-config",
|
||||
|
|
@ -1250,7 +1284,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-theme"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"almost",
|
||||
"cosmic-config",
|
||||
|
|
@ -2782,7 +2816,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"iced_accessibility",
|
||||
"iced_core",
|
||||
|
|
@ -2797,7 +2831,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_accessibility"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_unix",
|
||||
|
|
@ -2806,7 +2840,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_core"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"iced_accessibility",
|
||||
|
|
@ -2825,7 +2859,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_futures"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"iced_core",
|
||||
|
|
@ -2838,7 +2872,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_graphics"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bytemuck",
|
||||
|
|
@ -2862,7 +2896,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_renderer"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"iced_graphics",
|
||||
"iced_tiny_skia",
|
||||
|
|
@ -2874,7 +2908,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_runtime"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"iced_accessibility",
|
||||
"iced_core",
|
||||
|
|
@ -2886,7 +2920,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_sctk"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"enum-repr",
|
||||
"float-cmp",
|
||||
|
|
@ -2911,7 +2945,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_style"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"iced_core",
|
||||
"once_cell",
|
||||
|
|
@ -2921,7 +2955,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_tiny_skia"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cosmic-text",
|
||||
|
|
@ -2938,7 +2972,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_wgpu"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bytemuck",
|
||||
|
|
@ -2957,7 +2991,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_widget"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"iced_renderer",
|
||||
"iced_runtime",
|
||||
|
|
@ -3242,7 +3276,7 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
|||
[[package]]
|
||||
name = "libcosmic"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#64ecb0ea48f262e13b1036757211b70432fd42e5"
|
||||
source = "git+https://github.com/pop-os/libcosmic#bf331cad7ff2512b01e31ea593f885932166b0e7"
|
||||
dependencies = [
|
||||
"apply",
|
||||
"ashpd 0.7.0",
|
||||
|
|
@ -4234,9 +4268,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
|
@ -4379,9 +4413,9 @@ checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f"
|
|||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.15.6"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17ea23eedb4d938031b6d4343222444608727a6aa68ec355e13588d9947ffe92"
|
||||
checksum = "81c524658d3b77930a391f559756d91dbe829ab6cf4687083f615d395df99722"
|
||||
dependencies = [
|
||||
"font-types",
|
||||
]
|
||||
|
|
@ -5042,9 +5076,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "swash"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d06ff4664af8923625604261c645f5c4cc610cc83c84bec74b50d76237089de7"
|
||||
checksum = "9af636fb90d39858650cae1088a37e2862dab4e874a0bb49d6dfb5b2dacf0e24"
|
||||
dependencies = [
|
||||
"read-fonts",
|
||||
"yazi",
|
||||
|
|
@ -5104,9 +5138,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.0"
|
||||
version = "6.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331"
|
||||
checksum = "e8e9199467bcbc77c6a13cc6e32a6af21721ab8c96aa0261856c4fda5a4433f0"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"heck",
|
||||
|
|
@ -5161,18 +5195,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.57"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
|
||||
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.57"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
|
||||
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
[workspace]
|
||||
|
||||
default-members = ["cosmic-applets", "cosmic-panel-button"]
|
||||
members = [
|
||||
"cosmic-app-list",
|
||||
"cosmic-applets",
|
||||
"cosmic-applet-audio",
|
||||
"cosmic-applet-battery",
|
||||
"cosmic-applet-bluetooth",
|
||||
|
|
|
|||
14
cosmic-app-list/src/lib.rs
Normal file
14
cosmic-app-list/src/lib.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// SPDX-License-Identifier: MPL-2.0-only
|
||||
mod app;
|
||||
mod config;
|
||||
mod localize;
|
||||
mod wayland_handler;
|
||||
mod wayland_subscription;
|
||||
|
||||
use localize::localize;
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize();
|
||||
|
||||
app::run()
|
||||
}
|
||||
|
|
@ -1,23 +1,10 @@
|
|||
// SPDX-License-Identifier: MPL-2.0-only
|
||||
mod app;
|
||||
mod config;
|
||||
mod localize;
|
||||
mod wayland_handler;
|
||||
mod wayland_subscription;
|
||||
|
||||
use log::info;
|
||||
|
||||
use localize::localize;
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
// Initialize logger
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
info!("Iced Workspaces Applet ({VERSION})");
|
||||
// Prepare i18n
|
||||
localize();
|
||||
|
||||
app::run()
|
||||
tracing::info!("Starting cosmic-app-list with version {VERSION}");
|
||||
|
||||
cosmic_app_list::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ libpulse-glib-binding = "2.28.1"
|
|||
tokio = { version = "1.20.1", features=["full"] }
|
||||
libcosmic.workspace = true
|
||||
cosmic-time.workspace = true
|
||||
tracing = "0.1.40"
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-log.workspace = true
|
||||
# Application i18n
|
||||
|
|
|
|||
859
cosmic-applet-audio/src/lib.rs
Normal file
859
cosmic-applet-audio/src/lib.rs
Normal file
|
|
@ -0,0 +1,859 @@
|
|||
mod localize;
|
||||
|
||||
use crate::localize::localize;
|
||||
use crate::pulse::DeviceInfo;
|
||||
use config::AudioAppletConfig;
|
||||
use cosmic::app::Command;
|
||||
use cosmic::applet::cosmic_panel_config::PanelAnchor;
|
||||
use cosmic::applet::menu_button;
|
||||
use cosmic::applet::menu_control_padding;
|
||||
use cosmic::applet::padded_control;
|
||||
use cosmic::applet::token::subscription::{
|
||||
activation_token_subscription, TokenRequest, TokenUpdate,
|
||||
};
|
||||
use cosmic::cctk::sctk::reexports::calloop;
|
||||
use cosmic::cosmic_config::CosmicConfigEntry;
|
||||
use cosmic::iced::widget;
|
||||
use cosmic::iced::Limits;
|
||||
use cosmic::iced::{
|
||||
self,
|
||||
widget::{column, row, slider, text},
|
||||
window, Alignment, Length, Subscription,
|
||||
};
|
||||
use cosmic::iced_runtime::core::alignment::Horizontal;
|
||||
use cosmic::iced_style::application;
|
||||
use cosmic::widget::button;
|
||||
use cosmic::widget::horizontal_space;
|
||||
use cosmic::widget::Column;
|
||||
use cosmic::widget::Row;
|
||||
use cosmic::widget::{divider, icon};
|
||||
use cosmic::Renderer;
|
||||
use cosmic::{Element, Theme};
|
||||
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
|
||||
use iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use iced::widget::container;
|
||||
use libpulse_binding::volume::VolumeLinear;
|
||||
use mpris2_zbus::player::PlaybackStatus;
|
||||
use mpris_subscription::MprisRequest;
|
||||
use mpris_subscription::MprisUpdate;
|
||||
|
||||
mod config;
|
||||
mod mpris_subscription;
|
||||
mod pipewire;
|
||||
mod pulse;
|
||||
|
||||
static SHOW_MEDIA_CONTROLS: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
|
||||
|
||||
const GO_BACK: &str = "media-skip-backward-symbolic";
|
||||
const GO_NEXT: &str = "media-skip-forward-symbolic";
|
||||
const PAUSE: &str = "media-playback-pause-symbolic";
|
||||
const PLAY: &str = "media-playback-start-symbolic";
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize();
|
||||
cosmic::applet::run::<Audio>(true, ())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
core: cosmic::app::Core,
|
||||
is_open: IsOpen,
|
||||
current_output: Option<DeviceInfo>,
|
||||
current_input: Option<DeviceInfo>,
|
||||
outputs: Vec<DeviceInfo>,
|
||||
inputs: Vec<DeviceInfo>,
|
||||
pulse_state: PulseState,
|
||||
icon_name: String,
|
||||
input_icon_name: String,
|
||||
popup: Option<window::Id>,
|
||||
timeline: Timeline,
|
||||
config: AudioAppletConfig,
|
||||
player_status: Option<mpris_subscription::PlayerStatus>,
|
||||
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
||||
}
|
||||
|
||||
impl Audio {
|
||||
fn update_output(&mut self, output: Option<DeviceInfo>) {
|
||||
self.current_output = output;
|
||||
self.apply_output_volume();
|
||||
}
|
||||
|
||||
fn apply_output_volume(&mut self) {
|
||||
let Some(output) = self.current_output.as_ref() else {
|
||||
self.icon_name = "audio-volume-muted-symbolic".to_string();
|
||||
return;
|
||||
};
|
||||
|
||||
let volume = output.volume.avg();
|
||||
let output_volume = VolumeLinear::from(volume).0;
|
||||
if volume.is_muted() {
|
||||
self.icon_name = "audio-volume-muted-symbolic".to_string();
|
||||
} else if output_volume < 0.25 {
|
||||
self.icon_name = "audio-volume-low-symbolic".to_string();
|
||||
} else if output_volume < 0.5 {
|
||||
self.icon_name = "audio-volume-medium-symbolic".to_string();
|
||||
} else if output_volume < 0.75 {
|
||||
self.icon_name = "audio-volume-high-symbolic".to_string();
|
||||
} else {
|
||||
self.icon_name = "audio-volume-overamplified-symbolic".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_input(&mut self, input: Option<DeviceInfo>) {
|
||||
self.current_input = input;
|
||||
self.apply_input_volume();
|
||||
}
|
||||
|
||||
fn apply_input_volume(&mut self) {
|
||||
let Some(input) = self.current_input.as_ref() else {
|
||||
self.input_icon_name = "microphone-sensitivity-muted-symbolic".to_string();
|
||||
return;
|
||||
};
|
||||
|
||||
let volume = input.volume.avg();
|
||||
let input_volume = VolumeLinear::from(volume).0;
|
||||
if volume.is_muted() {
|
||||
self.input_icon_name = "microphone-sensitivity-muted-symbolic".to_string();
|
||||
} else if input_volume < 0.33 {
|
||||
self.input_icon_name = "microphone-sensitivity-low-symbolic".to_string();
|
||||
} else if input_volume < 0.66 {
|
||||
self.input_icon_name = "microphone-sensitivity-medium-symbolic".to_string();
|
||||
} else {
|
||||
self.input_icon_name = "microphone-sensitivity-high-symbolic".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum IsOpen {
|
||||
None,
|
||||
Output,
|
||||
Input,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
SetOutputVolume(f64),
|
||||
SetInputVolume(f64),
|
||||
OutputToggle,
|
||||
InputToggle,
|
||||
OutputChanged(String),
|
||||
InputChanged(String),
|
||||
Pulse(pulse::Event),
|
||||
TogglePopup,
|
||||
CloseRequested(window::Id),
|
||||
ToggleMediaControlsInTopPanel(chain::Toggler, bool),
|
||||
Frame(Instant),
|
||||
ConfigChanged(AudioAppletConfig),
|
||||
Mpris(mpris_subscription::MprisUpdate),
|
||||
MprisRequest(MprisRequest),
|
||||
Token(TokenUpdate),
|
||||
OpenSettings,
|
||||
}
|
||||
|
||||
impl Audio {
|
||||
fn playback_buttons(&self) -> Option<Element<Message>> {
|
||||
if self.player_status.is_some() && self.config.show_media_controls_in_top_panel {
|
||||
let mut elements = Vec::with_capacity(3);
|
||||
if self
|
||||
.player_status
|
||||
.as_ref()
|
||||
.map(|s| s.can_go_previous)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
elements.push(self.core.applet.icon_button(GO_BACK).into())
|
||||
}
|
||||
if let Some(play) = self.is_play() {
|
||||
elements.push(
|
||||
self.core
|
||||
.applet
|
||||
.icon_button(if play { PLAY } else { PAUSE })
|
||||
.on_press(if play {
|
||||
Message::MprisRequest(MprisRequest::Play)
|
||||
} else {
|
||||
Message::MprisRequest(MprisRequest::Pause)
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if self
|
||||
.player_status
|
||||
.as_ref()
|
||||
.map(|s| s.can_go_next)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
elements.push(self.core.applet.icon_button(GO_NEXT).into())
|
||||
}
|
||||
|
||||
Some(match self.core.applet.anchor {
|
||||
PanelAnchor::Left | PanelAnchor::Right => Column::with_children(elements)
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
PanelAnchor::Top | PanelAnchor::Bottom => Row::with_children(elements)
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn go_previous(&self, icon_size: u16) -> Option<Element<Message>> {
|
||||
self.player_status.as_ref().and_then(|s| {
|
||||
if s.can_go_previous {
|
||||
Some(
|
||||
button::icon(icon::from_name(GO_BACK).size(icon_size).symbolic(true))
|
||||
.extra_small()
|
||||
.style(cosmic::theme::Button::AppletIcon)
|
||||
.on_press(Message::MprisRequest(MprisRequest::Previous))
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn go_next(&self, icon_size: u16) -> Option<Element<Message>> {
|
||||
self.player_status.as_ref().and_then(|s| {
|
||||
if s.can_go_next {
|
||||
Some(
|
||||
button::icon(icon::from_name(GO_NEXT).size(icon_size).symbolic(true))
|
||||
.extra_small()
|
||||
.style(cosmic::theme::Button::AppletIcon)
|
||||
.on_press(Message::MprisRequest(MprisRequest::Next))
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn is_play(&self) -> Option<bool> {
|
||||
self.player_status.as_ref().and_then(|s| match s.status {
|
||||
PlaybackStatus::Playing => {
|
||||
if s.can_pause {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
PlaybackStatus::Paused | PlaybackStatus::Stopped => {
|
||||
if s.can_play {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl cosmic::Application for Audio {
|
||||
type Message = Message;
|
||||
type Executor = cosmic::SingleThreadExecutor;
|
||||
type Flags = ();
|
||||
const APP_ID: &'static str = "com.system76.CosmicAppletAudio";
|
||||
|
||||
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Self {
|
||||
core,
|
||||
is_open: IsOpen::None,
|
||||
current_output: None,
|
||||
current_input: None,
|
||||
outputs: vec![],
|
||||
inputs: vec![],
|
||||
icon_name: "audio-volume-high-symbolic".to_string(),
|
||||
input_icon_name: "audio-input-microphone-symbolic".to_string(),
|
||||
token_tx: None,
|
||||
..Default::default()
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn core(&self) -> &cosmic::app::Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut cosmic::app::Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
||||
Some(cosmic::applet::style())
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::Frame(now) => self.timeline.now(now),
|
||||
Message::TogglePopup => {
|
||||
if let Some(p) = self.popup.take() {
|
||||
return destroy_popup(p);
|
||||
} else {
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::UpdateConnection);
|
||||
}
|
||||
let new_id = window::Id::unique();
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.core.applet.get_popup_settings(
|
||||
window::Id::MAIN,
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_height(1.0)
|
||||
.min_width(1.0)
|
||||
.max_width(400.0)
|
||||
.max_height(1080.0);
|
||||
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::GetDefaultSink);
|
||||
conn.send(pulse::Message::GetDefaultSource);
|
||||
conn.send(pulse::Message::GetSinks);
|
||||
conn.send(pulse::Message::GetSources);
|
||||
}
|
||||
|
||||
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())
|
||||
});
|
||||
self.apply_output_volume();
|
||||
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(),
|
||||
device.volume,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SetInputVolume(vol) => {
|
||||
self.current_input.as_mut().map(|i| {
|
||||
i.volume
|
||||
.set(i.volume.len(), VolumeLinear(vol / 100.0).into())
|
||||
});
|
||||
self.apply_input_volume();
|
||||
if let PulseState::Connected(connection) = &mut self.pulse_state {
|
||||
if let Some(device) = &self.current_input {
|
||||
if let Some(name) = &device.name {
|
||||
tracing::info!("increasing volume of {}", name);
|
||||
connection.send(pulse::Message::SetSourceVolumeByName(
|
||||
name.clone(),
|
||||
device.volume,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::OutputChanged(val) => {
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
if let Some(val) = self.outputs.iter().find(|o| o.name.as_ref() == Some(&val)) {
|
||||
conn.send(pulse::Message::SetDefaultSink(val.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::InputChanged(val) => {
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
if let Some(val) = self.inputs.iter().find(|i| i.name.as_ref() == Some(&val)) {
|
||||
conn.send(pulse::Message::SetDefaultSource(val.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::OutputToggle => {
|
||||
self.is_open = if self.is_open == IsOpen::Output {
|
||||
IsOpen::None
|
||||
} else {
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::GetSinks);
|
||||
}
|
||||
IsOpen::Output
|
||||
}
|
||||
}
|
||||
Message::InputToggle => {
|
||||
self.is_open = if self.is_open == IsOpen::Input {
|
||||
IsOpen::None
|
||||
} else {
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::GetSources);
|
||||
}
|
||||
IsOpen::Input
|
||||
}
|
||||
}
|
||||
Message::Pulse(event) => match event {
|
||||
pulse::Event::Init(mut conn) => {
|
||||
conn.send(pulse::Message::UpdateConnection);
|
||||
self.pulse_state = PulseState::Disconnected(conn);
|
||||
}
|
||||
pulse::Event::Connected => {
|
||||
self.pulse_state.connected();
|
||||
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::GetSinks);
|
||||
conn.send(pulse::Message::GetSources);
|
||||
conn.send(pulse::Message::GetDefaultSink);
|
||||
conn.send(pulse::Message::GetDefaultSource);
|
||||
}
|
||||
}
|
||||
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.update_output(Some(sink));
|
||||
}
|
||||
pulse::Message::SetDefaultSource(source) => {
|
||||
self.update_input(Some(source));
|
||||
}
|
||||
pulse::Message::Disconnected => {
|
||||
panic!("Subscription error handling is bad. This should never happen.")
|
||||
}
|
||||
_ => {
|
||||
tracing::trace!("Received misc message")
|
||||
}
|
||||
}
|
||||
}
|
||||
pulse::Event::Disconnected => {
|
||||
self.pulse_state.disconnected();
|
||||
if let Some(mut conn) = self.pulse_state.connection().cloned() {
|
||||
_ = tokio::spawn(async move {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
|
||||
conn.send(pulse::Message::UpdateConnection);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::ToggleMediaControlsInTopPanel(chain, enabled) => {
|
||||
self.timeline.set_chain(chain).start();
|
||||
self.config.show_media_controls_in_top_panel = enabled;
|
||||
if let Ok(helper) =
|
||||
cosmic::cosmic_config::Config::new(Self::APP_ID, AudioAppletConfig::VERSION)
|
||||
{
|
||||
if let Err(err) = self.config.write_entry(&helper) {
|
||||
tracing::error!(?err, "Error writing config");
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::CloseRequested(id) => {
|
||||
if Some(id) == self.popup {
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
Message::ConfigChanged(c) => {
|
||||
self.config = c;
|
||||
}
|
||||
Message::Mpris(mpris_subscription::MprisUpdate::Player(p)) => {
|
||||
self.player_status = Some(p);
|
||||
}
|
||||
Message::Mpris(MprisUpdate::Finished) => {
|
||||
self.player_status = None;
|
||||
}
|
||||
Message::Mpris(MprisUpdate::Setup) => {
|
||||
self.player_status = None;
|
||||
}
|
||||
Message::MprisRequest(r) => {
|
||||
let Some(player_status) = self.player_status.as_ref() else {
|
||||
tracing::error!("No player found");
|
||||
return Command::none();
|
||||
};
|
||||
let player = player_status.player.clone();
|
||||
|
||||
match r {
|
||||
MprisRequest::Play => tokio::spawn(async move {
|
||||
let res = player.play().await;
|
||||
if let Err(err) = res {
|
||||
tracing::error!("Error playing: {}", err);
|
||||
}
|
||||
}),
|
||||
MprisRequest::Pause => tokio::spawn(async move {
|
||||
let res = player.pause().await;
|
||||
if let Err(err) = res {
|
||||
tracing::error!("Error pausing: {}", err);
|
||||
}
|
||||
}),
|
||||
MprisRequest::Next => tokio::spawn(async move {
|
||||
let res = player.next().await;
|
||||
if let Err(err) = res {
|
||||
tracing::error!("Error playing next: {}", err);
|
||||
}
|
||||
}),
|
||||
MprisRequest::Previous => tokio::spawn(async move {
|
||||
let res = player.previous().await;
|
||||
if let Err(err) = res {
|
||||
tracing::error!("Error playing previous: {}", err);
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
Message::OpenSettings => {
|
||||
let exec = "cosmic-settings sound".to_string();
|
||||
if let Some(tx) = self.token_tx.as_ref() {
|
||||
let _ = tx.send(TokenRequest {
|
||||
app_id: Self::APP_ID.to_string(),
|
||||
exec,
|
||||
});
|
||||
} else {
|
||||
tracing::error!("Wayland tx is None");
|
||||
};
|
||||
}
|
||||
Message::Token(u) => match u {
|
||||
TokenUpdate::Init(tx) => {
|
||||
self.token_tx = Some(tx);
|
||||
}
|
||||
TokenUpdate::Finished => {
|
||||
self.token_tx = None;
|
||||
}
|
||||
TokenUpdate::ActivationToken { token, .. } => {
|
||||
let mut cmd = std::process::Command::new("cosmic-settings");
|
||||
cmd.arg("sound");
|
||||
if let Some(token) = token {
|
||||
cmd.env("XDG_ACTIVATION_TOKEN", &token);
|
||||
cmd.env("DESKTOP_STARTUP_ID", &token);
|
||||
}
|
||||
cosmic::process::spawn(cmd);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch(vec![
|
||||
pulse::connect().map(Message::Pulse),
|
||||
self.timeline
|
||||
.as_subscription()
|
||||
.map(|(_, now)| Message::Frame(now)),
|
||||
self.core.watch_config(Self::APP_ID).map(|u| {
|
||||
for err in u.errors {
|
||||
tracing::error!(?err, "Error watching config");
|
||||
}
|
||||
Message::ConfigChanged(u.config)
|
||||
}),
|
||||
mpris_subscription::mpris_subscription(0).map(Message::Mpris),
|
||||
activation_token_subscription(0).map(Message::Token),
|
||||
])
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let btn = self
|
||||
.core
|
||||
.applet
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup);
|
||||
if let Some(playback_buttons) = self.playback_buttons() {
|
||||
match self.core.applet.anchor {
|
||||
PanelAnchor::Left | PanelAnchor::Right => {
|
||||
Column::with_children(vec![playback_buttons, btn.into()])
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
PanelAnchor::Top | PanelAnchor::Bottom => {
|
||||
Row::with_children(vec![playback_buttons, btn.into()])
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
btn.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn view_window(&self, _id: window::Id) -> Element<Message> {
|
||||
let audio_disabled = matches!(self.pulse_state, PulseState::Disconnected(_));
|
||||
let out_f64 = VolumeLinear::from(
|
||||
self.current_output
|
||||
.as_ref()
|
||||
.map(|o| o.volume.avg())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.0 * 100.0;
|
||||
let in_f64 = VolumeLinear::from(
|
||||
self.current_input
|
||||
.as_ref()
|
||||
.map(|o| o.volume.avg())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.0 * 100.0;
|
||||
|
||||
let mut audio_content = if audio_disabled {
|
||||
column![padded_control(
|
||||
text(fl!("disconnected"))
|
||||
.width(Length::Fill)
|
||||
.horizontal_alignment(Horizontal::Center)
|
||||
.size(24)
|
||||
)]
|
||||
} else {
|
||||
column![
|
||||
padded_control(
|
||||
row![
|
||||
icon::from_name(self.icon_name.as_str())
|
||||
.size(24)
|
||||
.symbolic(true),
|
||||
slider(0.0..=100.0, out_f64, Message::SetOutputVolume)
|
||||
.width(Length::FillPortion(5)),
|
||||
text(format!("{}%", out_f64.round()))
|
||||
.size(16)
|
||||
.width(Length::FillPortion(1))
|
||||
.horizontal_alignment(Horizontal::Right)
|
||||
]
|
||||
.spacing(12)
|
||||
.align_items(Alignment::Center)
|
||||
),
|
||||
padded_control(
|
||||
row![
|
||||
icon::from_name(self.input_icon_name.as_str())
|
||||
.size(24)
|
||||
.symbolic(true),
|
||||
slider(0.0..=100.0, in_f64, Message::SetInputVolume)
|
||||
.width(Length::FillPortion(5)),
|
||||
text(format!("{}%", in_f64.round()))
|
||||
.size(16)
|
||||
.width(Length::FillPortion(1))
|
||||
.horizontal_alignment(Horizontal::Right)
|
||||
]
|
||||
.spacing(12)
|
||||
.align_items(Alignment::Center)
|
||||
),
|
||||
padded_control(divider::horizontal::default()),
|
||||
revealer(
|
||||
self.is_open == IsOpen::Output,
|
||||
fl!("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| (
|
||||
output.name.clone().unwrap_or_default(),
|
||||
pretty_name(output.description)
|
||||
))
|
||||
.collect(),
|
||||
Message::OutputToggle,
|
||||
Message::OutputChanged,
|
||||
),
|
||||
revealer(
|
||||
self.is_open == IsOpen::Input,
|
||||
fl!("input"),
|
||||
match &self.current_input {
|
||||
Some(input) => pretty_name(input.description.clone()),
|
||||
None => fl!("no-device"),
|
||||
},
|
||||
self.inputs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|input| (
|
||||
input.name.clone().unwrap_or_default(),
|
||||
pretty_name(input.description)
|
||||
))
|
||||
.collect(),
|
||||
Message::InputToggle,
|
||||
Message::InputChanged,
|
||||
)
|
||||
]
|
||||
.align_items(Alignment::Start)
|
||||
};
|
||||
|
||||
if let Some(s) = self.player_status.as_ref() {
|
||||
let mut elements = Vec::with_capacity(5);
|
||||
|
||||
if let Some(icon_path) = s.icon.clone() {
|
||||
elements.push(icon(icon::from_path(icon_path)).size(36).into());
|
||||
}
|
||||
|
||||
let title = if let Some(title) = s.title.as_ref() {
|
||||
if title.chars().count() > 15 {
|
||||
let mut title_trunc = title.chars().take(15).collect::<String>();
|
||||
title_trunc.push_str("...");
|
||||
title_trunc
|
||||
} else {
|
||||
title.to_string()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let artists = if let Some(artists) = s.artists.as_ref() {
|
||||
let artists = artists.join(", ");
|
||||
if artists.chars().count() > 15 {
|
||||
let mut artists_trunc = artists.chars().take(15).collect::<String>();
|
||||
artists_trunc.push_str("...");
|
||||
artists_trunc
|
||||
} else {
|
||||
artists
|
||||
}
|
||||
} else {
|
||||
fl!("unknown-artist")
|
||||
};
|
||||
|
||||
elements.push(column![text(title).size(14), text(artists).size(10),].into());
|
||||
elements.push(horizontal_space(Length::Fill).into());
|
||||
|
||||
if let Some(go_prev) = self.go_previous(32) {
|
||||
elements.push(go_prev);
|
||||
}
|
||||
if let Some(play) = self.is_play() {
|
||||
elements.push(
|
||||
button::icon(
|
||||
icon::from_name(if play { PLAY } else { PAUSE })
|
||||
.size(32)
|
||||
.symbolic(true),
|
||||
)
|
||||
.extra_small()
|
||||
.style(cosmic::theme::Button::AppletIcon)
|
||||
.on_press(if play {
|
||||
Message::MprisRequest(MprisRequest::Play)
|
||||
} else {
|
||||
Message::MprisRequest(MprisRequest::Pause)
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if let Some(go_next) = self.go_next(32) {
|
||||
elements.push(go_next);
|
||||
}
|
||||
|
||||
audio_content = audio_content.push(padded_control(divider::horizontal::default()));
|
||||
audio_content = audio_content.push(
|
||||
Row::with_children(elements)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
.padding(menu_control_padding()),
|
||||
);
|
||||
}
|
||||
let content = column![
|
||||
audio_content,
|
||||
padded_control(divider::horizontal::default()),
|
||||
container(
|
||||
anim!(
|
||||
// toggler
|
||||
SHOW_MEDIA_CONTROLS,
|
||||
&self.timeline,
|
||||
Some(fl!("show-media-controls")),
|
||||
self.config.show_media_controls_in_top_panel,
|
||||
Message::ToggleMediaControlsInTopPanel,
|
||||
)
|
||||
.text_size(14)
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.padding([0, 24]),
|
||||
padded_control(divider::horizontal::default()),
|
||||
menu_button(text(fl!("sound-settings")).size(14)).on_press(Message::OpenSettings)
|
||||
]
|
||||
.align_items(Alignment::Start)
|
||||
.padding([8, 0]);
|
||||
|
||||
self.core.applet.popup_container(container(content)).into()
|
||||
}
|
||||
|
||||
fn on_close_requested(&self, id: window::Id) -> Option<Message> {
|
||||
Some(Message::CloseRequested(id))
|
||||
}
|
||||
}
|
||||
|
||||
fn revealer(
|
||||
open: bool,
|
||||
title: String,
|
||||
selected: String,
|
||||
options: Vec<(String, String)>,
|
||||
toggle: Message,
|
||||
mut change: impl FnMut(String) -> Message + 'static,
|
||||
) -> widget::Column<'static, Message, crate::Theme, Renderer> {
|
||||
if open {
|
||||
options.iter().fold(
|
||||
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|
||||
|col, (id, name)| {
|
||||
col.push(
|
||||
menu_button(text(name).size(14))
|
||||
.on_press(change(id.clone()))
|
||||
.width(Length::Fill)
|
||||
.padding([8, 48]),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
column![revealer_head(open, title, selected, toggle)]
|
||||
}
|
||||
}
|
||||
|
||||
fn revealer_head(
|
||||
_open: bool,
|
||||
title: String,
|
||||
selected: String,
|
||||
toggle: Message,
|
||||
) -> cosmic::widget::Button<'static, Message, cosmic::Theme, Renderer> {
|
||||
menu_button(column![
|
||||
text(title).width(Length::Fill).size(14),
|
||||
text(selected).size(10),
|
||||
])
|
||||
.on_press(toggle)
|
||||
}
|
||||
|
||||
fn pretty_name(name: Option<String>) -> String {
|
||||
match name {
|
||||
Some(n) => n,
|
||||
None => String::from("Generic"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum PulseState {
|
||||
#[default]
|
||||
Init,
|
||||
Disconnected(pulse::Connection),
|
||||
Connected(pulse::Connection),
|
||||
}
|
||||
|
||||
impl PulseState {
|
||||
fn connection(&mut self) -> Option<&mut pulse::Connection> {
|
||||
match self {
|
||||
Self::Disconnected(c) => Some(c),
|
||||
Self::Connected(c) => Some(c),
|
||||
Self::Init => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn connected(&mut self) {
|
||||
if let Self::Disconnected(c) = self {
|
||||
*self = Self::Connected(c.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn disconnected(&mut self) {
|
||||
if let Self::Connected(c) = self {
|
||||
*self = Self::Disconnected(c.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IsOpen {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
|
@ -1,868 +1,10 @@
|
|||
mod localize;
|
||||
|
||||
use crate::localize::localize;
|
||||
use crate::pulse::DeviceInfo;
|
||||
use config::AudioAppletConfig;
|
||||
use cosmic::app::Command;
|
||||
use cosmic::applet::cosmic_panel_config::PanelAnchor;
|
||||
use cosmic::applet::menu_button;
|
||||
use cosmic::applet::menu_control_padding;
|
||||
use cosmic::applet::padded_control;
|
||||
use cosmic::applet::token::subscription::{
|
||||
activation_token_subscription, TokenRequest, TokenUpdate,
|
||||
};
|
||||
use cosmic::cctk::sctk::reexports::calloop;
|
||||
use cosmic::cosmic_config::CosmicConfigEntry;
|
||||
use cosmic::iced::widget;
|
||||
use cosmic::iced::Limits;
|
||||
use cosmic::iced::{
|
||||
self,
|
||||
widget::{column, row, slider, text},
|
||||
window, Alignment, Length, Subscription,
|
||||
};
|
||||
use cosmic::iced_runtime::core::alignment::Horizontal;
|
||||
use cosmic::iced_style::application;
|
||||
use cosmic::widget::button;
|
||||
use cosmic::widget::horizontal_space;
|
||||
use cosmic::widget::Column;
|
||||
use cosmic::widget::Row;
|
||||
use cosmic::widget::{divider, icon};
|
||||
use cosmic::Renderer;
|
||||
use cosmic::{Element, Theme};
|
||||
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
|
||||
use iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use iced::widget::container;
|
||||
use libpulse_binding::volume::VolumeLinear;
|
||||
use mpris2_zbus::player::PlaybackStatus;
|
||||
use mpris_subscription::MprisRequest;
|
||||
use mpris_subscription::MprisUpdate;
|
||||
|
||||
mod config;
|
||||
mod mpris_subscription;
|
||||
mod pipewire;
|
||||
mod pulse;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub fn main() -> cosmic::iced::Result {
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
|
||||
// Prepare i18n
|
||||
localize();
|
||||
|
||||
tracing::info!("Starting audio applet with version {VERSION}");
|
||||
|
||||
cosmic::applet::run::<Audio>(true, ())
|
||||
}
|
||||
|
||||
static SHOW_MEDIA_CONTROLS: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
|
||||
|
||||
const GO_BACK: &str = "media-skip-backward-symbolic";
|
||||
const GO_NEXT: &str = "media-skip-forward-symbolic";
|
||||
const PAUSE: &str = "media-playback-pause-symbolic";
|
||||
const PLAY: &str = "media-playback-start-symbolic";
|
||||
|
||||
#[derive(Default)]
|
||||
struct Audio {
|
||||
core: cosmic::app::Core,
|
||||
is_open: IsOpen,
|
||||
current_output: Option<DeviceInfo>,
|
||||
current_input: Option<DeviceInfo>,
|
||||
outputs: Vec<DeviceInfo>,
|
||||
inputs: Vec<DeviceInfo>,
|
||||
pulse_state: PulseState,
|
||||
icon_name: String,
|
||||
input_icon_name: String,
|
||||
popup: Option<window::Id>,
|
||||
timeline: Timeline,
|
||||
config: AudioAppletConfig,
|
||||
player_status: Option<mpris_subscription::PlayerStatus>,
|
||||
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
||||
}
|
||||
|
||||
impl Audio {
|
||||
fn update_output(&mut self, output: Option<DeviceInfo>) {
|
||||
self.current_output = output;
|
||||
self.apply_output_volume();
|
||||
}
|
||||
|
||||
fn apply_output_volume(&mut self) {
|
||||
let Some(output) = self.current_output.as_ref() else {
|
||||
self.icon_name = "audio-volume-muted-symbolic".to_string();
|
||||
return;
|
||||
};
|
||||
|
||||
let volume = output.volume.avg();
|
||||
let output_volume = VolumeLinear::from(volume).0;
|
||||
if volume.is_muted() {
|
||||
self.icon_name = "audio-volume-muted-symbolic".to_string();
|
||||
} else if output_volume < 0.25 {
|
||||
self.icon_name = "audio-volume-low-symbolic".to_string();
|
||||
} else if output_volume < 0.5 {
|
||||
self.icon_name = "audio-volume-medium-symbolic".to_string();
|
||||
} else if output_volume < 0.75 {
|
||||
self.icon_name = "audio-volume-high-symbolic".to_string();
|
||||
} else {
|
||||
self.icon_name = "audio-volume-overamplified-symbolic".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_input(&mut self, input: Option<DeviceInfo>) {
|
||||
self.current_input = input;
|
||||
self.apply_input_volume();
|
||||
}
|
||||
|
||||
fn apply_input_volume(&mut self) {
|
||||
let Some(input) = self.current_input.as_ref() else {
|
||||
self.input_icon_name = "microphone-sensitivity-muted-symbolic".to_string();
|
||||
return;
|
||||
};
|
||||
|
||||
let volume = input.volume.avg();
|
||||
let input_volume = VolumeLinear::from(volume).0;
|
||||
if volume.is_muted() {
|
||||
self.input_icon_name = "microphone-sensitivity-muted-symbolic".to_string();
|
||||
} else if input_volume < 0.33 {
|
||||
self.input_icon_name = "microphone-sensitivity-low-symbolic".to_string();
|
||||
} else if input_volume < 0.66 {
|
||||
self.input_icon_name = "microphone-sensitivity-medium-symbolic".to_string();
|
||||
} else {
|
||||
self.input_icon_name = "microphone-sensitivity-high-symbolic".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum IsOpen {
|
||||
None,
|
||||
Output,
|
||||
Input,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
SetOutputVolume(f64),
|
||||
SetInputVolume(f64),
|
||||
OutputToggle,
|
||||
InputToggle,
|
||||
OutputChanged(String),
|
||||
InputChanged(String),
|
||||
Pulse(pulse::Event),
|
||||
TogglePopup,
|
||||
CloseRequested(window::Id),
|
||||
ToggleMediaControlsInTopPanel(chain::Toggler, bool),
|
||||
Frame(Instant),
|
||||
ConfigChanged(AudioAppletConfig),
|
||||
Mpris(mpris_subscription::MprisUpdate),
|
||||
MprisRequest(MprisRequest),
|
||||
Token(TokenUpdate),
|
||||
OpenSettings,
|
||||
}
|
||||
|
||||
impl Audio {
|
||||
fn playback_buttons(&self) -> Option<Element<Message>> {
|
||||
if self.player_status.is_some() && self.config.show_media_controls_in_top_panel {
|
||||
let mut elements = Vec::with_capacity(3);
|
||||
if self
|
||||
.player_status
|
||||
.as_ref()
|
||||
.map(|s| s.can_go_previous)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
elements.push(self.core.applet.icon_button(GO_BACK).into())
|
||||
}
|
||||
if let Some(play) = self.is_play() {
|
||||
elements.push(
|
||||
self.core
|
||||
.applet
|
||||
.icon_button(if play { PLAY } else { PAUSE })
|
||||
.on_press(if play {
|
||||
Message::MprisRequest(MprisRequest::Play)
|
||||
} else {
|
||||
Message::MprisRequest(MprisRequest::Pause)
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if self
|
||||
.player_status
|
||||
.as_ref()
|
||||
.map(|s| s.can_go_next)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
elements.push(self.core.applet.icon_button(GO_NEXT).into())
|
||||
}
|
||||
|
||||
Some(match self.core.applet.anchor {
|
||||
PanelAnchor::Left | PanelAnchor::Right => Column::with_children(elements)
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
PanelAnchor::Top | PanelAnchor::Bottom => Row::with_children(elements)
|
||||
.align_items(Alignment::Center)
|
||||
.into(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn go_previous(&self, icon_size: u16) -> Option<Element<Message>> {
|
||||
self.player_status.as_ref().and_then(|s| {
|
||||
if s.can_go_previous {
|
||||
Some(
|
||||
button::icon(icon::from_name(GO_BACK).size(icon_size).symbolic(true))
|
||||
.extra_small()
|
||||
.style(cosmic::theme::Button::AppletIcon)
|
||||
.on_press(Message::MprisRequest(MprisRequest::Previous))
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn go_next(&self, icon_size: u16) -> Option<Element<Message>> {
|
||||
self.player_status.as_ref().and_then(|s| {
|
||||
if s.can_go_next {
|
||||
Some(
|
||||
button::icon(icon::from_name(GO_NEXT).size(icon_size).symbolic(true))
|
||||
.extra_small()
|
||||
.style(cosmic::theme::Button::AppletIcon)
|
||||
.on_press(Message::MprisRequest(MprisRequest::Next))
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn is_play(&self) -> Option<bool> {
|
||||
self.player_status.as_ref().and_then(|s| match s.status {
|
||||
PlaybackStatus::Playing => {
|
||||
if s.can_pause {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
PlaybackStatus::Paused | PlaybackStatus::Stopped => {
|
||||
if s.can_play {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl cosmic::Application for Audio {
|
||||
type Message = Message;
|
||||
type Executor = cosmic::SingleThreadExecutor;
|
||||
type Flags = ();
|
||||
const APP_ID: &'static str = "com.system76.CosmicAppletAudio";
|
||||
|
||||
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Self {
|
||||
core,
|
||||
is_open: IsOpen::None,
|
||||
current_output: None,
|
||||
current_input: None,
|
||||
outputs: vec![],
|
||||
inputs: vec![],
|
||||
icon_name: "audio-volume-high-symbolic".to_string(),
|
||||
input_icon_name: "audio-input-microphone-symbolic".to_string(),
|
||||
token_tx: None,
|
||||
..Default::default()
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn core(&self) -> &cosmic::app::Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut cosmic::app::Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
||||
Some(cosmic::applet::style())
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::Frame(now) => self.timeline.now(now),
|
||||
Message::TogglePopup => {
|
||||
if let Some(p) = self.popup.take() {
|
||||
return destroy_popup(p);
|
||||
} else {
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::UpdateConnection);
|
||||
}
|
||||
let new_id = window::Id::unique();
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.core.applet.get_popup_settings(
|
||||
window::Id::MAIN,
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_height(1.0)
|
||||
.min_width(1.0)
|
||||
.max_width(400.0)
|
||||
.max_height(1080.0);
|
||||
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::GetDefaultSink);
|
||||
conn.send(pulse::Message::GetDefaultSource);
|
||||
conn.send(pulse::Message::GetSinks);
|
||||
conn.send(pulse::Message::GetSources);
|
||||
}
|
||||
|
||||
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())
|
||||
});
|
||||
self.apply_output_volume();
|
||||
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(),
|
||||
device.volume,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SetInputVolume(vol) => {
|
||||
self.current_input.as_mut().map(|i| {
|
||||
i.volume
|
||||
.set(i.volume.len(), VolumeLinear(vol / 100.0).into())
|
||||
});
|
||||
self.apply_input_volume();
|
||||
if let PulseState::Connected(connection) = &mut self.pulse_state {
|
||||
if let Some(device) = &self.current_input {
|
||||
if let Some(name) = &device.name {
|
||||
tracing::info!("increasing volume of {}", name);
|
||||
connection.send(pulse::Message::SetSourceVolumeByName(
|
||||
name.clone(),
|
||||
device.volume,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::OutputChanged(val) => {
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
if let Some(val) = self.outputs.iter().find(|o| o.name.as_ref() == Some(&val)) {
|
||||
conn.send(pulse::Message::SetDefaultSink(val.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::InputChanged(val) => {
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
if let Some(val) = self.inputs.iter().find(|i| i.name.as_ref() == Some(&val)) {
|
||||
conn.send(pulse::Message::SetDefaultSource(val.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::OutputToggle => {
|
||||
self.is_open = if self.is_open == IsOpen::Output {
|
||||
IsOpen::None
|
||||
} else {
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::GetSinks);
|
||||
}
|
||||
IsOpen::Output
|
||||
}
|
||||
}
|
||||
Message::InputToggle => {
|
||||
self.is_open = if self.is_open == IsOpen::Input {
|
||||
IsOpen::None
|
||||
} else {
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::GetSources);
|
||||
}
|
||||
IsOpen::Input
|
||||
}
|
||||
}
|
||||
Message::Pulse(event) => match event {
|
||||
pulse::Event::Init(mut conn) => {
|
||||
conn.send(pulse::Message::UpdateConnection);
|
||||
self.pulse_state = PulseState::Disconnected(conn);
|
||||
}
|
||||
pulse::Event::Connected => {
|
||||
self.pulse_state.connected();
|
||||
|
||||
if let Some(conn) = self.pulse_state.connection() {
|
||||
conn.send(pulse::Message::GetSinks);
|
||||
conn.send(pulse::Message::GetSources);
|
||||
conn.send(pulse::Message::GetDefaultSink);
|
||||
conn.send(pulse::Message::GetDefaultSource);
|
||||
}
|
||||
}
|
||||
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.update_output(Some(sink));
|
||||
}
|
||||
pulse::Message::SetDefaultSource(source) => {
|
||||
self.update_input(Some(source));
|
||||
}
|
||||
pulse::Message::Disconnected => {
|
||||
panic!("Subscription error handling is bad. This should never happen.")
|
||||
}
|
||||
_ => {
|
||||
tracing::trace!("Received misc message")
|
||||
}
|
||||
}
|
||||
}
|
||||
pulse::Event::Disconnected => {
|
||||
self.pulse_state.disconnected();
|
||||
if let Some(mut conn) = self.pulse_state.connection().cloned() {
|
||||
_ = tokio::spawn(async move {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
|
||||
conn.send(pulse::Message::UpdateConnection);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::ToggleMediaControlsInTopPanel(chain, enabled) => {
|
||||
self.timeline.set_chain(chain).start();
|
||||
self.config.show_media_controls_in_top_panel = enabled;
|
||||
if let Ok(helper) =
|
||||
cosmic::cosmic_config::Config::new(Self::APP_ID, AudioAppletConfig::VERSION)
|
||||
{
|
||||
if let Err(err) = self.config.write_entry(&helper) {
|
||||
tracing::error!(?err, "Error writing config");
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::CloseRequested(id) => {
|
||||
if Some(id) == self.popup {
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
Message::ConfigChanged(c) => {
|
||||
self.config = c;
|
||||
}
|
||||
Message::Mpris(mpris_subscription::MprisUpdate::Player(p)) => {
|
||||
self.player_status = Some(p);
|
||||
}
|
||||
Message::Mpris(MprisUpdate::Finished) => {
|
||||
self.player_status = None;
|
||||
}
|
||||
Message::Mpris(MprisUpdate::Setup) => {
|
||||
self.player_status = None;
|
||||
}
|
||||
Message::MprisRequest(r) => {
|
||||
let Some(player_status) = self.player_status.as_ref() else {
|
||||
tracing::error!("No player found");
|
||||
return Command::none();
|
||||
};
|
||||
let player = player_status.player.clone();
|
||||
|
||||
match r {
|
||||
MprisRequest::Play => tokio::spawn(async move {
|
||||
let res = player.play().await;
|
||||
if let Err(err) = res {
|
||||
tracing::error!("Error playing: {}", err);
|
||||
}
|
||||
}),
|
||||
MprisRequest::Pause => tokio::spawn(async move {
|
||||
let res = player.pause().await;
|
||||
if let Err(err) = res {
|
||||
tracing::error!("Error pausing: {}", err);
|
||||
}
|
||||
}),
|
||||
MprisRequest::Next => tokio::spawn(async move {
|
||||
let res = player.next().await;
|
||||
if let Err(err) = res {
|
||||
tracing::error!("Error playing next: {}", err);
|
||||
}
|
||||
}),
|
||||
MprisRequest::Previous => tokio::spawn(async move {
|
||||
let res = player.previous().await;
|
||||
if let Err(err) = res {
|
||||
tracing::error!("Error playing previous: {}", err);
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
Message::OpenSettings => {
|
||||
let exec = "cosmic-settings sound".to_string();
|
||||
if let Some(tx) = self.token_tx.as_ref() {
|
||||
let _ = tx.send(TokenRequest {
|
||||
app_id: Self::APP_ID.to_string(),
|
||||
exec,
|
||||
});
|
||||
} else {
|
||||
tracing::error!("Wayland tx is None");
|
||||
};
|
||||
}
|
||||
Message::Token(u) => match u {
|
||||
TokenUpdate::Init(tx) => {
|
||||
self.token_tx = Some(tx);
|
||||
}
|
||||
TokenUpdate::Finished => {
|
||||
self.token_tx = None;
|
||||
}
|
||||
TokenUpdate::ActivationToken { token, .. } => {
|
||||
let mut cmd = std::process::Command::new("cosmic-settings");
|
||||
cmd.arg("sound");
|
||||
if let Some(token) = token {
|
||||
cmd.env("XDG_ACTIVATION_TOKEN", &token);
|
||||
cmd.env("DESKTOP_STARTUP_ID", &token);
|
||||
}
|
||||
cosmic::process::spawn(cmd);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch(vec![
|
||||
pulse::connect().map(Message::Pulse),
|
||||
self.timeline
|
||||
.as_subscription()
|
||||
.map(|(_, now)| Message::Frame(now)),
|
||||
self.core.watch_config(Self::APP_ID).map(|u| {
|
||||
for err in u.errors {
|
||||
tracing::error!(?err, "Error watching config");
|
||||
}
|
||||
Message::ConfigChanged(u.config)
|
||||
}),
|
||||
mpris_subscription::mpris_subscription(0).map(Message::Mpris),
|
||||
activation_token_subscription(0).map(Message::Token),
|
||||
])
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let btn = self
|
||||
.core
|
||||
.applet
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup);
|
||||
if let Some(playback_buttons) = self.playback_buttons() {
|
||||
match self.core.applet.anchor {
|
||||
PanelAnchor::Left | PanelAnchor::Right => {
|
||||
Column::with_children(vec![playback_buttons, btn.into()])
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
PanelAnchor::Top | PanelAnchor::Bottom => {
|
||||
Row::with_children(vec![playback_buttons, btn.into()])
|
||||
.align_items(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
btn.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn view_window(&self, _id: window::Id) -> Element<Message> {
|
||||
let audio_disabled = matches!(self.pulse_state, PulseState::Disconnected(_));
|
||||
let out_f64 = VolumeLinear::from(
|
||||
self.current_output
|
||||
.as_ref()
|
||||
.map(|o| o.volume.avg())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.0 * 100.0;
|
||||
let in_f64 = VolumeLinear::from(
|
||||
self.current_input
|
||||
.as_ref()
|
||||
.map(|o| o.volume.avg())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.0 * 100.0;
|
||||
|
||||
let mut audio_content = if audio_disabled {
|
||||
column![padded_control(
|
||||
text(fl!("disconnected"))
|
||||
.width(Length::Fill)
|
||||
.horizontal_alignment(Horizontal::Center)
|
||||
.size(24)
|
||||
)]
|
||||
} else {
|
||||
column![
|
||||
padded_control(
|
||||
row![
|
||||
icon::from_name(self.icon_name.as_str())
|
||||
.size(24)
|
||||
.symbolic(true),
|
||||
slider(0.0..=100.0, out_f64, Message::SetOutputVolume)
|
||||
.width(Length::FillPortion(5)),
|
||||
text(format!("{}%", out_f64.round()))
|
||||
.size(16)
|
||||
.width(Length::FillPortion(1))
|
||||
.horizontal_alignment(Horizontal::Right)
|
||||
]
|
||||
.spacing(12)
|
||||
.align_items(Alignment::Center)
|
||||
),
|
||||
padded_control(
|
||||
row![
|
||||
icon::from_name(self.input_icon_name.as_str())
|
||||
.size(24)
|
||||
.symbolic(true),
|
||||
slider(0.0..=100.0, in_f64, Message::SetInputVolume)
|
||||
.width(Length::FillPortion(5)),
|
||||
text(format!("{}%", in_f64.round()))
|
||||
.size(16)
|
||||
.width(Length::FillPortion(1))
|
||||
.horizontal_alignment(Horizontal::Right)
|
||||
]
|
||||
.spacing(12)
|
||||
.align_items(Alignment::Center)
|
||||
),
|
||||
padded_control(divider::horizontal::default()),
|
||||
revealer(
|
||||
self.is_open == IsOpen::Output,
|
||||
fl!("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| (
|
||||
output.name.clone().unwrap_or_default(),
|
||||
pretty_name(output.description)
|
||||
))
|
||||
.collect(),
|
||||
Message::OutputToggle,
|
||||
Message::OutputChanged,
|
||||
),
|
||||
revealer(
|
||||
self.is_open == IsOpen::Input,
|
||||
fl!("input"),
|
||||
match &self.current_input {
|
||||
Some(input) => pretty_name(input.description.clone()),
|
||||
None => fl!("no-device"),
|
||||
},
|
||||
self.inputs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|input| (
|
||||
input.name.clone().unwrap_or_default(),
|
||||
pretty_name(input.description)
|
||||
))
|
||||
.collect(),
|
||||
Message::InputToggle,
|
||||
Message::InputChanged,
|
||||
)
|
||||
]
|
||||
.align_items(Alignment::Start)
|
||||
};
|
||||
|
||||
if let Some(s) = self.player_status.as_ref() {
|
||||
let mut elements = Vec::with_capacity(5);
|
||||
|
||||
if let Some(icon_path) = s.icon.clone() {
|
||||
elements.push(icon(icon::from_path(icon_path)).size(36).into());
|
||||
}
|
||||
|
||||
let title = if let Some(title) = s.title.as_ref() {
|
||||
if title.chars().count() > 15 {
|
||||
let mut title_trunc = title.chars().take(15).collect::<String>();
|
||||
title_trunc.push_str("...");
|
||||
title_trunc
|
||||
} else {
|
||||
title.to_string()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let artists = if let Some(artists) = s.artists.as_ref() {
|
||||
let artists = artists.join(", ");
|
||||
if artists.chars().count() > 15 {
|
||||
let mut artists_trunc = artists.chars().take(15).collect::<String>();
|
||||
artists_trunc.push_str("...");
|
||||
artists_trunc
|
||||
} else {
|
||||
artists
|
||||
}
|
||||
} else {
|
||||
fl!("unknown-artist")
|
||||
};
|
||||
|
||||
elements.push(column![text(title).size(14), text(artists).size(10),].into());
|
||||
elements.push(horizontal_space(Length::Fill).into());
|
||||
|
||||
if let Some(go_prev) = self.go_previous(32) {
|
||||
elements.push(go_prev);
|
||||
}
|
||||
if let Some(play) = self.is_play() {
|
||||
elements.push(
|
||||
button::icon(
|
||||
icon::from_name(if play { PLAY } else { PAUSE })
|
||||
.size(32)
|
||||
.symbolic(true),
|
||||
)
|
||||
.extra_small()
|
||||
.style(cosmic::theme::Button::AppletIcon)
|
||||
.on_press(if play {
|
||||
Message::MprisRequest(MprisRequest::Play)
|
||||
} else {
|
||||
Message::MprisRequest(MprisRequest::Pause)
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if let Some(go_next) = self.go_next(32) {
|
||||
elements.push(go_next);
|
||||
}
|
||||
|
||||
audio_content = audio_content.push(padded_control(divider::horizontal::default()));
|
||||
audio_content = audio_content.push(
|
||||
Row::with_children(elements)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
.padding(menu_control_padding()),
|
||||
);
|
||||
}
|
||||
let content = column![
|
||||
audio_content,
|
||||
padded_control(divider::horizontal::default()),
|
||||
container(
|
||||
anim!(
|
||||
// toggler
|
||||
SHOW_MEDIA_CONTROLS,
|
||||
&self.timeline,
|
||||
Some(fl!("show-media-controls")),
|
||||
self.config.show_media_controls_in_top_panel,
|
||||
Message::ToggleMediaControlsInTopPanel,
|
||||
)
|
||||
.text_size(14)
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.padding([0, 24]),
|
||||
padded_control(divider::horizontal::default()),
|
||||
menu_button(text(fl!("sound-settings")).size(14)).on_press(Message::OpenSettings)
|
||||
]
|
||||
.align_items(Alignment::Start)
|
||||
.padding([8, 0]);
|
||||
|
||||
self.core.applet.popup_container(container(content)).into()
|
||||
}
|
||||
|
||||
fn on_close_requested(&self, id: window::Id) -> Option<Message> {
|
||||
Some(Message::CloseRequested(id))
|
||||
}
|
||||
}
|
||||
|
||||
fn revealer(
|
||||
open: bool,
|
||||
title: String,
|
||||
selected: String,
|
||||
options: Vec<(String, String)>,
|
||||
toggle: Message,
|
||||
mut change: impl FnMut(String) -> Message + 'static,
|
||||
) -> widget::Column<'static, Message, crate::Theme, Renderer> {
|
||||
if open {
|
||||
options.iter().fold(
|
||||
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|
||||
|col, (id, name)| {
|
||||
col.push(
|
||||
menu_button(text(name).size(14))
|
||||
.on_press(change(id.clone()))
|
||||
.width(Length::Fill)
|
||||
.padding([8, 48]),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
column![revealer_head(open, title, selected, toggle)]
|
||||
}
|
||||
}
|
||||
|
||||
fn revealer_head(
|
||||
_open: bool,
|
||||
title: String,
|
||||
selected: String,
|
||||
toggle: Message,
|
||||
) -> cosmic::widget::Button<'static, Message, cosmic::Theme, Renderer> {
|
||||
menu_button(column![
|
||||
text(title).width(Length::Fill).size(14),
|
||||
text(selected).size(10),
|
||||
])
|
||||
.on_press(toggle)
|
||||
}
|
||||
|
||||
fn pretty_name(name: Option<String>) -> String {
|
||||
match name {
|
||||
Some(n) => n,
|
||||
None => String::from("Generic"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum PulseState {
|
||||
#[default]
|
||||
Init,
|
||||
Disconnected(pulse::Connection),
|
||||
Connected(pulse::Connection),
|
||||
}
|
||||
|
||||
impl PulseState {
|
||||
fn connection(&mut self) -> Option<&mut pulse::Connection> {
|
||||
match self {
|
||||
Self::Disconnected(c) => Some(c),
|
||||
Self::Connected(c) => Some(c),
|
||||
Self::Init => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn connected(&mut self) {
|
||||
if let Self::Disconnected(c) = self {
|
||||
*self = Self::Connected(c.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn disconnected(&mut self) {
|
||||
if let Self::Connected(c) = self {
|
||||
*self = Self::Disconnected(c.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IsOpen {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
cosmic_applet_audio::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
pub const APP_ID: &str = "com.system76.CosmicAppletButton";
|
||||
pub const PROFILE: &str = "";
|
||||
pub const VERSION: &str = "0.1.0";
|
||||
|
|
|
|||
17
cosmic-applet-battery/src/lib.rs
Normal file
17
cosmic-applet-battery/src/lib.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#[rustfmt::skip]
|
||||
mod backlight;
|
||||
mod app;
|
||||
mod config;
|
||||
mod dgpu;
|
||||
mod localize;
|
||||
mod power_daemon;
|
||||
mod upower;
|
||||
mod upower_device;
|
||||
mod upower_kbdbacklight;
|
||||
|
||||
use localize::localize;
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize();
|
||||
app::run()
|
||||
}
|
||||
|
|
@ -1,29 +1,10 @@
|
|||
#[rustfmt::skip]
|
||||
mod backlight;
|
||||
mod app;
|
||||
mod config;
|
||||
mod dgpu;
|
||||
mod localize;
|
||||
mod power_daemon;
|
||||
mod upower;
|
||||
mod upower_device;
|
||||
mod upower_kbdbacklight;
|
||||
use config::APP_ID;
|
||||
use log::info;
|
||||
|
||||
use localize::localize;
|
||||
|
||||
use crate::config::{PROFILE, VERSION};
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
// Initialize logger
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
info!("Iced Workspaces Applet ({})", APP_ID);
|
||||
info!("Version: {} ({})", VERSION, PROFILE);
|
||||
|
||||
// Prepare i18n
|
||||
localize();
|
||||
tracing::info!("Starting battery applet with version {VERSION}");
|
||||
|
||||
app::run()
|
||||
cosmic_applet_battery::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ libcosmic.workspace = true
|
|||
cosmic-time.workspace = true
|
||||
futures = "0.3"
|
||||
log = "0.4"
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-log.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
itertools = "0.10.3"
|
||||
slotmap = "1.0.6"
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
pub const APP_ID: &str = "com.system76.CosmicAppletBluetooth";
|
||||
pub const PROFILE: &str = "";
|
||||
pub const VERSION: &str = "0.1.0";
|
||||
|
|
|
|||
13
cosmic-applet-bluetooth/src/lib.rs
Normal file
13
cosmic-applet-bluetooth/src/lib.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
mod app;
|
||||
mod bluetooth;
|
||||
mod config;
|
||||
mod localize;
|
||||
|
||||
use crate::localize::localize;
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize();
|
||||
app::run()
|
||||
}
|
||||
|
|
@ -1,24 +1,10 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
mod app;
|
||||
mod bluetooth;
|
||||
mod config;
|
||||
mod localize;
|
||||
|
||||
use log::info;
|
||||
|
||||
use crate::config::{APP_ID, PROFILE, VERSION};
|
||||
use crate::localize::localize;
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
// Initialize logger
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
info!("Iced Workspaces Applet ({})", APP_ID);
|
||||
info!("Version: {} ({})", VERSION, PROFILE);
|
||||
|
||||
// Prepare i18n
|
||||
localize();
|
||||
tracing::info!("Starting bluetooth applet with version {VERSION}");
|
||||
|
||||
app::run()
|
||||
cosmic_applet_bluetooth::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ memmap2 = "0.9.0"
|
|||
rustix = { version = "0.38.0", features = ["fs"] }
|
||||
png = "0.17.5"
|
||||
tokio = { version = "1.17.0", features = ["sync", "macros"] }
|
||||
tracing = "0.1.40"
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-log.workspace = true
|
||||
tempfile = "3.5.0"
|
||||
|
|
|
|||
179
cosmic-applet-minimize/src/lib.rs
Normal file
179
cosmic-applet-minimize/src/lib.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
mod localize;
|
||||
pub(crate) mod wayland_handler;
|
||||
pub(crate) mod wayland_subscription;
|
||||
pub(crate) mod window_image;
|
||||
|
||||
use crate::localize::localize;
|
||||
use cosmic::app::Command;
|
||||
use cosmic::applet::cosmic_panel_config::PanelAnchor;
|
||||
use cosmic::cctk::cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
|
||||
use cosmic::cctk::sctk::reexports::calloop;
|
||||
use cosmic::cctk::toplevel_info::ToplevelInfo;
|
||||
use cosmic::desktop::DesktopEntryData;
|
||||
use cosmic::iced::{widget::text, Length, Subscription};
|
||||
|
||||
use cosmic::iced_style::application;
|
||||
use cosmic::iced_widget::{Column, Row};
|
||||
|
||||
use cosmic::widget::tooltip;
|
||||
use cosmic::{Element, Theme};
|
||||
use wayland_subscription::{
|
||||
ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
|
||||
};
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize();
|
||||
cosmic::applet::run::<Minimize>(true, ())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Minimize {
|
||||
core: cosmic::app::Core,
|
||||
apps: Vec<(
|
||||
ZcosmicToplevelHandleV1,
|
||||
ToplevelInfo,
|
||||
DesktopEntryData,
|
||||
Option<WaylandImage>,
|
||||
)>,
|
||||
tx: Option<calloop::channel::Sender<WaylandRequest>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Wayland(WaylandUpdate),
|
||||
Activate(ZcosmicToplevelHandleV1),
|
||||
}
|
||||
|
||||
impl cosmic::Application for Minimize {
|
||||
type Message = Message;
|
||||
type Executor = cosmic::SingleThreadExecutor;
|
||||
type Flags = ();
|
||||
const APP_ID: &'static str = "com.system76.CosmicAppletMinimize";
|
||||
|
||||
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Self {
|
||||
core,
|
||||
..Default::default()
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn core(&self) -> &cosmic::app::Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut cosmic::app::Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
||||
Some(cosmic::applet::style())
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::Wayland(update) => match update {
|
||||
WaylandUpdate::Init(tx) => {
|
||||
self.tx = Some(tx);
|
||||
}
|
||||
WaylandUpdate::Finished => {
|
||||
panic!("Wayland Subscription ended...")
|
||||
}
|
||||
WaylandUpdate::Toplevel(t) => match t {
|
||||
ToplevelUpdate::Add(handle, info) | ToplevelUpdate::Update(handle, info) => {
|
||||
let data = |id| {
|
||||
cosmic::desktop::load_applications_for_app_ids(
|
||||
None,
|
||||
std::iter::once(id),
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.remove(0)
|
||||
};
|
||||
if let Some(pos) = self.apps.iter_mut().position(|a| a.0 == handle) {
|
||||
if self.apps[pos].1.app_id != info.app_id {
|
||||
self.apps[pos].2 = data(&info.app_id)
|
||||
}
|
||||
self.apps[pos].1 = info;
|
||||
} else {
|
||||
let data = data(&info.app_id);
|
||||
self.apps.push((handle, info, data, None));
|
||||
}
|
||||
}
|
||||
ToplevelUpdate::Remove(handle) => self.apps.retain(|a| a.0 != handle),
|
||||
},
|
||||
WaylandUpdate::Image(handle, img) => {
|
||||
if let Some(pos) = self.apps.iter().position(|a| a.0 == handle) {
|
||||
self.apps[pos].3 = Some(img);
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::Activate(handle) => {
|
||||
if let Some(tx) = self.tx.as_ref() {
|
||||
let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle)));
|
||||
}
|
||||
}
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
wayland_subscription::wayland_subscription().map(Message::Wayland)
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let (width, _) = self.core.applet.suggested_size();
|
||||
let theme = self.core.system_theme().cosmic();
|
||||
let space_xxs = theme.space_xxs();
|
||||
let icon_buttons = self.apps.iter().map(|(handle, _, data, img)| {
|
||||
tooltip(
|
||||
Element::from(crate::window_image::WindowImage::new(
|
||||
img.clone(),
|
||||
&data.icon,
|
||||
width as f32,
|
||||
Message::Activate(handle.clone()),
|
||||
space_xxs,
|
||||
)),
|
||||
data.name.clone(),
|
||||
// tooltip::Position::FollowCursor,
|
||||
// FIXME tooltip fails to appear when created as indicated in design
|
||||
// maybe it should be a subsurface
|
||||
match self.core.applet.anchor {
|
||||
PanelAnchor::Left => tooltip::Position::Right,
|
||||
PanelAnchor::Right => tooltip::Position::Left,
|
||||
PanelAnchor::Top => tooltip::Position::Bottom,
|
||||
PanelAnchor::Bottom => tooltip::Position::Top,
|
||||
},
|
||||
)
|
||||
.snap_within_viewport(false)
|
||||
.text_shaping(text::Shaping::Advanced)
|
||||
.into()
|
||||
});
|
||||
|
||||
// TODO optional dividers on ends if detects app list neighbor
|
||||
// not sure the best way to tell if there is an adjacent app-list
|
||||
|
||||
if matches!(
|
||||
self.core.applet.anchor,
|
||||
PanelAnchor::Top | PanelAnchor::Bottom
|
||||
) {
|
||||
Row::with_children(icon_buttons)
|
||||
.align_items(cosmic::iced_core::Alignment::Center)
|
||||
.height(Length::Shrink)
|
||||
.width(Length::Shrink)
|
||||
.spacing(space_xxs)
|
||||
.padding([0, space_xxs])
|
||||
.into()
|
||||
} else {
|
||||
Column::with_children(icon_buttons)
|
||||
.align_items(cosmic::iced_core::Alignment::Center)
|
||||
.height(Length::Shrink)
|
||||
.width(Length::Shrink)
|
||||
.spacing(space_xxs)
|
||||
.padding([space_xxs, 0])
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,188 +1,10 @@
|
|||
mod localize;
|
||||
pub(crate) mod wayland_handler;
|
||||
pub(crate) mod wayland_subscription;
|
||||
pub(crate) mod window_image;
|
||||
|
||||
use crate::localize::localize;
|
||||
use cosmic::app::Command;
|
||||
use cosmic::applet::cosmic_panel_config::PanelAnchor;
|
||||
use cosmic::cctk::cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
|
||||
use cosmic::cctk::sctk::reexports::calloop;
|
||||
use cosmic::cctk::toplevel_info::ToplevelInfo;
|
||||
use cosmic::desktop::DesktopEntryData;
|
||||
use cosmic::iced::{widget::text, Length, Subscription};
|
||||
|
||||
use cosmic::iced_style::application;
|
||||
use cosmic::iced_widget::{Column, Row};
|
||||
|
||||
use cosmic::widget::tooltip;
|
||||
use cosmic::{Element, Theme};
|
||||
use wayland_subscription::{
|
||||
ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
|
||||
};
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub fn main() -> cosmic::iced::Result {
|
||||
fn main() -> cosmic::iced::Result {
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
|
||||
// Prepare i18n
|
||||
localize();
|
||||
|
||||
tracing::info!("Starting minimize applet with version {VERSION}");
|
||||
|
||||
cosmic::applet::run::<Minimize>(true, ())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Minimize {
|
||||
core: cosmic::app::Core,
|
||||
apps: Vec<(
|
||||
ZcosmicToplevelHandleV1,
|
||||
ToplevelInfo,
|
||||
DesktopEntryData,
|
||||
Option<WaylandImage>,
|
||||
)>,
|
||||
tx: Option<calloop::channel::Sender<WaylandRequest>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Wayland(WaylandUpdate),
|
||||
Activate(ZcosmicToplevelHandleV1),
|
||||
}
|
||||
|
||||
impl cosmic::Application for Minimize {
|
||||
type Message = Message;
|
||||
type Executor = cosmic::SingleThreadExecutor;
|
||||
type Flags = ();
|
||||
const APP_ID: &'static str = "com.system76.CosmicAppletMinimize";
|
||||
|
||||
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Self {
|
||||
core,
|
||||
..Default::default()
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn core(&self) -> &cosmic::app::Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut cosmic::app::Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
||||
Some(cosmic::applet::style())
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::Wayland(update) => match update {
|
||||
WaylandUpdate::Init(tx) => {
|
||||
self.tx = Some(tx);
|
||||
}
|
||||
WaylandUpdate::Finished => {
|
||||
panic!("Wayland Subscription ended...")
|
||||
}
|
||||
WaylandUpdate::Toplevel(t) => match t {
|
||||
ToplevelUpdate::Add(handle, info) | ToplevelUpdate::Update(handle, info) => {
|
||||
let data = |id| {
|
||||
cosmic::desktop::load_applications_for_app_ids(
|
||||
None,
|
||||
std::iter::once(id),
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.remove(0)
|
||||
};
|
||||
if let Some(pos) = self.apps.iter_mut().position(|a| a.0 == handle) {
|
||||
if self.apps[pos].1.app_id != info.app_id {
|
||||
self.apps[pos].2 = data(&info.app_id)
|
||||
}
|
||||
self.apps[pos].1 = info;
|
||||
} else {
|
||||
let data = data(&info.app_id);
|
||||
self.apps.push((handle, info, data, None));
|
||||
}
|
||||
}
|
||||
ToplevelUpdate::Remove(handle) => self.apps.retain(|a| a.0 != handle),
|
||||
},
|
||||
WaylandUpdate::Image(handle, img) => {
|
||||
if let Some(pos) = self.apps.iter().position(|a| a.0 == handle) {
|
||||
self.apps[pos].3 = Some(img);
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::Activate(handle) => {
|
||||
if let Some(tx) = self.tx.as_ref() {
|
||||
let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle)));
|
||||
}
|
||||
}
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
wayland_subscription::wayland_subscription().map(Message::Wayland)
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let (width, _) = self.core.applet.suggested_size();
|
||||
let theme = self.core.system_theme().cosmic();
|
||||
let space_xxs = theme.space_xxs();
|
||||
let icon_buttons = self.apps.iter().map(|(handle, _, data, img)| {
|
||||
tooltip(
|
||||
Element::from(crate::window_image::WindowImage::new(
|
||||
img.clone(),
|
||||
&data.icon,
|
||||
width as f32,
|
||||
Message::Activate(handle.clone()),
|
||||
space_xxs,
|
||||
)),
|
||||
data.name.clone(),
|
||||
// tooltip::Position::FollowCursor,
|
||||
// FIXME tooltip fails to appear when created as indicated in design
|
||||
// maybe it should be a subsurface
|
||||
match self.core.applet.anchor {
|
||||
PanelAnchor::Left => tooltip::Position::Right,
|
||||
PanelAnchor::Right => tooltip::Position::Left,
|
||||
PanelAnchor::Top => tooltip::Position::Bottom,
|
||||
PanelAnchor::Bottom => tooltip::Position::Top,
|
||||
},
|
||||
)
|
||||
.snap_within_viewport(false)
|
||||
.text_shaping(text::Shaping::Advanced)
|
||||
.into()
|
||||
});
|
||||
|
||||
// TODO optional dividers on ends if detects app list neighbor
|
||||
// not sure the best way to tell if there is an adjacent app-list
|
||||
|
||||
if matches!(
|
||||
self.core.applet.anchor,
|
||||
PanelAnchor::Top | PanelAnchor::Bottom
|
||||
) {
|
||||
Row::with_children(icon_buttons)
|
||||
.align_items(cosmic::iced_core::Alignment::Center)
|
||||
.height(Length::Shrink)
|
||||
.width(Length::Shrink)
|
||||
.spacing(space_xxs)
|
||||
.padding([0, space_xxs])
|
||||
.into()
|
||||
} else {
|
||||
Column::with_children(icon_buttons)
|
||||
.align_items(cosmic::iced_core::Alignment::Center)
|
||||
.height(Length::Shrink)
|
||||
.width(Length::Shrink)
|
||||
.spacing(space_xxs)
|
||||
.padding([space_xxs, 0])
|
||||
.into()
|
||||
}
|
||||
}
|
||||
cosmic_applet_minimize::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ cosmic-time.workspace = true
|
|||
futures = "0.3"
|
||||
zbus.workspace = true
|
||||
log = "0.4"
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-log.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
itertools = "0.10.3"
|
||||
slotmap = "1.0.6"
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
pub const APP_ID: &str = "com.system76.CosmicAppletNetwork";
|
||||
pub const PROFILE: &str = "";
|
||||
pub const VERSION: &str = "0.1.0";
|
||||
|
|
|
|||
13
cosmic-applet-network/src/lib.rs
Normal file
13
cosmic-applet-network/src/lib.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
mod app;
|
||||
mod config;
|
||||
mod localize;
|
||||
mod network_manager;
|
||||
|
||||
use crate::localize::localize;
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize();
|
||||
app::run()
|
||||
}
|
||||
|
|
@ -1,24 +1,10 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
mod app;
|
||||
mod config;
|
||||
mod localize;
|
||||
mod network_manager;
|
||||
|
||||
use log::info;
|
||||
|
||||
use crate::config::{APP_ID, PROFILE, VERSION};
|
||||
use crate::localize::localize;
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
// Initialize logger
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
info!("Iced Workspaces Applet ({})", APP_ID);
|
||||
info!("Version: {} ({})", VERSION, PROFILE);
|
||||
|
||||
// Prepare i18n
|
||||
localize();
|
||||
tracing::info!("Starting network applet with version {VERSION}");
|
||||
|
||||
app::run()
|
||||
cosmic_applet_network::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ cosmic-notifications-util = { git = "https://github.com/pop-os/cosmic-notificati
|
|||
cosmic-notifications-config = { git = "https://github.com/pop-os/cosmic-notifications" }
|
||||
# cosmic-notifications-util = { path = "../../cosmic-notifications-daemon/cosmic-notifications-util" }
|
||||
# cosmic-notifications-config = { path = "../../cosmic-notifications-daemon/cosmic-notifications-config" }
|
||||
tracing = "0.1"
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-log.workspace = true
|
||||
ron = "0.8"
|
||||
sendfd = { version = "0.4", features = ["tokio"] }
|
||||
bytemuck = "1"
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-log.workspace = true
|
||||
zbus.workspace = true
|
||||
# Application i18n
|
||||
i18n-embed = { version = "0.13.4", features = [
|
||||
|
|
|
|||
572
cosmic-applet-notifications/src/lib.rs
Normal file
572
cosmic-applet-notifications/src/lib.rs
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
mod localize;
|
||||
mod subscriptions;
|
||||
use cosmic::applet::token::subscription::{
|
||||
activation_token_subscription, TokenRequest, TokenUpdate,
|
||||
};
|
||||
use cosmic::applet::{menu_button, menu_control_padding, padded_control};
|
||||
use cosmic::cctk::sctk::reexports::calloop;
|
||||
use cosmic::cosmic_config::{Config, CosmicConfigEntry};
|
||||
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use cosmic::iced::Limits;
|
||||
use cosmic::iced::{
|
||||
widget::{column, row, text},
|
||||
window, Alignment, Length, Subscription,
|
||||
};
|
||||
use cosmic::iced_core::alignment::Horizontal;
|
||||
use cosmic::Command;
|
||||
|
||||
use cosmic::iced_futures::futures::executor::block_on;
|
||||
use cosmic::iced_style::application;
|
||||
|
||||
use cosmic::iced_widget::{scrollable, Column};
|
||||
use cosmic::widget::{button, container, divider, icon};
|
||||
use cosmic::{Element, Theme};
|
||||
use cosmic_notifications_config::NotificationsConfig;
|
||||
use cosmic_notifications_util::{Image, Notification};
|
||||
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use subscriptions::notifications::NotificationsAppletProxy;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::info;
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize::localize();
|
||||
cosmic::applet::run::<Notifications>(false, ())
|
||||
}
|
||||
|
||||
static DO_NOT_DISTURB: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
|
||||
|
||||
struct Notifications {
|
||||
core: cosmic::app::Core,
|
||||
config: NotificationsConfig,
|
||||
config_helper: Option<Config>,
|
||||
icon_name: String,
|
||||
popup: Option<window::Id>,
|
||||
// notifications: Vec<Notification>,
|
||||
timeline: Timeline,
|
||||
dbus_sender: Option<Sender<subscriptions::dbus::Input>>,
|
||||
cards: Vec<(id::Cards, Vec<Notification>, bool, String, String, String)>,
|
||||
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
||||
proxy: NotificationsAppletProxy<'static>,
|
||||
}
|
||||
|
||||
impl Notifications {
|
||||
fn update_cards(&mut self, id: id::Cards) {
|
||||
if let Some((id, _, card_value, ..)) = self.cards.iter_mut().find(|c| c.0 == id) {
|
||||
let chain = if *card_value {
|
||||
chain::Cards::on(id.clone(), 1.)
|
||||
} else {
|
||||
chain::Cards::off(id.clone(), 1.)
|
||||
};
|
||||
self.timeline.set_chain(chain);
|
||||
self.timeline.start();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_icon(&mut self) {
|
||||
self.icon_name = if self.config.do_not_disturb {
|
||||
"cosmic-applet-notification-disabled-symbolic"
|
||||
} else if self.cards.is_empty() {
|
||||
"cosmic-applet-notification-symbolic"
|
||||
} else {
|
||||
"cosmic-applet-notification-new-symbolic"
|
||||
}
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
TogglePopup,
|
||||
CloseRequested(window::Id),
|
||||
DoNotDisturb(chain::Toggler, bool),
|
||||
Frame(Instant),
|
||||
NotificationEvent(Notification),
|
||||
Config(NotificationsConfig),
|
||||
DbusEvent(subscriptions::dbus::Output),
|
||||
Dismissed(u32),
|
||||
ClearAll(Option<String>),
|
||||
CardsToggled(String, bool),
|
||||
Token(TokenUpdate),
|
||||
OpenSettings,
|
||||
}
|
||||
|
||||
impl cosmic::Application for Notifications {
|
||||
type Message = Message;
|
||||
type Executor = cosmic::SingleThreadExecutor;
|
||||
type Flags = ();
|
||||
const APP_ID: &'static str = "com.system76.CosmicAppletNotifications";
|
||||
|
||||
fn init(
|
||||
core: cosmic::app::Core,
|
||||
_flags: Self::Flags,
|
||||
) -> (
|
||||
Self,
|
||||
cosmic::iced::Command<cosmic::app::Message<Self::Message>>,
|
||||
) {
|
||||
let helper = Config::new(
|
||||
cosmic_notifications_config::ID,
|
||||
NotificationsConfig::VERSION,
|
||||
)
|
||||
.ok();
|
||||
|
||||
let config: NotificationsConfig = helper
|
||||
.as_ref()
|
||||
.map(|helper| {
|
||||
NotificationsConfig::get_entry(helper).unwrap_or_else(|(errors, config)| {
|
||||
for err in errors {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
config
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut _self = Self {
|
||||
core,
|
||||
config_helper: helper,
|
||||
config,
|
||||
icon_name: Default::default(),
|
||||
popup: None,
|
||||
timeline: Default::default(),
|
||||
dbus_sender: Default::default(),
|
||||
cards: Vec::new(),
|
||||
token_tx: Default::default(),
|
||||
proxy: block_on(crate::subscriptions::notifications::get_proxy())
|
||||
.expect("Failed to get proxy"),
|
||||
};
|
||||
_self.update_icon();
|
||||
(_self, Command::none())
|
||||
}
|
||||
|
||||
fn core(&self) -> &cosmic::app::Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut cosmic::app::Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
||||
Some(cosmic::applet::style())
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch(vec![
|
||||
self.core
|
||||
.watch_config(cosmic_notifications_config::ID)
|
||||
.map(|res| {
|
||||
for err in res.errors {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
Message::Config(res.config)
|
||||
}),
|
||||
self.timeline
|
||||
.as_subscription()
|
||||
.map(|(_, now)| Message::Frame(now)),
|
||||
subscriptions::dbus::proxy().map(Message::DbusEvent),
|
||||
subscriptions::notifications::notifications(self.proxy.clone())
|
||||
.map(Message::NotificationEvent),
|
||||
activation_token_subscription(0).map(Message::Token),
|
||||
])
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Self::Message,
|
||||
) -> cosmic::iced::Command<cosmic::app::Message<Self::Message>> {
|
||||
match message {
|
||||
Message::Frame(now) => {
|
||||
self.timeline.now(now);
|
||||
}
|
||||
Message::TogglePopup => {
|
||||
if let Some(p) = self.popup.take() {
|
||||
return destroy_popup(p);
|
||||
} else {
|
||||
let new_id = window::Id::unique();
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.core.applet.get_popup_settings(
|
||||
window::Id::MAIN,
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_width(1.0)
|
||||
.max_width(444.0)
|
||||
.min_height(100.0)
|
||||
.max_height(900.0);
|
||||
return get_popup(popup_settings);
|
||||
}
|
||||
}
|
||||
Message::DoNotDisturb(chain, b) => {
|
||||
self.timeline.set_chain(chain).start();
|
||||
self.config.do_not_disturb = b;
|
||||
if let Some(helper) = &self.config_helper {
|
||||
if let Err(err) = self.config.write_entry(helper) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::NotificationEvent(n) => {
|
||||
if let Some(c) = self
|
||||
.cards
|
||||
.iter_mut()
|
||||
.find(|c| c.1.iter().any(|notif| n.app_name == notif.app_name))
|
||||
{
|
||||
if let Some(notif) = c.1.iter_mut().find(|notif| n.id == notif.id) {
|
||||
*notif = n;
|
||||
} else {
|
||||
c.1.push(n);
|
||||
c.3 = fl!(
|
||||
"show-more",
|
||||
HashMap::from_iter(vec![("more", c.1.len().saturating_sub(1))])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.cards.push((
|
||||
id::Cards::new(n.app_name.clone()),
|
||||
vec![n],
|
||||
false,
|
||||
fl!("show-more", HashMap::from_iter(vec![("more", "1")])),
|
||||
fl!("show-less"),
|
||||
fl!("clear-all"),
|
||||
));
|
||||
}
|
||||
}
|
||||
Message::Config(config) => {
|
||||
self.config = config;
|
||||
}
|
||||
Message::Dismissed(id) => {
|
||||
info!("Dismissed {}", id);
|
||||
for c in &mut self.cards {
|
||||
c.1.retain(|n| n.id != id);
|
||||
}
|
||||
self.cards.retain(|c| !c.1.is_empty());
|
||||
|
||||
if let Some(tx) = &self.dbus_sender {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = tx.send(subscriptions::dbus::Input::Dismiss(id)).await {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Message::DbusEvent(e) => match e {
|
||||
subscriptions::dbus::Output::Ready(tx) => {
|
||||
self.dbus_sender.replace(tx);
|
||||
}
|
||||
subscriptions::dbus::Output::CloseEvent(id) => {
|
||||
for c in &mut self.cards {
|
||||
c.1.retain(|n| n.id != id);
|
||||
c.3 = fl!(
|
||||
"show-more",
|
||||
HashMap::from_iter(vec![("more", c.1.len().saturating_sub(1))])
|
||||
);
|
||||
}
|
||||
self.cards.retain(|c| !c.1.is_empty());
|
||||
}
|
||||
},
|
||||
Message::ClearAll(Some(app_name)) => {
|
||||
if let Some(pos) = self
|
||||
.cards
|
||||
.iter_mut()
|
||||
.position(|c| c.1.iter().any(|notif| app_name == notif.app_name))
|
||||
{
|
||||
for n in self.cards.remove(pos).1 {
|
||||
if let Some(tx) = &self.dbus_sender {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) =
|
||||
tx.send(subscriptions::dbus::Input::Dismiss(n.id)).await
|
||||
{
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::ClearAll(None) => {
|
||||
for n in self.cards.drain(..).map(|n| n.1).flatten() {
|
||||
if let Some(tx) = &self.dbus_sender {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) =
|
||||
tx.send(subscriptions::dbus::Input::Dismiss(n.id)).await
|
||||
{
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::CardsToggled(name, expanded) => {
|
||||
let id = if let Some((id, _, n_expanded, ..)) = self
|
||||
.cards
|
||||
.iter_mut()
|
||||
.find(|c| c.1.iter().any(|notif| name == notif.app_name))
|
||||
{
|
||||
*n_expanded = expanded;
|
||||
id.clone()
|
||||
} else {
|
||||
return Command::none();
|
||||
};
|
||||
self.update_cards(id);
|
||||
}
|
||||
Message::CloseRequested(id) => {
|
||||
if Some(id) == self.popup {
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
Message::OpenSettings => {
|
||||
let exec = "cosmic-settings notifications".to_string();
|
||||
if let Some(tx) = self.token_tx.as_ref() {
|
||||
let _ = tx.send(TokenRequest {
|
||||
app_id: Self::APP_ID.to_string(),
|
||||
exec,
|
||||
});
|
||||
}
|
||||
}
|
||||
Message::Token(u) => match u {
|
||||
TokenUpdate::Init(tx) => {
|
||||
self.token_tx = Some(tx);
|
||||
}
|
||||
TokenUpdate::Finished => {
|
||||
self.token_tx = None;
|
||||
}
|
||||
TokenUpdate::ActivationToken { token, .. } => {
|
||||
let mut cmd = std::process::Command::new("cosmic-settings");
|
||||
cmd.arg("notifications");
|
||||
if let Some(token) = token {
|
||||
cmd.env("XDG_ACTIVATION_TOKEN", &token);
|
||||
cmd.env("DESKTOP_STARTUP_ID", &token);
|
||||
}
|
||||
cosmic::process::spawn(cmd);
|
||||
}
|
||||
},
|
||||
};
|
||||
self.update_icon();
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
self.core
|
||||
.applet
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_window(&self, _id: window::Id) -> Element<Message> {
|
||||
let do_not_disturb = padded_control(row![anim!(
|
||||
DO_NOT_DISTURB,
|
||||
&self.timeline,
|
||||
fl!("do-not-disturb"),
|
||||
self.config.do_not_disturb,
|
||||
Message::DoNotDisturb
|
||||
)
|
||||
.text_size(14)
|
||||
.width(Length::Fill)]);
|
||||
|
||||
let settings = menu_button(text(fl!("notification-settings")).size(14))
|
||||
.on_press(Message::OpenSettings);
|
||||
|
||||
let notifications = if self.cards.is_empty() {
|
||||
row![container(
|
||||
column![
|
||||
text_icon("cosmic-applet-notification-symbolic", 40),
|
||||
text(&fl!("no-notifications")).size(14)
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.align_x(Horizontal::Center)]
|
||||
.spacing(12)
|
||||
} else {
|
||||
let mut notifs: Vec<Element<_>> = Vec::with_capacity(self.cards.len());
|
||||
notifs.push(
|
||||
container(
|
||||
cosmic::widget::button::text(fl!("clear-all"))
|
||||
.on_press(Message::ClearAll(None)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.align_x(Horizontal::Right)
|
||||
.into(),
|
||||
);
|
||||
for c in self.cards.iter().rev() {
|
||||
if c.1.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let name = c.1[0].app_name.clone();
|
||||
let notif_elems: Vec<_> = c
|
||||
.1
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|n| {
|
||||
let app_name = text(if n.app_name.len() > 24 {
|
||||
Cow::from(format!(
|
||||
"{:.26}...",
|
||||
n.app_name.lines().next().unwrap_or_default()
|
||||
))
|
||||
} else {
|
||||
Cow::from(&n.app_name)
|
||||
})
|
||||
.size(12)
|
||||
.width(Length::Fill);
|
||||
|
||||
let duration_since = text(duration_ago_msg(n)).size(10);
|
||||
|
||||
let close_notif = button(
|
||||
icon::from_name("window-close-symbolic")
|
||||
.size(16)
|
||||
.symbolic(true),
|
||||
)
|
||||
.on_press(Message::Dismissed(n.id))
|
||||
.style(cosmic::theme::Button::Text);
|
||||
Element::from(
|
||||
column!(
|
||||
match n.image() {
|
||||
Some(cosmic_notifications_util::Image::File(path)) => {
|
||||
row![
|
||||
icon::from_path(PathBuf::from(path)).icon().size(16),
|
||||
app_name,
|
||||
duration_since,
|
||||
close_notif
|
||||
]
|
||||
.spacing(8)
|
||||
.align_items(Alignment::Center)
|
||||
}
|
||||
Some(cosmic_notifications_util::Image::Name(name)) => {
|
||||
row![
|
||||
icon::from_name(name.as_str()).size(16),
|
||||
app_name,
|
||||
duration_since,
|
||||
close_notif
|
||||
]
|
||||
.spacing(8)
|
||||
.align_items(Alignment::Center)
|
||||
}
|
||||
Some(cosmic_notifications_util::Image::Data {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
}) => {
|
||||
row![
|
||||
icon::from_raster_pixels(*width, *height, data.clone())
|
||||
.icon()
|
||||
.size(16),
|
||||
app_name,
|
||||
duration_since,
|
||||
close_notif
|
||||
]
|
||||
.spacing(8)
|
||||
.align_items(Alignment::Center)
|
||||
}
|
||||
None => row![app_name, duration_since, close_notif]
|
||||
.spacing(8)
|
||||
.align_items(Alignment::Center),
|
||||
},
|
||||
column![
|
||||
text(n.summary.lines().next().unwrap_or_default())
|
||||
.width(Length::Fill)
|
||||
.size(14),
|
||||
text(n.body.lines().next().unwrap_or_default())
|
||||
.width(Length::Fill)
|
||||
.size(12)
|
||||
]
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let show_more_icon = c.1.last().and_then(|n| {
|
||||
info!("app_icon: {:?}", &n.app_icon);
|
||||
if n.app_icon.is_empty() {
|
||||
match n.image().cloned() {
|
||||
Some(Image::File(p)) => Some(cosmic::widget::icon::from_path(p)),
|
||||
Some(Image::Name(name)) => {
|
||||
Some(cosmic::widget::icon::from_name(name).handle())
|
||||
}
|
||||
Some(Image::Data {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
}) => Some(cosmic::widget::icon::from_raster_pixels(
|
||||
width, height, data,
|
||||
)),
|
||||
None => None,
|
||||
}
|
||||
} else if let Some(path) = url::Url::parse(&n.app_icon)
|
||||
.ok()
|
||||
.and_then(|u| u.to_file_path().ok())
|
||||
{
|
||||
Some(cosmic::widget::icon::from_path(path))
|
||||
} else {
|
||||
Some(cosmic::widget::icon::from_name(n.app_icon.clone()).handle())
|
||||
}
|
||||
});
|
||||
let card_list = anim!(
|
||||
//cards
|
||||
c.0.clone(),
|
||||
&self.timeline,
|
||||
notif_elems,
|
||||
Message::ClearAll(Some(name.clone())),
|
||||
move |_, e| Message::CardsToggled(name.clone(), e),
|
||||
&c.3,
|
||||
&c.4,
|
||||
&c.5,
|
||||
show_more_icon,
|
||||
c.2,
|
||||
);
|
||||
notifs.push(card_list.into());
|
||||
}
|
||||
|
||||
row!(scrollable(
|
||||
Column::with_children(notifs)
|
||||
.spacing(8)
|
||||
.height(Length::Shrink),
|
||||
)
|
||||
.height(Length::Shrink))
|
||||
.padding(menu_control_padding())
|
||||
};
|
||||
|
||||
let main_content = column![
|
||||
padded_control(divider::horizontal::default()),
|
||||
notifications,
|
||||
padded_control(divider::horizontal::default())
|
||||
];
|
||||
|
||||
let content = column![do_not_disturb, main_content, settings]
|
||||
.align_items(Alignment::Start)
|
||||
.padding([8, 0]);
|
||||
|
||||
self.core.applet.popup_container(content).into()
|
||||
}
|
||||
|
||||
fn on_close_requested(&self, id: window::Id) -> Option<Message> {
|
||||
Some(Message::CloseRequested(id))
|
||||
}
|
||||
}
|
||||
|
||||
fn text_icon(name: &str, size: u16) -> cosmic::widget::Icon {
|
||||
icon::from_name(name).size(size).symbolic(true).icon()
|
||||
}
|
||||
|
||||
fn duration_ago_msg(notification: &Notification) -> String {
|
||||
if let Some(d) = notification.duration_since() {
|
||||
let min = d.as_secs() / 60;
|
||||
let hrs = min / 60;
|
||||
if hrs > 0 {
|
||||
fl!("hours-ago", HashMap::from_iter(vec![("duration", hrs)]))
|
||||
} else {
|
||||
fl!("minutes-ago", HashMap::from_iter(vec![("duration", min)]))
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,578 +1,10 @@
|
|||
mod localize;
|
||||
mod subscriptions;
|
||||
use cosmic::applet::token::subscription::{
|
||||
activation_token_subscription, TokenRequest, TokenUpdate,
|
||||
};
|
||||
use cosmic::applet::{menu_button, menu_control_padding, padded_control};
|
||||
use cosmic::cctk::sctk::reexports::calloop;
|
||||
use cosmic::cosmic_config::{Config, CosmicConfigEntry};
|
||||
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use cosmic::iced::Limits;
|
||||
use cosmic::iced::{
|
||||
widget::{column, row, text},
|
||||
window, Alignment, Length, Subscription,
|
||||
};
|
||||
use cosmic::iced_core::alignment::Horizontal;
|
||||
use cosmic::Command;
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
use cosmic::iced_futures::futures::executor::block_on;
|
||||
use cosmic::iced_style::application;
|
||||
|
||||
use cosmic::iced_widget::{scrollable, Column};
|
||||
use cosmic::widget::{button, container, divider, icon};
|
||||
use cosmic::{Element, Theme};
|
||||
use cosmic_notifications_config::NotificationsConfig;
|
||||
use cosmic_notifications_util::{Image, Notification};
|
||||
use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use subscriptions::notifications::NotificationsAppletProxy;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::info;
|
||||
|
||||
pub fn main() -> cosmic::iced::Result {
|
||||
fn main() -> cosmic::iced::Result {
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
// Prepare i18n
|
||||
localize::localize();
|
||||
|
||||
info!("Notifications applet");
|
||||
tracing::info!("Starting notifications applet with version {VERSION}");
|
||||
|
||||
cosmic::applet::run::<Notifications>(false, ())
|
||||
}
|
||||
|
||||
static DO_NOT_DISTURB: Lazy<id::Toggler> = Lazy::new(id::Toggler::unique);
|
||||
|
||||
struct Notifications {
|
||||
core: cosmic::app::Core,
|
||||
config: NotificationsConfig,
|
||||
config_helper: Option<Config>,
|
||||
icon_name: String,
|
||||
popup: Option<window::Id>,
|
||||
// notifications: Vec<Notification>,
|
||||
timeline: Timeline,
|
||||
dbus_sender: Option<Sender<subscriptions::dbus::Input>>,
|
||||
cards: Vec<(id::Cards, Vec<Notification>, bool, String, String, String)>,
|
||||
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
||||
proxy: NotificationsAppletProxy<'static>,
|
||||
}
|
||||
|
||||
impl Notifications {
|
||||
fn update_cards(&mut self, id: id::Cards) {
|
||||
if let Some((id, _, card_value, ..)) = self.cards.iter_mut().find(|c| c.0 == id) {
|
||||
let chain = if *card_value {
|
||||
chain::Cards::on(id.clone(), 1.)
|
||||
} else {
|
||||
chain::Cards::off(id.clone(), 1.)
|
||||
};
|
||||
self.timeline.set_chain(chain);
|
||||
self.timeline.start();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_icon(&mut self) {
|
||||
self.icon_name = if self.config.do_not_disturb {
|
||||
"cosmic-applet-notification-disabled-symbolic"
|
||||
} else if self.cards.is_empty() {
|
||||
"cosmic-applet-notification-symbolic"
|
||||
} else {
|
||||
"cosmic-applet-notification-new-symbolic"
|
||||
}
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
TogglePopup,
|
||||
CloseRequested(window::Id),
|
||||
DoNotDisturb(chain::Toggler, bool),
|
||||
Frame(Instant),
|
||||
NotificationEvent(Notification),
|
||||
Config(NotificationsConfig),
|
||||
DbusEvent(subscriptions::dbus::Output),
|
||||
Dismissed(u32),
|
||||
ClearAll(Option<String>),
|
||||
CardsToggled(String, bool),
|
||||
Token(TokenUpdate),
|
||||
OpenSettings,
|
||||
}
|
||||
|
||||
impl cosmic::Application for Notifications {
|
||||
type Message = Message;
|
||||
type Executor = cosmic::SingleThreadExecutor;
|
||||
type Flags = ();
|
||||
const APP_ID: &'static str = "com.system76.CosmicAppletNotifications";
|
||||
|
||||
fn init(
|
||||
core: cosmic::app::Core,
|
||||
_flags: Self::Flags,
|
||||
) -> (
|
||||
Self,
|
||||
cosmic::iced::Command<cosmic::app::Message<Self::Message>>,
|
||||
) {
|
||||
let helper = Config::new(
|
||||
cosmic_notifications_config::ID,
|
||||
NotificationsConfig::VERSION,
|
||||
)
|
||||
.ok();
|
||||
|
||||
let config: NotificationsConfig = helper
|
||||
.as_ref()
|
||||
.map(|helper| {
|
||||
NotificationsConfig::get_entry(helper).unwrap_or_else(|(errors, config)| {
|
||||
for err in errors {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
config
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut _self = Self {
|
||||
core,
|
||||
config_helper: helper,
|
||||
config,
|
||||
icon_name: Default::default(),
|
||||
popup: None,
|
||||
timeline: Default::default(),
|
||||
dbus_sender: Default::default(),
|
||||
cards: Vec::new(),
|
||||
token_tx: Default::default(),
|
||||
proxy: block_on(crate::subscriptions::notifications::get_proxy())
|
||||
.expect("Failed to get proxy"),
|
||||
};
|
||||
_self.update_icon();
|
||||
(_self, Command::none())
|
||||
}
|
||||
|
||||
fn core(&self) -> &cosmic::app::Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut cosmic::app::Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
||||
Some(cosmic::applet::style())
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch(vec![
|
||||
self.core
|
||||
.watch_config(cosmic_notifications_config::ID)
|
||||
.map(|res| {
|
||||
for err in res.errors {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
Message::Config(res.config)
|
||||
}),
|
||||
self.timeline
|
||||
.as_subscription()
|
||||
.map(|(_, now)| Message::Frame(now)),
|
||||
subscriptions::dbus::proxy().map(Message::DbusEvent),
|
||||
subscriptions::notifications::notifications(self.proxy.clone())
|
||||
.map(Message::NotificationEvent),
|
||||
activation_token_subscription(0).map(Message::Token),
|
||||
])
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
message: Self::Message,
|
||||
) -> cosmic::iced::Command<cosmic::app::Message<Self::Message>> {
|
||||
match message {
|
||||
Message::Frame(now) => {
|
||||
self.timeline.now(now);
|
||||
}
|
||||
Message::TogglePopup => {
|
||||
if let Some(p) = self.popup.take() {
|
||||
return destroy_popup(p);
|
||||
} else {
|
||||
let new_id = window::Id::unique();
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.core.applet.get_popup_settings(
|
||||
window::Id::MAIN,
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_width(1.0)
|
||||
.max_width(444.0)
|
||||
.min_height(100.0)
|
||||
.max_height(900.0);
|
||||
return get_popup(popup_settings);
|
||||
}
|
||||
}
|
||||
Message::DoNotDisturb(chain, b) => {
|
||||
self.timeline.set_chain(chain).start();
|
||||
self.config.do_not_disturb = b;
|
||||
if let Some(helper) = &self.config_helper {
|
||||
if let Err(err) = self.config.write_entry(helper) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::NotificationEvent(n) => {
|
||||
if let Some(c) = self
|
||||
.cards
|
||||
.iter_mut()
|
||||
.find(|c| c.1.iter().any(|notif| n.app_name == notif.app_name))
|
||||
{
|
||||
if let Some(notif) = c.1.iter_mut().find(|notif| n.id == notif.id) {
|
||||
*notif = n;
|
||||
} else {
|
||||
c.1.push(n);
|
||||
c.3 = fl!(
|
||||
"show-more",
|
||||
HashMap::from_iter(vec![("more", c.1.len().saturating_sub(1))])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.cards.push((
|
||||
id::Cards::new(n.app_name.clone()),
|
||||
vec![n],
|
||||
false,
|
||||
fl!("show-more", HashMap::from_iter(vec![("more", "1")])),
|
||||
fl!("show-less"),
|
||||
fl!("clear-all"),
|
||||
));
|
||||
}
|
||||
}
|
||||
Message::Config(config) => {
|
||||
self.config = config;
|
||||
}
|
||||
Message::Dismissed(id) => {
|
||||
info!("Dismissed {}", id);
|
||||
for c in &mut self.cards {
|
||||
c.1.retain(|n| n.id != id);
|
||||
}
|
||||
self.cards.retain(|c| !c.1.is_empty());
|
||||
|
||||
if let Some(tx) = &self.dbus_sender {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = tx.send(subscriptions::dbus::Input::Dismiss(id)).await {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Message::DbusEvent(e) => match e {
|
||||
subscriptions::dbus::Output::Ready(tx) => {
|
||||
self.dbus_sender.replace(tx);
|
||||
}
|
||||
subscriptions::dbus::Output::CloseEvent(id) => {
|
||||
for c in &mut self.cards {
|
||||
c.1.retain(|n| n.id != id);
|
||||
c.3 = fl!(
|
||||
"show-more",
|
||||
HashMap::from_iter(vec![("more", c.1.len().saturating_sub(1))])
|
||||
);
|
||||
}
|
||||
self.cards.retain(|c| !c.1.is_empty());
|
||||
}
|
||||
},
|
||||
Message::ClearAll(Some(app_name)) => {
|
||||
if let Some(pos) = self
|
||||
.cards
|
||||
.iter_mut()
|
||||
.position(|c| c.1.iter().any(|notif| app_name == notif.app_name))
|
||||
{
|
||||
for n in self.cards.remove(pos).1 {
|
||||
if let Some(tx) = &self.dbus_sender {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) =
|
||||
tx.send(subscriptions::dbus::Input::Dismiss(n.id)).await
|
||||
{
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::ClearAll(None) => {
|
||||
for n in self.cards.drain(..).map(|n| n.1).flatten() {
|
||||
if let Some(tx) = &self.dbus_sender {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) =
|
||||
tx.send(subscriptions::dbus::Input::Dismiss(n.id)).await
|
||||
{
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::CardsToggled(name, expanded) => {
|
||||
let id = if let Some((id, _, n_expanded, ..)) = self
|
||||
.cards
|
||||
.iter_mut()
|
||||
.find(|c| c.1.iter().any(|notif| name == notif.app_name))
|
||||
{
|
||||
*n_expanded = expanded;
|
||||
id.clone()
|
||||
} else {
|
||||
return Command::none();
|
||||
};
|
||||
self.update_cards(id);
|
||||
}
|
||||
Message::CloseRequested(id) => {
|
||||
if Some(id) == self.popup {
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
Message::OpenSettings => {
|
||||
let exec = "cosmic-settings notifications".to_string();
|
||||
if let Some(tx) = self.token_tx.as_ref() {
|
||||
let _ = tx.send(TokenRequest {
|
||||
app_id: Self::APP_ID.to_string(),
|
||||
exec,
|
||||
});
|
||||
}
|
||||
}
|
||||
Message::Token(u) => match u {
|
||||
TokenUpdate::Init(tx) => {
|
||||
self.token_tx = Some(tx);
|
||||
}
|
||||
TokenUpdate::Finished => {
|
||||
self.token_tx = None;
|
||||
}
|
||||
TokenUpdate::ActivationToken { token, .. } => {
|
||||
let mut cmd = std::process::Command::new("cosmic-settings");
|
||||
cmd.arg("notifications");
|
||||
if let Some(token) = token {
|
||||
cmd.env("XDG_ACTIVATION_TOKEN", &token);
|
||||
cmd.env("DESKTOP_STARTUP_ID", &token);
|
||||
}
|
||||
cosmic::process::spawn(cmd);
|
||||
}
|
||||
},
|
||||
};
|
||||
self.update_icon();
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
self.core
|
||||
.applet
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_window(&self, _id: window::Id) -> Element<Message> {
|
||||
let do_not_disturb = padded_control(row![anim!(
|
||||
DO_NOT_DISTURB,
|
||||
&self.timeline,
|
||||
fl!("do-not-disturb"),
|
||||
self.config.do_not_disturb,
|
||||
Message::DoNotDisturb
|
||||
)
|
||||
.text_size(14)
|
||||
.width(Length::Fill)]);
|
||||
|
||||
let settings = menu_button(text(fl!("notification-settings")).size(14))
|
||||
.on_press(Message::OpenSettings);
|
||||
|
||||
let notifications = if self.cards.is_empty() {
|
||||
row![container(
|
||||
column![
|
||||
text_icon("cosmic-applet-notification-symbolic", 40),
|
||||
text(&fl!("no-notifications")).size(14)
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.align_x(Horizontal::Center)]
|
||||
.spacing(12)
|
||||
} else {
|
||||
let mut notifs: Vec<Element<_>> = Vec::with_capacity(self.cards.len());
|
||||
notifs.push(
|
||||
container(
|
||||
cosmic::widget::button::text(fl!("clear-all"))
|
||||
.on_press(Message::ClearAll(None)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.align_x(Horizontal::Right)
|
||||
.into(),
|
||||
);
|
||||
for c in self.cards.iter().rev() {
|
||||
if c.1.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let name = c.1[0].app_name.clone();
|
||||
let notif_elems: Vec<_> = c
|
||||
.1
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|n| {
|
||||
let app_name = text(if n.app_name.len() > 24 {
|
||||
Cow::from(format!(
|
||||
"{:.26}...",
|
||||
n.app_name.lines().next().unwrap_or_default()
|
||||
))
|
||||
} else {
|
||||
Cow::from(&n.app_name)
|
||||
})
|
||||
.size(12)
|
||||
.width(Length::Fill);
|
||||
|
||||
let duration_since = text(duration_ago_msg(n)).size(10);
|
||||
|
||||
let close_notif = button(
|
||||
icon::from_name("window-close-symbolic")
|
||||
.size(16)
|
||||
.symbolic(true),
|
||||
)
|
||||
.on_press(Message::Dismissed(n.id))
|
||||
.style(cosmic::theme::Button::Text);
|
||||
Element::from(
|
||||
column!(
|
||||
match n.image() {
|
||||
Some(cosmic_notifications_util::Image::File(path)) => {
|
||||
row![
|
||||
icon::from_path(PathBuf::from(path)).icon().size(16),
|
||||
app_name,
|
||||
duration_since,
|
||||
close_notif
|
||||
]
|
||||
.spacing(8)
|
||||
.align_items(Alignment::Center)
|
||||
}
|
||||
Some(cosmic_notifications_util::Image::Name(name)) => {
|
||||
row![
|
||||
icon::from_name(name.as_str()).size(16),
|
||||
app_name,
|
||||
duration_since,
|
||||
close_notif
|
||||
]
|
||||
.spacing(8)
|
||||
.align_items(Alignment::Center)
|
||||
}
|
||||
Some(cosmic_notifications_util::Image::Data {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
}) => {
|
||||
row![
|
||||
icon::from_raster_pixels(*width, *height, data.clone())
|
||||
.icon()
|
||||
.size(16),
|
||||
app_name,
|
||||
duration_since,
|
||||
close_notif
|
||||
]
|
||||
.spacing(8)
|
||||
.align_items(Alignment::Center)
|
||||
}
|
||||
None => row![app_name, duration_since, close_notif]
|
||||
.spacing(8)
|
||||
.align_items(Alignment::Center),
|
||||
},
|
||||
column![
|
||||
text(n.summary.lines().next().unwrap_or_default())
|
||||
.width(Length::Fill)
|
||||
.size(14),
|
||||
text(n.body.lines().next().unwrap_or_default())
|
||||
.width(Length::Fill)
|
||||
.size(12)
|
||||
]
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let show_more_icon = c.1.last().and_then(|n| {
|
||||
info!("app_icon: {:?}", &n.app_icon);
|
||||
if n.app_icon.is_empty() {
|
||||
match n.image().cloned() {
|
||||
Some(Image::File(p)) => Some(cosmic::widget::icon::from_path(p)),
|
||||
Some(Image::Name(name)) => {
|
||||
Some(cosmic::widget::icon::from_name(name).handle())
|
||||
}
|
||||
Some(Image::Data {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
}) => Some(cosmic::widget::icon::from_raster_pixels(
|
||||
width, height, data,
|
||||
)),
|
||||
None => None,
|
||||
}
|
||||
} else if let Some(path) = url::Url::parse(&n.app_icon)
|
||||
.ok()
|
||||
.and_then(|u| u.to_file_path().ok())
|
||||
{
|
||||
Some(cosmic::widget::icon::from_path(path))
|
||||
} else {
|
||||
Some(cosmic::widget::icon::from_name(n.app_icon.clone()).handle())
|
||||
}
|
||||
});
|
||||
let card_list = anim!(
|
||||
//cards
|
||||
c.0.clone(),
|
||||
&self.timeline,
|
||||
notif_elems,
|
||||
Message::ClearAll(Some(name.clone())),
|
||||
move |_, e| Message::CardsToggled(name.clone(), e),
|
||||
&c.3,
|
||||
&c.4,
|
||||
&c.5,
|
||||
show_more_icon,
|
||||
c.2,
|
||||
);
|
||||
notifs.push(card_list.into());
|
||||
}
|
||||
|
||||
row!(scrollable(
|
||||
Column::with_children(notifs)
|
||||
.spacing(8)
|
||||
.height(Length::Shrink),
|
||||
)
|
||||
.height(Length::Shrink))
|
||||
.padding(menu_control_padding())
|
||||
};
|
||||
|
||||
let main_content = column![
|
||||
padded_control(divider::horizontal::default()),
|
||||
notifications,
|
||||
padded_control(divider::horizontal::default())
|
||||
];
|
||||
|
||||
let content = column![do_not_disturb, main_content, settings]
|
||||
.align_items(Alignment::Start)
|
||||
.padding([8, 0]);
|
||||
|
||||
self.core.applet.popup_container(content).into()
|
||||
}
|
||||
|
||||
fn on_close_requested(&self, id: window::Id) -> Option<Message> {
|
||||
Some(Message::CloseRequested(id))
|
||||
}
|
||||
}
|
||||
|
||||
fn text_icon(name: &str, size: u16) -> cosmic::widget::Icon {
|
||||
icon::from_name(name).size(size).symbolic(true).icon()
|
||||
}
|
||||
|
||||
fn duration_ago_msg(notification: &Notification) -> String {
|
||||
if let Some(d) = notification.duration_since() {
|
||||
let min = d.as_secs() / 60;
|
||||
let hrs = min / 60;
|
||||
if hrs > 0 {
|
||||
fl!("hours-ago", HashMap::from_iter(vec![("duration", hrs)]))
|
||||
} else {
|
||||
fl!("minutes-ago", HashMap::from_iter(vec![("duration", min)]))
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
cosmic_applet_notifications::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,4 +20,6 @@ i18n-embed-fl = "0.6"
|
|||
rust-embed = "6.6"
|
||||
rust-embed-utils = "7.5.0"
|
||||
once_cell = "1.17.1"
|
||||
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-log.workspace = true
|
||||
436
cosmic-applet-power/src/lib.rs
Normal file
436
cosmic-applet-power/src/lib.rs
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
use std::collections::HashMap;
|
||||
use std::process;
|
||||
use std::time::Duration;
|
||||
|
||||
use cosmic::applet::{menu_button, padded_control};
|
||||
use cosmic::iced;
|
||||
use cosmic::iced::alignment::{Horizontal, Vertical};
|
||||
use cosmic::iced::event::wayland::{self, LayerEvent};
|
||||
use cosmic::iced::event::{listen_with, PlatformSpecific};
|
||||
use cosmic::iced::time;
|
||||
use cosmic::iced::wayland::actions::layer_surface::SctkLayerSurfaceSettings;
|
||||
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use cosmic::iced_core::{Border, Shadow};
|
||||
use cosmic::iced_runtime::core::layout::Limits;
|
||||
use cosmic::iced_sctk::commands::layer_surface::{
|
||||
destroy_layer_surface, get_layer_surface, Anchor, KeyboardInteractivity,
|
||||
};
|
||||
use cosmic::iced_widget::mouse_area;
|
||||
use cosmic::widget::{button, divider, icon};
|
||||
use cosmic::Renderer;
|
||||
|
||||
use cosmic::iced::Color;
|
||||
use cosmic::iced::{
|
||||
widget::{self, column, container, row, space::Space, text},
|
||||
window, Alignment, Length, Subscription,
|
||||
};
|
||||
use cosmic::iced_style::application;
|
||||
use cosmic::theme;
|
||||
use cosmic::{app::Command, Element, Theme};
|
||||
|
||||
use logind_zbus::manager::ManagerProxy;
|
||||
use logind_zbus::session::{SessionProxy, SessionType};
|
||||
use logind_zbus::user::UserProxy;
|
||||
use nix::unistd::getuid;
|
||||
use zbus::Connection;
|
||||
|
||||
pub mod cosmic_session;
|
||||
mod localize;
|
||||
pub mod session_manager;
|
||||
|
||||
use crate::cosmic_session::CosmicSessionProxy;
|
||||
use crate::session_manager::SessionManagerProxy;
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize::localize();
|
||||
|
||||
cosmic::applet::run::<Power>(false, ())
|
||||
}
|
||||
|
||||
const COUNTDOWN_LENGTH: u8 = 60;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Power {
|
||||
core: cosmic::app::Core,
|
||||
icon_name: String,
|
||||
popup: Option<window::Id>,
|
||||
action_to_confirm: Option<(window::Id, PowerAction, u8)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum PowerAction {
|
||||
Lock,
|
||||
LogOut,
|
||||
Suspend,
|
||||
Restart,
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Countdown,
|
||||
Action(PowerAction),
|
||||
TogglePopup,
|
||||
Settings,
|
||||
Confirm,
|
||||
Cancel,
|
||||
Zbus(Result<(), zbus::Error>),
|
||||
Closed(window::Id),
|
||||
}
|
||||
|
||||
impl cosmic::Application for Power {
|
||||
type Executor = cosmic::SingleThreadExecutor;
|
||||
type Flags = ();
|
||||
type Message = Message;
|
||||
const APP_ID: &'static str = "com.system76.CosmicAppletPower";
|
||||
|
||||
fn core(&self) -> &cosmic::app::Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut cosmic::app::Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Self {
|
||||
core,
|
||||
icon_name: "system-shutdown-symbolic".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_close_requested(&self, id: window::Id) -> Option<Message> {
|
||||
Some(Message::Closed(id))
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
let mut subscriptions = Vec::with_capacity(2);
|
||||
subscriptions.push(listen_with(|e, _status| match e {
|
||||
cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
wayland::Event::Layer(LayerEvent::Unfocused, ..),
|
||||
)) => Some(Message::Cancel),
|
||||
_ => None,
|
||||
}));
|
||||
if self.action_to_confirm.is_some() {
|
||||
subscriptions
|
||||
.push(time::every(Duration::from_millis(1000)).map(|_| Message::Countdown));
|
||||
}
|
||||
Subscription::batch(subscriptions)
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::TogglePopup => {
|
||||
if let Some(p) = self.popup.take() {
|
||||
destroy_popup(p)
|
||||
} else {
|
||||
let new_id = window::Id::unique();
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.core.applet.get_popup_settings(
|
||||
window::Id::MAIN,
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_width(100.0)
|
||||
.min_height(100.0)
|
||||
.max_height(400.0)
|
||||
.max_width(500.0);
|
||||
get_popup(popup_settings)
|
||||
}
|
||||
}
|
||||
Message::Settings => {
|
||||
let _ = process::Command::new("cosmic-settings").spawn();
|
||||
Command::none()
|
||||
}
|
||||
Message::Action(action) => {
|
||||
let id = window::Id::unique();
|
||||
self.action_to_confirm = Some((id, action, COUNTDOWN_LENGTH));
|
||||
get_layer_surface(SctkLayerSurfaceSettings {
|
||||
id,
|
||||
keyboard_interactivity: KeyboardInteractivity::None,
|
||||
anchor: Anchor::all(),
|
||||
namespace: "dialog".into(),
|
||||
size: Some((None, None)),
|
||||
size_limits: Limits::NONE.min_width(1.0).min_height(1.0),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
Message::Zbus(result) => {
|
||||
if let Err(e) = result {
|
||||
eprintln!("cosmic-applet-power ERROR: '{}'", e);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::Confirm => {
|
||||
if let Some((id, a, _)) = self.action_to_confirm.take() {
|
||||
let msg = |m| cosmic::app::message::app(Message::Zbus(m));
|
||||
Command::batch(vec![
|
||||
destroy_layer_surface(id),
|
||||
match a {
|
||||
PowerAction::Lock => iced::Command::perform(lock(), msg),
|
||||
PowerAction::LogOut => iced::Command::perform(log_out(), msg),
|
||||
PowerAction::Suspend => iced::Command::perform(suspend(), msg),
|
||||
PowerAction::Restart => iced::Command::perform(restart(), msg),
|
||||
PowerAction::Shutdown => iced::Command::perform(shutdown(), msg),
|
||||
},
|
||||
])
|
||||
} else {
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
Message::Cancel => {
|
||||
if let Some((id, _, _)) = self.action_to_confirm.take() {
|
||||
return destroy_layer_surface(id);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::Countdown => {
|
||||
if let Some((surface_id, a, countdown)) = self.action_to_confirm.as_mut() {
|
||||
*countdown -= 1;
|
||||
if *countdown == 0 {
|
||||
let id = *surface_id;
|
||||
let a = *a;
|
||||
|
||||
self.action_to_confirm = None;
|
||||
let msg = |m: zbus::Result<()>| cosmic::app::message::app(Message::Zbus(m));
|
||||
return Command::batch(vec![
|
||||
destroy_layer_surface(id),
|
||||
match a {
|
||||
PowerAction::Lock => iced::Command::perform(lock(), msg),
|
||||
PowerAction::LogOut => iced::Command::perform(log_out(), msg),
|
||||
PowerAction::Suspend => iced::Command::perform(suspend(), msg),
|
||||
PowerAction::Restart => iced::Command::perform(restart(), msg),
|
||||
PowerAction::Shutdown => iced::Command::perform(shutdown(), msg),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::Closed(id) => {
|
||||
if self.popup == Some(id) {
|
||||
self.popup = None;
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
self.core
|
||||
.applet
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_window(&self, id: window::Id) -> Element<Message> {
|
||||
if matches!(self.popup, Some(p) if p == id) {
|
||||
let settings = menu_button(text(fl!("settings")).size(14)).on_press(Message::Settings);
|
||||
|
||||
let session = column![
|
||||
menu_button(
|
||||
row![
|
||||
text_icon("system-lock-screen-symbolic", 24),
|
||||
text(fl!("lock-screen")).size(14),
|
||||
Space::with_width(Length::Fill),
|
||||
text(fl!("lock-screen-shortcut")).size(14),
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
)
|
||||
.on_press(Message::Action(PowerAction::Lock)),
|
||||
menu_button(
|
||||
row![
|
||||
text_icon("system-log-out-symbolic", 24),
|
||||
text(fl!("log-out")).size(14),
|
||||
Space::with_width(Length::Fill),
|
||||
text(fl!("log-out-shortcut")).size(14),
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
)
|
||||
.on_press(Message::Action(PowerAction::LogOut)),
|
||||
];
|
||||
|
||||
let power = row![
|
||||
power_buttons("system-suspend-symbolic", fl!("suspend"))
|
||||
.on_press(Message::Action(PowerAction::Suspend)),
|
||||
power_buttons("system-restart-symbolic", fl!("restart"))
|
||||
.on_press(Message::Action(PowerAction::Restart)),
|
||||
power_buttons("system-shutdown-symbolic", fl!("shutdown"))
|
||||
.on_press(Message::Action(PowerAction::Shutdown)),
|
||||
]
|
||||
.spacing(24)
|
||||
.padding([0, 24]);
|
||||
|
||||
let content = column![
|
||||
settings,
|
||||
padded_control(divider::horizontal::default()),
|
||||
session,
|
||||
padded_control(divider::horizontal::default()),
|
||||
power
|
||||
]
|
||||
.align_items(Alignment::Start)
|
||||
.padding([8, 0]);
|
||||
|
||||
self.core.applet.popup_container(content).into()
|
||||
} else if matches!(self.action_to_confirm, Some((c_id, _, _)) if c_id == id) {
|
||||
let (_, power_action, countdown) = self.action_to_confirm.as_ref().unwrap();
|
||||
let action = match power_action {
|
||||
PowerAction::Lock => "lock-screen",
|
||||
PowerAction::LogOut => "log-out",
|
||||
PowerAction::Suspend => "suspend",
|
||||
PowerAction::Restart => "restart",
|
||||
PowerAction::Shutdown => "shutdown",
|
||||
};
|
||||
let countdown = &countdown.to_string();
|
||||
let content = column![
|
||||
text(fl!(
|
||||
"confirm-question",
|
||||
HashMap::from_iter(vec![("action", action), ("countdown", countdown)])
|
||||
))
|
||||
.size(16),
|
||||
row![
|
||||
button(text(fl!("confirm")).size(14))
|
||||
.padding(8)
|
||||
.style(theme::Button::Suggested)
|
||||
.on_press(Message::Confirm),
|
||||
button(text(fl!("cancel")).size(14))
|
||||
.padding(8)
|
||||
.style(theme::Button::Standard)
|
||||
.on_press(Message::Cancel),
|
||||
]
|
||||
.spacing(24)
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(12)
|
||||
.padding(24);
|
||||
mouse_area(
|
||||
container(
|
||||
container(content)
|
||||
.style(cosmic::theme::Container::custom(|theme| {
|
||||
container::Appearance {
|
||||
icon_color: Some(theme.cosmic().background.on.into()),
|
||||
text_color: Some(theme.cosmic().background.on.into()),
|
||||
background: Some(
|
||||
Color::from(theme.cosmic().background.base).into(),
|
||||
),
|
||||
border: Border {
|
||||
radius: 12.0.into(),
|
||||
width: 2.0,
|
||||
color: theme.cosmic().bg_divider().into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
}))
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Shrink),
|
||||
)
|
||||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
)
|
||||
.on_press(Message::Cancel)
|
||||
.on_right_press(Message::Cancel)
|
||||
.on_middle_press(Message::Cancel)
|
||||
.into()
|
||||
} else {
|
||||
//panic!("no view for window {}", id.0)
|
||||
widget::text("").into()
|
||||
}
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
||||
Some(cosmic::applet::style())
|
||||
}
|
||||
}
|
||||
|
||||
fn power_buttons(
|
||||
name: &str,
|
||||
msg: String,
|
||||
) -> cosmic::widget::Button<Message, cosmic::Theme, Renderer> {
|
||||
cosmic::widget::button(
|
||||
column![text_icon(name, 40), text(msg).size(14)]
|
||||
.spacing(4)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(76.0))
|
||||
.style(theme::Button::Text)
|
||||
}
|
||||
|
||||
fn text_icon(name: &str, size: u16) -> cosmic::widget::Icon {
|
||||
icon::from_name(name).size(size).symbolic(true).icon()
|
||||
}
|
||||
|
||||
// ### System helpers
|
||||
|
||||
async fn restart() -> zbus::Result<()> {
|
||||
let connection = Connection::system().await?;
|
||||
let manager_proxy = ManagerProxy::new(&connection).await?;
|
||||
manager_proxy.reboot(true).await
|
||||
}
|
||||
|
||||
async fn shutdown() -> zbus::Result<()> {
|
||||
let connection = Connection::system().await?;
|
||||
let manager_proxy = ManagerProxy::new(&connection).await?;
|
||||
manager_proxy.power_off(true).await
|
||||
}
|
||||
|
||||
async fn suspend() -> zbus::Result<()> {
|
||||
let connection = Connection::system().await?;
|
||||
let manager_proxy = ManagerProxy::new(&connection).await?;
|
||||
manager_proxy.suspend(true).await
|
||||
}
|
||||
|
||||
async fn lock() -> zbus::Result<()> {
|
||||
let connection = Connection::system().await?;
|
||||
let manager_proxy = ManagerProxy::new(&connection).await?;
|
||||
// Get the session this current process is running in
|
||||
let our_uid = getuid().as_raw() as u32;
|
||||
let user_path = manager_proxy.get_user(our_uid).await?;
|
||||
let user = UserProxy::builder(&connection)
|
||||
.path(user_path)?
|
||||
.build()
|
||||
.await?;
|
||||
// Lock all non-TTY sessions of this user
|
||||
let sessions = user.sessions().await?;
|
||||
for (_, session_path) in sessions {
|
||||
let session = SessionProxy::builder(&connection)
|
||||
.path(session_path)?
|
||||
.build()
|
||||
.await?;
|
||||
if session.type_().await? != SessionType::TTY {
|
||||
session.lock().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn log_out() -> zbus::Result<()> {
|
||||
let session_type = std::env::var("XDG_CURRENT_DESKTOP").ok();
|
||||
let connection = Connection::session().await?;
|
||||
match session_type.as_ref().map(|s| s.trim()) {
|
||||
Some("pop:GNOME") => {
|
||||
let manager_proxy = SessionManagerProxy::new(&connection).await?;
|
||||
manager_proxy.logout(0).await?;
|
||||
}
|
||||
// By default assume COSMIC
|
||||
_ => {
|
||||
let cosmic_session = CosmicSessionProxy::new(&connection).await?;
|
||||
cosmic_session.exit().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,436 +1,10 @@
|
|||
use std::collections::HashMap;
|
||||
use std::process;
|
||||
use std::time::Duration;
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
use cosmic::applet::{menu_button, padded_control};
|
||||
use cosmic::iced;
|
||||
use cosmic::iced::alignment::{Horizontal, Vertical};
|
||||
use cosmic::iced::event::wayland::{self, LayerEvent};
|
||||
use cosmic::iced::event::{listen_with, PlatformSpecific};
|
||||
use cosmic::iced::time;
|
||||
use cosmic::iced::wayland::actions::layer_surface::SctkLayerSurfaceSettings;
|
||||
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
|
||||
use cosmic::iced_core::{Border, Shadow};
|
||||
use cosmic::iced_runtime::core::layout::Limits;
|
||||
use cosmic::iced_sctk::commands::layer_surface::{
|
||||
destroy_layer_surface, get_layer_surface, Anchor, KeyboardInteractivity,
|
||||
};
|
||||
use cosmic::iced_widget::mouse_area;
|
||||
use cosmic::widget::{button, divider, icon};
|
||||
use cosmic::Renderer;
|
||||
fn main() -> cosmic::iced::Result {
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
|
||||
use cosmic::iced::Color;
|
||||
use cosmic::iced::{
|
||||
widget::{self, column, container, row, space::Space, text},
|
||||
window, Alignment, Length, Subscription,
|
||||
};
|
||||
use cosmic::iced_style::application;
|
||||
use cosmic::theme;
|
||||
use cosmic::{app::Command, Element, Theme};
|
||||
tracing::info!("Starting power applet with version {VERSION}");
|
||||
|
||||
use logind_zbus::manager::ManagerProxy;
|
||||
use logind_zbus::session::{SessionProxy, SessionType};
|
||||
use logind_zbus::user::UserProxy;
|
||||
use nix::unistd::getuid;
|
||||
use zbus::Connection;
|
||||
|
||||
pub mod cosmic_session;
|
||||
mod localize;
|
||||
pub mod session_manager;
|
||||
|
||||
use crate::cosmic_session::CosmicSessionProxy;
|
||||
use crate::session_manager::SessionManagerProxy;
|
||||
|
||||
pub fn main() -> cosmic::iced::Result {
|
||||
localize::localize();
|
||||
|
||||
cosmic::applet::run::<Power>(false, ())
|
||||
}
|
||||
|
||||
const COUNTDOWN_LENGTH: u8 = 60;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Power {
|
||||
core: cosmic::app::Core,
|
||||
icon_name: String,
|
||||
popup: Option<window::Id>,
|
||||
action_to_confirm: Option<(window::Id, PowerAction, u8)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum PowerAction {
|
||||
Lock,
|
||||
LogOut,
|
||||
Suspend,
|
||||
Restart,
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Countdown,
|
||||
Action(PowerAction),
|
||||
TogglePopup,
|
||||
Settings,
|
||||
Confirm,
|
||||
Cancel,
|
||||
Zbus(Result<(), zbus::Error>),
|
||||
Closed(window::Id),
|
||||
}
|
||||
|
||||
impl cosmic::Application for Power {
|
||||
type Executor = cosmic::SingleThreadExecutor;
|
||||
type Flags = ();
|
||||
type Message = Message;
|
||||
const APP_ID: &'static str = "com.system76.CosmicAppletPower";
|
||||
|
||||
fn core(&self) -> &cosmic::app::Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut cosmic::app::Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
Self {
|
||||
core,
|
||||
icon_name: "system-shutdown-symbolic".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_close_requested(&self, id: window::Id) -> Option<Message> {
|
||||
Some(Message::Closed(id))
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
let mut subscriptions = Vec::with_capacity(2);
|
||||
subscriptions.push(listen_with(|e, _status| match e {
|
||||
cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
wayland::Event::Layer(LayerEvent::Unfocused, ..),
|
||||
)) => Some(Message::Cancel),
|
||||
_ => None,
|
||||
}));
|
||||
if self.action_to_confirm.is_some() {
|
||||
subscriptions
|
||||
.push(time::every(Duration::from_millis(1000)).map(|_| Message::Countdown));
|
||||
}
|
||||
Subscription::batch(subscriptions)
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::TogglePopup => {
|
||||
if let Some(p) = self.popup.take() {
|
||||
destroy_popup(p)
|
||||
} else {
|
||||
let new_id = window::Id::unique();
|
||||
self.popup.replace(new_id);
|
||||
|
||||
let mut popup_settings = self.core.applet.get_popup_settings(
|
||||
window::Id::MAIN,
|
||||
new_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
popup_settings.positioner.size_limits = Limits::NONE
|
||||
.min_width(100.0)
|
||||
.min_height(100.0)
|
||||
.max_height(400.0)
|
||||
.max_width(500.0);
|
||||
get_popup(popup_settings)
|
||||
}
|
||||
}
|
||||
Message::Settings => {
|
||||
let _ = process::Command::new("cosmic-settings").spawn();
|
||||
Command::none()
|
||||
}
|
||||
Message::Action(action) => {
|
||||
let id = window::Id::unique();
|
||||
self.action_to_confirm = Some((id, action, COUNTDOWN_LENGTH));
|
||||
get_layer_surface(SctkLayerSurfaceSettings {
|
||||
id,
|
||||
keyboard_interactivity: KeyboardInteractivity::None,
|
||||
anchor: Anchor::all(),
|
||||
namespace: "dialog".into(),
|
||||
size: Some((None, None)),
|
||||
size_limits: Limits::NONE.min_width(1.0).min_height(1.0),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
Message::Zbus(result) => {
|
||||
if let Err(e) = result {
|
||||
eprintln!("cosmic-applet-power ERROR: '{}'", e);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::Confirm => {
|
||||
if let Some((id, a, _)) = self.action_to_confirm.take() {
|
||||
let msg = |m| cosmic::app::message::app(Message::Zbus(m));
|
||||
Command::batch(vec![
|
||||
destroy_layer_surface(id),
|
||||
match a {
|
||||
PowerAction::Lock => iced::Command::perform(lock(), msg),
|
||||
PowerAction::LogOut => iced::Command::perform(log_out(), msg),
|
||||
PowerAction::Suspend => iced::Command::perform(suspend(), msg),
|
||||
PowerAction::Restart => iced::Command::perform(restart(), msg),
|
||||
PowerAction::Shutdown => iced::Command::perform(shutdown(), msg),
|
||||
},
|
||||
])
|
||||
} else {
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
Message::Cancel => {
|
||||
if let Some((id, _, _)) = self.action_to_confirm.take() {
|
||||
return destroy_layer_surface(id);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::Countdown => {
|
||||
if let Some((surface_id, a, countdown)) = self.action_to_confirm.as_mut() {
|
||||
*countdown -= 1;
|
||||
if *countdown == 0 {
|
||||
let id = *surface_id;
|
||||
let a = *a;
|
||||
|
||||
self.action_to_confirm = None;
|
||||
let msg = |m: zbus::Result<()>| cosmic::app::message::app(Message::Zbus(m));
|
||||
return Command::batch(vec![
|
||||
destroy_layer_surface(id),
|
||||
match a {
|
||||
PowerAction::Lock => iced::Command::perform(lock(), msg),
|
||||
PowerAction::LogOut => iced::Command::perform(log_out(), msg),
|
||||
PowerAction::Suspend => iced::Command::perform(suspend(), msg),
|
||||
PowerAction::Restart => iced::Command::perform(restart(), msg),
|
||||
PowerAction::Shutdown => iced::Command::perform(shutdown(), msg),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Message::Closed(id) => {
|
||||
if self.popup == Some(id) {
|
||||
self.popup = None;
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
self.core
|
||||
.applet
|
||||
.icon_button(&self.icon_name)
|
||||
.on_press(Message::TogglePopup)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_window(&self, id: window::Id) -> Element<Message> {
|
||||
if matches!(self.popup, Some(p) if p == id) {
|
||||
let settings = menu_button(text(fl!("settings")).size(14)).on_press(Message::Settings);
|
||||
|
||||
let session = column![
|
||||
menu_button(
|
||||
row![
|
||||
text_icon("system-lock-screen-symbolic", 24),
|
||||
text(fl!("lock-screen")).size(14),
|
||||
Space::with_width(Length::Fill),
|
||||
text(fl!("lock-screen-shortcut")).size(14),
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
)
|
||||
.on_press(Message::Action(PowerAction::Lock)),
|
||||
menu_button(
|
||||
row![
|
||||
text_icon("system-log-out-symbolic", 24),
|
||||
text(fl!("log-out")).size(14),
|
||||
Space::with_width(Length::Fill),
|
||||
text(fl!("log-out-shortcut")).size(14),
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
)
|
||||
.on_press(Message::Action(PowerAction::LogOut)),
|
||||
];
|
||||
|
||||
let power = row![
|
||||
power_buttons("system-suspend-symbolic", fl!("suspend"))
|
||||
.on_press(Message::Action(PowerAction::Suspend)),
|
||||
power_buttons("system-restart-symbolic", fl!("restart"))
|
||||
.on_press(Message::Action(PowerAction::Restart)),
|
||||
power_buttons("system-shutdown-symbolic", fl!("shutdown"))
|
||||
.on_press(Message::Action(PowerAction::Shutdown)),
|
||||
]
|
||||
.spacing(24)
|
||||
.padding([0, 24]);
|
||||
|
||||
let content = column![
|
||||
settings,
|
||||
padded_control(divider::horizontal::default()),
|
||||
session,
|
||||
padded_control(divider::horizontal::default()),
|
||||
power
|
||||
]
|
||||
.align_items(Alignment::Start)
|
||||
.padding([8, 0]);
|
||||
|
||||
self.core.applet.popup_container(content).into()
|
||||
} else if matches!(self.action_to_confirm, Some((c_id, _, _)) if c_id == id) {
|
||||
let (_, power_action, countdown) = self.action_to_confirm.as_ref().unwrap();
|
||||
let action = match power_action {
|
||||
PowerAction::Lock => "lock-screen",
|
||||
PowerAction::LogOut => "log-out",
|
||||
PowerAction::Suspend => "suspend",
|
||||
PowerAction::Restart => "restart",
|
||||
PowerAction::Shutdown => "shutdown",
|
||||
};
|
||||
let countdown = &countdown.to_string();
|
||||
let content = column![
|
||||
text(fl!(
|
||||
"confirm-question",
|
||||
HashMap::from_iter(vec![("action", action), ("countdown", countdown)])
|
||||
))
|
||||
.size(16),
|
||||
row![
|
||||
button(text(fl!("confirm")).size(14))
|
||||
.padding(8)
|
||||
.style(theme::Button::Suggested)
|
||||
.on_press(Message::Confirm),
|
||||
button(text(fl!("cancel")).size(14))
|
||||
.padding(8)
|
||||
.style(theme::Button::Standard)
|
||||
.on_press(Message::Cancel),
|
||||
]
|
||||
.spacing(24)
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(12)
|
||||
.padding(24);
|
||||
mouse_area(
|
||||
container(
|
||||
container(content)
|
||||
.style(cosmic::theme::Container::custom(|theme| {
|
||||
container::Appearance {
|
||||
icon_color: Some(theme.cosmic().background.on.into()),
|
||||
text_color: Some(theme.cosmic().background.on.into()),
|
||||
background: Some(
|
||||
Color::from(theme.cosmic().background.base).into(),
|
||||
),
|
||||
border: Border {
|
||||
radius: 12.0.into(),
|
||||
width: 2.0,
|
||||
color: theme.cosmic().bg_divider().into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
}))
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Shrink),
|
||||
)
|
||||
.align_x(Horizontal::Center)
|
||||
.align_y(Vertical::Center)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
)
|
||||
.on_press(Message::Cancel)
|
||||
.on_right_press(Message::Cancel)
|
||||
.on_middle_press(Message::Cancel)
|
||||
.into()
|
||||
} else {
|
||||
//panic!("no view for window {}", id.0)
|
||||
widget::text("").into()
|
||||
}
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
||||
Some(cosmic::applet::style())
|
||||
}
|
||||
}
|
||||
|
||||
fn power_buttons(
|
||||
name: &str,
|
||||
msg: String,
|
||||
) -> cosmic::widget::Button<Message, cosmic::Theme, Renderer> {
|
||||
cosmic::widget::button(
|
||||
column![text_icon(name, 40), text(msg).size(14)]
|
||||
.spacing(4)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(76.0))
|
||||
.style(theme::Button::Text)
|
||||
}
|
||||
|
||||
fn text_icon(name: &str, size: u16) -> cosmic::widget::Icon {
|
||||
icon::from_name(name).size(size).symbolic(true).icon()
|
||||
}
|
||||
|
||||
// ### System helpers
|
||||
|
||||
async fn restart() -> zbus::Result<()> {
|
||||
let connection = Connection::system().await?;
|
||||
let manager_proxy = ManagerProxy::new(&connection).await?;
|
||||
manager_proxy.reboot(true).await
|
||||
}
|
||||
|
||||
async fn shutdown() -> zbus::Result<()> {
|
||||
let connection = Connection::system().await?;
|
||||
let manager_proxy = ManagerProxy::new(&connection).await?;
|
||||
manager_proxy.power_off(true).await
|
||||
}
|
||||
|
||||
async fn suspend() -> zbus::Result<()> {
|
||||
let connection = Connection::system().await?;
|
||||
let manager_proxy = ManagerProxy::new(&connection).await?;
|
||||
manager_proxy.suspend(true).await
|
||||
}
|
||||
|
||||
async fn lock() -> zbus::Result<()> {
|
||||
let connection = Connection::system().await?;
|
||||
let manager_proxy = ManagerProxy::new(&connection).await?;
|
||||
// Get the session this current process is running in
|
||||
let our_uid = getuid().as_raw() as u32;
|
||||
let user_path = manager_proxy.get_user(our_uid).await?;
|
||||
let user = UserProxy::builder(&connection)
|
||||
.path(user_path)?
|
||||
.build()
|
||||
.await?;
|
||||
// Lock all non-TTY sessions of this user
|
||||
let sessions = user.sessions().await?;
|
||||
for (_, session_path) in sessions {
|
||||
let session = SessionProxy::builder(&connection)
|
||||
.path(session_path)?
|
||||
.build()
|
||||
.await?;
|
||||
if session.type_().await? != SessionType::TTY {
|
||||
session.lock().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn log_out() -> zbus::Result<()> {
|
||||
let session_type = std::env::var("XDG_CURRENT_DESKTOP").ok();
|
||||
let connection = Connection::session().await?;
|
||||
match session_type.as_ref().map(|s| s.trim()) {
|
||||
Some("pop:GNOME") => {
|
||||
let manager_proxy = SessionManagerProxy::new(&connection).await?;
|
||||
manager_proxy.logout(0).await?;
|
||||
}
|
||||
// By default assume COSMIC
|
||||
_ => {
|
||||
let cosmic_session = CosmicSessionProxy::new(&connection).await?;
|
||||
cosmic_session.exit().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
cosmic_applet_power::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,4 +9,7 @@ futures = "0.3"
|
|||
libcosmic.workspace = true
|
||||
serde = "1"
|
||||
tokio = { version = "1.23.0" }
|
||||
tracing.workspace = true
|
||||
tracing-log.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
zbus.workspace = true
|
||||
|
|
|
|||
6
cosmic-applet-status-area/src/lib.rs
Normal file
6
cosmic-applet-status-area/src/lib.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
mod components;
|
||||
mod subscriptions;
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
components::app::main()
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
mod components;
|
||||
mod subscriptions;
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
components::app::main()
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
|
||||
tracing::info!("Starting status-area applet with version {VERSION}");
|
||||
|
||||
cosmic_applet_status_area::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ once_cell = "1"
|
|||
i18n-embed = { version = "0.14.0", features = ["fluent-system", "desktop-requester"] }
|
||||
i18n-embed-fl = "0.7.0"
|
||||
rust-embed = "8.0.0"
|
||||
tracing = "0.1"
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-log.workspace = true
|
||||
cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" }
|
||||
tokio = { version = "1.17.0", features = ["sync", "rt"] }
|
||||
|
|
|
|||
12
cosmic-applet-tiling/src/lib.rs
Normal file
12
cosmic-applet-tiling/src/lib.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use crate::window::Window;
|
||||
|
||||
mod localize;
|
||||
mod wayland;
|
||||
mod wayland_subscription;
|
||||
mod window;
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize::localize();
|
||||
|
||||
cosmic::applet::run::<Window>(false, ())
|
||||
}
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
use crate::window::Window;
|
||||
|
||||
mod localize;
|
||||
mod wayland;
|
||||
mod wayland_subscription;
|
||||
mod window;
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
localize::localize();
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
|
||||
cosmic::applet::run::<Window>(false, ())
|
||||
tracing::info!("Starting tiling applet with version {VERSION}");
|
||||
|
||||
cosmic_applet_tiling::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,9 @@ use cctk::{
|
|||
use cosmic::iced::futures;
|
||||
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1::{self, TilingState};
|
||||
use futures::{channel::mpsc, executor::block_on, SinkExt};
|
||||
use std::{
|
||||
os::{
|
||||
use std::os::{
|
||||
fd::{FromRawFd, RawFd},
|
||||
unix::net::UnixStream,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::error;
|
||||
use wayland_client::{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ chrono = { version = "0.4.34", features = ["clock"] }
|
|||
once_cell = "1"
|
||||
tokio = { version = "1.36.0", features = ["time"] }
|
||||
tracing.workspace = true
|
||||
tracing-log.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
# Application i18n
|
||||
i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] }
|
||||
|
|
|
|||
11
cosmic-applet-time/src/lib.rs
Normal file
11
cosmic-applet-time/src/lib.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
mod localize;
|
||||
mod time;
|
||||
mod window;
|
||||
|
||||
use window::Window;
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize::localize();
|
||||
|
||||
cosmic::applet::run::<Window>(true, ())
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
mod localize;
|
||||
mod time;
|
||||
mod window;
|
||||
|
||||
use window::Window;
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
localize::localize();
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
|
||||
cosmic::applet::run::<Window>(true, ())
|
||||
tracing::info!("Starting time applet with version {VERSION}");
|
||||
|
||||
cosmic_applet_time::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ use cosmic::{
|
|||
};
|
||||
|
||||
use chrono::{DateTime, Datelike, DurationRound, Local, Months, NaiveDate, Timelike, Weekday};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::fl;
|
||||
use crate::time::get_calender_first;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ libcosmic.workspace = true
|
|||
cctk.workspace = true
|
||||
cosmic-protocols.workspace = true
|
||||
nix = "0.27.1"
|
||||
tracing = "0.1"
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-log.workspace = true
|
||||
once_cell = "1.9"
|
||||
|
|
|
|||
16
cosmic-applet-workspaces/src/lib.rs
Normal file
16
cosmic-applet-workspaces/src/lib.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
mod components;
|
||||
#[rustfmt::skip]
|
||||
mod config;
|
||||
mod localize;
|
||||
mod wayland;
|
||||
mod wayland_subscription;
|
||||
|
||||
use localize::localize;
|
||||
|
||||
use crate::components::app;
|
||||
|
||||
pub fn run() -> cosmic::iced::Result {
|
||||
localize();
|
||||
|
||||
app::run()
|
||||
}
|
||||
|
|
@ -1,28 +1,10 @@
|
|||
mod components;
|
||||
#[rustfmt::skip]
|
||||
mod config;
|
||||
mod localize;
|
||||
mod wayland;
|
||||
mod wayland_subscription;
|
||||
|
||||
use tracing::info;
|
||||
|
||||
use localize::localize;
|
||||
|
||||
use crate::components::app;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
// Initialize logger
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
|
||||
info!("Starting audio applet with version {VERSION}");
|
||||
info!("Iced Workspaces Applet ({VERSION})");
|
||||
tracing::info!("Starting workspaces applet with version {VERSION}");
|
||||
|
||||
// Prepare i18n
|
||||
localize();
|
||||
|
||||
app::run()
|
||||
cosmic_applet_workspaces::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,9 @@ use cctk::{
|
|||
};
|
||||
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
|
||||
use futures::{channel::mpsc, executor::block_on, SinkExt};
|
||||
use std::{
|
||||
os::{
|
||||
use std::os::{
|
||||
fd::{FromRawFd, RawFd},
|
||||
unix::net::UnixStream,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use wayland_client::backend::ObjectId;
|
||||
use wayland_client::{
|
||||
|
|
|
|||
22
cosmic-applets/Cargo.toml
Normal file
22
cosmic-applets/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "cosmic-applets"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cosmic-app-list = { path = "../cosmic-app-list" }
|
||||
cosmic-applet-audio = { path = "../cosmic-applet-audio" }
|
||||
cosmic-applet-battery = { path = "../cosmic-applet-battery" }
|
||||
cosmic-applet-bluetooth = { path = "../cosmic-applet-bluetooth" }
|
||||
cosmic-applet-minimize = { path = "../cosmic-applet-minimize" }
|
||||
cosmic-applet-network = { path = "../cosmic-applet-network" }
|
||||
cosmic-applet-notifications = { path = "../cosmic-applet-notifications" }
|
||||
cosmic-applet-power = { path = "../cosmic-applet-power" }
|
||||
cosmic-applet-status-area = { path = "../cosmic-applet-status-area" }
|
||||
cosmic-applet-tiling = { path = "../cosmic-applet-tiling" }
|
||||
cosmic-applet-time = { path = "../cosmic-applet-time" }
|
||||
cosmic-applet-workspaces = { path = "../cosmic-applet-workspaces" }
|
||||
libcosmic.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-log.workspace = true
|
||||
31
cosmic-applets/src/main.rs
Normal file
31
cosmic-applets/src/main.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
|
||||
let Some(applet) = std::env::args().next() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let start = applet.rfind('/').map(|v| v + 1).unwrap_or(0);
|
||||
let cmd = &applet.as_str()[start..];
|
||||
|
||||
tracing::info!("Starting `{cmd}` with version {VERSION}");
|
||||
|
||||
match cmd {
|
||||
"cosmic-app-list" => cosmic_app_list::run(),
|
||||
"cosmic-applet-audio" => cosmic_applet_audio::run(),
|
||||
"cosmic-applet-battery" => cosmic_applet_battery::run(),
|
||||
"cosmic-applet-bluetooth" => cosmic_applet_bluetooth::run(),
|
||||
"cosmic-applet-minimize" => cosmic_applet_minimize::run(),
|
||||
"cosmic-applet-network" => cosmic_applet_network::run(),
|
||||
"cosmic-applet-notifications" => cosmic_applet_notifications::run(),
|
||||
"cosmic-applet-power" => cosmic_applet_power::run(),
|
||||
"cosmic-applet-status-area" => cosmic_applet_status_area::run(),
|
||||
"cosmic-applet-tiling" => cosmic_applet_tiling::run(),
|
||||
"cosmic-applet-time" => cosmic_applet_time::run(),
|
||||
"cosmic-applet-workspaces" => cosmic_applet_workspaces::run(),
|
||||
_ => return Ok(()),
|
||||
}
|
||||
}
|
||||
12
debian/links
vendored
Normal file
12
debian/links
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/usr/bin/cosmic-applets /usr/bin/cosmic-app-list
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-audio
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-battery
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-bluetooth
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-minimize
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-network
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-notifications
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-power
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-status-area
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-tiling
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-time
|
||||
/usr/bin/cosmic-applets /usr/bin/cosmic-applet-workspaces
|
||||
12
debian/rules
vendored
12
debian/rules
vendored
|
|
@ -16,15 +16,15 @@ override_dh_auto_clean:
|
|||
fi
|
||||
|
||||
if ! ischroot && test "${VENDOR}" = "1"; then \
|
||||
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; \
|
||||
just vendor; \
|
||||
fi
|
||||
|
||||
override_dh_auto_build:
|
||||
just rootdir=$(DESTDIR) debug=$(DEBUG) vendor=$(VENDOR) build
|
||||
if test "${VENDOR}" = "1"; then \
|
||||
just rootdir=$(DESTDIR) debug=$(DEBUG) build-vendored; \
|
||||
else \
|
||||
just rootdir=$(DESTDIR) debug=$(DEBUG) build-release; \
|
||||
fi
|
||||
|
||||
override_dh_auto_install:
|
||||
just rootdir=$(DESTDIR) install
|
||||
|
|
|
|||
54
justfile
54
justfile
|
|
@ -14,9 +14,9 @@ iconsdir := sharedir + '/icons/hicolor'
|
|||
bindir := rootdir + prefix + '/bin'
|
||||
default-schema-target := sharedir / 'cosmic'
|
||||
|
||||
build: _extract_vendor
|
||||
#!/usr/bin/env bash
|
||||
cargo build {{cargo_args}}
|
||||
cosmic-applets-bin := bindir / 'cosmic-applets'
|
||||
|
||||
default: build-release
|
||||
|
||||
# Compiles with debug profile
|
||||
build-debug *args:
|
||||
|
|
@ -25,6 +25,12 @@ build-debug *args:
|
|||
# Compiles with release profile
|
||||
build-release *args: (build-debug '--release' args)
|
||||
|
||||
# Compile with a vendored tarball
|
||||
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
|
||||
|
||||
_link_applet name:
|
||||
ln -sf {{cosmic-applets-bin}} {{name}}
|
||||
|
||||
_install_icons name:
|
||||
find {{name}}/'data'/'icons' -type f -exec echo {} \; | rev | cut -d'/' -f-3 | rev | xargs -d '\n' -I {} install -Dm0644 {{name}}/'data'/'icons'/{} {{iconsdir}}/{}
|
||||
|
||||
|
|
@ -37,33 +43,25 @@ _install_desktop path:
|
|||
_install_bin name:
|
||||
install -Dm0755 {{targetdir}}/{{target}}/{{name}} {{bindir}}/{{name}}
|
||||
|
||||
_install id name: (_install_icons name) (_install_desktop name + '/data/' + id + '.desktop') (_install_bin name)
|
||||
_install_applet id name: (_install_icons name) \
|
||||
(_install_desktop name + '/data/' + id + '.desktop') \
|
||||
(_link_applet name)
|
||||
|
||||
_install_app_list: (_install 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list')
|
||||
_install_audio: (_install 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio')
|
||||
_install_battery: (_install 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery')
|
||||
_install_bluetooth: (_install 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth')
|
||||
_install_minimize: (_install 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize')
|
||||
_install_network: (_install 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network')
|
||||
_install_notifications: (_install 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications')
|
||||
_install_power: (_install 'com.system76.CosmicAppletPower' 'cosmic-applet-power')
|
||||
_install_workspace: (_install 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces')
|
||||
_install_time: (_install 'com.system76.CosmicAppletTime' 'cosmic-applet-time')
|
||||
_install_tiling: (_install 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling')
|
||||
_install_status_area: (_install 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area')
|
||||
|
||||
# TODO: Turn this into one configurable applet?
|
||||
_install_panel_button: (_install_bin 'cosmic-panel-button')
|
||||
_install_button id name: (_install_icons name) (_install_desktop name + '/data/' + id + '.desktop')
|
||||
_install_app_button: (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button')
|
||||
_install_workspaces_button: (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button')
|
||||
|
||||
# Installs files into the system
|
||||
install: _install_app_list _install_audio _install_battery _install_bluetooth _install_minimize _install_network _install_notifications _install_power _install_workspace _install_time _install_tiling _install_panel_button _install_app_button _install_workspaces_button _install_status_area
|
||||
install: (_install_bin 'cosmic-applets') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_bin 'cosmic-panel-button') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button')
|
||||
|
||||
# Extracts vendored dependencies if vendor=1
|
||||
_extract_vendor:
|
||||
#!/usr/bin/env sh
|
||||
if test {{vendor}} = 1; then
|
||||
rm -rf vendor; tar pxf vendor.tar
|
||||
fi
|
||||
# Vendor Cargo dependencies locally
|
||||
vendor:
|
||||
mkdir -p .cargo
|
||||
cargo vendor | head -n -1 > .cargo/config
|
||||
echo 'directory = "vendor"' >> .cargo/config
|
||||
tar pcf vendor.tar vendor
|
||||
rm -rf vendor
|
||||
|
||||
# Extracts vendored dependencies
|
||||
[private]
|
||||
vendor-extract:
|
||||
rm -rf vendor
|
||||
tar pxf vendor.tar
|
||||
Loading…
Add table
Add a link
Reference in a new issue