├── .gitattributes ├── .github └── workflows │ └── screenstub.yml ├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── ci.nix ├── config ├── Cargo.toml ├── keymaps.csv └── src │ ├── keymap.rs │ └── lib.rs ├── ddc ├── Cargo.toml └── src │ ├── ddc.rs │ ├── ddcutil.rs │ └── lib.rs ├── default.nix ├── derivation.nix ├── event ├── Cargo.toml └── src │ └── lib.rs ├── fd ├── Cargo.toml └── src │ └── lib.rs ├── flake.lock ├── flake.nix ├── home.nix ├── lock.nix ├── module.nix ├── nixos.nix ├── qemu ├── Cargo.toml └── src │ └── lib.rs ├── samples ├── config.yml ├── modules-load.d │ ├── i2c.conf │ └── uinput.conf ├── sway │ └── config ├── udev │ └── rules.d │ │ └── 99-uinput.rules └── xorg.conf.d │ └── 30-screenstub.conf ├── src ├── exec.rs ├── filter.rs ├── grab.rs ├── main.rs ├── process.rs ├── route.rs ├── sources.rs ├── spawner.rs └── util.rs ├── uinput ├── Cargo.toml └── src │ └── lib.rs └── x ├── Cargo.toml └── src └── lib.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.rs filter=tabspace 2 | -------------------------------------------------------------------------------- /.github/workflows/screenstub.yml: -------------------------------------------------------------------------------- 1 | env: 2 | CI_ALLOW_ROOT: '1' 3 | CI_CONFIG: ./ci.nix 4 | CI_PLATFORM: gh-actions 5 | jobs: 6 | ci-check: 7 | name: screenstub check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - id: checkout 11 | name: git clone 12 | uses: actions/checkout@v1 13 | with: 14 | submodules: true 15 | - id: nix-install 16 | name: nix install 17 | uses: arcnmx/ci/actions/nix/install@master 18 | - id: ci-action-build 19 | name: nix build ci.gh-actions.configFile 20 | uses: arcnmx/ci/actions/nix/build@master 21 | with: 22 | attrs: ci.gh-actions.configFile 23 | out-link: .ci/workflow.yml 24 | - id: ci-action-compare 25 | name: gh-actions compare 26 | uses: arcnmx/ci/actions/nix/run@master 27 | with: 28 | args: -u .github/workflows/screenstub.yml .ci/workflow.yml 29 | attrs: nixpkgs.diffutils 30 | command: diff 31 | nixos: 32 | name: screenstub-nixos 33 | runs-on: ubuntu-latest 34 | steps: 35 | - id: checkout 36 | name: git clone 37 | uses: actions/checkout@v1 38 | with: 39 | submodules: true 40 | - id: nix-install 41 | name: nix install 42 | uses: arcnmx/ci/actions/nix/install@master 43 | - id: ci-setup 44 | name: nix setup 45 | uses: arcnmx/ci/actions/nix/run@master 46 | with: 47 | attrs: ci.job.nixos.run.setup 48 | quiet: false 49 | - id: ci-dirty 50 | name: nix test dirty 51 | uses: arcnmx/ci/actions/nix/run@master 52 | with: 53 | attrs: ci.job.nixos.run.test 54 | command: ci-build-dirty 55 | quiet: false 56 | stdout: ${{ runner.temp }}/ci.build.dirty 57 | - id: ci-test 58 | name: nix test build 59 | uses: arcnmx/ci/actions/nix/run@master 60 | with: 61 | attrs: ci.job.nixos.run.test 62 | command: ci-build-realise 63 | ignore-exit-code: true 64 | quiet: false 65 | stdin: ${{ runner.temp }}/ci.build.dirty 66 | - env: 67 | CI_EXIT_CODE: ${{ steps.ci-test.outputs.exit-code }} 68 | id: ci-summary 69 | name: nix test results 70 | uses: arcnmx/ci/actions/nix/run@master 71 | with: 72 | attrs: ci.job.nixos.run.test 73 | command: ci-build-summarise 74 | quiet: false 75 | stdin: ${{ runner.temp }}/ci.build.dirty 76 | stdout: ${{ runner.temp }}/ci.build.cache 77 | - env: 78 | CACHIX_SIGNING_KEY: ${{ secrets.CACHIX_SIGNING_KEY }} 79 | id: ci-cache 80 | if: always() 81 | name: nix test cache 82 | uses: arcnmx/ci/actions/nix/run@master 83 | with: 84 | attrs: ci.job.nixos.run.test 85 | command: ci-build-cache 86 | quiet: false 87 | stdin: ${{ runner.temp }}/ci.build.cache 88 | name: screenstub 89 | 'on': 90 | - push 91 | - pull_request 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | Cargo.lock 3 | !/Cargo.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | sudo: false 5 | os: 6 | - linux 7 | addons: 8 | apt: 9 | packages: 10 | - libudev-dev 11 | - libxcb1-dev 12 | - libxcb-dpms0-dev 13 | - libxcb-xtest0-dev 14 | - libxcb-xkb-dev 15 | cache: 16 | directories: 17 | - "$HOME/.cargo" 18 | - target 19 | matrix: 20 | fast_finish: true 21 | allow_failures: 22 | - rust: nightly 23 | env: 24 | matrix: 25 | - CARGO_FEATURES= 26 | - CARGO_DEFAULT_FEATURES=false 27 | global: 28 | - CARGO_QUIET= 29 | before_install: 30 | - curl -L https://github.com/arcnmx/ci/archive/0.2.tar.gz | tar -xzC $HOME && . $HOME/ci-0.2/src 31 | script: 32 | - cd $TRAVIS_BUILD_DIR 33 | - cargo test 34 | - cargo build 35 | deploy: 36 | provider: script 37 | script: 'true' 38 | on: 39 | tags: true 40 | all_branches: true 41 | condition: "$TRAVIS_RUST_VERSION = stable && $CARGO_DEFAULT_FEATURES != false" 42 | before_deploy: 43 | - cd $TRAVIS_BUILD_DIR 44 | - cargo doc 45 | after_deploy: 46 | - cd $TRAVIS_BUILD_DIR 47 | - cargo pages-publish 48 | - cargo package 49 | - cargo publish 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screenstub" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | include = [ 7 | "/src/**/*.rs", 8 | "/README*", 9 | "/COPYING*", 10 | ] 11 | 12 | [dependencies] 13 | screenstub-config = { path = "config" } 14 | screenstub-uinput = { path = "uinput" } 15 | screenstub-event = { path = "event" } 16 | screenstub-qemu = { path = "qemu" } 17 | screenstub-ddc = { path = "ddc" } 18 | screenstub-x = { path = "x" } 19 | input-linux = "0.6" 20 | tokio = { version = "^1.0.0", default-features = false, features = ["process", "rt-multi-thread"] } 21 | anyhow = "^1.0.42" 22 | futures = { version = "^0.3.5", features = ["thread-pool"] } 23 | clap = "4" 24 | env_logger = "0.10" 25 | log = "0.4" 26 | serde_yaml = "^0.8.13" 27 | enumflags2 = "^0.6.4" 28 | result = "^1.0.0" 29 | ctrlc = { version = "^3.1.9", features = ["termination"] } 30 | qapi = { version = "0.11", features = ["qmp", "qga"] } 31 | 32 | [features] 33 | with-ddcutil = ["screenstub-ddc/with-ddcutil", "screenstub-config/with-ddcutil"] 34 | with-ddc = ["screenstub-ddc/with-ddc", "screenstub-config/with-ddc"] 35 | default = ["with-ddc"] 36 | 37 | [workspace] 38 | members = [ 39 | "config", 40 | "uinput", 41 | "event", 42 | "qemu", 43 | "ddc", 44 | "fd", 45 | "x", 46 | ] 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # screenstub 2 | 3 | An alternative approach to a software KVM switch for GPU passthrough that aims 4 | to appear much like [LookingGlass](https://github.com/gnif/LookingGlass) but 5 | without relaying frames. 6 | 7 | `screenstub` uses [DDC/CI](https://en.wikipedia.org/wiki/Display_Data_Channel) 8 | to switch monitor inputs when its window is visible and/or in focus. It is 9 | intended to be used in fullscreen with virtual workspaces, and switches to the 10 | VM input when its workspace is visible. Many options are available for working 11 | with input forwarding to a VM, and if used without DDC/CI it becomes similar to 12 | something like Synergy. 13 | 14 | ## Setup Overview 15 | 16 | 1. Install screenstub via [cargo](#installation). 17 | 2. Run `screenstub detect` to check that DDC/CI is working and your monitor 18 | is detected. You may need to enable DDC/CI in your monitor's settings, and/or 19 | [load the i2c drivers](#host-control). 20 | 3. [Configure](#configuration) screenstub by modifying the example as necessary, 21 | and setting up [the QEMU sockets](#qemu-control-sockets). 22 | 23 | Additional configuration may be required for advanced usage: 24 | 25 | 1. Install and set up [qemu-ga to run on Windows startup](#qemu-guest-agent). 26 | 2. Install a [command-line DDC/CI program in Windows](#windows). 27 | 3. Configure [permissions](#uinput-permissions), and check your [input devices](#guest-input-devices) in order to use the advanced routing modes. 28 | 29 | ## Installation 30 | 31 | Requires a modern stable [Rust toolchain](https://www.rust-lang.org/en-US/install.html), 32 | and can be installed and run like so: 33 | 34 | ```bash 35 | cargo install --force --git https://github.com/arcnmx/screenstub 36 | screenstub -c config.yml x 37 | ``` 38 | 39 | ### Dependencies 40 | 41 | - udev (Debian: libudev-dev) 42 | - libxcb (Debian: libxcb1-dev libxcb-dpms0-dev libxcb-xtest0-dev libxcb-xkb-dev) 43 | - python (build-time only to generate xcb bindings) 44 | 45 | ### Packages 46 | 47 | - [Nix{,OS}](https://github.com/arcnmx/nixexprs): `nix run -f https://github.com/nix-community/NUR/archive/master.tar.gz repos.arc.packages.screenstub -c screenstub` 48 | 49 | ## Configuration 50 | 51 | An [example configuration](samples/config.yml) is available to use as a starting 52 | point. There are a few specific items that need to be set up for everything to 53 | work. The `screenstub detect` command can be used to find information about 54 | DDC/CI capable monitors and their inputs. 55 | 56 | ### QEMU Control Sockets 57 | 58 | `screenstub` requires both QMP and guest agent sockets available to properly 59 | control the VM and QEMU itself. This requires something similar to the following 60 | command-line flags to be passed to QEMU (note libvirt may already expose some of 61 | these for you): 62 | 63 | ```bash 64 | qemu-system-x86_64 \ 65 | -chardev socket,path=/tmp/vfio-qga,server,nowait,id=qga0 \ 66 | -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 \ 67 | -chardev socket,path=/tmp/vfio-qmp,server,nowait,id=qmp0 \ 68 | -mon chardev=qmp0,id=qmp,mode=control 69 | ``` 70 | 71 | ### Guest Input Devices 72 | 73 | `screenstub` emulates three different input devices: a keyboard, a tablet for 74 | absolute coordinate mouse mapping, and a mouse for relative motion events. Each 75 | of these devices may be configured as USB devices, PS/2, or virtio. It is 76 | recommended that you remove all existing input devices from your QEMU command 77 | line configuration (`-usbdevice kbd`, `-usbdevice mouse`, `-usbdevice tablet`, `-device usb-kbd`, `-device usb-mouse`, `-device usb-tablet`). 78 | 79 | The default configuration sets them up for optimal compatibility, but virtio 80 | input drivers (vioinput) are recommended instead for performance reasons. These 81 | require drivers to be installed in the guest. You can [download them for Windows here](https://docs.fedoraproject.org/en-US/quick-docs/creating-windows-virtual-machines-using-virtio-drivers/index.html). 82 | 83 | ### Input Event Routing 84 | 85 | The routing mode describes how input events are translated from the host mouse 86 | and keyboard to the guest devices. The default `qmp` routing mode sends all input 87 | commands over the QEMU control socket. This requires no additional configuration 88 | on the host, but may not be optimal for performance. The other routing modes use 89 | `uinput` instead to transport events, which requires additional configuration. 90 | 91 | #### UInput Permissions 92 | 93 | To use the `virtio-host` or `input-linux` routing modes, `screenstub` needs 94 | access to `/dev/uinput` and the virtual `/dev/input/event*` devices. 95 | [udev rules](samples/udev/rules.d/99-uinput.rules) can be used to set up device 96 | permissions. Additional rules may be included for any external devices you want 97 | to "grab" and forward to the guest. 98 | 99 | Xorg also needs to be configured to ignore the virtual devices. Copy 100 | [the xorg config](samples/xorg.conf.d/30-screenstub.conf) into your `xorg.conf` or 101 | `/etc/X11/xorg.conf.d/` directory to prevent Xorg from trying to use the virtual 102 | input devices for the host. 103 | 104 | ### Host Control 105 | 106 | These are pretty straightforward to use when they work, however it is recommended 107 | to use the built-in DDC/CI support instead. You will probably need to load the `i2c-dev` 108 | kernel module for it to work, by placing [i2c.conf](samples/modules-load.d/i2c.conf) 109 | in `/etc/modules-load.d/`. 110 | 111 | - [ddcutil](http://www.ddcutil.com/) 112 | - [ddccontrol](https://github.com/ddccontrol/ddccontrol) 113 | 114 | #### NVIDIA 115 | 116 | Some NVIDIA Linux drivers have had broken DDC/CI support. 117 | [There are workarounds](http://www.ddcutil.com/nvidia/) but there may be issues 118 | when using DDC/CI over DisplayPort from the host. 119 | 120 | ### Guest Control 121 | 122 | Many monitors require a DDC/CI command to be issued by the GPU on the currently 123 | active input. In this case `screenstub` must issue a command to the guest OS 124 | to instruct it to relinquish control over the screen back to the host. 125 | QEMU Guest Agent and SSH are two common methods of executing commands inside 126 | of a guest. 127 | 128 | #### Windows 129 | 130 | - [ddcset](https://github.com/arcnmx/ddcset-rs) setvcp 60 3 131 | - [ScreenBright](http://www.overclock.net/forum/44-monitors-displays/1262322-guide-display-control-via-windows-brightness-contrast-etc-ddc-ci.html) -set 0x60 3 132 | - [ClickMonitorDDC](https://clickmonitorddc.bplaced.net/) s DVI1 133 | 134 | Note that Windows applications interfacing with the screen must run as a logged 135 | in graphical user. Services like QEMU Guest Agent or SSHd often run as a system 136 | service and may have trouble running these commands without adjustments. NVAPI 137 | for example (via `ddcset -b nvapi`) may be used as an alternative on NVIDIA cards. 138 | 139 | ##### QEMU Guest Agent 140 | 141 | A recent version of qemu-ga is required to be able to execute processes in the 142 | VM. It can be built from from source, or you may [download a compiled installer 143 | here](https://github.com/arcnmx/aur-qemu-guest-agent-windows/releases). 144 | 145 | It is recommended that you disable the default qemu-ga system service, and 146 | instead schedule it to run on user login. I run the following in a powershell 147 | script on startup: 148 | 149 | ```powershell 150 | Start-Process "C:\Program Files\Qemu-ga\qemu-ga.exe" -WindowStyle Hidden 151 | ``` 152 | 153 | This needs to run as admin, so you can use task scheduler for that pointing to 154 | a script containing the above: 155 | 156 | ```bat 157 | schtasks /create /sc onlogon /tn qemu-ga /rl highest /tr "powershell C:\path\to\qemu-ga.ps1" 158 | ``` 159 | 160 | ### macOS 161 | 162 | - [ddcctl](https://github.com/kfix/ddcctl) 163 | - [DDC/CI Tools for OS X](https://github.com/jontaylor/DDC-CI-Tools-for-OS-X) 164 | -------------------------------------------------------------------------------- /ci.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, ... }: with pkgs; with lib; let 2 | screenstub = import ./. { inherit pkgs; }; 3 | inherit (screenstub) checks packages; 4 | screenstub-checked = (packages.screenstub.override { 5 | buildType = "debug"; 6 | }).overrideAttrs (_: { 7 | doCheck = true; 8 | }); 9 | in { 10 | config = { 11 | name = "screenstub"; 12 | ci.gh-actions.enable = true; 13 | cache.cachix.arc.enable = true; 14 | channels = { 15 | nixpkgs = "22.11"; 16 | }; 17 | tasks = { 18 | build.inputs = singleton screenstub-checked; 19 | check-config.inputs = singleton (checks.check-config.override { 20 | screenstub = screenstub-checked; 21 | }); 22 | }; 23 | jobs = { 24 | nixos = { 25 | system = "x86_64-linux"; 26 | }; 27 | #macos.system = "x86_64-darwin"; 28 | }; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screenstub-config" 3 | version = "0.0.1" 4 | edition = "2018" 5 | 6 | include = [ 7 | "/src/**/*.rs", 8 | "/keymaps.csv", 9 | ] 10 | 11 | [dependencies] 12 | serde = { version = "^1.0.27", features = ["derive"] } 13 | serde-hex = "^0.1.0" 14 | csv = "^1.1.3" 15 | enumflags2 = "^0.6.4" 16 | humantime-serde = "^1.0.0" 17 | input-linux = { version = "0.6", features = ["serde"] } 18 | qapi-spec = { version = "0.3" } 19 | qapi-qmp = { version = "0.11" } 20 | 21 | [features] 22 | with-ddcutil = [] 23 | with-ddc = [] 24 | -------------------------------------------------------------------------------- /config/src/keymap.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use qapi_spec::Enum; 3 | use qapi_qmp::QKeyCode; 4 | 5 | mod hex_opt_serde { 6 | use std::str::FromStr; 7 | use std::fmt::Display; 8 | use serde::{Serialize, Serializer, Deserialize, Deserializer}; 9 | use serde_hex::{SerHex, CompactPfx}; 10 | 11 | pub fn deserialize<'d, D: Deserializer<'d>, R: FromStr + SerHex>(d: D) -> Result, D::Error> where ::Err: Display { 12 | use serde::de::Error; 13 | String::deserialize(d).and_then(|res| match res.is_empty() { 14 | true => Ok(None), 15 | false if res.starts_with("0x") => 16 | SerHex::from_hex(&res).map(Some) 17 | .map_err(D::Error::custom), 18 | false => FromStr::from_str(&res).map(Some) 19 | .map_err(D::Error::custom), 20 | }) 21 | } 22 | 23 | pub fn serialize>(this: &Option, s: S) -> Result { 24 | use serde::ser::Error; 25 | match this { 26 | Some(this) => this.into_hex() 27 | .map_err(S::Error::custom) 28 | .and_then(|v| v.serialize(s)), 29 | None => "".serialize(s), 30 | } 31 | } 32 | } 33 | 34 | mod hex_serde { 35 | use std::str::FromStr; 36 | use std::fmt::Display; 37 | use serde::{Serializer, Deserialize, Deserializer}; 38 | use serde_hex::{SerHex, CompactPfx}; 39 | 40 | pub fn deserialize<'d, D: Deserializer<'d>, R: FromStr + SerHex>(d: D) -> Result where ::Err: Display { 41 | use serde::de::Error; 42 | String::deserialize(d).and_then(|res| match res.starts_with("0x") { 43 | true => SerHex::from_hex(&res) 44 | .map_err(D::Error::custom), 45 | false => FromStr::from_str(&res) 46 | .map_err(D::Error::custom), 47 | }) 48 | } 49 | 50 | pub fn serialize>(this: &R, s: S) -> Result { 51 | this.serialize(s) 52 | } 53 | } 54 | 55 | #[derive(Debug, Clone, Serialize, Deserialize)] 56 | pub struct Keymap { 57 | #[serde(rename = "Linux Name")] 58 | linux_name: Option, 59 | #[serde(rename = "Linux Keycode", with = "hex_serde")] 60 | linux_keycode: u16, 61 | #[serde(rename = "OS-X Name")] 62 | mac_name: Option, 63 | #[serde(rename = "OS-X Keycode", with = "hex_opt_serde")] 64 | mac_keycode: Option, 65 | #[serde(rename = "AT set1 keycode", with = "hex_opt_serde")] 66 | at_set1_keycode: Option, 67 | #[serde(rename = "AT set2 keycode", with = "hex_opt_serde")] 68 | at_set2_keycode: Option, 69 | #[serde(rename = "AT set3 keycode", with = "hex_opt_serde")] 70 | at_set3_keycode: Option, 71 | #[serde(rename = "USB Keycodes", with = "hex_opt_serde")] 72 | usb_keycodes: Option, 73 | #[serde(rename = "Win32 Name")] 74 | win32_name: Option, 75 | #[serde(rename = "Win32 Keycode", with = "hex_opt_serde")] 76 | win32_keycode: Option, 77 | #[serde(rename = "Xwin XT")] 78 | xwin_xt: Option, 79 | #[serde(rename = "Xfree86 KBD XT")] 80 | xfree86_kbd_xt: Option, 81 | #[serde(rename = "X11 keysym name")] 82 | x11_keysym_name: Option, 83 | #[serde(rename = "X11 keysym", with = "hex_opt_serde")] 84 | x11_keysym: Option, 85 | #[serde(rename = "HTML code")] 86 | html_code: Option, 87 | #[serde(rename = "XKB key name")] 88 | xkb_key_name: Option, 89 | #[serde(rename = "QEMU QKeyCode")] 90 | qemu_qkeycode: Option, 91 | #[serde(rename = "Sun KBD", with = "hex_opt_serde")] 92 | sub_kbd: Option, 93 | #[serde(rename = "Apple ADB", with = "hex_opt_serde")] 94 | apple_adb: Option, 95 | } 96 | 97 | impl Keymap { 98 | pub fn qemu_qkeycode(&self) -> Option { 99 | self.qemu_qkeycode.as_ref() 100 | .map(|qkeycode| QKeyCode::from_name(&qkeycode.to_ascii_lowercase()).unwrap()) 101 | } 102 | 103 | /// xtkbd + special re-encoding of high bit 104 | pub fn qemu_qnum(&self) -> u8 { 105 | let at1 = self.at_set1_keycode.unwrap_or_default(); 106 | at1 as u8 | if at1 > 0x7f { 107 | 0x80 108 | } else { 109 | 0 110 | } 111 | } 112 | } 113 | 114 | #[derive(Debug, Clone, Serialize, Deserialize)] 115 | #[serde(transparent)] 116 | pub struct Keymaps { 117 | keymaps: Vec, 118 | } 119 | 120 | impl Keymaps { 121 | pub fn from_csv() -> Self { 122 | let csv_data = include_bytes!("../keymaps.csv"); 123 | let keymaps = csv::Reader::from_reader(&mut &csv_data[..]).deserialize() 124 | .collect::, _>>().unwrap(); 125 | Self { 126 | keymaps, 127 | } 128 | } 129 | 130 | pub fn qkeycode_keycodes(&self) -> Box<[QKeyCode]> { 131 | let max = self.keymaps.iter().map(|k| k.linux_keycode).max().unwrap_or_default(); 132 | (0..=max).map(|i| self.keymaps.iter().find(|k| k.linux_keycode == i) 133 | .and_then(|k| k.qemu_qkeycode()) 134 | .unwrap_or(QKeyCode::unmapped) 135 | ).collect() 136 | } 137 | 138 | pub fn qnum_keycodes(&self) -> Box<[u8]> { 139 | let max = self.keymaps.iter().map(|k| k.linux_keycode).max().unwrap_or_default(); 140 | (0..=max).map(|i| self.keymaps.iter().find(|k| k.linux_keycode == i) 141 | .map(|k| k.qemu_qnum()) 142 | .unwrap_or(0) 143 | ).collect() 144 | } 145 | } 146 | 147 | #[test] 148 | fn keymaps_csv() { 149 | let keymaps = Keymaps::from_csv(); 150 | println!("{:#?}", keymaps); 151 | println!("{:#?}", keymaps.qkeycode_keycodes()); 152 | } 153 | -------------------------------------------------------------------------------- /config/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate input_linux as input; 2 | 3 | use std::collections::HashMap; 4 | use std::time::Duration; 5 | use std::fmt; 6 | use enumflags2::BitFlags; 7 | use serde::{Serialize, Deserialize}; 8 | use input::{Key, InputEvent, EventRef}; 9 | 10 | pub mod keymap; 11 | 12 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 13 | #[serde(deny_unknown_fields)] 14 | pub struct Config { 15 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 16 | pub screens: Vec, 17 | 18 | #[serde(default)] 19 | pub qemu: ConfigQemu, 20 | 21 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 22 | pub hotkeys: Vec, 23 | #[serde(default, skip_serializing_if = "HashMap::is_empty")] 24 | pub key_remap: HashMap, 25 | 26 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 27 | pub exit_events: Vec, 28 | } 29 | 30 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 31 | #[serde(deny_unknown_fields)] 32 | pub struct ConfigScreen { 33 | #[serde(default)] 34 | pub monitor: ConfigMonitor, 35 | #[serde(default)] 36 | pub guest_source: ConfigSource, 37 | #[serde(default)] 38 | pub host_source: ConfigSource, 39 | 40 | #[serde(default, skip_serializing_if = "Option::is_none")] 41 | pub ddc: Option, 42 | 43 | #[serde(default, skip_serializing_if = "Option::is_none")] 44 | pub x_instance: Option, 45 | } 46 | 47 | #[derive(Debug, Clone, Deserialize, Serialize)] 48 | #[serde(rename_all = "snake_case")] 49 | pub enum ConfigDdcMethod { 50 | Ddc, 51 | Libddcutil, 52 | Ddcutil, 53 | Exec(Vec), 54 | GuestExec(Vec), 55 | GuestWait, 56 | } 57 | 58 | impl ConfigDdcMethod { 59 | #[cfg(all(not(feature = "with-ddc"), feature = "with-ddcutil"))] 60 | fn default_host() -> Vec { 61 | vec![ConfigDdcMethod::Libddcutil] 62 | } 63 | 64 | #[cfg(feature = "with-ddc")] 65 | fn default_host() -> Vec { 66 | vec![ConfigDdcMethod::Ddc] 67 | } 68 | 69 | #[cfg(not(any(feature = "with-ddcutil", feature = "with-ddc")))] 70 | fn default_host() -> Vec { 71 | Vec::new() 72 | } 73 | 74 | fn default_guest() -> Vec { 75 | Self::default_host() 76 | } 77 | } 78 | 79 | impl Default for ConfigDdc { 80 | fn default() -> Self { 81 | Self { 82 | host: ConfigDdcMethod::default_host(), 83 | guest: ConfigDdcMethod::default_guest(), 84 | minimal_delay: Self::default_delay(), 85 | } 86 | } 87 | } 88 | 89 | #[derive(Debug, Clone, Deserialize, Serialize)] 90 | #[serde(deny_unknown_fields)] 91 | pub struct ConfigDdc { 92 | #[serde(default = "ConfigDdcMethod::default_host")] 93 | pub host: Vec, 94 | #[serde(default = "ConfigDdcMethod::default_guest")] 95 | pub guest: Vec, 96 | #[serde(default = "ConfigDdc::default_delay", with = "humantime_serde")] 97 | pub minimal_delay: Duration, 98 | } 99 | 100 | impl ConfigDdc { 101 | fn default_delay() -> Duration { 102 | Duration::from_millis(100) 103 | } 104 | } 105 | 106 | #[derive(Debug, Clone, Deserialize, Serialize)] 107 | #[serde(deny_unknown_fields)] 108 | pub struct ConfigQemu { 109 | #[serde(default, skip_serializing_if = "Option::is_none")] 110 | pub ga_socket: Option, 111 | #[serde(default, skip_serializing_if = "Option::is_none")] 112 | pub qmp_socket: Option, 113 | 114 | #[serde(default)] 115 | pub driver: Option, 116 | #[serde(default)] 117 | pub keyboard_driver: Option, 118 | #[serde(default)] 119 | pub relative_driver: Option, 120 | #[serde(default)] 121 | pub absolute_driver: Option, 122 | 123 | #[serde(default = "ConfigQemuRouting::qmp")] 124 | pub routing: ConfigQemuRouting, 125 | } 126 | 127 | impl Default for ConfigQemu { 128 | fn default() -> Self { 129 | ConfigQemu { 130 | ga_socket: Default::default(), 131 | qmp_socket: Default::default(), 132 | driver: Default::default(), 133 | keyboard_driver: Default::default(), 134 | relative_driver: Default::default(), 135 | absolute_driver: Default::default(), 136 | routing: ConfigQemuRouting::Qmp, 137 | } 138 | } 139 | } 140 | 141 | impl ConfigQemu { 142 | pub fn keyboard_driver(&self) -> &ConfigQemuDriver { 143 | self.keyboard_driver 144 | .as_ref() 145 | .or(self.driver.as_ref()) 146 | .unwrap_or(&ConfigQemuDriver::Ps2) 147 | } 148 | 149 | pub fn relative_driver(&self) -> &ConfigQemuDriver { 150 | self.relative_driver 151 | .as_ref() 152 | .or(self.driver.as_ref()) 153 | .unwrap_or(&ConfigQemuDriver::Usb) 154 | } 155 | 156 | pub fn absolute_driver(&self) -> &ConfigQemuDriver { 157 | self.absolute_driver 158 | .as_ref() 159 | .or(self.driver.as_ref()) 160 | .unwrap_or(&ConfigQemuDriver::Usb) 161 | } 162 | } 163 | 164 | #[derive(Debug, Clone, Deserialize, Serialize)] 165 | #[serde(deny_unknown_fields, rename_all = "lowercase", remote = "ConfigQemuDriver")] 166 | pub enum ConfigQemuDriver { 167 | Ps2, 168 | Usb, 169 | Virtio { 170 | #[serde(default, skip_serializing_if = "Option::is_none")] 171 | bus: Option, 172 | }, 173 | } 174 | 175 | impl<'de> Deserialize<'de> for ConfigQemuDriver { 176 | fn deserialize>(deserializer: D) -> Result { 177 | #[derive(Deserialize)] 178 | #[serde(rename_all = "lowercase")] 179 | enum ConfigQemuDriverDeserializerPlain { 180 | Virtio, 181 | } 182 | 183 | #[derive(Deserialize)] 184 | #[serde(untagged)] 185 | enum ConfigQemuDriverDeserializer { 186 | Enum( 187 | #[serde(deserialize_with = "ConfigQemuDriver::deserialize")] 188 | ConfigQemuDriver 189 | ), 190 | Plain(ConfigQemuDriverDeserializerPlain), 191 | } 192 | 193 | impl From for ConfigQemuDriver { 194 | fn from(v: ConfigQemuDriverDeserializer) -> Self { 195 | match v { 196 | ConfigQemuDriverDeserializer::Enum(v) => v, 197 | ConfigQemuDriverDeserializer::Plain(e) => match e { 198 | ConfigQemuDriverDeserializerPlain::Virtio => ConfigQemuDriver::Virtio { 199 | bus: Default::default(), 200 | }, 201 | }, 202 | } 203 | } 204 | } 205 | 206 | ConfigQemuDriverDeserializer::deserialize(deserializer) 207 | .map(From::from) 208 | } 209 | } 210 | 211 | impl Serialize for ConfigQemuDriver { 212 | fn serialize(&self, serializer: S) -> Result { 213 | ConfigQemuDriver::serialize(self, serializer) 214 | } 215 | } 216 | 217 | impl ConfigQemuDriver { 218 | pub fn bus(&self) -> Option<&String> { 219 | match self { 220 | ConfigQemuDriver::Virtio { bus } => bus.as_ref(), 221 | _ => None, 222 | } 223 | } 224 | } 225 | 226 | #[derive(Debug, Copy, Clone, Deserialize, Serialize)] 227 | #[serde(rename_all = "kebab-case")] 228 | pub enum ConfigQemuRouting { 229 | InputLinux, 230 | VirtioHost, 231 | Spice, 232 | Qmp, 233 | } 234 | 235 | impl ConfigQemuRouting { 236 | fn qmp() -> Self { ConfigQemuRouting::Qmp } 237 | } 238 | 239 | #[derive(Debug, Clone, Deserialize, Serialize)] 240 | #[serde(deny_unknown_fields)] 241 | pub struct ConfigHotkey { 242 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 243 | pub triggers: Vec, 244 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 245 | pub modifiers: Vec, 246 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 247 | pub events: Vec, 248 | #[serde(default)] 249 | pub on_release: bool, 250 | #[serde(default)] 251 | pub global: bool, 252 | } 253 | 254 | #[derive(Debug, Clone, Deserialize, Serialize)] 255 | #[serde(rename_all = "snake_case")] 256 | pub enum ConfigEvent { 257 | Exec(Vec), 258 | GuestExec(Vec), 259 | GuestWait, 260 | ShowHost, 261 | ShowGuest, 262 | ToggleShow, 263 | ToggleGrab(ConfigGrab), 264 | Grab(ConfigGrab), 265 | Ungrab(ConfigGrabMode), 266 | UnstickHost, 267 | UnstickGuest, 268 | Shutdown, 269 | Reboot, 270 | Exit, 271 | } 272 | 273 | #[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Deserialize, Serialize, BitFlags)] 274 | #[repr(u8)] 275 | #[serde(rename_all = "snake_case")] 276 | pub enum ConfigInputEvent { 277 | Key = 0x01, 278 | Button = 0x02, 279 | Relative = 0x04, 280 | Absolute = 0x08, 281 | Misc = 0x10, 282 | Switch = 0x20, 283 | Led = 0x40, 284 | Sound = 0x80, 285 | } 286 | 287 | impl ConfigInputEvent { 288 | pub fn from_event(e: &InputEvent) -> Option { 289 | match EventRef::new(e) { 290 | Ok(EventRef::Key(key)) if key.key.is_key() => Some(ConfigInputEvent::Key), 291 | Ok(EventRef::Key(key)) if key.key.is_button() => Some(ConfigInputEvent::Button), 292 | Ok(EventRef::Relative(..)) => Some(ConfigInputEvent::Relative), 293 | Ok(EventRef::Absolute(..)) => Some(ConfigInputEvent::Absolute), 294 | Ok(EventRef::Misc(..)) => Some(ConfigInputEvent::Misc), 295 | Ok(EventRef::Switch(..)) => Some(ConfigInputEvent::Switch), 296 | Ok(EventRef::Led(..)) => Some(ConfigInputEvent::Led), 297 | Ok(EventRef::Sound(..)) => Some(ConfigInputEvent::Sound), 298 | _ => None, 299 | } 300 | } 301 | 302 | pub fn event_matches(&self, e: EventRef) -> bool { 303 | match (*self, e) { 304 | (ConfigInputEvent::Key, EventRef::Key(key)) if key.key.is_key() => true, 305 | (ConfigInputEvent::Button, EventRef::Key(key)) if key.key.is_button() => true, 306 | (ConfigInputEvent::Relative, EventRef::Relative(..)) => true, 307 | (ConfigInputEvent::Absolute, EventRef::Absolute(..)) => true, 308 | (ConfigInputEvent::Misc, EventRef::Misc(..)) => true, 309 | (ConfigInputEvent::Switch, EventRef::Switch(..)) => true, 310 | (ConfigInputEvent::Led, EventRef::Led(..)) => true, 311 | (ConfigInputEvent::Sound, EventRef::Sound(..)) => true, 312 | _ => false, 313 | } 314 | } 315 | } 316 | 317 | #[derive(Debug, Clone, Deserialize, Serialize)] 318 | #[serde(deny_unknown_fields, rename_all = "snake_case")] 319 | pub struct ConfigInputDevice { 320 | #[serde(default, skip_serializing_if = "Option::is_none")] 321 | pub id: Option, 322 | #[serde(default, skip_serializing_if = "String::is_empty")] 323 | pub name: String, 324 | #[serde(default, skip_serializing_if = "Option::is_none")] 325 | pub vendor_id: Option, 326 | #[serde(default, skip_serializing_if = "Option::is_none")] 327 | pub product_id: Option, 328 | // pub kind (keyboard, mouse, tablet, etc) 329 | } 330 | 331 | fn true_() -> bool { 332 | true 333 | } 334 | 335 | #[derive(Debug, Clone, Deserialize, Serialize)] 336 | #[serde(deny_unknown_fields, rename_all = "lowercase", remote = "ConfigGrab")] 337 | pub enum ConfigGrab { 338 | X { 339 | #[serde(default = "true_")] 340 | confine: bool, 341 | #[serde(default = "true_")] 342 | mouse: bool, 343 | #[serde(default = "ConfigGrab::default_ignore", skip_serializing_if = "Vec::is_empty")] 344 | ignore: Vec, 345 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 346 | devices: Vec, 347 | }, 348 | Evdev { 349 | #[serde(default)] 350 | exclusive: bool, 351 | #[serde(default, skip_serializing_if = "Option::is_none")] 352 | new_device_name: Option, 353 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 354 | xcore_ignore: Vec, 355 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 356 | evdev_ignore: Vec, 357 | devices: Vec, 358 | }, 359 | } 360 | 361 | impl<'de> Deserialize<'de> for ConfigGrab { 362 | fn deserialize>(deserializer: D) -> Result { 363 | #[derive(Deserialize)] 364 | #[serde(rename_all = "lowercase")] 365 | enum ConfigGrabDeserializerPlain { 366 | X, 367 | } 368 | 369 | #[derive(Deserialize)] 370 | #[serde(untagged)] 371 | enum ConfigGrabDeserializer { 372 | Enum( 373 | #[serde(deserialize_with = "ConfigGrab::deserialize")] 374 | ConfigGrab 375 | ), 376 | Plain(ConfigGrabDeserializerPlain), 377 | } 378 | 379 | impl From for ConfigGrab { 380 | fn from(v: ConfigGrabDeserializer) -> Self { 381 | match v { 382 | ConfigGrabDeserializer::Enum(v) => v, 383 | ConfigGrabDeserializer::Plain(e) => match e { 384 | ConfigGrabDeserializerPlain::X => ConfigGrab::X { 385 | confine: true, 386 | mouse: true, 387 | ignore: ConfigGrab::default_ignore(), 388 | devices: Default::default(), 389 | }, 390 | }, 391 | } 392 | } 393 | } 394 | 395 | ConfigGrabDeserializer::deserialize(deserializer) 396 | .map(From::from) 397 | } 398 | } 399 | 400 | impl Serialize for ConfigGrab { 401 | fn serialize(&self, serializer: S) -> Result { 402 | ConfigGrab::serialize(self, serializer) 403 | } 404 | } 405 | 406 | impl ConfigGrab { 407 | fn default_ignore() -> Vec { 408 | vec![ConfigInputEvent::Absolute] 409 | } 410 | 411 | pub fn mode(&self) -> ConfigGrabMode { 412 | match *self { 413 | ConfigGrab::X { .. } => ConfigGrabMode::X, 414 | ConfigGrab::Evdev { .. } => ConfigGrabMode::Evdev, 415 | } 416 | } 417 | } 418 | 419 | impl Default for ConfigGrab { 420 | fn default() -> Self { 421 | ConfigGrab::X { 422 | confine: true, 423 | mouse: true, 424 | ignore: Self::default_ignore(), 425 | devices: Default::default(), 426 | } 427 | } 428 | } 429 | 430 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] 431 | #[serde(rename_all = "lowercase")] 432 | pub enum ConfigGrabMode { 433 | Evdev, 434 | X, 435 | } 436 | 437 | impl Default for ConfigGrabMode { 438 | fn default() -> Self { 439 | ConfigGrabMode::X 440 | } 441 | } 442 | 443 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 444 | #[serde(deny_unknown_fields)] 445 | pub struct ConfigMonitor { 446 | #[serde(default, skip_serializing_if = "Option::is_none")] 447 | pub id: Option, 448 | #[serde(default, skip_serializing_if = "Option::is_none")] 449 | pub manufacturer: Option, 450 | #[serde(default, skip_serializing_if = "Option::is_none")] 451 | pub model: Option, 452 | #[serde(default, skip_serializing_if = "Option::is_none")] 453 | pub serial: Option, 454 | //#[serde(default, skip_serializing_if = "Option::is_none")] 455 | // pub path: Option, 456 | #[serde(default, skip_serializing_if = "Option::is_none")] 457 | pub xrandr_name: Option, 458 | } 459 | 460 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 461 | #[serde(deny_unknown_fields)] 462 | pub struct ConfigSource { 463 | #[serde(default, skip_serializing_if = "Option::is_none")] 464 | pub value: Option, 465 | #[serde(default, skip_serializing_if = "Option::is_none")] 466 | pub name: Option, 467 | } 468 | 469 | impl ConfigSource { 470 | pub fn value(&self) -> Option { 471 | self.value.or(self.name.as_ref().map(ConfigSourceName::value)) 472 | } 473 | } 474 | 475 | #[derive(Debug, Copy, Clone, Deserialize, Serialize)] 476 | #[repr(u8)] 477 | pub enum ConfigSourceName { 478 | #[serde(rename = "Analog-1")] 479 | Analog1 = 0x01, 480 | #[serde(rename = "Analog-2")] 481 | Analog2 = 0x02, 482 | #[serde(rename = "DVI-1")] 483 | DVI1 = 0x03, 484 | #[serde(rename = "DVI-2")] 485 | DVI2 = 0x04, 486 | #[serde(rename = "Composite-1")] 487 | Composite1 = 0x05, 488 | #[serde(rename = "Composite-2")] 489 | Composite2 = 0x06, 490 | #[serde(rename = "S-video-1")] 491 | SVideo1 = 0x07, 492 | #[serde(rename = "S-video-2")] 493 | SVideo2 = 0x08, 494 | #[serde(rename = "Tuner-1")] 495 | Tuner1 = 0x09, 496 | #[serde(rename = "Tuner-2")] 497 | Tuner2 = 0x0a, 498 | #[serde(rename = "Tuner-3")] 499 | Tuner3 = 0x0b, 500 | #[serde(rename = "Component-1")] 501 | Component1 = 0x0c, 502 | #[serde(rename = "Component-2")] 503 | Component2 = 0x0d, 504 | #[serde(rename = "Component-3")] 505 | Component3 = 0x0e, 506 | #[serde(rename = "DisplayPort-1")] 507 | DisplayPort1 = 0x0f, 508 | #[serde(rename = "DisplayPort-2")] 509 | DisplayPort2 = 0x10, 510 | #[serde(rename = "HDMI-1")] 511 | HDMI1 = 0x11, 512 | #[serde(rename = "HDMI-2")] 513 | HDMI2 = 0x12, 514 | } 515 | 516 | impl fmt::Display for ConfigSourceName { 517 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 518 | self.serialize(f) 519 | } 520 | } 521 | 522 | impl ConfigSourceName { 523 | pub fn value(&self) -> u8 { 524 | *self as _ 525 | } 526 | 527 | pub fn from_value(value: u8) -> Option { 528 | match value { 529 | 1..=0x12 => Some(unsafe { Self::from_value_unchecked(value) }), 530 | _ => None, 531 | } 532 | } 533 | 534 | pub unsafe fn from_value_unchecked(value: u8) -> Self { 535 | core::mem::transmute(value) 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /ddc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screenstub-ddc" 3 | version = "0.0.1" 4 | edition = "2018" 5 | 6 | include = [ 7 | "/src/**/*.rs", 8 | ] 9 | 10 | [dependencies] 11 | ddcutil = { version = "^0.0.3", optional = true } 12 | ddc-hi = { version = "^0.4.0", optional = true } 13 | mccs = { version = "^0.1.0", optional = true } 14 | anyhow = "^1.0.42" 15 | 16 | [features] 17 | with-ddcutil = ["ddcutil"] 18 | with-ddc = ["ddc-hi", "mccs"] 19 | -------------------------------------------------------------------------------- /ddc/src/ddc.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use ddc_hi::{Display, Query, Ddc}; 3 | use crate::{SearchDisplay, DdcMonitor, FEATURE_CODE_INPUT}; 4 | use anyhow::Error; 5 | 6 | pub struct Monitor { 7 | display: Display, 8 | sources: Vec, 9 | } 10 | 11 | impl From for Monitor { 12 | fn from(display: Display) -> Self { 13 | Self { 14 | display, 15 | sources: Default::default(), 16 | } 17 | } 18 | } 19 | 20 | fn query(search: &SearchDisplay) -> Query { 21 | let mut query = Query::Any; 22 | 23 | if let Some(id) = search.backend_id.as_ref() { 24 | query = Query::And(vec![ 25 | query, 26 | Query::Id(id.into()), 27 | ]); 28 | } 29 | 30 | if let Some(manufacturer) = search.manufacturer_id.as_ref() { 31 | query = Query::And(vec![ 32 | query, 33 | Query::ManufacturerId(manufacturer.into()), 34 | ]); 35 | } 36 | 37 | if let Some(model) = search.model_name.as_ref() { 38 | query = Query::And(vec![ 39 | query, 40 | Query::ModelName(model.into()), 41 | ]); 42 | } 43 | 44 | if let Some(serial) = search.serial_number.as_ref() { 45 | query = Query::And(vec![ 46 | query, 47 | Query::SerialNumber(serial.into()), 48 | ]); 49 | } 50 | 51 | query 52 | } 53 | 54 | impl DdcMonitor for Monitor { 55 | type Error = Error; 56 | 57 | fn enumerate() -> Result, Self::Error> where Self: Sized { 58 | Ok(Display::enumerate().into_iter().map(From::from).collect()) 59 | } 60 | 61 | fn matches(&self, search: &SearchDisplay) -> bool { 62 | let query = query(search); 63 | query.matches(&self.display.info) 64 | } 65 | 66 | fn sources(&mut self) -> Result, Self::Error> { 67 | if self.sources.is_empty() { 68 | let caps = self.display.handle.capabilities()?; 69 | self.sources = caps.vcp_features 70 | .get(&FEATURE_CODE_INPUT) 71 | .map(|desc| desc.values.iter().map(|(&value, _)| value).collect()) 72 | .unwrap_or_default(); 73 | } 74 | Ok(self.sources.clone()) 75 | } 76 | 77 | fn get_source(&mut self) -> Result { 78 | self.display.handle.get_vcp_feature(FEATURE_CODE_INPUT) 79 | .map(|v| v.value() as u8) 80 | } 81 | 82 | fn set_source(&mut self, value: u8) -> Result<(), Self::Error> { 83 | self.display.handle.set_vcp_feature(FEATURE_CODE_INPUT, value as u16) 84 | } 85 | } 86 | 87 | impl fmt::Display for Monitor { 88 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 89 | writeln!(f, "ID: {}", self.display.info.id)?; 90 | if let Some(mf) = self.display.info.manufacturer_id.as_ref() { 91 | writeln!(f, "Manufacturer: {}", mf)? 92 | } 93 | if let Some(model) = self.display.info.model_name.as_ref() { 94 | writeln!(f, "Model: {}", model)? 95 | } 96 | if let Some(serial) = self.display.info.serial_number.as_ref() { 97 | writeln!(f, "Serial: {}", serial)? 98 | } 99 | 100 | Ok(()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /ddc/src/ddcutil.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::mem::replace; 3 | use std::fmt; 4 | use anyhow::Error; 5 | use ddcutil::{DisplayInfo, Display, FeatureInfo}; 6 | use crate::{DdcError, DdcMonitor, SearchDisplay, FEATURE_CODE_INPUT}; 7 | 8 | impl SearchDisplay { 9 | pub fn matches(&self, info: &DisplayInfo) -> bool { 10 | let matches = [ 11 | (&info.manufacturer_id(), &self.manufacturer_id), 12 | (&info.model_name(), &self.model_name), 13 | (&info.serial_number(), &self.serial_number), 14 | ].iter().filter_map(|&(i, m)| m.as_ref().map(|m| (i, m))) 15 | .all(|(i, m)| i == m); 16 | 17 | if let Some(ref path) = self.path { 18 | matches && path == &info.path() 19 | } else { 20 | matches 21 | } 22 | } 23 | } 24 | 25 | #[derive(Debug)] 26 | pub enum Monitor { 27 | #[doc(hidden)] 28 | Search(SearchDisplay), 29 | #[doc(hidden)] 30 | Display { 31 | info: DisplayInfo, 32 | display: Display, 33 | input_values: HashMap, 34 | our_input: Option, 35 | search: SearchDisplay, 36 | }, 37 | } 38 | 39 | impl Monitor { 40 | pub fn new(search: SearchDisplay) -> Self { 41 | Monitor::Search(search) 42 | } 43 | 44 | pub fn enumerate() -> Result, Error> { 45 | DisplayInfo::enumerate()?.into_iter().map(|i| 46 | Self::from_display_info(i, None) 47 | ).collect() 48 | } 49 | 50 | pub fn search(&self) -> &SearchDisplay { 51 | match *self { 52 | Monitor::Search(ref search) => search, 53 | Monitor::Display { ref search, .. } => search, 54 | } 55 | } 56 | 57 | pub fn info(&self) -> Option<&DisplayInfo> { 58 | match *self { 59 | Monitor::Search(..) => None, 60 | Monitor::Display { ref info, .. } => Some(info), 61 | } 62 | } 63 | 64 | pub fn inputs(&self) -> Option<&HashMap> { 65 | match *self { 66 | Monitor::Search(..) => None, 67 | Monitor::Display { ref input_values, .. } => Some(input_values), 68 | } 69 | } 70 | 71 | pub fn our_input(&self) -> Option { 72 | match *self { 73 | Monitor::Search(..) => None, 74 | Monitor::Display { our_input, .. } => our_input, 75 | } 76 | } 77 | 78 | pub fn other_inputs(&self) -> Vec<(u8, &str)> { 79 | let ours = self.our_input(); 80 | self.inputs().map(|inputs| inputs.iter().filter(|&(&i, _)| Some(i) != ours) 81 | .map(|(i, s)| (*i, &s[..])).collect() 82 | ).unwrap_or(Vec::new()) 83 | } 84 | 85 | pub fn from_display_info(info: DisplayInfo, search: Option<&mut SearchDisplay>) -> Result { 86 | let display = info.open()?; 87 | let caps = display.capabilities()?; 88 | let input_caps = caps.features.get(&FEATURE_CODE_INPUT).ok_or(DdcError::FeatureCodeNotFound)?; 89 | let mut input_info = FeatureInfo::from_code(FEATURE_CODE_INPUT, caps.version)?; 90 | let input_values = input_caps.into_iter().map(|&val| 91 | (val, input_info.value_names.remove(&val).unwrap_or_else(|| "Unknown".into())) 92 | ).collect(); 93 | let our_input = display.vcp_get_value(FEATURE_CODE_INPUT).ok().map(|v| v.value() as u8); 94 | 95 | let search = if let Some(search) = search { 96 | replace(search, Default::default()) 97 | } else { 98 | Default::default() 99 | }; 100 | Ok(Monitor::Display { 101 | info, 102 | display, 103 | input_values, 104 | our_input, 105 | search, 106 | }) 107 | } 108 | 109 | fn find_display(&mut self) -> Result, Error> { 110 | match *self { 111 | Monitor::Search(ref mut search) => { 112 | let displays = DisplayInfo::enumerate()?; 113 | if let Some(info) = displays.into_iter().find(|d| search.matches(d)) { 114 | Self::from_display_info(info, Some(search)).map(Some) 115 | } else { 116 | Err(DdcError::DisplayNotFound.into()) 117 | } 118 | }, 119 | Monitor::Display { .. } => Ok(None), 120 | } 121 | } 122 | 123 | pub fn to_display(&mut self) -> Result<(), Error> { 124 | if let Some(monitor) = self.find_display()? { 125 | *self = monitor 126 | } 127 | 128 | Ok(()) 129 | } 130 | 131 | pub fn reset_handle(&mut self) { 132 | let search = match *self { 133 | Monitor::Search(..) => return, 134 | Monitor::Display { ref mut search, .. } => replace(search, Default::default()), 135 | }; 136 | 137 | *self = Monitor::Search(search); 138 | } 139 | 140 | pub fn display(&mut self) -> Result<&Display, Error> { 141 | self.to_display()?; 142 | 143 | match *self { 144 | Monitor::Search(..) => Err(DdcError::DisplayNotFound.into()), 145 | Monitor::Display { ref display, .. } => Ok(display), 146 | } 147 | } 148 | } 149 | 150 | impl DdcMonitor for Monitor { 151 | type Error = Error; 152 | 153 | fn search(search: &SearchDisplay) -> Result, Self::Error> where Self: Sized { 154 | let mut res = Monitor::Search(search.clone()); 155 | res.to_display()?; // TODO: can't distinguish not found vs connection error 156 | Ok(Some(res)) 157 | } 158 | 159 | fn matches(&self, search: &SearchDisplay) -> bool { 160 | match self { 161 | Monitor::Search(s) => s == search, 162 | Monitor::Display { info, .. } => search.matches(info), 163 | } 164 | } 165 | 166 | fn enumerate() -> Result, Self::Error> where Self: Sized { 167 | DisplayInfo::enumerate()?.into_iter().map(|i| 168 | Self::from_display_info(i, None) 169 | ).collect() 170 | } 171 | 172 | fn sources(&mut self) -> Result, Self::Error> { 173 | self.to_display()?; 174 | match self { 175 | Monitor::Search(..) => Ok(Default::default()), 176 | Monitor::Display { input_values, .. } => Ok(input_values 177 | .iter().map(|(&value, _)| value).collect() 178 | ), 179 | } 180 | } 181 | 182 | fn get_source(&mut self) -> Result { 183 | Ok(self.display()?.vcp_get_value(FEATURE_CODE_INPUT)?.value() as u8) 184 | } 185 | 186 | fn set_source(&mut self, value: u8) -> Result<(), Self::Error> { 187 | self.display()?.vcp_set_simple(FEATURE_CODE_INPUT, value).map_err(From::from) 188 | } 189 | } 190 | 191 | impl Default for Monitor { 192 | fn default() -> Self { 193 | Self::new(Default::default()) 194 | } 195 | } 196 | 197 | impl fmt::Display for Monitor { 198 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 199 | let info = match self { 200 | Monitor::Display { info, .. } => info, 201 | _ => return Err(fmt::Error), 202 | }; 203 | writeln!(f, "Manufacturer: {}\nModel: {}\nSerial: {}", 204 | info.manufacturer_id(), info.model_name(), info.serial_number() 205 | ) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /ddc/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use anyhow::{Error, format_err}; 3 | 4 | #[cfg(feature = "ddcutil")] 5 | pub mod ddcutil; 6 | 7 | #[cfg(feature = "ddc-hi")] 8 | pub mod ddc; 9 | 10 | pub const FEATURE_CODE_INPUT: u8 = 0x60; 11 | 12 | #[derive(Debug)] 13 | pub enum DdcError { 14 | DisplayNotFound, 15 | FeatureCodeNotFound, 16 | } 17 | 18 | impl fmt::Display for DdcError { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | let msg = match self { 21 | DdcError::DisplayNotFound => "Display not found", 22 | DdcError::FeatureCodeNotFound => "Feature code not found", 23 | }; 24 | f.write_str(msg) 25 | } 26 | } 27 | 28 | impl std::error::Error for DdcError { } 29 | 30 | #[derive(Debug, Clone, Default, PartialEq)] 31 | pub struct SearchDisplay { 32 | pub backend_id: Option, 33 | pub manufacturer_id: Option, 34 | pub model_name: Option, 35 | pub serial_number: Option, 36 | #[cfg(feature = "ddcutil")] 37 | pub path: Option<::ddcutil::DisplayPath>, 38 | #[cfg(not(feature = "ddcutil"))] 39 | pub path: Option<()>, 40 | } 41 | 42 | pub trait DdcMonitor: fmt::Display { 43 | type Error: Into; 44 | 45 | fn search(search: &SearchDisplay) -> Result, Self::Error> where Self: Sized { 46 | Self::enumerate().map(|displays| 47 | displays.into_iter().find(|d| d.matches(search)) 48 | ) 49 | } 50 | 51 | fn matches(&self, search: &SearchDisplay) -> bool; 52 | 53 | fn enumerate() -> Result, Self::Error> where Self: Sized; 54 | 55 | fn sources(&mut self) -> Result, Self::Error>; 56 | 57 | fn get_source(&mut self) -> Result; 58 | fn set_source(&mut self, value: u8) -> Result<(), Self::Error>; 59 | 60 | fn find_guest_source(&mut self, our_source: u8) -> Result, Self::Error> { 61 | self.sources() 62 | .map(|sources| find_guest_source(&sources, our_source)) 63 | } 64 | } 65 | 66 | pub fn find_guest_source(sources: &[u8], our_source: u8) -> Option { 67 | sources.iter().copied() 68 | .filter(|&value| value != our_source) 69 | .next() 70 | } 71 | 72 | pub struct DummyMonitor; 73 | 74 | impl DummyMonitor { 75 | fn error() -> Error { 76 | format_err!("ddc disabled") 77 | } 78 | } 79 | 80 | impl DdcMonitor for DummyMonitor { 81 | type Error = Error; 82 | 83 | fn search(_search: &SearchDisplay) -> Result, Self::Error> { 84 | Err(Self::error()) 85 | } 86 | 87 | fn matches(&self, _search: &SearchDisplay) -> bool { 88 | false 89 | } 90 | 91 | fn enumerate() -> Result, Self::Error> { 92 | Err(Self::error()) 93 | } 94 | 95 | fn sources(&mut self) -> Result, Self::Error> { 96 | Err(Self::error()) 97 | } 98 | 99 | fn get_source(&mut self) -> Result { 100 | Err(Self::error()) 101 | } 102 | 103 | fn set_source(&mut self, _value: u8) -> Result<(), Self::Error> { 104 | Err(Self::error()) 105 | } 106 | 107 | fn find_guest_source(&mut self, _our_source: u8) -> Result, Self::Error> { 108 | Err(Self::error()) 109 | } 110 | } 111 | 112 | impl fmt::Display for DummyMonitor { 113 | fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result { 114 | panic!("{}", Self::error()) 115 | } 116 | } 117 | 118 | #[cfg(feature = "ddc-hi")] 119 | pub type Monitor = ddc::Monitor; 120 | 121 | #[cfg(all(not(feature = "ddc-hi"), feature = "ddcutil"))] 122 | pub type Monitor = ddcutil::Monitor; 123 | 124 | #[cfg(not(any(feature = "ddcutil", feature = "ddc-hi")))] 125 | pub type Monitor = DummyMonitor; 126 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | lockData = builtins.fromJSON (builtins.readFile ./flake.lock); 3 | sourceInfo = lockData.nodes.std.locked; 4 | src = fetchTarball { 5 | url = "https://github.com/${sourceInfo.owner}/${sourceInfo.repo}/archive/${sourceInfo.rev}.tar.gz"; 6 | sha256 = sourceInfo.narHash; 7 | }; 8 | in (import src).Flake.Bootstrap { 9 | path = ./.; 10 | inherit lockData; 11 | loadWith.defaultPackage = "screenstub"; 12 | } 13 | -------------------------------------------------------------------------------- /derivation.nix: -------------------------------------------------------------------------------- 1 | let 2 | self = import ./. { pkgs = null; system = null; }; 3 | in { 4 | rustPlatform 5 | , udev 6 | , libxcb ? xorg.libxcb, xorg ? { } 7 | , python3, pkg-config 8 | , hostPlatform 9 | , lib 10 | , libiconv, CoreGraphics ? darwin.apple_sdk.frameworks.CoreGraphics, darwin 11 | , buildType ? "release" 12 | , cargoLock ? crate.cargoLock 13 | , source ? crate.src 14 | , crate ? self.lib.crate 15 | }: with lib; rustPlatform.buildRustPackage { 16 | pname = crate.name; 17 | inherit (crate) version; 18 | 19 | buildInputs = [ libxcb ] ++ 20 | optionals hostPlatform.isLinux [ udev ] 21 | ++ optionals hostPlatform.isDarwin [ libiconv CoreGraphics ]; 22 | nativeBuildInputs = [ pkg-config python3 ]; 23 | 24 | src = source; 25 | inherit cargoLock buildType; 26 | doCheck = false; 27 | 28 | meta = { 29 | description = "DDC/CI display control application"; 30 | homepage = "https://github.com/arcnmx/ddcset-rs"; 31 | license = licenses.mit; 32 | maintainers = [ maintainers.arcnmx ]; 33 | platforms = platforms.unix; 34 | mainProgram = "screenstub"; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /event/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screenstub-event" 3 | version = "0.0.1" 4 | edition = "2018" 5 | 6 | include = [ 7 | "/src/**/*.rs", 8 | ] 9 | 10 | [dependencies] 11 | input-linux = "0.6" 12 | smallvec = "^1.4.1" 13 | screenstub-x = { path = "../x" } 14 | log = "^0.4.1" 15 | -------------------------------------------------------------------------------- /event/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Mutex; 3 | use std::{slice, iter}; 4 | use smallvec::{SmallVec, smallvec}; 5 | use input_linux::{ 6 | EventRef, EventMut, InputEvent, SynchronizeEvent, 7 | KeyEvent, Key, KeyState, 8 | Bitmask, 9 | }; 10 | use log::warn; 11 | use screenstub_x::XEvent; 12 | 13 | #[derive(Debug)] 14 | pub enum UserEvent { 15 | Quit, 16 | ShowGuest, 17 | ShowHost, 18 | UnstickGuest, 19 | UnstickHost, 20 | } 21 | 22 | #[derive(Debug, Clone)] 23 | pub struct Hotkey { 24 | triggers: Vec, 25 | modifiers: Vec, 26 | events: Vec, 27 | } 28 | 29 | impl Hotkey { 30 | pub fn new, M: IntoIterator, E: IntoIterator>(triggers: T, modifiers: M, events: E) -> Self { 31 | Hotkey { 32 | triggers: triggers.into_iter().collect(), 33 | modifiers: modifiers.into_iter().collect(), 34 | events: events.into_iter().collect(), 35 | } 36 | } 37 | 38 | pub fn keys(&self) -> iter::Cloned, slice::Iter>> { 39 | self.triggers.iter().chain(self.modifiers.iter()).cloned() 40 | } 41 | } 42 | 43 | #[derive(Debug)] 44 | pub struct Events { 45 | triggers_press: HashMap>>, 46 | triggers_release: HashMap>>, 47 | remap: HashMap, 48 | keys: Mutex>, 49 | } 50 | 51 | #[derive(Debug)] 52 | pub enum ProcessedXEvent { 53 | UserEvent(UserEvent), 54 | InputEvent(InputEvent), 55 | } 56 | 57 | impl From for ProcessedXEvent { 58 | fn from(e: UserEvent) -> Self { 59 | ProcessedXEvent::UserEvent(e) 60 | } 61 | } 62 | 63 | impl> From for ProcessedXEvent { 64 | fn from(e: I) -> Self { 65 | ProcessedXEvent::InputEvent(e.into()) 66 | } 67 | } 68 | 69 | impl Events { 70 | pub fn new() -> Self { 71 | Events { 72 | triggers_press: Default::default(), 73 | triggers_release: Default::default(), 74 | remap: Default::default(), 75 | keys: Default::default(), 76 | } 77 | } 78 | 79 | pub fn add_hotkey(&mut self, hotkey: Hotkey, on_press: bool) where U: Clone { 80 | for &key in &hotkey.triggers { 81 | if on_press { 82 | &mut self.triggers_press 83 | } else { 84 | &mut self.triggers_release 85 | }.entry(key).or_insert(Default::default()).push(hotkey.clone()) 86 | } 87 | } 88 | 89 | pub fn add_remap(&mut self, from: Key, to: Key) { 90 | self.remap.insert(from, to); 91 | } 92 | 93 | pub fn map_input_event(&self, mut e: InputEvent) -> InputEvent { 94 | match EventMut::new(&mut e) { 95 | Ok(EventMut::Key(key)) => if let Some(remap) = self.remap.get(&key.key) { 96 | key.key = *remap; 97 | }, 98 | _ => (), 99 | } 100 | 101 | e 102 | } 103 | 104 | pub fn process_input_event<'a>(&'a self, e: &InputEvent) -> Vec<&'a U> { 105 | match EventRef::new(e) { 106 | Ok(e) => self.process_input_event_(e), 107 | Err(err) => { 108 | warn!("Unable to parse input event {:?} due to {:?}", e, err); 109 | Default::default() 110 | }, 111 | } 112 | } 113 | 114 | fn process_input_event_<'a>(&'a self, e: EventRef) -> Vec<&'a U> { 115 | match e { 116 | EventRef::Key(key) => { 117 | let state = key.value; 118 | 119 | let hotkeys = match state { 120 | KeyState::PRESSED => self.triggers_press.get(&key.key), 121 | KeyState::RELEASED => self.triggers_release.get(&key.key), 122 | _ => None, 123 | }; 124 | 125 | let mut keys = self.keys.lock().unwrap(); 126 | match state { 127 | KeyState::PRESSED => keys.insert(key.key), 128 | _ => (), 129 | } 130 | 131 | let events = if let Some(hotkeys) = hotkeys { 132 | hotkeys.iter() 133 | .filter(|h| h.keys().all(|k| keys.get(k))) 134 | .filter(|h| h.triggers.contains(&key.key)) 135 | .flat_map(|h| h.events.iter()) 136 | .collect() 137 | } else { 138 | Default::default() 139 | }; 140 | 141 | match state { 142 | KeyState::PRESSED => (), 143 | KeyState::RELEASED => keys.remove(key.key), 144 | state => warn!("Unknown key state {:?}", state), 145 | } 146 | 147 | events 148 | }, 149 | _ => Default::default(), 150 | } 151 | } 152 | 153 | pub fn process_x_event(&self, e: &XEvent) -> impl Iterator { 154 | match *e { 155 | XEvent::Close => 156 | smallvec![UserEvent::Quit.into()], 157 | XEvent::Visible(visible) => smallvec![if visible { 158 | UserEvent::ShowGuest.into() 159 | } else { 160 | UserEvent::ShowHost.into() 161 | }], 162 | XEvent::Focus(focus) => if !focus { 163 | self.unstick_guest_() 164 | } else { 165 | Default::default() 166 | }, 167 | XEvent::Input(e) => { 168 | smallvec![e.into()] 169 | }, 170 | }.into_iter() 171 | } 172 | 173 | fn unstick_events_<'a>(keys: &'a Bitmask) -> impl Iterator + 'a { 174 | keys.iter().map(|key| 175 | KeyEvent::new(Default::default(), key, KeyState::RELEASED).into() 176 | ).chain(iter::once(SynchronizeEvent::report(Default::default()).into())) 177 | } 178 | 179 | pub fn unstick_guest(&self) -> impl Iterator + Send { 180 | let mut keys = self.keys.lock().unwrap(); 181 | let res: SmallVec<[InputEvent; 4]> = Self::unstick_events_(&keys).collect(); 182 | keys.clear(); 183 | res.into_iter() 184 | } 185 | 186 | fn unstick_guest_(&self) -> SmallVec<[ProcessedXEvent; 4]> { 187 | // sad duplicate because it seems slightly more efficient :( 188 | let mut keys = self.keys.lock().unwrap(); 189 | let res = Self::unstick_events_(&keys).map(From::from).collect(); 190 | keys.clear(); 191 | res 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /fd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screenstub-fd" 3 | version = "0.0.1" 4 | edition = "2018" 5 | 6 | include = [ 7 | "/src/**/*.rs", 8 | ] 9 | -------------------------------------------------------------------------------- /fd/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::io::{AsRawFd, RawFd}; 2 | 3 | pub type FdRef<'a, T> = Fd<&'a T>; 4 | 5 | #[derive(Copy, Clone, Debug)] 6 | pub struct Fd(pub T); 7 | 8 | impl<'a, T: AsRawFd> AsRawFd for Fd<&'a T> { 9 | fn as_raw_fd(&self) -> RawFd { 10 | self.0.as_raw_fd() 11 | } 12 | } 13 | 14 | impl AsRawFd for Fd { 15 | fn as_raw_fd(&self) -> RawFd { 16 | self.0 17 | } 18 | } 19 | 20 | impl From for Fd { 21 | fn from(fd: T) -> Self { 22 | Self(fd) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fl-config": { 4 | "locked": { 5 | "lastModified": 1653159448, 6 | "narHash": "sha256-PvB9ha0r4w6p412MBPP71kS/ZTBnOjxL0brlmyucPBA=", 7 | "owner": "flakelib", 8 | "repo": "fl", 9 | "rev": "fcefb9738d5995308a24cda018a083ccb6b0f460", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "flakelib", 14 | "ref": "config", 15 | "repo": "fl", 16 | "type": "github" 17 | } 18 | }, 19 | "flakelib": { 20 | "inputs": { 21 | "fl-config": "fl-config", 22 | "std": "std" 23 | }, 24 | "locked": { 25 | "lastModified": 1671242652, 26 | "narHash": "sha256-pZYiNhTzCn0/Caw8aQJekJPxMfPf44PSwme+1yJm+5A=", 27 | "owner": "flakelib", 28 | "repo": "fl", 29 | "rev": "96ca3638fb39a0ae880ef442479f26bd82bebe20", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "flakelib", 34 | "repo": "fl", 35 | "type": "github" 36 | } 37 | }, 38 | "nix-std": { 39 | "locked": { 40 | "lastModified": 1671170529, 41 | "narHash": "sha256-015C6x3tZMEd83Vd2rpfLC86PSRJrbUca1U3Rysranw=", 42 | "owner": "chessai", 43 | "repo": "nix-std", 44 | "rev": "3b307d64ef7d7e8769d36b8c8bf33983efd1415a", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "chessai", 49 | "repo": "nix-std", 50 | "type": "github" 51 | } 52 | }, 53 | "nixpkgs": { 54 | "locked": { 55 | "lastModified": 1679850165, 56 | "narHash": "sha256-Aub2Uvl28f8IG92AKDLv1kHp6XmH8vsZR2DEkjbvQ78=", 57 | "owner": "NixOS", 58 | "repo": "nixpkgs", 59 | "rev": "a0be54df4cd0992896d34ff12264a1c9faff7dc5", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "id": "nixpkgs", 64 | "type": "indirect" 65 | } 66 | }, 67 | "root": { 68 | "inputs": { 69 | "flakelib": "flakelib", 70 | "nixpkgs": "nixpkgs", 71 | "rust": "rust" 72 | } 73 | }, 74 | "rust": { 75 | "inputs": { 76 | "nixpkgs": [ 77 | "nixpkgs" 78 | ] 79 | }, 80 | "locked": { 81 | "lastModified": 1678977050, 82 | "narHash": "sha256-1dJvJ3BVCp7dau6A8ag+uYSwqGvebGhdau3bD2q5kkU=", 83 | "owner": "arcnmx", 84 | "repo": "nixexprs-rust", 85 | "rev": "2f05575a23624c7325f6a5f03100fd5a82ce9b6e", 86 | "type": "github" 87 | }, 88 | "original": { 89 | "owner": "arcnmx", 90 | "repo": "nixexprs-rust", 91 | "type": "github" 92 | } 93 | }, 94 | "std": { 95 | "inputs": { 96 | "nix-std": "nix-std" 97 | }, 98 | "locked": { 99 | "lastModified": 1671225084, 100 | "narHash": "sha256-EzqxFHRifPyjUXQY4B8GJH75fTmWqFnQdj10Q984bR8=", 101 | "owner": "flakelib", 102 | "repo": "std", 103 | "rev": "8546115941a5498ddb03a239aacdba151d433f09", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "flakelib", 108 | "repo": "std", 109 | "type": "github" 110 | } 111 | } 112 | }, 113 | "root": "root", 114 | "version": 7 115 | } 116 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A VFIO KVM switch using DDC/CI"; 3 | inputs = { 4 | flakelib.url = "github:flakelib/fl"; 5 | nixpkgs = { }; 6 | rust = { 7 | url = "github:arcnmx/nixexprs-rust"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | }; 11 | outputs = { self, flakelib, nixpkgs, rust, ... }@inputs: let 12 | nixlib = nixpkgs.lib; 13 | in flakelib { 14 | inherit inputs; 15 | systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 16 | devShells = { 17 | plain = { 18 | mkShell, writeShellScriptBin, hostPlatform 19 | , udev 20 | , libxcb ? xorg.libxcb, xorg ? { } 21 | , pkg-config, python3 22 | , libiconv 23 | , CoreGraphics ? darwin.apple_sdk.frameworks.CoreGraphics, darwin 24 | , enableRust ? true, cargo 25 | , rustTools ? [ ] 26 | , nativeBuildInputs ? [ ] 27 | }: mkShell { 28 | inherit rustTools; 29 | buildInputs = [ libxcb ] 30 | ++ nixlib.optional hostPlatform.isLinux udev 31 | ++ nixlib.optionals hostPlatform.isDarwin [ libiconv CoreGraphics ]; 32 | nativeBuildInputs = [ pkg-config python3 ] 33 | ++ nixlib.optional enableRust cargo 34 | ++ nativeBuildInputs ++ [ 35 | (writeShellScriptBin "generate" ''nix run .#generate "$@"'') 36 | ]; 37 | RUST_LOG = "debug"; 38 | }; 39 | stable = { rust'stable, outputs'devShells'plain }: outputs'devShells'plain.override { 40 | inherit (rust'stable) mkShell; 41 | enableRust = false; 42 | }; 43 | dev = { rust'unstable, outputs'devShells'plain }: outputs'devShells'plain.override { 44 | inherit (rust'unstable) mkShell; 45 | enableRust = false; 46 | rustTools = [ "rust-analyzer" ]; 47 | }; 48 | default = { outputs'devShells }: outputs'devShells.plain; 49 | }; 50 | packages = { 51 | screenstub = { 52 | __functor = _: import ./derivation.nix; 53 | fl'config.args = { 54 | crate.fallback = self.lib.crate; 55 | }; 56 | }; 57 | default = { screenstub }: screenstub; 58 | }; 59 | legacyPackages = { callPackageSet }: callPackageSet { 60 | source = { rust'builders }: rust'builders.wrapSource self.lib.crate.src; 61 | 62 | generate = { rust'builders, outputHashes }: rust'builders.generateFiles { 63 | paths = { 64 | "lock.nix" = outputHashes; 65 | }; 66 | }; 67 | outputHashes = { rust'builders }: rust'builders.cargoOutputHashes { 68 | inherit (self.lib) crate; 69 | }; 70 | } { }; 71 | checks = { 72 | ${if false then "rustfmt" else null} = { rust'builders, screenstub }: rust'builders.check-rustfmt-unstable { 73 | inherit (screenstub) src; 74 | config = ./.rustfmt.toml; 75 | }; 76 | check-config = { screenstub, runCommand }: runCommand "screenstub-check-config" { 77 | nativeBuildInputs = [ screenstub ]; 78 | sampleConfig = ./samples/config.yml; 79 | } '' 80 | screenstub --config $sampleConfig check-config > $out 81 | ''; 82 | }; 83 | lib = with nixlib; { 84 | crate = rust.lib.importCargo { 85 | path = ./Cargo.toml; 86 | inherit (import ./lock.nix) outputHashes; 87 | }; 88 | inherit (self.lib.crate) version; 89 | releaseTag = "v${self.lib.version}"; 90 | }; 91 | config = rec { 92 | name = "screenstub"; 93 | }; 94 | overlays = { 95 | screenstub = final: prev: { 96 | screenstub = final.callPackage ./derivation.nix { 97 | inherit (self.lib) crate; 98 | }; 99 | }; 100 | default = self.overlays.screenstub; 101 | }; 102 | } // (let 103 | config = { 104 | _module.args.inputs'screenstub = nixlib.mkDefault self; 105 | }; 106 | in { 107 | nixosModules = { 108 | screenstub = { lib, ... }: { 109 | imports = [ ./nixos.nix ]; 110 | inherit config; 111 | }; 112 | default = self.nixosModules.screenstub; 113 | }; 114 | homeModules = { 115 | screenstub = { lib, ... }: { 116 | imports = [ ./home.nix ]; 117 | inherit config; 118 | }; 119 | default = self.homeModules.screenstub; 120 | }; 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /home.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, ... }: with lib; let 2 | cfg = config.programs.screenstub; 3 | in { 4 | imports = [ ./module.nix ]; 5 | 6 | config = mkIf cfg.enable { 7 | home.packages = [ cfg.finalPackage ]; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /lock.nix: -------------------------------------------------------------------------------- 1 | { 2 | outputHashes = { 3 | 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /module.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, inputs'screenstub, ... }: with lib; let 2 | cfg = config.programs.screenstub; 3 | settingsFormat = pkgs.formats.yaml { }; 4 | settingsModule = { config, ... }: { 5 | freeformType = settingsFormat.type; 6 | }; 7 | screenstubPackage = pkgs.callPackage (inputs'screenstub + "/derivation.nix") (optionalAttrs (inputs'screenstub ? lib.crate) { 8 | inherit (inputs'screenstub.lib) crate; 9 | }); 10 | in { 11 | options.programs.screenstub = with types; { 12 | enable = mkEnableOption "screenstub"; 13 | 14 | package = mkOption { 15 | type = package; 16 | default = screenstubPackage; 17 | }; 18 | 19 | finalPackage = mkOption { 20 | type = package; 21 | default = pkgs.writeShellScriptBin "screenstub" '' 22 | exec ${getExe cfg.package} -c ${cfg.configFile} "$@" 23 | ''; 24 | }; 25 | 26 | settings = mkOption { 27 | type = submodule settingsModule; 28 | default = { }; 29 | }; 30 | 31 | configFile = mkOption { 32 | type = path; 33 | default = settingsFormat.generate "screenstub.yml" cfg.settings; 34 | defaultText = literalExpression "config.programs.screenstub.settings"; 35 | }; 36 | }; 37 | 38 | config = { 39 | _module.args.inputs'screenstub = mkOptionDefault { 40 | outPath = ./.; 41 | }; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /nixos.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, ... }: with lib; let 2 | cfg = config.programs.screenstub; 3 | in { 4 | imports = [ ./module.nix ]; 5 | 6 | config = mkIf cfg.enable { 7 | environment.systemPackages = [ cfg.finalPackage ]; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /qemu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screenstub-qemu" 3 | version = "0.0.1" 4 | edition = "2018" 5 | 6 | include = [ 7 | "/src/**/*.rs", 8 | ] 9 | 10 | [dependencies] 11 | futures = "^0.3.5" 12 | anyhow = "^1.0.42" 13 | log = "^0.4.1" 14 | tokio = { version = "1", default-features = false, features = ["time", "sync"] } 15 | qapi = { version = "0.11", features = ["qmp", "qga", "async-tokio-net", "async-tokio-spawn"] } 16 | -------------------------------------------------------------------------------- /qemu/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::sync::{Mutex, Arc, Weak}; 3 | use std::future::Future; 4 | use anyhow::{Error, format_err}; 5 | use futures::{TryFutureExt, StreamExt}; 6 | use futures::future; 7 | use tokio::net::UnixStream; 8 | use tokio::time::{Duration, Instant, sleep, timeout}; 9 | use tokio::io::{ReadHalf, WriteHalf}; 10 | use tokio::sync::broadcast; 11 | use log::{trace, warn, info}; 12 | 13 | pub struct Qemu { 14 | socket_qmp: Option, 15 | socket_qga: Option, 16 | qmp: Mutex>, 17 | event_send: broadcast::Sender, 18 | connection_lock: futures::lock::Mutex<()>, 19 | } 20 | 21 | type QgaWrite = qapi::futures::QgaStreamTokio>; 22 | type QmpRead = qapi::futures::QmpStreamTokio>; 23 | type QmpWrite = qapi::futures::QmpStreamTokio>; 24 | pub type QgaService = qapi::futures::QapiService; 25 | pub type QmpService = qapi::futures::QapiService; 26 | pub type QmpStream = qapi::futures::QapiStream; 27 | pub type QmpEvents = qapi::futures::QapiEvents; 28 | 29 | impl Qemu { 30 | pub fn new(socket_qmp: Option, socket_qga: Option) -> Self { 31 | let (event_send, _event_recv) = broadcast::channel(8); 32 | Qemu { 33 | socket_qmp, 34 | socket_qga, 35 | event_send, 36 | qmp: Mutex::new(Weak::new()), 37 | connection_lock: Default::default(), 38 | } 39 | } 40 | 41 | pub fn qmp_events(&self) -> broadcast::Receiver { 42 | self.event_send.subscribe() 43 | } 44 | 45 | pub async fn connect_qmp(&self) -> Result, Error> { 46 | let _lock = self.connection_lock.lock().await; 47 | let qmp = self.qmp.lock().unwrap().upgrade(); 48 | match qmp { 49 | Some(res) => Ok(res), 50 | None => { 51 | let stream = self.connect_qmp_stream().await?; 52 | let mut qmp = self.qmp.lock().unwrap(); 53 | let (stream, mut events) = stream.into_parts(); 54 | Ok(match qmp.upgrade() { 55 | // if two threads fight for this, just ditch this new connection 56 | Some(qmp) => qmp, 57 | None => { 58 | let res = Arc::new(stream); 59 | *qmp = Arc::downgrade(&res); 60 | let event_send = self.event_send.clone(); 61 | let _ = events.release(); 62 | tokio::spawn(async move { 63 | while let Some(event) = events.next().await { 64 | match event { 65 | Ok(e) => match event_send.send(e) { 66 | Err(e) => { 67 | info!("QMP event ignored: {:?}", e.0); 68 | }, 69 | Ok(_) => (), 70 | }, 71 | Err(e) => { 72 | warn!("QMP stream error: {:?}", e); 73 | break 74 | }, 75 | } 76 | } 77 | }); 78 | res 79 | }, 80 | }) 81 | }, 82 | } 83 | } 84 | 85 | fn connect_qmp_stream(&self) -> impl Future> { 86 | let socket_qmp = self.socket_qmp.as_ref() 87 | .ok_or_else(|| io::Error::new(io::ErrorKind::AddrNotAvailable, "QMP socket not configured")) 88 | .map(|p| p.to_owned()); 89 | 90 | async move { 91 | let stream = qapi::futures::QmpStreamTokio::open_uds(socket_qmp?).await?; 92 | stream.negotiate().await 93 | } 94 | } 95 | 96 | pub fn connect_qga(&self) -> impl Future> { 97 | let socket_qga = self.socket_qga.as_ref() 98 | .ok_or_else(|| io::Error::new(io::ErrorKind::AddrNotAvailable, "QGA socket not configured")) 99 | .map(|p| p.to_owned()); 100 | 101 | async move { 102 | let stream = qapi::futures::QgaStreamTokio::open_uds(socket_qga?).await?; 103 | let (service, _) = stream.spawn_tokio(); 104 | 105 | Ok(service) 106 | } 107 | } 108 | 109 | pub async fn execute_qga(&self, command: C) -> qapi::ExecuteResult { 110 | let qga = self.connect_qga().await?; 111 | qga.execute(command).await 112 | } 113 | 114 | pub async fn execute_qmp(&self, command: C) -> Result { 115 | self.connect_qmp().await?.execute(command).await 116 | .map_err(From::from) 117 | } 118 | 119 | pub async fn device_add(&self, add: qapi::qmp::device_add, deadline: Instant) -> Result<(), Error> { 120 | let qmp = self.connect_qmp().await?; 121 | let id = add.id.as_ref() 122 | .ok_or_else(|| format_err!("device_add id not found"))? 123 | .to_owned(); 124 | let path = format!("/machine/peripheral/{}", id); 125 | let exists = match qmp.execute(qapi::qmp::qom_list { path }).await { 126 | Ok(_) => true, 127 | Err(qapi::ExecuteError::Qapi(qapi::Error { class: qapi::ErrorClass::DeviceNotFound, .. })) => false, 128 | Err(e) => return Err(e.into()), 129 | }; 130 | if exists { 131 | let mut events = self.qmp_events(); 132 | let delete = qmp.execute(qapi::qmp::device_del { id: id.clone() }) 133 | .map_err(Error::from) 134 | .map_ok(drop); 135 | let wait = async move { 136 | loop { 137 | match events.recv().await { 138 | Ok(qapi::qmp::Event::DEVICE_DELETED { ref data, .. }) if data.device.as_ref() == Some(&id) => { 139 | // work around qemu bug. without this delay, device_add will work but the new device might be immediately deleted 140 | sleep(Duration::from_millis(128)).await; 141 | 142 | break Ok(()) 143 | }, 144 | Err(broadcast::error::RecvError::Closed) => break Err(format_err!("Expected DEVICE_DELETED event")), 145 | _ => (), 146 | } 147 | } 148 | }; 149 | future::try_join(wait, delete).await?; 150 | } 151 | 152 | tokio::time::sleep_until(deadline).await; 153 | qmp.execute(add).await?; 154 | 155 | Ok(()) 156 | } 157 | 158 | pub fn guest_exec_(&self, exec: qapi::qga::guest_exec) -> impl Future> { 159 | let connect = self.connect_qga(); 160 | async move { 161 | trace!("QEMU GA Exec {:?}", exec); 162 | 163 | let qga = connect.await?; 164 | match qga.execute(exec).await { 165 | Ok(qapi::qga::GuestExec { pid }) => loop { 166 | match qga.execute(qapi::qga::guest_exec_status { pid }).await { 167 | Ok(r) if !r.exited => sleep(Duration::from_millis(100)).await, 168 | res => break res.map_err(From::from), 169 | } 170 | }, 171 | Err(e) => Err(e.into()), 172 | } 173 | } 174 | } 175 | 176 | pub fn guest_exec, S: Into>(&self, args: I) -> GuestExec { 177 | let mut args = args.into_iter(); 178 | let cmd = args.next().expect("at least one command argument expected"); 179 | 180 | let exec = qapi::qga::guest_exec { 181 | path: cmd.into(), 182 | arg: Some(args.map(Into::into).collect()), 183 | env: Default::default(), 184 | input_data: Default::default(), 185 | capture_output: Some(true), 186 | }; 187 | 188 | GuestExec { 189 | qemu: self, 190 | exec, 191 | } 192 | } 193 | 194 | pub fn guest_shutdown(&self, shutdown: qapi::qga::guest_shutdown) -> impl Future> { 195 | // TODO: a shutdown (but not reboot) can be verified waiting for exit event or socket close or with --no-shutdown, query-status is "shutdown". Ugh! 196 | 197 | let connect = self.connect_qga(); 198 | async move { 199 | let qga = connect.await?; 200 | match timeout(Duration::from_secs(1), qga.execute(shutdown)).await { 201 | Ok(res) => res.map(drop).map_err(From::from), 202 | Err(_) => { 203 | warn!("Shutdown response timed out"); 204 | Ok(()) 205 | }, 206 | } 207 | } 208 | } 209 | 210 | pub fn guest_wait(&self) -> impl Future> { 211 | self.connect_qga() 212 | .map_ok(drop).map_err(Error::from) 213 | } 214 | } 215 | 216 | pub struct GuestExec<'a> { 217 | qemu: &'a Qemu, 218 | exec: qapi::qga::guest_exec, 219 | } 220 | 221 | impl<'a> GuestExec<'a> { 222 | pub fn into_future(self) -> impl Future> { 223 | self.qemu.guest_exec_(self.exec) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /samples/config.yml: -------------------------------------------------------------------------------- 1 | screens: 2 | - monitor: # fill in with info from `screenstub detect` 3 | manufacturer: GSM 4 | model: LG Ultra HD 5 | #serial: "..." 6 | guest_source: # Could be automatically detected, but best to fill in if monitor has more than two inputs 7 | name: DisplayPort-1 8 | #value: 0x0f # can also specify raw VCP value 9 | #host_source: # Usually automatically detected 10 | #name: HDMI-1 11 | #value: 0x11 12 | ddc: 13 | #minimal_delay: 100ms # minimum time to wait between switching inputs again 14 | #guest: [] # disable input switching 15 | #host: [] # disable input switching 16 | guest: # configure how to switch to the guest 17 | - guest_wait # wait until guest agent responds, otherwise might get stranded on other input 18 | - ddc # (default) Use ddc-rs 19 | #- exec: [ddccontrol, -r, "0x60", -w, "{}", /dev/i2c-5] 20 | host: # configure how to switch back from the guest 21 | #- ddc (default) Controls DDC from the host GPU - requires no guest agent but many monitors won't support this 22 | - guest_exec: ["C:/ddcset.exe", "setvcp", "60", "{:x}"] # or "0x{:x}" for hex input value 23 | #- guest_exec: ["C:/ScreenBright.exe", "-set", "0x60", "{}"] # "{}" is for decimal input value 24 | #- exec: ["ssh", "user@vm", "ddcutil", "setvcp", "0x60", "{}"] # system commands can also be used 25 | 26 | qemu: 27 | #routing: qmp # (default) does not require extra configuration or dependencies 28 | #routing: spice # no external requirements # CURRENTLY UNIMPLEMENTED 29 | #routing: input-linux # requires uinput 30 | #routing: virtio-host # requires uinput, recommended for performance, requires vioinput drivers in guest 31 | #driver: ps2 # use PS/2 in the guest for all input devices (absolute mouse mode unsupported) 32 | #driver: usb # use USB keyboard/mouse/tablet in the guest 33 | #driver: virtio # Recommended but vioinput drivers must be installed in guest 34 | #keyboard_driver: ps2 # (default) can also be set separately per input type, this should rarely be necessary 35 | #relative_driver: usb # (default) 36 | #absolute_driver: usb # (default) 37 | qmp_socket: /tmp/vfio-qmp # path to QMP socket 38 | ga_socket: /tmp/vfio-qga # path to Guest Agent socket 39 | 40 | key_remap: # Arbitrary keys can be remapped in the guest 41 | # See https://docs.rs/input-linux/*/input_linux/enum.Key.html for a list of key names available (mouse buttons can also be used) 42 | LeftMeta: Reserved # disable the windows key 43 | RightAlt: LeftMeta # remap right alt to trigger the windows key 44 | 45 | hotkeys: # Trigger various events on key combinations 46 | - triggers: [G] 47 | modifiers: [LeftMeta] 48 | on_release: false # trigger on downpress of key 49 | global: false # optionally trigger even when not in focus # CURRENTLY UNIMPLEMENTED 50 | events: # Select which events to trigger with this hotkey 51 | - toggle_grab: 52 | x: # Standard Xorg window grab 53 | mouse: true 54 | # devices: # Only grab specific devices from Xorg (CURRENTLY UNIMPLEMENTED) 55 | # - "..." 56 | #- exec: [echo, hi] # Execute an arbitrary system command 57 | #- show_host # switch to the host display 58 | #- show_guest # switch to the guest display 59 | #- toggle_show # switch the current display 60 | #- unstick_guest # causes all held keys to be released in the guest 61 | #- shutdown # safely shuts the guest system down 62 | #- reboot # reboots the guest 63 | #- exit # quits screenstub 64 | - triggers: [Y] 65 | modifiers: [LeftMeta] 66 | events: 67 | - toggle_grab: 68 | x: # Confine input/mouse to window 69 | mouse: false 70 | ignore: [] 71 | - toggle_grab: 72 | evdev: # evdev grab is useful for playing games that don't work with absolute mouse events 73 | exclusive: false # grab exclusive access from the device(s) 74 | #new_device_name: "unique-grab-name" # create a new uinput device for this grab 75 | xcore_ignore: [absolute, button] # which events to ignore from the window (key, button, absolute) 76 | evdev_ignore: [key] # which events to ignore from the evdev device 77 | devices: # List of devices to forward to guest 78 | - /dev/input/by-id/my-event-mouse 79 | - unstick_host # force-depress all Xorg keys (prevents keys getting stuck) 80 | - triggers: [T] 81 | modifiers: [LeftMeta] 82 | on_release: false 83 | events: 84 | - toggle_show 85 | 86 | exit_events: # Events to trigger on window close / exit 87 | - show_host 88 | #- shutdown 89 | -------------------------------------------------------------------------------- /samples/modules-load.d/i2c.conf: -------------------------------------------------------------------------------- 1 | i2c-dev 2 | -------------------------------------------------------------------------------- /samples/modules-load.d/uinput.conf: -------------------------------------------------------------------------------- 1 | uinput 2 | -------------------------------------------------------------------------------- /samples/sway/config: -------------------------------------------------------------------------------- 1 | input 5824:1503:screenstub-tablet events disabled 2 | input 5824:1503:screenstub-mouse events disabled 3 | input 5824:1503:screenstub-kbd events disabled 4 | -------------------------------------------------------------------------------- /samples/udev/rules.d/99-uinput.rules: -------------------------------------------------------------------------------- 1 | SUBSYSTEM=="misc", KERNEL=="uinput", OPTIONS+="static_node=uinput", MODE="0660", GROUP="uinput" 2 | ACTION=="add", SUBSYSTEM=="input", DEVPATH=="/devices/virtual/input/*", MODE="0666", GROUP="users" 3 | -------------------------------------------------------------------------------- /samples/xorg.conf.d/30-screenstub.conf: -------------------------------------------------------------------------------- 1 | Section "InputClass" 2 | Identifier "screenstubignore" 3 | MatchDevicePath "/dev/input/event*" 4 | MatchUSBID "16c0:05df" 5 | Option "Ignore" "true" 6 | EndSection 7 | -------------------------------------------------------------------------------- /src/exec.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::future::Future; 3 | use std::process::{Stdio, ExitStatus}; 4 | use tokio::process::Command; 5 | use anyhow::{Error, format_err}; 6 | 7 | pub struct Builder { 8 | child: Option, 9 | } 10 | 11 | impl Builder { 12 | pub fn into_future(self) -> impl Future> + Send + 'static { 13 | async move { 14 | if let Some(mut child) = self.child { 15 | exit_status_error(child.status().await?) 16 | } else { 17 | Err(format_err!("Missing exec command")) 18 | } 19 | } 20 | } 21 | } 22 | 23 | pub fn exec, S: AsRef>(args: I) -> Builder { 24 | let mut args = args.into_iter(); 25 | let child = if let Some(cmd) = args.next() { 26 | let mut child = Command::new(cmd); 27 | child 28 | .args(args) 29 | .stdout(Stdio::null()) 30 | .stdin(Stdio::null()); 31 | Some(child) 32 | } else { 33 | None 34 | }; 35 | 36 | Builder { 37 | child, 38 | } 39 | } 40 | 41 | fn exit_status_error(status: ExitStatus) -> Result<(), Error> { 42 | if status.success() { 43 | Ok(()) 44 | } else { 45 | Err(if let Some(code) = status.code() { 46 | format_err!("process exited with code {}", code) 47 | } else { 48 | format_err!("process exited with a failure") 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/filter.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicU8, Ordering}; 2 | use enumflags2::BitFlags; 3 | use config::ConfigInputEvent; 4 | use input::InputEvent; 5 | 6 | pub struct InputEventFilter { 7 | filter: AtomicU8, 8 | } 9 | 10 | impl InputEventFilter { 11 | pub fn new>(filter: I) -> Self { 12 | let filter: BitFlags<_> = filter.into_iter().collect(); 13 | InputEventFilter { 14 | filter: AtomicU8::new(filter.bits()), 15 | } 16 | } 17 | 18 | pub fn empty() -> Self { 19 | InputEventFilter { 20 | filter: Default::default(), 21 | } 22 | } 23 | 24 | pub fn filter(&self) -> BitFlags { 25 | unsafe { 26 | BitFlags::new(self.filter.load(Ordering::Relaxed)) 27 | } 28 | } 29 | 30 | pub fn filter_set(&self, value: BitFlags) { 31 | self.filter.store(value.bits(), Ordering::Relaxed) 32 | } 33 | 34 | pub fn filter_modify)>(&self, f: F) { 35 | // TODO: inconsistent 36 | let mut filter = self.filter(); 37 | f(&mut filter); 38 | self.filter_set(filter); 39 | } 40 | 41 | pub fn filter_event(&self, e: &InputEvent) -> bool { 42 | if let Some(flags) = ConfigInputEvent::from_event(e).map(BitFlags::from) { 43 | !self.filter().contains(flags) 44 | } else { 45 | // just allow sync (and other unknown?) events through 46 | true 47 | } 48 | } 49 | 50 | pub fn set_filter>(&self, filter: I) { 51 | self.filter_modify(|f| f.insert(filter.into_iter().collect::>())) 52 | } 53 | 54 | pub fn unset_filter>(&self, filter: I) { 55 | self.filter_modify(|f| f.remove(filter.into_iter().collect::>())) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/grab.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::Path; 3 | use std::sync::Arc; 4 | use std::io; 5 | use futures::channel::mpsc as un_mpsc; 6 | use futures::{Sink, SinkExt, StreamExt, FutureExt, stream, future}; 7 | use anyhow::Error; 8 | use input::{InputEvent, InputId}; 9 | use uinput::{UInputSink, EvdevHandle, Evdev}; 10 | use config::ConfigInputEvent; 11 | use crate::filter::InputEventFilter; 12 | 13 | /*pub enum Grab { 14 | XCore, 15 | Evdev(GrabEvdev), 16 | } 17 | 18 | impl From for Grab { 19 | fn from(g: GrabEvdev) -> Self { 20 | Grab::Evdev(g) 21 | } 22 | }*/ 23 | 24 | pub struct GrabEvdev { 25 | devices: HashMap, 26 | filter: Arc, 27 | } 28 | 29 | impl GrabEvdev { 30 | pub fn new(devices: I, filter: F) -> Result where 31 | P: AsRef, 32 | I: IntoIterator, 33 | F: IntoIterator, 34 | { 35 | let devices: io::Result<_> = devices.into_iter().map(|dev| -> io::Result<_> { 36 | let dev = Evdev::open(&dev)?; 37 | 38 | let evdev = dev.evdev(); 39 | 40 | let id = evdev.device_id()?; 41 | let stream = dev.to_sink()?; 42 | 43 | Ok((id, stream)) 44 | }).collect(); 45 | 46 | Ok(GrabEvdev { 47 | devices: devices?, 48 | filter: Arc::new(InputEventFilter::new(filter)), 49 | }) 50 | } 51 | 52 | pub fn grab(&self, grab: bool) -> io::Result<()> { 53 | Ok(for (_, ref uinput) in &self.devices { 54 | if let Some(evdev) = uinput.evdev() { 55 | evdev.grab(grab)?; 56 | } 57 | }) 58 | } 59 | 60 | pub fn spawn(self, mut sink: S, mut error_sender: un_mpsc::Sender) -> future::AbortHandle where 61 | S: Sink + Unpin + Clone + Send + 'static, 62 | Error: From, 63 | { 64 | let fut = async move { 65 | let mut select = stream::select_all( 66 | self.devices.into_iter().map(|(_, stream)| stream) 67 | ); 68 | while let Some(e) = select.next().await { 69 | let e = e?; 70 | if self.filter.filter_event(&e) { 71 | if sink.send(e).await.is_err() { 72 | break 73 | } 74 | } 75 | } 76 | 77 | Ok(()) 78 | }.then(move |r| async move { match r { 79 | Err(e) => { 80 | let _ = error_sender.send(e).await; 81 | }, 82 | _ => (), 83 | } }); 84 | let (fut, handle) = future::abortable(fut); 85 | tokio::spawn(fut); 86 | handle 87 | } 88 | 89 | pub fn evdevs(&self) -> Vec { 90 | // TODO: come on 91 | self.devices.iter().filter_map(|(_, ref stream)| stream.evdev()).collect() 92 | } 93 | } 94 | 95 | /*impl Drop for GrabEvdev { 96 | fn drop(&mut self) { 97 | for (_, mut stream) in self.devices.drain() { 98 | // TODO: stream.close(); 99 | } 100 | } 101 | }*/ 102 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | extern crate input_linux as input; 4 | extern crate screenstub_uinput as uinput; 5 | extern crate screenstub_config as config; 6 | extern crate screenstub_event as event; 7 | extern crate screenstub_qemu as qemu; 8 | extern crate screenstub_ddc as ddc; 9 | extern crate screenstub_x as x; 10 | 11 | use std::process::exit; 12 | use std::time::Duration; 13 | use std::path::PathBuf; 14 | use std::pin::Pin; 15 | use std::sync::Arc; 16 | use std::io::{self, Write}; 17 | use futures::channel::{mpsc, oneshot}; 18 | use futures::{future, TryFutureExt, FutureExt, StreamExt, SinkExt}; 19 | use anyhow::{Error, format_err}; 20 | use log::{warn, error, info}; 21 | use clap::{Arg, Command, value_parser}; 22 | use input::{InputId, Key, RelativeAxis, AbsoluteAxis, InputEvent, EventKind}; 23 | use config::{Config, ConfigEvent, ConfigSourceName}; 24 | use event::{Hotkey, UserEvent, ProcessedXEvent}; 25 | use qemu::Qemu; 26 | use route::Route; 27 | use spawner::Spawner; 28 | use sources::Sources; 29 | use process::Process; 30 | use ddc::{Monitor, DdcMonitor}; 31 | use x::XRequest; 32 | 33 | mod route; 34 | mod grab; 35 | mod filter; 36 | mod sources; 37 | mod exec; 38 | mod process; 39 | mod util; 40 | mod spawner; 41 | 42 | type Events = event::Events>; 43 | 44 | const EVENT_BUFFER: usize = 8; 45 | 46 | fn main() { 47 | let runtime = tokio::runtime::Runtime::new().unwrap(); 48 | let spawner = Arc::new(Spawner::new()); 49 | 50 | let code = match runtime.block_on(main_result(&spawner)) { 51 | Ok(code) => code, 52 | Err(e) => { 53 | let _ = writeln!(io::stderr(), "{:?} {}", e, e); 54 | 1 55 | }, 56 | }; 57 | 58 | runtime.block_on(spawner.join_timeout(Duration::from_secs(2))).unwrap(); 59 | 60 | runtime.shutdown_timeout(Duration::from_secs(1)); 61 | 62 | exit(code); 63 | } 64 | 65 | async fn main_result(spawner: &Arc) -> Result { 66 | env_logger::init(); 67 | 68 | let app = Command::new("screenstub") 69 | .version(env!("CARGO_PKG_VERSION")) 70 | .author("arcnmx") 71 | .about("A software KVM") 72 | .arg(Arg::new("config") 73 | .short('c') 74 | .long("config") 75 | .value_name("CONFIG") 76 | .num_args(1) 77 | .value_parser(value_parser!(PathBuf)) 78 | .help("Configuration TOML file") 79 | ).arg(Arg::new("screen") 80 | .short('s') 81 | .long("screen") 82 | .value_name("SCREEN") 83 | .num_args(1) 84 | .value_parser(value_parser!(usize)) 85 | .help("Configuration screen index") 86 | ).subcommand(Command::new("x") 87 | .about("Start the KVM with a fullscreen X window") 88 | ).subcommand(Command::new("check-config") 89 | .about("Read config and exit") 90 | ).subcommand(Command::new("detect") 91 | .about("Detect available DDC/CI displays and their video inputs") 92 | ).subcommand(Command::new("source") 93 | .about("Change the configured monitor input source") 94 | .arg(Arg::new("confirm") 95 | .short('c') 96 | .long("confirm") 97 | .help("Check that the VM is running before switching input") 98 | ).arg(Arg::new("source") 99 | .value_name("DEST") 100 | .num_args(1) 101 | .required(true) 102 | .value_parser(["host", "guest"]) 103 | .help("Switch to either the host or guest monitor input") 104 | ) 105 | ).subcommand_required(true); 106 | 107 | let matches = app.get_matches(); 108 | let config = if let Some(config) = matches.get_one::("config") { 109 | use std::fs::File; 110 | 111 | let f = File::open(config)?; 112 | serde_yaml::from_reader(f)? 113 | } else { 114 | Config::default() 115 | }; 116 | 117 | let screen_index = matches.get_one("screen").unwrap_or(&0usize); 118 | let screen = config.screens.into_iter().nth(*screen_index) 119 | .ok_or_else(|| format_err!("expected a screen config"))?; 120 | 121 | match matches.subcommand() { 122 | Some(("x", ..)) => { 123 | let xinstance = screen.x_instance.unwrap_or("auto".into()); 124 | 125 | let (mut x_sender, mut x_receiver) = mpsc::channel(0x20); 126 | let (mut xreq_sender, mut xreq_receiver) = mpsc::channel(0x08); 127 | let x = x::XContext::xmain("screenstub", &xinstance, "screenstub")?; 128 | let xmain = tokio::spawn(async move { 129 | let mut x = x.fuse(); 130 | loop { 131 | futures::select! { 132 | req = xreq_receiver.next() => if let Some(req) = req { 133 | let _ = x.send(req).await; 134 | }, 135 | event = x.next() => match event { 136 | Some(Ok(event)) => { 137 | let _ = x_sender.send(event).await; 138 | }, 139 | Some(Err(e)) => { 140 | error!("X Error: {}: {:?}", e, e); 141 | break 142 | }, 143 | None => { 144 | break 145 | }, 146 | }, 147 | complete => break, 148 | } 149 | } 150 | }).map_err(From::from); 151 | 152 | let (keyboard_driver, relative_driver, absolute_driver) = 153 | (config.qemu.keyboard_driver().clone(), config.qemu.relative_driver().clone(), config.qemu.absolute_driver().clone()); 154 | 155 | let mut events = event::Events::new(); 156 | config.hotkeys.into_iter() 157 | .map(convert_hotkey) 158 | .for_each(|(hotkey, on_press)| events.add_hotkey(hotkey, on_press)); 159 | config.key_remap.into_iter().for_each(|(from, to)| events.add_remap(from, to)); 160 | 161 | let events = Arc::new(events); 162 | 163 | let qemu = Arc::new(Qemu::new(config.qemu.qmp_socket, config.qemu.ga_socket)); 164 | 165 | let ddc = screen.ddc.unwrap_or_default(); 166 | let mut sources = Sources::new(qemu.clone(), screen.monitor, screen.host_source, screen.guest_source, ddc.host, ddc.guest, ddc.minimal_delay); 167 | sources.fill().await?; 168 | 169 | let (mut event_sender, mut event_recv) = mpsc::channel(EVENT_BUFFER); 170 | let (error_sender, mut error_recv) = mpsc::channel(1); 171 | 172 | let process = Process::new( 173 | config.qemu.routing, keyboard_driver.clone(), relative_driver.clone(), absolute_driver.clone(), config.exit_events, 174 | qemu.clone(), events.clone(), sources, xreq_sender.clone(), event_sender.clone(), error_sender.clone(), 175 | spawner.clone(), 176 | ); 177 | 178 | process.devices_init().await?; 179 | 180 | let uinput_id = InputId { 181 | bustype: input::sys::BUS_VIRTUAL, 182 | vendor: 0x16c0, 183 | product: 0x05df, 184 | version: 1, 185 | }; 186 | 187 | let repeat = false; 188 | let mut route_keyboard = Route::new(config.qemu.routing, qemu.clone(), "screenstub-route-kbd".into(), keyboard_driver.bus().cloned(), repeat); 189 | if let Some(builder) = route_keyboard.builder() { 190 | builder 191 | .name("screenstub-kbd") 192 | .x_config_key(repeat) 193 | .id(&uinput_id); 194 | } 195 | let mut events_keyboard = route_keyboard.spawn(spawner, error_sender.clone()); 196 | 197 | let mut route_relative = Route::new(config.qemu.routing, qemu.clone(), "screenstub-route-mouse".into(), relative_driver.bus().cloned(), repeat); 198 | if let Some(builder) = route_relative.builder() { 199 | builder 200 | .name("screenstub-mouse") 201 | .x_config_rel() 202 | .id(&uinput_id); 203 | } 204 | let mut events_relative = route_relative.spawn(spawner, error_sender.clone()); 205 | 206 | let mut route_absolute = Route::new(config.qemu.routing, qemu.clone(), "screenstub-route-tablet".into(), absolute_driver.bus().cloned(), repeat); 207 | if let Some(builder) = route_absolute.builder() { 208 | builder 209 | .name("screenstub-tablet") 210 | .x_config_abs() 211 | .id(&uinput_id); 212 | } 213 | let mut events_absolute = route_absolute.spawn(spawner, error_sender.clone()); 214 | 215 | let x_filter = process.x_filter(); 216 | 217 | let process = Arc::new(process); 218 | 219 | let (mut user_sender, user_receiver) = mpsc::channel::>(0x08); 220 | let mut user_receiver = user_receiver 221 | .map({ 222 | let process = process.clone(); 223 | move |event| process.process_user_event(&event) 224 | }); 225 | 226 | let sigint_handler = ctrlc::set_handler({ 227 | let mut user_sender = user_sender.clone(); 228 | let mut repeat_quit = false; 229 | move || { 230 | if repeat_quit { 231 | info!("Repeated quit attempt detected, quitting forcefully"); 232 | exit(128 + ctrlc::Signal::SIGINT as i32); 233 | } else { 234 | match user_sender.try_send(Arc::new(ConfigEvent::Exit)) { 235 | Ok(()) => (), 236 | Err(_) => 237 | warn!("Failed to send Quit event to main loop"), 238 | } 239 | repeat_quit = true; 240 | } 241 | } 242 | }); 243 | match sigint_handler { 244 | Ok(()) => (), 245 | Err(e) => warn!("Failed to set up SIGINT handler: {}", e), 246 | } 247 | 248 | 249 | let (event_loop, event_loop_abort) = future::abortable({ 250 | let events = events.clone(); 251 | let process = process.clone(); 252 | let mut user_sender = user_sender.clone(); 253 | async move { 254 | while let Some(event) = event_recv.next().await { 255 | let user_events = events.process_input_event(&event); 256 | let inputevent = events.map_input_event(event); 257 | let user_sender = &mut user_sender; 258 | let f1 = async move { 259 | for e in user_events { 260 | let _ = user_sender.send(e.clone()).await; 261 | } 262 | }; 263 | let is_mouse = process.is_mouse(); 264 | 265 | let events_keyboard = &mut events_keyboard; 266 | let events_relative = &mut events_relative; 267 | let events_absolute = &mut events_absolute; 268 | let f2 = async move { 269 | match map_event_kind(&inputevent, is_mouse) { 270 | EventKind::Key => { 271 | let _ = events_keyboard.send(inputevent).await; 272 | }, 273 | EventKind::Relative => { 274 | let _ = events_relative.send(inputevent).await; 275 | }, 276 | EventKind::Absolute => { 277 | let _ = events_absolute.send(inputevent).await; 278 | }, 279 | EventKind::Synchronize => { 280 | let _ = future::try_join3( 281 | events_keyboard.send(inputevent), 282 | events_relative.send(inputevent), 283 | events_absolute.send(inputevent) 284 | ).await; 285 | }, 286 | _ => (), 287 | } 288 | }; 289 | let _ = future::join(f1, f2).await; 290 | } 291 | } 292 | }); 293 | let event_loop = tokio::spawn(event_loop.map(drop)) 294 | .map_err(Error::from); 295 | 296 | let (xevent_exit_send, xevent_exit_recv) = oneshot::channel(); 297 | let mut xevent_exit_recv = xevent_exit_recv.fuse(); 298 | let xevent_loop = tokio::spawn({ 299 | async move { 300 | while let Some(xevent) = x_receiver.next().await { 301 | for e in events.process_x_event(&xevent) { 302 | match e { 303 | ProcessedXEvent::UserEvent(e) => { 304 | let _ = user_sender.send(convert_user_event(e)).await; 305 | }, 306 | ProcessedXEvent::InputEvent(e) if x_filter.filter_event(&e) => { 307 | let _ = event_sender.send(e).await; 308 | }, 309 | ProcessedXEvent::InputEvent(_) => (), 310 | } 311 | } 312 | } 313 | 314 | let _ = xevent_exit_send.send(()); 315 | } 316 | }).map_err(From::from); 317 | 318 | let res = loop { 319 | futures::select! { 320 | _ = xevent_exit_recv => break Ok(()), 321 | error = error_recv.next() => if let Some(error) = error { 322 | break Err(error) 323 | }, 324 | event = user_receiver.next() => if let Some(event) = event { 325 | tokio::spawn(async move { 326 | match Pin::from(event).await { 327 | Err(e) => 328 | warn!("User event failed {} {:?}", e, e), 329 | Ok(()) => (), 330 | } 331 | }); 332 | }, 333 | } 334 | }; 335 | 336 | let _ = xreq_sender.send(XRequest::Quit).await; // ensure we kill x 337 | xreq_sender.close_channel(); 338 | drop(xreq_sender); 339 | drop(process); 340 | 341 | // seal off senders 342 | event_loop_abort.abort(); 343 | future::try_join3( 344 | event_loop, 345 | xevent_loop, 346 | xmain, 347 | ).await?; 348 | 349 | res.map(|()| 0) 350 | }, 351 | Some(("check-config", ..)) => { 352 | Ok(0) 353 | }, 354 | Some(("detect", ..)) => { 355 | Monitor::enumerate()?.into_iter().try_for_each(|mut m| { 356 | let sources = m.sources()?; 357 | let current_source = m.get_source()?; 358 | println!("{}", m); 359 | sources.into_iter().for_each(|i| 360 | println!(" Source: {} = 0x{:02x}{}", 361 | ConfigSourceName::from_value(i).map(|i| i.to_string()).unwrap_or("Unknown".into()), 362 | i, 363 | if i == current_source { " (Active)" } else { "" } 364 | ) 365 | ); 366 | 367 | Ok::<_, Error>(()) 368 | })?; 369 | 370 | Ok(0) 371 | }, 372 | Some(("source", matches)) => { 373 | let ddc = screen.ddc.unwrap_or_default(); 374 | 375 | let qemu = Arc::new(Qemu::new(config.qemu.qmp_socket, config.qemu.ga_socket)); 376 | let sources = Sources::new(qemu, screen.monitor, screen.host_source, screen.guest_source, ddc.host, ddc.guest, ddc.minimal_delay); 377 | 378 | match matches.get_one::("source").map(|s| &s[..]) { 379 | Some("host") => sources.show(true, true).await, 380 | Some("guest") => sources.show(false, true).await, 381 | _ => unreachable!("unknown source to switch to"), 382 | }.map(|_| 0) 383 | }, 384 | _ => unreachable!("unknown command"), 385 | } 386 | } 387 | 388 | fn axis_is_relative(axis: RelativeAxis) -> bool { 389 | match axis { 390 | RelativeAxis::X | RelativeAxis::Y => true, 391 | _ => false, 392 | } 393 | } 394 | 395 | fn axis_is_absolute(axis: AbsoluteAxis) -> bool { 396 | match axis { 397 | AbsoluteAxis::X | AbsoluteAxis::Y => true, 398 | _ => false, 399 | } 400 | } 401 | 402 | fn convert_user_event(event: UserEvent) -> Arc { 403 | Arc::new(match event { 404 | UserEvent::Quit => ConfigEvent::Exit, 405 | UserEvent::ShowGuest => ConfigEvent::ShowGuest, 406 | UserEvent::ShowHost => ConfigEvent::ShowHost, 407 | UserEvent::UnstickGuest => ConfigEvent::UnstickGuest, 408 | UserEvent::UnstickHost => ConfigEvent::UnstickHost, 409 | }) 410 | } 411 | 412 | fn convert_hotkey(hotkey: config::ConfigHotkey) -> (Hotkey>, bool) { 413 | ( 414 | Hotkey::new(hotkey.triggers, hotkey.modifiers, hotkey.events.into_iter().map(Arc::new)), 415 | !hotkey.on_release, 416 | ) 417 | } 418 | 419 | fn map_event_kind(inputevent: &InputEvent, is_mouse: bool) -> EventKind { 420 | match inputevent.kind { 421 | EventKind::Key if Key::from_code(inputevent.code).map(|k| k.is_button()).unwrap_or(false) => 422 | if is_mouse { 423 | EventKind::Relative 424 | } else { 425 | EventKind::Absolute 426 | }, 427 | EventKind::Key => 428 | EventKind::Key, 429 | EventKind::Absolute if inputevent.code == AbsoluteAxis::Volume as u16 => 430 | EventKind::Key, // is this right? 431 | EventKind::Relative if RelativeAxis::from_code(inputevent.code).map(|a| axis_is_relative(a)).unwrap_or(false) => 432 | EventKind::Relative, 433 | EventKind::Absolute if AbsoluteAxis::from_code(inputevent.code).map(|a| axis_is_absolute(a)).unwrap_or(false) => 434 | EventKind::Absolute, 435 | EventKind::Relative | EventKind::Absolute => 436 | if is_mouse { 437 | EventKind::Relative 438 | } else { 439 | EventKind::Absolute 440 | }, 441 | EventKind::Synchronize => 442 | EventKind::Synchronize, 443 | kind => { 444 | warn!("unforwarded event {:?}", kind); 445 | kind 446 | }, 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::future::Future; 3 | use std::sync::Arc; 4 | use std::pin::Pin; 5 | //use futures::{future, Stream, Future, IntoFuture}; 6 | use futures::{future, FutureExt, SinkExt, TryFutureExt}; 7 | use futures::channel::mpsc as un_mpsc; 8 | use std::sync::Mutex; 9 | use anyhow::{Error, format_err}; 10 | use config::{ConfigEvent, ConfigGrab, ConfigGrabMode, ConfigInputEvent, ConfigQemuRouting, ConfigQemuDriver}; 11 | use qapi::qga::{guest_shutdown, GuestShutdownMode}; 12 | use input::{self, InputEvent, RelativeAxis, InputId}; 13 | use qemu::Qemu; 14 | use crate::filter::InputEventFilter; 15 | use crate::sources::Sources; 16 | use crate::route::Route; 17 | use crate::grab::GrabEvdev; 18 | use crate::exec::exec; 19 | use x::XRequest; 20 | use crate::Events; 21 | use crate::spawner::Spawner; 22 | use log::{trace, info, error}; 23 | 24 | pub struct GrabHandle { 25 | grab: Option, 26 | x_filter: Vec, 27 | is_mouse: bool, 28 | } 29 | 30 | impl Drop for GrabHandle { 31 | fn drop(&mut self) { 32 | if let Some(grab) = self.grab.take() { 33 | grab.abort(); 34 | } 35 | } 36 | } 37 | 38 | pub struct Process { 39 | routing: ConfigQemuRouting, 40 | driver_keyboard: Arc, 41 | driver_relative: Arc, 42 | driver_absolute: Arc, 43 | exit_events: Vec, 44 | qemu: Arc, 45 | events: Arc, 46 | sources: Arc>>, 47 | grabs: Arc>>, 48 | x_input_filter: Arc, 49 | xreq_sender: un_mpsc::Sender, 50 | event_sender: un_mpsc::Sender, 51 | error_sender: un_mpsc::Sender, 52 | uinput_id: Arc, 53 | spawner: Arc, 54 | } 55 | 56 | #[derive(Debug, Copy, Clone)] 57 | enum InputDevice { 58 | Keyboard, 59 | Relative, 60 | Absolute, 61 | } 62 | 63 | impl Process { 64 | pub fn new(routing: ConfigQemuRouting, driver_keyboard: ConfigQemuDriver, driver_relative: ConfigQemuDriver, driver_absolute: ConfigQemuDriver, exit_events: Vec, qemu: Arc, events: Arc, sources: Sources, xreq_sender: un_mpsc::Sender, event_sender: un_mpsc::Sender, error_sender: un_mpsc::Sender, spawner: Arc) -> Self { 65 | Process { 66 | routing, 67 | driver_keyboard: Arc::new(driver_keyboard), 68 | driver_relative: Arc::new(driver_relative), 69 | driver_absolute: Arc::new(driver_absolute), 70 | exit_events, 71 | qemu, 72 | events, 73 | sources: Arc::new(Box::pin(sources)), 74 | grabs: Arc::new(Mutex::new(Default::default())), 75 | x_input_filter: Arc::new(InputEventFilter::empty()), 76 | xreq_sender, 77 | event_sender, 78 | error_sender, 79 | uinput_id: Arc::new(InputId { 80 | bustype: input::sys::BUS_VIRTUAL, 81 | vendor: 0x16c0, 82 | product: 0x05df, 83 | version: 1, 84 | }), 85 | spawner, 86 | } 87 | } 88 | 89 | pub fn x_filter(&self) -> Arc { 90 | self.x_input_filter.clone() 91 | } 92 | 93 | fn device_id(device: InputDevice) -> &'static str { 94 | match device { 95 | InputDevice::Keyboard => "screenstub-dev-kbd", 96 | InputDevice::Relative => "screenstub-dev-mouse", 97 | InputDevice::Absolute => "screenstub-dev-mouse", 98 | } 99 | } 100 | 101 | fn add_device_cmd(device: InputDevice, driver: &ConfigQemuDriver) -> Option { 102 | let driver = match (device, driver) { 103 | (InputDevice::Absolute, ConfigQemuDriver::Ps2) => panic!("PS/2 tablet not possible"), 104 | (_, ConfigQemuDriver::Ps2) => return None, 105 | (InputDevice::Keyboard, ConfigQemuDriver::Usb) => "usb-kbd", 106 | (InputDevice::Relative, ConfigQemuDriver::Usb) => "usb-mouse", 107 | (InputDevice::Absolute, ConfigQemuDriver::Usb) => "usb-tablet", 108 | (InputDevice::Keyboard, ConfigQemuDriver::Virtio { .. }) => "virtio-keyboard-pci", 109 | (InputDevice::Relative, ConfigQemuDriver::Virtio { .. }) => "virtio-mouse-pci", 110 | (InputDevice::Absolute, ConfigQemuDriver::Virtio { .. }) => "virtio-tablet-pci", 111 | }; 112 | 113 | let id = Self::device_id(device); 114 | Some(qapi::qmp::device_add::new(driver, Some(id.into()), None, Vec::new())) 115 | } 116 | 117 | async fn devices_init_cmd(qemu: Arc, routing: ConfigQemuRouting, device: InputDevice, driver: &ConfigQemuDriver) -> Result<(), Error> { 118 | match routing { 119 | ConfigQemuRouting::VirtioHost => return Ok(()), 120 | _ => (), 121 | }; 122 | 123 | if let Some(cmd) = Self::add_device_cmd(device, driver) { 124 | qemu.device_add(cmd, tokio::time::Instant::now()).await 125 | } else { 126 | Ok(()) 127 | } 128 | } 129 | 130 | pub async fn devices_init(&self) -> Result<(), Error> { 131 | Self::devices_init_cmd(self.qemu.clone(), self.routing, InputDevice::Keyboard, &self.driver_keyboard).await?; 132 | self.set_is_mouse(false).await?; // TODO: config option to start up in relative mode instead 133 | 134 | Ok(()) 135 | } 136 | 137 | async fn set_is_mouse_cmd(qemu: Arc, routing: ConfigQemuRouting, driver_relative: Arc, driver_absolute: Arc, is_mouse: bool) -> Result<(), Error> { 138 | let (device, driver) = if is_mouse { 139 | (InputDevice::Relative, driver_relative) 140 | } else { 141 | (InputDevice::Absolute, driver_absolute) 142 | }; 143 | 144 | Self::devices_init_cmd(qemu, routing, device, &driver).await 145 | } 146 | 147 | pub fn set_is_mouse(&self, is_mouse: bool) -> impl Future> { 148 | Self::set_is_mouse_cmd(self.qemu.clone(), self.routing, self.driver_relative.clone(), self.driver_absolute.clone(), is_mouse) 149 | } 150 | 151 | fn grab(&self, grab: &ConfigGrab) -> Pin> + Send>> { 152 | let mode = grab.mode(); 153 | 154 | match *grab { 155 | ConfigGrab::X { confine, mouse, ref ignore, ref devices } => { 156 | let qemu = self.qemu.clone(); 157 | let grabs = self.grabs.clone(); 158 | let routing = self.routing; 159 | let driver_relative = self.driver_relative.clone(); 160 | let driver_absolute = self.driver_absolute.clone(); 161 | let prev_is_mouse = self.is_mouse(); 162 | let ignore = ignore.clone(); 163 | let x_filter = self.x_input_filter.clone(); 164 | 165 | let grab = self.xreq(XRequest::Grab { 166 | xcore: confine, 167 | confine, 168 | motion: mouse, 169 | devices: devices.iter().map(|_| unimplemented!()).collect(), 170 | }); 171 | async move { 172 | grab.await?; 173 | 174 | x_filter.set_filter(ignore.iter().cloned()); 175 | 176 | grabs.lock().unwrap().insert(mode, GrabHandle { 177 | grab: None, 178 | x_filter: ignore, 179 | is_mouse: mouse, 180 | }); 181 | 182 | if mouse && !prev_is_mouse { 183 | Self::set_is_mouse_cmd(qemu, routing, driver_relative, driver_absolute, mouse).await?; 184 | } 185 | 186 | Ok(()) 187 | }.boxed() 188 | }, 189 | ConfigGrab::Evdev { exclusive, ref new_device_name, ref xcore_ignore, ref evdev_ignore, ref devices } => { 190 | let qemu = self.qemu.clone(); 191 | let grabs = self.grabs.clone(); 192 | let x_filter = self.x_input_filter.clone(); 193 | let xcore_ignore = xcore_ignore.clone(); 194 | let devname = new_device_name.clone(); 195 | let error_sender = self.error_sender.clone(); 196 | let event_sender = if new_device_name.is_some() { None } else { Some(self.event_sender.clone()) }; 197 | let routing = self.routing; 198 | let uinput_id = self.uinput_id.clone(); 199 | let driver_relative = self.driver_relative.clone(); 200 | let driver_absolute = self.driver_absolute.clone(); 201 | let prev_is_mouse = self.is_mouse(); 202 | let spawner = self.spawner.clone(); 203 | let grab = GrabEvdev::new(devices, evdev_ignore.iter().cloned()); 204 | 205 | async move { 206 | let grab = grab?; 207 | let event_sender = if let Some(devname) = devname { 208 | let id = format!("screenstub-uinput-{}", devname); 209 | let repeat = false; 210 | let bus = None; 211 | let qemu = qemu.clone(); 212 | let mut uinput = Route::new(routing, qemu, id, bus, repeat); 213 | 214 | let mut builder = uinput.builder(); 215 | 216 | if let Some(builder) = builder.as_mut() { 217 | builder.name(&devname); 218 | builder.id(&uinput_id); 219 | } 220 | 221 | for evdev in grab.evdevs() { 222 | if let Some(builder) = builder.as_mut() { 223 | builder.from_evdev(&evdev)?; 224 | } 225 | } 226 | 227 | if exclusive { 228 | grab.grab(true)?; 229 | } 230 | 231 | uinput.spawn(&spawner, error_sender.clone()) 232 | } else { 233 | event_sender.unwrap() 234 | }; 235 | 236 | let mut is_mouse = false; 237 | for evdev in grab.evdevs() { 238 | let rel = evdev.relative_bits()?; 239 | if rel.get(RelativeAxis::X) || rel.get(RelativeAxis::Y) { 240 | is_mouse = true; 241 | break 242 | } 243 | } 244 | 245 | let grab = grab.spawn(event_sender, error_sender); 246 | 247 | x_filter.set_filter(xcore_ignore.iter().cloned()); 248 | 249 | grabs.lock().unwrap().insert(mode, GrabHandle { 250 | grab: Some(grab), 251 | x_filter: xcore_ignore, 252 | is_mouse, 253 | }); 254 | 255 | if is_mouse && !prev_is_mouse { 256 | Self::set_is_mouse_cmd(qemu, routing, driver_relative, driver_absolute, is_mouse).await?; 257 | } 258 | Ok(()) 259 | }.boxed() 260 | }, 261 | _ => future::err(format_err!("grab {:?} unimplemented", mode)).boxed(), 262 | } 263 | } 264 | 265 | pub fn is_mouse(&self) -> bool { 266 | // TODO: no grabs doesn't necessarily mean absolute mode... 267 | self.grabs.lock().unwrap().iter().any(|(_, g)| g.is_mouse) 268 | } 269 | 270 | fn ungrab(&self, grab: ConfigGrabMode) -> Pin> + Send>> { 271 | match grab { 272 | ConfigGrabMode::X { .. } => { 273 | let ungrab = self.xreq(XRequest::Ungrab); 274 | let grab = self.grabs.lock().unwrap().remove(&grab); 275 | if let Some(mut grab) = grab { 276 | self.x_input_filter.unset_filter(grab.x_filter.drain(..)); 277 | if grab.is_mouse && !self.is_mouse() { 278 | let set = self.set_is_mouse(false); 279 | async move { 280 | set.await?; 281 | ungrab.await 282 | }.boxed() 283 | } else { 284 | ungrab 285 | } 286 | } else { 287 | ungrab 288 | } 289 | }, 290 | ConfigGrabMode::Evdev => { 291 | let grab = self.grabs.lock().unwrap().remove(&grab); 292 | if let Some(mut grab) = grab { 293 | self.x_input_filter.unset_filter(grab.x_filter.drain(..)); 294 | if grab.is_mouse && !self.is_mouse() { 295 | self.set_is_mouse(false).boxed() 296 | } else { 297 | future::ok(()).boxed() 298 | } 299 | } else { 300 | info!("requested non-existent grab"); 301 | future::ok(()).boxed() 302 | } 303 | }, 304 | grab => future::err(format_err!("ungrab {:?} unimplemented", grab)).boxed(), 305 | } 306 | } 307 | 308 | fn xreq(&self, req: XRequest) -> Pin> + Send>> { 309 | let mut xreq_sender = self.xreq_sender.clone(); 310 | async move { 311 | xreq_sender.send(req) 312 | .map_err(From::from).await 313 | }.boxed() 314 | } 315 | 316 | fn map_exec_arg>(s: S) -> Result { 317 | // TODO: variable substitution or something 318 | Ok(s.as_ref().into()) 319 | } 320 | 321 | pub fn process_user_event(&self, event: &ConfigEvent) -> Pin> + Send>> { 322 | trace!("process_user_event({:?})", event); 323 | info!("User event {:?}", event); 324 | match event { 325 | ConfigEvent::Exec(args) => { 326 | let args = args.iter() 327 | .map(|i| Self::map_exec_arg(i)) 328 | .collect::, Error>>(); 329 | match args { 330 | Err(e) => future::ready(Err(e)).boxed(), 331 | Ok(args) => exec(args).into_future().boxed(), 332 | } 333 | }, 334 | ConfigEvent::GuestExec(args) => { 335 | let args = args.iter() 336 | .map(|i| Self::map_exec_arg(i)) 337 | .collect::, Error>>(); 338 | match args { 339 | Err(e) => future::ready(Err(e)).boxed(), 340 | Ok(args) => self.qemu.guest_exec(args).into_future().map_ok(drop).boxed(), 341 | } 342 | }, 343 | ConfigEvent::GuestWait => 344 | self.qemu.guest_wait().boxed(), 345 | ConfigEvent::ShowHost => { 346 | self.sources.show_host().boxed() 347 | }, 348 | ConfigEvent::ShowGuest => { 349 | self.sources.show_guest().boxed() 350 | }, 351 | ConfigEvent::ToggleShow => { 352 | if self.sources.showing_guest().unwrap_or_default() { 353 | self.sources.show_host().boxed() 354 | } else { 355 | self.sources.show_guest().boxed() 356 | } 357 | }, 358 | ConfigEvent::ToggleGrab(ref grab) => { 359 | let mode = grab.mode(); 360 | if self.grabs.lock().unwrap().contains_key(&mode) { 361 | self.ungrab(mode) 362 | } else { 363 | self.grab(grab) 364 | } 365 | }, 366 | ConfigEvent::Grab(grab) => self.grab(grab), 367 | ConfigEvent::Ungrab(grab) => self.ungrab(*grab), 368 | ConfigEvent::UnstickGuest => { 369 | let mut event_sender = self.event_sender.clone(); 370 | let events = self.events.clone(); 371 | async move { 372 | for e in events.unstick_guest() { 373 | let _ = event_sender.send(e).await; 374 | } 375 | 376 | Ok(()) 377 | }.boxed() 378 | }, 379 | ConfigEvent::UnstickHost => { 380 | self.xreq(XRequest::UnstickHost) 381 | }, 382 | ConfigEvent::Shutdown => { 383 | self.qemu.guest_shutdown(guest_shutdown { mode: Some(GuestShutdownMode::Powerdown) }).boxed() 384 | }, 385 | ConfigEvent::Reboot => { 386 | self.qemu.guest_shutdown(guest_shutdown { mode: Some(GuestShutdownMode::Reboot) }).boxed() 387 | }, 388 | ConfigEvent::Exit => { 389 | let exit_events: Vec<_> = self.exit_events.iter() 390 | .filter_map(|e| match e { 391 | ConfigEvent::Exit => None, 392 | e => Some(e), 393 | }).map(|e| self.process_user_event(e)) 394 | .chain(std::iter::once(self.xreq(XRequest::Quit))) 395 | .collect(); 396 | async move { 397 | for e in exit_events { 398 | if let Err(e) = e.await { 399 | error!("Failed to run exit event: {} {:?}", e, e); 400 | } 401 | } 402 | Ok(()) 403 | }.boxed() 404 | } 405 | } 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/route.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::path::Path; 3 | use std::task::Poll; 4 | use std::sync::Once; 5 | use std::pin::Pin; 6 | use std::iter; 7 | use tokio::time::{Duration, Instant}; 8 | use input::{InputEvent, EventRef, KeyEvent, Key, RelativeAxis, AbsoluteAxis}; 9 | use futures::channel::mpsc; 10 | use futures::{StreamExt, SinkExt, Future, FutureExt, TryFutureExt}; 11 | use anyhow::{Error, Context}; 12 | use config::ConfigQemuRouting; 13 | use config::keymap::Keymaps; 14 | use qapi::{qmp, Any}; 15 | use qemu::Qemu; 16 | use uinput; 17 | use log::warn; 18 | use crate::spawner::Spawner; 19 | 20 | pub struct RouteQmp { 21 | qemu: Arc, 22 | qkeycodes: Arc<[u8]>, 23 | } 24 | 25 | impl RouteQmp { 26 | pub fn new(qemu: Arc) -> Self { 27 | let qkeycodes = unsafe { 28 | static mut QKEYCODES: Option> = None; 29 | static QKEYCODES_ONCE: Once = Once::new(); 30 | 31 | QKEYCODES_ONCE.call_once(|| { 32 | QKEYCODES = Some(Keymaps::from_csv().qnum_keycodes().into()); 33 | }); 34 | QKEYCODES.as_ref().unwrap().clone() 35 | }; 36 | RouteQmp { 37 | qemu, 38 | qkeycodes, 39 | } 40 | } 41 | 42 | fn convert_event(e: &InputEvent, qkeycodes: &[u8]) -> Option { 43 | Some(match EventRef::new(e) { 44 | Ok(EventRef::Key(ref key)) if key.key.is_button() => qmp::InputEvent::btn(qmp::InputBtnEvent { 45 | down: key.value.is_pressed(), 46 | button: match key.key { 47 | Key::ButtonLeft => qmp::InputButton::left, 48 | Key::ButtonMiddle => qmp::InputButton::middle, 49 | Key::ButtonRight => qmp::InputButton::right, 50 | Key::ButtonWheel => qmp::InputButton::wheel_down, 51 | Key::ButtonGearUp => qmp::InputButton::wheel_up, 52 | Key::ButtonSide => qmp::InputButton::side, 53 | Key::ButtonExtra => qmp::InputButton::extra, 54 | _ => return None, // TODO: warn/error/etc 55 | }, 56 | }.into()), 57 | Ok(EventRef::Key(KeyEvent { key: Key::Reserved, .. })) => 58 | return None, // ignore key 0 events 59 | Ok(EventRef::Key(ref key)) => match qkeycodes.get(key.key as usize) { 60 | Some(&qnum) => qmp::InputEvent::key(qmp::InputKeyEvent { 61 | down: key.value.is_pressed(), 62 | key: qmp::KeyValue::number(qnum.into()), 63 | }.into()), 64 | None => return None, 65 | }, 66 | Ok(EventRef::Relative(rel)) => qmp::InputEvent::rel(qmp::InputMoveEvent { 67 | axis: match rel.axis { 68 | RelativeAxis::X => qmp::InputAxis::x, 69 | RelativeAxis::Y => qmp::InputAxis::y, 70 | _ => return None, // TODO: warn/error/etc 71 | }, 72 | value: rel.value as _, 73 | }.into()), 74 | Ok(EventRef::Absolute(abs)) => qmp::InputEvent::abs(qmp::InputMoveEvent { 75 | axis: match abs.axis { 76 | AbsoluteAxis::X => qmp::InputAxis::x, 77 | AbsoluteAxis::Y => qmp::InputAxis::y, 78 | _ => return None, // TODO: warn/error/etc 79 | }, 80 | value: abs.value as _, 81 | }.into()), 82 | _ => return None, // TODO: warn/error/etc 83 | }) 84 | } 85 | 86 | fn convert_events<'a, I: IntoIterator + 'a>(e: I, qkeycodes: &'a [u8]) -> impl Iterator + 'a { 87 | e.into_iter().map(move |ref e| Self::convert_event(e, qkeycodes)).filter_map(|e| e) 88 | } 89 | 90 | pub fn spawn(&self, spawner: &Spawner, mut events: mpsc::Receiver, mut error_sender: mpsc::Sender) { 91 | let qemu = self.qemu.clone(); 92 | let qkeycodes = self.qkeycodes.clone(); 93 | spawner.spawn(async move { 94 | let qmp = qemu.connect_qmp().await?; 95 | let mut cmd = qmp::input_send_event { 96 | device: Default::default(), 97 | head: Default::default(), 98 | events: Default::default(), 99 | }; 100 | 'outer: while let Some(event) = events.next().await { 101 | const THRESHOLD: usize = 0x20; 102 | cmd.events.clear(); 103 | cmd.events.extend(RouteQmp::convert_events(iter::once(event), &qkeycodes)); 104 | while let Poll::Ready(event) = futures::poll!(events.next()) { 105 | match event { 106 | Some(event) => 107 | cmd.events.extend(RouteQmp::convert_events(iter::once(event), &qkeycodes)), 108 | None => break 'outer, 109 | } 110 | if cmd.events.len() > THRESHOLD { 111 | break 112 | } 113 | } 114 | if !cmd.events.is_empty() { 115 | match qmp.execute(&cmd).await { 116 | Ok(_) => (), 117 | Err(qapi::ExecuteError::Qapi(e @ qapi::Error { class: qapi::ErrorClass::GenericError, .. })) => 118 | warn!("QMP input routing error: {:?}", e), 119 | Err(e) => return Err(e.into()), 120 | } 121 | } 122 | } 123 | Ok(()) 124 | }.then(move |r| async move { match r { 125 | Err(e) => { 126 | let _ = error_sender.send(e).await; 127 | }, 128 | _ => (), 129 | } })); 130 | } 131 | } 132 | 133 | pub struct RouteUInput { 134 | qemu: Arc, 135 | builder: uinput::Builder, 136 | commands: Arc, 137 | } 138 | 139 | impl RouteUInput { 140 | pub fn builder(&mut self) -> &mut uinput::Builder { 141 | &mut self.builder 142 | } 143 | } 144 | 145 | impl RouteUInput { 146 | pub fn new_input_linux(qemu: Arc, id: String, repeat: bool) -> Self { 147 | Self::new(qemu, uinput::Builder::new(), RouteUInputInputLinux { 148 | id, 149 | repeat, 150 | }) 151 | } 152 | } 153 | 154 | impl RouteUInput { 155 | pub fn new_virtio_host(qemu: Arc, id: String, bus: Option) -> Self { 156 | Self::new(qemu, uinput::Builder::new(), RouteUInputVirtio { 157 | id, 158 | bus, 159 | }) 160 | } 161 | } 162 | 163 | pub trait UInputCommands: Send + Sync + 'static { 164 | fn command_create(&self, qemu: &Arc, path: &Path) -> Pin> + Send>>; 165 | fn command_delete(&self, qemu: &Arc) -> Pin> + Send>>; 166 | } 167 | 168 | pub struct RouteUInputVirtio { 169 | id: String, 170 | bus: Option, 171 | } 172 | 173 | impl UInputCommands for RouteUInputVirtio { 174 | fn command_create(&self, qemu: &Arc, path: &Path) -> Pin> + Send>> { 175 | let command = qmp::device_add::new("virtio-input-host-pci", Some(self.id.clone()), self.bus.clone(), vec![ 176 | ("evdev".into(), Any::String(path.display().to_string())), 177 | ("multifunction".into(), Any::Bool(true)), 178 | ]); 179 | let deadline = Instant::now() + Duration::from_millis(512); // HACK: wait for udev to see device and change permissions 180 | let qemu = qemu.clone(); 181 | async move { 182 | qemu.device_add(command, deadline).await 183 | }.boxed() 184 | } 185 | 186 | fn command_delete(&self, qemu: &Arc) -> Pin> + Send>> { 187 | let command = qmp::device_del { 188 | id: self.id.clone(), 189 | }; 190 | let qemu = qemu.clone(); 191 | async move { 192 | qemu.execute_qmp(command).map_ok(drop).await 193 | }.boxed() 194 | } 195 | } 196 | 197 | pub struct RouteUInputInputLinux { 198 | id: String, 199 | repeat: bool, 200 | } 201 | 202 | impl UInputCommands for RouteUInputInputLinux { 203 | fn command_create(&self, qemu: &Arc, path: &Path) -> Pin> + Send>> { 204 | let path = path.display(); 205 | let command = qmp::object_add::from(qmp::ObjectOptions::input_linux { 206 | id: self.id.clone(), 207 | input_linux: qmp::InputLinuxProperties { 208 | evdev: path.to_string(), 209 | repeat: Some(self.repeat), 210 | grab_all: None, 211 | grab_toggle: None, 212 | }, 213 | }); 214 | let delete_command = qmp::object_del { 215 | id: self.id.clone(), 216 | }; 217 | let qemu = qemu.clone(); 218 | async move { 219 | if qemu.execute_qmp(delete_command).await.is_ok() { 220 | tokio::time::sleep(Duration::from_millis(512)).await; 221 | } 222 | qemu.execute_qmp(command).map_ok(drop).await 223 | }.boxed() 224 | } 225 | 226 | fn command_delete(&self, qemu: &Arc) -> Pin> + Send>> { 227 | let command = qmp::object_del { 228 | id: self.id.clone(), 229 | }; 230 | let qemu = qemu.clone(); 231 | async move { 232 | qemu.execute_qmp(command).map_ok(drop).await 233 | }.boxed() 234 | } 235 | } 236 | 237 | impl RouteUInput { 238 | fn new(qemu: Arc, builder: uinput::Builder, commands: U) -> Self { 239 | RouteUInput { 240 | qemu, 241 | builder, 242 | commands: Arc::new(commands), 243 | } 244 | } 245 | } 246 | 247 | impl RouteUInput { 248 | pub fn spawn(&self, spawner: &Spawner, mut events: mpsc::Receiver, mut error_sender: mpsc::Sender) { 249 | let qemu = self.qemu.clone(); 250 | let uinput = self.builder.create(); 251 | let commands = self.commands.clone(); 252 | spawner.spawn(async move { 253 | let uinput = uinput?; 254 | let path = uinput.path().to_owned(); 255 | let mut uinput = uinput.to_sink()?; 256 | commands.command_create(&qemu, &path).await?; 257 | let res = async move { 258 | while let Some(e) = events.next().await { 259 | uinput.send(e).await 260 | .context("uinput write failed")?; 261 | } 262 | Ok(()) 263 | }.await; 264 | let qres = commands.command_delete(&qemu).await 265 | .map_err(From::from); 266 | res.and_then(move |()| qres) 267 | }.then(move |r: Result<(), Error>| async move { match r { 268 | Err(e) => { 269 | let _ = error_sender.send(e).await; 270 | }, 271 | _ => (), 272 | } })); 273 | } 274 | } 275 | 276 | pub enum Route { 277 | InputLinux(RouteUInput), 278 | VirtioHost(RouteUInput), 279 | Qmp(RouteQmp), 280 | //Spice(RouteInputSpice), 281 | } 282 | 283 | impl Route { 284 | pub fn new(routing: ConfigQemuRouting, qemu: Arc, id: String, bus: Option, repeat: bool) -> Self { 285 | match routing { 286 | ConfigQemuRouting::InputLinux => Route::InputLinux(RouteUInput::new_input_linux(qemu, id, repeat)), 287 | ConfigQemuRouting::VirtioHost => Route::VirtioHost(RouteUInput::new_virtio_host(qemu, id, bus)), 288 | ConfigQemuRouting::Qmp => Route::Qmp(RouteQmp::new(qemu)), 289 | ConfigQemuRouting::Spice => unimplemented!("SPICE routing"), 290 | } 291 | } 292 | 293 | pub fn builder(&mut self) -> Option<&mut uinput::Builder> { 294 | match *self { 295 | Route::InputLinux(ref mut uinput) => Some(uinput.builder()), 296 | Route::VirtioHost(ref mut uinput) => Some(uinput.builder()), 297 | Route::Qmp(..) => None, 298 | } 299 | } 300 | 301 | pub fn spawn(self, spawner: &Spawner, error_sender: mpsc::Sender) -> mpsc::Sender { 302 | let (sender, events) = mpsc::channel(crate::EVENT_BUFFER); 303 | 304 | match self { 305 | Route::InputLinux(ref uinput) => uinput.spawn(spawner, events, error_sender), 306 | Route::VirtioHost(ref uinput) => uinput.spawn(spawner, events, error_sender), 307 | Route::Qmp(ref qmp) => qmp.spawn(spawner, events, error_sender), 308 | } 309 | 310 | sender 311 | } 312 | } 313 | 314 | // TODO: spice input 315 | /*pub struct RouteInputSpice { 316 | }*/ 317 | -------------------------------------------------------------------------------- /src/sources.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; 3 | use std::sync::{Arc, Mutex as StdMutex}; 4 | use futures::lock::Mutex; 5 | use tokio::time::{Duration, Instant, sleep_until}; 6 | use anyhow::{Error, format_err}; 7 | use qemu::Qemu; 8 | use config::{ConfigSource, ConfigMonitor, ConfigDdcMethod}; 9 | use crate::exec::exec; 10 | use ddc::{SearchDisplay, DdcMonitor}; 11 | 12 | type DynMonitor = dyn DdcMonitor + Send; 13 | 14 | pub struct Sources { 15 | qemu: Arc, 16 | source_guest: Option, 17 | source_host: Option, 18 | target_showing: Arc, 19 | showing_guest: Arc, 20 | host: Vec>, 21 | guest: Vec>, 22 | monitor: Arc, 23 | ddc: Arc>>>, 24 | throttle: Arc>, 25 | throttle_duration: Duration, 26 | } 27 | 28 | fn convert_display(monitor: ConfigMonitor) -> SearchDisplay { 29 | SearchDisplay { 30 | backend_id: monitor.id, 31 | manufacturer_id: monitor.manufacturer, 32 | model_name: monitor.model, 33 | serial_number: monitor.serial, 34 | path: None, // TODO: i2c bus selection? 35 | } 36 | } 37 | 38 | impl Sources { 39 | pub fn new(qemu: Arc, display: ConfigMonitor, source_host: ConfigSource, source_guest: ConfigSource, host: Vec, guest: Vec, throttle_duration: Duration) -> Self { 40 | Sources { 41 | qemu, 42 | source_guest: source_guest.value(), 43 | source_host: source_host.value(), 44 | target_showing: Arc::new(AtomicBool::new(false)), 45 | showing_guest: Arc::new(AtomicU8::new(2)), 46 | host: host.into_iter().map(Arc::new).collect(), 47 | guest: guest.into_iter().map(Arc::new).collect(), 48 | monitor: Arc::new(convert_display(display)), 49 | ddc: Arc::new(StdMutex::new(None)), 50 | throttle: Arc::new(Mutex::new(Instant::now() - throttle_duration)), 51 | throttle_duration, 52 | } 53 | } 54 | 55 | pub async fn fill(&mut self) -> Result<(), Error> { 56 | tokio::task::block_in_place(move || { 57 | let mut ddc = self.ddc.lock().unwrap(); 58 | for method in &self.host { 59 | if self.source_host.is_some() && self.source_guest.is_some() { 60 | break 61 | } 62 | let ddc = Self::ddc_connect(&mut ddc, method, &self.monitor)?; 63 | let source_host = match self.source_host { 64 | Some(source) => source, 65 | None => { 66 | let source = ddc.get_source()?; 67 | self.source_host = Some(source); 68 | source 69 | }, 70 | }; 71 | match &self.source_guest { 72 | Some(..) => (), 73 | None => 74 | self.source_guest = ddc.find_guest_source(source_host)?, 75 | } 76 | } 77 | 78 | Ok(()) 79 | }) 80 | } 81 | 82 | fn map_source_arg>(s: S, source: Option, host: bool) -> Result { 83 | let source = source 84 | .ok_or_else(|| format_err!("DDC {} source not found", 85 | if host { "host" } else { "guest" } 86 | )); 87 | let s = s.as_ref(); 88 | Ok(if s == "{}" { 89 | format!("{}", source?) 90 | } else if s == "{:x}" { 91 | format!("{:02x}", source?) 92 | } else if s == "0x{:x}" { 93 | format!("0x{:02x}", source?) 94 | } else { 95 | s.to_owned() 96 | }) 97 | } 98 | 99 | // TODO: detect current showing state via ddc when unknown? 100 | 101 | pub fn showing_guest(&self) -> Option { 102 | Self::showing_guest_(&self.showing_guest) 103 | } 104 | 105 | fn showing_guest_(showing_guest: &AtomicU8) -> Option { 106 | match showing_guest.load(Ordering::Relaxed) { 107 | 0 => Some(false), 108 | 1 => Some(true), 109 | _ => None, 110 | } 111 | } 112 | 113 | pub fn show_guest(&self) -> impl Future> { 114 | self.show(false, false) 115 | } 116 | 117 | pub fn show_host(&self) -> impl Future> { 118 | self.show(true, false) 119 | } 120 | 121 | fn ddc_connect<'a>(ddc: &'a mut Option>, method: &ConfigDdcMethod, monitor: &SearchDisplay) -> Result<&'a mut Box, Error> { 122 | if ddc.is_some() { 123 | // mean workaround for lifetime issues 124 | match ddc.as_mut() { 125 | Some(ddc) => Ok(ddc), 126 | None => unsafe { core::hint::unreachable_unchecked() }, 127 | } 128 | } else { 129 | let res = match method { 130 | #[cfg(feature = "with-ddcutil")] 131 | ConfigDdcMethod::Libddcutil => 132 | ddc::ddcutil::Monitor::search(&monitor) 133 | .map(|r| r.map(|r| Box::new(r)))?, 134 | #[cfg(not(feature = "with-ddcutil"))] 135 | ConfigDdcMethod::Libddcutil => 136 | return Err(format_err!("Not compiled for libddcutil")), 137 | ConfigDdcMethod::Ddcutil => 138 | return Err(format_err!("ddcutil CLI support unimplemented")), 139 | _ => 140 | ddc::Monitor::search(&monitor) 141 | .map(|r| r.map(|r| Box::new(r)))?, 142 | }; 143 | match res { 144 | Some(res) => 145 | Ok(ddc.get_or_insert(res)), 146 | None => 147 | Err(format_err!("DDC monitor not found")), 148 | } 149 | } 150 | } 151 | 152 | pub fn show(&self, host: bool, force: bool) -> impl Future> { 153 | let show_host = self.show_commands(true); 154 | let show_guest = self.show_commands(false); 155 | 156 | self.target_showing.store(host, Ordering::Relaxed); 157 | 158 | let target_showing = self.target_showing.clone(); 159 | let showing_guest = self.showing_guest.clone(); 160 | let throttle = self.throttle.clone(); 161 | let throttle_duration = self.throttle_duration; 162 | async move { 163 | let mut throttle = throttle.lock().await; 164 | 165 | let throttle_until = *throttle; 166 | let now = Instant::now(); 167 | let throttle_until = if throttle_until <= now { 168 | // a short delay gives the event loop a chance to change its mind 169 | now + Duration::from_millis(48) 170 | } else { 171 | throttle_until 172 | }; 173 | sleep_until(throttle_until).await; 174 | 175 | let host = target_showing.load(Ordering::Relaxed); 176 | let guest = !host; 177 | 178 | if force || Self::showing_guest_(&showing_guest) != Some(guest) { 179 | let methods = if host { 180 | show_host 181 | } else { 182 | show_guest 183 | }; 184 | 185 | for method in methods { 186 | method.await?; 187 | } 188 | 189 | showing_guest.store(guest as u8, Ordering::Relaxed); 190 | *throttle = Instant::now() + throttle_duration; 191 | } 192 | 193 | Ok(()) 194 | } 195 | } 196 | 197 | fn show_commands(&self, host: bool) -> Vec>> { 198 | let methods = if host { 199 | &self.host 200 | } else { 201 | &self.guest 202 | }; 203 | methods.iter().cloned() 204 | .map(|method| 205 | self.show_(host, method) 206 | ).collect() 207 | } 208 | 209 | fn show_(&self, host: bool, method: Arc) -> impl Future> { 210 | let source = if host { 211 | &self.source_host 212 | } else { 213 | &self.source_guest 214 | }.clone(); 215 | let monitor = self.monitor.clone(); 216 | let (ddc, qemu) = ( 217 | self.ddc.clone(), 218 | self.qemu.clone(), 219 | ); 220 | async move { match &*method { 221 | ConfigDdcMethod::GuestWait => qemu.guest_wait().await, 222 | ConfigDdcMethod::Ddc | ConfigDdcMethod::Libddcutil | ConfigDdcMethod::Ddcutil => { 223 | tokio::task::spawn_blocking(move || { 224 | let mut ddc = ddc.lock().unwrap(); 225 | let ddc = Self::ddc_connect(&mut ddc, &method, &monitor)?; 226 | match source { 227 | Some(source) => 228 | ddc.set_source(source), 229 | None => 230 | Err(format_err!("DDC {} source not found", 231 | if host { "host" } else { "guest" } 232 | )), 233 | } 234 | }).await 235 | .map_err(From::from).and_then(|r| r) 236 | }, 237 | ConfigDdcMethod::Exec(args) => { 238 | let res = exec(args.iter() 239 | .map(|i| Self::map_source_arg(i, source, host)) 240 | .collect::, Error>>()? 241 | ).into_future().await; 242 | res 243 | }, 244 | ConfigDdcMethod::GuestExec(args) => { 245 | let res = qemu.guest_exec(args.iter() 246 | .map(|i| Self::map_source_arg(i, source, host)) 247 | .collect::, Error>>()? 248 | ).into_future().await; 249 | res.map(drop) 250 | }, 251 | } } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/spawner.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Mutex; 2 | use tokio::time::{Duration, Instant, timeout_at}; 3 | use tokio::task::JoinHandle; 4 | use futures::{Future, future}; 5 | use anyhow::Error; 6 | 7 | pub struct Spawner { 8 | handles: Mutex>>, 9 | } 10 | 11 | impl Spawner { 12 | pub fn new() -> Self { 13 | Self { 14 | handles: Mutex::new(Vec::new()), 15 | } 16 | } 17 | 18 | pub fn spawn + Send + 'static>(&self, f: F) { 19 | let handle = tokio::spawn(f); 20 | self.handles.lock().unwrap().push(handle); 21 | } 22 | 23 | pub async fn join_timeout(&self, timeout: Duration) -> Result<(), Error> { 24 | let deadline = Instant::now() + timeout; 25 | loop { 26 | let handles: Vec<_> = self.handles.lock().unwrap().drain(..).collect(); 27 | if handles.is_empty() { 28 | break Ok(()) 29 | } 30 | let _: Vec<()> = timeout_at(deadline, future::try_join_all(handles)).await??; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use std::future::Future; 3 | use std::hint; 4 | use futures::{future, TryFutureExt}; 5 | use anyhow::Error; 6 | use tokio::time; 7 | 8 | pub fn retry, F: Future>, FF: FnMut() -> F>(mut f: FF, retries: usize, timeout: Duration) -> impl Future> { 9 | time::timeout(timeout, async move { 10 | let mut err = None; 11 | for _ in 0..=retries { 12 | match f().await { 13 | Ok(res) => return Ok(res), 14 | Err(e) => err = Some(e), 15 | } 16 | } 17 | match err { 18 | Some(err) => Err(err.into()), 19 | None => unsafe { hint::unreachable_unchecked() }, 20 | } 21 | }).map_err(Error::from) 22 | .and_then(|r| future::ready(r)) 23 | } 24 | -------------------------------------------------------------------------------- /uinput/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screenstub-uinput" 3 | version = "0.0.1" 4 | edition = "2018" 5 | 6 | include = [ 7 | "/src/**/*.rs", 8 | ] 9 | 10 | [dependencies] 11 | screenstub-fd = { version = "^0.0.1", path = "../fd" } 12 | input-linux = { version = "0.6", features = ["tokio-util-0_7"] } 13 | futures = { version = "^0.3.4", features = ["bilock", "unstable"] } 14 | tokio = { version = "1", default-features = false, features = ["macros", "time", "io-util", "rt"] } 15 | tokio-util = { version = "0.7", default-features = false, features = ["codec"] } 16 | bytes = "1" 17 | log = "^0.4.1" 18 | libc = "^0.2.36" 19 | -------------------------------------------------------------------------------- /uinput/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::io::AsRawFd; 2 | use std::os::unix::fs::OpenOptionsExt; 3 | use std::path::{Path, PathBuf}; 4 | use std::fs::{File, OpenOptions}; 5 | use std::io::{self, Read, Write}; 6 | use std::{mem, slice}; 7 | use std::task::{Poll, Context}; 8 | use std::pin::Pin; 9 | use screenstub_fd::FdRef; 10 | use input_linux as input; 11 | use input_linux::{ 12 | UInputHandle, InputId, 13 | InputEvent, EventKind, 14 | AbsoluteAxis, RelativeAxis, Key, 15 | AbsoluteInfoSetup, AbsoluteInfo, Bitmask, 16 | EventCodec, 17 | }; 18 | use tokio_util::codec::{Decoder, Encoder}; 19 | use tokio::io::unix::AsyncFd; 20 | use futures::{Sink, Stream, ready}; 21 | use bytes::{BytesMut, BufMut}; 22 | use log::{trace, debug}; 23 | 24 | pub type EvdevHandle<'a> = input_linux::EvdevHandle>>; 25 | 26 | #[derive(Debug, Default, Clone)] 27 | pub struct Builder { 28 | name: String, 29 | id: InputId, 30 | abs: Vec, 31 | bits_events: Bitmask, 32 | bits_keys: Bitmask, 33 | bits_abs: Bitmask, 34 | bits_props: Bitmask, 35 | bits_rel: Bitmask, 36 | bits_misc: Bitmask, 37 | bits_led: Bitmask, 38 | bits_sound: Bitmask, 39 | bits_switch: Bitmask, 40 | } 41 | 42 | fn o_nonblock() -> OpenOptions { 43 | let mut o = OpenOptions::new(); 44 | o.custom_flags(libc::O_NONBLOCK); 45 | o 46 | } 47 | 48 | fn io_poll(res: io::Result) -> Poll> { 49 | match res { 50 | Err(e) if e.kind() == io::ErrorKind::WouldBlock => Poll::Pending, 51 | res => Poll::Ready(res), 52 | } 53 | } 54 | 55 | impl Builder { 56 | pub fn new() -> Self { 57 | Default::default() 58 | } 59 | 60 | pub fn name(&mut self, name: &str) -> &mut Self { 61 | self.name = name.to_owned(); 62 | 63 | self 64 | } 65 | 66 | pub fn id(&mut self, id: &InputId) -> &mut Self { 67 | self.id = id.clone(); 68 | 69 | self 70 | } 71 | 72 | pub fn absolute_axis(&mut self, setup: AbsoluteInfoSetup) -> &mut Self { 73 | self.bits_abs.insert(setup.axis); 74 | self.abs.push(setup); 75 | 76 | self 77 | } 78 | 79 | pub fn x_config_rel(&mut self) -> &mut Self { 80 | self.x_config_button(); 81 | self.bits_events.insert(EventKind::Relative); 82 | for &axis in &[RelativeAxis::X, RelativeAxis::Y, RelativeAxis::Wheel, RelativeAxis::HorizontalWheel] { 83 | self.bits_rel.insert(axis); 84 | } 85 | 86 | self 87 | } 88 | 89 | pub fn x_config_abs(&mut self) -> &mut Self { 90 | self.x_config_button(); 91 | self.bits_events.insert(EventKind::Absolute); 92 | for &axis in &[AbsoluteAxis::X, AbsoluteAxis::Y] { 93 | self.absolute_axis(AbsoluteInfoSetup { 94 | axis, 95 | info: AbsoluteInfo { 96 | maximum: 0x8000, 97 | resolution: 1, 98 | .. Default::default() 99 | }, 100 | }); 101 | } 102 | self.bits_events.insert(EventKind::Relative); 103 | for &axis in &[RelativeAxis::Wheel, RelativeAxis::HorizontalWheel] { 104 | self.bits_rel.insert(axis); 105 | } 106 | 107 | self 108 | } 109 | 110 | pub fn x_config_button(&mut self) -> &mut Self { 111 | self.bits_events.insert(EventKind::Key); 112 | self.bits_keys.or(Key::iter().filter(|k| k.is_button())); 113 | 114 | self 115 | } 116 | 117 | pub fn x_config_key(&mut self, repeat: bool) -> &mut Self { 118 | self.bits_events.insert(EventKind::Key); 119 | if repeat { 120 | // autorepeat is undesired, the VM will have its own implementation 121 | self.bits_events.insert(EventKind::Autorepeat); // kernel should handle this for us as long as it's set 122 | } 123 | self.bits_keys.or(Key::iter().filter(|k| k.is_key())); 124 | 125 | self 126 | } 127 | 128 | pub fn from_evdev(&mut self, evdev: &input_linux::EvdevHandle) -> io::Result<&mut Self> { 129 | evdev.device_properties()?.iter().for_each(|bit| self.bits_props.insert(bit)); 130 | evdev.event_bits()?.iter().for_each(|bit| self.bits_events.insert(bit)); 131 | evdev.key_bits()?.iter().for_each(|bit| self.bits_keys.insert(bit)); 132 | evdev.relative_bits()?.iter().for_each(|bit| self.bits_rel.insert(bit)); 133 | evdev.misc_bits()?.iter().for_each(|bit| self.bits_misc.insert(bit)); 134 | evdev.led_bits()?.iter().for_each(|bit| self.bits_led.insert(bit)); 135 | evdev.sound_bits()?.iter().for_each(|bit| self.bits_sound.insert(bit)); 136 | evdev.switch_bits()?.iter().for_each(|bit| self.bits_switch.insert(bit)); 137 | 138 | // TODO: FF bits? 139 | 140 | for axis in &evdev.absolute_bits()? { 141 | // TODO: this breaks things :< 142 | self.absolute_axis(AbsoluteInfoSetup { 143 | axis, 144 | info: evdev.absolute_info(axis)?, 145 | }); 146 | } 147 | 148 | Ok(self) 149 | } 150 | 151 | pub fn create(&self) -> io::Result { 152 | trace!("UInput open"); 153 | const FILENAME: &'static str = "/dev/uinput"; 154 | let mut open = o_nonblock(); 155 | open.read(true); 156 | open.write(true); 157 | let file = open.open(FILENAME)?; 158 | let fd = AsyncFd::new(file)?; 159 | 160 | let handle = UInputHandle::new(FdRef::from(&fd)); 161 | 162 | debug!("UInput props {:?}", self.bits_props); 163 | for bit in &self.bits_props { 164 | handle.set_propbit(bit)?; 165 | } 166 | 167 | debug!("UInput events {:?}", self.bits_events); 168 | for bit in &self.bits_events { 169 | handle.set_evbit(bit)?; 170 | } 171 | 172 | debug!("UInput keys {:?}", self.bits_keys); 173 | for bit in &self.bits_keys { 174 | handle.set_keybit(bit)?; 175 | } 176 | 177 | debug!("UInput rel {:?}", self.bits_rel); 178 | for bit in &self.bits_rel { 179 | handle.set_relbit(bit)?; 180 | } 181 | 182 | debug!("UInput abs {:?}", self.bits_abs); 183 | for bit in &self.bits_abs { 184 | handle.set_absbit(bit)?; 185 | } 186 | 187 | debug!("UInput misc {:?}", self.bits_misc); 188 | for bit in &self.bits_misc { 189 | handle.set_mscbit(bit)?; 190 | } 191 | 192 | debug!("UInput led {:?}", self.bits_led); 193 | for bit in &self.bits_led { 194 | handle.set_ledbit(bit)?; 195 | } 196 | 197 | debug!("UInput sound {:?}", self.bits_sound); 198 | for bit in &self.bits_sound { 199 | handle.set_sndbit(bit)?; 200 | } 201 | 202 | debug!("UInput switch {:?}", self.bits_switch); 203 | for bit in &self.bits_switch { 204 | handle.set_swbit(bit)?; 205 | } 206 | 207 | handle.create(&self.id, self.name.as_bytes(), 0, &self.abs)?; 208 | 209 | Ok(UInput { 210 | path: handle.evdev_path()?, 211 | fd, 212 | }) 213 | } 214 | } 215 | 216 | #[derive(Debug)] 217 | pub struct UInput { 218 | pub fd: AsyncFd, 219 | pub path: PathBuf, 220 | } 221 | 222 | impl UInput { 223 | pub fn path(&self) -> &Path { 224 | &self.path 225 | } 226 | 227 | pub fn write_events(&mut self, events: &[InputEvent]) -> io::Result { 228 | UInputHandle::new(FdRef::from(&self.fd)).write(unsafe { mem::transmute(events) }) 229 | } 230 | 231 | pub fn write_event(&mut self, event: &InputEvent) -> io::Result { 232 | self.write_events(slice::from_ref(event)) 233 | } 234 | 235 | pub fn to_sink(self) -> io::Result { 236 | //let uinput_write = FramedWrite::new(uinput_f, input::EventCodec::new()); 237 | 238 | Ok(UInputSink { 239 | fd: Some(self.fd), 240 | buffer_write: BytesMut::with_capacity(mem::size_of::() * 32), 241 | buffer_read: Default::default(), 242 | codec: EventCodec::new(), 243 | }) 244 | } 245 | } 246 | 247 | #[derive(Debug)] 248 | pub struct Evdev { 249 | fd: AsyncFd, 250 | } 251 | 252 | impl Evdev { 253 | pub fn open>(path: &P) -> io::Result { 254 | trace!("Evdev open"); 255 | let mut open = o_nonblock(); 256 | open.read(true); 257 | open.write(true); 258 | let file = open.open(path)?; 259 | 260 | Ok(Evdev { 261 | fd: AsyncFd::new(file)?, 262 | }) 263 | } 264 | 265 | pub fn evdev(&self) -> EvdevHandle { 266 | EvdevHandle::new(FdRef::from(&self.fd)) 267 | } 268 | 269 | pub fn to_sink(self) -> io::Result { 270 | //let uinput_write = FramedWrite::new(uinput_f, input::EventCodec::new()); 271 | 272 | Ok(UInputSink { 273 | fd: Some(self.fd), 274 | buffer_write: Default::default(), 275 | buffer_read: BytesMut::with_capacity(mem::size_of::() * 32), 276 | codec: EventCodec::new(), 277 | }) 278 | } 279 | } 280 | 281 | //#[derive(Debug)] 282 | pub struct UInputSink { 283 | fd: Option>, 284 | buffer_write: BytesMut, 285 | buffer_read: BytesMut, 286 | codec: EventCodec, 287 | } 288 | 289 | impl UInputSink { 290 | pub fn evdev(&self) -> Option { 291 | self.fd.as_ref().map(|fd| 292 | EvdevHandle::new(FdRef::from(fd)) 293 | ) 294 | } 295 | 296 | fn write_events(file: &mut File, buffer_write: &mut BytesMut) -> io::Result { 297 | if buffer_write.is_empty() { 298 | return Ok(0) 299 | } 300 | 301 | let n = file.write(buffer_write)?; 302 | 303 | if n == 0 { 304 | Err(io::Error::new(io::ErrorKind::WriteZero, "failed to write to uinput")) 305 | } else { 306 | let _ = buffer_write.split_to(n); 307 | 308 | Ok(n) 309 | } 310 | } 311 | 312 | fn read_events(file: &mut File, buffer_read: &mut BytesMut) -> io::Result { 313 | buffer_read.reserve(mem::size_of::()); 314 | unsafe { 315 | let n = { 316 | let buffer = buffer_read.chunk_mut(); 317 | debug_assert_eq!(buffer.len() % mem::size_of::(), 0); 318 | let buffer = slice::from_raw_parts_mut(buffer.as_mut_ptr(), buffer.len()); 319 | file.read(buffer) 320 | }?; 321 | buffer_read.advance_mut(n); 322 | 323 | Ok(n) 324 | } 325 | } 326 | } 327 | 328 | impl Sink for UInputSink { 329 | type Error = io::Error; 330 | 331 | fn start_send(self: Pin<&mut Self>, item: InputEvent) -> Result<(), Self::Error> { 332 | trace!("UInputSink start_send({:?})", item); 333 | 334 | let this = unsafe { self.get_unchecked_mut() }; 335 | if let Some(fd) = this.fd.as_mut() { 336 | this.codec.encode(item, &mut this.buffer_write)?; 337 | 338 | // attempt a single non-blocking write 339 | match io_poll(Self::write_events(fd.get_mut(), &mut this.buffer_write)) { 340 | Poll::Ready(Err(e)) => return Err(e), 341 | _ => (), 342 | } 343 | 344 | Ok(()) 345 | } else { 346 | Err(io::Error::new(io::ErrorKind::UnexpectedEof, "uinput fd is closed")) 347 | } 348 | } 349 | 350 | fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 351 | trace!("UInputSink poll_ready"); 352 | 353 | let this = unsafe { self.as_mut().get_unchecked_mut() }; 354 | if this.fd.is_some() { 355 | if this.buffer_write.len() > mem::size_of::() * 8 { 356 | self.poll_flush(cx) 357 | } else { 358 | Poll::Ready(Ok(())) 359 | } 360 | } else { 361 | Poll::Ready( 362 | Err(io::Error::new(io::ErrorKind::UnexpectedEof, "uinput fd is closed")) 363 | ) 364 | } 365 | } 366 | 367 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 368 | trace!("UInputSink poll_flush"); 369 | 370 | let this = unsafe { self.get_unchecked_mut() }; 371 | if let Some(fd) = this.fd.as_mut() { 372 | while !this.buffer_write.is_empty() { 373 | let mut ready = ready!(fd.poll_write_ready_mut(cx))?; 374 | let buffer = &mut this.buffer_write; 375 | match ready.try_io(|fd| Self::write_events(fd.get_mut(), buffer)) { 376 | Err(_) => continue, 377 | Ok(n) => { 378 | n?; 379 | }, 380 | } 381 | } 382 | 383 | Poll::Ready(Ok(())) 384 | } else if this.buffer_write.is_empty() { 385 | Poll::Ready(Ok(())) 386 | } else { 387 | Poll::Ready( 388 | Err(io::Error::new(io::ErrorKind::UnexpectedEof, "uinput fd is closed")) 389 | ) 390 | } 391 | } 392 | 393 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 394 | trace!("UInputSink poll_close"); 395 | 396 | let res = self.as_mut().poll_flush(cx); 397 | 398 | if res.is_ready() { 399 | self.fd = None; 400 | } 401 | res 402 | } 403 | } 404 | 405 | impl Stream for UInputSink { 406 | type Item = Result; 407 | 408 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 409 | trace!("UInputSink poll_next"); 410 | 411 | let this = unsafe { self.get_unchecked_mut() }; 412 | loop { 413 | if let Some(fd) = this.fd.as_mut() { 414 | if let Some(frame) = this.codec.decode(&mut this.buffer_read)? { 415 | return Poll::Ready(Some(Ok(frame))) 416 | } 417 | 418 | let mut ready = ready!(fd.poll_read_ready_mut(cx))?; 419 | let buffer = &mut this.buffer_read; 420 | let n = match ready.try_io(|fd| Self::read_events(fd.get_mut(), buffer)) { 421 | Err(_) => continue, 422 | Ok(n) => n?, 423 | }; 424 | if n == 0 { 425 | this.fd = None; 426 | } 427 | } else { 428 | return Poll::Ready(this.codec.decode_eof(&mut this.buffer_read).transpose()) 429 | } 430 | } 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /x/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screenstub-x" 3 | version = "0.0.1" 4 | edition = "2018" 5 | 6 | include = [ 7 | "/src/**/*.rs", 8 | ] 9 | 10 | [dependencies] 11 | screenstub-fd = { version = "^0.0.1", path = "../fd" } 12 | futures = { version = "^0.3.4", features = ["bilock", "unstable"] } 13 | tokio = { version = "^1.0.0", default-features = false, features = ["rt-multi-thread"] } 14 | anyhow = "^1.0.42" 15 | xcb = { version = "^0.9.0", features = ["xtest", "xkb", "dpms"] } 16 | input-linux = "0.6" 17 | log = "^0.4.1" 18 | -------------------------------------------------------------------------------- /x/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub extern crate xcb; 2 | 3 | use futures::{Sink, Stream, ready}; 4 | use anyhow::{Error, format_err}; 5 | use input_linux::{InputEvent, EventTime, KeyEvent, KeyState, Key, AbsoluteEvent, AbsoluteAxis, SynchronizeEvent}; 6 | use tokio::io::unix::AsyncFd; 7 | use tokio::io::Interest; 8 | use std::task::{Poll, Context, Waker}; 9 | use std::pin::Pin; 10 | use log::{trace, warn, info}; 11 | use screenstub_fd::Fd; 12 | 13 | #[derive(Debug, Clone, Copy, Default)] 14 | struct XState { 15 | pub width: u16, 16 | pub height: u16, 17 | pub running: bool, 18 | } 19 | 20 | #[derive(Debug)] 21 | pub struct XInputEvent { 22 | time: xcb::Time, 23 | data: XInputEventData, 24 | } 25 | 26 | #[derive(Debug)] 27 | pub enum XInputEventData { 28 | Mouse { 29 | x: i16, 30 | y: i16, 31 | }, 32 | Button { 33 | pressed: bool, 34 | button: xcb::Button, 35 | state: u16, 36 | }, 37 | Key { 38 | pressed: bool, 39 | keycode: xcb::Keycode, 40 | keysym: Option, 41 | state: u16, 42 | }, 43 | } 44 | 45 | #[derive(Debug)] 46 | pub enum XEvent { 47 | Visible(bool), 48 | Focus(bool), 49 | Close, 50 | Input(InputEvent), 51 | } 52 | 53 | #[derive(Debug)] 54 | pub enum XRequest { 55 | Quit, 56 | UnstickHost, 57 | Grab { 58 | xcore: bool, 59 | confine: bool, 60 | motion: bool, 61 | devices: Vec<()>, 62 | }, 63 | Ungrab, 64 | } 65 | 66 | pub struct XContext { 67 | conn: xcb::Connection, 68 | fd: AsyncFd, 69 | window: u32, 70 | 71 | keys: xcb::GetKeyboardMappingReply, 72 | mods: xcb::GetModifierMappingReply, 73 | state: XState, 74 | next_event: Option, 75 | next_request: Option, 76 | event_queue: Vec, 77 | stop_waker: Option, 78 | 79 | atom_wm_state: xcb::Atom, 80 | atom_wm_protocols: xcb::Atom, 81 | atom_wm_delete_window: xcb::Atom, 82 | atom_net_wm_state: xcb::Atom, 83 | atom_net_wm_state_fullscreen: xcb::Atom, 84 | } 85 | 86 | unsafe impl Send for XContext { } 87 | 88 | impl XContext { 89 | pub fn connect() -> Result { 90 | let (conn, screen_num) = xcb::Connection::connect(None)?; 91 | let fd = { 92 | let fd = unsafe { xcb::ffi::base::xcb_get_file_descriptor(conn.get_raw_conn()) }; 93 | AsyncFd::with_interest(fd.into(), Interest::READABLE) 94 | }?; 95 | let window = conn.generate_id(); 96 | let (keys, mods) = { 97 | let setup = conn.get_setup(); 98 | let screen = setup.roots().nth(screen_num as usize).unwrap(); 99 | 100 | xcb::create_window(&conn, 101 | xcb::COPY_FROM_PARENT as _, 102 | window, 103 | screen.root(), 104 | 0, 0, 105 | screen.width_in_pixels(), screen.height_in_pixels(), 106 | 0, 107 | xcb::WINDOW_CLASS_INPUT_OUTPUT as _, 108 | screen.root_visual(), 109 | &[ 110 | (xcb::CW_BACK_PIXEL, screen.black_pixel()), 111 | ( 112 | xcb::CW_EVENT_MASK, 113 | xcb::EVENT_MASK_VISIBILITY_CHANGE | xcb::EVENT_MASK_PROPERTY_CHANGE | 114 | xcb::EVENT_MASK_KEY_PRESS | xcb::EVENT_MASK_KEY_RELEASE | 115 | xcb::EVENT_MASK_BUTTON_PRESS | xcb::EVENT_MASK_BUTTON_RELEASE | 116 | xcb::EVENT_MASK_POINTER_MOTION | xcb::EVENT_MASK_BUTTON_MOTION | 117 | xcb::EVENT_MASK_STRUCTURE_NOTIFY | xcb::EVENT_MASK_FOCUS_CHANGE 118 | ), 119 | ] 120 | ); 121 | 122 | ( 123 | xcb::get_keyboard_mapping(&conn, setup.min_keycode(), setup.max_keycode() - setup.min_keycode()).get_reply()?, 124 | xcb::get_modifier_mapping(&conn).get_reply()?, 125 | ) 126 | }; 127 | 128 | Ok(Self { 129 | atom_wm_state: xcb::intern_atom(&conn, true, "WM_STATE").get_reply()?.atom(), 130 | atom_wm_protocols: xcb::intern_atom(&conn, true, "WM_PROTOCOLS").get_reply()?.atom(), 131 | atom_wm_delete_window: xcb::intern_atom(&conn, true, "WM_DELETE_WINDOW").get_reply()?.atom(), 132 | atom_net_wm_state: xcb::intern_atom(&conn, true, "_NET_WM_STATE").get_reply()?.atom(), 133 | atom_net_wm_state_fullscreen: xcb::intern_atom(&conn, true, "_NET_WM_STATE_FULLSCREEN").get_reply()?.atom(), 134 | 135 | keys, 136 | mods, 137 | state: Default::default(), 138 | next_event: None, 139 | 140 | event_queue: Default::default(), 141 | next_request: None, 142 | stop_waker: None, 143 | 144 | conn, 145 | fd, 146 | window, 147 | }) 148 | } 149 | 150 | pub fn set_wm_name(&self, name: &str) -> Result<(), Error> { 151 | // TODO: set _NET_WM_NAME instead? or both? 152 | 153 | xcb::change_property(&self.conn, 154 | xcb::PROP_MODE_REPLACE as _, 155 | self.window, 156 | xcb::ATOM_WM_NAME, 157 | xcb::ATOM_STRING, 8, 158 | name.as_bytes() 159 | ).request_check()?; 160 | 161 | Ok(()) 162 | } 163 | 164 | pub fn set_wm_class(&self, instance: &str, class: &str) -> Result<(), Error> { 165 | // TODO: ensure neither class or instance contain nul byte 166 | let wm_class_string = format!("{}\0{}", instance, class); 167 | 168 | xcb::change_property(&self.conn, 169 | xcb::PROP_MODE_REPLACE as _, 170 | self.window, 171 | xcb::ATOM_WM_CLASS, 172 | xcb::ATOM_STRING, 8, 173 | wm_class_string.as_bytes() 174 | ).request_check()?; 175 | 176 | Ok(()) 177 | } 178 | 179 | pub fn map_window(&self) -> Result<(), Error> { 180 | xcb::change_property(&self.conn, 181 | xcb::PROP_MODE_REPLACE as _, 182 | self.window, 183 | self.atom_wm_protocols, 184 | xcb::ATOM_ATOM, 32, 185 | &[self.atom_wm_delete_window] 186 | ).request_check()?; 187 | 188 | xcb::change_property(&self.conn, 189 | xcb::PROP_MODE_APPEND as _, 190 | self.window, 191 | self.atom_net_wm_state, 192 | xcb::ATOM_ATOM, 32, 193 | &[self.atom_net_wm_state_fullscreen] 194 | ).request_check()?; 195 | 196 | xcb::map_window(&self.conn, self.window); 197 | 198 | self.flush()?; 199 | 200 | xcb::grab_button(&self.conn, 201 | false, // owner_events? 202 | self.window, 203 | xcb::BUTTON_MASK_ANY as _, 204 | xcb::GRAB_MODE_ASYNC as _, 205 | xcb::GRAB_MODE_ASYNC as _, 206 | self.window, 207 | xcb::NONE, 208 | xcb::BUTTON_INDEX_ANY as _, 209 | xcb::MOD_MASK_ANY as _, 210 | ).request_check()?; 211 | 212 | Ok(()) 213 | } 214 | 215 | pub fn flush(&self) -> Result<(), xcb::ConnError> { 216 | if self.conn.flush() { 217 | Ok(()) 218 | } else { 219 | Err(self.connection_error().unwrap()) 220 | } 221 | } 222 | 223 | pub fn connection_error(&self) -> Option { 224 | self.conn.has_error().err() 225 | } 226 | 227 | pub fn connection(&self) -> &xcb::Connection { 228 | &self.conn 229 | } 230 | 231 | pub fn pump(&mut self) -> Result { 232 | match self.next_event.take() { 233 | Some(e) => Ok(e), 234 | None => { 235 | if let Some(event) = self.conn.wait_for_event() { 236 | Ok(event) 237 | } else { 238 | Err(self.connection_error().unwrap().into()) 239 | } 240 | } 241 | } 242 | } 243 | 244 | pub fn poll(&mut self) -> Result, Error> { 245 | match self.next_event.take() { 246 | Some(e) => Ok(Some(e)), 247 | None => { 248 | if let Some(event) = self.conn.poll_for_event() { 249 | Ok(Some(event)) 250 | } else { 251 | self.connection_error().map(|e| Err(e.into())).transpose() 252 | } 253 | } 254 | } 255 | } 256 | 257 | pub fn peek(&mut self) -> Option<&xcb::GenericEvent> { 258 | if self.next_event.is_none() { 259 | if let Some(event) = self.conn.poll_for_event() { 260 | Some(self.next_event.get_or_insert(event)) 261 | } else { 262 | None 263 | } 264 | } else { 265 | self.next_event.as_ref() 266 | } 267 | } 268 | 269 | pub fn keycode(&self, code: xcb::Keycode) -> xcb::Keycode { 270 | code - self.conn.get_setup().min_keycode() 271 | } 272 | 273 | pub fn keysym(&self, code: xcb::Keycode) -> Option { 274 | let modifier = 0; // TODO: ? 275 | match self.keys.keysyms().get(code as usize * self.keys.keysyms_per_keycode() as usize + modifier).cloned() { 276 | Some(0) => None, 277 | keysym => keysym, 278 | } 279 | } 280 | 281 | pub fn stop(&mut self) { 282 | log::trace!("XContext::stop()"); 283 | 284 | self.state.running = false; 285 | if let Some(waker) = self.stop_waker.take() { 286 | waker.wake(); 287 | } 288 | } 289 | 290 | pub fn xmain(name: &str, instance: &str, class: &str) -> Result { 291 | let mut xcontext = Self::connect()?; 292 | xcontext.state.running = true; 293 | xcontext.set_wm_name(name)?; 294 | xcontext.set_wm_class(instance, class)?; 295 | xcontext.map_window()?; 296 | Ok(xcontext) 297 | } 298 | 299 | fn handle_grab_status(&self, status: u8) -> Result<(), Error> { 300 | if status == xcb::GRAB_STATUS_SUCCESS as _ { 301 | Ok(()) 302 | } else { 303 | Err(format_err!("X failed to grab with status code {}", status)) 304 | } 305 | } 306 | 307 | pub fn process_request(&mut self, request: &XRequest) -> Result<(), Error> { 308 | trace!("processing X request {:?}", request); 309 | 310 | Ok(match *request { 311 | XRequest::Quit => { 312 | self.stop(); 313 | }, 314 | XRequest::UnstickHost => { 315 | let keys = xcb::query_keymap(&self.conn).get_reply()?; 316 | let keys = keys.keys(); 317 | let mut keycode = 0usize; 318 | for &key in keys { 319 | for i in 0..8 { 320 | if key & (1 << i) != 0 { 321 | xcb::test::fake_input(&self.conn, 322 | xcb::KEY_RELEASE, 323 | keycode as _, 324 | xcb::CURRENT_TIME, 325 | xcb::NONE, 0, 0, 326 | xcb::NONE as _ // can't find documentation for this device_id argument? 327 | ).request_check()? 328 | } 329 | keycode += 1; 330 | } 331 | } 332 | }, 333 | XRequest::Grab { xcore, motion, confine, ref devices } => { 334 | if xcore { 335 | let status = xcb::grab_keyboard(&self.conn, 336 | false, // owner_events, I don't quite understand how this works 337 | self.window, 338 | xcb::CURRENT_TIME, 339 | xcb::GRAB_MODE_ASYNC as _, 340 | xcb::GRAB_MODE_ASYNC as _, 341 | ).get_reply()?.status(); 342 | self.handle_grab_status(status)?; 343 | let status = xcb::grab_pointer(&self.conn, 344 | false, // owner_events, I don't quite understand how this works 345 | self.window, 346 | (xcb::EVENT_MASK_BUTTON_PRESS | xcb::EVENT_MASK_BUTTON_RELEASE | xcb::EVENT_MASK_POINTER_MOTION | xcb::EVENT_MASK_BUTTON_MOTION) as _, 347 | xcb::GRAB_MODE_ASYNC as _, 348 | xcb::GRAB_MODE_ASYNC as _, 349 | if confine { self.window } else { xcb::NONE }, // confine mouse to our window 350 | xcb::NONE, 351 | xcb::CURRENT_TIME, 352 | ).get_reply()?.status(); 353 | self.handle_grab_status(status)?; 354 | } 355 | }, 356 | XRequest::Ungrab => { 357 | xcb::ungrab_keyboard(&self.conn, xcb::CURRENT_TIME).request_check()?; 358 | xcb::ungrab_pointer(&self.conn, xcb::CURRENT_TIME).request_check()?; 359 | }, 360 | }) 361 | } 362 | 363 | fn process_event(&mut self, event: &xcb::GenericEvent) -> Result<(), xcb::GenericError> { 364 | let kind = event.response_type() & !0x80; 365 | trace!("processing X event {}", kind); 366 | 367 | Ok(match kind { 368 | xcb::VISIBILITY_NOTIFY => { 369 | let event = unsafe { xcb::cast_event::(event) }; 370 | 371 | let dpms_blank = { 372 | let power_level = xcb::dpms::info(&self.conn).get_reply() 373 | .map(|info| info.power_level() as u32); 374 | 375 | power_level.unwrap_or(xcb::dpms::DPMS_MODE_ON) != xcb::dpms::DPMS_MODE_ON 376 | }; 377 | self.event_queue.push(if dpms_blank { 378 | XEvent::Visible(false) 379 | } else { 380 | match event.state() as _ { 381 | xcb::VISIBILITY_FULLY_OBSCURED => { 382 | XEvent::Visible(false) 383 | }, 384 | xcb::VISIBILITY_UNOBSCURED => { 385 | XEvent::Visible(true) 386 | }, 387 | state => { 388 | warn!("unknown visibility {}", state); 389 | return Ok(()) 390 | }, 391 | } 392 | }); 393 | }, 394 | xcb::CLIENT_MESSAGE => { 395 | let event = unsafe { xcb::cast_event::(event) }; 396 | 397 | match event.data().data32().get(0) { 398 | Some(&atom) if atom == self.atom_wm_delete_window => { 399 | self.event_queue.push(XEvent::Close); 400 | }, 401 | Some(&atom) => { 402 | let atom = xcb::get_atom_name(&self.conn, atom).get_reply(); 403 | info!("unknown X client message {:?}", 404 | atom.as_ref().map(|a| a.name()).unwrap_or("UNKNOWN") 405 | ); 406 | }, 407 | None => { 408 | warn!("empty client message"); 409 | }, 410 | } 411 | }, 412 | xcb::PROPERTY_NOTIFY => { 413 | let event = unsafe { xcb::cast_event::(event) }; 414 | 415 | match event.atom() { 416 | atom if atom == self.atom_wm_state => { 417 | let r = xcb::get_property(&self.conn, false, event.window(), event.atom(), 0, 0, 1).get_reply()?; 418 | let x = r.value::(); 419 | let window_state_withdrawn = 0; 420 | // 1 is back but unobscured also works so ?? 421 | let window_state_iconic = 3; 422 | match x.get(0) { 423 | Some(&state) if state == window_state_withdrawn || state == window_state_iconic => { 424 | self.event_queue.push(XEvent::Visible(false)); 425 | }, 426 | Some(&state) => { 427 | info!("unknown WM_STATE {}", state); 428 | }, 429 | None => { 430 | warn!("expected WM_STATE state value"); 431 | }, 432 | } 433 | }, 434 | atom => { 435 | let atom = xcb::get_atom_name(&self.conn, atom).get_reply(); 436 | info!("unknown property notify {:?}", 437 | atom.as_ref().map(|a| a.name()).unwrap_or("UNKNOWN") 438 | ); 439 | }, 440 | } 441 | }, 442 | xcb::FOCUS_OUT | xcb::FOCUS_IN => { 443 | self.event_queue.push(XEvent::Focus(kind == xcb::FOCUS_IN)); 444 | }, 445 | xcb::KEY_PRESS | xcb::KEY_RELEASE => { 446 | let event = unsafe { xcb::cast_event::(event) }; 447 | 448 | // filter out autorepeat events 449 | let peek = if let Some(peek) = self.peek() { 450 | let peek_kind = peek.response_type() & !0x80; 451 | match peek_kind { 452 | xcb::KEY_PRESS | xcb::KEY_RELEASE => { 453 | let peek_event = unsafe { xcb::cast_event::(peek) }; 454 | Some((peek_kind, peek_event.time(), peek_event.detail())) 455 | }, 456 | _ => None, 457 | } 458 | } else { 459 | None 460 | }; 461 | if let Some((peek_kind, peek_time, peek_detail)) = peek { 462 | if peek_kind != kind && peek_time == event.time() && event.detail() == peek_detail { 463 | // TODO: I think this only matters on release? 464 | // repeat 465 | return Ok(()) 466 | } 467 | } 468 | 469 | let keycode = self.keycode(event.detail()); 470 | let keysym = self.keysym(keycode); 471 | 472 | let event = XInputEvent { 473 | time: event.time(), 474 | data: XInputEventData::Key { 475 | pressed: kind == xcb::KEY_PRESS, 476 | keycode, 477 | keysym: if keysym == Some(0) { None } else { keysym }, 478 | state: event.state(), 479 | }, 480 | }; 481 | self.convert_x_events(&event) 482 | }, 483 | xcb::BUTTON_PRESS | xcb::BUTTON_RELEASE => { 484 | let event = unsafe { xcb::cast_event::(event) }; 485 | let event = XInputEvent { 486 | time: event.time(), 487 | data: XInputEventData::Button { 488 | pressed: kind == xcb::BUTTON_PRESS, 489 | button: event.detail(), 490 | state: event.state(), 491 | }, 492 | }; 493 | self.convert_x_events(&event) 494 | }, 495 | xcb::MOTION_NOTIFY => { 496 | let event = unsafe { xcb::cast_event::(event) }; 497 | let event = XInputEvent { 498 | time: event.time(), 499 | data: XInputEventData::Mouse { 500 | x: event.event_x(), 501 | y: event.event_y(), 502 | }, 503 | }; 504 | self.convert_x_events(&event) 505 | }, 506 | xcb::MAPPING_NOTIFY => { 507 | let setup = self.conn.get_setup(); 508 | self.keys = xcb::get_keyboard_mapping(&self.conn, setup.min_keycode(), setup.max_keycode() - setup.min_keycode()).get_reply()?; 509 | self.mods = xcb::get_modifier_mapping(&self.conn).get_reply()?; 510 | }, 511 | xcb::CONFIGURE_NOTIFY => { 512 | let event = unsafe { xcb::cast_event::(event) }; 513 | self.state.width = event.width(); 514 | self.state.height = event.height(); 515 | }, 516 | _ => { 517 | info!("unknown X event {}", event.response_type()); 518 | }, 519 | }) 520 | } 521 | 522 | fn x_button(button: xcb::Button) -> Option { 523 | match button as _ { 524 | xcb::BUTTON_INDEX_1 => Some(Key::ButtonLeft), 525 | xcb::BUTTON_INDEX_2 => Some(Key::ButtonMiddle), 526 | xcb::BUTTON_INDEX_3 => Some(Key::ButtonRight), 527 | xcb::BUTTON_INDEX_4 => Some(Key::ButtonGearUp), 528 | xcb::BUTTON_INDEX_5 => Some(Key::ButtonWheel), // Key::ButtonGearDown 529 | // TODO: 6/7 is horizontal scroll left/right, but I think this requires sending relative wheel events? 530 | 8 => Some(Key::ButtonSide), 531 | 9 => Some(Key::ButtonExtra), 532 | // qemu input-linux.c doesn't support fwd/back, but virtio probably does 533 | 10 => Some(Key::ButtonForward), 534 | 11 => Some(Key::ButtonBack), 535 | _ => None, 536 | } 537 | } 538 | 539 | fn x_keycode(key: xcb::Keycode) -> Option { 540 | match Key::from_code(key as _) { 541 | Ok(code) => Some(code), 542 | Err(..) => None, 543 | } 544 | } 545 | 546 | fn x_keysym(_key: xcb::Keysym) -> Option { 547 | unimplemented!() 548 | } 549 | 550 | fn key_event(time: EventTime, key: Key, pressed: bool) -> InputEvent { 551 | KeyEvent::new(time, key, KeyState::pressed(pressed)).into() 552 | } 553 | 554 | /*fn event_time(millis: xcb::Time) -> EventTime { 555 | let seconds = millis / 1000; 556 | let remaining = seconds % 1000; 557 | let usecs = remaining as i64 * 1000; 558 | 559 | EventTime::new(seconds as i64, usecs) 560 | }*/ 561 | 562 | fn convert_x_events(&mut self, e: &XInputEvent) { 563 | //let time = Self::event_time(e.time); 564 | let time = Default::default(); 565 | match e.data { 566 | XInputEventData::Mouse { x, y } => { 567 | self.event_queue.extend([ 568 | (self.state.width, x, AbsoluteAxis::X), 569 | (self.state.height, y, AbsoluteAxis::Y), 570 | ].iter() 571 | .filter(|&&(dim, new, _)| dim != 0) 572 | .map(|&(dim, new, axis)| (dim, (new.max(0) as u16).min(dim), axis)) 573 | .map(|(dim, new, axis)| AbsoluteEvent::new( 574 | time, 575 | axis, 576 | 0x7fff.min(new as i32 * 0x8000 / dim as i32), 577 | )).map(|e| XEvent::Input(e.into()))); 578 | }, 579 | XInputEventData::Button { pressed, button, state: _ } => { 580 | if let Some(button) = Self::x_button(button) { 581 | self.event_queue.push(XEvent::Input(Self::key_event(time, button, pressed).into())); 582 | } else { 583 | warn!("unknown X button {}", button); 584 | } 585 | }, 586 | XInputEventData::Key { pressed, keycode, keysym, state: _ } => { 587 | if let Some(key) = Self::x_keycode(keycode) { 588 | self.event_queue.push(XEvent::Input(Self::key_event(time, key, pressed).into())); 589 | } else { 590 | warn!("unknown X keycode {} keysym {:?}", keycode, keysym); 591 | } 592 | }, 593 | } 594 | self.event_queue.push(XEvent::Input(SynchronizeEvent::report(time).into())); 595 | } 596 | 597 | fn event_queue_pop(&mut self) -> Option { 598 | if self.event_queue.is_empty() { 599 | None 600 | } else { 601 | Some(self.event_queue.remove(0)) 602 | } 603 | } 604 | } 605 | 606 | impl Stream for XContext { 607 | type Item = Result; 608 | 609 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 610 | { 611 | let this = self.as_mut().get_mut(); 612 | 613 | if !this.state.running { 614 | return Poll::Ready(None) 615 | } 616 | 617 | match this.event_queue_pop() { 618 | Some(res) => 619 | return Poll::Ready(Some(Ok(res))), 620 | None => (), 621 | } 622 | 623 | // wait for x event 624 | // blocking version: this.pump() 625 | match this.poll()? { 626 | Some(event) => 627 | tokio::task::block_in_place(|| this.process_event(&event))?, 628 | None => { 629 | match this.fd.poll_read_ready(cx) { 630 | Poll::Pending => { 631 | this.stop_waker = Some(cx.waker().clone()); 632 | return Poll::Pending 633 | }, 634 | Poll::Ready(ready) => { 635 | let mut ready = ready?; 636 | if let Some(event) = this.conn.poll_for_event() { 637 | // poll returned None, so we know next_event is empty 638 | this.next_event = Some(event); 639 | ready.retain_ready() 640 | } else { 641 | ready.clear_ready() 642 | } 643 | }, 644 | } 645 | }, 646 | } 647 | } 648 | 649 | // recurse to return new events or wait on IO 650 | self.poll_next(cx) 651 | } 652 | } 653 | 654 | impl Sink for XContext { 655 | type Error = Error; 656 | 657 | fn start_send(self: Pin<&mut Self>, item: XRequest) -> Result<(), Self::Error> { 658 | let this = self.get_mut(); 659 | 660 | this.next_request = Some(item); 661 | 662 | Ok(()) 663 | } 664 | 665 | fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 666 | let this = self.get_mut(); 667 | 668 | if let Some(req) = this.next_request.take() { 669 | // TODO: consider storing errors instead of returning them here 670 | tokio::task::block_in_place(|| this.process_request(&req))?; 671 | Poll::Ready(Ok(())) 672 | } else { 673 | Poll::Ready(Ok(())) 674 | } 675 | } 676 | 677 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 678 | self.poll_ready(cx) 679 | } 680 | 681 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 682 | if self.state.running { 683 | ready!(self.as_mut().poll_flush(cx))?; 684 | self.stop(); 685 | } 686 | Poll::Ready(Ok(())) 687 | } 688 | } 689 | --------------------------------------------------------------------------------