mirror of
https://github.com/OpenWhispr/openwhispr.git
synced 2026-10-21 06:40:37 +08:00
* fix(linux): keep the dictation pill clickable after the first hover
setMainWindowInteractivity returned the pill to click-through on mouse-leave
with setIgnoreMouseEvents(true, { forward: true }). Electron ignores `forward`
outside macOS: on Linux it sets a 1x1 X11 input region and nothing forwards
hover back, so the window never saw another mouseenter. The cancel X appeared
on the first hover and never again, and a recording could no longer be
cancelled from the pill.
Only macOS can forward hover through a click-through window, so treat Linux
like Windows and keep normal hit-testing. This is the same Electron limitation
that stranded the meeting card's buttons in #1456, and the companion pill
already guards it this way.
Reproduced on Ubuntu 26.04 GNOME Wayland (the app forces XWayland) with real
uinput pointer events: hover #1 logged setIgnoreMouseEvents(false) and revealed
the cancel button; after the mouse-leave put the window back into click-through,
hovers #2 and #3 produced no native calls at all.
Closes #2136
Claude-Session: https://claude.ai/code/session_01Rs6g9RUbpHn5Hg8gBRPdWx
* fix(dictation): keep the pill visible when Retry starts a new recording
Retry on the dictation error card starts the next dictation, and a successful
start dismisses the card. Leaving the error footprint releases the pill
visibility handoff, whose shouldAutoHide only asked whether "Auto-hide when
idle" was enabled — so it hid the whole pill window while that new recording
was running. The recording kept going with nothing on screen until the user
pressed the hotkey again. The same happens when the hotkey (rather than Retry)
starts a dictation while the card is up.
Feed the hook the live dictation state and let auto-hide act only when nothing
is recording or processing, which is the idle condition App.jsx's own auto-hide
effect already applies.
Reproduced on Ubuntu 26.04 GNOME Wayland: after the Retry click the main
process logged hide() and reported the window not visible for the rest of the
recording, while a capture stream stayed live.
Closes #2141
Claude-Session: https://claude.ai/code/session_01Rs6g9RUbpHn5Hg8gBRPdWx
* fix(linux): Preserve click-through around the dictation pill
Recover hover from native pointer coordinates so the pill and cancel control
remain usable without blocking clicks through the transparent window padding.
Suspend sampling while hidden and preserve capture during menus, errors,
panels, and dragging. Keep the tooltip aligned with recovered hover state.
Cover pointer recovery, visibility changes, stale replies, zoom conversion,
IPC sender validation, and unchanged Windows and macOS input handling.
* fix(linux): Restore pill hover with native input regions
Use X11 input shaping to keep the pill interactive without relying on cached
cursor coordinates. Preserve click-through padding and sequence overlay capture
through the same native writer.
Cover native hit testing, scaling, lifecycle, and helper failure fallback.
---------
Co-authored-by: boseq <307062491+boseq@users.noreply.github.com>
Co-authored-by: Chadpiha <chadpiha23@gmail.com>
216 lines
5.3 KiB
JavaScript
Executable File
216 lines
5.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
const { spawnSync } = require("child_process");
|
|
const crypto = require("crypto");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const isLinux = process.platform === "linux";
|
|
if (!isLinux) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const projectRoot = path.resolve(__dirname, "..");
|
|
const cSource = path.join(projectRoot, "resources", "linux-fast-paste.c");
|
|
const outputDir = path.join(projectRoot, "resources", "bin");
|
|
const outputBinary = path.join(outputDir, "linux-fast-paste");
|
|
const hashFile = path.join(outputDir, ".linux-fast-paste.hash");
|
|
|
|
function log(message) {
|
|
console.log(`[linux-fast-paste] ${message}`);
|
|
}
|
|
|
|
function ensureDir(dirPath) {
|
|
if (!fs.existsSync(dirPath)) {
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
}
|
|
}
|
|
|
|
if (!fs.existsSync(cSource)) {
|
|
console.error(`[linux-fast-paste] C source not found at ${cSource}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
ensureDir(outputDir);
|
|
|
|
let needsBuild = true;
|
|
if (fs.existsSync(outputBinary)) {
|
|
try {
|
|
const binaryStat = fs.statSync(outputBinary);
|
|
const sourceStat = fs.statSync(cSource);
|
|
if (binaryStat.mtimeMs >= sourceStat.mtimeMs) {
|
|
needsBuild = false;
|
|
}
|
|
} catch {
|
|
needsBuild = true;
|
|
}
|
|
}
|
|
|
|
function hasUinputHeaders() {
|
|
for (const compiler of ["gcc", "cc"]) {
|
|
try {
|
|
const result = spawnSync(compiler, ["-E", "-x", "c", "-"], {
|
|
input: "#include <linux/uinput.h>\n",
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
env: process.env,
|
|
});
|
|
if (result.status === 0) return true;
|
|
} catch {}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function hasGio() {
|
|
try {
|
|
const result = spawnSync("pkg-config", ["--exists", "gio-2.0"], {
|
|
stdio: "pipe",
|
|
});
|
|
return result.status === 0;
|
|
} catch {}
|
|
return false;
|
|
}
|
|
|
|
function getGioFlags() {
|
|
try {
|
|
const cflags = spawnSync("pkg-config", ["--cflags", "gio-2.0"], {
|
|
stdio: "pipe",
|
|
});
|
|
const libs = spawnSync("pkg-config", ["--libs", "gio-2.0"], {
|
|
stdio: "pipe",
|
|
});
|
|
if (cflags.status === 0 && libs.status === 0) {
|
|
return [
|
|
...cflags.stdout.toString().trim().split(/\s+/),
|
|
...libs.stdout.toString().trim().split(/\s+/),
|
|
].filter(Boolean);
|
|
}
|
|
} catch {}
|
|
return [];
|
|
}
|
|
|
|
function hasAtspi() {
|
|
try {
|
|
const result = spawnSync("pkg-config", ["--exists", "atspi-2"], {
|
|
stdio: "pipe",
|
|
});
|
|
return result.status === 0;
|
|
} catch {}
|
|
return false;
|
|
}
|
|
|
|
function getAtspiFlags() {
|
|
try {
|
|
const cflags = spawnSync("pkg-config", ["--cflags", "atspi-2"], {
|
|
stdio: "pipe",
|
|
});
|
|
const libs = spawnSync("pkg-config", ["--libs", "atspi-2"], {
|
|
stdio: "pipe",
|
|
});
|
|
if (cflags.status === 0 && libs.status === 0) {
|
|
return [
|
|
...cflags.stdout.toString().trim().split(/\s+/),
|
|
...libs.stdout.toString().trim().split(/\s+/),
|
|
].filter(Boolean);
|
|
}
|
|
} catch {}
|
|
return [];
|
|
}
|
|
|
|
const uinputAvailable = hasUinputHeaders();
|
|
const gioAvailable = hasGio();
|
|
const atspiAvailable = hasAtspi();
|
|
|
|
function computeBuildHash() {
|
|
const sourceContent = fs.readFileSync(cSource, "utf8");
|
|
const flags = [
|
|
uinputAvailable ? "uinput" : "nouinput",
|
|
gioAvailable ? "gio" : "nogio",
|
|
atspiAvailable ? "atspi" : "noatspi",
|
|
].join("+");
|
|
return crypto
|
|
.createHash("sha256")
|
|
.update(sourceContent + flags)
|
|
.digest("hex");
|
|
}
|
|
|
|
if (!needsBuild && fs.existsSync(outputBinary)) {
|
|
try {
|
|
const currentHash = computeBuildHash();
|
|
|
|
if (fs.existsSync(hashFile)) {
|
|
const savedHash = fs.readFileSync(hashFile, "utf8").trim();
|
|
if (savedHash !== currentHash) {
|
|
log("Source or build flags changed, rebuild needed");
|
|
needsBuild = true;
|
|
}
|
|
} else {
|
|
fs.writeFileSync(hashFile, currentHash);
|
|
}
|
|
} catch (err) {
|
|
log(`Hash check failed: ${err.message}, forcing rebuild`);
|
|
needsBuild = true;
|
|
}
|
|
}
|
|
|
|
if (!needsBuild) {
|
|
process.exit(0);
|
|
}
|
|
|
|
function attemptCompile(command, args) {
|
|
log(`Compiling with ${[command, ...args].join(" ")}`);
|
|
return spawnSync(command, args, {
|
|
stdio: "inherit",
|
|
env: process.env,
|
|
});
|
|
}
|
|
|
|
const compileArgs = ["-O2", cSource, "-o", outputBinary, "-lX11", "-lXtst", "-lXext", "-lm"];
|
|
|
|
if (uinputAvailable) {
|
|
log("uinput headers found, enabling uinput support");
|
|
compileArgs.push("-DHAVE_UINPUT");
|
|
} else {
|
|
log("uinput headers not found, building without uinput support");
|
|
}
|
|
|
|
if (gioAvailable) {
|
|
log("gio-2.0 found, enabling portal support");
|
|
compileArgs.push("-DHAVE_GIO", ...getGioFlags());
|
|
} else {
|
|
log("gio-2.0 not found, building without portal support");
|
|
}
|
|
|
|
if (atspiAvailable) {
|
|
log("atspi-2 found, enabling AT-SPI2 terminal detection");
|
|
compileArgs.push("-DHAVE_ATSPI", ...getAtspiFlags());
|
|
} else {
|
|
log("atspi-2 not found, building without AT-SPI2 terminal detection");
|
|
}
|
|
|
|
let result = attemptCompile("gcc", compileArgs);
|
|
|
|
if (result.status !== 0) {
|
|
result = attemptCompile("cc", compileArgs);
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
console.warn(
|
|
"[linux-fast-paste] Failed to compile Linux fast-paste binary. Install libx11-dev and libxtst-dev to enable native paste. Falling back to system tools."
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
try {
|
|
fs.chmodSync(outputBinary, 0o755);
|
|
} catch (error) {
|
|
console.warn(`[linux-fast-paste] Unable to set executable permissions: ${error.message}`);
|
|
}
|
|
|
|
try {
|
|
fs.writeFileSync(hashFile, computeBuildHash());
|
|
} catch (err) {
|
|
log(`Warning: Could not save source hash: ${err.message}`);
|
|
}
|
|
|
|
log("Successfully built Linux fast-paste binary.");
|