Skip to content

Commit

Permalink
WIP status-line-applet
Browse files Browse the repository at this point in the history
  • Loading branch information
ids1024 committed Apr 18, 2023
1 parent 8b46cc2 commit e22ad3f
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 1 deletion.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"cosmic-applet-network",
"cosmic-applet-notifications",
"cosmic-applet-power",
"cosmic-applet-status-line",
"cosmic-applet-time",
"cosmic-applet-workspaces",
"cosmic-panel-button",
Expand Down
11 changes: 11 additions & 0 deletions cosmic-applet-status-line/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "cosmic-applet-status-line"
version = "0.1.0"
edition = "2021"

[dependencies]
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.27", features = ["io-util", "process", "sync"] }
tokio-stream = "0.1"
91 changes: 91 additions & 0 deletions cosmic-applet-status-line/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// TODO: work vertically

use cosmic::{
applet::CosmicAppletHelper,
iced::{self, wayland::InitialSurface, Application},
iced_native::window,
iced_sctk::layout::Limits,
iced_style::application,
};

mod protocol;

#[derive(Debug)]
enum Msg {
Protocol(protocol::StatusLine),
CloseRequest,
}

#[derive(Default)]
struct App {
status_line: protocol::StatusLine,
}

impl iced::Application for App {
type Message = Msg;
type Theme = cosmic::Theme;
type Executor = cosmic::SingleThreadExecutor;
type Flags = ();

fn new(_flags: ()) -> (Self, iced::Command<Msg>) {
(App::default(), iced::Command::none())
}

fn title(&self) -> String {
String::from("Status Line")
}

fn close_requested(&self, _id: window::Id) -> Msg {
Msg::CloseRequest
}

fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| application::Appearance {
background_color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
}

fn subscription(&self) -> iced::Subscription<Msg> {
protocol::subscription().map(Msg::Protocol)
}

fn update(&mut self, message: Msg) -> iced::Command<Msg> {
match message {
Msg::Protocol(status_line) => {
println!("{:?}", status_line);
self.status_line = status_line;
}
Msg::CloseRequest => {}
}
iced::Command::none()
}

fn view(&self, _id: window::Id) -> cosmic::Element<Msg> {
iced::widget::row(self.status_line.blocks.iter().map(block_view).collect()).into()
}
}

// TODO seperator
fn block_view(block: &protocol::Block) -> cosmic::Element<Msg> {
let theme = block
.color
.map(cosmic::theme::Text::Color)
.unwrap_or(cosmic::theme::Text::Default);
cosmic::widget::text(&block.full_text).style(theme).into()
}

fn main() -> iced::Result {
let helper = CosmicAppletHelper::default();
let mut settings = helper.window_settings();
match &mut settings.initial_surface {
InitialSurface::XdgWindow(s) => {
s.iced_settings.min_size = Some((1, 1));
s.iced_settings.max_size = None;
s.autosize = true;
s.size_limits = Limits::NONE.min_height(1).min_width(1);
}
_ => unreachable!(),
};
App::run(settings)
}
111 changes: 111 additions & 0 deletions cosmic-applet-status-line/src/protocol/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/// TODO: if we get an error, terminate process with exit code 1. Let cosmic-panel restart us.
/// TODO: configuration for command? Use cosmic config system.
use cosmic::iced::{self, futures::FutureExt};
use std::{
fmt,
io::{BufRead, BufReader},
process::{self, Stdio},
thread,
};
use tokio::sync::mpsc;

mod serialization;
pub use serialization::Block;
use serialization::Header;

#[derive(Debug, Default)]
pub struct StatusLine {
pub blocks: Vec<Block>,
pub click_events: bool,
}

pub fn subscription() -> iced::Subscription<StatusLine> {
iced::subscription::run(
"status-cmd",
async {
let (sender, reciever) = mpsc::channel(20);
thread::spawn(move || {
let mut status_cmd = StatusCmd::spawn();
let mut deserializer =
serde_json::Deserializer::from_reader(&mut status_cmd.stdout);
deserialize_status_lines(&mut deserializer, |blocks| {
sender
.blocking_send(StatusLine {
blocks,
click_events: status_cmd.header.click_events,
})
.unwrap();
})
.unwrap();
status_cmd.wait();
});
tokio_stream::wrappers::ReceiverStream::new(reciever)
}
.flatten_stream(),
)
}

pub struct StatusCmd {
header: Header,
stdin: process::ChildStdin,
stdout: BufReader<process::ChildStdout>,
child: process::Child,
}

impl StatusCmd {
fn spawn() -> StatusCmd {
// XXX command
// XXX unwrap
let mut child = process::Command::new("i3status")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();

let mut stdout = BufReader::new(child.stdout.take().unwrap());
let mut header = String::new();
stdout.read_line(&mut header).unwrap();

StatusCmd {
header: serde_json::from_str(&header).unwrap(),
stdin: child.stdin.take().unwrap(),
stdout,
child,
}
}

fn wait(mut self) {
drop(self.stdin);
drop(self.stdout);
self.child.wait();
}
}

/// Deserialize a sequence of `Vec<Block>`s, executing a callback for each one.
/// Blocks thread until end of status line sequence.
fn deserialize_status_lines<'de, D: serde::Deserializer<'de>, F: FnMut(Vec<Block>)>(
deserializer: D,
cb: F,
) -> Result<(), D::Error> {
struct Visitor<F: FnMut(Vec<Block>)> {
cb: F,
}

impl<'de, F: FnMut(Vec<Block>)> serde::de::Visitor<'de> for Visitor<F> {
type Value = ();

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a sequence of status lines")
}

fn visit_seq<S: serde::de::SeqAccess<'de>>(mut self, mut seq: S) -> Result<(), S::Error> {
while let Some(blocks) = seq.next_element()? {
(self.cb)(blocks);
}
Ok(())
}
}

let visitor = Visitor { cb };
deserializer.deserialize_seq(visitor)
}
142 changes: 142 additions & 0 deletions cosmic-applet-status-line/src/protocol/serialization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// This implementation may be stricter in parsing than swaybar or i3bar. If this is an issue, the
// status command should probably be the one that's corrected to conform.

use cosmic::iced;
use serde::de::{Deserialize, Error};

fn sigcont() -> u8 {
18
}

fn sigstop() -> u8 {
19
}

#[derive(Clone, Debug, serde::Deserialize)]
pub struct Header {
pub version: u8,
#[serde(default)]
pub click_events: bool,
#[serde(default = "sigcont")]
pub cont_signal: u8,
#[serde(default = "sigstop")]
pub stop_signal: u8,
}

fn default_border() -> u32 {
1
}

fn default_seperator_block_width() -> u32 {
9
}

/// Deserialize string with RGB or RGBA color into `iced::Color`
fn deserialize_color<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<iced::Color>, D::Error> {
let s = String::deserialize(deserializer)?;

let unexpected_err = || {
D::Error::invalid_value(
serde::de::Unexpected::Str(&s),
&"a color string #RRGGBBAA or #RRGGBB",
)
};

// Must be 8 or 9 character string starting with #
if !s.starts_with("#") || (s.len() != 7 && s.len() != 9) {
return Err(unexpected_err());
}

let parse_hex = |component| u8::from_str_radix(component, 16).map_err(|_| unexpected_err());
let r = parse_hex(&s[1..3])?;
let g = parse_hex(&s[3..5])?;
let b = parse_hex(&s[5..7])?;
let a = if s.len() == 9 {
parse_hex(&s[7..])? as f32 / 1.0
} else {
1.0
};
Ok(Some(iced::Color::from_rgba8(r, g, b, a)))
}

#[derive(Clone, Debug, Default, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Align {
#[default]
Left,
Right,
Center,
}

#[derive(Clone, Debug, serde::Deserialize)]
#[serde(untagged)]
pub enum MinWidth {
Int(u32),
Str(String),
}

impl Default for MinWidth {
fn default() -> Self {
Self::Int(0)
}
}

#[derive(Clone, Debug, Default, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Markup {
#[default]
None,
Pango,
}

#[derive(Clone, Debug, serde::Deserialize)]
pub struct Block {
pub full_text: String,
pub short_text: Option<String>,
#[serde(default)]
#[serde(deserialize_with = "deserialize_color")]
pub color: Option<iced::Color>,
#[serde(default)]
#[serde(deserialize_with = "deserialize_color")]
pub background: Option<iced::Color>,
#[serde(default)]
#[serde(deserialize_with = "deserialize_color")]
pub border: Option<iced::Color>,
#[serde(default = "default_border")]
pub border_top: u32,
#[serde(default = "default_border")]
pub border_bottom: u32,
#[serde(default = "default_border")]
pub border_left: u32,
#[serde(default = "default_border")]
pub border_right: u32,
#[serde(default)]
pub min_width: MinWidth,
#[serde(default)]
pub align: Align,
pub name: Option<String>,
pub instance: Option<String>,
#[serde(default)]
pub urgent: bool,
#[serde(default)]
pub separator: bool,
#[serde(default = "default_seperator_block_width")]
pub separator_block_width: u32,
pub markup: Markup,
}

#[derive(Clone, Debug, serde::Serialize)]
struct ClickEvent {
name: Option<String>,
instance: Option<String>,
x: u32,
y: u32,
button: u32,
event: u32,
relative_x: u32,
relative_y: u32,
width: u32,
height: u32,
}
Loading

0 comments on commit e22ad3f

Please sign in to comment.