7 Commits

Author SHA1 Message Date
Jake Stanger
6871126bd8 chore(release): v0.5.1 2022-09-06 22:38:16 +01:00
Jake Stanger
481adfcaa4 chore(intellij): update run configs 2022-09-06 22:37:55 +01:00
Jake Stanger
64650fbf3a Merge pull request #14 from JakeStanger/fix/launcher-state
Fix launcher state issues
2022-09-06 21:56:21 +01:00
Jake Stanger
a35d25520c fix(launcher): item state changes not handled correctly
This completely rewrites the item open state handling code (again) in a more logical way that should prevent incorrect states, and removes some locking issues.
2022-09-06 21:46:02 +01:00
Jake Stanger
78e30b39fe docs: add some rustdoc comments throughout 2022-08-28 16:57:41 +01:00
Jake Stanger
b81927e3a5 fix(launcher): opening new instances when focused/urgent 2022-08-25 22:08:08 +01:00
JakeStanger
5d319e91f2 docs: update CHANGELOG.md for v0.5.0 [skip ci] 2022-08-25 20:55:43 +00:00
25 changed files with 301 additions and 74 deletions

17
.idea/runConfigurations/Format.xml generated Normal file
View 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>

View File

@@ -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="" />

View File

@@ -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" />

View File

@@ -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="" />

View File

@@ -4,6 +4,18 @@ 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))*
@@ -15,4 +27,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [`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.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

2
Cargo.lock generated
View File

@@ -1127,7 +1127,7 @@ dependencies = [
[[package]]
name = "ironbar"
version = "0.5.0"
version = "0.5.1"
dependencies = [
"chrono",
"color-eyre",

View File

@@ -1,6 +1,6 @@
[package]
name = "ironbar"
version = "0.5.0"
version = "0.5.1"
edition = "2021"
license = "MIT"
description = "Customisable wlroots/sway bar"

View File

@@ -7,6 +7,8 @@ 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,
@@ -53,6 +55,7 @@ pub fn create_bar(
Ok(())
}
/// Loads the configured modules onto a bar.
fn load_modules(
left: &gtk::Box,
center: &gtk::Box,
@@ -101,6 +104,8 @@ 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: &gtk::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo) -> Result<()> {
macro_rules! add_module {
($module:expr, $name:literal) => {{
@@ -127,6 +132,7 @@ fn add_modules(content: &gtk::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);

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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())

View File

@@ -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"))?;

View File

@@ -95,6 +95,7 @@ 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 outputs = {
let sway = get_client();
@@ -120,6 +121,7 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<
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| {

View File

@@ -10,13 +10,17 @@ use tokio::task::spawn_blocking;
#[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>,
}

View File

@@ -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);
}
}

View File

@@ -1,8 +1,10 @@
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::{get_client, SwayNode};
@@ -14,15 +16,21 @@ use std::rc::Rc;
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::task::spawn_blocking;
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>,
}
@@ -67,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);
@@ -104,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);
@@ -135,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);
@@ -161,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) {
@@ -184,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);
}
}

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ use tokio::time::sleep;
#[derive(Debug, Deserialize, Clone)]
pub struct SysInfoModule {
/// List of formatting strings.
format: Vec<String>,
}

View File

@@ -13,8 +13,10 @@ 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,
}

View File

@@ -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,

View File

@@ -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();

View File

@@ -55,11 +55,13 @@ pub struct SwayOutput {
pub name: String,
}
type Broadcaster<T> = Arc<Mutex<UnboundedBroadcast<T>>>;
pub struct SwayClient {
client: ksway::Client,
workspace_bc: Arc<Mutex<UnboundedBroadcast<WorkspaceEvent>>>,
window_bc: Arc<Mutex<UnboundedBroadcast<WindowEvent>>>,
workspace_bc: Broadcaster<WorkspaceEvent>,
window_bc: Broadcaster<WindowEvent>,
}
impl SwayClient {
@@ -189,15 +191,20 @@ 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();
@@ -206,6 +213,7 @@ impl<T: 'static + Clone + Send + Sync> UnboundedBroadcast<T> {
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())?;

View File

@@ -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)?;