Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
node_modules/
node_modules
dist/
*.bun-build
37 changes: 36 additions & 1 deletion desktop/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ ipcMain.handle("app:init", () => {
const layout = config.restore ? readJson(layoutFile()) : undefined;
const open = pendingSpec;
pendingSpec = undefined;
return { config, layout, open, version: VERSION, platform: process.platform };
return { config, layout, open, version: VERSION, platform: process.platform, smoke: !!process.env.HQTERM_DESKTOP_SMOKE };
});

ipcMain.handle("hosts:list", () => listHosts());
Expand Down Expand Up @@ -276,6 +276,10 @@ ipcMain.handle("clipboard:read", () => clipboard.readText());
ipcMain.on("clipboard:write", (_e, text) => {
if (typeof text === "string") clipboard.writeText(text);
});
// PRIMARY selection exists only on Linux (X11/Wayland); a no-op elsewhere.
ipcMain.on("selection:write", (_e, text) => {
if (typeof text === "string" && process.platform === "linux") clipboard.writeText(text, "selection");
});
ipcMain.on("open:url", (_e, url) => {
if (typeof url === "string" && /^https?:\/\//.test(url)) shell.openExternal(url);
});
Expand Down Expand Up @@ -328,6 +332,33 @@ if (!app.requestSingleInstanceLock()) {
win.webContents.sendInputEvent({ type: "keyDown", keyCode: "D", modifiers: ["control", "shift"] });
win.webContents.sendInputEvent({ type: "keyUp", keyCode: "D", modifiers: ["control", "shift"] });
}, wait * 0.7);
// Shift+drag must leave a selection that survives the mouse moving on,
// even when the app tracks every motion (hqtui hover, DECSET 1003).
let selection = "not run";
setTimeout(() => {
const p = [...ptys.values()].pop();
if (p) p.write("clear; printf 'select me please\\n\\033[?1003h'\r");
}, wait * 0.75);
setTimeout(async () => {
if (!win) return;
try {
const r = await win.webContents.executeJavaScript(`(() => {
const b = document.querySelector(".pane.focused .xterm-screen").getBoundingClientRect();
return { x: Math.round(b.left + 4), y: Math.round(b.top + 6), mode: window.hqSmoke.mouseMode() };
})()`);
const send = (type, x, modifiers) => win.webContents.sendInputEvent({ type, x, y: r.y, button: "left", clickCount: 1, modifiers });
send("mouseDown", r.x, ["shift"]);
for (let x = r.x; x <= r.x + 80; x += 10) send("mouseMove", x, ["shift", "leftButtonDown"]);
send("mouseUp", r.x + 80, ["shift"]);
await new Promise((res) => setTimeout(res, 100));
for (let x = r.x + 80; x <= r.x + 140; x += 10) win.webContents.sendInputEvent({ type: "mouseMove", x, y: r.y + 20 });
await new Promise((res) => setTimeout(res, 300));
const sel = await win.webContents.executeJavaScript("window.hqSmoke.selection()");
selection = r.mode !== "any" ? `mouse mode ${r.mode}, not any` : sel ? "" : "cleared after release";
} catch (err) {
selection = `error: ${err && err.message}`;
}
}, wait * 0.85);
setTimeout(async () => {
const shot = process.env.HQTERM_DESKTOP_SMOKE_SHOT;
if (shot && win) {
Expand Down Expand Up @@ -357,6 +388,10 @@ if (!app.requestSingleInstanceLock()) {
console.log(`hqterm-desktop smoke: clipped: ${clipped.join("; ")}`);
return app.exit(4);
}
if (selection) {
console.log(`hqterm-desktop smoke: selection: ${selection}`);
return app.exit(5);
}
console.log(`hqterm-desktop smoke: ok (ptys=${ptys.size})`);
app.exit(ptys.size > 0 ? 0 : 3);
}, wait);
Expand Down
4 changes: 2 additions & 2 deletions desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hqterm-desktop",
"productName": "hqterm",
"version": "0.3.0",
"version": "0.3.1",
"description": "hqterm desktop: a terminal window that shows hqtui/qc images and HD emoji (xterm.js + node-pty).",
"license": "MIT",
"author": "Profullstack, Inc. <hello@profullstack.com>",
Expand Down
1 change: 1 addition & 0 deletions desktop/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ contextBridge.exposeInMainWorld("hq", {
onOpen: (fn) => ipcRenderer.on("app:open", (_e, spec) => fn(spec)),
readClipboard: () => ipcRenderer.invoke("clipboard:read"),
writeClipboard: (text) => ipcRenderer.send("clipboard:write", text),
writeSelection: (text) => ipcRenderer.send("selection:write", text),
openUrl: (url) => ipcRenderer.send("open:url", url),
});
25 changes: 25 additions & 0 deletions desktop/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
const early = new Map(); // pty id -> { data: string[], exit?: [code, signal] }
let webglCount = 0;

// CI smoke test (main.js) checks the focused pane's selection through this.
if (init.smoke) {
window.hqSmoke = {
mouseMode: () => focusedRt()?.term.modes.mouseTrackingMode,
selection: () => focusedRt()?.term.getSelection() || "",
};
}

// ---------- PTY plumbing ----------

hq.onData((id, data) => {
Expand Down Expand Up @@ -117,6 +125,23 @@
rt.title = t;
renderBar();
});
// Shift+drag selects even when the app tracks the mouse, but xterm.js counts
// every mouse report as user input and clears the selection on it. With
// any-motion tracking (hqtui's hover) the first move after releasing the
// button wiped the selection, so hold back hover reports while text is selected.
el.addEventListener(
"mousemove",
(e) => {
if (!e.buttons && term.modes.mouseTrackingMode === "any" && term.hasSelection()) e.stopPropagation();
},
true,
);
// Like other Linux terminals, a selection is also the PRIMARY selection
// (middle-click pastes it); Ctrl+Shift+C still copies to the clipboard.
term.onSelectionChange(() => {
const sel = term.getSelection();
if (sel) hq.writeSelection(sel);
});
// One click focuses a pane.
el.addEventListener("mousedown", () => setFocus(pane.id), true);
el.addEventListener("focusin", () => setFocus(pane.id, false));
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hqterm",
"version": "0.3.0",
"version": "0.3.1",
"description": "Terminal session manager: persistent hqsh sessions with images and HD emoji, built on hqtui.",
"license": "MIT",
"private": true,
Expand Down
Loading