diff --git a/src/renderer/src/components/TabBar.tsx b/src/renderer/src/components/TabBar.tsx index bcddbee9a..c57d4e72d 100644 --- a/src/renderer/src/components/TabBar.tsx +++ b/src/renderer/src/components/TabBar.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { DndContext, closestCenter, @@ -16,12 +16,12 @@ import { import { CSS } from '@dnd-kit/utilities' import { X, Plus, Terminal as TerminalIcon } from 'lucide-react' import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuTrigger -} from '@/components/ui/context-menu' + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' import type { TerminalTab } from '../../../shared/types' interface SortableTabProps { @@ -50,6 +50,8 @@ const TAB_COLORS = [ { label: 'Gray', value: '#9ca3af' } ] +const CLOSE_ALL_CONTEXT_MENUS_EVENT = 'orca-close-all-context-menus' + function SortableTab({ tab, tabCount, @@ -72,10 +74,25 @@ function SortableTab({ zIndex: isDragging ? 10 : undefined, opacity: isDragging ? 0.8 : 1 } + const [menuOpen, setMenuOpen] = useState(false) + const [menuPoint, setMenuPoint] = useState({ x: 0, y: 0 }) + + useEffect(() => { + const closeMenu = (): void => setMenuOpen(false) + window.addEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu) + return () => window.removeEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu) + }, []) return ( - - + <> +
{ + event.preventDefault() + window.dispatchEvent(new Event(CLOSE_ALL_CONTEXT_MENUS_EVENT)) + setMenuPoint({ x: event.clientX, y: event.clientY }) + setMenuOpen(true) + }} + >
{ + if (e.button !== 0) return onActivate(tab.id) listeners?.onPointerDown?.(e) }} @@ -121,55 +139,69 @@ function SortableTab({
- +
- - onClose(tab.id)}>Close - onCloseOthers(tab.id)} disabled={tabCount <= 1}> - Close Others - - onCloseToRight(tab.id)} disabled={!hasTabsToRight}> - Close Tabs To The Right - - - { - const next = window.prompt('Change tab title', tab.customTitle ?? tab.title) - if (next === null) return - const trimmed = next.trim() - onSetCustomTitle(tab.id, trimmed.length > 0 ? trimmed : null) - }} - > - Change Title - -
-
Tab Color
-
- {TAB_COLORS.map((color) => { - const isSelected = tab.color === color.value - return ( - { - onSetTabColor(tab.id, color.value) - }} - > - {color.value === null && ( - - )} - - ) - })} + + +
- - + + + ) } diff --git a/src/renderer/src/components/TerminalPane.tsx b/src/renderer/src/components/TerminalPane.tsx index 92e714670..6c4e0a85c 100644 --- a/src/renderer/src/components/TerminalPane.tsx +++ b/src/renderer/src/components/TerminalPane.tsx @@ -1,5 +1,14 @@ -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' import { Restty, getBuiltinTheme } from 'restty' +import { Clipboard, Copy, Eraser, PanelBottomOpen, PanelRightOpen, X } from 'lucide-react' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' import { useAppStore } from '../store' type PtyTransport = { @@ -27,6 +36,8 @@ type PtyTransport = { destroy?: () => void | Promise } +const CLOSE_ALL_CONTEXT_MENUS_EVENT = 'orca-close-all-context-menus' + const OSC_TITLE_RE = /\x1b\]([012]);([^\x07\x1b]*?)(?:\x07|\x1b\\)/g function extractLastOscTitle(data: string): string | null { @@ -149,7 +160,16 @@ export default function TerminalPane({ }: TerminalPaneProps): React.JSX.Element { const containerRef = useRef(null) const resttyRef = useRef(null) + const contextPaneIdRef = useRef(null) const wasActiveRef = useRef(false) + const [terminalMenuOpen, setTerminalMenuOpen] = useState(false) + const [terminalMenuPoint, setTerminalMenuPoint] = useState({ x: 0, y: 0 }) + + useEffect(() => { + const closeMenu = (): void => setTerminalMenuOpen(false) + window.addEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu) + return () => window.removeEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu) + }, []) const updateTabTitle = useAppStore((s) => s.updateTabTitle) const updateTabPtyId = useAppStore((s) => s.updateTabPtyId) @@ -194,6 +214,7 @@ export default function TerminalPane({ createInitialPane: false, autoInit: false, shortcuts: { enabled: true }, + defaultContextMenu: false, appOptions: ({ id }) => { const onExit = (): void => { // Schedule close via parent @@ -341,11 +362,134 @@ export default function TerminalPane({ return () => window.removeEventListener('keydown', onKeyDown, { capture: true }) }, [isActive]) + const resolveMenuPane = () => { + const restty = resttyRef.current + if (!restty) return null + const panes = restty.getPanes() + + if (contextPaneIdRef.current !== null) { + const clickedPane = panes.find((p) => p.id === contextPaneIdRef.current) ?? null + if (clickedPane) return clickedPane + } + return restty.getActivePane() ?? panes[0] ?? null + } + + const handleCopy = async (): Promise => { + const pane = resolveMenuPane() + if (!pane) return + await pane.app.copySelectionToClipboard() + } + + const handlePaste = async (): Promise => { + const pane = resolveMenuPane() + if (!pane) return + await pane.app.pasteFromClipboard() + } + + const handleSplitRight = (): void => { + const pane = resolveMenuPane() + if (!pane) return + resttyRef.current?.splitPane(pane.id, 'vertical') + } + + const handleSplitDown = (): void => { + const pane = resolveMenuPane() + if (!pane) return + resttyRef.current?.splitPane(pane.id, 'horizontal') + } + + const handleClosePane = (): void => { + const pane = resolveMenuPane() + if (!pane) return + const panes = resttyRef.current?.getPanes() ?? [] + if (panes.length <= 1) return + resttyRef.current?.closePane(pane.id) + } + + const handleClearScreen = (): void => { + const pane = resolveMenuPane() + if (!pane) return + pane.app.clearScreen() + } + + const canClosePane = (resttyRef.current?.getPanes().length ?? 1) > 1 + return ( -
+ <> +
{ + event.preventDefault() + window.dispatchEvent(new Event(CLOSE_ALL_CONTEXT_MENUS_EVENT)) + + const restty = resttyRef.current + if (!restty) { + contextPaneIdRef.current = null + return + } + + const target = event.target + if (!(target instanceof Node)) { + contextPaneIdRef.current = null + return + } + const clickedPane = + restty.getPanes().find((pane) => pane.container.contains(target)) ?? null + contextPaneIdRef.current = clickedPane?.id ?? null + + const bounds = event.currentTarget.getBoundingClientRect() + setTerminalMenuPoint({ x: event.clientX - bounds.left, y: event.clientY - bounds.top }) + setTerminalMenuOpen(true) + }} + /> + + +