2025-04-18 17:44:24 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2025-08-26 00:04:53 +02:00
|
|
|
import type React from 'react';
|
2025-11-17 15:48:33 -08:00
|
|
|
import clipboardy from 'clipboardy';
|
2025-08-26 00:04:53 +02:00
|
|
|
import { useCallback, useEffect, useState, useRef } from 'react';
|
2025-11-19 15:49:39 -08:00
|
|
|
import { Box, Text, type DOMElement } from 'ink';
|
2025-09-15 12:49:23 -05:00
|
|
|
import { SuggestionsDisplay, MAX_WIDTH } from './SuggestionsDisplay.js';
|
2025-08-07 16:11:35 -07:00
|
|
|
import { theme } from '../semantic-colors.js';
|
2025-05-13 16:23:14 -07:00
|
|
|
import { useInputHistory } from '../hooks/useInputHistory.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
import type { TextBuffer } from './shared/text-buffer.js';
|
|
|
|
|
import { logicalPosToOffset } from './shared/text-buffer.js';
|
2025-08-21 16:04:04 +08:00
|
|
|
import { cpSlice, cpLen, toCodePoints } from '../utils/textUtils.js';
|
2025-05-20 16:50:32 -07:00
|
|
|
import chalk from 'chalk';
|
|
|
|
|
import stringWidth from 'string-width';
|
2025-06-17 22:17:16 -04:00
|
|
|
import { useShellHistory } from '../hooks/useShellHistory.js';
|
2025-08-04 00:53:24 +05:00
|
|
|
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
|
2025-08-04 13:35:26 -07:00
|
|
|
import { useCommandCompletion } from '../hooks/useCommandCompletion.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
import type { Key } from '../hooks/useKeypress.js';
|
|
|
|
|
import { useKeypress } from '../hooks/useKeypress.js';
|
2025-08-09 16:03:17 +09:00
|
|
|
import { keyMatchers, Command } from '../keyMatchers.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
|
|
|
|
import type { Config } from '@google/gemini-cli-core';
|
2025-09-11 10:34:29 -07:00
|
|
|
import { ApprovalMode } from '@google/gemini-cli-core';
|
2025-09-17 13:17:50 -07:00
|
|
|
import {
|
|
|
|
|
parseInputForHighlighting,
|
|
|
|
|
buildSegmentsForVisualSlice,
|
|
|
|
|
} from '../utils/highlight.js';
|
2025-10-01 15:21:57 -07:00
|
|
|
import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js';
|
2025-07-12 00:06:49 -04:00
|
|
|
import {
|
|
|
|
|
clipboardHasImage,
|
|
|
|
|
saveClipboardImage,
|
|
|
|
|
cleanupOldClipboardImages,
|
|
|
|
|
} from '../utils/clipboardUtils.js';
|
2025-12-01 12:29:03 -05:00
|
|
|
import {
|
|
|
|
|
isAutoExecutableCommand,
|
|
|
|
|
isSlashCommand,
|
|
|
|
|
} from '../utils/commandUtils.js';
|
2025-08-25 22:11:27 +02:00
|
|
|
import * as path from 'node:path';
|
2025-08-28 20:52:14 +00:00
|
|
|
import { SCREEN_READER_USER_PREFIX } from '../textConstants.js';
|
2025-09-20 10:59:37 -07:00
|
|
|
import { useShellFocusState } from '../contexts/ShellFocusContext.js';
|
2025-10-09 19:27:20 -07:00
|
|
|
import { useUIState } from '../contexts/UIStateContext.js';
|
2025-10-15 22:32:50 +05:30
|
|
|
import { StreamingState } from '../types.js';
|
2025-11-19 15:49:39 -08:00
|
|
|
import { useMouseClick } from '../hooks/useMouseClick.js';
|
2025-11-03 13:41:58 -08:00
|
|
|
import { useMouse, type MouseEvent } from '../contexts/MouseContext.js';
|
2025-11-19 15:49:39 -08:00
|
|
|
import { useUIActions } from '../contexts/UIActionsContext.js';
|
2025-10-01 15:21:57 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns if the terminal can be trusted to handle paste events atomically
|
|
|
|
|
* rather than potentially sending multiple paste events separated by line
|
|
|
|
|
* breaks which could trigger unintended command execution.
|
|
|
|
|
*/
|
|
|
|
|
export function isTerminalPasteTrusted(
|
|
|
|
|
kittyProtocolSupported: boolean,
|
|
|
|
|
): boolean {
|
|
|
|
|
// Ideally we could trust all VSCode family terminals as well but it appears
|
|
|
|
|
// we cannot as Cursor users on windows reported being impacted by this
|
|
|
|
|
// issue (https://github.com/google-gemini/gemini-cli/issues/3763).
|
|
|
|
|
return kittyProtocolSupported;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 23:01:26 -07:00
|
|
|
export interface InputPromptProps {
|
2025-06-13 09:59:09 -07:00
|
|
|
buffer: TextBuffer;
|
2025-04-17 18:06:21 -04:00
|
|
|
onSubmit: (value: string) => void;
|
2025-05-13 16:23:14 -07:00
|
|
|
userMessages: readonly string[];
|
2025-05-14 17:33:37 -07:00
|
|
|
onClearScreen: () => void;
|
2025-07-07 16:45:44 -04:00
|
|
|
config: Config;
|
2025-07-20 16:57:34 -04:00
|
|
|
slashCommands: readonly SlashCommand[];
|
2025-07-07 16:45:44 -04:00
|
|
|
commandContext: CommandContext;
|
2025-05-20 16:50:32 -07:00
|
|
|
placeholder?: string;
|
|
|
|
|
focus?: boolean;
|
2025-06-13 09:59:09 -07:00
|
|
|
inputWidth: number;
|
|
|
|
|
suggestionsWidth: number;
|
2025-05-18 01:18:32 -07:00
|
|
|
shellModeActive: boolean;
|
|
|
|
|
setShellModeActive: (value: boolean) => void;
|
2025-09-11 10:34:29 -07:00
|
|
|
approvalMode: ApprovalMode;
|
2025-08-10 06:26:43 +08:00
|
|
|
onEscapePromptChange?: (showPrompt: boolean) => void;
|
2025-11-11 07:50:11 -08:00
|
|
|
onSuggestionsVisibilityChange?: (visible: boolean) => void;
|
2025-07-25 15:36:42 -07:00
|
|
|
vimHandleInput?: (key: Key) => boolean;
|
2025-09-20 10:59:37 -07:00
|
|
|
isEmbeddedShellFocused?: boolean;
|
2025-10-15 22:32:50 +05:30
|
|
|
setQueueErrorMessage: (message: string | null) => void;
|
|
|
|
|
streamingState: StreamingState;
|
2025-12-01 17:33:03 -08:00
|
|
|
popAllMessages?: () => string | undefined;
|
2025-11-11 07:50:11 -08:00
|
|
|
suggestionsPosition?: 'above' | 'below';
|
2025-11-18 12:01:16 -05:00
|
|
|
setBannerVisible: (visible: boolean) => void;
|
2025-05-13 11:24:04 -07:00
|
|
|
}
|
|
|
|
|
|
2025-09-17 13:17:50 -07:00
|
|
|
// The input content, input container, and input suggestions list may have different widths
|
2025-10-09 19:27:20 -07:00
|
|
|
export const calculatePromptWidths = (mainContentWidth: number) => {
|
2025-09-17 13:17:50 -07:00
|
|
|
const FRAME_PADDING_AND_BORDER = 4; // Border (2) + padding (2)
|
|
|
|
|
const PROMPT_PREFIX_WIDTH = 2; // '> ' or '! '
|
|
|
|
|
|
|
|
|
|
const FRAME_OVERHEAD = FRAME_PADDING_AND_BORDER + PROMPT_PREFIX_WIDTH;
|
2025-10-09 19:27:20 -07:00
|
|
|
const suggestionsWidth = Math.max(20, mainContentWidth);
|
2025-09-17 13:17:50 -07:00
|
|
|
|
|
|
|
|
return {
|
2025-10-09 19:27:20 -07:00
|
|
|
inputWidth: Math.max(mainContentWidth - FRAME_OVERHEAD, 1),
|
|
|
|
|
containerWidth: mainContentWidth,
|
2025-09-17 13:17:50 -07:00
|
|
|
suggestionsWidth,
|
|
|
|
|
frameOverhead: FRAME_OVERHEAD,
|
|
|
|
|
} as const;
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-30 08:31:32 -07:00
|
|
|
export const InputPrompt: React.FC<InputPromptProps> = ({
|
2025-06-13 09:59:09 -07:00
|
|
|
buffer,
|
2025-04-30 08:31:32 -07:00
|
|
|
onSubmit,
|
2025-05-13 16:23:14 -07:00
|
|
|
userMessages,
|
2025-05-14 17:33:37 -07:00
|
|
|
onClearScreen,
|
2025-05-20 16:50:32 -07:00
|
|
|
config,
|
|
|
|
|
slashCommands,
|
2025-07-07 16:45:44 -04:00
|
|
|
commandContext,
|
2025-06-06 13:44:11 -07:00
|
|
|
placeholder = ' Type your message or @path/to/file',
|
2025-05-20 16:50:32 -07:00
|
|
|
focus = true,
|
2025-06-13 09:59:09 -07:00
|
|
|
inputWidth,
|
|
|
|
|
suggestionsWidth,
|
2025-05-18 01:18:32 -07:00
|
|
|
shellModeActive,
|
|
|
|
|
setShellModeActive,
|
2025-09-11 10:34:29 -07:00
|
|
|
approvalMode,
|
2025-08-10 06:26:43 +08:00
|
|
|
onEscapePromptChange,
|
2025-11-11 07:50:11 -08:00
|
|
|
onSuggestionsVisibilityChange,
|
2025-07-25 15:36:42 -07:00
|
|
|
vimHandleInput,
|
2025-09-20 10:59:37 -07:00
|
|
|
isEmbeddedShellFocused,
|
2025-10-15 22:32:50 +05:30
|
|
|
setQueueErrorMessage,
|
|
|
|
|
streamingState,
|
2025-10-16 17:04:13 -07:00
|
|
|
popAllMessages,
|
2025-11-11 07:50:11 -08:00
|
|
|
suggestionsPosition = 'below',
|
2025-11-18 12:01:16 -05:00
|
|
|
setBannerVisible,
|
2025-04-30 08:31:32 -07:00
|
|
|
}) => {
|
2025-10-01 15:21:57 -07:00
|
|
|
const kittyProtocol = useKittyKeyboardProtocol();
|
2025-09-20 10:59:37 -07:00
|
|
|
const isShellFocused = useShellFocusState();
|
2025-11-19 15:49:39 -08:00
|
|
|
const { setEmbeddedShellFocused } = useUIActions();
|
2025-10-09 19:27:20 -07:00
|
|
|
const { mainAreaWidth } = useUIState();
|
2025-06-03 23:01:26 -07:00
|
|
|
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
|
2025-10-23 00:40:29 -04:00
|
|
|
const escPressCount = useRef(0);
|
2025-08-10 06:26:43 +08:00
|
|
|
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
|
|
|
|
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
2025-10-01 15:21:57 -07:00
|
|
|
const [recentUnsafePasteTime, setRecentUnsafePasteTime] = useState<
|
|
|
|
|
number | null
|
|
|
|
|
>(null);
|
2025-09-06 07:38:53 +05:45
|
|
|
const pasteTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
2025-11-03 13:41:58 -08:00
|
|
|
const innerBoxRef = useRef<DOMElement>(null);
|
2025-07-18 14:54:10 -07:00
|
|
|
|
2025-08-04 00:53:24 +05:00
|
|
|
const [reverseSearchActive, setReverseSearchActive] = useState(false);
|
2025-09-15 12:49:23 -05:00
|
|
|
const [commandSearchActive, setCommandSearchActive] = useState(false);
|
2025-08-04 00:53:24 +05:00
|
|
|
const [textBeforeReverseSearch, setTextBeforeReverseSearch] = useState('');
|
|
|
|
|
const [cursorPosition, setCursorPosition] = useState<[number, number]>([
|
|
|
|
|
0, 0,
|
|
|
|
|
]);
|
2025-09-15 12:49:23 -05:00
|
|
|
const [expandedSuggestionIndex, setExpandedSuggestionIndex] =
|
|
|
|
|
useState<number>(-1);
|
|
|
|
|
const shellHistory = useShellHistory(config.getProjectRoot());
|
|
|
|
|
const shellHistoryData = shellHistory.history;
|
2025-07-31 05:38:20 +09:00
|
|
|
|
2025-08-04 13:35:26 -07:00
|
|
|
const completion = useCommandCompletion(
|
2025-07-24 21:41:35 -07:00
|
|
|
buffer,
|
2025-05-20 16:50:32 -07:00
|
|
|
config.getTargetDir(),
|
|
|
|
|
slashCommands,
|
2025-07-07 16:45:44 -04:00
|
|
|
commandContext,
|
2025-08-04 00:53:24 +05:00
|
|
|
reverseSearchActive,
|
2025-10-17 23:00:27 +05:30
|
|
|
shellModeActive,
|
Ignore folders files (#651)
# Add .gitignore-Aware File Filtering to gemini-cli
This pull request introduces .gitignore-based file filtering to the gemini-cli, ensuring that git-ignored files are automatically excluded from file-related operations and suggestions throughout the CLI. The update enhances usability, reduces noise from build artifacts and dependencies, and provides new configuration options for fine-tuning file discovery.
Key Improvements
.gitignore File Filtering
All @ (at) commands, file completions, and core discovery tools now honor .gitignore patterns by default.
Git-ignored files (such as node_modules/, dist/, .env, and .git) are excluded from results unless explicitly overridden.
The behavior can be customized via a new fileFiltering section in settings.json, including options for:
Turning .gitignore respect on/off.
Adding custom ignore patterns.
Allowing or excluding build artifacts.
Configuration & Documentation Updates
settings.json schema extended with fileFiltering options.
Documentation updated to explain new filtering controls and usage patterns.
Testing
New and updated integration/unit tests for file filtering logic, configuration merging, and edge cases.
Test coverage ensures .gitignore filtering works as intended across different workflows.
Internal Refactoring
Core file discovery logic refactored for maintainability and extensibility.
Underlying tools (ls, glob, read-many-files) now support git-aware filtering out of the box.
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
2025-06-03 21:40:46 -07:00
|
|
|
config,
|
2025-05-20 16:50:32 -07:00
|
|
|
);
|
|
|
|
|
|
2025-08-04 00:53:24 +05:00
|
|
|
const reverseSearchCompletion = useReverseSearchCompletion(
|
|
|
|
|
buffer,
|
2025-09-15 12:49:23 -05:00
|
|
|
shellHistoryData,
|
2025-08-04 00:53:24 +05:00
|
|
|
reverseSearchActive,
|
|
|
|
|
);
|
2025-09-15 12:49:23 -05:00
|
|
|
|
|
|
|
|
const commandSearchCompletion = useReverseSearchCompletion(
|
|
|
|
|
buffer,
|
|
|
|
|
userMessages,
|
|
|
|
|
commandSearchActive,
|
|
|
|
|
);
|
|
|
|
|
|
2025-05-20 16:50:32 -07:00
|
|
|
const resetCompletionState = completion.resetCompletionState;
|
2025-08-04 00:53:24 +05:00
|
|
|
const resetReverseSearchCompletionState =
|
|
|
|
|
reverseSearchCompletion.resetCompletionState;
|
2025-09-15 12:49:23 -05:00
|
|
|
const resetCommandSearchCompletionState =
|
|
|
|
|
commandSearchCompletion.resetCompletionState;
|
2025-05-20 16:50:32 -07:00
|
|
|
|
2025-09-20 10:59:37 -07:00
|
|
|
const showCursor = focus && isShellFocused && !isEmbeddedShellFocused;
|
|
|
|
|
|
2025-08-10 06:26:43 +08:00
|
|
|
const resetEscapeState = useCallback(() => {
|
|
|
|
|
if (escapeTimerRef.current) {
|
|
|
|
|
clearTimeout(escapeTimerRef.current);
|
|
|
|
|
escapeTimerRef.current = null;
|
|
|
|
|
}
|
2025-10-23 00:40:29 -04:00
|
|
|
escPressCount.current = 0;
|
2025-08-10 06:26:43 +08:00
|
|
|
setShowEscapePrompt(false);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Notify parent component about escape prompt state changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (onEscapePromptChange) {
|
|
|
|
|
onEscapePromptChange(showEscapePrompt);
|
|
|
|
|
}
|
|
|
|
|
}, [showEscapePrompt, onEscapePromptChange]);
|
|
|
|
|
|
|
|
|
|
// Clear escape prompt timer on unmount
|
|
|
|
|
useEffect(
|
|
|
|
|
() => () => {
|
|
|
|
|
if (escapeTimerRef.current) {
|
|
|
|
|
clearTimeout(escapeTimerRef.current);
|
|
|
|
|
}
|
2025-09-06 07:38:53 +05:45
|
|
|
if (pasteTimeoutRef.current) {
|
|
|
|
|
clearTimeout(pasteTimeoutRef.current);
|
|
|
|
|
}
|
2025-08-10 06:26:43 +08:00
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
2025-05-20 16:50:32 -07:00
|
|
|
const handleSubmitAndClear = useCallback(
|
2025-05-13 16:23:14 -07:00
|
|
|
(submittedValue: string) => {
|
2025-06-17 22:17:16 -04:00
|
|
|
if (shellModeActive) {
|
|
|
|
|
shellHistory.addCommandToHistory(submittedValue);
|
|
|
|
|
}
|
2025-05-30 15:16:06 -07:00
|
|
|
// Clear the buffer *before* calling onSubmit to prevent potential re-submission
|
|
|
|
|
// if onSubmit triggers a re-render while the buffer still holds the old value.
|
2025-05-20 16:50:32 -07:00
|
|
|
buffer.setText('');
|
2025-05-30 15:16:06 -07:00
|
|
|
onSubmit(submittedValue);
|
2025-05-20 16:50:32 -07:00
|
|
|
resetCompletionState();
|
2025-08-04 00:53:24 +05:00
|
|
|
resetReverseSearchCompletionState();
|
2025-05-13 16:23:14 -07:00
|
|
|
},
|
2025-08-04 00:53:24 +05:00
|
|
|
[
|
|
|
|
|
onSubmit,
|
|
|
|
|
buffer,
|
|
|
|
|
resetCompletionState,
|
|
|
|
|
shellModeActive,
|
|
|
|
|
shellHistory,
|
|
|
|
|
resetReverseSearchCompletionState,
|
|
|
|
|
],
|
2025-05-20 16:50:32 -07:00
|
|
|
);
|
|
|
|
|
|
2025-10-15 22:32:50 +05:30
|
|
|
const handleSubmit = useCallback(
|
|
|
|
|
(submittedValue: string) => {
|
|
|
|
|
const trimmedMessage = submittedValue.trim();
|
|
|
|
|
const isSlash = isSlashCommand(trimmedMessage);
|
|
|
|
|
|
|
|
|
|
const isShell = shellModeActive;
|
|
|
|
|
if (
|
|
|
|
|
(isSlash || isShell) &&
|
|
|
|
|
streamingState === StreamingState.Responding
|
|
|
|
|
) {
|
|
|
|
|
setQueueErrorMessage(
|
|
|
|
|
`${isShell ? 'Shell' : 'Slash'} commands cannot be queued`,
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
handleSubmitAndClear(trimmedMessage);
|
|
|
|
|
},
|
|
|
|
|
[
|
|
|
|
|
handleSubmitAndClear,
|
|
|
|
|
shellModeActive,
|
|
|
|
|
streamingState,
|
|
|
|
|
setQueueErrorMessage,
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
2025-06-03 23:01:26 -07:00
|
|
|
const customSetTextAndResetCompletionSignal = useCallback(
|
|
|
|
|
(newText: string) => {
|
|
|
|
|
buffer.setText(newText);
|
|
|
|
|
setJustNavigatedHistory(true);
|
|
|
|
|
},
|
|
|
|
|
[buffer, setJustNavigatedHistory],
|
|
|
|
|
);
|
|
|
|
|
|
2025-05-13 16:23:14 -07:00
|
|
|
const inputHistory = useInputHistory({
|
|
|
|
|
userMessages,
|
2025-05-20 16:50:32 -07:00
|
|
|
onSubmit: handleSubmitAndClear,
|
2025-07-18 01:30:39 +03:00
|
|
|
isActive:
|
|
|
|
|
(!completion.showSuggestions || completion.suggestions.length === 1) &&
|
|
|
|
|
!shellModeActive,
|
2025-05-20 16:50:32 -07:00
|
|
|
currentQuery: buffer.text,
|
2025-06-03 23:01:26 -07:00
|
|
|
onChange: customSetTextAndResetCompletionSignal,
|
2025-05-13 16:23:14 -07:00
|
|
|
});
|
|
|
|
|
|
2025-06-03 23:01:26 -07:00
|
|
|
// Effect to reset completion if history navigation just occurred and set the text
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (justNavigatedHistory) {
|
|
|
|
|
resetCompletionState();
|
2025-08-04 00:53:24 +05:00
|
|
|
resetReverseSearchCompletionState();
|
2025-09-15 12:49:23 -05:00
|
|
|
resetCommandSearchCompletionState();
|
|
|
|
|
setExpandedSuggestionIndex(-1);
|
2025-06-03 23:01:26 -07:00
|
|
|
setJustNavigatedHistory(false);
|
|
|
|
|
}
|
|
|
|
|
}, [
|
|
|
|
|
justNavigatedHistory,
|
|
|
|
|
buffer.text,
|
|
|
|
|
resetCompletionState,
|
|
|
|
|
setJustNavigatedHistory,
|
2025-08-04 00:53:24 +05:00
|
|
|
resetReverseSearchCompletionState,
|
2025-09-15 12:49:23 -05:00
|
|
|
resetCommandSearchCompletionState,
|
2025-06-03 23:01:26 -07:00
|
|
|
]);
|
|
|
|
|
|
2025-10-16 17:04:13 -07:00
|
|
|
// Helper function to handle loading queued messages into input
|
|
|
|
|
// Returns true if we should continue with input history navigation
|
|
|
|
|
const tryLoadQueuedMessages = useCallback(() => {
|
|
|
|
|
if (buffer.text.trim() === '' && popAllMessages) {
|
2025-12-01 17:33:03 -08:00
|
|
|
const allMessages = popAllMessages();
|
|
|
|
|
if (allMessages) {
|
|
|
|
|
buffer.setText(allMessages);
|
|
|
|
|
} else {
|
|
|
|
|
// No queued messages, proceed with input history
|
|
|
|
|
inputHistory.navigateUp();
|
|
|
|
|
}
|
2025-10-16 17:04:13 -07:00
|
|
|
return true; // We handled the up arrow key
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}, [buffer, popAllMessages, inputHistory]);
|
|
|
|
|
|
2025-07-12 00:06:49 -04:00
|
|
|
// Handle clipboard image pasting with Ctrl+V
|
2025-11-17 15:48:33 -08:00
|
|
|
const handleClipboardPaste = useCallback(async () => {
|
2025-07-12 00:06:49 -04:00
|
|
|
try {
|
|
|
|
|
if (await clipboardHasImage()) {
|
|
|
|
|
const imagePath = await saveClipboardImage(config.getTargetDir());
|
|
|
|
|
if (imagePath) {
|
|
|
|
|
// Clean up old images
|
|
|
|
|
cleanupOldClipboardImages(config.getTargetDir()).catch(() => {
|
|
|
|
|
// Ignore cleanup errors
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get relative path from current directory
|
|
|
|
|
const relativePath = path.relative(config.getTargetDir(), imagePath);
|
|
|
|
|
|
|
|
|
|
// Insert @path reference at cursor position
|
|
|
|
|
const insertText = `@${relativePath}`;
|
|
|
|
|
const currentText = buffer.text;
|
2025-11-17 15:48:33 -08:00
|
|
|
const offset = buffer.getOffset();
|
2025-07-12 00:06:49 -04:00
|
|
|
|
|
|
|
|
// Add spaces around the path if needed
|
|
|
|
|
let textToInsert = insertText;
|
|
|
|
|
const charBefore = offset > 0 ? currentText[offset - 1] : '';
|
|
|
|
|
const charAfter =
|
|
|
|
|
offset < currentText.length ? currentText[offset] : '';
|
|
|
|
|
|
|
|
|
|
if (charBefore && charBefore !== ' ' && charBefore !== '\n') {
|
|
|
|
|
textToInsert = ' ' + textToInsert;
|
|
|
|
|
}
|
|
|
|
|
if (!charAfter || (charAfter !== ' ' && charAfter !== '\n')) {
|
|
|
|
|
textToInsert = textToInsert + ' ';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert at cursor position
|
|
|
|
|
buffer.replaceRangeByOffset(offset, offset, textToInsert);
|
2025-11-17 15:48:33 -08:00
|
|
|
return;
|
2025-07-12 00:06:49 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-17 15:48:33 -08:00
|
|
|
|
|
|
|
|
const textToInsert = await clipboardy.read();
|
|
|
|
|
const offset = buffer.getOffset();
|
|
|
|
|
buffer.replaceRangeByOffset(offset, offset, textToInsert);
|
2025-07-12 00:06:49 -04:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error handling clipboard image:', error);
|
|
|
|
|
}
|
|
|
|
|
}, [buffer, config]);
|
|
|
|
|
|
2025-11-19 15:49:39 -08:00
|
|
|
useMouseClick(
|
|
|
|
|
innerBoxRef,
|
|
|
|
|
(_event, relX, relY) => {
|
|
|
|
|
if (isEmbeddedShellFocused) {
|
|
|
|
|
setEmbeddedShellFocused(false);
|
|
|
|
|
}
|
|
|
|
|
const visualRow = buffer.visualScrollRow + relY;
|
|
|
|
|
buffer.moveToVisualPosition(visualRow, relX);
|
|
|
|
|
},
|
|
|
|
|
{ isActive: focus },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useMouse(
|
2025-11-03 13:41:58 -08:00
|
|
|
(event: MouseEvent) => {
|
2025-11-19 15:49:39 -08:00
|
|
|
if (event.name === 'right-release') {
|
2025-12-05 16:12:49 -08:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2025-11-17 15:48:33 -08:00
|
|
|
handleClipboardPaste();
|
2025-11-03 13:41:58 -08:00
|
|
|
}
|
|
|
|
|
},
|
2025-11-19 15:49:39 -08:00
|
|
|
{ isActive: focus },
|
2025-11-03 13:41:58 -08:00
|
|
|
);
|
|
|
|
|
|
2025-06-27 10:57:32 -07:00
|
|
|
const handleInput = useCallback(
|
|
|
|
|
(key: Key) => {
|
2025-09-20 10:59:37 -07:00
|
|
|
// TODO(jacobr): this special case is likely not needed anymore.
|
|
|
|
|
// We should probably stop supporting paste if the InputPrompt is not
|
|
|
|
|
// focused.
|
2025-07-25 20:26:13 +00:00
|
|
|
/// We want to handle paste even when not focused to support drag and drop.
|
|
|
|
|
if (!focus && !key.paste) {
|
2025-05-20 16:50:32 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-19 13:41:08 -07:00
|
|
|
if (key.paste) {
|
2025-09-06 07:38:53 +05:45
|
|
|
// Record paste time to prevent accidental auto-submission
|
2025-11-19 11:37:30 -08:00
|
|
|
if (!isTerminalPasteTrusted(kittyProtocol.enabled)) {
|
2025-10-01 15:21:57 -07:00
|
|
|
setRecentUnsafePasteTime(Date.now());
|
2025-09-06 07:38:53 +05:45
|
|
|
|
2025-10-01 15:21:57 -07:00
|
|
|
// Clear any existing paste timeout
|
|
|
|
|
if (pasteTimeoutRef.current) {
|
|
|
|
|
clearTimeout(pasteTimeoutRef.current);
|
|
|
|
|
}
|
2025-09-06 07:38:53 +05:45
|
|
|
|
2025-10-01 15:21:57 -07:00
|
|
|
// Clear the paste protection after a very short delay to prevent
|
|
|
|
|
// false positives.
|
|
|
|
|
// Due to how we use a reducer for text buffer state updates, it is
|
|
|
|
|
// reasonable to expect that key events that are really part of the
|
|
|
|
|
// same paste will be processed in the same event loop tick. 40ms
|
|
|
|
|
// is chosen arbitrarily as it is faster than a typical human
|
|
|
|
|
// could go from pressing paste to pressing enter. The fastest typists
|
|
|
|
|
// can type at 200 words per minute which roughly translates to 50ms
|
|
|
|
|
// per letter.
|
|
|
|
|
pasteTimeoutRef.current = setTimeout(() => {
|
|
|
|
|
setRecentUnsafePasteTime(null);
|
|
|
|
|
pasteTimeoutRef.current = null;
|
|
|
|
|
}, 40);
|
|
|
|
|
}
|
2025-08-19 13:41:08 -07:00
|
|
|
// Ensure we never accidentally interpret paste as regular input.
|
|
|
|
|
buffer.handleInput(key);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 15:36:42 -07:00
|
|
|
if (vimHandleInput && vimHandleInput(key)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 06:26:43 +08:00
|
|
|
// Reset ESC count and hide prompt on any non-ESC key
|
|
|
|
|
if (key.name !== 'escape') {
|
2025-10-23 00:40:29 -04:00
|
|
|
if (escPressCount.current > 0 || showEscapePrompt) {
|
2025-08-10 06:26:43 +08:00
|
|
|
resetEscapeState();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
if (
|
|
|
|
|
key.sequence === '!' &&
|
|
|
|
|
buffer.text === '' &&
|
|
|
|
|
!completion.showSuggestions
|
|
|
|
|
) {
|
2025-05-18 01:18:32 -07:00
|
|
|
setShellModeActive(!shellModeActive);
|
2025-05-20 16:50:32 -07:00
|
|
|
buffer.setText(''); // Clear the '!' from input
|
2025-07-07 16:45:44 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.ESCAPE](key)) {
|
2025-09-15 12:49:23 -05:00
|
|
|
const cancelSearch = (
|
|
|
|
|
setActive: (active: boolean) => void,
|
|
|
|
|
resetCompletion: () => void,
|
|
|
|
|
) => {
|
|
|
|
|
setActive(false);
|
|
|
|
|
resetCompletion();
|
2025-08-04 00:53:24 +05:00
|
|
|
buffer.setText(textBeforeReverseSearch);
|
|
|
|
|
const offset = logicalPosToOffset(
|
|
|
|
|
buffer.lines,
|
|
|
|
|
cursorPosition[0],
|
|
|
|
|
cursorPosition[1],
|
|
|
|
|
);
|
|
|
|
|
buffer.moveToOffset(offset);
|
2025-09-15 12:49:23 -05:00
|
|
|
setExpandedSuggestionIndex(-1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (reverseSearchActive) {
|
|
|
|
|
cancelSearch(
|
|
|
|
|
setReverseSearchActive,
|
|
|
|
|
reverseSearchCompletion.resetCompletionState,
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (commandSearchActive) {
|
|
|
|
|
cancelSearch(
|
|
|
|
|
setCommandSearchActive,
|
|
|
|
|
commandSearchCompletion.resetCompletionState,
|
|
|
|
|
);
|
2025-08-04 00:53:24 +05:00
|
|
|
return;
|
|
|
|
|
}
|
2025-09-15 12:49:23 -05:00
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
if (shellModeActive) {
|
|
|
|
|
setShellModeActive(false);
|
2025-08-10 06:26:43 +08:00
|
|
|
resetEscapeState();
|
2025-07-07 16:45:44 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (completion.showSuggestions) {
|
|
|
|
|
completion.resetCompletionState();
|
2025-09-15 12:49:23 -05:00
|
|
|
setExpandedSuggestionIndex(-1);
|
2025-08-10 06:26:43 +08:00
|
|
|
resetEscapeState();
|
2025-07-07 16:45:44 -04:00
|
|
|
return;
|
|
|
|
|
}
|
2025-08-10 06:26:43 +08:00
|
|
|
|
|
|
|
|
// Handle double ESC for clearing input
|
2025-10-23 00:40:29 -04:00
|
|
|
if (escPressCount.current === 0) {
|
2025-08-10 06:26:43 +08:00
|
|
|
if (buffer.text === '') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-23 00:40:29 -04:00
|
|
|
escPressCount.current = 1;
|
2025-08-10 06:26:43 +08:00
|
|
|
setShowEscapePrompt(true);
|
|
|
|
|
if (escapeTimerRef.current) {
|
|
|
|
|
clearTimeout(escapeTimerRef.current);
|
|
|
|
|
}
|
|
|
|
|
escapeTimerRef.current = setTimeout(() => {
|
|
|
|
|
resetEscapeState();
|
|
|
|
|
}, 500);
|
|
|
|
|
} else {
|
|
|
|
|
// clear input and immediately reset state
|
|
|
|
|
buffer.setText('');
|
|
|
|
|
resetCompletionState();
|
|
|
|
|
resetEscapeState();
|
|
|
|
|
}
|
|
|
|
|
return;
|
2025-07-07 16:45:44 -04:00
|
|
|
}
|
|
|
|
|
|
2025-08-09 16:03:17 +09:00
|
|
|
if (shellModeActive && keyMatchers[Command.REVERSE_SEARCH](key)) {
|
2025-08-04 00:53:24 +05:00
|
|
|
setReverseSearchActive(true);
|
|
|
|
|
setTextBeforeReverseSearch(buffer.text);
|
|
|
|
|
setCursorPosition(buffer.cursor);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.CLEAR_SCREEN](key)) {
|
2025-11-18 12:01:16 -05:00
|
|
|
setBannerVisible(false);
|
2025-07-07 16:45:44 -04:00
|
|
|
onClearScreen();
|
|
|
|
|
return;
|
2025-05-18 01:18:32 -07:00
|
|
|
}
|
2025-05-20 16:50:32 -07:00
|
|
|
|
2025-09-15 12:49:23 -05:00
|
|
|
if (reverseSearchActive || commandSearchActive) {
|
|
|
|
|
const isCommandSearch = commandSearchActive;
|
|
|
|
|
|
|
|
|
|
const sc = isCommandSearch
|
|
|
|
|
? commandSearchCompletion
|
|
|
|
|
: reverseSearchCompletion;
|
|
|
|
|
|
2025-08-04 00:53:24 +05:00
|
|
|
const {
|
|
|
|
|
activeSuggestionIndex,
|
|
|
|
|
navigateUp,
|
|
|
|
|
navigateDown,
|
|
|
|
|
showSuggestions,
|
|
|
|
|
suggestions,
|
2025-09-15 12:49:23 -05:00
|
|
|
} = sc;
|
|
|
|
|
const setActive = isCommandSearch
|
|
|
|
|
? setCommandSearchActive
|
|
|
|
|
: setReverseSearchActive;
|
|
|
|
|
const resetState = sc.resetCompletionState;
|
2025-08-04 00:53:24 +05:00
|
|
|
|
|
|
|
|
if (showSuggestions) {
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.NAVIGATION_UP](key)) {
|
2025-08-04 00:53:24 +05:00
|
|
|
navigateUp();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.NAVIGATION_DOWN](key)) {
|
2025-08-04 00:53:24 +05:00
|
|
|
navigateDown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-09-15 12:49:23 -05:00
|
|
|
if (keyMatchers[Command.COLLAPSE_SUGGESTION](key)) {
|
|
|
|
|
if (suggestions[activeSuggestionIndex].value.length >= MAX_WIDTH) {
|
|
|
|
|
setExpandedSuggestionIndex(-1);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (keyMatchers[Command.EXPAND_SUGGESTION](key)) {
|
|
|
|
|
if (suggestions[activeSuggestionIndex].value.length >= MAX_WIDTH) {
|
|
|
|
|
setExpandedSuggestionIndex(activeSuggestionIndex);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH](key)) {
|
2025-09-15 12:49:23 -05:00
|
|
|
sc.handleAutocomplete(activeSuggestionIndex);
|
|
|
|
|
resetState();
|
|
|
|
|
setActive(false);
|
2025-08-04 00:53:24 +05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.SUBMIT_REVERSE_SEARCH](key)) {
|
2025-08-04 00:53:24 +05:00
|
|
|
const textToSubmit =
|
|
|
|
|
showSuggestions && activeSuggestionIndex > -1
|
|
|
|
|
? suggestions[activeSuggestionIndex].value
|
|
|
|
|
: buffer.text;
|
|
|
|
|
handleSubmitAndClear(textToSubmit);
|
2025-09-15 12:49:23 -05:00
|
|
|
resetState();
|
|
|
|
|
setActive(false);
|
2025-08-04 00:53:24 +05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prevent up/down from falling through to regular history navigation
|
2025-08-09 16:03:17 +09:00
|
|
|
if (
|
|
|
|
|
keyMatchers[Command.NAVIGATION_UP](key) ||
|
|
|
|
|
keyMatchers[Command.NAVIGATION_DOWN](key)
|
|
|
|
|
) {
|
2025-08-04 00:53:24 +05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-18 00:55:29 -04:00
|
|
|
// If the command is a perfect match, pressing enter should execute it.
|
2025-12-18 14:05:36 -10:00
|
|
|
// We prioritize execution unless the user is explicitly selecting a different suggestion.
|
|
|
|
|
if (
|
|
|
|
|
completion.isPerfectMatch &&
|
|
|
|
|
keyMatchers[Command.RETURN](key) &&
|
|
|
|
|
(!completion.showSuggestions || completion.activeSuggestionIndex <= 0)
|
|
|
|
|
) {
|
2025-10-15 22:32:50 +05:30
|
|
|
handleSubmit(buffer.text);
|
2025-07-18 00:55:29 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-20 16:50:32 -07:00
|
|
|
if (completion.showSuggestions) {
|
2025-07-18 01:30:39 +03:00
|
|
|
if (completion.suggestions.length > 1) {
|
2025-08-10 07:28:28 +09:00
|
|
|
if (keyMatchers[Command.COMPLETION_UP](key)) {
|
2025-07-18 01:30:39 +03:00
|
|
|
completion.navigateUp();
|
2025-09-15 12:49:23 -05:00
|
|
|
setExpandedSuggestionIndex(-1); // Reset expansion when navigating
|
2025-07-18 01:30:39 +03:00
|
|
|
return;
|
|
|
|
|
}
|
2025-08-10 07:28:28 +09:00
|
|
|
if (keyMatchers[Command.COMPLETION_DOWN](key)) {
|
2025-07-18 01:30:39 +03:00
|
|
|
completion.navigateDown();
|
2025-09-15 12:49:23 -05:00
|
|
|
setExpandedSuggestionIndex(-1); // Reset expansion when navigating
|
2025-07-18 01:30:39 +03:00
|
|
|
return;
|
|
|
|
|
}
|
2025-05-20 16:50:32 -07:00
|
|
|
}
|
2025-07-07 16:45:44 -04:00
|
|
|
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.ACCEPT_SUGGESTION](key)) {
|
2025-05-20 16:50:32 -07:00
|
|
|
if (completion.suggestions.length > 0) {
|
2025-05-07 12:30:32 -07:00
|
|
|
const targetIndex =
|
2025-05-20 16:50:32 -07:00
|
|
|
completion.activeSuggestionIndex === -1
|
2025-07-07 16:45:44 -04:00
|
|
|
? 0 // Default to the first if none is active
|
2025-05-20 16:50:32 -07:00
|
|
|
: completion.activeSuggestionIndex;
|
2025-12-01 12:29:03 -05:00
|
|
|
|
2025-05-20 16:50:32 -07:00
|
|
|
if (targetIndex < completion.suggestions.length) {
|
2025-12-01 12:29:03 -05:00
|
|
|
const suggestion = completion.suggestions[targetIndex];
|
|
|
|
|
|
|
|
|
|
const isEnterKey = key.name === 'return' && !key.ctrl;
|
|
|
|
|
|
|
|
|
|
if (isEnterKey && buffer.text.startsWith('/')) {
|
2025-12-08 16:32:39 -05:00
|
|
|
const { isArgumentCompletion, leafCommand } =
|
|
|
|
|
completion.slashCompletionRange;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
isArgumentCompletion &&
|
|
|
|
|
isAutoExecutableCommand(leafCommand)
|
|
|
|
|
) {
|
|
|
|
|
// isArgumentCompletion guarantees leafCommand exists
|
2025-12-01 12:29:03 -05:00
|
|
|
const completedText = completion.getCompletedText(suggestion);
|
|
|
|
|
if (completedText) {
|
|
|
|
|
setExpandedSuggestionIndex(-1);
|
|
|
|
|
handleSubmit(completedText.trim());
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-08 16:32:39 -05:00
|
|
|
} else if (!isArgumentCompletion) {
|
|
|
|
|
// Existing logic for command name completion
|
|
|
|
|
const command =
|
|
|
|
|
completion.getCommandFromSuggestion(suggestion);
|
|
|
|
|
|
|
|
|
|
// Only auto-execute if the command has no completion function
|
|
|
|
|
// (i.e., it doesn't require an argument to be selected)
|
|
|
|
|
if (
|
|
|
|
|
command &&
|
|
|
|
|
isAutoExecutableCommand(command) &&
|
|
|
|
|
!command.completion
|
|
|
|
|
) {
|
|
|
|
|
const completedText =
|
|
|
|
|
completion.getCompletedText(suggestion);
|
|
|
|
|
|
|
|
|
|
if (completedText) {
|
|
|
|
|
setExpandedSuggestionIndex(-1);
|
|
|
|
|
handleSubmit(completedText.trim());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-01 12:29:03 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default behavior: auto-complete to prompt box
|
2025-07-24 21:41:35 -07:00
|
|
|
completion.handleAutocomplete(targetIndex);
|
2025-09-15 12:49:23 -05:00
|
|
|
setExpandedSuggestionIndex(-1); // Reset expansion after selection
|
2025-05-07 12:30:32 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-20 16:50:32 -07:00
|
|
|
return;
|
|
|
|
|
}
|
2025-07-18 01:30:39 +03:00
|
|
|
}
|
|
|
|
|
|
2025-08-21 16:04:04 +08:00
|
|
|
// Handle Tab key for ghost text acceptance
|
|
|
|
|
if (
|
|
|
|
|
key.name === 'tab' &&
|
|
|
|
|
!completion.showSuggestions &&
|
|
|
|
|
completion.promptCompletion.text
|
|
|
|
|
) {
|
|
|
|
|
completion.promptCompletion.accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-18 01:30:39 +03:00
|
|
|
if (!shellModeActive) {
|
2025-09-15 12:49:23 -05:00
|
|
|
if (keyMatchers[Command.REVERSE_SEARCH](key)) {
|
|
|
|
|
setCommandSearchActive(true);
|
|
|
|
|
setTextBeforeReverseSearch(buffer.text);
|
|
|
|
|
setCursorPosition(buffer.cursor);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.HISTORY_UP](key)) {
|
2025-10-16 17:04:13 -07:00
|
|
|
// Check for queued messages first when input is empty
|
|
|
|
|
// If no queued messages, inputHistory.navigateUp() is called inside tryLoadQueuedMessages
|
|
|
|
|
if (tryLoadQueuedMessages()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Only navigate history if popAllMessages doesn't exist
|
2025-07-18 01:30:39 +03:00
|
|
|
inputHistory.navigateUp();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.HISTORY_DOWN](key)) {
|
2025-07-18 01:30:39 +03:00
|
|
|
inputHistory.navigateDown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Handle arrow-up/down for history on single-line or at edges
|
|
|
|
|
if (
|
2025-08-09 16:03:17 +09:00
|
|
|
keyMatchers[Command.NAVIGATION_UP](key) &&
|
2025-07-18 01:30:39 +03:00
|
|
|
(buffer.allVisualLines.length === 1 ||
|
|
|
|
|
(buffer.visualCursor[0] === 0 && buffer.visualScrollRow === 0))
|
|
|
|
|
) {
|
2025-10-16 17:04:13 -07:00
|
|
|
// Check for queued messages first when input is empty
|
|
|
|
|
// If no queued messages, inputHistory.navigateUp() is called inside tryLoadQueuedMessages
|
|
|
|
|
if (tryLoadQueuedMessages()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Only navigate history if popAllMessages doesn't exist
|
2025-07-18 01:30:39 +03:00
|
|
|
inputHistory.navigateUp();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (
|
2025-08-09 16:03:17 +09:00
|
|
|
keyMatchers[Command.NAVIGATION_DOWN](key) &&
|
2025-07-18 01:30:39 +03:00
|
|
|
(buffer.allVisualLines.length === 1 ||
|
|
|
|
|
buffer.visualCursor[0] === buffer.allVisualLines.length - 1)
|
|
|
|
|
) {
|
|
|
|
|
inputHistory.navigateDown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-05-14 17:33:37 -07:00
|
|
|
} else {
|
2025-08-09 16:03:17 +09:00
|
|
|
// Shell History Navigation
|
|
|
|
|
if (keyMatchers[Command.NAVIGATION_UP](key)) {
|
2025-07-18 01:30:39 +03:00
|
|
|
const prevCommand = shellHistory.getPreviousCommand();
|
|
|
|
|
if (prevCommand !== null) buffer.setText(prevCommand);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.NAVIGATION_DOWN](key)) {
|
2025-07-18 01:30:39 +03:00
|
|
|
const nextCommand = shellHistory.getNextCommand();
|
|
|
|
|
if (nextCommand !== null) buffer.setText(nextCommand);
|
|
|
|
|
return;
|
2025-07-07 16:45:44 -04:00
|
|
|
}
|
2025-07-18 01:30:39 +03:00
|
|
|
}
|
2025-08-09 16:03:17 +09:00
|
|
|
|
|
|
|
|
if (keyMatchers[Command.SUBMIT](key)) {
|
2025-07-18 01:30:39 +03:00
|
|
|
if (buffer.text.trim()) {
|
2025-09-06 07:38:53 +05:45
|
|
|
// Check if a paste operation occurred recently to prevent accidental auto-submission
|
2025-10-01 15:21:57 -07:00
|
|
|
if (recentUnsafePasteTime !== null) {
|
|
|
|
|
// Paste occurred recently in a terminal where we don't trust pastes
|
|
|
|
|
// to be reported correctly so assume this paste was really a
|
|
|
|
|
// newline that was part of the paste.
|
|
|
|
|
// This has the added benefit that in the worst case at least users
|
|
|
|
|
// get some feedback that their keypress was handled rather than
|
2025-11-21 22:59:42 +08:00
|
|
|
// wondering why it was completely ignored.
|
2025-10-01 15:21:57 -07:00
|
|
|
buffer.newline();
|
2025-09-06 07:38:53 +05:45
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-18 01:30:39 +03:00
|
|
|
const [row, col] = buffer.cursor;
|
|
|
|
|
const line = buffer.lines[row];
|
|
|
|
|
const charBefore = col > 0 ? cpSlice(line, col - 1, col) : '';
|
|
|
|
|
if (charBefore === '\\') {
|
|
|
|
|
buffer.backspace();
|
|
|
|
|
buffer.newline();
|
|
|
|
|
} else {
|
2025-10-15 22:32:50 +05:30
|
|
|
handleSubmit(buffer.text);
|
2025-07-07 16:45:44 -04:00
|
|
|
}
|
2025-05-20 16:50:32 -07:00
|
|
|
}
|
2025-07-18 01:30:39 +03:00
|
|
|
return;
|
2025-05-20 16:50:32 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
// Newline insertion
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.NEWLINE](key)) {
|
2025-07-07 16:45:44 -04:00
|
|
|
buffer.newline();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ctrl+A (Home) / Ctrl+E (End)
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.HOME](key)) {
|
2025-05-20 16:50:32 -07:00
|
|
|
buffer.move('home');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.END](key)) {
|
2025-05-20 16:50:32 -07:00
|
|
|
buffer.move('end');
|
2025-07-16 00:35:58 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Ctrl+C (Clear input)
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.CLEAR_INPUT](key)) {
|
2025-07-16 00:35:58 -03:00
|
|
|
if (buffer.text.length > 0) {
|
|
|
|
|
buffer.setText('');
|
|
|
|
|
resetCompletionState();
|
|
|
|
|
}
|
2025-05-20 16:50:32 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
// Kill line commands
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.KILL_LINE_RIGHT](key)) {
|
2025-05-20 16:50:32 -07:00
|
|
|
buffer.killLineRight();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.KILL_LINE_LEFT](key)) {
|
2025-05-20 16:50:32 -07:00
|
|
|
buffer.killLineLeft();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 21:00:41 -04:00
|
|
|
if (keyMatchers[Command.DELETE_WORD_BACKWARD](key)) {
|
|
|
|
|
buffer.deleteWordLeft();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-07 16:45:44 -04:00
|
|
|
// External editor
|
2025-08-09 16:03:17 +09:00
|
|
|
if (keyMatchers[Command.OPEN_EXTERNAL_EDITOR](key)) {
|
2025-12-05 16:12:49 -08:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2025-07-07 16:45:44 -04:00
|
|
|
buffer.openInExternalEditor();
|
2025-05-20 16:50:32 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 15:48:33 -08:00
|
|
|
// Ctrl+V for clipboard paste
|
|
|
|
|
if (keyMatchers[Command.PASTE_CLIPBOARD](key)) {
|
2025-12-05 16:12:49 -08:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2025-11-17 15:48:33 -08:00
|
|
|
handleClipboardPaste();
|
2025-07-12 00:06:49 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 17:54:44 -04:00
|
|
|
// Fall back to the text buffer's default input handling for all other keys
|
2025-06-27 10:57:32 -07:00
|
|
|
buffer.handleInput(key);
|
2025-08-21 16:04:04 +08:00
|
|
|
|
|
|
|
|
// Clear ghost text when user types regular characters (not navigation/control keys)
|
|
|
|
|
if (
|
|
|
|
|
completion.promptCompletion.text &&
|
|
|
|
|
key.sequence &&
|
|
|
|
|
key.sequence.length === 1 &&
|
|
|
|
|
!key.ctrl &&
|
|
|
|
|
!key.meta
|
|
|
|
|
) {
|
|
|
|
|
completion.promptCompletion.clear();
|
2025-09-15 12:49:23 -05:00
|
|
|
setExpandedSuggestionIndex(-1);
|
2025-08-21 16:04:04 +08:00
|
|
|
}
|
2025-04-19 19:45:42 +01:00
|
|
|
},
|
2025-06-27 10:57:32 -07:00
|
|
|
[
|
|
|
|
|
focus,
|
|
|
|
|
buffer,
|
|
|
|
|
completion,
|
|
|
|
|
shellModeActive,
|
|
|
|
|
setShellModeActive,
|
|
|
|
|
onClearScreen,
|
|
|
|
|
inputHistory,
|
|
|
|
|
handleSubmitAndClear,
|
2025-10-15 22:32:50 +05:30
|
|
|
handleSubmit,
|
2025-06-27 10:57:32 -07:00
|
|
|
shellHistory,
|
2025-08-04 00:53:24 +05:00
|
|
|
reverseSearchCompletion,
|
2025-11-17 15:48:33 -08:00
|
|
|
handleClipboardPaste,
|
2025-07-16 00:35:58 -03:00
|
|
|
resetCompletionState,
|
2025-08-10 06:26:43 +08:00
|
|
|
showEscapePrompt,
|
|
|
|
|
resetEscapeState,
|
2025-07-25 15:36:42 -07:00
|
|
|
vimHandleInput,
|
2025-08-04 00:53:24 +05:00
|
|
|
reverseSearchActive,
|
|
|
|
|
textBeforeReverseSearch,
|
|
|
|
|
cursorPosition,
|
2025-10-01 15:21:57 -07:00
|
|
|
recentUnsafePasteTime,
|
2025-09-15 12:49:23 -05:00
|
|
|
commandSearchActive,
|
|
|
|
|
commandSearchCompletion,
|
2025-11-19 11:37:30 -08:00
|
|
|
kittyProtocol.enabled,
|
2025-10-16 17:04:13 -07:00
|
|
|
tryLoadQueuedMessages,
|
2025-11-18 12:01:16 -05:00
|
|
|
setBannerVisible,
|
2025-06-27 10:57:32 -07:00
|
|
|
],
|
2025-04-19 19:45:42 +01:00
|
|
|
);
|
2025-04-18 17:06:16 +01:00
|
|
|
|
2025-09-20 10:59:37 -07:00
|
|
|
useKeypress(handleInput, { isActive: !isEmbeddedShellFocused });
|
2025-06-27 10:57:32 -07:00
|
|
|
|
2025-05-20 16:50:32 -07:00
|
|
|
const linesToRender = buffer.viewportVisualLines;
|
|
|
|
|
const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =
|
|
|
|
|
buffer.visualCursor;
|
|
|
|
|
const scrollVisualRow = buffer.visualScrollRow;
|
|
|
|
|
|
2025-08-21 16:04:04 +08:00
|
|
|
const getGhostTextLines = useCallback(() => {
|
|
|
|
|
if (
|
|
|
|
|
!completion.promptCompletion.text ||
|
|
|
|
|
!buffer.text ||
|
|
|
|
|
!completion.promptCompletion.text.startsWith(buffer.text)
|
|
|
|
|
) {
|
|
|
|
|
return { inlineGhost: '', additionalLines: [] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ghostSuffix = completion.promptCompletion.text.slice(
|
|
|
|
|
buffer.text.length,
|
|
|
|
|
);
|
|
|
|
|
if (!ghostSuffix) {
|
|
|
|
|
return { inlineGhost: '', additionalLines: [] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentLogicalLine = buffer.lines[buffer.cursor[0]] || '';
|
|
|
|
|
const cursorCol = buffer.cursor[1];
|
|
|
|
|
|
|
|
|
|
const textBeforeCursor = cpSlice(currentLogicalLine, 0, cursorCol);
|
|
|
|
|
const usedWidth = stringWidth(textBeforeCursor);
|
|
|
|
|
const remainingWidth = Math.max(0, inputWidth - usedWidth);
|
|
|
|
|
|
|
|
|
|
const ghostTextLinesRaw = ghostSuffix.split('\n');
|
|
|
|
|
const firstLineRaw = ghostTextLinesRaw.shift() || '';
|
|
|
|
|
|
|
|
|
|
let inlineGhost = '';
|
|
|
|
|
let remainingFirstLine = '';
|
|
|
|
|
|
|
|
|
|
if (stringWidth(firstLineRaw) <= remainingWidth) {
|
|
|
|
|
inlineGhost = firstLineRaw;
|
|
|
|
|
} else {
|
|
|
|
|
const words = firstLineRaw.split(' ');
|
|
|
|
|
let currentLine = '';
|
|
|
|
|
let wordIdx = 0;
|
|
|
|
|
for (const word of words) {
|
|
|
|
|
const prospectiveLine = currentLine ? `${currentLine} ${word}` : word;
|
|
|
|
|
if (stringWidth(prospectiveLine) > remainingWidth) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
currentLine = prospectiveLine;
|
|
|
|
|
wordIdx++;
|
|
|
|
|
}
|
|
|
|
|
inlineGhost = currentLine;
|
|
|
|
|
if (words.length > wordIdx) {
|
|
|
|
|
remainingFirstLine = words.slice(wordIdx).join(' ');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const linesToWrap = [];
|
|
|
|
|
if (remainingFirstLine) {
|
|
|
|
|
linesToWrap.push(remainingFirstLine);
|
|
|
|
|
}
|
|
|
|
|
linesToWrap.push(...ghostTextLinesRaw);
|
|
|
|
|
const remainingGhostText = linesToWrap.join('\n');
|
|
|
|
|
|
|
|
|
|
const additionalLines: string[] = [];
|
|
|
|
|
if (remainingGhostText) {
|
|
|
|
|
const textLines = remainingGhostText.split('\n');
|
|
|
|
|
for (const textLine of textLines) {
|
|
|
|
|
const words = textLine.split(' ');
|
|
|
|
|
let currentLine = '';
|
|
|
|
|
|
|
|
|
|
for (const word of words) {
|
|
|
|
|
const prospectiveLine = currentLine ? `${currentLine} ${word}` : word;
|
|
|
|
|
const prospectiveWidth = stringWidth(prospectiveLine);
|
|
|
|
|
|
|
|
|
|
if (prospectiveWidth > inputWidth) {
|
|
|
|
|
if (currentLine) {
|
|
|
|
|
additionalLines.push(currentLine);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let wordToProcess = word;
|
|
|
|
|
while (stringWidth(wordToProcess) > inputWidth) {
|
|
|
|
|
let part = '';
|
|
|
|
|
const wordCP = toCodePoints(wordToProcess);
|
|
|
|
|
let partWidth = 0;
|
|
|
|
|
let splitIndex = 0;
|
|
|
|
|
for (let i = 0; i < wordCP.length; i++) {
|
|
|
|
|
const char = wordCP[i];
|
|
|
|
|
const charWidth = stringWidth(char);
|
|
|
|
|
if (partWidth + charWidth > inputWidth) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
part += char;
|
|
|
|
|
partWidth += charWidth;
|
|
|
|
|
splitIndex = i + 1;
|
|
|
|
|
}
|
|
|
|
|
additionalLines.push(part);
|
|
|
|
|
wordToProcess = cpSlice(wordToProcess, splitIndex);
|
|
|
|
|
}
|
|
|
|
|
currentLine = wordToProcess;
|
|
|
|
|
} else {
|
|
|
|
|
currentLine = prospectiveLine;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (currentLine) {
|
|
|
|
|
additionalLines.push(currentLine);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { inlineGhost, additionalLines };
|
|
|
|
|
}, [
|
|
|
|
|
completion.promptCompletion.text,
|
|
|
|
|
buffer.text,
|
|
|
|
|
buffer.lines,
|
|
|
|
|
buffer.cursor,
|
|
|
|
|
inputWidth,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const { inlineGhost, additionalLines } = getGhostTextLines();
|
2025-09-15 12:49:23 -05:00
|
|
|
const getActiveCompletion = () => {
|
|
|
|
|
if (commandSearchActive) return commandSearchCompletion;
|
|
|
|
|
if (reverseSearchActive) return reverseSearchCompletion;
|
|
|
|
|
return completion;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const activeCompletion = getActiveCompletion();
|
|
|
|
|
const shouldShowSuggestions = activeCompletion.showSuggestions;
|
2025-08-21 16:04:04 +08:00
|
|
|
|
2025-11-11 07:50:11 -08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (onSuggestionsVisibilityChange) {
|
|
|
|
|
onSuggestionsVisibilityChange(shouldShowSuggestions);
|
|
|
|
|
}
|
|
|
|
|
}, [shouldShowSuggestions, onSuggestionsVisibilityChange]);
|
|
|
|
|
|
2025-09-11 10:34:29 -07:00
|
|
|
const showAutoAcceptStyling =
|
|
|
|
|
!shellModeActive && approvalMode === ApprovalMode.AUTO_EDIT;
|
|
|
|
|
const showYoloStyling =
|
|
|
|
|
!shellModeActive && approvalMode === ApprovalMode.YOLO;
|
|
|
|
|
|
|
|
|
|
let statusColor: string | undefined;
|
|
|
|
|
let statusText = '';
|
|
|
|
|
if (shellModeActive) {
|
|
|
|
|
statusColor = theme.ui.symbol;
|
|
|
|
|
statusText = 'Shell mode';
|
|
|
|
|
} else if (showYoloStyling) {
|
|
|
|
|
statusColor = theme.status.error;
|
|
|
|
|
statusText = 'YOLO mode';
|
|
|
|
|
} else if (showAutoAcceptStyling) {
|
|
|
|
|
statusColor = theme.status.warning;
|
|
|
|
|
statusText = 'Accepting edits';
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-11 07:50:11 -08:00
|
|
|
const suggestionsNode = shouldShowSuggestions ? (
|
|
|
|
|
<Box paddingRight={2}>
|
|
|
|
|
<SuggestionsDisplay
|
|
|
|
|
suggestions={activeCompletion.suggestions}
|
|
|
|
|
activeIndex={activeCompletion.activeSuggestionIndex}
|
|
|
|
|
isLoading={activeCompletion.isLoadingSuggestions}
|
|
|
|
|
width={suggestionsWidth}
|
|
|
|
|
scrollOffset={activeCompletion.visibleStartIndex}
|
|
|
|
|
userInput={buffer.text}
|
|
|
|
|
mode={
|
|
|
|
|
buffer.text.startsWith('/') &&
|
|
|
|
|
!reverseSearchActive &&
|
|
|
|
|
!commandSearchActive
|
|
|
|
|
? 'slash'
|
|
|
|
|
: 'reverse'
|
|
|
|
|
}
|
|
|
|
|
expandedIndex={expandedSuggestionIndex}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
) : null;
|
|
|
|
|
|
2025-04-17 18:06:21 -04:00
|
|
|
return (
|
2025-05-20 16:50:32 -07:00
|
|
|
<>
|
2025-11-11 07:50:11 -08:00
|
|
|
{suggestionsPosition === 'above' && suggestionsNode}
|
2025-05-20 16:50:32 -07:00
|
|
|
<Box
|
|
|
|
|
borderStyle="round"
|
2025-08-07 16:11:35 -07:00
|
|
|
borderColor={
|
2025-09-20 10:59:37 -07:00
|
|
|
isShellFocused && !isEmbeddedShellFocused
|
|
|
|
|
? (statusColor ?? theme.border.focused)
|
|
|
|
|
: theme.border.default
|
2025-08-07 16:11:35 -07:00
|
|
|
}
|
2025-05-20 16:50:32 -07:00
|
|
|
paddingX={1}
|
2025-10-09 19:27:20 -07:00
|
|
|
width={mainAreaWidth}
|
|
|
|
|
flexDirection="row"
|
|
|
|
|
alignItems="flex-start"
|
|
|
|
|
minHeight={3}
|
2025-05-20 16:50:32 -07:00
|
|
|
>
|
|
|
|
|
<Text
|
2025-09-11 10:34:29 -07:00
|
|
|
color={statusColor ?? theme.text.accent}
|
|
|
|
|
aria-label={statusText || undefined}
|
2025-05-20 16:50:32 -07:00
|
|
|
>
|
2025-08-04 00:53:24 +05:00
|
|
|
{shellModeActive ? (
|
|
|
|
|
reverseSearchActive ? (
|
2025-08-21 22:29:15 +00:00
|
|
|
<Text
|
|
|
|
|
color={theme.text.link}
|
|
|
|
|
aria-label={SCREEN_READER_USER_PREFIX}
|
|
|
|
|
>
|
|
|
|
|
(r:){' '}
|
|
|
|
|
</Text>
|
2025-08-04 00:53:24 +05:00
|
|
|
) : (
|
2025-09-11 10:34:29 -07:00
|
|
|
'!'
|
2025-08-04 00:53:24 +05:00
|
|
|
)
|
2025-09-15 12:49:23 -05:00
|
|
|
) : commandSearchActive ? (
|
|
|
|
|
<Text color={theme.text.accent}>(r:) </Text>
|
2025-09-11 10:34:29 -07:00
|
|
|
) : showYoloStyling ? (
|
|
|
|
|
'*'
|
2025-08-04 00:53:24 +05:00
|
|
|
) : (
|
2025-09-11 10:34:29 -07:00
|
|
|
'>'
|
|
|
|
|
)}{' '}
|
2025-05-20 16:50:32 -07:00
|
|
|
</Text>
|
2025-11-03 13:41:58 -08:00
|
|
|
<Box flexGrow={1} flexDirection="column" ref={innerBoxRef}>
|
2025-05-20 16:50:32 -07:00
|
|
|
{buffer.text.length === 0 && placeholder ? (
|
2025-09-20 10:59:37 -07:00
|
|
|
showCursor ? (
|
2025-08-15 20:18:31 -07:00
|
|
|
<Text>
|
2025-06-06 13:44:11 -07:00
|
|
|
{chalk.inverse(placeholder.slice(0, 1))}
|
2025-08-07 16:11:35 -07:00
|
|
|
<Text color={theme.text.secondary}>{placeholder.slice(1)}</Text>
|
2025-06-06 13:44:11 -07:00
|
|
|
</Text>
|
|
|
|
|
) : (
|
2025-08-07 16:11:35 -07:00
|
|
|
<Text color={theme.text.secondary}>{placeholder}</Text>
|
2025-06-06 13:44:11 -07:00
|
|
|
)
|
2025-05-20 16:50:32 -07:00
|
|
|
) : (
|
2025-08-21 16:04:04 +08:00
|
|
|
linesToRender
|
|
|
|
|
.map((lineText, visualIdxInRenderedSet) => {
|
2025-09-17 13:17:50 -07:00
|
|
|
const absoluteVisualIdx =
|
|
|
|
|
scrollVisualRow + visualIdxInRenderedSet;
|
|
|
|
|
const mapEntry = buffer.visualToLogicalMap[absoluteVisualIdx];
|
2025-08-21 16:04:04 +08:00
|
|
|
const cursorVisualRow =
|
|
|
|
|
cursorVisualRowAbsolute - scrollVisualRow;
|
|
|
|
|
const isOnCursorLine =
|
|
|
|
|
focus && visualIdxInRenderedSet === cursorVisualRow;
|
|
|
|
|
|
2025-09-02 09:21:55 -07:00
|
|
|
const renderedLine: React.ReactNode[] = [];
|
2025-09-17 13:17:50 -07:00
|
|
|
|
|
|
|
|
const [logicalLineIdx, logicalStartCol] = mapEntry;
|
|
|
|
|
const logicalLine = buffer.lines[logicalLineIdx] || '';
|
|
|
|
|
const tokens = parseInputForHighlighting(
|
|
|
|
|
logicalLine,
|
|
|
|
|
logicalLineIdx,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const visualStart = logicalStartCol;
|
|
|
|
|
const visualEnd = logicalStartCol + cpLen(lineText);
|
|
|
|
|
const segments = buildSegmentsForVisualSlice(
|
|
|
|
|
tokens,
|
|
|
|
|
visualStart,
|
|
|
|
|
visualEnd,
|
|
|
|
|
);
|
|
|
|
|
|
2025-09-02 09:21:55 -07:00
|
|
|
let charCount = 0;
|
2025-09-17 13:17:50 -07:00
|
|
|
segments.forEach((seg, segIdx) => {
|
|
|
|
|
const segLen = cpLen(seg.text);
|
|
|
|
|
let display = seg.text;
|
2025-08-21 16:04:04 +08:00
|
|
|
|
2025-09-02 09:21:55 -07:00
|
|
|
if (isOnCursorLine) {
|
|
|
|
|
const relativeVisualColForHighlight =
|
|
|
|
|
cursorVisualColAbsolute;
|
2025-09-17 13:17:50 -07:00
|
|
|
const segStart = charCount;
|
|
|
|
|
const segEnd = segStart + segLen;
|
2025-09-02 09:21:55 -07:00
|
|
|
if (
|
2025-09-17 13:17:50 -07:00
|
|
|
relativeVisualColForHighlight >= segStart &&
|
|
|
|
|
relativeVisualColForHighlight < segEnd
|
2025-09-02 09:21:55 -07:00
|
|
|
) {
|
|
|
|
|
const charToHighlight = cpSlice(
|
2025-09-17 13:17:50 -07:00
|
|
|
seg.text,
|
|
|
|
|
relativeVisualColForHighlight - segStart,
|
|
|
|
|
relativeVisualColForHighlight - segStart + 1,
|
2025-09-02 09:21:55 -07:00
|
|
|
);
|
2025-09-20 10:59:37 -07:00
|
|
|
const highlighted = showCursor
|
|
|
|
|
? chalk.inverse(charToHighlight)
|
|
|
|
|
: charToHighlight;
|
2025-08-21 16:04:04 +08:00
|
|
|
display =
|
2025-09-02 09:21:55 -07:00
|
|
|
cpSlice(
|
2025-09-17 13:17:50 -07:00
|
|
|
seg.text,
|
2025-09-02 09:21:55 -07:00
|
|
|
0,
|
2025-09-17 13:17:50 -07:00
|
|
|
relativeVisualColForHighlight - segStart,
|
2025-09-02 09:21:55 -07:00
|
|
|
) +
|
2025-08-21 16:04:04 +08:00
|
|
|
highlighted +
|
2025-09-02 09:21:55 -07:00
|
|
|
cpSlice(
|
2025-09-17 13:17:50 -07:00
|
|
|
seg.text,
|
|
|
|
|
relativeVisualColForHighlight - segStart + 1,
|
2025-09-02 09:21:55 -07:00
|
|
|
);
|
2025-08-21 16:04:04 +08:00
|
|
|
}
|
2025-09-17 13:17:50 -07:00
|
|
|
charCount = segEnd;
|
2025-09-02 09:21:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const color =
|
2025-09-17 13:17:50 -07:00
|
|
|
seg.type === 'command' || seg.type === 'file'
|
2025-09-02 09:21:55 -07:00
|
|
|
? theme.text.accent
|
2025-09-10 10:57:07 -07:00
|
|
|
: theme.text.primary;
|
2025-09-02 09:21:55 -07:00
|
|
|
|
|
|
|
|
renderedLine.push(
|
2025-09-17 13:17:50 -07:00
|
|
|
<Text key={`token-${segIdx}`} color={color}>
|
2025-09-02 09:21:55 -07:00
|
|
|
{display}
|
|
|
|
|
</Text>,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-17 13:17:50 -07:00
|
|
|
const currentLineGhost = isOnCursorLine ? inlineGhost : '';
|
2025-09-02 09:21:55 -07:00
|
|
|
if (
|
|
|
|
|
isOnCursorLine &&
|
|
|
|
|
cursorVisualColAbsolute === cpLen(lineText)
|
|
|
|
|
) {
|
|
|
|
|
if (!currentLineGhost) {
|
|
|
|
|
renderedLine.push(
|
2025-09-05 15:29:54 -07:00
|
|
|
<Text key={`cursor-end-${cursorVisualColAbsolute}`}>
|
2025-09-20 10:59:37 -07:00
|
|
|
{showCursor ? chalk.inverse(' ') : ' '}
|
2025-09-05 15:29:54 -07:00
|
|
|
</Text>,
|
2025-09-02 09:21:55 -07:00
|
|
|
);
|
2025-05-20 16:50:32 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-21 16:04:04 +08:00
|
|
|
|
|
|
|
|
const showCursorBeforeGhost =
|
|
|
|
|
focus &&
|
2025-09-02 09:21:55 -07:00
|
|
|
isOnCursorLine &&
|
|
|
|
|
cursorVisualColAbsolute === cpLen(lineText) &&
|
2025-08-21 16:04:04 +08:00
|
|
|
currentLineGhost;
|
|
|
|
|
|
|
|
|
|
return (
|
2025-09-05 15:29:54 -07:00
|
|
|
<Box key={`line-${visualIdxInRenderedSet}`} height={1}>
|
|
|
|
|
<Text>
|
|
|
|
|
{renderedLine}
|
2025-09-20 10:59:37 -07:00
|
|
|
{showCursorBeforeGhost &&
|
|
|
|
|
(showCursor ? chalk.inverse(' ') : ' ')}
|
2025-09-05 15:29:54 -07:00
|
|
|
{currentLineGhost && (
|
|
|
|
|
<Text color={theme.text.secondary}>
|
|
|
|
|
{currentLineGhost}
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
</Text>
|
|
|
|
|
</Box>
|
2025-08-21 16:04:04 +08:00
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
.concat(
|
|
|
|
|
additionalLines.map((ghostLine, index) => {
|
|
|
|
|
const padding = Math.max(
|
|
|
|
|
0,
|
|
|
|
|
inputWidth - stringWidth(ghostLine),
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<Text
|
|
|
|
|
key={`ghost-line-${index}`}
|
|
|
|
|
color={theme.text.secondary}
|
|
|
|
|
>
|
|
|
|
|
{ghostLine}
|
|
|
|
|
{' '.repeat(padding)}
|
|
|
|
|
</Text>
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
)
|
2025-05-20 16:50:32 -07:00
|
|
|
)}
|
|
|
|
|
</Box>
|
2025-04-19 12:38:09 -04:00
|
|
|
</Box>
|
2025-11-11 07:50:11 -08:00
|
|
|
{suggestionsPosition === 'below' && suggestionsNode}
|
2025-05-20 16:50:32 -07:00
|
|
|
</>
|
2025-04-17 18:06:21 -04:00
|
|
|
);
|
2025-04-18 18:08:43 -04:00
|
|
|
};
|