Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new profile popup #8021

Open
wants to merge 1 commit into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions app/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (focusedWindow?: BrowserWindow) => void> = {
const commands: Record<string, (focusedWindow?: BrowserWindow, event?: string) => 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 {
Expand Down Expand Up @@ -119,6 +121,16 @@ const commands: Record<string, (focusedWindow?: BrowserWindow) => 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);
},
Expand Down Expand Up @@ -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);
}
};
1 change: 1 addition & 0 deletions app/config/config-default.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"workingDirectory": "",
"showHamburgerMenu": "",
"showWindowControls": "",
"showPopupOnNewTab": "",
"padding": "12px 14px",
"colors": {
"black": "#000000",
Expand Down
8 changes: 8 additions & 0 deletions app/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
2 changes: 2 additions & 0 deletions app/keymaps/darwin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions app/keymaps/linux.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions app/keymaps/win32.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions app/ui/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
31 changes: 30 additions & 1 deletion lib/actions/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion lib/actions/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}
}
});
Expand Down
17 changes: 14 additions & 3 deletions lib/command-registry.ts
Original file line number Diff line number Diff line change
@@ -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<string, (event: any, dispatch: HyperDispatch) => 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<string, string>, actionName) => {
return Object.keys(keymaps).reduce((result: Record<string, string | string[]>, 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;
}, {});
Expand Down
111 changes: 111 additions & 0 deletions lib/components/profile-popup.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement, ProfilePopupConnectedProps>((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<HTMLDivElement>) => {
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 (
<div className="profile_popup" ref={ref}>
<div className="profile_popup_overlay" onClick={close}></div>
<div className="profile_popup_list_container" onKeyDown={handleKeyDown}>
<ul className="profile_popup_list">
{profiles.map((profile, index) => (
<li
tabIndex={0}
key={profile.name}
className="profile_list_item"
onMouseOver={() => listItemsRef.current[index]?.focus()}
ref={(el) => (listItemsRef.current[index] = el)}
onClick={() => handleOpen(profile.name)}
>
{profile.name}
</li>
))}
</ul>
</div>

<style jsx>{`
.profile_popup {
position: absolute;
inset: 0;
z-index: 1000;

display: flex;
align-items: center;
justify-content: center;
padding-bottom: 2%;
}
.profile_popup_overlay {
position: absolute;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.profile_popup_list_container {
background-color: ${backgroundColor};
border: 1px solid ${borderColor};
color: ${foregroundColor};
padding: 10px;
z-index: 10;
min-width: 180px;
}
.profile_popup_list {
list-style-type: none;
}
.profile_list_item {
padding: 8px 10px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
border-radius: 3px;
transition:
background-color 200ms ease,
box-shadow 200ms ease;
}
.profile_list_item:hover {
background-color: ${borderColor};
}
.profile_list_item:focus {
outline: none;
background-color: ${borderColor};
box-shadow: 0 0 0 3px rgba(0, 150, 136, 0.5);
}
`}</style>
</div>
);
});

ProfilePopup.displayName = 'ProfilePopup';

export default ProfilePopup;
1 change: 1 addition & 0 deletions lib/components/term-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class TermGroup_ extends React.PureComponent<TermGroupProps> {
padding: this.props.padding,
cleared: session.cleared,
search: session.search,
profilePopup: session.profilePopup,
cols: session.cols,
rows: session.rows,
copyOnSelect: this.props.copyOnSelect,
Expand Down
2 changes: 2 additions & 0 deletions lib/components/term.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -550,6 +551,7 @@ export default class Term extends React.PureComponent<
font={this.props.uiFontFamily}
/>
) : null}
{this.props.profilePopup ? <ProfilePopupContainer /> : null}

<style jsx global>{`
.term_fit {
Expand Down
1 change: 1 addition & 0 deletions lib/components/terms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export default class Terms extends React.Component<React.PropsWithChildren<Terms
onData: this.props.onData,
onOpenSearch: this.props.onOpenSearch,
onCloseSearch: this.props.onCloseSearch,
closeProfilePopup: this.props.closeProfilePopup,
onContextMenu: this.props.onContextMenu,
quickEdit: this.props.quickEdit,
webGLRenderer: this.props.webGLRenderer,
Expand Down
19 changes: 14 additions & 5 deletions lib/containers/hyper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,20 @@ const Hyper = forwardRef<HTMLDivElement, HyperProps>((props, ref) => {
mousetrap.current?.bind(
commandKeys,
(e) => {
const command = keys[commandKeys];
// We should tell xterm to ignore this event.
(e as any).catched = true;
props.execCommand(command, getCommandHandler(command), e);
shouldPreventDefault(command) && e.preventDefault();
const commandList = keys[commandKeys];
if (Array.isArray(commandList)) {
commandList.forEach((command) => {
// We should tell xterm to ignore this event.
(e as any).catched = true;
props.execCommand(command, getCommandHandler(command), e);
shouldPreventDefault(command) && e.preventDefault();
});
} else {
// We should tell xterm to ignore this event.
(e as any).catched = true;
props.execCommand(commandList, getCommandHandler(commandList), e);
shouldPreventDefault(commandList) && e.preventDefault();
}
},
'keydown'
);
Expand Down
Loading