From 69f3f88ae5d8a9c0b0f172775229cf2d94d0f7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nunzio=20Marf=C3=A8?= Date: Sun, 4 Jan 2026 16:29:11 +0100 Subject: [PATCH] Handle enter button (#481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus --- .github/workflows/docker.yml | 2 +- README.md | 2 +- src/hooks/use-confirmation.ts | 123 +++++++++++++----- .../apps/features/terminal/Terminal.tsx | 26 ++-- 4 files changed, 104 insertions(+), 49 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cc32f3d1..34f78ea8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -17,7 +17,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@v5 diff --git a/README.md b/README.md index 9ddf04d6..d01a3aab 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Supported Devices: - MSI Installer - Chocolatey Package Manager - Linux (x64/ia32) - - Portable + - Portable [(AUR available)](https://aur.archlinux.org/packages/termix-bin) - AppImage - Deb - Flatpak diff --git a/src/hooks/use-confirmation.ts b/src/hooks/use-confirmation.ts index eca415b4..2d1c1800 100644 --- a/src/hooks/use-confirmation.ts +++ b/src/hooks/use-confirmation.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect, useCallback } from "react"; import { toast } from "sonner"; interface ConfirmationOptions { @@ -9,10 +9,47 @@ interface ConfirmationOptions { variant?: "default" | "destructive"; } +interface ToastConfirmOptions { + confirmOnEnter?: boolean; + duration?: number; +} + export function useConfirmation() { const [isOpen, setIsOpen] = useState(false); const [options, setOptions] = useState(null); const [onConfirm, setOnConfirm] = useState<(() => void) | null>(null); + const [activeToastId, setActiveToastId] = useState(null); + const [pendingConfirmCallback, setPendingConfirmCallback] = useState<(() => void) | null>(null); + const [pendingResolve, setPendingResolve] = useState<((value: boolean) => void) | null>(null); + + const handleEnterKey = useCallback((event: KeyboardEvent) => { + if (event.key === "Enter" && activeToastId !== null) { + event.preventDefault(); + event.stopPropagation(); + + if (pendingConfirmCallback) { + pendingConfirmCallback(); + } + if (pendingResolve) { + pendingResolve(true); + } + + toast.dismiss(activeToastId); + setActiveToastId(null); + setPendingConfirmCallback(null); + setPendingResolve(null); + } + }, [activeToastId, pendingConfirmCallback, pendingResolve]); + + useEffect(() => { + if (activeToastId !== null) { + // Use capture phase to intercept Enter before terminal receives it + window.addEventListener("keydown", handleEnterKey, true); + return () => { + window.removeEventListener("keydown", handleEnterKey, true); + }; + } + }, [activeToastId, handleEnterKey]); const confirm = (opts: ConfirmationOptions, callback: () => void) => { setOptions(opts); @@ -40,6 +77,7 @@ export function useConfirmation() { callback?: () => void, variantOrConfirmLabel: "default" | "destructive" | string = "Confirm", cancelLabel: string = "Cancel", + toastOptions: ToastConfirmOptions = { confirmOnEnter: false }, ): Promise => { return new Promise((resolve) => { const isVariant = @@ -47,43 +85,56 @@ export function useConfirmation() { variantOrConfirmLabel === "destructive"; const confirmLabel = isVariant ? "Confirm" : variantOrConfirmLabel; - if (typeof opts === "string") { - toast(opts, { - action: { - label: confirmLabel, - onClick: () => { - if (callback) callback(); - resolve(true); - }, - }, - cancel: { - label: cancelLabel, - onClick: () => { - resolve(false); - }, - }, - } as any); - } else if (typeof opts === "object") { - const actualConfirmLabel = opts.confirmText || confirmLabel; - const actualCancelLabel = opts.cancelText || cancelLabel; + const { confirmOnEnter = false, duration = 8000 } = toastOptions; - toast(opts.description, { - action: { - label: actualConfirmLabel, - onClick: () => { - if (callback) callback(); - resolve(true); - }, - }, - cancel: { - label: actualCancelLabel, - onClick: () => { - resolve(false); - }, - }, - } as any); - } else { + const handleToastConfirm = () => { + if (callback) callback(); + resolve(true); + setActiveToastId(null); + setPendingConfirmCallback(null); + setPendingResolve(null); + }; + + const handleToastCancel = () => { resolve(false); + setActiveToastId(null); + setPendingConfirmCallback(null); + setPendingResolve(null); + }; + + const message = typeof opts === "string" ? opts : opts.description; + const actualConfirmLabel = typeof opts === "object" && opts.confirmText ? opts.confirmText : confirmLabel; + const actualCancelLabel = typeof opts === "object" && opts.cancelText ? opts.cancelText : cancelLabel; + + const toastId = toast(message, { + duration, + action: { + label: confirmOnEnter ? `${actualConfirmLabel} ↵` : actualConfirmLabel, + onClick: handleToastConfirm, + }, + cancel: { + label: actualCancelLabel, + onClick: handleToastCancel, + }, + onDismiss: () => { + setActiveToastId(null); + setPendingConfirmCallback(null); + setPendingResolve(null); + }, + onAutoClose: () => { + resolve(false); + setActiveToastId(null); + setPendingConfirmCallback(null); + setPendingResolve(null); + }, + } as any); + + if (confirmOnEnter) { + setActiveToastId(toastId); + setPendingConfirmCallback(() => () => { + if (callback) callback(); + }); + setPendingResolve(() => resolve); } }); }; diff --git a/src/ui/desktop/apps/features/terminal/Terminal.tsx b/src/ui/desktop/apps/features/terminal/Terminal.tsx index 5fa0a1c8..8e4a6c62 100644 --- a/src/ui/desktop/apps/features/terminal/Terminal.tsx +++ b/src/ui/desktop/apps/features/terminal/Terminal.tsx @@ -618,15 +618,15 @@ export const Terminal = forwardRef( ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002` : isElectron() ? (() => { - const baseUrl = - (window as { configuredServerUrl?: string }) - .configuredServerUrl || "http://127.0.0.1:30001"; - const wsProtocol = baseUrl.startsWith("https://") - ? "wss://" - : "ws://"; - const wsHost = baseUrl.replace(/^https?:\/\//, ""); - return `${wsProtocol}${wsHost}/ssh/websocket/`; - })() + const baseUrl = + (window as { configuredServerUrl?: string }) + .configuredServerUrl || "http://127.0.0.1:30001"; + const wsProtocol = baseUrl.startsWith("https://") + ? "wss://" + : "ws://"; + const wsHost = baseUrl.replace(/^https?:\/\//, ""); + return `${wsProtocol}${wsHost}/ssh/websocket/`; + })() : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`; if ( @@ -715,8 +715,11 @@ export const Terminal = forwardRef( : msg.data; terminal.write(outputData); + // Universal regex pattern for sudo password prompt + // Matches "[sudo]" prefix + any text + ":" suffix, works for all languages + // Also matches "sudo: a password is required" for some systems const sudoPasswordPattern = - /(?:\[sudo\] password for \S+:|sudo: a password is required)/; + /(?:\[sudo\][^\n]*:\s*$|sudo:[^\n]*password[^\n]*required)/i; const passwordToFill = hostConfig.terminalConfig?.sudoPassword || hostConfig.password; if ( @@ -746,6 +749,7 @@ export const Terminal = forwardRef( }, t("common.confirm"), t("common.cancel"), + { confirmOnEnter: true }, ); setTimeout(() => { sudoPromptShownRef.current = false; @@ -1383,7 +1387,7 @@ export const Terminal = forwardRef( const selectedCommand = autocompleteSuggestionsRef.current[ - autocompleteSelectedIndexRef.current + autocompleteSelectedIndexRef.current ]; const currentCmd = currentAutocompleteCommand.current; const completion = selectedCommand.substring(currentCmd.length);