mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-03-12 15:50:55 +03:00
Compare commits
8 Commits
dns-fix
...
rework-cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5ecc0a931 | ||
|
|
58383646f8 | ||
|
|
83c3319a26 | ||
|
|
5fb04176be | ||
|
|
d8e2c1ef02 | ||
|
|
7898f2362c | ||
|
|
50a778452e | ||
|
|
f247300f8c |
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
@@ -2005,8 +2005,10 @@ dependencies = [
|
|||||||
name = "lan-mouse-cli"
|
name = "lan-mouse-cli"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
"lan-mouse-ipc",
|
"lan-mouse-ipc",
|
||||||
|
"thiserror 2.0.0",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -144,10 +144,11 @@ rust toolchain.
|
|||||||
Additionally, available backends and frontends can be configured manually via
|
Additionally, available backends and frontends can be configured manually via
|
||||||
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
|
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
|
||||||
|
|
||||||
E.g. if only wayland support is needed, the following command produces
|
E.g. if only support for sway is needed, the following command produces
|
||||||
an executable with just support for wayland:
|
an executable with support for only the `layer-shell` capture backend
|
||||||
|
and `wlroots` emulation backend:
|
||||||
```sh
|
```sh
|
||||||
cargo build --no-default-features --features wayland
|
cargo build --no-default-features --features layer_shell_capture,wlroots_emulation
|
||||||
```
|
```
|
||||||
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
|
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
|
||||||
</details>
|
</details>
|
||||||
@@ -287,10 +288,10 @@ $ cargo run --release -- --frontend cli
|
|||||||
|
|
||||||
Lan Mouse can be launched in daemon mode to keep it running in the background (e.g. for use in a systemd-service).
|
Lan Mouse can be launched in daemon mode to keep it running in the background (e.g. for use in a systemd-service).
|
||||||
|
|
||||||
To do so, add `--daemon` to the commandline args:
|
To do so, use the `daemon` subcommand:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
lan-mouse --daemon
|
lan-mouse daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
In order to start lan-mouse with a graphical session automatically,
|
In order to start lan-mouse with a graphical session automatically,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ repository = "https://github.com/feschber/lan-mouse"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
|
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
|
||||||
|
clap = { version = "4.4.11", features = ["derive"] }
|
||||||
|
thiserror = "2.0.0"
|
||||||
tokio = { version = "1.32.0", features = [
|
tokio = { version = "1.32.0", features = [
|
||||||
"io-util",
|
"io-util",
|
||||||
"io-std",
|
"io-std",
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fmt::Display,
|
|
||||||
str::{FromStr, SplitWhitespace},
|
|
||||||
};
|
|
||||||
|
|
||||||
use lan_mouse_ipc::{ClientHandle, Position};
|
|
||||||
|
|
||||||
pub(super) enum CommandType {
|
|
||||||
NoCommand,
|
|
||||||
Help,
|
|
||||||
Connect,
|
|
||||||
Disconnect,
|
|
||||||
Activate,
|
|
||||||
Deactivate,
|
|
||||||
List,
|
|
||||||
SetHost,
|
|
||||||
SetPort,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(super) struct InvalidCommand {
|
|
||||||
cmd: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for InvalidCommand {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "invalid command: \"{}\"", self.cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for CommandType {
|
|
||||||
type Err = InvalidCommand;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"connect" => Ok(Self::Connect),
|
|
||||||
"disconnect" => Ok(Self::Disconnect),
|
|
||||||
"activate" => Ok(Self::Activate),
|
|
||||||
"deactivate" => Ok(Self::Deactivate),
|
|
||||||
"list" => Ok(Self::List),
|
|
||||||
"set-host" => Ok(Self::SetHost),
|
|
||||||
"set-port" => Ok(Self::SetPort),
|
|
||||||
"help" => Ok(Self::Help),
|
|
||||||
_ => Err(InvalidCommand { cmd: s.to_string() }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(super) enum Command {
|
|
||||||
None,
|
|
||||||
Help,
|
|
||||||
Connect(Position, String, Option<u16>),
|
|
||||||
Disconnect(ClientHandle),
|
|
||||||
Activate(ClientHandle),
|
|
||||||
Deactivate(ClientHandle),
|
|
||||||
List,
|
|
||||||
SetHost(ClientHandle, String),
|
|
||||||
SetPort(ClientHandle, Option<u16>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommandType {
|
|
||||||
pub(super) fn usage(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
CommandType::Help => "help",
|
|
||||||
CommandType::NoCommand => "",
|
|
||||||
CommandType::Connect => "connect left|right|top|bottom <host> [<port>]",
|
|
||||||
CommandType::Disconnect => "disconnect <id>",
|
|
||||||
CommandType::Activate => "activate <id>",
|
|
||||||
CommandType::Deactivate => "deactivate <id>",
|
|
||||||
CommandType::List => "list",
|
|
||||||
CommandType::SetHost => "set-host <id> <host>",
|
|
||||||
CommandType::SetPort => "set-port <id> <host>",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) enum CommandParseError {
|
|
||||||
Usage(CommandType),
|
|
||||||
Invalid(InvalidCommand),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for CommandParseError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Usage(cmd) => write!(f, "usage: {}", cmd.usage()),
|
|
||||||
Self::Invalid(cmd) => write!(f, "{}", cmd),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Command {
|
|
||||||
type Err = CommandParseError;
|
|
||||||
|
|
||||||
fn from_str(cmd: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut args = cmd.split_whitespace();
|
|
||||||
let cmd_type: CommandType = match args.next() {
|
|
||||||
Some(c) => c.parse().map_err(CommandParseError::Invalid),
|
|
||||||
None => Ok(CommandType::NoCommand),
|
|
||||||
}?;
|
|
||||||
match cmd_type {
|
|
||||||
CommandType::Help => Ok(Command::Help),
|
|
||||||
CommandType::NoCommand => Ok(Command::None),
|
|
||||||
CommandType::Connect => parse_connect_cmd(args),
|
|
||||||
CommandType::Disconnect => parse_disconnect_cmd(args),
|
|
||||||
CommandType::Activate => parse_activate_cmd(args),
|
|
||||||
CommandType::Deactivate => parse_deactivate_cmd(args),
|
|
||||||
CommandType::List => Ok(Command::List),
|
|
||||||
CommandType::SetHost => parse_set_host(args),
|
|
||||||
CommandType::SetPort => parse_set_port(args),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_connect_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
|
||||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Connect);
|
|
||||||
let pos = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
|
||||||
let host = args.next().ok_or(USAGE)?.to_string();
|
|
||||||
let port = args.next().and_then(|p| p.parse().ok());
|
|
||||||
Ok(Command::Connect(pos, host, port))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_disconnect_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
|
||||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Disconnect);
|
|
||||||
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
|
||||||
Ok(Command::Disconnect(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_activate_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
|
||||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Activate);
|
|
||||||
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
|
||||||
Ok(Command::Activate(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_deactivate_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
|
||||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Deactivate);
|
|
||||||
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
|
||||||
Ok(Command::Deactivate(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_set_host(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
|
||||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetHost);
|
|
||||||
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
|
||||||
let host = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
|
||||||
Ok(Command::SetHost(id, host))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_set_port(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
|
||||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetPort);
|
|
||||||
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
|
||||||
let port = args.next().and_then(|p| p.parse().ok());
|
|
||||||
Ok(Command::SetPort(id, port))
|
|
||||||
}
|
|
||||||
@@ -1,320 +1,167 @@
|
|||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use tokio::{
|
|
||||||
io::{AsyncBufReadExt, BufReader},
|
|
||||||
task::LocalSet,
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::io::{self, Write};
|
use std::{net::IpAddr, time::Duration};
|
||||||
|
use thiserror::Error;
|
||||||
use self::command::{Command, CommandType};
|
|
||||||
|
|
||||||
use lan_mouse_ipc::{
|
use lan_mouse_ipc::{
|
||||||
AsyncFrontendEventReader, AsyncFrontendRequestWriter, ClientConfig, ClientHandle, ClientState,
|
connect_async, ClientHandle, ConnectionError, FrontendEvent, FrontendRequest, IpcError,
|
||||||
FrontendEvent, FrontendRequest, IpcError, DEFAULT_PORT,
|
Position,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod command;
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CliError {
|
||||||
|
/// is the service running?
|
||||||
|
#[error("could not connect: `{0}` - is the service running?")]
|
||||||
|
ServiceNotRunning(#[from] ConnectionError),
|
||||||
|
#[error("error communicating with service: {0}")]
|
||||||
|
Ipc(#[from] IpcError),
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run() -> Result<(), IpcError> {
|
#[derive(Parser, Debug, PartialEq, Eq)]
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
#[command(name = "lan-mouse-cli", about = "LanMouse CLI interface")]
|
||||||
.enable_io()
|
pub struct CliArgs {
|
||||||
.enable_time()
|
#[command(subcommand)]
|
||||||
.build()?;
|
command: CliSubcommand,
|
||||||
runtime.block_on(LocalSet::new().run_until(async move {
|
}
|
||||||
let (rx, tx) = lan_mouse_ipc::connect_async().await?;
|
|
||||||
let mut cli = Cli::new(rx, tx);
|
#[derive(Args, Clone, Debug, PartialEq, Eq)]
|
||||||
cli.run().await
|
struct Client {
|
||||||
}))?;
|
#[arg(long)]
|
||||||
|
hostname: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
port: Option<u16>,
|
||||||
|
#[arg(long)]
|
||||||
|
ips: Option<Vec<IpAddr>>,
|
||||||
|
#[arg(long)]
|
||||||
|
enter_hook: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug, PartialEq, Eq)]
|
||||||
|
enum CliSubcommand {
|
||||||
|
/// add a new client
|
||||||
|
AddClient(Client),
|
||||||
|
/// remove an existing client
|
||||||
|
RemoveClient { id: ClientHandle },
|
||||||
|
/// activate a client
|
||||||
|
Activate { id: ClientHandle },
|
||||||
|
/// deactivate a client
|
||||||
|
Deactivate { id: ClientHandle },
|
||||||
|
/// list configured clients
|
||||||
|
List,
|
||||||
|
/// change hostname
|
||||||
|
SetHost {
|
||||||
|
id: ClientHandle,
|
||||||
|
host: Option<String>,
|
||||||
|
},
|
||||||
|
/// change port
|
||||||
|
SetPort { id: ClientHandle, port: u16 },
|
||||||
|
/// set position
|
||||||
|
SetPosition { id: ClientHandle, pos: Position },
|
||||||
|
/// set ips
|
||||||
|
SetIps { id: ClientHandle, ips: Vec<IpAddr> },
|
||||||
|
/// re-enable capture
|
||||||
|
EnableCapture,
|
||||||
|
/// re-enable emulation
|
||||||
|
EnableEmulation,
|
||||||
|
/// authorize a public key
|
||||||
|
AuthorizeKey {
|
||||||
|
description: String,
|
||||||
|
sha256_fingerprint: String,
|
||||||
|
},
|
||||||
|
/// deauthorize a public key
|
||||||
|
RemoveAuthorizedKey { sha256_fingerprint: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(args: CliArgs) -> Result<(), CliError> {
|
||||||
|
execute(args.command).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Cli {
|
async fn execute(cmd: CliSubcommand) -> Result<(), CliError> {
|
||||||
clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
|
let (mut rx, mut tx) = connect_async(Some(Duration::from_millis(500))).await?;
|
||||||
changed: Option<ClientHandle>,
|
match cmd {
|
||||||
rx: AsyncFrontendEventReader,
|
CliSubcommand::AddClient(Client {
|
||||||
tx: AsyncFrontendRequestWriter,
|
hostname,
|
||||||
}
|
port,
|
||||||
|
ips,
|
||||||
impl Cli {
|
enter_hook,
|
||||||
fn new(rx: AsyncFrontendEventReader, tx: AsyncFrontendRequestWriter) -> Cli {
|
}) => {
|
||||||
Self {
|
tx.request(FrontendRequest::Create).await?;
|
||||||
clients: vec![],
|
while let Some(e) = rx.next().await {
|
||||||
changed: None,
|
if let FrontendEvent::Created(handle, _, _) = e? {
|
||||||
rx,
|
if let Some(hostname) = hostname {
|
||||||
tx,
|
tx.request(FrontendRequest::UpdateHostname(handle, Some(hostname)))
|
||||||
}
|
.await?;
|
||||||
}
|
|
||||||
|
|
||||||
async fn run(&mut self) -> Result<(), IpcError> {
|
|
||||||
let stdin = tokio::io::stdin();
|
|
||||||
let stdin = BufReader::new(stdin);
|
|
||||||
let mut stdin = stdin.lines();
|
|
||||||
|
|
||||||
/* initial state sync */
|
|
||||||
self.clients = loop {
|
|
||||||
match self.rx.next().await {
|
|
||||||
Some(Ok(e)) => {
|
|
||||||
if let FrontendEvent::Enumerate(clients) = e {
|
|
||||||
break clients;
|
|
||||||
}
|
}
|
||||||
}
|
if let Some(port) = port {
|
||||||
Some(Err(e)) => return Err(e),
|
tx.request(FrontendRequest::UpdatePort(handle, port))
|
||||||
None => return Ok(()),
|
.await?;
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
|
||||||
prompt()?;
|
|
||||||
tokio::select! {
|
|
||||||
line = stdin.next_line() => {
|
|
||||||
let Some(line) = line? else {
|
|
||||||
break Ok(());
|
|
||||||
};
|
|
||||||
let cmd: Command = match line.parse() {
|
|
||||||
Ok(cmd) => cmd,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.execute(cmd).await?;
|
|
||||||
}
|
|
||||||
event = self.rx.next() => {
|
|
||||||
if let Some(event) = event {
|
|
||||||
self.handle_event(event?);
|
|
||||||
} else {
|
|
||||||
break Ok(());
|
|
||||||
}
|
}
|
||||||
}
|
if let Some(ips) = ips {
|
||||||
}
|
tx.request(FrontendRequest::UpdateFixIps(handle, ips))
|
||||||
if let Some(handle) = self.changed.take() {
|
.await?;
|
||||||
self.update_client(handle).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_client(&mut self, handle: ClientHandle) -> Result<(), IpcError> {
|
|
||||||
self.tx.request(FrontendRequest::GetState(handle)).await?;
|
|
||||||
while let Some(Ok(event)) = self.rx.next().await {
|
|
||||||
self.handle_event(event.clone());
|
|
||||||
if let FrontendEvent::State(_, _, _) | FrontendEvent::NoSuchClient(_) = event {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&mut self, cmd: Command) -> Result<(), IpcError> {
|
|
||||||
match cmd {
|
|
||||||
Command::None => {}
|
|
||||||
Command::Connect(pos, host, port) => {
|
|
||||||
let request = FrontendRequest::Create;
|
|
||||||
self.tx.request(request).await?;
|
|
||||||
let handle = loop {
|
|
||||||
if let Some(Ok(event)) = self.rx.next().await {
|
|
||||||
match event {
|
|
||||||
FrontendEvent::Created(h, c, s) => {
|
|
||||||
self.clients.push((h, c, s));
|
|
||||||
break h;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.handle_event(event);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
if let Some(enter_hook) = enter_hook {
|
||||||
for request in [
|
tx.request(FrontendRequest::UpdateEnterHook(handle, Some(enter_hook)))
|
||||||
FrontendRequest::UpdateHostname(handle, Some(host.clone())),
|
.await?;
|
||||||
FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)),
|
|
||||||
FrontendRequest::UpdatePosition(handle, pos),
|
|
||||||
] {
|
|
||||||
self.tx.request(request).await?;
|
|
||||||
}
|
|
||||||
self.update_client(handle).await?;
|
|
||||||
}
|
|
||||||
Command::Disconnect(id) => {
|
|
||||||
self.tx.request(FrontendRequest::Delete(id)).await?;
|
|
||||||
loop {
|
|
||||||
if let Some(Ok(event)) = self.rx.next().await {
|
|
||||||
self.handle_event(event.clone());
|
|
||||||
if let FrontendEvent::Deleted(_) = event {
|
|
||||||
self.handle_event(event);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
}
|
|
||||||
Command::Activate(id) => {
|
|
||||||
self.tx.request(FrontendRequest::Activate(id, true)).await?;
|
|
||||||
self.update_client(id).await?;
|
|
||||||
}
|
|
||||||
Command::Deactivate(id) => {
|
|
||||||
self.tx
|
|
||||||
.request(FrontendRequest::Activate(id, false))
|
|
||||||
.await?;
|
|
||||||
self.update_client(id).await?;
|
|
||||||
}
|
|
||||||
Command::List => {
|
|
||||||
self.tx.request(FrontendRequest::Enumerate()).await?;
|
|
||||||
while let Some(e) = self.rx.next().await {
|
|
||||||
let event = e?;
|
|
||||||
self.handle_event(event.clone());
|
|
||||||
if let FrontendEvent::Enumerate(_) = event {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::SetHost(handle, host) => {
|
|
||||||
let request = FrontendRequest::UpdateHostname(handle, Some(host.clone()));
|
|
||||||
self.tx.request(request).await?;
|
|
||||||
self.update_client(handle).await?;
|
|
||||||
}
|
|
||||||
Command::SetPort(handle, port) => {
|
|
||||||
let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT));
|
|
||||||
self.tx.request(request).await?;
|
|
||||||
self.update_client(handle).await?;
|
|
||||||
}
|
|
||||||
Command::Help => {
|
|
||||||
for cmd_type in [
|
|
||||||
CommandType::List,
|
|
||||||
CommandType::Connect,
|
|
||||||
CommandType::Disconnect,
|
|
||||||
CommandType::Activate,
|
|
||||||
CommandType::Deactivate,
|
|
||||||
CommandType::SetHost,
|
|
||||||
CommandType::SetPort,
|
|
||||||
] {
|
|
||||||
eprintln!("{}", cmd_type.usage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
CliSubcommand::RemoveClient { id } => tx.request(FrontendRequest::Delete(id)).await?,
|
||||||
}
|
CliSubcommand::Activate { id } => tx.request(FrontendRequest::Activate(id, true)).await?,
|
||||||
|
CliSubcommand::Deactivate { id } => {
|
||||||
fn find_mut(
|
tx.request(FrontendRequest::Activate(id, false)).await?
|
||||||
&mut self,
|
}
|
||||||
handle: ClientHandle,
|
CliSubcommand::List => {
|
||||||
) -> Option<&mut (ClientHandle, ClientConfig, ClientState)> {
|
tx.request(FrontendRequest::Enumerate()).await?;
|
||||||
self.clients.iter_mut().find(|(h, _, _)| *h == handle)
|
while let Some(e) = rx.next().await {
|
||||||
}
|
if let FrontendEvent::Enumerate(clients) = e? {
|
||||||
|
for (handle, config, state) in clients {
|
||||||
fn remove(
|
let host = config.hostname.unwrap_or("unknown".to_owned());
|
||||||
&mut self,
|
let port = config.port;
|
||||||
handle: ClientHandle,
|
let pos = config.pos;
|
||||||
) -> Option<(ClientHandle, ClientConfig, ClientState)> {
|
let active = state.active;
|
||||||
let idx = self.clients.iter().position(|(h, _, _)| *h == handle);
|
let ips = state.ips;
|
||||||
idx.map(|i| self.clients.swap_remove(i))
|
println!(
|
||||||
}
|
"id {handle}: {host}:{port} ({pos}) active: {active}, ips: {ips:?}"
|
||||||
|
|
||||||
fn handle_event(&mut self, event: FrontendEvent) {
|
|
||||||
match event {
|
|
||||||
FrontendEvent::Changed(h) => self.changed = Some(h),
|
|
||||||
FrontendEvent::Created(h, c, s) => {
|
|
||||||
eprint!("client added ({h}): ");
|
|
||||||
print_config(&c);
|
|
||||||
eprint!(" ");
|
|
||||||
print_state(&s);
|
|
||||||
eprintln!();
|
|
||||||
self.clients.push((h, c, s));
|
|
||||||
}
|
|
||||||
FrontendEvent::NoSuchClient(h) => {
|
|
||||||
eprintln!("no such client: {h}");
|
|
||||||
}
|
|
||||||
FrontendEvent::State(h, c, s) => {
|
|
||||||
if let Some((_, config, state)) = self.find_mut(h) {
|
|
||||||
let old_host = config.hostname.clone().unwrap_or("\"\"".into());
|
|
||||||
let new_host = c.hostname.clone().unwrap_or("\"\"".into());
|
|
||||||
if old_host != new_host {
|
|
||||||
eprintln!(
|
|
||||||
"client {h}: hostname updated ({} -> {})",
|
|
||||||
old_host, new_host
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if config.port != c.port {
|
break;
|
||||||
eprintln!("client {h} changed port: {} -> {}", config.port, c.port);
|
|
||||||
}
|
|
||||||
if config.fix_ips != c.fix_ips {
|
|
||||||
eprintln!("client {h} ips updated: {:?}", c.fix_ips)
|
|
||||||
}
|
|
||||||
*config = c;
|
|
||||||
if state.active ^ s.active {
|
|
||||||
eprintln!(
|
|
||||||
"client {h} {}",
|
|
||||||
if s.active { "activated" } else { "deactivated" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*state = s;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FrontendEvent::Deleted(h) => {
|
}
|
||||||
if let Some((h, c, _)) = self.remove(h) {
|
CliSubcommand::SetHost { id, host } => {
|
||||||
eprint!("client {h} removed (");
|
tx.request(FrontendRequest::UpdateHostname(id, host))
|
||||||
print_config(&c);
|
.await?
|
||||||
eprintln!(")");
|
}
|
||||||
}
|
CliSubcommand::SetPort { id, port } => {
|
||||||
}
|
tx.request(FrontendRequest::UpdatePort(id, port)).await?
|
||||||
FrontendEvent::PortChanged(p, e) => {
|
}
|
||||||
if let Some(e) = e {
|
CliSubcommand::SetPosition { id, pos } => {
|
||||||
eprintln!("failed to change port: {e}");
|
tx.request(FrontendRequest::UpdatePosition(id, pos)).await?
|
||||||
} else {
|
}
|
||||||
eprintln!("changed port to {p}");
|
CliSubcommand::SetIps { id, ips } => {
|
||||||
}
|
tx.request(FrontendRequest::UpdateFixIps(id, ips)).await?
|
||||||
}
|
}
|
||||||
FrontendEvent::Enumerate(clients) => {
|
CliSubcommand::EnableCapture => tx.request(FrontendRequest::EnableCapture).await?,
|
||||||
self.clients = clients;
|
CliSubcommand::EnableEmulation => tx.request(FrontendRequest::EnableEmulation).await?,
|
||||||
self.print_clients();
|
CliSubcommand::AuthorizeKey {
|
||||||
}
|
description,
|
||||||
FrontendEvent::Error(e) => {
|
sha256_fingerprint,
|
||||||
eprintln!("ERROR: {e}");
|
} => {
|
||||||
}
|
tx.request(FrontendRequest::AuthorizeKey(
|
||||||
FrontendEvent::CaptureStatus(s) => {
|
description,
|
||||||
eprintln!("capture status: {s:?}")
|
sha256_fingerprint,
|
||||||
}
|
))
|
||||||
FrontendEvent::EmulationStatus(s) => {
|
.await?
|
||||||
eprintln!("emulation status: {s:?}")
|
}
|
||||||
}
|
CliSubcommand::RemoveAuthorizedKey { sha256_fingerprint } => {
|
||||||
FrontendEvent::AuthorizedUpdated(fingerprints) => {
|
tx.request(FrontendRequest::RemoveAuthorizedKey(sha256_fingerprint))
|
||||||
eprintln!("authorized keys changed:");
|
.await?
|
||||||
for (desc, fp) in fingerprints {
|
|
||||||
eprintln!("{desc}: {fp}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FrontendEvent::PublicKeyFingerprint(fp) => {
|
|
||||||
eprintln!("the public key fingerprint of this device is {fp}");
|
|
||||||
}
|
|
||||||
FrontendEvent::IncomingConnected(..) => {}
|
|
||||||
FrontendEvent::IncomingDisconnected(..) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_clients(&mut self) {
|
|
||||||
for (h, c, s) in self.clients.iter() {
|
|
||||||
eprint!("client {h}: ");
|
|
||||||
print_config(c);
|
|
||||||
eprint!(" ");
|
|
||||||
print_state(s);
|
|
||||||
eprintln!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prompt() -> io::Result<()> {
|
|
||||||
eprint!("lan-mouse > ");
|
|
||||||
std::io::stderr().flush()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_config(c: &ClientConfig) {
|
|
||||||
eprint!(
|
|
||||||
"{}:{} ({}), ips: {:?}",
|
|
||||||
c.hostname.clone().unwrap_or("(no hostname)".into()),
|
|
||||||
c.port,
|
|
||||||
c.pos,
|
|
||||||
c.fix_ips
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_state(s: &ClientState) {
|
|
||||||
eprint!("active: {}, dns: {:?}", s.active, s.ips);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
<!-- enabled -->
|
<!-- enabled -->
|
||||||
<child type="prefix">
|
<child type="prefix">
|
||||||
<object class="GtkSwitch" id="enable_switch">
|
<object class="GtkSwitch" id="enable_switch">
|
||||||
<signal name="state_set" handler="handle_client_set_state" swapped="true"/>
|
|
||||||
<property name="valign">center</property>
|
<property name="valign">center</property>
|
||||||
<property name="halign">end</property>
|
<property name="halign">end</property>
|
||||||
<property name="tooltip-text" translatable="yes">enable</property>
|
<property name="tooltip-text" translatable="yes">enable</property>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use super::ClientData;
|
|||||||
#[properties(wrapper_type = super::ClientObject)]
|
#[properties(wrapper_type = super::ClientObject)]
|
||||||
pub struct ClientObject {
|
pub struct ClientObject {
|
||||||
#[property(name = "handle", get, set, type = ClientHandle, member = handle)]
|
#[property(name = "handle", get, set, type = ClientHandle, member = handle)]
|
||||||
#[property(name = "hostname", get, set, type = String, member = hostname)]
|
#[property(name = "hostname", get, set, type = Option<String>, member = hostname)]
|
||||||
#[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
|
#[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
|
||||||
#[property(name = "active", get, set, type = bool, member = active)]
|
#[property(name = "active", get, set, type = bool, member = active)]
|
||||||
#[property(name = "position", get, set, type = String, member = position)]
|
#[property(name = "position", get, set, type = String, member = position)]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use adw::prelude::*;
|
|||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use gtk::glib::{self, Object};
|
use gtk::glib::{self, Object};
|
||||||
|
|
||||||
use lan_mouse_ipc::DEFAULT_PORT;
|
use lan_mouse_ipc::{Position, DEFAULT_PORT};
|
||||||
|
|
||||||
use super::ClientObject;
|
use super::ClientObject;
|
||||||
|
|
||||||
@@ -15,25 +15,32 @@ glib::wrapper! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ClientRow {
|
impl ClientRow {
|
||||||
pub fn new(_client_object: &ClientObject) -> Self {
|
pub fn new(client_object: &ClientObject) -> Self {
|
||||||
Object::builder().build()
|
let client_row: Self = Object::builder().build();
|
||||||
|
client_row
|
||||||
|
.imp()
|
||||||
|
.client_object
|
||||||
|
.borrow_mut()
|
||||||
|
.replace(client_object.clone());
|
||||||
|
client_row
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind(&self, client_object: &ClientObject) {
|
pub fn bind(&self, client_object: &ClientObject) {
|
||||||
let mut bindings = self.imp().bindings.borrow_mut();
|
let mut bindings = self.imp().bindings.borrow_mut();
|
||||||
|
|
||||||
|
// bind client active to switch state
|
||||||
let active_binding = client_object
|
let active_binding = client_object
|
||||||
.bind_property("active", &self.imp().enable_switch.get(), "state")
|
.bind_property("active", &self.imp().enable_switch.get(), "state")
|
||||||
.bidirectional()
|
|
||||||
.sync_create()
|
.sync_create()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// bind client active to switch position
|
||||||
let switch_position_binding = client_object
|
let switch_position_binding = client_object
|
||||||
.bind_property("active", &self.imp().enable_switch.get(), "active")
|
.bind_property("active", &self.imp().enable_switch.get(), "active")
|
||||||
.bidirectional()
|
|
||||||
.sync_create()
|
.sync_create()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// bind hostname to hostname edit field
|
||||||
let hostname_binding = client_object
|
let hostname_binding = client_object
|
||||||
.bind_property("hostname", &self.imp().hostname.get(), "text")
|
.bind_property("hostname", &self.imp().hostname.get(), "text")
|
||||||
.transform_to(|_, v: Option<String>| {
|
.transform_to(|_, v: Option<String>| {
|
||||||
@@ -43,72 +50,48 @@ impl ClientRow {
|
|||||||
Some("".to_string())
|
Some("".to_string())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.transform_from(|_, v: String| {
|
|
||||||
if v.as_str().trim() == "" {
|
|
||||||
Some(None)
|
|
||||||
} else {
|
|
||||||
Some(Some(v))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.bidirectional()
|
|
||||||
.sync_create()
|
.sync_create()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// bind hostname to title
|
||||||
let title_binding = client_object
|
let title_binding = client_object
|
||||||
.bind_property("hostname", self, "title")
|
.bind_property("hostname", self, "title")
|
||||||
.transform_to(|_, v: Option<String>| {
|
.transform_to(|_, v: Option<String>| v.or(Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())))
|
||||||
if let Some(hostname) = v {
|
|
||||||
Some(hostname)
|
|
||||||
} else {
|
|
||||||
Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sync_create()
|
.sync_create()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// bind port to port edit field
|
||||||
let port_binding = client_object
|
let port_binding = client_object
|
||||||
.bind_property("port", &self.imp().port.get(), "text")
|
.bind_property("port", &self.imp().port.get(), "text")
|
||||||
.transform_from(|_, v: String| {
|
|
||||||
if v.is_empty() {
|
|
||||||
Some(DEFAULT_PORT as u32)
|
|
||||||
} else {
|
|
||||||
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.transform_to(|_, v: u32| {
|
.transform_to(|_, v: u32| {
|
||||||
if v == 4242 {
|
if v == DEFAULT_PORT as u32 {
|
||||||
Some("".to_string())
|
Some("".to_string())
|
||||||
} else {
|
} else {
|
||||||
Some(v.to_string())
|
Some(v.to_string())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.bidirectional()
|
|
||||||
.sync_create()
|
.sync_create()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// bind port to subtitle
|
||||||
let subtitle_binding = client_object
|
let subtitle_binding = client_object
|
||||||
.bind_property("port", self, "subtitle")
|
.bind_property("port", self, "subtitle")
|
||||||
.sync_create()
|
.sync_create()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// bind position to selected position
|
||||||
let position_binding = client_object
|
let position_binding = client_object
|
||||||
.bind_property("position", &self.imp().position.get(), "selected")
|
.bind_property("position", &self.imp().position.get(), "selected")
|
||||||
.transform_from(|_, v: u32| match v {
|
|
||||||
1 => Some("right"),
|
|
||||||
2 => Some("top"),
|
|
||||||
3 => Some("bottom"),
|
|
||||||
_ => Some("left"),
|
|
||||||
})
|
|
||||||
.transform_to(|_, v: String| match v.as_str() {
|
.transform_to(|_, v: String| match v.as_str() {
|
||||||
"right" => Some(1),
|
"right" => Some(1u32),
|
||||||
"top" => Some(2u32),
|
"top" => Some(2u32),
|
||||||
"bottom" => Some(3u32),
|
"bottom" => Some(3u32),
|
||||||
_ => Some(0u32),
|
_ => Some(0u32),
|
||||||
})
|
})
|
||||||
.bidirectional()
|
|
||||||
.sync_create()
|
.sync_create()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// bind resolving status to spinner visibility
|
||||||
let resolve_binding = client_object
|
let resolve_binding = client_object
|
||||||
.bind_property(
|
.bind_property(
|
||||||
"resolving",
|
"resolving",
|
||||||
@@ -118,6 +101,7 @@ impl ClientRow {
|
|||||||
.sync_create()
|
.sync_create()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// bind ips to tooltip-text
|
||||||
let ip_binding = client_object
|
let ip_binding = client_object
|
||||||
.bind_property("ips", &self.imp().dns_button.get(), "tooltip-text")
|
.bind_property("ips", &self.imp().dns_button.get(), "tooltip-text")
|
||||||
.transform_to(|_, ips: Vec<String>| {
|
.transform_to(|_, ips: Vec<String>| {
|
||||||
@@ -146,4 +130,24 @@ impl ClientRow {
|
|||||||
binding.unbind();
|
binding.unbind();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_active(&self, active: bool) {
|
||||||
|
self.imp().set_active(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_hostname(&self, hostname: Option<String>) {
|
||||||
|
self.imp().set_hostname(hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_port(&self, port: u16) {
|
||||||
|
self.imp().set_port(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_position(&self, pos: Position) {
|
||||||
|
self.imp().set_pos(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_dns_state(&self, resolved: bool) {
|
||||||
|
self.imp().set_dns_state(resolved);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ use std::cell::RefCell;
|
|||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use adw::{prelude::*, ActionRow, ComboRow};
|
use adw::{prelude::*, ActionRow, ComboRow};
|
||||||
use glib::{subclass::InitializingObject, Binding};
|
use glib::{subclass::InitializingObject, Binding};
|
||||||
use gtk::glib::clone;
|
|
||||||
use gtk::glib::subclass::Signal;
|
use gtk::glib::subclass::Signal;
|
||||||
use gtk::{glib, Button, CompositeTemplate, Switch};
|
use gtk::glib::{clone, SignalHandlerId};
|
||||||
|
use gtk::{glib, Button, CompositeTemplate, Entry, Switch};
|
||||||
|
use lan_mouse_ipc::Position;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use crate::client_object::ClientObject;
|
||||||
|
|
||||||
#[derive(CompositeTemplate, Default)]
|
#[derive(CompositeTemplate, Default)]
|
||||||
#[template(resource = "/de/feschber/LanMouse/client_row.ui")]
|
#[template(resource = "/de/feschber/LanMouse/client_row.ui")]
|
||||||
pub struct ClientRow {
|
pub struct ClientRow {
|
||||||
@@ -28,6 +31,11 @@ pub struct ClientRow {
|
|||||||
#[template_child]
|
#[template_child]
|
||||||
pub dns_loading_indicator: TemplateChild<gtk::Spinner>,
|
pub dns_loading_indicator: TemplateChild<gtk::Spinner>,
|
||||||
pub bindings: RefCell<Vec<Binding>>,
|
pub bindings: RefCell<Vec<Binding>>,
|
||||||
|
hostname_change_handler: RefCell<Option<SignalHandlerId>>,
|
||||||
|
port_change_handler: RefCell<Option<SignalHandlerId>>,
|
||||||
|
position_change_handler: RefCell<Option<SignalHandlerId>>,
|
||||||
|
set_state_handler: RefCell<Option<SignalHandlerId>>,
|
||||||
|
pub client_object: RefCell<Option<ClientObject>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
@@ -59,17 +67,61 @@ impl ObjectImpl for ClientRow {
|
|||||||
row.handle_client_delete(button);
|
row.handle_client_delete(button);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
let handler = self.hostname.connect_changed(clone!(
|
||||||
|
#[weak(rename_to = row)]
|
||||||
|
self,
|
||||||
|
move |entry| {
|
||||||
|
row.handle_hostname_changed(entry);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
self.hostname_change_handler.replace(Some(handler));
|
||||||
|
let handler = self.port.connect_changed(clone!(
|
||||||
|
#[weak(rename_to = row)]
|
||||||
|
self,
|
||||||
|
move |entry| {
|
||||||
|
row.handle_port_changed(entry);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
self.port_change_handler.replace(Some(handler));
|
||||||
|
let handler = self.position.connect_selected_notify(clone!(
|
||||||
|
#[weak(rename_to = row)]
|
||||||
|
self,
|
||||||
|
move |position| {
|
||||||
|
row.handle_position_changed(position);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
self.position_change_handler.replace(Some(handler));
|
||||||
|
let handler = self.enable_switch.connect_state_set(clone!(
|
||||||
|
#[weak(rename_to = row)]
|
||||||
|
self,
|
||||||
|
#[upgrade_or]
|
||||||
|
glib::Propagation::Proceed,
|
||||||
|
move |switch, state| {
|
||||||
|
row.handle_activate_switch(state, switch);
|
||||||
|
glib::Propagation::Proceed
|
||||||
|
}
|
||||||
|
));
|
||||||
|
self.set_state_handler.replace(Some(handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signals() -> &'static [glib::subclass::Signal] {
|
fn signals() -> &'static [glib::subclass::Signal] {
|
||||||
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
|
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
|
||||||
SIGNALS.get_or_init(|| {
|
SIGNALS.get_or_init(|| {
|
||||||
vec![
|
vec![
|
||||||
Signal::builder("request-dns").build(),
|
Signal::builder("request-activate")
|
||||||
Signal::builder("request-update")
|
|
||||||
.param_types([bool::static_type()])
|
.param_types([bool::static_type()])
|
||||||
.build(),
|
.build(),
|
||||||
Signal::builder("request-delete").build(),
|
Signal::builder("request-delete").build(),
|
||||||
|
Signal::builder("request-dns").build(),
|
||||||
|
Signal::builder("request-hostname-change")
|
||||||
|
.param_types([String::static_type()])
|
||||||
|
.build(),
|
||||||
|
Signal::builder("request-port-change")
|
||||||
|
.param_types([u32::static_type()])
|
||||||
|
.build(),
|
||||||
|
Signal::builder("request-position-change")
|
||||||
|
.param_types([u32::static_type()])
|
||||||
|
.build(),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -78,22 +130,97 @@ impl ObjectImpl for ClientRow {
|
|||||||
#[gtk::template_callbacks]
|
#[gtk::template_callbacks]
|
||||||
impl ClientRow {
|
impl ClientRow {
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn handle_client_set_state(&self, state: bool, _switch: &Switch) -> bool {
|
fn handle_activate_switch(&self, state: bool, _switch: &Switch) -> bool {
|
||||||
log::debug!("state change -> requesting update");
|
self.obj().emit_by_name::<()>("request-activate", &[&state]);
|
||||||
self.obj().emit_by_name::<()>("request-update", &[&state]);
|
|
||||||
true // dont run default handler
|
true // dont run default handler
|
||||||
}
|
}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn handle_request_dns(&self, _: Button) {
|
fn handle_request_dns(&self, _: &Button) {
|
||||||
self.obj().emit_by_name::<()>("request-dns", &[]);
|
self.obj().emit_by_name::<()>("request-dns", &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[template_callback]
|
#[template_callback]
|
||||||
fn handle_client_delete(&self, _button: &Button) {
|
fn handle_client_delete(&self, _button: &Button) {
|
||||||
log::debug!("delete button pressed -> requesting delete");
|
|
||||||
self.obj().emit_by_name::<()>("request-delete", &[]);
|
self.obj().emit_by_name::<()>("request-delete", &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_port_changed(&self, port_entry: &Entry) {
|
||||||
|
if let Ok(port) = port_entry.text().parse::<u16>() {
|
||||||
|
self.obj()
|
||||||
|
.emit_by_name::<()>("request-port-change", &[&(port as u32)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_hostname_changed(&self, hostname_entry: &Entry) {
|
||||||
|
self.obj()
|
||||||
|
.emit_by_name::<()>("request-hostname-change", &[&hostname_entry.text()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_position_changed(&self, position: &ComboRow) {
|
||||||
|
self.obj()
|
||||||
|
.emit_by_name("request-position-change", &[&position.selected()])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_hostname(&self, hostname: Option<String>) {
|
||||||
|
let position = self.hostname.position();
|
||||||
|
let handler = self.hostname_change_handler.borrow();
|
||||||
|
let handler = handler.as_ref().expect("signal handler");
|
||||||
|
self.hostname.block_signal(handler);
|
||||||
|
self.client_object
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.expect("client object")
|
||||||
|
.set_property("hostname", hostname);
|
||||||
|
self.hostname.unblock_signal(handler);
|
||||||
|
self.hostname.set_position(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_port(&self, port: u16) {
|
||||||
|
let position = self.port.position();
|
||||||
|
let handler = self.port_change_handler.borrow();
|
||||||
|
let handler = handler.as_ref().expect("signal handler");
|
||||||
|
self.port.block_signal(handler);
|
||||||
|
self.client_object
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.expect("client object")
|
||||||
|
.set_port(port as u32);
|
||||||
|
self.port.unblock_signal(handler);
|
||||||
|
self.port.set_position(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_pos(&self, pos: Position) {
|
||||||
|
let handler = self.position_change_handler.borrow();
|
||||||
|
let handler = handler.as_ref().expect("signal handler");
|
||||||
|
self.position.block_signal(handler);
|
||||||
|
self.client_object
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.expect("client object")
|
||||||
|
.set_position(pos.to_string());
|
||||||
|
self.position.unblock_signal(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_active(&self, active: bool) {
|
||||||
|
let handler = self.set_state_handler.borrow();
|
||||||
|
let handler = handler.as_ref().expect("signal handler");
|
||||||
|
self.enable_switch.block_signal(handler);
|
||||||
|
self.client_object
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.expect("client object")
|
||||||
|
.set_active(active);
|
||||||
|
self.enable_switch.unblock_signal(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_dns_state(&self, resolved: bool) {
|
||||||
|
if resolved {
|
||||||
|
self.dns_button.set_css_classes(&["success"])
|
||||||
|
} else {
|
||||||
|
self.dns_button.set_css_classes(&["warning"])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetImpl for ClientRow {}
|
impl WidgetImpl for ClientRow {}
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ use std::{env, process, str};
|
|||||||
|
|
||||||
use window::Window;
|
use window::Window;
|
||||||
|
|
||||||
use lan_mouse_ipc::{FrontendEvent, FrontendRequest};
|
use lan_mouse_ipc::FrontendEvent;
|
||||||
|
|
||||||
use adw::Application;
|
use adw::Application;
|
||||||
use gtk::{
|
use gtk::{gdk::Display, glib::clone, prelude::*, IconTheme};
|
||||||
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
|
|
||||||
};
|
|
||||||
use gtk::{gio, glib, prelude::ApplicationExt};
|
use gtk::{gio, glib, prelude::ApplicationExt};
|
||||||
|
|
||||||
use self::client_object::ClientObject;
|
use self::client_object::ClientObject;
|
||||||
@@ -127,54 +125,28 @@ fn build_ui(app: &Application) {
|
|||||||
loop {
|
loop {
|
||||||
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
|
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
|
||||||
match notify {
|
match notify {
|
||||||
FrontendEvent::Changed(handle) => {
|
|
||||||
window.request(FrontendRequest::GetState(handle));
|
|
||||||
}
|
|
||||||
FrontendEvent::Created(handle, client, state) => {
|
FrontendEvent::Created(handle, client, state) => {
|
||||||
window.new_client(handle, client, state);
|
window.new_client(handle, client, state)
|
||||||
}
|
|
||||||
FrontendEvent::Deleted(client) => {
|
|
||||||
window.delete_client(client);
|
|
||||||
}
|
}
|
||||||
|
FrontendEvent::Deleted(client) => window.delete_client(client),
|
||||||
FrontendEvent::State(handle, config, state) => {
|
FrontendEvent::State(handle, config, state) => {
|
||||||
window.update_client_config(handle, config);
|
window.update_client_config(handle, config);
|
||||||
window.update_client_state(handle, state);
|
window.update_client_state(handle, state);
|
||||||
}
|
}
|
||||||
FrontendEvent::NoSuchClient(_) => {}
|
FrontendEvent::NoSuchClient(_) => {}
|
||||||
FrontendEvent::Error(e) => {
|
FrontendEvent::Error(e) => window.show_toast(e.as_str()),
|
||||||
window.show_toast(e.as_str());
|
FrontendEvent::Enumerate(clients) => window.update_client_list(clients),
|
||||||
|
FrontendEvent::PortChanged(port, msg) => window.update_port(port, msg),
|
||||||
|
FrontendEvent::CaptureStatus(s) => window.set_capture(s.into()),
|
||||||
|
FrontendEvent::EmulationStatus(s) => window.set_emulation(s.into()),
|
||||||
|
FrontendEvent::AuthorizedUpdated(keys) => window.set_authorized_keys(keys),
|
||||||
|
FrontendEvent::PublicKeyFingerprint(fp) => window.set_pk_fp(&fp),
|
||||||
|
FrontendEvent::IncomingConnected(_fingerprint, addr, pos) => {
|
||||||
|
window.show_toast(format!("device connected: {addr} ({pos})").as_str());
|
||||||
}
|
}
|
||||||
FrontendEvent::Enumerate(clients) => {
|
FrontendEvent::IncomingDisconnected(addr) => {
|
||||||
for (handle, client, state) in clients {
|
window.show_toast(format!("{addr} disconnected").as_str());
|
||||||
if window.client_idx(handle).is_some() {
|
|
||||||
window.update_client_config(handle, client);
|
|
||||||
window.update_client_state(handle, state);
|
|
||||||
} else {
|
|
||||||
window.new_client(handle, client, state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FrontendEvent::PortChanged(port, msg) => {
|
|
||||||
match msg {
|
|
||||||
None => window.show_toast(format!("port changed: {port}").as_str()),
|
|
||||||
Some(msg) => window.show_toast(msg.as_str()),
|
|
||||||
}
|
|
||||||
window.imp().set_port(port);
|
|
||||||
}
|
|
||||||
FrontendEvent::CaptureStatus(s) => {
|
|
||||||
window.set_capture(s.into());
|
|
||||||
}
|
|
||||||
FrontendEvent::EmulationStatus(s) => {
|
|
||||||
window.set_emulation(s.into());
|
|
||||||
}
|
|
||||||
FrontendEvent::AuthorizedUpdated(keys) => {
|
|
||||||
window.set_authorized_keys(keys);
|
|
||||||
}
|
|
||||||
FrontendEvent::PublicKeyFingerprint(fp) => {
|
|
||||||
window.set_pk_fp(&fp);
|
|
||||||
}
|
|
||||||
FrontendEvent::IncomingConnected(..) => {}
|
|
||||||
FrontendEvent::IncomingDisconnected(..) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use glib::{clone, Object};
|
|||||||
use gtk::{
|
use gtk::{
|
||||||
gio,
|
gio,
|
||||||
glib::{self, closure_local},
|
glib::{self, closure_local},
|
||||||
ListBox, NoSelection,
|
NoSelection,
|
||||||
};
|
};
|
||||||
|
|
||||||
use lan_mouse_ipc::{
|
use lan_mouse_ipc::{
|
||||||
@@ -28,7 +28,7 @@ glib::wrapper! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
pub(crate) fn new(app: &adw::Application, conn: FrontendRequestWriter) -> Self {
|
pub(super) fn new(app: &adw::Application, conn: FrontendRequestWriter) -> Self {
|
||||||
let window: Self = Object::builder().property("application", app).build();
|
let window: Self = Object::builder().property("application", app).build();
|
||||||
window
|
window
|
||||||
.imp()
|
.imp()
|
||||||
@@ -38,7 +38,7 @@ impl Window {
|
|||||||
window
|
window
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clients(&self) -> gio::ListStore {
|
fn clients(&self) -> gio::ListStore {
|
||||||
self.imp()
|
self.imp()
|
||||||
.clients
|
.clients
|
||||||
.borrow()
|
.borrow()
|
||||||
@@ -46,7 +46,7 @@ impl Window {
|
|||||||
.expect("Could not get clients")
|
.expect("Could not get clients")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn authorized(&self) -> gio::ListStore {
|
fn authorized(&self) -> gio::ListStore {
|
||||||
self.imp()
|
self.imp()
|
||||||
.authorized
|
.authorized
|
||||||
.borrow()
|
.borrow()
|
||||||
@@ -62,6 +62,14 @@ impl Window {
|
|||||||
self.authorized().item(idx).map(|o| o.downcast().unwrap())
|
self.authorized().item(idx).map(|o| o.downcast().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn row_by_idx(&self, idx: i32) -> Option<ClientRow> {
|
||||||
|
self.imp()
|
||||||
|
.client_list
|
||||||
|
.get()
|
||||||
|
.row_at_index(idx)
|
||||||
|
.map(|o| o.downcast().expect("expected ClientRow"))
|
||||||
|
}
|
||||||
|
|
||||||
fn setup_authorized(&self) {
|
fn setup_authorized(&self) {
|
||||||
let store = gio::ListStore::new::<KeyObject>();
|
let store = gio::ListStore::new::<KeyObject>();
|
||||||
self.imp().authorized.replace(Some(store));
|
self.imp().authorized.replace(Some(store));
|
||||||
@@ -112,16 +120,57 @@ impl Window {
|
|||||||
.expect("Expected object of type `ClientObject`.");
|
.expect("Expected object of type `ClientObject`.");
|
||||||
let row = window.create_client_row(client_object);
|
let row = window.create_client_row(client_object);
|
||||||
row.connect_closure(
|
row.connect_closure(
|
||||||
"request-update",
|
"request-hostname-change",
|
||||||
|
false,
|
||||||
|
closure_local!(
|
||||||
|
#[strong]
|
||||||
|
window,
|
||||||
|
move |row: ClientRow, hostname: String| {
|
||||||
|
log::info!("request-hostname-change");
|
||||||
|
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
||||||
|
let hostname = Some(hostname).filter(|s| !s.is_empty());
|
||||||
|
/* changed in response to FrontendEvent
|
||||||
|
* -> do not request additional update */
|
||||||
|
window.request(FrontendRequest::UpdateHostname(
|
||||||
|
client.handle(),
|
||||||
|
hostname,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
row.connect_closure(
|
||||||
|
"request-port-change",
|
||||||
|
false,
|
||||||
|
closure_local!(
|
||||||
|
#[strong]
|
||||||
|
window,
|
||||||
|
move |row: ClientRow, port: u32| {
|
||||||
|
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
||||||
|
window.request(FrontendRequest::UpdatePort(
|
||||||
|
client.handle(),
|
||||||
|
port as u16,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
row.connect_closure(
|
||||||
|
"request-activate",
|
||||||
false,
|
false,
|
||||||
closure_local!(
|
closure_local!(
|
||||||
#[strong]
|
#[strong]
|
||||||
window,
|
window,
|
||||||
move |row: ClientRow, active: bool| {
|
move |row: ClientRow, active: bool| {
|
||||||
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
||||||
window.request_client_activate(&client, active);
|
log::info!(
|
||||||
window.request_client_update(&client);
|
"request: {} client",
|
||||||
window.request_client_state(&client);
|
if active { "activating" } else { "deactivating" }
|
||||||
|
);
|
||||||
|
window.request(FrontendRequest::Activate(
|
||||||
|
client.handle(),
|
||||||
|
active,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -134,7 +183,7 @@ impl Window {
|
|||||||
window,
|
window,
|
||||||
move |row: ClientRow| {
|
move |row: ClientRow| {
|
||||||
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
||||||
window.request_client_delete(&client);
|
window.request(FrontendRequest::Delete(client.handle()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -147,9 +196,31 @@ impl Window {
|
|||||||
window,
|
window,
|
||||||
move |row: ClientRow| {
|
move |row: ClientRow| {
|
||||||
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
||||||
window.request_client_update(&client);
|
window.request(FrontendRequest::ResolveDns(
|
||||||
window.request_dns(&client);
|
client.get_data().handle,
|
||||||
window.request_client_state(&client);
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
row.connect_closure(
|
||||||
|
"request-position-change",
|
||||||
|
false,
|
||||||
|
closure_local!(
|
||||||
|
#[strong]
|
||||||
|
window,
|
||||||
|
move |row: ClientRow, pos_idx: u32| {
|
||||||
|
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
||||||
|
let position = match pos_idx {
|
||||||
|
0 => Position::Left,
|
||||||
|
1 => Position::Right,
|
||||||
|
2 => Position::Top,
|
||||||
|
_ => Position::Bottom,
|
||||||
|
};
|
||||||
|
window.request(FrontendRequest::UpdatePosition(
|
||||||
|
client.handle(),
|
||||||
|
position,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -160,10 +231,14 @@ impl Window {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setup_icon(&self) {
|
||||||
|
self.set_icon_name(Some("de.feschber.LanMouse"));
|
||||||
|
}
|
||||||
|
|
||||||
/// workaround for a bug in libadwaita that shows an ugly line beneath
|
/// workaround for a bug in libadwaita that shows an ugly line beneath
|
||||||
/// the last element if a placeholder is set.
|
/// the last element if a placeholder is set.
|
||||||
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308
|
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308
|
||||||
pub fn update_placeholder_visibility(&self) {
|
fn update_placeholder_visibility(&self) {
|
||||||
let visible = self.clients().n_items() == 0;
|
let visible = self.clients().n_items() == 0;
|
||||||
let placeholder = self.imp().client_placeholder.get();
|
let placeholder = self.imp().client_placeholder.get();
|
||||||
self.imp().client_list.set_placeholder(match visible {
|
self.imp().client_list.set_placeholder(match visible {
|
||||||
@@ -172,7 +247,7 @@ impl Window {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_auth_placeholder_visibility(&self) {
|
fn update_auth_placeholder_visibility(&self) {
|
||||||
let visible = self.authorized().n_items() == 0;
|
let visible = self.authorized().n_items() == 0;
|
||||||
let placeholder = self.imp().authorized_placeholder.get();
|
let placeholder = self.imp().authorized_placeholder.get();
|
||||||
self.imp().authorized_list.set_placeholder(match visible {
|
self.imp().authorized_list.set_placeholder(match visible {
|
||||||
@@ -181,10 +256,6 @@ impl Window {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_icon(&self) {
|
|
||||||
self.set_icon_name(Some("de.feschber.LanMouse"));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
|
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
|
||||||
let row = ClientRow::new(client_object);
|
let row = ClientRow::new(client_object);
|
||||||
row.bind(client_object);
|
row.bind(client_object);
|
||||||
@@ -197,24 +268,46 @@ impl Window {
|
|||||||
row
|
row
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_client(&self, handle: ClientHandle, client: ClientConfig, state: ClientState) {
|
pub(super) fn new_client(
|
||||||
|
&self,
|
||||||
|
handle: ClientHandle,
|
||||||
|
client: ClientConfig,
|
||||||
|
state: ClientState,
|
||||||
|
) {
|
||||||
let client = ClientObject::new(handle, client, state.clone());
|
let client = ClientObject::new(handle, client, state.clone());
|
||||||
self.clients().append(&client);
|
self.clients().append(&client);
|
||||||
self.update_placeholder_visibility();
|
self.update_placeholder_visibility();
|
||||||
self.update_dns_state(handle, !state.ips.is_empty());
|
self.update_dns_state(handle, !state.ips.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
|
pub(super) fn update_client_list(
|
||||||
self.clients().iter::<ClientObject>().position(|c| {
|
&self,
|
||||||
if let Ok(c) = c {
|
clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
|
||||||
c.handle() == handle
|
) {
|
||||||
|
for (handle, client, state) in clients {
|
||||||
|
if self.client_idx(handle).is_some() {
|
||||||
|
self.update_client_config(handle, client);
|
||||||
|
self.update_client_state(handle, state);
|
||||||
} else {
|
} else {
|
||||||
false
|
self.new_client(handle, client, state);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_client(&self, handle: ClientHandle) {
|
pub(super) fn update_port(&self, port: u16, msg: Option<String>) {
|
||||||
|
if let Some(msg) = msg {
|
||||||
|
self.show_toast(msg.as_str());
|
||||||
|
}
|
||||||
|
self.imp().set_port(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
|
||||||
|
self.clients()
|
||||||
|
.iter::<ClientObject>()
|
||||||
|
.position(|c| c.ok().map(|c| c.handle() == handle).unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn delete_client(&self, handle: ClientHandle) {
|
||||||
let Some(idx) = self.client_idx(handle) else {
|
let Some(idx) = self.client_idx(handle) else {
|
||||||
log::warn!("could not find client with handle {handle}");
|
log::warn!("could not find client with handle {handle}");
|
||||||
return;
|
return;
|
||||||
@@ -226,46 +319,31 @@ impl Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
|
pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
|
||||||
let Some(idx) = self.client_idx(handle) else {
|
let Some(row) = self.row_for_handle(handle) else {
|
||||||
log::warn!("could not find client with handle {}", handle);
|
log::warn!("could not find row for handle {}", handle);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let client_object = self.clients().item(idx as u32).unwrap();
|
row.set_hostname(client.hostname);
|
||||||
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
|
row.set_port(client.port);
|
||||||
let data = client_object.get_data();
|
row.set_position(client.pos);
|
||||||
|
|
||||||
/* only change if it actually has changed, otherwise
|
|
||||||
* the update signal is triggered */
|
|
||||||
if data.hostname != client.hostname {
|
|
||||||
client_object.set_hostname(client.hostname.unwrap_or("".into()));
|
|
||||||
}
|
|
||||||
if data.port != client.port as u32 {
|
|
||||||
client_object.set_port(client.port as u32);
|
|
||||||
}
|
|
||||||
if data.position != client.pos.to_string() {
|
|
||||||
client_object.set_position(client.pos.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
|
pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
|
||||||
let Some(idx) = self.client_idx(handle) else {
|
let Some(row) = self.row_for_handle(handle) else {
|
||||||
log::warn!("could not find client with handle {}", handle);
|
log::warn!("could not find row for handle {}", handle);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(client_object) = self.client_object_for_handle(handle) else {
|
||||||
|
log::warn!("could not find row for handle {}", handle);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let client_object = self.clients().item(idx as u32).unwrap();
|
|
||||||
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
|
|
||||||
let data = client_object.get_data();
|
|
||||||
|
|
||||||
if state.active != data.active {
|
/* activation state */
|
||||||
client_object.set_active(state.active);
|
row.set_active(state.active);
|
||||||
log::debug!("set active to {}", state.active);
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.resolving != data.resolving {
|
/* dns state */
|
||||||
client_object.set_resolving(state.resolving);
|
client_object.set_resolving(state.resolving);
|
||||||
log::debug!("resolving {}: {}", data.handle, state.resolving);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.update_dns_state(handle, !state.ips.is_empty());
|
self.update_dns_state(handle, !state.ips.is_empty());
|
||||||
let ips = state
|
let ips = state
|
||||||
@@ -276,22 +354,23 @@ impl Window {
|
|||||||
client_object.set_ips(ips);
|
client_object.set_ips(ips);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_dns_state(&self, handle: ClientHandle, resolved: bool) {
|
fn client_object_for_handle(&self, handle: ClientHandle) -> Option<ClientObject> {
|
||||||
let Some(idx) = self.client_idx(handle) else {
|
self.client_idx(handle)
|
||||||
log::warn!("could not find client with handle {}", handle);
|
.and_then(|i| self.client_by_idx(i as u32))
|
||||||
return;
|
}
|
||||||
};
|
|
||||||
let list_box: ListBox = self.imp().client_list.get();
|
fn row_for_handle(&self, handle: ClientHandle) -> Option<ClientRow> {
|
||||||
let row = list_box.row_at_index(idx as i32).unwrap();
|
self.client_idx(handle)
|
||||||
let client_row: ClientRow = row.downcast().expect("expected ClientRow Object");
|
.and_then(|i| self.row_by_idx(i as i32))
|
||||||
if resolved {
|
}
|
||||||
client_row.imp().dns_button.set_css_classes(&["success"])
|
|
||||||
} else {
|
fn update_dns_state(&self, handle: ClientHandle, resolved: bool) {
|
||||||
client_row.imp().dns_button.set_css_classes(&["warning"])
|
if let Some(client_row) = self.row_for_handle(handle) {
|
||||||
|
client_row.set_dns_state(resolved);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_port_change(&self) {
|
fn request_port_change(&self) {
|
||||||
let port = self
|
let port = self
|
||||||
.imp()
|
.imp()
|
||||||
.port_entry
|
.port_entry
|
||||||
@@ -303,55 +382,19 @@ impl Window {
|
|||||||
self.request(FrontendRequest::ChangePort(port));
|
self.request(FrontendRequest::ChangePort(port));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_capture(&self) {
|
fn request_capture(&self) {
|
||||||
self.request(FrontendRequest::EnableCapture);
|
self.request(FrontendRequest::EnableCapture);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_emulation(&self) {
|
fn request_emulation(&self) {
|
||||||
self.request(FrontendRequest::EnableEmulation);
|
self.request(FrontendRequest::EnableEmulation);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_client_state(&self, client: &ClientObject) {
|
fn request_client_create(&self) {
|
||||||
self.request_client_state_for(client.handle());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_client_state_for(&self, handle: ClientHandle) {
|
|
||||||
self.request(FrontendRequest::GetState(handle));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_client_create(&self) {
|
|
||||||
self.request(FrontendRequest::Create);
|
self.request(FrontendRequest::Create);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_dns(&self, client: &ClientObject) {
|
fn open_fingerprint_dialog(&self) {
|
||||||
self.request(FrontendRequest::ResolveDns(client.get_data().handle));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_client_update(&self, client: &ClientObject) {
|
|
||||||
let handle = client.handle();
|
|
||||||
let data = client.get_data();
|
|
||||||
let position = Position::try_from(data.position.as_str()).expect("invalid position");
|
|
||||||
let hostname = data.hostname;
|
|
||||||
let port = data.port as u16;
|
|
||||||
|
|
||||||
for event in [
|
|
||||||
FrontendRequest::UpdateHostname(handle, hostname),
|
|
||||||
FrontendRequest::UpdatePosition(handle, position),
|
|
||||||
FrontendRequest::UpdatePort(handle, port),
|
|
||||||
] {
|
|
||||||
self.request(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_client_activate(&self, client: &ClientObject, active: bool) {
|
|
||||||
self.request(FrontendRequest::Activate(client.handle(), active));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_client_delete(&self, client: &ClientObject) {
|
|
||||||
self.request(FrontendRequest::Delete(client.handle()));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open_fingerprint_dialog(&self) {
|
|
||||||
let window = FingerprintWindow::new();
|
let window = FingerprintWindow::new();
|
||||||
window.set_transient_for(Some(self));
|
window.set_transient_for(Some(self));
|
||||||
window.connect_closure(
|
window.connect_closure(
|
||||||
@@ -369,15 +412,15 @@ impl Window {
|
|||||||
window.present();
|
window.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_fingerprint_add(&self, desc: String, fp: String) {
|
fn request_fingerprint_add(&self, desc: String, fp: String) {
|
||||||
self.request(FrontendRequest::AuthorizeKey(desc, fp));
|
self.request(FrontendRequest::AuthorizeKey(desc, fp));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_fingerprint_remove(&self, fp: String) {
|
fn request_fingerprint_remove(&self, fp: String) {
|
||||||
self.request(FrontendRequest::RemoveAuthorizedKey(fp));
|
self.request(FrontendRequest::RemoveAuthorizedKey(fp));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request(&self, request: FrontendRequest) {
|
fn request(&self, request: FrontendRequest) {
|
||||||
let mut requester = self.imp().frontend_request_writer.borrow_mut();
|
let mut requester = self.imp().frontend_request_writer.borrow_mut();
|
||||||
let requester = requester.as_mut().unwrap();
|
let requester = requester.as_mut().unwrap();
|
||||||
if let Err(e) = requester.request(request) {
|
if let Err(e) = requester.request(request) {
|
||||||
@@ -385,18 +428,18 @@ impl Window {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_toast(&self, msg: &str) {
|
pub(super) fn show_toast(&self, msg: &str) {
|
||||||
let toast = adw::Toast::new(msg);
|
let toast = adw::Toast::new(msg);
|
||||||
let toast_overlay = &self.imp().toast_overlay;
|
let toast_overlay = &self.imp().toast_overlay;
|
||||||
toast_overlay.add_toast(toast);
|
toast_overlay.add_toast(toast);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_capture(&self, active: bool) {
|
pub(super) fn set_capture(&self, active: bool) {
|
||||||
self.imp().capture_active.replace(active);
|
self.imp().capture_active.replace(active);
|
||||||
self.update_capture_emulation_status();
|
self.update_capture_emulation_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_emulation(&self, active: bool) {
|
pub(super) fn set_emulation(&self, active: bool) {
|
||||||
self.imp().emulation_active.replace(active);
|
self.imp().emulation_active.replace(active);
|
||||||
self.update_capture_emulation_status();
|
self.update_capture_emulation_status();
|
||||||
}
|
}
|
||||||
@@ -411,7 +454,7 @@ impl Window {
|
|||||||
.set_visible(!capture || !emulation);
|
.set_visible(!capture || !emulation);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_authorized_keys(&self, fingerprints: HashMap<String, String>) {
|
pub(super) fn set_authorized_keys(&self, fingerprints: HashMap<String, String>) {
|
||||||
let authorized = self.authorized();
|
let authorized = self.authorized();
|
||||||
// clear list
|
// clear list
|
||||||
authorized.remove_all();
|
authorized.remove_all();
|
||||||
@@ -423,7 +466,7 @@ impl Window {
|
|||||||
self.update_auth_placeholder_visibility();
|
self.update_auth_placeholder_visibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_pk_fp(&self, fingerprint: &str) {
|
pub(super) fn set_pk_fp(&self, fingerprint: &str) {
|
||||||
self.imp().fingerprint_row.set_subtitle(fingerprint);
|
self.imp().fingerprint_row.set_subtitle(fingerprint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
|
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::min,
|
cmp::min,
|
||||||
io,
|
|
||||||
task::{ready, Poll},
|
task::{ready, Poll},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -47,7 +46,7 @@ impl Stream for AsyncFrontendEventReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncFrontendRequestWriter {
|
impl AsyncFrontendRequestWriter {
|
||||||
pub async fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
|
pub async fn request(&mut self, request: FrontendRequest) -> Result<(), IpcError> {
|
||||||
let mut json = serde_json::to_string(&request).unwrap();
|
let mut json = serde_json::to_string(&request).unwrap();
|
||||||
log::debug!("requesting: {json}");
|
log::debug!("requesting: {json}");
|
||||||
json.push('\n');
|
json.push('\n');
|
||||||
@@ -57,8 +56,16 @@ impl AsyncFrontendRequestWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect_async(
|
pub async fn connect_async(
|
||||||
|
timeout: Option<Duration>,
|
||||||
) -> Result<(AsyncFrontendEventReader, AsyncFrontendRequestWriter), ConnectionError> {
|
) -> Result<(AsyncFrontendEventReader, AsyncFrontendRequestWriter), ConnectionError> {
|
||||||
let stream = wait_for_service().await?;
|
let stream = if let Some(duration) = timeout {
|
||||||
|
tokio::select! {
|
||||||
|
s = wait_for_service() => s?,
|
||||||
|
_ = tokio::time::sleep(duration) => return Err(ConnectionError::Timeout),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wait_for_service().await?
|
||||||
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
|
let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ pub enum ConnectionError {
|
|||||||
SocketPath(#[from] SocketPathError),
|
SocketPath(#[from] SocketPathError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Io(#[from] io::Error),
|
Io(#[from] io::Error),
|
||||||
|
#[error("connection timed out")]
|
||||||
|
Timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -177,8 +179,6 @@ pub struct ClientState {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum FrontendEvent {
|
pub enum FrontendEvent {
|
||||||
/// client state has changed, new state must be requested via [`FrontendRequest::GetState`]
|
|
||||||
Changed(ClientHandle),
|
|
||||||
/// a client was created
|
/// a client was created
|
||||||
Created(ClientHandle, ClientConfig, ClientState),
|
Created(ClientHandle, ClientConfig, ClientState),
|
||||||
/// no such client
|
/// no such client
|
||||||
@@ -229,8 +229,6 @@ pub enum FrontendRequest {
|
|||||||
UpdatePosition(ClientHandle, Position),
|
UpdatePosition(ClientHandle, Position),
|
||||||
/// update fix-ips
|
/// update fix-ips
|
||||||
UpdateFixIps(ClientHandle, Vec<IpAddr>),
|
UpdateFixIps(ClientHandle, Vec<IpAddr>),
|
||||||
/// request the state of the given client
|
|
||||||
GetState(ClientHandle),
|
|
||||||
/// request reenabling input capture
|
/// request reenabling input capture
|
||||||
EnableCapture,
|
EnableCapture,
|
||||||
/// request reenabling input emulation
|
/// request reenabling input emulation
|
||||||
@@ -241,6 +239,8 @@ pub enum FrontendRequest {
|
|||||||
AuthorizeKey(String, String),
|
AuthorizeKey(String, String),
|
||||||
/// remove fingerprint (fingerprint)
|
/// remove fingerprint (fingerprint)
|
||||||
RemoveAuthorizedKey(String),
|
RemoveAuthorizedKey(String),
|
||||||
|
/// change the hook command
|
||||||
|
UpdateEnterHook(u64, Option<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ in {
|
|||||||
};
|
};
|
||||||
Service = {
|
Service = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
ExecStart = "${cfg.package}/bin/lan-mouse --daemon";
|
ExecStart = "${cfg.package}/bin/lan-mouse daemon";
|
||||||
};
|
};
|
||||||
Install.WantedBy = [
|
Install.WantedBy = [
|
||||||
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
|
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
|
||||||
@@ -65,7 +65,7 @@ in {
|
|||||||
config = {
|
config = {
|
||||||
ProgramArguments = [
|
ProgramArguments = [
|
||||||
"${cfg.package}/bin/lan-mouse"
|
"${cfg.package}/bin/lan-mouse"
|
||||||
"--daemon"
|
"daemon"
|
||||||
];
|
];
|
||||||
KeepAlive = true;
|
KeepAlive = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ After=graphical-session.target
|
|||||||
BindsTo=graphical-session.target
|
BindsTo=graphical-session.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/lan-mouse --daemon
|
ExecStart=/usr/bin/lan-mouse daemon
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use clap::Args;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position};
|
use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position};
|
||||||
use input_event::{Event, KeyboardEvent};
|
use input_event::{Event, KeyboardEvent};
|
||||||
|
|
||||||
pub async fn run(config: Config) -> Result<(), InputCaptureError> {
|
#[derive(Args, Debug, Eq, PartialEq)]
|
||||||
|
pub struct TestCaptureArgs {}
|
||||||
|
|
||||||
|
pub async fn run(config: Config, _args: TestCaptureArgs) -> Result<(), InputCaptureError> {
|
||||||
log::info!("running input capture test");
|
log::info!("running input capture test");
|
||||||
log::info!("creating input capture");
|
log::info!("creating input capture");
|
||||||
let backend = config.capture_backend.map(|b| b.into());
|
let backend = config.capture_backend.map(|b| b.into());
|
||||||
|
|||||||
@@ -199,6 +199,13 @@ impl ClientManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// update the enter hook command of the client
|
||||||
|
pub(crate) fn set_enter_hook(&self, handle: ClientHandle, enter_hook: Option<String>) {
|
||||||
|
if let Some((c, _s)) = self.clients.borrow_mut().get_mut(handle as usize) {
|
||||||
|
c.cmd = enter_hook;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// set resolving status of the client
|
/// set resolving status of the client
|
||||||
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
|
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
|
||||||
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
|
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use clap::{Parser, ValueEnum};
|
use crate::capture_test::TestCaptureArgs;
|
||||||
|
use crate::emulation_test::TestEmulationArgs;
|
||||||
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env::{self, VarError};
|
use std::env::{self, VarError};
|
||||||
@@ -10,6 +12,7 @@ use std::{collections::HashSet, io};
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use toml;
|
use toml;
|
||||||
|
|
||||||
|
use lan_mouse_cli::CliArgs;
|
||||||
use lan_mouse_ipc::{Position, DEFAULT_PORT};
|
use lan_mouse_ipc::{Position, DEFAULT_PORT};
|
||||||
|
|
||||||
use input_event::scancode::{
|
use input_event::scancode::{
|
||||||
@@ -38,7 +41,6 @@ pub struct ConfigToml {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||||
pub struct TomlClient {
|
pub struct TomlClient {
|
||||||
pub capture_backend: Option<CaptureBackend>,
|
|
||||||
pub hostname: Option<String>,
|
pub hostname: Option<String>,
|
||||||
pub host_name: Option<String>,
|
pub host_name: Option<String>,
|
||||||
pub ips: Option<Vec<IpAddr>>,
|
pub ips: Option<Vec<IpAddr>>,
|
||||||
@@ -56,7 +58,7 @@ impl ConfigToml {
|
|||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version=build::CLAP_LONG_VERSION, about, long_about = None)]
|
#[command(author, version=build::CLAP_LONG_VERSION, about, long_about = None)]
|
||||||
struct CliArgs {
|
pub struct Args {
|
||||||
/// the listen port for lan-mouse
|
/// the listen port for lan-mouse
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
port: Option<u16>,
|
port: Option<u16>,
|
||||||
@@ -67,31 +69,34 @@ struct CliArgs {
|
|||||||
|
|
||||||
/// non-default config file location
|
/// non-default config file location
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
config: Option<String>,
|
pub config: Option<PathBuf>,
|
||||||
|
|
||||||
/// run only the service as a daemon without the frontend
|
#[command(subcommand)]
|
||||||
#[arg(short, long)]
|
pub command: Option<Command>,
|
||||||
daemon: bool,
|
|
||||||
|
|
||||||
/// test input capture
|
|
||||||
#[arg(long)]
|
|
||||||
test_capture: bool,
|
|
||||||
|
|
||||||
/// test input emulation
|
|
||||||
#[arg(long)]
|
|
||||||
test_emulation: bool,
|
|
||||||
|
|
||||||
/// capture backend override
|
/// capture backend override
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
capture_backend: Option<CaptureBackend>,
|
pub capture_backend: Option<CaptureBackend>,
|
||||||
|
|
||||||
/// emulation backend override
|
/// emulation backend override
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
emulation_backend: Option<EmulationBackend>,
|
pub emulation_backend: Option<EmulationBackend>,
|
||||||
|
|
||||||
/// path to non-default certificate location
|
/// path to non-default certificate location
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
cert_path: Option<PathBuf>,
|
pub cert_path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Command {
|
||||||
|
/// test input emulation
|
||||||
|
TestEmulation(TestEmulationArgs),
|
||||||
|
/// test input capture
|
||||||
|
TestCapture(TestCaptureArgs),
|
||||||
|
/// Lan Mouse commandline interface
|
||||||
|
Cli(CliArgs),
|
||||||
|
/// run in daemon mode
|
||||||
|
Daemon,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
|
||||||
@@ -219,8 +224,8 @@ impl Display for EmulationBackend {
|
|||||||
pub enum Frontend {
|
pub enum Frontend {
|
||||||
#[serde(rename = "gtk")]
|
#[serde(rename = "gtk")]
|
||||||
Gtk,
|
Gtk,
|
||||||
#[serde(rename = "cli")]
|
#[serde(rename = "none")]
|
||||||
Cli,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Frontend {
|
impl Default for Frontend {
|
||||||
@@ -228,7 +233,7 @@ impl Default for Frontend {
|
|||||||
if cfg!(feature = "gtk") {
|
if cfg!(feature = "gtk") {
|
||||||
Self::Gtk
|
Self::Gtk
|
||||||
} else {
|
} else {
|
||||||
Self::Cli
|
Self::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,8 +254,6 @@ pub struct Config {
|
|||||||
pub port: u16,
|
pub port: u16,
|
||||||
/// list of clients
|
/// list of clients
|
||||||
pub clients: Vec<(TomlClient, Position)>,
|
pub clients: Vec<(TomlClient, Position)>,
|
||||||
/// whether or not to run as a daemon
|
|
||||||
pub daemon: bool,
|
|
||||||
/// configured release bind
|
/// configured release bind
|
||||||
pub release_bind: Vec<scancode::Linux>,
|
pub release_bind: Vec<scancode::Linux>,
|
||||||
/// test capture instead of running the app
|
/// test capture instead of running the app
|
||||||
@@ -284,8 +287,7 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
|
|||||||
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
|
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new() -> Result<Self, ConfigError> {
|
pub fn new(args: &Args) -> Result<Self, ConfigError> {
|
||||||
let args = CliArgs::parse();
|
|
||||||
const CONFIG_FILE_NAME: &str = "config.toml";
|
const CONFIG_FILE_NAME: &str = "config.toml";
|
||||||
const CERT_FILE_NAME: &str = "lan-mouse.pem";
|
const CERT_FILE_NAME: &str = "lan-mouse.pem";
|
||||||
|
|
||||||
@@ -307,7 +309,7 @@ impl Config {
|
|||||||
let config_file = config_path.join(CONFIG_FILE_NAME);
|
let config_file = config_path.join(CONFIG_FILE_NAME);
|
||||||
|
|
||||||
// --config <file> overrules default location
|
// --config <file> overrules default location
|
||||||
let config_file = args.config.map(PathBuf::from).unwrap_or(config_file);
|
let config_file = args.config.clone().unwrap_or(config_file);
|
||||||
|
|
||||||
let mut config_toml = match ConfigToml::new(&config_file) {
|
let mut config_toml = match ConfigToml::new(&config_file) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -343,6 +345,7 @@ impl Config {
|
|||||||
|
|
||||||
let cert_path = args
|
let cert_path = args
|
||||||
.cert_path
|
.cert_path
|
||||||
|
.clone()
|
||||||
.or(config_toml.as_ref().and_then(|c| c.cert_path.clone()))
|
.or(config_toml.as_ref().and_then(|c| c.cert_path.clone()))
|
||||||
.unwrap_or(config_path.join(CERT_FILE_NAME));
|
.unwrap_or(config_path.join(CERT_FILE_NAME));
|
||||||
|
|
||||||
@@ -368,16 +371,14 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let daemon = args.daemon;
|
let test_capture = matches!(args.command, Some(Command::TestCapture(_)));
|
||||||
let test_capture = args.test_capture;
|
let test_emulation = matches!(args.command, Some(Command::TestEmulation(_)));
|
||||||
let test_emulation = args.test_emulation;
|
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
path: config_path,
|
path: config_path,
|
||||||
authorized_fingerprints,
|
authorized_fingerprints,
|
||||||
capture_backend,
|
capture_backend,
|
||||||
emulation_backend,
|
emulation_backend,
|
||||||
daemon,
|
|
||||||
frontend,
|
frontend,
|
||||||
clients,
|
clients,
|
||||||
port,
|
port,
|
||||||
|
|||||||
15
src/dns.rs
15
src/dns.rs
@@ -1,4 +1,4 @@
|
|||||||
use std::net::IpAddr;
|
use std::{collections::HashMap, net::IpAddr};
|
||||||
|
|
||||||
use local_channel::mpsc::{channel, Receiver, Sender};
|
use local_channel::mpsc::{channel, Receiver, Sender};
|
||||||
use tokio::task::{spawn_local, JoinHandle};
|
use tokio::task::{spawn_local, JoinHandle};
|
||||||
@@ -30,6 +30,7 @@ struct DnsTask {
|
|||||||
request_rx: Receiver<DnsRequest>,
|
request_rx: Receiver<DnsRequest>,
|
||||||
event_tx: Sender<DnsEvent>,
|
event_tx: Sender<DnsEvent>,
|
||||||
cancellation_token: CancellationToken,
|
cancellation_token: CancellationToken,
|
||||||
|
active_tasks: HashMap<ClientHandle, JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DnsResolver {
|
impl DnsResolver {
|
||||||
@@ -39,6 +40,7 @@ impl DnsResolver {
|
|||||||
let (event_tx, event_rx) = channel();
|
let (event_tx, event_rx) = channel();
|
||||||
let cancellation_token = CancellationToken::new();
|
let cancellation_token = CancellationToken::new();
|
||||||
let dns_task = DnsTask {
|
let dns_task = DnsTask {
|
||||||
|
active_tasks: Default::default(),
|
||||||
resolver,
|
resolver,
|
||||||
request_rx,
|
request_rx,
|
||||||
event_tx,
|
event_tx,
|
||||||
@@ -81,6 +83,14 @@ impl DnsTask {
|
|||||||
while let Some(dns_request) = self.request_rx.recv().await {
|
while let Some(dns_request) = self.request_rx.recv().await {
|
||||||
let DnsRequest { handle, hostname } = dns_request;
|
let DnsRequest { handle, hostname } = dns_request;
|
||||||
|
|
||||||
|
/* abort previous dns task */
|
||||||
|
let previous_task = self.active_tasks.remove(&handle);
|
||||||
|
if let Some(task) = previous_task {
|
||||||
|
if !task.is_finished() {
|
||||||
|
task.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.event_tx
|
self.event_tx
|
||||||
.send(DnsEvent::Resolving(handle))
|
.send(DnsEvent::Resolving(handle))
|
||||||
.expect("channel closed");
|
.expect("channel closed");
|
||||||
@@ -90,7 +100,7 @@ impl DnsTask {
|
|||||||
let resolver = self.resolver.clone();
|
let resolver = self.resolver.clone();
|
||||||
let cancellation_token = self.cancellation_token.clone();
|
let cancellation_token = self.cancellation_token.clone();
|
||||||
|
|
||||||
tokio::task::spawn_local(async move {
|
let task = tokio::task::spawn_local(async move {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
ips = resolver.lookup_ip(&hostname) => {
|
ips = resolver.lookup_ip(&hostname) => {
|
||||||
let ips = ips.map(|ips| ips.iter().collect::<Vec<_>>());
|
let ips = ips.map(|ips| ips.iter().collect::<Vec<_>>());
|
||||||
@@ -101,6 +111,7 @@ impl DnsTask {
|
|||||||
_ = cancellation_token.cancelled() => {},
|
_ = cancellation_token.cancelled() => {},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
self.active_tasks.insert(handle, task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use clap::Args;
|
||||||
use input_emulation::{InputEmulation, InputEmulationError};
|
use input_emulation::{InputEmulation, InputEmulationError};
|
||||||
use input_event::{Event, PointerEvent};
|
use input_event::{Event, PointerEvent};
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
@@ -7,7 +8,17 @@ use std::time::{Duration, Instant};
|
|||||||
const FREQUENCY_HZ: f64 = 1.0;
|
const FREQUENCY_HZ: f64 = 1.0;
|
||||||
const RADIUS: f64 = 100.0;
|
const RADIUS: f64 = 100.0;
|
||||||
|
|
||||||
pub async fn run(config: Config) -> Result<(), InputEmulationError> {
|
#[derive(Args, Debug, Eq, PartialEq)]
|
||||||
|
pub struct TestEmulationArgs {
|
||||||
|
#[arg(long)]
|
||||||
|
mouse: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
keyboard: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
scroll: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(config: Config, _args: TestEmulationArgs) -> Result<(), InputEmulationError> {
|
||||||
log::info!("running input emulation test");
|
log::info!("running input emulation test");
|
||||||
|
|
||||||
let backend = config.emulation_backend.map(|b| b.into());
|
let backend = config.emulation_backend.map(|b| b.into());
|
||||||
|
|||||||
69
src/main.rs
69
src/main.rs
@@ -1,12 +1,14 @@
|
|||||||
|
use clap::Parser;
|
||||||
use env_logger::Env;
|
use env_logger::Env;
|
||||||
use input_capture::InputCaptureError;
|
use input_capture::InputCaptureError;
|
||||||
use input_emulation::InputEmulationError;
|
use input_emulation::InputEmulationError;
|
||||||
use lan_mouse::{
|
use lan_mouse::{
|
||||||
capture_test,
|
capture_test,
|
||||||
config::{Config, ConfigError, Frontend},
|
config::{self, Config, ConfigError, Frontend},
|
||||||
emulation_test,
|
emulation_test,
|
||||||
service::{Service, ServiceError},
|
service::{Service, ServiceError},
|
||||||
};
|
};
|
||||||
|
use lan_mouse_cli::CliError;
|
||||||
use lan_mouse_ipc::{IpcError, IpcListenerCreationError};
|
use lan_mouse_ipc::{IpcError, IpcListenerCreationError};
|
||||||
use std::{
|
use std::{
|
||||||
future::Future,
|
future::Future,
|
||||||
@@ -30,6 +32,8 @@ enum LanMouseError {
|
|||||||
Capture(#[from] InputCaptureError),
|
Capture(#[from] InputCaptureError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Emulation(#[from] InputEmulationError),
|
Emulation(#[from] InputEmulationError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Cli(#[from] CliError),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -45,34 +49,39 @@ fn main() {
|
|||||||
|
|
||||||
fn run() -> Result<(), LanMouseError> {
|
fn run() -> Result<(), LanMouseError> {
|
||||||
// parse config file + cli args
|
// parse config file + cli args
|
||||||
let config = Config::new()?;
|
let args = config::Args::parse();
|
||||||
if config.test_capture {
|
let config = config::Config::new(&args)?;
|
||||||
run_async(capture_test::run(config))?;
|
match args.command {
|
||||||
} else if config.test_emulation {
|
Some(command) => match command {
|
||||||
run_async(emulation_test::run(config))?;
|
config::Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?,
|
||||||
} else if config.daemon {
|
config::Command::TestCapture(args) => run_async(capture_test::run(config, args))?,
|
||||||
// if daemon is specified we run the service
|
config::Command::Cli(cli_args) => run_async(lan_mouse_cli::run(cli_args))?,
|
||||||
match run_async(run_service(config)) {
|
config::Command::Daemon => {
|
||||||
Err(LanMouseError::Service(ServiceError::IpcListen(
|
// if daemon is specified we run the service
|
||||||
IpcListenerCreationError::AlreadyRunning,
|
match run_async(run_service(config)) {
|
||||||
))) => log::info!("service already running!"),
|
Err(LanMouseError::Service(ServiceError::IpcListen(
|
||||||
r => r?,
|
IpcListenerCreationError::AlreadyRunning,
|
||||||
}
|
))) => log::info!("service already running!"),
|
||||||
} else {
|
r => r?,
|
||||||
// otherwise start the service as a child process and
|
}
|
||||||
// run a frontend
|
|
||||||
let mut service = start_service()?;
|
|
||||||
run_frontend(&config)?;
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
// on unix we give the service a chance to terminate gracefully
|
|
||||||
let pid = service.id() as libc::pid_t;
|
|
||||||
unsafe {
|
|
||||||
libc::kill(pid, libc::SIGINT);
|
|
||||||
}
|
}
|
||||||
service.wait()?;
|
},
|
||||||
|
None => {
|
||||||
|
// otherwise start the service as a child process and
|
||||||
|
// run a frontend
|
||||||
|
let mut service = start_service()?;
|
||||||
|
run_frontend(&config)?;
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
// on unix we give the service a chance to terminate gracefully
|
||||||
|
let pid = service.id() as libc::pid_t;
|
||||||
|
unsafe {
|
||||||
|
libc::kill(pid, libc::SIGINT);
|
||||||
|
}
|
||||||
|
service.wait()?;
|
||||||
|
}
|
||||||
|
service.kill()?;
|
||||||
}
|
}
|
||||||
service.kill()?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -96,7 +105,7 @@ where
|
|||||||
fn start_service() -> Result<Child, io::Error> {
|
fn start_service() -> Result<Child, io::Error> {
|
||||||
let child = Command::new(std::env::current_exe()?)
|
let child = Command::new(std::env::current_exe()?)
|
||||||
.args(std::env::args().skip(1))
|
.args(std::env::args().skip(1))
|
||||||
.arg("--daemon")
|
.arg("daemon")
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
Ok(child)
|
Ok(child)
|
||||||
}
|
}
|
||||||
@@ -118,8 +127,8 @@ fn run_frontend(config: &Config) -> Result<(), IpcError> {
|
|||||||
}
|
}
|
||||||
#[cfg(not(feature = "gtk"))]
|
#[cfg(not(feature = "gtk"))]
|
||||||
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
|
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
|
||||||
Frontend::Cli => {
|
Frontend::None => {
|
||||||
lan_mouse_cli::run()?;
|
log::warn!("no frontend available!");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -186,7 +186,6 @@ impl Service {
|
|||||||
FrontendRequest::EnableCapture => self.capture.reenable(),
|
FrontendRequest::EnableCapture => self.capture.reenable(),
|
||||||
FrontendRequest::EnableEmulation => self.emulation.reenable(),
|
FrontendRequest::EnableEmulation => self.emulation.reenable(),
|
||||||
FrontendRequest::Enumerate() => self.enumerate(),
|
FrontendRequest::Enumerate() => self.enumerate(),
|
||||||
FrontendRequest::GetState(handle) => self.broadcast_client(handle),
|
|
||||||
FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
|
FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
|
||||||
FrontendRequest::UpdateHostname(handle, host) => self.update_hostname(handle, host),
|
FrontendRequest::UpdateHostname(handle, host) => self.update_hostname(handle, host),
|
||||||
FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
|
FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
|
||||||
@@ -194,6 +193,9 @@ impl Service {
|
|||||||
FrontendRequest::ResolveDns(handle) => self.resolve(handle),
|
FrontendRequest::ResolveDns(handle) => self.resolve(handle),
|
||||||
FrontendRequest::Sync => self.sync_frontend(),
|
FrontendRequest::Sync => self.sync_frontend(),
|
||||||
FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key),
|
FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key),
|
||||||
|
FrontendRequest::UpdateEnterHook(handle, enter_hook) => {
|
||||||
|
self.update_enter_hook(handle, enter_hook)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +285,7 @@ impl Service {
|
|||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.notify_frontend(FrontendEvent::Changed(handle));
|
self.broadcast_client(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve(&self, handle: ClientHandle) {
|
fn resolve(&self, handle: ClientHandle) {
|
||||||
@@ -399,7 +401,7 @@ impl Service {
|
|||||||
log::debug!("deactivating client {handle}");
|
log::debug!("deactivating client {handle}");
|
||||||
if self.client_manager.deactivate_client(handle) {
|
if self.client_manager.deactivate_client(handle) {
|
||||||
self.capture.destroy(handle);
|
self.capture.destroy(handle);
|
||||||
self.notify_frontend(FrontendEvent::Changed(handle));
|
self.broadcast_client(handle);
|
||||||
log::info!("deactivated client {handle}");
|
log::info!("deactivated client {handle}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +427,7 @@ impl Service {
|
|||||||
if self.client_manager.activate_client(handle) {
|
if self.client_manager.activate_client(handle) {
|
||||||
/* notify capture and frontends */
|
/* notify capture and frontends */
|
||||||
self.capture.create(handle, pos, CaptureType::Default);
|
self.capture.create(handle, pos, CaptureType::Default);
|
||||||
self.notify_frontend(FrontendEvent::Changed(handle));
|
self.broadcast_client(handle);
|
||||||
log::info!("activated client {handle} ({pos})");
|
log::info!("activated client {handle} ({pos})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,19 +454,20 @@ impl Service {
|
|||||||
|
|
||||||
fn update_fix_ips(&mut self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
|
fn update_fix_ips(&mut self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
|
||||||
self.client_manager.set_fix_ips(handle, fix_ips);
|
self.client_manager.set_fix_ips(handle, fix_ips);
|
||||||
self.notify_frontend(FrontendEvent::Changed(handle));
|
self.broadcast_client(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_hostname(&mut self, handle: ClientHandle, hostname: Option<String>) {
|
fn update_hostname(&mut self, handle: ClientHandle, hostname: Option<String>) {
|
||||||
|
log::info!("hostname changed: {hostname:?}");
|
||||||
if self.client_manager.set_hostname(handle, hostname.clone()) {
|
if self.client_manager.set_hostname(handle, hostname.clone()) {
|
||||||
self.resolve(handle);
|
self.resolve(handle);
|
||||||
}
|
}
|
||||||
self.notify_frontend(FrontendEvent::Changed(handle));
|
self.broadcast_client(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_port(&mut self, handle: ClientHandle, port: u16) {
|
fn update_port(&mut self, handle: ClientHandle, port: u16) {
|
||||||
self.client_manager.set_port(handle, port);
|
self.client_manager.set_port(handle, port);
|
||||||
self.notify_frontend(FrontendEvent::Changed(handle));
|
self.broadcast_client(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_pos(&mut self, handle: ClientHandle, pos: Position) {
|
fn update_pos(&mut self, handle: ClientHandle, pos: Position) {
|
||||||
@@ -473,7 +476,12 @@ impl Service {
|
|||||||
self.deactivate_client(handle);
|
self.deactivate_client(handle);
|
||||||
self.activate_client(handle);
|
self.activate_client(handle);
|
||||||
}
|
}
|
||||||
self.notify_frontend(FrontendEvent::Changed(handle));
|
self.broadcast_client(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_enter_hook(&mut self, handle: ClientHandle, enter_hook: Option<String>) {
|
||||||
|
self.client_manager.set_enter_hook(handle, enter_hook);
|
||||||
|
self.broadcast_client(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn broadcast_client(&mut self, handle: ClientHandle) {
|
fn broadcast_client(&mut self, handle: ClientHandle) {
|
||||||
|
|||||||
Reference in New Issue
Block a user