-
Notifications
You must be signed in to change notification settings - Fork 97
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
369 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
142
cosmic-applet-status-line/src/protocol/serialization.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
Oops, something went wrong.