Implement i3 connection

This commit is contained in:
2024-05-08 00:32:48 +02:00
parent 73570c320f
commit 4a968e5ba5
4 changed files with 494 additions and 0 deletions

45
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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"]

407
src/i3.rs Normal file
View File

@@ -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<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::Command(value.to_string())
}
}
impl From<serde_json::Error> 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<Command> 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<Version, Error> {
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<Outputs, Error> {
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<Workspaces, Error> {
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<PathBuf, Error> {
let cmd = process::Command::new("i3")
.arg("--get-socketpath")
.output()?;
let bytes = cmd
.stdout
.into_iter()
.take_while(|c| *c != b'\n')
.collect::<Vec<u8>>();
let string = OsStr::from_bytes(&bytes);
let path = PathBuf::from(string);
Ok(path)
}
pub fn connect() -> Result<Connection, Error> {
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<OutputPayload> for Output {
fn from(value: OutputPayload) -> Self {
Self {
name: value.name,
active: value.active,
primary: value.primary,
}
}
}
#[derive(Debug)]
pub struct Workspaces(Vec<Workspace>);
impl From<Vec<WorkspacePayload>> for Workspaces {
fn from(value: Vec<WorkspacePayload>) -> Self {
Self(value.into_iter().map(Into::into).collect())
}
}
impl IntoIterator for Workspaces {
type Item = Workspace;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[derive(Debug)]
pub struct Outputs(Vec<Output>);
impl From<Vec<OutputPayload>> for Outputs {
fn from(value: Vec<OutputPayload>) -> Self {
Self(
value
.into_iter()
.filter(|output| output.name != "xroot-0")
.map(Into::into)
.collect(),
)
}
}
impl IntoIterator for Outputs {
type Item = Output;
type IntoIter = <Vec<Output> 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<usize> for Outputs {
type Output = Output;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
enum Message {
Command(Command),
Workspaces,
Outputs,
Version,
}
impl From<Message> 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<u8> {
let payload: Option<String> = match self {
Message::Command(ref command) => Some(command.clone().into()),
Message::Workspaces => None,
Message::Outputs => None,
Message::Version => None,
};
let mut message: Vec<u8> = 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<VersionPayload> 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<String>,
}
#[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<WorkspacePayload> 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<WorkspacePayload>),
Command(Vec<CommandPayload>),
Outputs(Vec<OutputPayload>),
}
impl Response {
fn read(stream: &mut Connection) -> Result<Self, Error> {
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())),
}
}
}

View File

@@ -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<i3::Error> for Error {
fn from(value: i3::Error) -> Self {
Self::I3(value)
}
}
impl From<io::Error> 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()?;