├── .github
├── FUNDING.yml
├── README.md
├── showcase
│ ├── phase_1.png
│ ├── phase_2.png
│ ├── phase_3.png
│ └── phase_4.png
└── workflows
│ └── rust.yml.bak
├── .gitignore
├── Cargo.toml
├── LICENSE
├── docs
└── widgets.md
├── examples
├── helium.json
└── style.css
├── src
├── builder
│ ├── layer_builder.rs
│ ├── mod.rs
│ └── widgets_builder.rs
├── config
│ ├── mod.rs
│ └── user_config.rs
├── main.rs
├── modules
│ ├── battery.rs
│ ├── brightness.rs
│ ├── hyprland.rs
│ ├── mod.rs
│ └── tray.rs
├── network
│ ├── hyprland_socket.rs
│ └── mod.rs
├── utils
│ ├── command.rs
│ ├── constants.rs
│ ├── file_handler.rs
│ ├── listener.rs
│ ├── mod.rs
│ └── regex_matcher.rs
└── widgets
│ ├── LabelWidget.rs
│ └── mod.rs
└── stray
├── Cargo.toml
├── README.md
├── examples
└── simple.rs
└── src
├── dbus
├── dbusmenu_proxy.rs
├── mod.rs
├── notifier_item_proxy.rs
├── notifier_watcher_proxy.rs
└── notifier_watcher_service.rs
├── error.rs
├── lib.rs
├── message
├── menu.rs
├── mod.rs
└── tray.rs
├── notifier_host
└── mod.rs
└── notifier_watcher
├── mod.rs
└── notifier_address.rs
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: pwnwriter
2 |
--------------------------------------------------------------------------------
/.github/README.md:
--------------------------------------------------------------------------------
1 |
2 |
He 1s2 💭
— A noble, light and aesthetic bar for Wayland // wlroots
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ## WIP
17 |
18 | 
19 |
20 |
21 |
22 |
23 | 
24 | Copyright © 2023 pwnwriter xyz ☘️
25 |
26 |
--------------------------------------------------------------------------------
/.github/showcase/phase_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metis-os/heliumbar/eaf9bc3f5d3f0d33dc983a614a7a1cead8b05ccd/.github/showcase/phase_1.png
--------------------------------------------------------------------------------
/.github/showcase/phase_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metis-os/heliumbar/eaf9bc3f5d3f0d33dc983a614a7a1cead8b05ccd/.github/showcase/phase_2.png
--------------------------------------------------------------------------------
/.github/showcase/phase_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metis-os/heliumbar/eaf9bc3f5d3f0d33dc983a614a7a1cead8b05ccd/.github/showcase/phase_3.png
--------------------------------------------------------------------------------
/.github/showcase/phase_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/metis-os/heliumbar/eaf9bc3f5d3f0d33dc983a614a7a1cead8b05ccd/.github/showcase/phase_4.png
--------------------------------------------------------------------------------
/.github/workflows/rust.yml.bak:
--------------------------------------------------------------------------------
1 | name: Rust
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | env:
10 | CARGO_TERM_COLOR: always
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | toolchain:
19 | - stable
20 | - beta
21 | - nightly
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
26 | - name: Build
27 | run: cargo build --verbose
28 | - name: Run tests
29 | run: cargo test --verbose
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
3 | .vscode
4 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "heliumbar"
3 | version = "0.1.0"
4 | edition = "2021"
5 | authors = [ "PwnWriter < hey@pwnwriter.xyz >" ]
6 | description = "💭 A noble, light and aesthetic bar for Wayland // wlroots"
7 | readme = "README.md"
8 | repository = "https://github.com/pwnwriter/heliumbar"
9 | homepage = "https://github.com/pwnwriter/heliumbar.git"
10 | license = "MIT"
11 | keywords = ["unixporn", "Hyprland", "Wayland-bar"]
12 | categories = ["accessibility", "command-line" ]
13 |
14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
15 |
16 | [dependencies]
17 | gtk = { version = "0.17.0", package = "gtk" }
18 | tokio = { version = "1.26.0", features = ["full"] }
19 | gtk-layer-shell = "0.6.1"
20 | json = "0.12.4"
21 | glib = "0.18.1"
22 | regex = "1.9.3"
23 | tokio-uds = "0.2"
24 | inotify = "0.10.2"
25 | stray = {path = "stray"}
26 | once_cell = "1.18.0"
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 PwnWriter < pwnwriter.xyz >
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/widgets.md:
--------------------------------------------------------------------------------
1 | This is helper documentation
2 |
--------------------------------------------------------------------------------
/examples/helium.json:
--------------------------------------------------------------------------------
1 | {
2 | "alpha":0,
3 | "align":"top",
4 | "background":"#000000",
5 | "widgets":{
6 | "battery":{
7 | "format":" {percentage} %",
8 | "type":"battery",
9 | "is_json":false,
10 | "align":"left",
11 | "refresh-rate":1
12 | },
13 | "brightness":{
14 | "format":" {} %",
15 | "align":"left",
16 | "type":"brightness",
17 | "refresh-rate":1
18 | },
19 |
20 | "tray":{
21 | "type":"tray",
22 | "align":"right"
23 | },
24 | "hyprland":{
25 | "format":" {workspace}:{activewindow}",
26 | "type":"hyprland",
27 | "align":"center"
28 |
29 | }
30 |
31 | }
32 |
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/examples/style.css:
--------------------------------------------------------------------------------
1 | .root{
2 | font-size:15px;
3 | /* margin-top:10px; */
4 | padding:5px 5px;
5 | }
6 |
7 | .battery,.brightness,.tray,.hyprland{
8 | background-color:#5c5f77;
9 | padding:8px 20px;
10 | border-radius:7px;
11 | color:white;
12 | }
13 |
14 | .battery{
15 | border-radius:7px 0 0 7px;
16 | padding-right:5px;
17 | }
18 | .brightness{
19 | border-radius:0px 7px 7px 0;
20 | padding-left:5px;
21 | }
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/builder/layer_builder.rs:
--------------------------------------------------------------------------------
1 | // HELIUMBAR ui / ux
2 |
3 | use crate::builder::widgets_builder::build_widgets;
4 | use crate::config;
5 | use gtk::gdk::*;
6 | use gtk::prelude::*;
7 | use gtk::Orientation;
8 | use gtk::{Application, ApplicationWindow};
9 | use gtk_layer_shell;
10 | use gtk_layer_shell::Edge;
11 |
12 | pub fn build_ui(app: &Application) {
13 | let window = ApplicationWindow::new(app);
14 | gtk_layer_shell::init_for_window(&window);
15 |
16 | gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Top);
17 |
18 | gtk_layer_shell::auto_exclusive_zone_enable(&window);
19 | window.set_app_paintable(true);
20 |
21 | let config = read_window_config();
22 |
23 | let mut orientation = Orientation::Horizontal;
24 |
25 | if let Some(data) = config {
26 | match &data.position {
27 | Edge::Left => orientation = Orientation::Vertical,
28 | Edge::Right => orientation = Orientation::Vertical,
29 | _ => (),
30 | }
31 | align_layer(&window, &data.position);
32 | window.connect_draw(move |win, context| draw(win, context, &data));
33 | } else {
34 | align_layer(&window, &Edge::Top);
35 | }
36 |
37 | let display = Display::default().expect("Error happening");
38 | let monitor = display.monitor(0).expect("Error getting monitor");
39 | gtk_layer_shell::set_monitor(&window, &monitor);
40 |
41 | window.connect_destroy(|_| gtk::main_quit());
42 | build_widgets(&window, orientation);
43 | }
44 |
45 | pub fn align_layer(window: &ApplicationWindow, align: &Edge) {
46 | gtk_layer_shell::set_anchor(window, Edge::Left, true);
47 | gtk_layer_shell::set_anchor(window, Edge::Right, true);
48 | gtk_layer_shell::set_anchor(window, Edge::Top, true);
49 | gtk_layer_shell::set_anchor(window, Edge::Bottom, true);
50 | match align {
51 | Edge::Top => {
52 | gtk_layer_shell::set_anchor(window, Edge::Bottom, false);
53 | }
54 | Edge::Bottom => {
55 | gtk_layer_shell::set_anchor(window, Edge::Top, false);
56 | }
57 | Edge::Left => {
58 | gtk_layer_shell::set_anchor(window, Edge::Right, false);
59 | }
60 | Edge::Right => {
61 | gtk_layer_shell::set_anchor(window, Edge::Left, false);
62 | }
63 | _ => {
64 | gtk_layer_shell::set_anchor(window, Edge::Bottom, false);
65 | }
66 | } //match
67 | }
68 | pub struct LayerConfig {
69 | alpha: f64,
70 | color: (f64, f64, f64),
71 | position: Edge,
72 | }
73 |
74 | pub fn draw(_: &ApplicationWindow, context: &cairo::Context, config: &LayerConfig) -> Inhibit {
75 | context.set_source_rgba(config.color.0, config.color.1, config.color.2, config.alpha);
76 | context.set_operator(cairo::Operator::Screen);
77 | context.paint().unwrap_or_default();
78 | Inhibit(false)
79 | }
80 |
81 | pub fn extract_color(color: &str) -> Option<(f64, f64, f64)> {
82 | if color.len() != 7 || !color.starts_with('#') {
83 | return None;
84 | }
85 | let r_color = u8::from_str_radix(&color[1..3], 16).ok()? as f64 / 255_f64;
86 | let g_color = u8::from_str_radix(&color[3..5], 16).ok()? as f64 / 255_f64;
87 | let b_color = u8::from_str_radix(&color[5..7], 16).ok()? as f64 / 255_f64;
88 | Some((r_color, g_color, b_color))
89 | }
90 |
91 | pub fn read_window_config() -> Option {
92 | let config = config::user_config::read_config();
93 | if config.is_err() {
94 | return None;
95 | }
96 | let config = config.unwrap();
97 | let alpha: f64 = config["alpha"].as_f64().unwrap_or_default();
98 | let color: String = config["background"]
99 | .as_str()
100 | .unwrap_or("#000000")
101 | .to_string();
102 | let position: String = config["align"].as_str().unwrap_or("top").to_string();
103 | let pos: Edge;
104 | if position == "top" {
105 | pos = Edge::Top;
106 | } else if position == "bottom" {
107 | pos = Edge::Bottom;
108 | } else if position == "left" {
109 | pos = Edge::Left;
110 | } else if position == "right" {
111 | pos = Edge::Right;
112 | } else {
113 | pos = Edge::Top;
114 | }
115 |
116 | let mut sep_col = extract_color(&color);
117 | if sep_col.is_none() {
118 | sep_col = Some((0.0, 0.0, 0.0));
119 | }
120 | Some(LayerConfig {
121 | alpha,
122 | color: sep_col.unwrap(),
123 | position: pos,
124 | })
125 | }
126 |
--------------------------------------------------------------------------------
/src/builder/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod layer_builder;
2 | pub mod widgets_builder;
3 |
--------------------------------------------------------------------------------
/src/builder/widgets_builder.rs:
--------------------------------------------------------------------------------
1 | use gtk::prelude::*;
2 | use gtk::ApplicationWindow;
3 | use gtk::Orientation;
4 |
5 | use crate::config;
6 | use crate::modules;
7 | use crate::utils;
8 | use crate::widgets::LabelWidget;
9 | // s
10 | fn build_config_else_default(
11 | centered: >k::Box,
12 | configs: &Result,
13 | ) -> bool {
14 | if let Err(error) = configs {
15 | let label = gtk::Label::builder().label(error).margin_start(40).build();
16 | centered.add(&label);
17 | return false;
18 | }
19 | let configs = configs.as_ref().unwrap();
20 |
21 | if !configs.has_key("widgets") {
22 | let label = gtk::Label::builder()
23 | .label(
24 | "No widgets found in your config,please add some widgets to show in the status bar",
25 | )
26 | .margin_start(40)
27 | .build();
28 | centered.add(&label);
29 | return false;
30 | };
31 | true
32 | }
33 |
34 | fn load_css() {
35 | let user = std::env::var("HOME");
36 | if let Err(err) = user {
37 | println!("{}", err);
38 | return;
39 | }
40 | let mut path = user.unwrap();
41 | path.push_str(utils::constants::CONFIG_STYLE);
42 |
43 | let provider = gtk::CssProvider::new();
44 | if let Err(err) = provider.load_from_path(&path) {
45 | println!("{}", err);
46 | return;
47 | }
48 |
49 | let screen = gtk::gdk::Screen::default();
50 | if screen.is_none() {
51 | return
52 | }
53 |
54 | gtk::StyleContext::add_provider_for_screen(
55 | &screen.unwrap(),
56 | &provider,
57 | gtk::STYLE_PROVIDER_PRIORITY_USER,
58 | );
59 | }
60 |
61 | pub fn build_widgets(window: &ApplicationWindow, orientation: Orientation) {
62 | // let orientation = Orientation::Horizontal;
63 | let root = gtk::Box::new(orientation, 0);
64 | let left = gtk::Box::new(orientation, 0);
65 | let centered = gtk::Box::new(orientation, 0);
66 | let right = gtk::Box::new(orientation, 0);
67 |
68 | root.style_context().add_class("root");
69 | left.style_context().add_class("left");
70 | centered.style_context().add_class("center");
71 | right.style_context().add_class("right");
72 |
73 | root.set_center_widget(Some(¢ered));
74 | root.pack_end(&right, false, true, 0);
75 | root.add(&left);
76 |
77 | let configs = config::user_config::read_config();
78 | if build_config_else_default(¢ered, &configs) {
79 | render_widgets(left, right, centered, configs.unwrap());
80 | }
81 |
82 | window.add(&root);
83 | load_css();
84 | window.show_all();
85 | }
86 |
87 | pub enum Align {
88 | LEFT,
89 | CENTER,
90 | RIGHT,
91 | }
92 | pub struct WidgetConfig {
93 | pub format: String,
94 | // pub type_of_widget: String,
95 | pub align: Align,
96 | pub command: String,
97 | pub refresh_rate: i64,
98 | pub tooltip: String,
99 | pub name_of_widget: String,
100 | pub is_json: bool,
101 | }
102 |
103 | pub fn check_alignment(align: &String) -> Align {
104 | if align == "left" {
105 | return Align::LEFT;
106 | } else if align == "right" {
107 | return Align::RIGHT;
108 | } else if align == "center" {
109 | return Align::CENTER;
110 | } else {
111 | return Align::LEFT;
112 | }
113 | }
114 |
115 | pub fn render_widgets(
116 | left: gtk::Box,
117 | right: gtk::Box,
118 | centered: gtk::Box,
119 | configs: json::JsonValue,
120 | ) {
121 | let mut modules_name: Vec = Vec::new();
122 | modules_name.push("hyprland".to_string());
123 | modules_name.push("battery".to_string());
124 | modules_name.push("cpu".to_string());
125 | modules_name.push("ram".to_string());
126 | modules_name.push("time".to_string());
127 | modules_name.push("brightness".to_string());
128 | modules_name.push("volume".to_string());
129 | modules_name.push("tray".to_string());
130 |
131 | let widgets = configs["widgets"].entries();
132 | for (key, value_json) in widgets {
133 | let format = value_json["format"].as_str().unwrap_or("").to_string();
134 | let type_of_widget = value_json["type"].as_str().unwrap_or("").to_string();
135 | let align = check_alignment(&value_json["align"].as_str().unwrap_or("").to_string());
136 | let command = value_json["command"].as_str().unwrap_or("").to_string();
137 | let refresh_rate = value_json["refresh-rate"].as_i64().unwrap_or(0);
138 | let tooltip = value_json["tooltip"].as_str().unwrap_or("").to_string();
139 | let is_json = value_json["is_json"].as_bool().unwrap_or(false);
140 | let name_of_widget = key.to_string();
141 |
142 | let data = WidgetConfig {
143 | format,
144 | // type_of_widget,
145 | align,
146 | command,
147 | refresh_rate,
148 | is_json,
149 | tooltip,
150 | name_of_widget,
151 | };
152 | if modules_name.contains(&type_of_widget) {
153 | handle_builtin_widgets(&left, ¢ered, &right, data, &type_of_widget);
154 | } else if type_of_widget == "label" {
155 | LabelWidget::build_label(&left, ¢ered, &right, data);
156 | } else {
157 | LabelWidget::build_label(&left, ¢ered, &right, data);
158 | }
159 | } //for
160 | }
161 |
162 | fn handle_builtin_widgets(
163 | left: >k::Box,
164 | centered: >k::Box,
165 | right: >k::Box,
166 | config: WidgetConfig,
167 | type_of_widget: &String,
168 | ) {
169 | // println!("{}", type_of_widget);
170 | if type_of_widget == "hyprland" {
171 | modules::hyprland::build_label(&left, ¢ered, &right, config);
172 | } else if type_of_widget == "battery" {
173 | modules::battery::build_label(left, ¢ered, &right, config);
174 | } else if type_of_widget == "brightness" {
175 | modules::brightness::build_label(left, ¢ered, &right, config);
176 | } else if type_of_widget == "tray" {
177 | modules::tray::build_label(left, centered, right, config);
178 | }
179 | }
180 |
181 | pub fn build_and_align(
182 | text: &String,
183 | left: >k::Box,
184 | center: >k::Box,
185 | right: >k::Box,
186 | config: &WidgetConfig,
187 | ) -> gtk::Label {
188 | let label = gtk::Label::builder().label(text).build();
189 | label.style_context().add_class(&config.name_of_widget);
190 | match config.align {
191 | Align::CENTER => center.add(&label),
192 | Align::LEFT => left.add(&label),
193 | Align::RIGHT => right.add(&label),
194 | }
195 |
196 | return label;
197 | }
198 |
--------------------------------------------------------------------------------
/src/config/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod user_config;
2 |
3 | pub use user_config::*;
4 |
--------------------------------------------------------------------------------
/src/config/user_config.rs:
--------------------------------------------------------------------------------
1 | // use json;
2 | //hello
3 |
4 | use crate::utils;
5 |
6 | pub fn read_file(path: &String) -> Option {
7 | let data = match std::fs::read_to_string(&path) {
8 | Ok(content) => content,
9 | Err(err) => {
10 | if err.kind() == std::io::ErrorKind::NotFound {
11 | println!("{} file is not found in your system", path);
12 | return None;
13 | } else if err.kind() == std::io::ErrorKind::PermissionDenied {
14 | println!("Permission denied to access the {} file", path);
15 | return None;
16 | } else {
17 | println!("Something went wrong reading the {} file", path);
18 | return None;
19 | }
20 | } //error
21 | }; //reading the string
22 | Some(data)
23 | }
24 |
25 | pub fn get_config_path() -> Option {
26 | let user = std::env::var("HOME");
27 | if let Err(_) = user {
28 | println!("Unble to find the username of the system");
29 | return None;
30 | }
31 | let mut path = String::from(&user.unwrap());
32 | path.push_str(utils::constants::CONFIG_PATH);
33 | Some(path)
34 | }
35 |
36 | pub fn read_config() -> Result {
37 | let path = get_config_path();
38 |
39 | if let None = path {
40 | return Err("Unable to find the config path".to_string());
41 | }
42 | let data = read_file(&path.unwrap());
43 | if let None = data {
44 | return Err("Unable to read the config file".to_string());
45 | }
46 | parse_config(data)
47 | }
48 |
49 | pub fn parse_config(data: Option) -> Result {
50 | let config = json::parse(&data.unwrap());
51 |
52 | if let Err(error) = config {
53 | println!("Error occur parsing the json. Switching to default conf");
54 | return Err(error.to_string());
55 | }
56 | return Ok(config.unwrap());
57 | }
58 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | mod builder;
2 | mod config;
3 | mod modules;
4 | mod network;
5 | mod utils;
6 | mod widgets;
7 |
8 | use gtk::prelude::{ApplicationExt, ApplicationExtManual};
9 | use utils::command::run;
10 |
11 | use crate::builder::layer_builder::build_ui;
12 | use stray::{
13 | message::{
14 | menu::{MenuType, TrayMenu},
15 | tray::{IconPixmap, StatusNotifierItem},
16 | NotifierItemCommand,
17 | },
18 | NotifierItemMessage, StatusNotifierWatcher,
19 | };
20 | fn main() -> gtk::glib::ExitCode {
21 | const APP_ID: &str = "com.heliumbar";
22 |
23 | let app = gtk::Application::builder().application_id(APP_ID).build();
24 | // println!("{:?}", std::thread::current().id().to_owned());
25 | app.connect_activate(build_ui);
26 | // tray();
27 | app.run()
28 | } //main
29 |
30 | fn tray() {
31 | let (_sender, receiver) = tokio::sync::mpsc::channel(50);
32 |
33 | std::thread::spawn(move || {
34 | let tokio_runtime = tokio::runtime::Runtime::new().unwrap();
35 |
36 | tokio_runtime.block_on(async {
37 | let watcher = StatusNotifierWatcher::new(receiver).await.unwrap();
38 |
39 | let mut notifier_host = watcher.create_notifier_host("Hybrid").await;
40 | if let Err(err) = notifier_host {
41 | println!("Error::{}", err);
42 | return;
43 | }
44 | let mut notifier_host = notifier_host.unwrap();
45 | while let Ok(msg) = notifier_host.recv().await {
46 | match msg {
47 | NotifierItemMessage::Update {
48 | address,
49 | item,
50 | menu,
51 | } => {
52 | println!("update:{}{:?}", address, (*item).icon_name);
53 | } //on update,
54 | NotifierItemMessage::Remove { address } => {
55 | println!("Removed:{}", address);
56 | } //remove
57 | } //match msg
58 | } //while
59 | }) //runtime async fun
60 | });
61 | }
62 |
63 | ////////////////////////
64 | fn watcher_file() {
65 | let path = "/sys/class/backlight/amdgpu_bl1/brightness";
66 | let mut inotify = inotify::Inotify::init().unwrap();
67 | let watch = inotify.watches().add(path, inotify::WatchMask::MODIFY);
68 | if let Err(err) = watch {
69 | println!("{}", err);
70 | return;
71 | }
72 | // let watch = watch.unwrap();
73 | let mut buffer = [0u8; 4096];
74 | loop {
75 | let events = inotify.read_events_blocking(&mut buffer);
76 | if let Err(err) = events {
77 | println!("{}", err);
78 | }
79 | //for loop
80 | }
81 | } //watcher
--------------------------------------------------------------------------------
/src/modules/battery.rs:
--------------------------------------------------------------------------------
1 | use json::{Error, JsonValue};
2 | use std::collections::HashMap;
3 | use std::fs::{self, File};
4 | use std::io::{Read, Seek};
5 | use std::time::Duration;
6 |
7 | use crate::builder::widgets_builder::{self, Align, WidgetConfig};
8 | use crate::utils::constants::BATTERY_PATH;
9 | use crate::utils::file_handler::{get_particular_dir_path, read_file_for_monitor};
10 | use crate::utils::{command, listener, regex_matcher};
11 | use glib::{MainContext, Receiver, Sender};
12 | use gtk::prelude::*;
13 | // use super::workspace::listen;
14 |
15 | pub fn build_label(left: >k::Box, center: >k::Box, right: >k::Box, config: WidgetConfig) {
16 | let original: String = config.format.clone();
17 |
18 | let label = widgets_builder::build_and_align(&original, &left, ¢er, &right, &config);
19 | // println!("{}", text);
20 | update_widget(label, original, config.refresh_rate);
21 | }
22 |
23 | fn update_widget(label: gtk::Label, original: String, refresh_rate: i64) {
24 | // let path = "/sys/class/power_supply/BAT0/capacity";
25 | let base_path = get_particular_dir_path(BATTERY_PATH.to_string(), "capacity".to_string());
26 | if let None = base_path {
27 | return;
28 | }
29 | let base_path = base_path.unwrap();
30 | let path = format!("{}/capacity", base_path);
31 | let mut buffer = [0u8; 30];
32 | let mut file = match File::open(path) {
33 | Ok(file) => file,
34 | Err(err) => {
35 | println!("{}", err);
36 | return;
37 | }
38 | };
39 | let interval = if refresh_rate > 0 {
40 | std::time::Duration::from_secs(refresh_rate as u64)
41 | } else {
42 | std::time::Duration::from_secs(1)
43 | };
44 | let (sender, receiver) = MainContext::channel::<(String, String)>(glib::Priority::DEFAULT);
45 | //lister
46 | listener::listen(receiver, original, label); //listen and update according to it
47 | std::thread::spawn(move || {
48 | let mut previous_state: String = read_file_for_monitor(&mut file, &mut buffer);
49 | sender
50 | .send(("percentage".to_string(), previous_state.to_owned()))
51 | .unwrap_or_default();
52 | let mut current_state: String;
53 | loop {
54 | std::thread::sleep(interval);
55 | current_state = read_file_for_monitor(&mut file, &mut buffer);
56 | if previous_state == current_state {
57 | continue;
58 | }
59 | previous_state = current_state;
60 | sender
61 | .send(("percentage".to_string(), previous_state.to_owned()))
62 | .unwrap_or_default();
63 | // sender.send((previous_state.parse::().unwrap_or(1.0) / 1000000.0).to_string());
64 | } //loop
65 | }); //thread
66 | } //update widget
67 |
--------------------------------------------------------------------------------
/src/modules/brightness.rs:
--------------------------------------------------------------------------------
1 | use json::{Error, JsonValue};
2 | use std::collections::HashMap;
3 | use std::fs::{self, File};
4 | use std::io::{Read, Seek};
5 | use std::time::Duration;
6 |
7 | use crate::builder::widgets_builder::{self, Align, WidgetConfig};
8 | use crate::utils::constants::{BATTERY_PATH, BRIGHTNESS_PATH};
9 | use crate::utils::file_handler::{get_particular_dir_path, read_file_for_monitor};
10 | use crate::utils::{command, listener, regex_matcher};
11 | use glib::{MainContext, Receiver, Sender};
12 | use gtk::prelude::*;
13 | // use super::workspace::listen;
14 |
15 | pub fn build_label(left: >k::Box, center: >k::Box, right: >k::Box, config: WidgetConfig) {
16 | let original: String = config.format.clone();
17 |
18 | let label = widgets_builder::build_and_align(&original, &left, ¢er, &right, &config);
19 | // println!("{}", text);
20 | update_widget(label, original, config.refresh_rate);
21 | }
22 |
23 | fn update_widget(label: gtk::Label, original: String, refresh_rate: i64) {
24 | // let path = "/sys/class/power_supply/BAT0/capacity";
25 | let base_path = get_particular_dir_path(BRIGHTNESS_PATH.to_string(), "brightness".to_string());
26 | if let None = base_path {
27 | return;
28 | }
29 | let base_path = base_path.unwrap();
30 | let path = format!("{}/brightness", base_path);
31 | let mut buffer = [0u8; 60];
32 | let max = match File::open(format!("{}/max_brightness", base_path)) {
33 | Ok(mut file) => read_file_for_monitor(&mut file, &mut buffer)
34 | .parse::()
35 | .unwrap_or(255.0),
36 | Err(err) => {
37 | println!("{}", err);
38 | 255.0
39 | }
40 | }; //max
41 | let mut file = match File::open(&path) {
42 | Ok(file) => file,
43 | Err(err) => {
44 | println!("{}", err);
45 | return;
46 | }
47 | };
48 | let interval = if refresh_rate > 0 {
49 | std::time::Duration::from_secs(refresh_rate as u64)
50 | } else {
51 | std::time::Duration::from_secs(1)
52 | };
53 | let (sender, receiver) = MainContext::channel::<(String, String)>(glib::Priority::DEFAULT);
54 | //lister
55 | listener::listen(receiver, original, label); //listen and update according to it
56 | //inotify
57 | let mut inotify = inotify::Inotify::init().unwrap();
58 | let watch = inotify.watches().add(&path, inotify::WatchMask::MODIFY);
59 | if let Err(err) = watch {
60 | println!("{}", err);
61 | return;
62 | } //
63 |
64 | //inotify
65 | std::thread::spawn(move || {
66 | let mut previous_state: String = read_file_for_monitor(&mut file, &mut buffer);
67 | sender
68 | .send((
69 | "".to_string(),
70 | (((previous_state.parse::().unwrap_or(1.0) / max) * 100.0) as i64).to_string(),
71 | ))
72 | .unwrap_or_default();
73 | let mut current_state: String;
74 | loop {
75 | if let Err(err) = inotify.read_events_blocking(&mut buffer) {
76 | println!("{}", err);
77 | std::thread::sleep(interval);
78 | }
79 | // println!("I am changing now");
80 | current_state = read_file_for_monitor(&mut file, &mut buffer);
81 | if previous_state == current_state {
82 | continue;
83 | }
84 | previous_state = current_state;
85 | sender
86 | .send((
87 | "".to_string(),
88 | (((previous_state.parse::().unwrap_or(1.0) / max) * 100.0) as i64)
89 | .to_string(),
90 | ))
91 | .unwrap_or_default();
92 | // sender.send((previous_state.parse::().unwrap_or(1.0) / 1000000.0).to_string());
93 | } //loop
94 | }); //thread
95 | } //update widget
96 |
--------------------------------------------------------------------------------
/src/modules/hyprland.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use gtk::traits::ContainerExt;
4 | use json::{Error, JsonValue};
5 |
6 | use crate::builder::widgets_builder::{self, Align, WidgetConfig};
7 | use crate::network::hyprland_socket::listen;
8 | use crate::utils::{command, regex_matcher};
9 | use glib::MainContext;
10 | use gtk::prelude::*;
11 | // use super::workspace::listen;
12 |
13 | fn setup_default(mut text: String) -> (String, Result) {
14 | text = text
15 | .replace("{workspace}", "{workspace.id}")
16 | .replace("{activewindow}", "{title}");
17 | let out = command::run(&"hyprctl activewindow -j".to_string())
18 | .trim()
19 | .to_string();
20 | let mut jsondata = json::parse(&out);
21 | if let Ok(json) = jsondata {
22 | jsondata = Ok(json);
23 | }
24 | if let Some(data) = regex_matcher::format(&text, &out) {
25 | text = data;
26 | }
27 |
28 | return (text, jsondata);
29 | }
30 |
31 | pub fn build_label(left: >k::Box, center: >k::Box, right: >k::Box, config: WidgetConfig) {
32 | let original: String = config.format.clone();
33 |
34 | let (text, jsondata) = setup_default(original.clone());
35 |
36 | let label = widgets_builder::build_and_align(&text, &left, ¢er, &right, &config);
37 | // println!("{}", text);
38 |
39 | update_widget(label, original, jsondata);
40 | } //build_label
41 |
42 | pub fn update_widget(
43 | label: gtk::Label,
44 | original: String,
45 | jsondata: Result,
46 | ) {
47 | let (sender, receiver) = MainContext::channel::<(String, String)>(glib::Priority::DEFAULT);
48 |
49 | let rt = tokio::runtime::Builder::new_current_thread()
50 | .enable_all()
51 | .build();
52 |
53 | if let Err(err) = rt {
54 | println!("{}", err);
55 | return;
56 | }
57 | let mut params = regex_matcher::get_params(&original);
58 | if params.len() == 0 {
59 | return;
60 | }
61 | std::thread::spawn(move || rt.unwrap().block_on(listen(sender)));
62 |
63 | //listen for the socket
64 | params = set_current_win_wor(params, jsondata);
65 | hyprland_signal_receiver(receiver, params, original, label);
66 | }
67 |
68 | fn set_current_win_wor(
69 | mut params: HashMap,
70 | json: Result,
71 | ) -> HashMap {
72 | if let Ok(data) = json {
73 | if params.contains_key("workspace") {
74 | params.insert(
75 | "workspace".to_string(),
76 | data["workspace"]["name"].as_str().unwrap_or("").to_string(),
77 | );
78 | } //if workspace
79 | if params.contains_key("activewindow") {
80 | params.insert(
81 | "activewindow".to_string(),
82 | data["title"].as_str().unwrap_or("").to_string(),
83 | );
84 | } //if workspace
85 | } //jsonvale
86 |
87 | return params;
88 | }
89 |
90 | fn hyprland_signal_receiver(
91 | receiver: glib::Receiver<(String, String)>,
92 | mut params: HashMap,
93 | original: String,
94 | label: gtk::Label,
95 | ) {
96 | let mut format_text: String = String::new();
97 | //reciver is here
98 | receiver.attach(None, move |(name, value)| {
99 | // println!("{}", name);
100 |
101 | if params.contains_key(&name) {
102 | //
103 | if name == "activewindow" {
104 | if let Some((_class, title)) = value.split_once(",") {
105 | params.insert(name.trim().to_string(), title.trim().to_string());
106 | }
107 | } else {
108 | params.insert(name.trim().to_string(), value.trim().to_string());
109 | }
110 | format_text = original.clone();
111 | for (key, value) in params.clone().into_iter() {
112 | format_text = format_text.replace(&format!("{{{}}}", key), &value);
113 | }
114 | label.set_text(&format_text);
115 | }
116 | glib::ControlFlow::Continue
117 | });
118 | }
--------------------------------------------------------------------------------
/src/modules/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod battery;
2 | pub mod brightness;
3 | pub mod hyprland;
4 | pub mod tray;
5 |
--------------------------------------------------------------------------------
/src/modules/tray.rs:
--------------------------------------------------------------------------------
1 | use gtk::glib;
2 | use gtk::prelude::*;
3 | use gtk::{IconLookupFlags, IconTheme, Image, Menu, MenuBar, MenuItem, SeparatorMenuItem};
4 | use once_cell::sync::Lazy;
5 | use std::collections::HashMap;
6 | use std::sync::Mutex;
7 | use std::thread;
8 | use stray::message::menu::{MenuType, TrayMenu};
9 | use stray::message::tray::{IconPixmap, StatusNotifierItem};
10 | use stray::message::{NotifierItemCommand, NotifierItemMessage};
11 | use stray::StatusNotifierWatcher;
12 | use tokio::runtime::Runtime;
13 | use tokio::sync::mpsc;
14 |
15 | use crate::builder::widgets_builder;
16 | use crate::builder::widgets_builder::Align;
17 | use crate::builder::widgets_builder::WidgetConfig;
18 |
19 | struct NotifierItem {
20 | item: StatusNotifierItem,
21 | menu: Option,
22 | }
23 |
24 | pub struct StatusNotifierWrapper {
25 | menu: stray::message::menu::MenuItem,
26 | }
27 |
28 | static STATE: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new()));
29 |
30 | impl StatusNotifierWrapper {
31 | fn into_menu_item(
32 | self,
33 | sender: mpsc::Sender,
34 | notifier_address: String,
35 | menu_path: String,
36 | ) -> MenuItem {
37 | let item: Box> = match self.menu.menu_type {
38 | MenuType::Separator => Box::new(SeparatorMenuItem::new()),
39 | MenuType::Standard => Box::new(MenuItem::with_label(self.menu.label.as_str())),
40 | };
41 |
42 | let item = (*item).as_ref().clone();
43 |
44 | {
45 | let sender = sender.clone();
46 | let notifier_address = notifier_address.clone();
47 | let menu_path = menu_path.clone();
48 |
49 | item.connect_activate(move |_item| {
50 | sender
51 | .try_send(NotifierItemCommand::MenuItemClicked {
52 | submenu_id: self.menu.id,
53 | menu_path: menu_path.clone(),
54 | notifier_address: notifier_address.clone(),
55 | })
56 | .unwrap();
57 | });
58 | };
59 |
60 | let submenu = Menu::new();
61 | if !self.menu.submenu.is_empty() {
62 | for submenu_item in self.menu.submenu.iter().cloned() {
63 | let submenu_item = StatusNotifierWrapper { menu: submenu_item };
64 | let submenu_item = submenu_item.into_menu_item(
65 | sender.clone(),
66 | notifier_address.clone(),
67 | menu_path.clone(),
68 | );
69 | submenu.append(&submenu_item);
70 | }
71 |
72 | item.set_submenu(Some(&submenu));
73 | }
74 |
75 | item
76 | }
77 | }
78 |
79 | impl NotifierItem {
80 | fn get_icon(&self) -> Option {
81 | match &self.item.icon_pixmap {
82 | None => self.get_icon_from_theme(),
83 | Some(pixmaps) => self.get_icon_from_pixmaps(pixmaps),
84 | }
85 | }
86 |
87 | fn get_icon_from_pixmaps(&self, pixmaps: &[IconPixmap]) -> Option {
88 | let pixmap = pixmaps
89 | .iter()
90 | .find(|pm| pm.height > 20 && pm.height < 32)
91 | .expect("No icon of suitable size found");
92 |
93 | let pixbuf = gtk::gdk_pixbuf::Pixbuf::new(
94 | gtk::gdk_pixbuf::Colorspace::Rgb,
95 | true,
96 | 8,
97 | pixmap.width,
98 | pixmap.height,
99 | )
100 | .expect("Failed to allocate pixbuf");
101 |
102 | for y in 0..pixmap.height {
103 | for x in 0..pixmap.width {
104 | let index = (y * pixmap.width + x) * 4;
105 | let a = pixmap.pixels[index as usize];
106 | let r = pixmap.pixels[(index + 1) as usize];
107 | let g = pixmap.pixels[(index + 2) as usize];
108 | let b = pixmap.pixels[(index + 3) as usize];
109 | pixbuf.put_pixel(x as u32, y as u32, r, g, b, a);
110 | }
111 | }
112 |
113 | Some(Image::from_pixbuf(Some(&pixbuf)))
114 | }
115 |
116 | fn get_icon_from_theme(&self) -> Option {
117 | let theme = gtk::IconTheme::default().unwrap_or(IconTheme::new());
118 | theme.rescan_if_needed();
119 |
120 | if let Some(path) = self.item.icon_theme_path.as_ref() {
121 | theme.append_search_path(path);
122 | }
123 |
124 | let icon_name = self.item.icon_name.as_ref().unwrap();
125 | let icon = theme.lookup_icon(icon_name, 24, IconLookupFlags::GENERIC_FALLBACK);
126 |
127 | icon.map(|i| Image::from_pixbuf(i.load_icon().ok().as_ref()))
128 | }
129 | }
130 |
131 | pub fn build_label(left: >k::Box, center: >k::Box, right: >k::Box, config: WidgetConfig) {
132 | // println!("{}", text);
133 | // println!("Ia m here2");
134 | let menu_bar = MenuBar::new();
135 | menu_bar.style_context().add_class(&config.name_of_widget);
136 | match config.align {
137 | Align::CENTER => center.add(&menu_bar),
138 | Align::LEFT => left.add(&menu_bar),
139 | Align::RIGHT => right.add(&menu_bar),
140 | }
141 | // println!("Ia m here");
142 | update_widget(menu_bar);
143 | }
144 |
145 | fn update_widget(menu_bar: MenuBar) {
146 | let (sender, receiver) = mpsc::channel(32);
147 | let (cmd_tx, cmd_rx) = mpsc::channel(32);
148 |
149 | spawn_local_handler(menu_bar, receiver, cmd_tx);
150 | start_communication_thread(sender, cmd_rx);
151 | }
152 |
153 | fn spawn_local_handler(
154 | v_box: MenuBar,
155 | mut receiver: mpsc::Receiver,
156 | cmd_tx: mpsc::Sender,
157 | ) {
158 | let main_context = glib::MainContext::default();
159 | let future = async move {
160 | while let Some(item) = receiver.recv().await {
161 | let mut state = STATE.lock().unwrap();
162 |
163 | match item {
164 | NotifierItemMessage::Update {
165 | address: id,
166 | item,
167 | menu,
168 | } => {
169 | state.insert(id, NotifierItem { item: *item, menu });
170 | }
171 | NotifierItemMessage::Remove { address } => {
172 | state.remove(&address);
173 | }
174 | }
175 |
176 | for child in v_box.children() {
177 | v_box.remove(&child);
178 | }
179 |
180 | for (address, notifier_item) in state.iter() {
181 | if let Some(icon) = notifier_item.get_icon() {
182 | // Create the menu
183 |
184 | let menu_item = MenuItem::new();
185 | let menu_item_box = gtk::Box::default();
186 | menu_item_box.add(&icon);
187 | menu_item.add(&menu_item_box);
188 |
189 | if let Some(tray_menu) = ¬ifier_item.menu {
190 | let menu = Menu::new();
191 | tray_menu
192 | .submenus
193 | .iter()
194 | .map(|submenu| StatusNotifierWrapper {
195 | menu: submenu.to_owned(),
196 | })
197 | .map(|item| {
198 | let menu_path =
199 | notifier_item.item.menu.as_ref().unwrap().to_string();
200 | let address = address.to_string();
201 | item.into_menu_item(cmd_tx.clone(), address, menu_path)
202 | })
203 | .for_each(|item| menu.append(&item));
204 |
205 | if !tray_menu.submenus.is_empty() {
206 | menu_item.set_submenu(Some(&menu));
207 | }
208 | }
209 | v_box.append(&menu_item);
210 | };
211 |
212 | v_box.show_all();
213 | }
214 | }
215 | };
216 |
217 | main_context.spawn_local(future);
218 | }
219 |
220 | fn start_communication_thread(
221 | sender: mpsc::Sender,
222 | cmd_rx: mpsc::Receiver,
223 | ) {
224 | thread::spawn(move || {
225 | let runtime = Runtime::new().expect("Failed to create tokio RT");
226 |
227 | runtime.block_on(async {
228 | let tray = StatusNotifierWatcher::new(cmd_rx).await.unwrap();
229 | let mut host = tray.create_notifier_host("MyHost").await.unwrap();
230 |
231 | while let Ok(message) = host.recv().await {
232 | sender
233 | .send(message)
234 | .await
235 | .expect("failed to send message to UI");
236 | }
237 |
238 | host.destroy().await.unwrap();
239 | })
240 | });
241 | }
242 |
--------------------------------------------------------------------------------
/src/network/hyprland_socket.rs:
--------------------------------------------------------------------------------
1 | use tokio::io::{AsyncBufReadExt, BufReader};
2 | async fn connect_socket() -> Option {
3 | let uuid = std::env::var("HYPRLAND_INSTANCE_SIGNATURE");
4 | if let Err(err) = uuid {
5 | println!("{}", err);
6 | return None;
7 | }
8 | let path = format!("/tmp/hypr/{}/.socket2.sock", uuid.unwrap());
9 | let stream = tokio::net::UnixStream::connect(path).await;
10 | if let Err(err) = stream {
11 | println!("{}", err);
12 | return None;
13 | } //if
14 |
15 | return Some(stream.unwrap());
16 | }
17 |
18 | pub async fn listen(sender: glib::Sender<(String, String)>) {
19 | let stream = if let Some(stream) = connect_socket().await {
20 | stream
21 | } else {
22 | return;
23 | };
24 |
25 | let mut buffer = String::new();
26 | let mut reader = BufReader::new(stream);
27 | loop {
28 | while reader.read_line(&mut buffer).await.unwrap_or_default() > 0 {
29 | if let Some((action_name, action_value)) = buffer.split_once(">>") {
30 | sender
31 | .send((action_name.to_string(), action_value.to_string()))
32 | .unwrap_or_default();
33 | }
34 | buffer.clear();
35 | } //while
36 | } //loop
37 | } //func
38 |
--------------------------------------------------------------------------------
/src/network/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod hyprland_socket;
2 |
--------------------------------------------------------------------------------
/src/utils/command.rs:
--------------------------------------------------------------------------------
1 | //
2 | pub fn run(command: &String) -> String {
3 | match std::process::Command::new("zsh")
4 | .arg("-c")
5 | .arg(command)
6 | .output()
7 | {
8 | Ok(output) => {
9 | return String::from_utf8(output.stdout).unwrap();
10 | }
11 |
12 | Err(err) => return err.to_string(),
13 | } //match
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/constants.rs:
--------------------------------------------------------------------------------
1 | pub const CONFIG_PATH: &str = "/.config/heliumbar/helium.json";
2 | pub const CONFIG_STYLE: &str = "/.config/heliumbar/style.css";
3 | pub const BATTERY_PATH: &str = "/sys/class/power_supply/";
4 | pub const BRIGHTNESS_PATH: &str = "/sys/class/backlight/";
5 |
--------------------------------------------------------------------------------
/src/utils/file_handler.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::io::{Read, Seek};
3 | pub fn read_file_for_monitor(file: &mut File, buffer: &mut [u8]) -> String {
4 | match file.seek(std::io::SeekFrom::Start(0)) {
5 | Ok(_) => (),
6 | Err(err) => {
7 | println!("{}", err);
8 | return "".to_string();
9 | }
10 | } //match
11 |
12 | let stat = file.read(buffer);
13 | if let Err(err) = stat {
14 | println!("{}", err);
15 | return "".to_string();
16 | }
17 | let stat = stat.unwrap();
18 | if stat > 0 {
19 | String::from_utf8_lossy(&buffer[0..stat]).trim().to_string()
20 | } else {
21 | "".to_string()
22 | }
23 | }
24 |
25 | pub fn get_particular_dir_path(path: String, file_to_search: String) -> Option {
26 | let file_dirs = std::fs::read_dir(path);
27 | if let Err(err) = file_dirs {
28 | println!("{}", err);
29 | return None;
30 | }
31 | for each in file_dirs.unwrap() {
32 | if let Ok(dir) = each {
33 | let dir = dir.path();
34 | if dir.is_dir() {
35 | let found_dir = dir.to_str();
36 | if let Some(found_dir) = found_dir {
37 | let final_path = format!("{}/{}", found_dir, file_to_search);
38 | if std::path::Path::new(&final_path).exists() {
39 | return Some(found_dir.to_string());
40 | } //if path
41 | } //findal
42 | } //if dir
43 | } //found some files
44 | }
45 |
46 | return None;
47 | }
48 |
--------------------------------------------------------------------------------
/src/utils/listener.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::regex_matcher;
2 | use gtk::prelude::*;
3 | use std::collections::HashMap;
4 |
5 | //
6 |
7 | pub fn listen(receiver: glib::Receiver<(String, String)>, original: String, label: gtk::Label) {
8 | let mut params = regex_matcher::get_params(&original);
9 | let mut format_text: String = String::new();
10 | //reciver is here
11 | receiver.attach(None, move |(name, value)| {
12 | // println!("{}", name);
13 |
14 | if params.contains_key(&name) {
15 | //
16 | params.insert(name.trim().to_string(), value.trim().to_string());
17 | format_text = original.clone();
18 | for (key, value) in params.clone().into_iter() {
19 | format_text = format_text.replace(&format!("{{{}}}", key), &value);
20 | }
21 | label.set_text(&format_text);
22 | }
23 | glib::ControlFlow::Continue
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod command;
2 | pub mod constants;
3 | pub mod file_handler;
4 | pub mod listener;
5 | pub mod regex_matcher;
6 |
--------------------------------------------------------------------------------
/src/utils/regex_matcher.rs:
--------------------------------------------------------------------------------
1 | use json::{self, JsonValue};
2 | use std::collections::HashMap;
3 | pub fn format(string: &str, json_data: &str) -> Option {
4 | let json_parse = json::parse(&json_data);
5 | if let Err(err) = json_parse {
6 | println!("{}", err);
7 | return None;
8 | }
9 | let json_parse = json_parse.unwrap();
10 |
11 | let mut is_in_block = false;
12 | let mut word: String = String::new();
13 | let mut out = string.to_string();
14 | let mut temp;
15 |
16 | for c in string.chars() {
17 | if c == '{' {
18 | is_in_block = true;
19 | continue;
20 | } //if {}
21 |
22 | if is_in_block {
23 | if c != '}' {
24 | word.push(c);
25 | } else {
26 | is_in_block = false;
27 | let data: Vec<&str> = word.split(".").collect();
28 | if data.len() == 1 {
29 | temp = json_parse[&word].to_string();
30 | } else if data.len() == 2 {
31 | temp = json_parse[data[0]][data[1]].to_string();
32 | } else {
33 | temp = json_parse[data[0]][data[1]][data[2]].to_string();
34 | }
35 | out = out.replace(&format!("{{{}}}", word), &temp);
36 | word.clear();
37 | }
38 | }
39 | } //for loop
40 |
41 | return Some(out);
42 | // for capture in re.captures_iter(string) {
43 | // println!("{}", capture.get(1).unwrap().as_str());
44 | // }
45 | }
46 |
47 | pub fn get_params(string: &String) -> HashMap {
48 | let mut is_in_block = false;
49 | let mut word: String = String::new();
50 | // let mut array = Vec::::new();
51 | let mut params: HashMap = HashMap::new();
52 |
53 | for c in string.chars() {
54 | if c == '{' {
55 | is_in_block = true;
56 | continue;
57 | } //if {}
58 | if is_in_block {
59 | if c != '}' {
60 | word.push(c);
61 | } else {
62 | is_in_block = false;
63 | // println!("{}", word);
64 | params.insert(word.clone(), "".to_string());
65 | // array.push(word.clone());
66 | word.clear();
67 | }
68 | }
69 | } //for loop
70 |
71 | return params;
72 | } //
73 |
--------------------------------------------------------------------------------
/src/widgets/LabelWidget.rs:
--------------------------------------------------------------------------------
1 | use gtk::traits::ContainerExt;
2 |
3 | use crate::builder::widgets_builder::{self, Align, WidgetConfig};
4 | use crate::utils::{command, regex_matcher};
5 | use glib::MainContext;
6 | use gtk::prelude::*;
7 | use std::io::BufRead;
8 | use std::io::BufReader;
9 | use std::process::{Command, Stdio};
10 | pub fn build_label(left: >k::Box, center: >k::Box, right: >k::Box, config: WidgetConfig) {
11 | let original: String = config.format.clone();
12 | let mut text = original.clone();
13 | if config.command.len() > 0 && config.refresh_rate == 0 {
14 | let out = command::run(&config.command).trim().to_string();
15 | if config.is_json {
16 | if let Some(data) = regex_matcher::format(&original, &out) {
17 | text = data;
18 | }
19 | }
20 | //if json
21 | else {
22 | text = original.replace("{}", &out);
23 | }
24 | } //if command
25 |
26 | let label = widgets_builder::build_and_align(&text, &left, ¢er, &right, &config);
27 |
28 | if config.refresh_rate > 0 && config.command.len() > 0 {
29 | update_widget(
30 | label,
31 | original,
32 | config.is_json,
33 | config.refresh_rate,
34 | &config.command,
35 | );
36 | }
37 | // println!("lenght of command{}", config.command.len());
38 | }
39 |
40 | pub fn update_widget(
41 | label: gtk::Label,
42 | original: String,
43 | is_json: bool,
44 | refresh_rate: i64,
45 | command: &str,
46 | ) {
47 | let child = Command::new("zsh")
48 | .arg("-c")
49 | .arg(&format!(
50 | "while true; do;{};sleep {};done",
51 | command, refresh_rate
52 | ))
53 | .stdout(Stdio::piped())
54 | .spawn();
55 |
56 | if let Err(error) = child {
57 | println!("{}", error);
58 | return;
59 | }
60 | let stdout = child.unwrap().stdout.take().unwrap();
61 | let (sender, receiver) = MainContext::channel::(glib::Priority::DEFAULT);
62 |
63 | std::thread::spawn(move || {
64 | let reader = BufReader::new(stdout);
65 | for line in reader.lines() {
66 | if let Ok(data) = line {
67 | sender.send(data.to_string()).unwrap();
68 | } //if
69 | }
70 | });
71 |
72 | receiver.attach(None, move |data| {
73 | // println!("receiver found here{}", data);
74 | if is_json {
75 | if let Some(out) = regex_matcher::format(&original, &data) {
76 | label.set_text(&out);
77 | }
78 | } else {
79 | label.set_text(&original.replace("{}", &data.trim().to_string()));
80 | }
81 | glib::ControlFlow::Continue
82 | });
83 | }
84 |
--------------------------------------------------------------------------------
/src/widgets/mod.rs:
--------------------------------------------------------------------------------
1 | #[allow(non_snake_case)]
2 | #[allow(non_snake_case)]
3 | pub mod LabelWidget;
4 |
--------------------------------------------------------------------------------
/stray/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stray"
3 | version = "0.1.3"
4 | edition = "2021"
5 | license = "MIT"
6 | description = "A freedesktop StatusNotifierWatcher implementation"
7 | repository = "https://github.com/oknozor/stray"
8 | readme = "README.md"
9 |
10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
11 |
12 | [dependencies]
13 | tokio = { version = "1.17.0", features = ["rt-multi-thread", "macros"] }
14 | tokio-stream = "0.1.8"
15 | zbus = { version = "3.13.1", default-features = false, features = ["tokio", "gvariant"] }
16 | anyhow = "1.0.56"
17 | serde = "1.0.136"
18 | byteorder = "1.4.3"
19 | chrono = "0.4.19"
20 | log = "0.4.17"
21 | thiserror = "1.0.31"
22 | tracing = "0.1"
23 |
24 | [[example]]
25 | path = "examples/simple.rs"
26 | name = "simple"
27 |
--------------------------------------------------------------------------------
/stray/README.md:
--------------------------------------------------------------------------------
1 | # Stray
2 |
3 | Stray is a minimal [SystemNotifierWatcher](https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierWatcher/)
4 | implementation which goal is to provide a minimalistic API to access tray icons and menu.
5 |
6 | ## Examples
7 |
8 | ### Start the system tray and listen for changes
9 | ```rust, ignore
10 | use stray::{SystemTray};
11 | use tokio_stream::StreamExt;
12 | use stray::message::NotifierItemMessage;
13 | use stray::message::NotifierItemCommand;
14 |
15 | #[tokio::main]
16 | async fn main() {
17 |
18 | // A mpsc channel to send menu activation requests later
19 | let (ui_tx, ui_rx) = tokio::sync::mpsc::channel(32);
20 | let mut tray = SystemTray::new(ui_rx).await;
21 |
22 | while let Some(message) = tray.next().await {
23 | match message {
24 | NotifierItemMessage::Update { address: id, item, menu } => {
25 | println!("NotifierItem updated :
26 | id = {id},
27 | item = {item:?},
28 | menu = {menu:?}"
29 | )
30 | }
31 | NotifierItemMessage::Remove { address: id } => {
32 | println!("NotifierItem removed : id = {id}");
33 | }
34 | }
35 | }
36 | }
37 | ```
38 |
39 | ### Send menu activation request to the system tray
40 |
41 | ```rust, ignore
42 | // Assuming we stored our menu items in some UI state we can send menu item activation request:
43 | use stray::message::NotifierItemCommand;
44 |
45 | ui_tx.clone().try_send(NotifierItemCommand::MenuItemClicked {
46 | // The submenu to activate
47 | submenu_id: 32,
48 | // dbus menu path, available in the `StatusNotifierItem`
49 | menu_path: "/org/ayatana/NotificationItem/Element1/Menu".to_string(),
50 | // the notifier address we previously got from `NotifierItemMessage::Update`
51 | notifier_address: ":1.2161".to_string(),
52 | }).unwrap();
53 | ```
54 |
55 | ### Gtk example
56 |
57 | For a detailed, real life example, you can take a look at the [gtk-tray](https://github.com/oknozor/stray/tree/main/gtk-tray).
58 |
59 | ```shell
60 | git clone git@github.com:oknozor/stray.git
61 | cd stray/gtk-tray
62 | cargo run
63 | ```
64 |
--------------------------------------------------------------------------------
/stray/examples/simple.rs:
--------------------------------------------------------------------------------
1 | use stray::StatusNotifierWatcher;
2 | use tokio::join;
3 | use tokio::sync::mpsc;
4 |
5 | #[tokio::main]
6 | async fn main() -> stray::error::Result<()> {
7 | let (_cmd_tx, cmd_rx) = mpsc::channel(10);
8 | let tray = StatusNotifierWatcher::new(cmd_rx).await?;
9 |
10 | let mut host_one = tray.create_notifier_host("host_one").await.unwrap();
11 | let mut host_two = tray.create_notifier_host("host_two").await.unwrap();
12 |
13 | let one = tokio::spawn(async move {
14 | while let Ok(mesage) = host_one.recv().await {
15 | println!("Message from host one {:?}", mesage);
16 | }
17 | });
18 |
19 | let two = tokio::spawn(async move {
20 | let mut count = 0;
21 | while let Ok(mesage) = host_two.recv().await {
22 | count += 1;
23 | if count > 5 {
24 | break;
25 | }
26 | println!("Message from host two {:?}", mesage);
27 | }
28 |
29 | host_two.destroy().await?;
30 | stray::error::Result::<()>::Ok(())
31 | });
32 |
33 | let _ = join!(one, two);
34 | Ok(())
35 | }
36 |
--------------------------------------------------------------------------------
/stray/src/dbus/dbusmenu_proxy.rs:
--------------------------------------------------------------------------------
1 | //! # DBus interface proxy for: `com.canonical.dbusmenu`
2 | //!
3 | //! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
4 | //! Source: `org.cannonical.indicator.xml`.
5 | //!
6 | //! You may prefer to adapt it, instead of using it verbatim.
7 | //!
8 | //! More information can be found in the
9 | //! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html)
10 | //! section of the zbus documentation.
11 | //!
12 |
13 | use std::collections::HashMap;
14 |
15 | use zbus::dbus_proxy;
16 | use zbus::zvariant::OwnedValue;
17 |
18 | use serde::{Deserialize, Serialize};
19 | use zbus::zvariant::Type;
20 |
21 | #[derive(Deserialize, Serialize, Type, PartialEq, Debug)]
22 | pub struct MenuLayout {
23 | pub id: u32,
24 | pub fields: SubMenuLayout,
25 | }
26 |
27 | #[derive(Deserialize, Serialize, Type, PartialEq, Debug)]
28 | pub struct SubMenuLayout {
29 | pub id: i32,
30 | pub fields: HashMap,
31 | pub submenus: Vec,
32 | }
33 |
34 | #[allow(dead_code)]
35 | type GroupProperties = Vec<(i32, HashMap)>;
36 |
37 | #[dbus_proxy(interface = "com.canonical.dbusmenu", assume_defaults = true)]
38 | trait DBusMenu {
39 | fn about_to_show(&self, id: i32) -> zbus::Result;
40 |
41 | fn event(
42 | &self,
43 | id: i32,
44 | event_id: &str,
45 | data: &zbus::zvariant::Value<'_>,
46 | timestamp: u32,
47 | ) -> zbus::Result<()>;
48 |
49 | fn get_group_properties(
50 | &self,
51 | ids: &[i32],
52 | property_names: &[&str],
53 | ) -> zbus::Result<(u32, GroupProperties)>;
54 |
55 | fn get_layout(
56 | &self,
57 | parent_id: i32,
58 | recursion_depth: i32,
59 | property_names: &[&str],
60 | ) -> zbus::Result;
61 |
62 | fn get_property(&self, id: i32, name: &str) -> zbus::Result;
63 |
64 | #[dbus_proxy(signal)]
65 | fn item_activation_requested(&self, id: i32, timestamp: u32) -> zbus::Result<()>;
66 |
67 | #[dbus_proxy(signal)]
68 | fn items_properties_updated(
69 | &self,
70 | updated_props: Vec<(i32, HashMap<&str, zbus::zvariant::Value<'_>>)>,
71 | removed_props: Vec<(i32, Vec<&str>)>,
72 | ) -> zbus::Result<()>;
73 |
74 | #[dbus_proxy(signal)]
75 | fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>;
76 |
77 | #[dbus_proxy(property)]
78 | fn status(&self) -> zbus::Result;
79 |
80 | #[dbus_proxy(property)]
81 | fn version(&self) -> zbus::Result;
82 | }
83 |
--------------------------------------------------------------------------------
/stray/src/dbus/mod.rs:
--------------------------------------------------------------------------------
1 | pub(super) mod dbusmenu_proxy;
2 | pub(super) mod notifier_item_proxy;
3 | pub(super) mod notifier_watcher_proxy;
4 | pub(super) mod notifier_watcher_service;
5 |
--------------------------------------------------------------------------------
/stray/src/dbus/notifier_item_proxy.rs:
--------------------------------------------------------------------------------
1 | //! # DBus interface proxy for: `org.kde.StatusNotifierItem`
2 | //!
3 | //! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
4 | //! Source: `status-notifier-item.xml`.
5 | //!
6 | //! You may prefer to adapt it, instead of using it verbatim.
7 | //!
8 | //! More information can be found in the
9 | //! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html)
10 | //! section of the zbus documentation.
11 | //!
12 |
13 | use zbus::dbus_proxy;
14 |
15 | #[allow(dead_code)]
16 | type ToolTip = (String, Vec<(i32, i32, Vec)>);
17 |
18 | #[dbus_proxy(interface = "org.kde.StatusNotifierItem", assume_defaults = true)]
19 | trait StatusNotifierItem {
20 | /// Activate method
21 | fn activate(&self, x: i32, y: i32) -> zbus::Result<()>;
22 |
23 | /// ContextMenu method
24 | fn context_menu(&self, x: i32, y: i32) -> zbus::Result<()>;
25 |
26 | /// Scroll method
27 | fn scroll(&self, delta: i32, orientation: &str) -> zbus::Result<()>;
28 |
29 | /// SecondaryActivate method
30 | fn secondary_activate(&self, x: i32, y: i32) -> zbus::Result<()>;
31 |
32 | /// NewAttentionIcon signal
33 | #[dbus_proxy(signal)]
34 | fn new_attention_icon(&self) -> zbus::Result<()>;
35 |
36 | /// NewIcon signal
37 | #[dbus_proxy(signal)]
38 | fn new_icon(&self) -> zbus::Result<()>;
39 |
40 | /// NewOverlayIcon signal
41 | #[dbus_proxy(signal)]
42 | fn new_overlay_icon(&self) -> zbus::Result<()>;
43 |
44 | /// NewStatus signal
45 | #[dbus_proxy(signal)]
46 | fn new_status(&self, status: &str) -> zbus::Result<()>;
47 |
48 | /// NewTitle signal
49 | #[dbus_proxy(signal)]
50 | fn new_title(&self) -> zbus::Result<()>;
51 |
52 | /// NewToolTip signal
53 | #[dbus_proxy(signal)]
54 | fn new_tool_tip(&self) -> zbus::Result<()>;
55 |
56 | /// AttentionIconName property
57 | #[dbus_proxy(property)]
58 | fn attention_icon_name(&self) -> zbus::Result;
59 |
60 | /// AttentionIconPixmap property
61 | #[dbus_proxy(property)]
62 | fn attention_icon_pixmap(&self) -> zbus::Result)>>;
63 |
64 | /// AttentionMovieName property
65 | #[dbus_proxy(property)]
66 | fn attention_movie_name(&self) -> zbus::Result;
67 |
68 | /// Category property
69 | #[dbus_proxy(property)]
70 | fn category(&self) -> zbus::Result;
71 |
72 | /// IconName property
73 | #[dbus_proxy(property)]
74 | fn icon_name(&self) -> zbus::Result;
75 |
76 | /// IconPixmap property
77 | #[dbus_proxy(property)]
78 | fn icon_pixmap(&self) -> zbus::Result)>>;
79 |
80 | /// IconThemePath property
81 | #[dbus_proxy(property)]
82 | fn icon_theme_path(&self) -> zbus::Result;
83 |
84 | /// Id property
85 | #[dbus_proxy(property)]
86 | fn id(&self) -> zbus::Result;
87 |
88 | /// ItemIsMenu property
89 | #[dbus_proxy(property)]
90 | fn item_is_menu(&self) -> zbus::Result;
91 |
92 | /// Menu property
93 | #[dbus_proxy(property)]
94 | fn menu(&self) -> zbus::Result;
95 |
96 | /// OverlayIconName property
97 | #[dbus_proxy(property)]
98 | fn overlay_icon_name(&self) -> zbus::Result;
99 |
100 | /// OverlayIconPixmap property
101 | #[dbus_proxy(property)]
102 | fn overlay_icon_pixmap(&self) -> zbus::Result)>>;
103 |
104 | /// Status property
105 | #[dbus_proxy(property)]
106 | fn status(&self) -> zbus::Result;
107 |
108 | /// Title property
109 | #[dbus_proxy(property)]
110 | fn title(&self) -> zbus::Result;
111 |
112 | /// ToolTip property
113 | #[dbus_proxy(property)]
114 | fn tool_tip(&self) -> zbus::Result;
115 | }
116 |
--------------------------------------------------------------------------------
/stray/src/dbus/notifier_watcher_proxy.rs:
--------------------------------------------------------------------------------
1 | //! # DBus interface proxy for: `org.kde.StatusNotifierWatcher`
2 | //!
3 | //! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
4 | //! Source: `notfifier-watcher.xml`.
5 | //!
6 | //! You may prefer to adapt it, instead of using it verbatim.
7 | //!
8 | //! More information can be found in the
9 | //! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html)
10 | //! section of the zbus documentation.
11 | //!
12 |
13 | use zbus::dbus_proxy;
14 |
15 | #[dbus_proxy(
16 | interface = "org.kde.StatusNotifierWatcher",
17 | default_path = "/StatusNotifierWatcher"
18 | )]
19 | pub(crate) trait StatusNotifierWatcher {
20 | fn register_status_notifier_host(&self, service: &str) -> zbus::Result<()>;
21 |
22 | fn unregister_status_notifier_item(&self, service: &str) -> zbus::Result<()>;
23 |
24 | fn register_status_notifier_item(&self, service: &str) -> zbus::Result<()>;
25 |
26 | #[dbus_proxy(signal)]
27 | fn status_notifier_host_registered(&self) -> zbus::Result<()>;
28 |
29 | #[dbus_proxy(signal)]
30 | fn status_notifier_host_unregistered(&self) -> zbus::Result<()>;
31 |
32 | #[dbus_proxy(signal)]
33 | fn status_notifier_item_registered(&self, service: &str) -> zbus::Result<()>;
34 |
35 | #[dbus_proxy(signal)]
36 | fn status_notifier_item_unregistered(&self, service: &str) -> zbus::Result<()>;
37 |
38 | #[dbus_proxy(property)]
39 | fn is_status_notifier_host_registered(&self) -> zbus::Result;
40 |
41 | #[dbus_proxy(property)]
42 | fn protocol_version(&self) -> zbus::Result;
43 |
44 | #[dbus_proxy(property)]
45 | fn registered_status_notifier_items(&self) -> zbus::Result>;
46 | }
47 |
--------------------------------------------------------------------------------
/stray/src/dbus/notifier_watcher_service.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 | use tokio::sync::broadcast;
3 |
4 | use zbus::dbus_interface;
5 | use zbus::Result;
6 | use zbus::{MessageHeader, SignalContext};
7 |
8 | use crate::NotifierItemMessage;
9 |
10 | pub struct DbusNotifierWatcher {
11 | pub status_notifier_hosts: HashSet,
12 | pub registered_status_notifier_items: HashSet,
13 | pub protocol_version: i32,
14 | pub is_status_notifier_host_registered: bool,
15 | pub sender: broadcast::Sender,
16 | }
17 |
18 | impl DbusNotifierWatcher {
19 | pub(crate) fn new(sender: broadcast::Sender) -> Self {
20 | DbusNotifierWatcher {
21 | registered_status_notifier_items: HashSet::new(),
22 | protocol_version: 0,
23 | is_status_notifier_host_registered: false,
24 | status_notifier_hosts: HashSet::new(),
25 | sender,
26 | }
27 | }
28 | }
29 |
30 | impl DbusNotifierWatcher {
31 | pub async fn remove_notifier(&mut self, notifier_address: &str) -> Result<()> {
32 | let to_remove = self
33 | .registered_status_notifier_items
34 | .iter()
35 | .find(|item| item.contains(notifier_address))
36 | .cloned();
37 |
38 | if let Some(notifier) = to_remove {
39 | let removed = self.registered_status_notifier_items.remove(¬ifier);
40 | if removed {
41 | self.sender
42 | .send(NotifierItemMessage::Remove {
43 | address: notifier_address.to_string(),
44 | })
45 | .expect("Failed to dispatch notifier item removed message");
46 | }
47 | }
48 |
49 | Ok(())
50 | }
51 | }
52 |
53 | #[allow(dead_code)]
54 | #[dbus_interface(name = "org.kde.StatusNotifierWatcher")]
55 | impl DbusNotifierWatcher {
56 | async fn register_status_notifier_host(
57 | &mut self,
58 | service: &str,
59 | #[zbus(signal_context)] ctxt: SignalContext<'_>,
60 | ) {
61 | tracing::info!("StatusNotifierHost registered: '{}'", service);
62 | self.status_notifier_hosts.insert(service.to_string());
63 | self.is_status_notifier_host_registered = true;
64 | self.is_status_notifier_host_registered_changed(&ctxt)
65 | .await
66 | .unwrap();
67 | }
68 |
69 | async fn register_status_notifier_item(
70 | &mut self,
71 | service: &str,
72 | #[zbus(header)] header: MessageHeader<'_>,
73 | #[zbus(signal_context)] ctxt: SignalContext<'_>,
74 | ) {
75 | let address = header
76 | .sender()
77 | .expect("Failed to get message sender in header")
78 | .map(|name| name.to_string())
79 | .expect("Failed to get unique name for notifier");
80 |
81 | let notifier_item = format!("{}{}", address, service);
82 |
83 | self.registered_status_notifier_items
84 | .insert(notifier_item.clone());
85 |
86 | tracing::info!("StatusNotifierItem registered: '{}'", notifier_item);
87 |
88 | Self::status_notifier_item_registered(&ctxt, ¬ifier_item)
89 | .await
90 | .unwrap();
91 | }
92 |
93 | async fn unregister_status_notifier_item(&mut self, service: &str) {
94 | self.remove_notifier(service)
95 | .await
96 | .expect("Failed to unregister StatusNotifierItem")
97 | }
98 |
99 | #[dbus_interface(signal)]
100 | async fn status_notifier_host_registered(ctxt: &SignalContext<'_>) -> Result<()>;
101 |
102 | #[dbus_interface(signal)]
103 | async fn status_notifier_host_unregistered(ctxt: &SignalContext<'_>) -> Result<()>;
104 |
105 | #[dbus_interface(signal)]
106 | async fn status_notifier_item_registered(ctxt: &SignalContext<'_>, service: &str)
107 | -> Result<()>;
108 |
109 | #[dbus_interface(signal)]
110 | async fn status_notifier_item_unregistered(
111 | ctxt: &SignalContext<'_>,
112 | service: &str,
113 | ) -> Result<()>;
114 |
115 | #[dbus_interface(property)]
116 | async fn is_status_notifier_host_registered(&self) -> bool {
117 | self.is_status_notifier_host_registered
118 | }
119 |
120 | #[dbus_interface(property)]
121 | async fn protocol_version(&self) -> i32 {
122 | self.protocol_version
123 | }
124 |
125 | #[dbus_interface(property)]
126 | fn registered_status_notifier_items(&self) -> Vec {
127 | self.registered_status_notifier_items
128 | .iter()
129 | .cloned()
130 | .collect()
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/stray/src/error.rs:
--------------------------------------------------------------------------------
1 | use crate::NotifierItemMessage;
2 | use thiserror::Error;
3 | use tokio::sync::broadcast;
4 |
5 | pub type Result = std::result::Result;
6 |
7 | #[derive(Error, Debug)]
8 | pub enum StatusNotifierWatcherError {
9 | #[error("Dbus connection error")]
10 | DbusError(#[from] zbus::Error),
11 | #[error("Invalid DBus interface name")]
12 | InterfaceNameError(#[from] zbus::names::Error),
13 | #[error("Failed to call DBus standard interface method")]
14 | DBusStandardInterfaceError(#[from] zbus::fdo::Error),
15 | #[error("Serialization error")]
16 | ZvariantError(#[from] zbus::zvariant::Error),
17 | #[error("Service path {0} was not understood")]
18 | DbusAddressError(String),
19 | #[error("Failed to broadcast message to notifier hosts")]
20 | BroadCastSendError(#[from] broadcast::error::SendError),
21 | #[error("Error receiving broadcast message")]
22 | BroadCastRecvError(#[from] broadcast::error::RecvError),
23 | }
24 |
--------------------------------------------------------------------------------
/stray/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![doc = include_str ! ("../README.md")]
2 |
3 | pub use tokio;
4 | use zbus::names::InterfaceName;
5 |
6 | use crate::dbus::dbusmenu_proxy::MenuLayout;
7 | use crate::message::tray::StatusNotifierItem;
8 | use dbus::notifier_watcher_service::DbusNotifierWatcher;
9 |
10 | mod dbus;
11 | mod notifier_host;
12 | mod notifier_watcher;
13 |
14 | pub mod error;
15 | /// Messages sent and received by the [`SystemTray`]
16 | pub mod message;
17 |
18 | pub use message::NotifierItemMessage;
19 | pub use notifier_watcher::StatusNotifierWatcher;
20 |
--------------------------------------------------------------------------------
/stray/src/message/menu.rs:
--------------------------------------------------------------------------------
1 | use serde::Serialize;
2 | use std::str;
3 | use std::str::FromStr;
4 |
5 | use zbus::zvariant::{OwnedValue, Structure, Value};
6 |
7 | use crate::dbus::dbusmenu_proxy::MenuLayout;
8 |
9 | /// A menu that should be displayed when clicking corresponding tray icon
10 | #[derive(Debug, Serialize, Clone)]
11 | pub struct TrayMenu {
12 | /// The unique identifier of the menu
13 | pub id: u32,
14 | /// A recursive list of submenus
15 | pub submenus: Vec