diff --git a/README.md b/README.md index ad0486fc..19d4dc18 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,10 @@ networks: Termix Screenshot 11 Termix Screenshot 12 + +Termix Screenshot 13 +Termix Screenshot 14 + Some videos and images may be out of date or may not perfectly showcase features. diff --git a/repo-images/Image 1.png b/repo-images/Image 1.png index 7c34b997..0a31a317 100644 Binary files a/repo-images/Image 1.png and b/repo-images/Image 1.png differ diff --git a/repo-images/Image 10.png b/repo-images/Image 10.png index 9746422b..d3c8fc78 100644 Binary files a/repo-images/Image 10.png and b/repo-images/Image 10.png differ diff --git a/repo-images/Image 11.png b/repo-images/Image 11.png index e3a7e749..6c2894fa 100644 Binary files a/repo-images/Image 11.png and b/repo-images/Image 11.png differ diff --git a/repo-images/Image 12.png b/repo-images/Image 12.png index aff7cede..3ca62680 100644 Binary files a/repo-images/Image 12.png and b/repo-images/Image 12.png differ diff --git a/repo-images/Image 13.png b/repo-images/Image 13.png index bf4644d0..b611fc59 100644 Binary files a/repo-images/Image 13.png and b/repo-images/Image 13.png differ diff --git a/repo-images/Image 14.png b/repo-images/Image 14.png new file mode 100644 index 00000000..a4779bc5 Binary files /dev/null and b/repo-images/Image 14.png differ diff --git a/repo-images/Image 2.png b/repo-images/Image 2.png index fdb40ef7..56dd4bc2 100644 Binary files a/repo-images/Image 2.png and b/repo-images/Image 2.png differ diff --git a/repo-images/Image 3.png b/repo-images/Image 3.png index 6e4b5123..d8bab622 100644 Binary files a/repo-images/Image 3.png and b/repo-images/Image 3.png differ diff --git a/repo-images/Image 4.png b/repo-images/Image 4.png index b0f4befa..bb081907 100644 Binary files a/repo-images/Image 4.png and b/repo-images/Image 4.png differ diff --git a/repo-images/Image 5.png b/repo-images/Image 5.png index 74aebc44..16470066 100644 Binary files a/repo-images/Image 5.png and b/repo-images/Image 5.png differ diff --git a/repo-images/Image 6.png b/repo-images/Image 6.png index cfe1f33f..c4bdc37a 100644 Binary files a/repo-images/Image 6.png and b/repo-images/Image 6.png differ diff --git a/repo-images/Image 7.png b/repo-images/Image 7.png index b5e95dc6..bd6c08e9 100644 Binary files a/repo-images/Image 7.png and b/repo-images/Image 7.png differ diff --git a/repo-images/Image 8.png b/repo-images/Image 8.png index 2a6086cf..d4d1ec38 100644 Binary files a/repo-images/Image 8.png and b/repo-images/Image 8.png differ diff --git a/repo-images/Image 9.png b/repo-images/Image 9.png index e94fde27..926710ad 100644 Binary files a/repo-images/Image 9.png and b/repo-images/Image 9.png differ diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index bbeddb69..cfac02b7 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -4,7 +4,14 @@ import { Separator } from "@/components/separator"; import { Button } from "@/components/button"; import { Sheet, SheetContent } from "@/components/sheet"; import { ChevronLeft, ChevronRight, Maximize2 } from "lucide-react"; -import { useState, useRef, useCallback, useEffect, createRef } from "react"; +import { + useState, + useRef, + useCallback, + useEffect, + createRef, + createPortal, +} from "react"; import { useIsMobile } from "@/hooks/use-mobile"; import { MobileBottomBar } from "@/shell/MobileBottomBar"; import { CommandPalette } from "@/shell/CommandPalette"; @@ -166,6 +173,21 @@ export function AppShell({ const terminalRefs = useRef>>( new Map(), ); + const [paneContentEls, setPaneContentEls] = useState< + (HTMLDivElement | null)[] + >(Array(6).fill(null)); + + const onPaneContentRef = useCallback( + (paneIndex: number, el: HTMLDivElement | null) => { + setPaneContentEls((prev) => { + if (prev[paneIndex] === el) return prev; + const next = [...prev]; + next[paneIndex] = el; + return next; + }); + }, + [], + ); const sidebarTitle: Record = { hosts: "Hosts", @@ -721,6 +743,7 @@ export function AppShell({ onOpenSingletonTab={openSingletonTab} onOpenTab={openTab} onTerminalResize={resizeAllTerminals} + onPaneContentRef={onPaneContentRef} /> )} @@ -730,11 +753,34 @@ export function AppShell({ className="absolute inset-0 flex flex-col" style={{ display: isSplit && !isMobile ? "none" : "flex" }} > - {/* Terminal tabs: always in DOM, visibility-toggled so xterm keeps its dimensions */} + {/* Terminal tabs: always in DOM, visibility-toggled so xterm keeps its dimensions. + When split is active and this tab is assigned to a pane, portal it there. */} {tabs .filter((tab) => tab.type === "terminal") .map((tab) => { - const visible = tab.id === activeTabId; + const paneIdx = isSplit ? paneTabIds.indexOf(tab.id) : -1; + const inPane = paneIdx !== -1; + const paneEl = inPane ? paneContentEls[paneIdx] : null; + const visible = inPane || tab.id === activeTabId; + + const terminalContent = renderTabContent( + tab, + openSingletonTab, + openTab, + closeTab, + visible, + ); + + if (inPane && paneEl) { + return createPortal( +
+ {terminalContent} +
, + paneEl, + tab.id, + ); + } + return (
- {renderTabContent( - tab, - openSingletonTab, - openTab, - closeTab, - visible, - )} + {terminalContent}
); })} diff --git a/src/ui/shell/SplitView.tsx b/src/ui/shell/SplitView.tsx index 7493ef94..8f8abfe5 100644 --- a/src/ui/shell/SplitView.tsx +++ b/src/ui/shell/SplitView.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect, memo } from "react"; +import React, { useState, useRef, useEffect, memo, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { splitDragState, notifyDragEnd } from "@/lib/splitDragging"; import { renderTabContent, tabIcon } from "@/shell/tabUtils"; @@ -323,24 +323,39 @@ const Pane = memo(function Pane({ isDragging, onOpenSingletonTab, onOpenTab, + onPaneContentRef, }: { tab: Tab | null; paneIndex: number; isDragging: boolean; onOpenSingletonTab: (type: TabType) => void; onOpenTab: (host: Host, type: TabType) => void; + onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void; }) { + const isTerminal = tab?.type === "terminal"; + const contentRef = useCallback( + (el: HTMLDivElement | null) => { + onPaneContentRef?.(paneIndex, el); + }, + [paneIndex, onPaneContentRef], + ); + return (
-
+
{tab ? ( - + isTerminal ? ( + // Terminal tabs are portaled in from AppShell to avoid remounting +
+ ) : ( + + ) ) : ( )} @@ -366,6 +381,7 @@ const Row = memo(function Row({ onOpenTab, onColDivider, onColDividerTouch, + onPaneContentRef, }: { rowIdx: number; paneIndices: number[]; @@ -382,6 +398,7 @@ const Row = memo(function Row({ rowIdx: number, colIdx: number, ) => void; + onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void; }) { const widths = colWidths ?? []; return ( @@ -402,6 +419,7 @@ const Row = memo(function Row({ isDragging={isDragging} onOpenSingletonTab={onOpenSingletonTab} onOpenTab={onOpenTab} + onPaneContentRef={onPaneContentRef} />
{ci < paneIndices.length - 1 && ( @@ -426,6 +444,7 @@ export const SplitView = memo(function SplitView({ onOpenSingletonTab, onOpenTab, onTerminalResize, + onPaneContentRef, }: { tabs: Tab[]; paneTabIds: (string | null)[]; @@ -433,6 +452,7 @@ export const SplitView = memo(function SplitView({ onOpenSingletonTab: (type: TabType) => void; onOpenTab: (host: Host, type: TabType) => void; onTerminalResize?: () => void; + onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void; }) { const { rowSizes, @@ -485,6 +505,7 @@ export const SplitView = memo(function SplitView({ onOpenTab={onOpenTab} onColDivider={onColDivider} onColDividerTouch={onColDividerTouch} + onPaneContentRef={onPaneContentRef} /> )} @@ -500,6 +521,7 @@ export const SplitView = memo(function SplitView({ isDragging={isDragging} onOpenSingletonTab={onOpenSingletonTab} onOpenTab={onOpenTab} + onPaneContentRef={onPaneContentRef} />
@@ -555,6 +579,7 @@ export const SplitView = memo(function SplitView({ isDragging={isDragging} onOpenSingletonTab={onOpenSingletonTab} onOpenTab={onOpenTab} + onPaneContentRef={onPaneContentRef} /> @@ -582,6 +608,7 @@ export const SplitView = memo(function SplitView({ isDragging={isDragging} onOpenSingletonTab={onOpenSingletonTab} onOpenTab={onOpenTab} + onPaneContentRef={onPaneContentRef} /> @@ -601,6 +628,7 @@ export const SplitView = memo(function SplitView({ onOpenTab={onOpenTab} onColDivider={onColDivider} onColDividerTouch={onColDividerTouch} + onPaneContentRef={onPaneContentRef} /> onRowDivider(e, 0)} @@ -618,6 +646,7 @@ export const SplitView = memo(function SplitView({ onOpenTab={onOpenTab} onColDivider={onColDivider} onColDividerTouch={onColDividerTouch} + onPaneContentRef={onPaneContentRef} /> )} @@ -636,6 +665,7 @@ export const SplitView = memo(function SplitView({ onOpenTab={onOpenTab} onColDivider={onColDivider} onColDividerTouch={onColDividerTouch} + onPaneContentRef={onPaneContentRef} /> onRowDivider(e, 0)} @@ -653,6 +683,7 @@ export const SplitView = memo(function SplitView({ onOpenTab={onOpenTab} onColDivider={onColDivider} onColDividerTouch={onColDividerTouch} + onPaneContentRef={onPaneContentRef} /> )} @@ -671,6 +702,7 @@ export const SplitView = memo(function SplitView({ onOpenTab={onOpenTab} onColDivider={onColDivider} onColDividerTouch={onColDividerTouch} + onPaneContentRef={onPaneContentRef} /> onRowDivider(e, 0)} @@ -688,6 +720,7 @@ export const SplitView = memo(function SplitView({ onOpenTab={onOpenTab} onColDivider={onColDivider} onColDividerTouch={onColDividerTouch} + onPaneContentRef={onPaneContentRef} /> )}