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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { sanitizeForTitle } from "./commandBuffer";
import {
createTerminalInstance,
getDefaultTerminalBg,
setupClickToMoveCursor,
setupFocusListener,
setupKeyboardHandler,
setupPasteHandler,
Expand Down Expand Up @@ -342,15 +343,22 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => {
clearScrollbackRef.current({ paneId });
};

const handleWrite = (data: string) => {
if (!isExitedRef.current) {
writeRef.current({ paneId, data });
}
};

const cleanupKeyboard = setupKeyboardHandler(xterm, {
onShiftEnter: () => {
if (!isExitedRef.current) {
writeRef.current({ paneId, data: "\\\n" });
}
},
onShiftEnter: () => handleWrite("\\\n"),
onClear: handleClear,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Setup click-to-move cursor (click on prompt line to move cursor)
const cleanupClickToMove = setupClickToMoveCursor(xterm, {
onWrite: handleWrite,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Register clear callback for context menu access
registerClearCallbackRef.current(paneId, handleClear);

Expand All @@ -376,6 +384,7 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => {
inputDisposable.dispose();
keyDisposable.dispose();
cleanupKeyboard();
cleanupClickToMove();
cleanupFocus?.();
cleanupResize();
cleanupPaste();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,99 @@ export function setupResizeHandlers(
debouncedHandleResize.cancel();
};
}

export interface ClickToMoveOptions {
/** Callback to write data to the terminal PTY */
onWrite: (data: string) => void;
}

/**
* Convert mouse event coordinates to terminal cell coordinates.
* Returns null if coordinates cannot be determined.
*/
function getTerminalCoordsFromEvent(
xterm: XTerm,
event: MouseEvent,
): { col: number; row: number } | null {
const element = xterm.element;
if (!element) return null;

const rect = element.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;

// Note: xterm.js does not expose a public API for mouse-to-coords conversion,
// so we must access internal _core._renderService.dimensions. This is fragile
// and may break in future xterm.js versions.
const dimensions = (
xterm as unknown as {
_core?: {
_renderService?: {
dimensions?: { css: { cell: { width: number; height: number } } };
};
};
}
)._core?._renderService?.dimensions;
if (!dimensions?.css?.cell) return null;

const cellWidth = dimensions.css.cell.width;
const cellHeight = dimensions.css.cell.height;

if (cellWidth <= 0 || cellHeight <= 0) return null;

// Clamp to valid terminal grid range to prevent excessive delta calculations
const col = Math.max(0, Math.min(xterm.cols - 1, Math.floor(x / cellWidth)));
const row = Math.max(0, Math.min(xterm.rows - 1, Math.floor(y / cellHeight)));

return { col, row };
}

/**
* Setup click-to-move cursor functionality.
* Allows clicking on the current prompt line to move the cursor to that position.
*
* This works by calculating the difference between click position and cursor position,
* then sending the appropriate number of arrow key sequences to move the cursor.
*
* Limitations:
* - Only works on the current line (same row as cursor)
* - Only works at the shell prompt (not in full-screen apps like vim)
* - Requires the shell to interpret arrow key sequences
*
* Returns a cleanup function to remove the handler.
*/
export function setupClickToMoveCursor(
xterm: XTerm,
options: ClickToMoveOptions,
): () => void {
const handleClick = (event: MouseEvent) => {
// Don't interfere with full-screen apps (vim, less, etc. use alternate buffer)
if (xterm.buffer.active !== xterm.buffer.normal) return;
if (event.button !== 0) return;
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)
return;
if (xterm.hasSelection()) return;

const coords = getTerminalCoordsFromEvent(xterm, event);
if (!coords) return;

const buffer = xterm.buffer.active;
const clickBufferRow = coords.row + buffer.viewportY;

// Only move cursor on the same line (editable prompt area)
if (clickBufferRow !== buffer.cursorY + buffer.viewportY) return;

const delta = coords.col - buffer.cursorX;
if (delta === 0) return;

// Right arrow: \x1b[C, Left arrow: \x1b[D
const arrowKey = delta > 0 ? "\x1b[C" : "\x1b[D";
options.onWrite(arrowKey.repeat(Math.abs(delta)));
};

xterm.element?.addEventListener("click", handleClick);

return () => {
xterm.element?.removeEventListener("click", handleClick);
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading