├── packages ├── rubickbase │ ├── .gitignore │ ├── typings.d.ts │ ├── src │ │ ├── logger.ts │ │ ├── proto │ │ │ └── rubick.proto │ │ ├── event.ts │ │ ├── worker.ts │ │ ├── utils.ts │ │ ├── image.ts │ │ ├── backend.ts │ │ ├── types.ts │ │ └── index.ts │ ├── test │ │ └── index.test.js │ └── package.json └── rust-backend │ ├── index.js │ ├── build.rs │ ├── README.md │ ├── src │ ├── asar │ │ ├── README.md │ │ ├── util.rs │ │ ├── error.rs │ │ └── mod.rs │ ├── main.rs │ ├── ioio │ │ ├── devices │ │ │ ├── mouse.rs │ │ │ └── keyboard.rs │ │ └── mod.rs │ ├── sysapp │ │ ├── mod.rs │ │ ├── macos.rs │ │ ├── windows.rs │ │ └── linux.rs │ ├── imgtools.rs │ └── lib.rs │ ├── scripts │ ├── build.mjs │ └── publish.mjs │ ├── Cargo.toml │ ├── package.json │ └── index.d.ts ├── .gitignore ├── pnpm-workspace.yaml ├── example ├── pnpm-lock.yaml ├── package.json └── src │ └── index.js ├── .npmrc ├── .prettierrc ├── bob-esbuild.config.ts ├── scripts └── publish.mjs ├── tsconfig.json ├── package.json ├── .github └── workflows │ └── ci.yml ├── README.md ├── README-EN.md └── LICENSE /packages/rubickbase/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ -------------------------------------------------------------------------------- /packages/rust-backend/index.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | module.exports = require('./index.node') -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | **/dist 3 | **/target 4 | **/node_modules 5 | **/.DS_Store 6 | npm-debug.log* 7 | **/index.node -------------------------------------------------------------------------------- /packages/rubickbase/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.proto' { 2 | const value: object 3 | export default value 4 | } 5 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in subdirs of packages/ and components/ 3 | - 'packages/**' 4 | -------------------------------------------------------------------------------- /example/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.3 2 | 3 | specifiers: 4 | rubickbase: ^0.0.5 5 | 6 | devDependencies: 7 | rubickbase: link:../packages/rubickbase/dist 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | workspace-concurrency=Infinity 2 | # Remove when https://github.com/renovatebot/renovate/issues/8323 is fixed 3 | frozen-lockfile=false 4 | stream=true 5 | prefer-workspace-packages=true -------------------------------------------------------------------------------- /packages/rust-backend/build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), Box> { 2 | tonic_build::compile_protos("../rubickbase/src/proto/rubick.proto")?; 3 | Ok(()) 4 | } 5 | -------------------------------------------------------------------------------- /packages/rust-backend/README.md: -------------------------------------------------------------------------------- 1 | ## rubickbase rust_backend 2 | 3 | A native Node.js module to make RubickBase APIs. 4 | 5 | ## TODO 6 | 7 | - [ ] 优化异常处理 8 | - [ ] ioio_stop 9 | - [ ] test 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "requirePragma": false, 4 | "bracketSpacing": true, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "useTabs": true, 8 | "tabWidth": 4, 9 | "semi": false 10 | } 11 | -------------------------------------------------------------------------------- /packages/rubickbase/src/logger.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from './types' 2 | import consola from 'consola' 3 | 4 | // Globally redirect all outputs to consola. 5 | consola.wrapAll() 6 | 7 | export const defaultLogger: Logger = consola 8 | -------------------------------------------------------------------------------- /packages/rust-backend/src/asar/README.md: -------------------------------------------------------------------------------- 1 | ## Rasar 2 | 3 | License: MIT 4 | Author: Zerthox 5 | Repo: https://github.com/Zerthox/rasar 6 | Version: 0.1.6 7 | Commit: https://github.com/Zerthox/rasar/commit/a384c9413fc905b7eef5f48ef4e2f74f73e7addd 8 | -------------------------------------------------------------------------------- /bob-esbuild.config.ts: -------------------------------------------------------------------------------- 1 | import proto from 'rollup-plugin-proto' 2 | 3 | export const config: import('bob-esbuild').BobConfig = { 4 | tsc: { 5 | dirs: ['packages/*'], 6 | }, 7 | verbose: true, 8 | clean: true, 9 | plugins: [proto()], 10 | } 11 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rubickbase-example", 3 | "version": "1.4.2", 4 | "description": "example usage of rubickbase", 5 | "main": "src/index.js", 6 | "author": "sovlookup", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "rubickbase": "^0.8.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/rubickbase/src/proto/rubick.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package rubick; 3 | 4 | service Rubick { 5 | rpc ioio (DeviceEvent) returns (OK) {} 6 | } 7 | 8 | message DeviceEvent { 9 | string device = 1; 10 | string action = 2; 11 | string info = 3; 12 | } 13 | 14 | message OK { 15 | bool ok = 1; 16 | } -------------------------------------------------------------------------------- /scripts/publish.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import { cd } from 'zx' 3 | 4 | // publish rust-backend 5 | cd('packages/rust-backend') 6 | await $`pnpm publish-platform` 7 | 8 | 9 | // publish rubickbase 10 | if (process.platform === 'linux') { 11 | // build 12 | await $`pnpm build` 13 | 14 | cd('packages/rubickbase') 15 | await $`pnpm publish --access public --no-git-checks` 16 | } -------------------------------------------------------------------------------- /packages/rust-backend/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(path_try_exists)] 2 | #![allow(dead_code)] 3 | 4 | mod asar; 5 | mod imgtools; 6 | mod ioio; 7 | mod sysapp; 8 | 9 | fn sysapp() { 10 | let a = sysapp::find_apps(true, None); 11 | println!("{:?}", a) 12 | } 13 | 14 | fn main() { 15 | asar::extract( 16 | "/home/sovlookup/桌面/新建文件夹/a.asar", 17 | "/home/sovlookup/桌面/新建文件夹/output", 18 | ) 19 | .unwrap(); 20 | // ioio::send("Mouse", "Wheel", &ioio::Info::Button("Down".to_string())) 21 | // println!("{:?}", imgtools::get_all_screens()) 22 | } 23 | -------------------------------------------------------------------------------- /packages/rust-backend/src/ioio/devices/mouse.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[derive(Debug)] 4 | pub enum MouseKey { 5 | Left, 6 | Right, 7 | Middle, 8 | Unknown(f64), 9 | } 10 | 11 | #[derive(Debug)] 12 | pub struct MouseMove { 13 | pub x: f64, 14 | pub y: f64, 15 | } 16 | 17 | impl MouseMove { 18 | pub fn to_string(&self) -> String { 19 | format!("{{\"x\":{},\"y\":{}}}", self.x, self.y) 20 | } 21 | } 22 | 23 | #[derive(Debug)] 24 | pub enum MouseWheel { 25 | Up, 26 | Down, 27 | } 28 | 29 | #[derive(Debug)] 30 | pub enum MouseEvent { 31 | Press(MouseKey), 32 | Rlease(MouseKey), 33 | Move(MouseMove), 34 | Wheel(MouseWheel), 35 | } 36 | -------------------------------------------------------------------------------- /packages/rust-backend/src/asar/util.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | /// Align `size` by rounding it up to a multiple of 4. 3 | pub fn align_size(size: usize) -> usize { 4 | size + (4 - (size % 4)) % 4 5 | } 6 | 7 | /// Read a little-endian 32-bit unsigned integer from the buffer. 8 | pub fn read_u32(buffer: &[u8]) -> u32 { 9 | buffer 10 | .iter() 11 | .take(4) 12 | .enumerate() 13 | .fold(0, |result, (i, byte)| result + ((*byte as u32) << (i * 8))) 14 | } 15 | 16 | /// Write a little-endian 32-bit unsigned integer to the buffer. 17 | pub fn write_u32(buffer: &mut [u8], value: u32) { 18 | for (i, byte) in buffer.iter_mut().take(4).enumerate() { 19 | *byte = (value >> (i * 8)) as u8; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/rust-backend/scripts/build.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import { fs } from 'zx' 3 | const platform = process.platform 4 | import { join } from 'path' 5 | 6 | let target 7 | let suffix 8 | 9 | switch (platform) { 10 | case 'win32': { 11 | suffix = '.dll' 12 | target = 'x86_64-pc-windows-msvc' 13 | break 14 | } 15 | case 'darwin': { 16 | suffix = '.dylib' 17 | target = 'x86_64-apple-darwin' 18 | break 19 | } 20 | case 'linux': { 21 | suffix = '.so' 22 | target = 'x86_64-unknown-linux-gnu' 23 | break 24 | } 25 | } 26 | 27 | const targetPath = join('target', target, "release", `${platform === 'win32' ? "" : "lib"}rubick_backend${suffix}`) 28 | 29 | await $`pnpm build-${platform}` 30 | await fs.copyFile(targetPath, 'index.node') 31 | -------------------------------------------------------------------------------- /packages/rust-backend/scripts/publish.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import { fs, cd } from 'zx' 3 | 4 | const platform = process.platform 5 | const packagejson = JSON.parse(fs.readFileSync('package.json').toString()) 6 | 7 | const dist = 'dist' 8 | if (fs.existsSync(dist)) { 9 | await fs.remove(dist) 10 | } 11 | 12 | await fs.mkdirp(dist) 13 | await fs.copy('index.node', `${dist}/index.node`) 14 | await fs.copy('index.d.ts', `${dist}/index.d.ts`) 15 | await fs.copy('index.js', `${dist}/index.js`) 16 | await fs.copy('index.js', `${dist}/README.md`) 17 | 18 | packagejson['name'] = packagejson['name'] + `-${platform}` 19 | packagejson['os'] = [platform] 20 | 21 | await fs.writeJSON(`${dist}/package.json`, packagejson) 22 | 23 | cd(dist) 24 | 25 | await $`pnpm publish --access public --no-git-checks` 26 | -------------------------------------------------------------------------------- /packages/rubickbase/src/event.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { DeviceEvent, Logger } from './types' 3 | 4 | type EventCallback = (deviceEvent: DeviceEvent) => Promise | void 5 | 6 | class DeviceEventEmitter extends EventEmitter {} 7 | 8 | class EventChannelMap extends Map { 9 | logger: Logger 10 | constructor(logger: Logger) { 11 | super() 12 | this.logger = logger 13 | } 14 | set(key: string, value: EventCallback) { 15 | this.logger.info(`A new event channel [${key}] hooked`) 16 | return super.set(key, value) 17 | } 18 | delete(key: string) { 19 | this.logger.info(`Event channel [${key}] unhooked`) 20 | return super.delete(key) 21 | } 22 | } 23 | 24 | const deviceEventEmitter = new DeviceEventEmitter({ captureRejections: true }) 25 | 26 | export { deviceEventEmitter, EventChannelMap } 27 | export type { EventCallback } 28 | -------------------------------------------------------------------------------- /packages/rust-backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rubick_backend" 3 | version = "0.1.0" 4 | edition = "2018" 5 | exclude = ["index.node"] 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [profile.release] 11 | lto = true 12 | opt-level = "s" 13 | panic = "abort" 14 | codegen-units = 1 15 | incremental = false 16 | 17 | [dependencies] 18 | # mouse/keyboard event 19 | rdev = "0.5.1" 20 | # GRPC 21 | tonic = "0.5.2" 22 | prost = "0.8.0" 23 | # async 24 | tokio = { version = "1.12.0", features = ["full"] } 25 | # datetime 26 | chrono = "0.4.19" 27 | # screen capture 28 | scrap = "0.5.0" 29 | # image 30 | image = "0.23.14" 31 | base64 = "0.13.0" 32 | # de/composs 33 | zstd = "0.9" 34 | # parse apps 35 | xdg = "2.3.0" 36 | rust-ini = "0.17.0" 37 | walkdir = "2.3.2" 38 | serde = { version = "1.0.130", features = ["derive"] } 39 | serde_json = "1.0.68" 40 | # lang 41 | sys-locale = "0.1.0" 42 | 43 | [build-dependencies] 44 | tonic-build = "0.5.2" 45 | 46 | [dependencies.neon] 47 | version = "0.9.1" 48 | default-features = false 49 | features = ["napi-6","channel-api"] 50 | -------------------------------------------------------------------------------- /packages/rubickbase/test/index.test.js: -------------------------------------------------------------------------------- 1 | const { newRubickBase } = require('../dist') 2 | 3 | const rubickBase = newRubickBase() 4 | 5 | async function main() { 6 | // start rubickbase 7 | await rubickBase.start() 8 | const api = rubickBase.getAPI() 9 | 10 | // screen capture 11 | await api.screenCapture() 12 | 13 | // cursor Position 14 | let task = setInterval(async () => { 15 | const position = api.getCursorPosition() 16 | console.log("Now cursor at ", position) 17 | // screen around cursor 18 | const img = await api.screenCaptureAroundPosition(position, 2, 2) 19 | console.log(img.colorAt({ x: 1, y: 2 })) 20 | // console.log(await img.resize(800, 800).save('./a.png')) 21 | }, 2000) 22 | 23 | // hook device event 24 | const { registerHook } = rubickBase.setEventChannel({ 25 | 26 | }) 27 | console.log(rubickBase.allEventChannels()) 28 | registerHook('myeventchannel', async (e) => { console.log(e) }) 29 | console.log(rubickBase.allEventChannels()) 30 | setTimeout(async () => { 31 | await rubickBase.close() 32 | clearInterval(task) 33 | }, 10000) 34 | } 35 | 36 | main() 37 | -------------------------------------------------------------------------------- /packages/rust-backend/src/sysapp/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | mod linux; 3 | mod macos; 4 | mod windows; 5 | 6 | #[derive(Debug, Serialize, Deserialize)] 7 | pub struct SearchResult { 8 | pub name: String, 9 | pub icon_path: Option>, 10 | pub description: String, 11 | pub command: String, 12 | pub desktop_entry_path: Option, 13 | } 14 | 15 | // only linux can returns detail info now, parsing windows/macos shortcut app info is still need to be done 16 | #[allow(dead_code)] 17 | pub fn find_apps(_detail_json: bool, extra_dirs: Option>) -> Vec { 18 | let extra_dirs = match extra_dirs { 19 | Some(dir) => dir, 20 | None => vec![], 21 | }; 22 | 23 | #[cfg(target_os = "linux")] 24 | let apps = linux::find_apps_linux(_detail_json, extra_dirs); 25 | 26 | #[cfg(target_os = "windows")] 27 | let apps = windows::find_apps_windows(extra_dirs); 28 | 29 | #[cfg(target_os = "macos")] 30 | let apps = macos::find_apps_macos(extra_dirs); 31 | 32 | apps 33 | } 34 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | const { newRubickBase } = require('rubickbase') 2 | 3 | const rubickBase = newRubickBase() 4 | 5 | async function main() { 6 | // start rubickbase 7 | await rubickBase.start() 8 | const api = await rubickBase.getAPI() 9 | 10 | // screen capture 11 | await api.screenCapture() 12 | 13 | // cursor Position 14 | let task = setInterval(async () => { 15 | const position = api.getCursorPosition() 16 | console.log("Now cursor at ", position) 17 | // screen around cursor 18 | const img = await api.screenCaptureAroundPosition(position, 2, 2) 19 | console.log(img.colorAt({ x: 1, y: 2 })) 20 | console.log(await img.resize(800, 800).save('./a.png')) 21 | }, 2000) 22 | 23 | // hook device event 24 | const { registerHook } = api.setEventChannel({ 25 | device: 'Mouse', 26 | action: 'Press', 27 | info: 'Left', 28 | }) 29 | 30 | 31 | 32 | console.log(rubickBase.allEventChannels()) 33 | registerHook('myeventchannel', async (e) => { console.log(e) }) 34 | console.log(rubickBase.allEventChannels()) 35 | 36 | setTimeout(async () => { 37 | await rubickBase.close() 38 | clearInterval(task) 39 | }, 10000) 40 | } 41 | 42 | main() 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "typeRoots": ["node_modules/@types"], 5 | "moduleResolution": "Node", 6 | "declaration": true, 7 | "module": "commonjs" /* 指定模块代码生成: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 8 | "target": "ESNext" /* 指定ECMAScript目标版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 9 | "outDir": "dist" /* 将输出结构重定向到目录。 */, 10 | "rootDir": ".", 11 | "lib": ["esnext", "DOM"] /* 指定要包含在编译中的库文件。 */, 12 | "esModuleInterop": true /* 支持CommonJS和ES模块之间的互操作性。 */, 13 | "removeComments": true /* 输出清除注释。 */, 14 | "noEmitOnError": true /* 报错时不生成输出文件 */, 15 | "isolatedModules": true /* 强制要求每一个ts文件必须是一个模块 */, 16 | "experimentalDecorators": true /* 实验修饰符 */, 17 | "emitDecoratorMetadata": true /* 给源码里的装饰器声明加上设计类型元数据 */, 18 | "strictNullChecks": true /* 在严格的null检查模式下,null和undefined值不包含在任何类型里,只允许用它们自己和any来赋值(有个例外,undefined可以赋值到void) */, 19 | "allowJs": true, 20 | "checkJs": true 21 | }, 22 | "include": ["packages", "packages/rubickbase/typings.d.ts"], 23 | "exclude": ["**/example", "**/node_modules", "**/test", "**/scripts", "**/dist"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/rubickbase/src/worker.ts: -------------------------------------------------------------------------------- 1 | import newRustBackend, { RustBackendAPI } from './backend' 2 | import { defaultLogger } from './logger' 3 | import { Logger, WorkerSettings, Workers } from './types' 4 | 5 | export class RubickWorker { 6 | rustBackend!: RustBackendAPI 7 | logger: Logger 8 | port: number 9 | started: boolean 10 | constructor(workerSettings: WorkerSettings) { 11 | const { port, logger } = workerSettings 12 | this.port = port || 50068 13 | this.logger = logger || defaultLogger 14 | this.started = false 15 | } 16 | 17 | private log = (success: boolean, name: string) => { 18 | if (success) { 19 | this.logger.success(`Start ${name} worker`) 20 | } else { 21 | this.logger.error(`Start ${name} worker`) 22 | } 23 | } 24 | 25 | async start(workerName?: Workers) { 26 | if (!this.started) { 27 | this.rustBackend = await newRustBackend() 28 | this.started = true 29 | } 30 | if (workerName) { 31 | switch (workerName) { 32 | case 'ioio': 33 | this.log(await this.rustBackend?.ioioStart(this.port.toString()), 'ioio') 34 | break 35 | } 36 | } else { 37 | this.log(await this.rustBackend?.ioioStart(this.port.toString()), 'ioio') 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/rust-backend/src/sysapp/macos.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_os = "macos")] 2 | #![allow(dead_code)] 3 | use std::{fs, path::PathBuf}; 4 | use walkdir::WalkDir; 5 | 6 | pub fn find_apps_macos(mut extra_dirs: Vec) -> Vec { 7 | let mut apps = vec![]; 8 | let mut start_menu_dirs = vec![ 9 | "/System/Applications", 10 | "/Applications", 11 | "/System/Library/PreferencePanes", 12 | ] 13 | .into_iter() 14 | .map(|d| String::from(d)) 15 | .collect::>(); 16 | 17 | start_menu_dirs.append(&mut extra_dirs); 18 | 19 | let search_paths: Vec = start_menu_dirs 20 | .into_iter() 21 | .map(|dir| PathBuf::from(dir)) 22 | .collect::>(); 23 | 24 | for path in search_paths 25 | .into_iter() 26 | .filter(|path| fs::try_exists(path).unwrap()) 27 | { 28 | for entry in WalkDir::new(path).into_iter() { 29 | let entry = entry.unwrap(); 30 | let path = entry.path(); 31 | let valid = if let Some(ext) = path.extension() { 32 | ext == "app" || ext == "prefPane" 33 | } else { 34 | false 35 | }; 36 | 37 | if valid { 38 | apps.push(String::from(path.to_str().unwrap())); 39 | } 40 | } 41 | } 42 | apps 43 | } 44 | -------------------------------------------------------------------------------- /packages/rubickbase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rubickbase", 3 | "version": "1.4.2", 4 | "os": [ 5 | "linux", 6 | "win32", 7 | "darwin" 8 | ], 9 | "cpu": [ 10 | "x64" 11 | ], 12 | "description": "Expand native capabilities for nodejs and electron based on rust", 13 | "main": "dist/index.js", 14 | "module": "dist/index.mjs", 15 | "types": "dist/index.d.ts", 16 | "author": "sovlookup ", 17 | "email": "gonorth@qq.com", 18 | "license": "MPLv2", 19 | "homepage": "https://github.com/SOVLOOKUP/rubickbase", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/SOVLOOKUP/rubickbase" 23 | }, 24 | "exports": { 25 | ".": { 26 | "require": "./dist/index.js", 27 | "import": "./dist/index.mjs" 28 | }, 29 | "./*": { 30 | "require": "./dist/*.js", 31 | "import": "./dist/*.mjs" 32 | } 33 | }, 34 | "scripts": { 35 | "prepack": "bob-esbuild build --clean" 36 | }, 37 | "optionalDependencies": { 38 | "rubick_backend-darwin": "*", 39 | "rubick_backend-linux": "*", 40 | "rubick_backend-win32": "*" 41 | }, 42 | "dependencies": { 43 | "@grpc/grpc-js": "^1.4.1", 44 | "@grpc/proto-loader": "^0.6.5", 45 | "@silvia-odwyer/photon-node": "^0.3.1", 46 | "consola": "^2.15.3", 47 | "fs-extra": "^10.0.0", 48 | "mali": "^0.45.0" 49 | }, 50 | "devDependencies": { 51 | "protobufjs": "^6.11.2" 52 | }, 53 | "files": [ 54 | "dist" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rubickbase", 3 | "version": "1.4.2", 4 | "description": "Expand native capabilities for nodejs and electron based on rust", 5 | "author": "sovlookup ", 6 | "scripts": { 7 | "test": "ts-node test/index.test.ts", 8 | "build": "bob-esbuild tsc && pnpm prepack -r", 9 | "commit": "git add . && gitmoji -c", 10 | "release": "release-it", 11 | "ok": "pnpm commit && pnpm release", 12 | "ci:publish": "pnpm pretty:all && zx scripts/publish.mjs", 13 | "pretty:all": "prettier -w \"**/*.{ts,tsx,js,cjs,mjs}\"" 14 | }, 15 | "license": "MPLv2", 16 | "devDependencies": { 17 | "@release-it/bumper": "^3.0.1", 18 | "@types/estree": "^0.0.50", 19 | "@types/signale": "^1.4.2", 20 | "bob-esbuild": "^2.0.1", 21 | "bob-esbuild-cli": "^2.0.0", 22 | "esbuild": "^0.13.5", 23 | "gitmoji-changelog": "^2.2.1", 24 | "gitmoji-cli": "^4.7.0", 25 | "prettier": "^2.4.1", 26 | "release-it": "^14.11.6", 27 | "rollup-plugin-proto": "^1.1.2", 28 | "zx": "^4.2.0" 29 | }, 30 | "release-it": { 31 | "npm": { 32 | "publish": false 33 | }, 34 | "plugins": { 35 | "@release-it/bumper": { 36 | "in": "package.json", 37 | "out": [ 38 | "packages/rubickbase/package.json", 39 | "packages/rust-backend/package.json", 40 | "example/package.json" 41 | ] 42 | } 43 | }, 44 | "hooks": { 45 | "before:git:commit": "pnpm pretty:all" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/rust-backend/src/asar/error.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::From, error, fmt, io, num::ParseIntError}; 2 | 3 | /// Enum of all possible errors during manipulation of asar archives. 4 | #[derive(Debug)] 5 | pub enum Error { 6 | IoError(io::Error), 7 | ParseIntError(ParseIntError), 8 | JsonError(serde_json::Error), 9 | } 10 | 11 | impl fmt::Display for Error { 12 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 13 | match self { 14 | Error::IoError(ref err) => write!(f, "IO Error: {}", err), 15 | Error::ParseIntError(ref err) => write!(f, "Error parsing int: {}", err), 16 | Error::JsonError(ref err) => write!(f, "Error parsing JSON: {}", err), 17 | } 18 | } 19 | } 20 | 21 | impl error::Error for Error { 22 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 23 | match self { 24 | Error::IoError(ref err) => Some(err), 25 | Error::ParseIntError(ref err) => Some(err), 26 | Error::JsonError(ref err) => Some(err), 27 | } 28 | } 29 | } 30 | 31 | impl From for Error { 32 | fn from(err: io::Error) -> Self { 33 | Error::IoError(err) 34 | } 35 | } 36 | 37 | impl From for Error { 38 | fn from(err: serde_json::Error) -> Self { 39 | Error::JsonError(err) 40 | } 41 | } 42 | 43 | impl From for Error { 44 | fn from(err: ParseIntError) -> Self { 45 | Error::ParseIntError(err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/rust-backend/src/sysapp/windows.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_os = "windows")] 2 | #![allow(dead_code)] 3 | use std::{env, fs, path::PathBuf}; 4 | use walkdir::WalkDir; 5 | 6 | pub fn find_apps_windows(mut extra_dirs: Vec) -> Vec { 7 | let mut apps = vec![]; 8 | let mut start_menu_dirs = vec![ 9 | format!( 10 | r"{}\Microsoft\Windows\Start Menu\Programs", 11 | env::var("ProgramData").unwrap() 12 | ), 13 | format!( 14 | r"{}\Microsoft\Windows\Start Menu\Programs", 15 | env::var("AppData").unwrap() 16 | ), 17 | format!(r"{}\OneDrive\Desktop", env::var("USERPROFILE").unwrap()), 18 | format!(r"{}\Desktop", env::var("PUBLIC").unwrap()), 19 | ]; 20 | 21 | start_menu_dirs.append(&mut extra_dirs); 22 | 23 | let search_paths: Vec = start_menu_dirs 24 | .into_iter() 25 | .map(|dir| PathBuf::from(dir)) 26 | .collect::>(); 27 | 28 | for path in search_paths 29 | .into_iter() 30 | .filter(|path| fs::try_exists(path).unwrap()) 31 | { 32 | for entry in WalkDir::new(path).into_iter() { 33 | let entry = entry.unwrap(); 34 | let path = entry.path(); 35 | let valid = if let Some(ext) = path.extension() { 36 | ext == "lnk" 37 | } else { 38 | false 39 | }; 40 | 41 | if valid { 42 | apps.push(String::from(path.to_str().unwrap())); 43 | } 44 | } 45 | } 46 | apps 47 | } 48 | -------------------------------------------------------------------------------- /packages/rust-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rubick_backend", 3 | "version": "1.4.2", 4 | "description": "Native Node.js modules that listen/send simulated events to devices", 5 | "main": "index.js", 6 | "homepage": "https://github.com/SOVLOOKUP/rubickbase", 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/SOVLOOKUP/rubickbase" 13 | }, 14 | "license": "MPLv2", 15 | "author": "sovlookup ", 16 | "scripts": { 17 | "build": "zx scripts/build.mjs", 18 | "publish-platform": "pnpm build && zx scripts/publish.mjs", 19 | "build-linux": "cargo +nightly build --release --lib --target x86_64-unknown-linux-gnu", 20 | "build-win32": "cargo +nightly build --release --lib --target x86_64-pc-windows-msvc", 21 | "build-darwin": "cargo +nightly build --release --lib --target x86_64-apple-darwin", 22 | "build-linux-min": "cargo +nightly build --release --lib -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --target x86_64-unknown-linux-gnu", 23 | "build-win32-min": "cargo +nightly build --release --lib -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --target x86_64-pc-windows-msvc", 24 | "build-darwin-min": "cargo +nightly build --release --lib -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --target x86_64-apple-darwin", 25 | "test": "cargo test", 26 | "electron-rebuild": "electron-build-env neon build rubick_backend --release --lib" 27 | }, 28 | "devDependencies": { 29 | "zx": "^4.2.0" 30 | }, 31 | "files": [ 32 | "index.js", 33 | "index.node", 34 | "index.d.ts", 35 | "README.md" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /packages/rubickbase/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { defaultLogger } from './logger' 2 | import { DeviceEvent, Position } from './types' 3 | import { createServer } from 'net' 4 | 5 | // MIT LICENSE https://github.com/sindresorhus/rgb-hex 6 | const rgbToHex = (red: number, green: number, blue: number, alpha?: number) => { 7 | alpha = alpha || 1 8 | if (red > 255 || green > 255 || blue > 255) { 9 | defaultLogger.error('Expected three numbers below 256') 10 | } 11 | 12 | if (alpha >= 0 && alpha <= 1) { 13 | alpha = Math.round(255 * alpha) 14 | } 15 | 16 | return ( 17 | '#' + 18 | ( 19 | (blue | (green << 8) | (red << 16) | (1 << 24)).toString(16).slice(1) + 20 | (alpha | (1 << 8)).toString(16).slice(1) 21 | ).toUpperCase() 22 | ) 23 | } 24 | 25 | const infoEqual = (a: string | Position | number | undefined, b: string | Position | number) => 26 | typeof a === 'string' || typeof b === 'string' || typeof a === 'number' || typeof b === 'number' 27 | ? a === b 28 | : a?.x === b.x && a?.y === b.y 29 | 30 | const eventEqual = (deviceEvent: DeviceEvent, bindEvent: DeviceEvent) => 31 | (bindEvent.device ? deviceEvent.device === bindEvent.device : true) && 32 | (bindEvent.action ? deviceEvent.action === bindEvent.action : true) && 33 | (bindEvent.info ? infoEqual(deviceEvent.info, bindEvent.info) : true) 34 | 35 | const tryPort = (port: number): Promise => { 36 | class ApiError extends Error { 37 | code: string | undefined 38 | } 39 | const server = createServer().listen(port) 40 | return new Promise((resolve, reject) => { 41 | server.on('listening', () => { 42 | server.close() 43 | resolve(port) 44 | }) 45 | server.on('error', (err) => { 46 | if ((err as ApiError).code === 'EADDRINUSE') { 47 | resolve(tryPort(port + 1)) //如占用端口号+1 48 | console.warn(`The port ${port} is occupied try another.`) 49 | } else { 50 | reject(err) 51 | } 52 | }) 53 | }) 54 | } 55 | 56 | export { rgbToHex, eventEqual, tryPort } 57 | -------------------------------------------------------------------------------- /packages/rust-backend/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace rubick_backend { 2 | // input simulation 3 | function send_event_start( 4 | device: string, 5 | action: string, 6 | info: string | number | { x: number; y: number }, 7 | ): Promise 8 | // app finder 9 | function find_apps_start(detail_json: boolean, extra_dirs?: Array): Promise 10 | // input listen 11 | function ioio_start(port: string): Promise 12 | // screen capture 13 | function capture_base64_start(): Promise 14 | // screen all capture 15 | function capture_all_base64_start(): Promise> 16 | // screen color picker 17 | function screen_color_picker_start( 18 | x: number, 19 | y: number, 20 | ): Promise<{ 21 | r: number 22 | g: number 23 | b: number 24 | }> 25 | // capture screen around position 26 | function screen_capture_rect_base64_start( 27 | x: number, 28 | y: number, 29 | width: number, 30 | height: number, 31 | ): Promise 32 | // get local language 33 | function current_locale_language(): Promise 34 | function asar_list(path: string): Promise> 35 | function asar_extract_file(path: string, dest: string): Promise 36 | function asar_extract(path: string, dest: string): Promise 37 | function asar_pack(path: string, dest: string, level: number): Promise 38 | // Deprecated 39 | // compress 40 | // function lzma_compress_start(fromPath: string, toPath: string): Promise 41 | // decompress 42 | // function lzma_decompress_start(fromPath: string, toPath: string): Promise 43 | // function capture_start(path: string): Promise 44 | // function color_picker_start( 45 | // path: string, 46 | // x: number, 47 | // y: number, 48 | // ): Promise<{ 49 | // r: number 50 | // g: number 51 | // b: number 52 | // a: number 53 | // }> 54 | // function screen_capture_rect_start( 55 | // x: number, 56 | // y: number, 57 | // width: number, 58 | // height: number, 59 | // path: string, 60 | // ): Promise 61 | } 62 | 63 | export = rubick_backend 64 | -------------------------------------------------------------------------------- /packages/rust-backend/src/ioio/devices/keyboard.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #[derive(Debug)] 3 | pub enum KeyBoardKey { 4 | /// Alt key on Linux and Windows (option key on macOS) 5 | Alt, 6 | AltGr, 7 | Backspace, 8 | CapsLock, 9 | ControlLeft, 10 | ControlRight, 11 | Delete, 12 | DownArrow, 13 | End, 14 | Escape, 15 | F1, 16 | F10, 17 | F11, 18 | F12, 19 | F2, 20 | F3, 21 | F4, 22 | F5, 23 | F6, 24 | F7, 25 | F8, 26 | F9, 27 | Home, 28 | LeftArrow, 29 | /// also known as "windows", "super", and "command" 30 | MetaLeft, 31 | /// also known as "windows", "super", and "command" 32 | MetaRight, 33 | PageDown, 34 | PageUp, 35 | Return, 36 | RightArrow, 37 | ShiftLeft, 38 | ShiftRight, 39 | Space, 40 | Tab, 41 | UpArrow, 42 | PrintScreen, 43 | ScrollLock, 44 | Pause, 45 | NumLock, 46 | BackQuote, 47 | Num1, 48 | Num2, 49 | Num3, 50 | Num4, 51 | Num5, 52 | Num6, 53 | Num7, 54 | Num8, 55 | Num9, 56 | Num0, 57 | Minus, 58 | Equal, 59 | KeyQ, 60 | KeyW, 61 | KeyE, 62 | KeyR, 63 | KeyT, 64 | KeyY, 65 | KeyU, 66 | KeyI, 67 | KeyO, 68 | KeyP, 69 | LeftBracket, 70 | RightBracket, 71 | KeyA, 72 | KeyS, 73 | KeyD, 74 | KeyF, 75 | KeyG, 76 | KeyH, 77 | KeyJ, 78 | KeyK, 79 | KeyL, 80 | SemiColon, 81 | Quote, 82 | BackSlash, 83 | IntlBackslash, 84 | KeyZ, 85 | KeyX, 86 | KeyC, 87 | KeyV, 88 | KeyB, 89 | KeyN, 90 | KeyM, 91 | Comma, 92 | Dot, 93 | Slash, 94 | Insert, 95 | KpReturn, 96 | KpMinus, 97 | KpPlus, 98 | KpMultiply, 99 | KpDivide, 100 | Kp0, 101 | Kp1, 102 | Kp2, 103 | Kp3, 104 | Kp4, 105 | Kp5, 106 | Kp6, 107 | Kp7, 108 | Kp8, 109 | Kp9, 110 | KpDelete, 111 | Function, 112 | Unknown(f64), 113 | } 114 | #[derive(Debug)] 115 | pub enum KeyBoardEvent { 116 | Press(KeyBoardKey), 117 | Release(KeyBoardKey), 118 | } 119 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - '*' 5 | 6 | name: Release 7 | 8 | jobs: 9 | release: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | platform: [windows-latest, macos-latest, ubuntu-latest] 14 | 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | # nodejs 20 | - name: Cache pnpm modules 21 | uses: actions/cache@v2 22 | with: 23 | path: node_modules 24 | key: ${{ matrix.platform }}-${{ hashFiles('**/package.json') }} 25 | restore-keys: | 26 | ${{ matrix.platform }}- 27 | 28 | - uses: pnpm/action-setup@v2.0.1 29 | with: 30 | version: 6.15.1 31 | run_install: true 32 | 33 | - name: Changelog 34 | if: matrix.platform == 'ubuntu-latest' 35 | run: npx gitmoji-changelog --group-similar-commits 36 | 37 | - name: Release 38 | if: matrix.platform == 'ubuntu-latest' 39 | uses: taiki-e/create-gh-release-action@v1 40 | with: 41 | changelog: CHANGELOG.md 42 | title: $version 43 | draft: true 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.github_token }} 46 | 47 | # rust 48 | - uses: actions-rs/toolchain@v1 49 | with: 50 | toolchain: nightly 51 | - uses: Swatinem/rust-cache@v1 52 | with: 53 | working-directory: packages/rust-backend 54 | cache-on-failure: true 55 | 56 | # Prepare 57 | - name: Prepare linux 58 | if: matrix.platform == 'ubuntu-latest' 59 | run: | 60 | sudo apt-get install g++ pkg-config libx11-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxtst-dev libasound2-dev libssl-dev cmake libfreetype6-dev libexpat1-dev libxcb-composite0-dev 61 | rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu 62 | rustup component add rustfmt --toolchain nightly-x86_64-unknown-linux-gnu 63 | 64 | - name: Prepare windows 65 | if: matrix.platform == 'windows-latest' 66 | run: | 67 | rustup component add rust-src --toolchain nightly-x86_64-pc-windows-msvc 68 | rustup component add rustfmt --toolchain nightly-x86_64-pc-windows-msvc 69 | 70 | - name: Set up MinGW 71 | uses: egor-tensin/setup-mingw@v2 72 | if: matrix.platform == 'windows-latest' 73 | with: 74 | platform: x64 75 | 76 | - name: Prepare macos 77 | if: matrix.platform == 'macos-latest' 78 | run: | 79 | rustup component add rust-src --toolchain nightly-x86_64-apple-darwin 80 | rustup component add rustfmt --toolchain nightly-x86_64-apple-darwin 81 | 82 | # release 83 | - name: Release npm pkg 84 | uses: actions/setup-node@v2 85 | with: 86 | node-version: '16.x' 87 | registry-url: 'https://registry.npmjs.org' 88 | - run: pnpm ci:publish 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 92 | -------------------------------------------------------------------------------- /packages/rubickbase/src/image.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import { PhotonImage, resize, crop } from '@silvia-odwyer/photon-node' 3 | import { Color, Position } from './types' 4 | import { rgbToHex } from './utils' 5 | 6 | class Image { 7 | private photonImage: PhotonImage 8 | constructor(photonImage: PhotonImage) { 9 | this.photonImage = photonImage 10 | } 11 | 12 | toBase64(): string { 13 | return this.photonImage.get_base64() 14 | } 15 | 16 | get width(): number { 17 | return this.photonImage.get_width() 18 | } 19 | 20 | get height(): number { 21 | return this.photonImage.get_height() 22 | } 23 | 24 | async save(path: string) { 25 | let output_base64 = this.photonImage.get_base64() 26 | const output_data = output_base64.replace(/^data:image\/\w+;base64,/, '') 27 | await fs.writeFile(path, output_data, { encoding: 'base64' }) 28 | } 29 | 30 | /** resize the image 31 | * @param width 32 | * @param height 33 | * @param sampling_filter 最邻近差值算法 = 1, 二值寻找算法 = 2, CatmullRom插值算法 = 3, 高斯算法 = 4, 插值算法 = 5 34 | * @returns {Image} 35 | */ 36 | resize(width: number, height: number, sampling_filter?: 1 | 2 | 3 | 4 | 5): Image { 37 | sampling_filter = sampling_filter || 1 38 | const img = resize(this.photonImage, width, height, sampling_filter) 39 | return new Image(img) 40 | } 41 | 42 | /** get image raw pixels 43 | * 44 | * @returns image array 45 | */ 46 | getRawPixel() { 47 | return this.photonImage.get_raw_pixels() 48 | } 49 | 50 | /** crop image 51 | * 52 | * @param leftTopPosition left top point position 53 | * @param width img width 54 | * @param height img height 55 | * @returns img object 56 | */ 57 | crop(leftTopPosition: Position, width: number, height: number) { 58 | const [w, h] = [this.width, this.height] 59 | const limitValue = (value: number, min: number, max: number) => { 60 | if (value < min) { 61 | value = min 62 | } 63 | if (value > max) { 64 | value = max 65 | } 66 | return value 67 | } 68 | // limit boarder 69 | leftTopPosition.x = limitValue(leftTopPosition.x, 0, w) 70 | leftTopPosition.y = limitValue(leftTopPosition.y, 0, h) 71 | width = limitValue(width, 0, w - width) 72 | height = limitValue(height, 0, h - height) 73 | return new Image( 74 | crop(this.photonImage, leftTopPosition.x, leftTopPosition.y, width, height), 75 | ) 76 | } 77 | 78 | /** get pixel color at picture position 79 | * @param position 取色位置 80 | * @return {Color} 位置像素颜色 81 | */ 82 | colorAt(position: Position): Color { 83 | if ( 84 | 0 < position.x && 85 | position.x <= this.width && 86 | 0 < position.y && 87 | position.y <= this.height 88 | ) { 89 | const strip = 4 * (this.width * (position.y - 1) + position.x) 90 | const color = this.getRawPixel().slice(strip - 4, strip) 91 | return { 92 | hex16: rgbToHex(color[0], color[1], color[2], color[3]), 93 | rgba: { 94 | r: color[0], 95 | g: color[1], 96 | b: color[2], 97 | a: color[3], 98 | }, 99 | } 100 | } else { 101 | throw new Error('position out of bounds!') 102 | } 103 | } 104 | } 105 | 106 | const newImageFromFile = async (path: string): Promise => { 107 | let base64 = await fs.readFile(path, { encoding: 'base64' }) 108 | const data = base64.replace(/^data:image\/(png|jpg);base64,/, '') 109 | try { 110 | const img = PhotonImage.new_from_base64(data) 111 | return new Image(img) 112 | } catch (error) { 113 | throw error 114 | } 115 | } 116 | 117 | const newImageFromBase64 = (base64: string): Image => { 118 | if (base64 === 'error') { 119 | throw new Error('error image') 120 | } 121 | const data = base64.replace(/^data:image\/(png|jpg);base64,/, '') 122 | try { 123 | const img = PhotonImage.new_from_base64(data) 124 | return new Image(img) 125 | } catch (error) { 126 | throw error 127 | } 128 | } 129 | 130 | export { Image, newImageFromFile, newImageFromBase64 } 131 | -------------------------------------------------------------------------------- /packages/rubickbase/src/backend.ts: -------------------------------------------------------------------------------- 1 | import { DeviceEvent, Position, RGB } from './types' 2 | 3 | export interface RustBackendAPI { 4 | ioioStart: (port: string) => Promise 5 | captureToBase64: () => Promise 6 | screenColorPicker: (position: Position) => Promise 7 | screenCaptureAroundPositionToBase64: ( 8 | position: Position, 9 | width: number, 10 | height: number, 11 | ) => Promise 12 | getInstalledApps: (getDetailInfo: boolean, extraDirs?: Array) => Promise 13 | sendEvent: (event: DeviceEvent) => Promise 14 | language: () => Promise 15 | captureAllToBase64: () => Promise> 16 | asarList(path: string): Promise> 17 | asarExtractFile(path: string, dest: string): Promise 18 | asarExtract(path: string, dest: string): Promise 19 | asarPack(path: string, dest: string, level?: number): Promise 20 | // Deprecated 21 | // compress: (fromPath: string, toPath: string) => Promise 22 | // decompress: (fromPath: string, toPath: string) => Promise 23 | // capture: (path: string) => Promise 24 | // colorPicker: (path: string, position: Position) => Promise 25 | // screenCaptureAroundPosition: ( 26 | // position: Position, 27 | // width: number, 28 | // height: number, 29 | // path: string, 30 | // ) => Promise 31 | } 32 | 33 | async function newRustBackend(): Promise { 34 | let rustBackend = await import(`rubick_backend-${process.platform}`) 35 | if (!!rustBackend.default) { 36 | rustBackend = rustBackend.default 37 | } 38 | return { 39 | asarList: async (path: string) => { 40 | return await rustBackend.asar_list(path) 41 | }, 42 | asarExtract: async (path: string, dest: string) => { 43 | return await rustBackend.asar_extract(path, dest) 44 | }, 45 | asarExtractFile: async (path: string, dest: string) => { 46 | return await rustBackend.asar_extract_file(path, dest) 47 | }, 48 | asarPack: async (path: string, dest: string, level?: number) => { 49 | level = level || 0 50 | if (level < 0) level = 0 51 | if (level > 21) level = 21 52 | return await rustBackend.asar_pack(path, dest, level) 53 | }, 54 | captureAllToBase64: async () => { 55 | return await rustBackend.capture_all_base64_start() 56 | }, 57 | ioioStart: async (port: string) => { 58 | return await rustBackend.ioio_start(port) 59 | }, 60 | captureToBase64: async () => { 61 | return await rustBackend.capture_base64_start() 62 | }, 63 | screenColorPicker: async (position: Position) => { 64 | return await rustBackend.screen_color_picker_start(position.x, position.y) 65 | }, 66 | screenCaptureAroundPositionToBase64: async ( 67 | position: Position, 68 | width: number, 69 | height: number, 70 | ) => { 71 | return await rustBackend.screen_capture_rect_base64_start( 72 | position.x, 73 | position.y, 74 | width, 75 | height, 76 | ) 77 | }, 78 | getInstalledApps: async (getDetailInfo: boolean, extraDirs?: Array) => { 79 | return await rustBackend.find_apps_start(getDetailInfo, extraDirs || []) 80 | }, 81 | sendEvent: async (event: DeviceEvent) => { 82 | if (!event.device || !event.action || !event.info) { 83 | throw new Error('Not valid event!') 84 | } 85 | return await rustBackend.send_event_start(event.device, event.action, event.info) 86 | }, 87 | language: async () => { 88 | return await rustBackend.current_locale_language() 89 | }, 90 | // Deprecated 91 | // compress: async (fromPath: string, toPath: string) => { 92 | // return await rustBackend.lzma_compress_start(fromPath, toPath) 93 | // }, 94 | // decompress: async (fromPath: string, toPath: string) => { 95 | // return await rustBackend.lzma_decompress_start(fromPath, toPath) 96 | // }, 97 | // capture: async (path: string) => { 98 | // return await rustBackend.capture_start(path) 99 | // }, 100 | // colorPicker: async (path: string, position: Position) => { 101 | // return await rustBackend.color_picker_start(path, position.x, position.y) 102 | // }, 103 | // screenCaptureAroundPosition: async ( 104 | // position: Position, 105 | // width: number, 106 | // height: number, 107 | // path: string, 108 | // ) => { 109 | // return await rustBackend.screen_capture_rect_start( 110 | // position.x, 111 | // position.y, 112 | // width, 113 | // height, 114 | // path, 115 | // ) 116 | // }, 117 | } 118 | } 119 | 120 | export default newRustBackend 121 | -------------------------------------------------------------------------------- /packages/rubickbase/src/types.ts: -------------------------------------------------------------------------------- 1 | import { EventCallback } from './event' 2 | import { Image } from './image' 3 | export interface RGBA { 4 | r: number 5 | g: number 6 | b: number 7 | a: number 8 | } 9 | 10 | export interface RGB { 11 | r: number 12 | g: number 13 | b: number 14 | } 15 | 16 | export interface Color { 17 | hex16: string 18 | rgba: RGBA 19 | } 20 | 21 | export type DeviceEvent = MouseEvent | KeyBoardEvent 22 | 23 | export interface KeyBoardEvent { 24 | device?: 'KeyBoard' 25 | action?: 'Press' | 'Release' 26 | info?: 27 | | 'Alt' 28 | | 'AltGr' 29 | | 'Backspace' 30 | | 'CapsLock' 31 | | 'ControlLeft' 32 | | 'ControlRight' 33 | | 'Delete' 34 | | 'DownArrow' 35 | | 'End' 36 | | 'Escape' 37 | | 'F1' 38 | | 'F10' 39 | | 'F11' 40 | | 'F12' 41 | | 'F2' 42 | | 'F3' 43 | | 'F4' 44 | | 'F5' 45 | | 'F6' 46 | | 'F7' 47 | | 'F8' 48 | | 'F9' 49 | | 'Home' 50 | | 'LeftArrow' 51 | | 'MetaLeft' 52 | | 'MetaRight' 53 | | 'PageDown' 54 | | 'PageUp' 55 | | 'Return' 56 | | 'RightArrow' 57 | | 'ShiftLeft' 58 | | 'ShiftRight' 59 | | 'Space' 60 | | 'Tab' 61 | | 'UpArrow' 62 | | 'PrintScreen' 63 | | 'ScrollLock' 64 | | 'Pause' 65 | | 'NumLock' 66 | | 'BackQuote' 67 | | 'Num1' 68 | | 'Num2' 69 | | 'Num3' 70 | | 'Num4' 71 | | 'Num5' 72 | | 'Num6' 73 | | 'Num7' 74 | | 'Num8' 75 | | 'Num9' 76 | | 'Num0' 77 | | 'Minus' 78 | | 'Equal' 79 | | 'KeyQ' 80 | | 'KeyW' 81 | | 'KeyE' 82 | | 'KeyR' 83 | | 'KeyT' 84 | | 'KeyY' 85 | | 'KeyU' 86 | | 'KeyI' 87 | | 'KeyO' 88 | | 'KeyP' 89 | | 'LeftBracket' 90 | | 'RightBracket' 91 | | 'KeyA' 92 | | 'KeyS' 93 | | 'KeyD' 94 | | 'KeyF' 95 | | 'KeyG' 96 | | 'KeyH' 97 | | 'KeyJ' 98 | | 'KeyK' 99 | | 'KeyL' 100 | | 'SemiColon' 101 | | 'Quote' 102 | | 'BackSlash' 103 | | 'IntlBackslash' 104 | | 'KeyZ' 105 | | 'KeyX' 106 | | 'KeyC' 107 | | 'KeyV' 108 | | 'KeyB' 109 | | 'KeyN' 110 | | 'KeyM' 111 | | 'Comma' 112 | | 'Dot' 113 | | 'Slash' 114 | | 'Insert' 115 | | 'KpReturn' 116 | | 'KpMinus' 117 | | 'KpPlus' 118 | | 'KpMultiply' 119 | | 'KpDivide' 120 | | 'Kp0' 121 | | 'Kp1' 122 | | 'Kp2' 123 | | 'Kp3' 124 | | 'Kp4' 125 | | 'Kp5' 126 | | 'Kp6' 127 | | 'Kp7' 128 | | 'Kp8' 129 | | 'Kp9' 130 | | 'KpDelete' 131 | | 'Function' 132 | | number 133 | } 134 | 135 | export type MouseEvent = MouseClickEvent | MouseMoveEvent | MouseWheelEvent 136 | 137 | export interface MouseClickEvent { 138 | device?: 'Mouse' 139 | action?: 'Press' | 'Release' 140 | info?: 'Left' | 'Right' | 'Middle' | number 141 | } 142 | 143 | export interface MouseWheelEvent { 144 | device?: 'Mouse' 145 | action?: 'Wheel' 146 | info?: 'Up' | 'Down' 147 | } 148 | 149 | export interface MouseMoveEvent { 150 | device?: 'Mouse' 151 | action?: 'Move' 152 | info?: Position 153 | } 154 | 155 | export interface Position { 156 | x: number 157 | y: number 158 | } 159 | 160 | export interface RubickBaseSettings { 161 | // grpc server port 162 | port?: number 163 | // custom logger 164 | logger?: Logger 165 | // tmpdir for file storage 166 | tmpdir?: string 167 | // boot worker with rubickbase start 168 | workerBoot?: boolean 169 | // event callback will execute before all event 170 | ioEventCallback?: EventCallback 171 | } 172 | 173 | export type Workers = 'ioio' 174 | 175 | export interface WorkerSettings { 176 | // grpc server port 177 | port?: number 178 | // custom logger 179 | logger?: Logger 180 | } 181 | 182 | export interface Logger { 183 | error: Function 184 | debug: Function 185 | info: Function 186 | success: Function 187 | warn: Function 188 | } 189 | 190 | export interface BasicApi { 191 | language: () => Promise 192 | sendEvent: (event: DeviceEvent) => Promise 193 | getInstalledApps: ( 194 | getDetailInfo?: boolean, 195 | extraDirs?: string[] | undefined, 196 | ) => Promise 197 | screenCapture: () => Promise 198 | screenCaptureAroundPosition: ( 199 | position: Position, 200 | width: number, 201 | height: number, 202 | ) => Promise 203 | asarList(path: string): Promise | undefined> 204 | asarExtractFile(path: string, dest: string): Promise 205 | asarExtract(path: string, dest: string): Promise 206 | asarPack(path: string, dest: string, level?: CompressLevel): Promise 207 | } 208 | 209 | type CompressLevel = 210 | | 0 211 | | 1 212 | | 2 213 | | 3 214 | | 4 215 | | 5 216 | | 6 217 | | 7 218 | | 8 219 | | 9 220 | | 10 221 | | 11 222 | | 12 223 | | 13 224 | | 14 225 | | 15 226 | | 16 227 | | 17 228 | | 18 229 | | 19 230 | | 20 231 | | 21 232 | -------------------------------------------------------------------------------- /packages/rust-backend/src/imgtools.rs: -------------------------------------------------------------------------------- 1 | extern crate scrap; 2 | 3 | use base64::encode; 4 | use image::DynamicImage; 5 | use image::{imageops, ImageBuffer, ImageError, Rgb}; 6 | use scrap::{Capturer, Display}; 7 | use std::io::ErrorKind::WouldBlock; 8 | extern crate image; 9 | 10 | // capture primary screen return image raw 11 | fn screen_capture_raw(display: Display) -> ImageBuffer, Vec> { 12 | let mut capturer = Capturer::new(display).expect("Couldn't begin capture."); 13 | let (w, h) = (capturer.width(), capturer.height()); 14 | 15 | loop { 16 | // Wait until there's a frame. 17 | let buffer = match capturer.frame() { 18 | Ok(buffer) => buffer, 19 | Err(error) => { 20 | if error.kind() == WouldBlock { 21 | continue; 22 | } else { 23 | panic!("Error: {}", error); 24 | } 25 | } 26 | }; 27 | let stride = buffer.len() / h; 28 | let mut imgbuf = image::ImageBuffer::new(w as u32, h as u32); 29 | // Iterate over the coordinates and pixels of the image 30 | for (x, y, pixel) in imgbuf.enumerate_pixels_mut() { 31 | let i: usize = stride * y as usize + 4 * x as usize; 32 | *pixel = image::Rgb([buffer[i + 2], buffer[i + 1], buffer[i]]); 33 | } 34 | 35 | return imgbuf; 36 | } 37 | } 38 | 39 | fn valid_border(point: u32, limit: u32) -> u32 { 40 | if 0 < point && point < limit { 41 | point 42 | } else { 43 | if point == 0 { 44 | 1 45 | } else { 46 | limit - 1 47 | } 48 | } 49 | } 50 | 51 | fn screen_capture_rect_raw( 52 | x: u32, 53 | y: u32, 54 | width: u32, 55 | height: u32, 56 | ) -> Result, Vec>, ImageError> { 57 | let display = Display::primary().expect("Couldn't find primary display."); 58 | let mut img = screen_capture_raw(display); 59 | let halfw = width / 2; 60 | let halfh = height / 2; 61 | 62 | // valid top_left 63 | let top_left_x = if halfw >= x { 1 } else { x - halfw }; 64 | let top_left_y = if halfh >= y { 1 } else { y - halfh }; 65 | let bottom_right_x = x + halfw; 66 | let bottom_right_y = y + halfw; 67 | 68 | // valid bottom_right 69 | let bottom_right_x = if img.width() <= bottom_right_x { 70 | img.width() - 1 71 | } else { 72 | bottom_right_x 73 | }; 74 | let bottom_right_y = if img.height() <= bottom_right_y { 75 | img.height() - 1 76 | } else { 77 | bottom_right_y 78 | }; 79 | 80 | let width = bottom_right_x - top_left_x; 81 | let height = bottom_right_y - top_left_y; 82 | 83 | let img = imageops::crop(&mut img, top_left_x, top_left_y, width, height); 84 | 85 | Ok(img.to_image()) 86 | } 87 | 88 | // capture primary screen 89 | // #[allow(dead_code)] 90 | // pub fn screen_capture(path: String) -> Result<(), ImageError> { 91 | // screen_capture_raw().save_with_format(&path, image::ImageFormat::Png)?; 92 | // Ok(()) 93 | // } 94 | 95 | #[allow(dead_code)] 96 | pub fn screen_capture_base64() -> Result { 97 | let display = Display::primary().expect("Couldn't find primary display."); 98 | let img_rgb = DynamicImage::ImageRgb8(screen_capture_raw(display)); 99 | let mut buf = vec![]; 100 | img_rgb.write_to(&mut buf, image::ImageOutputFormat::Png)?; 101 | Ok(encode(&buf)) 102 | } 103 | 104 | #[allow(dead_code)] 105 | pub fn screen_capture_all_base64() -> Result, ImageError> { 106 | let displays = Display::all().expect("Couldn't find any display."); 107 | let captures = displays 108 | .into_iter() 109 | .map(|d| { 110 | let img_rgb = DynamicImage::ImageRgb8(screen_capture_raw(d)); 111 | let mut buf = vec![]; 112 | img_rgb 113 | .write_to(&mut buf, image::ImageOutputFormat::Png) 114 | .unwrap(); 115 | encode(&buf) 116 | }) 117 | .collect(); 118 | Ok(captures) 119 | } 120 | 121 | // #[allow(dead_code)] 122 | // pub fn screen_capture_rect( 123 | // x: u32, 124 | // y: u32, 125 | // width: u32, 126 | // height: u32, 127 | // path: String, 128 | // ) -> Result<(), ImageError> { 129 | // screen_capture_rect_raw(x, y, width, height)? 130 | // .save_with_format(path, image::ImageFormat::Png)?; 131 | // Ok(()) 132 | // } 133 | 134 | #[allow(dead_code)] 135 | pub fn screen_capture_rect_base64( 136 | x: u32, 137 | y: u32, 138 | width: u32, 139 | height: u32, 140 | ) -> Result { 141 | let img_rgb = DynamicImage::ImageRgb8(screen_capture_rect_raw(x, y, width, height)?); 142 | let mut buf = vec![]; 143 | img_rgb.write_to(&mut buf, image::ImageOutputFormat::Png)?; 144 | Ok(encode(&buf)) 145 | } 146 | 147 | // pick color from picture 148 | // #[allow(dead_code)] 149 | // pub fn color_picker(path: String, x: u32, y: u32) -> Result, ImageError> { 150 | // let img = image::io::Reader::open(path)? 151 | // .with_guessed_format()? 152 | // .decode()?; 153 | 154 | // let x = valid_border(x, img.width()); 155 | // let y = valid_border(y, img.height()); 156 | // let px = img.get_pixel(x, y); 157 | // Ok(px) 158 | // } 159 | 160 | // pick color from primary screen 161 | #[allow(dead_code)] 162 | pub fn screen_color_picker(x: u32, y: u32) -> Result, ImageError> { 163 | let display = Display::primary().expect("Couldn't find primary display."); 164 | let screen_capture = screen_capture_raw(display); 165 | 166 | let x = valid_border(x, screen_capture.width()); 167 | let y = valid_border(y, screen_capture.height()); 168 | let px = screen_capture.get_pixel(x, y); 169 | Ok(*px) 170 | } 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 项目迁移至 https://github.com/rubickCenter/native 2 | --- 3 | 简体中文 | [English](./README-EN.md) 4 | 5 | ![403295935](https://user-images.githubusercontent.com/53158137/135377195-7fc4bb2f-e456-4d95-b2ec-2585417e600b.jpg) 6 | 7 | # rubickbase 8 | 9 | 基于 Rust / WASM 提供截图、取色、键鼠事件监听模拟、图像处理、获取已安装应用等跨平台功能的现代异步 Nodejs 模块,占用空间小, 安装便捷, 使用简单, 高性能, 资源占用极小, 可取代 iohook 和 robotjs 10 | 11 | ## 功能 12 | 13 | **设备监听与模拟** 14 | 15 | - [x] 获取鼠标位置 16 | - [x] 键鼠事件监听 17 | - [x] 键盘事件模拟 18 | - [x] 鼠标事件模拟 19 | - [x] 订阅快捷键事件 20 | 21 | **图像与屏幕** 22 | 23 | - [x] 截图 24 | - [x] 获取鼠标像素颜色(主屏幕) 25 | - [x] 图片缩放 26 | - [x] 图片取色 27 | - [x] 图片裁剪 28 | - [x] 多屏幕截图 29 | 30 | **系统信息** 31 | 32 | - [x] 获取已安装的应用列表(linux✅/macos✅/windows✅) 33 | - [x] 获取已安装应用的详细信息(linux✅) 34 | - [x] 获取系统语言 35 | 36 | **其他工具** 37 | 38 | - [x] asar 打包压缩解压 (zstd 算法) 39 | 40 | ## 安装 41 | 42 | 与 iohook 与 robotjs 不同, 你不需要针对不同版本进行繁琐的重新编译, 一切开箱即用 43 | 44 | 无论你是在 node 中还是在 electron 中,都可以用你喜欢的包管理器直接安装: 45 | 46 | ``` 47 | # npm 48 | npm install --save rubickbase 49 | 50 | # yarn 51 | yarn add rubickbase 52 | 53 | # pnpm 54 | pnpm add rubickbase 55 | ``` 56 | 57 |
58 | 注意事项 59 | 60 | rubickbase 基于 [N-API](https://nodejs.org/api/n-api.html) v6 , 因此 Nodejs 环境推荐以下版本 61 | 62 | v10.x ,v12.x ,14.x, 15.x, **16.x** 63 | 64 | Electron 环境推荐以下版本 65 | 66 | v13.x,v14.x ,**v15.x** ,16.x 67 | 68 |
69 | 70 | ## 快速开始 71 | 72 | ### 引入依赖 73 | 74 | rubickbase 支持 cjs 和 esm 两种规范,当然你可以并且推荐在 TypeScript 中使用它 75 | 76 | ```js 77 | // cjs 78 | const { newRubickBase } = require('rubickbase') 79 | // esm / typescript 80 | import { newRubickBase } from 'rubickbase' 81 | ``` 82 | 83 | ### 基本使用 84 | 85 | 在这个例子中,你通过 `newRubickbase` 获得了 rubickbase 服务实例,你可以通过 `getAPI` 获取到 rubickbase 所有功能 86 | 87 | 这里每隔一秒获取当前的鼠标位置 88 | 89 | ```js 90 | const { newRubickBase } = require('rubickbase') 91 | 92 | // init rubickbase 93 | const rubickBase = newRubickBase() 94 | 95 | setInterval(async () => { 96 | // start rubickbase and get APIs 97 | const api = await rubickBase.getAPI() 98 | // print Cursor Position 99 | console.log(api.getCursorPosition()) 100 | }, 1000) 101 | ``` 102 | 103 |
104 | 可选初始化参数 105 | 106 | | 参数名称 | 参数意义 | 类型 | 107 | | --------------- | -------------------------- | ------------- | 108 | | port | GRPC 服务器的端口 | number | 109 | | logger | 日志器 | Logger | 110 | | tmpdir | 临时文件目录 | string | 111 | | workerBoot | 是否将 worker 一起启动 | boolean | 112 | | ioEventCallback | 侦听所有设备事件的回调函数 | EventCallback | 113 | 114 |
115 | 116 |
117 | 高级启动 118 | 119 | rubickbase 由 GRPC 服务器 master 与多个提供不同功能的 worker 组合运行 120 | 121 | 一般来说,当你调用 `getAPI` 时,rubickbase 会自动开启所有服务,但如果你需要在不同的地方或时间运行他们, 就可以手动控制他们的生命周期,达到更精细的控制 122 | 123 | 首先你需要在 master 启动时选择不启动 workers,这时候 master 会侦听来自 worker 的消息 124 | 125 | ```js 126 | // init rubickbase 127 | const rubickBase = newRubickBase({ workerBoot: false }) 128 | rubickBase.start() 129 | ``` 130 | 131 | 然后在需要的地方手动启动 workers 132 | 133 | ```js 134 | const rubickWorker = newRubickWorker() 135 | // 启动所有 worker 136 | rubickWorker.start() 137 | // 单独启动 ioio worker 138 | rubickWorker.start('ioio') 139 | ``` 140 | 141 | 注意, worker 的生命周期(存在时间)必须比 master 要短, 否则 worker 中的 GRPC client 会抛出找不到服务端的异常 142 | 143 | 并且如果你在启动 master 时更改了端口, 那么也要把端口传递给 worker 144 | 145 | ```js 146 | // init rubickbase 147 | const rubickBase = newRubickBase({ port: 8001, workerBoot: false }) 148 | rubickBase.start() 149 | // then 150 | const rubickWorker = newRubickWorker({ port: 8001 }) 151 | rubickWorker.start() 152 | ``` 153 | 154 |
155 | 156 |
157 | 直接使用底层无状态 API 158 | 159 | 允许你在不启动 master 和 worker 的情况下直接调用一些基础 API 160 | 161 | ```js 162 | const { 163 | language, 164 | sendEvent, 165 | getInstalledApps, 166 | screenCapture, 167 | screenCaptureAll, 168 | screenCaptureAroundPosition, 169 | } = await newRubickBase().getBasicAPI() 170 | ``` 171 | 172 |
173 | 174 | ### 设备输入事件模拟 175 | 176 | 模拟设备输入事件非常简单,只要调用 `sendEvent` 即可 177 | 178 | 由于 rubickbase 是用 TypeScript 书写,书写 Event 时编辑器会自动提示 179 | 180 | ```js 181 | // 这里将会模拟按下 F1 键 182 | api.sendEvent({ 183 | device: 'KeyBoard', 184 | action: 'Press', 185 | info: 'F1', 186 | }) 187 | 188 | // 这里将会模拟按下鼠标中键 189 | api.sendEvent({ 190 | device: 'Mouse', 191 | action: 'Press', 192 | info: 'Middle', 193 | }) 194 | ``` 195 | 196 | ### 设备输入事件侦听 197 | 198 | 通过 `setEventChannel` API 创建目标事件频道, 获取对应事件的订阅器 199 | 200 | ```js 201 | // 这里创建了监听鼠标左键的频道 202 | const register = api.setEventChannel({ 203 | device: 'Mouse', 204 | action: 'Press', 205 | info: 'Left', 206 | }) 207 | 208 | // 查看目前所有已创建的事件订阅 209 | console.log(api.allEventChannels()) 210 | 211 | // 通过 `registerHook` 注册打印函数 212 | register('myeventchannel', async (e) => { 213 | console.log(e) 214 | }) 215 | 216 | // 删除事件频道 217 | api.delEventChannel('myeventchannel') 218 | 219 | console.log(api.hasEventChannel('myeventchannel'), api.allEventChannels()) 220 | ``` 221 | 222 |
223 | 检索和关闭频道 224 | 225 | `allEventChannels` 可以获得目前所有已存在的事件频道 226 | 227 | `hasEventChannel` 可以判断是否有某个名字的频道 228 | 229 | `delEventChannel` 可以删除创建的事件频道 230 | 231 |
232 | 233 | ### 事件模糊匹配 234 | 235 | 一个设备事件有 `device` `action` `info` 三个约束条件, 你可以去掉其中的任何条件来完成事件模糊匹配 236 | 237 | ```js 238 | // 匹配鼠标左键的按下事件 239 | api.setEventChannel({ 240 | device: 'Mouse', 241 | action: 'Press', 242 | info: 'Left', 243 | }) 244 | 245 | // 匹配鼠标移动事件 246 | api.setEventChannel({ 247 | device: 'Mouse', 248 | action: 'Move', 249 | }) 250 | 251 | // 匹配鼠标所有键的按下事件 252 | api.setEventChannel({ 253 | device: 'Mouse', 254 | action: 'Press', 255 | }) 256 | 257 | // 匹配所有设备所有键的按下事件 258 | api.setEventChannel({ 259 | action: 'Press', 260 | }) 261 | ``` 262 | 263 | ### 图像处理 264 | 265 | rubickbase 基于 [Photon](https://silvia-odwyer.github.io/photon/) 的高性能 WASM 模块进行图像处理 266 | 267 | 1. 取色 Image.colorAt 268 | 269 | ```js 270 | const color = img.colorAt({ x: 1, y: 1 }) 271 | ``` 272 | 273 | 2. 缩放 Image.resize 274 | 275 | 输入宽和高,输出缩放后的图像 276 | 277 | ```js 278 | const newImg = img.resize(100, 100) 279 | ``` 280 | 281 |
282 | 可选缩放算法 283 | 284 | 默认最邻近差值算法,其他的算法的图像结果边缘更光滑,可以根据自己的需要进行选择 285 | 286 | 最邻近差值算法 = 1, 二值寻找算法 = 2, CatmullRom 插值算法 = 3, 高斯算法 = 4, 插值算法 = 5 287 | 288 | ```js 289 | const newImg = img.resize(100, 100, 1) 290 | ``` 291 | 292 |
293 | 294 | 3. 裁剪 Image.crop 295 | 296 | 输入左上角的点、宽、高,输出裁剪后的图像 297 | 298 | ```js 299 | const newImg = img.crop({ x: 5, y: 5 }, 10, 10) 300 | ``` 301 | 302 | ### 功能一览 303 | 304 | rubickbase 还有以下功能: 305 | 306 | 1. 获取鼠标当前座标 307 | getCursorPosition: () => Position 308 | 309 | 2. 获取鼠标当前座标的像素值 310 | _此 API 仅适用于主屏幕_ 311 | getCursorPositionPixelColor: () => Promise< Color > 312 | 313 | 3. 主屏幕截屏 314 | screenCapture: () => Promise< Image > 315 | 316 | 4. 所有屏幕截屏 317 | screenCaptureAll: () => Promise< Image[] > 318 | 319 | 5. 获取鼠标周围图像 320 | _此 API 仅适用于主屏幕_ 321 | screenCaptureAroundPosition: (position: Position, width: number, height: number) => Promise< Image > 322 | 323 | 6. 获取系统内已安装的应用列表 324 | getInstalledApps: (getDetailInfo: boolean = false, extraDirs?: Array< string >) => Promise< string > 325 | 326 | `getDetailInfo` 是否获取应用详细信息 默认否 (目前只有 Linux 有效) 327 | `extraDirs` 额外要扫描的目录 328 | return JSON 格式的快捷方式路径列表 如果 getDetailInfo 为 true, 那么返回应用详细信息列表 329 | 330 |
331 | 应用详细信息字段解释 332 | 333 | name: 名称 334 | icon_path: 各个尺寸的图标列表 335 | description: 应用描述 336 | command: 应用启动命令 337 | desktop_entry_path: 快捷方式路径 338 | 339 |
340 | 341 |
342 | 扫描原理 343 | 344 | 扫描系统存放快捷方式的目录来获取所有系统内安装的应用, 包含的扫描格式: 345 | 346 | | 平台 | 后辍名 | 347 | | ------- | ------------ | 348 | | linux | desktop | 349 | | macos | app,prefPane | 350 | | windows | lnk | 351 | 352 |
353 | 354 | 7. 获取系统语言 355 | language: () => Promise< string > 356 | 357 | 8. asar + zstd 压缩 358 | 359 | 是 electron 官方 asar 格式的超集,兼容打包解包官方 asar 文件,增加了 zstd 压缩算法,可减少文件60-85%的大小 360 | 361 | asarList(path: string): Promise< Array < string > | undefined> 362 | asarExtractFile(path: string, dest: string): Promise< undefined > 363 | asarExtract(path: string, dest: string): Promise< undefined > 364 | asarPack(path: string, dest: string, level?: number): Promise< undefined > 365 | 366 | ## 贡献与联系 367 | 368 | 欢迎任何形式的贡献与开源协作! 369 | 370 | 项目依赖 `pnpm` 包管理器, 你需要先安装它 371 | 372 | `npm install -g pnpm` 373 | 374 | 项目采用全自动化的代码检查与构建, 使用以下命令进行开发即可 375 | 376 | | Action | Command | 377 | | ------- | ---------------- | 378 | | Install | · `pnpm i` | 379 | | Build | · `pnpm build` | 380 | | Commit | · `pnpm commit` | 381 | | Release | · `pnpm release` | 382 | 383 | 关注公众号后发送`联系`关键字加我微信: 384 | 385 | ![wechat](https://z3.ax1x.com/2021/09/26/4yRpN9.jpg) 386 | 387 | ## 开源协议 388 | 389 | 本项目遵守 MPLv2 协议 390 | -------------------------------------------------------------------------------- /packages/rust-backend/src/sysapp/linux.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![cfg(target_os = "linux")] 3 | use crate::sysapp::SearchResult; 4 | use ini::Ini; 5 | use std::collections::HashMap; 6 | use std::env; 7 | use std::fs; 8 | use std::path::{Path, PathBuf}; 9 | use walkdir::WalkDir; 10 | use xdg::BaseDirectories; 11 | 12 | struct AppParser { 13 | icon_map: HashMap>, 14 | lang: Option, 15 | } 16 | 17 | fn get_appdirs() -> Vec { 18 | // get appdirs 19 | let base_dirs = BaseDirectories::new() 20 | .expect("Can't find xdg directories! Good luck and thanks for all the fish"); 21 | let mut app_dirs: Vec = Vec::new(); 22 | app_dirs.push(base_dirs.get_data_home()); 23 | app_dirs.append(&mut base_dirs.get_data_dirs()); 24 | app_dirs 25 | } 26 | 27 | impl AppParser { 28 | fn new_parser() -> Self { 29 | // Get icon dirs, add "icons" (/usr/share/icons ecc...) 30 | let mut icon_dirs: Vec = get_appdirs() 31 | .iter() 32 | .map(|dd| dd.join("icons").to_str().unwrap().to_string()) 33 | .collect(); 34 | // Add $HOME/.icons 35 | if let Ok(home) = env::var("HOME") { 36 | icon_dirs.insert(0, format!("{}/.icons", home)); 37 | } 38 | icon_dirs.insert(0, "/usr/share/pixmaps".to_string()); 39 | 40 | let mut icon_map: HashMap> = HashMap::new(); 41 | 42 | for dir in icon_dirs 43 | .into_iter() 44 | .filter(|path| fs::try_exists(path).unwrap()) 45 | { 46 | for entry in WalkDir::new(dir).into_iter() { 47 | let entry = entry.unwrap(); 48 | let icon_path = entry.path(); 49 | let valid = if let Some(ext) = icon_path.extension() { 50 | ext == "png" || ext == "svg" || ext == "xpm" 51 | } else { 52 | false 53 | }; 54 | if valid { 55 | let file_path = String::from(icon_path.to_str().unwrap()); 56 | let file_name = String::from(icon_path.file_name().unwrap().to_str().unwrap()); 57 | let file_name = file_name.replace( 58 | format!(".{}", icon_path.extension().unwrap().to_str().unwrap()).as_str(), 59 | "", 60 | ); 61 | 62 | match icon_map.get_mut(&file_name) { 63 | Some(v) => { 64 | v.push(file_path); 65 | } 66 | None => { 67 | icon_map.insert(file_name, vec![file_path]); 68 | } 69 | }; 70 | } 71 | } 72 | } 73 | 74 | let lang = match env::var("LANG") { 75 | Ok(lang) => { 76 | if let Some(lang) = lang.split(".").next() { 77 | Some(lang.to_string()) 78 | } else { 79 | None 80 | } 81 | } 82 | Err(_) => None, 83 | }; 84 | 85 | AppParser { icon_map, lang } 86 | } 87 | 88 | /// Given an icon name, search for the icon file. 89 | fn search_icon(&self, icon: &str) -> Option> { 90 | // if icon is a path return it 91 | if fs::try_exists(icon).unwrap() { 92 | return Some(vec![icon.to_string()]); 93 | } 94 | 95 | if let Some(icon) = self.icon_map.get(&icon.to_string()) { 96 | let mut icon_list = icon.to_vec(); 97 | icon_list.sort(); 98 | return Some(icon_list); 99 | } 100 | 101 | None 102 | } 103 | 104 | /// Given a desktop file path, try to build a SearchResult 105 | fn searchresult_from_desktopentry(&self, desktop_file_path: &Path) -> Option { 106 | let suffix; 107 | 108 | if let Some(lang) = &self.lang { 109 | suffix = format!("[{}]", lang); 110 | } else { 111 | suffix = "".to_string(); 112 | } 113 | 114 | let name = format!("Name{}", suffix); 115 | let comment = format!("Comment{}", suffix); 116 | // If anything we need can't be found, return None 117 | let info = match Ini::load_from_file(&desktop_file_path) { 118 | Ok(info) => info, 119 | Err(_) => return None, 120 | }; 121 | let section = match info.section(Some("Desktop Entry")) { 122 | Some(sec) => sec, 123 | None => return None, 124 | }; 125 | let name = match section.get(name) { 126 | Some(name) => name.to_string(), 127 | None => match section.get("Name") { 128 | Some(name) => name.to_string(), 129 | None => return None, 130 | }, 131 | }; 132 | let description = match section.get(comment) { 133 | Some(description) => description.to_string(), 134 | None => match section.get("Comment") { 135 | Some(description) => description.to_string(), 136 | None => return None, 137 | }, 138 | }; 139 | let icon = match section.get("Icon") { 140 | Some(icon) => icon, 141 | None => return None, 142 | }; 143 | let command = match section.get("Exec") { 144 | Some(command) => command.to_string(), 145 | None => return None, 146 | }; 147 | 148 | let desktop_entry_path = match desktop_file_path.to_str() { 149 | Some(path) => Some(path.to_string()), 150 | None => return None, 151 | }; 152 | 153 | Some(SearchResult { 154 | icon_path: self.search_icon(icon), 155 | desktop_entry_path, 156 | name, 157 | description, 158 | command, 159 | }) 160 | } 161 | } 162 | 163 | /// Given a binary file path, try to build a SearchResult 164 | // fn searchresult_from_bin(command_path: &Path) -> Option { 165 | // let name = match command_path.file_stem() { 166 | // Some(os_str) => { 167 | // if let Some(str_ref) = os_str.to_str() { 168 | // str_ref.to_string() 169 | // } else { 170 | // return None; 171 | // } 172 | // } 173 | // None => return None, 174 | // }; 175 | 176 | // let description = match command_path.as_os_str().to_str() { 177 | // Some(desc) => desc.to_string(), 178 | // None => return None, 179 | // }; 180 | // let command = description.clone(); 181 | 182 | // Some(SearchResult { 183 | // icon_path: search_icon("terminal"), 184 | // desktop_entry_path: None, 185 | // name, 186 | // description, 187 | // command, 188 | // }) 189 | // } 190 | 191 | /// Search all applications and collect them in a Vec of SearchResult 192 | /// This should be the only public api in this module. 193 | pub fn find_apps_linux(detail_json: bool, extra_dirs: Vec) -> Vec { 194 | let mut extra_dirs = extra_dirs.into_iter().map(|d| PathBuf::from(d)).collect(); 195 | let mut apps: Vec = Vec::new(); 196 | let mut app_parser = AppParser { 197 | icon_map: HashMap::new(), 198 | lang: None, 199 | }; 200 | 201 | if detail_json { 202 | app_parser = AppParser::new_parser(); 203 | } 204 | 205 | let mut app_dirs = get_appdirs(); 206 | app_dirs.append(&mut extra_dirs); 207 | 208 | // Build SearchResults for all desktop files we can find 209 | for mut app_dir in app_dirs { 210 | app_dir.push("applications"); 211 | if fs::try_exists(&app_dir).unwrap() { 212 | for entry in WalkDir::new(app_dir).into_iter() { 213 | let entry = entry.unwrap(); 214 | let app_path = entry.path(); 215 | let valid = if let Some(ext) = app_path.extension() { 216 | ext == "desktop" 217 | } else { 218 | false 219 | }; 220 | 221 | if valid { 222 | if detail_json { 223 | if let Some(res) = app_parser.searchresult_from_desktopentry(app_path) { 224 | apps.push(serde_json::to_string(&res).unwrap()); 225 | }; 226 | } else { 227 | apps.push(String::from(app_path.to_str().unwrap())); 228 | } 229 | } 230 | } 231 | } 232 | } 233 | // Now build SearchResults for all binaries we can find 234 | // let key = "PATH"; 235 | // match env::var_os(key) { 236 | // Some(paths) => { 237 | // for path in env::split_paths(&paths) { 238 | // if let Ok(entries) = fs::read_dir(path) { 239 | // results.append( 240 | // &mut entries 241 | // .filter_map(|path| searchresult_from_bin(&path.unwrap().path())) 242 | // .collect(), 243 | // ); 244 | // } 245 | // } 246 | // } 247 | // None => println!("{} is not defined in the environment.", key), 248 | // } 249 | // That's it, return 250 | apps 251 | } 252 | -------------------------------------------------------------------------------- /packages/rust-backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(path_try_exists)] 2 | mod asar; 3 | mod imgtools; 4 | mod ioio; 5 | mod sysapp; 6 | use neon::prelude::*; 7 | use std::thread; 8 | use sys_locale::get_locale; 9 | 10 | // 开启键鼠事件侦测 11 | fn ioio_start(mut cx: FunctionContext) -> JsResult { 12 | let port = cx.argument::(0)?.value(&mut cx); 13 | let channel = cx.channel(); 14 | thread::spawn(move || { 15 | ioio::start(port.as_str()).expect("Rpc client start error!"); 16 | channel.send(move |mut _cx| Ok(())) 17 | }); 18 | Ok(cx.boolean(true)) 19 | } 20 | 21 | // 主屏幕截图 base64 22 | fn capture_base64_start(mut cx: FunctionContext) -> JsResult { 23 | let res = imgtools::screen_capture_base64().expect("screen capture error"); 24 | Ok(cx.string(res)) 25 | } 26 | 27 | // 多屏幕截图 base64 28 | fn capture_all_base64_start(mut cx: FunctionContext) -> JsResult { 29 | let res = imgtools::screen_capture_all_base64().expect("screen capture error"); 30 | let captures = cx.empty_array(); 31 | for (i, v) in res.into_iter().enumerate() { 32 | let value = cx.string(v); 33 | captures.set(&mut cx, i as u32, value)?; 34 | } 35 | Ok(captures) 36 | } 37 | 38 | // 压缩 39 | // fn lzma_compress_start(mut cx: FunctionContext) -> JsResult { 40 | // let frompath = cx.argument::(0)?.value(&mut cx); 41 | // let topath = cx.argument::(1)?.value(&mut cx); 42 | // let channel = cx.channel(); 43 | // thread::spawn(move || { 44 | // dataprocess::lzma_compress(frompath.as_str(), topath.as_str()) 45 | // .expect("lzma_compress error!"); 46 | // channel.send(move |mut _cx| Ok(())) 47 | // }); 48 | // Ok(cx.undefined()) 49 | // } 50 | 51 | // 解压 52 | // fn lzma_decompress_start(mut cx: FunctionContext) -> JsResult { 53 | // let frompath = cx.argument::(0)?.value(&mut cx); 54 | // let topath = cx.argument::(1)?.value(&mut cx); 55 | // let channel = cx.channel(); 56 | // thread::spawn(move || { 57 | // dataprocess::lzma_decompress(frompath.as_str(), topath.as_str()) 58 | // .expect("lzma_decompress error!"); 59 | // channel.send(move |mut _cx| Ok(())) 60 | // }); 61 | // Ok(cx.undefined()) 62 | // } 63 | 64 | // 获取屏幕矩形区域截图 base64 65 | fn screen_capture_rect_base64_start(mut cx: FunctionContext) -> JsResult { 66 | let x = cx.argument::(0)?.value(&mut cx); 67 | let y = cx.argument::(1)?.value(&mut cx); 68 | let width = cx.argument::(2)?.value(&mut cx); 69 | let height = cx.argument::(3)?.value(&mut cx); 70 | let res = imgtools::screen_capture_rect_base64(x as u32, y as u32, width as u32, height as u32) 71 | .expect("screen capture rect error"); 72 | Ok(cx.string(res)) 73 | } 74 | 75 | // 从屏幕中取色 76 | fn screen_color_picker_start(mut cx: FunctionContext) -> JsResult { 77 | let x = cx.argument::(0)?.value(&mut cx); 78 | let y = cx.argument::(1)?.value(&mut cx); 79 | let color = imgtools::screen_color_picker(x as u32, y as u32).expect("color picker error!"); 80 | let obj = cx.empty_object(); 81 | let r = cx.number(color[0]); 82 | let g = cx.number(color[1]); 83 | let b = cx.number(color[2]); 84 | obj.set(&mut cx, "r", r)?; 85 | obj.set(&mut cx, "g", g)?; 86 | obj.set(&mut cx, "b", b)?; 87 | Ok(obj) 88 | } 89 | 90 | // 获取已安装的系统应用 输出JSON格式 91 | fn find_apps_start(mut cx: FunctionContext) -> JsResult { 92 | let detail_json = cx.argument::(0)?.value(&mut cx); 93 | let extra_dirs = if let Some(extra_dirs) = cx.argument_opt(1) { 94 | let dirs: Handle = extra_dirs.downcast_or_throw(&mut cx)?; 95 | let dirs: Vec = dirs 96 | .to_vec(&mut cx)? 97 | .into_iter() 98 | .map(|dir| { 99 | let dir: Handle = dir.downcast_or_throw(&mut cx).unwrap(); 100 | dir.value(&mut cx) 101 | }) 102 | .collect(); 103 | Some(dirs) 104 | } else { 105 | None 106 | }; 107 | 108 | let res = sysapp::find_apps(detail_json, extra_dirs); 109 | let apps = cx.string(serde_json::to_string(&res).unwrap()); 110 | Ok(apps) 111 | } 112 | 113 | // 模拟输入 114 | fn send_event_start(mut cx: FunctionContext) -> JsResult { 115 | let device = cx.argument::(0)?.value(&mut cx); 116 | let action = cx.argument::(1)?.value(&mut cx); 117 | let info = cx.argument::(2)?; 118 | let send_info = if let Ok(button) = info.downcast::>(&mut cx) { 119 | ioio::Info::Button(button.value(&mut cx)) 120 | } else { 121 | if let Ok(unknow_button) = info.downcast::>(&mut cx) { 122 | ioio::Info::UnknownButton(unknow_button.value(&mut cx)) 123 | } else { 124 | let position: Handle = info.downcast_or_throw(&mut cx).unwrap(); 125 | let x = position 126 | .get(&mut cx, "x")? 127 | .downcast_or_throw::>(&mut cx)? 128 | .value(&mut cx); 129 | let y = position 130 | .get(&mut cx, "y")? 131 | .downcast_or_throw::>(&mut cx)? 132 | .value(&mut cx); 133 | ioio::Info::Position { x, y } 134 | } 135 | }; 136 | ioio::send(device.as_str(), action.as_str(), &send_info); 137 | Ok(cx.undefined()) 138 | } 139 | 140 | fn current_locale_language(mut cx: FunctionContext) -> JsResult { 141 | let current_locale = get_locale().unwrap_or_else(|| String::from("en-US")); 142 | Ok(cx.string(current_locale)) 143 | } 144 | 145 | fn asar_pack(mut cx: FunctionContext) -> JsResult { 146 | let path = cx.argument::(0)?.value(&mut cx); 147 | let dest = cx.argument::(1)?.value(&mut cx); 148 | let level = cx.argument::(2)?.value(&mut cx); 149 | asar::pack(&path, &dest, level as i32).expect("asar pack error!"); 150 | Ok(cx.undefined()) 151 | } 152 | 153 | fn asar_extract(mut cx: FunctionContext) -> JsResult { 154 | let path = cx.argument::(0)?.value(&mut cx); 155 | let dest = cx.argument::(1)?.value(&mut cx); 156 | asar::extract(&path, &dest).expect("asar extract error!"); 157 | Ok(cx.undefined()) 158 | } 159 | 160 | fn asar_extract_file(mut cx: FunctionContext) -> JsResult { 161 | let path = cx.argument::(0)?.value(&mut cx); 162 | let dest = cx.argument::(1)?.value(&mut cx); 163 | asar::extract_file(&path, &dest).expect("asar extract file error!"); 164 | Ok(cx.undefined()) 165 | } 166 | 167 | fn asar_list(mut cx: FunctionContext) -> JsResult { 168 | let path = cx.argument::(0)?.value(&mut cx); 169 | let list = asar::list(&path).expect("asar extract file error!"); 170 | let list = list.into_iter().map(|p| String::from(p.to_str().unwrap())); 171 | let res = cx.empty_array(); 172 | for (i, v) in list.enumerate() { 173 | let value = cx.string(v); 174 | res.set(&mut cx, i as u32, value)?; 175 | } 176 | Ok(res) 177 | } 178 | 179 | #[neon::main] 180 | fn main(mut cx: ModuleContext) -> NeonResult<()> { 181 | // async task 182 | cx.export_function("asar_list", asar_list)?; 183 | cx.export_function("asar_extract_file", asar_extract_file)?; 184 | cx.export_function("asar_extract", asar_extract)?; 185 | cx.export_function("asar_pack", asar_pack)?; 186 | cx.export_function("current_locale_language", current_locale_language)?; 187 | cx.export_function("send_event_start", send_event_start)?; 188 | cx.export_function("find_apps_start", find_apps_start)?; 189 | cx.export_function("screen_color_picker_start", screen_color_picker_start)?; 190 | cx.export_function("capture_base64_start", capture_base64_start)?; 191 | cx.export_function("capture_all_base64_start", capture_all_base64_start)?; 192 | cx.export_function( 193 | "screen_capture_rect_base64_start", 194 | screen_capture_rect_base64_start, 195 | )?; 196 | // mutithread task 197 | cx.export_function("ioio_start", ioio_start)?; 198 | // cx.export_function("lzma_compress_start", lzma_compress_start)?; 199 | // cx.export_function("lzma_decompress_start", lzma_decompress_start)?; 200 | // Deprecated 201 | // cx.export_function("screen_capture_rect_start", screen_capture_rect_start)?; 202 | // cx.export_function("color_picker_start", color_picker_start)?; 203 | // cx.export_function("capture_start", capture_start)?; 204 | Ok(()) 205 | } 206 | 207 | // Deprecated 208 | // 获取图片某位置像素颜色 209 | // fn color_picker_start(mut cx: FunctionContext) -> JsResult { 210 | // let path = cx.argument::(0)?.value(&mut cx); 211 | // let x = cx.argument::(1)?.value(&mut cx); 212 | // let y = cx.argument::(2)?.value(&mut cx); 213 | // let color = imgtools::color_picker(path, x as u32, y as u32).expect("color pick error"); 214 | // let obj = cx.empty_object(); 215 | // let r = cx.number(color[0]); 216 | // let g = cx.number(color[1]); 217 | // let b = cx.number(color[2]); 218 | // let a = cx.number(color[3]); 219 | // obj.set(&mut cx, "r", r)?; 220 | // obj.set(&mut cx, "g", g)?; 221 | // obj.set(&mut cx, "b", b)?; 222 | // obj.set(&mut cx, "a", a)?; 223 | // Ok(obj) 224 | // } 225 | 226 | // 获取屏幕矩形区域截图 227 | // fn screen_capture_rect_start(mut cx: FunctionContext) -> JsResult { 228 | // let x = cx.argument::(0)?.value(&mut cx); 229 | // let y = cx.argument::(1)?.value(&mut cx); 230 | // let width = cx.argument::(2)?.value(&mut cx); 231 | // let height = cx.argument::(3)?.value(&mut cx); 232 | // let path = cx.argument::(4)?.value(&mut cx); 233 | // imgtools::screen_capture_rect(x as u32, y as u32, width as u32, height as u32, path) 234 | // .expect("screen capture rect error"); 235 | // Ok(cx.undefined()) 236 | // } 237 | 238 | // 主屏幕截图 239 | // fn capture_start(mut cx: FunctionContext) -> JsResult { 240 | // let path = cx.argument::(0)?.value(&mut cx); 241 | // imgtools::screen_capture(path).expect("screen capture error"); 242 | // Ok(cx.undefined()) 243 | // } 244 | -------------------------------------------------------------------------------- /packages/rust-backend/src/asar/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | mod error; 3 | mod util; 4 | 5 | pub use error::Error; 6 | use prost::bytes::Buf; 7 | use serde_json::{json, Value}; 8 | use std::{ 9 | env, fs, 10 | fs::File, 11 | io::{self, Read, Seek, SeekFrom, Write}, 12 | path::{Path, PathBuf}, 13 | }; 14 | use util::{align_size, read_u32, write_u32}; 15 | 16 | /// Maximum possible file size for files in asar archives. 17 | const MAX_SIZE: u64 = std::u32::MAX as u64; 18 | 19 | /// Read the header of an asar archive and extract the header size & json. 20 | /// 21 | /// This may return an `io::Error` if there is an error reading the file. 22 | fn read_header(reader: &mut File) -> Result<(u32, Value, bool), io::Error> { 23 | // read header bytes 24 | let mut header_buffer = vec![0u8; 16]; 25 | reader.read_exact(&mut header_buffer)?; 26 | 27 | // grab sizes 28 | let header_size = read_u32(&header_buffer[4..8]); 29 | let json_size = read_u32(&header_buffer[12..]); 30 | 31 | // read json bytes 32 | let mut json_buffer = vec![0u8; json_size as usize]; 33 | reader.read_exact(&mut json_buffer)?; 34 | 35 | // parse json 36 | let json: Value = serde_json::from_slice(&json_buffer)?; 37 | let compressed = if let Some(compressed) = json.get("compress") { 38 | if let Some(compressed) = compressed.as_bool() { 39 | compressed 40 | } else { 41 | false 42 | } 43 | } else { 44 | false 45 | }; 46 | 47 | Ok((header_size + 8, json, compressed)) 48 | } 49 | 50 | /// Iterate over all entries in an asar archive. 51 | fn iterate_entries(json: &Value, mut callback: impl FnMut(&Value, &PathBuf)) { 52 | iterate_entries_err(json, |current, path| { 53 | callback(current, path); 54 | Ok(()) 55 | }) 56 | .expect("Unexpected error while iterating archive entries"); 57 | } 58 | 59 | /// Iterate over all entries in an asar archive while forwarding errors from the passed closure. 60 | fn iterate_entries_err( 61 | json: &Value, 62 | mut callback: impl FnMut(&Value, &PathBuf) -> Result<(), Error>, 63 | ) -> Result<(), Error> { 64 | fn helper( 65 | current: &Value, 66 | path: PathBuf, 67 | callback: &mut impl FnMut(&Value, &PathBuf) -> Result<(), Error>, 68 | ) -> Result<(), Error> { 69 | callback(current, &path)?; 70 | if current["files"] != Value::Null { 71 | for (key, val) in current["files"].as_object().unwrap() { 72 | helper(&val, path.join(key), callback)?; 73 | } 74 | } 75 | Ok(()) 76 | } 77 | for (key, val) in json["files"].as_object().unwrap() { 78 | helper(val, PathBuf::new().join(key), &mut callback)?; 79 | } 80 | Ok(()) 81 | } 82 | 83 | /// Get a list of all files in an asar archive. 84 | /// 85 | /// # Examples 86 | /// 87 | /// ```no_run 88 | /// let file_entries = rasar::list("myarchive.asar").expect("Something went wrong"); 89 | /// ``` 90 | pub fn list(archive: &str) -> Result, io::Error> { 91 | let mut file = File::open(archive)?; 92 | 93 | // read header 94 | let (_, json, _) = read_header(&mut file)?; 95 | 96 | // list files 97 | let mut files = vec![]; 98 | iterate_entries(&json, |_, path| files.push(path.clone())); 99 | 100 | Ok(files) 101 | } 102 | 103 | /// Pack a directory into an asar archive. 104 | /// 105 | /// # Examples 106 | /// 107 | /// level 0-21, 0 is nocompress 108 | /// 109 | /// ```no_run 110 | /// match rasar::pack("myfolder", "myarchive.asar", 0) { 111 | /// Ok(()) => println!("Success!"), 112 | /// Err(err) => panic!("This should not have happened!") 113 | /// } 114 | /// ``` 115 | pub fn pack(path: &str, dest: &str, level: i32) -> Result<(), Error> { 116 | let mut header_json = json!({ 117 | "files": {}, 118 | "compress": if level == 0 {false} else {true} 119 | }); 120 | let tmp_file_name = format!(".{}", dest); 121 | let mut tmp_file = fs::File::create(&tmp_file_name)?; 122 | let dir = PathBuf::from(path); 123 | 124 | if fs::try_exists(&path).unwrap() { 125 | fn walk_dir( 126 | dir: impl AsRef, 127 | json: &mut Value, 128 | mut offset: &mut usize, 129 | level: i32, 130 | mut archive: &mut File, 131 | ) -> Result<(), Error> { 132 | for entry in fs::read_dir(dir)? { 133 | let entry = entry?; 134 | let name = entry 135 | .file_name() 136 | .into_string() 137 | .expect("Error converting OS path to string"); 138 | let meta = entry.metadata()?; 139 | let entry_path = entry.path(); 140 | if meta.is_file() { 141 | if meta.len() > MAX_SIZE { 142 | panic!( 143 | "File {} is above the maximum possible size of {} GB", 144 | name, 145 | MAX_SIZE as f64 / 1e9 146 | ); 147 | } 148 | let mut buf = vec![]; 149 | let size; 150 | if level == 0 { 151 | io::copy(&mut File::open(entry_path)?, &mut archive)?; 152 | size = meta.len() as usize; 153 | } else { 154 | // encoder; 155 | zstd::stream::copy_encode(&mut File::open(entry_path)?, &mut buf, level)?; 156 | size = archive.write(&buf)?; 157 | } 158 | json[&name] = json!({ 159 | "offset": offset, 160 | "size": size 161 | }); 162 | *offset += size; 163 | } else { 164 | json[&name] = json!({ 165 | "files": {} 166 | }); 167 | walk_dir( 168 | entry_path, 169 | &mut json[&name]["files"], 170 | &mut offset, 171 | level, 172 | &mut archive, 173 | )? 174 | } 175 | } 176 | Ok(()) 177 | } 178 | walk_dir(dir, &mut header_json["files"], &mut 0, level, &mut tmp_file)?; 179 | } else { 180 | panic!("No such file or directory {}!", path); 181 | } 182 | 183 | // create header buffer with json 184 | let mut header = serde_json::to_vec(&header_json)?; 185 | 186 | // compute sizes 187 | let json_size = header.len(); 188 | let size = align_size(json_size); 189 | 190 | // resize header 191 | header.resize(16 + size, 0); 192 | 193 | // copy json 194 | header.copy_within(0..json_size, 16); 195 | 196 | // write sizes into header 197 | write_u32(&mut header[0..4], 4); 198 | write_u32(&mut header[4..8], 8 + size as u32); 199 | write_u32(&mut header[8..12], 4 + size as u32); 200 | write_u32(&mut header[12..16], json_size as u32); 201 | 202 | let mut archive = fs::File::create(dest)?; 203 | // write header 204 | archive.write(&header)?; 205 | // write body 206 | io::copy(&mut File::open(&tmp_file_name)?, &mut archive)?; 207 | // remove tmp file 208 | fs::remove_file(tmp_file_name)?; 209 | 210 | Ok(()) 211 | } 212 | 213 | /// Extract all files from an asar archive. 214 | /// 215 | /// # Examples 216 | /// 217 | /// ```no_run 218 | /// match rasar::extract("myarchive.asar", "extracted") { 219 | /// Ok(()) => println!("Success!"), 220 | /// Err(err) => panic!("This should not have happened!") 221 | /// } 222 | /// ``` 223 | pub fn extract(archive: &str, dest: &str) -> Result<(), Error> { 224 | let mut file = File::open(archive)?; 225 | 226 | // read header 227 | let (header_size, json, compressed) = read_header(&mut file)?; 228 | 229 | // create destination folder 230 | let dest = PathBuf::from(dest); 231 | if !dest.exists() { 232 | fs::create_dir(&dest)?; 233 | } 234 | 235 | // file.seek(SeekFrom::Start(header_size as u64 + offset))?; 236 | // let a = file.read_to_end(&mut vec![])?; 237 | // println!("{} {} {}", a, header_size as u64 + offset, offset); 238 | 239 | // iterate over entries 240 | iterate_entries_err(&json, |val, path| { 241 | if val["offset"] != Value::Null { 242 | let offset = val.get("offset").unwrap().as_u64().unwrap(); 243 | let size = val.get("size").unwrap().as_u64().unwrap(); 244 | let mut buffer = vec![0u8; size as usize]; 245 | file.seek(SeekFrom::Start(header_size as u64 + offset))?; 246 | file.read_exact(&mut buffer)?; 247 | if compressed { 248 | zstd::stream::copy_decode( 249 | &mut buffer.reader(), 250 | &mut fs::File::create(dest.join(path))?, 251 | )?; 252 | } else { 253 | fs::write(dest.join(path), buffer)?; 254 | }; 255 | } else { 256 | let dir = dest.join(path); 257 | if !dir.exists() { 258 | fs::create_dir(dir)?; 259 | } 260 | } 261 | Ok(()) 262 | })?; 263 | 264 | Ok(()) 265 | } 266 | 267 | /// Extract a single file from an asar archive. 268 | /// 269 | /// # Examples 270 | /// 271 | /// ```no_run 272 | /// match rasar::extract("myarchive.asar", "file.txt") { 273 | /// Ok(()) => println!("Success!"), 274 | /// Err(err) => panic!("This should not have happened!") 275 | /// } 276 | /// ``` 277 | pub fn extract_file(archive: &str, dest: &str) -> Result<(), Error> { 278 | let cwd = env::current_dir()?; 279 | let full_path = cwd.join(dest); 280 | let dest = cwd.join(Path::new(dest).file_name().unwrap()); 281 | let mut file = File::open(archive)?; 282 | 283 | // read header 284 | let (header_size, json, compressed) = read_header(&mut file)?; 285 | 286 | // iterate over entries 287 | iterate_entries_err(&json, |val, path| { 288 | if cwd.join(path) == full_path { 289 | let offset = val.get("offset").unwrap().as_u64().unwrap(); 290 | let size = val.get("size").unwrap().as_u64().unwrap(); 291 | let mut buffer = vec![0u8; size as usize]; 292 | file.seek(SeekFrom::Start(header_size as u64 + offset))?; 293 | file.read_exact(&mut buffer)?; 294 | if compressed { 295 | zstd::stream::copy_decode(&mut buffer.reader(), &mut fs::File::create(&dest)?)?; 296 | } else { 297 | fs::write(&dest, buffer)?; 298 | }; 299 | } 300 | Ok(()) 301 | })?; 302 | 303 | Ok(()) 304 | } 305 | -------------------------------------------------------------------------------- /README-EN.md: -------------------------------------------------------------------------------- 1 | # rubickbase 2 | 3 | Based on Rust / WASM, a modern asynchronous Nodejs module that provides cross-platform functions such as screenshots, color picking, keyboard and mouse event monitoring simulation, image processing, and access to installed applications. It occupies a small space, is easy to install, simple to use, high performance, and consumes very little resources. , Can replace iohook and robotjs 4 | 5 | ## Features 6 | 7 | **Device event listening and simulation** 8 | 9 | - [x] Get mouse position 10 | - [x] Keyboard and mouse event monitoring 11 | - [x] Keyboard event simulation 12 | - [x] Mouse event simulation 13 | - [x] Subscribe to shortcut key events 14 | 15 | **Image and Screen** 16 | 17 | - [x] Screenshot 18 | - [x] Get mouse pixel color (main screen) 19 | - [x] Image zoom 20 | - [x] Picture color selection 21 | - [x] Picture cropping 22 | - [x] Multiple screenshots 23 | 24 | **System info** 25 | 26 | - [x] Get the list of installed applications (linux✅/macos✅/windows✅) 27 | - [x] Get detailed information of installed applications (linux✅) 28 | - [x] Get system language 29 | 30 | **Other tools** 31 | 32 | - [x] asar package compression and decompression (zstd algorithm) 33 | 34 | ## Install 35 | 36 | Unlike iohook and robotjs, you don't need to recompile tediously for different versions, everything works out of the box 37 | 38 | Whether you are in node or electron, you can install it directly with your favorite package manager: 39 | 40 | ``` 41 | # npm 42 | npm install --save rubickbase 43 | 44 | # yarn 45 | yarn add rubickbase 46 | 47 | # pnpm 48 | pnpm add rubickbase 49 | ``` 50 | 51 |
52 | Notes 53 | 54 | rubickbase is based on [N-API](https://nodejs.org/api/n-api.html) v6, so the following versions are recommended for Nodejs environment 55 | 56 | v10.x ,v12.x ,14.x, 15.x, **16.x** 57 | 58 | Electron environment recommends the following versions 59 | 60 | v13.x,v14.x ,**v15.x** ,16.x 61 | 62 |
63 | 64 | ## Quick start 65 | 66 | ### Introducing dependencies 67 | 68 | rubickbase supports both cjs and esm specifications, of course you can and recommend using it in TypeScript 69 | 70 | ```js 71 | // cjs 72 | const { newRubickBase } = require('rubickbase') 73 | // esm / typescript 74 | import { newRubickBase } from 'rubickbase' 75 | ``` 76 | 77 | ### Basic usage 78 | 79 | In this example, you get the rubickbase service instance through `newRubickbase`, you can get all the functions of rubickbase through `getAPI` 80 | 81 | Here get the current mouse position every second 82 | 83 | ```js 84 | const { newRubickBase } = require('rubickbase') 85 | 86 | // init rubickbase 87 | const rubickBase = newRubickBase() 88 | 89 | setInterval(async () => { 90 | // start rubickbase and get APIs 91 | const api = await rubickBase.getAPI() 92 | // print Cursor Position 93 | console.log(api.getCursorPosition()) 94 | }, 1000) 95 | ``` 96 | 97 |
98 | Optional initialization parameters 99 | 100 | | Parameter name | Parameter meaning | Type | 101 | | --------------- | --------------------------------------------------- | ------------- | 102 | | port | Port of the GRPC server | number | 103 | | logger | Logger | Logger | 104 | | tmpdir | Temporary file directory | string | 105 | | workerBoot | Whether to start workers together | boolean | 106 | | ioEventCallback | Callback function that listens to all device events | EventCallback | 107 | 108 |
109 | 110 |
111 | Advanced startup 112 | 113 | rubickbase is run by a combination of the GRPC server master and multiple workers that provide different functions 114 | 115 | Generally speaking, when you call `getAPI`, rubickbase will automatically turn on all services, but if you need to run them in a different place or time, you can manually control their life cycle to achieve more refined control 116 | 117 | First of all, you need to choose not to start the workers when the master starts. At this time, the master will listen to messages from the workers. 118 | 119 | ```js 120 | // init rubickbase 121 | const rubickBase = newRubickBase({ workerBoot: false }) 122 | rubickBase.start() 123 | ``` 124 | 125 | Then manually start workers where needed 126 | 127 | ```js 128 | const rubickWorker = newRubickWorker() 129 | // start all workers 130 | rubickWorker.start() 131 | // Start ioio worker separately 132 | rubickWorker.start('ioio') 133 | ``` 134 | 135 | Note that the life cycle (existence time) of the worker must be shorter than that of the master, otherwise the GRPC client in the worker will throw an exception that the server cannot be found 136 | 137 | And if you change the port when starting the master, then pass the port to the worker 138 | 139 | ```js 140 | // init rubickbase 141 | const rubickBase = newRubickBase({ port: 8001, workerBoot: false }) 142 | rubickBase.start() 143 | // then 144 | const rubickWorker = newRubickWorker({ port: 8001 }) 145 | rubickWorker.start() 146 | ``` 147 | 148 |
149 | 150 |
151 | Use the underlying stateless API directly 152 | 153 | Allows you to directly call some basic APIs without starting the master and workers 154 | 155 | ```js 156 | const { 157 | language, 158 | sendEvent, 159 | getInstalledApps, 160 | screenCapture, 161 | screenCaptureAll, 162 | screenCaptureAroundPosition, 163 | } = await newRubickBase().getBasicAPI() 164 | ``` 165 | 166 |
167 | 168 | ### Device input event simulation 169 | 170 | It is very simple to simulate mouse and keyboard input events, just call `sendEvent` 171 | 172 | Since rubickbase is written in TypeScript, the editor will automatically prompt when writing Event 173 | 174 | ```js 175 | // This will simulate pressing the F1 key 176 | api.sendEvent({ 177 | device: 'KeyBoard', 178 | action: 'Press', 179 | info: 'F1', 180 | }) 181 | 182 | // This will simulate pressing the middle mouse button 183 | api.sendEvent({ 184 | device: 'Mouse', 185 | action: 'Press', 186 | info: 'Middle', 187 | }) 188 | ``` 189 | 190 | ### Device input event listening 191 | 192 | Create a target event channel through the `setEventChannel` API, and get the subscriber of the corresponding event 193 | 194 | ```js 195 | // Created here to monitor the left mouse button channel 196 | const register = api.setEventChannel({ 197 | device: 'Mouse', 198 | action: 'Press', 199 | info: 'Left', 200 | }) 201 | 202 | // View all currently created event channels 203 | console.log(api.allEventChannels()) 204 | 205 | // Register the printing function through `registerHook` 206 | register('myeventchannel', async (e) => { 207 | console.log(e) 208 | }) 209 | 210 | // delete event channel 211 | api.delEventChannel('myeventchannel') 212 | 213 | console.log(api.hasEventChannel('myeventchannel'), api.allEventChannels()) 214 | ``` 215 | 216 |
217 | Retrieve and close channels 218 | 219 | `allEventChannels` can get all existing event channels 220 | 221 | `hasEventChannel` can determine whether there is a channel with a certain name 222 | 223 | `delEventChannel` can delete the created event channel 224 | 225 |
226 | 227 | ### Event fuzzy matching 228 | 229 | A device event has three constraints: `device` `action` `info`, you can remove any of these conditions to complete event fuzzy matching 230 | 231 | ```js 232 | // Match the press event of the left mouse button 233 | api.setEventChannel({ 234 | device: 'Mouse', 235 | action: 'Press', 236 | info: 'Left', 237 | }) 238 | 239 | // Match the mouse movement event 240 | api.setEventChannel({ 241 | device: 'Mouse', 242 | action: 'Move', 243 | }) 244 | 245 | // Match the press event of all the mouse buttons 246 | api.setEventChannel({ 247 | device: 'Mouse', 248 | action: 'Press', 249 | }) 250 | 251 | // Match all key press events of all devices 252 | api.setEventChannel({ 253 | action: 'Press', 254 | }) 255 | ``` 256 | 257 | ### Image Processing 258 | 259 | rubickbase is based on the high-performance WASM module of [Photon](https://silvia-odwyer.github.io/photon/) for image processing 260 | 261 | 1. Take color Image.colorAt 262 | 263 | ```js 264 | const color = img.colorAt({ x: 1, y: 1 }) 265 | ``` 266 | 267 | 2. Resize Image.resize 268 | 269 | Input width and height, output scaled image 270 | 271 | ```js 272 | const newImg = img.resize(100, 100) 273 | ``` 274 | 275 |
276 | Optional scaling algorithm 277 | 278 | The default nearest neighbor difference algorithm, the image results of other algorithms have smoother edges, you can choose according to your needs 279 | 280 | Nearest Neighbor Difference Algorithm = 1, Binary Finding Algorithm = 2, CatmullRom Interpolation Algorithm = 3, Gaussian Algorithm = 4, Interpolation Algorithm = 5 281 | 282 | ```js 283 | const newImg = img.resize(100, 100, 1) 284 | ``` 285 | 286 |
287 | 288 | 3. Crop Image.crop 289 | 290 | Input the point, width and height of the upper left corner, and output the cropped image 291 | 292 | ```js 293 | const newImg = img.crop({ x: 5, y: 5 }, 10, 10) 294 | ``` 295 | 296 | ### Features at a glance 297 | 298 | Rubickbase also has the following features: 299 | 300 | 1. Get the current coordinates of the mouse 301 | getCursorPosition: () => Position 302 | 303 | 2. Get the pixel value of the current coordinates of the mouse 304 | _This API is only available for the home screen_ 305 | getCursorPositionPixelColor: () => Promise< Color> 306 | 307 | 3. Main screen screenshot 308 | screenCapture: () => Promise< Image> 309 | 310 | 4. All screenshots 311 | screenCaptureAll: () => Promise< Image[]> 312 | 313 | 5. Get the image around the mouse 314 | _This API is only available for the home screen_ 315 | screenCaptureAroundPosition: (position: Position, width: number, height: number) => Promise< Image> 316 | 317 | 6. Get the list of installed applications in the system 318 | getInstalledApps: (getDetailInfo: boolean = false, extraDirs?: Array< string >) => Promise< string> 319 | 320 | `getDetailInfo` Whether to obtain application detailed information. Default no (currently only available on Linux) 321 | `extraDirs` additional directories to be scanned 322 | `extraDirs` additional directories to be scanned 323 | `extraDirs` additional directories to be scanned 324 | Return a list of shortcut paths in JSON format. If getDetailInfo is true, then return a list of application details 325 | 326 |
327 | Application details field explanation 328 | 329 | name: name 330 | icon_path: list of icons of various sizes 331 | icon_path: list of icons of various sizes 332 | icon_path: list of icons of various sizes 333 | description: application description 334 | description: application description 335 | description: application description 336 | command: application start command 337 | command: application start command 338 | command: application start command 339 | desktop_entry_path: shortcut path 340 | 341 |
342 | 343 |
344 | Scanning principle 345 | 346 | Scan the directory where the system stores the shortcuts to get all the applications installed in the system, including the scan format: 347 | 348 | | Platform | Suffix | 349 | | -------- | ------------ | 350 | | linux | desktop | 351 | | macos | app,prefPane | 352 | | windows | lnk | 353 | 354 |
355 | 356 | 7. Get system language 357 | language: () => Promise< string> 358 | 359 | 8. asar + zstd compression 360 | 361 | It is a superset of electron's official asar format, and zstd compression algorithm is added when packaging 362 | 363 | asarList(path: string): Promise< Array | undefined> 364 | asarExtractFile(path: string, dest: string): Promise< undefined> 365 | asarExtract(path: string, dest: string): Promise< undefined> 366 | asarPack(path: string, dest: string, level?: number): Promise< undefined> 367 | 368 | ## Contribution and contact 369 | 370 | Any kind of contribution and open source collaboration are welcome! 371 | 372 | The project depends on the `pnpm` package manager, you need to install it first 373 | 374 | `npm install -g pnpm` 375 | 376 | The project adopts fully automated code inspection and construction, and you can use the following commands to develop 377 | 378 | | Action | Command | 379 | | ------- | ---------------- | 380 | | Install | · `pnpm i` | 381 | | Build | · `pnpm build` | 382 | | Commit | · `pnpm commit` | 383 | | Release | · `pnpm release` | 384 | 385 | After paying attention to the official account, send the `contact` keyword to add me on WeChat: 386 | 387 | ![wechat](https://z3.ax1x.com/2021/09/26/4yRpN9.jpg) 388 | 389 | ## LISENCE 390 | 391 | MPLv2 392 | -------------------------------------------------------------------------------- /packages/rubickbase/src/index.ts: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import Mali from 'mali' 3 | import { 4 | Logger, 5 | RubickBaseSettings, 6 | DeviceEvent, 7 | Position, 8 | Color, 9 | WorkerSettings, 10 | BasicApi, 11 | KeyBoardEvent, 12 | MouseClickEvent, 13 | MouseMoveEvent, 14 | MouseWheelEvent, 15 | } from './types' 16 | import newRustBackend, { RustBackendAPI } from './backend' 17 | import { loadPackageDefinition } from '@grpc/grpc-js' 18 | import { fromJSON } from '@grpc/proto-loader' 19 | import { INamespace } from 'protobufjs' 20 | import fs from 'fs-extra' 21 | import { eventEqual, rgbToHex, tryPort } from './utils' 22 | import { defaultLogger } from './logger' 23 | import { deviceEventEmitter, EventCallback, EventChannelMap } from './event' 24 | import { newImageFromBase64, Image } from './image' 25 | import { RubickWorker } from './worker' 26 | 27 | export class RubickBase { 28 | private server!: Mali 29 | private rustBackend!: RustBackendAPI 30 | private basicAPI!: BasicApi 31 | private port: number 32 | private tmpdir: string 33 | private eventChannels: EventChannelMap 34 | private cursorPosition: Position = { x: 1, y: 1 } 35 | private workerBoot: boolean 36 | private ioEventCallback: EventCallback 37 | private started: boolean = false 38 | logger: Logger 39 | constructor(settings: RubickBaseSettings) { 40 | const { port, logger, tmpdir, workerBoot, ioEventCallback } = settings 41 | // settings 42 | this.port = port || 50068 43 | this.logger = logger || defaultLogger 44 | this.tmpdir = tmpdir || os.tmpdir() 45 | this.eventChannels = new EventChannelMap(this.logger) 46 | this.workerBoot = workerBoot !== undefined ? workerBoot : true 47 | this.ioEventCallback = ioEventCallback || ((_) => {}) 48 | } 49 | 50 | // ******************************* life cycle ******************************* 51 | async start() { 52 | if (this.started) { 53 | this.logger.error('Rubickbase has already started!') 54 | return 55 | } 56 | this.port = await tryPort(this.port) 57 | 58 | // start buitin service 59 | this.basicAPI = await this.getBasicAPI() 60 | this.server = new Mali(await this.loadProto(), 'Rubick') 61 | 62 | this.server.use('ioio', async (ctx: any) => { 63 | const event: DeviceEvent = ctx.request.req 64 | // mousemove info is still string here, need convert to Position object 65 | if ( 66 | event.device === 'Mouse' && 67 | event.action === 'Move' && 68 | ((event.info)).startsWith('{') 69 | ) { 70 | event.info = JSON.parse((event.info)) 71 | } 72 | // post event to global event channel 73 | deviceEventEmitter.emit('deviceEvent', event) 74 | ctx.res = { ok: true } 75 | }) 76 | 77 | // handle async event callback error 78 | deviceEventEmitter.on('error', (err) => { 79 | this.logger.error(err) 80 | }) 81 | 82 | // global listen event 83 | deviceEventEmitter.on('deviceEvent', async (event) => { 84 | if (this.ioEventCallback) await this.ioEventCallback(event) 85 | if (event.device === 'Mouse' && event.action === 'Move') { 86 | this.cursorPosition = event.info 87 | } 88 | }) 89 | 90 | await this.server.start(`127.0.0.1:${this.port}`) 91 | // bootstrap worker with rubickbase 92 | if (this.workerBoot) { 93 | await newRubickWorker({ 94 | port: this.port, 95 | logger: this.logger, 96 | }).start() 97 | } 98 | this.started = true 99 | } 100 | 101 | async close() { 102 | deviceEventEmitter.removeAllListeners() 103 | this.started = false 104 | await this.server.close() 105 | } 106 | 107 | // ******************************* Utils ******************************* 108 | private async loadProto(): Promise { 109 | let proto: string | object = './proto/rubick.proto' 110 | try { 111 | const protoJSON = await import('./proto/rubick.proto') 112 | proto = loadPackageDefinition(fromJSON(protoJSON as INamespace)) 113 | this.logger.info('You are in production mode, protoJSON loaded.') 114 | } catch {} 115 | return proto 116 | } 117 | 118 | // try rust-backend or log error 119 | private async tryBackend(func: () => Promise, errorReturn: () => T): Promise { 120 | try { 121 | return await func() 122 | } catch (error) { 123 | this.logger.error(error) 124 | return errorReturn() 125 | } 126 | } 127 | 128 | // valid directory and file then try rust-backend 129 | private async validAndTryBackend( 130 | func: () => Promise, 131 | errorReturn: () => T, 132 | dic: string[] | string = [], 133 | file: string[] | string = [], 134 | ): Promise { 135 | if (typeof dic === 'string') { 136 | dic = [dic] 137 | } 138 | if (typeof file === 'string') { 139 | file = [file] 140 | } 141 | let v1 = dic.map((dic) => fs.existsSync(dic) && fs.lstatSync(dic).isDirectory()) 142 | let v2 = file.map((path) => fs.existsSync(path) && fs.lstatSync(path).isFile()) 143 | let v = [...v1, ...v2] 144 | if (!v.includes(false)) { 145 | return await this.tryBackend(func, errorReturn) 146 | } else { 147 | this.logger.error('No such directory!') 148 | return errorReturn() 149 | } 150 | } 151 | 152 | // ******************************* errors ******************************* 153 | private colorError() { 154 | this.logger.error('Got an api color error!') 155 | return undefined 156 | } 157 | 158 | private imageError() { 159 | this.logger.error('Got an api image error!') 160 | return undefined 161 | } 162 | 163 | private appSearchError() { 164 | this.logger.error('Got an api app search error!') 165 | return undefined 166 | } 167 | 168 | private simulationError() { 169 | this.logger.error('Got an api simulation error!') 170 | return undefined 171 | } 172 | 173 | private getLanguageError() { 174 | this.logger.error('Got an api get language error!') 175 | return undefined 176 | } 177 | 178 | private asarError() { 179 | this.logger.error('Got an api asar error!') 180 | return undefined 181 | } 182 | 183 | // ******************************* expose APIs ******************************* 184 | async getAPI() { 185 | if (!this.started) await this.start() 186 | 187 | /** get cursor position 188 | * 189 | * @returns {Position} 190 | */ 191 | const getCursorPosition = (): Position => this.cursorPosition 192 | 193 | /** get pixel color at cursor position 194 | * 195 | * @return {Promise} color object 196 | */ 197 | const getCursorPositionPixelColor = async (): Promise => 198 | await this.tryBackend(async () => { 199 | const rgb = await this.rustBackend.screenColorPicker(getCursorPosition()) 200 | return { 201 | hex16: rgbToHex(rgb.r, rgb.g, rgb.b), 202 | rgba: { 203 | r: rgb.r, 204 | g: rgb.g, 205 | b: rgb.b, 206 | a: 255, 207 | }, 208 | } 209 | }, this.colorError) 210 | 211 | /** set a channel and get register 212 | * 213 | * @param bindEvent 214 | * @returns register - register a hook; 215 | */ 216 | const setEventChannel = (bindEvent: DeviceEvent) => { 217 | const register = (name: string, hook: EventCallback) => { 218 | const registerHook = (hook: EventCallback) => { 219 | const listener = async (deviceEvent: typeof bindEvent) => { 220 | if (eventEqual(deviceEvent, bindEvent)) await hook(deviceEvent) 221 | } 222 | 223 | // register in map 224 | this.eventChannels.set(name, listener) 225 | 226 | // hook callback 227 | deviceEventEmitter.on('deviceEvent', listener) 228 | } 229 | registerHook(hook) 230 | } 231 | 232 | // return register 233 | return register 234 | } 235 | 236 | /** get all channels 237 | * 238 | * @returns {IterableIterator} channels name 239 | */ 240 | const allEventChannels = (): IterableIterator => { 241 | return this.eventChannels.keys() 242 | } 243 | 244 | /** has channel or not 245 | * 246 | * @param name channel name 247 | * @returns {boolean} 248 | */ 249 | const hasEventChannel = (name: string): boolean => { 250 | return this.eventChannels.has(name) 251 | } 252 | 253 | /** del a channel 254 | * 255 | */ 256 | const delEventChannel = (name: string) => { 257 | if (this.eventChannels.has(name)) { 258 | // remove listener 259 | const listener = this.eventChannels.get(name) 260 | if (listener) deviceEventEmitter.removeListener('deviceEvent', listener) 261 | // remove register item 262 | this.eventChannels.delete(name) 263 | } else { 264 | this.logger.error(`no such handler: ${name}`) 265 | } 266 | } 267 | 268 | return { 269 | ...this.basicAPI, 270 | // ioio worker 271 | getCursorPosition, 272 | getCursorPositionPixelColor, 273 | setEventChannel, 274 | allEventChannels, 275 | hasEventChannel, 276 | delEventChannel, 277 | } 278 | } 279 | 280 | // these apis can work without any workers 281 | async getBasicAPI() { 282 | this.rustBackend = await newRustBackend() 283 | 284 | /** input simulation 285 | * 286 | * @param event device event to send 287 | * @returns {Promise} 288 | */ 289 | const sendEvent = async (event: DeviceEvent): Promise => 290 | await this.tryBackend( 291 | async () => await this.rustBackend.sendEvent(event), 292 | this.simulationError, 293 | ) 294 | 295 | /** get installed app or app detail info 296 | * 297 | * @param getDetailInfo get app detail info rather than app entry default false 298 | * @param extraDirs extra dirs to scan 299 | * @returns {Promise} 300 | */ 301 | const getInstalledApps = async ( 302 | getDetailInfo: boolean = false, 303 | extraDirs?: Array, 304 | ): Promise => 305 | await this.validAndTryBackend( 306 | async () => await this.rustBackend.getInstalledApps(getDetailInfo, extraDirs), 307 | this.appSearchError, 308 | extraDirs, 309 | ) 310 | 311 | /** capture primary screen 312 | * 313 | * @returns {Promise} image object 314 | */ 315 | const screenCapture = async (): Promise => 316 | await this.tryBackend(async () => { 317 | const imgBase64 = await this.rustBackend.captureToBase64() 318 | return newImageFromBase64(imgBase64) 319 | }, this.imageError) 320 | 321 | /** capture all screen 322 | * 323 | * @returns {Promise | undefined>} image object 324 | */ 325 | const screenCaptureAll = async (): Promise | undefined> => 326 | await this.tryBackend(async () => { 327 | const captures = await this.rustBackend.captureAllToBase64() 328 | return captures.map((capture) => newImageFromBase64(capture)) 329 | }, this.imageError) 330 | 331 | /** capture screen return the area around position 332 | * 333 | * @param position center of the image 334 | * @param width width 335 | * @param height height 336 | * @returns {Promise} image object 337 | */ 338 | const screenCaptureAroundPosition = async ( 339 | position: Position, 340 | width: number, 341 | height: number, 342 | ): Promise => 343 | await this.tryBackend(async () => { 344 | const imgBase64 = await this.rustBackend.screenCaptureAroundPositionToBase64( 345 | position, 346 | width, 347 | height, 348 | ) 349 | return newImageFromBase64(imgBase64) 350 | }, this.imageError) 351 | 352 | /** list all files in asar 353 | * 354 | * @param path asar path 355 | * @returns files path 356 | */ 357 | const asarList = async (path: string) => 358 | await this.validAndTryBackend( 359 | async () => await this.rustBackend.asarList(path), 360 | this.asarError, 361 | [], 362 | [path], 363 | ) 364 | 365 | /** asar extract all file 366 | * 367 | * @param path asar path 368 | * @param dest folder name 369 | */ 370 | const asarExtract = async (path: string, dest: string) => 371 | await this.validAndTryBackend( 372 | async () => await this.rustBackend.asarExtract(path, dest), 373 | this.asarError, 374 | [], 375 | [path], 376 | ) 377 | 378 | /** asar extract one file 379 | * 380 | * @param path asar path 381 | * @param dest file name 382 | */ 383 | const asarExtractFile = async (path: string, dest: string) => 384 | await this.validAndTryBackend( 385 | async () => await this.rustBackend.asarExtractFile(path, dest), 386 | this.asarError, 387 | [], 388 | [path], 389 | ) 390 | 391 | /** asar pack with zstd compress 392 | * 393 | * @param path asar path 394 | * @param dest output file path 395 | * @param level compress leve 0-21 default 0 396 | */ 397 | const asarPack = async (path: string, dest: string, level?: number) => 398 | await this.validAndTryBackend( 399 | async () => await this.rustBackend.asarPack(path, dest, level), 400 | this.asarError, 401 | [path], 402 | [], 403 | ) 404 | 405 | /** lzma compress 406 | * @param fromPath from file 407 | * @param toPath to file 408 | */ 409 | // const compress = async (fromPath: string, toPath: string) => 410 | // await this.validAndTryBackend( 411 | // async () => await this.rustBackend.compress(fromPath, toPath), 412 | // this.lzmaError, 413 | // [], 414 | // [fromPath], 415 | // ) 416 | 417 | /** lzma decompress 418 | * @param fromPath from file 419 | * @param toPath to file 420 | */ 421 | // const decompress = async (fromPath: string, toPath: string) => 422 | // await this.validAndTryBackend( 423 | // async () => await this.rustBackend.decompress(fromPath, toPath), 424 | // this.lzmaError, 425 | // [], 426 | // [fromPath], 427 | // ) 428 | 429 | /** get system locale language 430 | * 431 | * @returns system language 432 | */ 433 | const language = async () => 434 | await this.tryBackend( 435 | async () => await this.rustBackend.language(), 436 | this.getLanguageError, 437 | ) 438 | 439 | return { 440 | asarList, 441 | asarExtract, 442 | asarExtractFile, 443 | asarPack, 444 | screenCaptureAll, 445 | language, 446 | sendEvent, 447 | getInstalledApps, 448 | screenCapture, 449 | screenCaptureAroundPosition, 450 | } 451 | } 452 | } 453 | 454 | /** A new rubickbase service 455 | * 456 | * @param settings RubickBaseSettings 457 | * @returns {RubickBase} 458 | */ 459 | export const newRubickBase = (settings?: RubickBaseSettings): RubickBase => { 460 | return new RubickBase(settings || {}) 461 | } 462 | 463 | /** A new rubickworker client 464 | * 465 | * @param settings WorkerSettings 466 | * @returns {RubickBase} 467 | */ 468 | export const newRubickWorker = (settings?: WorkerSettings): RubickWorker => { 469 | return new RubickWorker(settings || {}) 470 | } 471 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /packages/rust-backend/src/ioio/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | pub mod devices; 3 | 4 | pub mod grpc_client { 5 | tonic::include_proto!("rubick"); 6 | } 7 | 8 | extern crate chrono; 9 | use chrono::prelude::*; 10 | use devices::DeviceEvent; 11 | use grpc_client::rubick_client::RubickClient; 12 | use grpc_client::DeviceEvent as GRPCDeviceEvent; 13 | use rdev::listen; 14 | use std::sync::mpsc; 15 | use std::thread; 16 | use tonic::transport::Channel; 17 | 18 | use crate::ioio::devices::mouse::{MouseEvent, MouseKey, MouseMove}; 19 | 20 | use self::devices::{ 21 | keyboard::{KeyBoardEvent, KeyBoardKey}, 22 | mouse::MouseWheel, 23 | }; 24 | 25 | pub struct Listener { 26 | timestamp: String, 27 | } 28 | 29 | impl Listener { 30 | fn start_listen(mut hook: T) 31 | where 32 | T: FnMut(DeviceEvent) + 'static, 33 | { 34 | if let Err(error) = listen(move |event| { 35 | let device_event = DeviceEvent::receive_from_keyboard_mouse_event(&event); 36 | hook(device_event); 37 | }) { 38 | println!("Error: {:?}", error) 39 | } 40 | } 41 | #[allow(dead_code)] 42 | pub fn new() -> Listener { 43 | Listener { 44 | timestamp: Local::now().to_string(), 45 | } 46 | } 47 | } 48 | 49 | trait Listen { 50 | fn start(&self, rubick: impl FnMut(DeviceEvent) + 'static); 51 | } 52 | 53 | impl Listen for Listener { 54 | fn start(&self, mut rubick: impl FnMut(DeviceEvent) + 'static) { 55 | Listener::start_listen(move |event| { 56 | rubick(event); 57 | }); 58 | } 59 | } 60 | 61 | // listen device send grpc event 62 | async fn send_event(client: &mut RubickClient) -> Result<(), Box> { 63 | let (tx, rx) = mpsc::channel(); 64 | thread::spawn(|| { 65 | Listener::new().start(move |event| { 66 | let request = match event { 67 | DeviceEvent::KeyBoardEvent(k) => match k { 68 | devices::keyboard::KeyBoardEvent::Press(k1) => { 69 | if let devices::keyboard::KeyBoardKey::Unknown(k2) = k1 { 70 | tonic::Request::new(GRPCDeviceEvent { 71 | device: String::from("KeyBoard"), 72 | action: String::from("Press"), 73 | info: k2.to_string(), 74 | }) 75 | } else { 76 | tonic::Request::new(GRPCDeviceEvent { 77 | device: String::from("KeyBoard"), 78 | action: String::from("Press"), 79 | info: format!("{:?}", k1), 80 | }) 81 | } 82 | } 83 | devices::keyboard::KeyBoardEvent::Release(k1) => { 84 | if let devices::keyboard::KeyBoardKey::Unknown(k2) = k1 { 85 | tonic::Request::new(GRPCDeviceEvent { 86 | device: String::from("KeyBoard"), 87 | action: String::from("Release"), 88 | info: k2.to_string(), 89 | }) 90 | } else { 91 | tonic::Request::new(GRPCDeviceEvent { 92 | device: String::from("KeyBoard"), 93 | action: String::from("Release"), 94 | info: format!("{:?}", k1), 95 | }) 96 | } 97 | } 98 | }, 99 | DeviceEvent::MouseEvent(m) => match m { 100 | devices::mouse::MouseEvent::Press(m1) => { 101 | if let devices::mouse::MouseKey::Unknown(m2) = m1 { 102 | tonic::Request::new(GRPCDeviceEvent { 103 | device: String::from("Mouse"), 104 | action: String::from("Press"), 105 | info: m2.to_string(), 106 | }) 107 | } else { 108 | tonic::Request::new(GRPCDeviceEvent { 109 | device: String::from("Mouse"), 110 | action: String::from("Press"), 111 | info: format!("{:?}", m1), 112 | }) 113 | } 114 | } 115 | devices::mouse::MouseEvent::Rlease(m1) => { 116 | if let devices::mouse::MouseKey::Unknown(m2) = m1 { 117 | tonic::Request::new(GRPCDeviceEvent { 118 | device: String::from("Mouse"), 119 | action: String::from("Rlease"), 120 | info: m2.to_string(), 121 | }) 122 | } else { 123 | tonic::Request::new(GRPCDeviceEvent { 124 | device: String::from("Mouse"), 125 | action: String::from("Rlease"), 126 | info: format!("{:?}", m1), 127 | }) 128 | } 129 | } 130 | devices::mouse::MouseEvent::Move(m1) => tonic::Request::new(GRPCDeviceEvent { 131 | device: String::from("Mouse"), 132 | action: String::from("Move"), 133 | info: m1.to_string(), 134 | }), 135 | devices::mouse::MouseEvent::Wheel(m1) => tonic::Request::new(GRPCDeviceEvent { 136 | device: String::from("Mouse"), 137 | action: String::from("Wheel"), 138 | info: format!("{:?}", m1), 139 | }), 140 | }, 141 | }; 142 | tx.send(request).expect("Send error"); 143 | }); 144 | }); 145 | for received in rx { 146 | client.ioio(received).await?; 147 | } 148 | Ok(()) 149 | } 150 | 151 | // start grpc client 152 | #[tokio::main] 153 | pub async fn start(port: &str) -> Result, Box> { 154 | let mut client = RubickClient::connect(format!("https://127.0.0.1:{}", port)).await?; 155 | send_event(&mut client).await?; 156 | Ok(client) 157 | } 158 | 159 | #[allow(dead_code)] 160 | pub enum Info { 161 | Button(String), 162 | UnknownButton(f64), 163 | Position { x: f64, y: f64 }, 164 | } 165 | 166 | #[allow(dead_code)] 167 | pub fn send(device: &str, action: &str, info: &Info) { 168 | let event = match device { 169 | "KeyBoard" => match action { 170 | "Press" => match info { 171 | Info::Button(k) => match k.as_str() { 172 | "Alt" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 173 | KeyBoardKey::Alt, 174 | ))), 175 | "AltGr" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 176 | KeyBoardKey::AltGr, 177 | ))), 178 | "Backspace" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 179 | KeyBoardKey::Backspace, 180 | ))), 181 | "CapsLock" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 182 | KeyBoardKey::CapsLock, 183 | ))), 184 | "ControlLeft" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 185 | KeyBoardKey::ControlLeft, 186 | ))), 187 | "ControlRight" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 188 | KeyBoardKey::ControlRight, 189 | ))), 190 | "Delete" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 191 | KeyBoardKey::Delete, 192 | ))), 193 | "DownArrow" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 194 | KeyBoardKey::DownArrow, 195 | ))), 196 | "End" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 197 | KeyBoardKey::End, 198 | ))), 199 | "Escape" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 200 | KeyBoardKey::Escape, 201 | ))), 202 | "F1" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 203 | KeyBoardKey::F1, 204 | ))), 205 | "F10" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 206 | KeyBoardKey::F10, 207 | ))), 208 | "F11" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 209 | KeyBoardKey::F11, 210 | ))), 211 | "F12" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 212 | KeyBoardKey::F12, 213 | ))), 214 | "F2" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 215 | KeyBoardKey::F2, 216 | ))), 217 | "F3" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 218 | KeyBoardKey::F3, 219 | ))), 220 | "F4" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 221 | KeyBoardKey::F4, 222 | ))), 223 | "F5" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 224 | KeyBoardKey::F5, 225 | ))), 226 | "F6" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 227 | KeyBoardKey::F6, 228 | ))), 229 | "F7" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 230 | KeyBoardKey::F7, 231 | ))), 232 | "F8" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 233 | KeyBoardKey::F8, 234 | ))), 235 | "F9" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 236 | KeyBoardKey::F9, 237 | ))), 238 | "Home" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 239 | KeyBoardKey::Home, 240 | ))), 241 | "LeftArrow" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 242 | KeyBoardKey::LeftArrow, 243 | ))), 244 | "MetaLeft" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 245 | KeyBoardKey::MetaLeft, 246 | ))), 247 | "MetaRight" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 248 | KeyBoardKey::MetaRight, 249 | ))), 250 | "PageDown" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 251 | KeyBoardKey::PageDown, 252 | ))), 253 | "PageUp" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 254 | KeyBoardKey::PageUp, 255 | ))), 256 | "Return" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 257 | KeyBoardKey::Return, 258 | ))), 259 | "RightArrow" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 260 | KeyBoardKey::RightArrow, 261 | ))), 262 | "ShiftLeft" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 263 | KeyBoardKey::ShiftLeft, 264 | ))), 265 | "ShiftRight" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 266 | KeyBoardKey::ShiftRight, 267 | ))), 268 | "Space" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 269 | KeyBoardKey::Space, 270 | ))), 271 | "Tab" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 272 | KeyBoardKey::Tab, 273 | ))), 274 | "UpArrow" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 275 | KeyBoardKey::UpArrow, 276 | ))), 277 | "PrintScreen" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 278 | KeyBoardKey::PrintScreen, 279 | ))), 280 | "ScrollLock" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 281 | KeyBoardKey::ScrollLock, 282 | ))), 283 | "Pause" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 284 | KeyBoardKey::Pause, 285 | ))), 286 | "NumLock" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 287 | KeyBoardKey::NumLock, 288 | ))), 289 | "BackQuote" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 290 | KeyBoardKey::BackQuote, 291 | ))), 292 | "Num1" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 293 | KeyBoardKey::Num1, 294 | ))), 295 | "Num2" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 296 | KeyBoardKey::Num2, 297 | ))), 298 | "Num3" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 299 | KeyBoardKey::Num3, 300 | ))), 301 | "Num4" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 302 | KeyBoardKey::Num4, 303 | ))), 304 | "Num5" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 305 | KeyBoardKey::Num5, 306 | ))), 307 | "Num6" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 308 | KeyBoardKey::Num6, 309 | ))), 310 | "Num7" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 311 | KeyBoardKey::Num7, 312 | ))), 313 | "Num8" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 314 | KeyBoardKey::Num8, 315 | ))), 316 | "Num9" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 317 | KeyBoardKey::Num9, 318 | ))), 319 | "Num0" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 320 | KeyBoardKey::Num0, 321 | ))), 322 | "Minus" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 323 | KeyBoardKey::Minus, 324 | ))), 325 | "Equal" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 326 | KeyBoardKey::Equal, 327 | ))), 328 | "KeyQ" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 329 | KeyBoardKey::KeyQ, 330 | ))), 331 | "KeyW" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 332 | KeyBoardKey::KeyW, 333 | ))), 334 | "KeyE" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 335 | KeyBoardKey::KeyE, 336 | ))), 337 | "KeyR" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 338 | KeyBoardKey::KeyR, 339 | ))), 340 | "KeyT" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 341 | KeyBoardKey::KeyT, 342 | ))), 343 | "KeyY" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 344 | KeyBoardKey::KeyY, 345 | ))), 346 | "KeyU" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 347 | KeyBoardKey::KeyU, 348 | ))), 349 | "KeyI" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 350 | KeyBoardKey::KeyI, 351 | ))), 352 | "KeyO" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 353 | KeyBoardKey::KeyO, 354 | ))), 355 | "KeyP" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 356 | KeyBoardKey::KeyP, 357 | ))), 358 | "LeftBracket" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 359 | KeyBoardKey::LeftBracket, 360 | ))), 361 | "RightBracket" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 362 | KeyBoardKey::RightBracket, 363 | ))), 364 | "KeyA" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 365 | KeyBoardKey::KeyA, 366 | ))), 367 | "KeyS" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 368 | KeyBoardKey::KeyS, 369 | ))), 370 | "KeyD" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 371 | KeyBoardKey::KeyD, 372 | ))), 373 | "KeyF" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 374 | KeyBoardKey::KeyF, 375 | ))), 376 | "KeyG" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 377 | KeyBoardKey::KeyG, 378 | ))), 379 | "KeyH" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 380 | KeyBoardKey::KeyH, 381 | ))), 382 | "KeyJ" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 383 | KeyBoardKey::KeyJ, 384 | ))), 385 | "KeyK" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 386 | KeyBoardKey::KeyK, 387 | ))), 388 | "KeyL" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 389 | KeyBoardKey::KeyL, 390 | ))), 391 | "SemiColon" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 392 | KeyBoardKey::SemiColon, 393 | ))), 394 | "Quote" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 395 | KeyBoardKey::Quote, 396 | ))), 397 | "BackSlash" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 398 | KeyBoardKey::BackSlash, 399 | ))), 400 | "IntlBackslash" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 401 | KeyBoardKey::IntlBackslash, 402 | ))), 403 | "KeyZ" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 404 | KeyBoardKey::KeyZ, 405 | ))), 406 | "KeyX" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 407 | KeyBoardKey::KeyX, 408 | ))), 409 | "KeyC" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 410 | KeyBoardKey::KeyC, 411 | ))), 412 | "KeyV" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 413 | KeyBoardKey::KeyV, 414 | ))), 415 | "KeyB" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 416 | KeyBoardKey::KeyB, 417 | ))), 418 | "KeyN" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 419 | KeyBoardKey::KeyN, 420 | ))), 421 | "KeyM" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 422 | KeyBoardKey::KeyM, 423 | ))), 424 | "Comma" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 425 | KeyBoardKey::Comma, 426 | ))), 427 | "Dot" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 428 | KeyBoardKey::Dot, 429 | ))), 430 | "Slash" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 431 | KeyBoardKey::Slash, 432 | ))), 433 | "Insert" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 434 | KeyBoardKey::Insert, 435 | ))), 436 | "KpReturn" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 437 | KeyBoardKey::KpReturn, 438 | ))), 439 | "KpMinus" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 440 | KeyBoardKey::KpMinus, 441 | ))), 442 | "KpPlus" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 443 | KeyBoardKey::KpPlus, 444 | ))), 445 | "KpMultiply" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 446 | KeyBoardKey::KpMultiply, 447 | ))), 448 | "KpDivide" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 449 | KeyBoardKey::KpDivide, 450 | ))), 451 | "Kp0" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 452 | KeyBoardKey::Kp0, 453 | ))), 454 | "Kp1" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 455 | KeyBoardKey::Kp1, 456 | ))), 457 | "Kp2" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 458 | KeyBoardKey::Kp2, 459 | ))), 460 | "Kp3" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 461 | KeyBoardKey::Kp3, 462 | ))), 463 | "Kp4" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 464 | KeyBoardKey::Kp4, 465 | ))), 466 | "Kp5" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 467 | KeyBoardKey::Kp5, 468 | ))), 469 | "Kp6" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 470 | KeyBoardKey::Kp6, 471 | ))), 472 | "Kp7" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 473 | KeyBoardKey::Kp7, 474 | ))), 475 | "Kp8" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 476 | KeyBoardKey::Kp8, 477 | ))), 478 | "Kp9" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 479 | KeyBoardKey::Kp9, 480 | ))), 481 | "KpDelete" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 482 | KeyBoardKey::KpDelete, 483 | ))), 484 | "Function" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 485 | KeyBoardKey::Function, 486 | ))), 487 | key => { 488 | println!("No such key {:?}", key); 489 | None 490 | } 491 | }, 492 | Info::UnknownButton(b) => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Press( 493 | KeyBoardKey::Unknown(*b), 494 | ))), 495 | Info::Position { x: _, y: _ } => { 496 | println!("No such action!"); 497 | None 498 | } 499 | }, 500 | "Release" => match info { 501 | Info::Button(k) => match k.as_str() { 502 | "Alt" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 503 | KeyBoardKey::Alt, 504 | ))), 505 | "AltGr" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 506 | KeyBoardKey::AltGr, 507 | ))), 508 | "Backspace" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 509 | KeyBoardKey::Backspace, 510 | ))), 511 | "CapsLock" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 512 | KeyBoardKey::CapsLock, 513 | ))), 514 | "ControlLeft" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 515 | KeyBoardKey::ControlLeft, 516 | ))), 517 | "ControlRight" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 518 | KeyBoardKey::ControlRight, 519 | ))), 520 | "Delete" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 521 | KeyBoardKey::Delete, 522 | ))), 523 | "DownArrow" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 524 | KeyBoardKey::DownArrow, 525 | ))), 526 | "End" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 527 | KeyBoardKey::End, 528 | ))), 529 | "Escape" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 530 | KeyBoardKey::Escape, 531 | ))), 532 | "F1" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 533 | KeyBoardKey::F1, 534 | ))), 535 | "F10" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 536 | KeyBoardKey::F10, 537 | ))), 538 | "F11" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 539 | KeyBoardKey::F11, 540 | ))), 541 | "F12" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 542 | KeyBoardKey::F12, 543 | ))), 544 | "F2" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 545 | KeyBoardKey::F2, 546 | ))), 547 | "F3" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 548 | KeyBoardKey::F3, 549 | ))), 550 | "F4" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 551 | KeyBoardKey::F4, 552 | ))), 553 | "F5" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 554 | KeyBoardKey::F5, 555 | ))), 556 | "F6" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 557 | KeyBoardKey::F6, 558 | ))), 559 | "F7" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 560 | KeyBoardKey::F7, 561 | ))), 562 | "F8" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 563 | KeyBoardKey::F8, 564 | ))), 565 | "F9" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 566 | KeyBoardKey::F9, 567 | ))), 568 | "Home" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 569 | KeyBoardKey::Home, 570 | ))), 571 | "LeftArrow" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 572 | KeyBoardKey::LeftArrow, 573 | ))), 574 | "MetaLeft" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 575 | KeyBoardKey::MetaLeft, 576 | ))), 577 | "MetaRight" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 578 | KeyBoardKey::MetaRight, 579 | ))), 580 | "PageDown" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 581 | KeyBoardKey::PageDown, 582 | ))), 583 | "PageUp" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 584 | KeyBoardKey::PageUp, 585 | ))), 586 | "Return" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 587 | KeyBoardKey::Return, 588 | ))), 589 | "RightArrow" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 590 | KeyBoardKey::RightArrow, 591 | ))), 592 | "ShiftLeft" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 593 | KeyBoardKey::ShiftLeft, 594 | ))), 595 | "ShiftRight" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 596 | KeyBoardKey::ShiftRight, 597 | ))), 598 | "Space" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 599 | KeyBoardKey::Space, 600 | ))), 601 | "Tab" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 602 | KeyBoardKey::Tab, 603 | ))), 604 | "UpArrow" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 605 | KeyBoardKey::UpArrow, 606 | ))), 607 | "PrintScreen" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 608 | KeyBoardKey::PrintScreen, 609 | ))), 610 | "ScrollLock" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 611 | KeyBoardKey::ScrollLock, 612 | ))), 613 | "Pause" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 614 | KeyBoardKey::Pause, 615 | ))), 616 | "NumLock" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 617 | KeyBoardKey::NumLock, 618 | ))), 619 | "BackQuote" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 620 | KeyBoardKey::BackQuote, 621 | ))), 622 | "Num1" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 623 | KeyBoardKey::Num1, 624 | ))), 625 | "Num2" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 626 | KeyBoardKey::Num2, 627 | ))), 628 | "Num3" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 629 | KeyBoardKey::Num3, 630 | ))), 631 | "Num4" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 632 | KeyBoardKey::Num4, 633 | ))), 634 | "Num5" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 635 | KeyBoardKey::Num5, 636 | ))), 637 | "Num6" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 638 | KeyBoardKey::Num6, 639 | ))), 640 | "Num7" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 641 | KeyBoardKey::Num7, 642 | ))), 643 | "Num8" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 644 | KeyBoardKey::Num8, 645 | ))), 646 | "Num9" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 647 | KeyBoardKey::Num9, 648 | ))), 649 | "Num0" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 650 | KeyBoardKey::Num0, 651 | ))), 652 | "Minus" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 653 | KeyBoardKey::Minus, 654 | ))), 655 | "Equal" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 656 | KeyBoardKey::Equal, 657 | ))), 658 | "KeyQ" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 659 | KeyBoardKey::KeyQ, 660 | ))), 661 | "KeyW" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 662 | KeyBoardKey::KeyW, 663 | ))), 664 | "KeyE" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 665 | KeyBoardKey::KeyE, 666 | ))), 667 | "KeyR" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 668 | KeyBoardKey::KeyR, 669 | ))), 670 | "KeyT" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 671 | KeyBoardKey::KeyT, 672 | ))), 673 | "KeyY" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 674 | KeyBoardKey::KeyY, 675 | ))), 676 | "KeyU" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 677 | KeyBoardKey::KeyU, 678 | ))), 679 | "KeyI" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 680 | KeyBoardKey::KeyI, 681 | ))), 682 | "KeyO" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 683 | KeyBoardKey::KeyO, 684 | ))), 685 | "KeyP" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 686 | KeyBoardKey::KeyP, 687 | ))), 688 | "LeftBracket" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 689 | KeyBoardKey::LeftBracket, 690 | ))), 691 | "RightBracket" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 692 | KeyBoardKey::RightBracket, 693 | ))), 694 | "KeyA" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 695 | KeyBoardKey::KeyA, 696 | ))), 697 | "KeyS" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 698 | KeyBoardKey::KeyS, 699 | ))), 700 | "KeyD" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 701 | KeyBoardKey::KeyD, 702 | ))), 703 | "KeyF" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 704 | KeyBoardKey::KeyF, 705 | ))), 706 | "KeyG" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 707 | KeyBoardKey::KeyG, 708 | ))), 709 | "KeyH" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 710 | KeyBoardKey::KeyH, 711 | ))), 712 | "KeyJ" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 713 | KeyBoardKey::KeyJ, 714 | ))), 715 | "KeyK" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 716 | KeyBoardKey::KeyK, 717 | ))), 718 | "KeyL" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 719 | KeyBoardKey::KeyL, 720 | ))), 721 | "SemiColon" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 722 | KeyBoardKey::SemiColon, 723 | ))), 724 | "Quote" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 725 | KeyBoardKey::Quote, 726 | ))), 727 | "BackSlash" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 728 | KeyBoardKey::BackSlash, 729 | ))), 730 | "IntlBackslash" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 731 | KeyBoardKey::IntlBackslash, 732 | ))), 733 | "KeyZ" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 734 | KeyBoardKey::KeyZ, 735 | ))), 736 | "KeyX" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 737 | KeyBoardKey::KeyX, 738 | ))), 739 | "KeyC" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 740 | KeyBoardKey::KeyC, 741 | ))), 742 | "KeyV" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 743 | KeyBoardKey::KeyV, 744 | ))), 745 | "KeyB" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 746 | KeyBoardKey::KeyB, 747 | ))), 748 | "KeyN" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 749 | KeyBoardKey::KeyN, 750 | ))), 751 | "KeyM" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 752 | KeyBoardKey::KeyM, 753 | ))), 754 | "Comma" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 755 | KeyBoardKey::Comma, 756 | ))), 757 | "Dot" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 758 | KeyBoardKey::Dot, 759 | ))), 760 | "Slash" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 761 | KeyBoardKey::Slash, 762 | ))), 763 | "Insert" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 764 | KeyBoardKey::Insert, 765 | ))), 766 | "KpReturn" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 767 | KeyBoardKey::KpReturn, 768 | ))), 769 | "KpMinus" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 770 | KeyBoardKey::KpMinus, 771 | ))), 772 | "KpPlus" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 773 | KeyBoardKey::KpPlus, 774 | ))), 775 | "KpMultiply" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 776 | KeyBoardKey::KpMultiply, 777 | ))), 778 | "KpDivide" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 779 | KeyBoardKey::KpDivide, 780 | ))), 781 | "Kp0" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 782 | KeyBoardKey::Kp0, 783 | ))), 784 | "Kp1" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 785 | KeyBoardKey::Kp1, 786 | ))), 787 | "Kp2" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 788 | KeyBoardKey::Kp2, 789 | ))), 790 | "Kp3" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 791 | KeyBoardKey::Kp3, 792 | ))), 793 | "Kp4" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 794 | KeyBoardKey::Kp4, 795 | ))), 796 | "Kp5" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 797 | KeyBoardKey::Kp5, 798 | ))), 799 | "Kp6" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 800 | KeyBoardKey::Kp6, 801 | ))), 802 | "Kp7" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 803 | KeyBoardKey::Kp7, 804 | ))), 805 | "Kp8" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 806 | KeyBoardKey::Kp8, 807 | ))), 808 | "Kp9" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 809 | KeyBoardKey::Kp9, 810 | ))), 811 | "KpDelete" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 812 | KeyBoardKey::KpDelete, 813 | ))), 814 | "Function" => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 815 | KeyBoardKey::Function, 816 | ))), 817 | key => { 818 | println!("No such key {:?}", key); 819 | None 820 | } 821 | }, 822 | Info::UnknownButton(b) => Some(DeviceEvent::KeyBoardEvent(KeyBoardEvent::Release( 823 | KeyBoardKey::Unknown(*b), 824 | ))), 825 | Info::Position { x: _, y: _ } => { 826 | println!("No such action!"); 827 | None 828 | } 829 | }, 830 | name => { 831 | println!("No such action {:?}", name); 832 | None 833 | } 834 | }, 835 | "Mouse" => match action { 836 | "Press" => match info { 837 | Info::Button(b) => match b.as_str() { 838 | "Left" => Some(DeviceEvent::MouseEvent(MouseEvent::Press(MouseKey::Left))), 839 | "Right" => Some(DeviceEvent::MouseEvent(MouseEvent::Press(MouseKey::Right))), 840 | "Middle" => Some(DeviceEvent::MouseEvent(MouseEvent::Press(MouseKey::Middle))), 841 | name => { 842 | println!("No such button {:?}", name); 843 | None 844 | } 845 | }, 846 | Info::UnknownButton(b) => Some(DeviceEvent::MouseEvent(MouseEvent::Press( 847 | MouseKey::Unknown(*b), 848 | ))), 849 | Info::Position { x: _, y: _ } => { 850 | println!("No such action!"); 851 | None 852 | } 853 | }, 854 | "Release" => match info { 855 | Info::Button(b) => match b.as_str() { 856 | "Left" => Some(DeviceEvent::MouseEvent(MouseEvent::Rlease(MouseKey::Left))), 857 | "Right" => Some(DeviceEvent::MouseEvent(MouseEvent::Rlease(MouseKey::Right))), 858 | "Middle" => Some(DeviceEvent::MouseEvent(MouseEvent::Rlease( 859 | MouseKey::Middle, 860 | ))), 861 | name => { 862 | println!("No such button {:?}", name); 863 | None 864 | } 865 | }, 866 | Info::UnknownButton(b) => Some(DeviceEvent::MouseEvent(MouseEvent::Rlease( 867 | MouseKey::Unknown(*b), 868 | ))), 869 | Info::Position { x: _, y: _ } => { 870 | println!("No such action!"); 871 | None 872 | } 873 | }, 874 | "Move" => match info { 875 | Info::Button(_) => { 876 | println!("No such action!"); 877 | None 878 | } 879 | Info::UnknownButton(_) => { 880 | println!("No such action!"); 881 | None 882 | } 883 | Info::Position { x, y } => { 884 | Some(DeviceEvent::MouseEvent(MouseEvent::Move(MouseMove { 885 | x: *x, 886 | y: *y, 887 | }))) 888 | } 889 | }, 890 | "Wheel" => match info { 891 | Info::Button(b) => match b.as_str() { 892 | "Up" => Some(DeviceEvent::MouseEvent(MouseEvent::Wheel(MouseWheel::Up))), 893 | "Down" => Some(DeviceEvent::MouseEvent(MouseEvent::Wheel(MouseWheel::Down))), 894 | name => { 895 | println!("No such button {:?}", name); 896 | None 897 | } 898 | }, 899 | Info::UnknownButton(_) => { 900 | println!("No such action!"); 901 | None 902 | } 903 | Info::Position { x: _, y: _ } => { 904 | println!("No such action!"); 905 | None 906 | } 907 | }, 908 | name => { 909 | println!("No such action {:?}", name); 910 | None 911 | } 912 | }, 913 | name => { 914 | println!("No such device {:?}", name); 915 | None 916 | } 917 | }; 918 | if let Some(event) = event { 919 | event.send_keyboard_mouse_event() 920 | } 921 | } 922 | --------------------------------------------------------------------------------