feat: add shell page

This commit is contained in:
Simon Chan 2021-10-13 13:37:28 +08:00
parent 868e3d3f10
commit 2053ee3238
6 changed files with 221 additions and 8 deletions

View file

@ -233,7 +233,7 @@ const features: FeatureDefinition[][] = [
step: 1,
initial: 34,
onChange: (value) => device.current!.demoMode.setTime(state.features.get('hour') as number | undefined ?? 34, value as number)
}
},
],
];

View file

@ -0,0 +1,84 @@
import { AdbShell } from "@yume-chan/adb";
import { encodeUtf8 } from "@yume-chan/adb-backend-webusb";
import { AutoDisposable } from "@yume-chan/event";
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search';
import { WebglAddon } from 'xterm-addon-webgl';
export class AdbTerminal extends AutoDisposable {
private element = document.createElement('div');
public terminal: Terminal = new Terminal({
scrollback: 9000,
});
public searchAddon = new SearchAddon();
private readonly fitAddon = new FitAddon();
private _shell: AdbShell | undefined;
public get socket() { return this._shell; }
public set socket(value) {
if (this._shell) {
// Remove event listeners
this.dispose();
}
this._shell = value;
if (value) {
this.terminal.clear();
this.terminal.reset();
this.addDisposable(value.onStdout(data => {
this.terminal.write(new Uint8Array(data));
}));
this.addDisposable(value.onStderr(data => {
this.terminal.write(new Uint8Array(data));
}));
this.addDisposable(this.terminal.onData(data => {
const buffer = encodeUtf8(data);
value.write(buffer);
}));
this.fit();
}
}
public constructor() {
super();
this.element.style.width = '100%';
this.element.style.height = '100%';
this.element.style.overflow = 'hidden';
this.terminal.setOption('fontFamily', '"Cascadia Code", Consolas, monospace, "Source Han Sans SC", "Microsoft YaHei"');
this.terminal.setOption('letterSpacing', 1);
this.terminal.setOption('cursorStyle', 'bar');
this.terminal.loadAddon(this.searchAddon);
this.terminal.loadAddon(this.fitAddon);
}
public setContainer(container: HTMLDivElement) {
container.appendChild(this.element);
if (!this.terminal.element) {
void this.element.offsetWidth;
this.terminal.open(this.element);
this.terminal.loadAddon(new WebglAddon());
// WebGL renderer ignores `cursorBlink` set before it initialized
this.terminal.setOption('cursorBlink', true);
this.fit();
}
}
public fit() {
this.fitAddon.fit();
// workaround https://github.com/xtermjs/xterm.js/issues/3504
(this.terminal as any)._core.viewport._refresh();
// Resize remote terminal
const { rows, cols } = this.terminal;
this._shell?.resize(rows, cols);
}
}

View file

@ -1,7 +1,7 @@
import { ActionButton, IComponentAsProps, IconButton, INavButtonProps, INavLink, initializeIcons, Link as FluentLink, mergeStyles, mergeStyleSets, Nav, Stack, StackItem } from "@fluentui/react";
import { IComponentAsProps, IconButton, INavButtonProps, initializeIcons, mergeStyles, mergeStyleSets, Nav, Stack, StackItem } from "@fluentui/react";
import type { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from "react";
import { Connect, ErrorDialogProvider, Logger, ToggleLogger } from "../components";
import '../styles/globals.css';
@ -24,7 +24,11 @@ const ROUTES = [
{
url: '/framebuffer',
name: 'Screen Capture',
}
},
{
url: '/shell',
name: 'Interactive Shell',
},
];
function NavLink({ link, defaultRender: DefaultRender, ...props }: IComponentAsProps<INavButtonProps>) {

View file

@ -476,7 +476,7 @@ const FileManager: NextPage = (): JSX.Element | null => {
state.loadFiles();
setUploading(false);
}
}, []);
}, [showErrorDialog]);
const [menuItems, setMenuItems] = useState<IContextualMenuItem[]>([]);
useEffect(() => {
@ -557,7 +557,7 @@ const FileManager: NextPage = (): JSX.Element | null => {
}
setMenuItems(result);
}, [selectedItems]);
}, [selectedItems, upload, showErrorDialog]);
const [contextMenuTarget, setContextMenuTarget] = useState<MouseEvent>();
const showContextMenu = useCallback((
@ -618,7 +618,7 @@ const FileManager: NextPage = (): JSX.Element | null => {
<Layer>
<Overlay onClick={hidePreview}>
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<img src={previewUrl} style={{ maxWidth: '100%', maxHeight: '100%' }} />
<img src={previewUrl} alt="" style={{ maxWidth: '100%', maxHeight: '100%' }} />
</div>
</Overlay>
</Layer>

View file

@ -51,7 +51,7 @@ const FrameBuffer: NextPage = (): JSX.Element | null => {
} catch (e) {
showErrorDialog(e instanceof Error ? e.message : `${e}`);
}
}, []);
}, [showErrorDialog]);
useEffect(() => {
return autorun(() => {

125
apps/demo/pages/shell.tsx Normal file
View file

@ -0,0 +1,125 @@
import { IconButton, SearchBox, Stack, StackItem } from '@fluentui/react';
import { reaction } from "mobx";
import { observer } from "mobx-react-lite";
import { NextPage } from "next";
import Head from "next/head";
import React, { CSSProperties, useCallback, useContext, useEffect, useRef, useState } from 'react';
import 'xterm/css/xterm.css';
import { ErrorDialogContext } from '../components/error-dialog';
import { device } from "../state";
import { ResizeObserver, RouteStackProps } from '../utils';
let terminal: import('../components/terminal').AdbTerminal;
if (typeof window !== 'undefined') {
const AdbTerminal: typeof import('../components/terminal').AdbTerminal = require('../components/terminal').AdbTerminal;
terminal = new AdbTerminal();
}
const ResizeObserverStyle: CSSProperties = {
width: '100%',
height: '100%',
};
const UpIconProps = { iconName: 'ChevronUp' };
const DownIconProps = { iconName: 'ChevronDown' };
const Shell: NextPage = (): JSX.Element | null => {
const { show: showErrorDialog } = useContext(ErrorDialogContext);
const [searchKeyword, setSearchKeyword] = useState('');
const handleSearchKeywordChange = useCallback((e, newValue?: string) => {
setSearchKeyword(newValue ?? '');
if (newValue) {
terminal.searchAddon.findNext(newValue, { incremental: true });
}
}, []);
const findPrevious = useCallback(() => {
terminal.searchAddon.findPrevious(searchKeyword);
}, [searchKeyword]);
const findNext = useCallback(() => {
terminal.searchAddon.findNext(searchKeyword);
}, [searchKeyword]);
const connectingRef = useRef(false);
useEffect(() => {
return reaction(
() => device.current,
async () => {
if (!device.current) {
terminal.socket = undefined;
return;
}
if (!!terminal.socket || connectingRef.current) {
return;
}
try {
connectingRef.current = true;
const socket = await device.current.childProcess.shell();
terminal.socket = socket;
} catch (e) {
showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally {
connectingRef.current = false;
}
},
{
fireImmediately: true,
}
);
}, [showErrorDialog]);
const handleResize = useCallback(() => {
terminal.fit();
}, []);
const handleContainerRef = useCallback((container: HTMLDivElement | null) => {
if (container) {
terminal.setContainer(container);
}
}, []);
return (
<Stack {...RouteStackProps}>
<Head>
<title>Screen Capture - WebADB</title>
</Head>
<StackItem>
<Stack horizontal>
<StackItem grow>
<SearchBox
placeholder="Find"
value={searchKeyword}
onChange={handleSearchKeywordChange}
onSearch={findNext}
/>
</StackItem>
<StackItem>
<IconButton
disabled={!searchKeyword}
iconProps={UpIconProps}
onClick={findPrevious}
/>
</StackItem>
<StackItem>
<IconButton
disabled={!searchKeyword}
iconProps={DownIconProps}
onClick={findNext}
/>
</StackItem>
</Stack>
</StackItem>
<StackItem grow styles={{ root: { minHeight: 0 } }}>
<ResizeObserver style={ResizeObserverStyle} onResize={handleResize}>
<div ref={handleContainerRef} style={{ height: '100%' }} />
</ResizeObserver>
</StackItem>
</Stack>
);
};
export default observer(Shell);