You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1160 lines
37 KiB
1160 lines
37 KiB
// 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 swayr client. |
|
|
|
use crate::config as cfg; |
|
use crate::layout; |
|
use crate::tree as t; |
|
use crate::tree::NodeMethods; |
|
use crate::util; |
|
use crate::util::DisplayFormat; |
|
use lazy_static::lazy_static; |
|
use rand::prelude::SliceRandom; |
|
use serde::{Deserialize, Serialize}; |
|
use std::collections::HashMap; |
|
use std::sync::atomic; |
|
use std::sync::Arc; |
|
use std::sync::RwLock; |
|
use swayipc as s; |
|
|
|
pub fn run_sway_command_1(cmd: &str) { |
|
log::debug!("Running sway command: {}", cmd); |
|
match s::Connection::new() { |
|
Ok(mut con) => { |
|
if let Err(err) = con.run_command(cmd) { |
|
log::error!("Could not run sway command: {}", err) |
|
} |
|
} |
|
Err(err) => panic!("{}", err), |
|
} |
|
} |
|
|
|
pub fn run_sway_command(args: &[&str]) { |
|
let cmd = args.join(" "); |
|
run_sway_command_1(&cmd); |
|
} |
|
|
|
#[derive(clap::Parser, Debug, Deserialize, Serialize, PartialEq, Clone)] |
|
pub enum ConsiderFloating { |
|
/// Include floating windows. |
|
IncludeFloating, |
|
/// Exclude floating windows. |
|
ExcludeFloating, |
|
} |
|
|
|
#[derive(clap::Parser, Debug, Deserialize, Serialize, PartialEq, Clone)] |
|
pub enum ConsiderWindows { |
|
/// Consider windows of all workspaces. |
|
AllWorkspaces, |
|
/// Consider windows of only the current workspaces. |
|
CurrentWorkspace, |
|
} |
|
|
|
#[derive(clap::Parser, Debug, Deserialize, Serialize)] |
|
pub enum SwayrCommand { |
|
/// Switch to next urgent window (if any) or to last recently used window. |
|
SwitchToUrgentOrLRUWindow, |
|
/// Switch to the given app (given by app_id or window class) if that's not |
|
/// currently focused. If it is, switch to the next urgent window (if any) |
|
/// or to last recently used window. |
|
SwitchToAppOrUrgentOrLRUWindow { name: String }, |
|
/// Focus the selected window. |
|
SwitchWindow, |
|
/// Switch to the selected workspace. |
|
SwitchWorkspace, |
|
/// Switch to the selected workspace. |
|
SwitchOutput, |
|
/// Switch to the selected workspace or focus the selected window. |
|
SwitchWorkspaceOrWindow, |
|
/// Switch to the selected workspace or focus the selected container, or |
|
/// window. |
|
SwitchWorkspaceContainerOrWindow, |
|
/// Switch to the selected output or workspace or focus the selected |
|
/// container, or window. |
|
SwitchTo, |
|
/// Quit the selected window. |
|
QuitWindow { |
|
#[clap( |
|
short, |
|
long, |
|
help = "Kill the window's process rather than just quitting it" |
|
)] |
|
kill: bool, |
|
}, |
|
/// Quit all windows of selected workspace or the selected window. |
|
QuitWorkspaceOrWindow, |
|
/// Quit all windows of selected workspace, or container or the selected |
|
/// window. |
|
QuitWorkspaceContainerOrWindow, |
|
/// Focus the next window in LRU order. |
|
NextWindow { |
|
#[clap(subcommand)] |
|
windows: ConsiderWindows, |
|
}, |
|
/// Focus the previous window in LRU order. |
|
PrevWindow { |
|
#[clap(subcommand)] |
|
windows: ConsiderWindows, |
|
}, |
|
/// Focus the next window of a tiled container. |
|
NextTiledWindow { |
|
#[clap(subcommand)] |
|
windows: ConsiderWindows, |
|
}, |
|
/// Focus the previous window of a tiled container. |
|
PrevTiledWindow { |
|
#[clap(subcommand)] |
|
windows: ConsiderWindows, |
|
}, |
|
/// Focus the next window of a tabbed or stacked container. |
|
NextTabbedOrStackedWindow { |
|
#[clap(subcommand)] |
|
windows: ConsiderWindows, |
|
}, |
|
/// Focus the previous window of a tabbed or stacked container. |
|
PrevTabbedOrStackedWindow { |
|
#[clap(subcommand)] |
|
windows: ConsiderWindows, |
|
}, |
|
/// Focus the next floating window. |
|
NextFloatingWindow { |
|
#[clap(subcommand)] |
|
windows: ConsiderWindows, |
|
}, |
|
/// Focus the previous floating window. |
|
PrevFloatingWindow { |
|
#[clap(subcommand)] |
|
windows: ConsiderWindows, |
|
}, |
|
/// Focus the next window having the same layout as the current one. |
|
NextWindowOfSameLayout { |
|
#[clap(subcommand)] |
|
windows: ConsiderWindows, |
|
}, |
|
/// Focus the previous window having the same layout as the current one. |
|
PrevWindowOfSameLayout { |
|
#[clap(subcommand)] |
|
windows: ConsiderWindows, |
|
}, |
|
/// Move the currently focused window or container to the selected |
|
/// workspace. |
|
MoveFocusedToWorkspace, |
|
/// Move the currently focused window or container to the selected output, |
|
/// workspace, container or window. |
|
MoveFocusedTo, |
|
/// Swap the currently focused window or container with the selected |
|
/// container or window. |
|
SwapFocusedWith, |
|
/// Tab or shuffle-and-tile the windows on the current workspace, including |
|
/// or excluding floating windows. |
|
ToggleTabShuffleTileWorkspace { |
|
#[clap(subcommand)] |
|
floating: ConsiderFloating, |
|
}, |
|
/// Tiles the windows on the current workspace, including or excluding |
|
/// floating windows. |
|
TileWorkspace { |
|
#[clap(subcommand)] |
|
floating: ConsiderFloating, |
|
}, |
|
/// Tabs the windows on the current workspace, including or excluding |
|
/// floating windows. |
|
TabWorkspace { |
|
#[clap(subcommand)] |
|
floating: ConsiderFloating, |
|
}, |
|
/// Shuffles and tiles the windows on the current workspace, including or |
|
/// excluding floating windows. |
|
ShuffleTileWorkspace { |
|
#[clap(subcommand)] |
|
floating: ConsiderFloating, |
|
}, |
|
/// Select and execute a swaymsg command. |
|
ExecuteSwaymsgCommand, |
|
/// Select and execute a swayr command. |
|
ExecuteSwayrCommand, |
|
/// Configure outputs. |
|
ConfigureOutputs, |
|
} |
|
|
|
impl SwayrCommand { |
|
fn is_prev_next_window_variant(&self) -> bool { |
|
matches!( |
|
self, |
|
SwayrCommand::NextWindow { .. } |
|
| SwayrCommand::PrevWindow { .. } |
|
| SwayrCommand::NextTiledWindow { .. } |
|
| SwayrCommand::PrevTiledWindow { .. } |
|
| SwayrCommand::NextTabbedOrStackedWindow { .. } |
|
| SwayrCommand::PrevTabbedOrStackedWindow { .. } |
|
| SwayrCommand::NextFloatingWindow { .. } |
|
| SwayrCommand::PrevFloatingWindow { .. } |
|
| SwayrCommand::NextWindowOfSameLayout { .. } |
|
| SwayrCommand::PrevWindowOfSameLayout { .. } |
|
) |
|
} |
|
} |
|
|
|
pub struct ExecSwayrCmdArgs<'a> { |
|
pub cmd: &'a SwayrCommand, |
|
pub extra_props: Arc<RwLock<HashMap<i64, t::ExtraProps>>>, |
|
} |
|
|
|
impl DisplayFormat for SwayrCommand { |
|
fn format_for_display(&self, _: &cfg::Config) -> std::string::String { |
|
// TODO: It would be very nice if the display format was exactly like |
|
// the swayr invocation in the shell. Can that somehow be retrieved |
|
// from clap? |
|
format!("{:?}", self) |
|
} |
|
|
|
fn get_indent_level(&self) -> usize { |
|
0 |
|
} |
|
} |
|
|
|
fn always_true(_x: &t::DisplayNode) -> bool { |
|
true |
|
} |
|
|
|
static IN_NEXT_PREV_WINDOW_SEQ: atomic::AtomicBool = |
|
atomic::AtomicBool::new(false); |
|
|
|
pub fn exec_swayr_cmd(args: ExecSwayrCmdArgs) { |
|
let props = args.extra_props; |
|
|
|
if args.cmd.is_prev_next_window_variant() { |
|
let before = |
|
IN_NEXT_PREV_WINDOW_SEQ.swap(true, atomic::Ordering::SeqCst); |
|
if !before { |
|
let mut map = props.write().unwrap(); |
|
for val in map.values_mut() { |
|
val.last_focus_tick_for_next_prev_seq = val.last_focus_tick; |
|
} |
|
} |
|
} else { |
|
IN_NEXT_PREV_WINDOW_SEQ.store(false, atomic::Ordering::SeqCst); |
|
} |
|
|
|
match args.cmd { |
|
SwayrCommand::SwitchToUrgentOrLRUWindow => { |
|
switch_to_urgent_or_lru_window(&*props.read().unwrap()) |
|
} |
|
SwayrCommand::SwitchToAppOrUrgentOrLRUWindow { name } => { |
|
switch_to_app_or_urgent_or_lru_window( |
|
Some(name), |
|
&*props.read().unwrap(), |
|
) |
|
} |
|
SwayrCommand::SwitchWindow => switch_window(&*props.read().unwrap()), |
|
SwayrCommand::SwitchWorkspace => { |
|
switch_workspace(&*props.read().unwrap()) |
|
} |
|
SwayrCommand::SwitchOutput => switch_output(&*props.read().unwrap()), |
|
SwayrCommand::SwitchWorkspaceOrWindow => { |
|
switch_workspace_or_window(&*props.read().unwrap()) |
|
} |
|
SwayrCommand::SwitchWorkspaceContainerOrWindow => { |
|
switch_workspace_container_or_window(&*props.read().unwrap()) |
|
} |
|
SwayrCommand::SwitchTo => switch_to(&*props.read().unwrap()), |
|
SwayrCommand::QuitWindow { kill } => { |
|
quit_window(&*props.read().unwrap(), *kill) |
|
} |
|
SwayrCommand::QuitWorkspaceOrWindow => { |
|
quit_workspace_or_window(&*props.read().unwrap()) |
|
} |
|
SwayrCommand::QuitWorkspaceContainerOrWindow => { |
|
quit_workspace_container_or_window(&*props.read().unwrap()) |
|
} |
|
SwayrCommand::MoveFocusedToWorkspace => { |
|
move_focused_to_workspace(&*props.read().unwrap()) |
|
} |
|
SwayrCommand::MoveFocusedTo => move_focused_to(&*props.read().unwrap()), |
|
SwayrCommand::SwapFocusedWith => { |
|
swap_focused_with(&*props.read().unwrap()) |
|
} |
|
SwayrCommand::NextWindow { windows } => focus_window_in_direction( |
|
Direction::Forward, |
|
windows, |
|
&*props.read().unwrap(), |
|
Box::new(always_true), |
|
), |
|
SwayrCommand::PrevWindow { windows } => focus_window_in_direction( |
|
Direction::Backward, |
|
windows, |
|
&*props.read().unwrap(), |
|
Box::new(always_true), |
|
), |
|
SwayrCommand::NextTiledWindow { windows } => focus_window_in_direction( |
|
Direction::Forward, |
|
windows, |
|
&*props.read().unwrap(), |
|
Box::new(|dn: &t::DisplayNode| { |
|
!dn.node.is_floating() |
|
&& dn.tree.is_child_of_tiled_container(dn.node.id) |
|
}), |
|
), |
|
SwayrCommand::PrevTiledWindow { windows } => focus_window_in_direction( |
|
Direction::Backward, |
|
windows, |
|
&*props.read().unwrap(), |
|
Box::new(|dn: &t::DisplayNode| { |
|
!dn.node.is_floating() |
|
&& dn.tree.is_child_of_tiled_container(dn.node.id) |
|
}), |
|
), |
|
SwayrCommand::NextTabbedOrStackedWindow { windows } => { |
|
focus_window_in_direction( |
|
Direction::Forward, |
|
windows, |
|
&*props.read().unwrap(), |
|
Box::new(|dn: &t::DisplayNode| { |
|
!dn.node.is_floating() |
|
&& dn |
|
.tree |
|
.is_child_of_tabbed_or_stacked_container(dn.node.id) |
|
}), |
|
) |
|
} |
|
SwayrCommand::PrevTabbedOrStackedWindow { windows } => { |
|
focus_window_in_direction( |
|
Direction::Backward, |
|
windows, |
|
&*props.read().unwrap(), |
|
Box::new(|dn: &t::DisplayNode| { |
|
!dn.node.is_floating() |
|
&& dn |
|
.tree |
|
.is_child_of_tabbed_or_stacked_container(dn.node.id) |
|
}), |
|
) |
|
} |
|
SwayrCommand::NextFloatingWindow { windows } => { |
|
focus_window_in_direction( |
|
Direction::Forward, |
|
windows, |
|
&*props.read().unwrap(), |
|
Box::new(|dn: &t::DisplayNode| dn.node.is_floating()), |
|
) |
|
} |
|
SwayrCommand::PrevFloatingWindow { windows } => { |
|
focus_window_in_direction( |
|
Direction::Backward, |
|
windows, |
|
&*props.read().unwrap(), |
|
Box::new(|dn: &t::DisplayNode| dn.node.is_floating()), |
|
) |
|
} |
|
SwayrCommand::NextWindowOfSameLayout { windows } => { |
|
focus_window_of_same_layout_in_direction( |
|
Direction::Forward, |
|
windows, |
|
&*props.read().unwrap(), |
|
) |
|
} |
|
SwayrCommand::PrevWindowOfSameLayout { windows } => { |
|
focus_window_of_same_layout_in_direction( |
|
Direction::Backward, |
|
windows, |
|
&*props.read().unwrap(), |
|
) |
|
} |
|
SwayrCommand::TileWorkspace { floating } => { |
|
tile_current_workspace(floating, false) |
|
} |
|
SwayrCommand::TabWorkspace { floating } => { |
|
tab_current_workspace(floating) |
|
} |
|
SwayrCommand::ShuffleTileWorkspace { floating } => { |
|
tile_current_workspace(floating, true) |
|
} |
|
SwayrCommand::ToggleTabShuffleTileWorkspace { floating } => { |
|
toggle_tab_tile_current_workspace(floating) |
|
} |
|
SwayrCommand::ConfigureOutputs => configure_outputs(), |
|
SwayrCommand::ExecuteSwaymsgCommand => exec_swaymsg_command(), |
|
SwayrCommand::ExecuteSwayrCommand => { |
|
let mut cmds = vec![ |
|
SwayrCommand::MoveFocusedToWorkspace, |
|
SwayrCommand::MoveFocusedTo, |
|
SwayrCommand::SwapFocusedWith, |
|
SwayrCommand::QuitWorkspaceOrWindow, |
|
SwayrCommand::SwitchWindow, |
|
SwayrCommand::SwitchWorkspace, |
|
SwayrCommand::SwitchOutput, |
|
SwayrCommand::SwitchWorkspaceOrWindow, |
|
SwayrCommand::SwitchToUrgentOrLRUWindow, |
|
SwayrCommand::ConfigureOutputs, |
|
SwayrCommand::ExecuteSwaymsgCommand, |
|
]; |
|
for f in [ |
|
ConsiderFloating::ExcludeFloating, |
|
ConsiderFloating::IncludeFloating, |
|
] { |
|
cmds.push(SwayrCommand::ToggleTabShuffleTileWorkspace { |
|
floating: f.clone(), |
|
}); |
|
cmds.push(SwayrCommand::TileWorkspace { |
|
floating: f.clone(), |
|
}); |
|
cmds.push(SwayrCommand::TabWorkspace { |
|
floating: f.clone(), |
|
}); |
|
cmds.push(SwayrCommand::ShuffleTileWorkspace { |
|
floating: f.clone(), |
|
}); |
|
} |
|
|
|
for kill in [false, true] { |
|
cmds.push(SwayrCommand::QuitWindow { kill }); |
|
} |
|
|
|
for w in [ |
|
ConsiderWindows::AllWorkspaces, |
|
ConsiderWindows::CurrentWorkspace, |
|
] { |
|
cmds.push(SwayrCommand::NextWindow { windows: w.clone() }); |
|
cmds.push(SwayrCommand::PrevWindow { windows: w.clone() }); |
|
cmds.push(SwayrCommand::NextTiledWindow { windows: w.clone() }); |
|
cmds.push(SwayrCommand::PrevTiledWindow { windows: w.clone() }); |
|
cmds.push(SwayrCommand::NextTabbedOrStackedWindow { |
|
windows: w.clone(), |
|
}); |
|
cmds.push(SwayrCommand::PrevTabbedOrStackedWindow { |
|
windows: w.clone(), |
|
}); |
|
cmds.push(SwayrCommand::NextFloatingWindow { |
|
windows: w.clone(), |
|
}); |
|
cmds.push(SwayrCommand::PrevFloatingWindow { |
|
windows: w.clone(), |
|
}) |
|
} |
|
|
|
if let Ok(c) = util::select_from_menu("Select swayr command", &cmds) |
|
{ |
|
exec_swayr_cmd(ExecSwayrCmdArgs { |
|
cmd: c, |
|
extra_props: props, |
|
}); |
|
} |
|
} |
|
} |
|
} |
|
|
|
fn focus_window_by_id(id: i64) { |
|
run_sway_command(&[format!("[con_id={}]", id).as_str(), "focus"]); |
|
} |
|
|
|
fn quit_window_by_id(id: i64) { |
|
run_sway_command(&[format!("[con_id={}]", id).as_str(), "kill"]); |
|
} |
|
|
|
pub fn get_tree(include_scratchpad: bool) -> s::Node { |
|
match s::Connection::new() { |
|
Ok(mut con) => { |
|
let mut root = con.get_tree().expect("Got no root node"); |
|
if !include_scratchpad { |
|
root.nodes.retain(|o| !o.is_scratchpad()); |
|
} |
|
root |
|
} |
|
Err(err) => panic!("{}", err), |
|
} |
|
} |
|
|
|
pub fn get_outputs() -> Vec<s::Output> { |
|
match s::Connection::new() { |
|
Ok(mut con) => con.get_outputs().expect("Got no outputs"), |
|
Err(err) => panic!("{}", err), |
|
} |
|
} |
|
|
|
pub fn switch_to_urgent_or_lru_window( |
|
extra_props: &HashMap<i64, t::ExtraProps>, |
|
) { |
|
switch_to_app_or_urgent_or_lru_window(None, extra_props) |
|
} |
|
|
|
pub fn switch_to_app_or_urgent_or_lru_window( |
|
name: Option<&str>, |
|
extra_props: &HashMap<i64, t::ExtraProps>, |
|
) { |
|
let root = get_tree(false); |
|
let tree = t::get_tree(&root, extra_props); |
|
let wins = tree.get_windows(); |
|
let app_win = |
|
name.and_then(|n| wins.iter().find(|w| w.node.get_app_name() == n)); |
|
match app_win { |
|
Some(app_win) if !app_win.node.is_current() => { |
|
focus_window_by_id(app_win.node.id) |
|
} |
|
_ => { |
|
if let Some(win) = wins.get(0) { |
|
focus_window_by_id(win.node.id) |
|
} else { |
|
log::debug!("No window to switch to.") |
|
} |
|
} |
|
} |
|
} |
|
|
|
lazy_static! { |
|
static ref DIGIT_AND_NAME: regex::Regex = |
|
regex::Regex::new(r"^(\d):(.*)").unwrap(); |
|
} |
|
|
|
fn create_workspace(ws_name: &str) { |
|
if DIGIT_AND_NAME.is_match(ws_name) { |
|
run_sway_command(&["workspace", "number", ws_name]); |
|
} else { |
|
run_sway_command(&["workspace", ws_name]); |
|
} |
|
} |
|
|
|
lazy_static! { |
|
static ref SPECIAL_WORKSPACE: regex::Regex = |
|
regex::Regex::new(r"^#*w:(.*)").unwrap(); |
|
static ref SPECIAL_SWAY: regex::Regex = |
|
regex::Regex::new(r"^#*s:(.*)").unwrap(); |
|
} |
|
|
|
fn chop_workspace_shortcut(input: &str) -> &str { |
|
match SPECIAL_WORKSPACE.captures(input) { |
|
Some(c) => c.get(1).unwrap().as_str(), |
|
None => input, |
|
} |
|
} |
|
|
|
fn chop_sway_shortcut(input: &str) -> &str { |
|
match SPECIAL_SWAY.captures(input) { |
|
Some(c) => c.get(1).unwrap().as_str(), |
|
None => input, |
|
} |
|
} |
|
|
|
fn handle_non_matching_input(input: &str) { |
|
if input.is_empty() { |
|
return; |
|
} |
|
|
|
if let Some(c) = SPECIAL_SWAY.captures(input) { |
|
run_sway_command(&c[1].split_ascii_whitespace().collect::<Vec<&str>>()); |
|
} else { |
|
let ws = chop_workspace_shortcut(input); |
|
create_workspace(ws); |
|
} |
|
} |
|
|
|
fn select_and_focus(prompt: &str, choices: &[t::DisplayNode]) { |
|
match util::select_from_menu(prompt, choices) { |
|
Ok(tn) => match tn.node.get_type() { |
|
t::Type::Output => { |
|
if !tn.node.is_scratchpad() { |
|
run_sway_command(&["focus output", tn.node.get_name()]); |
|
} |
|
} |
|
t::Type::Workspace => { |
|
if !tn.node.is_scratchpad() { |
|
run_sway_command(&["workspace", tn.node.get_name()]); |
|
} |
|
} |
|
t::Type::Window | t::Type::Container => { |
|
focus_window_by_id(tn.node.id) |
|
} |
|
t => { |
|
log::error!("Cannot handle {:?} in select_and_focus", t) |
|
} |
|
}, |
|
Err(non_matching_input) => { |
|
handle_non_matching_input(&non_matching_input) |
|
} |
|
} |
|
} |
|
|
|
pub fn switch_window(extra_props: &HashMap<i64, t::ExtraProps>) { |
|
let root = get_tree(true); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_focus("Select window", &tree.get_windows()); |
|
} |
|
|
|
pub fn switch_workspace(extra_props: &HashMap<i64, t::ExtraProps>) { |
|
let root = get_tree(false); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_focus("Select workspace", &tree.get_workspaces()); |
|
} |
|
|
|
pub fn switch_output(extra_props: &HashMap<i64, t::ExtraProps>) { |
|
let root = get_tree(false); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_focus("Select output", &tree.get_outputs()); |
|
} |
|
|
|
pub fn switch_workspace_or_window(extra_props: &HashMap<i64, t::ExtraProps>) { |
|
let root = get_tree(true); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_focus( |
|
"Select workspace or window", |
|
&tree.get_workspaces_and_windows(), |
|
); |
|
} |
|
|
|
pub fn switch_workspace_container_or_window( |
|
extra_props: &HashMap<i64, t::ExtraProps>, |
|
) { |
|
let root = get_tree(true); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_focus( |
|
"Select workspace, container or window", |
|
&tree.get_workspaces_containers_and_windows(), |
|
); |
|
} |
|
|
|
pub fn switch_to(extra_props: &HashMap<i64, t::ExtraProps>) { |
|
let root = get_tree(true); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_focus( |
|
"Select output, workspace, container or window", |
|
&tree.get_outputs_workspaces_containers_and_windows(), |
|
); |
|
} |
|
|
|
fn kill_process_by_pid(pid: Option<i32>) { |
|
if let Some(pid) = pid { |
|
if let Err(err) = std::process::Command::new("kill") |
|
.arg("-9") |
|
.arg(format!("{}", pid)) |
|
.output() |
|
{ |
|
log::error!("Error killing process {}: {}", pid, err) |
|
} |
|
} else { |
|
log::error!("Cannot kill window with no pid."); |
|
} |
|
} |
|
|
|
fn select_and_quit(prompt: &str, choices: &[t::DisplayNode], kill: bool) { |
|
if let Ok(tn) = util::select_from_menu(prompt, choices) { |
|
match tn.node.get_type() { |
|
t::Type::Workspace | t::Type::Container => { |
|
for win in |
|
tn.node.iter().filter(|n| n.get_type() == t::Type::Window) |
|
{ |
|
quit_window_by_id(win.id) |
|
} |
|
} |
|
t::Type::Window => { |
|
if kill { |
|
kill_process_by_pid(tn.node.pid) |
|
} else { |
|
quit_window_by_id(tn.node.id) |
|
} |
|
} |
|
t => { |
|
log::error!("Cannot handle {:?} in quit_workspace_or_window", t) |
|
} |
|
} |
|
} |
|
} |
|
|
|
pub fn quit_window(extra_props: &HashMap<i64, t::ExtraProps>, kill: bool) { |
|
let root = get_tree(true); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_quit("Quit window", &tree.get_windows(), kill); |
|
} |
|
|
|
pub fn quit_workspace_or_window(extra_props: &HashMap<i64, t::ExtraProps>) { |
|
let root = get_tree(true); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_quit( |
|
"Quit workspace or window", |
|
&tree.get_workspaces_and_windows(), |
|
false, |
|
); |
|
} |
|
|
|
pub fn quit_workspace_container_or_window( |
|
extra_props: &HashMap<i64, t::ExtraProps>, |
|
) { |
|
let root = get_tree(true); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_quit( |
|
"Quit workspace, container or window", |
|
&tree.get_workspaces_containers_and_windows(), |
|
false, |
|
); |
|
} |
|
|
|
fn move_focused_to_workspace_1(ws_name: &str) { |
|
if DIGIT_AND_NAME.is_match(ws_name) { |
|
run_sway_command(&[ |
|
"move", |
|
"container", |
|
"to", |
|
"workspace", |
|
"number", |
|
ws_name, |
|
]); |
|
} else { |
|
run_sway_command(&["move", "container", "to", "workspace", ws_name]); |
|
} |
|
} |
|
|
|
fn move_focused_to_container_or_window(id: i64) { |
|
run_sway_command(&[ |
|
&format!("[con_id=\"{}\"]", id), |
|
"mark", |
|
"--add", |
|
"__SWAYR_MOVE_TARGET__", |
|
]); |
|
run_sway_command(&["move", "to", "mark", "__SWAYR_MOVE_TARGET__"]); |
|
run_sway_command(&["unmark", "__SWAYR_MOVE_TARGET__"]); |
|
} |
|
|
|
fn select_and_move_focused_to(prompt: &str, choices: &[t::DisplayNode]) { |
|
match util::select_from_menu(prompt, choices) { |
|
Ok(tn) => match tn.node.get_type() { |
|
t::Type::Output => { |
|
if tn.node.is_scratchpad() { |
|
run_sway_command_1("move container to scratchpad") |
|
} else { |
|
run_sway_command(&[ |
|
"move container to output", |
|
tn.node.get_name(), |
|
]) |
|
} |
|
} |
|
t::Type::Workspace => { |
|
if tn.node.is_scratchpad() { |
|
run_sway_command_1("move container to scratchpad") |
|
} else { |
|
move_focused_to_workspace_1(tn.node.get_name()) |
|
} |
|
} |
|
t::Type::Container | t::Type::Window => { |
|
move_focused_to_container_or_window(tn.node.id) |
|
} |
|
t => log::error!("Cannot move focused to {:?}", t), |
|
}, |
|
Err(input) => { |
|
let ws_name = chop_workspace_shortcut(&input); |
|
move_focused_to_workspace_1(ws_name); |
|
} |
|
} |
|
} |
|
|
|
pub fn move_focused_to_workspace(extra_props: &HashMap<i64, t::ExtraProps>) { |
|
let root = get_tree(true); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_move_focused_to( |
|
"Move focused container to workspace", |
|
&tree.get_workspaces(), |
|
); |
|
} |
|
|
|
pub fn move_focused_to(extra_props: &HashMap<i64, t::ExtraProps>) { |
|
let root = get_tree(true); |
|
let tree = t::get_tree(&root, extra_props); |
|
select_and_move_focused_to( |
|
"Move focused container to workspace or container", |
|
&tree.get_outputs_workspaces_containers_and_windows(), |
|
); |
|
} |
|
|
|
pub fn swap_focused_with(extra_props: &HashMap<i64, t::ExtraProps>) { |
|
let root = get_tree(true); |
|
let tree = t::get_tree(&root, extra_props); |
|
match util::select_from_menu( |
|
"Swap focused with", |
|
&tree.get_workspaces_containers_and_windows(), |
|
) { |
|
Ok(tn) => match tn.node.get_type() { |
|
t::Type::Workspace | t::Type::Container | t::Type::Window => { |
|
run_sway_command(&[ |
|
"swap", |
|
"container", |
|
"with", |
|
"con_id", |
|
&format!("{}", tn.node.id), |
|
]) |
|
} |
|
t => log::error!("Cannot move focused to {:?}", t), |
|
}, |
|
Err(input) => { |
|
let ws_name = chop_workspace_shortcut(&input); |
|
move_focused_to_workspace_1(ws_name); |
|
} |
|
} |
|
} |
|
|
|
pub enum Direction { |
|
Backward, |
|
Forward, |
|
} |
|
|
|
pub fn focus_window_in_direction( |
|
dir: Direction, |
|
consider_wins: &ConsiderWindows, |
|
extra_props: &HashMap<i64, t::ExtraProps>, |
|
pred: Box<dyn Fn(&t::DisplayNode) -> bool>, |
|
) { |
|
let root = get_tree(false); |
|
let tree = t::get_tree(&root, extra_props); |
|
let mut wins = tree.get_windows(); |
|
|
|
if consider_wins == &ConsiderWindows::CurrentWorkspace { |
|
let cur_ws = tree.get_current_workspace(); |
|
wins.retain(|w| { |
|
tree.get_parent_node_of_type(w.node.id, t::Type::Workspace) |
|
.unwrap() |
|
.id |
|
== cur_ws.id |
|
}); |
|
} |
|
|
|
wins.retain(pred); |
|
|
|
if wins.len() < 2 { |
|
return; |
|
} |
|
|
|
wins.sort_by(|a, b| { |
|
let lru_a = tree.last_focus_tick_for_next_prev_seq(a.node.id); |
|
let lru_b = tree.last_focus_tick_for_next_prev_seq(b.node.id); |
|
lru_a.cmp(&lru_b).reverse() |
|
}); |
|
|
|
let is_focused_window: Box<dyn Fn(&t::DisplayNode) -> bool> = |
|
if !wins.iter().any(|w| w.node.focused) { |
|
let last_focused_win_id = wins.get(0).unwrap().node.id; |
|
Box::new(move |dn| dn.node.id == last_focused_win_id) |
|
} else { |
|
Box::new(|dn| dn.node.focused) |
|
}; |
|
|
|
let mut iter: Box<dyn Iterator<Item = &t::DisplayNode>> = match dir { |
|
Direction::Forward => Box::new(wins.iter().rev().cycle()), |
|
Direction::Backward => Box::new(wins.iter().cycle()), |
|
}; |
|
|
|
loop { |
|
let win = iter.next().unwrap(); |
|
if is_focused_window(win) { |
|
let win = iter.next().unwrap(); |
|
focus_window_by_id(win.node.id); |
|
return; |
|
} |
|
} |
|
} |
|
|
|
pub fn focus_window_of_same_layout_in_direction( |
|
dir: Direction, |
|
consider_wins: &ConsiderWindows, |
|
extra_props: &HashMap<i64, t::ExtraProps>, |
|
) { |
|
let root = get_tree(false); |
|
let tree = t::get_tree(&root, extra_props); |
|
let wins = tree.get_windows(); |
|
let cur_win = wins.iter().find(|w| w.node.focused); |
|
|
|
if let Some(cur_win) = cur_win { |
|
focus_window_in_direction( |
|
dir, |
|
consider_wins, |
|
extra_props, |
|
if cur_win.node.is_floating() { |
|
Box::new(|dn| dn.node.is_floating()) |
|
} else if !cur_win.node.is_floating() |
|
&& cur_win |
|
.tree |
|
.is_child_of_tabbed_or_stacked_container(cur_win.node.id) |
|
{ |
|
Box::new(|dn| { |
|
!dn.node.is_floating() |
|
&& dn |
|
.tree |
|
.is_child_of_tabbed_or_stacked_container(dn.node.id) |
|
}) |
|
} else if !cur_win.node.is_floating() |
|
&& cur_win.tree.is_child_of_tiled_container(cur_win.node.id) |
|
{ |
|
Box::new(|dn| { |
|
!dn.node.is_floating() |
|
&& dn.tree.is_child_of_tiled_container(dn.node.id) |
|
}) |
|
} else { |
|
Box::new(always_true) |
|
}, |
|
) |
|
} |
|
} |
|
|
|
fn tile_current_workspace(floating: &ConsiderFloating, shuffle: bool) { |
|
match layout::relayout_current_workspace( |
|
floating == &ConsiderFloating::IncludeFloating, |
|
Box::new(move |wins, con: &mut s::Connection| { |
|
con.run_command("focus parent")?; |
|
con.run_command("layout splith")?; |
|
|
|
let mut placed_wins = vec![]; |
|
let mut rng = rand::thread_rng(); |
|
if shuffle { |
|
wins.shuffle(&mut rng); |
|
} else { |
|
wins.reverse() |
|
} |
|
for win in wins { |
|
if win.is_floating() { |
|
con.run_command(format!( |
|
"[con_id={}] floating disable", |
|
win.id |
|
))?; |
|
} |
|
std::thread::sleep(std::time::Duration::from_millis(25)); |
|
con.run_command(format!( |
|
"[con_id={}] move to workspace current", |
|
win.id |
|
))?; |
|
placed_wins.push(win); |
|
if shuffle { |
|
std::thread::sleep(std::time::Duration::from_millis(25)); |
|
if let Some(win) = placed_wins.choose(&mut rng) { |
|
con.run_command(format!("[con_id={}] focus", win.id))?; |
|
} |
|
} |
|
} |
|
Ok(()) |
|
}), |
|
) { |
|
Ok(_) => (), |
|
Err(err) => log::error!("Error retiling workspace: {:?}", err), |
|
} |
|
} |
|
|
|
fn tab_current_workspace(floating: &ConsiderFloating) { |
|
match layout::relayout_current_workspace( |
|
floating == &ConsiderFloating::IncludeFloating, |
|
Box::new(move |wins, con: &mut s::Connection| { |
|
con.run_command("focus parent")?; |
|
con.run_command("layout tabbed")?; |
|
|
|
let mut placed_wins = vec![]; |
|
wins.reverse(); |
|
for win in wins { |
|
if win.is_floating() { |
|
con.run_command(format!( |
|
"[con_id={}] floating disable", |
|
win.id |
|
))?; |
|
} |
|
|
|
std::thread::sleep(std::time::Duration::from_millis(25)); |
|
con.run_command(format!( |
|
"[con_id={}] move to workspace current", |
|
win.id |
|
))?; |
|
placed_wins.push(win); |
|
} |
|
Ok(()) |
|
}), |
|
) { |
|
Ok(_) => (), |
|
Err(err) => log::error!("Error retiling workspace: {:?}", err), |
|
} |
|
} |
|
|
|
fn toggle_tab_tile_current_workspace(floating: &ConsiderFloating) { |
|
let tree = get_tree(false); |
|
let workspaces = tree.nodes_of_type(t::Type::Workspace); |
|
let cur_ws = workspaces.iter().find(|w| w.is_current()).unwrap(); |
|
if cur_ws.layout == s::NodeLayout::Tabbed { |
|
tile_current_workspace(floating, true); |
|
} else { |
|
tab_current_workspace(floating); |
|
} |
|
} |
|
|
|
fn get_swaymsg_commands() -> Vec<SwaymsgCmd> { |
|
let mut cmds: Vec<String> = vec![]; |
|
|
|
for b in &["none", "normal", "csd", "pixel"] { |
|
cmds.push(format!["border {}", b]); |
|
} |
|
|
|
cmds.push("exit".to_string()); |
|
cmds.push("floating toggle".to_string()); |
|
cmds.push("focus child".to_string()); |
|
cmds.push("focus parent".to_string()); |
|
cmds.push("focus tiling".to_string()); |
|
cmds.push("focus floating".to_string()); |
|
cmds.push("focus mode_toggle".to_string()); |
|
cmds.push("fullscreen toggle".to_string()); |
|
cmds.push("reload".to_string()); |
|
cmds.push("sticky toggle".to_string()); |
|
cmds.push("kill".to_string()); |
|
cmds.push("tiling_drag toggle".to_string()); |
|
|
|
for x in &["focus", "fullscreen", "open", "none", "visible"] { |
|
cmds.push(format!["inhibit_idle {}", x]) |
|
} |
|
|
|
for l in &["default", "splith", "splitv", "stacking", "tiling"] { |
|
cmds.push(format!["layout {}", l]) |
|
} |
|
|
|
for e in &["enable", "disable"] { |
|
cmds.push(format!["shortcuts_inhibitor {}", e]) |
|
} |
|
|
|
for x in &["yes", "no", "always"] { |
|
cmds.push(format!["focus_follows_mouse {}", x]) |
|
} |
|
|
|
for x in &["smart", "urgent", "focus", "none"] { |
|
cmds.push(format!["focus_on_window_activation {}", x]) |
|
} |
|
|
|
for x in &["yes", "no", "force", "workspace"] { |
|
cmds.push(format!["focus_wrapping {}", x]) |
|
} |
|
|
|
for x in &[ |
|
"none", |
|
"vertical", |
|
"horizontal", |
|
"both", |
|
"smart", |
|
"smart_no_gaps", |
|
] { |
|
cmds.push(format!["hide_edge_borders {}", x]) |
|
} |
|
|
|
for x in &["on", "no_gaps", "off"] { |
|
cmds.push(format!["smart_borders {}", x]) |
|
} |
|
|
|
for x in &["on", "off"] { |
|
cmds.push(format!["smart_gaps {}", x]) |
|
} |
|
|
|
for x in &["output", "container", "none"] { |
|
cmds.push(format!["mouse_warping {}", x]) |
|
} |
|
|
|
for x in &["smart", "ignore", "leave_fullscreen"] { |
|
cmds.push(format!["popup_during_fullscreen {}", x]) |
|
} |
|
|
|
for x in &["yes", "no"] { |
|
cmds.push(format!["show_marks {}", x]); |
|
cmds.push(format!["workspace_auto_back_and_forth {}", x]); |
|
} |
|
|
|
for x in &["left", "center", "right"] { |
|
cmds.push(format!["title_align {}", x]); |
|
} |
|
|
|
for x in &["enable", "disable", "allow", "deny"] { |
|
cmds.push(format!["urgent {}", x]) |
|
} |
|
|
|
cmds.sort(); |
|
|
|
cmds.into_iter().map(|c| SwaymsgCmd { cmd: c }).collect() |
|
} |
|
|
|
struct SwaymsgCmd { |
|
cmd: String, |
|
} |
|
|
|
impl DisplayFormat for SwaymsgCmd { |
|
fn format_for_display(&self, _: &cfg::Config) -> std::string::String { |
|
self.cmd.clone() |
|
} |
|
|
|
fn get_indent_level(&self) -> usize { |
|
0 |
|
} |
|
} |
|
|
|
pub fn exec_swaymsg_command() { |
|
let cmds = get_swaymsg_commands(); |
|
let cmd = util::select_from_menu("Execute swaymsg command", &cmds); |
|
match cmd { |
|
Ok(cmd) => run_sway_command_1(&cmd.cmd), |
|
Err(cmd) if !cmd.is_empty() => { |
|
let cmd = chop_sway_shortcut(&cmd); |
|
run_sway_command_1(cmd); |
|
} |
|
Err(_) => (), |
|
} |
|
} |
|
|
|
pub fn configure_outputs() { |
|
let outputs = get_outputs(); |
|
|
|
let mut cmds = vec![]; |
|
for o in outputs { |
|
cmds.push(format!("output {} toggle", o.name)); |
|
|
|
for mode in o.modes { |
|
cmds.push(format!( |
|
"output {} mode {}x{}", |
|
o.name, mode.width, mode.height |
|
)); |
|
} |
|
|
|
for on_off in ["on", "off"] { |
|
cmds.push(format!("output {} dpms {}", o.name, on_off)); |
|
cmds.push(format!("output {} adaptive_sync {}", o.name, on_off)); |
|
} |
|
|
|
for transform in [ |
|
"normal", |
|
"90", |
|
"180", |
|
"270", |
|
"360", |
|
"flipped", |
|
"flipped-90", |
|
"flipped-180", |
|
"flipped-270", |
|
] { |
|
for dir in ["clockwise", "anticlockwise"] { |
|
cmds.push(format!( |
|
"output {} transform {} {}", |
|
o.name, transform, dir |
|
)); |
|
} |
|
} |
|
|
|
for pix in ["rgb", "bgr", "vrbg", "vbgr", "none"] { |
|
cmds.push(format!("output {} subpixel {}", o.name, pix)); |
|
} |
|
|
|
for pix in ["linear", "nearest", "smart"] { |
|
cmds.push(format!("output {} scale_filter {}", o.name, pix)); |
|
} |
|
} |
|
cmds.sort(); |
|
let cmds: Vec<SwaymsgCmd> = |
|
cmds.into_iter().map(|c| SwaymsgCmd { cmd: c }).collect(); |
|
|
|
while let Ok(cmd) = util::select_from_menu("Output command", &cmds) { |
|
run_sway_command_1(&cmd.cmd); |
|
} |
|
}
|
|
|