Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6871126bd8 | ||
|
|
481adfcaa4 | ||
|
|
64650fbf3a | ||
|
|
a35d25520c | ||
|
|
78e30b39fe | ||
|
|
b81927e3a5 | ||
|
|
5d319e91f2 | ||
|
|
015dcd3204 | ||
|
|
1e38719996 | ||
|
|
6dcae66570 | ||
|
|
649b0efb19 | ||
|
|
023c2fb118 | ||
|
|
ea57f5e18d | ||
|
|
53142d1bea | ||
|
|
7e0f2cad1c |
3
.github/workflows/deploy.yml
vendored
3
.github/workflows/deploy.yml
vendored
@@ -17,6 +17,9 @@ jobs:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install build deps
|
||||
run: sudo apt install libgtk-3-dev libgtk-layer-shell-dev
|
||||
|
||||
- name: Update CHANGELOG
|
||||
id: changelog
|
||||
uses: Requarks/changelog-action@v1
|
||||
|
||||
17
.idea/runConfigurations/Format.xml
generated
Normal file
17
.idea/runConfigurations/Format.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Format" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="command" value="fmt" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="true" />
|
||||
<option name="allFeatures" value="false" />
|
||||
<option name="emulateTerminal" value="false" />
|
||||
<option name="withSudo" value="false" />
|
||||
<option name="buildTarget" value="REMOTE" />
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<envs />
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
1
.idea/runConfigurations/Run.xml
generated
1
.idea/runConfigurations/Run.xml
generated
@@ -12,6 +12,7 @@
|
||||
<envs>
|
||||
<env name="IRONBAR_CONFIG" value="examples/config.json" />
|
||||
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
|
||||
<env name="RUST_LOG" value="trace" />
|
||||
</envs>
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run (Debug)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<configuration default="false" name="Run (GTK Debug)" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="command" value="run --package ironbar --bin ironbar" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
1
.idea/runConfigurations/Run__Live_Config_.xml
generated
1
.idea/runConfigurations/Run__Live_Config_.xml
generated
@@ -11,6 +11,7 @@
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<envs>
|
||||
<env name="PATH" value="/usr/local/bin:/usr/bin:$USER_HOME$/.local/share/npm/bin" />
|
||||
<env name="RUST_LOG" value="trace" />
|
||||
</envs>
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
|
||||
31
CHANGELOG.md
Normal file
31
CHANGELOG.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [v0.5.0] - 2022-08-25
|
||||
### :sparkles: New Features
|
||||
- [`1e38719`](https://github.com/JakeStanger/ironbar/commit/1e387199962b81caeb40ffbd99a956f24abdf4e3) - introduce logging in some areas *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`023c2fb`](https://github.com/JakeStanger/ironbar/commit/023c2fb118f46f3592f1dfe1a6704014c062ab3f) - **workspaces**: not listening to move event *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
- [`6dcae66`](https://github.com/JakeStanger/ironbar/commit/6dcae66570cf5434e077ec823cded33771b4239c) - avoid creating loads of sway/mpd clients *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :wrench: Chores
|
||||
- [`015dcd3`](https://github.com/JakeStanger/ironbar/commit/015dcd3204dfa6a1ebcef1b4f3b345ed733fee2f) - **release**: v0.5.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
## [v0.4.0] - 2022-08-22
|
||||
### :sparkles: New Features
|
||||
- [`ab8f7ec`](https://github.com/JakeStanger/ironbar/commit/ab8f7ecfc8fa4b96fce78518af75794641950140) - logging support and proper error handling *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :bug: Bug Fixes
|
||||
- [`f2ee2df`](https://github.com/JakeStanger/ironbar/commit/f2ee2dfe7a0f5575d0c3ec09644ca990b088cd85) - error when using with `swaybar_command` *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
### :wrench: Chores
|
||||
- [`1d7c377`](https://github.com/JakeStanger/ironbar/commit/1d7c3772e4b97c7198043cb55fe9c71695a211ab) - **release**: v0.4.0 *(commit by [@JakeStanger](https://github.com/JakeStanger))*
|
||||
|
||||
|
||||
[v0.4.0]: https://github.com/JakeStanger/ironbar/compare/v0.3.0...v0.4.0
|
||||
[v0.5.0]: https://github.com/JakeStanger/ironbar/compare/v0.4.0...v0.5.0
|
||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -1127,7 +1127,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ironbar"
|
||||
version = "0.4.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"color-eyre",
|
||||
@@ -1139,6 +1139,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"gtk-layer-shell",
|
||||
"ksway",
|
||||
"lazy_static",
|
||||
"mpd_client",
|
||||
"notify",
|
||||
"regex",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ironbar"
|
||||
version = "0.4.0"
|
||||
version = "0.5.1"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Customisable wlroots/sway bar"
|
||||
@@ -25,6 +25,7 @@ serde_json = "1.0.82"
|
||||
serde_yaml = "0.9.4"
|
||||
toml = "0.5.9"
|
||||
cornfig = "0.2.0"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.6.0"
|
||||
stray = "0.1.1"
|
||||
dirs = "4.0.0"
|
||||
|
||||
10
src/bar.rs
10
src/bar.rs
@@ -5,7 +5,10 @@ use color_eyre::Result;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow, Orientation};
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Creates a new window for a bar,
|
||||
/// sets it up and adds its widgets.
|
||||
pub fn create_bar(
|
||||
app: &Application,
|
||||
monitor: &Monitor,
|
||||
@@ -41,15 +44,18 @@ pub fn create_bar(
|
||||
win.add(&content);
|
||||
|
||||
win.connect_destroy_event(|_, _| {
|
||||
info!("Shutting down");
|
||||
gtk::main_quit();
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
debug!("Showing bar");
|
||||
win.show_all();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads the configured modules onto a bar.
|
||||
fn load_modules(
|
||||
left: >k::Box,
|
||||
center: >k::Box,
|
||||
@@ -98,12 +104,15 @@ fn load_modules(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds modules into a provided GTK box,
|
||||
/// which should be one of its left, center or right containers.
|
||||
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)?;
|
||||
widget.set_widget_name($name);
|
||||
content.add(&widget);
|
||||
debug!("Added module of type {}", $name);
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -123,6 +132,7 @@ fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets up GTK layer shell for a provided aplication window.
|
||||
fn setup_layer_shell(win: &ApplicationWindow, monitor: &Monitor, position: &BarPosition) {
|
||||
gtk_layer_shell::init_for_window(win);
|
||||
gtk_layer_shell::set_monitor(win, monitor);
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct Collection<TKey, TData> {
|
||||
}
|
||||
|
||||
impl<TKey: PartialEq, TData> Collection<TKey, TData> {
|
||||
/// Creates a new empty collection.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
keys: vec![],
|
||||
@@ -18,6 +19,7 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a new key/value pair at the end of the collection.
|
||||
pub fn insert(&mut self, key: TKey, value: TData) {
|
||||
self.keys.push(key);
|
||||
self.values.push(value);
|
||||
@@ -25,6 +27,8 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
|
||||
assert_eq!(self.keys.len(), self.values.len());
|
||||
}
|
||||
|
||||
/// Gets a reference of the value for the specified key
|
||||
/// if it exists in the collection.
|
||||
pub fn get(&self, key: &TKey) -> Option<&TData> {
|
||||
let index = self.keys.iter().position(|k| k == key);
|
||||
match index {
|
||||
@@ -33,6 +37,8 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a mutable reference for the value with the specified key
|
||||
/// if it exists in the collection.
|
||||
pub fn get_mut(&mut self, key: &TKey) -> Option<&mut TData> {
|
||||
let index = self.keys.iter().position(|k| k == key);
|
||||
match index {
|
||||
@@ -41,6 +47,9 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the key/value from the collection
|
||||
/// if it exists
|
||||
/// and returns the removed value.
|
||||
pub fn remove(&mut self, key: &TKey) -> Option<TData> {
|
||||
assert_eq!(self.keys.len(), self.values.len());
|
||||
|
||||
@@ -53,26 +62,32 @@ impl<TKey: PartialEq, TData> Collection<TKey, TData> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the length of the collection.
|
||||
pub fn len(&self) -> usize {
|
||||
self.keys.len()
|
||||
}
|
||||
|
||||
/// Gets a reference to the first value in the collection.
|
||||
pub fn first(&self) -> Option<&TData> {
|
||||
self.values.first()
|
||||
}
|
||||
|
||||
/// Gets the values as a slice.
|
||||
pub fn as_slice(&self) -> &[TData] {
|
||||
self.values.as_slice()
|
||||
}
|
||||
|
||||
/// Checks whether the collection is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.keys.is_empty()
|
||||
}
|
||||
|
||||
/// Gets an iterator for the collection.
|
||||
pub fn iter(&self) -> Iter<'_, TData> {
|
||||
self.values.iter()
|
||||
}
|
||||
|
||||
/// Gets a mutable iterator for the collection
|
||||
pub fn iter_mut(&mut self) -> IterMut<'_, TData> {
|
||||
self.values.iter_mut()
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ const fn default_bar_height() -> i32 {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Attempts to load the config file from file,
|
||||
/// parse it and return a new instance of `Self`.
|
||||
pub fn load() -> Result<Self> {
|
||||
let config_path = if let Ok(config_path) = env::var("IRONBAR_CONFIG") {
|
||||
let path = PathBuf::from(config_path);
|
||||
@@ -87,6 +89,10 @@ impl Config {
|
||||
Self::load_file(&config_path)
|
||||
}
|
||||
|
||||
/// Attempts to discover the location of the config file
|
||||
/// by checking each valid format's extension.
|
||||
///
|
||||
/// Returns the path of the first valid match, if any.
|
||||
fn try_find_config() -> Result<PathBuf> {
|
||||
let config_dir = config_dir().wrap_err("Failed to locate user config dir")?;
|
||||
|
||||
@@ -110,6 +116,8 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the config file at the specified path
|
||||
/// and parses it into `Self` based on its extension.
|
||||
fn load_file(path: &Path) -> Result<Self> {
|
||||
let file = fs::read(path).wrap_err("Failed to read config file")?;
|
||||
let extension = path
|
||||
|
||||
@@ -86,6 +86,10 @@ enum IconLocation {
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
/// Attempts to get the location of an icon.
|
||||
///
|
||||
/// Handles icons that are part of a GTK theme, icons specified as path
|
||||
/// and icons for steam games.
|
||||
fn get_icon_location(theme: &IconTheme, app_id: &str, size: i32) -> Option<IconLocation> {
|
||||
let has_icon = theme
|
||||
.lookup_icon(app_id, size, IconLookupFlags::empty())
|
||||
|
||||
@@ -26,6 +26,10 @@ impl<'a> MakeWriter<'a> for MakeFileWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs tracing into the current application.
|
||||
///
|
||||
/// The returned `WorkerGuard` must remain in scope
|
||||
/// for the lifetime of the application for logging to file to work.
|
||||
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"))?;
|
||||
|
||||
25
src/main.rs
25
src/main.rs
@@ -11,14 +11,13 @@ mod sway;
|
||||
use crate::bar::create_bar;
|
||||
use crate::config::{Config, MonitorConfig};
|
||||
use crate::style::load_css;
|
||||
use crate::sway::{get_client_error, SwayOutput};
|
||||
use crate::sway::{get_client, SwayOutput};
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::Report;
|
||||
use dirs::config_dir;
|
||||
use gtk::gdk::Display;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use ksway::client::Client;
|
||||
use ksway::IpcCommand;
|
||||
use std::env;
|
||||
use std::process::exit;
|
||||
@@ -96,25 +95,33 @@ async fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates each of the bars across each of the (configured) outputs.
|
||||
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 = {
|
||||
let sway = get_client();
|
||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
||||
|
||||
let outputs = match sway_client.ipc(IpcCommand::GetOutputs) {
|
||||
Ok(outputs) => Ok(outputs),
|
||||
Err(err) => Err(get_client_error(err)),
|
||||
let outputs = sway.ipc(IpcCommand::GetOutputs);
|
||||
|
||||
match outputs {
|
||||
Ok(outputs) => Ok(outputs),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}?;
|
||||
|
||||
let outputs = serde_json::from_slice::<Vec<SwayOutput>>(&outputs)?;
|
||||
|
||||
debug!("Received {} outputs from Sway IPC", outputs.len());
|
||||
|
||||
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;
|
||||
|
||||
info!("Creating bar on '{}'", monitor_name);
|
||||
|
||||
// TODO: Could we use an Arc<Config> here to avoid cloning?
|
||||
config.monitors.as_ref().map_or_else(
|
||||
|| create_bar(app, &monitor, monitor_name, config.clone()),
|
||||
|config| {
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
use crate::icon;
|
||||
use crate::modules::{Module, ModuleInfo};
|
||||
use crate::sway::{SwayClient, WindowEvent};
|
||||
use crate::sway::get_client;
|
||||
use color_eyre::Result;
|
||||
use glib::Continue;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconTheme, Image, Label, Orientation};
|
||||
use ksway::IpcEvent;
|
||||
use serde::Deserialize;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct FocusedModule {
|
||||
/// Whether to show icon on the bar.
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_icon: bool,
|
||||
/// Whether to show app name on the bar.
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_title: bool,
|
||||
|
||||
/// Icon size in pixels.
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
/// GTK icon theme to use.
|
||||
icon_theme: Option<String>,
|
||||
}
|
||||
|
||||
@@ -42,42 +44,39 @@ impl Module<gtk::Box> for FocusedModule {
|
||||
container.add(&icon);
|
||||
container.add(&label);
|
||||
|
||||
let mut sway = SwayClient::connect()?;
|
||||
|
||||
let srx = sway.subscribe(vec![IpcEvent::Window])?;
|
||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
let focused = sway
|
||||
.get_open_windows()?
|
||||
.into_iter()
|
||||
.find(|node| node.focused);
|
||||
let focused = {
|
||||
let sway = get_client();
|
||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
||||
sway.get_open_windows()?
|
||||
.into_iter()
|
||||
.find(|node| node.focused)
|
||||
};
|
||||
|
||||
if let Some(focused) = focused {
|
||||
tx.send(focused)?;
|
||||
}
|
||||
|
||||
spawn_blocking(move || loop {
|
||||
while let Ok((_, payload)) = srx.try_recv() {
|
||||
match serde_json::from_slice::<WindowEvent>(&payload) {
|
||||
Ok(payload) => {
|
||||
let update = match payload.change.as_str() {
|
||||
"focus" => true,
|
||||
"title" => payload.container.focused,
|
||||
_ => false,
|
||||
};
|
||||
spawn_blocking(move || {
|
||||
let srx = {
|
||||
let sway = get_client();
|
||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
||||
sway.subscribe_window()
|
||||
};
|
||||
|
||||
if update {
|
||||
tx.send(payload.container)
|
||||
.expect("Failed to sendf focus update");
|
||||
}
|
||||
}
|
||||
Err(err) => error!("{:?}", err),
|
||||
while let Ok(payload) = srx.recv() {
|
||||
let update = match payload.change.as_str() {
|
||||
"focus" => true,
|
||||
"title" => payload.container.focused,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if update {
|
||||
tx.send(payload.container)
|
||||
.expect("Failed to sendf focus update");
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = sway.poll() {
|
||||
error!("{:?}", err);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::collection::Collection;
|
||||
use crate::icon::{find_desktop_file, get_icon};
|
||||
use crate::modules::launcher::open_state::OpenState;
|
||||
use crate::modules::launcher::popup::Popup;
|
||||
use crate::modules::launcher::FocusEvent;
|
||||
use crate::sway::SwayNode;
|
||||
@@ -9,7 +10,7 @@ use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::error;
|
||||
@@ -18,7 +19,7 @@ use tracing::error;
|
||||
pub struct LauncherItem {
|
||||
pub app_id: String,
|
||||
pub favorite: bool,
|
||||
pub windows: Rc<Mutex<Collection<i32, LauncherWindow>>>,
|
||||
pub windows: Rc<RwLock<Collection<i32, LauncherWindow>>>,
|
||||
pub state: Arc<RwLock<State>>,
|
||||
pub button: Button,
|
||||
}
|
||||
@@ -27,38 +28,7 @@ pub struct LauncherItem {
|
||||
pub struct LauncherWindow {
|
||||
pub con_id: i32,
|
||||
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
|
||||
}
|
||||
}
|
||||
pub open_state: OpenState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -89,7 +59,7 @@ impl LauncherItem {
|
||||
let item = Self {
|
||||
app_id,
|
||||
favorite,
|
||||
windows: Rc::new(Mutex::new(Collection::new())),
|
||||
windows: Rc::new(RwLock::new(Collection::new())),
|
||||
state: Arc::new(RwLock::new(state)),
|
||||
button,
|
||||
};
|
||||
@@ -107,6 +77,7 @@ impl LauncherItem {
|
||||
LauncherWindow {
|
||||
con_id: node.id,
|
||||
name: node.name.clone(),
|
||||
open_state: OpenState::from_node(node),
|
||||
},
|
||||
));
|
||||
|
||||
@@ -118,7 +89,7 @@ impl LauncherItem {
|
||||
let item = Self {
|
||||
app_id: node.get_id().to_string(),
|
||||
favorite: false,
|
||||
windows: Rc::new(Mutex::new(windows)),
|
||||
windows: Rc::new(RwLock::new(windows)),
|
||||
state: Arc::new(RwLock::new(state)),
|
||||
button,
|
||||
};
|
||||
@@ -130,7 +101,10 @@ impl LauncherItem {
|
||||
fn configure_button(&self, config: &ButtonConfig) {
|
||||
let button = &self.button;
|
||||
|
||||
let windows = self.windows.lock().expect("Failed to get lock on windows");
|
||||
let windows = self
|
||||
.windows
|
||||
.read()
|
||||
.expect("Failed to get read lock on windows");
|
||||
|
||||
let name = if windows.len() == 1 {
|
||||
windows
|
||||
@@ -163,7 +137,7 @@ impl LauncherItem {
|
||||
|
||||
button.connect_clicked(move |_| {
|
||||
let state = state.read().expect("Failed to get read lock on state");
|
||||
if state.open_state == OpenState::Open {
|
||||
if state.open_state.is_open() {
|
||||
focus_tx.try_send(()).expect("Failed to send focus event");
|
||||
} else {
|
||||
// attempt to find desktop file and launch
|
||||
@@ -215,7 +189,7 @@ impl LauncherItem {
|
||||
let tx_hover = config.tx.clone();
|
||||
|
||||
button.connect_enter_notify_event(move |button, _| {
|
||||
let windows = windows.lock().expect("Failed to get lock on windows");
|
||||
let windows = windows.read().expect("Failed to get read lock on windows");
|
||||
if windows.len() > 1 {
|
||||
popup.set_windows(windows.as_slice(), &tx_hover);
|
||||
popup.show(button);
|
||||
@@ -266,22 +240,53 @@ impl LauncherItem {
|
||||
style.remove_class("favorite");
|
||||
}
|
||||
|
||||
if state.open_state == OpenState::Open {
|
||||
if state.open_state.is_open() {
|
||||
style.add_class("open");
|
||||
} else {
|
||||
style.remove_class("open");
|
||||
}
|
||||
|
||||
if state.open_state == OpenState::Focused {
|
||||
if state.open_state.is_focused() {
|
||||
style.add_class("focused");
|
||||
} else {
|
||||
style.remove_class("focused");
|
||||
}
|
||||
|
||||
if state.open_state == OpenState::Urgent {
|
||||
if state.open_state.is_urgent() {
|
||||
style.add_class("urgent");
|
||||
} else {
|
||||
style.remove_class("urgent");
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the open state for a specific window on the item
|
||||
/// and updates the item state based on all its windows.
|
||||
pub fn set_window_open_state(&self, window_id: i32, new_state: OpenState, state: &mut State) {
|
||||
let mut windows = self
|
||||
.windows
|
||||
.write()
|
||||
.expect("Failed to get write lock on windows");
|
||||
|
||||
let window = windows.iter_mut().find(|w| w.con_id == window_id);
|
||||
if let Some(window) = window {
|
||||
window.open_state = new_state;
|
||||
|
||||
state.open_state =
|
||||
OpenState::merge_states(windows.iter().map(|w| &w.open_state).collect());
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the open state on the item and all its windows.
|
||||
/// This overrides the existing open states.
|
||||
pub fn set_open_state(&self, new_state: OpenState, state: &mut State) {
|
||||
state.open_state = new_state;
|
||||
let mut windows = self
|
||||
.windows
|
||||
.write()
|
||||
.expect("Failed to get write lock on windows");
|
||||
|
||||
windows
|
||||
.iter_mut()
|
||||
.for_each(|window| window.open_state = new_state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
mod item;
|
||||
mod open_state;
|
||||
mod popup;
|
||||
|
||||
use crate::collection::Collection;
|
||||
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow, OpenState};
|
||||
use crate::modules::launcher::item::{ButtonConfig, LauncherItem, LauncherWindow};
|
||||
use crate::modules::launcher::open_state::OpenState;
|
||||
use crate::modules::launcher::popup::Popup;
|
||||
use crate::modules::{Module, ModuleInfo};
|
||||
use crate::sway::{SwayClient, SwayNode, WindowEvent};
|
||||
use crate::sway::{get_client, SwayNode};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconTheme, Orientation};
|
||||
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;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct LauncherModule {
|
||||
/// List of app IDs (or classes) to always show regardles of open state,
|
||||
/// in the order specified.
|
||||
favorites: Option<Vec<String>>,
|
||||
/// Whether to show application names on the bar.
|
||||
#[serde(default = "crate::config::default_false")]
|
||||
show_names: bool,
|
||||
/// Whether to show application icons on the bar.
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_icons: bool,
|
||||
|
||||
/// Name of the GTK icon theme to use.
|
||||
icon_theme: Option<String>,
|
||||
}
|
||||
|
||||
@@ -69,31 +75,37 @@ impl Launcher {
|
||||
/// Adds a new window to the launcher.
|
||||
/// This gets added to an existing group
|
||||
/// if an instance of the program is already open.
|
||||
fn add_window(&mut self, window: SwayNode) {
|
||||
let id = window.get_id().to_string();
|
||||
fn add_window(&mut self, node: SwayNode) {
|
||||
let id = node.get_id().to_string();
|
||||
|
||||
debug!("Adding window with ID {}", id);
|
||||
|
||||
if let Some(item) = self.items.get_mut(&id) {
|
||||
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();
|
||||
let new_open_state = OpenState::from_node(&node);
|
||||
state.open_state = OpenState::merge_states(vec![&state.open_state, &new_open_state]);
|
||||
state.is_xwayland = node.is_xwayland();
|
||||
|
||||
item.update_button_classes(&state);
|
||||
|
||||
let mut windows = item.windows.lock().expect("Failed to get lock on windows");
|
||||
let mut windows = item
|
||||
.windows
|
||||
.write()
|
||||
.expect("Failed to get write lock on windows");
|
||||
|
||||
windows.insert(
|
||||
window.id,
|
||||
node.id,
|
||||
LauncherWindow {
|
||||
con_id: window.id,
|
||||
name: window.name,
|
||||
con_id: node.id,
|
||||
name: node.name,
|
||||
open_state: new_open_state,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let item = LauncherItem::from_node(&window, &self.button_config);
|
||||
let item = LauncherItem::from_node(&node, &self.button_config);
|
||||
|
||||
self.container.add(&item.button);
|
||||
self.items.insert(id, item);
|
||||
@@ -106,11 +118,15 @@ impl Launcher {
|
||||
fn remove_window(&mut self, window: &SwayNode) {
|
||||
let id = window.get_id().to_string();
|
||||
|
||||
debug!("Removing window with ID {}", id);
|
||||
|
||||
let item = self.items.get_mut(&id);
|
||||
|
||||
let remove = if let Some(item) = item {
|
||||
let windows = Rc::clone(&item.windows);
|
||||
let mut windows = windows.lock().expect("Failed to get lock on windows");
|
||||
let mut windows = windows
|
||||
.write()
|
||||
.expect("Failed to get write lock on windows");
|
||||
|
||||
windows.remove(&window.id);
|
||||
|
||||
@@ -137,24 +153,33 @@ impl Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_window_focused(&mut self, window: &SwayNode) {
|
||||
let id = window.get_id().to_string();
|
||||
/// Unfocuses the currently focused window
|
||||
/// and focuses the newly focused one.
|
||||
fn set_window_focused(&mut self, node: &SwayNode) {
|
||||
let id = node.get_id().to_string();
|
||||
|
||||
let currently_focused = self.items.iter_mut().find(|item| {
|
||||
debug!("Setting window with ID {} focused", id);
|
||||
|
||||
let prev_focused = self.items.iter_mut().find(|item| {
|
||||
item.state
|
||||
.read()
|
||||
.expect("Failed to get read lock on state")
|
||||
.open_state
|
||||
== OpenState::Focused
|
||||
.is_focused()
|
||||
});
|
||||
|
||||
if let Some(currently_focused) = currently_focused {
|
||||
let mut state = currently_focused
|
||||
if let Some(prev_focused) = prev_focused {
|
||||
let mut state = prev_focused
|
||||
.state
|
||||
.write()
|
||||
.expect("Failed to get write lock on state");
|
||||
state.open_state = OpenState::Open;
|
||||
currently_focused.update_button_classes(&state);
|
||||
|
||||
// if a window from the same item took focus,
|
||||
// we don't need to unfocus the item.
|
||||
if prev_focused.app_id != id {
|
||||
prev_focused.set_open_state(OpenState::open(), &mut state);
|
||||
prev_focused.update_button_classes(&state);
|
||||
}
|
||||
}
|
||||
|
||||
let item = self.items.get_mut(&id);
|
||||
@@ -163,17 +188,23 @@ impl Launcher {
|
||||
.state
|
||||
.write()
|
||||
.expect("Failed to get write lock on state");
|
||||
state.open_state = OpenState::Focused;
|
||||
item.set_window_open_state(node.id, OpenState::focused(), &mut state);
|
||||
item.update_button_classes(&state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the window title for the given node.
|
||||
fn set_window_title(&mut self, window: SwayNode) {
|
||||
let id = window.get_id().to_string();
|
||||
let item = self.items.get_mut(&id);
|
||||
|
||||
debug!("Updating title for window with ID {}", id);
|
||||
|
||||
if let (Some(item), Some(name)) = (item, window.name) {
|
||||
let mut windows = item.windows.lock().expect("Failed to get lock on windows");
|
||||
let mut windows = item
|
||||
.windows
|
||||
.write()
|
||||
.expect("Failed to get write lock on windows");
|
||||
if windows.len() == 1 {
|
||||
item.set_title(&name, &self.button_config);
|
||||
} else if let Some(window) = windows.get_mut(&window.id) {
|
||||
@@ -186,17 +217,23 @@ impl Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_window_urgent(&mut self, window: &SwayNode) {
|
||||
let id = window.get_id().to_string();
|
||||
/// Updates the window urgency based on the given node.
|
||||
fn set_window_urgent(&mut self, node: &SwayNode) {
|
||||
let id = node.get_id().to_string();
|
||||
let item = self.items.get_mut(&id);
|
||||
|
||||
debug!(
|
||||
"Setting urgency to {} for window with ID {}",
|
||||
node.urgent, id
|
||||
);
|
||||
|
||||
if let Some(item) = item {
|
||||
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.set_window_open_state(node.id, OpenState::urgent(node.urgent), &mut state);
|
||||
item.update_button_classes(&state);
|
||||
}
|
||||
}
|
||||
@@ -210,8 +247,6 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
icon_theme.set_custom_theme(Some(&theme));
|
||||
}
|
||||
|
||||
let mut sway = SwayClient::connect()?;
|
||||
|
||||
let popup = Popup::new(
|
||||
"popup-launcher",
|
||||
info.app,
|
||||
@@ -237,28 +272,28 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
button_config,
|
||||
);
|
||||
|
||||
let open_windows = sway.get_open_windows()?;
|
||||
let open_windows = {
|
||||
let sway = get_client();
|
||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
||||
sway.get_open_windows()
|
||||
}?;
|
||||
|
||||
for window in open_windows {
|
||||
launcher.add_window(window);
|
||||
}
|
||||
|
||||
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() {
|
||||
match serde_json::from_slice::<WindowEvent>(&payload) {
|
||||
Ok(payload) => {
|
||||
tx.send(payload)
|
||||
.expect("Failed to send window event payload");
|
||||
}
|
||||
Err(err) => error!("{:?}", err),
|
||||
}
|
||||
}
|
||||
spawn_blocking(move || {
|
||||
let srx = {
|
||||
let sway = get_client();
|
||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
||||
sway.subscribe_window()
|
||||
};
|
||||
|
||||
if let Err(err) = sway.poll() {
|
||||
error!("{:?}", err);
|
||||
while let Ok(payload) = srx.recv() {
|
||||
tx.send(payload)
|
||||
.expect("Failed to send window event payload");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -278,14 +313,15 @@ impl Module<gtk::Box> for LauncherModule {
|
||||
}
|
||||
|
||||
spawn(async move {
|
||||
let mut sway = SwayClient::connect()?;
|
||||
let sway = get_client();
|
||||
|
||||
while let Some(event) = ui_rx.recv().await {
|
||||
let selector = match event {
|
||||
FocusEvent::AppId(app_id) => format!("[app_id={}]", app_id),
|
||||
FocusEvent::Class(class) => format!("[class={}]", class),
|
||||
FocusEvent::ConId(id) => format!("[con_id={}]", id),
|
||||
};
|
||||
|
||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
||||
sway.run(format!("{} focus", selector))?;
|
||||
}
|
||||
|
||||
|
||||
74
src/modules/launcher/open_state.rs
Normal file
74
src/modules/launcher/open_state.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::sway::SwayNode;
|
||||
|
||||
/// Open state for a launcher item, or item window.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
|
||||
pub enum OpenState {
|
||||
Closed,
|
||||
Open { focused: bool, urgent: bool },
|
||||
}
|
||||
|
||||
impl OpenState {
|
||||
/// Creates from `SwayNode`
|
||||
pub const fn from_node(node: &SwayNode) -> Self {
|
||||
Self::Open {
|
||||
focused: node.focused,
|
||||
urgent: node.urgent,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates open without focused/urgent
|
||||
pub const fn open() -> Self {
|
||||
Self::Open {
|
||||
focused: false,
|
||||
urgent: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates open with focused
|
||||
pub const fn focused() -> Self {
|
||||
Self::Open {
|
||||
focused: true,
|
||||
urgent: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates open with urgent
|
||||
pub const fn urgent(urgent: bool) -> Self {
|
||||
Self::Open {
|
||||
focused: false,
|
||||
urgent,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if open
|
||||
pub fn is_open(self) -> bool {
|
||||
self != Self::Closed
|
||||
}
|
||||
|
||||
/// Checks if open with focus
|
||||
pub const fn is_focused(self) -> bool {
|
||||
matches!(self, Self::Open { focused: true, .. })
|
||||
}
|
||||
|
||||
/// check if open with urgent
|
||||
pub const fn is_urgent(self) -> bool {
|
||||
matches!(self, Self::Open { urgent: true, .. })
|
||||
}
|
||||
|
||||
/// Merges states together to produce a single state.
|
||||
/// This is effectively an OR operation,
|
||||
/// so sets state to open and flags to true if any state is open
|
||||
/// or any instance of the flag is true.
|
||||
pub fn merge_states(states: Vec<&Self>) -> Self {
|
||||
states.iter().fold(Self::Closed, |merged, current| {
|
||||
if merged.is_open() || current.is_open() {
|
||||
Self::Open {
|
||||
focused: merged.is_focused() || current.is_focused(),
|
||||
urgent: merged.is_urgent() || current.is_urgent(),
|
||||
}
|
||||
} else {
|
||||
Self::Closed
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,67 @@
|
||||
use lazy_static::lazy_static;
|
||||
use mpd_client::commands::responses::Status;
|
||||
use mpd_client::raw::MpdProtocolError;
|
||||
use mpd_client::{Client, Connection};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::{TcpStream, UnixStream};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub async fn wait_for_connection(
|
||||
hosts: Vec<String>,
|
||||
lazy_static! {
|
||||
static ref CLIENTS: Arc<Mutex<HashMap<String, Arc<Client>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub async fn get_connection(host: &str) -> Option<Arc<Client>> {
|
||||
let mut clients = CLIENTS.lock().await;
|
||||
|
||||
match clients.get(host) {
|
||||
Some(client) => Some(Arc::clone(client)),
|
||||
None => {
|
||||
let client = wait_for_connection(host, Duration::from_secs(5), None).await?;
|
||||
let client = Arc::new(client);
|
||||
clients.insert(host.to_string(), Arc::clone(&client));
|
||||
Some(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_connection(
|
||||
host: &str,
|
||||
interval: Duration,
|
||||
max_retries: Option<usize>,
|
||||
) -> Option<Client> {
|
||||
let mut retries = 0;
|
||||
let max_retries = max_retries.unwrap_or(usize::MAX);
|
||||
|
||||
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;
|
||||
loop {
|
||||
if retries == max_retries {
|
||||
break None;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("Error occurred while handling tasks")
|
||||
|
||||
if let Some(conn) = try_get_mpd_conn(host).await {
|
||||
break Some(conn.0);
|
||||
}
|
||||
|
||||
retries += 1;
|
||||
sleep(interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
};
|
||||
async fn try_get_mpd_conn(host: &str) -> Option<Connection> {
|
||||
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
|
||||
connection.ok()
|
||||
}
|
||||
|
||||
fn is_unix_socket(host: &str) -> bool {
|
||||
|
||||
@@ -2,7 +2,7 @@ mod client;
|
||||
mod popup;
|
||||
|
||||
use self::popup::Popup;
|
||||
use crate::modules::mpd::client::{get_duration, get_elapsed, wait_for_connection};
|
||||
use crate::modules::mpd::client::{get_connection, get_duration, get_elapsed};
|
||||
use crate::modules::mpd::popup::{MpdPopup, PopupEvent};
|
||||
use crate::modules::{Module, ModuleInfo};
|
||||
use color_eyre::Result;
|
||||
@@ -23,15 +23,20 @@ use tracing::error;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct MpdModule {
|
||||
/// TCP or Unix socket address.
|
||||
#[serde(default = "default_socket")]
|
||||
host: String,
|
||||
/// Format of current song info to display on the bar.
|
||||
#[serde(default = "default_format")]
|
||||
format: String,
|
||||
/// Icon to display when playing.
|
||||
#[serde(default = "default_icon_play")]
|
||||
icon_play: Option<String>,
|
||||
/// Icon to display when paused.
|
||||
#[serde(default = "default_icon_pause")]
|
||||
icon_pause: Option<String>,
|
||||
|
||||
/// Path to root of music directory.
|
||||
#[serde(default = "default_music_dir")]
|
||||
music_dir: PathBuf,
|
||||
}
|
||||
@@ -120,7 +125,7 @@ impl Module<Button> for MpdModule {
|
||||
let host = self.host.clone();
|
||||
let host2 = self.host.clone();
|
||||
spawn(async move {
|
||||
let client = wait_for_connection(vec![host], Duration::from_secs(1), None)
|
||||
let client = get_connection(&host)
|
||||
.await
|
||||
.expect("Unexpected error when trying to connect to MPD server");
|
||||
|
||||
@@ -145,7 +150,7 @@ impl Module<Button> for MpdModule {
|
||||
});
|
||||
|
||||
spawn(async move {
|
||||
let client = wait_for_connection(vec![host2], Duration::from_secs(1), None)
|
||||
let client = get_connection(&host2)
|
||||
.await
|
||||
.expect("Unexpected error when trying to connect to MPD server");
|
||||
|
||||
@@ -242,4 +247,4 @@ impl MpdModule {
|
||||
};
|
||||
s.unwrap_or_default().to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ use tracing::{error, instrument};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ScriptModule {
|
||||
/// Path to script to execute.
|
||||
path: String,
|
||||
/// Time in milliseconds between executions.
|
||||
#[serde(default = "default_interval")]
|
||||
interval: u64,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use tokio::time::sleep;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct SysInfoModule {
|
||||
/// List of formatting strings.
|
||||
format: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
use crate::modules::{Module, ModuleInfo};
|
||||
use crate::sway::{SwayClient, Workspace, WorkspaceEvent};
|
||||
use crate::sway::{get_client, Workspace};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, Orientation};
|
||||
use ksway::{IpcCommand, IpcEvent};
|
||||
use ksway::IpcCommand;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::error;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct WorkspacesModule {
|
||||
/// Map of actual workspace names to custom names.
|
||||
name_map: Option<HashMap<String, String>>,
|
||||
|
||||
/// Whether to display icons for all monitors.
|
||||
#[serde(default = "crate::config::default_false")]
|
||||
all_monitors: bool,
|
||||
}
|
||||
@@ -47,17 +49,19 @@ impl Workspace {
|
||||
|
||||
impl Module<gtk::Box> for WorkspacesModule {
|
||||
fn into_widget(self, info: &ModuleInfo) -> Result<gtk::Box> {
|
||||
let mut sway = SwayClient::connect()?;
|
||||
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
let workspaces = {
|
||||
trace!("Getting current workspaces");
|
||||
let sway = get_client();
|
||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
||||
let raw = sway.ipc(IpcCommand::GetWorkspaces)?;
|
||||
let workspaces = serde_json::from_slice::<Vec<Workspace>>(&raw)?;
|
||||
|
||||
if self.all_monitors {
|
||||
workspaces
|
||||
} else {
|
||||
trace!("Filtering workspaces to current monitor only");
|
||||
workspaces
|
||||
.into_iter()
|
||||
.filter(|workspace| workspace.output == info.output_name)
|
||||
@@ -71,32 +75,35 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
|
||||
let (ui_tx, mut ui_rx) = mpsc::channel(32);
|
||||
|
||||
trace!("Creating workspace buttons");
|
||||
for workspace in workspaces {
|
||||
let item = workspace.as_button(&name_map, &ui_tx);
|
||||
container.add(&item);
|
||||
button_map.insert(workspace.name, item);
|
||||
}
|
||||
|
||||
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() {
|
||||
match serde_json::from_slice::<WorkspaceEvent>(&payload) {
|
||||
Ok(payload) => tx.send(payload).expect("Failed to send workspace event"),
|
||||
Err(err) => error!("{:?}", err),
|
||||
}
|
||||
}
|
||||
spawn_blocking(move || {
|
||||
trace!("Starting workspace event listener task");
|
||||
let srx = {
|
||||
let sway = get_client();
|
||||
let mut sway = sway.lock().expect("Failed to get lock on Sway IPC client");
|
||||
|
||||
if let Err(err) = sway.poll() {
|
||||
error!("{:?}", err);
|
||||
sway.subscribe_workspace()
|
||||
};
|
||||
|
||||
while let Ok(payload) = srx.recv() {
|
||||
tx.send(payload).expect("Failed to send workspace event");
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
trace!("Setting up sway event handler");
|
||||
let menubar = container.clone();
|
||||
let output_name = info.output_name.to_string();
|
||||
rx.attach(None, move |event| {
|
||||
debug!("Received workspace event {:?}", event);
|
||||
match event.change.as_str() {
|
||||
"focus" => {
|
||||
let old = event.old.and_then(|old| button_map.get(&old.name));
|
||||
@@ -108,6 +115,8 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
if let Some(new) = new {
|
||||
new.style_context().add_class("focused");
|
||||
}
|
||||
|
||||
trace!("{:?} {:?}", old, new);
|
||||
}
|
||||
"init" => {
|
||||
if let Some(workspace) = event.current {
|
||||
@@ -120,6 +129,21 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
}
|
||||
}
|
||||
}
|
||||
"move" => {
|
||||
if let Some(workspace) = event.current {
|
||||
if !self.all_monitors {
|
||||
if workspace.output == output_name {
|
||||
let item = workspace.as_button(&name_map, &ui_tx);
|
||||
|
||||
item.show();
|
||||
menubar.add(&item);
|
||||
button_map.insert(workspace.name, item);
|
||||
} else if let Some(item) = button_map.get(&workspace.name) {
|
||||
menubar.remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"empty" => {
|
||||
if let Some(workspace) = event.current {
|
||||
if let Some(item) = button_map.get(&workspace.name) {
|
||||
@@ -135,8 +159,12 @@ impl Module<gtk::Box> for WorkspacesModule {
|
||||
}
|
||||
|
||||
spawn(async move {
|
||||
let mut sway = SwayClient::connect()?;
|
||||
trace!("Setting up UI event handler");
|
||||
let sway = get_client();
|
||||
while let Some(name) = ui_rx.recv().await {
|
||||
let mut sway = sway
|
||||
.lock()
|
||||
.expect("Failed to get write lock on Sway IPC client");
|
||||
sway.run(format!("workspace {}", name))?;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ pub struct Popup {
|
||||
}
|
||||
|
||||
impl Popup {
|
||||
/// Creates a new popup window.
|
||||
/// This includes setting up gtk-layer-shell
|
||||
/// and an empty `gtk::Box` container.
|
||||
pub fn new(
|
||||
name: &str,
|
||||
app: &Application,
|
||||
|
||||
@@ -9,6 +9,11 @@ use std::time::Duration;
|
||||
use tokio::spawn;
|
||||
use tracing::{error, info};
|
||||
|
||||
/// Attempts to load CSS file at the given path
|
||||
/// and attach if to the current GTK application.
|
||||
///
|
||||
/// Installs a file watcher and reloads CSS when
|
||||
/// write changes are detected on the file.
|
||||
pub fn load_css(style_path: PathBuf) {
|
||||
let provider = CssProvider::new();
|
||||
|
||||
|
||||
167
src/sway/mod.rs
167
src/sway/mod.rs
@@ -1,17 +1,22 @@
|
||||
use color_eyre::{Report, Result};
|
||||
use crossbeam_channel::Receiver;
|
||||
use ksway::{Error, IpcCommand, IpcEvent};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::spawn;
|
||||
use tracing::{debug, info, trace};
|
||||
|
||||
pub mod node;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct WorkspaceEvent {
|
||||
pub change: String,
|
||||
pub old: Option<Workspace>,
|
||||
pub current: Option<Workspace>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Workspace {
|
||||
pub name: String,
|
||||
pub focused: bool,
|
||||
@@ -19,13 +24,13 @@ pub struct Workspace {
|
||||
pub output: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct WindowEvent {
|
||||
pub change: String,
|
||||
pub container: SwayNode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct SwayNode {
|
||||
#[serde(rename = "type")]
|
||||
pub node_type: String,
|
||||
@@ -40,7 +45,7 @@ pub struct SwayNode {
|
||||
pub window_properties: Option<WindowProperties>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct WindowProperties {
|
||||
pub class: Option<String>,
|
||||
}
|
||||
@@ -50,51 +55,115 @@ pub struct SwayOutput {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
type Broadcaster<T> = Arc<Mutex<UnboundedBroadcast<T>>>;
|
||||
|
||||
pub struct SwayClient {
|
||||
client: ksway::Client,
|
||||
|
||||
workspace_bc: Broadcaster<WorkspaceEvent>,
|
||||
window_bc: Broadcaster<WindowEvent>,
|
||||
}
|
||||
|
||||
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> {
|
||||
fn connect() -> Result<Self> {
|
||||
let client = match ksway::Client::connect() {
|
||||
Ok(client) => Ok(client),
|
||||
Err(err) => Err(get_client_error(err)),
|
||||
}?;
|
||||
info!("Sway IPC client connected");
|
||||
|
||||
Ok(Self { client })
|
||||
let workspace_bc = Arc::new(Mutex::new(UnboundedBroadcast::new()));
|
||||
let window_bc = Arc::new(Mutex::new(UnboundedBroadcast::new()));
|
||||
|
||||
let workspace_bc2 = workspace_bc.clone();
|
||||
let window_bc2 = window_bc.clone();
|
||||
spawn(async move {
|
||||
let mut sub_client = match ksway::Client::connect() {
|
||||
Ok(client) => Ok(client),
|
||||
Err(err) => Err(get_client_error(err)),
|
||||
}
|
||||
.expect("Failed to connect to Sway IPC server");
|
||||
info!("Sway IPC subscription client connected");
|
||||
|
||||
let event_types = vec![IpcEvent::Window, IpcEvent::Workspace];
|
||||
let rx = match sub_client.subscribe(event_types) {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(get_client_error(err)),
|
||||
}
|
||||
.expect("Failed to subscribe to Sway IPC server");
|
||||
|
||||
loop {
|
||||
while let Ok((ev_type, payload)) = rx.try_recv() {
|
||||
debug!("Received sway event {:?}", ev_type);
|
||||
match ev_type {
|
||||
IpcEvent::Workspace => {
|
||||
let json = serde_json::from_slice::<WorkspaceEvent>(&payload).expect(
|
||||
"Received invalid workspace event payload from Sway IPC server",
|
||||
);
|
||||
workspace_bc
|
||||
.lock()
|
||||
.expect("Failed to get lock on workspace event bus")
|
||||
.send(json)
|
||||
.expect("Failed to broadcast workspace event");
|
||||
}
|
||||
IpcEvent::Window => {
|
||||
let json = serde_json::from_slice::<WindowEvent>(&payload).expect(
|
||||
"Received invalid window event payload from Sway IPC server",
|
||||
);
|
||||
window_bc
|
||||
.lock()
|
||||
.expect("Failed to get lock on window event bus")
|
||||
.send(json)
|
||||
.expect("Failed to broadcast window event");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
match sub_client.poll() {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) => Err(get_client_error(err)),
|
||||
}
|
||||
.expect("Failed to poll Sway IPC client");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
workspace_bc: workspace_bc2,
|
||||
window_bc: window_bc2,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ipc(&mut self, command: IpcCommand) -> Result<Vec<u8>> {
|
||||
debug!("Sending command: {:?}", command);
|
||||
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) {
|
||||
pub(crate) fn run(&mut self, cmd: String) -> Result<Vec<u8>> {
|
||||
debug!("Sending command: {}", cmd);
|
||||
match self.client.run(cmd) {
|
||||
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)),
|
||||
}
|
||||
pub fn subscribe_workspace(&mut self) -> Receiver<WorkspaceEvent> {
|
||||
trace!("Adding new workspace subscriber");
|
||||
self.workspace_bc
|
||||
.lock()
|
||||
.expect("Failed to get lock on workspace event bus")
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
pub fn subscribe_window(&mut self) -> Receiver<WindowEvent> {
|
||||
trace!("Adding new window subscriber");
|
||||
self.window_bc
|
||||
.lock()
|
||||
.expect("Failed to get lock on window event bus")
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,3 +176,49 @@ pub fn get_client_error(error: Error) -> Report {
|
||||
Error::Io(err) => Report::new(err),
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: Arc<Mutex<SwayClient>> = {
|
||||
let client = SwayClient::connect();
|
||||
match client {
|
||||
Ok(client) => Arc::new(Mutex::new(client)),
|
||||
Err(err) => panic!("{:?}", err),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_client() -> Arc<Mutex<SwayClient>> {
|
||||
Arc::clone(&CLIENT)
|
||||
}
|
||||
|
||||
/// Crossbeam channel wrapper
|
||||
/// which sends messages to all receivers.
|
||||
pub struct UnboundedBroadcast<T> {
|
||||
channels: Vec<crossbeam_channel::Sender<T>>,
|
||||
}
|
||||
|
||||
impl<T: 'static + Clone + Send + Sync> UnboundedBroadcast<T> {
|
||||
/// Creates a new broadcaster.
|
||||
pub const fn new() -> Self {
|
||||
Self { channels: vec![] }
|
||||
}
|
||||
|
||||
/// Creates a new sender/receiver pair.
|
||||
/// The sender is stored locally and the receiver is returned.
|
||||
pub fn subscribe(&mut self) -> Receiver<T> {
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
|
||||
self.channels.push(tx);
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
/// Attempts to send a messsge to all receivers.
|
||||
pub fn send(&self, message: T) -> Result<(), crossbeam_channel::SendError<T>> {
|
||||
for c in &self.channels {
|
||||
c.send(message.clone())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ use color_eyre::Result;
|
||||
use ksway::IpcCommand;
|
||||
|
||||
impl SwayNode {
|
||||
/// Gets either the `app_id` or `class`
|
||||
/// depending on whether this is a native Wayland
|
||||
/// or xwayland application.
|
||||
pub fn get_id(&self) -> &str {
|
||||
self.app_id.as_ref().map_or_else(
|
||||
|| {
|
||||
@@ -17,11 +20,15 @@ impl SwayNode {
|
||||
)
|
||||
}
|
||||
|
||||
/// Checks whether this application
|
||||
/// is running under xwayland.
|
||||
pub fn is_xwayland(&self) -> bool {
|
||||
self.shell == Some(String::from("xwayland"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively checks the provided node for any child application nodes.
|
||||
/// Returns a list of any found application nodes.
|
||||
fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
|
||||
if node.name.is_some() && (node.node_type == "con" || node.node_type == "floating_con") {
|
||||
window_nodes.push(node);
|
||||
@@ -37,6 +44,7 @@ fn check_node(node: SwayNode, window_nodes: &mut Vec<SwayNode>) {
|
||||
}
|
||||
|
||||
impl SwayClient {
|
||||
/// Gets a flat vector of all currently open windows.
|
||||
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)?;
|
||||
|
||||
Reference in New Issue
Block a user