Skip to content

Commit b21566a

Browse files
committed
WIP status-line-applet
1 parent 8b46cc2 commit b21566a

File tree

6 files changed

+358
-0
lines changed

6 files changed

+358
-0
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ members = [
99
"cosmic-applet-network",
1010
"cosmic-applet-notifications",
1111
"cosmic-applet-power",
12+
"cosmic-applet-status-line",
1213
"cosmic-applet-time",
1314
"cosmic-applet-workspaces",
1415
"cosmic-panel-button",

cosmic-applet-status-line/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "cosmic-applet-status-line"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
8+
serde = { version = "1.0", features = ["derive"] }
9+
serde_json = "1.0"
10+
tokio = { version = "1.27", features = ["io-util", "process", "sync"] }
11+
tokio-stream = "0.1"

cosmic-applet-status-line/src/main.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// TODO: work vertically
2+
3+
use cosmic::{
4+
applet::CosmicAppletHelper,
5+
iced::{self, Application},
6+
iced_native::window,
7+
iced_style::application,
8+
};
9+
10+
mod protocol;
11+
12+
#[derive(Debug)]
13+
enum Msg {
14+
Protocol(protocol::StatusLine),
15+
CloseRequest,
16+
}
17+
18+
#[derive(Default)]
19+
struct App {
20+
status_line: protocol::StatusLine,
21+
}
22+
23+
impl iced::Application for App {
24+
type Message = Msg;
25+
type Theme = cosmic::Theme;
26+
type Executor = cosmic::SingleThreadExecutor;
27+
type Flags = ();
28+
29+
fn new(_flags: ()) -> (Self, iced::Command<Msg>) {
30+
(App::default(), iced::Command::none())
31+
}
32+
33+
fn title(&self) -> String {
34+
String::from("Status Line")
35+
}
36+
37+
fn close_requested(&self, _id: window::Id) -> Msg {
38+
Msg::CloseRequest
39+
}
40+
41+
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
42+
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| application::Appearance {
43+
background_color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0),
44+
text_color: theme.cosmic().on_bg_color().into(),
45+
})
46+
}
47+
48+
fn subscription(&self) -> iced::Subscription<Msg> {
49+
protocol::subscription().map(Msg::Protocol)
50+
}
51+
52+
fn update(&mut self, message: Msg) -> iced::Command<Msg> {
53+
match message {
54+
Msg::Protocol(status_line) => {
55+
println!("{:?}", status_line);
56+
self.status_line = status_line;
57+
}
58+
Msg::CloseRequest => {}
59+
}
60+
iced::Command::none()
61+
}
62+
63+
fn view(&self, _id: window::Id) -> cosmic::Element<Msg> {
64+
iced::widget::row(self.status_line.blocks.iter().map(block_view).collect()).into()
65+
}
66+
}
67+
68+
// TODO seperator
69+
fn block_view(block: &protocol::Block) -> cosmic::Element<Msg> {
70+
let theme = block
71+
.color
72+
.map(cosmic::theme::Text::Color)
73+
.unwrap_or(cosmic::theme::Text::Default);
74+
cosmic::widget::text(&block.full_text).style(theme).into()
75+
}
76+
77+
fn main() -> iced::Result {
78+
let helper = CosmicAppletHelper::default();
79+
// TODO size window to fit context?
80+
let settings = helper.window_settings();
81+
App::run(settings)
82+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/// TODO: if we get an error, terminate process with exit code 1. Let cosmic-panel restart us.
2+
/// TODO: configuration for command? Use cosmic config system.
3+
use cosmic::iced::{self, futures::FutureExt};
4+
use std::{
5+
fmt,
6+
io::{BufRead, BufReader},
7+
process::{self, Stdio},
8+
thread,
9+
};
10+
use tokio::sync::mpsc;
11+
12+
mod serialization;
13+
pub use serialization::Block;
14+
use serialization::Header;
15+
16+
#[derive(Debug, Default)]
17+
pub struct StatusLine {
18+
pub blocks: Vec<Block>,
19+
pub click_events: bool,
20+
}
21+
22+
pub fn subscription() -> iced::Subscription<StatusLine> {
23+
iced::subscription::run(
24+
"status-cmd",
25+
async {
26+
let (sender, reciever) = mpsc::channel(20);
27+
thread::spawn(move || {
28+
let mut status_cmd = StatusCmd::spawn();
29+
let mut deserializer =
30+
serde_json::Deserializer::from_reader(&mut status_cmd.stdout);
31+
deserialize_status_lines(&mut deserializer, |blocks| {
32+
sender
33+
.blocking_send(StatusLine {
34+
blocks,
35+
click_events: status_cmd.header.click_events,
36+
})
37+
.unwrap();
38+
})
39+
.unwrap();
40+
status_cmd.wait();
41+
});
42+
tokio_stream::wrappers::ReceiverStream::new(reciever)
43+
}
44+
.flatten_stream(),
45+
)
46+
}
47+
48+
pub struct StatusCmd {
49+
header: Header,
50+
stdin: process::ChildStdin,
51+
stdout: BufReader<process::ChildStdout>,
52+
child: process::Child,
53+
}
54+
55+
impl StatusCmd {
56+
fn spawn() -> StatusCmd {
57+
// XXX command
58+
// XXX unwrap
59+
let mut child = process::Command::new("i3status")
60+
.stdin(Stdio::piped())
61+
.stdout(Stdio::piped())
62+
.spawn()
63+
.unwrap();
64+
65+
let mut stdout = BufReader::new(child.stdout.take().unwrap());
66+
let mut header = String::new();
67+
stdout.read_line(&mut header).unwrap();
68+
69+
StatusCmd {
70+
header: serde_json::from_str(&header).unwrap(),
71+
stdin: child.stdin.take().unwrap(),
72+
stdout,
73+
child,
74+
}
75+
}
76+
77+
fn wait(mut self) {
78+
drop(self.stdin);
79+
drop(self.stdout);
80+
self.child.wait();
81+
}
82+
}
83+
84+
/// Deserialize a sequence of `Vec<Block>`s, executing a callback for each one.
85+
/// Blocks thread until end of status line sequence.
86+
fn deserialize_status_lines<'de, D: serde::Deserializer<'de>, F: FnMut(Vec<Block>)>(
87+
deserializer: D,
88+
cb: F,
89+
) -> Result<(), D::Error> {
90+
struct Visitor<F: FnMut(Vec<Block>)> {
91+
cb: F,
92+
}
93+
94+
impl<'de, F: FnMut(Vec<Block>)> serde::de::Visitor<'de> for Visitor<F> {
95+
type Value = ();
96+
97+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
98+
formatter.write_str("a sequence of status lines")
99+
}
100+
101+
fn visit_seq<S: serde::de::SeqAccess<'de>>(mut self, mut seq: S) -> Result<(), S::Error> {
102+
while let Some(blocks) = seq.next_element()? {
103+
(self.cb)(blocks);
104+
}
105+
Ok(())
106+
}
107+
}
108+
109+
let visitor = Visitor { cb };
110+
deserializer.deserialize_seq(visitor)
111+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// This implementation may be stricter in parsing than swaybar or i3bar. If this is an issue, the
2+
// status command should probably be the one that's corrected to conform.
3+
4+
use cosmic::iced;
5+
use serde::de::{Deserialize, Error};
6+
7+
fn sigcont() -> u8 {
8+
18
9+
}
10+
11+
fn sigstop() -> u8 {
12+
19
13+
}
14+
15+
#[derive(Clone, Debug, serde::Deserialize)]
16+
pub struct Header {
17+
pub version: u8,
18+
#[serde(default)]
19+
pub click_events: bool,
20+
#[serde(default = "sigcont")]
21+
pub cont_signal: u8,
22+
#[serde(default = "sigstop")]
23+
pub stop_signal: u8,
24+
}
25+
26+
fn default_border() -> u32 {
27+
1
28+
}
29+
30+
fn default_seperator_block_width() -> u32 {
31+
9
32+
}
33+
34+
/// Deserialize string with RGB or RGBA color into `iced::Color`
35+
fn deserialize_color<'de, D: serde::Deserializer<'de>>(
36+
deserializer: D,
37+
) -> Result<Option<iced::Color>, D::Error> {
38+
let s = String::deserialize(deserializer)?;
39+
40+
let unexpected_err = || {
41+
D::Error::invalid_value(
42+
serde::de::Unexpected::Str(&s),
43+
&"a color string #RRGGBBAA or #RRGGBB",
44+
)
45+
};
46+
47+
// Must be 8 or 9 character string starting with #
48+
if !s.starts_with("#") || (s.len() != 7 && s.len() != 9) {
49+
return Err(unexpected_err());
50+
}
51+
52+
let parse_hex = |component| u8::from_str_radix(component, 16).map_err(|_| unexpected_err());
53+
let r = parse_hex(&s[1..3])?;
54+
let g = parse_hex(&s[3..5])?;
55+
let b = parse_hex(&s[5..7])?;
56+
let a = if s.len() == 9 {
57+
parse_hex(&s[7..])? as f32 / 1.0
58+
} else {
59+
1.0
60+
};
61+
Ok(Some(iced::Color::from_rgba8(r, g, b, a)))
62+
}
63+
64+
#[derive(Clone, Debug, Default, serde::Deserialize)]
65+
#[serde(rename_all = "snake_case")]
66+
pub enum Align {
67+
#[default]
68+
Left,
69+
Right,
70+
Center,
71+
}
72+
73+
#[derive(Clone, Debug, serde::Deserialize)]
74+
#[serde(untagged)]
75+
pub enum MinWidth {
76+
Int(u32),
77+
Str(String),
78+
}
79+
80+
impl Default for MinWidth {
81+
fn default() -> Self {
82+
Self::Int(0)
83+
}
84+
}
85+
86+
#[derive(Clone, Debug, Default, serde::Deserialize)]
87+
#[serde(rename_all = "snake_case")]
88+
pub enum Markup {
89+
#[default]
90+
None,
91+
Pango,
92+
}
93+
94+
#[derive(Clone, Debug, serde::Deserialize)]
95+
pub struct Block {
96+
pub full_text: String,
97+
pub short_text: Option<String>,
98+
#[serde(default)]
99+
#[serde(deserialize_with = "deserialize_color")]
100+
pub color: Option<iced::Color>,
101+
#[serde(default)]
102+
#[serde(deserialize_with = "deserialize_color")]
103+
pub background: Option<iced::Color>,
104+
#[serde(default)]
105+
#[serde(deserialize_with = "deserialize_color")]
106+
pub border: Option<iced::Color>,
107+
#[serde(default = "default_border")]
108+
pub border_top: u32,
109+
#[serde(default = "default_border")]
110+
pub border_bottom: u32,
111+
#[serde(default = "default_border")]
112+
pub border_left: u32,
113+
#[serde(default = "default_border")]
114+
pub border_right: u32,
115+
#[serde(default)]
116+
pub min_width: MinWidth,
117+
#[serde(default)]
118+
pub align: Align,
119+
pub name: Option<String>,
120+
pub instance: Option<String>,
121+
#[serde(default)]
122+
pub urgent: bool,
123+
#[serde(default)]
124+
pub separator: bool,
125+
#[serde(default = "default_seperator_block_width")]
126+
pub separator_block_width: u32,
127+
pub markup: Markup,
128+
}
129+
130+
#[derive(Clone, Debug, serde::Serialize)]
131+
struct ClickEvent {
132+
name: Option<String>,
133+
instance: Option<String>,
134+
x: u32,
135+
y: u32,
136+
button: u32,
137+
event: u32,
138+
relative_x: u32,
139+
relative_y: u32,
140+
width: u32,
141+
height: u32,
142+
}

0 commit comments

Comments
 (0)