Compare commits
87 Commits
timeout_ol
...
main
Author | SHA1 | Date |
---|---|---|
Taeyeon Mori | cce54affe7 | 2 years ago |
Taeyeon Mori | fbb824f83e | 2 years ago |
Taeyeon Mori | fc1f0a58a0 | 2 years ago |
Tassilo Horn | 3dfb962654 | 2 years ago |
Tassilo Horn | 6aa7d05d07 | 2 years ago |
Tassilo Horn | f0b0f71e38 | 3 years ago |
Tassilo Horn | 7ac8990341 | 3 years ago |
Tassilo Horn | 6b87e5e51d | 3 years ago |
Tassilo Horn | 90f43c0ec9 | 3 years ago |
Tassilo Horn | efb9f43496 | 3 years ago |
Tassilo Horn | 1088c95092 | 3 years ago |
Tassilo Horn | 3605214639 | 3 years ago |
Tassilo Horn | 8f5e3fc3ed | 3 years ago |
Tassilo Horn | 23ca1f7f2f | 3 years ago |
William Barsse | 5682d3dadc | 3 years ago |
Tassilo Horn | d7aa1d856c | 3 years ago |
William Barsse | 33f85b576c | 3 years ago |
William Barsse | 559bf941e5 | 3 years ago |
Tassilo Horn | f484eb0420 | 3 years ago |
Tassilo Horn | 55521a9c97 | 3 years ago |
Tassilo Horn | 7d5f1643c1 | 3 years ago |
Tassilo Horn | 56c816ee62 | 3 years ago |
Tassilo Horn | 167b753363 | 3 years ago |
William Barsse | 613b4eeaeb | 3 years ago |
Tassilo Horn | 8f06f4c196 | 3 years ago |
Tassilo Horn | 039743a29a | 3 years ago |
Tassilo Horn | a98bfc16ec | 3 years ago |
Tassilo Horn | 02be0a83de | 3 years ago |
Tassilo Horn | 2d57b48974 | 3 years ago |
Tassilo Horn | a9ba59acd2 | 3 years ago |
Tassilo Horn | 0d4e9430b3 | 3 years ago |
Tassilo Horn | fe16a2a658 | 3 years ago |
Tassilo Horn | 8afb54cec9 | 3 years ago |
Tassilo Horn | 913235a51c | 3 years ago |
Tassilo Horn | c8a3bdbf8e | 3 years ago |
Tassilo Horn | f02c8bc8ca | 3 years ago |
Tassilo Horn | 93677b195b | 3 years ago |
Tassilo Horn | 7550585867 | 3 years ago |
Tassilo Horn | 8545c24502 | 3 years ago |
Tassilo Horn | f95cb58060 | 3 years ago |
Tassilo Horn | 6b95202f3d | 3 years ago |
Tassilo Horn | a1d3cb21de | 3 years ago |
Tassilo Horn | 25ca74f2be | 3 years ago |
Tassilo Horn | faa04788cc | 3 years ago |
Tassilo Horn | 3cf075fbbf | 3 years ago |
Tassilo Horn | 59b5da7bdf | 3 years ago |
Tassilo Horn | 471a5190a2 | 3 years ago |
Tassilo Horn | b6ee541008 | 3 years ago |
Tassilo Horn | 1399e306b5 | 3 years ago |
Tassilo Horn | 6745321eeb | 3 years ago |
Tassilo Horn | e15386ad8f | 3 years ago |
Tassilo Horn | 3a3f16554c | 3 years ago |
Tassilo Horn | 96201e5fba | 3 years ago |
Tassilo Horn | 2b9d4bf198 | 3 years ago |
Tassilo Horn | 59664d3b21 | 3 years ago |
Tassilo Horn | 591915e0ed | 3 years ago |
Tassilo Horn | 35c49cba40 | 3 years ago |
Tassilo Horn | 1cb4b5cc42 | 3 years ago |
Tassilo Horn | 8d37fd95d1 | 3 years ago |
Tassilo Horn | 5e80619761 | 3 years ago |
Tassilo Horn | a2e1b3343c | 3 years ago |
Tassilo Horn | 80a8a3a262 | 3 years ago |
Tassilo Horn | 6c6d237164 | 3 years ago |
Tassilo Horn | 6e10aa4c24 | 3 years ago |
Tassilo Horn | 7035268413 | 3 years ago |
Tassilo Horn | 726d9a0a9e | 3 years ago |
Tassilo Horn | 24f5929dd8 | 3 years ago |
Tassilo Horn | 20760c898d | 3 years ago |
Tassilo Horn | 2f36e073a3 | 3 years ago |
Tassilo Horn | 4d4937d292 | 3 years ago |
Tassilo Horn | f92586630d | 3 years ago |
Tassilo Horn | 345c91a559 | 3 years ago |
Tassilo Horn | 588b7153fb | 3 years ago |
Tassilo Horn | cdb62c0ce8 | 3 years ago |
Tassilo Horn | 160b7d645c | 3 years ago |
Tassilo Horn | 5d1fbeba06 | 3 years ago |
Tassilo Horn | bf9337571a | 3 years ago |
Tassilo Horn | 9dd0a1aa10 | 3 years ago |
Tassilo Horn | 37c85880e7 | 3 years ago |
Tassilo Horn | 59fa701ab5 | 3 years ago |
Tassilo Horn | b778869ca3 | 3 years ago |
Tassilo Horn | 89959253cf | 3 years ago |
Tassilo Horn | c330a35624 | 3 years ago |
Tassilo Horn | 061c3589ac | 3 years ago |
Tassilo Horn | 6a204e619e | 3 years ago |
Tassilo Horn | 6b266902c2 | 3 years ago |
Tassilo Horn | eed12e087f | 3 years ago |
42 changed files with 3883 additions and 1447 deletions
@ -1,30 +1,9 @@ |
|||||||
[package] |
[workspace] |
||||||
name = "swayr" |
members = [ |
||||||
version = "0.16.0" |
"swayr", |
||||||
description = "A LRU window-switcher (and more) for the sway window manager" |
"swayrbar", |
||||||
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] |
[profile.release] |
||||||
lto = "thin" |
lto = "thin" |
||||||
|
strip = "symbols" |
@ -1 +1,8 @@ |
|||||||
- Switch from lazy_static to once_cell once the latter is in stable rust. |
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. |
After Width: | Height: | Size: 49 KiB |
@ -1,292 +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 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."); |
|
||||||
} |
|
||||||
} |
|
@ -1,103 +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/>.
|
|
||||||
|
|
||||||
//! 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", "..."), "..."); |
|
||||||
} |
|
@ -1,598 +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::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(), "..."); |
|
||||||
} |
|
@ -0,0 +1,23 @@ |
|||||||
|
[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,3 +1,30 @@ |
|||||||
|
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 |
swayr v0.16.0 |
||||||
============= |
============= |
||||||
|
|
@ -0,0 +1 @@ |
|||||||
|
../README.md |
@ -0,0 +1,412 @@ |
|||||||
|
// 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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,77 @@ |
|||||||
|
// 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), |
||||||
|
} |
@ -0,0 +1,124 @@ |
|||||||
|
// 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() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,254 @@ |
|||||||
|
// 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); |
||||||
|
} |
@ -0,0 +1,175 @@ |
|||||||
|
// 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) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
// 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; |
@ -0,0 +1,411 @@ |
|||||||
|
// 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 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
[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" |
@ -0,0 +1,15 @@ |
|||||||
|
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. |
@ -0,0 +1 @@ |
|||||||
|
../README.md |
@ -0,0 +1,317 @@ |
|||||||
|
// 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) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
// 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); |
||||||
|
} |
@ -0,0 +1,80 @@ |
|||||||
|
// 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); |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
// 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; |
@ -0,0 +1,79 @@ |
|||||||
|
// 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>>; |
||||||
|
} |
@ -0,0 +1,174 @@ |
|||||||
|
// 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(), |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,94 @@ |
|||||||
|
// 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()) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,190 @@ |
|||||||
|
// 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(), |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,198 @@ |
|||||||
|
// 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(), |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,162 @@ |
|||||||
|
// 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) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
../../swayr/src/shared |
Loading…
Reference in new issue