1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-01 02:31:04 +02:00

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,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)
}
}