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