From 54f1c4d9e4ddcc1b52044d0e5ba9210a729d26db Mon Sep 17 00:00:00 2001 From: fan xia Date: Thu, 7 Dec 2023 01:34:07 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AE=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cargo/config.toml | 5 + .npmignore | 13 ++ Cargo.toml | 27 +++ build.rs | 5 + lib/clipboard.ts | 25 +++ lib/deprecated/cb.ts | 76 +++++++++ lib/deprecated/package.json | 14 ++ lib/folder.ts | 85 ++++++++++ lib/index.ts | 6 + lib/monitor.ts | 158 ++++++++++++++++++ lib/simulation.ts | 62 +++++++ lib/sysapp/index.ts | 48 ++++++ lib/sysapp/windows.ts | 29 ++++ npm/darwin-arm64/README.md | 3 + npm/darwin-arm64/package.json | 18 ++ npm/darwin-universal/README.md | 3 + npm/darwin-universal/package.json | 15 ++ npm/darwin-x64/README.md | 3 + npm/darwin-x64/package.json | 18 ++ npm/linux-arm64-gnu/README.md | 3 + npm/linux-arm64-gnu/package.json | 21 +++ npm/linux-arm64-musl/README.md | 3 + npm/linux-arm64-musl/package.json | 21 +++ npm/linux-x64-gnu/README.md | 3 + npm/linux-x64-gnu/package.json | 21 +++ npm/linux-x64-musl/README.md | 3 + npm/linux-x64-musl/package.json | 21 +++ npm/win32-arm64-msvc/README.md | 3 + npm/win32-arm64-msvc/package.json | 18 ++ npm/win32-ia32-msvc/README.md | 3 + npm/win32-ia32-msvc/package.json | 18 ++ npm/win32-x64-msvc/README.md | 3 + npm/win32-x64-msvc/package.json | 18 ++ postbuild.js | 10 ++ rustfmt.toml | 2 + src/clipboard/mod.rs | 118 +++++++++++++ src/lib.rs | 9 + src/main.rs | 20 +++ src/monitor/mod.rs | 65 ++++++++ src/shotcut/exelook/dib.rs | 267 ++++++++++++++++++++++++++++++ src/shotcut/exelook/mod.rs | 197 ++++++++++++++++++++++ src/shotcut/mod.rs | 69 ++++++++ src/simulation/mod.rs | 122 ++++++++++++++ 43 files changed, 1651 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .npmignore create mode 100644 Cargo.toml create mode 100644 build.rs create mode 100644 lib/clipboard.ts create mode 100644 lib/deprecated/cb.ts create mode 100644 lib/deprecated/package.json create mode 100644 lib/folder.ts create mode 100644 lib/index.ts create mode 100644 lib/monitor.ts create mode 100644 lib/simulation.ts create mode 100644 lib/sysapp/index.ts create mode 100644 lib/sysapp/windows.ts create mode 100644 npm/darwin-arm64/README.md create mode 100644 npm/darwin-arm64/package.json create mode 100644 npm/darwin-universal/README.md create mode 100644 npm/darwin-universal/package.json create mode 100644 npm/darwin-x64/README.md create mode 100644 npm/darwin-x64/package.json create mode 100644 npm/linux-arm64-gnu/README.md create mode 100644 npm/linux-arm64-gnu/package.json create mode 100644 npm/linux-arm64-musl/README.md create mode 100644 npm/linux-arm64-musl/package.json create mode 100644 npm/linux-x64-gnu/README.md create mode 100644 npm/linux-x64-gnu/package.json create mode 100644 npm/linux-x64-musl/README.md create mode 100644 npm/linux-x64-musl/package.json create mode 100644 npm/win32-arm64-msvc/README.md create mode 100644 npm/win32-arm64-msvc/package.json create mode 100644 npm/win32-ia32-msvc/README.md create mode 100644 npm/win32-ia32-msvc/package.json create mode 100644 npm/win32-x64-msvc/README.md create mode 100644 npm/win32-x64-msvc/package.json create mode 100644 postbuild.js create mode 100644 rustfmt.toml create mode 100644 src/clipboard/mod.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/monitor/mod.rs create mode 100644 src/shotcut/exelook/dib.rs create mode 100644 src/shotcut/exelook/mod.rs create mode 100644 src/shotcut/mod.rs create mode 100644 src/simulation/mod.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..5bd0907 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-nostartfiles", "-C", "target-feature=-crt-static"] + +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..ec144db --- /dev/null +++ b/.npmignore @@ -0,0 +1,13 @@ +target +Cargo.lock +.cargo +.github +npm +.eslintrc +.prettierignore +rustfmt.toml +yarn.lock +*.node +.yarn +__test__ +renovate.json diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6f2941d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +edition = "2021" +name = "rubick-native" +version = "0.0.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +rdev = { version = "0.5", features = ["serialize", "unstable_grab"] } +clipboard-files = "0.1" +copypasta = "0.10" +enigo = "0.1" +napi = { version = "2", features = ["async"] } +napi-derive = "2" +serde_json = "1" +lnk_parser = "0.4" +parselnk = "0.1" +pelite = "0.10" +base64 = "0.21" +tokio = { version = "1", features = ["full"] } + +[build-dependencies] +napi-build = "2" + +[profile.release] +lto = true diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..1f866b6 --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/lib/clipboard.ts b/lib/clipboard.ts new file mode 100644 index 0000000..897d769 --- /dev/null +++ b/lib/clipboard.ts @@ -0,0 +1,25 @@ +import { getClipboardContent as gcbc } from "addon" + +interface ClipboardContentText { + type: "text", + content: string +} + +interface ClipboardContentFile { + type: "file", + content: string[] +} + +export type ClipboardContent = ClipboardContentText | ClipboardContentFile | null + +export const getClipboardContent = (): ClipboardContent => { + const c = gcbc() + if (c?.type === 'text') { + return { + type: "text", + content: c.content.at(0) + } + } else { + return c as ClipboardContent + } +} \ No newline at end of file diff --git a/lib/deprecated/cb.ts b/lib/deprecated/cb.ts new file mode 100644 index 0000000..1fa1ebe --- /dev/null +++ b/lib/deprecated/cb.ts @@ -0,0 +1,76 @@ +// import { arch, platform, homedir } from "os" +// import { onClipboardChange } from "../../addon" +// import got from "got" +// import { Extract } from "unzip-stream" +// import { access, mkdir, constants } from "fs/promises" +// import { join } from "path" +// import { execaCommand } from "execa" +// const getKey = (stdout: string, key: string) => stdout.split(`"${key}": `).at(1)?.split(`,\r\n`).at(0)! +// import { asyncFolderWalker } from "async-folder-walker" + +// // 启动剪切板程序 +// export default async () => { +// let latestNum = 0 +// const repoURL = "https://ghproxy.com/https://github.com/Slackadays/Clipboard" +// let a = arch() +// let p: string = platform() +// // 确保目录存在 +// const dirPath = join(homedir(), "cb") +// try { +// await access(dirPath, constants.O_DIRECTORY) +// } catch { +// await mkdir(dirPath) +// } +// // cb 路径 +// const cbPath = join(dirPath, 'bin', p === "win32" ? "cb.exe" : "cb") +// // 同步剪切板内容 +// const execCB = async () => { +// const stdout = (await execaCommand(cbPath + " info", { env: { "CLIPBOARD_SILENT": "true" } })).stdout +// // 最新缓存 +// latestNum = Number(getKey(stdout, "totalEntries")) - 1 +// return stdout +// } +// try { +// await access(cbPath) +// } catch { +// switch (a) { +// case "arm64": +// break; +// case "x64": +// a = "amd64" +// break; +// default: +// throw new Error("Not Support Your Sys Arch") +// } +// switch (p) { +// case "freebsd": +// case "linux": +// case "netbsd": +// case "openbsd": +// break; +// case "win32": +// p = "windows" +// break; +// case "darwin": +// p = "macos" +// break; +// default: +// throw new Error("Not Support Your Sys Arch") +// } +// const latest = (await fetch(repoURL + "/releases/latest")).url.split("/").pop() +// const durl = repoURL + `/releases/download/${latest}/clipboard-${p}-${p === "macos" ? 'arm64-amd64' : a}.zip` +// got.stream(durl).pipe(Extract({ path: dirPath })) +// } finally { +// // 剪切板历史路径 +// const basePath = join(getKey(await execCB(), "path").replaceAll('"', ''), "data") +// // 剪切板监听 +// onClipboardChange(execCB) +// return { +// latest: () => { +// const latestPath = join(basePath, latestNum.toString()) +// const walker = asyncFolderWalker(latestPath, { maxDepth: 0 }) +// return walker +// } +// } +// } +// } diff --git a/lib/deprecated/package.json b/lib/deprecated/package.json new file mode 100644 index 0000000..652ba33 --- /dev/null +++ b/lib/deprecated/package.json @@ -0,0 +1,14 @@ +{ + "type": "module", + "devDependencies": { + "@types/got": "^9.6.12", + "@types/node": "^20.7.1", + "@types/unzip-stream": "^0.3.2" + }, + "dependencies": { + "async-folder-walker": "^2.2.1", + "execa": "^8.0.1", + "got": "^13.0.0", + "unzip-stream": "^0.3.1" + } +} \ No newline at end of file diff --git a/lib/folder.ts b/lib/folder.ts new file mode 100644 index 0000000..b4a48c3 --- /dev/null +++ b/lib/folder.ts @@ -0,0 +1,85 @@ +import { activeWindow } from "@miniben90/x-win" +import { lstat } from "fs/promises" +import { homedir } from "os" +import { join } from "path" + +// 获取活动的文件夹路径 +export const getFolderOpenPath = async () => { + if (process.platform === 'darwin') { + const { execa } = await import("execa") + const res = await execa('osascript', ['-e', ` + tell app "Finder" + try + POSIX path of (insertion location as alias) + on error + POSIX path of (path to desktop folder as alias) + end try + end tell + `]) + return res.stdout; + } + + if (process.platform === 'win32') { + const win = activeWindow() + if (win.info.execName === 'explorer') { + const base = homedir() + let path: string + switch (win.title) { + case 'Home': + case '主文件夹': + path = base + break; + + case 'Downloads': + case '下载': + path = join(base, 'Downloads') + break; + + case 'Documents': + case '文档': + path = join(base, 'Documents') + break; + + case 'Desktop': + case '桌面': + path = join(base, 'Desktop') + break; + + case 'Videos': + case '视频': + path = join(base, 'Videos') + break; + + case 'Pictures': + case '图片': + path = join(base, 'Pictures') + break; + + case 'Music': + case '音乐': + path = join(base, 'Music') + break; + + case 'Links': + case '链接': + path = join(base, 'Music') + break; + + default: + path = win.title + break; + } + try { + const s = await lstat(path) + if (s.isDirectory()) { + return path + } + } catch { + return null + } + } + } + + // todo linux + return null +} \ No newline at end of file diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..6b7a6bd --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,6 @@ +export { activeWindow as getActiveWin, openWindows as getOpenWin } from "@miniben90/x-win" +export * from "./clipboard" +export * from "./folder" +export * from "./simulation" +export * from "./monitor" +export * from './sysapp' \ No newline at end of file diff --git a/lib/monitor.ts b/lib/monitor.ts new file mode 100644 index 0000000..921515a --- /dev/null +++ b/lib/monitor.ts @@ -0,0 +1,158 @@ +import { onInputEvent as oie, grabInputEvent as gie } from "addon" + +export type EventKeyType = "Alt" | + "AltGr" | + "Backspace" | + "CapsLock" | + "ControlLeft" | + "ControlRight" | + "Delete" | + "DownArrow" | + "End" | + "Escape" | + "F1" | + "F10" | + "F11" | + "F12" | + "F2" | + "F3" | + "F4" | + "F5" | + "F6" | + "F7" | + "F8" | + "F9" | + "Home" | + "LeftArrow" | + "MetaLeft" | + "MetaRight" | + "PageDown" | + "PageUp" | + "Return" | + "RightArrow" | + "ShiftLeft" | + "ShiftRight" | + "Space" | + "Tab" | + "UpArrow" | + "PrintScreen" | + "ScrollLock" | + "Pause" | + "NumLock" | + "BackQuote" | + "Num1" | + "Num2" | + "Num3" | + "Num4" | + "Num5" | + "Num6" | + "Num7" | + "Num8" | + "Num9" | + "Num0" | + "Minus" | + "Equal" | + "KeyQ" | + "KeyW" | + "KeyE" | + "KeyR" | + "KeyT" | + "KeyY" | + "KeyU" | + "KeyI" | + "KeyO" | + "KeyP" | + "LeftBracket" | + "RightBracket" | + "KeyA" | + "KeyS" | + "KeyD" | + "KeyF" | + "KeyG" | + "KeyH" | + "KeyJ" | + "KeyK" | + "KeyL" | + "SemiColon" | + "Quote" | + "BackSlash" | + "IntlBackslash" | + "KeyZ" | + "KeyX" | + "KeyC" | + "KeyV" | + "KeyB" | + "KeyN" | + "KeyM" | + "Comma" | + "Dot" | + "Slash" | + "Insert" | + "KpReturn" | + "KpMinus" | + "KpPlus" | + "KpMultiply" | + "KpDivide" | + "Kp0" | + "Kp1" | + "Kp2" | + "Kp3" | + "Kp4" | + "Kp5" | + "Kp6" | + "Kp7" | + "Kp8" | + "Kp9" | + "KpDelete" | + "Function" | { + Unknown: number + } + +export type EventBtnType = "Left" | + "Right" | + "Middle" | { + Unknown: number + } + +export interface MouseKeyBoardEventOther { + time: { + secs_since_epoch: number, + nanos_since_epoch: number + }, + name: null, + event: { + type: "KeyRelease" | "ButtonPress" | "ButtonRelease" + value: EventKeyType + } | { + type: "MouseMove" + value: { x: number, y: number } + } | { + type: "Wheel", + value: { delta_x: number, delta_y: number } + } +} + +export interface MouseKeyBoardEventKeyPress { + time: { + secs_since_epoch: number, + nanos_since_epoch: number + }, + name: string, + event: { + type: "KeyPress" + value: EventKeyType + } +} + +export type MouseKeyBoardEvent = MouseKeyBoardEventOther | MouseKeyBoardEventKeyPress +const parse = (e: string) => { + const event = JSON.parse(e) + const [type, value] = Object.entries(event.event_type).pop()! + return { + time: event.time, + name: event.name, + event: { type, value } + } as MouseKeyBoardEvent +} +export const onInputEvent = (callback: (event: MouseKeyBoardEvent) => void) => oie((event) => callback(parse(event))) +export const grabInputEvent = (callback: (event: MouseKeyBoardEvent) => boolean) => gie((event) => callback(parse(event))) \ No newline at end of file diff --git a/lib/simulation.ts b/lib/simulation.ts new file mode 100644 index 0000000..388b1a1 --- /dev/null +++ b/lib/simulation.ts @@ -0,0 +1,62 @@ +import { + Position, + sendKeyboardSimulation as ks, + sendMouseSimulation as ms, +} from "addon"; + +export type MouseBtn = + | "Left" + | "Middle" + | "Right" + | "Back" + | "Forward" + +export interface MoveMoveInput { + type: "relative" | "absolute"; + data: Position; +} + +// example: {+CTRL}a{-CTRL}{+SHIFT}Hello World{-SHIFT} +// 所有可用键 https://github.com/enigo-rs/enigo/blob/master/src/keycodes.rs +export const sendKeyboardSimulation = (cmd: string) => ks(cmd); + +export const mouseScrollX = (len: number) => { + ms({ action: 3, data: { x: len, y: 0 } }); +} + +export const mouseScrollY = (len: number) => { + ms({ action: 4, data: { x: 0, y: len } }); +}; + +export const mouseMove = (input: MoveMoveInput) => { + ms({ action: input.type === "absolute" ? 2 : 1, data: input.data }); +}; + +export const mouseLocaion = () => ms({ action: 0 }) + +const mouseDUC = (btn: MouseBtn, action: 5 | 6 | 7) => { + let button = 0; + switch (btn) { + case "Left": + break; + case "Middle": + button = 1; + break; + case "Right": + button = 2; + break; + case "Back": + button = 3; + break; + case "Forward": + button = 4; + break; + default: + break; + } + ms({ action, button }); +}; + +export const mouseDown = (btn: MouseBtn) => mouseDUC(btn, 6) +export const mouseUp = (btn: MouseBtn) => mouseDUC(btn, 5) +export const mouseClick = (btn: MouseBtn) => mouseDUC(btn, 7) diff --git a/lib/sysapp/index.ts b/lib/sysapp/index.ts new file mode 100644 index 0000000..89e7aff --- /dev/null +++ b/lib/sysapp/index.ts @@ -0,0 +1,48 @@ +import { shortcutWin } from "./windows" +import { platform } from "os" +import { exeLookBase64 } from "addon" +import { ParsedPath } from "path"; + +export type CallBack = (app: App) => void | Promise + +export interface App extends ParsedPath { + name: string; + description: string; + execPath: string; + shortCutPath: string; + workingDir: string; +} + +// todo linux/macos +export const getSystemApp = async (callback: CallBack, extraPath?: string[]) => { + switch (platform()) { + case "win32": + return await shortcutWin(callback, extraPath) + + // case "linux": + // break; + + // case "darwin": + // break; + + default: + throw new Error("Your System is Not Supported"); + } +} + +export const getAppIcon = (path: string): string => { + switch (platform()) { + case "win32": + return exeLookBase64(path) + + // case "linux": + // break; + + // case "darwin": + // break; + + default: + throw new Error("Your System is Not Supported"); + } + +} \ No newline at end of file diff --git a/lib/sysapp/windows.ts b/lib/sysapp/windows.ts new file mode 100644 index 0000000..2507722 --- /dev/null +++ b/lib/sysapp/windows.ts @@ -0,0 +1,29 @@ +import { join, parse } from "path"; +import { homedir } from "os" +import { fdir } from "fdir"; +import { parseLnk } from "addon" +import { CallBack } from "."; + +export const shortcutWin = async (callback: CallBack, extraPath: string[] = []) => { + const hdir = homedir() + const f = new fdir().glob("./**/*.lnk").withFullPaths() + .filter((t) => { + const d = parseLnk(t) + callback({ + ...parse(t), + description: d.nameString, + execPath: d.fullPath, + shortCutPath: t, + workingDir: d.workingDir + }) + return true + }) + const defaultPaths = [ + join(process.env.ProgramData, "/Microsoft/Windows/Start Menu/Programs"), + join(process.env.AppData, "/Microsoft/Windows/Start Menu/Programs"), + join(process.env.PUBLIC, 'Desktop'), + join(hdir, 'Desktop'), + ...extraPath + ] + await Promise.allSettled(defaultPaths.map(path => f.crawl(path).withPromise())) +} diff --git a/npm/darwin-arm64/README.md b/npm/darwin-arm64/README.md new file mode 100644 index 0000000..b15a0c7 --- /dev/null +++ b/npm/darwin-arm64/README.md @@ -0,0 +1,3 @@ +# `rubick-native-darwin-arm64` + +This is the **aarch64-apple-darwin** binary for `rubick-native` diff --git a/npm/darwin-arm64/package.json b/npm/darwin-arm64/package.json new file mode 100644 index 0000000..6e76e76 --- /dev/null +++ b/npm/darwin-arm64/package.json @@ -0,0 +1,18 @@ +{ + "name": "rubick-native-darwin-arm64", + "version": "0.0.0", + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "main": "rubick-native.darwin-arm64.node", + "files": [ + "rubick-native.darwin-arm64.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} \ No newline at end of file diff --git a/npm/darwin-universal/README.md b/npm/darwin-universal/README.md new file mode 100644 index 0000000..b1ce8a2 --- /dev/null +++ b/npm/darwin-universal/README.md @@ -0,0 +1,3 @@ +# `rubick-native-darwin-universal` + +This is the **universal-apple-darwin** binary for `rubick-native` diff --git a/npm/darwin-universal/package.json b/npm/darwin-universal/package.json new file mode 100644 index 0000000..7549c36 --- /dev/null +++ b/npm/darwin-universal/package.json @@ -0,0 +1,15 @@ +{ + "name": "rubick-native-darwin-universal", + "version": "0.0.0", + "os": [ + "darwin" + ], + "main": "rubick-native.darwin-universal.node", + "files": [ + "rubick-native.darwin-universal.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} \ No newline at end of file diff --git a/npm/darwin-x64/README.md b/npm/darwin-x64/README.md new file mode 100644 index 0000000..c8edca6 --- /dev/null +++ b/npm/darwin-x64/README.md @@ -0,0 +1,3 @@ +# `rubick-native-darwin-x64` + +This is the **x86_64-apple-darwin** binary for `rubick-native` diff --git a/npm/darwin-x64/package.json b/npm/darwin-x64/package.json new file mode 100644 index 0000000..c273255 --- /dev/null +++ b/npm/darwin-x64/package.json @@ -0,0 +1,18 @@ +{ + "name": "rubick-native-darwin-x64", + "version": "0.0.0", + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "main": "rubick-native.darwin-x64.node", + "files": [ + "rubick-native.darwin-x64.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} \ No newline at end of file diff --git a/npm/linux-arm64-gnu/README.md b/npm/linux-arm64-gnu/README.md new file mode 100644 index 0000000..cf79acf --- /dev/null +++ b/npm/linux-arm64-gnu/README.md @@ -0,0 +1,3 @@ +# `rubick-native-linux-arm64-gnu` + +This is the **aarch64-unknown-linux-gnu** binary for `rubick-native` diff --git a/npm/linux-arm64-gnu/package.json b/npm/linux-arm64-gnu/package.json new file mode 100644 index 0000000..92b45b0 --- /dev/null +++ b/npm/linux-arm64-gnu/package.json @@ -0,0 +1,21 @@ +{ + "name": "rubick-native-linux-arm64-gnu", + "version": "0.0.0", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "main": "rubick-native.linux-arm64-gnu.node", + "files": [ + "rubick-native.linux-arm64-gnu.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "libc": [ + "glibc" + ] +} \ No newline at end of file diff --git a/npm/linux-arm64-musl/README.md b/npm/linux-arm64-musl/README.md new file mode 100644 index 0000000..c4dcd99 --- /dev/null +++ b/npm/linux-arm64-musl/README.md @@ -0,0 +1,3 @@ +# `rubick-native-linux-arm64-musl` + +This is the **aarch64-unknown-linux-musl** binary for `rubick-native` diff --git a/npm/linux-arm64-musl/package.json b/npm/linux-arm64-musl/package.json new file mode 100644 index 0000000..6653c47 --- /dev/null +++ b/npm/linux-arm64-musl/package.json @@ -0,0 +1,21 @@ +{ + "name": "rubick-native-linux-arm64-musl", + "version": "0.0.0", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "main": "rubick-native.linux-arm64-musl.node", + "files": [ + "rubick-native.linux-arm64-musl.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "libc": [ + "musl" + ] +} \ No newline at end of file diff --git a/npm/linux-x64-gnu/README.md b/npm/linux-x64-gnu/README.md new file mode 100644 index 0000000..608713a --- /dev/null +++ b/npm/linux-x64-gnu/README.md @@ -0,0 +1,3 @@ +# `rubick-native-linux-x64-gnu` + +This is the **x86_64-unknown-linux-gnu** binary for `rubick-native` diff --git a/npm/linux-x64-gnu/package.json b/npm/linux-x64-gnu/package.json new file mode 100644 index 0000000..49cc8d0 --- /dev/null +++ b/npm/linux-x64-gnu/package.json @@ -0,0 +1,21 @@ +{ + "name": "rubick-native-linux-x64-gnu", + "version": "0.0.0", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "main": "rubick-native.linux-x64-gnu.node", + "files": [ + "rubick-native.linux-x64-gnu.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "libc": [ + "glibc" + ] +} \ No newline at end of file diff --git a/npm/linux-x64-musl/README.md b/npm/linux-x64-musl/README.md new file mode 100644 index 0000000..befeb78 --- /dev/null +++ b/npm/linux-x64-musl/README.md @@ -0,0 +1,3 @@ +# `rubick-native-linux-x64-musl` + +This is the **x86_64-unknown-linux-musl** binary for `rubick-native` diff --git a/npm/linux-x64-musl/package.json b/npm/linux-x64-musl/package.json new file mode 100644 index 0000000..f26de6c --- /dev/null +++ b/npm/linux-x64-musl/package.json @@ -0,0 +1,21 @@ +{ + "name": "rubick-native-linux-x64-musl", + "version": "0.0.0", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "main": "rubick-native.linux-x64-musl.node", + "files": [ + "rubick-native.linux-x64-musl.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "libc": [ + "musl" + ] +} \ No newline at end of file diff --git a/npm/win32-arm64-msvc/README.md b/npm/win32-arm64-msvc/README.md new file mode 100644 index 0000000..2133730 --- /dev/null +++ b/npm/win32-arm64-msvc/README.md @@ -0,0 +1,3 @@ +# `rubick-native-win32-arm64-msvc` + +This is the **aarch64-pc-windows-msvc** binary for `rubick-native` diff --git a/npm/win32-arm64-msvc/package.json b/npm/win32-arm64-msvc/package.json new file mode 100644 index 0000000..1806cd3 --- /dev/null +++ b/npm/win32-arm64-msvc/package.json @@ -0,0 +1,18 @@ +{ + "name": "rubick-native-win32-arm64-msvc", + "version": "0.0.0", + "os": [ + "win32" + ], + "cpu": [ + "arm64" + ], + "main": "rubick-native.win32-arm64-msvc.node", + "files": [ + "rubick-native.win32-arm64-msvc.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} \ No newline at end of file diff --git a/npm/win32-ia32-msvc/README.md b/npm/win32-ia32-msvc/README.md new file mode 100644 index 0000000..7b700ea --- /dev/null +++ b/npm/win32-ia32-msvc/README.md @@ -0,0 +1,3 @@ +# `rubick-native-win32-ia32-msvc` + +This is the **i686-pc-windows-msvc** binary for `rubick-native` diff --git a/npm/win32-ia32-msvc/package.json b/npm/win32-ia32-msvc/package.json new file mode 100644 index 0000000..97f2045 --- /dev/null +++ b/npm/win32-ia32-msvc/package.json @@ -0,0 +1,18 @@ +{ + "name": "rubick-native-win32-ia32-msvc", + "version": "0.0.0", + "os": [ + "win32" + ], + "cpu": [ + "ia32" + ], + "main": "rubick-native.win32-ia32-msvc.node", + "files": [ + "rubick-native.win32-ia32-msvc.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} \ No newline at end of file diff --git a/npm/win32-x64-msvc/README.md b/npm/win32-x64-msvc/README.md new file mode 100644 index 0000000..505afb3 --- /dev/null +++ b/npm/win32-x64-msvc/README.md @@ -0,0 +1,3 @@ +# `rubick-native-win32-x64-msvc` + +This is the **x86_64-pc-windows-msvc** binary for `rubick-native` diff --git a/npm/win32-x64-msvc/package.json b/npm/win32-x64-msvc/package.json new file mode 100644 index 0000000..986a9cd --- /dev/null +++ b/npm/win32-x64-msvc/package.json @@ -0,0 +1,18 @@ +{ + "name": "rubick-native-win32-x64-msvc", + "version": "0.0.0", + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "main": "rubick-native.win32-x64-msvc.node", + "files": [ + "rubick-native.win32-x64-msvc.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} \ No newline at end of file diff --git a/postbuild.js b/postbuild.js new file mode 100644 index 0000000..39c9edb --- /dev/null +++ b/postbuild.js @@ -0,0 +1,10 @@ +const { readFileSync, writeFileSync } = require("fs") +const path = require("path"); + +const ijs_path = path.join(__dirname, "index.js") +const ijs = readFileSync(ijs_path, "utf-8") +const twoijs = ijs.split('binding\`)\n}\n') + +const newijs = 'const { join } = require("path")\nconst nativeBinding = (new Function(`require`,`__dirname`,`' + twoijs[0].replaceAll('`', '\\`').replaceAll('$', '\\$') + 'binding\\`)\n}\nreturn nativeBinding`))((path)=>require(path.replace("./","../")),join(__dirname,".."))\n' + twoijs[1] + +writeFileSync(ijs_path, newijs) \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..cab5731 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +tab_spaces = 2 +edition = "2021" diff --git a/src/clipboard/mod.rs b/src/clipboard/mod.rs new file mode 100644 index 0000000..2acd2b8 --- /dev/null +++ b/src/clipboard/mod.rs @@ -0,0 +1,118 @@ +use clipboard_files; +use copypasta::{ClipboardContext, ClipboardProvider}; + +// use std::{ +// path::PathBuf, +// sync::mpsc::{self, Sender}, +// thread::spawn, +// }; + +// use napi::{ +// bindgen_prelude::*, +// threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode}, +// }; + +// enum ClipBoardContent { +// File(Vec), +// Text(String), +// } + +#[napi(object)] +pub struct ClipBoardContentJson { + #[napi(ts_type = "'file' | 'text'")] + pub r#type: String, + pub content: Vec, +} + +// struct Handler { +// pub tx: Sender, +// pub ctx: WindowsClipboardContext, +// } + +// impl ClipboardHandler for Handler { +// fn on_clipboard_change(&mut self) -> CallbackResult { +// let files = clipboard_files::read(); +// match files { +// Ok(f) => { +// println!("{:#?}", f); +// self.tx.send(ClipBoardContent::File(f)).unwrap(); +// } +// Err(err1) => { +// println!("{:#?}", err1); +// let content = self.ctx.get_contents(); +// match content { +// Ok(text) => { +// self.tx.send(ClipBoardContent::Text(text)).unwrap(); +// } +// Err(err) => { +// println!("{:#?}", err); +// // self.tx.send(None).unwrap(); +// } +// } +// } +// } +// CallbackResult::Next +// } + +// fn on_clipboard_error(&mut self, error: std::io::Error) -> CallbackResult { +// println!("{:#?}", error); +// CallbackResult::Next +// } +// } + +// #[napi(ts_args_type = "callback: (content: {type:'file'|'text',content:string[]}) => void")] +// pub fn on_clipboard_change(callback: JsFunction) { +// let jsfn: ThreadsafeFunction = callback +// .create_threadsafe_function(0, |ctx| match ctx.value { +// ClipBoardContent::File(f) => Ok(vec![ClipBoardContentJson { +// r#type: "file".to_string(), +// content: f +// .into_iter() +// .map(|c| c.to_str().unwrap().to_string()) +// .collect::>(), +// }]), +// ClipBoardContent::Text(t) => Ok(vec![ClipBoardContentJson { +// r#type: "text".to_string(), +// content: vec![t], +// }]), +// }) +// .unwrap(); + +// let (tx, rx) = mpsc::channel(); +// let ctx = ClipboardContext::new().unwrap(); + +// spawn(|| { +// let _ = Master::new(Handler { tx, ctx }).run(); +// }); +// spawn(move || { +// for c in rx { +// jsfn.call(c, ThreadsafeFunctionCallMode::Blocking); +// } +// }); +// } + +// 获取剪切板文件或者文本 +#[napi] +pub fn get_clipboard_content() -> Option { + let files = clipboard_files::read(); + let mut ctx = ClipboardContext::new().unwrap(); + match files { + Ok(f) => Some(ClipBoardContentJson { + r#type: "file".to_string(), + content: f + .into_iter() + .map(|c| c.to_str().unwrap().to_string()) + .collect::>(), + }), + Err(_) => { + let content = ctx.get_contents(); + match content { + Ok(text) => Some(ClipBoardContentJson { + r#type: "text".to_string(), + content: vec![text], + }), + Err(_) => None, + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..338bcf1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +#![deny(clippy::all)] +#![feature(absolute_path)] +#[macro_use] +extern crate napi_derive; + +pub mod clipboard; +pub mod monitor; +pub mod shotcut; +pub mod simulation; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0190204 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,20 @@ +// #[cfg(feature = "unstable_grab")] +use rdev::{grab, Event}; + +fn main() { + // #[cfg(feature = "unstable_grab")] + let callback = |event: Event| -> Option { + println!("{}", serde_json::to_string_pretty(&event).unwrap()); + None // CapsLock is now effectively disabled + // if let EventType::KeyPress(Key::CapsLock) = event.event_type { + // } else { + // Some(event) + // } + }; + // This will block. + // #[cfg(feature = "unstable_grab")] + println!("{}", 1); + if let Err(error) = grab(callback) { + println!("Error: {:?}", error) + } +} diff --git a/src/monitor/mod.rs b/src/monitor/mod.rs new file mode 100644 index 0000000..e52ce07 --- /dev/null +++ b/src/monitor/mod.rs @@ -0,0 +1,65 @@ +use napi::{ + bindgen_prelude::*, + threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode}, + JsBoolean, Result, +}; +use rdev::{grab, listen, Event}; +use std::{ + sync::mpsc::{self, Sender}, + thread::spawn, +}; + +#[napi(ts_args_type = "callback: (event: string) => void")] +pub fn on_input_event(callback: JsFunction) -> Result<()> { + let jsfn: ThreadsafeFunction = + callback.create_threadsafe_function(0, |ctx| Ok(vec![ctx.value]))?; + + spawn(|| { + if let Err(error) = listen(move |event| { + jsfn.call( + serde_json::to_string(&event).unwrap(), + ThreadsafeFunctionCallMode::NonBlocking, + ); + }) { + println!("Error: {:?}", error) + } + }); + Ok(()) +} + +#[napi(ts_args_type = "callback: (event: string) => boolean")] +pub fn grab_input_event(callback: JsFunction) -> Result<()> { + let jsfn: ThreadsafeFunction = + callback.create_threadsafe_function(0, |ctx| Ok(vec![ctx.value]))?; + + let gcallback = move |event: Event| -> Option { + let (s, r): (Sender, mpsc::Receiver) = mpsc::channel::(); + jsfn.call_with_return_value( + serde_json::to_string(&event).unwrap(), + ThreadsafeFunctionCallMode::NonBlocking, + move |e: JsBoolean| { + if let Ok(goon) = e.get_value() { + if !goon { + // 需要拦截事件 + s.send(false).unwrap(); + } + } + s.send(true).unwrap(); + Ok(()) + }, + ); + for i in r { + if !i { + return None; + } + } + Some(event) + }; + + spawn(|| { + if let Err(error) = grab(gcallback) { + println!("GrabError: {:?}", error) + } + }); + Ok(()) +} diff --git a/src/shotcut/exelook/dib.rs b/src/shotcut/exelook/dib.rs new file mode 100644 index 0000000..2f1c30b --- /dev/null +++ b/src/shotcut/exelook/dib.rs @@ -0,0 +1,267 @@ +use super::{Error, Result}; +use pelite::Error::Bounds; +use std::{convert::TryInto, fmt}; + +pub(crate) struct BitmapInfoHeader<'a> { + bytes: &'a [u8], +} + +#[derive(Debug)] +pub struct Pixel { + pub red: u8, + pub green: u8, + pub blue: u8, + pub alpha: u8, +} + +impl Pixel { + fn copy_to_vec(&self, vec: &mut Vec) { + vec.push(self.red); + vec.push(self.green); + vec.push(self.blue); + vec.push(self.alpha); + } +} + +pub struct DIB<'a> { + palette: &'a [u8], + xor_mask: &'a [u8], + and_mask: &'a [u8], + row_size: usize, + mask_row_size: usize, + real_height: usize, + width: usize, + bit_count: u16, + upside_down: bool, +} + +impl<'a> DIB<'a> { + fn pixel_at_1bpp(&self, x: usize, ry: usize, rym: usize) -> Pixel { + let idx = (self.xor_mask[ry + x / 8] >> (7 - (x % 8))) as usize & 1; + let blue = self.palette[idx * 4]; + let green = self.palette[idx * 4 + 1]; + let red = self.palette[idx * 4 + 2]; + let alpha = (self.and_mask[rym + x / 8] >> (7 - (x % 8))) & 1; + Pixel { + red, + green, + blue, + alpha: if alpha == 0 { 255 } else { 0 }, + } + } + fn pixel_at_4bpp(&self, x: usize, ry: usize, rym: usize) -> Pixel { + let idx = (self.xor_mask[ry + x / 2] >> (if x % 2 == 0 { 4 } else { 0 })) as usize & 15; + let blue = self.palette[idx * 4]; + let green = self.palette[idx * 4 + 1]; + let red = self.palette[idx * 4 + 2]; + let alpha = (self.and_mask[rym + x / 8] >> (7 - (x % 8))) & 1; + Pixel { + red, + green, + blue, + alpha: if alpha == 0 { 255 } else { 0 }, + } + } + fn pixel_at_8bpp(&self, x: usize, ry: usize, rym: usize) -> Pixel { + let idx = self.xor_mask[ry + x] as usize; + let blue = self.palette[idx * 4]; + let green = self.palette[idx * 4 + 1]; + let red = self.palette[idx * 4 + 2]; + let alpha = (self.and_mask[rym + x / 8] >> (7 - (x % 8))) & 1; + Pixel { + red, + green, + blue, + alpha: if alpha == 0 { 255 } else { 0 }, + } + } + fn pixel_at_24bpp(&self, x: usize, ry: usize, rym: usize) -> Pixel { + let blue = self.xor_mask[ry + x * 3]; + let green = self.xor_mask[ry + x * 3 + 1]; + let red = self.xor_mask[ry + x * 3 + 2]; + let alpha = (self.and_mask[rym + x / 8] >> (7 - (x % 8))) & 1; + Pixel { + red, + green, + blue, + alpha: if alpha == 0 { 255 } else { 0 }, + } + } + fn pixel_at_32bpp(&self, x: usize, ry: usize, rym: usize) -> Pixel { + let blue = self.xor_mask[ry + x * 4]; + let green = self.xor_mask[ry + x * 4 + 1]; + let red = self.xor_mask[ry + x * 4 + 2]; + let alpha = self.xor_mask[ry + x * 4 + 3]; + let mask = (self.and_mask[rym + x / 8] >> (7 - (x % 8))) & 1; + Pixel { + red, + green, + blue, + alpha: if alpha == 0 && mask == 0 { 255 } else { alpha }, + } + } + fn from_bytes<'b>(hdr: &'b BitmapInfoHeader, bytes: &'b [u8]) -> Result> { + match hdr.bit_count() { + 1 => { + let row_size = hdr.width() as usize / 8 + if hdr.width() % 8 != 0 { 1 } else { 0 }; + DIB::from_bytes_shared(hdr, bytes, 4 * 2, row_size) + } + 4 => { + let row_size = (hdr.width() / 2 + hdr.width() % 2) as usize; + DIB::from_bytes_shared(hdr, bytes, 4 * 16, row_size) + } + 8 => DIB::from_bytes_shared(hdr, bytes, 4 * 256, hdr.width() as usize), + 24 => DIB::from_bytes_shared(hdr, bytes, 0, hdr.width() as usize * 3), + 32 => DIB::from_bytes_shared(hdr, bytes, 0, hdr.width() as usize * 4), + _ => Err(Error::UnrecognizedBPP), + } + } + fn decode(&self) -> Vec { + let mut pixels = Vec::with_capacity(self.real_height * self.width * 4); + for y in 0..self.real_height { + for x in 0..self.width { + let ry = if self.upside_down { + (self.real_height - 1 - y) * self.row_size + } else { + y * self.row_size + }; + let rym = if self.upside_down { + (self.real_height - 1 - y) * self.mask_row_size + } else { + y * self.mask_row_size + }; + match self.bit_count { + // hoping that loop unswitching will kick in here + 1 => { + self.pixel_at_1bpp(x, ry, rym).copy_to_vec(&mut pixels); + } + 4 => { + self.pixel_at_4bpp(x, ry, rym).copy_to_vec(&mut pixels); + } + 8 => { + self.pixel_at_8bpp(x, ry, rym).copy_to_vec(&mut pixels); + } + 24 => { + self.pixel_at_24bpp(x, ry, rym).copy_to_vec(&mut pixels); + } + 32 => { + self.pixel_at_32bpp(x, ry, rym).copy_to_vec(&mut pixels); + } + _ => { + unreachable!(); + } + } + } + } + pixels + } + + fn from_bytes_shared<'b>( + hdr: &'b BitmapInfoHeader, + bytes: &'b [u8], + palette_size: usize, + mut row_size: usize, + ) -> Result> { + let header_size = 40; + let image_data_offset = header_size + palette_size; + let real_height = (hdr.height().abs() / 2) as usize; + if row_size % 4 != 0 { + row_size += 4 - row_size % 4; + } + let xor_mask_size = row_size * real_height; + let and_mask_offset = image_data_offset + xor_mask_size; + let mut mask_row_size = + hdr.width() as usize / 8 + if hdr.width() as usize % 8 != 0 { 1 } else { 0 }; + if mask_row_size % 4 != 0 { + mask_row_size += 4 - mask_row_size % 4; + } + let and_mask_size = mask_row_size * real_height; + let image_end = and_mask_offset + and_mask_size; + let and_mask = bytes + .get(and_mask_offset..image_end) + .ok_or_else(|| Error::from(Bounds))?; + let palette = &bytes[header_size..image_data_offset]; // first check dominates the following checks + let xor_mask = &bytes[image_data_offset..and_mask_offset]; // keep it above them to use only one .get + let upside_down = hdr.height() > 0; + Ok(DIB { + palette, + xor_mask, + and_mask, + row_size, + mask_row_size, + real_height, + upside_down, + width: hdr.width() as usize, + bit_count: hdr.bit_count(), + }) + } +} + +pub(crate) fn decode_dib(bytes: &[u8]) -> Result> { + let hdr = BitmapInfoHeader::from_bytes(bytes)?; + let dib = DIB::from_bytes(&hdr, bytes)?; + Ok(dib.decode()) +} + +impl<'a> BitmapInfoHeader<'a> { + pub(crate) fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() >= 40 { + Ok(BitmapInfoHeader { + bytes: &bytes[..40], + }) + } else { + Err(Bounds.into()) + } + } + pub(crate) fn size(&self) -> u32 { + u32::from_le_bytes(self.bytes[0..4].try_into().unwrap()) + } + pub(crate) fn width(&self) -> i32 { + i32::from_le_bytes(self.bytes[4..8].try_into().unwrap()) + } + pub(crate) fn height(&self) -> i32 { + i32::from_le_bytes(self.bytes[8..12].try_into().unwrap()) + } + pub(crate) fn planes(&self) -> u16 { + u16::from_le_bytes(self.bytes[12..14].try_into().unwrap()) + } + pub(crate) fn bit_count(&self) -> u16 { + u16::from_le_bytes(self.bytes[14..16].try_into().unwrap()) + } + pub(crate) fn compression(&self) -> u32 { + u32::from_le_bytes(self.bytes[16..20].try_into().unwrap()) + } + pub(crate) fn image_size(&self) -> u32 { + u32::from_le_bytes(self.bytes[20..24].try_into().unwrap()) + } + pub(crate) fn x_px_per_meter(&self) -> i32 { + i32::from_le_bytes(self.bytes[24..28].try_into().unwrap()) + } + pub(crate) fn y_px_per_meter(&self) -> i32 { + i32::from_le_bytes(self.bytes[28..32].try_into().unwrap()) + } + pub(crate) fn colors_used(&self) -> u32 { + u32::from_le_bytes(self.bytes[32..36].try_into().unwrap()) + } + pub(crate) fn colors_important(&self) -> u32 { + u32::from_le_bytes(self.bytes[36..40].try_into().unwrap()) + } +} + +impl<'a> fmt::Debug for BitmapInfoHeader<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("BitmapInfoHeader") + .field("size", &self.size()) + .field("width", &self.width()) + .field("height", &self.height()) + .field("planes", &self.planes()) + .field("bit_count", &self.bit_count()) + .field("compression", &self.compression()) + .field("image_size", &self.image_size()) + .field("x_px_per_meter", &self.x_px_per_meter()) + .field("y_px_per_meter", &self.y_px_per_meter()) + .field("colors_used", &self.colors_used()) + .field("colors_important", &self.colors_important()) + .finish() + } +} diff --git a/src/shotcut/exelook/mod.rs b/src/shotcut/exelook/mod.rs new file mode 100644 index 0000000..efb0b8c --- /dev/null +++ b/src/shotcut/exelook/mod.rs @@ -0,0 +1,197 @@ +// 借用该项目源码(Apache-2.0 license): https://github.com/Lucius-Q-User/ExeLook +mod dib; + +use base64::{engine::general_purpose, Engine}; + +use pelite::Error::Bounds; +use pelite::{ + self, + resources::{FindError, Resources}, + FileMap, PeFile, +}; + +use std::{ + convert::{From, TryInto}, + io, + str::Utf8Error, +}; + +impl<'a> BitmapInfoHeader<'a> { + pub(crate) fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() >= 40 { + Ok(BitmapInfoHeader { + bytes: &bytes[..40], + }) + } else { + Err(Bounds.into()) + } + } + pub(crate) fn width(&self) -> i32 { + i32::from_le_bytes(self.bytes[4..8].try_into().unwrap()) + } + pub(crate) fn height(&self) -> i32 { + i32::from_le_bytes(self.bytes[8..12].try_into().unwrap()) + } + pub(crate) fn planes(&self) -> u16 { + u16::from_le_bytes(self.bytes[12..14].try_into().unwrap()) + } + pub(crate) fn bit_count(&self) -> u16 { + u16::from_le_bytes(self.bytes[14..16].try_into().unwrap()) + } + pub(crate) fn compression(&self) -> u32 { + u32::from_le_bytes(self.bytes[16..20].try_into().unwrap()) + } +} + +struct BitmapInfoHeader<'a> { + bytes: &'a [u8], +} + +#[derive(Debug)] +pub enum Error { + Io(io::Error), + Pe(pelite::Error), + UTF(Utf8Error), + NoIconFound, + PlanarNotSupported, + UnrecognizedBPP, + UnknownCompression, + MalformedPng, +} + +impl From for Error { + fn from(err: Utf8Error) -> Self { + Error::UTF(err) + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: pelite::Error) -> Self { + Error::Pe(err) + } +} + +impl From for Error { + fn from(_err: FindError) -> Self { + Error::NoIconFound + } +} + +struct PngHeader<'a> { + bytes: &'a [u8], +} + +impl<'a> PngHeader<'a> { + fn from_bytes<'b>(bytes: &'b [u8]) -> Result> { + if bytes.len() < 24 || bytes[12..16] != [b'I', b'H', b'D', b'R'] { + Err(Error::MalformedPng) + } else { + Ok(PngHeader { bytes }) + } + } + fn width(&self) -> i32 { + u32::from_be_bytes(self.bytes[16..20].try_into().unwrap()) as i32 + } + fn height(&self) -> i32 { + u32::from_be_bytes(self.bytes[20..24].try_into().unwrap()) as i32 + } +} + +pub type Result = ::std::result::Result; + +fn get_resources(bytes: &[u8]) -> Result { + let res = PeFile::from_bytes(bytes)?.resources(); + if let Err(pelite::Error::Null) = res { + Err(Error::NoIconFound) + } else { + res.map_err(Into::into) + } +} + +fn is_png(bytes: &[u8]) -> bool { + bytes.starts_with(&[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) +} + +fn icon_compare_key(icon: &[u8]) -> Result { + Ok(if is_png(icon) { + let hdr = PngHeader::from_bytes(icon)?; + (hdr.width(), hdr.height(), 64) + } else { + let hdr = BitmapInfoHeader::from_bytes(icon)?; + (hdr.width(), hdr.height(), hdr.bit_count()) + }) +} + +fn best_icon<'a>(mut icons: impl Iterator>) -> Result<&'a [u8]> { + let mut cur_max: &'a [u8] = if let Some(x) = icons.next() { + x? + } else { + return Err(Error::NoIconFound); + }; + let mut cur_max_key = icon_compare_key(cur_max)?; + for icon in icons { + let icon = icon?; + let key = icon_compare_key(icon)?; + if key > cur_max_key { + cur_max = icon; + cur_max_key = key; + } + } + Ok(cur_max) +} + +#[napi] +pub struct ShorCutImg { + pub data: Vec, + pub width: i32, + pub height: i32, +} + +fn _exelook(file_name: String) -> Result { + let map_region = FileMap::open(&file_name)?; + let resources = get_resources(map_region.as_ref())?; + let (_, icon_group) = resources.icons().next().ok_or(Error::NoIconFound)??; + let icons = icon_group + .entries() + .iter() + .map(|ent| icon_group.image(ent.nId).map_err(Into::into)); + + let best_icon = best_icon(icons)?; + if is_png(best_icon) { + Ok(ShorCutImg { + data: best_icon.to_owned(), + width: 0, + height: 0, + }) + } else { + let infoheader = BitmapInfoHeader::from_bytes(best_icon)?; + if infoheader.planes() != 1 { + return Err(Error::PlanarNotSupported); + } + if infoheader.compression() != 0 { + return Err(Error::UnknownCompression); + } + let data = dib::decode_dib(best_icon)?; + + Ok(ShorCutImg { + data, + width: infoheader.width(), + height: infoheader.height() / 2, + }) + } +} + +#[napi] +pub fn exe_look_base64(file_name: String) -> napi::Result { + let look = _exelook(file_name); + match look { + Ok(l) => Ok("data:image/*;base64,".to_owned() + &general_purpose::STANDARD.encode(l.data)), + Err(e) => Err(napi::Error::from_reason(format!("{:?}", e))), + } +} diff --git a/src/shotcut/mod.rs b/src/shotcut/mod.rs new file mode 100644 index 0000000..36bc8a7 --- /dev/null +++ b/src/shotcut/mod.rs @@ -0,0 +1,69 @@ +use lnk_parser::LNKParser; +use napi::Result; +use std::path::{absolute, PathBuf}; +pub mod exelook; + +#[napi] +pub fn parse_lnk(path: String) -> Result { + let lnk_file = LNKParser::from_path(&path); + match lnk_file { + Ok(f) => { + let name_string = f.get_name_string().as_ref().map(|f| f.to_string()); + let full_path = f + .get_target_full_path() + .as_ref() + .map(|f| { + if f.starts_with("MY_COMPUTER\\") { + Some(f.to_string().replace("MY_COMPUTER\\", "")) + } else { + Some(f.to_string()) + } + }) + .map_or(None, |f| f); + let working_dir = f.get_working_dir().as_ref().map(|f| f.to_string()); + let icon_location = f.get_icon_location().as_ref().map(|f| f.to_string()); + + Ok(LnkData { + name_string, + full_path, + working_dir, + icon_location, + }) + } + Err(_) => { + let lnk_path = std::path::Path::new(&path); + let lnk = parselnk::Lnk::try_from(lnk_path); + match lnk { + Ok(l) => { + let s = absolute( + PathBuf::from(lnk_path) + .parent() + .unwrap() + .join(l.string_data.relative_path.unwrap()), + ) + .map_or(None, |f| Some(f)); + + Ok(LnkData { + name_string: l.string_data.name_string, + full_path: convert(s), + working_dir: convert(l.string_data.working_dir), + icon_location: convert(l.string_data.icon_location), + }) + } + Err(e) => Err(napi::Error::from_reason(e.to_string())), + } + } + } +} + +#[napi] +pub struct LnkData { + pub name_string: Option, + pub full_path: Option, + pub working_dir: Option, + pub icon_location: Option, +} + +fn convert(p: Option) -> Option { + p.map(|f| f.to_string_lossy().to_string()) +} diff --git a/src/simulation/mod.rs b/src/simulation/mod.rs new file mode 100644 index 0000000..9358790 --- /dev/null +++ b/src/simulation/mod.rs @@ -0,0 +1,122 @@ +use enigo::{Enigo, KeyboardControllable, MouseButton, MouseControllable}; +use napi::Result; + +#[napi] +pub fn send_keyboard_simulation(cmd: String) -> Result<()> { + let mut enigo = Enigo::new(); + if let Err(e) = enigo.key_sequence_parse_try(&cmd) { + Err(napi::Error::from_reason(e.to_string())) + } else { + Ok(()) + } +} + +#[napi] +#[derive(Debug)] +pub enum MouseBtn { + Left, + Middle, + Right, + Back, + Forward, +} + +#[napi] +pub enum MouseAction { + Locaion, + MoveRelative, + MoveTo, + ScrollX, + ScrollY, + Up, + Down, + Click, +} + +#[napi(object)] +pub struct MouseActionInput { + pub action: MouseAction, + pub data: Option, + pub button: Option, +} + +#[napi(object)] +pub struct Position { + pub x: i32, + pub y: i32, +} + +fn convert_btn(btn: Option) -> Option { + match btn { + Some(MouseBtn::Left) => Some(MouseButton::Left), + Some(MouseBtn::Middle) => Some(MouseButton::Middle), + Some(MouseBtn::Right) => Some(MouseButton::Right), + #[cfg(any(target_os = "windows", target_os = "linux"))] + Some(MouseBtn::Back) => Some(MouseButton::Back), + #[cfg(any(target_os = "windows", target_os = "linux"))] + Some(MouseBtn::Forward) => Some(MouseButton::Forward), + #[allow(unreachable_patterns)] + Some(b) => { + println!("未识别按钮: {:#?}", b); + None + } + None => { + println!("未输入按钮"); + None + } + } +} + +#[napi] +pub fn send_mouse_simulation(input: MouseActionInput) -> Option { + let mut enigo = Enigo::new(); + + match input.action { + MouseAction::MoveRelative => { + if let Some(p) = input.data { + enigo.mouse_move_relative(p.x, p.y); + } + None + } + MouseAction::MoveTo => { + if let Some(p) = input.data { + enigo.mouse_move_to(p.x, p.y); + } + None + } + MouseAction::ScrollX => { + if let Some(p) = input.data { + enigo.mouse_scroll_x(p.x); + } + None + } + MouseAction::ScrollY => { + if let Some(p) = input.data { + enigo.mouse_scroll_y(p.y); + } + None + } + MouseAction::Locaion => { + let (x, y) = enigo.mouse_location(); + Some(Position { x, y }) + } + MouseAction::Down => { + if let Some(b) = convert_btn(input.button) { + enigo.mouse_down(b); + } + None + } + MouseAction::Up => { + if let Some(b) = convert_btn(input.button) { + enigo.mouse_up(b); + } + None + } + MouseAction::Click => { + if let Some(b) = convert_btn(input.button) { + enigo.mouse_click(b); + } + None + } + } +}