extern crate byteorder; extern crate clap; extern crate serde_json; use serde::{Deserialize, Serialize}; use clap::{Args, Parser, Subcommand}; use std::env; use std::io::Cursor; use std::io::{Read, Write}; use std::mem; use std::os::unix::net::UnixStream; use std::path::Path; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; const RUN_COMMAND: u32 = 0; const GET_WORKSPACES: u32 = 1; // const SUBSCRIBE: u32 = 2; const GET_OUTPUTS: u32 = 3; #[derive(Parser, Debug)] #[clap(author, version, about = "Better multimonitor handling for sway", long_about = None)] #[clap(propagate_version = true)] struct Cli { #[clap(subcommand)] command: Command, } #[derive(Subcommand, Debug)] enum Command { #[clap(about = "Initialize the workspaces for all the outputs")] Init(InitAction), #[clap(about = "Move the focused container to another workspace on the same output")] Move(MoveAction), #[clap(about = "Focus to another workspace on the same output")] Focus(FocusAction), #[clap(about = "Focus to output")] FocusOutput(FocusAction), #[clap(about = "Focus to another workspace on all the outputs")] FocusAllOutputs(FocusAction), #[clap(about = "Move the focused container to the next output")] NextOutput, #[clap(about = "Move the focused container to the previous output")] PrevOutput, #[clap(about = "Move the focused container to the next group")] NextGroup, #[clap(about = "Move the focused container to the previous group")] PrevGroup, #[clap(about = "Rearrange already opened workspaces")] RearrangeWorkspaces, } #[derive(Args, Debug)] struct InitAction { #[clap(value_name = "index", help = "The index to initialize with")] index: usize, } #[derive(Args, Debug)] struct FocusAction { #[clap(value_name = "index", help = "The index to focus on")] index: usize, } #[derive(Args, Debug)] struct MoveAction { #[clap(value_name = "index", help = "The index to move the container to")] index: usize, } fn get_stream() -> UnixStream { let socket_path = match env::var("I3SOCK") { Ok(val) => val, Err(_e) => { panic!("couldn't find i3/sway socket"); } }; let socket = Path::new(&socket_path); match UnixStream::connect(&socket) { Err(_) => panic!("couldn't connect to i3/sway socket"), Ok(stream) => stream, } } fn send_msg(mut stream: &UnixStream, msg_type: u32, payload: &str) { let payload_length = payload.len() as u32; let mut msg_prefix: [u8; 6 * mem::size_of::() + 2 * mem::size_of::()] = *b"i3-ipc00000000"; msg_prefix[6..] .as_mut() .write_u32::(payload_length) .expect("Unable to write"); msg_prefix[10..] .as_mut() .write_u32::(msg_type) .expect("Unable to write"); let mut msg: Vec = msg_prefix[..].to_vec(); msg.extend(payload.as_bytes()); if stream.write_all(&msg[..]).is_err() { panic!("couldn't send message"); } } fn send_command(stream: &UnixStream, command: &str) { eprint!("Sending command: '{}' - ", &command); send_msg(stream, RUN_COMMAND, command); check_success(stream); } fn read_msg(mut stream: &UnixStream) -> Result { let mut response_header: [u8; 14] = *b"uninitialized."; stream.read_exact(&mut response_header).unwrap(); if &response_header[0..6] == b"i3-ipc" { let mut v = Cursor::new(vec![ response_header[6], response_header[7], response_header[8], response_header[9], ]); let payload_length = v.read_u32::().unwrap(); let mut payload = vec![0; payload_length as usize]; stream.read_exact(&mut payload[..]).unwrap(); let payload_str = String::from_utf8(payload).unwrap(); Ok(payload_str) } else { eprint!("Not an i3-icp packet, emptying the buffer: "); let mut v = vec![]; stream.read_to_end(&mut v).unwrap(); eprintln!("{:?}", v); Err("Unable to read i3-ipc packet") } } fn check_success(stream: &UnixStream) { match read_msg(stream) { Ok(msg) => { let r: Vec = serde_json::from_str(&msg).unwrap(); match r[0]["success"] { serde_json::Value::Bool(true) => eprintln!("Command successful"), _ => panic!("Command failed: {:#?}", r), } } Err(_) => panic!("Unable to read response"), }; } #[derive(Serialize, Deserialize, Debug)] struct Output { name: String, #[serde(default)] focused: bool, active: bool, } fn get_outputs(stream: &UnixStream) -> Vec { send_msg(stream, GET_OUTPUTS, ""); let o = match read_msg(stream) { Ok(msg) => msg, Err(_) => panic!("Unable to get outputs"), }; let mut outputs: Vec = serde_json::from_str(&o).unwrap(); outputs.sort_by(|x, y| x.name.cmp(&y.name)); // sort_by_key doesn't work here (https://stackoverflow.com/a/47126516) outputs } #[derive(Serialize, Deserialize, Debug)] struct Workspace { num: usize, output: String, visible: bool, } fn get_workspaces(stream: &UnixStream) -> Vec { send_msg(stream, GET_WORKSPACES, ""); let ws = match read_msg(stream) { Ok(msg) => msg, Err(_) => panic!("Unable to get current workspace"), }; let mut workspaces: Vec = serde_json::from_str(&ws).unwrap(); workspaces.sort_by_key(|x| x.num); workspaces } fn get_current_output_index(stream: &UnixStream) -> usize { let outputs = get_outputs(stream); match outputs.iter().position(|x| x.focused) { Some(i) => i + 1, None => panic!("WTF! No focused output???"), } } fn get_current_output_name(stream: &UnixStream) -> String { let outputs = get_outputs(stream); let focused_output_index = match outputs.iter().find(|x| x.focused) { Some(i) => i.name.as_str(), None => panic!("WTF! No focused output???"), }; focused_output_index.to_string() } fn get_current_workspace(stream: &UnixStream) -> Workspace { let outputs = get_outputs(stream); let workspaces = get_workspaces(stream); workspaces .into_iter() .find(|w| w.visible && outputs.iter().find(|o| o.name == w.output).unwrap().focused) .unwrap() } fn move_container_to_workspace(stream: &UnixStream, workspace_index: usize) { if workspace_index < 10 { let mut cmd: String = "move container to workspace number ".to_string(); let full_ws_name = format!("{}{}", get_current_output_index(stream), workspace_index) .parse::() .unwrap(); cmd.push_str(&full_ws_name.to_string()); send_command(stream, &cmd); } else { move_container_to_workspace_absolute(stream, workspace_index); } } fn move_container_to_workspace_absolute(stream: &UnixStream, workspace_index: usize) { let output_index = (workspace_index / 10) as usize; let outputs = get_outputs(stream); let workspaces = get_workspaces(stream); // If the workspace already exists match workspaces.iter().find(|w| w.num == workspace_index) { Some(_) => { let mut focus_cmd: String = "move container to workspace number ".to_string(); focus_cmd.push_str(&workspace_index.to_string()); send_command(stream, &focus_cmd); } None => { let target_group = workspace_index / 10; let target_screen_index = match workspaces.iter().find(|w| w.num / 10 == target_group) { // If other workspaces on the same group exists Some(other_workspace) => Some( outputs .iter() .enumerate() .find(|i| i.1.name == other_workspace.output) .unwrap() .0, ), None => { // Or if the targeted output is currently connected if output_index < outputs.len() { Some(output_index) } else { None } } }; match target_screen_index { // If we have to send it to another screen Some(target_screen_index) => { let target_output = &outputs[target_screen_index - 1]; let current_output_name = get_current_output_name(stream); let mut focus_cmd: String = "focus output ".to_string(); focus_cmd.push_str(&target_output.name); send_command(stream, &focus_cmd); let focused_workspace_index = get_current_workspace(stream).num; let mut focus_cmd: String = "workspace ".to_string(); focus_cmd.push_str(&workspace_index.to_string()); send_command(stream, &focus_cmd); let mut focus_cmd: String = "focus output ".to_string(); focus_cmd.push_str(¤t_output_name); send_command(stream, &focus_cmd); let mut focus_cmd: String = "move container to workspace ".to_string(); focus_cmd.push_str(&workspace_index.to_string()); send_command(stream, &focus_cmd); let mut focus_cmd: String = "focus output ".to_string(); focus_cmd.push_str(&target_output.name); send_command(stream, &focus_cmd); let mut focus_cmd: String = "workspace ".to_string(); focus_cmd.push_str(&focused_workspace_index.to_string()); send_command(stream, &focus_cmd); let mut focus_cmd: String = "focus output ".to_string(); focus_cmd.push_str(¤t_output_name); send_command(stream, &focus_cmd); } None => { // Else, we send the container on the current output let mut focus_cmd: String = "move container to workspace ".to_string(); focus_cmd.push_str(&workspace_index.to_string()); send_command(stream, &focus_cmd); } }; } } } fn focus_to_workspace(stream: &UnixStream, workspace_index: usize) { if workspace_index < 10 { let mut cmd: String = "workspace number ".to_string(); let full_ws_name = format!("{}{}", get_current_output_index(stream), &workspace_index) .parse::() .unwrap(); cmd.push_str(&full_ws_name.to_string()); send_command(stream, &cmd); } else { focus_to_workspace_absolute(stream, workspace_index); } } fn focus_to_workspace_absolute(stream: &UnixStream, workspace_index: usize) { let output_index = (workspace_index / 10) as usize; let outputs = get_outputs(stream); let workspaces = get_workspaces(stream); // If the workspace already exists match workspaces.iter().find(|w| w.num == workspace_index) { Some(_) => { let mut focus_cmd: String = "workspace number ".to_string(); focus_cmd.push_str(&workspace_index.to_string()); send_command(stream, &focus_cmd); } None => { let target_group = workspace_index / 10; let target_screen_index = match workspaces.iter().find(|w| w.num / 10 == target_group) { // If other workspaces on the same group exists Some(other_workspace) => Some( outputs .iter() .enumerate() .find(|i| i.1.name == other_workspace.output) .unwrap() .0, ), None => { // Or if the targeted output is currently connected if output_index < outputs.len() { Some(output_index) } else { None } } }; match target_screen_index { // If we have to send it to another screen Some(target_screen_index) => { let target_output = &outputs[target_screen_index - 1]; let mut focus_cmd: String = "focus output ".to_string(); focus_cmd.push_str(&target_output.name); send_command(stream, &focus_cmd); } None => {} }; // Then we focus the workspace let mut focus_cmd: String = "workspace number ".to_string(); focus_cmd.push_str(&workspace_index.to_string()); send_command(stream, &focus_cmd); } } } fn focus_to_workspace_virtual_output(stream: &UnixStream, workspace_index: usize) { let current_workspace_index: usize = get_current_workspace(stream).num; let focused_output_index = current_workspace_index / 10; let mut cmd: String = "workspace number ".to_string(); let full_ws_name = format!("{}{}", focused_output_index, &workspace_index) .parse::() .unwrap(); cmd.push_str(&full_ws_name.to_string()); send_command(stream, &cmd); } fn focus_to_output(stream: &UnixStream, target_output_index: usize) { let outputs = get_outputs(stream); if target_output_index < outputs.len() { let mut cmd: String = "focus output ".to_string(); cmd.push_str(&outputs[target_output_index - 1].name); send_command(stream, &cmd); } else { let current_workspace_index = get_current_workspace(stream).num; let contextual_workspace_index = current_workspace_index - (current_workspace_index / 10) * 10; focus_to_workspace_virtual_output(stream, contextual_workspace_index); } } fn focus_all_outputs_to_workspace(stream: &UnixStream, workspace_index: usize) { let current_output = get_current_output_name(stream); // Iterate on all outputs to focus on the given workspace let outputs = get_outputs(stream); for output in outputs.iter() { let mut cmd: String = "focus output ".to_string(); cmd.push_str(output.name.as_str()); send_command(stream, &cmd); focus_to_workspace(stream, workspace_index); } // Get back to currently focused output let mut cmd: String = "focus output ".to_string(); cmd.push_str(¤t_output); send_command(stream, &cmd); } fn move_container_to_next_output(stream: &UnixStream) { move_container_to_next_or_prev_output(stream, false); } fn move_container_to_prev_output(stream: &UnixStream) { move_container_to_next_or_prev_output(stream, true); } fn move_container_to_next_or_prev_output(stream: &UnixStream, go_to_prev: bool) { let outputs = get_outputs(stream); let focused_output_index = get_current_output_index(stream); let target_output = if go_to_prev { &outputs[(focused_output_index + outputs.len() - 1) % outputs.len() - 1] } else { &outputs[(focused_output_index + 1) % outputs.len() - 1] }; let workspaces = get_workspaces(stream); let target_workspace = workspaces .iter() .find(|x| x.output == target_output.name && x.visible) .unwrap(); // Move container to target workspace let mut cmd: String = "move container to workspace number ".to_string(); cmd.push_str(&target_workspace.num.to_string()); send_command(stream, &cmd); // Focus that workspace to follow the container let mut cmd: String = "workspace number ".to_string(); cmd.push_str(&target_workspace.num.to_string()); send_command(stream, &cmd); } fn move_container_to_next_group(stream: &UnixStream) { move_container_to_next_or_prev_group(stream, false); } fn move_container_to_prev_group(stream: &UnixStream) { move_container_to_next_or_prev_group(stream, true); } fn move_container_to_next_or_prev_group(stream: &UnixStream, go_to_prev: bool) { let current_workspace_index: usize = get_current_workspace(stream).num; let focused_group_index = current_workspace_index / 10; let outputs = get_outputs(stream); if focused_group_index < outputs.len() { let target_output = if go_to_prev { &outputs[(focused_group_index - 1) - 1] } else { &outputs[(focused_group_index + 1) - 1] }; let mut cmd: String = "focus output ".to_string(); cmd.push_str(&target_output.name); send_command(stream, &cmd); } else { let target_workspace = if go_to_prev { current_workspace_index - 10 } else { current_workspace_index + 10 }; let mut cmd: String = "workspace number ".to_string(); cmd.push_str(&target_workspace.to_string()); send_command(stream, &cmd); }; } fn init_workspaces(stream: &UnixStream, workspace_index: usize) { let outputs = get_outputs(stream); let cmd_prefix: String = "focus output ".to_string(); for output in outputs.iter().filter(|x| x.active).rev() { let mut cmd = cmd_prefix.clone(); cmd.push_str(output.name.as_str()); println!("{:?}", cmd.clone()); send_command(stream, &cmd); focus_to_workspace(stream, workspace_index); } } fn rearrange_workspaces(stream: &UnixStream) { let outputs = get_outputs(stream); let workspaces = get_workspaces(stream); let focus_cmd_prefix: String = "workspace number ".to_string(); let move_cmd_prefix: String = "move workspace to ".to_string(); for workspace in workspaces.iter() { let mut focus_cmd = focus_cmd_prefix.clone(); focus_cmd.push_str(&workspace.num.to_string()); send_command(stream, &focus_cmd); let output_index = workspace.num / 10; if output_index < outputs.len() { let mut move_cmd = move_cmd_prefix.clone(); move_cmd.push_str(&outputs[output_index - 1].name); send_command(stream, &move_cmd); } } } fn main() { let cli = Cli::parse(); let stream = get_stream(); match &cli.command { Command::Init(action) => { init_workspaces(&stream, action.index); } Command::Move(action) => { move_container_to_workspace(&stream, action.index); } Command::Focus(action) => { focus_to_workspace(&stream, action.index); } Command::FocusOutput(action) => { focus_to_output(&stream, action.index); } Command::FocusAllOutputs(action) => { focus_all_outputs_to_workspace(&stream, action.index); } Command::NextOutput => { move_container_to_next_output(&stream); } Command::PrevOutput => { move_container_to_prev_output(&stream); } Command::NextGroup => { move_container_to_next_group(&stream); } Command::PrevGroup => { move_container_to_prev_group(&stream); } Command::RearrangeWorkspaces => { rearrange_workspaces(&stream); } } }