From 4a968e5ba5b12bf7eed64ee13c20cffee6126669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Wed, 8 May 2024 00:32:48 +0200 Subject: [PATCH] Implement i3 connection --- Cargo.lock | 45 ++++++ Cargo.toml | 2 + src/i3.rs | 407 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 40 ++++++ 4 files changed, 494 insertions(+) create mode 100644 src/i3.rs diff --git a/Cargo.lock b/Cargo.lock index 85903ef..a1e6c86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,12 @@ version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "proc-macro2" version = "1.0.82" @@ -127,11 +133,50 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "screencfg" version = "0.1.0" dependencies = [ "clap", + "serde", + "serde_json", +] + +[[package]] +name = "serde" +version = "1.0.200" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.200" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +dependencies = [ + "itoa", + "ryu", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4881698..94a1e70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [dependencies] clap = { version = "4.5.4", default-features = false, features = ["std", "derive"] } +serde = { version = "1.0.200", features = ["derive"] } +serde_json = "1.0.116" [features] default = ["full"] diff --git a/src/i3.rs b/src/i3.rs new file mode 100644 index 0000000..c53d51f --- /dev/null +++ b/src/i3.rs @@ -0,0 +1,407 @@ +use std::ffi::OsStr; +use std::fmt; +use std::io::{self, Read, Write}; +use std::ops::{Deref, DerefMut, Index}; +use std::os::unix::ffi::OsStrExt as _; +use std::os::unix::net; +use std::path::PathBuf; +use std::process; +use std::time::Duration; + +#[derive(Debug)] +pub enum Error { + Connection(String), + Command(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Error::Connection(msg) => format!("connection failed: {msg}"), + Error::Command(msg) => format!("command failed: {msg}"), + } + ) + } +} + +impl From for Error { + fn from(value: io::Error) -> Self { + Self::Command(value.to_string()) + } +} + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Self::Connection(value.to_string()) + } +} + +impl std::error::Error for Error {} + +pub struct Connection(net::UnixStream); + +#[derive(Clone)] +pub enum Command { + Nop, + MoveWorkspace { id: usize, output: Output }, +} + +impl From for String { + fn from(value: Command) -> Self { + match value { + Command::Nop => "nop".to_string(), + Command::MoveWorkspace { id, output } => { + format!( + "[workspace=\"{id}\"] move workspace to output {}", + output.name + ) + } + } + } +} + +impl Connection { + pub fn version(&mut self) -> Result { + Message::Version.send(self)?; + + let response = Response::read(self)?; + + match response { + Response::Version(version) => Ok(version.into()), + _ => Err(Error::Connection( + "received invalid response from i3".into(), + )), + } + } + + pub fn outputs(&mut self) -> Result { + Message::Outputs.send(self)?; + let response = Response::read(self)?; + + match response { + Response::Outputs(outputs) => Ok(outputs.into()), + _ => Err(Error::Connection( + "received invalid response from i3".into(), + )), + } + } + + pub fn workspaces(&mut self) -> Result { + Message::Workspaces.send(self)?; + + let response = Response::read(self)?; + + match response { + Response::Workspaces(workspaces) => Ok(workspaces.into()), + _ => Err(Error::Connection( + "received invalid response from i3".into(), + )), + } + } + + pub fn command(&mut self, command: Command) -> Result<(), Error> { + Message::Command(command).send(self)?; + + let response = Response::read(self)?; + match response { + Response::Command(commands) => { + for payload in commands { + if !payload.success { + return Err(Error::Command( + payload.error.unwrap_or_else(|| "unknown error".into()), + )); + } + } + Ok(()) + } + _ => Err(Error::Connection( + "received invalid response from i3".into(), + )), + } + } +} + +fn get_socketpath() -> Result { + let cmd = process::Command::new("i3") + .arg("--get-socketpath") + .output()?; + + let bytes = cmd + .stdout + .into_iter() + .take_while(|c| *c != b'\n') + .collect::>(); + + let string = OsStr::from_bytes(&bytes); + + let path = PathBuf::from(string); + + Ok(path) +} + +pub fn connect() -> Result { + let socketpath = get_socketpath()?; + + let socket = net::SocketAddr::from_pathname(socketpath)?; + + let stream = net::UnixStream::connect_addr(&socket)?; + stream.set_read_timeout(Some(Duration::from_millis(100)))?; + + Ok(Connection(stream)) +} + +#[derive(Debug, serde::Deserialize)] +struct OutputPayload { + name: String, + active: bool, + primary: bool, +} + +#[derive(Clone, Debug)] +pub struct Output { + name: String, + active: bool, + primary: bool, +} + +impl fmt::Display for Output { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name)?; + if self.active { + write!(f, " [active]")?; + } + if self.primary { + write!(f, " [primary]")?; + } + Ok(()) + } +} + +impl From for Output { + fn from(value: OutputPayload) -> Self { + Self { + name: value.name, + active: value.active, + primary: value.primary, + } + } +} + +#[derive(Debug)] +pub struct Workspaces(Vec); + +impl From> for Workspaces { + fn from(value: Vec) -> Self { + Self(value.into_iter().map(Into::into).collect()) + } +} + +impl IntoIterator for Workspaces { + type Item = Workspace; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[derive(Debug)] +pub struct Outputs(Vec); + +impl From> for Outputs { + fn from(value: Vec) -> Self { + Self( + value + .into_iter() + .filter(|output| output.name != "xroot-0") + .map(Into::into) + .collect(), + ) + } +} + +impl IntoIterator for Outputs { + type Item = Output; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl Deref for Outputs { + type Target = [Output]; + + fn deref(&self) -> &[Output] { + &self.0[..] + } +} +impl DerefMut for Outputs { + fn deref_mut(&mut self) -> &mut [Output] { + &mut self.0[..] + } +} + +impl Index for Outputs { + type Output = Output; + + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} + +enum Message { + Command(Command), + Workspaces, + Outputs, + Version, +} + +impl From for u32 { + fn from(value: Message) -> Self { + match value { + Message::Command(_) => 0, + Message::Workspaces => 1, + Message::Outputs => 3, + Message::Version => 7, + } + } +} + +impl Message { + fn bytes(self) -> Vec { + let payload: Option = match self { + Message::Command(ref command) => Some(command.clone().into()), + Message::Workspaces => None, + Message::Outputs => None, + Message::Version => None, + }; + + let mut message: Vec = vec![]; + let command_number: u32 = self.into(); + + message.extend_from_slice("i3-ipc".as_bytes()); + message.extend_from_slice( + &(payload.as_ref().map_or(0, |payload| payload.len()) as u32).to_ne_bytes(), + ); + message.extend_from_slice(&(command_number.to_ne_bytes())); + if let Some(payload) = payload { + message.extend_from_slice(payload.as_bytes()) + } + message + } + + fn send(self, socket: &mut Connection) -> Result<(), Error> { + let message = self.bytes(); + println!("{message:?}"); + socket.0.write_all(&message)?; + Ok(()) + } +} + +#[allow(dead_code)] +#[derive(Debug, serde::Deserialize)] +struct VersionPayload { + human_readable: String, + loaded_config_file_name: String, + major: usize, + minor: usize, + patch: usize, +} + +pub struct Version { + minor: usize, + patch: usize, + major: usize, +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl From for Version { + fn from(value: VersionPayload) -> Self { + Self { + major: value.major, + minor: value.minor, + patch: value.patch, + } + } +} + +#[derive(Debug, serde::Deserialize)] +struct WorkspacePayload { + #[allow(dead_code)] + id: usize, + num: usize, + name: String, + output: String, +} + +#[derive(Debug, serde::Deserialize)] +struct CommandPayload { + success: bool, + error: Option, +} + +#[derive(Debug)] +pub struct Workspace { + num: usize, + #[allow(dead_code)] + name: String, + output: String, +} + +impl fmt::Display for Workspace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} on {}", self.num, self.output) + } +} + +impl From for Workspace { + fn from(value: WorkspacePayload) -> Self { + Self { + num: value.num, + name: value.name, + output: value.output, + } + } +} + +#[allow(dead_code)] +#[derive(Debug)] +enum Response { + Version(VersionPayload), + Workspaces(Vec), + Command(Vec), + Outputs(Vec), +} + +impl Response { + fn read(stream: &mut Connection) -> Result { + let mut response = vec![0; "i3-ipc".chars().count() + 4 + 4]; + + stream.0.read_exact(&mut response)?; + + assert_eq!(&response[0..6], "i3-ipc".as_bytes()); + let response_length = u32::from_ne_bytes(response[6..10].try_into().unwrap()); + let response_command = u32::from_ne_bytes(response[10..14].try_into().unwrap()); + + response = vec![0; response_length as usize]; + + stream.0.read_exact(&mut response)?; + + match response_command { + 0 => Ok(Response::Command(serde_json::from_slice(&response)?)), + 1 => Ok(Response::Workspaces(serde_json::from_slice(&response)?)), + 3 => Ok(Response::Outputs(serde_json::from_slice(&response)?)), + 7 => Ok(Response::Version(serde_json::from_slice(&response)?)), + _ => return Err(Error::Connection("unknown response type".into())), + } + } +} diff --git a/src/main.rs b/src/main.rs index 9b503e1..3955640 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ use std::string; use clap::{Args, Parser, ValueEnum}; +mod i3; + #[derive(Debug)] enum Error { Command(String), @@ -12,6 +14,7 @@ enum Error { Workstation(String), Plan(String), Apply(String), + I3(i3::Error), } impl fmt::Display for Error { @@ -25,11 +28,18 @@ impl fmt::Display for Error { Error::Workstation(msg) => format!("workstation failed: {msg}"), Error::Plan(msg) => format!("plan failed: {msg}"), Error::Apply(msg) => format!("apply failed: {msg}"), + Error::I3(e) => format!("i3: {e}"), }, ) } } +impl From for Error { + fn from(value: i3::Error) -> Self { + Self::I3(value) + } +} + impl From for Error { fn from(value: io::Error) -> Self { Self::Command(value.to_string()) @@ -430,7 +440,37 @@ struct Cli { diagram: bool, } +#[allow(unreachable_code)] fn run() -> Result<(), Error> { + let mut i3_connection = i3::connect()?; + + let version = i3_connection.version()?; + + println!("i3 version: {version}"); + + let workspaces = i3_connection.workspaces()?; + + println!("i3 workspaces:"); + for workspace in workspaces { + println!("- {workspace}"); + } + + i3_connection.command(i3::Command::Nop)?; + + let outputs = i3_connection.outputs()?; + + println!("i3 outputs:"); + for output in outputs.iter() { + println!("- {output}") + } + + // i3_connection.command(i3::Command::MoveWorkspace { + // id: 3, + // output: outputs[0], + // })?; + + return Ok(()); + let args = Cli::parse(); let monitors = Monitor::findall()?;