Implement auto_tile feature

timeout_old
Tassilo Horn 3 years ago
parent e93a833401
commit 045e8e7411
  1. 2
      src/bin/swayr.rs
  2. 4
      src/client.rs
  3. 47
      src/cmds.rs
  4. 106
      src/con.rs
  5. 48
      src/config.rs
  6. 58
      src/demon.rs
  7. 122
      src/ipc.rs
  8. 104
      src/layout.rs
  9. 2
      src/lib.rs

@ -27,7 +27,7 @@ use clap::{crate_version, Clap};
)]
struct Opts {
#[clap(subcommand)]
command: swayr::ipc::SwayrCommand,
command: swayr::cmds::SwayrCommand,
}
fn main() {

@ -13,13 +13,13 @@
// 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 crate::ipc;
use crate::cmds;
use crate::util;
use std::io::Write;
use std::os::unix::net::UnixStream;
pub fn send_swayr_cmd(
cmd: ipc::SwayrCommand,
cmd: cmds::SwayrCommand,
) -> std::result::Result<(), std::io::Error> {
let mut sock = UnixStream::connect(util::get_swayr_socket_path())?;
sock.write_all(serde_json::to_string(&cmd).unwrap().as_bytes())

@ -17,18 +17,43 @@
use crate::con;
use crate::config as cfg;
use crate::ipc;
use crate::ipc::SwayrCommand;
use crate::util;
use crate::util::DisplayFormat;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
use swayipc as s;
use clap::Clap;
#[derive(Clap, Debug, Deserialize, Serialize)]
pub enum SwayrCommand {
/// Switch to next urgent window (if any) or to last recently used window.
SwitchToUrgentOrLRUWindow,
/// Focus the selected window
SwitchWindow,
/// Focus the next window.
NextWindow,
/// Focus the previous window.
PrevWindow,
/// Quit the selected window
QuitWindow,
/// Switch to the selected workspace
SwitchWorkspace,
/// Switch to the selected workspace or focus the selected window
SwitchWorkspaceOrWindow,
/// Quit all windows of selected workspace or the selected window
QuitWorkspaceOrWindow,
/// Select and execute a swaymsg command
ExecuteSwaymsgCommand,
/// Select and execute a swayr command
ExecuteSwayrCommand,
}
pub struct ExecSwayrCmdArgs<'a> {
pub cmd: &'a SwayrCommand,
pub extra_props: Arc<RwLock<HashMap<i64, ipc::ExtraProps>>>,
pub extra_props: Arc<RwLock<HashMap<i64, con::ExtraProps>>>,
}
impl DisplayFormat for SwayrCommand {
@ -98,7 +123,7 @@ fn quit_window_by_id(id: i64) {
run_sway_command(&[format!("[con_id={}]", id).as_str(), "kill"]);
}
fn get_tree() -> s::Node {
pub fn get_tree() -> s::Node {
match s::Connection::new() {
Ok(mut con) => con.get_tree().expect("Got no root node"),
Err(err) => panic!("{}", err),
@ -106,7 +131,7 @@ fn get_tree() -> s::Node {
}
pub fn switch_to_urgent_or_lru_window(
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
extra_props: Option<&HashMap<i64, con::ExtraProps>>,
) {
let root = get_tree();
let windows = con::get_windows(&root, false, extra_props);
@ -122,7 +147,7 @@ pub fn switch_to_urgent_or_lru_window(
}
}
pub fn switch_window(extra_props: Option<&HashMap<i64, ipc::ExtraProps>>) {
pub fn switch_window(extra_props: Option<&HashMap<i64, con::ExtraProps>>) {
let root = get_tree();
let windows = con::get_windows(&root, true, extra_props);
@ -138,7 +163,7 @@ pub enum Direction {
pub fn focus_next_window_in_direction(
dir: Direction,
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
extra_props: Option<&HashMap<i64, con::ExtraProps>>,
) {
let root = get_tree();
let windows = con::get_windows(&root, false, None);
@ -174,7 +199,7 @@ pub fn focus_next_window_in_direction(
}
}
pub fn switch_workspace(extra_props: Option<&HashMap<i64, ipc::ExtraProps>>) {
pub fn switch_workspace(extra_props: Option<&HashMap<i64, con::ExtraProps>>) {
let root = get_tree();
let workspaces = con::get_workspaces(&root, false, extra_props);
@ -186,7 +211,7 @@ pub fn switch_workspace(extra_props: Option<&HashMap<i64, ipc::ExtraProps>>) {
}
pub fn switch_workspace_or_window(
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
extra_props: Option<&HashMap<i64, con::ExtraProps>>,
) {
let root = get_tree();
let workspaces = con::get_workspaces(&root, true, extra_props);
@ -204,7 +229,7 @@ pub fn switch_workspace_or_window(
}
}
pub fn quit_window(extra_props: Option<&HashMap<i64, ipc::ExtraProps>>) {
pub fn quit_window(extra_props: Option<&HashMap<i64, con::ExtraProps>>) {
let root = get_tree();
let windows = con::get_windows(&root, true, extra_props);
@ -214,7 +239,7 @@ pub fn quit_window(extra_props: Option<&HashMap<i64, ipc::ExtraProps>>) {
}
pub fn quit_workspace_or_window(
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
extra_props: Option<&HashMap<i64, con::ExtraProps>>,
) {
let root = get_tree();
let workspaces = con::get_workspaces(&root, true, extra_props);

@ -16,20 +16,110 @@
//! Convenience data structures built from the IPC structs.
use crate::config as cfg;
use crate::ipc;
use crate::ipc::NodeMethods;
use crate::util;
use crate::util::DisplayFormat;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::cmp;
use std::collections::HashMap;
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> {
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
}
}
}
/// Extension methods for [`swayipc::Node`].
pub trait NodeMethods {
/// Returns an iterator for this [`swayipc::Node`] and its childres.
fn iter(&self) -> NodeIter;
fn is_window(&self) -> bool;
/// Either a workspace or a con holding windows, e.g. a vertical split side
/// in a horizontally split workspace.
fn is_container(&self) -> bool;
/// Returns all nodes being application windows.
fn windows(&self) -> Vec<&s::Node>;
/// Returns all nodes being workspaces.
fn workspaces(&self) -> Vec<&s::Node>;
fn is_scratchpad(&self) -> bool;
}
impl NodeMethods for s::Node {
fn iter(&self) -> NodeIter {
NodeIter::new(self)
}
fn is_window(&self) -> bool {
(self.node_type == s::NodeType::Con
|| self.node_type == s::NodeType::FloatingCon)
&& self.name.is_some()
}
fn is_container(&self) -> bool {
self.node_type == s::NodeType::Workspace
|| self.node_type == s::NodeType::Con
&& self.name.is_none()
&& self.layout != s::NodeLayout::None
}
fn windows(&self) -> Vec<&s::Node> {
self.iter().filter(|n| n.is_window()).collect()
}
fn workspaces(&self) -> Vec<&s::Node> {
self.iter()
.filter(|n| n.node_type == s::NodeType::Workspace)
.collect()
}
fn is_scratchpad(&self) -> bool {
self.name.is_some() && self.name.as_ref().unwrap().eq("__i3_scratch")
}
}
/// Extra properties gathered by swayrd for windows and workspaces.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ExtraProps {
/// Milliseconds since UNIX epoch.
pub last_focus_time: u128,
}
#[derive(Debug)]
pub struct Window<'a> {
node: &'a s::Node,
workspace: &'a s::Node,
extra_props: Option<ipc::ExtraProps>,
extra_props: Option<ExtraProps>,
}
impl Window<'_> {
@ -183,7 +273,7 @@ impl<'a> DisplayFormat for Window<'a> {
fn build_windows<'a>(
root: &'a s::Node,
include_scratchpad_windows: bool,
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
extra_props: Option<&HashMap<i64, ExtraProps>>,
) -> Vec<Window<'a>> {
let mut v = vec![];
for workspace in root.workspaces() {
@ -205,7 +295,7 @@ fn build_windows<'a>(
fn build_workspaces<'a>(
root: &'a s::Node,
include_scratchpad: bool,
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
extra_props: Option<&HashMap<i64, ExtraProps>>,
) -> Vec<Workspace<'a>> {
let mut v = vec![];
for workspace in root.workspaces() {
@ -240,7 +330,7 @@ fn build_workspaces<'a>(
pub fn get_windows<'a>(
root: &'a s::Node,
include_scratchpad_windows: bool,
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
extra_props: Option<&HashMap<i64, ExtraProps>>,
) -> Vec<Window<'a>> {
let extra_props_given = extra_props.is_some();
let mut wins = build_windows(root, include_scratchpad_windows, extra_props);
@ -254,7 +344,7 @@ pub fn get_windows<'a>(
pub fn get_workspaces<'a>(
root: &'a s::Node,
include_scratchpad: bool,
extra_props: Option<&HashMap<i64, ipc::ExtraProps>>,
extra_props: Option<&HashMap<i64, ExtraProps>>,
) -> Vec<Workspace<'a>> {
let mut workspaces =
build_workspaces(root, include_scratchpad, extra_props);
@ -314,7 +404,7 @@ pub fn select_workspace_or_window<'a>(
pub struct Workspace<'a> {
node: &'a s::Node,
extra_props: Option<ipc::ExtraProps>,
extra_props: Option<ExtraProps>,
pub windows: Vec<Window<'a>>,
}

@ -17,6 +17,7 @@
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::DirBuilder;
use std::fs::OpenOptions;
use std::io::{Read, Write};
@ -26,6 +27,7 @@ use std::path::Path;
pub struct Config {
pub menu: Option<Menu>,
pub format: Option<Format>,
pub layout: Option<Layout>,
}
#[derive(Debug, Serialize, Deserialize)]
@ -44,6 +46,28 @@ pub struct Format {
pub fallback_icon: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Layout {
pub auto_tile: Option<bool>,
pub auto_tile_min_window_width_per_output_width: Option<Vec<[i32; 2]>>,
}
impl Layout {
pub fn auto_tile_min_window_width_per_output_width_as_map(
&self,
) -> Option<HashMap<i32, i32>> {
if let Some(vec) = &self.auto_tile_min_window_width_per_output_width {
let mut map = HashMap::new();
for tup in vec {
map.insert(tup[0], tup[1]);
}
Some(map)
} else {
None
}
}
}
impl Default for Menu {
fn default() -> Self {
Menu {
@ -90,11 +114,35 @@ impl Default for Format {
}
}
impl Default for Layout {
fn default() -> Layout {
let resolution_min_width_vec = vec![
[1024, 500],
[1280, 600],
[1400, 680],
[1440, 700],
[1600, 780],
[1920, 920],
[2560, 1000],
[3440, 1000],
[4096, 1200],
];
Layout {
auto_tile: Some(false),
auto_tile_min_window_width_per_output_width: Some(
resolution_min_width_vec,
),
}
}
}
impl Default for Config {
fn default() -> Self {
Config {
menu: Some(Menu::default()),
format: Some(Format::default()),
layout: Some(Layout::default()),
}
}
}

@ -16,9 +16,10 @@
//! Functions and data structures of the swayrd demon.
use crate::cmds;
use crate::ipc;
use crate::con;
use crate::config;
use crate::layout;
use crate::util;
use std::collections::HashMap;
use std::io::Read;
use std::os::unix::net::{UnixListener, UnixStream};
@ -26,11 +27,10 @@ use std::sync::Arc;
use std::sync::RwLock;
use std::thread;
use std::time::{SystemTime, UNIX_EPOCH};
use swayipc as s;
pub fn run_demon() {
let extra_props: Arc<RwLock<HashMap<i64, ipc::ExtraProps>>> =
let extra_props: Arc<RwLock<HashMap<i64, con::ExtraProps>>> =
Arc::new(RwLock::new(HashMap::new()));
let extra_props_for_ev_handler = extra_props.clone();
@ -47,8 +47,11 @@ fn connect_and_subscribe() -> s::Fallible<s::EventStream> {
}
pub fn monitor_sway_events(
extra_props: Arc<RwLock<HashMap<i64, ipc::ExtraProps>>>,
extra_props: Arc<RwLock<HashMap<i64, con::ExtraProps>>>,
) {
let config = config::load_config();
let layout = config.layout.unwrap_or_else(config::Layout::default);
'reset: loop {
println!("Connecting to sway for subscribing to events...");
match connect_and_subscribe() {
@ -67,6 +70,7 @@ pub fn monitor_sway_events(
handled = handle_window_event(
win_ev,
extra_props_clone,
&layout,
);
}
s::Event::Workspace(ws_ev) => {
@ -99,31 +103,55 @@ pub fn monitor_sway_events(
}
}
fn maybe_auto_tile(layout: &config::Layout) {
if layout.auto_tile == Some(true) {
println!("\nauto_tile: start");
layout::auto_tile(layout);
println!("auto_tile: end\n");
}
}
fn handle_window_event(
ev: Box<s::WindowEvent>,
extra_props: Arc<RwLock<HashMap<i64, ipc::ExtraProps>>>,
extra_props: Arc<RwLock<HashMap<i64, con::ExtraProps>>>,
layout: &config::Layout,
) -> bool {
let s::WindowEvent {
change, container, ..
} = *ev;
match change {
s::WindowChange::New | s::WindowChange::Focus => {
s::WindowChange::Focus => {
update_last_focus_time(container.id, extra_props);
println!("Handled window event type {:?}", change);
true
}
s::WindowChange::New => {
maybe_auto_tile(layout);
update_last_focus_time(container.id, extra_props);
println!("Handled window event type {:?}", change);
true
}
s::WindowChange::Close => {
remove_extra_props(container.id, extra_props);
maybe_auto_tile(layout);
println!("Handled window event type {:?}", change);
true
}
_ => false,
s::WindowChange::Move | s::WindowChange::Floating => {
maybe_auto_tile(layout);
println!("Handled window event type {:?}", change);
false // We don't affect the extra_props state here.
}
_ => {
println!("Unhandled window event type {:?}", change);
false
}
}
}
fn handle_workspace_event(
ev: Box<s::WorkspaceEvent>,
extra_props: Arc<RwLock<HashMap<i64, ipc::ExtraProps>>>,
extra_props: Arc<RwLock<HashMap<i64, con::ExtraProps>>>,
) -> bool {
let s::WorkspaceEvent {
change,
@ -156,7 +184,7 @@ fn handle_workspace_event(
fn update_last_focus_time(
id: i64,
extra_props: Arc<RwLock<HashMap<i64, ipc::ExtraProps>>>,
extra_props: Arc<RwLock<HashMap<i64, con::ExtraProps>>>,
) {
let mut write_lock = extra_props.write().unwrap();
if let Some(wp) = write_lock.get_mut(&id) {
@ -164,7 +192,7 @@ fn update_last_focus_time(
} else {
write_lock.insert(
id,
ipc::ExtraProps {
con::ExtraProps {
last_focus_time: get_epoch_time_as_millis(),
},
);
@ -173,7 +201,7 @@ fn update_last_focus_time(
fn remove_extra_props(
id: i64,
extra_props: Arc<RwLock<HashMap<i64, ipc::ExtraProps>>>,
extra_props: Arc<RwLock<HashMap<i64, con::ExtraProps>>>,
) {
extra_props.write().unwrap().remove(&id);
}
@ -186,7 +214,7 @@ fn get_epoch_time_as_millis() -> u128 {
}
pub fn serve_client_requests(
extra_props: Arc<RwLock<HashMap<i64, ipc::ExtraProps>>>,
extra_props: Arc<RwLock<HashMap<i64, con::ExtraProps>>>,
) {
match std::fs::remove_file(util::get_swayr_socket_path()) {
Ok(()) => println!("Deleted stale socket from previous run."),
@ -215,11 +243,11 @@ pub fn serve_client_requests(
fn handle_client_request(
mut stream: UnixStream,
extra_props: Arc<RwLock<HashMap<i64, ipc::ExtraProps>>>,
extra_props: Arc<RwLock<HashMap<i64, con::ExtraProps>>>,
) {
let mut cmd_str = String::new();
if stream.read_to_string(&mut cmd_str).is_ok() {
if let Ok(cmd) = serde_json::from_str::<ipc::SwayrCommand>(&cmd_str) {
if let Ok(cmd) = serde_json::from_str::<cmds::SwayrCommand>(&cmd_str) {
cmds::exec_swayr_cmd(cmds::ExecSwayrCmdArgs {
cmd: &cmd,
extra_props,

@ -1,122 +0,0 @@
// Copyright (C) 2021 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/>.
//! Extensions of swayipc types and IPC structs.
use clap::Clap;
use serde::{Deserialize, Serialize};
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> {
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
}
}
}
/// Extension methods for [`swayipc::reply::Node`].
pub trait NodeMethods {
/// Returns an iterator for this [`swayipc::reply::Node`] and its childres.
fn iter(&self) -> NodeIter;
/// Returns all nodes being application windows.
fn windows(&self) -> Vec<&s::Node>;
/// Returns all nodes being workspaces.
fn workspaces(&self) -> Vec<&s::Node>;
fn is_scratchpad(&self) -> bool;
}
impl NodeMethods for s::Node {
fn iter(&self) -> NodeIter {
NodeIter::new(self)
}
fn windows(&self) -> Vec<&s::Node> {
self.iter()
.filter(|n| {
(n.node_type == s::NodeType::Con
|| n.node_type == s::NodeType::FloatingCon)
&& n.name.is_some()
})
.collect()
}
fn workspaces(&self) -> Vec<&s::Node> {
self.iter()
.filter(|n| n.node_type == s::NodeType::Workspace)
.collect()
}
fn is_scratchpad(&self) -> bool {
self.name.is_some() && self.name.as_ref().unwrap().eq("__i3_scratch")
}
}
#[derive(Clap, Debug, Deserialize, Serialize)]
pub enum SwayrCommand {
/// Switch to next urgent window (if any) or to last recently used window.
SwitchToUrgentOrLRUWindow,
/// Focus the selected window
SwitchWindow,
/// Focus the next window.
NextWindow,
/// Focus the previous window.
PrevWindow,
/// Quit the selected window
QuitWindow,
/// Switch to the selected workspace
SwitchWorkspace,
/// Switch to the selected workspace or focus the selected window
SwitchWorkspaceOrWindow,
/// Quit all windows of selected workspace or the selected window
QuitWorkspaceOrWindow,
/// Select and execute a swaymsg command
ExecuteSwaymsgCommand,
/// Select and execute a swayr command
ExecuteSwayrCommand,
}
/// Extra properties gathered by swayrd for windows and workspaces.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ExtraProps {
/// Milliseconds since UNIX epoch.
pub last_focus_time: u128,
}

@ -0,0 +1,104 @@
// Copyright (C) 2021 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::con;
use crate::con::NodeMethods;
use crate::config;
use swayipc as s;
pub fn auto_tile(layout: &config::Layout) {
if let Ok(mut con) = s::Connection::new() {
if let Ok(tree) = con.get_tree() {
let config_map =
layout.auto_tile_min_window_width_per_output_width_as_map();
let default_map = config::Layout::default()
.auto_tile_min_window_width_per_output_width_as_map()
.unwrap();
for output in &tree.nodes {
println!("output: {:?}", output.name);
let output_width = output.rect.width;
let min_window_width = &config_map
.as_ref()
.unwrap_or(&default_map)
.get(&output_width);
if let Some(min_window_width) = min_window_width {
for container in
con::NodeIter::new(output).filter(|n| n.is_container())
{
println!(
" container: {:?}, {} nodes",
container.node_type,
container.nodes.len()
);
for child_win in
container.nodes.iter().filter(|n| n.is_window())
{
println!(" child_win: {:?}", child_win.app_id);
// Width if we'd split once more.
let estimated_width =
child_win.rect.width as f32 / 2.0;
println!("estimated_width = {}", estimated_width);
let split = if container.layout
== s::NodeLayout::SplitH
&& estimated_width <= **min_window_width as f32
{
Some("splitv")
} else if container.layout == s::NodeLayout::SplitV
&& estimated_width > **min_window_width as f32
{
Some("splith")
} else {
None
};
if let Some(split) = split {
println!(
"Auto-tiling performing {} on window {} \
because estimated width after another \
split is {} and the minimum window width \
is {} on this output.",
split,
child_win.id,
estimated_width,
min_window_width
);
match con.run_command(format!(
"[con_id={}] {}",
child_win.id, split
)) {
Ok(_) => (),
Err(e) => eprintln!(
"Couldn't set {} on con {}: {:?}",
split, child_win.id, e
),
}
}
}
}
} else {
eprintln!("No layout.auto_tile_min_window_width_per_output_width \
setting for output_width {}", output_width);
}
}
} else {
eprintln!("Couldn't call get_tree during auto_tile.");
}
} else {
eprintln!("Couldn't get connection for auto_tile");
}
}

@ -28,5 +28,5 @@ pub mod cmds;
pub mod con;
pub mod config;
pub mod demon;
pub mod ipc;
pub mod layout;
pub mod util;

Loading…
Cancel
Save