├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build.bat ├── build.ps1 ├── dsclient ├── Cargo.toml └── src │ ├── bitmap.rs │ ├── client.rs │ └── main.rs ├── dscom ├── Cargo.toml └── src │ ├── convert.rs │ └── lib.rs ├── dsserver ├── Cargo.toml ├── build.rs └── src │ ├── convert.rs │ ├── key_mouse.rs │ ├── main.rs │ ├── screen.rs │ └── server.rs └── libs ├── enigo ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── feature_request.md │ │ └── question.md │ ├── actions │ │ ├── headless_display │ │ │ └── action.yml │ │ ├── install_deps │ │ │ └── action.yml │ │ └── screenshot │ │ │ └── action.yml │ ├── dependabot.yml │ └── workflows │ │ ├── build.yml │ │ ├── failing_tests.yml │ │ ├── integration.yml │ │ └── tests.yml ├── .gitignore ├── CHANGES.md ├── Cargo.toml ├── LICENSE ├── Permissions.md ├── README.md ├── examples │ ├── key.rs │ ├── keyboard.rs │ ├── layout.rs │ ├── mouse.rs │ ├── platform_specific.rs │ ├── serde.rs │ └── timer.rs ├── rustfmt.toml ├── src │ ├── agent.rs │ ├── keycodes.rs │ ├── lib.rs │ ├── linux │ │ ├── constants.rs │ │ ├── keymap.rs │ │ ├── libei.rs │ │ ├── mod.rs │ │ ├── wayland.rs │ │ ├── x11rb.rs │ │ └── xdo.rs │ ├── macos │ │ ├── macos_impl.rs │ │ └── mod.rs │ ├── platform.rs │ ├── tests │ │ ├── keyboard.rs │ │ ├── mod.rs │ │ └── mouse.rs │ └── win │ │ ├── mod.rs │ │ └── win_impl.rs └── tests │ ├── common │ ├── browser.rs │ ├── browser_events.rs │ ├── enigo_test.rs │ └── mod.rs │ ├── index.html │ └── integration_browser.rs ├── env-libvpx-sys ├── .github │ ├── dependabot.yml │ └── workflows │ │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── build.rs ├── ffi.h ├── generated │ ├── vpx-ffi-1.10.0.rs │ ├── vpx-ffi-1.11.0.rs │ ├── vpx-ffi-1.12.0.rs │ ├── vpx-ffi-1.13.0.rs │ ├── vpx-ffi-1.3.0.rs │ ├── vpx-ffi-1.4.0.rs │ ├── vpx-ffi-1.5.0.rs │ ├── vpx-ffi-1.6.1.rs │ ├── vpx-ffi-1.7.0.rs │ ├── vpx-ffi-1.8.0.rs │ ├── vpx-ffi-1.8.1.rs │ ├── vpx-ffi-1.8.2.rs │ └── vpx-ffi-1.9.0.rs ├── regen-ffi.bat ├── regen-ffi.sh ├── src │ └── lib.rs └── vpx-sys-test │ ├── .gitignore │ ├── Cargo.toml │ └── src │ └── main.rs └── vpx-codec ├── Cargo.toml └── src ├── decoder.rs ├── encoder.rs └── lib.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build 3 | 4 | on: 5 | push: 6 | branches: 7 | - "**" 8 | pull_request: 9 | branches: 10 | - master 11 | schedule: 12 | - cron: '30 13 * * *' 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | build: 20 | runs-on: ${{ matrix.os }} 21 | timeout-minutes: 120 22 | strategy: 23 | matrix: 24 | os: 25 | - ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v2 29 | - name: Build 30 | working-directory: "." 31 | shell: bash 32 | run: | 33 | function log 34 | { 35 | declare -rAi TAG=( 36 | [error]=31 37 | [info]=32 38 | [audit]=33 39 | ) 40 | printf '%(%y-%m-%d_%T)T\x1b[%dm\t%s:\t%b\x1b[0m\n' -1 "${TAG[${1,,:?}]}" "${1^^}" "${2:?}" 1>&2 41 | if [[ ${1} == 'error' ]]; then 42 | return 1 43 | fi 44 | } 45 | export -f log 46 | if [[ ${RUNNER_OS} == "Linux" ]]; then 47 | log 'info' 'Download dep' 48 | sudo apt-get update 49 | sudo apt-get install -y ninja-build lib{x11,xext,xft,xinerama,xcursor,xrender,xfixes,pango1.0,gl1-mesa,glu1-mesa,xdo,xcb-randr0}-dev 50 | log 'info' 'Cargo Clippy' 51 | cargo clippy --quiet 52 | log 'info' 'Cargo Build' 53 | cargo build --quiet --release 54 | fi > /dev/null 55 | - name: Archive 56 | uses: actions/upload-artifact@v4 57 | with: 58 | retention-days: 1 59 | path: target/release/* 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "dscom", 6 | "dsserver", 7 | "dsclient" 8 | ] 9 | 10 | [profile.release] 11 | strip = true 12 | opt-level = "z" 13 | lto = true 14 | panic = "abort" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diffscreen 2 | 3 | A toy remote desktop implemented by rust. 4 | 5 | The python implemented: https://github.com/pysrc/remote-desktop 6 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @REM Download https://github.com/ShiftMediaProject/libvpx/releases/download/v1.10.0/libvpx_v1.10.0_msvc16.zip 2 | @REM and unzip into %HomeDrive%%HomePath%\libvpx_v1.10.0_msvc16 3 | set VPX_STATIC=1 4 | set VPX_VERSION=1.10.0 5 | set VPX_LIB_DIR=%HomeDrive%%HomePath%\libvpx_v1.10.0_msvc16\lib\x64 6 | set VPX_INCLUDE_DIR=%HomeDrive%%HomePath%\libvpx_v1.10.0_msvc16\include 7 | 8 | @REM Download llvm from https://releases.llvm.org/download.html 9 | @REM # https://github.com/llvm/llvm-project/releases/download/llvmorg-18.1.8/clang+llvm-18.1.8-x86_64-pc-windows-msvc.tar.xz 10 | set LIBCLANG_PATH=%HomeDrive%%HomePath%\clang+llvm-18.1.8-x86_64-pc-windows-msvc\bin 11 | cargo build --release 12 | 13 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | # Download https://github.com/ShiftMediaProject/libvpx/releases/download/v1.10.0/libvpx_v1.10.0_msvc16.zip 2 | # and unzip into %HomeDrive%%HomePath%\libvpx_v1.10.0_msvc16 3 | $env:VPX_STATIC="1" 4 | $env:VPX_VERSION="1.10.0" 5 | $env:VPX_LIB_DIR="$env:HomeDrive$env:HomePath\libvpx_v1.10.0_msvc16\lib\x64" 6 | $env:VPX_INCLUDE_DIR="$env:HomeDrive$env:HomePath\libvpx_v1.10.0_msvc16\include" 7 | 8 | # Download llvm from https://releases.llvm.org/download.html 9 | # https://github.com/llvm/llvm-project/releases/download/llvmorg-18.1.8/clang+llvm-18.1.8-x86_64-pc-windows-msvc.tar.xz 10 | $env:LIBCLANG_PATH="$env:HomeDrive$env:HomePath\clang+llvm-18.1.8-x86_64-pc-windows-msvc\bin" 11 | cargo build --release 12 | -------------------------------------------------------------------------------- /dsclient/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dsclient" 3 | version = "0.5.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [profile.release] 9 | opt-level = "z" 10 | lto = true 11 | codegen-units = 1 12 | panic = "abort" 13 | 14 | [dependencies] 15 | dscom = {path = "../dscom"} 16 | 17 | fltk = { version = "1.5", features = ["fltk-bundled"] } 18 | 19 | vpx-codec = { path = "../libs/vpx-codec" } 20 | -------------------------------------------------------------------------------- /dsclient/src/bitmap.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | pub struct Bitmap(u128, u128); 4 | 5 | impl Bitmap { 6 | pub fn new() -> Self { 7 | Bitmap(0, 0) 8 | } 9 | pub fn push(&mut self, key: u8) -> bool { 10 | if key <= 127 { 11 | // 0-127 12 | let b = 1 << key; 13 | if self.1 & b == b { 14 | return false; 15 | } 16 | self.1 |= b; 17 | } else { 18 | // 128-255 19 | let b = 1 << (key - 128); 20 | if self.0 & b == b { 21 | return false; 22 | } 23 | self.0 |= b; 24 | } 25 | return true; 26 | } 27 | 28 | pub fn remove(&mut self, key: u8) { 29 | if key <= 127 { 30 | let b = !(1 << key); 31 | self.1 &= b; 32 | } else { 33 | let b = !(1 << (key - 128)); 34 | self.0 &= b; 35 | } 36 | } 37 | } 38 | 39 | impl Display for Bitmap { 40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { 41 | write!(f, "({:b}, {:b})", self.0, self.1) 42 | } 43 | } 44 | 45 | #[test] 46 | fn test() { 47 | let mut bm = Bitmap::new(); 48 | assert_eq!(bm.push(0), true); 49 | assert_eq!(bm.push(10), true); 50 | assert_eq!(bm.push(127), true); 51 | assert_eq!(bm.push(128), true); 52 | assert_eq!(bm.push(168), true); 53 | assert_eq!(bm.push(255), true); 54 | 55 | assert_eq!(bm.push(0), false); 56 | assert_eq!(bm.push(10), false); 57 | assert_eq!(bm.push(127), false); 58 | assert_eq!(bm.push(128), false); 59 | assert_eq!(bm.push(168), false); 60 | assert_eq!(bm.push(255), false); 61 | 62 | bm.remove(10); 63 | bm.remove(168); 64 | 65 | assert_eq!(bm.push(10), true); 66 | assert_eq!(bm.push(168), true); 67 | } 68 | -------------------------------------------------------------------------------- /dsclient/src/client.rs: -------------------------------------------------------------------------------- 1 | use fltk::button::Button; 2 | use fltk::enums::Color; 3 | use fltk::frame::Frame; 4 | use fltk::input::Input; 5 | use fltk::input::SecretInput; 6 | use fltk::prelude::InputExt; 7 | use fltk::window::Window; 8 | use std::collections::hash_map::DefaultHasher; 9 | use std::hash::Hasher; 10 | use std::io::Read; 11 | use std::io::Write; 12 | use std::net::TcpStream; 13 | use std::sync::Arc; 14 | use std::sync::RwLock; 15 | 16 | use fltk::app; 17 | use fltk::enums; 18 | use fltk::enums::Event; 19 | use fltk::image; 20 | use fltk::prelude::GroupExt; 21 | use fltk::prelude::ImageExt; 22 | use fltk::prelude::WidgetBase; 23 | use fltk::prelude::WidgetExt; 24 | 25 | use crate::bitmap; 26 | 27 | pub fn app_run() { 28 | let app = app::App::default(); 29 | let (sw, sh) = app::screen_size(); 30 | // 开始绘制wind窗口 31 | let mut wind = Window::new( 32 | (sw / 2.0) as i32 - 170, 33 | (sh / 2.0) as i32 - 70, 34 | 340, 35 | 140, 36 | "Diffscreen", 37 | ); 38 | wind.set_color(Color::from_rgb(255, 255, 255)); 39 | let mut host_ipt = Input::new(80, 20, 200, 25, "HOST:"); 40 | host_ipt.set_value("127.0.0.1:38971"); 41 | let mut pwd_ipt = SecretInput::new(80, 50, 200, 25, "PASS:"); 42 | pwd_ipt.set_value("diffscreen"); 43 | let mut login_btn = Button::new(200, 80, 80, 40, "Login"); 44 | // wind窗口结束绘制 45 | wind.end(); 46 | wind.show(); 47 | 48 | login_btn.set_callback(move |_| { 49 | wind.hide(); 50 | draw(host_ipt.value(), pwd_ipt.value()); 51 | }); 52 | app.run().unwrap(); 53 | } 54 | 55 | enum Msg { 56 | Draw, 57 | } 58 | 59 | // 解包 60 | #[inline] 61 | fn depack(buffer: &[u8]) -> usize { 62 | ((buffer[0] as usize) << 16) | ((buffer[1] as usize) << 8) | (buffer[2] as usize) 63 | } 64 | 65 | fn draw(host: String, pwd: String) { 66 | let mut conn = TcpStream::connect(host).unwrap(); 67 | // 认证 68 | let mut hasher = DefaultHasher::new(); 69 | hasher.write(pwd.as_bytes()); 70 | let pk = hasher.finish(); 71 | conn.write_all(&[ 72 | (pk >> (7 * 8)) as u8, 73 | (pk >> (6 * 8)) as u8, 74 | (pk >> (5 * 8)) as u8, 75 | (pk >> (4 * 8)) as u8, 76 | (pk >> (3 * 8)) as u8, 77 | (pk >> (2 * 8)) as u8, 78 | (pk >> (1 * 8)) as u8, 79 | pk as u8, 80 | ]) 81 | .unwrap(); 82 | let mut suc = [0u8]; 83 | conn.read_exact(&mut suc).unwrap(); 84 | if suc[0] != 1 { 85 | if suc[0] == 2 { 86 | panic!("Password error !"); 87 | } else { 88 | panic!("Some error !"); 89 | } 90 | } 91 | 92 | // 开始绘制wind2窗口 93 | let (sw, sh) = app::screen_size(); 94 | let mut wind_screen = Window::default() 95 | .with_size((sw / 2.0) as i32, (sh / 2.0) as i32) 96 | .with_label("Diffscreen"); 97 | let mut frame = Frame::default().size_of(&wind_screen); 98 | wind_screen.make_resizable(true); 99 | wind_screen.end(); 100 | wind_screen.show(); 101 | 102 | // 发送指令socket 103 | let mut txc = conn.try_clone().unwrap(); 104 | // 接收meta信息 105 | let mut meta = [0u8; 4]; 106 | if let Err(_) = conn.read_exact(&mut meta) { 107 | return; 108 | } 109 | let iw = (((meta[0] as u16) << 8) | meta[1] as u16) as i32; 110 | let ih = (((meta[2] as u16) << 8) | meta[3] as u16) as i32; 111 | 112 | let work_buf = Arc::new(RwLock::new(vec![0u8; (iw * ih * 3) as _])); 113 | let draw_work_buf = work_buf.clone(); 114 | let mut hooked = false; 115 | let mut bmap = bitmap::Bitmap::new(); 116 | let mut cmd_buf = [0u8; 5]; 117 | frame.handle(move |f, ev| { 118 | let (w, h) = (iw, ih); 119 | match ev { 120 | Event::Enter => { 121 | // 进入窗口 122 | hooked = true; 123 | } 124 | Event::Leave => { 125 | // 离开窗口 126 | hooked = false; 127 | } 128 | Event::KeyDown if hooked => { 129 | // 按键按下 130 | let key = app::event_key().bits() as u8; 131 | cmd_buf[0] = dscom::KEY_DOWN; 132 | cmd_buf[1] = key; 133 | if bmap.push(key) { 134 | txc.write_all(&cmd_buf[..2]).unwrap(); 135 | } 136 | } 137 | Event::Shortcut if hooked => { 138 | // 按键按下 139 | let key = app::event_key().bits() as u8; 140 | cmd_buf[0] = dscom::KEY_DOWN; 141 | cmd_buf[1] = key; 142 | if bmap.push(key) { 143 | txc.write_all(&cmd_buf[..2]).unwrap(); 144 | } 145 | } 146 | Event::KeyUp if hooked => { 147 | // 按键放开 148 | let key = app::event_key().bits() as u8; 149 | bmap.remove(key); 150 | cmd_buf[0] = dscom::KEY_UP; 151 | cmd_buf[1] = key; 152 | txc.write_all(&cmd_buf[..2]).unwrap(); 153 | } 154 | Event::Move if hooked => { 155 | // 鼠标移动 156 | let relx = (w * app::event_x() / f.width()) as u16; 157 | let rely = (h * app::event_y() / f.height()) as u16; 158 | // MOVE xu xd yu yd 159 | cmd_buf[0] = dscom::MOVE; 160 | cmd_buf[1] = (relx >> 8) as u8; 161 | cmd_buf[2] = relx as u8; 162 | cmd_buf[3] = (rely >> 8) as u8; 163 | cmd_buf[4] = rely as u8; 164 | txc.write_all(&cmd_buf).unwrap(); 165 | } 166 | Event::Push if hooked => { 167 | // 鼠标按下 168 | cmd_buf[0] = dscom::MOUSE_KEY_DOWN; 169 | cmd_buf[1] = app::event_key().bits() as u8; 170 | txc.write_all(&cmd_buf[..2]).unwrap(); 171 | } 172 | Event::Released if hooked => { 173 | // 鼠标释放 174 | cmd_buf[0] = dscom::MOUSE_KEY_UP; 175 | cmd_buf[1] = app::event_key().bits() as u8; 176 | txc.write_all(&cmd_buf[..2]).unwrap(); 177 | } 178 | Event::Drag if hooked => { 179 | // 鼠标按下移动 180 | let relx = (w * app::event_x() / f.width()) as u16; 181 | let rely = (h * app::event_y() / f.height()) as u16; 182 | // MOVE xu xd yu yd 183 | cmd_buf[0] = dscom::MOVE; 184 | cmd_buf[1] = (relx >> 8) as u8; 185 | cmd_buf[2] = relx as u8; 186 | cmd_buf[3] = (rely >> 8) as u8; 187 | cmd_buf[4] = rely as u8; 188 | txc.write_all(&cmd_buf).unwrap(); 189 | } 190 | Event::MouseWheel if hooked => { 191 | // app::MouseWheel::Down; 192 | match app::event_dy() { 193 | app::MouseWheel::Down => { 194 | // 滚轮下滚 195 | cmd_buf[0] = dscom::MOUSE_WHEEL_DOWN; 196 | txc.write_all(&cmd_buf[..1]).unwrap(); 197 | } 198 | app::MouseWheel::Up => { 199 | // 滚轮上滚 200 | cmd_buf[0] = dscom::MOUSE_WHEEL_UP; 201 | txc.write_all(&cmd_buf[..1]).unwrap(); 202 | } 203 | _ => {} 204 | } 205 | } 206 | _ => { 207 | if hooked { 208 | println!("{}", ev); 209 | } 210 | } 211 | } 212 | true 213 | }); 214 | frame.draw(move |frame|{ 215 | if let Ok(p) = draw_work_buf.read() { 216 | unsafe { 217 | if let Ok(mut image) = 218 | image::RgbImage::from_data2(&p, iw as _, ih as _, enums::ColorDepth::Rgb8 as i32, 0) 219 | { 220 | image.scale(frame.width(), frame.height(), false, true); 221 | image.draw(frame.x(), frame.y(), frame.width(), frame.height()); 222 | } 223 | } 224 | } 225 | }); 226 | 227 | let (tx, rx) = app::channel::(); 228 | 229 | std::thread::spawn(move || { 230 | let mut buf = Vec::::new(); 231 | let fps = 30; 232 | 233 | let ecfg = vpx_codec::decoder::Config { 234 | width: iw as _, 235 | height: ih as _, 236 | timebase: [1, (fps as i32) * 1000], // 120fps 237 | bitrate: 8192, 238 | codec: vpx_codec::decoder::VideoCodecId::VP8, 239 | }; 240 | 241 | let mut dec = vpx_codec::decoder::Decoder::new(ecfg).unwrap(); 242 | 243 | loop { 244 | let mut header = [0u8; 3]; 245 | if let Err(_) = conn.read_exact(&mut header) { 246 | return; 247 | } 248 | let recv_len = depack(&header); 249 | 250 | buf.resize(recv_len, 0u8); 251 | if let Err(e) = conn.read_exact(&mut buf) { 252 | println!("error {}", e); 253 | return; 254 | } 255 | 256 | if let Ok(pkgs) = dec.decode(&buf) { 257 | for ele in pkgs { 258 | let (y, u, v) = ele.data(); 259 | if let Ok(mut p) = work_buf.write() { 260 | dscom::convert::i420_to_rgb(ele.width(), ele.height(), y, u, v, &mut p, iw as _, ih as _); 261 | } 262 | tx.send(Msg::Draw); 263 | } 264 | } 265 | } 266 | }); 267 | while app::wait() { 268 | match rx.recv() { 269 | Some(Msg::Draw) => { 270 | frame.redraw(); 271 | } 272 | _ => {} 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /dsclient/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | mod bitmap; 3 | mod client; 4 | 5 | fn main() { 6 | client::app_run(); 7 | } 8 | -------------------------------------------------------------------------------- /dscom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dscom" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | -------------------------------------------------------------------------------- /dscom/src/convert.rs: -------------------------------------------------------------------------------- 1 | pub fn bgra_to_i420(width: usize, height: usize, src: &[u8], dest: &mut Vec) { 2 | let stride = src.len() / height; 3 | 4 | dest.clear(); 5 | 6 | for y in 0..height { 7 | for x in 0..width { 8 | let o = y * stride + 4 * x; 9 | 10 | let b = src[o] as i32; 11 | let g = src[o + 1] as i32; 12 | let r = src[o + 2] as i32; 13 | 14 | let y = (66 * r + 129 * g + 25 * b + 128) / 256 + 16; 15 | dest.push(clamp(y)); 16 | } 17 | } 18 | 19 | for y in (0..height).step_by(2) { 20 | for x in (0..width).step_by(2) { 21 | let o = y * stride + 4 * x; 22 | 23 | let b = src[o] as i32; 24 | let g = src[o + 1] as i32; 25 | let r = src[o + 2] as i32; 26 | 27 | let u = (-38 * r - 74 * g + 112 * b + 128) / 256 + 128; 28 | dest.push(clamp(u)); 29 | } 30 | } 31 | 32 | for y in (0..height).step_by(2) { 33 | for x in (0..width).step_by(2) { 34 | let o = y * stride + 4 * x; 35 | 36 | let b = src[o] as i32; 37 | let g = src[o + 1] as i32; 38 | let r = src[o + 2] as i32; 39 | 40 | let v = (112 * r - 94 * g - 18 * b + 128) / 256 + 128; 41 | dest.push(clamp(v)); 42 | } 43 | } 44 | } 45 | 46 | fn clamp(x: i32) -> u8 { 47 | x.min(255).max(0) as u8 48 | } 49 | 50 | #[allow(dead_code)] 51 | pub fn i420_to_rgb(width: usize, height: usize, sy: &[u8], su: &[u8], sv: &[u8], dest: &mut [u8], crop_width: usize, crop_height: usize) { 52 | // 确保裁剪尺寸不超过原始尺寸 53 | let crop_width = crop_width.min(width); 54 | let crop_height = crop_height.min(height); 55 | let uvw = width >> 1; 56 | for i in 0..crop_height { 57 | let sw = i * width; 58 | let swc = i * crop_width; 59 | let t = (i >> 1) * uvw; 60 | for j in 0..crop_width { 61 | let rgbstart = sw + j; 62 | let mut rgbstartc = swc + j; 63 | let uvi = t + (j >> 1); 64 | 65 | let y = sy[rgbstart] as i32; 66 | let u = su[uvi] as i32 - 128; 67 | let v = sv[uvi] as i32 - 128; 68 | 69 | rgbstartc *= 3; 70 | dest[rgbstartc] = clamp(y + (v * 359 >> 8)); 71 | dest[rgbstartc + 1] = clamp(y - (u * 88 >> 8) - (v * 182 >> 8)); 72 | dest[rgbstartc + 2] = clamp(y + (u * 453 >> 8)); 73 | 74 | } 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /dscom/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | // key事件 start 3 | pub const KEY_UP: u8 = 1; 4 | pub const KEY_DOWN: u8 = 2; 5 | pub const MOUSE_KEY_UP: u8 = 3; 6 | pub const MOUSE_KEY_DOWN: u8 = 4; 7 | pub const MOUSE_WHEEL_UP: u8 = 5; 8 | pub const MOUSE_WHEEL_DOWN: u8 = 6; 9 | pub const MOVE: u8 = 7; 10 | // key事件 end 11 | pub mod convert; -------------------------------------------------------------------------------- /dsserver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dsserver" 3 | version = "0.5.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | build = "build.rs" 8 | 9 | [dependencies] 10 | dscom = {path = "../dscom"} 11 | 12 | scrap = "0.5" 13 | 14 | vpx-codec = { path = "../libs/vpx-codec" } 15 | enigo = {path = "../libs/enigo"} 16 | 17 | [target.'cfg(windows)'.build-dependencies] 18 | winres = "0.1" 19 | -------------------------------------------------------------------------------- /dsserver/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "windows")] 2 | fn main() { 3 | let mut res = winres::WindowsResource::new(); 4 | // res.set_icon("test.ico"); 5 | res.set_manifest( 6 | r#" 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | "#, 16 | ); 17 | res.compile().unwrap(); 18 | } 19 | 20 | #[cfg(target_os = "linux")] 21 | fn main() {} -------------------------------------------------------------------------------- /dsserver/src/convert.rs: -------------------------------------------------------------------------------- 1 | pub fn bgra_to_i420(width: usize, height: usize, src: &[u8], dest: &mut Vec) { 2 | let stride = src.len() / height; 3 | 4 | dest.clear(); 5 | 6 | for y in 0..height { 7 | for x in 0..width { 8 | let o = y * stride + 4 * x; 9 | 10 | let b = src[o] as i32; 11 | let g = src[o + 1] as i32; 12 | let r = src[o + 2] as i32; 13 | 14 | let y = (66 * r + 129 * g + 25 * b + 128) / 256 + 16; 15 | dest.push(clamp(y)); 16 | } 17 | } 18 | 19 | for y in (0..height).step_by(2) { 20 | for x in (0..width).step_by(2) { 21 | let o = y * stride + 4 * x; 22 | 23 | let b = src[o] as i32; 24 | let g = src[o + 1] as i32; 25 | let r = src[o + 2] as i32; 26 | 27 | let u = (-38 * r - 74 * g + 112 * b + 128) / 256 + 128; 28 | dest.push(clamp(u)); 29 | } 30 | } 31 | 32 | for y in (0..height).step_by(2) { 33 | for x in (0..width).step_by(2) { 34 | let o = y * stride + 4 * x; 35 | 36 | let b = src[o] as i32; 37 | let g = src[o + 1] as i32; 38 | let r = src[o + 2] as i32; 39 | 40 | let v = (112 * r - 94 * g - 18 * b + 128) / 256 + 128; 41 | dest.push(clamp(v)); 42 | } 43 | } 44 | } 45 | 46 | fn clamp(x: i32) -> u8 { 47 | x.min(255).max(0) as u8 48 | } 49 | 50 | #[allow(dead_code)] 51 | pub fn i420_to_rgb(width: usize, height: usize, sy: &[u8], su: &[u8], sv: &[u8], dest: &mut [u8]) { 52 | let uvw = width >> 1; 53 | for i in 0..height { 54 | let sw = i * width; 55 | let t = (i >> 1) * uvw; 56 | for j in 0..width { 57 | let mut rgbstart = sw + j; 58 | let uvi = t + (j >> 1); 59 | 60 | let y = sy[rgbstart] as i32; 61 | let u = su[uvi] as i32 - 128; 62 | let v = sv[uvi] as i32 - 128; 63 | 64 | rgbstart *= 3; 65 | dest[rgbstart] = clamp(y + (v * 359 >> 8)); 66 | dest[rgbstart + 1] = clamp(y - (u * 88 >> 8) - (v * 182 >> 8)); 67 | dest[rgbstart + 2] = clamp(y + (u * 453 >> 8)); 68 | 69 | } 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /dsserver/src/key_mouse.rs: -------------------------------------------------------------------------------- 1 | pub fn mouse_to_engin(key: u8) -> Option { 2 | match key { 3 | 233 => Some(enigo::Button::Left), 4 | 235 => Some(enigo::Button::Right), 5 | _ => None, 6 | } 7 | } 8 | 9 | pub fn key_to_enigo(key: u8) -> Option { 10 | match key { 11 | 27 => Some(enigo::Key::Escape), 12 | 190 => Some(enigo::Key::F1), 13 | 191 => Some(enigo::Key::F2), 14 | 192 => Some(enigo::Key::F3), 15 | 193 => Some(enigo::Key::F4), 16 | 194 => Some(enigo::Key::F5), 17 | 195 => Some(enigo::Key::F6), 18 | 196 => Some(enigo::Key::F7), 19 | 197 => Some(enigo::Key::F8), 20 | 198 => Some(enigo::Key::F9), 21 | 199 => Some(enigo::Key::F10), 22 | 200 => Some(enigo::Key::F11), 23 | 201 => Some(enigo::Key::F12), 24 | // 19 => Some(enigo::Key::Pause), // Pause 25 | // 97 => Some(enigo::Key::Print), // Print 26 | 255 => Some(enigo::Key::Delete), 27 | 87 => Some(enigo::Key::End), 28 | 96 => Some(enigo::Key::Unicode('`')), 29 | 48 => Some(enigo::Key::Unicode('0')), 30 | 49 => Some(enigo::Key::Unicode('1')), 31 | 50 => Some(enigo::Key::Unicode('2')), 32 | 51 => Some(enigo::Key::Unicode('3')), 33 | 52 => Some(enigo::Key::Unicode('4')), 34 | 53 => Some(enigo::Key::Unicode('5')), 35 | 54 => Some(enigo::Key::Unicode('6')), 36 | 55 => Some(enigo::Key::Unicode('7')), 37 | 56 => Some(enigo::Key::Unicode('8')), 38 | 57 => Some(enigo::Key::Unicode('9')), 39 | 45 => Some(enigo::Key::Unicode('-')), 40 | 61 => Some(enigo::Key::Unicode('=')), 41 | 8 => Some(enigo::Key::Backspace), 42 | 9 => Some(enigo::Key::Tab), 43 | 91 => Some(enigo::Key::Unicode('[')), 44 | 93 => Some(enigo::Key::Unicode(']')), 45 | 92 => Some(enigo::Key::Unicode('\\')), 46 | 229 => Some(enigo::Key::CapsLock), 47 | 59 => Some(enigo::Key::Unicode(';')), 48 | 39 => Some(enigo::Key::Unicode('\'')), 49 | 13 => Some(enigo::Key::Return), 50 | 225 => Some(enigo::Key::Shift), // ShiftL 51 | 44 => Some(enigo::Key::Unicode(',')), 52 | 46 => Some(enigo::Key::Unicode('.')), 53 | 47 => Some(enigo::Key::Unicode('/')), 54 | 226 => Some(enigo::Key::Shift), // ShiftR 55 | 82 => Some(enigo::Key::UpArrow), 56 | 227 => Some(enigo::Key::Control), // ControlL 57 | 233 => Some(enigo::Key::Alt), // AltL 58 | 32 => Some(enigo::Key::Space), 59 | 234 => Some(enigo::Key::Alt), // AltR 60 | // 103 => Some(enigo::Key::Menu), 61 | 228 => Some(enigo::Key::Control), // ControlR 62 | 81 => Some(enigo::Key::LeftArrow), 63 | 84 => Some(enigo::Key::DownArrow), 64 | 83 => Some(enigo::Key::RightArrow), 65 | // 99 => Some(enigo::Key::Raw(45)), // Insert 66 | 86 => Some(enigo::Key::PageDown), 67 | 80 => Some(enigo::Key::Home), 68 | 85 => Some(enigo::Key::PageUp), 69 | a if a >= 97 && a <= 122 => Some(enigo::Key::Unicode((a - 97 + ('a' as u8)) as char)), 70 | _ => None, 71 | } 72 | } -------------------------------------------------------------------------------- /dsserver/src/main.rs: -------------------------------------------------------------------------------- 1 | mod key_mouse; 2 | mod screen; 3 | mod server; 4 | mod convert; 5 | fn main() { 6 | let args: Vec = std::env::args().collect(); 7 | 8 | // defalut password 9 | let mut pwd = String::from("diffscreen"); 10 | if args.len() >= 2 { 11 | pwd = args[1].clone(); 12 | } 13 | 14 | // defalut port 15 | let mut port = 38971; 16 | if args.len() >= 3 { 17 | port = args[2].parse::().unwrap(); 18 | } 19 | 20 | // run forever 21 | server::run(port, pwd); 22 | } 23 | -------------------------------------------------------------------------------- /dsserver/src/screen.rs: -------------------------------------------------------------------------------- 1 | use scrap::Capturer; 2 | use scrap::Display; 3 | use std::io::ErrorKind::WouldBlock; 4 | use std::slice::from_raw_parts; 5 | use std::sync::LazyLock; 6 | use std::sync::Mutex; 7 | use std::time::Duration; 8 | 9 | use crate::convert; 10 | 11 | static mut CAP: LazyLock> = LazyLock::new(||{ 12 | Mutex::new(Cap::new()) 13 | }); 14 | 15 | pub fn cap_wh() -> Option<(usize, usize)> { 16 | unsafe { 17 | match CAP.lock() { 18 | Ok(cap)=> { 19 | Some(cap.wh()) 20 | } 21 | Err(_) => { 22 | None 23 | } 24 | } 25 | } 26 | } 27 | 28 | pub fn cap_screen(yuv: &mut Vec) -> Option<(usize, usize)> { 29 | unsafe { 30 | match CAP.lock() { 31 | Ok(mut cap)=> { 32 | let (bgra, width, height) = cap.cap(); 33 | convert::bgra_to_i420(width, height, &bgra, yuv); 34 | Some((width, height)) 35 | } 36 | Err(_) => { 37 | None 38 | } 39 | } 40 | } 41 | 42 | } 43 | 44 | 45 | /** 46 | * 截屏 47 | */ 48 | pub struct Cap { 49 | w: usize, 50 | h: usize, 51 | capturer: Option, 52 | sleep: Duration, 53 | } 54 | impl Cap { 55 | pub fn new() -> Cap { 56 | let display = Display::primary().unwrap(); 57 | let capturer = Capturer::new(display).unwrap(); 58 | let (w, h) = (capturer.width(), capturer.height()); 59 | Cap { 60 | w, 61 | h, 62 | capturer: Some(capturer), 63 | sleep: Duration::new(1, 0) / 60, 64 | } 65 | } 66 | fn reload(&mut self) { 67 | println!("Reload capturer"); 68 | drop(self.capturer.take()); 69 | let display = match Display::primary() { 70 | Ok(display) => display, 71 | Err(_) => { 72 | return; 73 | } 74 | }; 75 | 76 | let capturer = match Capturer::new(display) { 77 | Ok(capturer) => capturer, 78 | Err(_) => return, 79 | }; 80 | self.h = capturer.height(); 81 | self.w = capturer.width(); 82 | self.capturer = Some(capturer); 83 | } 84 | pub fn wh(&self) -> (usize, usize) { 85 | (self.w, self.h) 86 | } 87 | #[inline] 88 | pub fn cap(&mut self) -> (&[u8], usize, usize) { 89 | loop { 90 | match &mut self.capturer { 91 | Some(capturer) => { 92 | // Wait until there's a frame. 93 | let cp = capturer.frame(); 94 | let buffer = match cp { 95 | Ok(buffer) => buffer, 96 | Err(error) => { 97 | std::thread::sleep(self.sleep); 98 | if error.kind() == WouldBlock { 99 | // Keep spinning. 100 | continue; 101 | } else { 102 | std::thread::sleep(std::time::Duration::from_millis(200)); 103 | self.reload(); 104 | continue; 105 | } 106 | } 107 | }; 108 | return (unsafe { from_raw_parts(buffer.as_ptr(), buffer.len()) }, self.w, self.h); 109 | // return (buffer.as_ptr(), buffer.len()); 110 | } 111 | None => { 112 | std::thread::sleep(std::time::Duration::from_millis(200)); 113 | self.reload(); 114 | continue; 115 | } 116 | }; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /dsserver/src/server.rs: -------------------------------------------------------------------------------- 1 | use enigo::Axis; 2 | use enigo::Coordinate; 3 | use enigo::Direction; 4 | use enigo::Enigo; 5 | use enigo::Keyboard; 6 | use enigo::Mouse; 7 | use enigo::Settings; 8 | 9 | use crate::key_mouse; 10 | use crate::screen; 11 | use std::collections::hash_map::DefaultHasher; 12 | use std::hash::Hasher; 13 | use std::io::Read; 14 | use std::io::Write; 15 | use std::net::TcpListener; 16 | use std::net::TcpStream; 17 | use std::sync::mpsc::channel; 18 | use std::sync::LazyLock; 19 | use std::sync::Mutex; 20 | use std::thread; 21 | use std::time; 22 | 23 | pub fn run(port: u16, pwd: String) { 24 | let mut hasher = DefaultHasher::new(); 25 | hasher.write(pwd.as_bytes()); 26 | let pk = hasher.finish(); 27 | let suc = [ 28 | (pk >> (7 * 8)) as u8, 29 | (pk >> (6 * 8)) as u8, 30 | (pk >> (5 * 8)) as u8, 31 | (pk >> (4 * 8)) as u8, 32 | (pk >> (3 * 8)) as u8, 33 | (pk >> (2 * 8)) as u8, 34 | (pk >> (1 * 8)) as u8, 35 | pk as u8, 36 | ]; 37 | let (tx6, rx) = channel::(); 38 | if cfg!(target_os = "windows") { 39 | let tx4 = tx6.clone(); 40 | std::thread::spawn(move || { 41 | let listener_ipv4 = TcpListener::bind(format!("0.0.0.0:{}", port)).unwrap(); 42 | for sr in listener_ipv4.incoming() { 43 | match sr { 44 | Ok(stream) => { 45 | tx4.send(stream).unwrap(); 46 | } 47 | Err(e) => { 48 | println!("error {}", e); 49 | } 50 | } 51 | } 52 | }); 53 | } 54 | std::thread::spawn(move || { 55 | let listener_ipv6 = TcpListener::bind(format!("[::0]:{}", port)).unwrap(); 56 | for sr in listener_ipv6.incoming() { 57 | match sr { 58 | Ok(stream) => { 59 | tx6.send(stream).unwrap(); 60 | } 61 | Err(e) => { 62 | println!("error {}", e); 63 | } 64 | } 65 | } 66 | }); 67 | 68 | loop { 69 | match rx.recv() { 70 | Ok(mut stream) => { 71 | // 检查连接合法性 72 | let mut check = [0u8; 8]; 73 | match stream.read_exact(&mut check) { 74 | Ok(_) => { 75 | if suc != check { 76 | println!("Password error"); 77 | let _ = stream.write_all(&[2]); 78 | continue; 79 | } 80 | } 81 | Err(_) => { 82 | println!("Request error"); 83 | continue; 84 | } 85 | } 86 | if let Err(_) = stream.write_all(&[1]) { 87 | continue; 88 | } 89 | let ss = stream.try_clone().unwrap(); 90 | let th1 = std::thread::spawn(move || { 91 | if let Err(e) = std::panic::catch_unwind(||{ 92 | screen_stream(ss); 93 | }) { 94 | eprintln!("{:?}", e); 95 | } 96 | }); 97 | let th2 = std::thread::spawn(move || { 98 | if let Err(e) = std::panic::catch_unwind(||{ 99 | event(stream); 100 | }) { 101 | eprintln!("{:?}", e); 102 | } 103 | }); 104 | th1.join().unwrap(); 105 | th2.join().unwrap(); 106 | println!("Break !"); 107 | } 108 | Err(_) => { 109 | return; 110 | } 111 | } 112 | } 113 | } 114 | 115 | 116 | static mut ENIGO: LazyLock> = 117 | LazyLock::new(|| Mutex::new(Enigo::new(&Settings::default()).unwrap())); 118 | 119 | /** 120 | * 事件处理 121 | */ 122 | fn event(mut stream: TcpStream) { 123 | let mut cmd = [0u8]; 124 | let mut move_cmd = [0u8; 4]; 125 | while let Ok(_) = stream.read_exact(&mut cmd) { 126 | match cmd[0] { 127 | dscom::KEY_UP => { 128 | stream.read_exact(&mut cmd).unwrap(); 129 | if let Some(key) = key_mouse::key_to_enigo(cmd[0]) { 130 | unsafe { 131 | if let std::result::Result::Ok(mut eg) = ENIGO.lock() { 132 | let _ = eg.key(key, Direction::Release); 133 | } 134 | } 135 | } 136 | } 137 | dscom::KEY_DOWN => { 138 | stream.read_exact(&mut cmd).unwrap(); 139 | if let Some(key) = key_mouse::key_to_enigo(cmd[0]) { 140 | unsafe { 141 | if let std::result::Result::Ok(mut eg) = ENIGO.lock() { 142 | let _ = eg.key(key, Direction::Press); 143 | } 144 | } 145 | } 146 | } 147 | dscom::MOUSE_KEY_UP => { 148 | stream.read_exact(&mut cmd).unwrap(); 149 | if let Some(button) = key_mouse::mouse_to_engin(cmd[0]) { 150 | unsafe { 151 | if let std::result::Result::Ok(mut eg) = ENIGO.lock() { 152 | let _ = eg.button(button, Direction::Release); 153 | } 154 | } 155 | } 156 | } 157 | dscom::MOUSE_KEY_DOWN => { 158 | stream.read_exact(&mut cmd).unwrap(); 159 | if let Some(button) = key_mouse::mouse_to_engin(cmd[0]) { 160 | unsafe { 161 | if let std::result::Result::Ok(mut eg) = ENIGO.lock() { 162 | let _ = eg.button(button, Direction::Press); 163 | } 164 | } 165 | } 166 | } 167 | dscom::MOUSE_WHEEL_UP => unsafe { 168 | if let std::result::Result::Ok(mut eg) = ENIGO.lock() { 169 | let _ = eg.scroll(-2, Axis::Vertical); 170 | } 171 | } 172 | dscom::MOUSE_WHEEL_DOWN => unsafe { 173 | if let std::result::Result::Ok(mut eg) = ENIGO.lock() { 174 | let _ = eg.scroll(2, Axis::Vertical); 175 | } 176 | } 177 | dscom::MOVE => { 178 | stream.read_exact(&mut move_cmd).unwrap(); 179 | let x = ((move_cmd[0] as i32) << 8) | (move_cmd[1] as i32); 180 | let y = ((move_cmd[2] as i32) << 8) | (move_cmd[3] as i32); 181 | unsafe { 182 | if let std::result::Result::Ok(mut eg) = ENIGO.lock() { 183 | let _ = eg.move_mouse(x, y, Coordinate::Abs); 184 | } 185 | } 186 | } 187 | _ => { 188 | return; 189 | } 190 | } 191 | } 192 | } 193 | 194 | /** 195 | * 编码数据header 196 | */ 197 | #[inline] 198 | fn encode(data_len: usize, res: &mut [u8]) { 199 | res[0] = (data_len >> 16) as u8; 200 | res[1] = (data_len >> 8) as u8; 201 | res[2] = data_len as u8; 202 | } 203 | 204 | 205 | /* 206 | 图像字节序 207 | +------------+ 208 | | 24 | 209 | +------------+ 210 | | length | 211 | +------------+ 212 | | data | 213 | +------------+ 214 | length: 数据长度 215 | data: 数据 216 | */ 217 | fn screen_stream(mut stream: TcpStream) { 218 | let fps = 30; 219 | let spf = time::Duration::from_nanos(1_000_000_000 / fps); 220 | // vpxencode 221 | let (iw, ih) = screen::cap_wh().unwrap(); 222 | let ecfg = vpx_codec::encoder::Config { 223 | width: iw as _, 224 | height: ih as _, 225 | timebase: [1, (fps as i32) * 1000], // 120fps 226 | bitrate: 8192, 227 | codec: vpx_codec::encoder::VideoCodecId::VP8, 228 | }; 229 | let mut enc = vpx_codec::encoder::Encoder::new(ecfg).unwrap(); 230 | 231 | 232 | let (w, h) = (iw as usize, ih as usize); 233 | 234 | // 发送w, h 235 | let mut meta = [0u8; 4]; 236 | meta[0] = (w >> 8) as u8; 237 | meta[1] = w as u8; 238 | meta[2] = (h >> 8) as u8; 239 | meta[3] = h as u8; 240 | if let Err(_) = stream.write_all(&meta) { 241 | return; 242 | } 243 | 244 | let mut header = [0u8; 3]; 245 | let start = time::Instant::now(); 246 | let mut yuv = Vec::::new(); 247 | loop { 248 | let now = time::Instant::now(); 249 | let time = now - start; 250 | let ms = time.as_secs() * 1000 + time.subsec_millis() as u64; 251 | match screen::cap_screen(&mut yuv) { 252 | Some((_iw, _ih)) => { 253 | if iw != _iw || ih != _ih { 254 | let _ = enc.finish(); 255 | println!("encode break work."); 256 | return; 257 | } 258 | 259 | for f in enc.encode(ms as i64, &yuv).unwrap() { 260 | let len = f.data.len(); 261 | encode(len, &mut header); 262 | if let Err(_) = stream.write_all(&header) { 263 | return; 264 | } 265 | if let Err(_) = stream.write_all(f.data) { 266 | return; 267 | } 268 | } 269 | let dt = now.elapsed(); 270 | if dt < spf { 271 | thread::sleep(spf - dt); 272 | } 273 | } 274 | None => { 275 | thread::sleep(time::Duration::from_millis(10)); 276 | } 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug, needs investigation 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps or a minimal code example to reproduce the behavior. 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Environment (please complete the following information):** 19 | 20 | - OS: [e.g. Linux, Windows, macOS ..] 21 | - Rust [e.g. rustc --version] 22 | - Library Version [e.g. enigo 0.0.13 or commit hash fa448be ] 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement, needs investigation 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /libs/enigo/.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask your Question here 4 | title: "" 5 | labels: question 6 | assignees: "" 7 | --- 8 | 9 | **Describe your Question** 10 | A clear and concise description of what you want to know. 11 | 12 | **Describe your Goal** 13 | A clear and concise description of what you want to achieve. Consider the [XYProblem](http://xyproblem.info/) 14 | 15 | **Environment (please complete the following information):** 16 | 17 | - OS: [e.g. Linux, Windows, macOS ..] 18 | - Rust [e.g. rustc --version] 19 | - Library Version [e.g. enigo 0.0.13 or commit hash fa448be ] 20 | -------------------------------------------------------------------------------- /libs/enigo/.github/actions/headless_display/action.yml: -------------------------------------------------------------------------------- 1 | name: "headless_display" 2 | description: "Creates a virtual display so e.g Firefox can start" 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Install Xvfb on Linux 8 | if: runner.os == 'Linux' 9 | run: | 10 | sudo apt-get install -y xvfb 11 | which Xvfb 12 | # Set DISPLAY env variable 13 | echo "DISPLAY=:99.0" >> $GITHUB_ENV 14 | # Create virtual display with Xvfb 15 | Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 16 | # Wait for Xvfb to start 17 | sleep 3 18 | shell: bash 19 | -------------------------------------------------------------------------------- /libs/enigo/.github/actions/install_deps/action.yml: -------------------------------------------------------------------------------- 1 | name: "Install_deps" 2 | description: "Installs the dependencies and updates the system" 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Install dependencies 8 | run: | 9 | if [ "$RUNNER_OS" == "Linux" ]; then 10 | sudo apt-get update -q -y && sudo apt-get upgrade -y 11 | sudo apt-get install -y libxdo-dev libxkbcommon-dev libxi-dev libxtst-dev 12 | echo "$RUNNER_OS" 13 | elif [ "$RUNNER_OS" == "Windows" ]; then 14 | echo "$RUNNER_OS" 15 | elif [ "$RUNNER_OS" == "macOS" ]; then 16 | echo "$RUNNER_OS" 17 | else 18 | echo "$RUNNER_OS not supported" 19 | exit 1 20 | fi 21 | shell: bash 22 | -------------------------------------------------------------------------------- /libs/enigo/.github/actions/screenshot/action.yml: -------------------------------------------------------------------------------- 1 | name: "screenshot" 2 | description: "Creates a screenshot" 3 | inputs: 4 | rust: 5 | description: 'Name of the Rust version' 6 | required: true 7 | platform: 8 | description: 'Name of the OS' 9 | required: true 10 | feature: 11 | description: 'Activated features' 12 | required: true 13 | 14 | runs: 15 | using: "composite" 16 | steps: 17 | - name: Echo inputs 18 | shell: bash 19 | run: echo "${{ inputs.platform }}_${{ inputs.rust }}_${{ inputs.feature }}" 20 | - uses: OrbitalOwen/desktop-screenshot-action@7f96d072f57e00c3bad73e9f3282f1258262b85d 21 | if: runner.os != 'Linux' 22 | with: 23 | file-name: '${{ inputs.platform }}_${{ inputs.rust }}_${{ inputs.feature }}.jpg' 24 | - name: Take screenshot on Linux 25 | if: runner.os == 'Linux' 26 | shell: bash 27 | run: DISPLAY=$DISPLAY import -window root -quality 90 ./'${{ inputs.platform }}_${{ inputs.rust }}_${{ inputs.feature }}.jpg' 28 | - uses: actions/upload-artifact@v4 29 | if: runner.os == 'Linux' 30 | with: 31 | name: '${{ inputs.platform }}_${{ inputs.rust }}_${{ inputs.feature }}.jpg' 32 | path: '${{ inputs.platform }}_${{ inputs.rust }}_${{ inputs.feature }}.jpg' 33 | overwrite: true -------------------------------------------------------------------------------- /libs/enigo/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | 10 | # Maintain dependencies for cargo 11 | - package-ecosystem: "cargo" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /libs/enigo/.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | permissions: 4 | contents: read 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | branches: 9 | - main 10 | push: 11 | branches: 12 | - main 13 | 14 | env: 15 | CARGO_TERM_COLOR: always 16 | RUST_BACKTRACE: 1 17 | 18 | jobs: 19 | build: 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | rust: 24 | - stable 25 | - nightly 26 | - "1.80.0" 27 | platform: 28 | - ubuntu-latest 29 | - windows-latest 30 | - macos-latest 31 | features: 32 | - "libei,wayland,xdo,x11rb" 33 | - "default" 34 | - "libei" 35 | - "wayland" 36 | - "xdo" 37 | - "x11rb" 38 | exclude: 39 | - platform: windows-latest 40 | features: "libei,wayland,xdo,x11rb" 41 | - platform: windows-latest 42 | features: "libei" 43 | - platform: windows-latest 44 | features: "wayland" 45 | - platform: windows-latest 46 | features: "xdo" 47 | - platform: windows-latest 48 | features: "x11rb" 49 | - platform: macos-latest 50 | features: "libei,wayland,xdo,x11rb" 51 | - platform: macos-latest 52 | features: "libei" 53 | - platform: macos-latest 54 | features: "wayland" 55 | - platform: macos-latest 56 | features: "xdo" 57 | - platform: macos-latest 58 | features: "x11rb" 59 | runs-on: ${{ matrix.platform }} 60 | steps: 61 | - uses: actions/checkout@v4 62 | - uses: ./.github/actions/install_deps 63 | - uses: dtolnay/rust-toolchain@master 64 | with: 65 | toolchain: ${{ matrix.rust }} 66 | components: rustfmt, clippy 67 | 68 | - name: Check the code format 69 | if: matrix.rust == 'nightly' # Avoid differences between the versions 70 | run: cargo fmt -- --check 71 | 72 | - name: Check clippy lints 73 | run: cargo clippy --no-default-features --features ${{ matrix.features }} -- -D clippy::pedantic 74 | - name: Check clippy lints for the examples 75 | run: cargo clippy --no-default-features --features ${{ matrix.features }} --examples -- -D clippy::pedantic 76 | 77 | - name: Build the code 78 | run: cargo build --no-default-features --features ${{ matrix.features }} 79 | 80 | - name: Build the docs 81 | run: cargo doc --no-deps --no-default-features --features ${{ matrix.features }} 82 | 83 | - name: Build the examples 84 | run: cargo build --examples --no-default-features --features ${{ matrix.features }} 85 | 86 | - name: Build the examples in release mode 87 | run: cargo build --release --examples --no-default-features --features ${{ matrix.features }} -------------------------------------------------------------------------------- /libs/enigo/.github/workflows/failing_tests.yml: -------------------------------------------------------------------------------- 1 | name: Failing_tests 2 | 3 | permissions: 4 | contents: read 5 | on: 6 | workflow_dispatch: 7 | workflow_run: 8 | workflows: ["Integration"] 9 | types: 10 | - completed 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | RUST_BACKTRACE: 1 15 | 16 | jobs: 17 | additional_tests: 18 | if: ${{ github.event.workflow_run.conclusion == 'success' }} # Only run if the integration tests succeeded 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | rust: 23 | - stable 24 | - nightly 25 | - "1.80.0" 26 | platform: 27 | - ubuntu-latest 28 | - windows-latest 29 | - macos-latest 30 | features: 31 | # The tests will fail on Ubuntu for libei and wayland, because the compositor of the Github runner does not support it 32 | #- "libei,wayland,xdo,x11rb" 33 | - "default" 34 | #- "libei" 35 | #- "wayland" 36 | - "xdo" 37 | - "x11rb" 38 | exclude: 39 | # The implementation on Windows and macOS does not have any features so we can reduce the number of combinations 40 | #- platform: windows-latest 41 | # features: "libei,wayland,xdo,x11rb" 42 | #- platform: windows-latest 43 | # features: "libei" 44 | #- platform: windows-latest 45 | # features: "wayland" 46 | - platform: windows-latest 47 | features: "xdo" 48 | - platform: windows-latest 49 | features: "x11rb" 50 | #- platform: macos-latest 51 | # features: "libei,wayland,xdo,x11rb" 52 | #- platform: macos-latest 53 | # features: "libei" 54 | #- platform: macos-latest 55 | # features: "wayland" 56 | - platform: macos-latest 57 | features: "xdo" 58 | - platform: macos-latest 59 | features: "x11rb" 60 | runs-on: ${{ matrix.platform }} 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: ./.github/actions/install_deps 64 | - uses: dtolnay/rust-toolchain@master 65 | with: 66 | toolchain: ${{ matrix.rust }} 67 | components: rustfmt, clippy 68 | 69 | - name: Setup headless display for tests on Linux 70 | if: runner.os == 'Linux' # This step is only needed on Linux. The other OSs don't need to be set up 71 | uses: ./.github/actions/headless_display 72 | 73 | - name: Run the ignored unit tests 74 | run: cargo test unit --no-default-features --features ${{ matrix.features }} -- --ignored --test-threads=1 --nocapture 75 | 76 | - name: Run the ignored unit tests in release mode 77 | run: cargo test unit --release --no-default-features --features ${{ matrix.features }} -- --ignored --test-threads=1 --nocapture 78 | 79 | - name: Run ignored integration tests 80 | run: cargo test integration --no-default-features --features ${{ matrix.features }} -- --ignored --test-threads=1 --nocapture 81 | 82 | - name: Run ignored integration tests in release mode 83 | run: cargo test integration --release --no-default-features --features ${{ matrix.features }} -- --ignored --test-threads=1 --nocapture 84 | 85 | - name: Take screenshot 86 | if: always() && runner.os != 'macOS' 87 | uses: ./.github/actions/screenshot 88 | with: 89 | platform: ${{ matrix.platform }} 90 | rust: ${{ matrix.rust }} 91 | feature: ${{ matrix.features }} -------------------------------------------------------------------------------- /libs/enigo/.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Integration 2 | 3 | permissions: 4 | contents: read 5 | on: 6 | workflow_dispatch: 7 | workflow_run: 8 | workflows: ["Build"] 9 | types: 10 | - completed 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | RUST_BACKTRACE: 1 15 | 16 | jobs: 17 | integration: 18 | if: ${{ github.event.workflow_run.conclusion == 'success' }} # Only run if the build succeeded 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | rust: 23 | - stable 24 | - nightly 25 | - "1.80.0" 26 | platform: 27 | - ubuntu-latest 28 | - windows-latest 29 | - macos-latest 30 | features: 31 | # - "libei,wayland,xdo,x11rb" 32 | - "default" 33 | # - "libei" 34 | # - "wayland" 35 | - "xdo" 36 | - "x11rb" 37 | exclude: 38 | # - platform: windows-latest 39 | # features: "libei,wayland,xdo,x11rb" 40 | # - platform: windows-latest 41 | # features: "libei" 42 | # - platform: windows-latest 43 | # features: "wayland" 44 | - platform: windows-latest 45 | features: "xdo" 46 | - platform: windows-latest 47 | features: "x11rb" 48 | # - platform: macos-latest 49 | # features: "libei,wayland,xdo,x11rb" 50 | # - platform: macos-latest 51 | # features: "libei" 52 | # - platform: macos-latest 53 | # features: "wayland" 54 | - platform: macos-latest 55 | features: "xdo" 56 | - platform: macos-latest 57 | features: "x11rb" 58 | runs-on: ${{ matrix.platform }} 59 | steps: 60 | - uses: actions/checkout@v4 61 | - uses: ./.github/actions/install_deps 62 | - uses: dtolnay/rust-toolchain@master 63 | with: 64 | toolchain: ${{ matrix.rust }} 65 | components: rustfmt, clippy 66 | 67 | - name: Setup headless display for tests on Linux 68 | if: runner.os == 'Linux' # This step is only needed on Linux. The other OSs don't need to be set up 69 | uses: ./.github/actions/headless_display 70 | 71 | - name: Run integration tests in release mode 72 | if: matrix.features != 'wayland' && matrix.features != 'libei' && matrix.features != 'libei,wayland,xdo,x11rb' # On Linux, the integration tests only work with X11 for now 73 | run: cargo test integration --release --no-default-features --features ${{ matrix.features }} -- --test-threads=1 --nocapture --include-ignored 74 | 75 | - name: Take screenshot 76 | if: always() && runner.os != 'macOS' 77 | uses: ./.github/actions/screenshot 78 | with: 79 | platform: ${{ matrix.platform }} 80 | rust: ${{ matrix.rust }} 81 | feature: ${{ matrix.features }} 82 | -------------------------------------------------------------------------------- /libs/enigo/.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | permissions: 4 | contents: read 5 | on: 6 | workflow_dispatch: 7 | workflow_run: 8 | workflows: ["Build"] 9 | types: 10 | - completed 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | RUST_BACKTRACE: 1 15 | 16 | jobs: 17 | test: 18 | if: ${{ github.event.workflow_run.conclusion == 'success' }} # Only run if the build succeeded 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | rust: 23 | - stable 24 | - nightly 25 | - "1.80.0" 26 | platform: 27 | - ubuntu-latest 28 | - windows-latest 29 | - macos-latest 30 | features: 31 | # The tests will fail on Ubuntu for libei and wayland, because the compositor of the Github runner does not support it 32 | #- "libei,wayland,xdo,x11rb" 33 | - "default" 34 | #- "libei" 35 | #- "wayland" 36 | - "xdo" 37 | - "x11rb" 38 | exclude: 39 | # The implementation on Windows and macOS does not have any features so we can reduce the number of combinations 40 | #- platform: windows-latest 41 | # features: "libei,wayland,xdo,x11rb" 42 | #- platform: windows-latest 43 | # features: "libei" 44 | #- platform: windows-latest 45 | # features: "wayland" 46 | - platform: windows-latest 47 | features: "xdo" 48 | - platform: windows-latest 49 | features: "x11rb" 50 | #- platform: macos-latest 51 | # features: "libei,wayland,xdo,x11rb" 52 | #- platform: macos-latest 53 | # features: "libei" 54 | #- platform: macos-latest 55 | # features: "wayland" 56 | - platform: macos-latest 57 | features: "xdo" 58 | - platform: macos-latest 59 | features: "x11rb" 60 | runs-on: ${{ matrix.platform }} 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: ./.github/actions/install_deps 64 | - uses: dtolnay/rust-toolchain@master 65 | with: 66 | toolchain: ${{ matrix.rust }} 67 | components: rustfmt, clippy 68 | 69 | - name: Setup headless display for tests on Linux 70 | if: runner.os == 'Linux' # This step is only needed on Linux. The other OSs don't need to be set up 71 | uses: ./.github/actions/headless_display 72 | 73 | - name: Run the unit tests 74 | run: cargo test unit --no-default-features --features ${{ matrix.features }} -- --test-threads=1 --nocapture 75 | 76 | - name: Run the unit tests in release mode 77 | run: cargo test unit --release --no-default-features --features ${{ matrix.features }} -- --test-threads=1 --nocapture 78 | 79 | - name: Take screenshot 80 | if: always() && runner.os != 'macOS' 81 | uses: ./.github/actions/screenshot 82 | with: 83 | platform: ${{ matrix.platform }} 84 | rust: ${{ matrix.rust }} 85 | feature: ${{ matrix.features }} 86 | -------------------------------------------------------------------------------- /libs/enigo/.gitignore: -------------------------------------------------------------------------------- 1 | # macOS Metadata 2 | .DS_Store 3 | 4 | # Generated by Cargo 5 | # will have compiled files and executables 6 | target/ 7 | 8 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 9 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 10 | Cargo.lock 11 | 12 | # RustFmt files 13 | **/*.rs.bk 14 | 15 | # IntelliJ 16 | .idea/ 17 | 18 | # VSCode 19 | .vscode/ 20 | -------------------------------------------------------------------------------- /libs/enigo/CHANGES.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | ## Changed 3 | - all: The keys `Print` and `Snapshot` were deprecated because `Print` had the wrong virtual key associated with it on Windows. Use `Key::PrintScr` instead 4 | 5 | ## Added 6 | - all: `Key::PrintScr` 7 | - all: There finally are some tests in the CI to increase the development speed and prevent regressions 8 | - win, macOS: Allow marking events that were created by enigo. Have a look at the additional field of the `Settings` struct and the new method `get_marker_value` of the `enigo` struct (only available on Windows and macOS) 9 | 10 | ## Fixed 11 | win: Respect the language of the current window to determine the which scancodes to send 12 | win: Send the virtual key and its scan code in the events to work with programs that only look at one of the two 13 | 14 | # 0.2.1 15 | ## Changed 16 | - all: Use serde(default) to make the serialized strings less verbose 17 | 18 | ## Added 19 | - all: Serialized tokens can be less verbose because serde aliases were added 20 | - win, macOS: Allow marking events that were created by enigo. Have a look at the additional field of the `Settings` struct and the new method `get_marker_value` of the `enigo` struct (only available on Windows and macOS) 21 | - all: The enums `Button`, `Direction`, `Axis` and `Coordinate` implement `Default` 22 | 23 | ## Fixed 24 | - windows: The `move_mouse` function moves the mouse to the correct absolute coordinates again 25 | 26 | # 0.2.0 27 | 28 | ## Changed 29 | - All: A new Enigo struct is now always created with some settings 30 | - Rust: MSRV is 1.75 31 | - All held keys are released when Enigo is dropped 32 | - win: Don't panic if it was not possible to move the mouse 33 | - All: Never panic! All functions return Results now 34 | - win: Don't move the mouse to a relative position if it was not possible to get the current position 35 | - All: The feature `with_serde` was renamed to `serde` 36 | - All: Renamed `Key::Layout(char)` to `Key::Unicode(char)` and clarified its docs 37 | - All: Split off entering raw keycodes into it's own function 38 | - All: Renamed `key_sequence` function to `text` 39 | - All: Renamed `enter_key` function to `key` 40 | - All: Renamed `send_mouse_button_event` function to `button` 41 | - All: Renamed `send_motion_notify_event` function to `move_mouse` 42 | - All: Renamed `mouse_scroll_event` function to `scroll` 43 | - All: Renamed `mouse_location` function to `location` 44 | - All: Renamed `MouseButton` enum to `Button` 45 | - DSL: The DSL was removed and replaced with the `Agent` trait. Activate the `serde` feature to use it. Have a look at the `serde` example to get an idea how to use it 46 | 47 | ## Added 48 | - Linux: Partial support for `libei` was added. Use the experimental feature `libei` to test it. This works on GNOME 46 and above. Entering text often simulates the wrong characters. 49 | - Linux: Support X11 without `xdotools`. Use the experimental feature `x11rb` to test it 50 | - Linux: Partial support for Wayland was added. Use the experimental feature `wayland` to test it. Only the virtual_keyboard and input_method protocol can be used. This is not going to work on GNOME, but should work for example with phosh 51 | - Linux: Added `MicMute` key to enter `XF86_AudioMicMute` keysym 52 | - win: Use DirectInput in addition to the SetCursorPos function in order to support DirectX 53 | - All: You can now chose how long the delay between keypresses should be on each platform and change it during the runtime 54 | - All: You can now use a logger to investigate errors 55 | 56 | ## Fixed 57 | - *BSD: Fix the build for BSDs 58 | - macOS: Add info how much a mouse was moved relative to the last position 59 | - macOS: A mouse drag with the right key is now possible too 60 | - win, linux: `key_sequence()` and `key_click(Key::Layout())` can properly enter new lines and tabs 61 | - linux: You can enter `Key::ScrollLock` now 62 | - win: No more sleeps! Simulating input is done in 1 ms instead of 40+ ms. This is most obvious when entering long strings 63 | - macOS: Added keys to control media, brightness, contrast, illumination and more 64 | - macOS: Fix entering text that starts with newline characters 65 | 66 | # 0.1.3 67 | 68 | ## Changed 69 | 70 | ## Added 71 | - Linux: Add Media and Volume keys 72 | 73 | ## Fixed 74 | - Linux: Fixed a Segfault when running in release mode 75 | 76 | # 0.1.2 77 | 78 | ## Changed 79 | - Windows: Bumped `windows` dependency to `0.48` because `0.47` was yanked. 80 | 81 | # 0.1.1 82 | 83 | ## Changed 84 | - Windows: `Key::Control` presses `Control` and no longer the left `Control`. 85 | 86 | ## Added 87 | - all: Added a ton of keys (e.g F21-F24 keys and the XBUTTON1 & XBUTTON2 mouse buttons are now available on Windows). Some of them are OS specific. Use conditional compilation (e.g `#[cfg(target_os = "windows")]`) to use them to not break the build on other OSs. 88 | - examples: New example `platform_specific.rs` to demonstrate how to use keys/buttons that are platform specific 89 | 90 | ## Fixed 91 | - macOS: Fixed entering Key::Layout 92 | 93 | # 0.1.0 94 | We should have bumped the minor version with the last release. Sorry about that. Have a look at the changes of 0.0.15 if you come from an earlier version. 95 | 96 | # 0.0.15 97 | 98 | ## Changed 99 | - Windows: `mouse_scroll_y` with a positive number scrolls down just like on the other platforms 100 | - Windows: replaced `winapi` with the official `windows` crate 101 | - Rust: Using Rust version 2021 102 | - Rust: Minimum supported Rust version (MSRV) is set in Cargo.toml 103 | - Rust: MSRV is 1.64 104 | - macOS, Windows: Moved the functions `main_display_size` and `mouse_location` from `Enigo` to `MouseControllable` 105 | 106 | ## Added 107 | - DSL: Additional ParseError variants to give better feedback what the problem was 108 | - DSL: Additional keys 109 | - All: Added support for F10-F20 110 | - CI/CD: Github Workflows to make sure the code builds and the tests pass 111 | - Traits: Added the functions `main_display_size` and `mouse_location` to `MouseControllable` 112 | - Linux: Implemented the functions `main_display_size` and `mouse_location` for `MouseControllable` 113 | 114 | ## Fixed 115 | - Windows: panicked at `cannot transmute_copy if U is larger than T` (https://github.com/enigo-rs/enigo/issues/121) 116 | - Windows: Inconsistent behavior between the `mouse_move_relative` and `mouse_move_to` functions (https://github.com/enigo-rs/enigo/issues/91) 117 | - Windows, macOS: Stop panicking when `mouse_down` or `mouse_up` is called with either of `MouseButton::ScrollUp`, `MouseButton::ScrollDown`, `MouseButton::ScrollLeft`, `MouseButton::ScrollRight` and instead scroll 118 | - Windows: Always use key codes to be layout independent. Only use scan codes for `Key::Layout` (Fixes https://github.com/enigo-rs/enigo/issues/99, https://github.com/enigo-rs/enigo/issues/84) 119 | - macOS: `key_click` no longer triggers a segmentation fault when called with `Key::Layout` argument (Fixes https://github.com/enigo-rs/enigo/issues/124) 120 | - macOS: Double clicks now work (https://github.com/enigo-rs/enigo/issues/82) 121 | -------------------------------------------------------------------------------- /libs/enigo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "enigo" 3 | version = "0.2.1" 4 | authors = [ 5 | "pentamassiv ", 6 | "Dustin Bensing ", 7 | ] 8 | edition = "2021" 9 | rust-version = "1.75" # The CI already runs on 1.80 because the tests needed it 10 | description = "Cross-platform (Linux, Windows, macOS & BSD) library to simulate keyboard and mouse events" 11 | documentation = "https://docs.rs/enigo/" 12 | homepage = "https://github.com/enigo-rs/enigo" 13 | repository = "https://github.com/enigo-rs/enigo" 14 | readme = "README.md" 15 | keywords = ["simulate", "input", "mouse", "keyboard", "automation"] 16 | categories = [ 17 | "development-tools::testing", 18 | "api-bindings", 19 | "hardware-support", 20 | "os", 21 | "simulation", 22 | ] 23 | license = "MIT" 24 | exclude = [".github", "examples", ".gitignore", "rustfmt.toml"] 25 | 26 | [package.metadata.docs.rs] 27 | all-features = true 28 | 29 | [features] 30 | default = ["xdo"] 31 | libei = ["dep:reis", "dep:ashpd", "dep:pollster", "dep:once_cell"] 32 | serde = ["dep:serde"] 33 | wayland = [ 34 | "dep:wayland-client", 35 | "dep:wayland-protocols-misc", 36 | "dep:wayland-protocols-wlr", 37 | "dep:wayland-protocols-plasma", 38 | "dep:tempfile", 39 | ] 40 | xdo = [] 41 | x11rb = ["dep:x11rb"] 42 | 43 | [dependencies] 44 | log = "0.4" 45 | serde = { version = "1", features = ["derive"], optional = true } 46 | 47 | [target.'cfg(target_os = "windows")'.dependencies] 48 | windows = { version = "0.58", features = [ 49 | "Win32_Foundation", 50 | "Win32_UI_TextServices", 51 | "Win32_UI_WindowsAndMessaging", 52 | "Win32_UI_Input_KeyboardAndMouse", 53 | ] } 54 | 55 | [target.'cfg(target_os = "macos")'.dependencies] 56 | core-graphics = { version = "0.24", features = ["highsierra"] } 57 | icrate = { version = "0.1", features = [ 58 | "AppKit_all", 59 | ] } # AppKit_NSGraphicsContext 60 | objc2 = { version = "0.5", features = ["relax-void-encoding"] } 61 | foreign-types-shared = "0.3" 62 | 63 | [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] 64 | libc = "0.2" 65 | reis = { version = "0.2.0", optional = true } 66 | ashpd = { version = "0.9.1", optional = true } 67 | pollster = { version = "0.3.0", optional = true } 68 | once_cell = { version = "1.19.0", optional = true } 69 | wayland-protocols-misc = { version = "0.3", features = [ 70 | "client", 71 | ], optional = true } 72 | wayland-protocols-wlr = { version = "0.3", features = [ 73 | "client", 74 | ], optional = true } 75 | wayland-protocols-plasma = { version = "0.3", features = [ 76 | "client", 77 | ], optional = true } 78 | wayland-client = { version = "0.31", optional = true } 79 | x11rb = { version = "0.13", features = [ 80 | "randr", 81 | "xinput", 82 | "xtest", 83 | ], optional = true } 84 | xkbcommon = "0.8" 85 | xkeysym = "0.2" 86 | tempfile = { version = "3", optional = true } 87 | 88 | [dev-dependencies] 89 | env_logger = "0.11" 90 | serde = { version = "1", features = ["derive"] } 91 | tungstenite = "0.23" 92 | url = "2" 93 | webbrowser = "1.0" 94 | ron = "0.8" 95 | strum = "0.26" 96 | strum_macros = "0.26" 97 | rdev = "0.5" # Test the main_display() function 98 | mouse_position = "0.1.4" # Test the location() function 99 | 100 | [[example]] 101 | name = "serde" 102 | path = "examples/serde.rs" 103 | required-features = ["serde"] 104 | -------------------------------------------------------------------------------- /libs/enigo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 pythoneer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/enigo/Permissions.md: -------------------------------------------------------------------------------- 1 | # Permissions 2 | 3 | ## Linux 4 | No elevated privileges are needed 5 | 6 | ## Windows 7 | [UIPI](https://en.wikipedia.org/wiki/User_Interface_Privilege_Isolation) is a security measure that "prevents processes with a lower "integrity level" (IL) from sending messages to higher IL processes". If your program does not have elevated privileges, you won't be able to use `enigo` is some situations. It won't be possible to use it with the task manager for example. Run your program as an admin, if you need to use `enigo` with processes with a higher "integrity level". 8 | 9 | ## macOS 10 | You need to grant the application to access your Mac. You can find official instructions [here](https://web.archive.org/web/20231005204542/https://support.apple.com/guide/mac-help/allow-accessibility-apps-to-access-your-mac-mh43185/mac). -------------------------------------------------------------------------------- /libs/enigo/README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://img.shields.io/github/actions/workflow/status/enigo-rs/enigo/CI.yml?branch=main)](https://github.com/enigo-rs/enigo/actions/workflows/CI.yml) 2 | [![Docs](https://docs.rs/enigo/badge.svg)](https://docs.rs/enigo) 3 | [![Dependency status](https://deps.rs/repo/github/enigo-rs/enigo/status.svg)](https://deps.rs/repo/github/enigo-rs/enigo) 4 | 5 | ![Rust version](https://img.shields.io/badge/rust--version-1.75+-brightgreen.svg) 6 | [![Crates.io](https://img.shields.io/crates/v/enigo.svg)](https://crates.io/crates/enigo) 7 | 8 | # enigo 9 | 10 | Cross platform input simulation in Rust! 11 | 12 | - [x] Serialize/Deserialize 13 | - [x] Linux (X11) mouse 14 | - [x] Linux (X11) text 15 | - [x] Linux (Wayland) mouse 16 | - [x] Linux (Wayland) text 17 | - [x] Linux (libei) mouse 18 | - [x] Linux (libei) text 19 | - [x] MacOS mouse 20 | - [x] MacOS text 21 | - [x] Windows mouse 22 | - [x] Windows text 23 | 24 | Enigo also works on *BSDs if they use X11 or Wayland. I don't have a machine to test it and there are no Github Action runners for it, so the BSD support is not explicitly listed. 25 | 26 | ```Rust 27 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 28 | 29 | enigo.move_mouse(500, 200, Abs).unwrap(); 30 | enigo.button(Button::Left, Click).unwrap(); 31 | enigo.text("Hello World! here is a lot of text ❤️").unwrap(); 32 | ``` 33 | 34 | For more, look at the ([examples](examples)). 35 | 36 | ## Features 37 | 38 | By default, enigo currently works on Windows, macOS and Linux (X11). If you want to be able to serialize and deserialize commands for enigo ([example](examples/serde.rs)), you need to activate the `serde` feature. 39 | 40 | There are multiple ways how to simulate input on Linux and not all systems support everything. Enigo can also use wayland protocols and libei to simulate input but there are currently some bugs with it. That is why they are hidden behind feature flags. 41 | 42 | If you do not want your users to have to install any runtime dependencies on Linux when using X11, you can try the experimental `x11rb` feature. 43 | 44 | 45 | ## Runtime dependencies 46 | 47 | Linux users may have to install `libxdo-dev` if they are using `X11`. For example, on Debian-based distros: 48 | 49 | ```Bash 50 | apt install libxdo-dev 51 | ``` 52 | 53 | On Arch: 54 | 55 | ```Bash 56 | pacman -S xdotool 57 | ``` 58 | 59 | On Fedora: 60 | 61 | ```Bash 62 | dnf install libX11-devel libxdo-devel 63 | ``` 64 | 65 | On Gentoo: 66 | 67 | ```Bash 68 | emerge -a xdotool 69 | ``` 70 | 71 | ## Migrating from a previous version 72 | 73 | Please have a look at our [changelog](CHANGES.md) to find out what you have to do, if you used a previous version. 74 | 75 | ## Permissions 76 | 77 | Some platforms have security measures in place to prevent programs from entering keys or controlling the mouse. Have a look at the [permissions](Permissions.md) documentation to see what you need to do to allow it. -------------------------------------------------------------------------------- /libs/enigo/examples/key.rs: -------------------------------------------------------------------------------- 1 | use enigo::{ 2 | Direction::{Press, Release}, 3 | Enigo, Key, Keyboard, Settings, 4 | }; 5 | use std::thread; 6 | use std::time::Duration; 7 | 8 | fn main() { 9 | env_logger::init(); 10 | thread::sleep(Duration::from_secs(2)); 11 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 12 | 13 | enigo.key(Key::Unicode('a'), Press).unwrap(); 14 | thread::sleep(Duration::from_secs(1)); 15 | enigo.key(Key::Unicode('a'), Release).unwrap(); 16 | } 17 | -------------------------------------------------------------------------------- /libs/enigo/examples/keyboard.rs: -------------------------------------------------------------------------------- 1 | use enigo::{ 2 | Direction::{Click, Press, Release}, 3 | Enigo, Key, Keyboard, Settings, 4 | }; 5 | use std::thread; 6 | use std::time::Duration; 7 | 8 | fn main() { 9 | env_logger::init(); 10 | thread::sleep(Duration::from_secs(2)); 11 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 12 | 13 | // write text 14 | enigo 15 | .text("Hello World! here is a lot of text ❤️") 16 | .unwrap(); 17 | 18 | // select all 19 | enigo.key(Key::Control, Press).unwrap(); 20 | enigo.key(Key::Unicode('a'), Click).unwrap(); 21 | enigo.key(Key::Control, Release).unwrap(); 22 | } 23 | -------------------------------------------------------------------------------- /libs/enigo/examples/layout.rs: -------------------------------------------------------------------------------- 1 | use enigo::{Direction::Click, Enigo, Key, Keyboard, Settings}; 2 | use std::thread; 3 | use std::time::Duration; 4 | 5 | fn main() { 6 | env_logger::init(); 7 | thread::sleep(Duration::from_secs(4)); 8 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 9 | 10 | enigo.key(Key::PageDown, Click).unwrap(); 11 | enigo.key(enigo::Key::UpArrow, Click).unwrap(); 12 | enigo.key(enigo::Key::UpArrow, Click).unwrap(); 13 | enigo.key(enigo::Key::DownArrow, Click).unwrap(); 14 | enigo.key(enigo::Key::LeftArrow, Click).unwrap(); 15 | enigo.key(enigo::Key::LeftArrow, Click).unwrap(); 16 | enigo.key(enigo::Key::RightArrow, Click).unwrap(); 17 | enigo.text("𝕊").unwrap(); // Special char which needs two u16s to be encoded 18 | } 19 | -------------------------------------------------------------------------------- /libs/enigo/examples/mouse.rs: -------------------------------------------------------------------------------- 1 | use enigo::{ 2 | Button, 3 | Direction::{Click, Press, Release}, 4 | Enigo, Mouse, Settings, 5 | {Axis::Horizontal, Axis::Vertical}, 6 | {Coordinate::Abs, Coordinate::Rel}, 7 | }; 8 | use std::thread; 9 | use std::time::Duration; 10 | 11 | fn main() { 12 | env_logger::init(); 13 | let wait_time = Duration::from_secs(2); 14 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 15 | 16 | thread::sleep(Duration::from_secs(4)); 17 | println!("screen dimensions: {:?}", enigo.main_display().unwrap()); 18 | println!("mouse location: {:?}", enigo.location().unwrap()); 19 | 20 | thread::sleep(wait_time); 21 | 22 | enigo.move_mouse(500, 200, Abs).unwrap(); 23 | thread::sleep(wait_time); 24 | 25 | enigo.button(Button::Left, Press).unwrap(); 26 | thread::sleep(wait_time); 27 | 28 | enigo.move_mouse(100, 100, Rel).unwrap(); 29 | thread::sleep(wait_time); 30 | 31 | enigo.button(Button::Left, Release).unwrap(); 32 | thread::sleep(wait_time); 33 | 34 | enigo.button(Button::Left, Click).unwrap(); 35 | thread::sleep(wait_time); 36 | 37 | enigo.scroll(2, Horizontal).unwrap(); 38 | thread::sleep(wait_time); 39 | 40 | enigo.scroll(-2, Horizontal).unwrap(); 41 | thread::sleep(wait_time); 42 | 43 | enigo.scroll(2, Vertical).unwrap(); 44 | thread::sleep(wait_time); 45 | 46 | enigo.scroll(-2, Vertical).unwrap(); 47 | thread::sleep(wait_time); 48 | 49 | println!("mouse location: {:?}", enigo.location().unwrap()); 50 | } 51 | -------------------------------------------------------------------------------- /libs/enigo/examples/platform_specific.rs: -------------------------------------------------------------------------------- 1 | use enigo::{Direction::Click, Enigo, Key, Keyboard, Settings}; 2 | use std::thread; 3 | use std::time::Duration; 4 | 5 | // This example will do different things depending on the platform 6 | fn main() { 7 | env_logger::init(); 8 | thread::sleep(Duration::from_secs(2)); 9 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 10 | 11 | #[cfg(target_os = "macos")] 12 | enigo.key(Key::Launchpad, Click).unwrap(); // macOS: Open launchpad 13 | 14 | #[cfg(all(unix, not(target_os = "macos")))] 15 | enigo.key(Key::Meta, Click).unwrap(); // linux: Open launcher 16 | 17 | #[cfg(target_os = "windows")] 18 | { 19 | // windows: Enter divide symbol (slash) 20 | enigo.key(Key::Divide, Click).unwrap(); 21 | 22 | // windows: Press and release the NumLock key. Without the EXT bit set, it would 23 | // enter the Pause key 24 | enigo.raw(45 | enigo::EXT, enigo::Direction::Click).unwrap(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /libs/enigo/examples/serde.rs: -------------------------------------------------------------------------------- 1 | use enigo::{ 2 | agent::{Agent, Token}, 3 | Button, Enigo, Key, Settings, 4 | }; 5 | use std::{thread, time::Duration}; 6 | 7 | fn main() { 8 | env_logger::init(); 9 | thread::sleep(Duration::from_secs(2)); 10 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 11 | 12 | // write text, move the mouse (10/10) relative from the cursors position, scroll 13 | // down, enter the unicode U+1F525 (🔥) and then select all 14 | let tokens = vec![ 15 | Token::Text("Hello World! ❤️".to_string()), 16 | Token::MoveMouse(10, 10, enigo::Coordinate::Rel), 17 | Token::Scroll(5, enigo::Axis::Vertical), 18 | Token::Button(Button::Left, enigo::Direction::Click), 19 | Token::Key(Key::Unicode('🔥'), enigo::Direction::Click), 20 | Token::Key(Key::Control, enigo::Direction::Press), 21 | Token::Key(Key::Unicode('a'), enigo::Direction::Click), 22 | Token::Key(Key::Control, enigo::Direction::Release), 23 | ]; 24 | 25 | // There are serde aliases so you could also deserialize the same tokens from 26 | // the following string let serialized=r#"[t("Hello World! 27 | // ❤\u{fe0f}"),m(10,10,r),s(5),b(l),k(uni('🔥')),k(ctrl,p),k(uni('a')), 28 | // k(ctrl,r)]"#.to_string(); 29 | let serialized = ron::to_string(&tokens).unwrap(); 30 | println!("serialized = {serialized}"); 31 | 32 | let deserialized_tokens: Vec<_> = ron::from_str(&serialized).unwrap(); 33 | for token in &deserialized_tokens { 34 | enigo.execute(token).unwrap(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/enigo/examples/timer.rs: -------------------------------------------------------------------------------- 1 | use enigo::{ 2 | Direction::{Click, Press, Release}, 3 | Enigo, Key, Keyboard, Settings, 4 | }; 5 | use std::{ 6 | thread, 7 | time::{Duration, Instant}, 8 | }; 9 | 10 | fn main() { 11 | env_logger::init(); 12 | thread::sleep(Duration::from_secs(2)); 13 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 14 | 15 | let now = Instant::now(); 16 | 17 | // write text 18 | enigo.text("Hello World! ❤️").unwrap(); 19 | 20 | let time = now.elapsed(); 21 | println!("{time:?}"); 22 | 23 | // select all 24 | let control_or_command = if cfg!(target_os = "macos") { 25 | Key::Meta 26 | } else { 27 | Key::Control 28 | }; 29 | enigo.key(control_or_command, Press).unwrap(); 30 | enigo.key(Key::Unicode('a'), Click).unwrap(); 31 | enigo.key(control_or_command, Release).unwrap(); 32 | } 33 | -------------------------------------------------------------------------------- /libs/enigo/rustfmt.toml: -------------------------------------------------------------------------------- 1 | wrap_comments = true 2 | -------------------------------------------------------------------------------- /libs/enigo/src/agent.rs: -------------------------------------------------------------------------------- 1 | use crate::{Axis, Button, Coordinate, Direction, Enigo, InputResult, Key, Keyboard, Mouse}; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 7 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 8 | pub enum Token { 9 | /// Call the [`Keyboard::text`] fn with the string as text 10 | #[cfg_attr(feature = "serde", serde(alias = "T"))] 11 | #[cfg_attr(feature = "serde", serde(alias = "t"))] 12 | Text(String), 13 | /// Call the [`Keyboard::key`] fn with the given key and direction 14 | #[cfg_attr(feature = "serde", serde(alias = "K"))] 15 | #[cfg_attr(feature = "serde", serde(alias = "k"))] 16 | Key( 17 | Key, 18 | #[cfg_attr(feature = "serde", serde(default))] Direction, 19 | ), 20 | /// Call the [`Keyboard::raw`] fn with the given keycode and direction 21 | #[cfg_attr(feature = "serde", serde(alias = "R"))] 22 | #[cfg_attr(feature = "serde", serde(alias = "r"))] 23 | Raw( 24 | u16, 25 | #[cfg_attr(feature = "serde", serde(default))] Direction, 26 | ), 27 | /// Call the [`Mouse::button`] fn with the given mouse button and direction 28 | #[cfg_attr(feature = "serde", serde(alias = "B"))] 29 | #[cfg_attr(feature = "serde", serde(alias = "b"))] 30 | Button( 31 | Button, 32 | #[cfg_attr(feature = "serde", serde(default))] Direction, 33 | ), 34 | /// Call the [`Mouse::move_mouse`] fn. The first i32 is the value to move on 35 | /// the x-axis and the second i32 is the value to move on the y-axis. The 36 | /// coordinate defines if the given coordinates are absolute of relative to 37 | /// the current position of the mouse. 38 | #[cfg_attr(feature = "serde", serde(alias = "M"))] 39 | #[cfg_attr(feature = "serde", serde(alias = "m"))] 40 | MoveMouse( 41 | i32, 42 | i32, 43 | #[cfg_attr(feature = "serde", serde(default))] Coordinate, 44 | ), 45 | /// Call the [`Mouse::scroll`] fn. 46 | #[cfg_attr(feature = "serde", serde(alias = "S"))] 47 | #[cfg_attr(feature = "serde", serde(alias = "s"))] 48 | Scroll(i32, #[cfg_attr(feature = "serde", serde(default))] Axis), 49 | } 50 | 51 | pub trait Agent 52 | where 53 | Self: Keyboard, 54 | Self: Mouse, 55 | { 56 | /// Execute the action associated with the token. A [`Token::Text`] will 57 | /// enter text, a [`Token::Scroll`] will scroll and so forth. Have a look at 58 | /// the documentation of the [`Token`] enum for more information. 59 | /// 60 | /// # Errors 61 | /// 62 | /// Same as the individual functions. Have a look at [`InputResult`] for a 63 | /// list of possible errors 64 | fn execute(&mut self, token: &Token) -> InputResult<()> { 65 | match token { 66 | Token::Text(text) => self.text(text), 67 | Token::Key(key, direction) => self.key(*key, *direction), 68 | Token::Raw(keycode, direction) => self.raw(*keycode, *direction), 69 | Token::Button(button, direction) => self.button(*button, *direction), 70 | Token::MoveMouse(x, y, coordinate) => self.move_mouse(*x, *y, *coordinate), 71 | Token::Scroll(length, axis) => self.scroll(*length, *axis), 72 | } 73 | } 74 | } 75 | 76 | impl Agent for Enigo {} 77 | -------------------------------------------------------------------------------- /libs/enigo/src/linux/constants.rs: -------------------------------------------------------------------------------- 1 | pub const KEYMAP_BEGINNING: &[u8; 7319] = b"xkb_keymap { 2 | xkb_keycodes { 3 | minimum = 8; 4 | maximum = 255; 5 | 6 | = 8; 7 | = 9; 8 | = 10; 9 | = 11; 10 | = 12; 11 | = 13; 12 | = 14; 13 | = 15; 14 | = 16; 15 | = 17; 16 | = 18; 17 | = 19; 18 | = 20; 19 | = 21; 20 | = 22; 21 | = 23; 22 | = 24; 23 | = 25; 24 | = 26; 25 | = 27; 26 | = 28; 27 | = 29; 28 | = 30; 29 | = 31; 30 | = 32; 31 | = 33; 32 | = 34; 33 | = 35; 34 | = 36; 35 | = 37; 36 | = 38; 37 | = 39; 38 | = 40; 39 | = 41; 40 | = 42; 41 | = 43; 42 | = 44; 43 | = 45; 44 | = 46; 45 | = 47; 46 | = 48; 47 | = 49; 48 | = 50; 49 | = 51; 50 | = 52; 51 | = 53; 52 | = 54; 53 | = 55; 54 | = 56; 55 | = 57; 56 | = 58; 57 | = 59; 58 | = 60; 59 | = 61; 60 | = 62; 61 | = 63; 62 | = 64; 63 | = 65; 64 | = 66; 65 | = 67; 66 | = 68; 67 | = 69; 68 | = 70; 69 | = 71; 70 | = 72; 71 | = 73; 72 | = 74; 73 | = 75; 74 | = 76; 75 | = 77; 76 | = 78; 77 | = 79; 78 | = 80; 79 | = 81; 80 | = 82; 81 | = 83; 82 | = 84; 83 | = 85; 84 | = 86; 85 | = 87; 86 | = 88; 87 | = 89; 88 | = 90; 89 | = 91; 90 | = 92; 91 | = 93; 92 | = 94; 93 | = 95; 94 | = 96; 95 | = 97; 96 | = 98; 97 | = 99; 98 | = 100; 99 | = 101; 100 | = 102; 101 | = 103; 102 | = 104; 103 | = 105; 104 | = 106; 105 | = 107; 106 | = 108; 107 | = 109; 108 | = 110; 109 | = 111; 110 | = 112; 111 | = 113; 112 | = 114; 113 | = 115; 114 | = 116; 115 | = 117; 116 | = 118; 117 | = 119; 118 | = 120; 119 | = 121; 120 | = 122; 121 | = 123; 122 | = 124; 123 | = 125; 124 | = 126; 125 | = 127; 126 | = 128; 127 | = 129; 128 | = 130; 129 | = 131; 130 | = 132; 131 | = 133; 132 | = 134; 133 | = 135; 134 | = 136; 135 | = 137; 136 | = 138; 137 | = 139; 138 | = 140; 139 | = 141; 140 | = 142; 141 | = 143; 142 | = 144; 143 | = 145; 144 | = 146; 145 | = 147; 146 | = 148; 147 | = 149; 148 | = 150; 149 | = 151; 150 | = 152; 151 | = 153; 152 | = 154; 153 | = 155; 154 | = 156; 155 | = 157; 156 | = 158; 157 | = 159; 158 | = 160; 159 | = 161; 160 | = 162; 161 | = 163; 162 | = 164; 163 | = 165; 164 | = 166; 165 | = 167; 166 | = 168; 167 | = 169; 168 | = 170; 169 | = 171; 170 | = 172; 171 | = 173; 172 | = 174; 173 | = 175; 174 | = 176; 175 | = 177; 176 | = 178; 177 | = 179; 178 | = 180; 179 | = 181; 180 | = 182; 181 | = 183; 182 | = 184; 183 | = 185; 184 | = 186; 185 | = 187; 186 | = 188; 187 | = 189; 188 | = 190; 189 | = 191; 190 | = 192; 191 | = 193; 192 | = 194; 193 | = 195; 194 | = 196; 195 | = 197; 196 | = 198; 197 | = 199; 198 | = 200; 199 | = 201; 200 | = 202; 201 | = 203; 202 | = 204; 203 | = 205; 204 | = 206; 205 | = 207; 206 | = 208; 207 | = 209; 208 | = 210; 209 | = 211; 210 | = 212; 211 | = 213; 212 | = 214; 213 | = 215; 214 | = 216; 215 | = 217; 216 | = 218; 217 | = 219; 218 | = 220; 219 | = 221; 220 | = 222; 221 | = 223; 222 | = 224; 223 | = 225; 224 | = 226; 225 | = 227; 226 | = 228; 227 | = 229; 228 | = 230; 229 | = 231; 230 | = 232; 231 | = 233; 232 | = 234; 233 | = 235; 234 | = 236; 235 | = 237; 236 | = 238; 237 | = 239; 238 | = 240; 239 | = 241; 240 | = 242; 241 | = 243; 242 | = 244; 243 | = 245; 244 | = 246; 245 | = 247; 246 | = 248; 247 | = 249; 248 | = 250; 249 | = 251; 250 | = 252; 251 | = 253; 252 | = 254; 253 | = 255; 254 | 255 | indicator 1 = \"Caps Lock\"; // Needed for Xwayland 256 | }; 257 | xkb_types { 258 | // Do NOT change this part. It is required by Xorg/Xwayland. 259 | virtual_modifiers OSK; 260 | type \"ONE_LEVEL\" { 261 | modifiers= none; 262 | level_name[Level1]= \"Any\"; 263 | }; 264 | type \"TWO_LEVEL\" { 265 | level_name[Level1]= \"Base\"; 266 | }; 267 | type \"ALPHABETIC\" { 268 | level_name[Level1]= \"Base\"; 269 | }; 270 | type \"KEYPAD\" { 271 | level_name[Level1]= \"Base\"; 272 | }; 273 | type \"SHIFT+ALT\" { 274 | level_name[Level1]= \"Base\"; 275 | }; 276 | }; 277 | xkb_compatibility { 278 | // Do NOT change this part. It is required by Xorg/Xwayland. 279 | interpret Any+AnyOf(all) { 280 | action= SetMods(modifiers=modMapMods,clearLocks); 281 | }; 282 | }; 283 | xkb_symbols {"; 284 | 285 | pub const KEYMAP_END: &[u8; 27] = b" 286 | }; 287 | 288 | };"; 289 | -------------------------------------------------------------------------------- /libs/enigo/src/linux/keymap.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | use std::convert::TryInto; 3 | use std::fmt::Display; 4 | 5 | use log::{debug, trace}; 6 | pub(super) use xkeysym::{KeyCode, Keysym}; 7 | 8 | #[cfg(feature = "wayland")] 9 | use crate::keycodes::ModifierBitflag; 10 | use crate::{Direction, InputError, InputResult, Key}; 11 | 12 | /// The "empty" keyboard symbol. 13 | // TODO: Replace it with the NoSymbol from xkeysym, once a new version was 14 | // published 15 | pub const NO_SYMBOL: Keysym = Keysym::new(0); 16 | #[cfg(feature = "x11rb")] 17 | const DEFAULT_DELAY: u32 = 12; 18 | 19 | #[derive(Debug)] 20 | pub struct KeyMap { 21 | pub(super) additionally_mapped: HashMap, 22 | keycode_min: Keycode, 23 | keycode_max: Keycode, 24 | keysyms_per_keycode: u8, 25 | keysyms: Vec, 26 | 27 | unused_keycodes: VecDeque, 28 | held_keycodes: Vec, // cannot get unmapped 29 | needs_regeneration: bool, 30 | #[cfg(feature = "wayland")] 31 | pub(super) file: Option, // temporary file that contains the keymap 32 | #[cfg(feature = "wayland")] 33 | modifiers: ModifierBitflag, // state of the modifiers 34 | #[cfg(feature = "x11rb")] 35 | last_keys: Vec, // last pressed keycodes 36 | #[cfg(feature = "x11rb")] 37 | delay: u32, // milliseconds 38 | #[cfg(feature = "x11rb")] 39 | last_event_before_delays: std::time::Instant, // time of the last event 40 | #[cfg(feature = "x11rb")] 41 | pending_delays: u32, 42 | } 43 | 44 | // TODO: Check if the bounds can be simplified 45 | impl< 46 | Keycode: std::ops::Sub 47 | + PartialEq 48 | + Copy 49 | + Clone 50 | + Display 51 | + TryInto 52 | + std::convert::TryFrom, 53 | > KeyMap 54 | where 55 | >::Error: std::fmt::Debug, 56 | >::Error: std::fmt::Debug, 57 | { 58 | /// Create a new `KeyMap` 59 | pub fn new( 60 | keycode_min: Keycode, 61 | keycode_max: Keycode, 62 | unused_keycodes: VecDeque, 63 | keysyms_per_keycode: u8, 64 | keysyms: Vec, 65 | ) -> Self { 66 | let capacity: usize = keycode_max.try_into().unwrap() - keycode_min.try_into().unwrap(); 67 | let capacity = capacity + 1; 68 | let keymap = HashMap::with_capacity(capacity); 69 | let held_keycodes = vec![]; 70 | let needs_regeneration = true; 71 | #[cfg(feature = "wayland")] 72 | let file = None; 73 | #[cfg(feature = "wayland")] 74 | let modifiers = 0; 75 | #[cfg(feature = "x11rb")] 76 | let last_keys = vec![]; 77 | #[cfg(feature = "x11rb")] 78 | let delay = DEFAULT_DELAY; 79 | #[cfg(feature = "x11rb")] 80 | let last_event_before_delays = std::time::Instant::now(); 81 | #[cfg(feature = "x11rb")] 82 | let pending_delays = 0; 83 | Self { 84 | additionally_mapped: keymap, 85 | keycode_min, 86 | keycode_max, 87 | keysyms_per_keycode, 88 | keysyms, 89 | unused_keycodes, 90 | held_keycodes, 91 | needs_regeneration, 92 | #[cfg(feature = "wayland")] 93 | file, 94 | #[cfg(feature = "wayland")] 95 | modifiers, 96 | #[cfg(feature = "x11rb")] 97 | last_keys, 98 | #[cfg(feature = "x11rb")] 99 | delay, 100 | #[cfg(feature = "x11rb")] 101 | last_event_before_delays, 102 | #[cfg(feature = "x11rb")] 103 | pending_delays, 104 | } 105 | } 106 | 107 | fn keysym_to_keycode(&self, keysym: Keysym) -> Option { 108 | let keycode_min: usize = self.keycode_min.try_into().unwrap(); 109 | let keycode_max: usize = self.keycode_max.try_into().unwrap(); 110 | 111 | // TODO: Change this range to 0..self.keysyms_per_keycode once we find out how 112 | // to detect the level and switch it 113 | for j in 0..1 { 114 | for i in keycode_min..=keycode_max { 115 | let i: u32 = i.try_into().unwrap(); 116 | let min_keycode: u32 = keycode_min.try_into().unwrap(); 117 | let keycode = KeyCode::from(i); 118 | let min_keycode = KeyCode::from(min_keycode); 119 | if let Some(ks) = xkeysym::keysym( 120 | keycode, 121 | j, 122 | min_keycode, 123 | self.keysyms_per_keycode, 124 | &self.keysyms, 125 | ) { 126 | if ks == keysym { 127 | let i: usize = i.try_into().unwrap(); 128 | let i: Keycode = i.try_into().unwrap(); 129 | trace!("found keysym in row {i}, col {j}"); 130 | return Some(i); 131 | } 132 | } 133 | } 134 | } 135 | None 136 | } 137 | 138 | // Try to enter the key 139 | #[allow(clippy::unnecessary_wraps)] 140 | pub fn key_to_keycode>(&mut self, c: &C, key: Key) -> InputResult { 141 | let sym = Keysym::from(key); 142 | 143 | if let Some(keycode) = self.keysym_to_keycode(sym) { 144 | return Ok(keycode); 145 | } 146 | 147 | let keycode = { 148 | if let Some(&keycode) = self.additionally_mapped.get(&sym) { 149 | // The keysym is already mapped and cached in the keymap 150 | keycode 151 | } else { 152 | // Unmap keysyms if there are no unused keycodes 153 | self.make_room(c)?; 154 | // The keysym needs to get mapped to an unused keycode. 155 | // Always map the keycode if it has not yet been mapped, so it is layer agnostic 156 | self.map(c, sym)? 157 | } 158 | }; 159 | 160 | #[cfg(feature = "x11rb")] 161 | self.update_delays(keycode); 162 | Ok(keycode) 163 | } 164 | 165 | /// Get the pending delay 166 | #[cfg(feature = "x11rb")] 167 | pub fn pending_delays(&self) -> u32 { 168 | self.pending_delays 169 | } 170 | 171 | /// Add the Keysym to the keymap 172 | /// 173 | /// This does not apply the changes 174 | pub fn map>(&mut self, c: &C, keysym: Keysym) -> InputResult { 175 | match self.unused_keycodes.pop_front() { 176 | // A keycode is unused so a mapping is possible 177 | Some(unused_keycode) => { 178 | trace!( 179 | "trying to map keycode {} to keysym {:?}", 180 | unused_keycode, 181 | keysym 182 | ); 183 | if c.bind_key(unused_keycode, keysym).is_err() { 184 | return Err(InputError::Mapping(format!("{keysym:?}"))); 185 | }; 186 | self.needs_regeneration = true; 187 | self.additionally_mapped.insert(keysym, unused_keycode); 188 | debug!("mapped keycode {} to keysym {:?}", unused_keycode, keysym); 189 | Ok(unused_keycode) 190 | } 191 | // All keycodes are being used. A mapping is not possible 192 | None => Err(InputError::Mapping(format!("{keysym:?}"))), 193 | } 194 | } 195 | 196 | /// Remove the Keysym from the keymap 197 | /// 198 | /// This does not apply the changes 199 | pub fn unmap>( 200 | &mut self, 201 | c: &C, 202 | keysym: Keysym, 203 | keycode: Keycode, 204 | ) -> InputResult<()> { 205 | trace!("trying to unmap keysym {:?}", keysym); 206 | if c.bind_key(keycode, NO_SYMBOL).is_err() { 207 | return Err(InputError::Unmapping(format!("{keysym:?}"))); 208 | }; 209 | self.needs_regeneration = true; 210 | self.unused_keycodes.push_back(keycode); 211 | self.additionally_mapped.remove(&keysym); 212 | debug!("unmapped keysym {:?}", keysym); 213 | Ok(()) 214 | } 215 | 216 | // Update the delay 217 | // TODO: A delay of 1 ms in all cases seems to work on my machine. Maybe 218 | // this is not needed? 219 | #[cfg(feature = "x11rb")] 220 | pub fn update_delays(&mut self, keycode: Keycode) { 221 | // Check if a delay is needed 222 | // A delay is required, if one of the keycodes was recently entered and there 223 | // was no delay between it 224 | 225 | // e.g. A quick rabbit 226 | // Chunk 1: 'A quick' # Add a delay before the second space 227 | // Chunk 2: ' rab' # Add a delay before the second 'b' 228 | // Chunk 3: 'bit' # Enter the remaining chars 229 | 230 | if self.last_keys.contains(&keycode) { 231 | let elapsed_ms = self 232 | .last_event_before_delays 233 | .elapsed() 234 | .as_millis() 235 | .try_into() 236 | .unwrap_or(u32::MAX); 237 | self.pending_delays = self.delay.saturating_sub(elapsed_ms); 238 | trace!("delay needed"); 239 | self.last_keys.clear(); 240 | } else { 241 | trace!("no delay needed"); 242 | self.pending_delays = 1; 243 | } 244 | self.last_keys.push(keycode); 245 | } 246 | 247 | /// Check if there are still unused keycodes available. If there aren't, 248 | /// make some room by freeing the already mapped keycodes. 249 | /// Returns true, if keys were unmapped and the keymap needs to be 250 | /// regenerated 251 | fn make_room>(&mut self, c: &C) -> InputResult<()> { 252 | // Unmap all keys, if all keycodes are already being used 253 | if self.unused_keycodes.is_empty() { 254 | let mapped_keys = self.additionally_mapped.clone(); 255 | let held_keycodes = self.held_keycodes.clone(); 256 | let mut made_room = false; 257 | 258 | for (&sym, &keycode) in mapped_keys 259 | .iter() 260 | .filter(|(_, keycode)| !held_keycodes.contains(keycode)) 261 | { 262 | self.unmap(c, sym, keycode)?; 263 | made_room = true; 264 | } 265 | if made_room { 266 | return Ok(()); 267 | } 268 | return Err(InputError::Unmapping("all keys that were mapped are also currently held. no way to make room for new mappings".to_string())); 269 | } 270 | Ok(()) 271 | } 272 | 273 | /// Regenerate the keymap if there were any changes 274 | /// and write the new keymap to a temporary file 275 | /// 276 | /// If there was the need to regenerate the keymap, the size of the keymap 277 | /// is returned 278 | #[cfg(feature = "wayland")] 279 | pub fn regenerate(&mut self) -> Result, std::io::Error> { 280 | use super::{KEYMAP_BEGINNING, KEYMAP_END}; 281 | use std::io::{Seek, SeekFrom, Write}; 282 | use xkbcommon::xkb::keysym_get_name; 283 | 284 | // Don't do anything if there were no changes 285 | if !self.needs_regeneration { 286 | debug!("keymap did not change and does not require regeneration"); 287 | return Ok(None); 288 | } 289 | 290 | // Create a file to store the layout 291 | if self.file.is_none() { 292 | let mut temp_file = tempfile::tempfile()?; 293 | temp_file.write_all(KEYMAP_BEGINNING)?; 294 | self.file = Some(temp_file); 295 | } 296 | 297 | let keymap_file = self 298 | .file 299 | .as_mut() 300 | .expect("There was no file to write to. This should not be possible!"); 301 | // Move the virtual cursor of the file to the end of the part of the keymap that 302 | // is always the same so we only overwrite the parts that can change. 303 | keymap_file.seek(SeekFrom::Start(KEYMAP_BEGINNING.len() as u64))?; 304 | for (&keysym, &keycode) in &self.additionally_mapped { 305 | write!( 306 | keymap_file, 307 | " 308 | key {{ [ {} ] }}; // \\n", 309 | keycode, 310 | keysym_get_name(keysym) 311 | )?; 312 | } 313 | keymap_file.write_all(KEYMAP_END)?; 314 | // Truncate the file at the current cursor position in order to cut off any old 315 | // data in case the keymap was smaller than the old one 316 | let keymap_len = keymap_file.stream_position()?; 317 | keymap_file.set_len(keymap_len)?; 318 | self.needs_regeneration = false; 319 | match keymap_len.try_into() { 320 | Ok(v) => { 321 | debug!("regenerated the keymap"); 322 | Ok(Some(v)) 323 | } 324 | Err(_) => Err(std::io::Error::new( 325 | std::io::ErrorKind::Other, 326 | "the length of the new keymap exceeds the u32::MAX", 327 | )), 328 | } 329 | } 330 | 331 | /// Tells the keymap that a modifier was pressed 332 | /// Updates the internal state of the modifiers and returns the new bitflag 333 | /// representing the state of the modifiers 334 | #[cfg(feature = "wayland")] 335 | pub fn enter_modifier( 336 | &mut self, 337 | modifier: ModifierBitflag, 338 | direction: crate::Direction, 339 | ) -> ModifierBitflag { 340 | match direction { 341 | crate::Direction::Press => { 342 | self.modifiers |= modifier; 343 | self.modifiers 344 | } 345 | crate::Direction::Release => { 346 | self.modifiers &= !modifier; 347 | self.modifiers 348 | } 349 | crate::Direction::Click => self.modifiers, 350 | } 351 | } 352 | 353 | pub fn key(&mut self, keycode: Keycode, direction: Direction) { 354 | match direction { 355 | Direction::Press => { 356 | debug!("added the key {keycode} to the held keycodes"); 357 | self.held_keycodes.push(keycode); 358 | } 359 | Direction::Release => { 360 | debug!("removed the key {keycode} from the held keycodes"); 361 | self.held_keycodes.retain(|&k| k != keycode); 362 | } 363 | Direction::Click => (), 364 | } 365 | 366 | #[cfg(feature = "x11rb")] 367 | { 368 | self.last_event_before_delays = std::time::Instant::now(); 369 | } 370 | } 371 | } 372 | 373 | pub trait Bind { 374 | // Map the keysym to the given keycode 375 | // Only use keycodes that are not used, otherwise the existing mapping is 376 | // overwritten 377 | // If the keycode is mapped to the NoSymbol keysym, the key is unbound and can 378 | // get used again later 379 | fn bind_key(&self, _: Keycode, _: Keysym) -> Result<(), ()> { 380 | Ok(()) // No need to do anything 381 | } 382 | } 383 | 384 | impl Bind for () {} 385 | -------------------------------------------------------------------------------- /libs/enigo/src/linux/mod.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, error, trace, warn}; 2 | 3 | use crate::{ 4 | Axis, Button, Coordinate, Direction, InputError, InputResult, Key, Keyboard, Mouse, 5 | NewConError, Settings, 6 | }; 7 | 8 | // If none of these features is enabled, there is no way to simulate input 9 | #[cfg(not(any( 10 | feature = "wayland", 11 | feature = "x11rb", 12 | feature = "xdo", 13 | feature = "libei" 14 | )))] 15 | compile_error!( 16 | "either feature `wayland`, `x11rb`, `xdo` or `libei` must be enabled for this crate when using linux" 17 | ); 18 | 19 | #[cfg(feature = "libei")] 20 | mod libei; 21 | 22 | #[cfg(feature = "wayland")] 23 | mod wayland; 24 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 25 | #[cfg_attr(feature = "x11rb", path = "x11rb.rs")] 26 | #[cfg_attr(not(feature = "x11rb"), path = "xdo.rs")] 27 | mod x11; 28 | 29 | #[cfg(feature = "wayland")] 30 | mod constants; 31 | #[cfg(feature = "wayland")] 32 | use constants::{KEYMAP_BEGINNING, KEYMAP_END}; 33 | 34 | #[cfg(any(feature = "wayland", feature = "x11rb"))] 35 | mod keymap; 36 | 37 | pub struct Enigo { 38 | held: (Vec, Vec), // Currently held keys and held keycodes 39 | release_keys_when_dropped: bool, 40 | #[cfg(feature = "wayland")] 41 | wayland: Option, 42 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 43 | x11: Option, 44 | #[cfg(feature = "libei")] 45 | libei: Option, 46 | } 47 | 48 | impl Enigo { 49 | /// Create a new Enigo struct to establish the connection to simulate input 50 | /// with the specified settings 51 | /// 52 | /// # Errors 53 | /// Have a look at the documentation of `NewConError` to see under which 54 | /// conditions an error will be returned. 55 | pub fn new(settings: &Settings) -> Result { 56 | let mut connection_established = false; 57 | #[allow(unused_variables)] 58 | let Settings { 59 | linux_delay, 60 | x11_display, 61 | wayland_display, 62 | release_keys_when_dropped, 63 | .. 64 | } = settings; 65 | 66 | let held = (Vec::new(), Vec::new()); 67 | #[cfg(feature = "wayland")] 68 | let wayland = match wayland::Con::new(wayland_display) { 69 | Ok(con) => { 70 | connection_established = true; 71 | debug!("wayland connection established"); 72 | Some(con) 73 | } 74 | Err(e) => { 75 | warn!("{e}"); 76 | None 77 | } 78 | }; 79 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 80 | match x11_display { 81 | Some(name) => { 82 | debug!( 83 | "\x1b[93mtrying to establish a x11 connection to: {}\x1b[0m", 84 | name 85 | ); 86 | } 87 | None => { 88 | debug!("\x1b[93mtrying to establish a x11 connection to $DISPLAY\x1b[0m"); 89 | } 90 | } 91 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 92 | let x11 = match x11::Con::new(x11_display, *linux_delay) { 93 | Ok(con) => { 94 | connection_established = true; 95 | debug!("x11 connection established"); 96 | Some(con) 97 | } 98 | Err(e) => { 99 | warn!("failed to establish x11 connection: {e}"); 100 | None 101 | } 102 | }; 103 | #[cfg(feature = "libei")] 104 | let libei = match libei::Con::new() { 105 | Ok(con) => { 106 | connection_established = true; 107 | debug!("libei connection established"); 108 | Some(con) 109 | } 110 | Err(e) => { 111 | warn!("failed to establish libei connection: {e}"); 112 | None 113 | } 114 | }; 115 | if !connection_established { 116 | error!("no successful connection"); 117 | return Err(NewConError::EstablishCon("no successful connection")); 118 | } 119 | 120 | Ok(Self { 121 | held, 122 | release_keys_when_dropped: *release_keys_when_dropped, 123 | #[cfg(feature = "wayland")] 124 | wayland, 125 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 126 | x11, 127 | #[cfg(feature = "libei")] 128 | libei, 129 | }) 130 | } 131 | 132 | /// Get the delay per keypress 133 | #[must_use] 134 | pub fn delay(&self) -> u32 { 135 | // On Wayland there is no delay 136 | 137 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 138 | if let Some(con) = self.x11.as_ref() { 139 | return con.delay(); 140 | } 141 | 0 142 | } 143 | 144 | /// Set the delay per keypress 145 | #[allow(unused_variables)] 146 | pub fn set_delay(&mut self, delay: u32) { 147 | // On Wayland there is no delay 148 | 149 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 150 | if let Some(con) = self.x11.as_mut() { 151 | con.set_delay(delay); 152 | } 153 | } 154 | 155 | /// Returns a list of all currently pressed keys 156 | pub fn held(&mut self) -> (Vec, Vec) { 157 | self.held.clone() 158 | } 159 | } 160 | 161 | impl Mouse for Enigo { 162 | fn button(&mut self, button: Button, direction: Direction) -> InputResult<()> { 163 | debug!("\x1b[93mbutton(button: {button:?}, direction: {direction:?})\x1b[0m"); 164 | let mut success = false; 165 | #[cfg(feature = "libei")] 166 | if let Some(con) = self.libei.as_mut() { 167 | trace!("try sending button event via libei"); 168 | con.button(button, direction)?; 169 | debug!("sent button event via libei"); 170 | success = true; 171 | } 172 | #[cfg(feature = "wayland")] 173 | if let Some(con) = self.wayland.as_mut() { 174 | trace!("try sending button event via wayland"); 175 | con.button(button, direction)?; 176 | debug!("sent button event via wayland"); 177 | success = true; 178 | } 179 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 180 | if let Some(con) = self.x11.as_mut() { 181 | trace!("try sending button event via x11"); 182 | con.button(button, direction)?; 183 | debug!("sent button event via x11"); 184 | success = true; 185 | } 186 | if success { 187 | debug!("sent button event"); 188 | Ok(()) 189 | } else { 190 | Err(InputError::Simulate("No protocol to enter the result")) 191 | } 192 | } 193 | 194 | fn move_mouse(&mut self, x: i32, y: i32, coordinate: Coordinate) -> InputResult<()> { 195 | debug!("\x1b[93mmove_mouse(x: {x:?}, y: {y:?}, coordinate:{coordinate:?})\x1b[0m"); 196 | let mut success = false; 197 | #[cfg(feature = "libei")] 198 | if let Some(con) = self.libei.as_mut() { 199 | trace!("try moving the mouse via libei"); 200 | con.move_mouse(x, y, coordinate)?; 201 | debug!("moved the mouse via libei"); 202 | success = true; 203 | } 204 | #[cfg(feature = "wayland")] 205 | if let Some(con) = self.wayland.as_mut() { 206 | trace!("try moving the mouse via wayland"); 207 | con.move_mouse(x, y, coordinate)?; 208 | debug!("moved the mouse via wayland"); 209 | success = true; 210 | } 211 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 212 | if let Some(con) = self.x11.as_mut() { 213 | trace!("try moving the mouse via x11"); 214 | con.move_mouse(x, y, coordinate)?; 215 | debug!("moved the mouse via x11"); 216 | success = true; 217 | } 218 | if success { 219 | debug!("moved the mouse"); 220 | Ok(()) 221 | } else { 222 | Err(InputError::Simulate("No protocol to enter the result")) 223 | } 224 | } 225 | 226 | fn scroll(&mut self, length: i32, axis: Axis) -> InputResult<()> { 227 | debug!("\x1b[93mscroll(length: {length:?}, axis: {axis:?})\x1b[0m"); 228 | let mut success = false; 229 | #[cfg(feature = "libei")] 230 | if let Some(con) = self.libei.as_mut() { 231 | trace!("try scrolling via libei"); 232 | con.scroll(length, axis)?; 233 | debug!("scrolled via libei"); 234 | success = true; 235 | } 236 | #[cfg(feature = "wayland")] 237 | if let Some(con) = self.wayland.as_mut() { 238 | trace!("try scrolling via wayland"); 239 | con.scroll(length, axis)?; 240 | debug!("scrolled via wayland"); 241 | success = true; 242 | } 243 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 244 | if let Some(con) = self.x11.as_mut() { 245 | trace!("try scrolling via x11"); 246 | con.scroll(length, axis)?; 247 | debug!("scrolled via x11"); 248 | success = true; 249 | } 250 | if success { 251 | debug!("scrolled"); 252 | Ok(()) 253 | } else { 254 | Err(InputError::Simulate("No protocol to enter the result")) 255 | } 256 | } 257 | 258 | fn main_display(&self) -> InputResult<(i32, i32)> { 259 | debug!("\x1b[93mmain_display()\x1b[0m"); 260 | #[cfg(feature = "libei")] 261 | if let Some(con) = self.libei.as_ref() { 262 | trace!("try getting the dimensions of the display via libei"); 263 | return con.main_display(); 264 | } 265 | #[cfg(feature = "wayland")] 266 | if let Some(con) = self.wayland.as_ref() { 267 | trace!("try getting the dimensions of the display via wayland"); 268 | return con.main_display(); 269 | } 270 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 271 | if let Some(con) = self.x11.as_ref() { 272 | trace!("try getting the dimensions of the display via x11"); 273 | return con.main_display(); 274 | } 275 | Err(InputError::Simulate("No protocol to enter the result")) 276 | } 277 | 278 | fn location(&self) -> InputResult<(i32, i32)> { 279 | debug!("\x1b[93mlocation()\x1b[0m"); 280 | #[cfg(feature = "libei")] 281 | if let Some(con) = self.libei.as_ref() { 282 | trace!("try getting the mouse location via libei"); 283 | return con.location(); 284 | } 285 | #[cfg(feature = "wayland")] 286 | if let Some(con) = self.wayland.as_ref() { 287 | trace!("try getting the mouse location via wayland"); 288 | return con.location(); 289 | } 290 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 291 | if let Some(con) = self.x11.as_ref() { 292 | trace!("try getting the mouse location via x11"); 293 | return con.location(); 294 | } 295 | Err(InputError::Simulate("No protocol to enter the result")) 296 | } 297 | } 298 | 299 | impl Keyboard for Enigo { 300 | fn fast_text(&mut self, text: &str) -> InputResult> { 301 | debug!("\x1b[93mfast_text(text: {text})\x1b[0m"); 302 | 303 | #[cfg(feature = "libei")] 304 | if let Some(con) = self.libei.as_mut() { 305 | trace!("try entering text fast via libei"); 306 | con.text(text)?; 307 | } 308 | #[cfg(feature = "wayland")] 309 | if let Some(con) = self.wayland.as_mut() { 310 | trace!("try entering text fast via wayland"); 311 | con.text(text)?; 312 | } 313 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 314 | if let Some(con) = self.x11.as_mut() { 315 | trace!("try entering text fast via x11"); 316 | con.text(text)?; 317 | } 318 | debug!("entered the text fast"); 319 | Ok(Some(())) 320 | } 321 | 322 | fn key(&mut self, key: Key, direction: Direction) -> InputResult<()> { 323 | debug!("\x1b[93mkey(key: {key:?}, direction: {direction:?})\x1b[0m"); 324 | // Nothing to do 325 | if key == Key::Unicode('\0') { 326 | debug!("entering the null byte is a noop"); 327 | return Ok(()); 328 | } 329 | 330 | #[cfg(feature = "libei")] 331 | if let Some(con) = self.libei.as_mut() { 332 | trace!("try entering the key via libei"); 333 | con.key(key, direction)?; 334 | debug!("entered the key via libei"); 335 | } 336 | 337 | #[cfg(feature = "wayland")] 338 | if let Some(con) = self.wayland.as_mut() { 339 | trace!("try entering the key via wayland"); 340 | con.key(key, direction)?; 341 | debug!("entered the key via wayland"); 342 | } 343 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 344 | if let Some(con) = self.x11.as_mut() { 345 | trace!("try entering the key via x11"); 346 | con.key(key, direction)?; 347 | debug!("entered the key via x11"); 348 | } 349 | 350 | match direction { 351 | Direction::Press => { 352 | debug!("added the key {key:?} to the held keys"); 353 | self.held.0.push(key); 354 | } 355 | Direction::Release => { 356 | debug!("removed the key {key:?} from the held keys"); 357 | self.held.0.retain(|&k| k != key); 358 | } 359 | Direction::Click => (), 360 | } 361 | 362 | debug!("entered the key"); 363 | Ok(()) 364 | } 365 | 366 | fn raw(&mut self, keycode: u16, direction: Direction) -> InputResult<()> { 367 | debug!("\x1b[93mraw(keycode: {keycode:?}, direction: {direction:?})\x1b[0m"); 368 | 369 | #[cfg(feature = "libei")] 370 | if let Some(con) = self.libei.as_mut() { 371 | trace!("try entering the keycode via libei"); 372 | con.raw(keycode, direction)?; 373 | debug!("entered the keycode via libei"); 374 | } 375 | #[cfg(feature = "wayland")] 376 | if let Some(con) = self.wayland.as_mut() { 377 | trace!("try entering the keycode via wayland"); 378 | con.raw(keycode, direction)?; 379 | debug!("entered the keycode via wayland"); 380 | } 381 | #[cfg(any(feature = "x11rb", feature = "xdo"))] 382 | if let Some(con) = self.x11.as_mut() { 383 | trace!("try entering the keycode via x11"); 384 | con.raw(keycode, direction)?; 385 | debug!("entered the keycode via x11"); 386 | } 387 | 388 | match direction { 389 | Direction::Press => { 390 | debug!("added the keycode {keycode:?} to the held keys"); 391 | self.held.1.push(keycode); 392 | } 393 | Direction::Release => { 394 | debug!("removed the keycode {keycode:?} from the held keys"); 395 | self.held.1.retain(|&k| k != keycode); 396 | } 397 | Direction::Click => (), 398 | } 399 | 400 | debug!("entered the keycode"); 401 | Ok(()) 402 | } 403 | } 404 | 405 | impl Drop for Enigo { 406 | // Release the held keys before the connection is dropped 407 | fn drop(&mut self) { 408 | if !self.release_keys_when_dropped { 409 | return; 410 | } 411 | let (held_keys, held_keycodes) = self.held(); 412 | for &key in &held_keys { 413 | if self.key(key, Direction::Release).is_err() { 414 | error!("unable to release {:?}", key); 415 | }; 416 | } 417 | for &keycode in &held_keycodes { 418 | if self.raw(keycode, Direction::Release).is_err() { 419 | error!("unable to release {:?}", keycode); 420 | }; 421 | } 422 | debug!("released all held keys and held keycodes"); 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /libs/enigo/src/linux/xdo.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::{c_char, c_int, c_ulong, c_void, CString}, 3 | ptr, 4 | }; 5 | 6 | use libc::useconds_t; 7 | 8 | use log::debug; 9 | 10 | use crate::{ 11 | Axis, Button, Coordinate, Direction, InputError, InputResult, Key, Keyboard, Mouse, NewConError, 12 | }; 13 | use xkeysym::Keysym; 14 | 15 | const CURRENT_WINDOW: c_ulong = 0; 16 | const XDO_SUCCESS: c_int = 0; 17 | 18 | type Window = c_ulong; 19 | type Xdo = *const c_void; 20 | 21 | #[link(name = "xdo")] 22 | extern "C" { 23 | fn xdo_free(xdo: Xdo); 24 | fn xdo_new(display: *const c_char) -> Xdo; 25 | 26 | fn xdo_click_window(xdo: Xdo, window: Window, button: c_int) -> c_int; 27 | fn xdo_mouse_down(xdo: Xdo, window: Window, button: c_int) -> c_int; 28 | fn xdo_mouse_up(xdo: Xdo, window: Window, button: c_int) -> c_int; 29 | fn xdo_move_mouse(xdo: Xdo, x: c_int, y: c_int, screen: c_int) -> c_int; 30 | fn xdo_move_mouse_relative(xdo: Xdo, x: c_int, y: c_int) -> c_int; 31 | 32 | fn xdo_enter_text_window( 33 | xdo: Xdo, 34 | window: Window, 35 | string: *const c_char, 36 | delay: useconds_t, 37 | ) -> c_int; 38 | fn xdo_send_keysequence_window( 39 | xdo: Xdo, 40 | window: Window, 41 | string: *const c_char, 42 | delay: useconds_t, 43 | ) -> c_int; 44 | fn xdo_send_keysequence_window_down( 45 | xdo: Xdo, 46 | window: Window, 47 | string: *const c_char, 48 | delay: useconds_t, 49 | ) -> c_int; 50 | fn xdo_send_keysequence_window_up( 51 | xdo: Xdo, 52 | window: Window, 53 | string: *const c_char, 54 | delay: useconds_t, 55 | ) -> c_int; 56 | 57 | fn xdo_get_viewport_dimensions( 58 | xdo: Xdo, 59 | width: *mut c_int, 60 | height: *mut c_int, 61 | screen: c_int, 62 | ) -> c_int; 63 | 64 | fn xdo_get_mouse_location2( 65 | xdo: Xdo, 66 | x: *mut c_int, 67 | y: *mut c_int, 68 | screen: *mut c_int, 69 | window: *mut Window, 70 | ) -> c_int; 71 | } 72 | 73 | fn mousebutton(button: Button) -> c_int { 74 | match button { 75 | Button::Left => 1, 76 | Button::Middle => 2, 77 | Button::Right => 3, 78 | Button::ScrollUp => 4, 79 | Button::ScrollDown => 5, 80 | Button::ScrollLeft => 6, 81 | Button::ScrollRight => 7, 82 | Button::Back => 8, 83 | Button::Forward => 9, 84 | } 85 | } 86 | 87 | /// The main struct for handling the event emitting 88 | pub struct Con { 89 | xdo: Xdo, 90 | delay: u32, // microseconds 91 | } 92 | // This is safe, we have a unique pointer. 93 | // TODO: use Unique once stable. 94 | unsafe impl Send for Con {} 95 | 96 | impl Con { 97 | /// Create a new Enigo instance 98 | /// If no `dyp_name` is provided, the $DISPLAY environment variable is read 99 | /// and used instead 100 | pub fn new(dyp_name: &Option, delay: u32) -> Result { 101 | debug!("using xdo"); 102 | let xdo = match dyp_name { 103 | Some(name) => { 104 | let Ok(string) = CString::new(name.as_bytes()) else { 105 | return Err(NewConError::EstablishCon( 106 | "the display name contained a null byte", 107 | )); 108 | }; 109 | unsafe { xdo_new(string.as_ptr()) } 110 | } 111 | None => unsafe { xdo_new(ptr::null()) }, 112 | }; 113 | // If it was not possible to establish a connection, a NULL pointer is returned 114 | if xdo.is_null() { 115 | return Err(NewConError::EstablishCon( 116 | "establishing a connection to the display name was unsuccessful", 117 | )); 118 | } 119 | Ok(Self { 120 | xdo, 121 | delay: delay * 1000, 122 | }) 123 | } 124 | 125 | /// Get the delay per keypress in milliseconds 126 | #[must_use] 127 | pub fn delay(&self) -> u32 { 128 | self.delay / 1000 129 | } 130 | 131 | /// Set the delay per keypress in milliseconds 132 | pub fn set_delay(&mut self, delay: u32) { 133 | self.delay = delay * 1000; 134 | } 135 | } 136 | 137 | impl Drop for Con { 138 | fn drop(&mut self) { 139 | unsafe { 140 | xdo_free(self.xdo); 141 | } 142 | } 143 | } 144 | 145 | impl Keyboard for Con { 146 | fn fast_text(&mut self, text: &str) -> InputResult> { 147 | let Ok(string) = CString::new(text) else { 148 | return Err(InputError::InvalidInput( 149 | "the text to enter contained a NULL byte ('\\0’), which is not allowed", 150 | )); 151 | }; 152 | debug!( 153 | "xdo_enter_text_window with string {:?}, delay {}", 154 | string, self.delay 155 | ); 156 | let res = unsafe { 157 | xdo_enter_text_window( 158 | self.xdo, 159 | CURRENT_WINDOW, 160 | string.as_ptr(), 161 | self.delay as useconds_t, 162 | ) 163 | }; 164 | if res != XDO_SUCCESS { 165 | return Err(InputError::Simulate("unable to enter text")); 166 | } 167 | Ok(Some(())) 168 | } 169 | 170 | fn key(&mut self, key: Key, direction: Direction) -> InputResult<()> { 171 | let keysym = Keysym::from(key); 172 | let Some(keysym_name) = keysym.name() else { 173 | // this should never happen, because we only use keysyms with a known name 174 | return Err(InputError::InvalidInput("the keysym does not have a name")); 175 | }; 176 | let keysym_name = keysym_name.replace("XK_", ""); // TODO: remove if xkeysym changed their names (https://github.com/rust-windowing/xkeysym/issues/18) 177 | 178 | let Ok(string) = CString::new(keysym_name) else { 179 | // this should never happen, because none of the names contain NULL bytes 180 | return Err(InputError::InvalidInput( 181 | "the name of the keysym contained a null byte", 182 | )); 183 | }; 184 | 185 | let res = match direction { 186 | Direction::Click => { 187 | debug!( 188 | "xdo_send_keysequence_window with string {:?}, delay {}", 189 | string, self.delay 190 | ); 191 | unsafe { 192 | xdo_send_keysequence_window( 193 | self.xdo, 194 | CURRENT_WINDOW, 195 | string.as_ptr(), 196 | self.delay as useconds_t, 197 | ) 198 | } 199 | } 200 | Direction::Press => { 201 | debug!( 202 | "xdo_send_keysequence_window_down with string {:?}, delay {}", 203 | string, self.delay 204 | ); 205 | unsafe { 206 | xdo_send_keysequence_window_down( 207 | self.xdo, 208 | CURRENT_WINDOW, 209 | string.as_ptr(), 210 | self.delay as useconds_t, 211 | ) 212 | } 213 | } 214 | Direction::Release => { 215 | debug!( 216 | "xdo_send_keysequence_window_up with string {:?}, delay {}", 217 | string, self.delay 218 | ); 219 | unsafe { 220 | xdo_send_keysequence_window_up( 221 | self.xdo, 222 | CURRENT_WINDOW, 223 | string.as_ptr(), 224 | self.delay as useconds_t, 225 | ) 226 | } 227 | } 228 | }; 229 | if res != XDO_SUCCESS { 230 | return Err(InputError::Simulate("unable to enter key")); 231 | } 232 | Ok(()) 233 | } 234 | 235 | fn raw(&mut self, _keycode: u16, _direction: Direction) -> InputResult<()> { 236 | // TODO: Lookup the key name for the keycode and then enter that with xdotool. 237 | // This is a bit weird, because xdotool will then do the reverse. Maybe there is 238 | // a better way? 239 | todo!("You cant enter raw keycodes with xdotool") 240 | } 241 | } 242 | 243 | impl Mouse for Con { 244 | fn button(&mut self, button: Button, direction: Direction) -> InputResult<()> { 245 | let button = mousebutton(button); 246 | let res = match direction { 247 | Direction::Press => { 248 | debug!("xdo_mouse_down with mouse button {}", button); 249 | unsafe { xdo_mouse_down(self.xdo, CURRENT_WINDOW, button) } 250 | } 251 | Direction::Release => { 252 | debug!("xdo_mouse_up with mouse button {}", button); 253 | unsafe { xdo_mouse_up(self.xdo, CURRENT_WINDOW, button) } 254 | } 255 | Direction::Click => { 256 | debug!("xdo_click_window with mouse button {}", button); 257 | unsafe { xdo_click_window(self.xdo, CURRENT_WINDOW, button) } 258 | } 259 | }; 260 | if res != XDO_SUCCESS { 261 | return Err(InputError::Simulate("unable to enter mouse button")); 262 | } 263 | Ok(()) 264 | } 265 | 266 | fn move_mouse(&mut self, x: i32, y: i32, coordinate: Coordinate) -> InputResult<()> { 267 | let res = match coordinate { 268 | Coordinate::Rel => { 269 | debug!("xdo_move_mouse_relative with x {}, y {}", x, y); 270 | unsafe { xdo_move_mouse_relative(self.xdo, x as c_int, y as c_int) } 271 | } 272 | Coordinate::Abs => { 273 | debug!("xdo_move_mouse with mouse button with x {}, y {}", x, y); 274 | unsafe { xdo_move_mouse(self.xdo, x as c_int, y as c_int, 0) } 275 | } 276 | }; 277 | if res != XDO_SUCCESS { 278 | return Err(InputError::Simulate("unable to move the mouse")); 279 | } 280 | Ok(()) 281 | } 282 | 283 | fn scroll(&mut self, length: i32, axis: Axis) -> InputResult<()> { 284 | let mut length = length; 285 | let button = if length < 0 { 286 | length = -length; 287 | match axis { 288 | Axis::Horizontal => Button::ScrollLeft, 289 | Axis::Vertical => Button::ScrollUp, 290 | } 291 | } else { 292 | match axis { 293 | Axis::Horizontal => Button::ScrollRight, 294 | Axis::Vertical => Button::ScrollDown, 295 | } 296 | }; 297 | for _ in 0..length { 298 | self.button(button, Direction::Click)?; 299 | } 300 | Ok(()) 301 | } 302 | 303 | fn main_display(&self) -> InputResult<(i32, i32)> { 304 | const MAIN_SCREEN: i32 = 0; 305 | let mut width = 0; 306 | let mut height = 0; 307 | 308 | debug!("xdo_get_viewport_dimensions"); 309 | let res = 310 | unsafe { xdo_get_viewport_dimensions(self.xdo, &mut width, &mut height, MAIN_SCREEN) }; 311 | 312 | if res != XDO_SUCCESS { 313 | return Err(InputError::Simulate("unable to get the main display")); 314 | } 315 | Ok((width, height)) 316 | } 317 | 318 | fn location(&self) -> InputResult<(i32, i32)> { 319 | let mut x = 0; 320 | let mut y = 0; 321 | let mut unused_screen_index = 0; 322 | let mut unused_window_index = CURRENT_WINDOW; 323 | debug!("xdo_get_mouse_location2"); 324 | let res = unsafe { 325 | xdo_get_mouse_location2( 326 | self.xdo, 327 | &mut x, 328 | &mut y, 329 | &mut unused_screen_index, 330 | &mut unused_window_index, 331 | ) 332 | }; 333 | if res != XDO_SUCCESS { 334 | return Err(InputError::Simulate( 335 | "unable to get the position of the mouse", 336 | )); 337 | } 338 | Ok((x, y)) 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /libs/enigo/src/macos/mod.rs: -------------------------------------------------------------------------------- 1 | mod macos_impl; 2 | pub use macos_impl::Enigo; 3 | -------------------------------------------------------------------------------- /libs/enigo/src/platform.rs: -------------------------------------------------------------------------------- 1 | use crate::{Keyboard, Mouse}; 2 | 3 | // Enum without any variants 4 | // This can never get constructed 5 | // See https://github.com/enigo-rs/enigo/pull/269 for more details 6 | enum Never {} 7 | 8 | pub struct Enigo { 9 | never: Never, 10 | } 11 | 12 | impl Mouse for Enigo { 13 | fn button(&mut self, _: crate::Button, _: crate::Direction) -> crate::InputResult<()> { 14 | match self.never {} 15 | } 16 | 17 | fn move_mouse(&mut self, _: i32, _: i32, _: crate::Coordinate) -> crate::InputResult<()> { 18 | match self.never {} 19 | } 20 | 21 | fn scroll(&mut self, _: i32, _: crate::Axis) -> crate::InputResult<()> { 22 | match self.never {} 23 | } 24 | 25 | fn main_display(&self) -> crate::InputResult<(i32, i32)> { 26 | match self.never {} 27 | } 28 | 29 | fn location(&self) -> crate::InputResult<(i32, i32)> { 30 | match self.never {} 31 | } 32 | } 33 | 34 | impl Keyboard for Enigo { 35 | fn fast_text(&mut self, _: &str) -> crate::InputResult> { 36 | match self.never {} 37 | } 38 | 39 | fn key(&mut self, _: crate::Key, _: crate::Direction) -> crate::InputResult<()> { 40 | match self.never {} 41 | } 42 | 43 | fn raw(&mut self, _: u16, _: crate::Direction) -> crate::InputResult<()> { 44 | match self.never {} 45 | } 46 | } 47 | 48 | impl Drop for Enigo { 49 | fn drop(&mut self) { 50 | match self.never {} 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /libs/enigo/src/tests/keyboard.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Direction::{Click, Press, Release}, 3 | Enigo, Key, Keyboard, Settings, 4 | }; 5 | use std::thread; 6 | 7 | #[test] 8 | // Try entering various texts that were selected to test edge cases. 9 | // Because it is hard to test if they succeed, 10 | // we assume it worked as long as there was no panic 11 | fn unit_text() { 12 | thread::sleep(super::get_delay()); 13 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 14 | 15 | let sequences = vec![ 16 | "", /* Empty string */ 17 | "a", // Simple character 18 | "z", // Simple character // TODO: This enters "y" on my computer 19 | "9", // Number 20 | "%", // Special character 21 | "𝕊", // Special char which needs two u16s to be encoded 22 | "❤️", // Single emoji 23 | "abcde", // Simple short character string (shorter than 20 chars) 24 | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", /* Simple long character string (longer than 20 chars to test the restrictions of the macOS implementation) */ 25 | "اَلْعَرَبِيَّةُ", // Short arabic string (meaning "Arabic") 26 | "中文", // Short chinese string (meaning "Chinese") 27 | "日本語", // Short japanese string (meaning "Japanese") // TODO: On my computer "日" is 28 | // not entered 29 | "aaaaaaaaaaaaaaaaaaa𝕊𝕊", // Long character string where a character starts at the 19th 30 | // byte and ends at the 20th byte 31 | "aaaaaaaaaaaaaaaaaaa❤️❤️", // Long character string where an emoji starts at the 19th byte 32 | // and ends at the 20th byte 33 | "𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊𝕊", // Long string where all 22 characters have a length of two in 34 | // the utf-16 encoding 35 | "اَلْعَرَبِيَّةُاَلْعَرَبِيَّةُاَلْعَرَبِيَّةُاَلْعَرَبِيَّةُاَلْعَرَبِيَّةُاَلْعَرَبِيَّةُ", // Long arabic string (longer than 20 36 | // chars to test the restrictions of the 37 | // macOS implementation) 38 | // TODO: This is missing the character on the very right 39 | "中文中文中文中文中文中文", // Long chinese string 40 | "日本語日本語日本語日本語日本語日本語日本語", // Long japanese string 41 | "H3llo World ❤️💯. What'𝕊 üp {}#𝄞\\日本語اَلْعَرَبِيَّةُ", /* Long string including characters 42 | * from various languages, emoji and 43 | * complex characters */ 44 | ]; 45 | 46 | for sequence in sequences { 47 | enigo.text(sequence).unwrap(); 48 | } 49 | } 50 | 51 | #[ignore] // TODO: Currently ignored because not all chars are valid CStrings 52 | #[test] 53 | // Try entering all chars with the text function. 54 | // Because it is hard to test if they succeed, 55 | // we assume it worked as long as there was no panic 56 | fn unit_text_all_utf16() { 57 | thread::sleep(super::get_delay()); 58 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 59 | for c in 0x0000..0x0010_FFFF { 60 | if let Some(character) = char::from_u32(c) { 61 | let string = character.to_string(); 62 | assert_eq!( 63 | enigo.text(&string), 64 | Ok(()), 65 | "Didn't expect an error for string: {string}" 66 | ); 67 | }; 68 | } 69 | } 70 | 71 | #[test] 72 | // Test all the keys, make sure none of them panic 73 | fn unit_key() { 74 | use strum::IntoEnumIterator; 75 | 76 | thread::sleep(super::get_delay()); 77 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 78 | for key in Key::iter() { 79 | assert_eq!( 80 | enigo.key(key, Press), 81 | Ok(()), 82 | "Didn't expect an error for key: {key:?}" 83 | ); 84 | assert_eq!( 85 | enigo.key(key, Release), 86 | Ok(()), 87 | "Didn't expect an error for key: {key:?}" 88 | ); 89 | assert_eq!( 90 | enigo.key(key, Click), 91 | Ok(()), 92 | "Didn't expect an error for key: {key:?}" 93 | ); 94 | } 95 | // Key::Raw and Key::Layout are ignored. They are tested separately 96 | } 97 | 98 | #[ignore] 99 | #[test] 100 | // Try entering all chars with Key::Layout and make sure none of them panic 101 | fn unit_key_unicode_all_utf16() { 102 | thread::sleep(super::get_delay()); 103 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 104 | for c in 0x0000..=0x0010_FFFF { 105 | if let Some(character) = char::from_u32(c) { 106 | assert_eq!( 107 | enigo.key(Key::Unicode(character), Press), 108 | Ok(()), 109 | "Didn't expect an error for character: {character}" 110 | ); 111 | assert_eq!( 112 | enigo.key(Key::Unicode(character), Release), 113 | Ok(()), 114 | "Didn't expect an error for character: {character}" 115 | ); 116 | assert_eq!( 117 | enigo.key(Key::Unicode(character), Click), 118 | Ok(()), 119 | "Didn't expect an error for character: {character}" 120 | ); 121 | }; 122 | } 123 | } 124 | 125 | #[ignore] 126 | #[test] 127 | // Try entering all possible raw keycodes with Key::Raw and make sure none of 128 | // them panic 129 | // On Windows it is expected that all keycodes > u16::MAX return an Err, because 130 | // that is their maximum value 131 | fn unit_key_other_all_keycodes() { 132 | use crate::InputError; 133 | 134 | thread::sleep(super::get_delay()); 135 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 136 | let max = if cfg!(target_os = "windows") { 137 | u16::MAX as u32 138 | } else { 139 | u32::MAX 140 | }; 141 | for raw_keycode in 0..=max { 142 | assert_eq!( 143 | enigo.key(Key::Other(raw_keycode), Press), 144 | Ok(()), 145 | "Didn't expect an error for keycode: {raw_keycode}" 146 | ); 147 | assert_eq!( 148 | enigo.key(Key::Other(raw_keycode), Release), 149 | Ok(()), 150 | "Didn't expect an error for keycode: {raw_keycode}" 151 | ); 152 | assert_eq!( 153 | enigo.key(Key::Other(raw_keycode), Click), 154 | Ok(()), 155 | "Didn't expect an error for keycode: {raw_keycode}" 156 | ); 157 | } 158 | 159 | // This will only run on Windows 160 | for raw_keycode in max..=max { 161 | assert_eq!( 162 | enigo.key(Key::Other(raw_keycode), Press), 163 | Err(InputError::InvalidInput( 164 | "virtual keycodes on Windows have to fit into u16" 165 | )), 166 | "Expected an error for keycode: {raw_keycode}" 167 | ); 168 | assert_eq!( 169 | enigo.key(Key::Other(raw_keycode), Release), 170 | Err(InputError::InvalidInput( 171 | "virtual keycodes on Windows have to fit into u16" 172 | )), 173 | "Expected an error for keycode: {raw_keycode}" 174 | ); 175 | assert_eq!( 176 | enigo.key(Key::Other(raw_keycode), Click), 177 | Err(InputError::InvalidInput( 178 | "virtual keycodes on Windows have to fit into u16" 179 | )), 180 | "Expected an error for keycode: {raw_keycode}" 181 | ); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /libs/enigo/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | /// Module containing all the tests related to the `Keyboard` trait 4 | /// that are platform independent 5 | mod keyboard; 6 | /// Module containing all the tests related to the `Mouse` trait 7 | /// that are platform independent 8 | mod mouse; 9 | 10 | // Check if the code is running in the CI 11 | fn is_ci() -> bool { 12 | matches!(std::env::var("CI").as_deref(), Ok("true")) 13 | } 14 | 15 | // Add a longer delay if it is not ran in the CI so the user can observe the 16 | // mouse moves but don't waste time in the CI 17 | fn get_delay() -> Duration { 18 | if is_ci() { 19 | Duration::from_millis(20) 20 | } else { 21 | Duration::from_secs(2) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /libs/enigo/src/tests/mouse.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Button, 3 | Direction::{Click, Press, Release}, 4 | Enigo, Mouse, Settings, 5 | {Axis::Horizontal, Axis::Vertical}, 6 | {Coordinate::Abs, Coordinate::Rel}, 7 | }; 8 | use std::thread; 9 | 10 | use super::is_ci; 11 | 12 | // TODO: Mouse acceleration on Windows will result in the wrong coordinates when 13 | // doing a relative mouse move The Github runner has the following settings: 14 | // MouseSpeed 1 15 | // MouseThreshold1 6 16 | // MouseThreshold2 10 17 | // Maybe they can be used to calculate the resulting location even with enabled 18 | // mouse acceleration 19 | fn test_mouse_move( 20 | enigo: &mut Enigo, 21 | test_cases: Vec>, 22 | coordinate: crate::Coordinate, 23 | start: (i32, i32), 24 | ) { 25 | let delay = super::get_delay(); 26 | thread::sleep(delay); 27 | 28 | let error_text = match coordinate { 29 | Abs => "Failed to move to", 30 | Rel => "Failed to relatively move to", 31 | }; 32 | 33 | enigo.move_mouse(start.0, start.1, Abs).unwrap(); // Move to absolute start position 34 | 35 | for test_case in test_cases { 36 | for mouse_action in test_case { 37 | enigo 38 | .move_mouse(mouse_action.0 .0, mouse_action.0 .1, coordinate) 39 | .unwrap(); 40 | thread::sleep(delay); 41 | let (x_res, y_res) = enigo.location().unwrap(); 42 | assert_eq!( 43 | (mouse_action.1 .0, mouse_action.1 .1), 44 | (x_res, y_res), 45 | "{} {}, {}", 46 | error_text, 47 | mouse_action.0 .0, 48 | mouse_action.0 .1 49 | ); 50 | thread::sleep(delay); 51 | } 52 | } 53 | } 54 | 55 | #[test] 56 | // Test the move_mouse function and check it with the mouse_location function 57 | fn unit_move_mouse_to() { 58 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 59 | 60 | // Make a square of 100 pixels starting at the top left corner of the screen and 61 | // moving down, right, up and left 62 | let square = vec![ 63 | ((0, 0), (0, 0)), 64 | ((0, 100), (0, 100)), 65 | ((100, 100), (100, 100)), 66 | ((100, 0), (100, 0)), 67 | ((0, 0), (0, 0)), 68 | ]; 69 | let test_cases = vec![square]; 70 | 71 | test_mouse_move(&mut enigo, test_cases, Abs, (0, 0)); 72 | } 73 | 74 | #[ignore] 75 | #[test] 76 | // Test the move_mouse function and check it with the mouse_location 77 | // function 78 | fn unit_move_mouse_rel() { 79 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 80 | 81 | // Make a square of 100 pixels starting at the top left corner of the screen and 82 | // moving down, right, up and left 83 | let square = vec![ 84 | ((0, 0), (0, 0)), 85 | ((0, 100), (0, 100)), 86 | ((100, 0), (100, 100)), 87 | ((0, -100), (100, 0)), 88 | ((-100, 0), (0, 0)), 89 | ]; 90 | let test_cases = vec![square]; 91 | 92 | test_mouse_move(&mut enigo, test_cases, Rel, (0, 0)); 93 | } 94 | 95 | #[ignore] 96 | #[test] 97 | // Test the move_mouse function and check it with the mouse_location function 98 | fn unit_move_mouse_to_boundaries() { 99 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 100 | 101 | let display_size = enigo.main_display().unwrap(); 102 | println!("Display size {} x {}", display_size.0, display_size.1); 103 | 104 | // Move the mouse outside of the boundaries of the screen 105 | let screen_boundaries = vec![ 106 | ((-3, 8), (0, 8)), // Negative x coordinate 107 | ((8, -3), (8, 0)), // Negative y coordinate 108 | ((-30, -3), (0, 0)), // Try to go to negative x and y coordinates 109 | ((567_546_546, 20), (display_size.0 - 1, 20)), // Huge x coordinate > screen width 110 | ((20, 567_546_546), (20, display_size.1 - 1)), // Huge y coordinate > screen heigth 111 | ( 112 | (567_546_546, 567_546_546), 113 | (display_size.0 - 1, display_size.1 - 1), 114 | ), /* Huge x and y coordinate > screen width 115 | * and screen 116 | * height */ 117 | ((i32::MAX, 37), (0, 37)), // Max x coordinate 118 | ((20, i32::MAX), (20, 0)), // Max y coordinate 119 | ((i32::MAX, i32::MAX), (0, 0)), // Max x and max y coordinate 120 | ((i32::MAX - 1, i32::MAX - 1), (0, 0)), // Max x and max y coordinate -1 121 | ((i32::MIN, 20), (0, 20)), // Min x coordinate 122 | ((20, i32::MIN), (20, 0)), // Min y coordinate 123 | ((i32::MIN, i32::MIN), (0, 0)), // Min x and min y coordinate 124 | ((i32::MIN, i32::MAX), (0, 0)), // Min x and max y coordinate 125 | ((i32::MAX, i32::MIN), (0, 0)), // Max x and min y coordinate 126 | ]; 127 | let test_cases = vec![screen_boundaries]; 128 | 129 | test_mouse_move(&mut enigo, test_cases, Abs, (0, 0)); 130 | } 131 | 132 | #[ignore] // TODO: Mouse acceleration on Windows will result in the wrong coordinates 133 | #[test] 134 | // Test the move_mouse function and check it with the mouse_location 135 | // function 136 | fn unit_move_mouse_rel_boundaries() { 137 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 138 | 139 | let display_size = enigo.main_display().unwrap(); 140 | println!("Display size {} x {}", display_size.0, display_size.1); 141 | 142 | // Move the mouse outside of the boundaries of the screen 143 | let screen_boundaries = vec![ 144 | ((-3, 8), (0, 8)), // Negative x coordinate 145 | ((8, -10), (8, 0)), // Negative y coordinate 146 | ((-30, -3), (0, 0)), // Try to go to negative x and y coordinates 147 | ((567_546_546, 20), (display_size.0 - 1, 20)), // Huge x coordinate > screen width 148 | ((20, 567_546_546), (display_size.0 - 1, display_size.1 - 1)), /* Huge y coordinate > 149 | * screen heigth */ 150 | ( 151 | (567_546_546, 567_546_546), 152 | (display_size.0 - 1, display_size.1 - 1), 153 | ), /* Huge x and y coordinate > screen width 154 | * and screen 155 | * height */ 156 | ((-display_size.0, -display_size.1), (0, 0)), // Reset to (0,0) 157 | ((i32::MAX, 37), (0, 37)), // Max x coordinate 158 | ((20, i32::MAX), (20, 37)), // Max y coordinate 159 | ((i32::MAX, i32::MAX), (0, 0)), // Max x and max y coordinate 160 | ((i32::MAX - 1, i32::MAX - 1), (0, 0)), // Max x and max y coordinate -1 161 | ((i32::MIN, 20), (0, 20)), // Min x coordinate 162 | ((20, i32::MIN), (20, 0)), // Min y coordinate 163 | ((i32::MIN, i32::MIN), (0, 0)), // Min x and min y coordinate 164 | ((i32::MIN, i32::MAX), (0, 0)), // Min x and max y coordinate 165 | ((i32::MAX, i32::MIN), (0, 0)), // Max x and min y coordinate 166 | ]; 167 | let test_cases = vec![screen_boundaries]; 168 | 169 | test_mouse_move(&mut enigo, test_cases, Rel, (0, 0)); 170 | } 171 | 172 | #[test] 173 | // Test the main_display function 174 | // The CI's virtual display has a dimension of 1024x768 (except on macOS where 175 | // it is 1920x1080). If the test is ran outside of the CI, we don't know the 176 | // displays dimensions so we just make sure it is greater than 0x0. 177 | fn unit_display_size() { 178 | let enigo = Enigo::new(&Settings::default()).unwrap(); 179 | let display_size = enigo.main_display().unwrap(); 180 | println!("Main display size: {}x{}", display_size.0, display_size.1); 181 | if !is_ci() { 182 | assert!(display_size.0 > 0); 183 | assert!(display_size.1 > 0); 184 | return; 185 | } 186 | 187 | let ci_display = if cfg!(target_os = "macos") { 188 | (1920, 1080) 189 | } else { 190 | (1024, 768) 191 | }; 192 | 193 | assert_eq!( 194 | (display_size.0, display_size.1), 195 | (ci_display.0, ci_display.1) 196 | ); 197 | } 198 | 199 | #[test] 200 | // Test all the mouse buttons, make sure none of them panic 201 | fn unit_button_click() { 202 | use strum::IntoEnumIterator; 203 | 204 | thread::sleep(super::get_delay()); 205 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 206 | for button in Button::iter() { 207 | assert_eq!( 208 | enigo.button(button, Press), 209 | Ok(()), 210 | "Didn't expect an error for button: {button:#?}" 211 | ); 212 | assert_eq!( 213 | enigo.button(button, Release), 214 | Ok(()), 215 | "Didn't expect an error for button: {button:#?}" 216 | ); 217 | assert_eq!( 218 | enigo.button(button, Click), 219 | Ok(()), 220 | "Didn't expect an error for button: {button:#?}" 221 | ); 222 | } 223 | } 224 | 225 | #[test] 226 | // Click each mouse button ten times, make sure none of them panic 227 | fn unit_10th_click() { 228 | use strum::IntoEnumIterator; 229 | 230 | thread::sleep(super::get_delay()); 231 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 232 | for button in Button::iter() { 233 | for _ in 0..10 { 234 | assert_eq!( 235 | enigo.button(button, Click), 236 | Ok(()), 237 | "Didn't expect an error for button: {button:#?}" 238 | ); 239 | } 240 | } 241 | } 242 | 243 | #[ignore] // Hangs with x11rb 244 | #[test] 245 | fn unit_scroll() { 246 | let delay = super::get_delay(); 247 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 248 | 249 | let test_cases = vec![0, 1, 5, 100, 57899, -57899, -0, -1, -5, -100]; 250 | 251 | for length in &test_cases { 252 | thread::sleep(delay); 253 | assert_eq!( 254 | enigo.scroll(*length, Horizontal), 255 | Ok(()), 256 | "Didn't expect an error when horizontally scrolling: {length}" 257 | ); 258 | } 259 | for length in &test_cases { 260 | thread::sleep(delay); 261 | assert_eq!( 262 | enigo.scroll(*length, Vertical), 263 | Ok(()), 264 | "Didn't expect an error when vertically scrolling: {length}" 265 | ); 266 | } 267 | } 268 | 269 | #[ignore] // Contains a relative mouse move so it does not work on Windows 270 | #[test] 271 | // Press down and drag the mouse 272 | fn unit_mouse_drag() { 273 | let delay = super::get_delay(); 274 | let mut enigo = Enigo::new(&Settings::default()).unwrap(); 275 | 276 | enigo.move_mouse(500, 200, Abs).unwrap(); 277 | enigo.button(Button::Left, Press).unwrap(); 278 | enigo.move_mouse(100, 100, Rel).unwrap(); 279 | thread::sleep(delay); 280 | enigo.button(Button::Left, Release).unwrap(); 281 | } 282 | -------------------------------------------------------------------------------- /libs/enigo/src/win/mod.rs: -------------------------------------------------------------------------------- 1 | mod win_impl; 2 | pub use win_impl::{Enigo, EXT}; 3 | -------------------------------------------------------------------------------- /libs/enigo/tests/common/browser.rs: -------------------------------------------------------------------------------- 1 | // n.b. static items do not call [`Drop`] on program termination, so this won't 2 | // be deallocated. this is fine, as the OS can deallocate the terminated program 3 | // faster than we can free memory but tools like valgrind might report "memory 4 | // leaks" as it isn't obvious this is intentional. Launch Firefox on Linux and 5 | // Windows and launch Safari on macOS 6 | pub static BROWSER_INSTANCE: std::sync::LazyLock> = 7 | std::sync::LazyLock::new(|| { 8 | // Construct the URL 9 | let url = format!( 10 | "file://{}/tests/index.html", 11 | std::env::current_dir().unwrap().to_str().unwrap() 12 | ); 13 | 14 | let child = if cfg!(target_os = "windows") { 15 | // On Windows, use cmd.exe to run the "start" command 16 | std::process::Command::new("cmd") 17 | .args(["/C", "start", "firefox", &url]) 18 | .spawn() 19 | .expect("Failed to start Firefox on Windows") 20 | } else if cfg!(target_os = "macos") { 21 | // On macOS, use the "open" command to run Safari (Firefox is not preinstalled) 22 | std::process::Command::new("open") 23 | .args(["-a", "Safari", &url]) 24 | .spawn() 25 | .expect("Failed to start Safari on macOS") 26 | } else { 27 | // On Linux, use the "firefox" command 28 | std::process::Command::new("firefox") 29 | .arg(&url) 30 | .spawn() 31 | .expect("Failed to start Firefox on Linux") 32 | }; 33 | Some(child) 34 | }); 35 | -------------------------------------------------------------------------------- /libs/enigo/tests/common/browser_events.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use tungstenite::Message; 3 | 4 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 5 | pub enum BrowserEvent { 6 | Text(String), 7 | KeyDown(String), 8 | KeyUp(String), 9 | MouseDown(u32), 10 | MouseUp(u32), 11 | MouseMove((i32, i32), (i32, i32)), // (relative, absolute) 12 | MouseScroll(i32, i32), 13 | Open, 14 | Close, 15 | } 16 | 17 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 18 | pub enum BrowserEventError { 19 | UnknownMessageType, 20 | ParseError, 21 | } 22 | 23 | impl TryFrom for BrowserEvent { 24 | type Error = BrowserEventError; 25 | 26 | fn try_from(message: Message) -> Result { 27 | match message { 28 | Message::Close(_) => { 29 | println!("Message::Close received"); 30 | Ok(BrowserEvent::Close) 31 | } 32 | Message::Text(msg) => { 33 | println!("Message::Text received"); 34 | println!("msg: {msg:?}"); 35 | 36 | // Attempt to deserialize the text message into a BrowserEvent 37 | if let Ok(event) = ron::from_str::(&msg) { 38 | Ok(event) 39 | } else { 40 | println!("Parse error"); 41 | Err(BrowserEventError::ParseError) 42 | } 43 | } 44 | _ => { 45 | println!("Other Message received"); 46 | Err(BrowserEventError::UnknownMessageType) 47 | } 48 | } 49 | } 50 | } 51 | 52 | #[test] 53 | fn deserialize_browser_events() { 54 | let messages = vec![ 55 | ( 56 | Message::Text("Text(\"Testing\")".to_string()), 57 | BrowserEvent::Text("Testing".to_string()), 58 | ), 59 | ( 60 | Message::Text("Text(\"Hi how are you?❤️ äüß$3\")".to_string()), 61 | BrowserEvent::Text("Hi how are you?❤️ äüß$3".to_string()), 62 | ), 63 | ( 64 | Message::Text("KeyDown(\"F11\")".to_string()), 65 | BrowserEvent::KeyDown("F11".to_string()), 66 | ), 67 | ( 68 | Message::Text("KeyUp(\"F11\")".to_string()), 69 | BrowserEvent::KeyUp("F11".to_string()), 70 | ), 71 | ( 72 | Message::Text("MouseDown(0)".to_string()), 73 | BrowserEvent::MouseDown(0), 74 | ), 75 | ( 76 | Message::Text("MouseUp(0)".to_string()), 77 | BrowserEvent::MouseUp(0), 78 | ), 79 | ( 80 | Message::Text("MouseMove((-1806, -487), (200, 200))".to_string()), 81 | BrowserEvent::MouseMove((-1806, -487), (200, 200)), 82 | ), 83 | ( 84 | Message::Text("MouseScroll(3, -2)".to_string()), 85 | BrowserEvent::MouseScroll(3, -2), 86 | ), 87 | ]; 88 | 89 | for (msg, event) in messages { 90 | let serialized = ron::to_string(&event).unwrap(); 91 | println!("serialized = {serialized}"); 92 | 93 | assert!(BrowserEvent::try_from(msg).unwrap() == event); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /libs/enigo/tests/common/enigo_test.rs: -------------------------------------------------------------------------------- 1 | use std::net::{TcpListener, TcpStream}; 2 | 3 | use tungstenite::accept; 4 | 5 | use enigo::{ 6 | Axis, Button, 7 | Coordinate::{self, Abs}, 8 | Direction::{self, Click, Press, Release}, 9 | Enigo, Key, Keyboard, Mouse, Settings, 10 | }; 11 | 12 | use super::browser_events::BrowserEvent; 13 | 14 | const TIMEOUT: u64 = 5; // Number of minutes the test is allowed to run before timing out 15 | // This is needed, because some of the websocket functions are blocking and 16 | // would run indefinitely without a timeout if they don't receive a message 17 | const INPUT_DELAY: u64 = 40; // Number of milliseconds to wait for the input to have an effect 18 | const SCROLL_STEP: (i32, i32) = (20, 114); // (horizontal, vertical) 19 | 20 | pub struct EnigoTest { 21 | enigo: Enigo, 22 | websocket: tungstenite::WebSocket, 23 | } 24 | 25 | impl EnigoTest { 26 | pub fn new(settings: &Settings) -> Self { 27 | env_logger::init(); 28 | EnigoTest::start_timeout_thread(); 29 | let enigo = Enigo::new(settings).unwrap(); 30 | let _ = &*super::browser::BROWSER_INSTANCE; // Launch Firefox 31 | let websocket = Self::websocket(); 32 | 33 | std::thread::sleep(std::time::Duration::from_secs(10)); // Give Firefox some time to launch 34 | Self { enigo, websocket } 35 | } 36 | 37 | // Maximize Firefox by pressing keys or moving the mouse 38 | pub fn maximize_browser(&mut self) { 39 | if cfg!(target_os = "macos") { 40 | // self.key(Key::Control, Press).unwrap(); 41 | // self.key(Key::Meta, Press).unwrap(); 42 | self.key(Key::Unicode('f'), Click).unwrap(); 43 | // self.key(Key::Meta, Release).unwrap(); 44 | // self.key(Key::Control, Release).unwrap(); 45 | } else { 46 | self.key(Key::F11, Click).unwrap(); 47 | self.move_mouse(200, 200, Abs).unwrap(); 48 | self.button(Button::Left, Click).unwrap(); 49 | }; 50 | 51 | // Wait for full screen animation 52 | std::thread::sleep(std::time::Duration::from_millis(3000)); 53 | } 54 | 55 | fn websocket() -> tungstenite::WebSocket { 56 | let listener = TcpListener::bind("127.0.0.1:26541").unwrap(); 57 | println!("TcpListener was created"); 58 | let (stream, addr) = listener.accept().expect("Unable to accept the connection"); 59 | println!("New connection was made from {addr:?}"); 60 | let websocket = accept(stream).expect("Unable to accept connections on the websocket"); 61 | println!("WebSocket was successfully created"); 62 | websocket 63 | } 64 | 65 | fn send_message(&mut self, msg: &str) { 66 | println!("Sending message: {msg}"); 67 | self.websocket 68 | .send(tungstenite::Message::Text(msg.to_string())) 69 | .expect("Unable to send the message"); 70 | println!("Sent message"); 71 | } 72 | 73 | fn read_message(&mut self) -> BrowserEvent { 74 | println!("Waiting for message on Websocket"); 75 | let message = self.websocket.read().unwrap(); 76 | println!("Processing message"); 77 | 78 | let Ok(browser_event) = BrowserEvent::try_from(message) else { 79 | panic!("Other text received"); 80 | }; 81 | assert!( 82 | !(browser_event == BrowserEvent::Close), 83 | "Received a Close event" 84 | ); 85 | browser_event 86 | } 87 | 88 | fn start_timeout_thread() { 89 | // Spawn a thread to handle the timeout 90 | std::thread::spawn(move || { 91 | std::thread::sleep(std::time::Duration::from_secs(TIMEOUT * 60)); 92 | println!("Test suite exceeded the maximum allowed time of {TIMEOUT} minutes."); 93 | std::process::exit(1); // Exit with error code 94 | }); 95 | } 96 | } 97 | 98 | impl Keyboard for EnigoTest { 99 | // This does not work for all text or the library does not work properly 100 | fn fast_text(&mut self, text: &str) -> enigo::InputResult> { 101 | self.send_message("ClearText"); 102 | let res = self.enigo.text(text); 103 | std::thread::sleep(std::time::Duration::from_millis(INPUT_DELAY)); // Wait for input to have an effect 104 | self.send_message("GetText"); 105 | 106 | loop { 107 | if let BrowserEvent::Text(received_text) = self.read_message() { 108 | println!("received text: {received_text}"); 109 | assert_eq!(text, received_text); 110 | break; 111 | } 112 | } 113 | res.map(Some) // TODO: Check if this is always correct 114 | } 115 | 116 | fn key(&mut self, key: Key, direction: Direction) -> enigo::InputResult<()> { 117 | let res = self.enigo.key(key, direction); 118 | if direction == Press || direction == Click { 119 | let ev = self.read_message(); 120 | if let BrowserEvent::KeyDown(name) = ev { 121 | println!("received pressed key: {name}"); 122 | let key_name = if let Key::Unicode(char) = key { 123 | format!("{char}") 124 | } else { 125 | format!("{key:?}").to_lowercase() 126 | }; 127 | println!("key_name: {key_name}"); 128 | assert_eq!(key_name, name.to_lowercase()); 129 | } else { 130 | panic!("BrowserEvent was not a KeyDown: {ev:?}"); 131 | } 132 | } 133 | if direction == Release || direction == Click { 134 | std::thread::sleep(std::time::Duration::from_millis(INPUT_DELAY)); // Wait for input to have an effect 135 | let ev = self.read_message(); 136 | if let BrowserEvent::KeyUp(name) = ev { 137 | println!("received released key: {name}"); 138 | let key_name = if let Key::Unicode(char) = key { 139 | format!("{char}") 140 | } else { 141 | format!("{key:?}").to_lowercase() 142 | }; 143 | println!("key_name: {key_name}"); 144 | assert_eq!(key_name, name.to_lowercase()); 145 | } else { 146 | panic!("BrowserEvent was not a KeyUp: {ev:?}"); 147 | } 148 | } 149 | println!("enigo.key() was a success"); 150 | res 151 | } 152 | 153 | fn raw(&mut self, keycode: u16, direction: enigo::Direction) -> enigo::InputResult<()> { 154 | todo!() 155 | } 156 | } 157 | 158 | impl Mouse for EnigoTest { 159 | fn button(&mut self, button: enigo::Button, direction: Direction) -> enigo::InputResult<()> { 160 | let res = self.enigo.button(button, direction); 161 | if direction == Press || direction == Click { 162 | let ev = self.read_message(); 163 | if let BrowserEvent::MouseDown(name) = ev { 164 | println!("received pressed button: {name}"); 165 | assert_eq!(button as u32, name); 166 | } else { 167 | panic!("BrowserEvent was not a MouseDown: {ev:?}"); 168 | } 169 | } 170 | if direction == Release || direction == Click { 171 | std::thread::sleep(std::time::Duration::from_millis(INPUT_DELAY)); // Wait for input to have an effect 172 | let ev = self.read_message(); 173 | if let BrowserEvent::MouseUp(name) = ev { 174 | println!("received released button: {name}"); 175 | assert_eq!(button as u32, name); 176 | } else { 177 | panic!("BrowserEvent was not a MouseUp: {ev:?}"); 178 | } 179 | } 180 | println!("enigo.button() was a success"); 181 | res 182 | } 183 | 184 | fn move_mouse(&mut self, x: i32, y: i32, coordinate: Coordinate) -> enigo::InputResult<()> { 185 | let res = self.enigo.move_mouse(x, y, coordinate); 186 | println!("Executed enigo.move_mouse"); 187 | std::thread::sleep(std::time::Duration::from_millis(INPUT_DELAY)); // Wait for input to have an effect 188 | 189 | let ev = self.read_message(); 190 | println!("Done waiting"); 191 | 192 | let mouse_position = if let BrowserEvent::MouseMove(pos_rel, pos_abs) = ev { 193 | match coordinate { 194 | Coordinate::Rel => pos_rel, 195 | Coordinate::Abs => pos_abs, 196 | } 197 | } else { 198 | panic!("BrowserEvent was not a MouseMove: {ev:?}"); 199 | }; 200 | 201 | assert_eq!(x, mouse_position.0); 202 | assert_eq!(y, mouse_position.1); 203 | println!("enigo.move_mouse() was a success"); 204 | res 205 | } 206 | 207 | fn scroll(&mut self, length: i32, axis: Axis) -> enigo::InputResult<()> { 208 | let mut length = length; 209 | let res = self.enigo.scroll(length, axis); 210 | println!("Executed Enigo"); 211 | std::thread::sleep(std::time::Duration::from_millis(INPUT_DELAY)); // Wait for input to have an effect 212 | 213 | // On some platforms it is not possible to scroll multiple lines so we 214 | // repeatedly scroll. In order for this test to work on all platforms, both 215 | // cases are not differentiated 216 | let mut mouse_scroll; 217 | let mut step; 218 | while length > 0 { 219 | let ev = self.read_message(); 220 | println!("Done waiting"); 221 | 222 | (mouse_scroll, step) = 223 | if let BrowserEvent::MouseScroll(horizontal_scroll, vertical_scroll) = ev { 224 | match axis { 225 | Axis::Horizontal => (horizontal_scroll, SCROLL_STEP.0), 226 | Axis::Vertical => (vertical_scroll, SCROLL_STEP.1), 227 | } 228 | } else { 229 | panic!("BrowserEvent was not a MouseScroll: {ev:?}"); 230 | }; 231 | length -= mouse_scroll / step; 232 | } 233 | 234 | println!("enigo.scroll() was a success"); 235 | res 236 | } 237 | 238 | fn main_display(&self) -> enigo::InputResult<(i32, i32)> { 239 | let res = self.enigo.main_display(); 240 | match res { 241 | Ok((x, y)) => { 242 | let (rdev_x, rdev_y) = rdev_main_display(); 243 | println!("enigo display: {x},{y}"); 244 | println!("rdev_display: {rdev_x},{rdev_y}"); 245 | assert_eq!(x, rdev_x); 246 | assert_eq!(y, rdev_y); 247 | } 248 | Err(_) => todo!(), 249 | } 250 | res 251 | } 252 | 253 | // Edge cases don't work (mouse is at the left most border and can't move one to 254 | // the left) 255 | fn location(&self) -> enigo::InputResult<(i32, i32)> { 256 | let res = self.enigo.location(); 257 | match res { 258 | Ok((x, y)) => { 259 | let (mouse_x, mouse_y) = mouse_position(); 260 | println!("enigo_position: {x},{y}"); 261 | println!("mouse_position: {mouse_x},{mouse_y}"); 262 | assert_eq!(x, mouse_x); 263 | assert_eq!(y, mouse_y); 264 | } 265 | Err(_) => todo!(), 266 | } 267 | res 268 | } 269 | } 270 | 271 | fn rdev_main_display() -> (i32, i32) { 272 | use rdev::display_size; 273 | let (x, y) = display_size().unwrap(); 274 | (x.try_into().unwrap(), y.try_into().unwrap()) 275 | } 276 | 277 | fn mouse_position() -> (i32, i32) { 278 | use mouse_position::mouse_position::Mouse; 279 | 280 | if let Mouse::Position { x, y } = Mouse::get_mouse_position() { 281 | (x, y) 282 | } else { 283 | panic!("the crate mouse_location was unable to get the position of the mouse"); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /libs/enigo/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | mod browser; 2 | mod browser_events; 3 | pub mod enigo_test; 4 | -------------------------------------------------------------------------------- /libs/enigo/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Enigo Universal Test 9 | 10 | 11 | 12 |

Conducted tests

13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 |
32 | 33 |

Test did not start. Do you have JavaScript enabled?

34 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /libs/enigo/tests/integration_browser.rs: -------------------------------------------------------------------------------- 1 | use enigo::{ 2 | Direction::{Click, Press, Release}, 3 | Key, Keyboard, Settings, 4 | }; 5 | 6 | mod common; 7 | use common::enigo_test::EnigoTest as Enigo; 8 | 9 | #[test] 10 | fn integration_browser_events() { 11 | let mut enigo = Enigo::new(&Settings::default()); 12 | 13 | enigo.maximize_browser(); 14 | 15 | enigo.text("TestText❤️").unwrap(); // Fails on Windows (Message is empty???) 16 | enigo.key(Key::F1, Click).unwrap(); 17 | enigo.key(Key::Control, Click).unwrap(); 18 | enigo.key(Key::Backspace, Click).unwrap(); 19 | enigo.key(Key::PageUp, Click).unwrap(); // Failing on Windows 20 | 21 | enigo.key(Key::Backspace, Press).unwrap(); 22 | enigo.key(Key::Backspace, Release).unwrap(); 23 | 24 | println!("Test mouse"); /* 25 | enigo.button(Button::Left, Click).unwrap(); 26 | enigo.move_mouse(100, 100, Abs).unwrap(); 27 | enigo.move_mouse(200, 200, Abs).unwrap(); 28 | // let (x, y) = enigo.location().unwrap(); 29 | // assert_eq!((200, 200), (x, y)); 30 | // Relative moves fail on Windows 31 | // For some reason the values are wrong 32 | // enigo.move_mouse(20, 20, Rel).unwrap(); 33 | // enigo.move_mouse(-20, 20, Rel).unwrap(); 34 | // enigo.move_mouse(20, -20, Rel).unwrap(); 35 | // enigo.move_mouse(-20, -20, Rel).unwrap(); 36 | // enigo.scroll(1, Vertical).unwrap(); 37 | // enigo.scroll(1, Horizontal).unwrap(); Fails on Windows 38 | enigo.main_display().unwrap(); 39 | enigo.location().unwrap(); */ 40 | } 41 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build and Run 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | # At 23:25 on Thursday. 8 | - cron: "25 23 * * 4" 9 | 10 | jobs: 11 | windows-vpx1_8_2: 12 | strategy: 13 | matrix: 14 | runs-on: [windows-2019, windows-2022] 15 | runs-on: ${{ matrix.runs-on }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: download and extract libvpx 19 | run: | 20 | curl -sSfL -O https://github.com/ShiftMediaProject/libvpx/releases/download/v1.8.2/libvpx_v1.8.2_msvc16.zip 21 | unzip libvpx_v1.8.2_msvc16.zip -d $HOME\unzipped 22 | dir 23 | dir $HOME\unzipped 24 | dir $HOME\unzipped\lib\x64 25 | - name: Run test 26 | run: | 27 | cd vpx-sys-test 28 | $Env:VPX_STATIC = "1" 29 | $Env:VPX_VERSION = "1.8.2" 30 | $Env:VPX_LIB_DIR = "$HOME\unzipped\lib\x64" 31 | $Env:VPX_INCLUDE_DIR = "$HOME\unzipped\include" 32 | cargo run 33 | 34 | windows-vpx1_9_0: 35 | strategy: 36 | matrix: 37 | runs-on: [windows-2019, windows-2022] 38 | runs-on: ${{ matrix.runs-on }} 39 | steps: 40 | - uses: actions/checkout@v3 41 | - name: download and extract libvpx 42 | run: | 43 | curl -sSfL -O https://github.com/ShiftMediaProject/libvpx/releases/download/v1.9.0/libvpx_v1.9.0_msvc16.zip 44 | unzip libvpx_v1.9.0_msvc16.zip -d $HOME\unzipped 45 | dir 46 | dir $HOME\unzipped 47 | dir $HOME\unzipped\lib\x64 48 | - name: Run test 49 | run: | 50 | cd vpx-sys-test 51 | $Env:VPX_STATIC = "1" 52 | $Env:VPX_VERSION = "1.9.0" 53 | $Env:VPX_LIB_DIR = "$HOME\unzipped\lib\x64" 54 | $Env:VPX_INCLUDE_DIR = "$HOME\unzipped\include" 55 | cargo run 56 | 57 | windows-vpx1_10_0: 58 | strategy: 59 | matrix: 60 | runs-on: [windows-2019, windows-2022] 61 | runs-on: ${{ matrix.runs-on }} 62 | steps: 63 | - uses: actions/checkout@v3 64 | - name: download and extract libvpx 65 | run: | 66 | curl -sSfL -O https://github.com/ShiftMediaProject/libvpx/releases/download/v1.10.0/libvpx_v1.10.0_msvc16.zip 67 | unzip libvpx_v1.10.0_msvc16.zip -d $HOME\unzipped 68 | dir 69 | dir $HOME\unzipped 70 | dir $HOME\unzipped\lib\x64 71 | - name: Run test 72 | run: | 73 | cd vpx-sys-test 74 | $Env:VPX_STATIC = "1" 75 | $Env:VPX_VERSION = "1.10.0" 76 | $Env:VPX_LIB_DIR = "$HOME\unzipped\lib\x64" 77 | $Env:VPX_INCLUDE_DIR = "$HOME\unzipped\include" 78 | cargo run 79 | 80 | windows-vpx1_13_0: 81 | strategy: 82 | matrix: 83 | runs-on: [windows-2019, windows-2022] 84 | runs-on: ${{ matrix.runs-on }} 85 | steps: 86 | - uses: actions/checkout@v3 87 | - name: download and extract libvpx 88 | run: | 89 | curl -sSfL -O https://github.com/ShiftMediaProject/libvpx/releases/download/v1.13.0/libvpx_v1.13.0_msvc16.zip 90 | unzip libvpx_v1.13.0_msvc16.zip -d $HOME\unzipped 91 | dir 92 | dir $HOME\unzipped 93 | dir $HOME\unzipped\lib\x64 94 | - name: Run test 95 | run: | 96 | cd vpx-sys-test 97 | $Env:VPX_STATIC = "1" 98 | $Env:VPX_VERSION = "1.13.0" 99 | $Env:VPX_LIB_DIR = "$HOME\unzipped\lib\x64" 100 | $Env:VPX_INCLUDE_DIR = "$HOME\unzipped\include" 101 | cargo run 102 | 103 | macos-latest-vpx1_8_1: 104 | runs-on: macos-latest 105 | steps: 106 | - uses: actions/checkout@v3 107 | - name: download and extract libvpx 108 | run: | 109 | cd /tmp 110 | curl --silent -O -L https://strawlab-cdn.com/assets/libvpx-1.8.1.sierra.bottle.tar.gz 111 | sudo mkdir -p /opt 112 | sudo tar xvzf /tmp/libvpx-1.8.1.sierra.bottle.tar.gz -C /opt 113 | - name: install rustup 114 | run: | 115 | curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal 116 | - name: Run test 117 | run: | 118 | cd vpx-sys-test 119 | export VPX_STATIC=1 120 | export VPX_VERSION="1.8.1" 121 | export VPX_LIB_DIR="/opt/libvpx/1.8.1/lib" 122 | export VPX_INCLUDE_DIR="/opt/libvpx/1.8.1/include" 123 | export PATH="$PATH:$HOME/.cargo/bin" 124 | cargo run 125 | 126 | macos-latest-homebrew-vpx-generate: 127 | runs-on: macos-latest 128 | steps: 129 | - uses: actions/checkout@v3 130 | - name: install libvpx using homebrew 131 | run: | 132 | brew install libvpx 133 | - name: install rustup 134 | run: | 135 | curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal 136 | - name: Run test 137 | run: | 138 | cd vpx-sys-test 139 | cargo run --features env-libvpx-sys/generate 140 | 141 | ubuntu-20_04-vpx1_8: 142 | runs-on: ubuntu-20.04 143 | steps: 144 | - uses: actions/checkout@v3 145 | - name: download and extract libvpx 146 | run: | 147 | sudo apt-get update -y 148 | sudo apt-get install -y libvpx-dev 149 | - name: Run test 150 | run: | 151 | cd vpx-sys-test 152 | cargo run 153 | 154 | ubuntu-20_04-vpx1_8-generate: 155 | runs-on: ubuntu-20.04 156 | steps: 157 | - uses: actions/checkout@v3 158 | - name: download and extract libvpx 159 | run: | 160 | sudo apt-get update -y 161 | sudo apt-get install -y libvpx-dev 162 | - name: Run test 163 | run: | 164 | rm -rf generated 165 | cd vpx-sys-test 166 | cargo run --features env-libvpx-sys/generate 167 | 168 | ubuntu-22_04-vpx1_11: 169 | runs-on: ubuntu-22.04 170 | steps: 171 | - uses: actions/checkout@v3 172 | - name: download and extract libvpx 173 | run: | 174 | sudo apt-get update -y 175 | sudo apt-get install -y libvpx-dev 176 | - name: Run test 177 | run: | 178 | cd vpx-sys-test 179 | cargo run 180 | 181 | ubuntu-22_04-vpx-generate: 182 | runs-on: ubuntu-22.04 183 | steps: 184 | - uses: actions/checkout@v3 185 | - name: download and extract libvpx 186 | run: | 187 | sudo apt-get update -y 188 | sudo apt-get install -y libvpx-dev 189 | - name: Run test 190 | run: | 191 | rm -rf generated 192 | cd vpx-sys-test 193 | cargo run --features env-libvpx-sys/generate 194 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 5.1.3 - 2023-09-10 2 | 3 | ## Added 4 | 5 | * Support for libvpx 1.13 6 | 7 | # 5.1.1 - 2022-05-30 8 | 9 | ## Added 10 | 11 | * Support for libvpx 1.11 12 | 13 | # 5.1.0 - 2021-06-07 14 | 15 | ## Changed 16 | 17 | * Simplified logic in `build.rs`. 18 | 19 | ## Added 20 | 21 | * Support for libvpx 1.10 22 | 23 | # 5.0.0 - 2020-10-23 24 | 25 | ## Changed 26 | 27 | * [breaking] Remove implementations of `Default` for `vpx_codec_enc_cfg`, 28 | `vpx_codec_ctx`, `vpx_image_t`. The old code zero-initialized these, which is 29 | not valid and actually undefined behavior. This nevertheless worked for older 30 | compilers, but triggers a panic with newer versions of rust. The correct 31 | technique is to use `mem::MaybeUninit`. 32 | 33 | ## Added 34 | 35 | * Support for libvpx 1.9 36 | 37 | # 4.0.13 - 2020-03-27 38 | 39 | ## Changed 40 | 41 | * Update github actions to perform better CI testing on Windows, Linux, Mac 42 | * Recompile if environment variables change. 43 | 44 | # 4.0.12 - 2019-12-02 45 | 46 | ## Added 47 | 48 | * Use github actions to perform CI testing on Windows, Linux, Mac 49 | 50 | ## Changed 51 | 52 | * The name of the windows static library is changed from `vpxmt` to `libvpx`. 53 | The new name is the name used by [Shift Media 54 | Project](https://github.com/ShiftMediaProject/libvpx) in their Windows builds. 55 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "env-libvpx-sys" 3 | authors = ["Richard Diamond ", "Kornel Lesiński ", "Andrew Straw "] 4 | description = "Rust bindings to libvpx" 5 | license = "MPL-2.0" 6 | links = "vpx" 7 | repository = "https://github.com/astraw/env-libvpx-sys" 8 | version = "5.1.3" 9 | edition = "2018" 10 | keywords = ["vp8", "vp9", "webm", "bindings", "video"] 11 | categories = ["external-ffi-bindings", "multimedia::images", "multimedia::encoding", "multimedia::video"] 12 | 13 | [build-dependencies] 14 | pkg-config = "0.3.5" 15 | 16 | [build-dependencies.bindgen] 17 | optional = true 18 | version = "0.69.0" 19 | 20 | [features] 21 | default = [] 22 | generate = ["bindgen"] 23 | 24 | [lib] 25 | name = "vpx_sys" 26 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/README.md: -------------------------------------------------------------------------------- 1 | # env-libvpx-sys 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/env-libvpx-sys.svg)](https://crates.io/crates/env-libvpx-sys) 4 | ![build](https://github.com/astraw/env-libvpx-sys/workflows/Build%20and%20Run/badge.svg) 5 | [![Documentation](https://docs.rs/env-libvpx-sys/badge.svg)](https://docs.rs/env-libvpx-sys/) 6 | 7 | ⚠⚠ This repository is now archived. I no longer use it and cannot maintain it. 8 | Please fork it and use it yourself. If your are interested in taking ownership 9 | of the project please contact me ([@astraw](https://github.com/astraw)). ⚠⚠ 10 | 11 | Rust bindings to libvpx. 12 | 13 | ## Features and characteristics 14 | 15 | The `env-libvpx-sys` crate offers the following: 16 | 17 | * It provides only the `-sys` layer. VPX header files are wrapped with bindgen 18 | and the native library is linked. However, no higher-level Rust interface is 19 | provided. (See the [vpx-encode crate](https://crates.io/crates/vpx-encode) for 20 | a simple higher-level interface). 21 | * It adds [Continuous Integration tests for Windows, Linux and 22 | Mac](https://github.com/astraw/env-libvpx-sys/actions). 23 | * It includes bundled bindgen-generated FFI wrapper for a few versions of 24 | libvpx. You can also enable `generate` feature of this crate to generate FFI 25 | on the fly for a custom version of libvpx. 26 | * It originally started as a fork of 27 | [libvpx-native-sys](https://crates.io/crates/libvpx-native-sys) (see 28 | [history](#History) below). 29 | 30 | ## How libvpx version is selected 31 | 32 | At compilation time, `build.rs` determines how to link libvpx, including what 33 | version to use. 34 | 35 | ### Option 1: let `pkg-config` find it 36 | 37 | This scenario is the default and is used when the environment variable 38 | `VPX_LIB_DIR` is not set. In this case, 39 | [`pkg-config`](https://crates.io/crates/pkg-config) will attempt to 40 | automatically discover libvpx. 41 | 42 | If `VPX_VERSION` is set, `build.rs` will ensure that `pkg-config` returns the 43 | same version. If `VPX_VERSION` is not set, the version returned by `pkg-config` 44 | will be used. 45 | 46 | Note that `pkg-config` will check the `VPX_STATIC` environment variable, and if 47 | it is set, will attempt static linking. 48 | 49 | ### Option 2: specify libvpx location manually 50 | 51 | In this scenario, set the following environment variables: `VPX_LIB_DIR`, 52 | `VPX_INCLUDE_DIR`, and `VPX_VERSION` appropriately. Caution: if `VPX_VERSION` 53 | does not match the linked library 54 | 55 | Additionally, `VPX_STATIC` may be set to `1` to force static linking. 56 | 57 | ### Discussion about theoretical alternative of using cargo features to specify library version 58 | 59 | At one point, cargo features were considered as a means to select the library 60 | version used. However, this meant 61 | the final application binary would need to specify the library version used. 62 | This would place a requirement on all crates in the dependency chain from the 63 | final application binary to `env-libvpx-sys` that they must explicitly depend on 64 | `env-libvpx-sys` (even if, as is very likely beyond a vpx wrapper crate such as 65 | [`vpx-encode`](https://crates.io/crates/vpx-encode)) the intermediate or final 66 | crate does not directly call into `env-libvpx-sys`. 67 | 68 | As an additional problem, because cargo features are additive, the possibility 69 | for conflicting build requests with two sets of features would be possible in 70 | this scenario. The present alternative, namely setting the environment variable 71 | `VPX_VERSION`, naturally enforces the selection of only a single version. 72 | 73 | ## (Re)generating the bindings with bindgen 74 | 75 | If the bindings for your version are not pre-generated in the `generated/` 76 | directory, you may let [`bindgen`](https://crates.io/crates/bindgen) 77 | automatically generate them during the build process by using the `generate` 78 | cargo feature. 79 | 80 | To save your (re)generated bindings and commit them to this repository, build 81 | using with the `generate` cargo feature. The easiest way to do this is to use 82 | the script `regen-ffi.sh` (or `regen-ffi.bat` on Windows). Then, copy the 83 | generated file in `target/debug/build/env-libvpx-sys-/out/ffi.rs` to 84 | `generated/vpx-ffi-.rs`. Finally, add this file to version control. 85 | 86 | ## History and thanks 87 | 88 | This began as a fork of 89 | [libvpx-native-sys](https://crates.io/crates/libvpx-native-sys) with a [fix to 90 | simplify working with Windows](https://github.com/kornelski/rust-vpx/pull/1). 91 | Thanks to those authors! 92 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::Path; 3 | 4 | pub fn main() { 5 | println!("cargo:rerun-if-env-changed=VPX_VERSION"); 6 | println!("cargo:rerun-if-env-changed=VPX_LIB_DIR"); 7 | println!("cargo:rerun-if-env-changed=VPX_INCLUDE_DIR"); 8 | println!("cargo:rerun-if-env-changed=VPX_STATIC"); 9 | println!("cargo:rerun-if-env-changed=VPX_DYNAMIC"); 10 | 11 | let requested_version = env::var("VPX_VERSION").ok(); 12 | 13 | let src_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap(); 14 | let src_dir = Path::new(&src_dir); 15 | 16 | let ffi_header = src_dir.join("ffi.h"); 17 | let ffi_rs = { 18 | let out_dir = env::var_os("OUT_DIR").unwrap(); 19 | let out_dir = Path::new(&out_dir); 20 | out_dir.join("ffi.rs") 21 | }; 22 | 23 | #[allow(unused_variables)] 24 | let (found_version, include_paths) = match env::var_os("VPX_LIB_DIR") { 25 | None => { 26 | // use VPX config from pkg-config 27 | let lib = pkg_config::probe_library("vpx").unwrap(); 28 | 29 | if let Some(v) = requested_version { 30 | if lib.version != v { 31 | panic!( 32 | "version mismatch. pkg-config returns version {}, but VPX_VERSION \ 33 | environment variable is {}.", 34 | lib.version, v 35 | ); 36 | } 37 | } 38 | (lib.version, lib.include_paths) 39 | } 40 | Some(vpx_libdir) => { 41 | // use VPX config from environment variable 42 | let libdir = std::path::Path::new(&vpx_libdir); 43 | 44 | // Set lib search path. 45 | println!("cargo:rustc-link-search=native={}", libdir.display()); 46 | 47 | // Get static using pkg-config-rs rules. 48 | let statik = infer_static("VPX"); 49 | 50 | // Set libname. 51 | if statik { 52 | #[cfg(target_os = "windows")] 53 | println!("cargo:rustc-link-lib=static=libvpx"); 54 | #[cfg(not(target_os = "windows"))] 55 | println!("cargo:rustc-link-lib=static=vpx"); 56 | } else { 57 | println!("cargo:rustc-link-lib=vpx"); 58 | } 59 | 60 | let mut include_paths = vec![]; 61 | if let Some(include_dir) = env::var_os("VPX_INCLUDE_DIR") { 62 | include_paths.push(include_dir.into()); 63 | } 64 | let version = requested_version.unwrap_or_else(|| { 65 | panic!("If VPX_LIB_DIR is set, VPX_VERSION must also be defined.") 66 | }); 67 | (version, include_paths) 68 | } 69 | }; 70 | 71 | println!("rerun-if-changed={}", ffi_header.display()); 72 | for dir in &include_paths { 73 | println!("rerun-if-changed={}", dir.display()); 74 | } 75 | 76 | #[cfg(feature = "generate")] 77 | generate_bindings(&ffi_header, &include_paths, &ffi_rs); 78 | 79 | #[cfg(not(feature = "generate"))] 80 | { 81 | let src = format!("vpx-ffi-{}.rs", found_version); 82 | let full_src = std::path::PathBuf::from("generated").join(src); 83 | if !full_src.exists() { 84 | panic!( 85 | "Expected file \"{}\" not found but 'generate' cargo feature not used.", 86 | full_src.display() 87 | ); 88 | } 89 | std::fs::copy(&full_src, &ffi_rs).unwrap(); 90 | } 91 | } 92 | 93 | // This function was modified from pkg-config-rs and should have same behavior. 94 | fn infer_static(name: &str) -> bool { 95 | if env::var_os(&format!("{}_STATIC", name)).is_some() { 96 | true 97 | } else if env::var_os(&format!("{}_DYNAMIC", name)).is_some() { 98 | false 99 | } else if env::var_os("PKG_CONFIG_ALL_STATIC").is_some() { 100 | true 101 | } else if env::var_os("PKG_CONFIG_ALL_DYNAMIC").is_some() { 102 | false 103 | } else { 104 | false 105 | } 106 | } 107 | 108 | #[cfg(feature = "generate")] 109 | fn generate_bindings(ffi_header: &Path, include_paths: &[std::path::PathBuf], ffi_rs: &Path) { 110 | let mut b = bindgen::builder() 111 | .header(ffi_header.to_str().unwrap()) 112 | .allowlist_type("^[vV].*") 113 | .allowlist_var("^[vV].*") 114 | .allowlist_function("^[vV].*") 115 | .rustified_enum("^v.*") 116 | .trust_clang_mangling(false) 117 | .layout_tests(false) // breaks 32/64-bit compat 118 | .generate_comments(false); // vpx comments have prefix /*!\ 119 | 120 | for dir in include_paths { 121 | b = b.clang_arg(format!("-I{}", dir.display())); 122 | } 123 | 124 | b.generate().unwrap().write_to_file(ffi_rs).unwrap(); 125 | } 126 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/ffi.h: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/regen-ffi.bat: -------------------------------------------------------------------------------- 1 | @REM Download https://github.com/ShiftMediaProject/libvpx/releases/download/v1.10.0/libvpx_v1.10.0_msvc16.zip 2 | @REM and unzip into %HomeDrive%%HomePath%\libvpx_v1.10.0_msvc16 3 | set VPX_STATIC=1 4 | set VPX_VERSION=1.10.0 5 | set VPX_LIB_DIR=%HomeDrive%%HomePath%\libvpx_v1.10.0_msvc16\lib\x64 6 | set VPX_INCLUDE_DIR=%HomeDrive%%HomePath%\libvpx_v1.10.0_msvc16\include 7 | 8 | @REM Download llvm from https://releases.llvm.org/download.html 9 | set LIBCLANG_PATH=C:\Program Files\LLVM\bin 10 | cargo build --no-default-features --features=generate 11 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/regen-ffi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -o errexit 3 | 4 | touch build.rs 5 | cargo build --no-default-features --features=generate 6 | echo "Generated '/ffi.rs'. You may want to copy this into 'generated/' with an appropriate name." 7 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | #![allow(non_snake_case)] 3 | #![allow(non_upper_case_globals)] 4 | #![allow(improper_ctypes)] 5 | 6 | // VP9 7 | #[repr(i32)] 8 | pub enum AQ_MODE { 9 | NO_AQ = 0, 10 | VARIANCE_AQ = 1, 11 | COMPLEXITY_AQ = 2, 12 | CYCLIC_REFRESH_AQ = 3, 13 | EQUATOR360_AQ = 4, 14 | // AQ based on lookahead temporal 15 | // variance (only valid for altref frames) 16 | LOOKAHEAD_AQ = 5, 17 | } 18 | 19 | // Back compat 20 | pub use vpx_codec_err_t::*; 21 | 22 | include!(concat!(env!("OUT_DIR"), "/ffi.rs")); 23 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/vpx-sys-test/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/vpx-sys-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vpx-sys-test" 3 | version = "0.1.0" 4 | authors = ["Andrew Straw "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | env-libvpx-sys = {path=".."} 9 | -------------------------------------------------------------------------------- /libs/env-libvpx-sys/vpx-sys-test/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let version = unsafe {vpx_sys::vpx_codec_version()}; 3 | println!("VPX version: 0x{:x}", version); 4 | } 5 | -------------------------------------------------------------------------------- /libs/vpx-codec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vpx-codec" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | env-libvpx-sys = { path = "../env-libvpx-sys" } 10 | thiserror = "1" 11 | 12 | [features] 13 | vp9 = [] 14 | backtrace = [] 15 | -------------------------------------------------------------------------------- /libs/vpx-codec/src/decoder.rs: -------------------------------------------------------------------------------- 1 | //! Rust interface to libvpx encoder 2 | //! 3 | //! This crate provides a Rust API to use 4 | //! [libvpx](https://en.wikipedia.org/wiki/Libvpx) for encoding images. 5 | //! 6 | //! It it based entirely on code from [srs](https://crates.io/crates/srs). 7 | //! Compared to the original `srs`, this code has been simplified for use as a 8 | //! library and updated to add support for both the VP8 codec and (optionally) 9 | //! the VP9 codec. 10 | //! 11 | //! # Optional features 12 | //! 13 | //! Compile with the cargo feature `vp9` to enable support for the VP9 codec. 14 | //! 15 | //! # Example 16 | //! 17 | //! An example of using `vpx-encode` can be found in the [`record-screen`]() 18 | //! program. The source code for `record-screen` is in the [vpx-encode git 19 | //! repository](). 20 | //! 21 | //! # Contributing 22 | //! 23 | //! All contributions are appreciated. 24 | 25 | // vpx_sys is provided by the `env-libvpx-sys` crate 26 | 27 | #![cfg_attr( 28 | feature = "backtrace", 29 | feature(error_generic_member_access, provide_any) 30 | )] 31 | 32 | use std::{ 33 | mem::MaybeUninit, 34 | os::raw::{c_int, c_uint}, 35 | }; 36 | 37 | #[cfg(feature = "backtrace")] 38 | use std::backtrace::Backtrace; 39 | use std::{ptr, slice}; 40 | 41 | use thiserror::Error; 42 | 43 | #[allow(unused_imports)] 44 | #[cfg(feature = "vp9")] 45 | use vpx_sys::vp8e_enc_control_id::*; 46 | use vpx_sys::*; 47 | 48 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 49 | pub enum VideoCodecId { 50 | VP8, 51 | #[cfg(feature = "vp9")] 52 | VP9, 53 | } 54 | 55 | impl Default for VideoCodecId { 56 | #[cfg(not(feature = "vp9"))] 57 | fn default() -> VideoCodecId { 58 | VideoCodecId::VP8 59 | } 60 | 61 | #[cfg(feature = "vp9")] 62 | fn default() -> VideoCodecId { 63 | VideoCodecId::VP9 64 | } 65 | } 66 | 67 | pub struct Decoder { 68 | ctx: vpx_codec_ctx_t, 69 | } 70 | 71 | #[derive(Debug, Error)] 72 | #[error("VPX encode error: {msg}")] 73 | pub struct Error { 74 | msg: String, 75 | #[cfg(feature = "backtrace")] 76 | #[backtrace] 77 | backtrace: Backtrace, 78 | } 79 | 80 | impl From for Error { 81 | fn from(msg: String) -> Self { 82 | Self { 83 | msg, 84 | #[cfg(feature = "backtrace")] 85 | backtrace: Backtrace::capture(), 86 | } 87 | } 88 | } 89 | 90 | pub type Result = std::result::Result; 91 | 92 | macro_rules! call_vpx { 93 | ($x:expr) => {{ 94 | let result = unsafe { $x }; // original expression 95 | let result_int = unsafe { std::mem::transmute::<_, i32>(result) }; 96 | // if result != VPX_CODEC_OK { 97 | if result_int != 0 { 98 | return Err(Error::from(format!( 99 | "Function call failed (error code {}).", 100 | result_int 101 | ))); 102 | } 103 | result 104 | }}; 105 | } 106 | 107 | macro_rules! call_vpx_ptr { 108 | ($x:expr) => {{ 109 | let result = unsafe { $x }; // original expression 110 | if result.is_null() { 111 | return Err(Error::from("Bad pointer.".to_string())); 112 | } 113 | result 114 | }}; 115 | } 116 | 117 | impl Decoder { 118 | pub fn new(config: Config) -> Result { 119 | let i = match config.codec { 120 | VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()), 121 | #[cfg(feature = "vp9")] 122 | VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()), 123 | }; 124 | 125 | if config.width % 2 != 0 { 126 | return Err(Error::from("Width must be divisible by 2".to_string())); 127 | } 128 | if config.height % 2 != 0 { 129 | return Err(Error::from("Height must be divisible by 2".to_string())); 130 | } 131 | 132 | let cfg = vpx_codec_dec_cfg_t { 133 | threads: 1, 134 | w: 0, 135 | h: 0, 136 | }; 137 | 138 | let ctx = MaybeUninit::zeroed(); 139 | let mut ctx = unsafe { ctx.assume_init() }; 140 | 141 | match config.codec { 142 | VideoCodecId::VP8 => { 143 | call_vpx!(vpx_codec_dec_init_ver( 144 | &mut ctx, 145 | i, 146 | &cfg, 147 | 0, 148 | vpx_sys::VPX_DECODER_ABI_VERSION as i32 149 | )); 150 | } 151 | #[cfg(feature = "vp9")] 152 | VideoCodecId::VP9 => { 153 | call_vpx!(vpx_codec_dec_init_ver( 154 | &mut ctx, 155 | i, 156 | &cfg, 157 | 0, 158 | vpx_sys::VPX_DECODER_ABI_VERSION as i32 159 | )); 160 | } 161 | }; 162 | 163 | Ok(Self { 164 | ctx, 165 | }) 166 | } 167 | 168 | pub fn decode(&mut self, data: &[u8]) -> Result { 169 | call_vpx!(vpx_codec_decode( 170 | &mut self.ctx, 171 | data.as_ptr(), 172 | data.len() as _, 173 | ptr::null_mut(), 174 | 0, 175 | )); 176 | 177 | Ok(Packets { 178 | ctx: &mut self.ctx, 179 | iter: ptr::null(), 180 | }) 181 | } 182 | 183 | pub fn finish(mut self) -> Result { 184 | call_vpx!(vpx_codec_decode( 185 | &mut self.ctx, 186 | ptr::null(), 187 | 0, // PTS 188 | ptr::null_mut(), 189 | 0, // Flags 190 | )); 191 | 192 | Ok(Finish { 193 | enc: self, 194 | iter: ptr::null(), 195 | }) 196 | } 197 | } 198 | 199 | impl Drop for Decoder { 200 | fn drop(&mut self) { 201 | unsafe { 202 | let result = vpx_codec_destroy(&mut self.ctx); 203 | if result != vpx_sys::VPX_CODEC_OK { 204 | eprintln!("failed to destroy vpx codec: {result:?}"); 205 | } 206 | } 207 | } 208 | } 209 | 210 | #[derive(Clone, Copy, Debug)] 211 | pub struct Frame<'a> { 212 | /// Compressed data. 213 | pub data: &'a [u8], 214 | /// Whether the frame is a keyframe. 215 | pub key: bool, 216 | /// Presentation timestamp (in timebase units). 217 | pub pts: i64, 218 | } 219 | 220 | #[derive(Clone, Copy, Debug)] 221 | pub struct Config { 222 | /// The width (in pixels). 223 | pub width: c_uint, 224 | /// The height (in pixels). 225 | pub height: c_uint, 226 | /// The timebase numerator and denominator (in seconds). 227 | pub timebase: [c_int; 2], 228 | /// The target bitrate (in kilobits per second). 229 | pub bitrate: c_uint, 230 | /// The codec 231 | pub codec: VideoCodecId, 232 | } 233 | 234 | pub struct Image(*mut vpx_image_t); 235 | impl Image { 236 | #[inline] 237 | pub fn new() -> Self { 238 | Self(std::ptr::null_mut()) 239 | } 240 | 241 | #[inline] 242 | pub fn is_null(&self) -> bool { 243 | self.0.is_null() 244 | } 245 | 246 | #[inline] 247 | pub fn format(&self) -> vpx_img_fmt_t { 248 | // VPX_IMG_FMT_I420 249 | self.inner().fmt 250 | } 251 | 252 | #[inline] 253 | pub fn inner(&self) -> &vpx_image_t { 254 | unsafe { &*self.0 } 255 | } 256 | 257 | #[inline] 258 | pub fn width(&self) -> usize { 259 | self.stride()[0] as usize 260 | // self.inner().d_w as _ 261 | } 262 | 263 | #[inline] 264 | pub fn height(&self) -> usize { 265 | self.inner().d_h as _ 266 | } 267 | 268 | #[inline] 269 | pub fn stride(&self) -> Vec { 270 | self.inner().stride.iter().map(|x| *x as i32).collect() 271 | } 272 | 273 | #[inline] 274 | fn planes(&self) -> Vec<*mut u8> { 275 | self.inner().planes.iter().map(|p| *p as *mut u8).collect() 276 | } 277 | 278 | #[inline] 279 | pub fn data(&self) -> (&[u8], &[u8], &[u8]) { 280 | unsafe { 281 | let stride = self.stride(); 282 | let planes = self.planes(); 283 | let h = (self.height() as usize + 1) & !1; 284 | let n = stride[0] as usize * h; 285 | let y = slice::from_raw_parts(planes[0], n); 286 | let n = stride[1] as usize * (h >> 1); 287 | let u = slice::from_raw_parts(planes[1], n); 288 | let v = slice::from_raw_parts(planes[2], n); 289 | (y, u, v) 290 | } 291 | } 292 | } 293 | 294 | impl Drop for Image { 295 | fn drop(&mut self) { 296 | if !self.0.is_null() { 297 | unsafe { vpx_img_free(self.0) }; 298 | } 299 | } 300 | } 301 | 302 | pub struct Packets<'a> { 303 | ctx: &'a mut vpx_codec_ctx_t, 304 | iter: vpx_codec_iter_t, 305 | } 306 | 307 | impl<'a> Iterator for Packets<'a> { 308 | type Item = Image; 309 | fn next(&mut self) -> Option { 310 | loop { 311 | unsafe { 312 | let pkt = vpx_codec_get_frame(self.ctx, &mut self.iter); 313 | if pkt.is_null() { 314 | return None; 315 | } else { 316 | return Some(Image(pkt)); 317 | } 318 | } 319 | } 320 | } 321 | } 322 | 323 | pub struct Finish { 324 | enc: Decoder, 325 | iter: vpx_codec_iter_t, 326 | } 327 | 328 | impl Finish { 329 | pub fn next(&mut self) -> Result> { 330 | let mut tmp = Packets { 331 | ctx: &mut self.enc.ctx, 332 | iter: self.iter, 333 | }; 334 | 335 | if let Some(packet) = tmp.next() { 336 | self.iter = tmp.iter; 337 | Ok(Some(packet)) 338 | } else { 339 | call_vpx!(vpx_codec_decode( 340 | tmp.ctx, 341 | ptr::null(), 342 | 0, 343 | ptr::null_mut(), 344 | 0, 345 | )); 346 | 347 | tmp.iter = ptr::null(); 348 | if let Some(packet) = tmp.next() { 349 | self.iter = tmp.iter; 350 | Ok(Some(packet)) 351 | } else { 352 | Ok(None) 353 | } 354 | } 355 | } 356 | } 357 | 358 | -------------------------------------------------------------------------------- /libs/vpx-codec/src/encoder.rs: -------------------------------------------------------------------------------- 1 | //! Rust interface to libvpx encoder 2 | //! 3 | //! This crate provides a Rust API to use 4 | //! [libvpx](https://en.wikipedia.org/wiki/Libvpx) for encoding images. 5 | //! 6 | //! It it based entirely on code from [srs](https://crates.io/crates/srs). 7 | //! Compared to the original `srs`, this code has been simplified for use as a 8 | //! library and updated to add support for both the VP8 codec and (optionally) 9 | //! the VP9 codec. 10 | //! 11 | //! # Optional features 12 | //! 13 | //! Compile with the cargo feature `vp9` to enable support for the VP9 codec. 14 | //! 15 | //! # Example 16 | //! 17 | //! An example of using `vpx-encode` can be found in the [`record-screen`]() 18 | //! program. The source code for `record-screen` is in the [vpx-encode git 19 | //! repository](). 20 | //! 21 | //! # Contributing 22 | //! 23 | //! All contributions are appreciated. 24 | 25 | // vpx_sys is provided by the `env-libvpx-sys` crate 26 | 27 | #![cfg_attr( 28 | feature = "backtrace", 29 | feature(error_generic_member_access, provide_any) 30 | )] 31 | 32 | use std::{ 33 | mem::MaybeUninit, 34 | os::raw::{c_int, c_uint, c_ulong}, 35 | }; 36 | 37 | #[cfg(feature = "backtrace")] 38 | use std::backtrace::Backtrace; 39 | use std::{ptr, slice}; 40 | 41 | use thiserror::Error; 42 | 43 | #[cfg(feature = "vp9")] 44 | use vpx_sys::vp8e_enc_control_id::*; 45 | use vpx_sys::vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT; 46 | use vpx_sys::*; 47 | 48 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 49 | pub enum VideoCodecId { 50 | VP8, 51 | #[cfg(feature = "vp9")] 52 | VP9, 53 | } 54 | 55 | impl Default for VideoCodecId { 56 | #[cfg(not(feature = "vp9"))] 57 | fn default() -> VideoCodecId { 58 | VideoCodecId::VP8 59 | } 60 | 61 | #[cfg(feature = "vp9")] 62 | fn default() -> VideoCodecId { 63 | VideoCodecId::VP9 64 | } 65 | } 66 | 67 | pub struct Encoder { 68 | ctx: vpx_codec_ctx_t, 69 | width: usize, 70 | height: usize, 71 | } 72 | 73 | #[derive(Debug, Error)] 74 | #[error("VPX encode error: {msg}")] 75 | pub struct Error { 76 | msg: String, 77 | #[cfg(feature = "backtrace")] 78 | #[backtrace] 79 | backtrace: Backtrace, 80 | } 81 | 82 | impl From for Error { 83 | fn from(msg: String) -> Self { 84 | Self { 85 | msg, 86 | #[cfg(feature = "backtrace")] 87 | backtrace: Backtrace::capture(), 88 | } 89 | } 90 | } 91 | 92 | pub type Result = std::result::Result; 93 | 94 | macro_rules! call_vpx { 95 | ($x:expr) => {{ 96 | let result = unsafe { $x }; // original expression 97 | let result_int = unsafe { std::mem::transmute::<_, i32>(result) }; 98 | // if result != VPX_CODEC_OK { 99 | if result_int != 0 { 100 | return Err(Error::from(format!( 101 | "Function call failed (error code {}).", 102 | result_int 103 | ))); 104 | } 105 | result 106 | }}; 107 | } 108 | 109 | macro_rules! call_vpx_ptr { 110 | ($x:expr) => {{ 111 | let result = unsafe { $x }; // original expression 112 | if result.is_null() { 113 | return Err(Error::from("Bad pointer.".to_string())); 114 | } 115 | result 116 | }}; 117 | } 118 | 119 | impl Encoder { 120 | pub fn new(config: Config) -> Result { 121 | let i = match config.codec { 122 | VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()), 123 | #[cfg(feature = "vp9")] 124 | VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()), 125 | }; 126 | 127 | if config.width % 2 != 0 { 128 | return Err(Error::from("Width must be divisible by 2".to_string())); 129 | } 130 | if config.height % 2 != 0 { 131 | return Err(Error::from("Height must be divisible by 2".to_string())); 132 | } 133 | 134 | let c = MaybeUninit::zeroed(); 135 | let mut c = unsafe { c.assume_init() }; 136 | call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0)); 137 | 138 | c.g_w = config.width; 139 | c.g_h = config.height; 140 | c.g_timebase.num = config.timebase[0]; 141 | c.g_timebase.den = config.timebase[1]; 142 | c.rc_target_bitrate = config.bitrate; 143 | 144 | c.g_threads = 8; 145 | c.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT; 146 | 147 | let ctx = MaybeUninit::zeroed(); 148 | let mut ctx = unsafe { ctx.assume_init() }; 149 | 150 | match config.codec { 151 | VideoCodecId::VP8 => { 152 | call_vpx!(vpx_codec_enc_init_ver( 153 | &mut ctx, 154 | i, 155 | &c, 156 | 0, 157 | vpx_sys::VPX_ENCODER_ABI_VERSION as i32 158 | )); 159 | } 160 | #[cfg(feature = "vp9")] 161 | VideoCodecId::VP9 => { 162 | call_vpx!(vpx_codec_enc_init_ver( 163 | &mut ctx, 164 | i, 165 | &c, 166 | 0, 167 | vpx_sys::VPX_ENCODER_ABI_VERSION as i32 168 | )); 169 | // set encoder internal speed settings 170 | call_vpx!(vpx_codec_control_( 171 | &mut ctx, 172 | VP8E_SET_CPUUSED as _, 173 | 6 as c_int 174 | )); 175 | // set row level multi-threading 176 | call_vpx!(vpx_codec_control_( 177 | &mut ctx, 178 | VP9E_SET_ROW_MT as _, 179 | 1 as c_int 180 | )); 181 | } 182 | }; 183 | 184 | Ok(Self { 185 | ctx, 186 | width: config.width as usize, 187 | height: config.height as usize, 188 | }) 189 | } 190 | 191 | pub fn encode(&mut self, pts: i64, data: &[u8]) -> Result { 192 | assert!(2 * data.len() >= 3 * self.width * self.height); 193 | 194 | let image = MaybeUninit::zeroed(); 195 | let mut image = unsafe { image.assume_init() }; 196 | 197 | call_vpx_ptr!(vpx_img_wrap( 198 | &mut image, 199 | vpx_img_fmt::VPX_IMG_FMT_I420, 200 | self.width as _, 201 | self.height as _, 202 | 1, 203 | data.as_ptr() as _, 204 | )); 205 | 206 | call_vpx!(vpx_codec_encode( 207 | &mut self.ctx, 208 | &image, 209 | pts, 210 | 1, // Duration 211 | 0, // Flags 212 | vpx_sys::VPX_DL_REALTIME as c_ulong, 213 | )); 214 | 215 | Ok(Packets { 216 | ctx: &mut self.ctx, 217 | iter: ptr::null(), 218 | }) 219 | } 220 | 221 | pub fn finish(mut self) -> Result { 222 | call_vpx!(vpx_codec_encode( 223 | &mut self.ctx, 224 | ptr::null(), 225 | -1, // PTS 226 | 1, // Duration 227 | 0, // Flags 228 | vpx_sys::VPX_DL_REALTIME as c_ulong, 229 | )); 230 | 231 | Ok(Finish { 232 | enc: self, 233 | iter: ptr::null(), 234 | }) 235 | } 236 | } 237 | 238 | impl Drop for Encoder { 239 | fn drop(&mut self) { 240 | unsafe { 241 | let result = vpx_codec_destroy(&mut self.ctx); 242 | if result != vpx_sys::VPX_CODEC_OK { 243 | eprintln!("failed to destroy vpx codec: {result:?}"); 244 | } 245 | } 246 | } 247 | } 248 | 249 | #[derive(Clone, Copy, Debug)] 250 | pub struct Frame<'a> { 251 | /// Compressed data. 252 | pub data: &'a [u8], 253 | /// Whether the frame is a keyframe. 254 | pub key: bool, 255 | /// Presentation timestamp (in timebase units). 256 | pub pts: i64, 257 | } 258 | 259 | #[derive(Clone, Copy, Debug)] 260 | pub struct Config { 261 | /// The width (in pixels). 262 | pub width: c_uint, 263 | /// The height (in pixels). 264 | pub height: c_uint, 265 | /// The timebase numerator and denominator (in seconds). 266 | pub timebase: [c_int; 2], 267 | /// The target bitrate (in kilobits per second). 268 | pub bitrate: c_uint, 269 | /// The codec 270 | pub codec: VideoCodecId, 271 | } 272 | 273 | pub struct Packets<'a> { 274 | ctx: &'a mut vpx_codec_ctx_t, 275 | iter: vpx_codec_iter_t, 276 | } 277 | 278 | impl<'a> Iterator for Packets<'a> { 279 | type Item = Frame<'a>; 280 | fn next(&mut self) -> Option { 281 | loop { 282 | unsafe { 283 | let pkt = vpx_codec_get_cx_data(self.ctx, &mut self.iter); 284 | if pkt.is_null() { 285 | return None; 286 | } else if (*pkt).kind == VPX_CODEC_CX_FRAME_PKT { 287 | let f = &(*pkt).data.frame; 288 | return Some(Frame { 289 | data: slice::from_raw_parts(f.buf as _, f.sz as usize), 290 | key: (f.flags & VPX_FRAME_IS_KEY) != 0, 291 | pts: f.pts, 292 | }); 293 | } else { 294 | // Ignore the packet. 295 | } 296 | } 297 | } 298 | } 299 | } 300 | 301 | pub struct Finish { 302 | enc: Encoder, 303 | iter: vpx_codec_iter_t, 304 | } 305 | 306 | impl Finish { 307 | pub fn next(&mut self) -> Result> { 308 | let mut tmp = Packets { 309 | ctx: &mut self.enc.ctx, 310 | iter: self.iter, 311 | }; 312 | 313 | if let Some(packet) = tmp.next() { 314 | self.iter = tmp.iter; 315 | Ok(Some(packet)) 316 | } else { 317 | call_vpx!(vpx_codec_encode( 318 | tmp.ctx, 319 | ptr::null(), 320 | -1, // PTS 321 | 1, // Duration 322 | 0, // Flags 323 | vpx_sys::VPX_DL_REALTIME as c_ulong, 324 | )); 325 | 326 | tmp.iter = ptr::null(); 327 | if let Some(packet) = tmp.next() { 328 | self.iter = tmp.iter; 329 | Ok(Some(packet)) 330 | } else { 331 | Ok(None) 332 | } 333 | } 334 | } 335 | } -------------------------------------------------------------------------------- /libs/vpx-codec/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod encoder; 2 | pub mod decoder; 3 | --------------------------------------------------------------------------------