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