diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js index 2a5bcd554..fda71c4aa 100644 --- a/src/extensionsIntegrated/Terminal/main.js +++ b/src/extensionsIntegrated/Terminal/main.js @@ -42,6 +42,7 @@ define(function (require, exports, module) { const Menus = require("command/Menus"); const Commands = require("command/Commands"); + const KeyBindingManager = require("command/KeyBindingManager"); const NotificationUI = require("widgets/NotificationUI"); const TerminalInstance = require("./TerminalInstance"); const ShellProfiles = require("./ShellProfiles"); @@ -90,6 +91,7 @@ define(function (require, exports, module) { let processInfo = {}; // id -> processName from PTY let originalDefaultShellName = null; // System-detected default shell name let _focusToastShown = false; // Show focus hint toast only once per session + let _clearHintShown = false; // Show clear buffer hint toast only once per session let $panel, $contentArea, $shellDropdown, $flyoutList; /** @@ -145,8 +147,41 @@ define(function (require, exports, module) { // Dropdown chevron button toggles shell selector $panel.find(".terminal-flyout-dropdown-btn").on("click", _onDropdownButtonClick); - // Refresh process info when user hovers over the flyout - $panel.find(".terminal-tab-flyout").on("mouseenter", _refreshAllProcesses); + // When the terminal is focused, prevent Phoenix keybindings from + // stealing keys that should go to the shell (e.g. Ctrl+L for clear). + // The EDITOR_SHORTCUTS list in TerminalInstance.js already defines which + // Ctrl combos should pass through to Phoenix; everything else should + // reach xterm/the PTY. + KeyBindingManager.addGlobalKeydownHook(function (event) { + if (event.type !== "keydown") { + return false; + } + // Only intercept when a terminal textarea is focused + const el = document.activeElement; + if (!el || !$contentArea[0].contains(el)) { + return false; + } + // Let the terminal handle Ctrl/Cmd key combos that aren't + // reserved for the editor (those are handled by TerminalInstance's + // _customKeyHandler which returns false for them). + const ctrlOrMeta = event.ctrlKey || event.metaKey; + const key = event.key.toLowerCase(); + if (ctrlOrMeta && !event.shiftKey && key === "l") { + _showClearBufferHintToast(); + return true; // Block Phoenix, let xterm handle Ctrl+L + } + // Ctrl+K (Cmd+K on mac): clear terminal scrollback + if (ctrlOrMeta && !event.shiftKey && key === "k") { + event.preventDefault(); + _clearActiveTerminal(); + return true; + } + return false; + }); + + // Refresh process info when the tab bar gains focus or mouse enters + $panel.find(".terminal-tab-bar").on("mouseenter", _refreshAllProcesses); + $panel.find(".terminal-tab-bar").on("focusin", _refreshAllProcesses); // Listen for panel resize WorkspaceManager.on("workspaceUpdateLayout", _handleResize); @@ -155,6 +190,7 @@ define(function (require, exports, module) { const PanelView = require("view/PanelView"); PanelView.on(PanelView.EVENT_PANEL_SHOWN, function (_event, panelId) { if (panelId === PANEL_ID) { + _updateTabBarMode(); const active = _getActiveTerminal(); if (active) { active.handleResize(); @@ -333,6 +369,7 @@ define(function (require, exports, module) { } _updateFlyout(); + _refreshAllProcesses(); } /** @@ -586,14 +623,23 @@ define(function (require, exports, module) { } } + /** + * Update the expanded/collapsed tab bar class based on panel width + */ + function _updateTabBarMode() { + $panel.toggleClass("terminal-tabs-expanded", $panel.width() >= 840); + } + /** * Handle workspace resize */ function _handleResize() { + _updateTabBarMode(); const active = _getActiveTerminal(); if (active) { active.handleResize(); } + _refreshAllProcesses(); } /** @@ -622,6 +668,24 @@ define(function (require, exports, module) { }); } + /** + * Show a one-time toast hint about Ctrl/Cmd+K to clear terminal buffer + */ + function _showClearBufferHintToast() { + if (_clearHintShown) { + return; + } + _clearHintShown = true; + + const isMac = brackets.platform === "mac"; + const shortcutKey = isMac ? 'Cmd+K' : 'Ctrl+K'; + const message = StringUtils.format(Strings.TERMINAL_CLEAR_BUFFER_HINT, shortcutKey); + NotificationUI.showToastOn($contentArea[0], message, { + autoCloseTimeS: 5, + dismissOnClick: true + }); + } + /** * Escape HTML special characters */ @@ -814,6 +878,7 @@ define(function (require, exports, module) { if (Phoenix.isTestWindow) { exports._getActiveTerminal = _getActiveTerminal; + exports._refreshAllProcesses = _refreshAllProcesses; /** * Write data to the active terminal's PTY. Test-only helper. diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index aaa44a8fb..0b8ac5a1b 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1784,6 +1784,7 @@ define({ "TERMINAL_CLOSE_ALL_STOP_BTN": "Close All & Stop Processes", "TERMINAL_FOCUS_HINT": "Press {0} to switch between editor and terminal", "TERMINAL_CLEAR": "Clear Terminal", + "TERMINAL_CLEAR_BUFFER_HINT": "💡 Press {0} to clear terminal buffer", "EXTENDED_COMMIT_MESSAGE": "EXTENDED", "GETTING_STAGED_DIFF_PROGRESS": "Getting diff of staged files\u2026", "GIT_COMMIT": "Git commit\u2026", diff --git a/src/styles/Extn-Terminal.less b/src/styles/Extn-Terminal.less index ac253797c..3d2a1033b 100644 --- a/src/styles/Extn-Terminal.less +++ b/src/styles/Extn-Terminal.less @@ -96,35 +96,44 @@ position: relative; } -/* Tab bar: 30px flex spacer, flyout sits inside absolutely */ +/* Tab bar: static sidebar, width switches via .terminal-tabs-expanded on container */ .terminal-tab-bar { position: relative; - width: 30px; - min-width: 30px; - overflow: visible; + width: 60px; + min-width: 60px; +} + +.terminal-tabs-expanded .terminal-tab-bar { + width: 159px; + min-width: 159px; } -/* ─── Unified flyout: collapses to 30px, expands on hover ─── */ +/* ─── Tab sidebar: static, no flyout/hover ─── */ .terminal-tab-flyout { display: flex; - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 30px; + position: relative; + width: 100%; + height: 100%; background: var(--terminal-tab-bg); border-left: 1px solid var(--terminal-border); - z-index: 20; flex-direction: column; - transition: width 0.08s ease; + overflow: hidden; } -.terminal-tab-flyout:hover { - width: 170px; - box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3); +/* Fade gradient at bottom of tab list to hint overflow */ +.terminal-tab-flyout::after { + content: ''; + position: absolute; + bottom: 29px; /* sits above the actions row */ + left: 0; + right: 0; + height: 20px; + background: linear-gradient(to bottom, transparent, var(--terminal-tab-bg)); + pointer-events: none; + z-index: 1; } -/* Flyout list: scrollable, scrollbar hidden in collapsed mode */ +/* Tab list: scrollable, scrollbar hidden, scroll via wheel/trackpad */ .terminal-tab-flyout .terminal-flyout-list { flex: 1; min-height: 0; @@ -136,13 +145,6 @@ } } -.terminal-tab-flyout:hover .terminal-flyout-list { - scrollbar-width: auto; - &::-webkit-scrollbar { - display: block; - } -} - /* Flyout item */ .terminal-flyout-item { position: relative; @@ -151,8 +153,8 @@ height: 28px; min-height: 28px; cursor: pointer; - color: var(--terminal-tab-text); - font-size: 11px; + color: var(--terminal-tab-active-text); + font-size: 12px; white-space: nowrap; overflow: hidden; } @@ -167,58 +169,47 @@ box-shadow: inset 2px 0 0 #007acc; } -/* Icon column: fixed 30px slot, always visible in collapsed mode. - In expanded mode, CSS order moves it to the right end. */ +/* Icon column: fixed 30px slot */ .terminal-flyout-icon { width: 30px; min-width: 30px; display: flex; align-items: center; justify-content: center; - font-size: 11px; + font-size: 12px; flex-shrink: 0; } -/* Title: fills remaining space */ +/* Title & CWD: hidden in collapsed, shown in expanded */ .terminal-flyout-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; + display: none; } -/* CWD basename shown on the right */ .terminal-flyout-cwd { - font-size: 11px; + font-size: 12px; color: var(--terminal-tab-text); flex-shrink: 1; min-width: 0; max-width: 50%; margin-left: 4px; margin-right: 2px; - opacity: 0.6; + opacity: 0.8; overflow: hidden; text-overflow: ellipsis; + display: none; } -/* Expanded mode: reorder so icon is at the right end, - add left padding for close button area */ -.terminal-tab-flyout:hover .terminal-flyout-item { - padding-left: 30px; -} - -.terminal-tab-flyout:hover .terminal-flyout-icon { - order: 99; -} - -/* Close button: overlays left padding area, visible only when - flyout is expanded AND the specific item is hovered */ +/* Close button: absolutely positioned on the right, visible on item hover only */ .terminal-flyout-close { position: absolute; - left: 0; + right: 0; top: 0; bottom: 0; - width: 30px; + width: 20px; display: none; align-items: center; justify-content: center; @@ -228,7 +219,7 @@ z-index: 1; } -.terminal-tab-flyout:hover .terminal-flyout-item:hover .terminal-flyout-close { +.terminal-flyout-item:hover .terminal-flyout-close { display: flex; } @@ -237,6 +228,21 @@ background: rgba(255, 255, 255, 0.1); } +/* ─── Expanded mode ─── */ +.terminal-tabs-expanded .terminal-flyout-title { + display: block; +} + +.terminal-tabs-expanded .terminal-flyout-cwd { + display: block; +} + +/* Reserve space so cwd text doesn't sit under the close button */ +.terminal-tabs-expanded .terminal-flyout-cwd { + margin-right: 20px; +} + + /* ─── Flyout bottom actions ─── */ .terminal-flyout-actions { border-top: 1px solid var(--terminal-border); @@ -259,8 +265,8 @@ height: 100%; padding: 0; cursor: pointer; - color: var(--terminal-tab-text); - font-size: 11px; + color: var(--terminal-tab-active-text); + font-size: 12px; background: transparent; border: none; overflow: hidden; @@ -269,12 +275,11 @@ .terminal-flyout-new-btn:hover { background: rgba(255, 255, 255, 0.05); - color: var(--terminal-tab-active-text); } -/* Dropdown chevron: hidden in collapsed mode, shown inline when expanded */ +/* Dropdown chevron: visible in both modes */ .terminal-flyout-dropdown-btn { - display: none; + display: flex; align-items: center; justify-content: center; width: 30px; @@ -289,10 +294,6 @@ flex-shrink: 0; } -.terminal-tab-flyout:hover .terminal-flyout-dropdown-btn { - display: flex; -} - .terminal-flyout-dropdown-btn:hover { background: rgba(255, 255, 255, 0.05); color: var(--terminal-tab-active-text); @@ -304,12 +305,12 @@ flex-shrink: 0; } -/* Hide button labels in collapsed mode; show when expanded */ -.terminal-tab-flyout .terminal-btn-label { +/* Button labels: hidden in collapsed, shown in expanded */ +.terminal-btn-label { display: none; } -.terminal-tab-flyout:hover .terminal-btn-label { +.terminal-tabs-expanded .terminal-btn-label { display: inline; } diff --git a/test/spec/Terminal-integ-test.js b/test/spec/Terminal-integ-test.js index 9cc256f62..3066abd28 100644 --- a/test/spec/Terminal-integ-test.js +++ b/test/spec/Terminal-integ-test.js @@ -138,12 +138,14 @@ define(function (require, exports, module) { } /** - * Trigger a flyout process refresh so tab titles + * Trigger a process refresh so tab titles * reflect the current foreground process. */ function triggerFlyoutRefresh() { - testWindow.$(".terminal-tab-flyout") - .trigger("mouseenter"); + const termModule = testWindow.brackets.getModule( + "extensionsIntegrated/Terminal/main" + ); + termModule._refreshAllProcesses(); } /**