changeset 18:3f7b7a3ad8fe

Build three binaries instead of using arguments.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Thu, 26 Aug 2021 22:15:55 +0200
parents 0bce7fe96937
children ba09079686a0
files Cargo.toml src/bin/client.rs src/bin/server.rs src/bin/standalone.rs src/client.rs src/gtk.rs src/lib.rs src/main.rs src/protocol.rs src/server.rs
diffstat 10 files changed, 594 insertions(+), 607 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.toml	Thu Aug 26 22:00:55 2021 +0200
+++ b/Cargo.toml	Thu Aug 26 22:15:55 2021 +0200
@@ -24,3 +24,15 @@
 
 [profile.release]
 lto = true
+
+[[bin]]
+name = "tablet-emu"
+path = "src/bin/standalone.rs"
+
+[[bin]]
+name = "tablet-emud"
+path = "src/bin/server.rs"
+
+[[bin]]
+name = "tablet-emu-remote"
+path = "src/bin/client.rs"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/bin/client.rs	Thu Aug 26 22:15:55 2021 +0200
@@ -0,0 +1,212 @@
+// Tablet emulator, for people who don’t own one
+// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use gio::prelude::*;
+use glib::clone;
+use gtk::prelude::*;
+use input_linux::Key;
+use std::net::UdpSocket;
+use std::sync::{Arc, Mutex};
+use tablet_emu::protocol::{Buttons, Event};
+use tablet_emu::{state::FakeState, DEFAULT_HEIGHT, DEFAULT_WIDTH};
+
+fn build_main_menu(app: &gtk::Application) {
+    let quit = gio::SimpleAction::new("quit", None);
+    app.set_accels_for_action("app.quit", &["<Control>q"]);
+    app.add_action(&quit);
+    quit.connect_activate(clone!(@weak app => move |_, _| app.quit()));
+
+    let about = gio::SimpleAction::new("about", None);
+    app.add_action(&about);
+    about.connect_activate(|_, _| {
+        let about = gtk::AboutDialog::builder()
+            .program_name("TabletEmu")
+            .logo_icon_name("input-tablet")
+            .website("https://hg.linkmauve.fr/tablet-emu")
+            .version("0.1")
+            .license_type(gtk::License::Agpl30)
+            .copyright("© 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>")
+            .build();
+        //about.run();
+        about.destroy();
+    });
+
+    let menu = gio::Menu::new();
+    {
+        let file = gio::Menu::new();
+        file.append(Some("_Quit"), Some("app.quit"));
+        menu.append_submenu(Some("_File"), &file);
+    }
+    {
+        let help = gio::Menu::new();
+        help.append(Some("_About"), Some("app.about"));
+        menu.append_submenu(Some("_Help"), &help);
+    }
+    app.set_menubar(Some(&menu));
+}
+
+fn build_ui(app: &gtk::Application) {
+    build_main_menu(app);
+
+    let address = "127.0.0.1:16150";
+    let socket = Arc::new(Mutex::new(UdpSocket::bind("127.0.0.1:4321").unwrap()));
+    println!("opened {:?}", socket);
+
+    let send = move |event: &Event| {
+        // TODO: Yolo-alignment.
+        let buf: &[u8; std::mem::size_of::<Event>()] = unsafe { std::mem::transmute(event) };
+        let socket = socket.lock().unwrap();
+        let amount = socket.send_to(buf, address).unwrap();
+        assert_eq!(amount, std::mem::size_of::<Event>());
+    };
+    let send2 = send.clone();
+    let send3 = send.clone();
+    let send4 = send.clone();
+
+    let state = Arc::new(Mutex::new(FakeState::new()));
+
+    let hbox = gtk::Box::builder()
+        .orientation(gtk::Orientation::Horizontal)
+        .build();
+    let tools_box = gtk::Box::builder()
+        .orientation(gtk::Orientation::Vertical)
+        .build();
+
+    macro_rules! impl_tool {
+        ($tool:tt) => {
+            let tool = gtk::Button::with_mnemonic($tool);
+            let state_weak = Arc::downgrade(&state);
+            let send = send.clone();
+            tool.connect_clicked(move |b| {
+                let state = state_weak.upgrade().unwrap();
+                let mut state = state.lock().unwrap();
+                let tool = match b.label().unwrap().as_str() {
+                    "_Pen" => Key::ButtonToolPen,
+                    "_Rubber" => Key::ButtonToolRubber,
+                    "_Brush" => Key::ButtonToolBrush,
+                    "P_encil" => Key::ButtonToolPencil,
+                    "_Airbrush" => Key::ButtonToolAirbrush,
+                    _ => unreachable!(),
+                };
+                state.select_tool(tool);
+
+                let mut event: Event = Default::default();
+                event.buttons = match tool {
+                    Key::ButtonToolPen => Buttons::A,
+                    Key::ButtonToolRubber => Buttons::B,
+                    Key::ButtonToolBrush => Buttons::X,
+                    Key::ButtonToolPencil => Buttons::Y,
+                    Key::ButtonToolAirbrush => Buttons::SELECT,
+                    _ => unreachable!(),
+                };
+                send(&event);
+            });
+            tools_box.append(&tool);
+        };
+    }
+    impl_tool!("_Pen");
+    impl_tool!("_Rubber");
+    impl_tool!("_Brush");
+    impl_tool!("P_encil");
+    impl_tool!("_Airbrush");
+
+    let drawing_area = gtk::DrawingArea::builder()
+        .content_width(DEFAULT_WIDTH)
+        .content_height(DEFAULT_HEIGHT)
+        .hexpand(true)
+        .build();
+    let gesture_click = gtk::GestureClick::new();
+    let event_controller = gtk::EventControllerMotion::new();
+    let state_weak = Arc::downgrade(&state);
+    drawing_area.connect_resize(move |_, width, height| {
+        let state = state_weak.upgrade().unwrap();
+        let mut state = state.lock().unwrap();
+        state.set_size(width, height);
+
+        let mut event: Event = Default::default();
+        event.buttons = Buttons::RESIZE;
+        event.touch = (width as u16, height as u16);
+        send(&event);
+    });
+    let state_weak = Arc::downgrade(&state);
+    gesture_click.connect_pressed(move |_, n_press, x, y| {
+        if n_press != 1 {
+            return;
+        }
+
+        let state = state_weak.upgrade().unwrap();
+        let mut state = state.lock().unwrap();
+        state.press(x, y).unwrap();
+
+        let mut event: Event = Default::default();
+        event.buttons = Buttons::TOUCH;
+        event.touch = (x as u16, y as u16);
+        send2(&event);
+    });
+    let state_weak = Arc::downgrade(&state);
+    gesture_click.connect_released(move |_, n_press, x, y| {
+        if n_press != 1 {
+            return;
+        }
+
+        let state = state_weak.upgrade().unwrap();
+        let mut state = state.lock().unwrap();
+        state.release(x, y).unwrap();
+
+        let mut event: Event = Default::default();
+        event.touch = (x as u16, y as u16);
+        send3(&event);
+    });
+    event_controller.connect_motion(move |_, x, y| {
+        let mut state = state.lock().unwrap();
+        state.motion(x, y).unwrap();
+
+        if state.is_pressed() {
+            let mut event: Event = Default::default();
+            event.buttons = Buttons::TOUCH;
+            event.touch = (x as u16, y as u16);
+            send4(&event);
+        }
+    });
+    drawing_area.add_controller(&gesture_click);
+    drawing_area.add_controller(&event_controller);
+    drawing_area.set_draw_func(move |_, ctx, _, _| {
+        ctx.set_source_rgb(1., 0., 0.);
+        ctx.set_operator(cairo::Operator::Screen);
+        ctx.paint().unwrap();
+    });
+
+    hbox.append(&tools_box);
+    hbox.append(&drawing_area);
+
+    let window = gtk::ApplicationWindow::builder()
+        .application(app)
+        .title("tablet-emu")
+        .default_width(800)
+        .default_height(480)
+        .child(&hbox)
+        .build();
+
+    window.show();
+}
+
+pub fn main() {
+    let app = gtk::Application::builder()
+        .application_id("fr.linkmauve.TabletEmu")
+        .build();
+    app.connect_activate(build_ui);
+    app.run();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/bin/server.rs	Thu Aug 26 22:15:55 2021 +0200
@@ -0,0 +1,102 @@
+// Tablet emulator, for people who don’t own one
+// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use input_linux::Key;
+use std::io::{self, ErrorKind};
+use std::net::UdpSocket;
+use tablet_emu::protocol::{Buttons, Event};
+use tablet_emu::state::State;
+
+pub fn run_server(address: &str) -> io::Result<()> {
+    let mut state = match State::new() {
+        Ok(state) => state,
+        Err(err) => {
+            match err.kind() {
+                ErrorKind::NotFound => {
+                    eprintln!("Couldn’t find /dev/uinput: {}", err);
+                    eprintln!("Maybe you forgot to `modprobe uinput`?");
+                }
+                ErrorKind::PermissionDenied => {
+                    eprintln!("Couldn’t open /dev/uinput for writing: {}", err);
+                    eprintln!("Maybe you aren’t allowed to create input devices?");
+                }
+                _ => eprintln!("Couldn’t open /dev/uinput for writing: {}", err),
+            }
+            std::process::exit(1);
+        }
+    };
+
+    let socket = UdpSocket::bind(address)?;
+    println!("Listening on {:?}", socket);
+    println!("Here is an example client: https://hg.linkmauve.fr/remote-gamepad");
+
+    let mut event: Event = Default::default();
+    let mut last = Some((0., 0.));
+    state.set_size(320, 240);
+    loop {
+        // TODO: Yolo-alignment.
+        let buf: &mut [u8; 16] = unsafe { std::mem::transmute(&mut event) };
+        let (amount, source) = socket.recv_from(buf)?;
+        if amount != std::mem::size_of::<Event>() {
+            eprintln!("Invalid data length: {}", amount);
+            continue;
+        }
+        println!("{:?} from {:?}", event, source);
+        if event.buttons.contains(Buttons::A) {
+            state.select_tool(Key::ButtonToolPen);
+        } else if event.buttons.contains(Buttons::B) {
+            state.select_tool(Key::ButtonToolRubber);
+        } else if event.buttons.contains(Buttons::X) {
+            state.select_tool(Key::ButtonToolBrush);
+        } else if event.buttons.contains(Buttons::Y) {
+            state.select_tool(Key::ButtonToolPencil);
+        } else if event.buttons.contains(Buttons::SELECT) {
+            state.select_tool(Key::ButtonToolAirbrush);
+        } else if event.buttons.contains(Buttons::RESIZE) {
+            println!(
+                "set_size({}, {})",
+                event.touch.0 as i32, event.touch.1 as i32
+            );
+            state.set_size(event.touch.0 as i32, event.touch.1 as i32);
+        }
+        let (x, y) = event.touch;
+        if event.buttons.contains(Buttons::TOUCH) {
+            if let None = last {
+                state.press(x as f64, y as f64)?;
+                last = Some((x as f64, y as f64));
+                continue;
+            }
+        } else {
+            if let Some((x, y)) = last {
+                state.release(x, y)?;
+                last = None;
+                continue;
+            }
+        }
+        state.motion(x as f64, y as f64)?;
+        last = Some((x as f64, y as f64));
+    }
+}
+
+pub fn main() {
+    let args: Vec<_> = std::env::args().collect();
+    let address = if args.len() > 1 {
+        args[1].clone()
+    } else {
+        String::from("0.0.0.0:16150")
+    };
+    run_server(&address).unwrap();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/bin/standalone.rs	Thu Aug 26 22:15:55 2021 +0200
@@ -0,0 +1,179 @@
+// Tablet emulator, for people who don’t own one
+// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use gio::prelude::*;
+use glib::clone;
+use gtk::prelude::*;
+use input_linux::Key;
+use std::io::ErrorKind;
+use std::sync::{Arc, Mutex};
+use tablet_emu::{state::State, DEFAULT_HEIGHT, DEFAULT_WIDTH};
+
+fn build_main_menu(app: &gtk::Application) {
+    let quit = gio::SimpleAction::new("quit", None);
+    app.set_accels_for_action("app.quit", &["<Control>q"]);
+    app.add_action(&quit);
+    quit.connect_activate(clone!(@weak app => move |_, _| app.quit()));
+
+    let about = gio::SimpleAction::new("about", None);
+    app.add_action(&about);
+    about.connect_activate(|_, _| {
+        let about = gtk::AboutDialog::builder()
+            .program_name("TabletEmu")
+            .logo_icon_name("input-tablet")
+            .website("https://hg.linkmauve.fr/tablet-emu")
+            .version("0.1")
+            .license_type(gtk::License::Agpl30)
+            .copyright("© 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>")
+            .build();
+        //about.run();
+        about.destroy();
+    });
+
+    let menu = gio::Menu::new();
+    {
+        let file = gio::Menu::new();
+        file.append(Some("_Quit"), Some("app.quit"));
+        menu.append_submenu(Some("_File"), &file);
+    }
+    {
+        let help = gio::Menu::new();
+        help.append(Some("_About"), Some("app.about"));
+        menu.append_submenu(Some("_Help"), &help);
+    }
+    app.set_menubar(Some(&menu));
+}
+
+fn build_ui(app: &gtk::Application) {
+    build_main_menu(app);
+
+    let state = match State::new() {
+        Ok(state) => Arc::new(Mutex::new(state)),
+        Err(err) => {
+            match err.kind() {
+                ErrorKind::NotFound => {
+                    eprintln!("Couldn’t find /dev/uinput: {}", err);
+                    eprintln!("Maybe you forgot to `modprobe uinput`?");
+                }
+                ErrorKind::PermissionDenied => {
+                    eprintln!("Couldn’t open /dev/uinput for writing: {}", err);
+                    eprintln!("Maybe you aren’t allowed to create input devices?");
+                }
+                _ => eprintln!("Couldn’t open /dev/uinput for writing: {}", err),
+            }
+            std::process::exit(1);
+        }
+    };
+
+    let hbox = gtk::Box::builder()
+        .orientation(gtk::Orientation::Horizontal)
+        .build();
+    let tools_box = gtk::Box::builder()
+        .orientation(gtk::Orientation::Vertical)
+        .build();
+
+    macro_rules! impl_tool {
+        ($tool:tt) => {
+            let tool = gtk::Button::with_mnemonic($tool);
+            let state_weak = Arc::downgrade(&state);
+            tool.connect_clicked(move |b| {
+                let state = state_weak.upgrade().unwrap();
+                let mut state = state.lock().unwrap();
+                let tool = match b.label().unwrap().as_str() {
+                    "_Pen" => Key::ButtonToolPen,
+                    "_Rubber" => Key::ButtonToolRubber,
+                    "_Brush" => Key::ButtonToolBrush,
+                    "P_encil" => Key::ButtonToolPencil,
+                    "_Airbrush" => Key::ButtonToolAirbrush,
+                    _ => unreachable!(),
+                };
+                state.select_tool(tool);
+            });
+            tools_box.append(&tool);
+        };
+    }
+    impl_tool!("_Pen");
+    impl_tool!("_Rubber");
+    impl_tool!("_Brush");
+    impl_tool!("P_encil");
+    impl_tool!("_Airbrush");
+
+    let drawing_area = gtk::DrawingArea::builder()
+        .content_width(DEFAULT_WIDTH)
+        .content_height(DEFAULT_HEIGHT)
+        .hexpand(true)
+        .build();
+    let gesture_click = gtk::GestureClick::new();
+    let event_controller = gtk::EventControllerMotion::new();
+    let state_weak = Arc::downgrade(&state);
+    drawing_area.connect_resize(move |_, width, height| {
+        let state = state_weak.upgrade().unwrap();
+        let mut state = state.lock().unwrap();
+        state.set_size(width, height);
+    });
+    let state_weak = Arc::downgrade(&state);
+    gesture_click.connect_pressed(move |_, n_press, x, y| {
+        if n_press != 1 {
+            return;
+        }
+
+        let state = state_weak.upgrade().unwrap();
+        let mut state = state.lock().unwrap();
+        state.press(x, y).unwrap();
+    });
+    let state_weak = Arc::downgrade(&state);
+    gesture_click.connect_released(move |_, n_press, x, y| {
+        if n_press != 1 {
+            return;
+        }
+
+        let state = state_weak.upgrade().unwrap();
+        let mut state = state.lock().unwrap();
+        state.release(x, y).unwrap();
+    });
+    event_controller.connect_motion(move |_, x, y| {
+        let mut state = state.lock().unwrap();
+        state.motion(x, y).unwrap();
+    });
+    drawing_area.add_controller(&gesture_click);
+    drawing_area.add_controller(&event_controller);
+    drawing_area.set_draw_func(move |_, ctx, _, _| {
+        ctx.set_source_rgb(1., 0., 0.);
+        ctx.set_operator(cairo::Operator::Screen);
+        ctx.paint().unwrap();
+    });
+
+    hbox.append(&tools_box);
+    hbox.append(&drawing_area);
+
+    let window = gtk::ApplicationWindow::builder()
+        .application(app)
+        .title("tablet-emu")
+        .default_width(800)
+        .default_height(480)
+        .child(&hbox)
+        .build();
+
+    window.show();
+}
+
+pub fn main() {
+    let app = gtk::Application::builder()
+        .application_id("fr.linkmauve.TabletEmu")
+        .build();
+    app.connect_activate(build_ui);
+    app.run();
+}
--- a/src/client.rs	Thu Aug 26 22:00:55 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,213 +0,0 @@
-// Tablet emulator, for people who don’t own one
-// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-use crate::{state::FakeState, DEFAULT_HEIGHT, DEFAULT_WIDTH};
-use gio::prelude::*;
-use glib::clone;
-use gtk::prelude::*;
-use input_linux::Key;
-use std::sync::{Arc, Mutex};
-use std::net::UdpSocket;
-use crate::Event;
-use crate::server::Buttons;
-
-fn build_main_menu(app: &gtk::Application) {
-    let quit = gio::SimpleAction::new("quit", None);
-    app.set_accels_for_action("app.quit", &["<Control>q"]);
-    app.add_action(&quit);
-    quit.connect_activate(clone!(@weak app => move |_, _| app.quit()));
-
-    let about = gio::SimpleAction::new("about", None);
-    app.add_action(&about);
-    about.connect_activate(|_, _| {
-        let about = gtk::AboutDialog::builder()
-            .program_name("TabletEmu")
-            .logo_icon_name("input-tablet")
-            .website("https://hg.linkmauve.fr/tablet-emu")
-            .version("0.1")
-            .license_type(gtk::License::Agpl30)
-            .copyright("© 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>")
-            .build();
-        //about.run();
-        about.destroy();
-    });
-
-    let menu = gio::Menu::new();
-    {
-        let file = gio::Menu::new();
-        file.append(Some("_Quit"), Some("app.quit"));
-        menu.append_submenu(Some("_File"), &file);
-    }
-    {
-        let help = gio::Menu::new();
-        help.append(Some("_About"), Some("app.about"));
-        menu.append_submenu(Some("_Help"), &help);
-    }
-    app.set_menubar(Some(&menu));
-}
-
-fn build_ui(app: &gtk::Application) {
-    build_main_menu(app);
-
-    let address = "127.0.0.1:16150";
-    let socket = Arc::new(Mutex::new(UdpSocket::bind("127.0.0.1:4321").unwrap()));
-    println!("opened {:?}", socket);
-
-    let send = move |event: &Event| {
-        // TODO: Yolo-alignment.
-        let buf: &[u8; std::mem::size_of::<Event>()] = unsafe { std::mem::transmute(event) };
-        let socket = socket.lock().unwrap();
-        let amount = socket.send_to(buf, address).unwrap();
-        assert_eq!(amount, std::mem::size_of::<Event>());
-    };
-    let send2 = send.clone();
-    let send3 = send.clone();
-    let send4 = send.clone();
-
-    let state = Arc::new(Mutex::new(FakeState::new()));
-
-    let hbox = gtk::Box::builder()
-        .orientation(gtk::Orientation::Horizontal)
-        .build();
-    let tools_box = gtk::Box::builder()
-        .orientation(gtk::Orientation::Vertical)
-        .build();
-
-    macro_rules! impl_tool {
-        ($tool:tt) => {
-            let tool = gtk::Button::with_mnemonic($tool);
-            let state_weak = Arc::downgrade(&state);
-            let send = send.clone();
-            tool.connect_clicked(move |b| {
-                let state = state_weak.upgrade().unwrap();
-                let mut state = state.lock().unwrap();
-                let tool = match b.label().unwrap().as_str() {
-                    "_Pen" => Key::ButtonToolPen,
-                    "_Rubber" => Key::ButtonToolRubber,
-                    "_Brush" => Key::ButtonToolBrush,
-                    "P_encil" => Key::ButtonToolPencil,
-                    "_Airbrush" => Key::ButtonToolAirbrush,
-                    _ => unreachable!(),
-                };
-                state.select_tool(tool);
-
-                let mut event: Event = Default::default();
-                event.buttons = match tool {
-                    Key::ButtonToolPen => Buttons::A,
-                    Key::ButtonToolRubber => Buttons::B,
-                    Key::ButtonToolBrush => Buttons::X,
-                    Key::ButtonToolPencil => Buttons::Y,
-                    Key::ButtonToolAirbrush => Buttons::SELECT,
-                    _ => unreachable!(),
-                };
-                send(&event);
-            });
-            tools_box.append(&tool);
-        };
-    }
-    impl_tool!("_Pen");
-    impl_tool!("_Rubber");
-    impl_tool!("_Brush");
-    impl_tool!("P_encil");
-    impl_tool!("_Airbrush");
-
-    let drawing_area = gtk::DrawingArea::builder()
-        .content_width(DEFAULT_WIDTH)
-        .content_height(DEFAULT_HEIGHT)
-        .hexpand(true)
-        .build();
-    let gesture_click = gtk::GestureClick::new();
-    let event_controller = gtk::EventControllerMotion::new();
-    let state_weak = Arc::downgrade(&state);
-    drawing_area.connect_resize(move |_, width, height| {
-        let state = state_weak.upgrade().unwrap();
-        let mut state = state.lock().unwrap();
-        state.set_size(width, height);
-
-        let mut event: Event = Default::default();
-        event.buttons = Buttons::RESIZE;
-        event.touch = (width as u16, height as u16);
-        send(&event);
-    });
-    let state_weak = Arc::downgrade(&state);
-    gesture_click.connect_pressed(move |_, n_press, x, y| {
-        if n_press != 1 {
-            return;
-        }
-
-        let state = state_weak.upgrade().unwrap();
-        let mut state = state.lock().unwrap();
-        state.press(x, y).unwrap();
-
-        let mut event: Event = Default::default();
-        event.buttons = Buttons::TOUCH;
-        event.touch = (x as u16, y as u16);
-        send2(&event);
-    });
-    let state_weak = Arc::downgrade(&state);
-    gesture_click.connect_released(move |_, n_press, x, y| {
-        if n_press != 1 {
-            return;
-        }
-
-        let state = state_weak.upgrade().unwrap();
-        let mut state = state.lock().unwrap();
-        state.release(x, y).unwrap();
-
-        let mut event: Event = Default::default();
-        event.touch = (x as u16, y as u16);
-        send3(&event);
-    });
-    event_controller.connect_motion(move |_, x, y| {
-        let mut state = state.lock().unwrap();
-        state.motion(x, y).unwrap();
-
-        if state.is_pressed() {
-            let mut event: Event = Default::default();
-            event.buttons = Buttons::TOUCH;
-            event.touch = (x as u16, y as u16);
-            send4(&event);
-        }
-    });
-    drawing_area.add_controller(&gesture_click);
-    drawing_area.add_controller(&event_controller);
-    drawing_area.set_draw_func(move |_, ctx, _, _| {
-        ctx.set_source_rgb(1., 0., 0.);
-        ctx.set_operator(cairo::Operator::Screen);
-        ctx.paint().unwrap();
-    });
-
-    hbox.append(&tools_box);
-    hbox.append(&drawing_area);
-
-    let window = gtk::ApplicationWindow::builder()
-        .application(app)
-        .title("tablet-emu")
-        .default_width(800)
-        .default_height(480)
-        .child(&hbox)
-        .build();
-
-    window.show();
-}
-
-pub fn main(_args: &[String]) {
-    let app = gtk::Application::builder()
-        .application_id("fr.linkmauve.TabletEmu")
-        .build();
-    app.connect_activate(build_ui);
-    app.run();
-}
--- a/src/gtk.rs	Thu Aug 26 22:00:55 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,179 +0,0 @@
-// Tablet emulator, for people who don’t own one
-// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-use crate::{state::State, DEFAULT_HEIGHT, DEFAULT_WIDTH};
-use gio::prelude::*;
-use glib::clone;
-use gtk::prelude::*;
-use input_linux::Key;
-use std::io::ErrorKind;
-use std::sync::{Arc, Mutex};
-
-fn build_main_menu(app: &gtk::Application) {
-    let quit = gio::SimpleAction::new("quit", None);
-    app.set_accels_for_action("app.quit", &["<Control>q"]);
-    app.add_action(&quit);
-    quit.connect_activate(clone!(@weak app => move |_, _| app.quit()));
-
-    let about = gio::SimpleAction::new("about", None);
-    app.add_action(&about);
-    about.connect_activate(|_, _| {
-        let about = gtk::AboutDialog::builder()
-            .program_name("TabletEmu")
-            .logo_icon_name("input-tablet")
-            .website("https://hg.linkmauve.fr/tablet-emu")
-            .version("0.1")
-            .license_type(gtk::License::Agpl30)
-            .copyright("© 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>")
-            .build();
-        //about.run();
-        about.destroy();
-    });
-
-    let menu = gio::Menu::new();
-    {
-        let file = gio::Menu::new();
-        file.append(Some("_Quit"), Some("app.quit"));
-        menu.append_submenu(Some("_File"), &file);
-    }
-    {
-        let help = gio::Menu::new();
-        help.append(Some("_About"), Some("app.about"));
-        menu.append_submenu(Some("_Help"), &help);
-    }
-    app.set_menubar(Some(&menu));
-}
-
-fn build_ui(app: &gtk::Application) {
-    build_main_menu(app);
-
-    let state = match State::new() {
-        Ok(state) => Arc::new(Mutex::new(state)),
-        Err(err) => {
-            match err.kind() {
-                ErrorKind::NotFound => {
-                    eprintln!("Couldn’t find /dev/uinput: {}", err);
-                    eprintln!("Maybe you forgot to `modprobe uinput`?");
-                }
-                ErrorKind::PermissionDenied => {
-                    eprintln!("Couldn’t open /dev/uinput for writing: {}", err);
-                    eprintln!("Maybe you aren’t allowed to create input devices?");
-                }
-                _ => eprintln!("Couldn’t open /dev/uinput for writing: {}", err),
-            }
-            std::process::exit(1);
-        }
-    };
-
-    let hbox = gtk::Box::builder()
-        .orientation(gtk::Orientation::Horizontal)
-        .build();
-    let tools_box = gtk::Box::builder()
-        .orientation(gtk::Orientation::Vertical)
-        .build();
-
-    macro_rules! impl_tool {
-        ($tool:tt) => {
-            let tool = gtk::Button::with_mnemonic($tool);
-            let state_weak = Arc::downgrade(&state);
-            tool.connect_clicked(move |b| {
-                let state = state_weak.upgrade().unwrap();
-                let mut state = state.lock().unwrap();
-                let tool = match b.label().unwrap().as_str() {
-                    "_Pen" => Key::ButtonToolPen,
-                    "_Rubber" => Key::ButtonToolRubber,
-                    "_Brush" => Key::ButtonToolBrush,
-                    "P_encil" => Key::ButtonToolPencil,
-                    "_Airbrush" => Key::ButtonToolAirbrush,
-                    _ => unreachable!(),
-                };
-                state.select_tool(tool);
-            });
-            tools_box.append(&tool);
-        };
-    }
-    impl_tool!("_Pen");
-    impl_tool!("_Rubber");
-    impl_tool!("_Brush");
-    impl_tool!("P_encil");
-    impl_tool!("_Airbrush");
-
-    let drawing_area = gtk::DrawingArea::builder()
-        .content_width(DEFAULT_WIDTH)
-        .content_height(DEFAULT_HEIGHT)
-        .hexpand(true)
-        .build();
-    let gesture_click = gtk::GestureClick::new();
-    let event_controller = gtk::EventControllerMotion::new();
-    let state_weak = Arc::downgrade(&state);
-    drawing_area.connect_resize(move |_, width, height| {
-        let state = state_weak.upgrade().unwrap();
-        let mut state = state.lock().unwrap();
-        state.set_size(width, height);
-    });
-    let state_weak = Arc::downgrade(&state);
-    gesture_click.connect_pressed(move |_, n_press, x, y| {
-        if n_press != 1 {
-            return;
-        }
-
-        let state = state_weak.upgrade().unwrap();
-        let mut state = state.lock().unwrap();
-        state.press(x, y).unwrap();
-    });
-    let state_weak = Arc::downgrade(&state);
-    gesture_click.connect_released(move |_, n_press, x, y| {
-        if n_press != 1 {
-            return;
-        }
-
-        let state = state_weak.upgrade().unwrap();
-        let mut state = state.lock().unwrap();
-        state.release(x, y).unwrap();
-    });
-    event_controller.connect_motion(move |_, x, y| {
-        let mut state = state.lock().unwrap();
-        state.motion(x, y).unwrap();
-    });
-    drawing_area.add_controller(&gesture_click);
-    drawing_area.add_controller(&event_controller);
-    drawing_area.set_draw_func(move |_, ctx, _, _| {
-        ctx.set_source_rgb(1., 0., 0.);
-        ctx.set_operator(cairo::Operator::Screen);
-        ctx.paint().unwrap();
-    });
-
-    hbox.append(&tools_box);
-    hbox.append(&drawing_area);
-
-    let window = gtk::ApplicationWindow::builder()
-        .application(app)
-        .title("tablet-emu")
-        .default_width(800)
-        .default_height(480)
-        .child(&hbox)
-        .build();
-
-    window.show();
-}
-
-pub fn main(_args: &[String]) {
-    let app = gtk::Application::builder()
-        .application_id("fr.linkmauve.TabletEmu")
-        .build();
-    app.connect_activate(build_ui);
-    app.run();
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib.rs	Thu Aug 26 22:15:55 2021 +0200
@@ -0,0 +1,25 @@
+// Tablet emulator, for people who don’t own one
+// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+pub mod protocol;
+pub mod state;
+pub mod uinput;
+
+pub const MAX_X: i32 = 69920;
+pub const MAX_Y: i32 = 39980;
+
+pub const DEFAULT_WIDTH: i32 = 320;
+pub const DEFAULT_HEIGHT: i32 = 180;
--- a/src/main.rs	Thu Aug 26 22:00:55 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-// Tablet emulator, for people who don’t own one
-// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-#[cfg(feature = "gui")]
-mod gtk;
-#[cfg(feature = "gui")]
-mod client;
-mod server;
-mod state;
-mod uinput;
-
-pub use server::Event;
-
-use std::env::args;
-
-const MAX_X: i32 = 69920;
-const MAX_Y: i32 = 39980;
-
-const DEFAULT_WIDTH: i32 = 320;
-const DEFAULT_HEIGHT: i32 = 180;
-
-#[derive(Debug)]
-enum Ui {
-    Gtk,
-    Server,
-    Client,
-}
-
-fn main() {
-    let mut args: Vec<_> = args().collect();
-    let ui = match if args.len() > 1 {
-        args.remove(1)
-    } else {
-        String::from("client")
-    }
-    .as_str()
-    {
-        "gui" => Ui::Gtk,
-        "server" => Ui::Server,
-        "client" => Ui::Client,
-        name => {
-            eprintln!("Wrong UI “{}”, expected gui or server.", name);
-            std::process::exit(2);
-        }
-    };
-
-    match ui {
-        #[cfg(feature = "gui")]
-        Ui::Gtk => gtk::main(&args),
-
-        #[cfg(not(feature = "gui"))]
-        Ui::Gtk => panic!("tablet-emu has been compiled without GUI support."),
-
-        Ui::Server => server::main(&args),
-        Ui::Client => client::main(&args),
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/protocol.rs	Thu Aug 26 22:15:55 2021 +0200
@@ -0,0 +1,64 @@
+// Tablet emulator, for people who don’t own one
+// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use bitflags::bitflags;
+
+bitflags! {
+    /// This is the memory layout of the buttons on the 3DS.
+    #[derive(Default)]
+    pub struct Buttons: u32 {
+        const A = 0x00000001;
+        const B = 0x00000002;
+        const SELECT = 0x00000004;
+        const START = 0x00000008;
+        const RIGHT = 0x00000010;
+        const LEFT = 0x00000020;
+        const UP = 0x00000040;
+        const DOWN = 0x00000080;
+        const R = 0x00000100;
+        const L = 0x00000200;
+        const X = 0x00000400;
+        const Y = 0x00000800;
+        // Nothing
+        // Nothing
+        const ZL = 0x00004000;
+        const ZR = 0x00008000;
+        const RESIZE = 0x00010000; // Not an actual 3DS button!
+        // Nothing
+        // Nothing
+        // Nothing
+        const TOUCH = 0x00100000;
+        // Nothing
+        // Nothing
+        // Nothing
+        const C_RIGHT = 0x01000000;
+        const C_LEFT = 0x02000000;
+        const C_UP = 0x04000000;
+        const C_DOWN = 0x08000000;
+        const CIRCLE_RIGHT = 0x10000000;
+        const CIRCLE_LEFT = 0x20000000;
+        const CIRCLE_UP = 0x40000000;
+        const CIRCLE_DOWN = 0x80000000;
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct Event {
+    pub buttons: Buttons,
+    pad: (i16, i16),
+    c_pad: (i16, i16),
+    pub touch: (u16, u16),
+}
--- a/src/server.rs	Thu Aug 26 22:00:55 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,145 +0,0 @@
-// Tablet emulator, for people who don’t own one
-// Copyright © 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-use crate::state::State;
-use bitflags::bitflags;
-use input_linux::Key;
-use std::io::{self, ErrorKind};
-use std::net::UdpSocket;
-
-bitflags! {
-    /// This is the memory layout of the buttons on the 3DS.
-    #[derive(Default)]
-    pub struct Buttons: u32 {
-        const A = 0x00000001;
-        const B = 0x00000002;
-        const SELECT = 0x00000004;
-        const START = 0x00000008;
-        const RIGHT = 0x00000010;
-        const LEFT = 0x00000020;
-        const UP = 0x00000040;
-        const DOWN = 0x00000080;
-        const R = 0x00000100;
-        const L = 0x00000200;
-        const X = 0x00000400;
-        const Y = 0x00000800;
-        // Nothing
-        // Nothing
-        const ZL = 0x00004000;
-        const ZR = 0x00008000;
-        const RESIZE = 0x00010000; // Not an actual 3DS button!
-        // Nothing
-        // Nothing
-        // Nothing
-        const TOUCH = 0x00100000;
-        // Nothing
-        // Nothing
-        // Nothing
-        const C_RIGHT = 0x01000000;
-        const C_LEFT = 0x02000000;
-        const C_UP = 0x04000000;
-        const C_DOWN = 0x08000000;
-        const CIRCLE_RIGHT = 0x10000000;
-        const CIRCLE_LEFT = 0x20000000;
-        const CIRCLE_UP = 0x40000000;
-        const CIRCLE_DOWN = 0x80000000;
-    }
-}
-
-#[derive(Debug, Default)]
-pub struct Event {
-    pub buttons: Buttons,
-    pad: (i16, i16),
-    c_pad: (i16, i16),
-    pub touch: (u16, u16),
-}
-
-pub fn run_server(address: &str) -> io::Result<()> {
-    let mut state = match State::new() {
-        Ok(state) => state,
-        Err(err) => {
-            match err.kind() {
-                ErrorKind::NotFound => {
-                    eprintln!("Couldn’t find /dev/uinput: {}", err);
-                    eprintln!("Maybe you forgot to `modprobe uinput`?");
-                }
-                ErrorKind::PermissionDenied => {
-                    eprintln!("Couldn’t open /dev/uinput for writing: {}", err);
-                    eprintln!("Maybe you aren’t allowed to create input devices?");
-                }
-                _ => eprintln!("Couldn’t open /dev/uinput for writing: {}", err),
-            }
-            std::process::exit(1);
-        }
-    };
-
-    let socket = UdpSocket::bind(address)?;
-    println!("Listening on {:?}", socket);
-    println!("Here is an example client: https://hg.linkmauve.fr/remote-gamepad");
-
-    let mut event: Event = Default::default();
-    let mut last = Some((0., 0.));
-    state.set_size(320, 240);
-    loop {
-        // TODO: Yolo-alignment.
-        let buf: &mut [u8; 16] = unsafe { std::mem::transmute(&mut event) };
-        let (amount, source) = socket.recv_from(buf)?;
-        if amount != std::mem::size_of::<Event>() {
-            eprintln!("Invalid data length: {}", amount);
-            continue;
-        }
-        println!("{:?} from {:?}", event, source);
-        if event.buttons.contains(Buttons::A) {
-            state.select_tool(Key::ButtonToolPen);
-        } else if event.buttons.contains(Buttons::B) {
-            state.select_tool(Key::ButtonToolRubber);
-        } else if event.buttons.contains(Buttons::X) {
-            state.select_tool(Key::ButtonToolBrush);
-        } else if event.buttons.contains(Buttons::Y) {
-            state.select_tool(Key::ButtonToolPencil);
-        } else if event.buttons.contains(Buttons::SELECT) {
-            state.select_tool(Key::ButtonToolAirbrush);
-        } else if event.buttons.contains(Buttons::RESIZE) {
-            println!("set_size({}, {})", event.touch.0 as i32, event.touch.1 as i32);
-            state.set_size(event.touch.0 as i32, event.touch.1 as i32);
-        }
-        let (x, y) = event.touch;
-        if event.buttons.contains(Buttons::TOUCH) {
-            if let None = last {
-                state.press(x as f64, y as f64)?;
-                last = Some((x as f64, y as f64));
-                continue;
-            }
-        } else {
-            if let Some((x, y)) = last {
-                state.release(x, y)?;
-                last = None;
-                continue;
-            }
-        }
-        state.motion(x as f64, y as f64)?;
-        last = Some((x as f64, y as f64));
-    }
-}
-
-pub fn main(args: &[String]) {
-    let address = if args.len() > 1 {
-        args[1].clone()
-    } else {
-        String::from("0.0.0.0:16150")
-    };
-    run_server(&address).unwrap();
-}