feat: logging support and proper error handling

This commit is contained in:
Jake Stanger 2022-08-21 23:36:07 +01:00
parent 917838c98c
commit ab8f7ecfc8
No known key found for this signature in database
GPG Key ID: C51FC8F9CB0BEA61
28 changed files with 1056 additions and 388 deletions

View File

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clippy (Strict)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used" />
<option name="command" value="clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="false" />

View File

@ -9,7 +9,9 @@
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs />
<envs>
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
</envs>
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">

273
Cargo.lock generated
View File

@ -2,6 +2,21 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
@ -22,12 +37,27 @@ dependencies = [
"memchr",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "anyhow"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "async-broadcast"
version = "0.4.1"
@ -142,6 +172,21 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
dependencies = [
"addr2line",
"cc",
"cfg-if 1.0.0",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -244,7 +289,7 @@ dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"time 0.1.44",
"winapi 0.3.9",
]
@ -287,6 +332,33 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "color-eyre"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204"
dependencies = [
"backtrace",
"color-spantrace",
"eyre",
"indenter",
"once_cell",
"owo-colors",
"tracing-error",
]
[[package]]
name = "color-spantrace"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce"
dependencies = [
"once_cell",
"owo-colors",
"tracing-core",
"tracing-error",
]
[[package]]
name = "colored"
version = "2.0.0"
@ -500,6 +572,16 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "eyre"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
dependencies = [
"indenter",
"once_cell",
]
[[package]]
name = "fastrand"
version = "1.8.0"
@ -737,6 +819,12 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "gimli"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]]
name = "gio"
version = "0.15.12"
@ -983,6 +1071,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "1.9.1"
@ -1036,7 +1130,9 @@ name = "ironbar"
version = "0.3.0"
dependencies = [
"chrono",
"color-eyre",
"cornfig",
"crossbeam-channel 0.3.9",
"dirs",
"futures-util",
"glib",
@ -1050,9 +1146,14 @@ dependencies = [
"serde_json",
"serde_yaml 0.9.4",
"stray",
"strip-ansi-escapes",
"sysinfo",
"tokio",
"toml",
"tracing",
"tracing-appender",
"tracing-error",
"tracing-subscriber",
"walkdir",
]
@ -1141,6 +1242,15 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.5.0"
@ -1162,6 +1272,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.6.23"
@ -1345,6 +1464,24 @@ dependencies = [
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.13.0"
@ -1367,6 +1504,12 @@ version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
[[package]]
name = "owo-colors"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
[[package]]
name = "pango"
version = "0.15.10"
@ -1644,6 +1787,15 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.27"
@ -1659,6 +1811,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
name = "rustc_version"
version = "0.2.3"
@ -1824,6 +1982,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]]
name = "slab"
version = "0.4.7"
@ -1871,6 +2038,15 @@ dependencies = [
"zbus",
]
[[package]]
name = "strip-ansi-escapes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8"
dependencies = [
"vte",
]
[[package]]
name = "strsim"
version = "0.10.0"
@ -1996,6 +2172,17 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "time"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45"
dependencies = [
"itoa",
"libc",
"num_threads",
]
[[package]]
name = "tokio"
version = "1.20.1"
@ -2058,6 +2245,17 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e"
dependencies = [
"crossbeam-channel 0.5.6",
"time 0.3.13",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.22"
@ -2076,6 +2274,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-error"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
dependencies = [
"tracing",
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b"
dependencies = [
"ansi_term",
"matchers",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@ -2118,6 +2356,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "931179334a56395bcf64ba5e0ff56781381c1a5832178280c7d7f91d1679aeb0"
[[package]]
name = "utf8parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version-compare"
version = "0.1.0"
@ -2130,6 +2380,27 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vte"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
dependencies = [
"arrayvec",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
dependencies = [
"proc-macro2 1.0.42",
"quote 1.0.20",
]
[[package]]
name = "waker-fn"
version = "1.1.0"

View File

@ -11,8 +11,13 @@ description = "Customisable wlroots/sway bar"
gtk = "0.15.5"
gtk-layer-shell = "0.4.1"
glib = "0.15.12"
stray = "0.1.1"
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread", "time"] }
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
tracing-error = "0.2.0"
tracing-appender = "0.2.2"
strip-ansi-escapes = "0.1.1"
color-eyre = "0.6.2"
futures-util = "0.3.21"
chrono = "0.4.19"
serde = { version = "1.0.141", features = ["derive"] }
@ -20,10 +25,13 @@ serde_json = "1.0.82"
serde_yaml = "0.9.4"
toml = "0.5.9"
cornfig = "0.2.0"
mpd_client = "0.7.5"
regex = "1.6.0"
ksway = "0.1.0"
sysinfo = "0.25.1"
stray = "0.1.1"
dirs = "4.0.0"
walkdir = "2.3.2"
notify = "4.0.17"
notify = "4.0.17"
mpd_client = "0.7.5"
ksway = "0.1.0"
sysinfo = "0.25.1"
# required for wrapping ksway
crossbeam-channel = "0.3.9"

View File

@ -26,6 +26,16 @@ cargo install ironbar
yay -S ironbar-git
```
### Source
```sh
git clone https://github.com/jakestanger/ironbar.git
cd ironbar
cargo build --release
# change path to wherever you want to install
install target/release/ironbar ~/.local/bin/ironbar
```
[aur package](https://aur.archlinux.org/packages/ironbar-git)
## Configuration
@ -44,37 +54,22 @@ A full styling guide can be found [here](https://github.com/JakeStanger/ironbar/
## Project Status
This project is in very early stages:
This project is in alpha, but should be usable.
Everything that is implemented works and should be documented.
Proper error handling is in place so things should either fail gracefully with detail, or not fail at all.
- Error handling is barely implemented - expect crashes
- There will be bugs!
- Lots of modules need more configuration options
- There's room for lots of modules
- The code is messy and quite prototypal in places
- Config options aren't set in stone - expect breaking changes
- Documentation is probably missing in lots of places
There is currently room for lots more modules, and lots more configuration options for the existing modules.
The current configuration schema is not set in stone and breaking changes could come along at any point;
until the project matures I am more interested in ease of use than backwards compatibility.
That said, it will be *actively developed* as I am using it on my daily driver.
A few bugs do exist, and I am sure there are plenty more to be found.
The project will be *actively developed* as I am using it on my daily driver.
Bugs will be fixed, features will be added, code will be refactored.
## Contribution Guidelines
I welcome contributions of any kind with open arms. That said, please do stick to some basics:
- For code contributions:
- Fix any `cargo clippy` warnings, using at least the default configuration.
- Make sure your code is formatted using `cargo fmt`.
- Keep any documentation up to date.
- I won't enforce it, but preferably stick to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
- For PRs:
- Please open an issue or discussion beforehand.
I'll accept most contributions, but it's best to make sure you're not working on something that won't get accepted :)
- For issues:
- Please provide as much information as you can - share your config, any logs, steps to reproduce...
Please check [here](https://github.com/JakeStanger/ironbar/blob/master/CONTRIBUTING.md).
## Acknowledgements

View File

@ -1,11 +1,17 @@
use crate::config::{BarPosition, ModuleConfig};
use crate::modules::{Module, ModuleInfo, ModuleLocation};
use crate::Config;
use color_eyre::Result;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Orientation};
pub fn create_bar(app: &Application, monitor: &Monitor, monitor_name: &str, config: Config) {
pub fn create_bar(
app: &Application,
monitor: &Monitor,
monitor_name: &str,
config: Config,
) -> Result<()> {
let win = ApplicationWindow::builder().application(app).build();
setup_layer_shell(&win, monitor, &config.position);
@ -31,7 +37,7 @@ pub fn create_bar(app: &Application, monitor: &Monitor, monitor_name: &str, conf
content.set_center_widget(Some(&center));
content.pack_end(&right, false, false, 0);
load_modules(&left, &center, &right, app, config, monitor, monitor_name);
load_modules(&left, &center, &right, app, config, monitor, monitor_name)?;
win.add(&content);
win.connect_destroy_event(|_, _| {
@ -40,6 +46,8 @@ pub fn create_bar(app: &Application, monitor: &Monitor, monitor_name: &str, conf
});
win.show_all();
Ok(())
}
fn load_modules(
@ -50,7 +58,7 @@ fn load_modules(
config: Config,
monitor: &Monitor,
output_name: &str,
) {
) -> Result<()> {
if let Some(modules) = config.left {
let info = ModuleInfo {
app,
@ -60,7 +68,7 @@ fn load_modules(
output_name,
};
add_modules(left, modules, &info);
add_modules(left, modules, &info)?;
}
if let Some(modules) = config.center {
@ -72,7 +80,7 @@ fn load_modules(
output_name,
};
add_modules(center, modules, &info);
add_modules(center, modules, &info)?;
}
if let Some(modules) = config.right {
@ -84,14 +92,16 @@ fn load_modules(
output_name,
};
add_modules(right, modules, &info);
add_modules(right, modules, &info)?;
}
Ok(())
}
fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) {
fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> {
macro_rules! add_module {
($module:expr, $name:literal) => {{
let widget = $module.into_widget(&info);
let widget = $module.into_widget(&info)?;
widget.set_widget_name($name);
content.add(&widget);
}};
@ -109,6 +119,8 @@ fn add_modules(content: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
ModuleConfig::Focused(module) => add_module!(module, "focused"),
}
}
Ok(())
}
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) {

View File

@ -6,7 +6,10 @@ use crate::modules::script::ScriptModule;
use crate::modules::sysinfo::SysInfoModule;
use crate::modules::tray::TrayModule;
use crate::modules::workspaces::WorkspacesModule;
use color_eyre::eyre::{Context, ContextCompat};
use color_eyre::{eyre, Help, Report};
use dirs::config_dir;
use eyre::Result;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
@ -68,49 +71,66 @@ const fn default_bar_height() -> i32 {
}
impl Config {
pub fn load() -> Option<Self> {
if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
pub fn load() -> Result<Self> {
let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
let path = PathBuf::from(config_path);
Self::load_file(
&path,
path.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
)
if path.exists() {
Ok(path)
} else {
Err(Report::msg("Specified config file does not exist")
.note("Config file was specified using `IRONBAR_CONFIG` environment variable"))
}
} else {
let config_dir = config_dir().expect("Failed to locate user config dir");
Self::try_find_config()
}?;
let extensions = vec!["json", "toml", "yaml", "yml", "corn"];
Self::load_file(&config_path)
}
extensions.into_iter().find_map(|extension| {
let full_path = config_dir
.join("ironbar")
.join(format!("config.{extension}"));
fn try_find_config() -> Result<PathBuf> {
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
Self::load_file(&full_path, extension)
})
let extensions = vec!["json", "toml", "yaml", "yml", "corn"];
let file = extensions.into_iter().find_map(|extension| {
let full_path = config_dir
.join("ironbar")
.join(format!("config.{extension}"));
if Path::exists(&full_path) {
Some(full_path)
} else {
None
}
});
match file {
Some(file) => Ok(file),
None => Err(Report::msg("Could not find config file")),
}
}
fn load_file(path: &Path, extension: &str) -> Option<Self> {
if path.exists() {
let file = fs::read(path).expect("Failed to read config file");
Some(match extension {
"json" => serde_json::from_slice(&file).expect("Invalid JSON config"),
"toml" => toml::from_slice(&file).expect("Invalid TOML config"),
"yaml" | "yml" => serde_yaml::from_slice(&file).expect("Invalid YAML config"),
"corn" => {
// corn doesn't support deserialization yet
// so serialize the interpreted result then deserialize that
let file = String::from_utf8(file).expect("Config file contains invalid UTF-8");
let config = cornfig::parse(&file).expect("Invalid corn config").value;
serde_json::from_str(&serde_json::to_string(&config).unwrap()).unwrap()
}
_ => unreachable!(),
})
} else {
None
fn load_file(path: &Path) -> Result<Self> {
let file = fs::read(path).wrap_err("Failed to read config file")?;
let extension = path
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
match extension {
"json" => serde_json::from_slice(&file).wrap_err("Invalid JSON config"),
"toml" => toml::from_slice(&file).wrap_err("Invalid TOML config"),
"yaml" | "yml" => serde_yaml::from_slice(&file).wrap_err("Invalid YAML config"),
"corn" => {
// corn doesn't support deserialization yet
// so serialize the interpreted result then deserialize that
let file =
String::from_utf8(file).wrap_err("Config file contains invalid UTF-8")?;
let config = cornfig::parse(&file).wrap_err("Invalid corn config")?.value;
Ok(serde_json::from_str(&serde_json::to_string(&config)?)?)
}
_ => unreachable!(),
}
}
}

View File

@ -58,9 +58,7 @@ fn parse_desktop_file(path: PathBuf) -> io::Result<HashMap<String, String>> {
let mut map = HashMap::new();
for line in lines.flatten() {
let is_pair = line.contains('=');
if is_pair {
let (key, value) = line.split_once('=').unwrap();
if let Some((key, value)) = line.split_once('=') {
map.insert(key.to_string(), value.to_string());
}
}
@ -100,13 +98,18 @@ fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconL
let is_steam_game = app_id.starts_with("steam_app_");
if is_steam_game {
let steam_id: String = app_id.chars().skip("steam_app_".len()).collect();
let home_dir = dirs::data_dir().unwrap();
let path = home_dir.join(format!(
"icons/hicolor/32x32/apps/steam_icon_{}.png",
steam_id
));
return Some(IconLocation::File(path));
return match dirs::data_dir() {
Some(dir) => {
let path = dir.join(format!(
"icons/hicolor/32x32/apps/steam_icon_{}.png",
steam_id
));
return Some(IconLocation::File(path));
}
None => None,
};
}
let icon_name = get_desktop_icon_name(app_id);

53
src/logging.rs Normal file
View File

@ -0,0 +1,53 @@
use color_eyre::Result;
use dirs::data_dir;
use std::env;
use strip_ansi_escapes::Writer;
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_error::ErrorLayer;
use tracing_subscriber::fmt::{Layer, MakeWriter};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
struct MakeFileWriter {
file_writer: NonBlocking,
}
impl MakeFileWriter {
const fn new(file_writer: NonBlocking) -> Self {
Self { file_writer }
}
}
impl<'a> MakeWriter<'a> for MakeFileWriter {
type Writer = Writer<NonBlocking>;
fn make_writer(&'a self) -> Self::Writer {
Writer::new(self.file_writer.clone())
}
}
pub fn install_tracing() -> Result<WorkerGuard> {
let fmt_layer = fmt::layer().with_target(true);
let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;
let file_filter_layer =
EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("warn"))?;
let log_path = data_dir().unwrap_or(env::current_dir()?).join("ironbar");
let appender = tracing_appender::rolling::never(log_path, "error.log");
let (file_writer, guard) = tracing_appender::non_blocking(appender);
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(ErrorLayer::default())
.with(
Layer::default()
.with_writer(MakeFileWriter::new(file_writer))
.with_ansi(false)
.with_filter(file_filter_layer),
)
.init();
Ok(guard)
}

View File

@ -2,6 +2,7 @@ mod bar;
mod collection;
mod config;
mod icon;
mod logging;
mod modules;
mod popup;
mod style;
@ -10,74 +11,128 @@ mod sway;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};
use crate::style::load_css;
use crate::sway::SwayOutput;
use crate::sway::{get_client_error, SwayOutput};
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir;
use gtk::gdk::Display;
use gtk::prelude::*;
use gtk::{gdk, Application};
use gtk::Application;
use ksway::client::Client;
use ksway::IpcCommand;
use std::env;
use std::process::exit;
use crate::logging::install_tracing;
use tracing::{debug, error, info};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[tokio::main]
async fn main() {
async fn main() -> Result<()> {
// Disable backtraces by default
if env::var("RUST_LIB_BACKTRACE").is_err() {
env::set_var("RUST_LIB_BACKTRACE", "0");
}
// keep guard in scope
// otherwise file logging drops
let _guard = install_tracing()?;
color_eyre::install()?;
info!("Ironbar version {}", VERSION);
info!("Starting application");
let app = Application::builder()
.application_id("dev.jstanger.waylandbar")
.application_id("dev.jstanger.ironbar")
.build();
let mut sway_client = Client::connect().expect("Failed to connect to Sway IPC");
let outputs = sway_client
.ipc(IpcCommand::GetOutputs)
.expect("Failed to get Sway outputs");
let outputs = serde_json::from_slice::<Vec<SwayOutput>>(&outputs)
.expect("Failed to deserialize outputs message from Sway IPC");
app.connect_activate(move |app| {
let config = Config::load().unwrap_or_default();
let display = match Display::default() {
Some(display) => display,
None => {
let report = Report::msg("Failed to get default GTK display");
error!("{:?}", report);
exit(1)
}
};
// TODO: Better logging (https://crates.io/crates/tracing)
// TODO: error handling (https://crates.io/crates/color-eyre)
let config = match Config::load() {
Ok(config) => config,
Err(err) => {
error!("{:?}", err);
Config::default()
}
};
debug!("Loaded config file");
// TODO: Embedded Deno/lua - build custom modules via script???
let display = gdk::Display::default().expect("Failed to get default GDK display");
let num_monitors = display.n_monitors();
for i in 0..num_monitors {
let monitor = display.monitor(i).unwrap();
let monitor_name = &outputs
.get(i as usize)
.expect("GTK monitor output differs from Sway's")
.name;
config.monitors.as_ref().map_or_else(
|| {
create_bar(app, &monitor, monitor_name, config.clone());
},
|config| {
let config = config.get(monitor_name);
match &config {
Some(MonitorConfig::Single(config)) => {
create_bar(app, &monitor, monitor_name, config.clone());
}
Some(MonitorConfig::Multiple(configs)) => {
for config in configs {
create_bar(app, &monitor, monitor_name, config.clone());
}
}
_ => {}
}
},
)
if let Err(err) = create_bars(app, &display, &config) {
error!("{:?}", err);
exit(2);
}
let style_path = config_dir()
.expect("Failed to locate user config dir")
.join("ironbar")
.join("style.css");
debug!("Created bars");
let style_path = match config_dir() {
Some(dir) => dir.join("ironbar").join("style.css"),
None => {
let report = Report::msg("Failed to locate user config dir");
error!("{:?}", report);
exit(3);
}
};
if style_path.exists() {
load_css(style_path);
debug!("Loaded CSS watcher file");
}
});
app.run();
Ok(())
}
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> {
let mut sway_client = match Client::connect() {
Ok(client) => Ok(client),
Err(err) => Err(get_client_error(err)),
}?;
let outputs = match sway_client.ipc(IpcCommand::GetOutputs) {
Ok(outputs) => Ok(outputs),
Err(err) => Err(get_client_error(err)),
}?;
let outputs = serde_json::from_slice::<Vec<SwayOutput>>(&outputs)?;
let num_monitors = display.n_monitors();
for i in 0..num_monitors {
let monitor = display.monitor(i).ok_or_else(|| Report::msg("GTK and Sway are reporting a different number of outputs - this is a severe bug and should never happen"))?;
let monitor_name = &outputs.get(i as usize).ok_or_else(|| Report::msg("GTK and Sway are reporting a different set of outputs - this is a severe bug and should never happen"))?.name;
config.monitors.as_ref().map_or_else(
|| create_bar(app, &monitor, monitor_name, config.clone()),
|config| {
let config = config.get(monitor_name);
match &config {
Some(MonitorConfig::Single(config)) => {
create_bar(app, &monitor, monitor_name, config.clone())
}
Some(MonitorConfig::Multiple(configs)) => {
for config in configs {
create_bar(app, &monitor, monitor_name, config.clone())?;
}
Ok(())
}
_ => Ok(()),
}
},
)?;
}
Ok(())
}

View File

@ -3,6 +3,7 @@ mod popup;
use self::popup::Popup;
use crate::modules::{Module, ModuleInfo};
use chrono::Local;
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
use gtk::{Button, Orientation};
@ -26,7 +27,7 @@ fn default_format() -> String {
}
impl Module<Button> for ClockModule {
fn into_widget(self, info: &ModuleInfo) -> Button {
fn into_widget(self, info: &ModuleInfo) -> Result<Button> {
let button = Button::new();
let popup = Popup::new(
@ -51,7 +52,8 @@ impl Module<Button> for ClockModule {
let date = Local::now();
let date_string = format!("{}", date.format(format));
tx.send(date_string).unwrap();
tx.send(date_string).expect("Failed to send date string");
sleep(tokio::time::Duration::from_millis(500)).await;
}
});
@ -64,6 +66,6 @@ impl Module<Button> for ClockModule {
});
}
button
Ok(button)
}
}

View File

@ -24,7 +24,8 @@ impl Popup {
let date = Local::now();
let date_string = format!("{}", date.format(format));
tx.send(date_string).unwrap();
tx.send(date_string).expect("Failed to send date string");
sleep(tokio::time::Duration::from_millis(500)).await;
}
});

View File

@ -1,13 +1,14 @@
use crate::icon;
use crate::modules::{Module, ModuleInfo};
use crate::sway::node::get_open_windows;
use crate::sway::WindowEvent;
use crate::sway::{SwayClient, WindowEvent};
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
use gtk::{IconTheme, Image, Label, Orientation};
use ksway::{Client, IpcEvent};
use ksway::IpcEvent;
use serde::Deserialize;
use tokio::task::spawn_blocking;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct FocusedModule {
@ -26,7 +27,7 @@ const fn default_icon_size() -> i32 {
}
impl Module<gtk::Box> for FocusedModule {
fn into_widget(self, _info: &ModuleInfo) -> gtk::Box {
fn into_widget(self, _info: &ModuleInfo) -> Result<gtk::Box> {
let icon_theme = IconTheme::new();
if let Some(theme) = self.icon_theme {
@ -41,34 +42,42 @@ impl Module<gtk::Box> for FocusedModule {
container.add(&icon);
container.add(&label);
let mut sway = Client::connect().unwrap();
let mut sway = SwayClient::connect()?;
let srx = sway.subscribe(vec![IpcEvent::Window]).unwrap();
let srx = sway.subscribe(vec![IpcEvent::Window])?;
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let focused = get_open_windows(&mut sway)
let focused = sway
.get_open_windows()?
.into_iter()
.find(|node| node.focused);
if let Some(focused) = focused {
tx.send(focused).unwrap();
tx.send(focused)?;
}
spawn_blocking(move || loop {
while let Ok((_, payload)) = srx.try_recv() {
let payload: WindowEvent = serde_json::from_slice(&payload).unwrap();
match serde_json::from_slice::<WindowEvent>(&payload) {
Ok(payload) => {
let update = match payload.change.as_str() {
"focus" => true,
"title" => payload.container.focused,
_ => false,
};
let update = match payload.change.as_str() {
"focus" => true,
"title" => payload.container.focused,
_ => false,
};
if update {
tx.send(payload.container).unwrap();
if update {
tx.send(payload.container)
.expect("Failed to sendf focus update");
}
}
Err(err) => error!("{:?}", err),
}
}
sway.poll().unwrap();
if let Err(err) = sway.poll() {
error!("{:?}", err);
}
});
{
@ -89,6 +98,6 @@ impl Module<gtk::Box> for FocusedModule {
});
}
container
Ok(container)
}
}

View File

@ -3,6 +3,8 @@ use crate::icon::{find_desktop_file, get_icon};
use crate::modules::launcher::popup::Popup;
use crate::modules::launcher::FocusEvent;
use crate::sway::SwayNode;
use crate::Report;
use color_eyre::Help;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image};
use std::process::{Command, Stdio};
@ -10,6 +12,7 @@ use std::rc::Rc;
use std::sync::{Arc, Mutex, RwLock};
use tokio::spawn;
use tokio::sync::mpsc;
use tracing::error;
#[derive(Debug, Clone)]
pub struct LauncherItem {
@ -26,12 +29,42 @@ pub struct LauncherWindow {
pub name: Option<String>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum OpenState {
Closed,
Open,
Focused,
Urgent,
}
impl OpenState {
pub const fn from_node(node: &SwayNode) -> Self {
if node.focused {
Self::Urgent
} else if node.urgent {
Self::Focused
} else {
Self::Open
}
}
pub fn highest_of(a: &Self, b: &Self) -> Self {
if a == &Self::Urgent || b == &Self::Urgent {
Self::Urgent
} else if a == &Self::Focused || b == &Self::Focused {
Self::Focused
} else if a == &Self::Open || b == &Self::Open {
Self::Open
} else {
Self::Closed
}
}
}
#[derive(Debug, Clone)]
pub struct State {
pub is_xwayland: bool,
pub open: bool,
pub focused: bool,
pub urgent: bool,
pub open_state: OpenState,
}
#[derive(Debug, Clone)]
@ -49,9 +82,7 @@ impl LauncherItem {
button.style_context().add_class("item");
let state = State {
open: false,
focused: false,
urgent: false,
open_state: OpenState::Closed,
is_xwayland: false,
};
@ -80,9 +111,7 @@ impl LauncherItem {
));
let state = State {
open: true,
focused: node.focused,
urgent: node.urgent,
open_state: OpenState::from_node(node),
is_xwayland: node.is_xwayland(),
};
@ -101,10 +130,14 @@ impl LauncherItem {
fn configure_button(&self, config: &ButtonConfig) {
let button = &self.button;
let windows = self.windows.lock().unwrap();
let windows = self.windows.lock().expect("Failed to get lock on windows");
let name = if windows.len() == 1 {
windows.first().unwrap().name.as_ref()
windows
.first()
.expect("Failed to get first window")
.name
.as_ref()
} else {
Some(&self.app_id)
};
@ -129,21 +162,31 @@ impl LauncherItem {
let (focus_tx, mut focus_rx) = mpsc::channel(32);
button.connect_clicked(move |_| {
let state = state.read().unwrap();
if state.open {
focus_tx.try_send(()).unwrap();
let state = state.read().expect("Failed to get read lock on state");
if state.open_state == OpenState::Open {
focus_tx.try_send(()).expect("Failed to send focus event");
} else {
// attempt to find desktop file and launch
match find_desktop_file(&app_id) {
Some(file) => {
Command::new("gtk-launch")
.arg(file.file_name().unwrap())
if let Err(err) = Command::new("gtk-launch")
.arg(
file.file_name()
.expect("File segment missing from path to desktop file"),
)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
{
error!(
"{:?}",
Report::new(err)
.wrap_err("Failed to run gtk-launch command.")
.suggestion("Perhaps the desktop file is invalid?")
);
}
}
None => (),
None => error!("Could not find desktop file for {}", app_id),
}
}
});
@ -153,15 +196,15 @@ impl LauncherItem {
spawn(async move {
while focus_rx.recv().await == Some(()) {
let state = state.read().unwrap();
let state = state.read().expect("Failed to get read lock on state");
if state.is_xwayland {
tx_click
.try_send(FocusEvent::Class(app_id.clone()))
.unwrap();
.expect("Failed to send focus event");
} else {
tx_click
.try_send(FocusEvent::AppId(app_id.clone()))
.unwrap();
.expect("Failed to send focus event");
}
}
});
@ -172,7 +215,7 @@ impl LauncherItem {
let tx_hover = config.tx.clone();
button.connect_enter_notify_event(move |button, _| {
let windows = windows.lock().unwrap();
let windows = windows.lock().expect("Failed to get lock on windows");
if windows.len() > 1 {
popup.set_windows(windows.as_slice(), &tx_hover);
popup.show(button);
@ -196,7 +239,7 @@ impl LauncherItem {
let style = button.style_context();
style.add_class("launcher-item");
self.update_button_classes(&self.state.read().unwrap());
self.update_button_classes(&self.state.read().expect("Failed to get read lock on state"));
button.show_all();
}
@ -223,19 +266,19 @@ impl LauncherItem {
style.remove_class("favorite");
}
if state.open {
if state.open_state == OpenState::Open {
style.add_class("open");
} else {
style.remove_class("open");
}
if state.focused {
if state.open_state == OpenState::Focused {
style.add_class("focused");
} else {
style.remove_class("focused");
}
if state.urgent {
if state.open_state == OpenState::Urgent {
style.add_class("urgent");
} else {
style.remove_class("urgent");

View File

@ -2,19 +2,20 @@ mod item;
mod popup;
use crate::collection::Collection;
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow};
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow, OpenState};
use crate::modules::launcher::popup::Popup;
use crate::modules::{Module, ModuleInfo};
use crate::sway::node::get_open_windows;
use crate::sway::{SwayNode, WindowEvent};
use crate::sway::{SwayClient, SwayNode, WindowEvent};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{IconTheme, Orientation};
use ksway::{Client, IpcEvent};
use ksway::IpcEvent;
use serde::Deserialize;
use std::rc::Rc;
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::task::spawn_blocking;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct LauncherModule {
@ -72,15 +73,17 @@ impl Launcher {
let id = window.get_id().to_string();
if let Some(item) = self.items.get_mut(&id) {
let mut state = item.state.write().unwrap();
state.open = true;
state.focused = window.focused || state.focused;
state.urgent = window.urgent || state.urgent;
let mut state = item
.state
.write()
.expect("Failed to get write lock on state");
let new_open_state = OpenState::from_node(&window);
state.open_state = OpenState::highest_of(&state.open_state, &new_open_state);
state.is_xwayland = window.is_xwayland();
item.update_button_classes(&state);
let mut windows = item.windows.lock().unwrap();
let mut windows = item.windows.lock().expect("Failed to get lock on windows");
windows.insert(
window.id,
@ -107,13 +110,13 @@ impl Launcher {
let remove = if let Some(item) = item {
let windows = Rc::clone(&item.windows);
let mut windows = windows.lock().unwrap();
let mut windows = windows.lock().expect("Failed to get lock on windows");
windows.remove(&window.id);
if windows.is_empty() {
let mut state = item.state.write().unwrap();
state.open = false;
let mut state = item.state.write().expect("Failed to get lock on windows");
state.open_state = OpenState::Closed;
item.update_button_classes(&state);
if item.favorite {
@ -137,20 +140,30 @@ impl Launcher {
fn set_window_focused(&mut self, window: &SwayNode) {
let id = window.get_id().to_string();
let currently_focused = self
.items
.iter_mut()
.find(|item| item.state.read().unwrap().focused);
let currently_focused = self.items.iter_mut().find(|item| {
item.state
.read()
.expect("Failed to get read lock on state")
.open_state
== OpenState::Focused
});
if let Some(currently_focused) = currently_focused {
let mut state = currently_focused.state.write().unwrap();
state.focused = false;
let mut state = currently_focused
.state
.write()
.expect("Failed to get write lock on state");
state.open_state = OpenState::Open;
currently_focused.update_button_classes(&state);
}
let item = self.items.get_mut(&id);
if let Some(item) = item {
let mut state = item.state.write().unwrap();
state.focused = true;
let mut state = item
.state
.write()
.expect("Failed to get write lock on state");
state.open_state = OpenState::Focused;
item.update_button_classes(&state);
}
}
@ -160,11 +173,15 @@ impl Launcher {
let item = self.items.get_mut(&id);
if let (Some(item), Some(name)) = (item, window.name) {
let mut windows = item.windows.lock().unwrap();
let mut windows = item.windows.lock().expect("Failed to get lock on windows");
if windows.len() == 1 {
item.set_title(&name, &self.button_config);
} else if let Some(window) = windows.get_mut(&window.id) {
window.name = Some(name);
} else {
windows.get_mut(&window.id).unwrap().name = Some(name);
// This should never happen
// But makes more sense to wipe title than keep old one in case of error
item.set_title("", &self.button_config);
}
}
}
@ -174,22 +191,26 @@ impl Launcher {
let item = self.items.get_mut(&id);
if let Some(item) = item {
let mut state = item.state.write().unwrap();
state.urgent = window.urgent;
let mut state = item
.state
.write()
.expect("Failed to get write lock on state");
state.open_state =
OpenState::highest_of(&state.open_state, &OpenState::from_node(window));
item.update_button_classes(&state);
}
}
}
impl Module<gtk::Box> for LauncherModule {
fn into_widget(self, info: &ModuleInfo) -> gtk::Box {
fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
let icon_theme = IconTheme::new();
if let Some(theme) = self.icon_theme {
icon_theme.set_custom_theme(Some(&theme));
}
let mut sway = Client::connect().unwrap();
let mut sway = SwayClient::connect()?;
let popup = Popup::new(
"popup-launcher",
@ -216,22 +237,29 @@ impl Module<gtk::Box> for LauncherModule {
button_config,
);
let open_windows = get_open_windows(&mut sway);
let open_windows = sway.get_open_windows()?;
for window in open_windows {
launcher.add_window(window);
}
let srx = sway.subscribe(vec![IpcEvent::Window]).unwrap();
let srx = sway.subscribe(vec![IpcEvent::Window])?;
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn_blocking(move || loop {
while let Ok((_, payload)) = srx.try_recv() {
let payload: WindowEvent = serde_json::from_slice(&payload).unwrap();
tx.send(payload).unwrap();
match serde_json::from_slice::<WindowEvent>(&payload) {
Ok(payload) => {
tx.send(payload)
.expect("Failed to send window event payload");
}
Err(err) => error!("{:?}", err),
}
}
if let Err(err) = sway.poll() {
error!("{:?}", err);
}
sway.poll().unwrap();
});
{
@ -250,7 +278,7 @@ impl Module<gtk::Box> for LauncherModule {
}
spawn(async move {
let mut sway = Client::connect().unwrap();
let mut sway = SwayClient::connect()?;
while let Some(event) = ui_rx.recv().await {
let selector = match event {
FocusEvent::AppId(app_id) => format!("[app_id={}]", app_id),
@ -258,10 +286,12 @@ impl Module<gtk::Box> for LauncherModule {
FocusEvent::ConId(id) => format!("[con_id={}]", id),
};
sway.run(format!("{} focus", selector)).unwrap();
sway.run(format!("{} focus", selector))?;
}
Ok::<(), Report>(())
});
container
Ok(container)
}
}

View File

@ -25,7 +25,8 @@ impl Popup {
let window = self.window.clone();
let tx = tx.clone();
button.connect_clicked(move |_| {
tx.try_send(FocusEvent::ConId(con_id)).unwrap();
tx.try_send(FocusEvent::ConId(con_id))
.expect("Failed to send focus event");
window.hide();
});

View File

@ -14,13 +14,12 @@ pub mod tray;
pub mod workspaces;
use crate::config::BarPosition;
use color_eyre::Result;
/// Shamelessly stolen from here:
/// <https://github.com/zeroeightysix/rustbar/blob/master/src/modules/module.rs>
use glib::IsA;
use gtk::gdk::Monitor;
use gtk::{Application, Widget};
use serde::de::DeserializeOwned;
use serde_json::Value;
#[derive(Clone)]
pub enum ModuleLocation {
@ -43,12 +42,5 @@ where
{
/// Consumes the module config
/// and produces a GTK widget of type `W`
fn into_widget(self, info: &ModuleInfo) -> W;
fn from_value(v: &Value) -> Box<Self>
where
Self: DeserializeOwned,
{
serde_json::from_value(v.clone()).unwrap()
}
fn into_widget(self, info: &ModuleInfo) -> Result<W>;
}

View File

@ -2,57 +2,76 @@ use mpd_client::commands::responses::Status;
use mpd_client::raw::MpdProtocolError;
use mpd_client::{Client, Connection};
use std::path::PathBuf;
use std::time::Duration;
use tokio::net::{TcpStream, UnixStream};
use tokio::spawn;
use tokio::time::sleep;
fn is_unix_socket(host: &String) -> bool {
pub async fn wait_for_connection(
hosts: Vec<String>,
interval: Duration,
max_retries: Option<usize>,
) -> Option<Client> {
let mut retries = 0;
spawn(async move {
let max_retries = max_retries.unwrap_or(usize::MAX);
loop {
if retries == max_retries {
break None;
}
if let Some(conn) = try_get_mpd_conn(&hosts).await {
break Some(conn.0);
}
retries += 1;
sleep(interval).await;
}
})
.await
.expect("Error occurred while handling tasks")
}
/// Cycles through each MPD host and
/// returns the first one which connects,
/// or none if there are none
async fn try_get_mpd_conn(hosts: &[String]) -> Option<Connection> {
for host in hosts {
let connection = if is_unix_socket(host) {
connect_unix(host).await
} else {
connect_tcp(host).await
};
if let Ok(connection) = connection {
return Some(connection);
}
}
None
}
fn is_unix_socket(host: &str) -> bool {
PathBuf::from(host).is_file()
}
pub async fn get_connection(host: &String) -> Result<Connection, MpdProtocolError> {
if is_unix_socket(host) {
connect_unix(host).await
} else {
connect_tcp(host).await
}
}
async fn connect_unix(host: &String) -> Result<Connection, MpdProtocolError> {
let connection = UnixStream::connect(host)
.await
.unwrap_or_else(|_| panic!("Error connecting to unix socket: {}", host));
async fn connect_unix(host: &str) -> Result<Connection, MpdProtocolError> {
let connection = UnixStream::connect(host).await?;
Client::connect(connection).await
}
async fn connect_tcp(host: &String) -> Result<Connection, MpdProtocolError> {
let connection = TcpStream::connect(host)
.await
.unwrap_or_else(|_| panic!("Error connecting to unix socket: {}", host));
async fn connect_tcp(host: &str) -> Result<Connection, MpdProtocolError> {
let connection = TcpStream::connect(host).await?;
Client::connect(connection).await
}
// /// Gets MPD server status.
// /// Panics on error.
// pub async fn get_status(client: &Client) -> Status {
// client
// .command(commands::Status)
// .await
// .expect("Failed to get MPD server status")
// }
/// Gets the duration of the current song
pub fn get_duration(status: &Status) -> u64 {
status
.duration
.expect("Failed to get duration from MPD status")
.as_secs()
pub fn get_duration(status: &Status) -> Option<u64> {
status.duration.map(|duration| duration.as_secs())
}
/// Gets the elapsed time of the current song
pub fn get_elapsed(status: &Status) -> u64 {
status
.elapsed
.expect("Failed to get elapsed time from MPD status")
.as_secs()
pub fn get_elapsed(status: &Status) -> Option<u64> {
status.elapsed.map(|duration| duration.as_secs())
}

View File

@ -2,10 +2,11 @@ mod client;
mod popup;
use self::popup::Popup;
use crate::modules::mpd::client::{get_connection, get_duration, get_elapsed};
use crate::modules::mpd::client::{get_duration, get_elapsed, wait_for_connection};
use crate::modules::mpd::popup::{MpdPopup, PopupEvent};
use crate::modules::{Module, ModuleInfo};
use dirs::home_dir;
use color_eyre::Result;
use dirs::{audio_dir, home_dir};
use glib::Continue;
use gtk::prelude::*;
use gtk::{Button, Orientation};
@ -14,9 +15,11 @@ use mpd_client::{commands, Tag};
use regex::Regex;
use serde::Deserialize;
use std::path::PathBuf;
use std::time::Duration;
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::time::sleep;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct MpdModule {
@ -41,16 +44,18 @@ fn default_format() -> String {
String::from("{icon} {title} / {artist}")
}
#[allow(clippy::unnecessary_wraps)]
fn default_icon_play() -> Option<String> {
Some(String::from(""))
}
#[allow(clippy::unnecessary_wraps)]
fn default_icon_pause() -> Option<String> {
Some(String::from(""))
}
fn default_music_dir() -> PathBuf {
home_dir().unwrap().join("Music")
audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default())
}
/// Attempts to read the first value for a tag
@ -84,8 +89,8 @@ enum Event {
}
impl Module<Button> for MpdModule {
fn into_widget(self, info: &ModuleInfo) -> Button {
let re = Regex::new(r"\{([\w-]+)}").unwrap();
fn into_widget(self, info: &ModuleInfo) -> Result<Button> {
let re = Regex::new(r"\{([\w-]+)}")?;
let tokens = get_tokens(&re, self.format.as_str());
let button = Button::new();
@ -107,13 +112,17 @@ impl Module<Button> for MpdModule {
let music_dir = self.music_dir.clone();
button.connect_clicked(move |_| {
click_tx.send(Event::Open).unwrap();
click_tx
.send(Event::Open)
.expect("Failed to send popup open event");
});
let host = self.host.clone();
let host2 = self.host.clone();
spawn(async move {
let (client, _) = get_connection(&host).await.unwrap(); // TODO: Handle connecting properly
let client = wait_for_connection(vec![host], Duration::from_secs(1), None)
.await
.expect("Unexpected error when trying to connect to MPD server");
loop {
let current_song = client.command(commands::CurrentSong).await;
@ -125,32 +134,38 @@ impl Module<Button> for MpdModule {
.await;
tx.send(Event::Update(Box::new(Some((song.song, status, string)))))
.unwrap();
.expect("Failed to send update event");
} else {
tx.send(Event::Update(Box::new(None))).unwrap();
tx.send(Event::Update(Box::new(None)))
.expect("Failed to send update event");
}
sleep(tokio::time::Duration::from_secs(1)).await;
sleep(Duration::from_secs(1)).await;
}
});
spawn(async move {
let (client, _) = get_connection(&host2).await.unwrap(); // TODO: Handle connecting properly
let client = wait_for_connection(vec![host2], Duration::from_secs(1), None)
.await
.expect("Unexpected error when trying to connect to MPD server");
while let Some(event) = ui_rx.recv().await {
match event {
let res = match event {
PopupEvent::Previous => client.command(commands::Previous).await,
PopupEvent::Toggle => {
let status = client.command(commands::Status).await.unwrap();
match status.state {
PopupEvent::Toggle => match client.command(commands::Status).await {
Ok(status) => match status.state {
PlayState::Playing => client.command(commands::SetPause(true)).await,
PlayState::Paused => client.command(commands::SetPause(false)).await,
PlayState::Stopped => Ok(()),
}
}
},
Err(err) => Err(err),
},
PopupEvent::Next => client.command(commands::Next).await,
};
if let Err(err) = res {
error!("Failed to send command to MPD server: {:?}", err);
}
.unwrap();
}
});
@ -178,7 +193,7 @@ impl Module<Button> for MpdModule {
});
};
button
Ok(button)
}
}
@ -220,10 +235,11 @@ impl MpdModule {
"disc" => try_get_first_tag(song.tags.get(&Tag::Disc)),
"genre" => try_get_first_tag(song.tags.get(&Tag::Genre)),
"track" => try_get_first_tag(song.tags.get(&Tag::Track)),
"duration" => return format_time(get_duration(status)),
"elapsed" => return format_time(get_elapsed(status)),
_ => return token.to_string(),
"duration" => return get_duration(status).map(format_time).unwrap_or_default(),
"elapsed" => return get_elapsed(status).map(format_time).unwrap_or_default(),
_ => Some(token),
};
s.unwrap_or_default().to_string()
}
}
}

View File

@ -90,17 +90,23 @@ impl MpdPopup {
let tx_prev = tx.clone();
btn_prev.connect_clicked(move |_| {
tx_prev.try_send(PopupEvent::Previous).unwrap();
tx_prev
.try_send(PopupEvent::Previous)
.expect("Failed to send prev track message");
});
let tx_toggle = tx.clone();
btn_play_pause.connect_clicked(move |_| {
tx_toggle.try_send(PopupEvent::Toggle).unwrap();
tx_toggle
.try_send(PopupEvent::Toggle)
.expect("Failed to send play/pause track message");
});
let tx_next = tx;
btn_next.connect_clicked(move |_| {
tx_next.try_send(PopupEvent::Next).unwrap();
tx_next
.try_send(PopupEvent::Next)
.expect("Failed to send next track message");
});
Self {
@ -121,7 +127,12 @@ impl MpdPopup {
// only update art when album changes
if prev_album != curr_album {
let cover_path = path.join(song.file_path().parent().unwrap().join("cover.jpg"));
let cover_path = path.join(
song.file_path()
.parent()
.expect("Song path should not be root")
.join("cover.jpg"),
);
if let Ok(pixbuf) = Pixbuf::from_file_at_scale(cover_path, 128, 128, true) {
self.cover.set_from_pixbuf(Some(&pixbuf));

View File

@ -1,10 +1,12 @@
use crate::modules::{Module, ModuleInfo};
use color_eyre::{eyre::Report, eyre::Result, eyre::WrapErr, Section};
use gtk::prelude::*;
use gtk::Label;
use serde::Deserialize;
use std::process::Command;
use tokio::spawn;
use tokio::time::sleep;
use tracing::{error, instrument};
#[derive(Debug, Deserialize, Clone)]
pub struct ScriptModule {
@ -19,19 +21,15 @@ const fn default_interval() -> u64 {
}
impl Module<Label> for ScriptModule {
fn into_widget(self, _info: &ModuleInfo) -> Label {
fn into_widget(self, _info: &ModuleInfo) -> Result<Label> {
let label = Label::builder().use_markup(true).build();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
loop {
let output = Command::new("sh").arg("-c").arg(&self.path).output();
if let Ok(output) = output {
let stdout = String::from_utf8(output.stdout)
.map(|output| output.trim().to_string())
.expect("Script output not valid UTF-8");
tx.send(stdout).unwrap();
match self.run_script() {
Ok(stdout) => tx.send(stdout).expect("Failed to send stdout"),
Err(err) => error!("{:?}", err),
}
sleep(tokio::time::Duration::from_millis(self.interval)).await;
@ -46,6 +44,34 @@ impl Module<Label> for ScriptModule {
});
}
label
Ok(label)
}
}
impl ScriptModule {
#[instrument]
fn run_script(&self) -> Result<String> {
let output = Command::new("sh")
.arg("-c")
.arg(&self.path)
.output()
.wrap_err("Failed to get script output")?;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)
.map(|output| output.trim().to_string())
.wrap_err("Script stdout not valid UTF-8")?;
Ok(stdout)
} else {
let stderr = String::from_utf8(output.stderr)
.map(|output| output.trim().to_string())
.wrap_err("Script stderr not valid UTF-8")?;
Err(Report::msg(stderr)
.wrap_err("Script returned non-zero error code")
.suggestion("Check the path to your script")
.suggestion("Check the script for errors"))
}
}
}

View File

@ -1,4 +1,5 @@
use crate::modules::{Module, ModuleInfo};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::{Label, Orientation};
use regex::{Captures, Regex};
@ -14,8 +15,8 @@ pub struct SysInfoModule {
}
impl Module<gtk::Box> for SysInfoModule {
fn into_widget(self, _info: &ModuleInfo) -> gtk::Box {
let re = Regex::new(r"\{([\w-]+)}").unwrap();
fn into_widget(self, _info: &ModuleInfo) -> Result<gtk::Box> {
let re = Regex::new(r"\{([\w-]+)}")?;
let container = gtk::Box::new(Orientation::Horizontal, 10);
@ -46,7 +47,8 @@ impl Module<gtk::Box> for SysInfoModule {
format_info.insert("memory-percent", format!("{:0>2.0}", memory_percent));
format_info.insert("cpu-percent", format!("{:0>2.0}", cpu_percent));
tx.send(format_info).unwrap();
tx.send(format_info)
.expect("Failed to send system info map");
sleep(tokio::time::Duration::from_secs(1)).await;
}
@ -69,6 +71,6 @@ impl Module<gtk::Box> for SysInfoModule {
});
}
container
Ok(container)
}
}

View File

@ -1,4 +1,5 @@
use crate::modules::{Module, ModuleInfo};
use color_eyre::Result;
use futures_util::StreamExt;
use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme, Image, Menu, MenuBar, MenuItem, SeparatorMenuItem};
@ -26,10 +27,11 @@ fn get_icon(item: &StatusNotifierItem) -> Option<Image> {
item.icon_theme_path.as_ref().and_then(|path| {
let theme = IconTheme::new();
theme.append_search_path(&path);
let icon_name = item.icon_name.as_ref().unwrap();
let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
item.icon_name.as_ref().and_then(|icon_name| {
let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
})
})
}
@ -38,8 +40,8 @@ fn get_icon(item: &StatusNotifierItem) -> Option<Image> {
fn get_menu_items(
menu: &[MenuItemInfo],
tx: &mpsc::Sender<NotifierItemCommand>,
id: String,
path: String,
id: &str,
path: &str,
) -> Vec<MenuItem> {
menu.iter()
.map(|item_info| {
@ -53,7 +55,7 @@ fn get_menu_items(
if !item_info.submenu.is_empty() {
let menu = Menu::new();
get_menu_items(&item_info.submenu, &tx.clone(), id.clone(), path.clone())
get_menu_items(&item_info.submenu, &tx.clone(), id, path)
.iter()
.for_each(|item| menu.add(item));
@ -63,8 +65,8 @@ fn get_menu_items(
let item = builder.build();
let info = item_info.clone();
let id = id.clone();
let path = path.clone();
let id = id.to_string();
let path = path.to_string();
{
let tx = tx.clone();
@ -74,7 +76,7 @@ fn get_menu_items(
menu_path: path.clone(),
notifier_address: id.clone(),
})
.unwrap();
.expect("Failed to send menu item clicked event");
});
}
@ -88,7 +90,7 @@ fn get_menu_items(
}
impl Module<MenuBar> for TrayModule {
fn into_widget(self, _info: &ModuleInfo) -> MenuBar {
fn into_widget(self, _info: &ModuleInfo) -> Result<MenuBar> {
let container = MenuBar::new();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
@ -107,10 +109,11 @@ impl Module<MenuBar> for TrayModule {
menu,
} => {
tx.send(TrayUpdate::Update(id, Box::new(item), menu))
.unwrap();
.expect("Failed to send tray update event");
}
NotifierItemMessage::Remove { address: id } => {
tx.send(TrayUpdate::Remove(id)).unwrap();
tx.send(TrayUpdate::Remove(id))
.expect("Failed to send tray remove event");
}
}
}
@ -138,13 +141,11 @@ impl Module<MenuBar> for TrayModule {
menu_item
});
if let Some(menu_opts) = menu {
let menu_path = item.menu.as_ref().unwrap().to_string();
if let (Some(menu_opts), Some(menu_path)) = (menu, item.menu) {
let submenus = menu_opts.submenus;
if !submenus.is_empty() {
let menu = Menu::new();
get_menu_items(&submenus, &ui_tx.clone(), id.clone(), menu_path)
get_menu_items(&submenus, &ui_tx.clone(), &id, &menu_path)
.iter()
.for_each(|item| menu.add(item));
menu_item.set_submenu(Some(&menu));
@ -154,8 +155,9 @@ impl Module<MenuBar> for TrayModule {
widgets.insert(id, menu_item);
}
TrayUpdate::Remove(id) => {
let widget = widgets.get(&id).unwrap();
container.remove(widget);
if let Some(widget) = widgets.get(&id) {
container.remove(widget);
}
}
};
@ -163,6 +165,6 @@ impl Module<MenuBar> for TrayModule {
});
};
container
Ok(container)
}
}

View File

@ -1,14 +1,15 @@
use crate::modules::{Module, ModuleInfo};
use crate::sway::{Workspace, WorkspaceEvent};
use crate::sway::{SwayClient, Workspace, WorkspaceEvent};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, Orientation};
use ksway::client::Client;
use ksway::{IpcCommand, IpcEvent};
use serde::Deserialize;
use std::collections::HashMap;
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::task::spawn_blocking;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct WorkspacesModule {
@ -34,7 +35,10 @@ impl Workspace {
{
let tx = tx.clone();
let name = self.name.clone();
button.connect_clicked(move |_item| tx.try_send(name.clone()).unwrap());
button.connect_clicked(move |_item| {
tx.try_send(name.clone())
.expect("Failed to send workspace click event");
});
}
button
@ -42,14 +46,14 @@ impl Workspace {
}
impl Module<gtk::Box> for WorkspacesModule {
fn into_widget(self, info: &ModuleInfo) -> gtk::Box {
let mut sway = Client::connect().unwrap();
fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
let mut sway = SwayClient::connect()?;
let container = gtk::Box::new(Orientation::Horizontal, 0);
let workspaces = {
let raw = sway.ipc(IpcCommand::GetWorkspaces).unwrap();
let workspaces = serde_json::from_slice::<Vec<Workspace>>(&raw).unwrap();
let raw = sway.ipc(IpcCommand::GetWorkspaces)?;
let workspaces = serde_json::from_slice::<Vec<Workspace>>(&raw)?;
if self.all_monitors {
workspaces
@ -73,15 +77,20 @@ impl Module<gtk::Box> for WorkspacesModule {
button_map.insert(workspace.name, item);
}
let srx = sway.subscribe(vec![IpcEvent::Workspace]).unwrap();
let srx = sway.subscribe(vec![IpcEvent::Workspace])?;
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn_blocking(move || loop {
while let Ok((_, payload)) = srx.try_recv() {
let payload: WorkspaceEvent = serde_json::from_slice(&payload).unwrap();
tx.send(payload).unwrap();
match serde_json::from_slice::<WorkspaceEvent>(&payload) {
Ok(payload) => tx.send(payload).expect("Failed to send workspace event"),
Err(err) => error!("{:?}", err),
}
}
if let Err(err) = sway.poll() {
error!("{:?}", err);
}
sway.poll().unwrap();
});
{
@ -90,30 +99,32 @@ impl Module<gtk::Box> for WorkspacesModule {
rx.attach(None, move |event| {
match event.change.as_str() {
"focus" => {
let old = event.old.unwrap();
if let Some(old_button) = button_map.get(&old.name) {
old_button.style_context().remove_class("focused");
let old = event.old.and_then(|old| button_map.get(&old.name));
if let Some(old) = old {
old.style_context().remove_class("focused");
}
let new = event.current.unwrap();
if let Some(new_button) = button_map.get(&new.name) {
new_button.style_context().add_class("focused");
let new = event.current.and_then(|new| button_map.get(&new.name));
if let Some(new) = new {
new.style_context().add_class("focused");
}
}
"init" => {
let workspace = event.current.unwrap();
if self.all_monitors || workspace.output == output_name {
let item = workspace.as_button(&name_map, &ui_tx);
if let Some(workspace) = event.current {
if self.all_monitors || workspace.output == output_name {
let item = workspace.as_button(&name_map, &ui_tx);
item.show();
menubar.add(&item);
button_map.insert(workspace.name, item);
item.show();
menubar.add(&item);
button_map.insert(workspace.name, item);
}
}
}
"empty" => {
let current = event.current.unwrap();
if let Some(item) = button_map.get(&current.name) {
menubar.remove(item);
if let Some(workspace) = event.current {
if let Some(item) = button_map.get(&workspace.name) {
menubar.remove(item);
}
}
}
_ => {}
@ -124,12 +135,14 @@ impl Module<gtk::Box> for WorkspacesModule {
}
spawn(async move {
let mut sway = Client::connect().unwrap();
let mut sway = SwayClient::connect()?;
while let Some(name) = ui_rx.recv().await {
sway.run(format!("workspace {}", name)).unwrap();
sway.run(format!("workspace {}", name))?;
}
Ok::<(), Report>(())
});
container
Ok(container)
}
}

View File

@ -107,9 +107,10 @@ impl Popup {
let screen_width = self.monitor.workarea().width();
let popup_width = self.window.allocated_width();
let top_level = button.toplevel().expect("Failed to get top-level widget");
let (widget_x, _) = button
.translate_coordinates(&button.toplevel().unwrap(), 0, 0)
.unwrap();
.translate_coordinates(&top_level, 0, 0)
.unwrap_or((0, 0));
let widget_center = f64::from(widget_x) + f64::from(widget_width) / 2.0;

View File

@ -1,3 +1,4 @@
use color_eyre::{Help, Report};
use glib::Continue;
use gtk::prelude::CssProviderExt;
use gtk::{gdk, gio, CssProvider, StyleContext};
@ -6,40 +7,56 @@ use std::path::PathBuf;
use std::sync::mpsc;
use std::time::Duration;
use tokio::spawn;
use tracing::{error, info};
pub fn load_css(style_path: PathBuf) {
let provider = CssProvider::new();
provider
.load_from_file(&gio::File::for_path(&style_path))
.expect("Couldn't load custom style");
StyleContext::add_provider_for_screen(
&gdk::Screen::default().expect("Couldn't get default GDK screen"),
&provider,
800,
);
if let Err(err) = provider.load_from_file(&gio::File::for_path(&style_path)) {
error!("{:?}", Report::new(err)
.wrap_err("Failed to load CSS")
.suggestion("Check the CSS file for errors")
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
);
}
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
StyleContext::add_provider_for_screen(&screen, &provider, 800);
let (watcher_tx, watcher_rx) = mpsc::channel::<DebouncedEvent>();
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
spawn(async move {
let mut watcher = notify::watcher(watcher_tx, Duration::from_millis(500)).unwrap();
watcher
.watch(&style_path, RecursiveMode::NonRecursive)
.unwrap();
match notify::watcher(watcher_tx, Duration::from_millis(500)) {
Ok(mut watcher) => {
watcher
.watch(&style_path, RecursiveMode::NonRecursive)
.expect("Unexpected error when attempting to watch CSS");
loop {
if let Ok(DebouncedEvent::Write(path)) = watcher_rx.recv() {
tx.send(path).unwrap();
loop {
if let Ok(DebouncedEvent::Write(path)) = watcher_rx.recv() {
tx.send(path).expect("Failed to send style changed message");
}
}
}
Err(err) => error!(
"{:?}",
Report::new(err).wrap_err("Failed to start CSS watcher")
),
}
});
{
rx.attach(None, move |path| {
println!("Reloading CSS");
provider
.load_from_file(&gio::File::for_path(path))
.expect("Couldn't load custom style");
info!("Reloading CSS");
if let Err(err) = provider
.load_from_file(&gio::File::for_path(path)) {
error!("{:?}", Report::new(err)
.wrap_err("Failed to load CSS")
.suggestion("Check the CSS file for errors")
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
);
}
Continue(true)
});

View File

@ -1,3 +1,5 @@
use color_eyre::{Report, Result};
use ksway::{Error, IpcCommand, IpcEvent};
use serde::Deserialize;
pub mod node;
@ -40,10 +42,68 @@ pub struct SwayNode {
#[derive(Debug, Deserialize)]
pub struct WindowProperties {
pub class: String,
pub class: Option<String>,
}
#[derive(Deserialize)]
pub struct SwayOutput {
pub name: String,
}
pub struct SwayClient {
client: ksway::Client,
}
impl SwayClient {
pub(crate) fn run(&mut self, cmd: String) -> Result<Vec<u8>> {
match self.client.run(cmd) {
Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)),
}
}
}
impl SwayClient {
pub fn connect() -> Result<Self> {
let client = match ksway::Client::connect() {
Ok(client) => Ok(client),
Err(err) => Err(get_client_error(err)),
}?;
Ok(Self { client })
}
pub fn ipc(&mut self, command: IpcCommand) -> Result<Vec<u8>> {
match self.client.ipc(command) {
Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)),
}
}
pub fn subscribe(
&mut self,
event_types: Vec<IpcEvent>,
) -> Result<crossbeam_channel::Receiver<(IpcEvent, Vec<u8>)>> {
match self.client.subscribe(event_types) {
Ok(res) => Ok(res),
Err(err) => Err(get_client_error(err)),
}
}
pub fn poll(&mut self) -> Result<()> {
match self.client.poll() {
Ok(()) => Ok(()),
Err(err) => Err(get_client_error(err)),
}
}
}
/// Gets an error report from a `ksway` error enum variant
pub fn get_client_error(error: Error) -> Report {
match error {
Error::SockPathNotFound => Report::msg("Sway socket path not found"),
Error::SubscriptionError => Report::msg("Sway IPC subscription error"),
Error::AlreadySubscribed => Report::msg("Already subscribed to Sway IPC server"),
Error::Io(err) => Report::new(err),
}
}

View File

@ -1,15 +1,17 @@
use crate::sway::SwayNode;
use ksway::{Client, IpcCommand};
use crate::sway::{SwayClient, SwayNode};
use color_eyre::Result;
use ksway::IpcCommand;
impl SwayNode {
pub fn get_id(&self) -> &str {
self.app_id.as_ref().map_or_else(
|| {
&self
.window_properties
self.window_properties
.as_ref()
.expect("cannot find node name")
.expect("Cannot find node window properties")
.class
.as_ref()
.expect("Cannot find node name")
},
|app_id| app_id,
)
@ -34,12 +36,14 @@ fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
}
}
pub fn get_open_windows(sway: &mut Client) -> Vec<SwayNode> {
let raw = sway.ipc(IpcCommand::GetTree).unwrap();
let root_node = serde_json::from_slice::<SwayNode>(&raw).unwrap();
impl SwayClient {
pub fn get_open_windows(&mut self) -> Result<Vec<SwayNode>> {
let root_node = self.ipc(IpcCommand::GetTree)?;
let root_node = serde_json::from_slice(&root_node)?;
let mut window_nodes = vec![];
check_node(root_node, &mut window_nodes);
let mut window_nodes = vec![];
check_node(root_node, &mut window_nodes);
window_nodes
Ok(window_nodes)
}
}