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:
parent
917838c98c
commit
ab8f7ecfc8
28 changed files with 1056 additions and 388 deletions
28
src/bar.rs
28
src/bar.rs
|
@ -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(¢er));
|
||||
content.pack_end(&right, false, false, 0);
|
||||
|
||||
load_modules(&left, ¢er, &right, app, config, monitor, monitor_name);
|
||||
load_modules(&left, ¢er, &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: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) {
|
||||
fn add_modules(content: >k::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: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
|
|||
ModuleConfig::Focused(module) => add_module!(module, "focused"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) {
|
||||
|
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
21
src/icon.rs
21
src/icon.rs
|
@ -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
53
src/logging.rs
Normal 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)
|
||||
}
|
153
src/main.rs
153
src/main.rs
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¤t.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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
55
src/style.rs
55
src/style.rs
|
@ -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)
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue