Implement i3 connection
This commit is contained in:
45
Cargo.lock
generated
45
Cargo.lock
generated
@@ -109,6 +109,12 @@ version = "1.70.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.82"
|
version = "1.0.82"
|
||||||
@@ -127,11 +133,50 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "screencfg"
|
name = "screencfg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"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]]
|
[[package]]
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.4", default-features = false, features = ["std", "derive"] }
|
clap = { version = "4.5.4", default-features = false, features = ["std", "derive"] }
|
||||||
|
serde = { version = "1.0.200", features = ["derive"] }
|
||||||
|
serde_json = "1.0.116"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["full"]
|
default = ["full"]
|
||||||
|
|||||||
407
src/i3.rs
Normal file
407
src/i3.rs
Normal 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())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/main.rs
40
src/main.rs
@@ -5,6 +5,8 @@ use std::string;
|
|||||||
|
|
||||||
use clap::{Args, Parser, ValueEnum};
|
use clap::{Args, Parser, ValueEnum};
|
||||||
|
|
||||||
|
mod i3;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Error {
|
enum Error {
|
||||||
Command(String),
|
Command(String),
|
||||||
@@ -12,6 +14,7 @@ enum Error {
|
|||||||
Workstation(String),
|
Workstation(String),
|
||||||
Plan(String),
|
Plan(String),
|
||||||
Apply(String),
|
Apply(String),
|
||||||
|
I3(i3::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
@@ -25,11 +28,18 @@ impl fmt::Display for Error {
|
|||||||
Error::Workstation(msg) => format!("workstation failed: {msg}"),
|
Error::Workstation(msg) => format!("workstation failed: {msg}"),
|
||||||
Error::Plan(msg) => format!("plan failed: {msg}"),
|
Error::Plan(msg) => format!("plan failed: {msg}"),
|
||||||
Error::Apply(msg) => format!("apply 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 {
|
impl From<io::Error> for Error {
|
||||||
fn from(value: io::Error) -> Self {
|
fn from(value: io::Error) -> Self {
|
||||||
Self::Command(value.to_string())
|
Self::Command(value.to_string())
|
||||||
@@ -430,7 +440,37 @@ struct Cli {
|
|||||||
diagram: bool,
|
diagram: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unreachable_code)]
|
||||||
fn run() -> Result<(), Error> {
|
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 args = Cli::parse();
|
||||||
|
|
||||||
let monitors = Monitor::findall()?;
|
let monitors = Monitor::findall()?;
|
||||||
|
|||||||
Reference in New Issue
Block a user