Compare commits
3 Commits
main
...
timeout_ol
Author | SHA1 | Date |
---|---|---|
Taeyeon Mori | 4c61d6e76a | 3 years ago |
Taeyeon Mori | f907833d78 | 3 years ago |
Taeyeon Mori | 97d33f5856 | 3 years ago |
42 changed files with 1447 additions and 3883 deletions
@ -1,9 +1,30 @@ |
||||
[workspace] |
||||
members = [ |
||||
"swayr", |
||||
"swayrbar", |
||||
] |
||||
[package] |
||||
name = "swayr" |
||||
version = "0.16.0" |
||||
description = "A LRU window-switcher (and more) for the sway window manager" |
||||
homepage = "https://sr.ht/~tsdh/swayr/" |
||||
repository = "https://git.sr.ht/~tsdh/swayr" |
||||
authors = ["Tassilo Horn <tsdh@gnu.org>"] |
||||
license = "GPL-3.0+" |
||||
edition = "2018" |
||||
exclude = ["misc/"] |
||||
|
||||
[dependencies] |
||||
serde = { version = "1.0.126", features = ["derive"] } |
||||
serde_json = "1.0.64" |
||||
clap = {version = "3.0.0", features = ["derive"] } |
||||
swayipc = "3.0.0" |
||||
toml = "0.5.8" |
||||
directories = "4.0" |
||||
regex = "1.5.4" |
||||
lazy_static = "1.4.0" |
||||
rand = "0.8.4" |
||||
rt-format = "0.3.0" |
||||
log = "0.4" |
||||
env_logger = { version = "0.9.0", default-features = false, features = ["termcolor", "atty", "humantime"] } # without regex |
||||
|
||||
[profile.dev] |
||||
lto = "thin" |
||||
|
||||
[profile.release] |
||||
lto = "thin" |
||||
strip = "symbols" |
@ -1,30 +1,3 @@ |
||||
swayr v0.19.0 |
||||
============= |
||||
|
||||
- There's a new command `switch-to-matching-or-urgent-or-lru-window` which |
||||
switches to the (first) window matching the given criteria (see section |
||||
`CRITERIA` in `sway(5)`) if it exists and is not already focused. Otherwise, |
||||
switch to the next urgent window (if any) or to the last recently used |
||||
window. |
||||
|
||||
swayr v0.18.0 |
||||
============= |
||||
|
||||
- The LRU window order will no longer be immediately updated when there is a |
||||
focus change. Instead there is now a short (configurable) delay |
||||
(`focus.lockin_delay`) before the update. The user-visible change is that |
||||
quickly moving over windows with the mouse, or moving through them using |
||||
keyboard navigation, will only register the start and destination windows in |
||||
the LRU sequence. |
||||
- A `nop` command can be used to interrupt a sequence of window-cycling |
||||
commands. |
||||
|
||||
swayr v0.17.0 |
||||
============= |
||||
|
||||
- No user-visible changes but a major restructuring and refactoring in order to |
||||
share code between swayr and swayrbar. |
||||
|
||||
swayr v0.16.0 |
||||
============= |
||||
|
@ -1,8 +1 @@ |
||||
Swayr |
||||
===== |
||||
|
||||
Swayrbar |
||||
======== |
||||
- Maybe add a launcher bar module |
||||
- Make the window module subscribe to sway window events and trigger an early |
||||
refresh on focus changes. |
||||
- Switch from lazy_static to once_cell once the latter is in stable rust. |
||||
|
Before Width: | Height: | Size: 49 KiB |
@ -0,0 +1,292 @@ |
||||
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Functions and data structures of the swayrd demon.
|
||||
|
||||
use crate::cmds; |
||||
use crate::config; |
||||
use crate::layout; |
||||
use crate::tree as t; |
||||
use crate::util; |
||||
use std::collections::HashMap; |
||||
use std::io::Read; |
||||
use std::os::unix::net::{UnixListener, UnixStream}; |
||||
use std::sync::Arc; |
||||
use std::sync::RwLock; |
||||
use std::thread; |
||||
use std::time::Duration; |
||||
use swayipc as s; |
||||
|
||||
pub fn run_demon() { |
||||
let config = config::load_config(); |
||||
let extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>> = |
||||
Arc::new(RwLock::new(HashMap::new())); |
||||
|
||||
let config_for_ev_handler = config.clone(); |
||||
let extra_props_for_ev_handler = extra_props.clone(); |
||||
|
||||
thread::spawn(move || { |
||||
monitor_sway_events(extra_props_for_ev_handler, config_for_ev_handler); |
||||
}); |
||||
|
||||
serve_client_requests(extra_props, config); |
||||
} |
||||
|
||||
fn connect_and_subscribe() -> s::Fallible<s::EventStream> { |
||||
s::Connection::new()?.subscribe(&[ |
||||
s::EventType::Window, |
||||
s::EventType::Workspace, |
||||
s::EventType::Shutdown, |
||||
]) |
||||
} |
||||
|
||||
pub fn monitor_sway_events( |
||||
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>, |
||||
config: config::Config, |
||||
) { |
||||
let mut focus_counter = 0; |
||||
let mut resets = 0; |
||||
let max_resets = 10; |
||||
|
||||
'reset: loop { |
||||
if resets >= max_resets { |
||||
break; |
||||
} |
||||
resets += 1; |
||||
|
||||
log::debug!("Connecting to sway for subscribing to events..."); |
||||
match connect_and_subscribe() { |
||||
Err(err) => { |
||||
log::warn!("Could not connect and subscribe: {}", err); |
||||
std::thread::sleep(std::time::Duration::from_secs(3)); |
||||
} |
||||
Ok(iter) => { |
||||
for ev_result in iter { |
||||
let show_extra_props_state; |
||||
resets = 0; |
||||
match ev_result { |
||||
Ok(ev) => match ev { |
||||
s::Event::Window(win_ev) => { |
||||
let extra_props_clone = extra_props.clone(); |
||||
focus_counter += 1; |
||||
show_extra_props_state = handle_window_event( |
||||
win_ev, |
||||
extra_props_clone, |
||||
&config, |
||||
focus_counter, |
||||
); |
||||
} |
||||
s::Event::Workspace(ws_ev) => { |
||||
let extra_props_clone = extra_props.clone(); |
||||
focus_counter += 1; |
||||
show_extra_props_state = handle_workspace_event( |
||||
ws_ev, |
||||
extra_props_clone, |
||||
focus_counter, |
||||
); |
||||
} |
||||
s::Event::Shutdown(sd_ev) => { |
||||
log::debug!( |
||||
"Sway shuts down with reason '{:?}'.", |
||||
sd_ev.change |
||||
); |
||||
break 'reset; |
||||
} |
||||
_ => show_extra_props_state = false, |
||||
}, |
||||
Err(e) => { |
||||
log::warn!("Error while receiving events: {}", e); |
||||
std::thread::sleep(std::time::Duration::from_secs( |
||||
3, |
||||
)); |
||||
show_extra_props_state = false; |
||||
log::warn!("Resetting!"); |
||||
} |
||||
} |
||||
if show_extra_props_state { |
||||
log::debug!( |
||||
"New extra_props state:\n{:#?}", |
||||
*extra_props.read().unwrap() |
||||
); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
log::debug!("Swayr demon shutting down.") |
||||
} |
||||
|
||||
fn handle_window_event( |
||||
ev: Box<s::WindowEvent>, |
||||
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>, |
||||
config: &config::Config, |
||||
focus_val: u64, |
||||
) -> bool { |
||||
let s::WindowEvent { |
||||
change, container, .. |
||||
} = *ev; |
||||
match change { |
||||
s::WindowChange::Focus => { |
||||
layout::maybe_auto_tile(config); |
||||
update_last_focus_tick(container.id, extra_props, focus_val); |
||||
log::debug!("Handled window event type {:?}", change); |
||||
true |
||||
} |
||||
s::WindowChange::New => { |
||||
layout::maybe_auto_tile(config); |
||||
update_last_focus_tick(container.id, extra_props, focus_val); |
||||
log::debug!("Handled window event type {:?}", change); |
||||
true |
||||
} |
||||
s::WindowChange::Close => { |
||||
remove_extra_props(container.id, extra_props); |
||||
layout::maybe_auto_tile(config); |
||||
log::debug!("Handled window event type {:?}", change); |
||||
true |
||||
} |
||||
s::WindowChange::Move | s::WindowChange::Floating => { |
||||
layout::maybe_auto_tile(config); |
||||
log::debug!("Handled window event type {:?}", change); |
||||
false // We don't affect the extra_props state here.
|
||||
} |
||||
_ => { |
||||
log::debug!("Unhandled window event type {:?}", change); |
||||
false |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn handle_workspace_event( |
||||
ev: Box<s::WorkspaceEvent>, |
||||
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>, |
||||
focus_val: u64, |
||||
) -> bool { |
||||
let s::WorkspaceEvent { |
||||
change, |
||||
current, |
||||
old: _, |
||||
.. |
||||
} = *ev; |
||||
match change { |
||||
s::WorkspaceChange::Init | s::WorkspaceChange::Focus => { |
||||
update_last_focus_tick( |
||||
current |
||||
.expect("No current in Init or Focus workspace event") |
||||
.id, |
||||
extra_props, |
||||
focus_val, |
||||
); |
||||
log::debug!("Handled workspace event type {:?}", change); |
||||
true |
||||
} |
||||
s::WorkspaceChange::Empty => { |
||||
remove_extra_props( |
||||
current.expect("No current in Empty workspace event").id, |
||||
extra_props, |
||||
); |
||||
log::debug!("Handled workspace event type {:?}", change); |
||||
true |
||||
} |
||||
_ => false, |
||||
} |
||||
} |
||||
|
||||
fn update_last_focus_tick( |
||||
id: i64, |
||||
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>, |
||||
focus_val: u64, |
||||
) { |
||||
let mut write_lock = extra_props.write().unwrap(); |
||||
if let Some(wp) = write_lock.get_mut(&id) { |
||||
wp.last_focus_tick = focus_val; |
||||
} else { |
||||
write_lock.insert( |
||||
id, |
||||
t::ExtraProps { |
||||
last_focus_tick: focus_val, |
||||
last_focus_tick_for_next_prev_seq: focus_val, |
||||
}, |
||||
); |
||||
} |
||||
} |
||||
|
||||
fn remove_extra_props( |
||||
id: i64, |
||||
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>, |
||||
) { |
||||
extra_props.write().unwrap().remove(&id); |
||||
} |
||||
|
||||
pub fn serve_client_requests( |
||||
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>, |
||||
config: config::Config, |
||||
) { |
||||
match std::fs::remove_file(util::get_swayr_socket_path()) { |
||||
Ok(()) => log::debug!("Deleted stale socket from previous run."), |
||||
Err(e) => log::error!("Could not delete socket:\n{:?}", e), |
||||
} |
||||
|
||||
let timeout = match config.get_sequence_timeout() { |
||||
0 => None, |
||||
secs => Some(Duration::from_secs(secs)), |
||||
}; |
||||
|
||||
match UnixListener::bind(util::get_swayr_socket_path()) { |
||||
Ok(listener) => { |
||||
for stream in listener.incoming() { |
||||
match stream { |
||||
Ok(stream) => { |
||||
handle_client_request( |
||||
stream, |
||||
extra_props.clone(), |
||||
timeout, |
||||
); |
||||
} |
||||
Err(err) => { |
||||
log::error!("Error handling client request: {}", err); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Err(err) => { |
||||
log::error!("Could not bind socket: {}", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn handle_client_request( |
||||
mut stream: UnixStream, |
||||
extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>, |
||||
sequence_timeout: Option<Duration>, |
||||
) { |
||||
let mut cmd_str = String::new(); |
||||
if stream.read_to_string(&mut cmd_str).is_ok() { |
||||
if let Ok(cmd) = serde_json::from_str::<cmds::SwayrCommand>(&cmd_str) { |
||||
cmds::exec_swayr_cmd(cmds::ExecSwayrCmdArgs { |
||||
cmd: &cmd, |
||||
extra_props, |
||||
sequence_timeout, |
||||
}); |
||||
} else { |
||||
log::error!( |
||||
"Could not serialize following string to SwayrCommand.\n{}", |
||||
cmd_str |
||||
); |
||||
} |
||||
} else { |
||||
log::error!("Could not read command from client."); |
||||
} |
||||
} |
@ -0,0 +1,103 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Provides runtime formatting of strings since our format strings are read
|
||||
//! from the swayr config, not specified as literals, e.g., `{name:{:.30}}` in
|
||||
//! `format.window_format`.
|
||||
|
||||
use rt_format::{ |
||||
Format, FormatArgument, NoNamedArguments, ParsedFormat, Specifier, |
||||
}; |
||||
use std::fmt; |
||||
|
||||
enum FmtArg<'a> { |
||||
Str(&'a str), |
||||
} |
||||
|
||||
impl<'a> FormatArgument for FmtArg<'a> { |
||||
fn supports_format(&self, spec: &Specifier) -> bool { |
||||
spec.format == Format::Display |
||||
} |
||||
|
||||
fn fmt_display(&self, f: &mut fmt::Formatter) -> fmt::Result { |
||||
match self { |
||||
Self::Str(val) => fmt::Display::fmt(&val, f), |
||||
} |
||||
} |
||||
|
||||
fn fmt_debug(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_octal(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_lower_hex(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_upper_hex(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_binary(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_lower_exp(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_upper_exp(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
} |
||||
|
||||
pub fn format(fmt: &str, arg: &str, clipped_str: &str) -> String { |
||||
let args = &[FmtArg::Str(arg)]; |
||||
|
||||
if let Ok(pf) = ParsedFormat::parse(fmt, args, &NoNamedArguments) { |
||||
let mut s = format!("{}", pf); |
||||
|
||||
if !clipped_str.is_empty() && !s.contains(arg) { |
||||
remove_last_n_chars(&mut s, clipped_str.chars().count()); |
||||
s.push_str(clipped_str); |
||||
} |
||||
s |
||||
} else { |
||||
format!("Invalid format string: {}", fmt) |
||||
} |
||||
} |
||||
|
||||
fn remove_last_n_chars(s: &mut String, n: usize) { |
||||
match s.char_indices().nth_back(n) { |
||||
Some((pos, ch)) => s.truncate(pos + ch.len_utf8()), |
||||
None => s.clear(), |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_format() { |
||||
assert_eq!(format("{:.10}", "sway", ""), "sway"); |
||||
assert_eq!(format("{:.10}", "sway", "…"), "sway"); |
||||
assert_eq!(format("{:.4}", "𝔰𝔴𝔞𝔶", "……"), "𝔰𝔴𝔞𝔶"); |
||||
|
||||
assert_eq!(format("{:.3}", "sway", ""), "swa"); |
||||
assert_eq!(format("{:.3}", "sway", "…"), "sw…"); |
||||
assert_eq!(format("{:.5}", "𝔰𝔴𝔞𝔶 𝔴𝔦𝔫𝔡𝔬𝔴", "…?"), "𝔰𝔴𝔞…?"); |
||||
assert_eq!(format("{:.5}", "sway window", "..."), "sw..."); |
||||
assert_eq!(format("{:.2}", "sway", "..."), "..."); |
||||
} |
@ -0,0 +1,598 @@ |
||||
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Convenience data structures built from the IPC structs.
|
||||
|
||||
use crate::config; |
||||
use crate::rtfmt; |
||||
use crate::util; |
||||
use crate::util::DisplayFormat; |
||||
use lazy_static::lazy_static; |
||||
use serde::{Deserialize, Serialize}; |
||||
use std::cell::RefCell; |
||||
use std::cmp; |
||||
use std::collections::HashMap; |
||||
use std::rc::Rc; |
||||
use swayipc as s; |
||||
|
||||
/// Immutable Node Iterator
|
||||
///
|
||||
/// Iterates nodes in depth-first order, tiled nodes before floating nodes.
|
||||
pub struct NodeIter<'a> { |
||||
stack: Vec<&'a s::Node>, |
||||
} |
||||
|
||||
impl<'a> NodeIter<'a> { |
||||
fn new(node: &'a s::Node) -> NodeIter { |
||||
NodeIter { stack: vec![node] } |
||||
} |
||||
} |
||||
|
||||
impl<'a> Iterator for NodeIter<'a> { |
||||
type Item = &'a s::Node; |
||||
|
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
if let Some(node) = self.stack.pop() { |
||||
for n in &node.floating_nodes { |
||||
self.stack.push(n); |
||||
} |
||||
for n in &node.nodes { |
||||
self.stack.push(n); |
||||
} |
||||
Some(node) |
||||
} else { |
||||
None |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)] |
||||
pub enum Type { |
||||
Root, |
||||
Output, |
||||
Workspace, |
||||
Container, |
||||
Window, |
||||
} |
||||
|
||||
/// Extension methods for [`swayipc::Node`].
|
||||
pub trait NodeMethods { |
||||
fn iter(&self) -> NodeIter; |
||||
fn get_type(&self) -> Type; |
||||
fn get_app_name(&self) -> &str; |
||||
fn nodes_of_type(&self, t: Type) -> Vec<&s::Node>; |
||||
fn get_name(&self) -> &str; |
||||
fn is_scratchpad(&self) -> bool; |
||||
fn is_floating(&self) -> bool; |
||||
fn is_current(&self) -> bool; |
||||
} |
||||
|
||||
impl NodeMethods for s::Node { |
||||
fn iter(&self) -> NodeIter { |
||||
NodeIter::new(self) |
||||
} |
||||
|
||||
fn get_type(&self) -> Type { |
||||
match self.node_type { |
||||
s::NodeType::Root => Type::Root, |
||||
s::NodeType::Output => Type::Output, |
||||
s::NodeType::Workspace => Type::Workspace, |
||||
s::NodeType::FloatingCon => Type::Window, |
||||
_ => { |
||||
if self.node_type == s::NodeType::Con |
||||
&& self.name.is_none() |
||||
&& self.app_id.is_none() |
||||
&& self.pid.is_none() |
||||
&& self.shell.is_none() |
||||
&& self.window_properties.is_none() |
||||
&& self.layout != s::NodeLayout::None |
||||
{ |
||||
Type::Container |
||||
} else if (self.node_type == s::NodeType::Con |
||||
|| self.node_type == s::NodeType::FloatingCon) |
||||
// Apparently there can be windows without app_id, name,
|
||||
// and window_properties.class, e.g., dolphin-emu-nogui.
|
||||
&& self.pid.is_some() |
||||
// FIXME: While technically correct, old sway versions (up to
|
||||
// at least sway-1.4) don't expose shell in IPC. So comment in
|
||||
// again when all major distros have a recent enough sway
|
||||
// package.
|
||||
//&& self.shell.is_some()
|
||||
{ |
||||
Type::Window |
||||
} else { |
||||
panic!( |
||||
"Don't know type of node with id {} and node_type {:?}\n{:?}", |
||||
self.id, self.node_type, self |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn get_name(&self) -> &str { |
||||
if let Some(name) = &self.name { |
||||
name.as_ref() |
||||
} else { |
||||
"<unnamed>" |
||||
} |
||||
} |
||||
|
||||
fn get_app_name(&self) -> &str { |
||||
if let Some(app_id) = &self.app_id { |
||||
app_id |
||||
} else if let Some(wp_class) = self |
||||
.window_properties |
||||
.as_ref() |
||||
.and_then(|wp| wp.class.as_ref()) |
||||
{ |
||||
wp_class |
||||
} else { |
||||
"<unknown_app>" |
||||
} |
||||
} |
||||
|
||||
fn is_scratchpad(&self) -> bool { |
||||
let name = self.get_name(); |
||||
name.eq("__i3") || name.eq("__i3_scratch") |
||||
} |
||||
|
||||
fn nodes_of_type(&self, t: Type) -> Vec<&s::Node> { |
||||
self.iter().filter(|n| n.get_type() == t).collect() |
||||
} |
||||
|
||||
fn is_floating(&self) -> bool { |
||||
self.node_type == s::NodeType::FloatingCon |
||||
} |
||||
|
||||
fn is_current(&self) -> bool { |
||||
self.iter().any(|n| n.focused) |
||||
} |
||||
} |
||||
|
||||
/// Extra properties gathered by swayrd for windows and workspaces.
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize)] |
||||
pub struct ExtraProps { |
||||
pub last_focus_tick: u64, |
||||
pub last_focus_tick_for_next_prev_seq: u64, |
||||
} |
||||
|
||||
pub struct Tree<'a> { |
||||
root: &'a s::Node, |
||||
id_node: HashMap<i64, &'a s::Node>, |
||||
id_parent: HashMap<i64, i64>, |
||||
extra_props: &'a HashMap<i64, ExtraProps>, |
||||
} |
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)] |
||||
enum IndentLevel { |
||||
Fixed(usize), |
||||
WorkspacesZeroWindowsOne, |
||||
TreeDepth(usize), |
||||
} |
||||
|
||||
pub struct DisplayNode<'a> { |
||||
pub node: &'a s::Node, |
||||
pub tree: &'a Tree<'a>, |
||||
indent_level: IndentLevel, |
||||
} |
||||
|
||||
impl<'a> Tree<'a> { |
||||
fn get_node_by_id(&self, id: i64) -> &&s::Node { |
||||
self.id_node |
||||
.get(&id) |
||||
.unwrap_or_else(|| panic!("No node with id {}", id)) |
||||
} |
||||
|
||||
fn get_parent_node(&self, id: i64) -> Option<&&s::Node> { |
||||
self.id_parent.get(&id).map(|pid| self.get_node_by_id(*pid)) |
||||
} |
||||
|
||||
pub fn get_parent_node_of_type( |
||||
&self, |
||||
id: i64, |
||||
t: Type, |
||||
) -> Option<&&s::Node> { |
||||
let n = self.get_node_by_id(id); |
||||
if n.get_type() == t { |
||||
Some(n) |
||||
} else if let Some(pid) = self.id_parent.get(&id) { |
||||
self.get_parent_node_of_type(*pid, t) |
||||
} else { |
||||
None |
||||
} |
||||
} |
||||
|
||||
pub fn last_focus_tick(&self, id: i64) -> u64 { |
||||
self.extra_props.get(&id).map_or(0, |wp| wp.last_focus_tick) |
||||
} |
||||
|
||||
pub fn last_focus_tick_for_next_prev_seq(&self, id: i64) -> u64 { |
||||
self.extra_props |
||||
.get(&id) |
||||
.map_or(0, |wp| wp.last_focus_tick_for_next_prev_seq) |
||||
} |
||||
|
||||
fn sorted_nodes_of_type_1( |
||||
&self, |
||||
node: &'a s::Node, |
||||
t: Type, |
||||
) -> Vec<&s::Node> { |
||||
let mut v: Vec<&s::Node> = node.nodes_of_type(t); |
||||
self.sort_by_urgency_and_lru_time_1(&mut v); |
||||
v |
||||
} |
||||
|
||||
fn sorted_nodes_of_type(&self, t: Type) -> Vec<&s::Node> { |
||||
self.sorted_nodes_of_type_1(self.root, t) |
||||
} |
||||
|
||||
fn as_display_nodes( |
||||
&self, |
||||
v: &[&'a s::Node], |
||||
indent_level: IndentLevel, |
||||
) -> Vec<DisplayNode> { |
||||
v.iter() |
||||
.map(|node| DisplayNode { |
||||
node, |
||||
tree: self, |
||||
indent_level, |
||||
}) |
||||
.collect() |
||||
} |
||||
|
||||
pub fn get_current_workspace(&self) -> &s::Node { |
||||
self.root |
||||
.iter() |
||||
.find(|n| n.get_type() == Type::Workspace && n.is_current()) |
||||
.expect("No current Workspace") |
||||
} |
||||
|
||||
pub fn get_outputs(&self) -> Vec<DisplayNode> { |
||||
let outputs: Vec<&s::Node> = self |
||||
.root |
||||
.iter() |
||||
.filter(|n| n.get_type() == Type::Output && !n.is_scratchpad()) |
||||
.collect(); |
||||
self.as_display_nodes(&outputs, IndentLevel::Fixed(0)) |
||||
} |
||||
|
||||
pub fn get_workspaces(&self) -> Vec<DisplayNode> { |
||||
let mut v = self.sorted_nodes_of_type(Type::Workspace); |
||||
if !v.is_empty() { |
||||
v.rotate_left(1); |
||||
} |
||||
self.as_display_nodes(&v, IndentLevel::Fixed(0)) |
||||
} |
||||
|
||||
pub fn get_windows(&self) -> Vec<DisplayNode> { |
||||
let mut v = self.sorted_nodes_of_type(Type::Window); |
||||
// Rotate, but only non-urgent windows. Those should stay at the front
|
||||
// as they are the most likely switch candidates.
|
||||
let mut x; |
||||
if !v.is_empty() { |
||||
x = vec![]; |
||||
loop { |
||||
if !v.is_empty() && v[0].urgent { |
||||
x.push(v.remove(0)); |
||||
} else { |
||||
break; |
||||
} |
||||
} |
||||
if !v.is_empty() { |
||||
v.rotate_left(1); |
||||
x.append(&mut v); |
||||
} |
||||
} else { |
||||
x = v; |
||||
} |
||||
self.as_display_nodes(&x, IndentLevel::Fixed(0)) |
||||
} |
||||
|
||||
pub fn get_workspaces_and_windows(&self) -> Vec<DisplayNode> { |
||||
let workspaces = self.sorted_nodes_of_type(Type::Workspace); |
||||
let mut first = true; |
||||
let mut v = vec![]; |
||||
for ws in workspaces { |
||||
v.push(ws); |
||||
let mut wins = self.sorted_nodes_of_type_1(ws, Type::Window); |
||||
if first && !wins.is_empty() { |
||||
wins.rotate_left(1); |
||||
first = false; |
||||
} |
||||
v.append(&mut wins); |
||||
} |
||||
|
||||
self.as_display_nodes(&v, IndentLevel::WorkspacesZeroWindowsOne) |
||||
} |
||||
|
||||
fn sort_by_urgency_and_lru_time_1(&self, v: &mut Vec<&s::Node>) { |
||||
v.sort_by(|a, b| { |
||||
if a.urgent && !b.urgent { |
||||
cmp::Ordering::Less |
||||
} else if !a.urgent && b.urgent { |
||||
cmp::Ordering::Greater |
||||
} else { |
||||
let lru_a = self.last_focus_tick(a.id); |
||||
let lru_b = self.last_focus_tick(b.id); |
||||
lru_a.cmp(&lru_b).reverse() |
||||
} |
||||
}); |
||||
} |
||||
|
||||
fn push_subtree_sorted( |
||||
&self, |
||||
n: &'a s::Node, |
||||
v: Rc<RefCell<Vec<&'a s::Node>>>, |
||||
) { |
||||
v.borrow_mut().push(n); |
||||
|
||||
let mut children: Vec<&s::Node> = n.nodes.iter().collect(); |
||||
children.append(&mut n.floating_nodes.iter().collect()); |
||||
self.sort_by_urgency_and_lru_time_1(&mut children); |
||||
|
||||
for c in children { |
||||
self.push_subtree_sorted(c, Rc::clone(&v)); |
||||
} |
||||
} |
||||
|
||||
pub fn get_outputs_workspaces_containers_and_windows( |
||||
&self, |
||||
) -> Vec<DisplayNode> { |
||||
let outputs = self.sorted_nodes_of_type(Type::Output); |
||||
let v: Rc<RefCell<Vec<&s::Node>>> = Rc::new(RefCell::new(vec![])); |
||||
for o in outputs { |
||||
self.push_subtree_sorted(o, Rc::clone(&v)); |
||||
} |
||||
|
||||
let x = self.as_display_nodes(&*v.borrow(), IndentLevel::TreeDepth(1)); |
||||
x |
||||
} |
||||
|
||||
pub fn get_workspaces_containers_and_windows(&self) -> Vec<DisplayNode> { |
||||
let workspaces = self.sorted_nodes_of_type(Type::Workspace); |
||||
let v: Rc<RefCell<Vec<&s::Node>>> = Rc::new(RefCell::new(vec![])); |
||||
for ws in workspaces { |
||||
self.push_subtree_sorted(ws, Rc::clone(&v)); |
||||
} |
||||
|
||||
let x = self.as_display_nodes(&*v.borrow(), IndentLevel::TreeDepth(2)); |
||||
x |
||||
} |
||||
|
||||
pub fn is_child_of_tiled_container(&self, id: i64) -> bool { |
||||
match self.get_parent_node(id) { |
||||
Some(n) => { |
||||
n.layout == s::NodeLayout::SplitH |
||||
|| n.layout == s::NodeLayout::SplitV |
||||
} |
||||
None => false, |
||||
} |
||||
} |
||||
|
||||
pub fn is_child_of_tabbed_or_stacked_container(&self, id: i64) -> bool { |
||||
match self.get_parent_node(id) { |
||||
Some(n) => { |
||||
n.layout == s::NodeLayout::Tabbed |
||||
|| n.layout == s::NodeLayout::Stacked |
||||
} |
||||
None => false, |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn init_id_parent<'a>( |
||||
n: &'a s::Node, |
||||
parent: Option<&'a s::Node>, |
||||
id_node: &mut HashMap<i64, &'a s::Node>, |
||||
id_parent: &mut HashMap<i64, i64>, |
||||
) { |
||||
id_node.insert(n.id, n); |
||||
|
||||
if let Some(p) = parent { |
||||
id_parent.insert(n.id, p.id); |
||||
} |
||||
|
||||
for c in &n.nodes { |
||||
init_id_parent(c, Some(n), id_node, id_parent); |
||||
} |
||||
for c in &n.floating_nodes { |
||||
init_id_parent(c, Some(n), id_node, id_parent); |
||||
} |
||||
} |
||||
|
||||
pub fn get_tree<'a>( |
||||
root: &'a s::Node, |
||||
extra_props: &'a HashMap<i64, ExtraProps>, |
||||
) -> Tree<'a> { |
||||
let mut id_node: HashMap<i64, &s::Node> = HashMap::new(); |
||||
let mut id_parent: HashMap<i64, i64> = HashMap::new(); |
||||
init_id_parent(root, None, &mut id_node, &mut id_parent); |
||||
|
||||
Tree { |
||||
root, |
||||
id_node, |
||||
id_parent, |
||||
extra_props, |
||||
} |
||||
} |
||||
|
||||
lazy_static! { |
||||
static ref APP_NAME_AND_VERSION_RX: regex::Regex = |
||||
regex::Regex::new("(.+)(-[0-9.]+)").unwrap(); |
||||
static ref PLACEHOLDER_RX: regex::Regex = regex::Regex::new( |
||||
r"\{(?P<name>[^}:]+)(?::(?P<fmtstr>\{[^}]*\})(?P<clipstr>[^}]*))?\}" |
||||
) |
||||
.unwrap(); |
||||
} |
||||
|
||||
fn maybe_html_escape(do_it: bool, text: String) -> String { |
||||
if do_it { |
||||
text.replace('<', "<") |
||||
.replace('>', ">") |
||||
.replace('&', "&") |
||||
} else { |
||||
text |
||||
} |
||||
} |
||||
|
||||
fn format_marks(marks: &[String]) -> String { |
||||
if marks.is_empty() { |
||||
"".to_string() |
||||
} else { |
||||
format!("[{}]", marks.join(", ")) |
||||
} |
||||
} |
||||
|
||||
impl DisplayFormat for DisplayNode<'_> { |
||||
fn format_for_display(&self, cfg: &config::Config) -> String { |
||||
let indent = cfg.get_format_indent(); |
||||
let html_escape = cfg.get_format_html_escape(); |
||||
let urgency_start = cfg.get_format_urgency_start(); |
||||
let urgency_end = cfg.get_format_urgency_end(); |
||||
let icon_dirs = cfg.get_format_icon_dirs(); |
||||
// fallback_icon has no default value.
|
||||
let fallback_icon: Option<Box<std::path::Path>> = cfg |
||||
.get_format_fallback_icon() |
||||
.as_ref() |
||||
.map(|i| std::path::Path::new(i).to_owned().into_boxed_path()); |
||||
|
||||
let app_name_no_version = |
||||
APP_NAME_AND_VERSION_RX.replace(self.node.get_app_name(), "$1"); |
||||
|
||||
let fmt = match self.node.get_type() { |
||||
Type::Root => String::from("Cannot format Root"), |
||||
Type::Output => cfg.get_format_output_format(), |
||||
Type::Workspace => cfg.get_format_workspace_format(), |
||||
Type::Container => cfg.get_format_container_format(), |
||||
Type::Window => cfg.get_format_window_format(), |
||||
}; |
||||
let fmt = fmt |
||||
.replace("{indent}", &indent.repeat(self.get_indent_level())) |
||||
.replace( |
||||
"{urgency_start}", |
||||
if self.node.urgent { |
||||
urgency_start.as_str() |
||||
} else { |
||||
"" |
||||
}, |
||||
) |
||||
.replace( |
||||
"{urgency_end}", |
||||
if self.node.urgent { |
||||
urgency_end.as_str() |
||||
} else { |
||||
"" |
||||
}, |
||||
) |
||||
.replace( |
||||
"{app_icon}", |
||||
util::get_icon(self.node.get_app_name(), &icon_dirs) |
||||
.or_else(|| { |
||||
util::get_icon(&app_name_no_version, &icon_dirs) |
||||
}) |
||||
.or_else(|| { |
||||
util::get_icon( |
||||
&app_name_no_version.to_lowercase(), |
||||
&icon_dirs, |
||||
) |
||||
}) |
||||
.or(fallback_icon) |
||||
.map(|i| i.to_string_lossy().into_owned()) |
||||
.unwrap_or_else(String::new) |
||||
.as_str(), |
||||
); |
||||
|
||||
PLACEHOLDER_RX |
||||
.replace_all(&fmt, |caps: ®ex::Captures| { |
||||
let value = match &caps["name"] { |
||||
"id" => self.node.id.to_string(), |
||||
"app_name" => self.node.get_app_name().to_string(), |
||||
"layout" => format!("{:?}", self.node.layout), |
||||
"name" | "title" => self.node.get_name().to_string(), |
||||
"output_name" => self |
||||
.tree |
||||
.get_parent_node_of_type(self.node.id, Type::Output) |
||||
.map_or("<no_output>", |w| w.get_name()) |
||||
.to_string(), |
||||
"workspace_name" => self |
||||
.tree |
||||
.get_parent_node_of_type(self.node.id, Type::Workspace) |
||||
.map_or("<no_workspace>", |w| w.get_name()) |
||||
.to_string(), |
||||
"marks" => format_marks(&self.node.marks), |
||||
_ => caps[0].to_string(), |
||||
}; |
||||
let fmt_str = caps.name("fmtstr").map_or("{}", |m| m.as_str()); |
||||
let clipped_str = |
||||
caps.name("clipstr").map_or("", |m| m.as_str()); |
||||
|
||||
maybe_html_escape( |
||||
html_escape, |
||||
rtfmt::format(fmt_str, &value, clipped_str), |
||||
) |
||||
}) |
||||
.into() |
||||
} |
||||
|
||||
fn get_indent_level(&self) -> usize { |
||||
match self.indent_level { |
||||
IndentLevel::Fixed(level) => level as usize, |
||||
IndentLevel::WorkspacesZeroWindowsOne => { |
||||
match self.node.get_type(){ |
||||
Type::Workspace => 0, |
||||
Type::Window => 1, |
||||
_ => panic!("Only Workspaces and Windows expected. File a bug report!") |
||||
} |
||||
} |
||||
IndentLevel::TreeDepth(offset) => { |
||||
let mut depth: usize = 0; |
||||
let mut node = self.node; |
||||
while let Some(p) = self.tree.get_parent_node(node.id) { |
||||
depth += 1; |
||||
node = p; |
||||
} |
||||
if offset > depth { |
||||
0 |
||||
} else { |
||||
depth - offset as usize |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_placeholder_rx() { |
||||
let caps = PLACEHOLDER_RX.captures("Hello, {place}!").unwrap(); |
||||
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
||||
assert_eq!(caps.name("fmtstr"), None); |
||||
assert_eq!(caps.name("clipstr"), None); |
||||
|
||||
let caps = PLACEHOLDER_RX.captures("Hi, {place:{:>10.10}}!").unwrap(); |
||||
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
||||
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:>10.10}"); |
||||
assert_eq!(caps.name("clipstr").unwrap().as_str(), ""); |
||||
|
||||
let caps = PLACEHOLDER_RX.captures("Hello, {place:{:.5}…}!").unwrap(); |
||||
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
||||
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:.5}"); |
||||
assert_eq!(caps.name("clipstr").unwrap().as_str(), "…"); |
||||
|
||||
let caps = PLACEHOLDER_RX.captures("Hello, {place:{:.5}...}!").unwrap(); |
||||
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
||||
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:.5}"); |
||||
assert_eq!(caps.name("clipstr").unwrap().as_str(), "..."); |
||||
} |
@ -1,23 +0,0 @@ |
||||
[package] |
||||
name = "swayr" |
||||
version = "0.19.0" |
||||
description = "A LRU window-switcher (and more) for the sway window manager" |
||||
homepage = "https://sr.ht/~tsdh/swayr/" |
||||
repository = "https://git.sr.ht/~tsdh/swayr" |
||||
authors = ["Tassilo Horn <tsdh@gnu.org>"] |
||||
license = "GPL-3.0+" |
||||
edition = "2021" |
||||
|
||||
[dependencies] |
||||
clap = {version = "3.0.0", features = ["derive"] } |
||||
directories = "4.0" |
||||
env_logger = { version = "0.9.0", default-features = false, features = ["termcolor", "atty", "humantime"] } # without regex |
||||
log = "0.4" |
||||
once_cell = "1.10.0" |
||||
rand = "0.8.4" |
||||
regex = "1.5.5" |
||||
rt-format = "0.3.0" |
||||
serde = { version = "1.0.126", features = ["derive"] } |
||||
serde_json = "1.0.64" |
||||
swayipc = "3.0.0" |
||||
toml = "0.5.8" |
@ -1 +0,0 @@ |
||||
../README.md |
@ -1,412 +0,0 @@ |
||||
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Functions and data structures of the swayrd daemon.
|
||||
|
||||
use crate::cmds; |
||||
use crate::config::{self, Config}; |
||||
use crate::focus::FocusData; |
||||
use crate::focus::FocusEvent; |
||||
use crate::focus::FocusMessage; |
||||
use crate::layout; |
||||
use crate::util; |
||||
use std::collections::HashMap; |
||||
use std::io::Read; |
||||
use std::os::unix::net::{UnixListener, UnixStream}; |
||||
use std::sync::mpsc; |
||||
use std::sync::Arc; |
||||
use std::sync::RwLock; |
||||
use std::thread; |
||||
use std::time::Duration; |
||||
use std::time::Instant; |
||||
use swayipc as s; |
||||
|
||||
pub fn run_daemon() { |
||||
let (focus_tx, focus_rx) = mpsc::channel(); |
||||
let fdata = FocusData { |
||||
focus_tick_by_id: Arc::new(RwLock::new(HashMap::new())), |
||||
focus_chan: focus_tx, |
||||
}; |
||||
|
||||
let config = config::load_config(); |
||||
let lockin_delay = config.get_focus_lockin_delay(); |
||||
let sequence_timeout = config.get_focus_sequence_timeout(); |
||||
|
||||
{ |
||||
let fdata = fdata.clone(); |
||||
thread::spawn(move || { |
||||
monitor_sway_events(fdata, &config); |
||||
}); |
||||
} |
||||
|
||||
{ |
||||
let fdata = fdata.clone(); |
||||
thread::spawn(move || { |
||||
focus_lock_in_handler( |
||||
focus_rx, |
||||
fdata, |
||||
lockin_delay, |
||||
sequence_timeout, |
||||
); |
||||
}); |
||||
} |
||||
|
||||
serve_client_requests(fdata); |
||||
} |
||||
|
||||
fn connect_and_subscribe() -> s::Fallible<s::EventStream> { |
||||
s::Connection::new()?.subscribe(&[ |
||||
s::EventType::Window, |
||||
s::EventType::Workspace, |
||||
s::EventType::Shutdown, |
||||
]) |
||||
} |
||||
|
||||
pub fn monitor_sway_events(fdata: FocusData, config: &Config) { |
||||
let mut focus_counter = 0; |
||||
let mut resets = 0; |
||||
let max_resets = 10; |
||||
|
||||
'reset: loop { |
||||
if resets >= max_resets { |
||||
break; |
||||
} |
||||
resets += 1; |
||||
|
||||
log::debug!("Connecting to sway for subscribing to events..."); |
||||
match connect_and_subscribe() { |
||||
Err(err) => { |
||||
log::warn!("Could not connect and subscribe: {}", err); |
||||
std::thread::sleep(std::time::Duration::from_secs(3)); |
||||
} |
||||
Ok(iter) => { |
||||
for ev_result in iter { |
||||
let show_extra_props_state; |
||||
resets = 0; |
||||
match ev_result { |
||||
Ok(ev) => match ev { |
||||
s::Event::Window(win_ev) => { |
||||
focus_counter += 1; |
||||
show_extra_props_state = handle_window_event( |
||||
win_ev, |
||||
&fdata, |
||||
config, |
||||
focus_counter, |
||||
); |
||||
} |
||||
s::Event::Workspace(ws_ev) => { |
||||
focus_counter += 1; |
||||
show_extra_props_state = handle_workspace_event( |
||||
ws_ev, |
||||
&fdata, |
||||
focus_counter, |
||||
); |
||||
} |
||||
s::Event::Shutdown(sd_ev) => { |
||||
log::debug!( |
||||
"Sway shuts down with reason '{:?}'.", |
||||
sd_ev.change |
||||
); |
||||
break 'reset; |
||||
} |
||||
_ => show_extra_props_state = false, |
||||
}, |
||||
Err(e) => { |
||||
log::warn!("Error while receiving events: {}", e); |
||||
std::thread::sleep(std::time::Duration::from_secs( |
||||
3, |
||||
)); |
||||
show_extra_props_state = false; |
||||
log::warn!("Resetting!"); |
||||
} |
||||
} |
||||
if show_extra_props_state { |
||||
log::debug!( |
||||
"New extra_props state:\n{:#?}", |
||||
*fdata.focus_tick_by_id.read().unwrap() |
||||
); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
log::debug!("Swayr daemon shutting down.") |
||||
} |
||||
|
||||
fn handle_window_event( |
||||
ev: Box<s::WindowEvent>, |
||||
fdata: &FocusData, |
||||
config: &config::Config, |
||||
focus_val: u64, |
||||
) -> bool { |
||||
let s::WindowEvent { |
||||
change, container, .. |
||||
} = *ev; |
||||
match change { |
||||
s::WindowChange::Focus => { |
||||
layout::maybe_auto_tile(config); |
||||
fdata.send(FocusMessage::FocusEvent(FocusEvent { |
||||
node_id: container.id, |
||||
ev_focus_ctr: focus_val, |
||||
})); |
||||
log::debug!("Handled window event type {:?}", change); |
||||
true |
||||
} |
||||
s::WindowChange::New => { |
||||
layout::maybe_auto_tile(config); |
||||
fdata.ensure_id(container.id); |
||||
log::debug!("Handled window event type {:?}", change); |
||||
true |
||||
} |
||||
s::WindowChange::Close => { |
||||
fdata.remove_focus_data(container.id); |
||||
layout::maybe_auto_tile(config); |
||||
log::debug!("Handled window event type {:?}", change); |
||||
true |
||||
} |
||||
s::WindowChange::Move | s::WindowChange::Floating => { |
||||
layout::maybe_auto_tile(config); |
||||
log::debug!("Handled window event type {:?}", change); |
||||
false // We don't affect the extra_props state here.
|
||||
} |
||||
_ => { |
||||
log::debug!("Unhandled window event type {:?}", change); |
||||
false |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn handle_workspace_event( |
||||
ev: Box<s::WorkspaceEvent>, |
||||
fdata: &FocusData, |
||||
focus_val: u64, |
||||
) -> bool { |
||||
let s::WorkspaceEvent { |
||||
change, |
||||
current, |
||||
old: _, |
||||
.. |
||||
} = *ev; |
||||
match change { |
||||
s::WorkspaceChange::Init | s::WorkspaceChange::Focus => { |
||||
let id = current |
||||
.expect("No current in Init or Focus workspace event") |
||||
.id; |
||||
fdata.send(FocusMessage::FocusEvent(FocusEvent { |
||||
node_id: id, |
||||
ev_focus_ctr: focus_val, |
||||
})); |
||||
log::debug!("Handled workspace event type {:?}", change); |
||||
true |
||||
} |
||||
s::WorkspaceChange::Empty => { |
||||
fdata.remove_focus_data( |
||||
current.expect("No current in Empty workspace event").id, |
||||
); |
||||
log::debug!("Handled workspace event type {:?}", change); |
||||
true |
||||
} |
||||
_ => false, |
||||
} |
||||
} |
||||
|
||||
pub fn serve_client_requests(fdata: FocusData) { |
||||
match std::fs::remove_file(util::get_swayr_socket_path()) { |
||||
Ok(()) => log::debug!("Deleted stale socket from previous run."), |
||||
Err(e) => log::error!("Could not delete socket:\n{:?}", e), |
||||
} |
||||
|
||||
match UnixListener::bind(util::get_swayr_socket_path()) { |
||||
Ok(listener) => { |
||||
for stream in listener.incoming() { |
||||
match stream { |
||||
Ok(stream) => { |
||||
handle_client_request(stream, &fdata); |
||||
} |
||||
Err(err) => { |
||||
log::error!("Error handling client request: {}", err); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Err(err) => { |
||||
log::error!("Could not bind socket: {}", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn handle_client_request(mut stream: UnixStream, fdata: &FocusData) { |
||||
let mut cmd_str = String::new(); |
||||
if stream.read_to_string(&mut cmd_str).is_ok() { |
||||
if let Ok(cmd) = serde_json::from_str::<cmds::SwayrCommand>(&cmd_str) { |
||||
cmds::exec_swayr_cmd(cmds::ExecSwayrCmdArgs { |
||||
cmd: &cmd, |
||||
focus_data: fdata, |
||||
}); |
||||
} else { |
||||
log::error!( |
||||
"Could not serialize following string to SwayrCommand.\n{}", |
||||
cmd_str |
||||
); |
||||
} |
||||
} else { |
||||
log::error!("Could not read command from client."); |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
enum InhibitState { |
||||
FocusInhibit, |
||||
FocusInhibitUntil(Instant), |
||||
FocusActive, |
||||
} |
||||
|
||||
impl InhibitState { |
||||
pub fn is_inhibited(&self) -> bool { |
||||
match self { |
||||
InhibitState::FocusActive => false, |
||||
InhibitState::FocusInhibit => true, |
||||
InhibitState::FocusInhibitUntil(t) => &Instant::now() < t, |
||||
} |
||||
} |
||||
|
||||
pub fn set(&mut self, timeout: Option<Duration>) { |
||||
*self = match timeout { |
||||
None => match *self { |
||||
InhibitState::FocusInhibit => InhibitState::FocusInhibit, |
||||
_ => { |
||||
log::debug!("Inhibiting tick focus updates"); |
||||
InhibitState::FocusInhibit |
||||
} |
||||
}, |
||||
Some(d) => { |
||||
let new_time = Instant::now() + d; |
||||
match *self { |
||||
// Inhibit only ever gets extended unless clear() is called
|
||||
InhibitState::FocusInhibit => InhibitState::FocusInhibit, |
||||
InhibitState::FocusInhibitUntil(old_time) => { |
||||
if old_time > new_time { |
||||
InhibitState::FocusInhibitUntil(old_time) |
||||
} else { |
||||
log::debug!( |
||||
"Extending tick focus updates inhibit by {}ms", |
||||
(new_time - old_time).as_millis() |
||||
); |
||||
InhibitState::FocusInhibitUntil(new_time) |
||||
} |
||||
} |
||||
InhibitState::FocusActive => { |
||||
log::debug!( |
||||
"Inhibiting tick focus updates for {}ms", |
||||
d.as_millis() |
||||
); |
||||
InhibitState::FocusInhibitUntil(new_time) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
pub fn clear(&mut self) { |
||||
if self.is_inhibited() { |
||||
log::debug!("Activating tick focus updates"); |
||||
} |
||||
*self = InhibitState::FocusActive; |
||||
} |
||||
} |
||||
|
||||
fn focus_lock_in_handler( |
||||
focus_chan: mpsc::Receiver<FocusMessage>, |
||||
fdata: FocusData, |
||||
lockin_delay: Duration, |
||||
sequence_timeout: Option<Duration>, |
||||
) { |
||||
// Focus event that has not yet been locked-in to the LRU order
|
||||
let mut pending_fev: Option<FocusEvent> = None; |
||||
|
||||
// Toggle to inhibit LRU focus updates
|
||||
let mut inhibit = InhibitState::FocusActive; |
||||
|
||||
let update_focus = |fev: Option<FocusEvent>| { |
||||
if let Some(fev) = fev { |
||||
log::debug!("Locking-in focus on {}", fev.node_id); |
||||
fdata.update_last_focus_tick(fev.node_id, fev.ev_focus_ctr) |
||||
} |
||||
}; |
||||
|
||||
// outer loop, waiting for focus events
|
||||
loop { |
||||
let fmsg = match focus_chan.recv() { |
||||
Ok(fmsg) => fmsg, |
||||
Err(mpsc::RecvError) => return, |
||||
}; |
||||
|
||||
let mut fev = match fmsg { |
||||
FocusMessage::TickUpdateInhibit => { |
||||
inhibit.set(sequence_timeout); |
||||
continue; |
||||
} |
||||
FocusMessage::TickUpdateActivate => { |
||||
inhibit.clear(); |
||||
update_focus(pending_fev.take()); |
||||
continue; |
||||
} |
||||
FocusMessage::FocusEvent(fev) => { |
||||
if inhibit.is_inhibited() { |
||||
// update the pending event but take no further action
|
||||
pending_fev = Some(fev); |
||||
continue; |
||||
} |
||||
fev |
||||
} |
||||
}; |
||||
|
||||
// Inner loop, waiting for the lock-in delay to expire
|
||||
loop { |
||||
let fmsg = match focus_chan.recv_timeout(lockin_delay) { |
||||
Ok(fmsg) => fmsg, |
||||
Err(mpsc::RecvTimeoutError::Timeout) => { |
||||
update_focus(Some(fev)); |
||||
break; // return to outer loop
|
||||
} |
||||
Err(mpsc::RecvTimeoutError::Disconnected) => return, |
||||
}; |
||||
|
||||
match fmsg { |
||||
FocusMessage::TickUpdateInhibit => { |
||||
// inhibit requested before currently focused container
|
||||
// was locked-in, set it as pending in case no other
|
||||
// focus changes are made while updates remain inhibited
|
||||
inhibit.set(sequence_timeout); |
||||
pending_fev = Some(fev); |
||||
break; // return to outer loop with a preset pending_fev
|
||||
} |
||||
FocusMessage::TickUpdateActivate => { |
||||
// updates reactivated while we were waiting to lockin
|
||||
// Immediately lockin fev
|
||||
inhibit.clear(); |
||||
update_focus(Some(fev)); |
||||
break; |
||||
} |
||||
FocusMessage::FocusEvent(new_fev) => { |
||||
// start a new wait (inner) loop with the most recent
|
||||
// focus event
|
||||
fev = new_fev; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,77 +0,0 @@ |
||||
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Structure to hold window focus timestamps used by swayrd
|
||||
|
||||
use std::collections::HashMap; |
||||
use std::sync::mpsc; |
||||
use std::sync::Arc; |
||||
use std::sync::RwLock; |
||||
|
||||
/// Data tracking most recent focus events for Sway windows/containers
|
||||
#[derive(Clone)] |
||||
pub struct FocusData { |
||||
pub focus_tick_by_id: Arc<RwLock<HashMap<i64, u64>>>, |
||||
pub focus_chan: mpsc::Sender<FocusMessage>, |
||||
} |
||||
|
||||
impl FocusData { |
||||
pub fn last_focus_tick(&self, id: i64) -> u64 { |
||||
*self.focus_tick_by_id.read().unwrap().get(&id).unwrap_or(&0) |
||||
} |
||||
|
||||
pub fn update_last_focus_tick(&self, id: i64, focus_val: u64) { |
||||
let mut write_lock = self.focus_tick_by_id.write().unwrap(); |
||||
if let Some(tick) = write_lock.get_mut(&id) { |
||||
*tick = focus_val; |
||||
} |
||||
// else the node has since been closed before this focus event got locked in
|
||||
} |
||||
|
||||
pub fn remove_focus_data(&self, id: i64) { |
||||
self.focus_tick_by_id.write().unwrap().remove(&id); |
||||
} |
||||
|
||||
/// Ensures that a given node_id is present in the ExtraProps map, this
|
||||
/// later used to distinguish between the case where a container was
|
||||
/// closed (it will no longer be in the map) or
|
||||
pub fn ensure_id(&self, id: i64) { |
||||
let mut write_lock = self.focus_tick_by_id.write().unwrap(); |
||||
if write_lock.get(&id).is_none() { |
||||
write_lock.insert(id, 0); |
||||
} |
||||
} |
||||
|
||||
pub fn send(&self, fmsg: FocusMessage) { |
||||
// todo can this be removed?
|
||||
if let FocusMessage::FocusEvent(ref fev) = fmsg { |
||||
self.ensure_id(fev.node_id); |
||||
} |
||||
self.focus_chan |
||||
.send(fmsg) |
||||
.expect("Failed to send focus event over channel"); |
||||
} |
||||
} |
||||
|
||||
pub struct FocusEvent { |
||||
pub node_id: i64, // node receiving the focus
|
||||
pub ev_focus_ctr: u64, // Counter for this specific focus event
|
||||
} |
||||
|
||||
pub enum FocusMessage { |
||||
TickUpdateInhibit, |
||||
TickUpdateActivate, |
||||
FocusEvent(FocusEvent), |
||||
} |
@ -1,124 +0,0 @@ |
||||
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Config file loading stuff.
|
||||
use directories::ProjectDirs; |
||||
use serde::de::DeserializeOwned; |
||||
use serde::Serialize; |
||||
use std::fs::{DirBuilder, OpenOptions}; |
||||
use std::io::{Read, Write}; |
||||
use std::path::Path; |
||||
|
||||
pub fn get_config_file_path(project: &str) -> Box<Path> { |
||||
let proj_dirs = ProjectDirs::from("", "", project).expect(""); |
||||
let user_config_dir = proj_dirs.config_dir(); |
||||
if !user_config_dir.exists() { |
||||
let sys_path = format!("/etc/xdg/{}/config.toml", project); |
||||
let sys_config_file = Path::new(sys_path.as_str()); |
||||
if sys_config_file.exists() { |
||||
return sys_config_file.into(); |
||||
} |
||||
DirBuilder::new() |
||||
.recursive(true) |
||||
.create(user_config_dir) |
||||
.unwrap(); |
||||
} |
||||
user_config_dir.join("config.toml").into_boxed_path() |
||||
} |
||||
|
||||
pub fn save_config<T>(project: &str, cfg: T) |
||||
where |
||||
T: Serialize, |
||||
{ |
||||
let path = get_config_file_path(project); |
||||
let content = |
||||
toml::to_string_pretty::<T>(&cfg).expect("Cannot serialize config."); |
||||
let mut file = OpenOptions::new() |
||||
.read(false) |
||||
.write(true) |
||||
.create(true) |
||||
.open(path) |
||||
.unwrap(); |
||||
file.write_all(content.as_str().as_bytes()).unwrap(); |
||||
} |
||||
|
||||
pub fn load_config<T>(project: &str) -> T |
||||
where |
||||
T: Serialize + DeserializeOwned + Default, |
||||
{ |
||||
let path = get_config_file_path(project); |
||||
if !path.exists() { |
||||
save_config(project, T::default()); |
||||
// Tell the user that a fresh default config has been created.
|
||||
std::process::Command::new("swaynag") |
||||
.arg("--background") |
||||
.arg("00FF44") |
||||
.arg("--text") |
||||
.arg("0000CC") |
||||
.arg("--message") |
||||
.arg( |
||||
if project == "swayr" { |
||||
"Welcome to swayr! ".to_owned() |
||||
+ "I've created a fresh config for use with wofi for you in " |
||||
+ &path.to_string_lossy() |
||||
+ ". Adapt it to your needs." |
||||
} else { |
||||
"Welcome to swayrbar! ".to_owned() |
||||
+ "I've created a fresh config for for you in " |
||||
+ &path.to_string_lossy() |
||||
+ ". Adapt it to your needs." |
||||
}, |
||||
) |
||||
.arg("--type") |
||||
.arg("warning") |
||||
.arg("--dismiss-button") |
||||
.arg("Thanks!") |
||||
.spawn() |
||||
.ok(); |
||||
log::debug!("Created new config in {}.", path.to_string_lossy()); |
||||
} |
||||
|
||||
load_config_file(&path) |
||||
} |
||||
|
||||
pub fn load_config_file<T>(config_file: &Path) -> T |
||||
where |
||||
T: Serialize + DeserializeOwned + Default, |
||||
{ |
||||
if !config_file.exists() { |
||||
panic!( |
||||
"Config file {} does not exist.", |
||||
config_file.to_string_lossy() |
||||
); |
||||
} else { |
||||
log::debug!("Loading config from {}.", config_file.to_string_lossy()); |
||||
} |
||||
let mut file = OpenOptions::new() |
||||
.read(true) |
||||
.write(false) |
||||
.create(false) |
||||
.open(config_file) |
||||
.unwrap(); |
||||
let mut buf: String = String::new(); |
||||
file.read_to_string(&mut buf).unwrap(); |
||||
match toml::from_str::<T>(&buf) { |
||||
Ok(cfg) => cfg, |
||||
Err(err) => { |
||||
log::error!("Invalid config: {}", err); |
||||
log::error!("Using default configuration."); |
||||
T::default() |
||||
} |
||||
} |
||||
} |
@ -1,254 +0,0 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use once_cell::sync::Lazy; |
||||
use regex::Regex; |
||||
use rt_format::{ |
||||
Format, FormatArgument, NoNamedArguments, ParsedFormat, Specifier, |
||||
}; |
||||
use std::fmt; |
||||
|
||||
pub enum FmtArg { |
||||
I64(i64), |
||||
I32(i32), |
||||
U8(u8), |
||||
F64(f64), |
||||
F32(f32), |
||||
String(String), |
||||
} |
||||
|
||||
impl From<i64> for FmtArg { |
||||
fn from(x: i64) -> FmtArg { |
||||
FmtArg::I64(x) |
||||
} |
||||
} |
||||
|
||||
impl From<i32> for FmtArg { |
||||
fn from(x: i32) -> FmtArg { |
||||
FmtArg::I32(x) |
||||
} |
||||
} |
||||
|
||||
impl From<u8> for FmtArg { |
||||
fn from(x: u8) -> FmtArg { |
||||
FmtArg::U8(x) |
||||
} |
||||
} |
||||
|
||||
impl From<f64> for FmtArg { |
||||
fn from(x: f64) -> FmtArg { |
||||
FmtArg::F64(x) |
||||
} |
||||
} |
||||
|
||||
impl From<f32> for FmtArg { |
||||
fn from(x: f32) -> FmtArg { |
||||
FmtArg::F32(x) |
||||
} |
||||
} |
||||
|
||||
impl From<&str> for FmtArg { |
||||
fn from(x: &str) -> FmtArg { |
||||
FmtArg::String(x.to_string()) |
||||
} |
||||
} |
||||
|
||||
impl From<String> for FmtArg { |
||||
fn from(x: String) -> FmtArg { |
||||
FmtArg::String(x) |
||||
} |
||||
} |
||||
|
||||
impl ToString for FmtArg { |
||||
fn to_string(&self) -> String { |
||||
match self { |
||||
FmtArg::String(x) => x.clone(), |
||||
FmtArg::I64(x) => x.to_string(), |
||||
FmtArg::I32(x) => x.to_string(), |
||||
FmtArg::U8(x) => x.to_string(), |
||||
FmtArg::F64(x) => x.to_string(), |
||||
FmtArg::F32(x) => x.to_string(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl FormatArgument for FmtArg { |
||||
fn supports_format(&self, spec: &Specifier) -> bool { |
||||
spec.format == Format::Display |
||||
} |
||||
|
||||
fn fmt_display(&self, f: &mut fmt::Formatter) -> fmt::Result { |
||||
match self { |
||||
Self::String(val) => fmt::Display::fmt(&val, f), |
||||
Self::I64(val) => fmt::Display::fmt(&val, f), |
||||
Self::I32(val) => fmt::Display::fmt(&val, f), |
||||
Self::U8(val) => fmt::Display::fmt(&val, f), |
||||
Self::F64(val) => fmt::Display::fmt(&val, f), |
||||
Self::F32(val) => fmt::Display::fmt(&val, f), |
||||
} |
||||
} |
||||
|
||||
fn fmt_debug(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_octal(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_lower_hex(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_upper_hex(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_binary(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_lower_exp(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
|
||||
fn fmt_upper_exp(&self, _f: &mut fmt::Formatter) -> fmt::Result { |
||||
Err(fmt::Error) |
||||
} |
||||
} |
||||
|
||||
pub fn rt_format(fmt: &str, arg: FmtArg, clipped_str: &str) -> String { |
||||
let arg_string = arg.to_string(); |
||||
|
||||
if let Ok(pf) = ParsedFormat::parse(fmt, &[arg], &NoNamedArguments) { |
||||
let mut s = format!("{}", pf); |
||||
|
||||
if !clipped_str.is_empty() && !s.contains(arg_string.as_str()) { |
||||
remove_last_n_chars(&mut s, clipped_str.chars().count()); |
||||
s.push_str(clipped_str); |
||||
} |
||||
s |
||||
} else { |
||||
format!("Invalid format string: {}", fmt) |
||||
} |
||||
} |
||||
|
||||
fn remove_last_n_chars(s: &mut String, n: usize) { |
||||
match s.char_indices().nth_back(n) { |
||||
Some((pos, ch)) => s.truncate(pos + ch.len_utf8()), |
||||
None => s.clear(), |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn test_format() { |
||||
assert_eq!(rt_format("{:.10}", FmtArg::from("sway"), ""), "sway"); |
||||
assert_eq!(rt_format("{:.10}", FmtArg::from("sway"), "…"), "sway"); |
||||
assert_eq!(rt_format("{:.4}", FmtArg::from("𝔰𝔴𝔞𝔶"), "……"), "𝔰𝔴𝔞𝔶"); |
||||
|
||||
assert_eq!(rt_format("{:.3}", FmtArg::from("sway"), ""), "swa"); |
||||
assert_eq!(rt_format("{:.3}", FmtArg::from("sway"), "…"), "sw…"); |
||||
assert_eq!( |
||||
rt_format("{:.5}", FmtArg::from("𝔰𝔴𝔞𝔶 𝔴𝔦𝔫𝔡𝔬𝔴"), "…?"), |
||||
"𝔰𝔴𝔞…?" |
||||
); |
||||
assert_eq!( |
||||
rt_format("{:.5}", FmtArg::from("sway window"), "..."), |
||||
"sw..." |
||||
); |
||||
assert_eq!(rt_format("{:.2}", FmtArg::from("sway"), "..."), "..."); |
||||
} |
||||
|
||||
pub static PLACEHOLDER_RX: Lazy<Regex> = Lazy::new(|| { |
||||
Regex::new( |
||||
r"\{(?P<name>[^}:]+)(?::(?P<fmtstr>\{[^}]*\})(?P<clipstr>[^}]*))?\}", |
||||
) |
||||
.unwrap() |
||||
}); |
||||
|
||||
#[test] |
||||
fn test_placeholder_rx() { |
||||
let caps = PLACEHOLDER_RX.captures("Hello, {place}!").unwrap(); |
||||
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
||||
assert_eq!(caps.name("fmtstr"), None); |
||||
assert_eq!(caps.name("clipstr"), None); |
||||
|
||||
let caps = PLACEHOLDER_RX.captures("Hi, {place:{:>10.10}}!").unwrap(); |
||||
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
||||
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:>10.10}"); |
||||
assert_eq!(caps.name("clipstr").unwrap().as_str(), ""); |
||||
|
||||
let caps = PLACEHOLDER_RX.captures("Hello, {place:{:.5}…}!").unwrap(); |
||||
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
||||
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:.5}"); |
||||
assert_eq!(caps.name("clipstr").unwrap().as_str(), "…"); |
||||
|
||||
let caps = PLACEHOLDER_RX.captures("Hello, {place:{:.5}...}!").unwrap(); |
||||
assert_eq!(caps.name("name").unwrap().as_str(), "place"); |
||||
assert_eq!(caps.name("fmtstr").unwrap().as_str(), "{:.5}"); |
||||
assert_eq!(caps.name("clipstr").unwrap().as_str(), "..."); |
||||
} |
||||
|
||||
pub fn maybe_html_escape(do_it: bool, text: String) -> String { |
||||
if do_it { |
||||
text.replace('<', "<") |
||||
.replace('>', ">") |
||||
.replace('&', "&") |
||||
} else { |
||||
text |
||||
} |
||||
} |
||||
|
||||
macro_rules! subst_placeholders { |
||||
( $fmt_str:expr, $html_escape:expr, |
||||
{ $( $($pat:pat_param)|+ => $exp:expr, )+ } |
||||
) => { |
||||
$crate::shared::fmt::PLACEHOLDER_RX |
||||
.replace_all($fmt_str, |caps: ®ex::Captures| { |
||||
let value: String = match &caps["name"] { |
||||
$( |
||||
$( $pat )|+ => { |
||||
let val = $crate::shared::fmt::FmtArg::from($exp); |
||||
let fmt_str = caps.name("fmtstr") |
||||
.map_or("{}", |m| m.as_str()); |
||||
let clipped_str = caps.name("clipstr") |
||||
.map_or("", |m| m.as_str()); |
||||
$crate::shared::fmt::maybe_html_escape( |
||||
$html_escape, |
||||
$crate::shared::fmt::rt_format(fmt_str, val, clipped_str), |
||||
) |
||||
} |
||||
)+ |
||||
_ => caps[0].to_string(), |
||||
}; |
||||
value |
||||
}).into() |
||||
}; |
||||
} |
||||
|
||||
pub(crate) use subst_placeholders; |
||||
|
||||
#[test] |
||||
fn test_subst_placeholders() { |
||||
let foo = "{a}, {b} = {d}"; |
||||
let html_escape = true; |
||||
let x: String = subst_placeholders!(foo, html_escape, { |
||||
"a" => "1".to_string(), |
||||
"b" | "d" => "2".to_string(), |
||||
"c" => "3".to_owned(), |
||||
}); |
||||
|
||||
assert_eq!("1, 2 = 2", x); |
||||
} |
@ -1,175 +0,0 @@ |
||||
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Basic sway IPC.
|
||||
|
||||
use std::{cell::RefCell, sync::Mutex}; |
||||
|
||||
use once_cell::sync::Lazy; |
||||
use swayipc as s; |
||||
|
||||
static SWAY_IPC_CONNECTION: Lazy<Mutex<RefCell<s::Connection>>> = |
||||
Lazy::new(|| { |
||||
Mutex::new(RefCell::new( |
||||
s::Connection::new().expect("Could not open sway IPC connection."), |
||||
)) |
||||
}); |
||||
|
||||
pub fn get_root_node(include_scratchpad: bool) -> s::Node { |
||||
let mut root = match SWAY_IPC_CONNECTION.lock() { |
||||
Ok(cell) => cell.borrow_mut().get_tree().expect("Couldn't get tree"), |
||||
Err(err) => panic!("{}", err), |
||||
}; |
||||
|
||||
if !include_scratchpad { |
||||
root.nodes.retain(|o| !o.is_scratchpad()); |
||||
} |
||||
root |
||||
} |
||||
|
||||
/// Immutable Node Iterator
|
||||
///
|
||||
/// Iterates nodes in depth-first order, tiled nodes before floating nodes.
|
||||
pub struct NodeIter<'a> { |
||||
stack: Vec<&'a s::Node>, |
||||
} |
||||
|
||||
impl<'a> NodeIter<'a> { |
||||
pub fn new(node: &'a s::Node) -> NodeIter { |
||||
NodeIter { stack: vec![node] } |
||||
} |
||||
} |
||||
|
||||
impl<'a> Iterator for NodeIter<'a> { |
||||
type Item = &'a s::Node; |
||||
|
||||
fn next(&mut self) -> Option<Self::Item> { |
||||
if let Some(node) = self.stack.pop() { |
||||
for n in &node.floating_nodes { |
||||
self.stack.push(n); |
||||
} |
||||
for n in &node.nodes { |
||||
self.stack.push(n); |
||||
} |
||||
Some(node) |
||||
} else { |
||||
None |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)] |
||||
pub enum Type { |
||||
Root, |
||||
Output, |
||||
Workspace, |
||||
Container, |
||||
Window, |
||||
} |
||||
|
||||
/// Extension methods for [`swayipc::Node`].
|
||||
pub trait NodeMethods { |
||||
fn iter(&self) -> NodeIter; |
||||
fn get_type(&self) -> Type; |
||||
fn get_app_name(&self) -> &str; |
||||
fn nodes_of_type(&self, t: Type) -> Vec<&s::Node>; |
||||
fn get_name(&self) -> &str; |
||||
fn is_scratchpad(&self) -> bool; |
||||
fn is_floating(&self) -> bool; |
||||
fn is_current(&self) -> bool; |
||||
} |
||||
|
||||
impl NodeMethods for s::Node { |
||||
fn iter(&self) -> NodeIter { |
||||
NodeIter::new(self) |
||||
} |
||||
|
||||
fn get_type(&self) -> Type { |
||||
match self.node_type { |
||||
s::NodeType::Root => Type::Root, |
||||
s::NodeType::Output => Type::Output, |
||||
s::NodeType::Workspace => Type::Workspace, |
||||
s::NodeType::FloatingCon => Type::Window, |
||||
_ => { |
||||
if self.node_type == s::NodeType::Con |
||||
&& self.name.is_none() |
||||
&& self.app_id.is_none() |
||||
&& self.pid.is_none() |
||||
&& self.shell.is_none() |
||||
&& self.window_properties.is_none() |
||||
&& self.layout != s::NodeLayout::None |
||||
{ |
||||
Type::Container |
||||
} else if (self.node_type == s::NodeType::Con |
||||
|| self.node_type == s::NodeType::FloatingCon) |
||||
// Apparently there can be windows without app_id, name,
|
||||
// and window_properties.class, e.g., dolphin-emu-nogui.
|
||||
&& self.pid.is_some() |
||||
// FIXME: While technically correct, old sway versions (up to
|
||||
// at least sway-1.4) don't expose shell in IPC. So comment in
|
||||
// again when all major distros have a recent enough sway
|
||||
// package.
|
||||
//&& self.shell.is_some()
|
||||
{ |
||||
Type::Window |
||||
} else { |
||||
panic!( |
||||
"Don't know type of node with id {} and node_type {:?}\n{:?}", |
||||
self.id, self.node_type, self |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn get_name(&self) -> &str { |
||||
if let Some(name) = &self.name { |
||||
name.as_ref() |
||||
} else { |
||||
"<unnamed>" |
||||
} |
||||
} |
||||
|
||||
fn get_app_name(&self) -> &str { |
||||
if let Some(app_id) = &self.app_id { |
||||
app_id |
||||
} else if let Some(wp_class) = self |
||||
.window_properties |
||||
.as_ref() |
||||
.and_then(|wp| wp.class.as_ref()) |
||||
{ |
||||
wp_class |
||||
} else { |
||||
"<unknown_app>" |
||||
} |
||||
} |
||||
|
||||
fn is_scratchpad(&self) -> bool { |
||||
let name = self.get_name(); |
||||
name.eq("__i3") || name.eq("__i3_scratch") |
||||
} |
||||
|
||||
fn nodes_of_type(&self, t: Type) -> Vec<&s::Node> { |
||||
self.iter().filter(|n| n.get_type() == t).collect() |
||||
} |
||||
|
||||
fn is_floating(&self) -> bool { |
||||
self.node_type == s::NodeType::FloatingCon |
||||
} |
||||
|
||||
fn is_current(&self) -> bool { |
||||
self.iter().any(|n| n.focused) |
||||
} |
||||
} |
@ -1,18 +0,0 @@ |
||||
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
pub mod cfg; |
||||
pub mod fmt; |
||||
pub mod ipc; |
@ -1,411 +0,0 @@ |
||||
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Convenience data structures built from the IPC structs.
|
||||
|
||||
use crate::config; |
||||
use crate::focus::FocusData; |
||||
use crate::shared::fmt::subst_placeholders; |
||||
use crate::shared::ipc; |
||||
use crate::shared::ipc::NodeMethods; |
||||
use crate::util; |
||||
use crate::util::DisplayFormat; |
||||
use once_cell::sync::Lazy; |
||||
use regex::Regex; |
||||
use std::cell::RefCell; |
||||
use std::cmp; |
||||
use std::collections::HashMap; |
||||
use std::rc::Rc; |
||||
use swayipc as s; |
||||
|
||||
pub struct Tree<'a> { |
||||
root: &'a s::Node, |
||||
id_node: HashMap<i64, &'a s::Node>, |
||||
id_parent: HashMap<i64, i64>, |
||||
} |
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)] |
||||
enum IndentLevel { |
||||
Fixed(usize), |
||||
WorkspacesZeroWindowsOne, |
||||
TreeDepth(usize), |
||||
} |
||||
|
||||
pub struct DisplayNode<'a> { |
||||
pub node: &'a s::Node, |
||||
pub tree: &'a Tree<'a>, |
||||
indent_level: IndentLevel, |
||||
} |
||||
|
||||
impl<'a> Tree<'a> { |
||||
fn get_node_by_id(&self, id: i64) -> &&s::Node { |
||||
self.id_node |
||||
.get(&id) |
||||
.unwrap_or_else(|| panic!("No node with id {}", id)) |
||||
} |
||||
|
||||
fn get_parent_node(&self, id: i64) -> Option<&&s::Node> { |
||||
self.id_parent.get(&id).map(|pid| self.get_node_by_id(*pid)) |
||||
} |
||||
|
||||
pub fn get_parent_node_of_type( |
||||
&self, |
||||
id: i64, |
||||
t: ipc::Type, |
||||
) -> Option<&&s::Node> { |
||||
let n = self.get_node_by_id(id); |
||||
if n.get_type() == t { |
||||
Some(n) |
||||
} else if let Some(pid) = self.id_parent.get(&id) { |
||||
self.get_parent_node_of_type(*pid, t) |
||||
} else { |
||||
None |
||||
} |
||||
} |
||||
|
||||
fn sorted_nodes_of_type_1( |
||||
&self, |
||||
node: &'a s::Node, |
||||
t: ipc::Type, |
||||
fdata: &FocusData, |
||||
) -> Vec<&s::Node> { |
||||
let mut v: Vec<&s::Node> = node.nodes_of_type(t); |
||||
self.sort_by_urgency_and_lru_time_1(&mut v, fdata); |
||||
v |
||||
} |
||||
|
||||
fn sorted_nodes_of_type( |
||||
&self, |
||||
t: ipc::Type, |
||||
fdata: &FocusData, |
||||
) -> Vec<&s::Node> { |
||||
self.sorted_nodes_of_type_1(self.root, t, fdata) |
||||
} |
||||
|
||||
fn as_display_nodes( |
||||
&self, |
||||
v: &[&'a s::Node], |
||||
indent_level: IndentLevel, |
||||
) -> Vec<DisplayNode> { |
||||
v.iter() |
||||
.map(|node| DisplayNode { |
||||
node, |
||||
tree: self, |
||||
indent_level, |
||||
}) |
||||
.collect() |
||||
} |
||||
|
||||
pub fn get_current_workspace(&self) -> &s::Node { |
||||
self.root |
||||
.iter() |
||||
.find(|n| n.get_type() == ipc::Type::Workspace && n.is_current()) |
||||
.expect("No current Workspace") |
||||
} |
||||
|
||||
pub fn get_outputs(&self) -> Vec<DisplayNode> { |
||||
let outputs: Vec<&s::Node> = self |
||||
.root |
||||
.iter() |
||||
.filter(|n| n.get_type() == ipc::Type::Output && !n.is_scratchpad()) |
||||
.collect(); |
||||
self.as_display_nodes(&outputs, IndentLevel::Fixed(0)) |
||||
} |
||||
|
||||
pub fn get_workspaces(&self, fdata: &FocusData) -> Vec<DisplayNode> { |
||||
let mut v = self.sorted_nodes_of_type(ipc::Type::Workspace, fdata); |
||||
if !v.is_empty() { |
||||
v.rotate_left(1); |
||||
} |
||||
self.as_display_nodes(&v, IndentLevel::Fixed(0)) |
||||
} |
||||
|
||||
pub fn get_windows(&self, fdata: &FocusData) -> Vec<DisplayNode> { |
||||
let mut v = self.sorted_nodes_of_type(ipc::Type::Window, fdata); |
||||
// Rotate, but only non-urgent windows. Those should stay at the front
|
||||
// as they are the most likely switch candidates.
|
||||
let mut x; |
||||
if !v.is_empty() { |
||||
x = vec![]; |
||||
loop { |
||||
if !v.is_empty() && v[0].urgent { |
||||
x.push(v.remove(0)); |
||||
} else { |
||||
break; |
||||
} |
||||
} |
||||
if !v.is_empty() { |
||||
v.rotate_left(1); |
||||
x.append(&mut v); |
||||
} |
||||
} else { |
||||
x = v; |
||||
} |
||||
self.as_display_nodes(&x, IndentLevel::Fixed(0)) |
||||
} |
||||
|
||||
pub fn get_workspaces_and_windows( |
||||
&self, |
||||
fdata: &FocusData, |
||||
) -> Vec<DisplayNode> { |
||||
let workspaces = self.sorted_nodes_of_type(ipc::Type::Workspace, fdata); |
||||
let mut first = true; |
||||
let mut v = vec![]; |
||||
for ws in workspaces { |
||||
v.push(ws); |
||||
let mut wins = |
||||
self.sorted_nodes_of_type_1(ws, ipc::Type::Window, fdata); |
||||
if first && !wins.is_empty() { |
||||
wins.rotate_left(1); |
||||
first = false; |
||||
} |
||||
v.append(&mut wins); |
||||
} |
||||
|
||||
self.as_display_nodes(&v, IndentLevel::WorkspacesZeroWindowsOne) |
||||
} |
||||
|
||||
fn sort_by_urgency_and_lru_time_1( |
||||
&self, |
||||
v: &mut [&s::Node], |
||||
fdata: &FocusData, |
||||
) { |
||||
v.sort_by(|a, b| { |
||||
if a.urgent && !b.urgent { |
||||
cmp::Ordering::Less |
||||
} else if !a.urgent && b.urgent { |
||||
cmp::Ordering::Greater |
||||
} else { |
||||
let lru_a = fdata.last_focus_tick(a.id); |
||||
let lru_b = fdata.last_focus_tick(b.id); |
||||
lru_a.cmp(&lru_b).reverse() |
||||
} |
||||
}); |
||||
} |
||||
|
||||
fn push_subtree_sorted( |
||||
&self, |
||||
n: &'a s::Node, |
||||
v: Rc<RefCell<Vec<&'a s::Node>>>, |
||||
fdata: &FocusData, |
||||
) { |
||||
v.borrow_mut().push(n); |
||||
|
||||
let mut children: Vec<&s::Node> = n.nodes.iter().collect(); |
||||
children.append(&mut n.floating_nodes.iter().collect()); |
||||
self.sort_by_urgency_and_lru_time_1(&mut children, fdata); |
||||
|
||||
for c in children { |
||||
self.push_subtree_sorted(c, Rc::clone(&v), fdata); |
||||
} |
||||
} |
||||
|
||||
pub fn get_outputs_workspaces_containers_and_windows( |
||||
&self, |
||||
fdata: &FocusData, |
||||
) -> Vec<DisplayNode> { |
||||
let outputs = self.sorted_nodes_of_type(ipc::Type::Output, fdata); |
||||
let v: Rc<RefCell<Vec<&s::Node>>> = Rc::new(RefCell::new(vec![])); |
||||
for o in outputs { |
||||
self.push_subtree_sorted(o, Rc::clone(&v), fdata); |
||||
} |
||||
|
||||
let x = self.as_display_nodes(&*v.borrow(), IndentLevel::TreeDepth(1)); |
||||
x |
||||
} |
||||
|
||||
pub fn get_workspaces_containers_and_windows( |
||||
&self, |
||||
fdata: &FocusData, |
||||
) -> Vec<DisplayNode> { |
||||
let workspaces = self.sorted_nodes_of_type(ipc::Type::Workspace, fdata); |
||||
let v: Rc<RefCell<Vec<&s::Node>>> = Rc::new(RefCell::new(vec![])); |
||||
for ws in workspaces { |
||||
self.push_subtree_sorted(ws, Rc::clone(&v), fdata); |
||||
} |
||||
|
||||
let x = self.as_display_nodes(&*v.borrow(), IndentLevel::TreeDepth(2)); |
||||
x |
||||
} |
||||
|
||||
pub fn is_child_of_tiled_container(&self, id: i64) -> bool { |
||||
match self.get_parent_node(id) { |
||||
Some(n) => { |
||||
n.layout == s::NodeLayout::SplitH |
||||
|| n.layout == s::NodeLayout::SplitV |
||||
} |
||||
None => false, |
||||
} |
||||
} |
||||
|
||||
pub fn is_child_of_tabbed_or_stacked_container(&self, id: i64) -> bool { |
||||
match self.get_parent_node(id) { |
||||
Some(n) => { |
||||
n.layout == s::NodeLayout::Tabbed |
||||
|| n.layout == s::NodeLayout::Stacked |
||||
} |
||||
None => false, |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn init_id_parent<'a>( |
||||
n: &'a s::Node, |
||||
parent: Option<&'a s::Node>, |
||||
id_node: &mut HashMap<i64, &'a s::Node>, |
||||
id_parent: &mut HashMap<i64, i64>, |
||||
) { |
||||
id_node.insert(n.id, n); |
||||
|
||||
if let Some(p) = parent { |
||||
id_parent.insert(n.id, p.id); |
||||
} |
||||
|
||||
for c in &n.nodes { |
||||
init_id_parent(c, Some(n), id_node, id_parent); |
||||
} |
||||
for c in &n.floating_nodes { |
||||
init_id_parent(c, Some(n), id_node, id_parent); |
||||
} |
||||
} |
||||
|
||||
pub fn get_tree(root: &s::Node) -> Tree { |
||||
let mut id_node: HashMap<i64, &s::Node> = HashMap::new(); |
||||
let mut id_parent: HashMap<i64, i64> = HashMap::new(); |
||||
init_id_parent(root, None, &mut id_node, &mut id_parent); |
||||
|
||||
Tree { |
||||
root, |
||||
id_node, |
||||
id_parent, |
||||
} |
||||
} |
||||
|
||||
static APP_NAME_AND_VERSION_RX: Lazy<Regex> = |
||||
Lazy::new(|| Regex::new("(.+)(-[0-9.]+)").unwrap()); |
||||
|
||||
fn format_marks(marks: &[String]) -> String { |
||||
if marks.is_empty() { |
||||
"".to_string() |
||||
} else { |
||||
format!("[{}]", marks.join(", ")) |
||||
} |
||||
} |
||||
|
||||
impl DisplayFormat for DisplayNode<'_> { |
||||
fn format_for_display(&self, cfg: &config::Config) -> String { |
||||
let indent = cfg.get_format_indent(); |
||||
let html_escape = cfg.get_format_html_escape(); |
||||
let urgency_start = cfg.get_format_urgency_start(); |
||||
let urgency_end = cfg.get_format_urgency_end(); |
||||
let icon_dirs = cfg.get_format_icon_dirs(); |
||||
// fallback_icon has no default value.
|
||||
let fallback_icon: Option<Box<std::path::Path>> = cfg |
||||
.get_format_fallback_icon() |
||||
.as_ref() |
||||
.map(|i| std::path::Path::new(i).to_owned().into_boxed_path()); |
||||
|
||||
let app_name_no_version = |
||||
APP_NAME_AND_VERSION_RX.replace(self.node.get_app_name(), "$1"); |
||||
|
||||
let fmt = match self.node.get_type() { |
||||
ipc::Type::Root => String::from("Cannot format Root"), |
||||
ipc::Type::Output => cfg.get_format_output_format(), |
||||
ipc::Type::Workspace => cfg.get_format_workspace_format(), |
||||
ipc::Type::Container => cfg.get_format_container_format(), |
||||
ipc::Type::Window => cfg.get_format_window_format(), |
||||
}; |
||||
let fmt = fmt |
||||
.replace( |
||||
"{indent}", |
||||
indent.repeat(self.get_indent_level()).as_str(), |
||||
) |
||||
.replace( |
||||
"{urgency_start}", |
||||
if self.node.urgent { |
||||
urgency_start.as_str() |
||||
} else { |
||||
"" |
||||
}, |
||||
) |
||||
.replace( |
||||
"{urgency_end}", |
||||
if self.node.urgent { |
||||
urgency_end.as_str() |
||||
} else { |
||||
"" |
||||
}, |
||||
) |
||||
.replace( |
||||
"{app_icon}", |
||||
util::get_icon(self.node.get_app_name(), &icon_dirs) |
||||
.or_else(|| { |
||||
util::get_icon(&app_name_no_version, &icon_dirs) |
||||
}) |
||||
.or_else(|| { |
||||
util::get_icon( |
||||
&app_name_no_version.to_lowercase(), |
||||
&icon_dirs, |
||||
) |
||||
}) |
||||
.or(fallback_icon) |
||||
.map(|i| i.to_string_lossy().into_owned()) |
||||
.unwrap_or_else(String::new) |
||||
.as_str(), |
||||
); |
||||
|
||||
subst_placeholders!(&fmt, html_escape, { |
||||
"id" => self.node.id, |
||||
"app_name" => self.node.get_app_name(), |
||||
"layout" => format!("{:?}", self.node.layout), |
||||
"name" | "title" => self.node.get_name(), |
||||
"output_name" => self |
||||
.tree |
||||
.get_parent_node_of_type(self.node.id, ipc::Type::Output) |
||||
.map_or("<no_output>", |w| w.get_name()), |
||||
"workspace_name" => self |
||||
.tree |
||||
.get_parent_node_of_type(self.node.id, ipc::Type::Workspace) |
||||
.map_or("<no_workspace>", |w| w.get_name()), |
||||
"marks" => format_marks(&self.node.marks), |
||||
}) |
||||
} |
||||
|
||||
fn get_indent_level(&self) -> usize { |
||||
match self.indent_level { |
||||
IndentLevel::Fixed(level) => level as usize, |
||||
IndentLevel::WorkspacesZeroWindowsOne => { |
||||
match self.node.get_type(){ |
||||
ipc::Type::Workspace => 0, |
||||
ipc::Type::Window => 1, |
||||
_ => panic!("Only Workspaces and Windows expected. File a bug report!") |
||||
} |
||||
} |
||||
IndentLevel::TreeDepth(offset) => { |
||||
let mut depth: usize = 0; |
||||
let mut node = self.node; |
||||
while let Some(p) = self.tree.get_parent_node(node.id) { |
||||
depth += 1; |
||||
node = p; |
||||
} |
||||
if offset > depth { |
||||
0 |
||||
} else { |
||||
depth - offset as usize |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,26 +0,0 @@ |
||||
[package] |
||||
name = "swayrbar" |
||||
version = "0.2.2" |
||||
edition = "2021" |
||||
homepage = "https://sr.ht/~tsdh/swayr/#swayrbar" |
||||
repository = "https://git.sr.ht/~tsdh/swayr" |
||||
description = "A swaybar-protocol implementation for sway/swaybar" |
||||
authors = ["Tassilo Horn <tsdh@gnu.org>"] |
||||
license = "GPL-3.0+" |
||||
|
||||
[dependencies] |
||||
clap = {version = "3.0.0", features = ["derive"] } |
||||
battery = "0.7.8" |
||||
chrono = "0.4" |
||||
directories = "4.0" |
||||
env_logger = { version = "0.9.0", default-features = false, features = ["termcolor", "atty", "humantime"] } # without regex |
||||
log = "0.4" |
||||
once_cell = "1.10.0" |
||||
regex = "1.5.5" |
||||
rt-format = "0.3.0" |
||||
serde = { version = "1.0.126", features = ["derive"] } |
||||
serde_json = "1.0.64" |
||||
swaybar-types = "3.0.0" |
||||
swayipc = "3.0.0" |
||||
sysinfo = "0.23" |
||||
toml = "0.5.8" |
@ -1,15 +0,0 @@ |
||||
swayrbar 0.2.0 |
||||
============== |
||||
|
||||
- If a window module is used, subscribe to sway events in order to immediately |
||||
refresh it on window/workspace changes. |
||||
|
||||
swayrbar 0.1.1 |
||||
============== |
||||
|
||||
- Only refresh the module which received the click event. |
||||
|
||||
swayrbar 0.1.0 |
||||
============== |
||||
|
||||
- Add pactl module. |
@ -1 +0,0 @@ |
||||
../README.md |
@ -1,317 +0,0 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! `swayrbar` lib.
|
||||
|
||||
use crate::config; |
||||
use crate::module; |
||||
use crate::module::{BarModuleFn, NameInstanceAndReason, RefreshReason}; |
||||
use env_logger::Env; |
||||
use serde_json; |
||||
use std::io; |
||||
use std::path::Path; |
||||
use std::process as p; |
||||
use std::sync::mpsc::sync_channel; |
||||
use std::sync::mpsc::Receiver; |
||||
use std::sync::mpsc::SyncSender; |
||||
use std::time::Duration; |
||||
use std::{sync::Arc, thread}; |
||||
use swaybar_types as sbt; |
||||
use swayipc as si; |
||||
|
||||
#[derive(clap::Parser)] |
||||
#[clap(about, version, author)] |
||||
pub struct Opts { |
||||
#[clap(
|
||||
short = 'c', |
||||
long, |
||||
help = "Path to a config.toml configuration file. |
||||
If not specified, the default config ~/.config/swayrbar/config.toml or |
||||
/etc/xdg/swayrbar/config.toml is used." |
||||
)] |
||||
config_file: Option<String>, |
||||
} |
||||
|
||||
pub fn start(opts: Opts) { |
||||
env_logger::Builder::from_env(Env::default().default_filter_or("warn")) |
||||
.init(); |
||||
|
||||
let config = match opts.config_file { |
||||
None => config::load_config(), |
||||
Some(config_file) => { |
||||
let path = Path::new(&config_file); |
||||
crate::shared::cfg::load_config_file(path) |
||||
} |
||||
}; |
||||
let refresh_interval = config.refresh_interval; |
||||
let mods: Arc<Vec<Box<dyn BarModuleFn>>> = Arc::new(create_modules(config)); |
||||
let mods_for_input = mods.clone(); |
||||
|
||||
let (sender, receiver) = sync_channel(16); |
||||
let sender_for_ticker = sender.clone(); |
||||
thread::spawn(move || { |
||||
tick_periodically(refresh_interval, sender_for_ticker) |
||||
}); |
||||
|
||||
let sender_for_input = sender.clone(); |
||||
thread::spawn(move || handle_input(mods_for_input, sender_for_input)); |
||||
|
||||
let window_mods: Vec<(String, String)> = mods |
||||
.iter() |
||||
.filter(|m| m.get_config().name == "window") |
||||
.map(|m| (m.get_config().name.clone(), m.get_config().instance.clone())) |
||||
.collect(); |
||||
if !window_mods.is_empty() { |
||||
// There's at least one window module, so subscribe to focus events for
|
||||
// immediate refreshes.
|
||||
thread::spawn(move || handle_sway_events(window_mods, sender)); |
||||
} |
||||
|
||||
generate_status(&mods, receiver); |
||||
} |
||||
|
||||
fn tick_periodically( |
||||
refresh_interval: u64, |
||||
sender: SyncSender<Option<NameInstanceAndReason>>, |
||||
) { |
||||
loop { |
||||
send_refresh_event(&sender, None); |
||||
thread::sleep(Duration::from_millis(refresh_interval)); |
||||
} |
||||
} |
||||
|
||||
fn create_modules(config: config::Config) -> Vec<Box<dyn BarModuleFn>> { |
||||
let mut mods = vec![]; |
||||
for mc in config.modules { |
||||
let m = match mc.name.as_str() { |
||||
"window" => module::window::BarModuleWindow::create(mc), |
||||
"sysinfo" => module::sysinfo::BarModuleSysInfo::create(mc), |
||||
"battery" => module::battery::BarModuleBattery::create(mc), |
||||
"date" => module::date::BarModuleDate::create(mc), |
||||
"pactl" => module::pactl::BarModulePactl::create(mc), |
||||
unknown => { |
||||
log::warn!("Unknown module name '{}'. Ignoring...", unknown); |
||||
continue; |
||||
} |
||||
}; |
||||
mods.push(m); |
||||
} |
||||
mods |
||||
} |
||||
|
||||
fn handle_input( |
||||
mods: Arc<Vec<Box<dyn BarModuleFn>>>, |
||||
sender: SyncSender<Option<NameInstanceAndReason>>, |
||||
) { |
||||
let mut sb = String::new(); |
||||
io::stdin() |
||||
.read_line(&mut sb) |
||||
.expect("Could not read from stdin"); |
||||
|
||||
if "[\n" != sb { |
||||
log::error!("Expected [\\n but got {}", sb); |
||||
log::error!("Sorry, input events won't work is this session."); |
||||
return; |
||||
} |
||||
|
||||
loop { |
||||
let mut buf = String::new(); |
||||
if let Err(err) = io::stdin().read_line(&mut buf) { |
||||
log::error!("Error while reading from stdin: {}", err); |
||||
log::error!("Skipping this input line..."); |
||||
continue; |
||||
} |
||||
|
||||
let click = match serde_json::from_str::<sbt::Click>( |
||||
buf.strip_prefix(',').unwrap_or(&buf), |
||||
) { |
||||
Ok(click) => click, |
||||
Err(err) => { |
||||
log::error!("Error while parsing str to Click: {}", err); |
||||
log::error!("The string was '{}'.", buf); |
||||
log::error!("Skipping this input line..."); |
||||
continue; |
||||
} |
||||
}; |
||||
log::debug!("Received click: {:?}", click); |
||||
let event = handle_click(click, mods.clone()); |
||||
if event.is_some() { |
||||
send_refresh_event(&sender, event); |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn send_refresh_event( |
||||
sender: &SyncSender<Option<NameInstanceAndReason>>, |
||||
event: Option<NameInstanceAndReason>, |
||||
) { |
||||
if event.is_some() { |
||||
log::debug!("Sending refresh event {:?}", event); |
||||
} |
||||
if let Err(err) = sender.send(event) { |
||||
log::error!("Error at send: {}", err); |
||||
} |
||||
} |
||||
|
||||
fn handle_click( |
||||
click: sbt::Click, |
||||
mods: Arc<Vec<Box<dyn BarModuleFn>>>, |
||||
) -> Option<NameInstanceAndReason> { |
||||
let name = click.name?; |
||||
let instance = click.instance?; |
||||
let button_str = format!("{:?}", click.button); |
||||
for m in mods.iter() { |
||||
if let Some(on_click) = m.get_on_click_map(&name, &instance) { |
||||
if let Some(cmd) = on_click.get(&button_str) { |
||||
match m.subst_args(cmd) { |
||||
Some(cmd) => execute_command(&cmd), |
||||
None => execute_command(cmd), |
||||
} |
||||
let cfg = m.get_config(); |
||||
// No refresh for click events for window modules because the
|
||||
// refresh will be triggered by a sway event anyhow.
|
||||
//
|
||||
// TODO: That's too much coupling. The bar module shouldn't do
|
||||
// specific stuff for certain modules.
|
||||
if cfg.name == module::window::NAME { |
||||
return None; |
||||
} |
||||
return Some(( |
||||
cfg.name.clone(), |
||||
cfg.instance.clone(), |
||||
RefreshReason::ClickEvent, |
||||
)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
None |
||||
} |
||||
|
||||
fn execute_command(cmd: &[String]) { |
||||
log::debug!("Executing command: {:?}", cmd); |
||||
match p::Command::new(&cmd[0]).args(&cmd[1..]).status() { |
||||
Ok(exit_status) => { |
||||
// TODO: Better use exit_ok() once that has stabilized.
|
||||
if !exit_status.success() { |
||||
log::warn!( |
||||
"Command finished with status code {:?}.", |
||||
exit_status.code() |
||||
) |
||||
} |
||||
} |
||||
Err(err) => { |
||||
log::error!("Error running shell command '{}':", cmd.join(" ")); |
||||
log::error!("{}", err); |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn sway_subscribe() -> si::Fallible<si::EventStream> { |
||||
si::Connection::new()?.subscribe(&[ |
||||
si::EventType::Window, |
||||
si::EventType::Shutdown, |
||||
si::EventType::Workspace, |
||||
]) |
||||
} |
||||
|
||||
fn handle_sway_events( |
||||
window_mods: Vec<(String, String)>, |
||||
sender: SyncSender<Option<NameInstanceAndReason>>, |
||||
) { |
||||
let mut resets = 0; |
||||
let max_resets = 10; |
||||
|
||||
'reset: loop { |
||||
if resets >= max_resets { |
||||
break; |
||||
} |
||||
resets += 1; |
||||
|
||||
log::debug!("Connecting to sway for subscribing to events..."); |
||||
|
||||
match sway_subscribe() { |
||||
Err(err) => { |
||||
log::warn!("Could not connect and subscribe: {}", err); |
||||
std::thread::sleep(std::time::Duration::from_secs(3)); |
||||
} |
||||
Ok(iter) => { |
||||
for ev_result in iter { |
||||
resets = 0; |
||||
match ev_result { |
||||
Ok(ev) => match ev { |
||||
si::Event::Window(_) | si::Event::Workspace(_) => { |
||||
log::trace!( |
||||
"Window or Workspace event: {:?}", |
||||
ev |
||||
); |
||||
for m in &window_mods { |
||||
let event = Some(( |
||||
m.0.to_owned(), |
||||
m.1.to_owned(), |
||||
RefreshReason::SwayEvent, |
||||
)); |
||||
send_refresh_event(&sender, event); |
||||
} |
||||
} |
||||
si::Event::Shutdown(sd_ev) => { |
||||
log::debug!( |
||||
"Sway shuts down with reason '{:?}'.", |
||||
sd_ev.change |
||||
); |
||||
break 'reset; |
||||
} |
||||
_ => (), |
||||
}, |
||||
Err(e) => { |
||||
log::warn!("Error while receiving events: {}", e); |
||||
std::thread::sleep(std::time::Duration::from_secs( |
||||
3, |
||||
)); |
||||
log::warn!("Resetting!"); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn generate_status_1( |
||||
mods: &[Box<dyn BarModuleFn>], |
||||
name_and_instance: &Option<NameInstanceAndReason>, |
||||
) { |
||||
let mut blocks = vec![]; |
||||
for m in mods { |
||||
blocks.push(m.build(name_and_instance)); |
||||
} |
||||
let json = serde_json::to_string_pretty(&blocks) |
||||
.unwrap_or_else(|_| "".to_string()); |
||||
println!("{},", json); |
||||
} |
||||
|
||||
fn generate_status( |
||||
mods: &[Box<dyn BarModuleFn>], |
||||
receiver: Receiver<Option<NameInstanceAndReason>>, |
||||
) { |
||||
println!("{{\"version\": 1, \"click_events\": true}}"); |
||||
// status_command should output an infinite array meaning we emit an
|
||||
// opening [ and never the closing bracket.
|
||||
println!("["); |
||||
|
||||
for ev in receiver.iter() { |
||||
generate_status_1(mods, &ev) |
||||
} |
||||
} |
@ -1,24 +0,0 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The `swayrbar` binary.
|
||||
|
||||
use clap::Parser; |
||||
use swayrbar::bar::Opts; |
||||
|
||||
fn main() { |
||||
let opts: Opts = Opts::parse(); |
||||
swayrbar::bar::start(opts); |
||||
} |
@ -1,80 +0,0 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! TOML configuration for swayrbar.
|
||||
|
||||
use crate::module::BarModuleFn; |
||||
use crate::shared::cfg; |
||||
use serde::{Deserialize, Serialize}; |
||||
use std::collections::HashMap; |
||||
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub struct Config { |
||||
/// The status is refreshed every `refresh_interval` milliseconds.
|
||||
pub refresh_interval: u64, |
||||
/// The list of modules to display in the given order, each one specified
|
||||
/// as `"<module_type>/<instance>"`.
|
||||
pub modules: Vec<ModuleConfig>, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub struct ModuleConfig { |
||||
pub name: String, |
||||
pub instance: String, |
||||
pub format: String, |
||||
pub html_escape: Option<bool>, |
||||
pub on_click: Option<HashMap<String, Vec<String>>>, |
||||
} |
||||
|
||||
impl ModuleConfig { |
||||
pub fn is_html_escape(&self) -> bool { |
||||
self.html_escape.unwrap_or(false) |
||||
} |
||||
} |
||||
|
||||
impl Default for Config { |
||||
fn default() -> Self { |
||||
Config { |
||||
refresh_interval: 1000, |
||||
modules: vec![ |
||||
crate::module::window::BarModuleWindow::default_config( |
||||
"0".to_owned(), |
||||
), |
||||
crate::module::sysinfo::BarModuleSysInfo::default_config( |
||||
"0".to_owned(), |
||||
), |
||||
crate::module::battery::BarModuleBattery::default_config( |
||||
"0".to_owned(), |
||||
), |
||||
crate::module::pactl::BarModulePactl::default_config( |
||||
"0".to_owned(), |
||||
), |
||||
crate::module::date::BarModuleDate::default_config( |
||||
"0".to_owned(), |
||||
), |
||||
], |
||||
} |
||||
} |
||||
} |
||||
|
||||
pub fn load_config() -> Config { |
||||
cfg::load_config::<Config>("swayrbar") |
||||
} |
||||
|
||||
#[test] |
||||
fn test_load_swayrbar_config() { |
||||
let cfg = cfg::load_config::<Config>("swayrbar"); |
||||
println!("{:?}", cfg); |
||||
} |
@ -1,19 +0,0 @@ |
||||
// Copyright (C) 2021-2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
pub mod bar; |
||||
pub mod config; |
||||
pub mod module; |
||||
pub mod shared; |
@ -1,79 +0,0 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::collections::HashMap; |
||||
|
||||
use crate::config; |
||||
use swaybar_types as s; |
||||
|
||||
pub mod battery; |
||||
pub mod date; |
||||
pub mod pactl; |
||||
pub mod sysinfo; |
||||
pub mod window; |
||||
|
||||
#[derive(Debug, PartialEq)] |
||||
pub enum RefreshReason { |
||||
ClickEvent, |
||||
SwayEvent, |
||||
} |
||||
|
||||
pub type NameInstanceAndReason = (String, String, RefreshReason); |
||||
|
||||
pub trait BarModuleFn: Sync + Send { |
||||
fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn> |
||||
where |
||||
Self: Sized; |
||||
|
||||
fn default_config(instance: String) -> config::ModuleConfig |
||||
where |
||||
Self: Sized; |
||||
|
||||
fn get_config(&self) -> &config::ModuleConfig; |
||||
|
||||
fn get_on_click_map( |
||||
&self, |
||||
name: &str, |
||||
instance: &str, |
||||
) -> Option<&HashMap<String, Vec<String>>> { |
||||
let cfg = self.get_config(); |
||||
if name == cfg.name && instance == cfg.instance { |
||||
cfg.on_click.as_ref() |
||||
} else { |
||||
None |
||||
} |
||||
} |
||||
|
||||
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block; |
||||
|
||||
fn should_refresh( |
||||
&self, |
||||
nai: &Option<NameInstanceAndReason>, |
||||
periodic: bool, |
||||
reasons: &[RefreshReason], |
||||
) -> bool { |
||||
let cfg = self.get_config(); |
||||
match nai { |
||||
None => periodic, |
||||
Some((n, i, r)) => { |
||||
n == &cfg.name |
||||
&& i == &cfg.instance |
||||
&& reasons.iter().any(|x| x == r) |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn subst_args<'a>(&'a self, _cmd: &'a [String]) -> Option<Vec<String>>; |
||||
} |
@ -1,174 +0,0 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The date `swayrbar` module.
|
||||
|
||||
use crate::config; |
||||
use crate::module::{BarModuleFn, NameInstanceAndReason}; |
||||
use crate::shared::fmt::subst_placeholders; |
||||
use battery as bat; |
||||
use std::collections::HashSet; |
||||
use std::sync::Mutex; |
||||
use swaybar_types as s; |
||||
|
||||
const NAME: &str = "battery"; |
||||
|
||||
struct State { |
||||
state_of_charge: f32, |
||||
state_of_health: f32, |
||||
state: String, |
||||
cached_text: String, |
||||
} |
||||
|
||||
pub struct BarModuleBattery { |
||||
config: config::ModuleConfig, |
||||
state: Mutex<State>, |
||||
} |
||||
|
||||
fn get_refreshed_batteries( |
||||
manager: &bat::Manager, |
||||
) -> Result<Vec<bat::Battery>, bat::Error> { |
||||
let mut bats = vec![]; |
||||
for bat in manager.batteries()? { |
||||
let mut bat = bat?; |
||||
if manager.refresh(&mut bat).is_ok() { |
||||
bats.push(bat); |
||||
} |
||||
} |
||||
|
||||
Ok(bats) |
||||
} |
||||
|
||||
fn refresh_state(state: &mut State, fmt_str: &str, html_escape: bool) { |
||||
// FIXME: Creating the Manager on every refresh is bad but internally
|
||||
// it uses an Rc so if I keep it as a field of BarModuleBattery, that
|
||||
// cannot be Sync.
|
||||
let manager = battery::Manager::new().unwrap(); |
||||
match get_refreshed_batteries(&manager) { |
||||
Ok(bats) => { |
||||
state.state_of_charge = |
||||
bats.iter().map(|b| b.state_of_charge().value).sum::<f32>() |
||||
/ bats.len() as f32 |
||||
* 100_f32; |
||||
state.state_of_health = |
||||
bats.iter().map(|b| b.state_of_health().value).sum::<f32>() |
||||
/ bats.len() as f32 |
||||
* 100_f32; |
||||
state.state = { |
||||
let states = bats |
||||
.iter() |
||||
.map(|b| format!("{:?}", b.state())) |
||||
.collect::<HashSet<String>>(); |
||||
if states.len() == 1 { |
||||
states.iter().next().unwrap().to_owned() |
||||
} else { |
||||
let mut comma_sep_string = String::from("["); |
||||
let mut first = true; |
||||
for state in states { |
||||
if first { |
||||
comma_sep_string = comma_sep_string + &state; |
||||
first = false; |
||||
} else { |
||||
comma_sep_string = comma_sep_string + ", " + &state; |
||||
} |
||||
} |
||||
comma_sep_string += "]"; |
||||
comma_sep_string |
||||
} |
||||
}; |
||||
state.cached_text = subst_placeholders(fmt_str, html_escape, state); |
||||
} |
||||
Err(err) => { |
||||
log::error!("Could not update battery state: {}", err); |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn subst_placeholders(fmt: &str, html_escape: bool, state: &State) -> String { |
||||
subst_placeholders!(fmt, html_escape, { |
||||
"state_of_charge" => state.state_of_charge, |
||||
"state_of_health" => state.state_of_health, |
||||
"state" => state.state.as_str(), |
||||
}) |
||||
} |
||||
|
||||
impl BarModuleFn for BarModuleBattery { |
||||
fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn> { |
||||
Box::new(BarModuleBattery { |
||||
config, |
||||
state: Mutex::new(State { |
||||
state_of_charge: 0.0, |
||||
state_of_health: 0.0, |
||||
state: "Unknown".to_owned(), |
||||
cached_text: String::new(), |
||||
}), |
||||
}) |
||||
} |
||||
|
||||
fn default_config(instance: String) -> config::ModuleConfig { |
||||
config::ModuleConfig { |
||||
name: NAME.to_owned(), |
||||
instance, |
||||
format: "🔋 Bat: {state_of_charge:{:5.1}}%, {state}, Health: {state_of_health:{:5.1}}%".to_owned(), |
||||
html_escape: Some(false), |
||||
on_click: None, |
||||
} |
||||
} |
||||
|
||||
fn get_config(&self) -> &config::ModuleConfig { |
||||
&self.config |
||||
} |
||||
|
||||
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block { |
||||
let mut state = self.state.lock().expect("Could not lock state."); |
||||
|
||||
if self.should_refresh(nai, true, &[]) { |
||||
refresh_state( |
||||
&mut state, |
||||
&self.config.format, |
||||
self.get_config().is_html_escape(), |
||||
); |
||||
} |
||||
|
||||
s::Block { |
||||
name: Some(NAME.to_owned()), |
||||
instance: Some(self.config.instance.clone()), |
||||
full_text: state.cached_text.to_owned(), |
||||
align: Some(s::Align::Left), |
||||
markup: Some(s::Markup::Pango), |
||||
short_text: None, |
||||
color: None, |
||||
background: None, |
||||
border: None, |
||||
border_top: None, |
||||
border_bottom: None, |
||||
border_left: None, |
||||
border_right: None, |
||||
min_width: None, |
||||
urgent: None, |
||||
separator: Some(true), |
||||
separator_block_width: None, |
||||
} |
||||
} |
||||
|
||||
fn subst_args<'a>(&'a self, cmd: &'a [String]) -> Option<Vec<String>> { |
||||
let state = self.state.lock().expect("Could not lock state."); |
||||
Some( |
||||
cmd.iter() |
||||
.map(|arg| subst_placeholders(arg, false, &state)) |
||||
.collect(), |
||||
) |
||||
} |
||||
} |
@ -1,94 +0,0 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The date `swayrbar` module.
|
||||
|
||||
use std::sync::Mutex; |
||||
|
||||
use crate::module::config; |
||||
use crate::module::{BarModuleFn, NameInstanceAndReason}; |
||||
use swaybar_types as s; |
||||
|
||||
const NAME: &str = "date"; |
||||
|
||||
struct State { |
||||
cached_text: String, |
||||
} |
||||
|
||||
pub struct BarModuleDate { |
||||
config: config::ModuleConfig, |
||||
state: Mutex<State>, |
||||
} |
||||
|
||||
fn chrono_format(s: &str) -> String { |
||||
chrono::Local::now().format(s).to_string() |
||||
} |
||||
|
||||
impl BarModuleFn for BarModuleDate { |
||||
fn create(cfg: config::ModuleConfig) -> Box<dyn BarModuleFn> { |
||||
Box::new(BarModuleDate { |
||||
config: cfg, |
||||
state: Mutex::new(State { |
||||
cached_text: String::new(), |
||||
}), |
||||
}) |
||||
} |
||||
|
||||
fn default_config(instance: String) -> config::ModuleConfig { |
||||
config::ModuleConfig { |
||||
name: NAME.to_owned(), |
||||
instance, |
||||
format: "⏰ %F %X".to_owned(), |
||||
html_escape: Some(false), |
||||
on_click: None, |
||||
} |
||||
} |
||||
|
||||
fn get_config(&self) -> &config::ModuleConfig { |
||||
&self.config |
||||
} |
||||
|
||||
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block { |
||||
let mut state = self.state.lock().expect("Could not lock state."); |
||||
|
||||
if self.should_refresh(nai, true, &[]) { |
||||
state.cached_text = chrono_format(&self.config.format); |
||||
} |
||||
|
||||
s::Block { |
||||
name: Some(NAME.to_owned()), |
||||
instance: Some(self.config.instance.clone()), |
||||
full_text: state.cached_text.to_owned(), |
||||
align: Some(s::Align::Left), |
||||
markup: Some(s::Markup::Pango), |
||||
short_text: None, |
||||
color: None, |
||||
background: None, |
||||
border: None, |
||||
border_top: None, |
||||
border_bottom: None, |
||||
border_left: None, |
||||
border_right: None, |
||||
min_width: None, |
||||
urgent: None, |
||||
separator: Some(true), |
||||
separator_block_width: None, |
||||
} |
||||
} |
||||
|
||||
fn subst_args<'a>(&'a self, cmd: &'a [String]) -> Option<Vec<String>> { |
||||
Some(cmd.iter().map(|arg| chrono_format(arg)).collect()) |
||||
} |
||||
} |
@ -1,190 +0,0 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The pactl `swayrbar` module.
|
||||
|
||||
use crate::config; |
||||
use crate::module::{BarModuleFn, NameInstanceAndReason}; |
||||
use crate::shared::fmt::subst_placeholders; |
||||
use once_cell::sync::Lazy; |
||||
use regex::Regex; |
||||
use std::collections::HashMap; |
||||
use std::process::Command; |
||||
use std::sync::Mutex; |
||||
use swaybar_types as s; |
||||
|
||||
use super::RefreshReason; |
||||
|
||||
const NAME: &str = "pactl"; |
||||
|
||||
struct State { |
||||
volume: u8, |
||||
muted: bool, |
||||
cached_text: String, |
||||
} |
||||
|
||||
pub static VOLUME_RX: Lazy<Regex> = |
||||
Lazy::new(|| Regex::new(r".?* (\d+)%.*").unwrap()); |
||||
|
||||
fn run_pactl(args: &[&str]) -> String { |
||||
match Command::new("pactl").args(args).output() { |
||||
Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(), |
||||
Err(err) => { |
||||
log::error!("Could not run pactl: {}", err); |
||||
String::new() |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn get_volume() -> u8 { |
||||
let output = run_pactl(&["get-sink-volume", "@DEFAULT_SINK@"]); |
||||
VOLUME_RX |
||||
.captures(&output) |
||||
.map(|c| c.get(1).unwrap().as_str().parse::<u8>().unwrap()) |
||||
.unwrap_or(255_u8) |
||||
} |
||||
|
||||
fn get_mute_state() -> bool { |
||||
run_pactl(&["get-sink-mute", "@DEFAULT_SINK@"]).contains("yes") |
||||
} |
||||
|
||||
pub struct BarModulePactl { |
||||
config: config::ModuleConfig, |
||||
state: Mutex<State>, |
||||
} |
||||
|
||||
fn refresh_state(state: &mut State, fmt_str: &str, html_escape: bool) { |
||||
state.volume = get_volume(); |
||||
state.muted = get_mute_state(); |
||||
state.cached_text = subst_placeholders(fmt_str, html_escape, state); |
||||
} |
||||
|
||||
fn subst_placeholders(fmt: &str, html_escape: bool, state: &State) -> String { |
||||
subst_placeholders!(fmt, html_escape, { |
||||
"volume" => { |
||||
state.volume |
||||
}, |
||||
"muted" =>{ |
||||
if state.muted { |
||||
" muted" |
||||
} else { |
||||
"" |
||||
} |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
impl BarModuleFn for BarModulePactl { |
||||
fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn> |
||||
where |
||||
Self: Sized, |
||||
{ |
||||
Box::new(BarModulePactl { |
||||
config, |
||||
state: Mutex::new(State { |
||||
volume: 255_u8, |
||||
muted: false, |
||||
cached_text: String::new(), |
||||
}), |
||||
}) |
||||
} |
||||
|
||||
fn default_config(instance: String) -> config::ModuleConfig |
||||
where |
||||
Self: Sized, |
||||
{ |
||||
config::ModuleConfig { |
||||
name: NAME.to_owned(), |
||||
instance, |
||||
format: "🔈 Vol: {volume:{:3}}%{muted}".to_owned(), |
||||
html_escape: Some(true), |
||||
on_click: Some(HashMap::from([ |
||||
("Left".to_owned(), vec!["pavucontrol".to_owned()]), |
||||
( |
||||
"Right".to_owned(), |
||||
vec![ |
||||
"pactl".to_owned(), |
||||
"set-sink-mute".to_owned(), |
||||
"@DEFAULT_SINK@".to_owned(), |
||||
"toggle".to_owned(), |
||||
], |
||||
), |
||||
( |
||||
"WheelUp".to_owned(), |
||||
vec![ |
||||
"pactl".to_owned(), |
||||
"set-sink-volume".to_owned(), |
||||
"@DEFAULT_SINK@".to_owned(), |
||||
"+1%".to_owned(), |
||||
], |
||||
), |
||||
( |
||||
"WheelDown".to_owned(), |
||||
vec![ |
||||
"pactl".to_owned(), |
||||
"set-sink-volume".to_owned(), |
||||
"@DEFAULT_SINK@".to_owned(), |
||||
"-1%".to_owned(), |
||||
], |
||||
), |
||||
])), |
||||
} |
||||
} |
||||
|
||||
fn get_config(&self) -> &config::ModuleConfig { |
||||
&self.config |
||||
} |
||||
|
||||
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block { |
||||
let mut state = self.state.lock().expect("Could not lock state."); |
||||
|
||||
if self.should_refresh(nai, true, &[RefreshReason::ClickEvent]) { |
||||
refresh_state( |
||||
&mut state, |
||||
&self.config.format, |
||||
self.config.is_html_escape(), |
||||
); |
||||
} |
||||
|
||||
s::Block { |
||||
name: Some(NAME.to_owned()), |
||||
instance: Some(self.config.instance.clone()), |
||||
full_text: state.cached_text.to_owned(), |
||||
align: Some(s::Align::Left), |
||||
markup: Some(s::Markup::Pango), |
||||
short_text: None, |
||||
color: None, |
||||
background: None, |
||||
border: None, |
||||
border_top: None, |
||||
border_bottom: None, |
||||
border_left: None, |
||||
border_right: None, |
||||
min_width: None, |
||||
urgent: None, |
||||
separator: Some(true), |
||||
separator_block_width: None, |
||||
} |
||||
} |
||||
|
||||
fn subst_args<'a>(&'a self, cmd: &'a [String]) -> Option<Vec<String>> { |
||||
let state = self.state.lock().expect("Could not lock state."); |
||||
Some( |
||||
cmd.iter() |
||||
.map(|arg| subst_placeholders(arg, false, &state)) |
||||
.collect(), |
||||
) |
||||
} |
||||
} |
@ -1,198 +0,0 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The date `swayrbar` module.
|
||||
|
||||
use crate::config; |
||||
use crate::module::{BarModuleFn, NameInstanceAndReason}; |
||||
use crate::shared::fmt::subst_placeholders; |
||||
use std::collections::HashMap; |
||||
use std::sync::Mutex; |
||||
use std::sync::Once; |
||||
use swaybar_types as s; |
||||
use sysinfo as si; |
||||
use sysinfo::ProcessorExt; |
||||
use sysinfo::SystemExt; |
||||
|
||||
const NAME: &str = "sysinfo"; |
||||
|
||||
struct State { |
||||
cpu_usage: f32, |
||||
mem_usage: f64, |
||||
load_avg_1: f64, |
||||
load_avg_5: f64, |
||||
load_avg_15: f64, |
||||
cached_text: String, |
||||
} |
||||
|
||||
pub struct BarModuleSysInfo { |
||||
config: config::ModuleConfig, |
||||
system: Mutex<si::System>, |
||||
state: Mutex<State>, |
||||
} |
||||
|
||||
struct OnceRefresher { |
||||
cpu: Once, |
||||
memory: Once, |
||||
} |
||||
|
||||
impl OnceRefresher { |
||||
fn new() -> OnceRefresher { |
||||
OnceRefresher { |
||||
cpu: Once::new(), |
||||
memory: Once::new(), |
||||
} |
||||
} |
||||
|
||||
fn refresh_cpu(&self, sys: &mut si::System) { |
||||
self.cpu.call_once(|| sys.refresh_cpu()); |
||||
} |
||||
|
||||
fn refresh_memory(&self, sys: &mut si::System) { |
||||
self.memory.call_once(|| sys.refresh_memory()); |
||||
} |
||||
} |
||||
|
||||
fn get_cpu_usage(sys: &mut si::System, upd: &OnceRefresher) -> f32 { |
||||
upd.refresh_cpu(sys); |
||||
sys.global_processor_info().cpu_usage() |
||||
} |
||||
|
||||
fn get_memory_usage(sys: &mut si::System, upd: &OnceRefresher) -> f64 { |
||||
upd.refresh_memory(sys); |
||||
sys.used_memory() as f64 * 100_f64 / sys.total_memory() as f64 |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
enum LoadAvg { |
||||
One, |
||||
Five, |
||||
Fifteen, |
||||
} |
||||
|
||||
fn get_load_average( |
||||
sys: &mut si::System, |
||||
avg: LoadAvg, |
||||
upd: &OnceRefresher, |
||||
) -> f64 { |
||||
upd.refresh_cpu(sys); |
||||
let load_avg = sys.load_average(); |
||||
match avg { |
||||
LoadAvg::One => load_avg.one, |
||||
LoadAvg::Five => load_avg.five, |
||||
LoadAvg::Fifteen => load_avg.fifteen, |
||||
} |
||||
} |
||||
|
||||
fn refresh_state( |
||||
sys: &mut si::System, |
||||
state: &mut State, |
||||
fmt_str: &str, |
||||
html_escape: bool, |
||||
) { |
||||
let updater = OnceRefresher::new(); |
||||
state.cpu_usage = get_cpu_usage(sys, &updater); |
||||
state.mem_usage = get_memory_usage(sys, &updater); |
||||
state.load_avg_1 = get_load_average(sys, LoadAvg::One, &updater); |
||||
state.load_avg_5 = get_load_average(sys, LoadAvg::Five, &updater); |
||||
state.load_avg_15 = get_load_average(sys, LoadAvg::Fifteen, &updater); |
||||
state.cached_text = subst_placeholders(fmt_str, html_escape, state); |
||||
} |
||||
|
||||
fn subst_placeholders(fmt: &str, html_escape: bool, state: &State) -> String { |
||||
subst_placeholders!(fmt, html_escape, { |
||||
"cpu_usage" => state.cpu_usage, |
||||
"mem_usage" => state.mem_usage, |
||||
"load_avg_1" => state.load_avg_1, |
||||
"load_avg_5" => state.load_avg_5, |
||||
"load_avg_15" => state.load_avg_15, |
||||
}) |
||||
} |
||||
|
||||
impl BarModuleFn for BarModuleSysInfo { |
||||
fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn> { |
||||
Box::new(BarModuleSysInfo { |
||||
config, |
||||
system: Mutex::new(si::System::new_all()), |
||||
state: Mutex::new(State { |
||||
cpu_usage: 0.0, |
||||
mem_usage: 0.0, |
||||
load_avg_1: 0.0, |
||||
load_avg_5: 0.0, |
||||
load_avg_15: 0.0, |
||||
cached_text: String::new(), |
||||
}), |
||||
}) |
||||
} |
||||
|
||||
fn default_config(instance: String) -> config::ModuleConfig { |
||||
config::ModuleConfig { |
||||
name: NAME.to_owned(), |
||||
instance, |
||||
format: "💻 CPU: {cpu_usage:{:5.1}}% Mem: {mem_usage:{:5.1}}% Load: {load_avg_1:{:5.2}} / {load_avg_5:{:5.2}} / {load_avg_15:{:5.2}}".to_owned(), |
||||
html_escape: Some(false), |
||||
on_click: Some(HashMap::from([ |
||||
("Left".to_owned(), |
||||
vec!["foot".to_owned(), "htop".to_owned()])])), |
||||
} |
||||
} |
||||
|
||||
fn get_config(&self) -> &config::ModuleConfig { |
||||
&self.config |
||||
} |
||||
|
||||
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block { |
||||
let mut sys = self.system.lock().expect("Could not lock state."); |
||||
let mut state = self.state.lock().expect("Could not lock state."); |
||||
|
||||
if self.should_refresh(nai, true, &[]) { |
||||
refresh_state( |
||||
&mut sys, |
||||
&mut state, |
||||
&self.config.format, |
||||
self.config.is_html_escape(), |
||||
); |
||||
} |
||||
|
||||
s::Block { |
||||
name: Some(NAME.to_owned()), |
||||
instance: Some(self.config.instance.clone()), |
||||
full_text: state.cached_text.to_owned(), |
||||
align: Some(s::Align::Left), |
||||
markup: Some(s::Markup::Pango), |
||||
short_text: None, |
||||
color: None, |
||||
background: None, |
||||
border: None, |
||||
border_top: None, |
||||
border_bottom: None, |
||||
border_left: None, |
||||
border_right: None, |
||||
min_width: None, |
||||
urgent: None, |
||||
separator: Some(true), |
||||
separator_block_width: None, |
||||
} |
||||
} |
||||
|
||||
fn subst_args<'a>(&'a self, cmd: &'a [String]) -> Option<Vec<String>> { |
||||
let state = self.state.lock().expect("Could not lock state."); |
||||
Some( |
||||
cmd.iter() |
||||
.map(|arg| subst_placeholders(arg, false, &state)) |
||||
.collect(), |
||||
) |
||||
} |
||||
} |
@ -1,162 +0,0 @@ |
||||
// Copyright (C) 2022 Tassilo Horn <tsdh@gnu.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! The window `swayrbar` module.
|
||||
|
||||
use std::collections::HashMap; |
||||
use std::sync::Mutex; |
||||
|
||||
use crate::config; |
||||
use crate::module::{BarModuleFn, NameInstanceAndReason}; |
||||
use crate::shared::fmt::subst_placeholders; |
||||
use crate::shared::ipc; |
||||
use crate::shared::ipc::NodeMethods; |
||||
use swaybar_types as s; |
||||
|
||||
use super::RefreshReason; |
||||
|
||||
pub const NAME: &str = "window"; |
||||
|
||||
const INITIAL_PID: i32 = -128; |
||||
const NO_WINDOW_PID: i32 = -1; |
||||
const UNKNOWN_PID: i32 = -2; |
||||
|
||||
struct State { |
||||
name: String, |
||||
app_name: String, |
||||
pid: i32, |
||||
cached_text: String, |
||||
} |
||||
|
||||
pub struct BarModuleWindow { |
||||
config: config::ModuleConfig, |
||||
state: Mutex<State>, |
||||
} |
||||
|
||||
fn refresh_state(state: &mut State, fmt_str: &str, html_escape: bool) { |
||||
let root = ipc::get_root_node(false); |
||||
let focused_win = root |
||||
.iter() |
||||
.find(|n| n.focused && n.get_type() == ipc::Type::Window); |
||||
|
||||
match focused_win { |
||||
Some(win) => { |
||||
state.name = win.get_name().to_owned(); |
||||
state.app_name = win.get_app_name().to_owned(); |
||||
state.pid = win.pid.unwrap_or(UNKNOWN_PID); |
||||
state.cached_text = subst_placeholders(fmt_str, html_escape, state); |
||||
} |
||||
None => { |
||||
state.name.clear(); |
||||
state.app_name.clear(); |
||||
state.pid = NO_WINDOW_PID; |
||||
state.cached_text.clear(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
fn subst_placeholders(s: &str, html_escape: bool, state: &State) -> String { |
||||
subst_placeholders!(s, html_escape, { |
||||
"title" | "name" => state.name.clone(), |
||||
"app_name" => state.app_name.clone(), |
||||
"pid" => state.pid, |
||||
}) |
||||
} |
||||
|
||||
impl BarModuleFn for BarModuleWindow { |
||||
fn create(config: config::ModuleConfig) -> Box<dyn BarModuleFn> { |
||||
Box::new(BarModuleWindow { |
||||
config, |
||||
state: Mutex::new(State { |
||||
name: String::new(), |
||||
app_name: String::new(), |
||||
pid: INITIAL_PID, |
||||
cached_text: String::new(), |
||||
}), |
||||
}) |
||||
} |
||||
|
||||
fn default_config(instance: String) -> config::ModuleConfig { |
||||
config::ModuleConfig { |
||||
name: NAME.to_owned(), |
||||
instance, |
||||
format: "🪟 {title} — {app_name}".to_owned(), |
||||
html_escape: Some(false), |
||||
on_click: Some(HashMap::from([ |
||||
( |
||||
"Left".to_owned(), |
||||
vec![ |
||||
"swayr".to_owned(), |
||||
"switch-to-urgent-or-lru-window".to_owned(), |
||||
], |
||||
), |
||||
( |
||||
"Right".to_owned(), |
||||
vec!["kill".to_owned(), "{pid}".to_owned()], |
||||
), |
||||
])), |
||||
} |
||||
} |
||||
|
||||
fn get_config(&self) -> &config::ModuleConfig { |
||||
&self.config |
||||
} |
||||
|
||||
fn build(&self, nai: &Option<NameInstanceAndReason>) -> s::Block { |
||||
let mut state = self.state.lock().expect("Could not lock state."); |
||||
|
||||
// In contrast to other modules, this one should only refresh its state
|
||||
// initially at startup and when explicitly named by `nai` (caused by a
|
||||
// window or workspace event).
|
||||
if state.pid == INITIAL_PID |
||||
|| (self.should_refresh(nai, false, &[RefreshReason::SwayEvent])) |
||||
{ |
||||
refresh_state( |
||||
&mut state, |
||||
&self.config.format, |
||||
self.config.is_html_escape(), |
||||
); |
||||
} |
||||
|
||||
s::Block { |
||||
name: Some(NAME.to_owned()), |
||||
instance: Some(self.config.instance.clone()), |
||||
full_text: state.cached_text.clone(), |
||||
align: Some(s::Align::Left), |
||||
markup: Some(s::Markup::Pango), |
||||
short_text: None, |
||||
color: None, |
||||
background: None, |
||||
border: None, |
||||
border_top: None, |
||||
border_bottom: None, |
||||
border_left: None, |
||||
border_right: None, |
||||
min_width: None, |
||||
urgent: None, |
||||
separator: Some(true), |
||||
separator_block_width: None, |
||||
} |
||||
} |
||||
|
||||
fn subst_args<'b>(&'b self, cmd: &'b [String]) -> Option<Vec<String>> { |
||||
let state = self.state.lock().expect("Could not lock state."); |
||||
let cmd = cmd |
||||
.iter() |
||||
.map(|arg| subst_placeholders(arg, false, &*state)) |
||||
.collect(); |
||||
Some(cmd) |
||||
} |
||||
} |
@ -1 +0,0 @@ |
||||
../../swayr/src/shared |
Loading…
Reference in new issue