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