Skip to content

Commit

Permalink
wip(panel/console): added hotkeys
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed Jan 9, 2024
1 parent 33aabcf commit 2eae28c
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 39 deletions.
4 changes: 2 additions & 2 deletions docs/dev_notes.md
Expand Up @@ -9,11 +9,11 @@
- [x] basic xterm.js (canvas mode)
- [x] auto re-fit
- [x] scroll to bottom button
- [ ] search addon + search bar
- [x] search addon + search bar
- find next: f3, enter
- find previous: shift+f3, shift+enter
- [x] custom event handler for f5, esc/ctrl+f (search), and ctrl+c
- hotkeys should work on terminal, page and input
- [ ] custom event handler for f5, esc/ctrl+f (search), and ctrl+c
- [ ] command history (arrows only) without local storage
- [ ] socket.io connection
- [ ] fix z-order, cant click postpone/support on the warning bar
Expand Down
27 changes: 15 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion panel/package.json
Expand Up @@ -31,13 +31,13 @@
"@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.0.5",
"@tanstack/react-virtual": "^3.0.1",
"@types/throttle-debounce": "^5.0.2",
"@xterm/addon-canvas": "0.6.0-beta.20",
"@xterm/addon-fit": "0.9.0-beta.28",
"@xterm/addon-search": "0.14.0-beta.20",
"@xterm/xterm": "5.4.0-beta.20",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"debounce": "^2.0.0",
"is-mobile": "^4.0.0",
"jotai": "^2.5.1",
"jotai-effect": "^0.2.3",
Expand All @@ -51,6 +51,7 @@
"socket.io-client": "^4.7.2",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"throttle-debounce": "^5.0.0",
"usehooks-ts": "^2.9.1",
"wouter": "^2.12.1"
},
Expand Down
14 changes: 8 additions & 6 deletions panel/src/global.d.ts
@@ -1,18 +1,20 @@
import { InjectedTxConsts } from '@shared/otherTypes';

type LogoutNoticeMessage = { type: 'logoutNotice' }
type OpenAccountModalMessage = { type: 'openAccountModal' }
type OpenPlayerModalMessage = { type: 'openPlayerModal', ref: PlayerModalRefType }
type navigateToPageMessage = { type: 'navigateToPage', href: string }
type LogoutNoticeMessage = { type: 'logoutNotice' };
type OpenAccountModalMessage = { type: 'openAccountModal' };
type OpenPlayerModalMessage = { type: 'openPlayerModal', ref: PlayerModalRefType };
type navigateToPageMessage = { type: 'navigateToPage', href: string };
type liveConsoleSearchHotkeyMessage = { type: 'liveConsoleSearchHotkey', action: string };

export declare global {
interface Window {
txConsts: InjectedTxConsts;
txIsMobile: boolean;
invokeNative?: (nativeName: string, ...args: any[]) => void;
}
type MessageEventFromIframe = MessageEvent<LogoutNoticeMessage>
type TxMessageEvent = MessageEvent<LogoutNoticeMessage>
| MessageEvent<OpenAccountModalMessage>
| MessageEvent<OpenPlayerModalMessage>
| MessageEvent<navigateToPageMessage>;
| MessageEvent<navigateToPageMessage>
| MessageEvent<liveConsoleSearchHotkeyMessage>;
}
2 changes: 1 addition & 1 deletion panel/src/layout/MainShell.tsx
Expand Up @@ -27,7 +27,7 @@ export default function MainShell() {
const toggleTheme = useToggleTheme();

//Listener for messages from child iframes (legacy routes)
useEventListener('message', (e: MessageEventFromIframe) => {
useEventListener('message', (e: TxMessageEvent) => {
if (e.data.type === 'logoutNotice') {
expireSession('child iframe', 'got logoutNotice');
} else if (e.data.type === 'openAccountModal') {
Expand Down
91 changes: 84 additions & 7 deletions panel/src/pages/LiveConsole/LiveConsole.tsx
Expand Up @@ -4,8 +4,8 @@ import { FitAddon } from '@xterm/addon-fit';
import { SearchAddon } from '@xterm/addon-search';
import { useEffect, useMemo, useRef, useState } from "react";
import { useEventListener } from 'usehooks-ts';
import { useSetPageTitle } from "@/hooks/pages";
import debounce from 'debounce';
import { useContentRefresh, useSetPageTitle } from "@/hooks/pages";
import { debounce, throttle } from 'throttle-debounce';

import { ChevronsDownIcon } from "lucide-react";
import LiveConsoleFooter from "./LiveConsoleFooter";
Expand All @@ -19,11 +19,14 @@ import './xtermOverrides.css';
import '@xterm/xterm/css/xterm.css';


const keyDebounceTime = 150; //ms

export default function LiveConsole() {
// const [isSaveSheetOpen, setIsSaveSheetOpen] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const [setshowSearchBar, setShowSearchBar] = useState(false);
const [showSearchBar, setShowSearchBar] = useState(false);
const setPageTitle = useSetPageTitle();
const refreshPage = useContentRefresh();
setPageTitle('Live Console');


Expand All @@ -37,6 +40,13 @@ export default function LiveConsole() {
const fitAddon = useMemo(() => new FitAddon(), []);
const searchAddon = useMemo(() => new SearchAddon(), []);

const sendSearchKeyEvent = throttle(keyDebounceTime, (action: string) => {
window.postMessage({
type: 'liveConsoleSearchHotkey',
action,
});
}, { noTrailing: true });

const refitTerminal = () => {
if (containerRef.current && term.element && fitAddon) {
//fitAddon does not get the correct height, so we have to calculate it ourselves
Expand All @@ -55,7 +65,7 @@ export default function LiveConsole() {
console.log('refitTerminal: no containerRef.current or term.element or fitAddon');
}
}
useEventListener('resize', debounce(refitTerminal, 100));
useEventListener('resize', debounce(100, refitTerminal));

useEffect(() => {
if (containerRef.current && termElRef.current && !term.element) {
Expand All @@ -69,6 +79,52 @@ export default function LiveConsole() {
refitTerminal();
term.write('\x1b[?25l'); //hide cursor

const scrollPageUp = throttle(keyDebounceTime, () => {
term.scrollLines(Math.min(1, 2 - term.rows));
}, { noTrailing: true });
const scrollPageDown = throttle(keyDebounceTime, () => {
term.scrollLines(Math.max(1, term.rows - 2));
}, { noTrailing: true });
const scrollTop = throttle(keyDebounceTime, () => {
term.scrollToTop();
}, { noTrailing: true });
const scrollBottom = throttle(keyDebounceTime, () => {
term.scrollToBottom();
}, { noTrailing: true });

term.attachCustomKeyEventHandler((e: KeyboardEvent) => {
if (e.code === 'F5') {
// let live console handle it
return false;
} else if (e.code === 'Escape') {
// let live console handle it
return false;
} else if (e.code === 'KeyF' && (e.ctrlKey || e.metaKey)) {
// let live console handle it
return false;
} else if (e.code === 'F3') {
// let live console handle it
return false;
} else if (e.code === 'KeyC' && (e.ctrlKey || e.metaKey)) {
document.execCommand('copy');
term.clearSelection();
return false;
} else if (e.code === 'PageUp') {
scrollPageUp();
return false;
} else if (e.code === 'PageDown') {
scrollPageDown();
return false;
} else if (e.code === 'Home') {
scrollTop();
return false;
} else if (e.code === 'End') {
scrollBottom();
return false;
}
return true;
});

//DEBUG
term.writeln('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo. \u001b[1m\u001b[33m CanvasAddon');
for (let i = 0; i < 40; i++) {
Expand All @@ -77,6 +133,27 @@ export default function LiveConsole() {
}
}, [term]);

useEventListener('keydown', (e: KeyboardEvent) => {
console.log('live console keydown', e.code);
if (e.code === 'F5') {
refreshPage();
e.preventDefault();
} else if (e.code === 'Escape') {
searchAddon.clearDecorations();
setShowSearchBar(false);
} else if (e.code === 'KeyF' && (e.ctrlKey || e.metaKey)) {
if (showSearchBar) {
sendSearchKeyEvent('focus');
} else {
setShowSearchBar(true);
}
e.preventDefault();
} else if (e.code === 'F3') {
sendSearchKeyEvent(e.shiftKey ? 'previous' : 'next');
e.preventDefault();
}
});

//DEBUG
useEffect(() => {
let cnt = 0;
Expand All @@ -88,7 +165,7 @@ export default function LiveConsole() {
'\u001b[1m\u001b[31m=\u001b[0m'.repeat(mod) +
'\u001b[1m\u001b[33m.\u001b[0m'.repeat(60 - mod)
);
}, 150);
}, 100);
return () => clearInterval(interval);
}, []);

Expand All @@ -103,7 +180,7 @@ export default function LiveConsole() {
term.clear();
}
const toggleSearchBar = () => {
setShowSearchBar(!setshowSearchBar);
setShowSearchBar(!showSearchBar);
}
const toggleSaveSheet = () => {
// setIsSaveSheetOpen(!isSaveSheetOpen);
Expand All @@ -122,7 +199,7 @@ export default function LiveConsole() {
</div>

<LiveConsoleSearchBar
show={setshowSearchBar}
show={showSearchBar}
setShow={setShowSearchBar}
searchAddon={searchAddon}
/>
Expand Down
32 changes: 22 additions & 10 deletions panel/src/pages/LiveConsole/LiveConsoleSearchBar.tsx
Expand Up @@ -3,6 +3,7 @@ import { cn } from "@/lib/utils";
import type { ISearchDecorationOptions, ISearchOptions, SearchAddon } from "@xterm/addon-search";
import { ArrowDownIcon, ArrowUpIcon, CaseSensitiveIcon, RegexIcon, WholeWordIcon, XIcon } from "lucide-react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { useEventListener } from "usehooks-ts";


type ButtonProps = {
Expand Down Expand Up @@ -92,27 +93,26 @@ export default function LiveConsoleSearchBar({ show, setShow, searchAddon }: Liv

//Handlers
const handlePrevious = () => {
if (!inputRef.current) return;
if (!inputRef.current || !inputRef.current.value) return;
console.log('backward search for', inputRef.current.value);
searchAddon.findPrevious(inputRef.current.value, getSearchOptions());
}
const handleNext = () => {
if (!inputRef.current) return;
if (!inputRef.current || !inputRef.current.value) return;
console.log('forward search for', inputRef.current.value);
searchAddon.findNext(inputRef.current.value, getSearchOptions());
}

const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!inputRef.current) return;
if ((e.key === 'Enter' || e.key === 'F3') && !e.shiftKey) {
handleNext();
e.preventDefault();
} else if ((e.key === 'Enter' || e.key === 'F3') && e.shiftKey) {
handlePrevious();
console.log('search input keydown', e.code);
if (e.code === 'Enter') {
if (e.shiftKey) {
handlePrevious();
} else {
handleNext();
}
e.preventDefault();
} else if (e.key === 'Escape') {
setShow(false);
clearSearchState(labelNoResults);
}
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -139,6 +139,18 @@ export default function LiveConsoleSearchBar({ show, setShow, searchAddon }: Liv
searchAddon.findNext(inputRef.current.value, getSearchOptions({ regex: !regex }));
}

//This is required so hotkeys in the page also apply in here
useEventListener('message', (e: TxMessageEvent) => {
if (e.data.type !== 'liveConsoleSearchHotkey') return;
if (e.data.action === 'previous') {
handlePrevious();
} else if (e.data.action === 'next') {
handleNext();
} else if (e.data.action === 'focus') {
inputRef.current?.focus();
}
});

if (!show) return null;
return (
<div className="absolute top-0 xs:right-4 bg-secondary border z-10 flex items-center justify-center gap-1 xs:gap-4 shadow-xl p-1 rounded-b-lg border-t-0 w-full xs:w-auto flex-wrap">
Expand Down

0 comments on commit 2eae28c

Please sign in to comment.