mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-06 03:50:18 +02:00
feat(demo): use tabby as terminal emulator (#541)
This commit is contained in:
parent
58794f0d69
commit
04d7c08b78
51 changed files with 2429 additions and 390 deletions
|
@ -10,6 +10,7 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@angular/compiler": "^15.2.6",
|
||||||
"@fluentui/react": "^8.107.5",
|
"@fluentui/react": "^8.107.5",
|
||||||
"@fluentui/react-file-type-icons": "^8.8.13",
|
"@fluentui/react-file-type-icons": "^8.8.13",
|
||||||
"@fluentui/react-hooks": "^8.6.20",
|
"@fluentui/react-hooks": "^8.6.20",
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
"@griffel/react": "^1.5.7",
|
"@griffel/react": "^1.5.7",
|
||||||
"@yume-chan/adb": "workspace:^0.0.19",
|
"@yume-chan/adb": "workspace:^0.0.19",
|
||||||
"@yume-chan/adb-backend-direct-sockets": "workspace:^0.0.9",
|
"@yume-chan/adb-backend-direct-sockets": "workspace:^0.0.9",
|
||||||
|
"@yume-chan/adb-backend-proxy": "workspace:^0.0.9",
|
||||||
"@yume-chan/adb-backend-webusb": "workspace:^0.0.19",
|
"@yume-chan/adb-backend-webusb": "workspace:^0.0.19",
|
||||||
"@yume-chan/adb-backend-ws": "workspace:^0.0.9",
|
"@yume-chan/adb-backend-ws": "workspace:^0.0.9",
|
||||||
"@yume-chan/adb-credential-web": "workspace:^0.0.19",
|
"@yume-chan/adb-credential-web": "workspace:^0.0.19",
|
||||||
|
@ -34,18 +36,23 @@
|
||||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
"@yume-chan/stream-saver": "^2.0.6",
|
"@yume-chan/stream-saver": "^2.0.6",
|
||||||
"@yume-chan/struct": "workspace:^0.0.19",
|
"@yume-chan/struct": "workspace:^0.0.19",
|
||||||
|
"@yume-chan/tabby-tango": "workspace:^0.0.19",
|
||||||
"@yume-chan/undici-browser": "5.21.2-mod.9",
|
"@yume-chan/undici-browser": "5.21.2-mod.9",
|
||||||
"fflate": "^0.7.4",
|
"fflate": "^0.7.4",
|
||||||
|
"yaml": "^2.2.1",
|
||||||
"mobx": "^6.7.0",
|
"mobx": "^6.7.0",
|
||||||
"mobx-react-lite": "^3.4.3",
|
"mobx-react-lite": "^3.4.3",
|
||||||
"next": "13.3.0",
|
"next": "13.3.0",
|
||||||
|
"tabby-core": "^1.0.197-nightly.0",
|
||||||
|
"tabby-settings": "^1.0.197-nightly.0",
|
||||||
|
"tabby-terminal": "^1.0.197-nightly.0",
|
||||||
|
"tabby-community-color-schemes": "^1.0.197-nightly.0",
|
||||||
|
"tabby-web": "^1.0.197-nightly.0",
|
||||||
|
"tabby-web-container": "^1.0.197-nightly.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"webm-muxer": "^2.2.3",
|
"rxjs": "^7.8.0",
|
||||||
"xterm": "^5.1.0",
|
"webm-muxer": "^2.2.3"
|
||||||
"xterm-addon-fit": "^0.7.0",
|
|
||||||
"xterm-addon-search": "^0.11.0",
|
|
||||||
"xterm-addon-webgl": "^0.14.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mdx-js/loader": "^2.2.1",
|
"@mdx-js/loader": "^2.2.1",
|
||||||
|
|
|
@ -27,3 +27,27 @@ fs.writeFileSync(
|
||||||
),
|
),
|
||||||
"utf8"
|
"utf8"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
new URL(
|
||||||
|
"../node_modules/tabby-web-container/dist/preload.mjs",
|
||||||
|
import.meta.url
|
||||||
|
),
|
||||||
|
"export {};\n" +
|
||||||
|
fs
|
||||||
|
.readFileSync(
|
||||||
|
new URL(
|
||||||
|
"../node_modules/tabby-web-container/dist/preload.js",
|
||||||
|
import.meta.url
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
.replaceAll(/__webpack_require__\.p \+ "(.+)"/g, (_, match) => {
|
||||||
|
return `new URL("./${match}", import.meta.url).toString()`;
|
||||||
|
})
|
||||||
|
.replaceAll(/__webpack_require__/g, "__webpack_require_nested__")
|
||||||
|
.replace(
|
||||||
|
"var scriptUrl;",
|
||||||
|
"var scriptUrl = import.meta.url.toString();"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const PublicFolder = path.resolve(__dirname, '..', 'public');
|
|
||||||
|
|
||||||
const SourceFolder = path.dirname(require.resolve('streamsaver'));
|
|
||||||
|
|
||||||
const DistFolder = path.resolve(PublicFolder, 'StreamSaver');
|
|
||||||
|
|
||||||
if (!fs.existsSync(DistFolder)) {
|
|
||||||
fs.mkdirSync(DistFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyFile(name) {
|
|
||||||
fs.copyFileSync(path.resolve(SourceFolder, name), path.resolve(DistFolder, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
copyFile('mitm.html');
|
|
||||||
copyFile('sw.js');
|
|
|
@ -1,11 +1,12 @@
|
||||||
export * from './command-bar';
|
export * from "./command-bar";
|
||||||
export * from './connect';
|
export * from "./connect";
|
||||||
export * from './demo-mode-panel';
|
export * from "./demo-mode-panel";
|
||||||
export * from './device-view';
|
export * from "./device-view";
|
||||||
export * from './error-dialog';
|
export * from "./error-dialog";
|
||||||
export * from './external-link';
|
export * from "./external-link";
|
||||||
export * from './grid';
|
export * from "./grid";
|
||||||
export * from './hex-viewer';
|
export * from "./hex-viewer";
|
||||||
export * from "./list-selection";
|
export * from "./list-selection";
|
||||||
export * from './log-view';
|
export * from "./log-view";
|
||||||
export * from './resize-observer';
|
export * from "./resize-observer";
|
||||||
|
export * from "./tabby-frame-manager";
|
||||||
|
|
80
apps/demo/src/components/tabby-frame-manager.tsx
Normal file
80
apps/demo/src/components/tabby-frame-manager.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { AdbProxyServer } from "@yume-chan/adb-backend-proxy";
|
||||||
|
import { autorun } from "mobx";
|
||||||
|
import getConfig from "next/config";
|
||||||
|
import { GLOBAL_STATE } from "../state";
|
||||||
|
|
||||||
|
let proxy: AdbProxyServer | undefined;
|
||||||
|
let resizeObserver: ResizeObserver | undefined;
|
||||||
|
let frame: HTMLIFrameElement | undefined;
|
||||||
|
|
||||||
|
function syncDevice() {
|
||||||
|
if (proxy) {
|
||||||
|
proxy.dispose();
|
||||||
|
proxy = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GLOBAL_STATE.device && frame) {
|
||||||
|
const proxy = new AdbProxyServer(GLOBAL_STATE.device);
|
||||||
|
const info = proxy.createPort();
|
||||||
|
frame.contentWindow?.postMessage(
|
||||||
|
{
|
||||||
|
type: "adb",
|
||||||
|
...info,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
[info.port]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachTabbyFrame(container: HTMLDivElement | null) {
|
||||||
|
if (container === null) {
|
||||||
|
if (resizeObserver !== undefined) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
if (frame !== undefined) {
|
||||||
|
frame.style.visibility = "hidden";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frame) {
|
||||||
|
const {
|
||||||
|
publicRuntimeConfig: { basePath },
|
||||||
|
} = getConfig();
|
||||||
|
|
||||||
|
frame = document.createElement("iframe");
|
||||||
|
frame.src = `${basePath}/tabby-frame`;
|
||||||
|
frame.style.display = "block";
|
||||||
|
frame.style.position = "fixed";
|
||||||
|
frame.style.border = "none";
|
||||||
|
document.body.appendChild(frame);
|
||||||
|
|
||||||
|
window.addEventListener("message", (e) => {
|
||||||
|
// Wait for Tabby to be ready
|
||||||
|
if (e.source === frame?.contentWindow && e.data === "adb") {
|
||||||
|
syncDevice();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync device when it's changed
|
||||||
|
autorun(syncDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because re-parent an iframe will cause it to reload,
|
||||||
|
// use visibility to show/hide it
|
||||||
|
// and use a ResizeObserver to put it in the right place.
|
||||||
|
frame.style.visibility = "visible";
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
const { top, left, width, height } = container.getBoundingClientRect();
|
||||||
|
if (width === 0 || height === 0) {
|
||||||
|
// zero size makes xterm.js wrap lines incorrectly
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frame!.style.top = `${top}px`;
|
||||||
|
frame!.style.left = `${left}px`;
|
||||||
|
frame!.style.width = `${width}px`;
|
||||||
|
frame!.style.height = `${height}px`;
|
||||||
|
});
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
}
|
|
@ -1,112 +0,0 @@
|
||||||
// cspell: ignore scrollback
|
|
||||||
|
|
||||||
import { AdbSubprocessProtocol, encodeUtf8 } from "@yume-chan/adb";
|
|
||||||
import { AutoDisposable } from "@yume-chan/event";
|
|
||||||
import {
|
|
||||||
AbortController,
|
|
||||||
Consumable,
|
|
||||||
WritableStream,
|
|
||||||
} from "@yume-chan/stream-extra";
|
|
||||||
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({
|
|
||||||
allowProposedApi: true,
|
|
||||||
allowTransparency: true,
|
|
||||||
cursorStyle: "bar",
|
|
||||||
cursorBlink: true,
|
|
||||||
fontFamily:
|
|
||||||
'"Cascadia Code", Consolas, monospace, "Source Han Sans SC", "Microsoft YaHei"',
|
|
||||||
letterSpacing: 1,
|
|
||||||
scrollback: 9000,
|
|
||||||
smoothScrollDuration: 50,
|
|
||||||
overviewRulerWidth: 20,
|
|
||||||
});
|
|
||||||
|
|
||||||
public searchAddon = new SearchAddon();
|
|
||||||
|
|
||||||
private readonly fitAddon = new FitAddon();
|
|
||||||
|
|
||||||
private _socket: AdbSubprocessProtocol | undefined;
|
|
||||||
private _socketAbortController: AbortController | undefined;
|
|
||||||
public get socket() {
|
|
||||||
return this._socket;
|
|
||||||
}
|
|
||||||
public set socket(value) {
|
|
||||||
if (this._socket) {
|
|
||||||
// Remove event listeners
|
|
||||||
this.dispose();
|
|
||||||
this._socketAbortController?.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._socket = value;
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
this.terminal.clear();
|
|
||||||
this.terminal.reset();
|
|
||||||
|
|
||||||
this._socketAbortController = new AbortController();
|
|
||||||
|
|
||||||
// pty mode only has one stream
|
|
||||||
value.stdout
|
|
||||||
.pipeTo(
|
|
||||||
new WritableStream<Uint8Array>({
|
|
||||||
write: (chunk) => {
|
|
||||||
this.terminal.write(chunk);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
signal: this._socketAbortController.signal,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.catch(() => {});
|
|
||||||
|
|
||||||
const _writer = value.stdin.getWriter();
|
|
||||||
this.addDisposable(
|
|
||||||
this.terminal.onData((data) => {
|
|
||||||
const buffer = encodeUtf8(data);
|
|
||||||
const output = new Consumable(buffer);
|
|
||||||
_writer.write(output);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.fit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.element.style.width = "100%";
|
|
||||||
this.element.style.height = "100%";
|
|
||||||
this.element.style.overflow = "hidden";
|
|
||||||
|
|
||||||
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);
|
|
||||||
// WebGL addon requires terminal to be attached to DOM
|
|
||||||
this.terminal.loadAddon(new WebglAddon());
|
|
||||||
// WebGL renderer ignores `cursorBlink` set before it initialized
|
|
||||||
this.terminal.options.cursorBlink = true;
|
|
||||||
this.fit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fit() {
|
|
||||||
this.fitAddon.fit();
|
|
||||||
// Resize remote terminal
|
|
||||||
const { rows, cols } = this.terminal;
|
|
||||||
this._socket?.resize(rows, cols);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -105,6 +105,7 @@ function ChromeDevToolsFrame() {
|
||||||
document.body.appendChild(script);
|
document.body.appendChild(script);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// DevTools will set `document.title` to debugged page's title.
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className={classes.body}
|
className={classes.body}
|
||||||
|
@ -117,5 +118,7 @@ function ChromeDevToolsFrame() {
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ChromeDevToolsFrame.noLayout = true;
|
ChromeDevToolsFrame.noLayout = true;
|
||||||
|
|
||||||
export default ChromeDevToolsFrame;
|
export default ChromeDevToolsFrame;
|
||||||
|
|
|
@ -1,192 +1,40 @@
|
||||||
import { IconButton, SearchBox, Stack, StackItem } from "@fluentui/react";
|
import { makeStyles } from "@griffel/react";
|
||||||
import { makeStyles, shorthands } from "@griffel/react";
|
|
||||||
import { action, autorun, makeAutoObservable, runInAction } from "mobx";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback } from "react";
|
||||||
import { ISearchOptions } from "xterm-addon-search";
|
import { attachTabbyFrame } from "../components";
|
||||||
import "xterm/css/xterm.css";
|
|
||||||
import { ResizeObserver } from "../components";
|
|
||||||
import { GLOBAL_STATE } from "../state";
|
|
||||||
import { Icons, RouteStackProps } from "../utils";
|
|
||||||
|
|
||||||
const useClasses = makeStyles({
|
const useClasses = makeStyles({
|
||||||
count: {
|
container: {
|
||||||
...shorthands.padding("0", "8px"),
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let terminal: import("../components/terminal").AdbTerminal | undefined;
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const { AdbTerminal } = require("../components/terminal");
|
|
||||||
terminal = new AdbTerminal();
|
|
||||||
}
|
|
||||||
|
|
||||||
const SEARCH_OPTIONS: ISearchOptions = {
|
|
||||||
decorations: {
|
|
||||||
matchBackground: "#42557b",
|
|
||||||
matchOverviewRuler: "#d18616",
|
|
||||||
activeMatchBackground: "#6199ff2f",
|
|
||||||
activeMatchColorOverviewRuler: "#d186167e",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = makeAutoObservable(
|
|
||||||
{
|
|
||||||
visible: false,
|
|
||||||
index: undefined as number | undefined,
|
|
||||||
count: undefined as number | undefined,
|
|
||||||
setVisible(value: boolean) {
|
|
||||||
this.visible = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
searchKeyword: "",
|
|
||||||
setSearchKeyword(value: string) {
|
|
||||||
this.searchKeyword = value;
|
|
||||||
terminal!.searchAddon.findNext(value, {
|
|
||||||
...SEARCH_OPTIONS,
|
|
||||||
incremental: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
searchPrevious() {
|
|
||||||
terminal!.searchAddon.findPrevious(
|
|
||||||
this.searchKeyword,
|
|
||||||
SEARCH_OPTIONS
|
|
||||||
);
|
|
||||||
},
|
|
||||||
searchNext() {
|
|
||||||
terminal!.searchAddon.findNext(this.searchKeyword, SEARCH_OPTIONS);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
searchPrevious: action.bound,
|
|
||||||
searchNext: action.bound,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (terminal) {
|
|
||||||
terminal.searchAddon.onDidChangeResults((e) => {
|
|
||||||
console.log(e);
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
if (e) {
|
|
||||||
state.index = e.resultIndex;
|
|
||||||
state.count = e.resultCount;
|
|
||||||
} else {
|
|
||||||
state.index = undefined;
|
|
||||||
state.count = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
autorun(() => {
|
|
||||||
if (!terminal) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GLOBAL_STATE.device) {
|
|
||||||
terminal.socket = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!terminal.socket && state.visible) {
|
|
||||||
GLOBAL_STATE.device.subprocess.shell().then(
|
|
||||||
action((shell) => {
|
|
||||||
terminal!.socket = shell;
|
|
||||||
}),
|
|
||||||
(e) => {
|
|
||||||
GLOBAL_STATE.showErrorDialog(e);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const UpIconProps = { iconName: Icons.ChevronUp };
|
|
||||||
const DownIconProps = { iconName: Icons.ChevronDown };
|
|
||||||
|
|
||||||
const Shell: NextPage = (): JSX.Element | null => {
|
const Shell: NextPage = (): JSX.Element | null => {
|
||||||
const classes = useClasses();
|
const classes = useClasses();
|
||||||
|
|
||||||
const handleSearchKeywordChange = useCallback(
|
|
||||||
(e: unknown, value?: string) => {
|
|
||||||
state.setSearchKeyword(value ?? "");
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleResize = useCallback(() => {
|
|
||||||
terminal!.fit();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleContainerRef = useCallback(
|
const handleContainerRef = useCallback(
|
||||||
(container: HTMLDivElement | null) => {
|
(container: HTMLDivElement | null) => {
|
||||||
if (container) {
|
// invoke it with `null` to hide the iframe
|
||||||
terminal!.setContainer(container);
|
attachTabbyFrame(container);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
state.setVisible(true);
|
|
||||||
return () => {
|
|
||||||
state.setVisible(false);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack {...RouteStackProps}>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Interactive Shell - Tango</title>
|
<title>Interactive Shell - Tango</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<StackItem>
|
<div ref={handleContainerRef} className={classes.container}>
|
||||||
<Stack horizontal>
|
<div>Loading Tabby...</div>
|
||||||
<StackItem grow>
|
</div>
|
||||||
<SearchBox
|
</>
|
||||||
placeholder="Find"
|
|
||||||
value={state.searchKeyword}
|
|
||||||
onChange={handleSearchKeywordChange}
|
|
||||||
onSearch={state.searchNext}
|
|
||||||
/>
|
|
||||||
</StackItem>
|
|
||||||
{state.count === 0 ? (
|
|
||||||
<StackItem className={classes.count} align="center">
|
|
||||||
No results
|
|
||||||
</StackItem>
|
|
||||||
) : state.count !== undefined ? (
|
|
||||||
<StackItem className={classes.count} align="center">
|
|
||||||
{state.index! + 1} of {state.count}
|
|
||||||
</StackItem>
|
|
||||||
) : null}
|
|
||||||
<StackItem>
|
|
||||||
<IconButton
|
|
||||||
disabled={!state.searchKeyword}
|
|
||||||
iconProps={UpIconProps}
|
|
||||||
onClick={state.searchPrevious}
|
|
||||||
/>
|
|
||||||
</StackItem>
|
|
||||||
<StackItem>
|
|
||||||
<IconButton
|
|
||||||
disabled={!state.searchKeyword}
|
|
||||||
iconProps={DownIconProps}
|
|
||||||
onClick={state.searchNext}
|
|
||||||
/>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
</StackItem>
|
|
||||||
|
|
||||||
<StackItem
|
|
||||||
grow
|
|
||||||
styles={{ root: { position: "relative", minHeight: 0 } }}
|
|
||||||
>
|
|
||||||
<ResizeObserver onResize={handleResize} />
|
|
||||||
<div ref={handleContainerRef} style={{ height: "100%" }} />
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
235
apps/demo/src/pages/tabby-frame.tsx
Normal file
235
apps/demo/src/pages/tabby-frame.tsx
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
import { Adb } from "@yume-chan/adb";
|
||||||
|
import {
|
||||||
|
AdbProxyBackend,
|
||||||
|
AdbProxyServerInfo,
|
||||||
|
} from "@yume-chan/adb-backend-proxy";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
import Yaml from "yaml";
|
||||||
|
|
||||||
|
export class NotImplementedSocket {
|
||||||
|
connect$ = new Subject<void>();
|
||||||
|
data$ = new Subject<Buffer>();
|
||||||
|
error$ = new Subject<Error>();
|
||||||
|
close$ = new Subject<Buffer>();
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
this.error$.next(new Error("Socket is not implemented in Web"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage of connector is scattered around the Tabby codebase,
|
||||||
|
// but these is all methods that are required.
|
||||||
|
class WebConnector {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
async loadConfig(): Promise<string> {
|
||||||
|
let config;
|
||||||
|
|
||||||
|
const text = localStorage.getItem("tabby-config");
|
||||||
|
if (text) {
|
||||||
|
config = Yaml.parse(text);
|
||||||
|
} else {
|
||||||
|
config = {
|
||||||
|
recoverTabs: false,
|
||||||
|
web: {
|
||||||
|
preventAccidentalTabClosure: false,
|
||||||
|
},
|
||||||
|
terminal: {
|
||||||
|
fontSize: 11,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
config.providerBlacklist = [
|
||||||
|
...(config.providerBlacklist ?? []),
|
||||||
|
"settings:ProfilesSettingsTabProvider",
|
||||||
|
];
|
||||||
|
config.commandBlacklist = [
|
||||||
|
...(config.commandBlacklist ?? []),
|
||||||
|
"core:profile-selector",
|
||||||
|
];
|
||||||
|
|
||||||
|
return Yaml.stringify(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveConfig(content: string): Promise<void> {
|
||||||
|
localStorage.setItem("tabby-config", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAppVersion(): string {
|
||||||
|
return "1.0.197-nightly.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
createSocket() {
|
||||||
|
return new NotImplementedSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabbyWeb {
|
||||||
|
registerPluginModule(packageName: string, exports: unknown): void;
|
||||||
|
bootstrap(options: unknown): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabbyPluginModule {
|
||||||
|
pluginName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalExtension {
|
||||||
|
__connector__: WebConnector | undefined;
|
||||||
|
module: any;
|
||||||
|
exports: any;
|
||||||
|
Tabby: TabbyWeb;
|
||||||
|
pluginModules: TabbyPluginModule[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalExtension = globalThis as unknown as GlobalExtension;
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
const connector = new WebConnector();
|
||||||
|
globalExtension["__connector__"] = connector;
|
||||||
|
|
||||||
|
await import("@angular/compiler");
|
||||||
|
// @ts-expect-error
|
||||||
|
await import("tabby-web-container/dist/preload.mjs");
|
||||||
|
// @ts-expect-error
|
||||||
|
await import("tabby-web-container/dist/bundle.js");
|
||||||
|
|
||||||
|
async function webRequire(url: string | URL) {
|
||||||
|
if (typeof url === "object") {
|
||||||
|
url = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Loading ${url}`);
|
||||||
|
const e = document.createElement("script");
|
||||||
|
globalExtension["module"] = { exports: {} };
|
||||||
|
globalExtension["exports"] = globalExtension["module"].exports;
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
e.onload = resolve;
|
||||||
|
e.src = url as string;
|
||||||
|
document.head.appendChild(e);
|
||||||
|
});
|
||||||
|
return globalExtension["module"].exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prefetchURL(url: string | URL) {
|
||||||
|
await (await fetch(url)).text();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabby = globalExtension.Tabby;
|
||||||
|
|
||||||
|
const pluginInfos: {
|
||||||
|
pluginName: string;
|
||||||
|
packageName: string;
|
||||||
|
url: URL;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
pluginName: "core",
|
||||||
|
packageName: "tabby-core",
|
||||||
|
url: new URL("tabby-core/dist/index.js", import.meta.url),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginName: "settings",
|
||||||
|
packageName: "tabby-settings",
|
||||||
|
url: new URL("tabby-settings/dist/index.js", import.meta.url),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginName: "terminal",
|
||||||
|
packageName: "tabby-terminal",
|
||||||
|
url: new URL("tabby-terminal/dist/index.js", import.meta.url),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginName: "community-color-schemes",
|
||||||
|
packageName: "tabby-community-color-schemes",
|
||||||
|
url: new URL(
|
||||||
|
"tabby-community-color-schemes/dist/index.js",
|
||||||
|
import.meta.url
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginName: "web",
|
||||||
|
packageName: "tabby-web",
|
||||||
|
url: new URL("tabby-web/dist/index.js", import.meta.url),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
pluginInfos.map((pluginInfo) => prefetchURL(pluginInfo.url))
|
||||||
|
);
|
||||||
|
|
||||||
|
const pluginModules = [];
|
||||||
|
for (const info of pluginInfos) {
|
||||||
|
const result = await webRequire(info.url);
|
||||||
|
result.pluginName = info.pluginName;
|
||||||
|
tabby.registerPluginModule(info.packageName, result);
|
||||||
|
pluginModules.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabbyTango = await webRequire(
|
||||||
|
new URL("@yume-chan/tabby-tango/dist/index.js", import.meta.url)
|
||||||
|
);
|
||||||
|
TabbyTango.pluginName = "tango";
|
||||||
|
|
||||||
|
window.addEventListener("message", (e) => {
|
||||||
|
if ("type" in e.data && e.data.type === "adb") {
|
||||||
|
const { port, version, maxPayloadSize, banner } =
|
||||||
|
e.data as AdbProxyServerInfo;
|
||||||
|
const backend = new AdbProxyBackend(port);
|
||||||
|
const connection = backend.connect();
|
||||||
|
TabbyTango.AdbState.value = new Adb(
|
||||||
|
connection,
|
||||||
|
version,
|
||||||
|
maxPayloadSize,
|
||||||
|
banner
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.parent.postMessage("adb", "*");
|
||||||
|
|
||||||
|
pluginModules.push(TabbyTango);
|
||||||
|
|
||||||
|
globalExtension["pluginModules"] = pluginModules.map((plugin) => {
|
||||||
|
if ("default" in plugin) {
|
||||||
|
plugin.default.pluginName = plugin.pluginName;
|
||||||
|
return plugin.default;
|
||||||
|
}
|
||||||
|
return plugin;
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = connector.loadConfig();
|
||||||
|
await tabby.bootstrap({
|
||||||
|
packageModules: pluginModules,
|
||||||
|
bootstrapData: {
|
||||||
|
config,
|
||||||
|
executable: "web",
|
||||||
|
isFirstWindow: true,
|
||||||
|
windowID: 1,
|
||||||
|
installedPlugins: [],
|
||||||
|
userPluginsPath: "/",
|
||||||
|
},
|
||||||
|
debugMode: false,
|
||||||
|
connector,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabbyFrame() {
|
||||||
|
useEffect(() => {
|
||||||
|
// Only run at client side.
|
||||||
|
start().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<style id="custom-css" />
|
||||||
|
|
||||||
|
{/* @ts-expect-error */}
|
||||||
|
<app-root />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TabbyFrame.noLayout = true;
|
||||||
|
|
||||||
|
export default TabbyFrame;
|
1055
common/config/rush/pnpm-lock.yaml
generated
1055
common/config/rush/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
||||||
{
|
{
|
||||||
"pnpmShrinkwrapHash": "e870870fc8dda7ac7abb3a0c8dcebb664c086262",
|
"pnpmShrinkwrapHash": "197ab75f0fe12b6ece795fc8734be482ac57e9aa",
|
||||||
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yume-chan/adb": "workspace:^0.0.19",
|
"@yume-chan/adb": "workspace:^0.0.19",
|
||||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||||
|
|
11
libraries/adb-backend-proxy/.eslintrc.cjs
Normal file
11
libraries/adb-backend-proxy/.eslintrc.cjs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module.exports = {
|
||||||
|
"extends": [
|
||||||
|
"@yume-chan"
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: [
|
||||||
|
"./tsconfig.build.json"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
16
libraries/adb-backend-proxy/.npmignore
Normal file
16
libraries/adb-backend-proxy/.npmignore
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.rush
|
||||||
|
|
||||||
|
# Test
|
||||||
|
coverage
|
||||||
|
**/*.spec.ts
|
||||||
|
**/*.spec.js
|
||||||
|
**/*.spec.js.map
|
||||||
|
**/__helpers__
|
||||||
|
jest.config.js
|
||||||
|
|
||||||
|
.eslintrc.cjs
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.test.json
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
5
libraries/adb-backend-proxy/README.md
Normal file
5
libraries/adb-backend-proxy/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# `@yume-chan/adb-backend-proxy`
|
||||||
|
|
||||||
|
Proxies streams to another `Adb` instance, so multiple clients can access the same device.
|
||||||
|
|
||||||
|
Prototype of `adb-backend-server` to support connecting to ADB servers.
|
49
libraries/adb-backend-proxy/package.json
Normal file
49
libraries/adb-backend-proxy/package.json
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "@yume-chan/adb-backend-proxy",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.9",
|
||||||
|
"description": "Backend for `@yume-chan/adb` by proxy sockets to another `Adb` instance.",
|
||||||
|
"keywords": [
|
||||||
|
"adb",
|
||||||
|
"proxy"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "Simon Chan",
|
||||||
|
"email": "cnsimonchan@live.com",
|
||||||
|
"url": "https://chensi.moe/blog"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/yume-chan/ya-webadb/tree/main/libraries/adb-backend-proxy#readme",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/yume-chan/ya-webadb.git",
|
||||||
|
"directory": "libraries/adb-backend-proxy"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"main": "esm/index.js",
|
||||||
|
"types": "esm/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -b tsconfig.build.json",
|
||||||
|
"build:watch": "tsc -b tsconfig.build.json",
|
||||||
|
"lint": "eslint src/**/*.ts --fix && prettier src/**/*.ts --write --tab-width 4",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@yume-chan/adb": "workspace:^0.0.19",
|
||||||
|
"@yume-chan/async": "^2.2.0",
|
||||||
|
"@yume-chan/event": "workspace:^0.0.19",
|
||||||
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
|
"@yume-chan/struct": "workspace:^0.0.19",
|
||||||
|
"tslib": "^2.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||||
|
"@yume-chan/tsconfig": "workspace:^1.0.0",
|
||||||
|
"eslint": "^8.36.0",
|
||||||
|
"prettier": "^2.8.4",
|
||||||
|
"typescript": "^5.0.3"
|
||||||
|
}
|
||||||
|
}
|
104
libraries/adb-backend-proxy/src/client.ts
Normal file
104
libraries/adb-backend-proxy/src/client.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import type { AdbBackend, AdbPacketData, AdbPacketInit } from "@yume-chan/adb";
|
||||||
|
import { AdbCommand } from "@yume-chan/adb";
|
||||||
|
import type { Consumable, ReadableWritablePair } from "@yume-chan/stream-extra";
|
||||||
|
import {
|
||||||
|
DuplexStreamFactory,
|
||||||
|
ReadableStream,
|
||||||
|
WritableStream,
|
||||||
|
} from "@yume-chan/stream-extra";
|
||||||
|
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
|
||||||
|
|
||||||
|
class AdbProxyConnection
|
||||||
|
implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
|
||||||
|
{
|
||||||
|
public readonly readable: ReadableStream<AdbPacketData>;
|
||||||
|
public readonly writable: WritableStream<Consumable<AdbPacketInit>>;
|
||||||
|
|
||||||
|
public constructor(port: MessagePort) {
|
||||||
|
const duplex = new DuplexStreamFactory<
|
||||||
|
AdbPacketData,
|
||||||
|
Consumable<AdbPacketInit>
|
||||||
|
>({
|
||||||
|
close: () => {
|
||||||
|
// CNXN means disconnected
|
||||||
|
port.postMessage({
|
||||||
|
command: AdbCommand.Connect,
|
||||||
|
arg0: 0,
|
||||||
|
arg1: 0,
|
||||||
|
payload: EMPTY_UINT8_ARRAY,
|
||||||
|
} satisfies AdbPacketData);
|
||||||
|
port.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.readable = duplex.wrapReadable(
|
||||||
|
new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
port.onmessage = (e) => {
|
||||||
|
const data = e.data as AdbPacketData;
|
||||||
|
switch (data.command) {
|
||||||
|
case AdbCommand.Auth:
|
||||||
|
case AdbCommand.Connect:
|
||||||
|
controller.close();
|
||||||
|
break;
|
||||||
|
case AdbCommand.Open:
|
||||||
|
case AdbCommand.Close:
|
||||||
|
case AdbCommand.OK:
|
||||||
|
case AdbCommand.Write:
|
||||||
|
controller.enqueue(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.writable = duplex.createWritable(
|
||||||
|
new WritableStream({
|
||||||
|
write(chunk) {
|
||||||
|
switch (chunk.value.command) {
|
||||||
|
case AdbCommand.Auth:
|
||||||
|
throw new Error(
|
||||||
|
`Can't send AUTH packet through proxy`
|
||||||
|
);
|
||||||
|
case AdbCommand.Connect:
|
||||||
|
throw new Error(
|
||||||
|
`Can't send CNXN packet through proxy`
|
||||||
|
);
|
||||||
|
case AdbCommand.Open:
|
||||||
|
case AdbCommand.Close:
|
||||||
|
case AdbCommand.OK:
|
||||||
|
case AdbCommand.Write: {
|
||||||
|
// Can't transfer `chunk.payload`, will break clients
|
||||||
|
port.postMessage(chunk.value);
|
||||||
|
chunk.consume();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdbProxyBackend implements AdbBackend {
|
||||||
|
public static isSupported(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly port: MessagePort;
|
||||||
|
|
||||||
|
public readonly serial: string;
|
||||||
|
|
||||||
|
public name: string | undefined;
|
||||||
|
|
||||||
|
public constructor(port: MessagePort, name?: string) {
|
||||||
|
this.port = port;
|
||||||
|
this.serial = `proxy`;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public connect() {
|
||||||
|
return new AdbProxyConnection(this.port);
|
||||||
|
}
|
||||||
|
}
|
2
libraries/adb-backend-proxy/src/index.ts
Normal file
2
libraries/adb-backend-proxy/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./client.js";
|
||||||
|
export * from "./server.js";
|
267
libraries/adb-backend-proxy/src/server.ts
Normal file
267
libraries/adb-backend-proxy/src/server.ts
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
import type { Adb, AdbPacketData, AdbSocket } from "@yume-chan/adb";
|
||||||
|
import { AdbCommand } from "@yume-chan/adb";
|
||||||
|
import { PromiseResolver } from "@yume-chan/async";
|
||||||
|
import { AutoDisposable, EventEmitter } from "@yume-chan/event";
|
||||||
|
import type {
|
||||||
|
Consumable,
|
||||||
|
WritableStreamDefaultWriter,
|
||||||
|
} from "@yume-chan/stream-extra";
|
||||||
|
import {
|
||||||
|
ConsumableWritableStream,
|
||||||
|
WritableStream,
|
||||||
|
} from "@yume-chan/stream-extra";
|
||||||
|
import { EMPTY_UINT8_ARRAY, decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
|
||||||
|
|
||||||
|
const NOOP = () => {
|
||||||
|
// no-op
|
||||||
|
};
|
||||||
|
|
||||||
|
class AdbProxyServerSocket {
|
||||||
|
public handle: AdbProxyServerConnection;
|
||||||
|
public localId: number;
|
||||||
|
public pendingRead?: PromiseResolver<void>;
|
||||||
|
|
||||||
|
private _writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
handle: AdbProxyServerConnection,
|
||||||
|
socket: AdbSocket,
|
||||||
|
localId: number
|
||||||
|
) {
|
||||||
|
this.handle = handle;
|
||||||
|
this.localId = localId;
|
||||||
|
this._writer = socket.writable.getWriter();
|
||||||
|
|
||||||
|
socket.readable
|
||||||
|
.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write: this.handleData.bind(this),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.finally(this.handleClose.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle data from server
|
||||||
|
*/
|
||||||
|
private async handleData(payload: Uint8Array) {
|
||||||
|
const promiseResolver = new PromiseResolver<void>();
|
||||||
|
this.pendingRead = promiseResolver;
|
||||||
|
this.handle.postMessage(AdbCommand.Write, 1, this.localId, payload);
|
||||||
|
await promiseResolver.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle close from server
|
||||||
|
*/
|
||||||
|
private handleClose() {
|
||||||
|
this.handle.postMessage(AdbCommand.Close, 1, this.localId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write data to server
|
||||||
|
*/
|
||||||
|
public async write(payload: Uint8Array) {
|
||||||
|
await ConsumableWritableStream.write(this._writer, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send close to server
|
||||||
|
*/
|
||||||
|
public async close() {
|
||||||
|
await this._writer.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdbProxyServerInfo {
|
||||||
|
port: MessagePort;
|
||||||
|
version: number;
|
||||||
|
maxPayloadSize: number;
|
||||||
|
banner: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdbProxyServerConnection {
|
||||||
|
public adb: Adb;
|
||||||
|
public port: MessagePort;
|
||||||
|
|
||||||
|
private _closed = new EventEmitter<void>();
|
||||||
|
public get closed() {
|
||||||
|
return this._closed.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _pendingSockets = new Map<number, PromiseResolver<boolean>>();
|
||||||
|
private _sockets = new Map<number, AdbProxyServerSocket>();
|
||||||
|
|
||||||
|
public constructor(adb: Adb, port: MessagePort) {
|
||||||
|
this.adb = adb;
|
||||||
|
this.port = port;
|
||||||
|
port.onmessage = this.handleMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle messages from client
|
||||||
|
*/
|
||||||
|
private async handleMessage(e: MessageEvent) {
|
||||||
|
const data = e.data as AdbPacketData;
|
||||||
|
switch (data.command) {
|
||||||
|
case AdbCommand.Auth:
|
||||||
|
throw new Error(`Can't send AUTH packet through proxy`);
|
||||||
|
case AdbCommand.Connect:
|
||||||
|
throw new Error(`Can't send CNXN packet through proxy`);
|
||||||
|
case AdbCommand.Open:
|
||||||
|
await this.handleOpen(data);
|
||||||
|
break;
|
||||||
|
case AdbCommand.Write: {
|
||||||
|
const socket = this._sockets.get(data.arg0);
|
||||||
|
if (!socket) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await socket.write(data.payload);
|
||||||
|
this.postMessage(AdbCommand.OK, 1, data.arg0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AdbCommand.Close: {
|
||||||
|
if (data.arg0 === 0) {
|
||||||
|
this._pendingSockets.get(data.arg1)?.resolve(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const socket = this._sockets.get(data.arg0);
|
||||||
|
if (!socket) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await socket.close();
|
||||||
|
this._sockets.delete(data.arg0);
|
||||||
|
|
||||||
|
this.postMessage(AdbCommand.Close, 1, data.arg0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AdbCommand.OK: {
|
||||||
|
const socket = this._sockets.get(data.arg1);
|
||||||
|
if (!socket) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
socket.pendingRead?.resolve();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle open from client
|
||||||
|
*/
|
||||||
|
private async handleOpen(data: AdbPacketData) {
|
||||||
|
if (data.arg1 !== 0) {
|
||||||
|
this._pendingSockets.get(data.arg1)?.resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localId = data.arg0;
|
||||||
|
try {
|
||||||
|
const service = decodeUtf8(data.payload);
|
||||||
|
const socket = await this.adb.createSocket(service);
|
||||||
|
this._sockets.set(
|
||||||
|
localId,
|
||||||
|
new AdbProxyServerSocket(this, socket, localId)
|
||||||
|
);
|
||||||
|
// remoteId doesn't matter
|
||||||
|
this.postMessage(AdbCommand.OK, 1, localId);
|
||||||
|
} catch {
|
||||||
|
this.postMessage(AdbCommand.Close, 0, localId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message to client
|
||||||
|
*/
|
||||||
|
public postMessage(
|
||||||
|
command: AdbCommand,
|
||||||
|
arg0: number,
|
||||||
|
arg1: number,
|
||||||
|
payload = EMPTY_UINT8_ARRAY
|
||||||
|
) {
|
||||||
|
this.port.postMessage({
|
||||||
|
command,
|
||||||
|
arg0,
|
||||||
|
arg1,
|
||||||
|
payload,
|
||||||
|
} satisfies AdbPacketData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for client to accept the incoming socket
|
||||||
|
*/
|
||||||
|
public async waitOpenResult(remoteId: number) {
|
||||||
|
const promiseResolver = new PromiseResolver<boolean>();
|
||||||
|
this._pendingSockets.set(remoteId, promiseResolver);
|
||||||
|
return promiseResolver.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the connection to both client and server
|
||||||
|
*/
|
||||||
|
public async close(): Promise<void> {
|
||||||
|
for (const socket of this._sockets.values()) {
|
||||||
|
await socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.postMessage(AdbCommand.Connect, 0, 0);
|
||||||
|
this.port.close();
|
||||||
|
|
||||||
|
this._closed.fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdbProxyServer extends AutoDisposable {
|
||||||
|
private _adb: Adb;
|
||||||
|
|
||||||
|
private _ports: Set<AdbProxyServerConnection> = new Set();
|
||||||
|
|
||||||
|
constructor(adb: Adb) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._adb = adb;
|
||||||
|
|
||||||
|
this.addDisposable(
|
||||||
|
this._adb.onIncomingSocket(async (socket) => {
|
||||||
|
for (const port of this._ports) {
|
||||||
|
port.postMessage(
|
||||||
|
AdbCommand.Open,
|
||||||
|
0,
|
||||||
|
socket.remoteId,
|
||||||
|
encodeUtf8(socket.serviceString)
|
||||||
|
);
|
||||||
|
if (await port.waitOpenResult(socket.remoteId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._adb.disconnected.then(async () => {
|
||||||
|
for (const port of this._ports) {
|
||||||
|
// CNXN means disconnected
|
||||||
|
port.postMessage(AdbCommand.Connect, 0, 0);
|
||||||
|
await port.close();
|
||||||
|
}
|
||||||
|
}, NOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a port that can be used to create a client
|
||||||
|
*/
|
||||||
|
public createPort(): AdbProxyServerInfo {
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
const port = new AdbProxyServerConnection(this._adb, channel.port1);
|
||||||
|
this._ports.add(port);
|
||||||
|
port.closed(() => {
|
||||||
|
this._ports.delete(port);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
port: channel.port2,
|
||||||
|
version: this._adb.protocolVersion,
|
||||||
|
maxPayloadSize: this._adb.maxPayloadSize,
|
||||||
|
banner: this._adb.banner,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
9
libraries/adb-backend-proxy/tsconfig.build.json
Normal file
9
libraries/adb-backend-proxy/tsconfig.build.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
19
libraries/adb-backend-proxy/tsconfig.json
Normal file
19
libraries/adb-backend-proxy/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../adb/tsconfig.build.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../event/tsconfig.build.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../stream-extra/tsconfig.build.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../struct/tsconfig.build.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.build.json"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
|
@ -35,7 +35,7 @@
|
||||||
"@yume-chan/adb": "workspace:^0.0.19",
|
"@yume-chan/adb": "workspace:^0.0.19",
|
||||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
"@yume-chan/struct": "workspace:^0.0.19",
|
"@yume-chan/struct": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yume-chan/adb": "workspace:^0.0.19",
|
"@yume-chan/adb": "workspace:^0.0.19",
|
||||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yume-chan/adb": "workspace:^0.0.19",
|
"@yume-chan/adb": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"@yume-chan/scrcpy": "workspace:^0.0.19",
|
"@yume-chan/scrcpy": "workspace:^0.0.19",
|
||||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
"@yume-chan/struct": "workspace:^0.0.19",
|
"@yume-chan/struct": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^29.5.0",
|
"@jest/globals": "^29.5.0",
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
"@yume-chan/event": "workspace:^0.0.19",
|
"@yume-chan/event": "workspace:^0.0.19",
|
||||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
"@yume-chan/struct": "workspace:^0.0.19",
|
"@yume-chan/struct": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^29.5.0",
|
"@jest/globals": "^29.5.0",
|
||||||
|
|
|
@ -193,6 +193,11 @@ export class Adb implements Closeable {
|
||||||
return this._maxPayloadSize;
|
return this._maxPayloadSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _banner: string;
|
||||||
|
public get banner() {
|
||||||
|
return this._banner;
|
||||||
|
}
|
||||||
|
|
||||||
private _product: string | undefined;
|
private _product: string | undefined;
|
||||||
public get product() {
|
public get product() {
|
||||||
return this._product;
|
return this._product;
|
||||||
|
@ -227,6 +232,7 @@ export class Adb implements Closeable {
|
||||||
maxPayloadSize: number,
|
maxPayloadSize: number,
|
||||||
banner: string
|
banner: string
|
||||||
) {
|
) {
|
||||||
|
this._banner = banner;
|
||||||
this.parseBanner(banner);
|
this.parseBanner(banner);
|
||||||
|
|
||||||
let calculateChecksum: boolean;
|
let calculateChecksum: boolean;
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"@yume-chan/adb": "workspace:^0.0.19",
|
"@yume-chan/adb": "workspace:^0.0.19",
|
||||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
"@yume-chan/struct": "workspace:^0.0.19",
|
"@yume-chan/struct": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^29.5.0",
|
"@jest/globals": "^29.5.0",
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yume-chan/async": "^2.2.0",
|
"@yume-chan/async": "^2.2.0",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^29.5.0",
|
"@jest/globals": "^29.5.0",
|
||||||
|
|
|
@ -1,3 +1,30 @@
|
||||||
# @yume-chan/pcm-player
|
# @yume-chan/pcm-player
|
||||||
|
|
||||||
Play raw audio sample stream using Web Audio API
|
Play raw audio sample stream using Web Audio API.
|
||||||
|
|
||||||
|
Only support stereo audio.
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
|
||||||
|
- [ ] resample audio to compensate for audio buffer underrun
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Depends on the sample format, there are multiple player classes:
|
||||||
|
|
||||||
|
- `Int16PcmPlayer` (little endian)
|
||||||
|
- `Float32PcmPlayer`
|
||||||
|
- `Float32PlanerPcmPlayer`
|
||||||
|
|
||||||
|
No `Planer`: audio samples are interleaved (left channel first).
|
||||||
|
|
||||||
|
With `Planer`: audio samples are in a two-dimensional array (left channel first).
|
||||||
|
|
||||||
|
The constructors require user activation (must be invoked in a user event handler, e.g. `onclick`), because they create `AudioContext`s.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
var player = Int16PcmPlayer(44100);
|
||||||
|
player.start();
|
||||||
|
player.feed(new Int16Array([0, 0, 0, 0, 0, 0, 0, 0]));
|
||||||
|
player.stop(); // `AudioContext` will be closed, so can't be restarted
|
||||||
|
```
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^29.5.0",
|
"@jest/globals": "^29.5.0",
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"@yume-chan/scrcpy": "workspace:^0.0.19",
|
"@yume-chan/scrcpy": "workspace:^0.0.19",
|
||||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
"tinyh264": "^0.0.7",
|
"tinyh264": "^0.0.7",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.5.0",
|
||||||
"yuv-buffer": "^1.0.0",
|
"yuv-buffer": "^1.0.0",
|
||||||
"yuv-canvas": "^1.2.11"
|
"yuv-canvas": "^1.2.11"
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
"@yume-chan/scrcpy": "workspace:^0.0.19",
|
"@yume-chan/scrcpy": "workspace:^0.0.19",
|
||||||
"@yume-chan/scrcpy-decoder-tinyh264": "workspace:^0.0.19",
|
"@yume-chan/scrcpy-decoder-tinyh264": "workspace:^0.0.19",
|
||||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^29.5.0",
|
"@jest/globals": "^29.5.0",
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
"@yume-chan/struct": "workspace:^0.0.19",
|
"@yume-chan/struct": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^29.5.0",
|
"@jest/globals": "^29.5.0",
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yume-chan/async": "^2.2.0",
|
"@yume-chan/async": "^2.2.0",
|
||||||
"@yume-chan/struct": "workspace:^0.0.19",
|
"@yume-chan/struct": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.5.0",
|
||||||
"web-streams-polyfill": "^4.0.0-beta.3"
|
"web-streams-polyfill": "^4.0.0-beta.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -11,12 +11,12 @@ interface Console {
|
||||||
createTask(name: string): Task;
|
createTask(name: string): Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GlobalEx {
|
interface GlobalExtension {
|
||||||
console: Console;
|
console: Console;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `createTask` allows browser DevTools to track the call stack across async boundaries.
|
// `createTask` allows browser DevTools to track the call stack across async boundaries.
|
||||||
const { console } = globalThis as unknown as GlobalEx;
|
const { console } = globalThis as unknown as GlobalExtension;
|
||||||
const createTask: Console["createTask"] =
|
const createTask: Console["createTask"] =
|
||||||
console.createTask?.bind(console) ??
|
console.createTask?.bind(console) ??
|
||||||
(() => ({
|
(() => ({
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yume-chan/dataview-bigint-polyfill": "workspace:^0.0.19",
|
"@yume-chan/dataview-bigint-polyfill": "workspace:^0.0.19",
|
||||||
"tslib": "^2.4.1"
|
"tslib": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^29.5.0",
|
"@jest/globals": "^29.5.0",
|
||||||
|
|
42
libraries/tabby-tango/package.json
Normal file
42
libraries/tabby-tango/package.json
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"name": "@yume-chan/tabby-tango",
|
||||||
|
"version": "0.0.19",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --progress --color",
|
||||||
|
"build:watch": "webpack --progress --color",
|
||||||
|
"watch": "webpack --progress --color --watch"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"data",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"author": "Eugene Pankov",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular/compiler": "^15.2.6",
|
||||||
|
"@angular/compiler-cli": "^15.2.6",
|
||||||
|
"@angular/core": "^15.2.6",
|
||||||
|
"@angular/forms": "^15.2.6",
|
||||||
|
"@angular/platform-browser": "^15.2.6",
|
||||||
|
"@ng-bootstrap/ng-bootstrap": "^14.1.0",
|
||||||
|
"@ngtools/webpack": "^15.2.5",
|
||||||
|
"@types/node": "^18.15.3",
|
||||||
|
"@types/webpack-env": "^1.16.0",
|
||||||
|
"@yume-chan/adb": "workspace:^0.0.19",
|
||||||
|
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||||
|
"babel-loader": "^9.1.2",
|
||||||
|
"mobx": "^6.7.0",
|
||||||
|
"source-map-loader": "^4.0.1",
|
||||||
|
"tabby-core": "^1.0.197-nightly.0",
|
||||||
|
"tabby-terminal": "^1.0.197-nightly.0",
|
||||||
|
"tslib": "^2.5.0",
|
||||||
|
"webpack": "^5.75.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.7.0",
|
||||||
|
"webpack-cli": "^5.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"svg-inline-loader": "^0.8.2"
|
||||||
|
}
|
||||||
|
}
|
44
libraries/tabby-tango/src/buttonProvider.ts
Normal file
44
libraries/tabby-tango/src/buttonProvider.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import {
|
||||||
|
AppService,
|
||||||
|
NotificationsService,
|
||||||
|
ProfilesService,
|
||||||
|
ToolbarButton,
|
||||||
|
ToolbarButtonProvider,
|
||||||
|
} from "tabby-core";
|
||||||
|
import { AdbState } from "./state";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ButtonProvider extends ToolbarButtonProvider {
|
||||||
|
constructor(
|
||||||
|
app: AppService,
|
||||||
|
private profile: ProfilesService,
|
||||||
|
private notification: NotificationsService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
app.ready$.subscribe(() => {
|
||||||
|
this.launchProfile(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private launchProfile(showError: boolean) {
|
||||||
|
if (AdbState.value) {
|
||||||
|
this.profile.openNewTabForProfile({ type: "adb", name: "Shell" });
|
||||||
|
} else if (showError) {
|
||||||
|
this.notification.error("Please connect your device first");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(): ToolbarButton[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: require("./icons/plus.svg"),
|
||||||
|
title: "New tab",
|
||||||
|
click: () => {
|
||||||
|
this.launchProfile(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Component, Injector } from "@angular/core";
|
||||||
|
import {
|
||||||
|
BaseSession,
|
||||||
|
BaseTerminalProfile,
|
||||||
|
BaseTerminalTabComponent,
|
||||||
|
} from "tabby-terminal";
|
||||||
|
import { AdbSession } from "../session";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "adbTerminalTab",
|
||||||
|
template: BaseTerminalTabComponent.template,
|
||||||
|
styles: BaseTerminalTabComponent.styles,
|
||||||
|
animations: BaseTerminalTabComponent.animations,
|
||||||
|
})
|
||||||
|
export class AdbTerminalTabComponent extends BaseTerminalTabComponent<BaseTerminalProfile> {
|
||||||
|
constructor(injector: Injector) {
|
||||||
|
super(injector);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.logger = this.log.create("terminalTab");
|
||||||
|
this.session = new AdbSession(
|
||||||
|
this.injector,
|
||||||
|
this.logger
|
||||||
|
) as unknown as BaseSession;
|
||||||
|
super.ngOnInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onFrontendReady(): void {
|
||||||
|
this.initializeSession();
|
||||||
|
super.onFrontendReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSession(): void {
|
||||||
|
this.session!.start(undefined);
|
||||||
|
this.attachSessionHandlers(true);
|
||||||
|
this.recoveryStateChangedHint.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
this.session?.destroy();
|
||||||
|
}
|
||||||
|
}
|
1
libraries/tabby-tango/src/icons/plus.svg
Normal file
1
libraries/tabby-tango/src/icons/plus.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="plus" class="svg-inline--fa fa-plus" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M432 256C432 264.8 424.8 272 416 272h-176V448c0 8.844-7.156 16.01-16 16.01S208 456.8 208 448V272H32c-8.844 0-16-7.15-16-15.99C16 247.2 23.16 240 32 240h176V64c0-8.844 7.156-15.99 16-15.99S240 55.16 240 64v176H416C424.8 240 432 247.2 432 256z"></path></svg>
|
After Width: | Height: | Size: 462 B |
36
libraries/tabby-tango/src/index.ts
Normal file
36
libraries/tabby-tango/src/index.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { FormsModule } from "@angular/forms";
|
||||||
|
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
|
||||||
|
import TabbyCorePlugin, {
|
||||||
|
AppService,
|
||||||
|
ProfileProvider,
|
||||||
|
ToolbarButtonProvider,
|
||||||
|
} from "tabby-core";
|
||||||
|
import TabbyTerminalModule from "tabby-terminal";
|
||||||
|
import { ButtonProvider } from "./buttonProvider";
|
||||||
|
import { AdbTerminalTabComponent } from "./components/terminalTab.component";
|
||||||
|
import { AdbProfilesService } from "./profiles";
|
||||||
|
import { AdbState } from "./state";
|
||||||
|
|
||||||
|
export { AdbState };
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [FormsModule, NgbModule, TabbyCorePlugin, TabbyTerminalModule],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ProfileProvider,
|
||||||
|
useClass: AdbProfilesService,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ToolbarButtonProvider,
|
||||||
|
useClass: ButtonProvider,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
declarations: [AdbTerminalTabComponent],
|
||||||
|
exports: [AdbTerminalTabComponent],
|
||||||
|
})
|
||||||
|
export default class AdbTerminalModule {
|
||||||
|
constructor(app: AppService) {}
|
||||||
|
}
|
41
libraries/tabby-tango/src/profiles.ts
Normal file
41
libraries/tabby-tango/src/profiles.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import {
|
||||||
|
NewTabParameters,
|
||||||
|
PartialProfile,
|
||||||
|
Profile,
|
||||||
|
ProfileProvider,
|
||||||
|
} from "tabby-core";
|
||||||
|
import { AdbTerminalTabComponent } from "./components/terminalTab.component";
|
||||||
|
|
||||||
|
@Injectable({ providedIn: "root" })
|
||||||
|
export class AdbProfilesService extends ProfileProvider<Profile> {
|
||||||
|
id = "adb";
|
||||||
|
name = "Adb shell";
|
||||||
|
|
||||||
|
async getBuiltinProfiles(): Promise<PartialProfile<Profile>[]> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "adb",
|
||||||
|
type: "adb",
|
||||||
|
name: "ADB shell",
|
||||||
|
icon: "fas fa-microchip",
|
||||||
|
isBuiltin: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNewTabParameters(
|
||||||
|
profile: Profile
|
||||||
|
): Promise<NewTabParameters<AdbTerminalTabComponent>> {
|
||||||
|
return {
|
||||||
|
type: AdbTerminalTabComponent,
|
||||||
|
inputs: {
|
||||||
|
profile,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(_profile: Profile): string {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
65
libraries/tabby-tango/src/session.ts
Normal file
65
libraries/tabby-tango/src/session.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import type { Injector } from "@angular/core";
|
||||||
|
import { AdbSubprocessProtocol } from "@yume-chan/adb";
|
||||||
|
import {
|
||||||
|
Consumable,
|
||||||
|
ConsumableWritableStream,
|
||||||
|
WritableStream,
|
||||||
|
WritableStreamDefaultWriter,
|
||||||
|
} from "@yume-chan/stream-extra";
|
||||||
|
import type { Logger } from "tabby-core";
|
||||||
|
import { BaseSession } from "tabby-terminal";
|
||||||
|
import { AdbState } from "./state";
|
||||||
|
|
||||||
|
export class AdbSession extends BaseSession {
|
||||||
|
private shell!: AdbSubprocessProtocol;
|
||||||
|
private writer!: WritableStreamDefaultWriter<Consumable<Uint8Array>>;
|
||||||
|
|
||||||
|
constructor(injector: Injector, logger: Logger) {
|
||||||
|
super(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
const adb = AdbState.value;
|
||||||
|
if (!adb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.open = true;
|
||||||
|
|
||||||
|
this.shell = await adb.subprocess.shell();
|
||||||
|
this.writer = this.shell.stdin.getWriter();
|
||||||
|
this.shell.stdout.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write: (chunk) => {
|
||||||
|
this.emitOutput(Buffer.from(chunk));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
adb.disconnected.then(() => {
|
||||||
|
this.emitOutput(Buffer.from("\n[Disconnected]\n", "utf8"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resize(columns: number, rows: number): void {
|
||||||
|
this.shell?.resize(columns, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
write(data: Buffer): void {
|
||||||
|
ConsumableWritableStream.write(this.writer, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
kill(_signal?: string): void {
|
||||||
|
this.shell?.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
async gracefullyKillProcess(): Promise<void> {}
|
||||||
|
|
||||||
|
supportsWorkingDirectory(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkingDirectory(): Promise<string | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
6
libraries/tabby-tango/src/state.ts
Normal file
6
libraries/tabby-tango/src/state.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { Adb } from "@yume-chan/adb";
|
||||||
|
import { makeAutoObservable } from "mobx";
|
||||||
|
|
||||||
|
export const AdbState = makeAutoObservable<{ value: Adb | null }>({
|
||||||
|
value: null,
|
||||||
|
});
|
37
libraries/tabby-tango/tsconfig.json
Normal file
37
libraries/tabby-tango/tsconfig.json
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src",
|
||||||
|
"module": "es2015",
|
||||||
|
"target": "es2016",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"removeComments": false,
|
||||||
|
"emitDeclarationOnly": false,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"ES5",
|
||||||
|
"ES6",
|
||||||
|
"ES7",
|
||||||
|
"ES2015",
|
||||||
|
"ES2017",
|
||||||
|
"ES2019",
|
||||||
|
"ES2021"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"strictTemplates": true,
|
||||||
|
"enableResourceInlining": true,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
}
|
||||||
|
}
|
10
libraries/tabby-tango/webpack.config.mjs
Normal file
10
libraries/tabby-tango/webpack.config.mjs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import * as url from "url";
|
||||||
|
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
||||||
|
|
||||||
|
import config from "./webpack.plugin.config.mjs";
|
||||||
|
|
||||||
|
export default () =>
|
||||||
|
config({
|
||||||
|
name: "web-demo",
|
||||||
|
dirname: __dirname,
|
||||||
|
});
|
182
libraries/tabby-tango/webpack.plugin.config.mjs
Normal file
182
libraries/tabby-tango/webpack.plugin.config.mjs
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
import { AngularWebpackPlugin } from "@ngtools/webpack";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
|
||||||
|
|
||||||
|
const bundleAnalyzer = new BundleAnalyzerPlugin({
|
||||||
|
analyzerPort: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
import { createEs2015LinkerPlugin } from "@angular/compiler-cli/linker/babel";
|
||||||
|
const linkerPlugin = createEs2015LinkerPlugin({
|
||||||
|
linkerJitMode: true,
|
||||||
|
fileSystem: {
|
||||||
|
resolve: path.resolve,
|
||||||
|
exists: fs.existsSync,
|
||||||
|
dirname: path.dirname,
|
||||||
|
relative: path.relative,
|
||||||
|
readFile: fs.readFileSync,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default (options) => {
|
||||||
|
const isDev = !!process.env.TABBY_DEV;
|
||||||
|
const config = {
|
||||||
|
target: "node",
|
||||||
|
entry: "./src/index.ts",
|
||||||
|
context: options.dirname,
|
||||||
|
devtool: "source-map",
|
||||||
|
output: {
|
||||||
|
path: path.resolve(options.dirname, "dist"),
|
||||||
|
filename: "index.js",
|
||||||
|
pathinfo: true,
|
||||||
|
libraryTarget: "umd",
|
||||||
|
publicPath: "auto",
|
||||||
|
},
|
||||||
|
mode: isDev ? "development" : "production",
|
||||||
|
optimization: {
|
||||||
|
minimize: false,
|
||||||
|
},
|
||||||
|
cache: !isDev
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
type: "filesystem",
|
||||||
|
cacheDirectory: path.resolve(
|
||||||
|
options.dirname,
|
||||||
|
"node_modules",
|
||||||
|
".webpack-cache"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: options.alias ?? {},
|
||||||
|
extensions: [".ts", ".js"],
|
||||||
|
mainFields: ["esm2015", "browser", "module", "main"],
|
||||||
|
},
|
||||||
|
ignoreWarnings: [/Failed to parse source map/],
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
...(options.rules ?? []),
|
||||||
|
{
|
||||||
|
test: /.*\.m?js$/,
|
||||||
|
enforce: "pre",
|
||||||
|
use: ["source-map-loader"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.m?js$/,
|
||||||
|
loader: "babel-loader",
|
||||||
|
options: {
|
||||||
|
plugins: [linkerPlugin],
|
||||||
|
compact: false,
|
||||||
|
cacheDirectory: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
fullySpecified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: "@ngtools/webpack",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.pug$/,
|
||||||
|
use: [
|
||||||
|
"apply-loader",
|
||||||
|
{
|
||||||
|
loader: "pug-loader",
|
||||||
|
options: {
|
||||||
|
pretty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [
|
||||||
|
"@tabby-gang/to-string-loader",
|
||||||
|
"css-loader",
|
||||||
|
"sass-loader",
|
||||||
|
],
|
||||||
|
include: /(theme.*|component)\.scss/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: ["style-loader", "css-loader", "sass-loader"],
|
||||||
|
exclude: /(theme.*|component)\.scss/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ["@tabby-gang/to-string-loader", "css-loader"],
|
||||||
|
include: /component\.css/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ["style-loader", "css-loader"],
|
||||||
|
exclude: /component\.css/,
|
||||||
|
},
|
||||||
|
{ test: /\.yaml$/, use: ["yaml-loader"] },
|
||||||
|
{ test: /\.svg/, use: ["svg-inline-loader"] },
|
||||||
|
{
|
||||||
|
test: /\.(eot|otf|woff|woff2|ogg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
|
type: "asset",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ttf$/,
|
||||||
|
type: "asset/inline",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.po$/,
|
||||||
|
use: [
|
||||||
|
{ loader: "json-loader" },
|
||||||
|
{ loader: "po-gettext-loader" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
externals: [
|
||||||
|
"@electron/remote",
|
||||||
|
"@serialport/bindings",
|
||||||
|
"@serialport/bindings-cpp",
|
||||||
|
"any-promise",
|
||||||
|
"child_process",
|
||||||
|
"electron-promise-ipc",
|
||||||
|
"electron-updater",
|
||||||
|
"electron",
|
||||||
|
"fontmanager-redux",
|
||||||
|
"fs",
|
||||||
|
"keytar",
|
||||||
|
"macos-native-processlist",
|
||||||
|
"native-process-working-directory",
|
||||||
|
"net",
|
||||||
|
"ngx-toastr",
|
||||||
|
"os",
|
||||||
|
"path",
|
||||||
|
"readline",
|
||||||
|
"@luminati-io/socksv5",
|
||||||
|
"stream",
|
||||||
|
"windows-native-registry",
|
||||||
|
"windows-process-tree",
|
||||||
|
"windows-process-tree/build/Release/windows_process_tree.node",
|
||||||
|
/^@angular(?!\/common\/locales)/,
|
||||||
|
/^@ng-bootstrap/,
|
||||||
|
/^rxjs/,
|
||||||
|
/^tabby-/,
|
||||||
|
...(options.externals || []),
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
new AngularWebpackPlugin({
|
||||||
|
tsconfig: path.resolve(options.dirname, "tsconfig.json"),
|
||||||
|
directTemplateLoading: false,
|
||||||
|
jitMode: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
if (process.env.PLUGIN_BUNDLE_ANALYZER === options.name) {
|
||||||
|
config.plugins.push(bundleAnalyzer);
|
||||||
|
config.cache = false;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
};
|
|
@ -476,5 +476,13 @@
|
||||||
"projectFolder": "libraries/adb-scrcpy",
|
"projectFolder": "libraries/adb-scrcpy",
|
||||||
"versionPolicyName": "adb"
|
"versionPolicyName": "adb"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"packageName": "@yume-chan/tabby-tango",
|
||||||
|
"projectFolder": "libraries/tabby-tango"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"packageName": "@yume-chan/adb-backend-proxy",
|
||||||
|
"projectFolder": "libraries/adb-backend-proxy"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue