From e230d7063a69d6b661cf036080c252e251b80f4a Mon Sep 17 00:00:00 2001 From: Elora <56364111+eloraa@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:06:52 -0500 Subject: [PATCH] add new profile popup --- app/commands.ts | 20 ++++-- app/config/config-default.json | 1 + app/config/schema.json | 8 +++ app/keymaps/darwin.json | 2 + app/keymaps/linux.json | 2 + app/keymaps/win32.json | 2 + app/ui/window.ts | 4 +- lib/actions/sessions.ts | 31 ++++++++- lib/actions/ui.ts | 2 +- lib/command-registry.ts | 17 ++++- lib/components/profile-popup.tsx | 111 +++++++++++++++++++++++++++++++ lib/components/term-group.tsx | 1 + lib/components/term.tsx | 2 + lib/components/terms.tsx | 1 + lib/containers/hyper.tsx | 19 ++++-- lib/containers/profile-popup.ts | 31 +++++++++ lib/containers/terms.ts | 7 +- lib/index.tsx | 8 +++ lib/reducers/sessions.ts | 7 +- lib/reducers/ui.ts | 1 + lib/utils/plugins.ts | 4 +- typings/common.d.ts | 4 +- typings/config.d.ts | 6 ++ typings/constants/sessions.d.ts | 9 ++- typings/hyper.d.ts | 4 ++ 25 files changed, 283 insertions(+), 21 deletions(-) create mode 100644 lib/components/profile-popup.tsx create mode 100644 lib/containers/profile-popup.ts diff --git a/app/commands.ts b/app/commands.ts index 469a5a3d7f2c..cccaedb7e594 100644 --- a/app/commands.ts +++ b/app/commands.ts @@ -6,12 +6,14 @@ import {updatePlugins} from './plugins'; import {installCLI} from './utils/cli-install'; import * as systemContextMenu from './utils/system-context-menu'; -const commands: Record void> = { +const commands: Record void> = { 'window:new': () => { // If window is created on the same tick, it will consume event too setTimeout(app.createWindow, 0); }, - 'tab:new': (focusedWindow) => { + 'tab:new': (focusedWindow, event) => { + if (getConfig().showPopupOnNewTab && event === 'keydown') return; + if (focusedWindow) { focusedWindow.rpc.emit('termgroup add req', {}); } else { @@ -119,6 +121,16 @@ const commands: Record void> = { 'editor:search-close': (focusedWindow) => { focusedWindow?.rpc.emit('session search close'); }, + 'editor:profilePopup': (focusedWindow) => { + if (getConfig().showPopupOnNewTab && focusedWindow) { + focusedWindow.rpc.emit('session profilePopup'); + } + }, + 'editor:profilePopup-close': (focusedWindow) => { + if (focusedWindow) { + focusedWindow.rpc.emit('session profilePopup close'); + } + }, 'cli:install': () => { void installCLI(true); }, @@ -162,9 +174,9 @@ getConfig().profiles.forEach((profile) => { }; }); -export const execCommand = (command: string, focusedWindow?: BrowserWindow) => { +export const execCommand = (command: string, focusedWindow?: BrowserWindow, event?: string) => { const fn = commands[command]; if (fn) { - fn(focusedWindow); + fn(focusedWindow, event); } }; diff --git a/app/config/config-default.json b/app/config/config-default.json index 2a6a66ff618a..9006c07db6c7 100644 --- a/app/config/config-default.json +++ b/app/config/config-default.json @@ -22,6 +22,7 @@ "workingDirectory": "", "showHamburgerMenu": "", "showWindowControls": "", + "showPopupOnNewTab": "", "padding": "12px 14px", "colors": { "black": "#000000", diff --git a/app/config/schema.json b/app/config/schema.json index 6bcf850036eb..2756887a3bb0 100644 --- a/app/config/schema.json +++ b/app/config/schema.json @@ -261,6 +261,14 @@ true ] }, + "showPopupOnTab": { + "description": "set to `true` if you want to show a popup of your profile when creating a new tab\n\ndefault: `false`", + "enum": [ + "", + false, + true + ] + }, "showWindowControls": { "description": "set to `false` if you want to hide the minimize, maximize and close buttons\n\nadditionally, set to `'left'` if you want them on the left, like in Ubuntu\n\ndefault: `true` on Windows and Linux, ignored on macOS", "enum": [ diff --git a/app/keymaps/darwin.json b/app/keymaps/darwin.json index 30f0f2763028..2173c0f7150d 100644 --- a/app/keymaps/darwin.json +++ b/app/keymaps/darwin.json @@ -41,6 +41,8 @@ "editor:selectAll": "command+a", "editor:search": "command+f", "editor:search-close": "esc", + "editor:profilePopup": "command+a", + "editor:profilePopup-close": "esc", "editor:movePreviousWord": "alt+left", "editor:moveNextWord": "alt+right", "editor:moveBeginningLine": "command+left", diff --git a/app/keymaps/linux.json b/app/keymaps/linux.json index da66d671d7f3..3329c81da98a 100644 --- a/app/keymaps/linux.json +++ b/app/keymaps/linux.json @@ -39,6 +39,8 @@ "editor:selectAll": "ctrl+shift+a", "editor:search": "ctrl+shift+f", "editor:search-close": "esc", + "editor:profilePopup": "ctrl+shift+t", + "editor:profilePopup-close": "esc", "editor:movePreviousWord": "ctrl+left", "editor:moveNextWord": "ctrl+right", "editor:moveBeginningLine": "home", diff --git a/app/keymaps/win32.json b/app/keymaps/win32.json index 8e7b19134c49..ae920f72a425 100644 --- a/app/keymaps/win32.json +++ b/app/keymaps/win32.json @@ -36,6 +36,8 @@ "editor:selectAll": "ctrl+shift+a", "editor:search": "ctrl+shift+f", "editor:search-close": "esc", + "editor:profilePopup": "ctrl+shift+t", + "editor:profilePopup-close": "esc", "editor:movePreviousWord": "", "editor:moveNextWord": "", "editor:moveBeginningLine": "Home", diff --git a/app/ui/window.ts b/app/ui/window.ts index 87eba6d7266c..74483a29b3a3 100644 --- a/app/ui/window.ts +++ b/app/ui/window.ts @@ -270,9 +270,9 @@ export function newWindow( rpc.on('close', () => { window.close(); }); - rpc.on('command', (command) => { + rpc.on('command', ({command, event}) => { const focusedWindow = BrowserWindow.getFocusedWindow(); - execCommand(command, focusedWindow!); + execCommand(command, focusedWindow!, event); }); // pass on the full screen events from the window to react rpc.win.on('enter-full-screen', () => { diff --git a/lib/actions/sessions.ts b/lib/actions/sessions.ts index 4cb53f9d2867..27f460f82a81 100644 --- a/lib/actions/sessions.ts +++ b/lib/actions/sessions.ts @@ -11,7 +11,8 @@ import { SESSION_CLEAR_ACTIVE, SESSION_USER_DATA, SESSION_SET_XTERM_TITLE, - SESSION_SEARCH + SESSION_SEARCH, + SESSION_PROFILE_POPUP } from '../../typings/constants/sessions'; import type {HyperState, HyperDispatch, HyperActions} from '../../typings/hyper'; import rpc from '../rpc'; @@ -163,6 +164,34 @@ export function closeSearch(uid?: string, keyEvent?: any) { }; } +export function openProfilePopup(uid?: string) { + return (dispatch: HyperDispatch, getState: () => HyperState) => { + const targetUid = uid || getState().sessions.activeUid!; + dispatch({ + type: SESSION_PROFILE_POPUP, + value: true, + uid: targetUid + }); + }; +} + +export function closeProfilePopup(uid?: string, keyEvent?: any) { + return (dispatch: HyperDispatch, getState: () => HyperState) => { + const targetUid = uid || getState().sessions.activeUid!; + if (getState().sessions.sessions[targetUid]?.profilePopup) { + dispatch({ + type: SESSION_PROFILE_POPUP, + uid: targetUid, + value: false + }); + } else { + if (keyEvent) { + keyEvent.catched = false; + } + } + }; +} + export function sendSessionData(uid: string | null, data: string, escaped?: boolean) { return (dispatch: HyperDispatch, getState: () => HyperState) => { dispatch({ diff --git a/lib/actions/ui.ts b/lib/actions/ui.ts index 92da4ceea35d..532f8955ea3e 100644 --- a/lib/actions/ui.ts +++ b/lib/actions/ui.ts @@ -327,7 +327,7 @@ export function execCommand(command: string, fn: (e: any, dispatch: HyperDispatc if (fn) { fn(e, dispatch); } else { - rpc.emit('command', command); + rpc.emit('command', {command, event: e.type}); } } }); diff --git a/lib/command-registry.ts b/lib/command-registry.ts index f72c08c98bde..976d191da8b4 100644 --- a/lib/command-registry.ts +++ b/lib/command-registry.ts @@ -1,22 +1,33 @@ import type {HyperDispatch} from '../typings/hyper'; -import {closeSearch} from './actions/sessions'; +import {closeProfilePopup, closeSearch} from './actions/sessions'; import {ipcRenderer} from './utils/ipc'; let commands: Record void> = { 'editor:search-close': (e, dispatch) => { dispatch(closeSearch(undefined, e)); window.focusActiveTerm(); + }, + 'editor:close-profile-popup': (e, dispatch) => { + dispatch(closeProfilePopup(undefined, e)); + window.focusActiveTerm(); } }; export const getRegisteredKeys = async () => { const keymaps = await ipcRenderer.invoke('getDecoratedKeymaps'); - return Object.keys(keymaps).reduce((result: Record, actionName) => { + return Object.keys(keymaps).reduce((result: Record, actionName) => { const commandKeys = keymaps[actionName]; commandKeys.forEach((shortcut) => { - result[shortcut] = actionName; + if (result[shortcut]) { + if (typeof result[shortcut] === 'string') { + result[shortcut] = [result[shortcut] as string]; + } + (result[shortcut] as string[]).push(actionName); + } else { + result[shortcut] = actionName; + } }); return result; }, {}); diff --git a/lib/components/profile-popup.tsx b/lib/components/profile-popup.tsx new file mode 100644 index 000000000000..956651957c81 --- /dev/null +++ b/lib/components/profile-popup.tsx @@ -0,0 +1,111 @@ +import React, {forwardRef, useEffect, useRef} from 'react'; +import type {KeyboardEvent} from 'react'; + +import type {ProfilePopupConnectedProps} from '../containers/profile-popup'; + +const ProfilePopup = forwardRef((props, ref) => { + const {backgroundColor, foregroundColor, borderColor, profiles, openNewTab, close} = props; + const listItemsRef = useRef<(HTMLLIElement | null)[]>([]); + + useEffect(() => { + listItemsRef.current[0]?.focus(); + }, [listItemsRef.current]); + + const handleKeyDown = (e: KeyboardEvent) => { + const currentIndex = listItemsRef.current.findIndex((item) => item === document.activeElement); + + if (e.key === 'ArrowDown') { + e.preventDefault(); + const nextIndex = (currentIndex + 1) % profiles.length; + listItemsRef.current[nextIndex]?.focus(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + const prevIndex = (currentIndex - 1 + profiles.length) % profiles.length; + listItemsRef.current[prevIndex]?.focus(); + } else if (e.key === 'Tab') { + e.preventDefault(); + const nextIndex = (currentIndex + 1) % profiles.length; + listItemsRef.current[nextIndex]?.focus(); + } else if (e.key === 'Enter') { + e.preventDefault(); + listItemsRef.current[currentIndex]?.click(); + } + }; + + const handleOpen = (profile: string) => { + openNewTab(profile); + close(); + }; + return ( +
+
+
+
    + {profiles.map((profile, index) => ( +
  • listItemsRef.current[index]?.focus()} + ref={(el) => (listItemsRef.current[index] = el)} + onClick={() => handleOpen(profile.name)} + > + {profile.name} +
  • + ))} +
+
+ + +
+ ); +}); + +ProfilePopup.displayName = 'ProfilePopup'; + +export default ProfilePopup; diff --git a/lib/components/term-group.tsx b/lib/components/term-group.tsx index 5e6e66ec2eb3..2d300579d427 100644 --- a/lib/components/term-group.tsx +++ b/lib/components/term-group.tsx @@ -87,6 +87,7 @@ class TermGroup_ extends React.PureComponent { padding: this.props.padding, cleared: session.cleared, search: session.search, + profilePopup: session.profilePopup, cols: session.cols, rows: session.rows, copyOnSelect: this.props.copyOnSelect, diff --git a/lib/components/term.tsx b/lib/components/term.tsx index 45c1464c97cc..4ad589065999 100644 --- a/lib/components/term.tsx +++ b/lib/components/term.tsx @@ -17,6 +17,7 @@ import {WebLinksAddon} from 'xterm-addon-web-links'; import {WebglAddon} from 'xterm-addon-webgl'; import type {TermProps} from '../../typings/hyper'; +import {ProfilePopupContainer} from '../containers/profile-popup'; import terms from '../terms'; import processClipboard from '../utils/paste'; import {decorate} from '../utils/plugins'; @@ -550,6 +551,7 @@ export default class Term extends React.PureComponent< font={this.props.uiFontFamily} /> ) : null} + {this.props.profilePopup ? : null}