mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-03-07 11:59:59 +03:00
committed by
GitHub
parent
c50b746816
commit
d042c0aa4a
1
.github/workflows/pre-release.yml
vendored
1
.github/workflows/pre-release.yml
vendored
@@ -18,6 +18,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libx11-dev libxtst-dev
|
||||
sudo apt-get install libadwaita-1-dev libgtk-4-dev
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Upload build artifact
|
||||
|
||||
1
.github/workflows/rust.yml
vendored
1
.github/workflows/rust.yml
vendored
@@ -20,6 +20,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libx11-dev libxtst-dev
|
||||
sudo apt-get install libadwaita-1-dev libgtk-4-dev
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
|
||||
1
.github/workflows/tagged-release.yml
vendored
1
.github/workflows/tagged-release.yml
vendored
@@ -14,6 +14,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libx11-dev libxtst-dev
|
||||
sudo apt-get install libadwaita-1-dev libgtk-4-dev
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Upload build artifact
|
||||
|
||||
53
Cargo.lock
generated
53
Cargo.lock
generated
@@ -155,14 +155,14 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.5.1"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
|
||||
checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -423,6 +423,12 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-build-tools"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3431c56f463443cba9bc3600248bc6d680cb614c2ee1cdd39dab5415bd12ac5c"
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.18.2"
|
||||
@@ -601,17 +607,6 @@ version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.4.0"
|
||||
@@ -684,6 +679,7 @@ version = "0.2.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"env_logger",
|
||||
"glib-build-tools",
|
||||
"gtk4",
|
||||
"libadwaita",
|
||||
"libc",
|
||||
@@ -705,12 +701,6 @@ dependencies = [
|
||||
"x11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libadwaita"
|
||||
version = "0.5.2"
|
||||
@@ -802,12 +792,6 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.3"
|
||||
@@ -1433,9 +1417,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "trust-dns-proto"
|
||||
version = "0.22.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
|
||||
checksum = "0dc775440033cb114085f6f2437682b194fa7546466024b1037e82a48a052a69"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if",
|
||||
@@ -1444,9 +1428,9 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"idna 0.2.3",
|
||||
"idna",
|
||||
"ipnet",
|
||||
"lazy_static",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
@@ -1458,16 +1442,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "trust-dns-resolver"
|
||||
version = "0.22.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
|
||||
checksum = "2dff7aed33ef3e8bf2c9966fccdfed93f93d46f432282ea875cd66faabc6ef2f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"ipconfig",
|
||||
"lazy_static",
|
||||
"lru-cache",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
@@ -1504,7 +1489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna 0.4.0",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -13,8 +13,8 @@ strip = true
|
||||
lto = "fat"
|
||||
|
||||
[dependencies]
|
||||
tempfile = "3.6"
|
||||
trust-dns-resolver = "0.22"
|
||||
tempfile = "3.8"
|
||||
trust-dns-resolver = "0.23"
|
||||
memmap = "0.7"
|
||||
toml = "0.7"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -33,16 +33,30 @@ wayland-protocols-misc = { version="0.1.0", features=["client"], optional = true
|
||||
wayland-protocols-plasma = { version="0.1.0", features=["client"], optional = true }
|
||||
mio-signals = "0.2.0"
|
||||
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
|
||||
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_8"], optional = true }
|
||||
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_3"], optional = true }
|
||||
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_6"], optional = true }
|
||||
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3.9", features = ["winuser"] }
|
||||
|
||||
[target.'cfg(unix)'.build-dependencies]
|
||||
glib-build-tools = "0.18.0"
|
||||
|
||||
[features]
|
||||
default = ["wayland", "x11", "xdg_desktop_portal", "libei"]
|
||||
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc", "dep:wayland-protocols-plasma"]
|
||||
x11 = ["dep:x11"]
|
||||
default = [
|
||||
"wayland",
|
||||
"x11",
|
||||
"xdg_desktop_portal",
|
||||
"libei",
|
||||
"gtk",
|
||||
]
|
||||
wayland = [
|
||||
"dep:wayland-client",
|
||||
"dep:wayland-protocols",
|
||||
"dep:wayland-protocols-wlr",
|
||||
"dep:wayland-protocols-misc",
|
||||
"dep:wayland-protocols-plasma" ]
|
||||
x11 = [ "dep:x11" ]
|
||||
xdg_desktop_portal = []
|
||||
libei = []
|
||||
gtk = ["dep:gtk", "dep:adw"]
|
||||
|
||||
58
README.md
58
README.md
@@ -1,4 +1,7 @@
|
||||
# Lan Mouse Share
|
||||
|
||||

|
||||
|
||||
Goal of this project is to be an open-source replacement for proprietary tools like [Synergy](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
|
||||
|
||||
Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
|
||||
@@ -7,6 +10,8 @@ Of course ***blazingly fast™*** and stable, because it's written in rust.
|
||||
|
||||
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
|
||||
|
||||
_Now with a gtk frontend_
|
||||
|
||||
## Configuration
|
||||
Configuration is done through the file `config.toml`,
|
||||
which must be located in the current working directory when
|
||||
@@ -16,32 +21,43 @@ executing lan-mouse.
|
||||
A minimal config file could look like this:
|
||||
|
||||
```toml
|
||||
# example configuration
|
||||
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# # optional frontend -> defaults to gtk if available
|
||||
# # possible values are "cli" and "gtk"
|
||||
# frontend = "gtk"
|
||||
|
||||
# define a client on the right side with host name "iridium"
|
||||
[right]
|
||||
# hostname
|
||||
host_name = "iridium"
|
||||
# optional list of (known) ip addresses
|
||||
ips = ["192.168.178.156"]
|
||||
|
||||
# define a client on the left side with IP address 192.168.178.189
|
||||
[left]
|
||||
host_name = "my-laptop"
|
||||
# The hostname is optional: When no hostname is specified,
|
||||
# at least one ip address needs to be specified.
|
||||
host_name = "thorium"
|
||||
# ips for ethernet and wifi
|
||||
ips = ["192.168.178.189"]
|
||||
```
|
||||
|
||||
Where `left` can be either `left`, `right`, `top` or `bottom`.
|
||||
|
||||
> :warning: Note, that with the gtk frontend, the clients from the config
|
||||
> file are currently ignored.
|
||||
|
||||
### Additional options
|
||||
Additionally
|
||||
- a preferred backend
|
||||
- a port override for the default port (4242)
|
||||
|
||||
can be specified.
|
||||
|
||||
Supported backends currently include "wlroots", "x11" and "windows".
|
||||
|
||||
These two options can also be specified via the commandline
|
||||
options `--backend` and `--port` respectively.
|
||||
|
||||
## Build and Run
|
||||
Build only
|
||||
Build in release mode:
|
||||
```sh
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Run
|
||||
Run directly:
|
||||
```sh
|
||||
cargo run --release
|
||||
```
|
||||
@@ -133,12 +149,8 @@ This is to be looked into in the future.
|
||||
(this works natively on sway versions >= 1.8)
|
||||
|
||||
## Windows support
|
||||
Currently windows can receive mouse and keyboard events, however unlike
|
||||
with the wlroots back-end,
|
||||
|
||||
the scancodes are not translated between keyboard layouts.
|
||||
|
||||
Event emitting is WIP.
|
||||
Currently windows can receive mouse and keyboard events,
|
||||
event producing on windows is WIP.
|
||||
|
||||
|
||||
## TODOS
|
||||
@@ -151,11 +163,11 @@ Event emitting is WIP.
|
||||
- [x] Button support
|
||||
- [ ] Latency measurement + logging
|
||||
- [ ] Bandwidth usage approximation + logging
|
||||
- [ ] Multiple IP addresses -> check which one is reachable
|
||||
- [x] Multiple IP addresses -> check which one is reachable
|
||||
- [x] Merge server and client -> Both client and server can send and receive events depending on what mouse is used where
|
||||
- [ ] Liveness tracking (automatically ungrab mouse when client unreachable)
|
||||
- [x] Liveness tracking (automatically ungrab mouse when client unreachable)
|
||||
- [ ] Clipboard support
|
||||
- [ ] Graphical frontend (gtk?)
|
||||
- [x] Graphical frontend (gtk?)
|
||||
- [ ] *Encrytion*
|
||||
- [ ] Gnome Shell Extension (layer shell is not supported)
|
||||
- [ ] respect xdg-config-home for config file location.
|
||||
|
||||
9
build.rs
Normal file
9
build.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
fn main() {
|
||||
// composite_templates
|
||||
#[cfg(unix)]
|
||||
glib_build_tools::compile_resources(
|
||||
&["resources"],
|
||||
"resources/resources.gresource.xml",
|
||||
"lan-mouse.gresource",
|
||||
);
|
||||
}
|
||||
@@ -2,19 +2,20 @@
|
||||
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# optional frontend -> defaults to gtk if available
|
||||
# frontend = "gtk"
|
||||
|
||||
# define a client on the right side with host name "iridium"
|
||||
[right]
|
||||
# hostname
|
||||
host_name = "iridium"
|
||||
# optional list of (known) ip addresses
|
||||
ips = ["192.168.178.141"]
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
ips = ["192.168.178.156"]
|
||||
|
||||
# define a client on the left side with IP address 192.168.178.189
|
||||
[left]
|
||||
# The hostname is optional: When no hostname is specified,
|
||||
# at least one ip address needs to be specified.
|
||||
host_name = "thorium"
|
||||
ips = ["192.168.178.189"]
|
||||
# ips for ethernet and wifi
|
||||
ips = ["192.168.178.189", "192.168.178.172"]
|
||||
|
||||
74
resources/client_row.ui
Normal file
74
resources/client_row.ui
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="ClientRow" parent="AdwExpanderRow">
|
||||
<property name="title">hostname</property>
|
||||
<!-- enabled -->
|
||||
<child type="prefix">
|
||||
<object class="GtkSwitch" id="enable_switch">
|
||||
<signal name="state_set" handler="handle_client_set_state" swapped="true"/>
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="tooltip-text" translatable="yes">enable</property>
|
||||
</object>
|
||||
</child>
|
||||
<!-- host -->
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title">hostname</property>
|
||||
<property name="subtitle">port</property>
|
||||
<!-- hostname -->
|
||||
<child>
|
||||
<object class="GtkEntry" id="hostname">
|
||||
<!-- <property name="title" translatable="yes">hostname</property> -->
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="placeholder-text">hostname</property>
|
||||
<property name="width-chars">-1</property>
|
||||
</object>
|
||||
</child>
|
||||
<!-- port -->
|
||||
<child>
|
||||
<object class="GtkEntry" id="port">
|
||||
<!-- <property name="title" translatable="yes">port</property> -->
|
||||
<property name="input_purpose">GTK_INPUT_PURPOSE_NUMBER</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="placeholder-text">4242</property>
|
||||
<property name="width-chars">5</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<!-- position -->
|
||||
<child>
|
||||
<object class="AdwComboRow" id="position">
|
||||
<property name="title" translatable="yes">position</property>
|
||||
<property name="model">
|
||||
<object class="GtkStringList">
|
||||
<items>
|
||||
<item>Left</item>
|
||||
<item>Right</item>
|
||||
<item>Top</item>
|
||||
<item>Bottom</item>
|
||||
</items>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<!-- delete button -->
|
||||
<child>
|
||||
<object class="AdwActionRow" id="delete_row">
|
||||
<property name="title">delete this client</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="delete_button">
|
||||
<signal name="activate" handler="handle_client_delete" object="delete_row" swapped="true"/>
|
||||
<property name="icon-name">user-trash-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="name">delete-button</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
171
resources/mouse-icon.svg
Normal file
171
resources/mouse-icon.svg
Normal file
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
sodipodi:docname="mouse-icon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="22.737887"
|
||||
inkscape:cx="19.54887"
|
||||
inkscape:cy="26.167778"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1374"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g20"
|
||||
transform="translate(1.1586889,0.39019296)">
|
||||
<g
|
||||
id="g8"
|
||||
transform="translate(-0.11519282,-3.9659242)">
|
||||
<g
|
||||
id="g6"
|
||||
transform="translate(0.67275315,0.39959697)">
|
||||
<g
|
||||
id="g5">
|
||||
<rect
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="rect4"
|
||||
width="1.3032579"
|
||||
height="1.3032579"
|
||||
x="1.7199994"
|
||||
y="7.5408325"
|
||||
ry="0.3373504" />
|
||||
<rect
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="rect4-2"
|
||||
width="1.3032579"
|
||||
height="1.3032579"
|
||||
x="3.8428385"
|
||||
y="7.5408325"
|
||||
ry="0.3373504" />
|
||||
</g>
|
||||
<rect
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="rect4-3"
|
||||
width="1.3032579"
|
||||
height="1.3032579"
|
||||
x="2.781419"
|
||||
y="5.1382394"
|
||||
ry="0.3373504" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
d="M 1.1519282,7.3907619 H 7.059674"
|
||||
id="path5" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
d="M 4.1058009,6.8410941 V 7.3907617"
|
||||
id="path6" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
d="m 5.1672204,7.9404294 2e-7,-0.5496677"
|
||||
id="path7" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
d="M 3.0443815,7.9404294 V 7.3907617"
|
||||
id="path8" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
d="M 6.9444811,3.4248375 Z"
|
||||
id="path9" />
|
||||
<path
|
||||
style="color:#000000;fill:#000000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 6.9840449,3.4464199 c -0.072714,-0.0035 -0.1209639,-0.2113583 -0.125,-0.1386718 -0.0035,0.072714 0.052314,0.1346357 0.125,0.1386718 0,0 0.6614057,0.034643 1.3535156,0.4765625 0.6921097,0.4419191 1.4111567,1.2803292 1.5136717,2.9433594 0.05132,0.832563 -0.07521,1.3855916 -0.279297,1.75 -0.20409,0.3644084 -0.482943,0.5482749 -0.777343,0.640625 -0.5888014,0.1847002 -1.2265629,-0.021484 -1.2265629,-0.021484 -0.069024,-0.023541 -0.144095,0.013122 -0.1679688,0.082031 -0.023366,0.069587 0.014295,0.1449093 0.083984,0.1679687 0,0 0.6961634,0.2406696 1.3886717,0.023437 C 9.2189712,9.4003039 9.5672292,9.1706004 9.8043572,8.7472012 10.041486,8.323802 10.170261,7.7150888 10.116858,6.8487637 10.009921,5.1140179 9.2320232,4.3532014 8.4801387,3.8731154 7.7282538,3.3930294 6.9840449,3.4464198 6.9840449,3.4464199 Z"
|
||||
id="path18"
|
||||
sodipodi:nodetypes="cccsssscccssssc" />
|
||||
<g
|
||||
id="g19"
|
||||
transform="matrix(1.8148709,0,0,1.8148709,-4.1533763,-7.8818885)">
|
||||
<g
|
||||
id="g17"
|
||||
transform="translate(0.01163623,0.23038484)">
|
||||
<ellipse
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="path10"
|
||||
cx="3.9823804"
|
||||
cy="8.17869"
|
||||
rx="0.49368349"
|
||||
ry="0.62533247" />
|
||||
<ellipse
|
||||
style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.168876;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="ellipse17"
|
||||
cx="3.9823804"
|
||||
cy="8.17869"
|
||||
rx="0.31096464"
|
||||
ry="0.40317491" />
|
||||
</g>
|
||||
<path
|
||||
id="path11"
|
||||
style="stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="M 7.479305,9.4704944 C 7.4964603,9.9336885 6.9306558,9.9678313 5.3811502,10.087599 3.2109768,10.255341 2.4751992,9.6707727 2.4355055,9.5280908 2.3112754,9.0815374 3.8270232,8.4090748 5.3811502,8.4090748 c 1.5633309,0 2.0816988,0.6171052 2.0981548,1.0614196 z"
|
||||
sodipodi:nodetypes="sssss" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="path12"
|
||||
cx="3.5281858"
|
||||
cy="9.0632057"
|
||||
r="0.18513133" />
|
||||
<g
|
||||
id="g18"
|
||||
transform="translate(0.01163623,0.23038484)">
|
||||
<ellipse
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="path10-2"
|
||||
cx="4.6085634"
|
||||
cy="8.17869"
|
||||
rx="0.49368349"
|
||||
ry="0.62533247" />
|
||||
<ellipse
|
||||
style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.168876;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="ellipse16"
|
||||
cx="4.6085634"
|
||||
cy="8.17869"
|
||||
rx="0.31096464"
|
||||
ry="0.40317491" />
|
||||
</g>
|
||||
<ellipse
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.112226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="circle18"
|
||||
cx="3.5003331"
|
||||
cy="9.0344076"
|
||||
rx="0.078639306"
|
||||
ry="0.07816644" />
|
||||
<ellipse
|
||||
style="fill:#4f4f4f;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path19"
|
||||
cx="2.4818404"
|
||||
cy="9.4499254"
|
||||
rx="0.05348238"
|
||||
ry="0.11930636" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
12
resources/resources.gresource.xml
Normal file
12
resources/resources.gresource.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/de/feschber/LanMouse">
|
||||
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
|
||||
<file compressed="true">style.css</file>
|
||||
<file compressed="true">style-dark.css</file>
|
||||
</gresource>
|
||||
<gresource prefix="/de/feschber/LanMouse/icons">
|
||||
<file compressed="true" preprocess="xml-stripblanks">mouse-icon.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
3
resources/style-dark.css
Normal file
3
resources/style-dark.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#delete-button {
|
||||
color: @red_1;
|
||||
}
|
||||
3
resources/style.css
Normal file
3
resources/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#delete-button {
|
||||
color: @red_3;
|
||||
}
|
||||
121
resources/window.ui
Normal file
121
resources/window.ui
Normal file
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<menu id="main-menu">
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Close window</attribute>
|
||||
<attribute name="action">window.close</attribute>
|
||||
</item>
|
||||
</menu>
|
||||
<template class="LanMouseWindow" parent="GtkApplicationWindow">
|
||||
<property name="width-request">800</property>
|
||||
<property name="title" translatable="yes">Lan Mouse</property>
|
||||
<property name="show-menubar">True</property>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar">
|
||||
<child type ="end">
|
||||
<object class="GtkMenuButton">
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
<property name="menu-model">main-menu</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwStatusPage">
|
||||
<property name="title" translatable="yes">Lan Mouse</property>
|
||||
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
|
||||
<property name="icon-name">mouse-icon</property>
|
||||
<property name="child">
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">600</property>
|
||||
<property name="tightening-threshold">300</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">General</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="yes">enable</property>
|
||||
<child type="suffix">
|
||||
<object class="GtkSwitch">
|
||||
<property name="valign">center</property>
|
||||
<property name="tooltip-text" translatable="yes">enable</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title">port</property>
|
||||
<child>
|
||||
<object class="GtkEntry">
|
||||
<!-- <property name="title" translatable="yes">port</property> -->
|
||||
<property name="placeholder-text">4242</property>
|
||||
<property name="width-chars">5</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="valign">center</property>
|
||||
<!-- <property name="show-apply-button">True</property> -->
|
||||
<property name="input-purpose">GTK_INPUT_PURPOSE_DIGITS</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Connections</property>
|
||||
<child>
|
||||
<object class="GtkListBox" id="client_list">
|
||||
<property name="selection-mode">none</property>
|
||||
<child type="placeholder">
|
||||
<object class="AdwActionRow" id="client_placeholder">
|
||||
<property name="title">No connections!</property>
|
||||
<property name="subtitle">add a new client via the button below</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="boxed-list" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<child>
|
||||
<object class="GtkButton" id="add_client_button">
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="tooltip-text">connect a new computer</property>
|
||||
<property name="child">
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
<property name="label" translatable="yes">Add</property>
|
||||
<property name="use-underline">True</property>
|
||||
</object>
|
||||
</property>
|
||||
<!-- <signal name="clicked" handler="handle_add_client" swapped="true"/> -->
|
||||
<style>
|
||||
<class name="pill"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
@@ -129,6 +129,15 @@ impl Window {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Window {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("destroying window!");
|
||||
self.layer_surface.destroy();
|
||||
self.surface.destroy();
|
||||
self.buffer.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(f: &mut File, (width, height): (u32, u32)) {
|
||||
let mut buf = BufWriter::new(f);
|
||||
for _ in 0..height {
|
||||
@@ -301,6 +310,7 @@ impl State {
|
||||
|
||||
fn add_client(&mut self, client: ClientHandle, pos: Position) {
|
||||
let window = Rc::new(Window::new(&self.g, &self.qh, pos));
|
||||
assert!(Rc::strong_count(&window) == 1);
|
||||
self.client_for_window.push((window, client));
|
||||
}
|
||||
}
|
||||
@@ -414,8 +424,15 @@ impl EventProducer for WaylandEventProducer {
|
||||
fn notify(&mut self, client_event: ClientEvent) {
|
||||
if let ClientEvent::Create(handle, pos) = client_event {
|
||||
self.state.add_client(handle, pos);
|
||||
self.flush_events();
|
||||
}
|
||||
if let ClientEvent::Destroy(handle) = client_event {
|
||||
if let Some(i) = self.state.client_for_window.iter().position(|(_,c)| *c == handle) {
|
||||
let w = self.state.client_for_window.remove(i);
|
||||
self.state.focused = None;
|
||||
assert!(Rc::strong_count(&w.0) == 1);
|
||||
}
|
||||
}
|
||||
self.flush_events();
|
||||
}
|
||||
|
||||
fn release(&mut self) {
|
||||
@@ -466,15 +483,16 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
} => {
|
||||
// get client corresponding to the focused surface
|
||||
log::trace!("produce: enter()");
|
||||
|
||||
{
|
||||
let (window, client) = app
|
||||
if let Some((window, client)) = app
|
||||
.client_for_window
|
||||
.iter()
|
||||
.find(|(w, _c)| w.surface == surface)
|
||||
.unwrap();
|
||||
app.focused = Some((window.clone(), *client));
|
||||
app.grab(&surface, pointer, serial.clone(), qh);
|
||||
.find(|(w, _c)| w.surface == surface) {
|
||||
app.focused = Some((window.clone(), *client));
|
||||
app.grab(&surface, pointer, serial.clone(), qh);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let (_, client) = app
|
||||
.client_for_window
|
||||
@@ -641,17 +659,17 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event {
|
||||
let (window, _client) = app
|
||||
if let Some((window, _client)) = app
|
||||
.client_for_window
|
||||
.iter()
|
||||
.find(|(w, _c)| &w.layer_surface == layer_surface)
|
||||
.unwrap();
|
||||
// client corresponding to the layer_surface
|
||||
let surface = &window.surface;
|
||||
let buffer = &window.buffer;
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
layer_surface.ack_configure(serial);
|
||||
surface.commit();
|
||||
.find(|(w, _c)| &w.layer_surface == layer_surface) {
|
||||
// client corresponding to the layer_surface
|
||||
let surface = &window.surface;
|
||||
let buffer = &window.buffer;
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
layer_surface.ack_configure(serial);
|
||||
surface.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,46 +121,46 @@ impl ClientManager {
|
||||
pub fn last_ping(&self, client: ClientHandle) -> Option<Duration> {
|
||||
let last_ping = self.last_ping
|
||||
.iter()
|
||||
.find(|(c,_)| *c == client)
|
||||
.unwrap().1;
|
||||
.find(|(c,_)| *c == client)?.1;
|
||||
last_ping.map(|p| p.elapsed())
|
||||
}
|
||||
|
||||
pub fn last_seen(&self, client: ClientHandle) -> Option<Duration> {
|
||||
let last_seen = self.last_seen
|
||||
.iter()
|
||||
.find(|(c, _)| *c == client)
|
||||
.unwrap().1;
|
||||
.find(|(c, _)| *c == client)?.1;
|
||||
last_seen.map(|t| t.elapsed())
|
||||
}
|
||||
|
||||
pub fn last_replied(&self, client: ClientHandle) -> Option<Duration> {
|
||||
let last_replied = self.last_replied
|
||||
.iter()
|
||||
.find(|(c, _)| *c == client)
|
||||
.unwrap().1;
|
||||
.find(|(c, _)| *c == client)?.1;
|
||||
last_replied.map(|t| t.elapsed())
|
||||
}
|
||||
|
||||
pub fn reset_last_ping(&mut self, client: ClientHandle) {
|
||||
self.last_ping
|
||||
if let Some(c) = self.last_ping
|
||||
.iter_mut()
|
||||
.find(|(c, _)| *c == client)
|
||||
.unwrap().1 = Some(Instant::now());
|
||||
.find(|(c, _)| *c == client) {
|
||||
c.1 = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_last_seen(&mut self, client: ClientHandle) {
|
||||
self.last_seen
|
||||
if let Some(c) = self.last_seen
|
||||
.iter_mut()
|
||||
.find(|(c, _)| *c == client)
|
||||
.unwrap().1 = Some(Instant::now());
|
||||
.find(|(c, _)| *c == client) {
|
||||
c.1 = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_last_replied(&mut self, client: ClientHandle) {
|
||||
self.last_replied
|
||||
if let Some(c) = self.last_replied
|
||||
.iter_mut()
|
||||
.find(|(c, _)| *c == client)
|
||||
.unwrap().1 = Some(Instant::now());
|
||||
.find(|(c, _)| *c == client) {
|
||||
c.1 = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_client(&self, addr: SocketAddr) -> Option<ClientHandle> {
|
||||
@@ -170,6 +170,15 @@ impl ClientManager {
|
||||
.map(|c| c.handle)
|
||||
}
|
||||
|
||||
pub fn remove_client(&mut self, client: ClientHandle) {
|
||||
if let Some(i) = self.clients.iter().position(|c| c.handle == client) {
|
||||
self.clients.remove(i);
|
||||
self.last_ping.remove(i);
|
||||
self.last_seen.remove(i);
|
||||
self.last_replied.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
fn next_id(&mut self) -> ClientHandle {
|
||||
let handle = self.next_client_id;
|
||||
self.next_client_id += 1;
|
||||
|
||||
@@ -64,6 +64,7 @@ fn find_arg(key: &'static str) -> Result<Option<String>, MissingParameter> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum Frontend {
|
||||
Gtk,
|
||||
Cli,
|
||||
@@ -96,6 +97,9 @@ impl Config {
|
||||
};
|
||||
|
||||
let frontend = match frontend {
|
||||
#[cfg(all(unix, feature = "gtk"))]
|
||||
None => Frontend::Gtk,
|
||||
#[cfg(any(not(feature = "gtk"), not(unix)))]
|
||||
None => Frontend::Cli,
|
||||
Some(s) => match s.as_str() {
|
||||
"cli" => Frontend::Cli,
|
||||
|
||||
@@ -6,7 +6,7 @@ use mio_signals::{Signals, Signal, SignalSet};
|
||||
|
||||
use std::{net::SocketAddr, io::ErrorKind};
|
||||
|
||||
use crate::{client::{ClientEvent, ClientManager, Position}, consumer::EventConsumer, producer::EventProducer, frontend::{FrontendEvent, FrontendAdapter}};
|
||||
use crate::{client::{ClientEvent, ClientManager, Position}, consumer::EventConsumer, producer::EventProducer, frontend::{FrontendEvent, FrontendAdapter}, dns};
|
||||
use super::Event;
|
||||
|
||||
/// keeps track of state to prevent a feedback loop
|
||||
@@ -94,10 +94,25 @@ impl Server {
|
||||
|
||||
pub fn add_client(&mut self, addr: HashSet<SocketAddr>, pos: Position) {
|
||||
let client = self.client_manager.add_client(addr, pos);
|
||||
log::debug!("add_client {client}");
|
||||
self.producer.notify(ClientEvent::Create(client, pos));
|
||||
self.consumer.notify(ClientEvent::Create(client, pos));
|
||||
}
|
||||
|
||||
pub fn remove_client(&mut self, host: String, port: u16) {
|
||||
if let Ok(ips) = dns::resolve(host.as_str()) {
|
||||
if let Some(ip) = ips.iter().next() {
|
||||
let addr = SocketAddr::new(*ip, port);
|
||||
if let Some(handle) = self.client_manager.get_client(addr) {
|
||||
log::debug!("remove_client {handle}");
|
||||
self.client_manager.remove_client(handle);
|
||||
self.producer.notify(ClientEvent::Destroy(handle));
|
||||
self.consumer.notify(ClientEvent::Destroy(handle));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_udp_rx(&mut self) {
|
||||
loop {
|
||||
let (event, addr) = match self.receive_event() {
|
||||
@@ -134,6 +149,9 @@ impl Server {
|
||||
if let Err(e) = Self::send_event(&self.socket, Event::Pong(), addr) {
|
||||
log::error!("udp send: {}", e);
|
||||
}
|
||||
// we release the mouse here,
|
||||
// since its very likely, that we wont get a release event
|
||||
self.producer.release();
|
||||
}
|
||||
(event, addr) => {
|
||||
match self.state {
|
||||
@@ -171,6 +189,7 @@ impl Server {
|
||||
|
||||
fn handle_producer_rx(&mut self) {
|
||||
let events = self.producer.read_events();
|
||||
let mut should_release = false;
|
||||
for (c, e) in events.into_iter() {
|
||||
// in receiving state, only release events
|
||||
// must be transmitted
|
||||
@@ -200,6 +219,11 @@ impl Server {
|
||||
continue
|
||||
}
|
||||
|
||||
// release mouse if client didnt respond to the first ping
|
||||
if last_ping.is_some() && last_ping.unwrap() < Duration::from_secs(1) {
|
||||
should_release = true;
|
||||
}
|
||||
|
||||
// last ping > 500ms ago -> ping all interfaces
|
||||
self.client_manager.reset_last_ping(c);
|
||||
if let Some(iter) = self.client_manager.get_addrs(c) {
|
||||
@@ -210,27 +234,43 @@ impl Server {
|
||||
log::error!("udp send: {}", e);
|
||||
}
|
||||
}
|
||||
// send additional release event, in case client is still in sending mode
|
||||
if let Err(e) = Self::send_event(&self.socket, Event::Release(), addr) {
|
||||
if e.kind() != ErrorKind::WouldBlock {
|
||||
log::error!("udp send: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO should repeat dns lookup
|
||||
}
|
||||
}
|
||||
|
||||
if should_release && self.state != State::Receiving {
|
||||
log::info!("client not responding - releasing pointer");
|
||||
self.producer.release();
|
||||
self.state = State::Receiving;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn handle_frontend_rx(&mut self) -> bool {
|
||||
loop {
|
||||
match self.frontend.read_event() {
|
||||
Ok(event) => match event {
|
||||
FrontendEvent::RequestPortChange(_) => todo!(),
|
||||
FrontendEvent::RequestClientAdd(addr, pos) => {
|
||||
self.add_client(HashSet::from_iter(&mut [addr].into_iter()), pos);
|
||||
FrontendEvent::AddClient(host, port, pos) => {
|
||||
if let Ok(ips) = dns::resolve(host.as_str()) {
|
||||
let addrs = ips.iter().map(|i| SocketAddr::new(*i, port));
|
||||
self.add_client(HashSet::from_iter(addrs), pos);
|
||||
}
|
||||
}
|
||||
FrontendEvent::RequestClientDelete(_) => todo!(),
|
||||
FrontendEvent::RequestClientUpdate(_) => todo!(),
|
||||
FrontendEvent::RequestShutdown() => {
|
||||
FrontendEvent::DelClient(host, port) => self.remove_client(host, port),
|
||||
FrontendEvent::Shutdown() => {
|
||||
log::info!("terminating gracefully...");
|
||||
return true;
|
||||
},
|
||||
FrontendEvent::ChangePort(_) => todo!(),
|
||||
FrontendEvent::AddIp(_, _) => todo!(),
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => return false,
|
||||
Err(e) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::io::{Read, Result};
|
||||
use std::{str, net::SocketAddr};
|
||||
use std::net::IpAddr;
|
||||
use std::str;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::{env, path::{Path, PathBuf}};
|
||||
@@ -24,11 +25,11 @@ pub mod gtk;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendEvent {
|
||||
RequestPortChange(u16),
|
||||
RequestClientAdd(SocketAddr, Position),
|
||||
RequestClientDelete(Client),
|
||||
RequestClientUpdate(Client),
|
||||
RequestShutdown(),
|
||||
ChangePort(u16),
|
||||
AddClient(String, u16, Position),
|
||||
DelClient(String, u16),
|
||||
AddIp(String, Option<IpAddr>),
|
||||
Shutdown(),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -78,6 +79,7 @@ impl FrontendAdapter {
|
||||
let json = str::from_utf8(&buf)
|
||||
.unwrap()
|
||||
.trim_end_matches(char::from(0)); // remove trailing 0-bytes
|
||||
log::debug!("{json}");
|
||||
let event = serde_json::from_str(json).unwrap();
|
||||
log::debug!("{:?}", event);
|
||||
Ok(event)
|
||||
@@ -117,5 +119,3 @@ impl Drop for FrontendAdapter {
|
||||
std::fs::remove_file(&self.socket_path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Frontend { }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use std::{thread, io::Write, net::SocketAddr};
|
||||
use std::{thread::{self, JoinHandle}, io::Write};
|
||||
#[cfg(windows)]
|
||||
use std::net::SocketAddrV4;
|
||||
|
||||
@@ -8,74 +8,88 @@ use std::{os::unix::net::UnixStream, path::Path, env};
|
||||
#[cfg(windows)]
|
||||
use std::net::TcpStream;
|
||||
|
||||
use crate::client::Position;
|
||||
use crate::{client::Position, config::DEFAULT_PORT};
|
||||
|
||||
use super::{FrontendEvent, Frontend};
|
||||
use super::FrontendEvent;
|
||||
|
||||
pub struct CliFrontend;
|
||||
|
||||
impl Frontend for CliFrontend {}
|
||||
|
||||
impl CliFrontend {
|
||||
pub fn new() -> Result<CliFrontend> {
|
||||
#[cfg(unix)]
|
||||
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
|
||||
thread::Builder::new()
|
||||
.name("cli-frontend".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
eprint!("lan-mouse > ");
|
||||
std::io::stderr().flush().unwrap();
|
||||
let mut buf = String::new();
|
||||
match std::io::stdin().read_line(&mut buf) {
|
||||
Ok(len) => {
|
||||
if let Some(event) = parse_event(buf, len) {
|
||||
#[cfg(unix)]
|
||||
let Ok(mut stream) = UnixStream::connect(&socket_path) else {
|
||||
log::error!("Could not connect to lan-mouse-socket");
|
||||
continue;
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let Ok(mut stream) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else {
|
||||
log::error!("Could not connect to lan-mouse-server");
|
||||
continue;
|
||||
};
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
if let Err(e) = stream.write(json.as_bytes()) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if event == FrontendEvent::RequestShutdown() {
|
||||
break;
|
||||
}
|
||||
pub fn start() -> Result<JoinHandle<()>> {
|
||||
#[cfg(unix)]
|
||||
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
|
||||
Ok(thread::Builder::new()
|
||||
.name("cli-frontend".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
eprint!("lan-mouse > ");
|
||||
std::io::stderr().flush().unwrap();
|
||||
let mut buf = String::new();
|
||||
match std::io::stdin().read_line(&mut buf) {
|
||||
Ok(len) => {
|
||||
if let Some(event) = parse_cmd(buf, len) {
|
||||
#[cfg(unix)]
|
||||
let Ok(mut stream) = UnixStream::connect(&socket_path) else {
|
||||
log::error!("Could not connect to lan-mouse-socket");
|
||||
continue;
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let Ok(mut stream) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else {
|
||||
log::error!("Could not connect to lan-mouse-server");
|
||||
continue;
|
||||
};
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
if let Err(e) = stream.write(json.as_bytes()) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if event == FrontendEvent::Shutdown() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
break
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
break
|
||||
}
|
||||
}
|
||||
}).unwrap();
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
})?)
|
||||
}
|
||||
|
||||
fn parse_event(s: String, len: usize) -> Option<FrontendEvent> {
|
||||
fn parse_cmd(s: String, len: usize) -> Option<FrontendEvent> {
|
||||
if len == 0 {
|
||||
return Some(FrontendEvent::RequestShutdown())
|
||||
return Some(FrontendEvent::Shutdown())
|
||||
}
|
||||
let mut l = s.split_whitespace();
|
||||
let cmd = l.next()?;
|
||||
match cmd {
|
||||
"connect" => {
|
||||
let addr = match l.next()?.parse() {
|
||||
Ok(addr) => SocketAddr::V4(addr),
|
||||
let host = l.next()?.to_owned();
|
||||
let pos = match l.next()? {
|
||||
"right" => Position::Right,
|
||||
"top" => Position::Top,
|
||||
"bottom" => Position::Bottom,
|
||||
_ => Position::Left,
|
||||
};
|
||||
let port = match l.next() {
|
||||
Some(p) => match p.parse() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None => DEFAULT_PORT,
|
||||
};
|
||||
Some(FrontendEvent::AddClient(host, port, pos))
|
||||
}
|
||||
"disconnect" => {
|
||||
let host = l.next()?.to_owned();
|
||||
let port = match l.next()?.parse() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::error!("parse error: {e}");
|
||||
log::error!("{e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(FrontendEvent::RequestClientAdd(addr, Position::Left ))
|
||||
Some(FrontendEvent::DelClient(host, port))
|
||||
}
|
||||
_ => {
|
||||
log::error!("unknown command: {s}");
|
||||
|
||||
93
src/frontend/gtk.rs
Normal file
93
src/frontend/gtk.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
mod window;
|
||||
mod client_object;
|
||||
mod client_row;
|
||||
|
||||
use std::{io::Result, thread::{self, JoinHandle}};
|
||||
|
||||
use crate::frontend::gtk::window::Window;
|
||||
|
||||
use gtk::{prelude::*, IconTheme, gdk::Display, gio::{SimpleAction, SimpleActionGroup}, glib::clone, CssProvider};
|
||||
use adw::Application;
|
||||
use gtk::{gio, glib, prelude::ApplicationExt};
|
||||
|
||||
use self::client_object::ClientObject;
|
||||
|
||||
pub fn start() -> Result<JoinHandle<glib::ExitCode>> {
|
||||
thread::Builder::new()
|
||||
.name("gtk-thread".into())
|
||||
.spawn(gtk_main)
|
||||
}
|
||||
|
||||
fn gtk_main() -> glib::ExitCode {
|
||||
gio::resources_register_include!("lan-mouse.gresource")
|
||||
.expect("Failed to register resources.");
|
||||
|
||||
let app = Application::builder()
|
||||
.application_id("de.feschber.lan-mouse")
|
||||
.build();
|
||||
|
||||
app.connect_startup(|_| load_icons());
|
||||
app.connect_startup(|_| load_css());
|
||||
app.connect_activate(build_ui);
|
||||
|
||||
app.run()
|
||||
}
|
||||
|
||||
fn load_css() {
|
||||
let provider = CssProvider::new();
|
||||
provider.load_from_resource("de/feschber/LanMouse/style.css");
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&Display::default().expect("Could not connect to a display."),
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
}
|
||||
|
||||
fn load_icons() {
|
||||
let icon_theme = IconTheme::for_display(&Display::default().expect("Could not connect to a display."));
|
||||
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
|
||||
}
|
||||
|
||||
fn build_ui(app: &Application) {
|
||||
let window = Window::new(app);
|
||||
let action_client_activate = SimpleAction::new(
|
||||
"activate-client",
|
||||
Some(&i32::static_variant_type()),
|
||||
);
|
||||
let action_client_delete = SimpleAction::new(
|
||||
"delete-client",
|
||||
Some(&i32::static_variant_type()),
|
||||
);
|
||||
action_client_activate.connect_activate(clone!(@weak window => move |_action, param| {
|
||||
log::debug!("activate-client");
|
||||
let index = param.unwrap()
|
||||
.get::<i32>()
|
||||
.unwrap();
|
||||
let Some(client) = window.clients().item(index as u32) else {
|
||||
return;
|
||||
};
|
||||
let client = client.downcast_ref::<ClientObject>().unwrap();
|
||||
window.update_client(client);
|
||||
}));
|
||||
action_client_delete.connect_activate(clone!(@weak window => move |_action, param| {
|
||||
log::debug!("delete-client");
|
||||
let index = param.unwrap()
|
||||
.get::<i32>()
|
||||
.unwrap();
|
||||
let Some(client) = window.clients().item(index as u32) else {
|
||||
return;
|
||||
};
|
||||
let client = client.downcast_ref::<ClientObject>().unwrap();
|
||||
window.update_client(client);
|
||||
window.clients().remove(index as u32);
|
||||
if window.clients().n_items() == 0 {
|
||||
window.set_placeholder_visible(true);
|
||||
}
|
||||
}));
|
||||
|
||||
let actions = SimpleActionGroup::new();
|
||||
window.insert_action_group("win", Some(&actions));
|
||||
actions.add_action(&action_client_activate);
|
||||
actions.add_action(&action_client_delete);
|
||||
window.present();
|
||||
}
|
||||
31
src/frontend/gtk/client_object.rs
Normal file
31
src/frontend/gtk/client_object.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
mod imp;
|
||||
|
||||
use gtk::glib::{self, Object};
|
||||
use adw::subclass::prelude::*;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
|
||||
}
|
||||
|
||||
impl ClientObject {
|
||||
pub fn new(hostname: String, port: u32, active: bool, position: String) -> Self {
|
||||
Object::builder()
|
||||
.property("hostname", hostname)
|
||||
.property("port", port)
|
||||
.property("active", active)
|
||||
.property("position", position)
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn get_data(&self) -> ClientData {
|
||||
self.imp().data.borrow().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ClientData {
|
||||
pub hostname: String,
|
||||
pub port: u32,
|
||||
pub active: bool,
|
||||
pub position: String,
|
||||
}
|
||||
27
src/frontend/gtk/client_object/imp.rs
Normal file
27
src/frontend/gtk/client_object/imp.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use glib::Properties;
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
|
||||
use super::ClientData;
|
||||
|
||||
#[derive(Properties, Default)]
|
||||
#[properties(wrapper_type = super::ClientObject)]
|
||||
pub struct ClientObject {
|
||||
#[property(name = "hostname", get, set, type = String, member = hostname)]
|
||||
#[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 = "position", get, set, type = String, member = position)]
|
||||
pub data: RefCell<ClientData>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ClientObject {
|
||||
const NAME: &'static str = "ClientObject";
|
||||
type Type = super::ClientObject;
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for ClientObject {}
|
||||
98
src/frontend/gtk/client_row.rs
Normal file
98
src/frontend/gtk/client_row.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
mod imp;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
use crate::config::DEFAULT_PORT;
|
||||
|
||||
use super::ClientObject;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ClientRow(ObjectSubclass<imp::ClientRow>)
|
||||
@extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ExpanderRow,
|
||||
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
|
||||
}
|
||||
|
||||
impl ClientRow {
|
||||
pub fn new(_client_object: &ClientObject) -> Self {
|
||||
Object::builder()
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn bind(&self, client_object: &ClientObject) {
|
||||
let mut bindings = self.imp().bindings.borrow_mut();
|
||||
|
||||
let active_binding = client_object
|
||||
.bind_property("active", &self.imp().enable_switch.get(), "state")
|
||||
.bidirectional()
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let hostname_binding = client_object
|
||||
.bind_property("hostname", &self.imp().hostname.get(), "text")
|
||||
.transform_from(|_, v: String| {
|
||||
if v == "" { Some("hostname".into()) } else { Some(v) }
|
||||
})
|
||||
.bidirectional()
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let title_binding = client_object
|
||||
.bind_property("hostname", self, "title")
|
||||
.build();
|
||||
|
||||
let port_binding = client_object
|
||||
.bind_property("port", &self.imp().port.get(), "text")
|
||||
.transform_from(|_, v: String| {
|
||||
if v == "" {
|
||||
Some(4242)
|
||||
} else {
|
||||
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
|
||||
}
|
||||
})
|
||||
.bidirectional()
|
||||
.build();
|
||||
|
||||
let subtitle_binding = client_object
|
||||
.bind_property("port", self, "subtitle")
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
|
||||
let position_binding = client_object
|
||||
.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() {
|
||||
"right" => Some(1),
|
||||
"top" => Some(2u32),
|
||||
"bottom" => Some(3u32),
|
||||
_ => Some(0u32),
|
||||
}
|
||||
})
|
||||
.bidirectional()
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
bindings.push(active_binding);
|
||||
bindings.push(hostname_binding);
|
||||
bindings.push(title_binding);
|
||||
bindings.push(port_binding);
|
||||
bindings.push(subtitle_binding);
|
||||
bindings.push(position_binding);
|
||||
}
|
||||
|
||||
pub fn unbind(&self) {
|
||||
for binding in self.imp().bindings.borrow_mut().drain(..) {
|
||||
binding.unbind();
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/frontend/gtk/client_row/imp.rs
Normal file
76
src/frontend/gtk/client_row/imp.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use glib::{Binding, subclass::InitializingObject};
|
||||
use adw::{prelude::*, ComboRow, ActionRow};
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::clone;
|
||||
use gtk::{glib, CompositeTemplate, Switch, Button};
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/de/feschber/LanMouse/client_row.ui")]
|
||||
pub struct ClientRow {
|
||||
#[template_child]
|
||||
pub enable_switch: TemplateChild<gtk::Switch>,
|
||||
#[template_child]
|
||||
pub hostname: TemplateChild<gtk::Entry>,
|
||||
#[template_child]
|
||||
pub port: TemplateChild<gtk::Entry>,
|
||||
#[template_child]
|
||||
pub position: TemplateChild<ComboRow>,
|
||||
#[template_child]
|
||||
pub delete_row: TemplateChild<ActionRow>,
|
||||
#[template_child]
|
||||
pub delete_button: TemplateChild<gtk::Button>,
|
||||
pub bindings: RefCell<Vec<Binding>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ClientRow {
|
||||
// `NAME` needs to match `class` attribute of template
|
||||
const NAME: &'static str = "ClientRow";
|
||||
type Type = super::ClientRow;
|
||||
type ParentType = adw::ExpanderRow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
klass.bind_template_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for ClientRow {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
self.delete_button.connect_clicked(clone!(@weak self as row => move |button| {
|
||||
row.handle_client_delete(button);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl ClientRow {
|
||||
#[template_callback]
|
||||
fn handle_client_set_state(&self, state: bool, switch: &Switch) -> bool {
|
||||
let idx = self.obj().index();
|
||||
switch.activate_action("win.activate-client", Some(&idx.to_variant())).unwrap();
|
||||
switch.set_state(state);
|
||||
|
||||
true // dont run default handler
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_client_delete(&self, button: &Button) {
|
||||
log::debug!("delete button pressed");
|
||||
let idx = self.obj().index();
|
||||
button.activate_action("win.delete-client", Some(&idx.to_variant())).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for ClientRow {}
|
||||
impl BoxImpl for ClientRow {}
|
||||
impl ListBoxRowImpl for ClientRow {}
|
||||
impl PreferencesRowImpl for ClientRow {}
|
||||
impl ExpanderRowImpl for ClientRow {}
|
||||
127
src/frontend/gtk/window.rs
Normal file
127
src/frontend/gtk/window.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
mod imp;
|
||||
|
||||
use std::{path::{Path, PathBuf}, env, process, os::unix::net::UnixStream, io::Write};
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{glib, gio, NoSelection};
|
||||
use glib::{clone, Object};
|
||||
|
||||
use crate::{frontend::{gtk::client_object::ClientObject, FrontendEvent}, config::DEFAULT_PORT, client::Position};
|
||||
|
||||
use super::client_row::ClientRow;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Window(ObjectSubclass<imp::Window>)
|
||||
@extends adw::ApplicationWindow, gtk::Window, gtk::Widget,
|
||||
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
|
||||
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new(app: &adw::Application) -> Self {
|
||||
Object::builder().property("application", app).build()
|
||||
}
|
||||
|
||||
pub fn clients(&self) -> gio::ListStore {
|
||||
self.imp()
|
||||
.clients
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("Could not get clients")
|
||||
}
|
||||
|
||||
fn setup_clients(&self) {
|
||||
let model = gio::ListStore::new::<ClientObject>();
|
||||
self.imp().clients.replace(Some(model));
|
||||
|
||||
let selection_model = NoSelection::new(Some(self.clients()));
|
||||
self.imp().client_list.bind_model(
|
||||
Some(&selection_model),
|
||||
clone!(@weak self as window => @default-panic, move |obj| {
|
||||
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
|
||||
let row = window.create_client_row(client_object);
|
||||
row.upcast()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// workaround for a bug in libadwaita that shows an ugly line beneath
|
||||
/// the last element if a placeholder is set.
|
||||
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308
|
||||
pub fn set_placeholder_visible(&self, visible: bool) {
|
||||
let placeholder = self.imp().client_placeholder.get();
|
||||
self.imp().client_list.set_placeholder(match visible {
|
||||
true => Some(&placeholder),
|
||||
false => None,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_icon(&self) {
|
||||
self.set_icon_name(Some("mouse-icon"));
|
||||
}
|
||||
|
||||
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
|
||||
let row = ClientRow::new(client_object);
|
||||
row.bind(client_object);
|
||||
row
|
||||
}
|
||||
|
||||
fn new_client(&self) {
|
||||
let client = ClientObject::new(String::from(""), DEFAULT_PORT as u32, false, "left".into());
|
||||
self.clients().append(&client);
|
||||
}
|
||||
|
||||
pub fn update_client(&self, client: &ClientObject) {
|
||||
let data = client.get_data();
|
||||
let socket_path = self.imp().socket_path.borrow();
|
||||
let socket_path = socket_path.as_ref().unwrap().as_path();
|
||||
let host_name = data.hostname;
|
||||
let position = match data.position.as_str() {
|
||||
"left" => Position::Left,
|
||||
"right" => Position::Right,
|
||||
"top" => Position::Top,
|
||||
"bottom" => Position::Bottom,
|
||||
_ => {
|
||||
log::error!("invalid position: {}", data.position);
|
||||
return
|
||||
}
|
||||
};
|
||||
let port = data.port;
|
||||
let event = if client.active() {
|
||||
FrontendEvent::DelClient(host_name, port as u16)
|
||||
} else {
|
||||
FrontendEvent::AddClient(host_name, port as u16, position)
|
||||
};
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
let Ok(mut stream) = UnixStream::connect(socket_path) else {
|
||||
log::error!("Could not connect to lan-mouse-socket @ {socket_path:?}");
|
||||
return;
|
||||
};
|
||||
if let Err(e) = stream.write(json.as_bytes()) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
}
|
||||
|
||||
fn setup_callbacks(&self) {
|
||||
self.imp()
|
||||
.add_client_button
|
||||
.connect_clicked(clone!(@weak self as window => move |_| {
|
||||
window.new_client();
|
||||
window.set_placeholder_visible(false);
|
||||
}));
|
||||
}
|
||||
|
||||
fn connect_stream(&self) {
|
||||
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let socket_path = Path::new(xdg_runtime_dir.as_str())
|
||||
.join("lan-mouse-socket.sock");
|
||||
self.imp().socket_path.borrow_mut().replace(PathBuf::from(socket_path));
|
||||
}
|
||||
}
|
||||
63
src/frontend/gtk/window/imp.rs
Normal file
63
src/frontend/gtk/window/imp.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use std::{cell::{Cell, RefCell}, path::PathBuf};
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
use adw::{prelude::*, ActionRow};
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{glib, Button, CompositeTemplate, ListBox, gio};
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/de/feschber/LanMouse/window.ui")]
|
||||
pub struct Window {
|
||||
pub number: Cell<i32>,
|
||||
#[template_child]
|
||||
pub add_client_button: TemplateChild<Button>,
|
||||
#[template_child]
|
||||
pub client_list: TemplateChild<ListBox>,
|
||||
#[template_child]
|
||||
pub client_placeholder: TemplateChild<ActionRow>,
|
||||
pub clients: RefCell<Option<gio::ListStore>>,
|
||||
pub socket_path: RefCell<Option<PathBuf>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Window {
|
||||
// `NAME` needs to match `class` attribute of template
|
||||
const NAME: &'static str = "LanMouseWindow";
|
||||
type Type = super::Window;
|
||||
type ParentType = gtk::ApplicationWindow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
klass.bind_template_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl Window {
|
||||
#[template_callback]
|
||||
fn handle_button_clicked(&self, button: &Button) {
|
||||
let number_increased = self.number.get() + 1;
|
||||
self.number.set(number_increased);
|
||||
button.set_label(&number_increased.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ObjectImpl for Window {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
obj.setup_icon();
|
||||
obj.setup_clients();
|
||||
obj.setup_callbacks();
|
||||
obj.connect_stream();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for Window {}
|
||||
impl WindowImpl for Window {}
|
||||
impl ApplicationWindowImpl for Window {}
|
||||
51
src/main.rs
51
src/main.rs
@@ -3,10 +3,13 @@ use std::{process, error::Error};
|
||||
use env_logger::Env;
|
||||
use lan_mouse::{
|
||||
consumer, producer,
|
||||
config::{Config, Frontend::{Gtk, Cli}}, event::server::Server,
|
||||
frontend::{FrontendAdapter, cli::CliFrontend},
|
||||
config::{Config, Frontend::{Cli, Gtk}}, event::server::Server,
|
||||
frontend::{FrontendAdapter, cli},
|
||||
};
|
||||
|
||||
#[cfg(all(unix, feature = "gtk"))]
|
||||
use lan_mouse::frontend::gtk;
|
||||
|
||||
pub fn main() {
|
||||
|
||||
// init logging
|
||||
@@ -33,30 +36,34 @@ pub fn run() -> Result<(), Box<dyn Error>> {
|
||||
// start sending and receiving events
|
||||
let mut event_server = Server::new(config.port, producer, consumer, frontend_adapter)?;
|
||||
|
||||
// add clients form config
|
||||
config.get_clients().into_iter().for_each(|(c, h, p)| {
|
||||
let host_name = match h {
|
||||
Some(h) => format!(" '{}'", h),
|
||||
None => "".to_owned(),
|
||||
};
|
||||
if c.len() == 0 {
|
||||
log::warn!("ignoring client{} with 0 assigned ips!", host_name);
|
||||
}
|
||||
log::info!("adding client [{}]{} @ {:?}", p, host_name, c);
|
||||
event_server.add_client(c, p);
|
||||
});
|
||||
|
||||
// any threads need to be started after event_server sets up signal handling
|
||||
match config.frontend {
|
||||
Gtk => {
|
||||
#[cfg(all(unix, feature = "gtk"))]
|
||||
frontend::gtk::create();
|
||||
#[cfg(not(feature = "gtk"))]
|
||||
panic!("gtk frontend requested but feature not enabled!");
|
||||
},
|
||||
Cli => Box::new(CliFrontend::new()?),
|
||||
#[cfg(all(unix, feature = "gtk"))]
|
||||
Gtk => { gtk::start()?; }
|
||||
#[cfg(any(not(feature = "gtk"), not(unix)))]
|
||||
Gtk => panic!("gtk frontend requested but feature not enabled!"),
|
||||
Cli => { cli::start()?; }
|
||||
};
|
||||
|
||||
// this currently causes issues, because the clients from
|
||||
// the config arent communicated to gtk yet.
|
||||
if config.frontend == Gtk {
|
||||
log::warn!("clients defined in config currently have no effect with the gtk frontend");
|
||||
} else {
|
||||
// add clients from config
|
||||
config.get_clients().into_iter().for_each(|(c, h, p)| {
|
||||
let host_name = match h {
|
||||
Some(h) => format!(" '{}'", h),
|
||||
None => "".to_owned(),
|
||||
};
|
||||
if c.len() == 0 {
|
||||
log::warn!("ignoring client{} with 0 assigned ips!", host_name);
|
||||
}
|
||||
log::info!("adding client [{}]{} @ {:?}", p, host_name, c);
|
||||
event_server.add_client(c, p);
|
||||
});
|
||||
}
|
||||
|
||||
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
|
||||
// run event loop
|
||||
event_server.run()?;
|
||||
|
||||
Reference in New Issue
Block a user