From 148661b18d39549ba0f8569b3b6f5c69fe5483a5 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Tue, 9 Apr 2024 17:09:37 +1000 Subject: [PATCH 1/3] feat(i3status): new applet to embed `i3status` in panel/dock (stub) --- Cargo.lock | 20 +++++ Cargo.toml | 1 + cosmic-applet-i3status/Cargo.toml | 21 +++++ .../com.system76.CosmicAppletI3status.desktop | 12 +++ cosmic-applet-i3status/i18n.toml | 4 + .../i18n/en/cosmic_applet_i3status.ftl | 0 cosmic-applet-i3status/src/lib.rs | 84 +++++++++++++++++++ cosmic-applet-i3status/src/localize.rs | 47 +++++++++++ cosmic-applet-i3status/src/main.rs | 10 +++ cosmic-applets/Cargo.toml | 3 +- cosmic-applets/src/main.rs | 1 + justfile | 7 +- 12 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 cosmic-applet-i3status/Cargo.toml create mode 100644 cosmic-applet-i3status/data/com.system76.CosmicAppletI3status.desktop create mode 100644 cosmic-applet-i3status/i18n.toml create mode 100644 cosmic-applet-i3status/i18n/en/cosmic_applet_i3status.ftl create mode 100644 cosmic-applet-i3status/src/lib.rs create mode 100644 cosmic-applet-i3status/src/localize.rs create mode 100644 cosmic-applet-i3status/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index de7e0e26e..4bea60f8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -958,6 +958,25 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "cosmic-applet-i3status" +version = "0.1.1" +dependencies = [ + "anyhow", + "i18n-embed", + "i18n-embed-fl", + "image 0.25.0", + "libcosmic", + "memmap2 0.9.4", + "once_cell", + "rust-embed", + "rustix 0.38.32", + "tokio", + "tracing", + "tracing-log", + "tracing-subscriber", +] + [[package]] name = "cosmic-applet-minimize" version = "0.1.1" @@ -1112,6 +1131,7 @@ dependencies = [ "cosmic-applet-audio", "cosmic-applet-battery", "cosmic-applet-bluetooth", + "cosmic-applet-i3status", "cosmic-applet-minimize", "cosmic-applet-network", "cosmic-applet-notifications", diff --git a/Cargo.toml b/Cargo.toml index 348ab45b2..0b941b70d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "cosmic-applet-audio", "cosmic-applet-battery", "cosmic-applet-bluetooth", + "cosmic-applet-i3status", "cosmic-applet-minimize", "cosmic-applet-network", "cosmic-applet-notifications", diff --git a/cosmic-applet-i3status/Cargo.toml b/cosmic-applet-i3status/Cargo.toml new file mode 100644 index 000000000..2ea2bc1d6 --- /dev/null +++ b/cosmic-applet-i3status/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "cosmic-applet-i3status" +version = "0.1.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow.workspace = true +i18n-embed-fl.workspace = true +i18n-embed.workspace = true +image = { version = "0.25.0", default-features = false } +libcosmic.workspace = true +memmap2 = "0.9.4" +once_cell = "1" +rust-embed.workspace = true +rustix.workspace = true +tokio = { version = "1.36.0", features = ["sync", "macros"] } +tracing-log.workspace = true +tracing-subscriber.workspace = true +tracing.workspace = true diff --git a/cosmic-applet-i3status/data/com.system76.CosmicAppletI3status.desktop b/cosmic-applet-i3status/data/com.system76.CosmicAppletI3status.desktop new file mode 100644 index 000000000..dd84291ba --- /dev/null +++ b/cosmic-applet-i3status/data/com.system76.CosmicAppletI3status.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Cosmic Applet i3status +Comment=Applet for Cosmic Panel +Type=Application +Exec=cosmic-applet-i3status +Terminal=false +Categories=Cosmic;Iced; +Keywords=Cosmic;Iced; +StartupNotify=true +NoDisplay=true +X-CosmicApplet=true +X-HostWaylandDisplay=true diff --git a/cosmic-applet-i3status/i18n.toml b/cosmic-applet-i3status/i18n.toml new file mode 100644 index 000000000..05c50ba2b --- /dev/null +++ b/cosmic-applet-i3status/i18n.toml @@ -0,0 +1,4 @@ +fallback_language = "en" + +[fluent] +assets_dir = "i18n" \ No newline at end of file diff --git a/cosmic-applet-i3status/i18n/en/cosmic_applet_i3status.ftl b/cosmic-applet-i3status/i18n/en/cosmic_applet_i3status.ftl new file mode 100644 index 000000000..e69de29bb diff --git a/cosmic-applet-i3status/src/lib.rs b/cosmic-applet-i3status/src/lib.rs new file mode 100644 index 000000000..94f6318e0 --- /dev/null +++ b/cosmic-applet-i3status/src/lib.rs @@ -0,0 +1,84 @@ +mod localize; + +use crate::localize::localize; +use cosmic::app::Command; +use cosmic::applet::cosmic_panel_config::PanelAnchor; +use cosmic::iced::Length; + +use cosmic::iced_style::application; +use cosmic::iced_widget::{Column, Row}; + +use cosmic::{Element, Theme}; + +pub fn run() -> cosmic::iced::Result { + localize(); + cosmic::applet::run::(true, ()) +} + +#[derive(Default)] +struct I3status { + core: cosmic::app::Core, +} + +#[derive(Debug, Clone)] +enum Message {} + +impl cosmic::Application for I3status { + type Message = Message; + type Executor = cosmic::SingleThreadExecutor; + type Flags = (); + const APP_ID: &'static str = "com.system76.CosmicAppletI3status"; + + fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command) { + (Self { core }, 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<::Style> { + Some(cosmic::applet::style()) + } + + fn update(&mut self, _message: Message) -> Command { + Command::none() + } + + // TODO: subscribe? to new output from the `i3status` process + // fn subscription(&self) -> Subscription { + // } + + fn view(&self) -> Element { + let theme = self.core.system_theme().cosmic(); + let space_xxs = theme.space_xxs(); + + let label = cosmic::iced_widget::text("i3status here"); + let children = vec![label.into()]; + + if matches!( + self.core.applet.anchor, + PanelAnchor::Top | PanelAnchor::Bottom + ) { + Row::with_children(children) + .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(children) + .align_items(cosmic::iced_core::Alignment::Center) + .height(Length::Shrink) + .width(Length::Shrink) + .spacing(space_xxs) + .padding([space_xxs, 0]) + .into() + } + } +} diff --git a/cosmic-applet-i3status/src/localize.rs b/cosmic-applet-i3status/src/localize.rs new file mode 100644 index 000000000..caf9d66f7 --- /dev/null +++ b/cosmic-applet-i3status/src/localize.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MPL-2.0-only + +use i18n_embed::{ + fluent::{fluent_language_loader, FluentLanguageLoader}, + DefaultLocalizer, LanguageLoader, Localizer, +}; +use once_cell::sync::Lazy; +use rust_embed::RustEmbed; + +#[derive(RustEmbed)] +#[folder = "i18n/"] +struct Localizations; + +pub static LANGUAGE_LOADER: Lazy = Lazy::new(|| { + let loader: FluentLanguageLoader = fluent_language_loader!(); + + loader + .load_fallback_language(&Localizations) + .expect("Error while loading fallback language"); + + loader +}); + +#[macro_export] +macro_rules! fl { + ($message_id:literal) => {{ + i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id) + }}; + + ($message_id:literal, $($args:expr),*) => {{ + i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *) + }}; +} + +// Get the `Localizer` to be used for localizing this library. +pub fn localizer() -> Box { + Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) +} + +pub fn localize() { + let localizer = localizer(); + let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); + + if let Err(error) = localizer.select(&requested_languages) { + eprintln!("Error while loading language for i3status {}", error); + } +} diff --git a/cosmic-applet-i3status/src/main.rs b/cosmic-applet-i3status/src/main.rs new file mode 100644 index 000000000..7dd2c6815 --- /dev/null +++ b/cosmic-applet-i3status/src/main.rs @@ -0,0 +1,10 @@ +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +fn main() -> cosmic::iced::Result { + tracing_subscriber::fmt::init(); + let _ = tracing_log::LogTracer::init(); + + tracing::info!("Starting minimize applet with version {VERSION}"); + + cosmic_applet_i3status::run() +} diff --git a/cosmic-applets/Cargo.toml b/cosmic-applets/Cargo.toml index 70965bf20..3a7ba9aa6 100644 --- a/cosmic-applets/Cargo.toml +++ b/cosmic-applets/Cargo.toml @@ -8,6 +8,7 @@ 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-i3status = { path = "../cosmic-applet-i3status" } cosmic-applet-minimize = { path = "../cosmic-applet-minimize" } cosmic-applet-network = { path = "../cosmic-applet-network" } cosmic-applet-notifications = { path = "../cosmic-applet-notifications" } @@ -19,4 +20,4 @@ cosmic-applet-workspaces = { path = "../cosmic-applet-workspaces" } libcosmic.workspace = true tracing.workspace = true tracing-subscriber.workspace = true -tracing-log.workspace = true \ No newline at end of file +tracing-log.workspace = true diff --git a/cosmic-applets/src/main.rs b/cosmic-applets/src/main.rs index 389ce2d3a..693d08a1e 100644 --- a/cosmic-applets/src/main.rs +++ b/cosmic-applets/src/main.rs @@ -18,6 +18,7 @@ fn main() -> cosmic::iced::Result { "cosmic-applet-audio" => cosmic_applet_audio::run(), "cosmic-applet-battery" => cosmic_applet_battery::run(), "cosmic-applet-bluetooth" => cosmic_applet_bluetooth::run(), + "cosmic-applet-i3status" => cosmic_applet_i3status::run(), "cosmic-applet-minimize" => cosmic_applet_minimize::run(), "cosmic-applet-network" => cosmic_applet_network::run(), "cosmic-applet-notifications" => cosmic_applet_notifications::run(), diff --git a/justfile b/justfile index a7d196fc0..e91bf42af 100644 --- a/justfile +++ b/justfile @@ -48,10 +48,13 @@ _install_applet id name: (_install_icons name) \ (_install_desktop name + '/data/' + id + '.desktop') \ (_link_applet name) +_install_applet_noicon id name: (_install_desktop name + '/data/' + id + '.desktop') \ + (_link_applet name) + _install_button id name: (_install_icons name) (_install_desktop name + '/data/' + id + '.desktop') # Installs files into the system -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.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') +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.CosmicAppletI3status' 'cosmic-applet-i3status') (_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.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') # Vendor Cargo dependencies locally vendor: @@ -65,4 +68,4 @@ vendor: [private] vendor-extract: rm -rf vendor - tar pxf vendor.tar \ No newline at end of file + tar pxf vendor.tar From f1ba338942155f4c452eeae2f0a4ff87443c647f Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Wed, 10 Apr 2024 11:28:27 +1000 Subject: [PATCH 2/3] feat(i3status): new applet to embed `i3status` in panel/dock (stdout) --- cosmic-applet-i3status/Cargo.toml | 2 +- cosmic-applet-i3status/src/lib.rs | 29 +++++++---- cosmic-applet-i3status/src/subprocess.rs | 61 ++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 cosmic-applet-i3status/src/subprocess.rs diff --git a/cosmic-applet-i3status/Cargo.toml b/cosmic-applet-i3status/Cargo.toml index 2ea2bc1d6..20b9e25ff 100644 --- a/cosmic-applet-i3status/Cargo.toml +++ b/cosmic-applet-i3status/Cargo.toml @@ -15,7 +15,7 @@ memmap2 = "0.9.4" once_cell = "1" rust-embed.workspace = true rustix.workspace = true -tokio = { version = "1.36.0", features = ["sync", "macros"] } +tokio = { version = "1.36.0", features = ["process", "sync", "macros"] } tracing-log.workspace = true tracing-subscriber.workspace = true tracing.workspace = true diff --git a/cosmic-applet-i3status/src/lib.rs b/cosmic-applet-i3status/src/lib.rs index 94f6318e0..859faec13 100644 --- a/cosmic-applet-i3status/src/lib.rs +++ b/cosmic-applet-i3status/src/lib.rs @@ -1,14 +1,17 @@ mod localize; +mod subprocess; use crate::localize::localize; use cosmic::app::Command; use cosmic::applet::cosmic_panel_config::PanelAnchor; use cosmic::iced::Length; +use cosmic::iced_futures::Subscription; use cosmic::iced_style::application; use cosmic::iced_widget::{Column, Row}; use cosmic::{Element, Theme}; +use subprocess::Message; pub fn run() -> cosmic::iced::Result { localize(); @@ -18,11 +21,9 @@ pub fn run() -> cosmic::iced::Result { #[derive(Default)] struct I3status { core: cosmic::app::Core, + msg: String, } -#[derive(Debug, Clone)] -enum Message {} - impl cosmic::Application for I3status { type Message = Message; type Executor = cosmic::SingleThreadExecutor; @@ -30,7 +31,13 @@ impl cosmic::Application for I3status { const APP_ID: &'static str = "com.system76.CosmicAppletI3status"; fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command) { - (Self { core }, Command::none()) + ( + Self { + core, + msg: String::from("i3status here"), + }, + Command::none(), + ) } fn core(&self) -> &cosmic::app::Core { @@ -45,19 +52,23 @@ impl cosmic::Application for I3status { Some(cosmic::applet::style()) } - fn update(&mut self, _message: Message) -> Command { + fn update(&mut self, message: Message) -> Command { + match message { + Message::Output(msg) => self.msg = msg, + Message::Error(msg) => self.msg = msg, + } Command::none() } - // TODO: subscribe? to new output from the `i3status` process - // fn subscription(&self) -> Subscription { - // } + fn subscription(&self) -> Subscription { + subprocess::start() + } fn view(&self) -> Element { let theme = self.core.system_theme().cosmic(); let space_xxs = theme.space_xxs(); - let label = cosmic::iced_widget::text("i3status here"); + let label = cosmic::iced_widget::text(&self.msg); let children = vec![label.into()]; if matches!( diff --git a/cosmic-applet-i3status/src/subprocess.rs b/cosmic-applet-i3status/src/subprocess.rs new file mode 100644 index 000000000..12c2be38f --- /dev/null +++ b/cosmic-applet-i3status/src/subprocess.rs @@ -0,0 +1,61 @@ +use std::{any::TypeId, process::Stdio}; + +use cosmic::{iced::subscription, iced_futures::futures::SinkExt}; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + process::Command, +}; + +const CHANNEL_SIZE: usize = 1; + +// TODO: support auto-detection of `i3status` and `i3status-rs` executables +// TODO: support basename-only commands by searching PATH (using "which" crate?) +// TODO: provide GUI for user to add their own preferred command +const COMMAND: &str = "/usr/bin/i3status-rs"; + +pub fn start() -> cosmic::iced::Subscription { + struct Worker; + subscription::channel( + TypeId::of::(), + CHANNEL_SIZE, + |mut output| async move { + loop { + let mut cmd = Command::new(COMMAND) + .args(["--version"]) + .stdout(Stdio::piped()) + .spawn() + .expect("{command} should start"); + let stdout = cmd + .stdout + .as_mut() + .expect("should capture stdout from {command}"); + let reader = BufReader::new(stdout); + let mut lines = reader.lines(); + while let Some(line) = lines + .next_line() + .await + .expect("should read from buffered stdout") + { + output + .send(Message::Output(line)) + .await + .expect("should send stdout line"); + } + + // effectively end the subscription by never sending more values + cosmic::iced::futures::future::pending().await + } + }, + ) +} + +#[derive(Clone, Debug)] +pub enum Message { + Output(String), + Error(String), +} + +enum State { + Starting, + Ready(), +} From c7c0c70a97ebb44f8f5b693dde6509679762593f Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Sat, 13 Apr 2024 14:31:25 +1000 Subject: [PATCH 3/3] feat(i3status): new applet to embed `i3status` in panel/dock (blocks) --- Cargo.lock | 21 +++- cosmic-applet-i3status/Cargo.toml | 7 +- cosmic-applet-i3status/src/lib.rs | 51 +++++--- cosmic-applet-i3status/src/subprocess.rs | 149 ++++++++++++++++------- 4 files changed, 161 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4bea60f8f..d5a2a7c26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -965,12 +965,11 @@ dependencies = [ "anyhow", "i18n-embed", "i18n-embed-fl", - "image 0.25.0", "libcosmic", - "memmap2 0.9.4", "once_cell", "rust-embed", - "rustix 0.38.32", + "serde_json", + "swaybar-types", "tokio", "tracing", "tracing-log", @@ -4961,6 +4960,16 @@ dependencies = [ "zeno", ] +[[package]] +name = "swaybar-types" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c0b435952b89d872f882cf7ae0756303ef68d310bfa44b9c8012fda88ae143" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "switcheroo-control" version = "0.1.0" @@ -5863,7 +5872,7 @@ dependencies = [ "js-sys", "log", "naga", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "profiling", "raw-window-handle 0.6.0", "smallvec", @@ -5890,7 +5899,7 @@ dependencies = [ "log", "naga", "once_cell", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "profiling", "raw-window-handle 0.6.0", "rustc-hash", @@ -5930,7 +5939,7 @@ dependencies = [ "naga", "objc", "once_cell", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "profiling", "range-alloc", "raw-window-handle 0.6.0", diff --git a/cosmic-applet-i3status/Cargo.toml b/cosmic-applet-i3status/Cargo.toml index 20b9e25ff..bcecec844 100644 --- a/cosmic-applet-i3status/Cargo.toml +++ b/cosmic-applet-i3status/Cargo.toml @@ -9,13 +9,12 @@ edition = "2021" anyhow.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true -image = { version = "0.25.0", default-features = false } libcosmic.workspace = true -memmap2 = "0.9.4" once_cell = "1" rust-embed.workspace = true -rustix.workspace = true -tokio = { version = "1.36.0", features = ["process", "sync", "macros"] } +serde_json = "1" +swaybar-types = "3.0.0" +tokio = { version = "1.36.0", features = ["time", "process"] } tracing-log.workspace = true tracing-subscriber.workspace = true tracing.workspace = true diff --git a/cosmic-applet-i3status/src/lib.rs b/cosmic-applet-i3status/src/lib.rs index 859faec13..e123f16ff 100644 --- a/cosmic-applet-i3status/src/lib.rs +++ b/cosmic-applet-i3status/src/lib.rs @@ -5,13 +5,13 @@ use crate::localize::localize; use cosmic::app::Command; use cosmic::applet::cosmic_panel_config::PanelAnchor; use cosmic::iced::Length; - use cosmic::iced_futures::Subscription; use cosmic::iced_style::application; use cosmic::iced_widget::{Column, Row}; - use cosmic::{Element, Theme}; -use subprocess::Message; +use subprocess::Output; +use swaybar_types::Block; +use tracing::{span, Level}; pub fn run() -> cosmic::iced::Result { localize(); @@ -20,21 +20,23 @@ pub fn run() -> cosmic::iced::Result { #[derive(Default)] struct I3status { + blocks: Vec, core: cosmic::app::Core, - msg: String, + text: String, } impl cosmic::Application for I3status { - type Message = Message; + type Message = Output; type Executor = cosmic::SingleThreadExecutor; type Flags = (); const APP_ID: &'static str = "com.system76.CosmicAppletI3status"; - fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command) { + fn init(core: cosmic::app::Core, _flags: ()) -> (Self, Command) { ( Self { + blocks: vec![], core, - msg: String::from("i3status here"), + text: String::new(), }, Command::none(), ) @@ -52,24 +54,43 @@ impl cosmic::Application for I3status { Some(cosmic::applet::style()) } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Output) -> Command { + let span = span!(Level::TRACE, "I3status::update()"); + let _ = span.enter(); match message { - Message::Output(msg) => self.msg = msg, - Message::Error(msg) => self.msg = msg, + Output::Blocks(blocks) => { + self.blocks = blocks; + self.text = String::new(); + } + Output::Raw(output) => { + self.blocks = vec![]; + self.text = output; + } + Output::None => {} } Command::none() } - fn subscription(&self) -> Subscription { - subprocess::start() + fn subscription(&self) -> Subscription { + let span = span!(Level::TRACE, "I3status::subscription()"); + let _ = span.enter(); + subprocess::child_process() } - fn view(&self) -> Element { + fn view(&self) -> Element { let theme = self.core.system_theme().cosmic(); let space_xxs = theme.space_xxs(); - let label = cosmic::iced_widget::text(&self.msg); - let children = vec![label.into()]; + let children = if !self.blocks.is_empty() { + self.blocks + .iter() + .map(|block| cosmic::iced_widget::text(&block.full_text).into()) + .collect::>>() + } else if !self.text.is_empty() { + vec![cosmic::iced_widget::text(&self.text).into()] + } else { + vec![cosmic::iced_widget::text("no output").into()] + }; if matches!( self.core.applet.anchor, diff --git a/cosmic-applet-i3status/src/subprocess.rs b/cosmic-applet-i3status/src/subprocess.rs index 12c2be38f..7534d610c 100644 --- a/cosmic-applet-i3status/src/subprocess.rs +++ b/cosmic-applet-i3status/src/subprocess.rs @@ -1,61 +1,126 @@ -use std::{any::TypeId, process::Stdio}; +use std::{any::TypeId, io, process::Stdio, time::Duration}; -use cosmic::{iced::subscription, iced_futures::futures::SinkExt}; +use cosmic::iced_futures::futures::future; +use swaybar_types::Block; use tokio::{ - io::{AsyncBufReadExt, BufReader}, - process::Command, + io::{AsyncBufReadExt, BufReader, Lines}, + process::{Child, ChildStdout, Command}, + time::sleep, }; -const CHANNEL_SIZE: usize = 1; - // TODO: support auto-detection of `i3status` and `i3status-rs` executables // TODO: support basename-only commands by searching PATH (using "which" crate?) // TODO: provide GUI for user to add their own preferred command const COMMAND: &str = "/usr/bin/i3status-rs"; -pub fn start() -> cosmic::iced::Subscription { - struct Worker; - subscription::channel( - TypeId::of::(), - CHANNEL_SIZE, - |mut output| async move { - loop { - let mut cmd = Command::new(COMMAND) - .args(["--version"]) - .stdout(Stdio::piped()) - .spawn() - .expect("{command} should start"); - let stdout = cmd +fn spawn() -> io::Result { + Command::new(COMMAND) + // `.stdin(Stdio::null())` is faster, but `i3status-rs` requires a working stdio + // (and the i3bar protocol doesn't specify either way) + // TODO: support click events on blocks + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() +} + +async fn read_blocks(state: State) -> (Output, State) { + match state { + State::Ready => match spawn() { + Ok(mut child) => { + let stdout = child .stdout - .as_mut() - .expect("should capture stdout from {command}"); + .take() + .expect("should capture stdout from i3status"); let reader = BufReader::new(stdout); - let mut lines = reader.lines(); - while let Some(line) = lines - .next_line() - .await - .expect("should read from buffered stdout") - { - output - .send(Message::Output(line)) - .await - .expect("should send stdout line"); - } - - // effectively end the subscription by never sending more values - cosmic::iced::futures::future::pending().await + let stdout_lines = reader.lines(); + ( + Output::Raw(String::from("i3status started")), + State::Running { + child, + stdout_lines, + }, + ) } + Err(_) => ( + Output::Raw(String::from("cannot spawn i3status")), + State::Finished, + ), }, - ) + State::Running { + child, + mut stdout_lines, + } => { + match stdout_lines.next_line().await { + Ok(Some(line)) => { + // for more information about the protocol, + // see: https://i3wm.org/docs/i3bar-protocol.html + + // the "endless array" output means we have a dangling comma to remove + let line = line.trim_end_matches(','); + + if let Ok(blocks) = serde_json::from_str::>(line) { + ( + Output::Blocks(blocks), + State::Running { + child, + stdout_lines, + }, + ) + } else { + ( + Output::Raw(String::from(line)), + State::Running { + child, + stdout_lines, + }, + ) + } + } + Ok(None) => { + sleep(Duration::from_secs(3)).await; + ( + Output::None, + State::Running { + child, + stdout_lines, + }, + ) + } + Err(_) => ( + Output::Raw(String::from("cannot read i3status stdout")), + State::Finished, + ), + } + } + State::Finished => { + // We do not let the stream die, as it would start a + // new download repeatedly if the user is not careful + // in case of errors. + future::pending().await + } + } +} + +pub fn child_process() -> cosmic::iced::Subscription { + struct SomeWorker; + cosmic::iced::subscription::unfold(TypeId::of::(), State::Ready, |state| { + read_blocks(state) + }) } #[derive(Clone, Debug)] -pub enum Message { - Output(String), - Error(String), +pub enum Output { + Blocks(Vec), + Raw(String), + None, } -enum State { - Starting, - Ready(), +#[derive(Debug)] +pub enum State { + Ready, + Running { + child: Child, + stdout_lines: Lines>, + }, + Finished, }