-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #56 from warjiang/feature/turborepo
Feature/turborepo
Showing
39 changed files
with
3,039 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,3 +26,4 @@ _output | |
# sub chart tgz | ||
charts/*/charts | ||
cmd/ops | ||
.turbo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# Changesets | ||
|
||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works | ||
with multi-package repos, or single-package repos to help you version and publish your code. You can | ||
find the full documentation for it [in our repository](https://github.com/changesets/changesets) | ||
|
||
We have a quick list of common questions to get you started engaging with this project in | ||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"$schema": "https://unpkg.com/@changesets/config@3.0.1/schema.json", | ||
"changelog": "@changesets/cli/changelog", | ||
"commit": false, | ||
"fixed": [], | ||
"linked": [], | ||
"access": "restricted", | ||
"baseBranch": "main", | ||
"updateInternalDependencies": "patch", | ||
"ignore": [] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
node_modules | ||
build | ||
coverage | ||
dist | ||
pnpm-* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"trailingComma": "all", | ||
"singleQuote": true, | ||
"printWidth": 80, | ||
"tabWidth": 2, | ||
"endOfLine": "auto" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# @karmada/terminal | ||
|
||
## 1.0.4 | ||
|
||
### Patch Changes | ||
|
||
- update declare file for terminal | ||
|
||
## 1.0.3 | ||
|
||
### Major Changes | ||
|
||
- export typings of terminal | ||
|
||
## 1.0.2 | ||
|
||
### Patch Changes | ||
|
||
- add container terminal for k8s pod | ||
|
||
## 1.0.1 | ||
|
||
### Patch Changes | ||
|
||
- add ttyd terminal | ||
- Updated dependencies | ||
- @karmada/xterm-addon-overlay@1.0.1 | ||
- @karmada/xterm-addon-zmodem@1.0.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
{ | ||
"name": "@karmada/terminal", | ||
"version": "1.0.4", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"description": "", | ||
"exports": { | ||
".": { | ||
"require": "./dist/index.js", | ||
"import": "./dist/index.mjs" | ||
} | ||
}, | ||
"types": "dist/index.d.ts", | ||
"scripts": { | ||
"prepublish": "tsup --config ./tsup.config.ts", | ||
"build": "tsup --config ./tsup.config.ts" | ||
}, | ||
"lint-staged": { | ||
"**/*.{js,jsx,ts,tsx,json,css,md}": [ | ||
"prettier --write" | ||
] | ||
}, | ||
"keywords": [ | ||
"terminal", | ||
"xterm", | ||
"xterm.js" | ||
], | ||
"author": "", | ||
"license": "ISC", | ||
"dependencies": { | ||
"@karmada/xterm-addon-overlay": "workspace:*", | ||
"@karmada/xterm-addon-zmodem": "workspace:*", | ||
"@xterm/addon-canvas": "^0.7.0", | ||
"@xterm/addon-clipboard": "^0.1.0", | ||
"@xterm/addon-fit": "^0.10.0", | ||
"@xterm/addon-ligatures": "^0.9.0", | ||
"@xterm/addon-search": "^0.15.0", | ||
"@xterm/addon-unicode11": "^0.8.0", | ||
"@xterm/addon-web-links": "^0.11.0", | ||
"@xterm/addon-webgl": "^0.18.0", | ||
"@xterm/xterm": "^5.0.0", | ||
"debug": "^4.3.5", | ||
"lodash.isempty": "^4.4.0", | ||
"sockjs-client": "^1.6.1" | ||
}, | ||
"devDependencies": { | ||
"@types/debug": "^4.1.12", | ||
"@types/lodash.isempty": "^4.4.9", | ||
"@types/react": "^18.3.3", | ||
"@types/react-dom": "^18.3.0", | ||
"@types/sockjs-client": "^1.5.4" | ||
}, | ||
"peerDependencies": { | ||
"react": "^18", | ||
"react-dom": "^18" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
import { IDisposable, ITerminalAddon, Terminal } from '@xterm/xterm'; | ||
import { FitAddon } from '@xterm/addon-fit'; | ||
import { WebLinksAddon } from '@xterm/addon-web-links'; | ||
import { SearchAddon } from '@xterm/addon-search'; | ||
import { CanvasAddon } from '@xterm/addon-canvas'; | ||
import { WebglAddon } from '@xterm/addon-webgl'; | ||
import isEmpty from 'lodash.isempty'; | ||
import { addEventListener, getDebugger } from './utils.ts'; | ||
import { | ||
AddonInfo, | ||
AddonType, | ||
innerTerminal, | ||
RendererType, | ||
BaseTerminalOptions, | ||
} from './typing'; | ||
|
||
const log = getDebugger('BaseTerminal'); | ||
|
||
class BaseTerminal implements IDisposable { | ||
// terminal is a xterm instance, use inherited type for add method on xterm instance | ||
protected terminal!: innerTerminal; | ||
// use `isXtermOpened` flag avoid create xterm instance multiple times | ||
protected isXtermOpened: boolean = false; | ||
/* | ||
* baseTerminalOptions consists of clientOptions and xtermOptions | ||
* clientOptions: custom options for terminal | ||
* xtermOptions: options for xterm | ||
*/ | ||
protected baseTerminalOptions: BaseTerminalOptions; | ||
// disposables used for store clean function for resources | ||
private disposables: IDisposable[] = []; | ||
// addons store the addons of xterm | ||
protected addons = {} as Record<AddonType, AddonInfo>; | ||
|
||
constructor(options: BaseTerminalOptions) { | ||
this.baseTerminalOptions = options; | ||
this.addons = { | ||
...this.addons, | ||
fit: { | ||
name: 'fit', | ||
ctor: new FitAddon(), | ||
}, | ||
webLinks: { | ||
name: 'webLinks', | ||
ctor: new WebLinksAddon(), | ||
}, | ||
search: { | ||
name: 'search', | ||
ctor: new SearchAddon(), | ||
}, | ||
}; | ||
} | ||
|
||
public register = <T extends IDisposable>(...disposables: T[]): T[] => { | ||
for (const disposable of disposables) { | ||
this.disposables.push(disposable); | ||
} | ||
return disposables; | ||
}; | ||
|
||
private initListeners() { | ||
this.register( | ||
addEventListener(window, 'resize', () => | ||
this.mustGetAddon<FitAddon>('fit').fit(), | ||
), | ||
); | ||
} | ||
|
||
public dispose = () => { | ||
for (const d of this.disposables) { | ||
d.dispose(); | ||
} | ||
this.disposables.length = 0; | ||
}; | ||
|
||
public setRendererType(value: RendererType) { | ||
log('setRendererType', value); | ||
switch (value) { | ||
case 'canvas': | ||
this.deleteAddon('webgl'); | ||
this.setAddon('canvas', new CanvasAddon()); | ||
log('[ttyd] canvas renderer loaded'); | ||
break; | ||
case 'webgl': | ||
this.deleteAddon('canvas'); | ||
this.setAddon('webgl', new WebglAddon()); | ||
log('[ttyd] WebGL renderer loaded'); | ||
break; | ||
case 'dom': | ||
this.deleteAddon('webgl'); | ||
this.deleteAddon('canvas'); | ||
log('[ttyd] dom renderer loaded'); | ||
break; | ||
default: | ||
break; | ||
} | ||
} | ||
|
||
/** | ||
* return the addon accoring to addon name, but the function doesn't ensure | ||
* you can get the addon as expected, if the addon exist, it will return the addon, | ||
* otherwise you'll get null | ||
* @param name name of the addon | ||
* */ | ||
public getAddon = <T extends ITerminalAddon>(name: AddonType): T | null => { | ||
if (name in this.addons) { | ||
return this.addons[name].ctor as unknown as T; | ||
} | ||
return null; | ||
}; | ||
|
||
/** | ||
* mustGetAddon will ensure you get the addon as expected, if the addon not exist, it | ||
* will throw an expection. | ||
* @param name name of the addon | ||
*/ | ||
public mustGetAddon = <T extends ITerminalAddon>(name: AddonType): T => { | ||
if (name in this.addons) { | ||
return this.addons[name].ctor as unknown as T; | ||
} | ||
throw Error(`Cannot find addon[${name}]`); | ||
}; | ||
|
||
/** | ||
* setAddon will store the addon into addons, and make the terminal load addon. | ||
* If you setAddon multiple times, it will first invoke the dipose method of addon | ||
* @param name name of the addon | ||
* @param addon addon for xterm | ||
*/ | ||
protected setAddon = <T extends ITerminalAddon>( | ||
name: AddonType, | ||
addon: T, | ||
) => { | ||
this.getAddon(name)?.dispose(); | ||
this.addons[name] = { | ||
name: name, | ||
ctor: addon, | ||
}; | ||
this.terminal.loadAddon(addon); | ||
}; | ||
|
||
/** | ||
* deleteAddon will remove the addon from addons, and inovke dipose method of addon. | ||
* @param name name of the addon | ||
*/ | ||
protected deleteAddon = (name: AddonType) => { | ||
const addon = this.getAddon(name); | ||
if (addon) { | ||
addon.dispose(); | ||
delete this.addons[name]; | ||
} | ||
}; | ||
|
||
/** | ||
* write support write a bunch of data into terminal, it will split input data | ||
* by '\n' and iterate the return lines by writing line to the terminal | ||
* @param data | ||
*/ | ||
public write = (data: string) => { | ||
if (isEmpty(data)) return; | ||
data.split('\n').map((line) => { | ||
this.terminal.writeln(line); | ||
}); | ||
}; | ||
|
||
/** | ||
* open method is used to initialize the BaseTerminal instance, inside the open | ||
* method, we will pass the containerElement to initialize the xterm, loading build-in | ||
* addons and init esssential listeners for the xterm | ||
* @param containerElement dom element which is used to initialize Terminal | ||
*/ | ||
public open = (containerElement: HTMLElement) => { | ||
if (this.isXtermOpened) return; | ||
log('execute open method'); | ||
log('termOptions', this.baseTerminalOptions.xtermOptions); | ||
if (this.terminal) { | ||
log('terminal already exists, dispose it first'); | ||
this.terminal.dispose(); | ||
// this.dispose(); | ||
} | ||
|
||
const terminal = new Terminal( | ||
this.baseTerminalOptions.xtermOptions, | ||
) as innerTerminal; | ||
this.terminal = terminal; | ||
terminal.fit = () => { | ||
this.mustGetAddon<FitAddon>('fit').fit(); | ||
}; | ||
for (const name in this.addons) { | ||
log('load addon =>', name); | ||
terminal.loadAddon(this.addons[name as AddonType].ctor); | ||
} | ||
terminal.open(containerElement); | ||
this.setRendererType(this.baseTerminalOptions.clientOptions.rendererType); | ||
this.initListeners(); | ||
terminal.fit(); | ||
this.isXtermOpened = true; | ||
}; | ||
|
||
/** | ||
* return the terminal object for some special use-cases | ||
*/ | ||
public getTerminal = () => { | ||
return this.terminal; | ||
}; | ||
} | ||
|
||
export default BaseTerminal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import SockJS from 'sockjs-client/dist/sockjs'; | ||
import BaseTerminal from './base.ts'; | ||
import { BaseTerminalOptions } from './typing'; | ||
import { getDebugger } from './utils.ts'; | ||
|
||
interface ContainerTerminalOptions { | ||
namespace: string; | ||
pod: string; | ||
container: string; | ||
sessionIdUrl: string; | ||
} | ||
|
||
export interface SockJSSimpleEvent { | ||
type: string; | ||
|
||
toString(): string; | ||
} | ||
|
||
export interface SJSCloseEvent extends SockJSSimpleEvent { | ||
code: number; | ||
reason: string; | ||
wasClean: boolean; | ||
} | ||
|
||
export interface SJSMessageEvent extends SockJSSimpleEvent { | ||
data: string; | ||
} | ||
|
||
export interface ShellFrame { | ||
Op: string; | ||
Data?: string; | ||
SessionID?: string; | ||
Rows?: number; | ||
Cols?: number; | ||
} | ||
|
||
const log = getDebugger('ContainerTerminal'); | ||
|
||
class ContainerTerminal extends BaseTerminal { | ||
private containerOptions: ContainerTerminalOptions; | ||
private sessionId!: string; | ||
private socket!: WebSocket; | ||
private connected: boolean = false; | ||
private connecting: boolean = false; | ||
// @ts-ignore | ||
private connectionClosed: boolean = true; | ||
|
||
constructor( | ||
options: BaseTerminalOptions, | ||
containerOptions: ContainerTerminalOptions, | ||
) { | ||
super(options); | ||
this.containerOptions = containerOptions; | ||
} | ||
|
||
public getSessionId = async () => { | ||
try { | ||
const url = this.containerOptions.sessionIdUrl; | ||
const replacedUrl = url | ||
.replace('{{namespace}}', this.containerOptions.namespace) | ||
.replace('{{pod}}', this.containerOptions.pod) | ||
.replace('{{container}}', this.containerOptions.container); | ||
log(`request url: ${replacedUrl}`); | ||
const resp = await fetch(replacedUrl); | ||
if (resp.ok) { | ||
const json = await resp.json(); | ||
this.sessionId = json.data.id; | ||
} | ||
} catch (e) { | ||
log(`[ttyd] fetch ${this.containerOptions.sessionIdUrl}: `, e); | ||
} | ||
}; | ||
|
||
private initContainerListeners = () => { | ||
const { terminal } = this; | ||
this.register( | ||
terminal.onData(this.onTerminalSendString.bind(this)), | ||
terminal.onResize(this.onTerminalResize.bind(this)), | ||
); | ||
}; | ||
|
||
public connect = () => { | ||
if (this.connected || this.connecting) return; | ||
this.connecting = true; | ||
this.connectionClosed = false; | ||
|
||
this.socket = new SockJS(`/api/sockjs?${this.sessionId}`); | ||
const { socket } = this; | ||
socket.onopen = this.onConnectionOpen.bind(this, this.sessionId); | ||
socket.onmessage = this.onConnectionMessage.bind(this); | ||
socket.onclose = this.onConnectionClose.bind(this); | ||
}; | ||
|
||
private onConnectionOpen = (sessionId: string) => { | ||
const startData = { Op: 'bind', SessionID: sessionId }; | ||
this.socket.send(JSON.stringify(startData)); | ||
this.connected = true; | ||
this.connecting = false; | ||
this.connectionClosed = false; | ||
|
||
// Make sure the terminal is with correct display size. | ||
this.onTerminalResize(); | ||
|
||
this.initContainerListeners(); | ||
|
||
// Focus on connection | ||
this.terminal.focus(); | ||
}; | ||
|
||
private onConnectionMessage = (evt: SJSMessageEvent) => { | ||
const msg = JSON.parse(evt.data); | ||
this.handleConnectionMessage(msg); | ||
}; | ||
|
||
private handleConnectionMessage = (frame: ShellFrame) => { | ||
if (frame.Op === 'stdout') { | ||
this.terminal.write(frame.Data || ''); | ||
} | ||
if (frame.Op === 'toast') { | ||
} | ||
}; | ||
|
||
private onConnectionClose = (_evt?: SJSCloseEvent) => { | ||
if (!this.connected) { | ||
return; | ||
} | ||
this.socket.close(); | ||
this.connected = false; | ||
this.connecting = false; | ||
this.connectionClosed = true; | ||
}; | ||
|
||
private onTerminalResize(): void { | ||
if (this.connected) { | ||
this.socket.send( | ||
JSON.stringify({ | ||
Op: 'resize', | ||
Cols: this.terminal.cols, | ||
Rows: this.terminal.rows, | ||
}), | ||
); | ||
} | ||
} | ||
|
||
private onTerminalSendString = (str: string) => { | ||
if (this.connected) { | ||
this.socket.send( | ||
JSON.stringify({ | ||
Op: 'stdin', | ||
Data: str, | ||
Cols: this.terminal.cols, | ||
Rows: this.terminal.rows, | ||
}), | ||
); | ||
} | ||
}; | ||
} | ||
|
||
export default ContainerTerminal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { createContext } from 'react'; | ||
import BaseTerminal from './base.ts'; | ||
import '@xterm/xterm/css/xterm.css'; | ||
|
||
export const TerminalContext = createContext<{ | ||
terminal: BaseTerminal | null; | ||
}>({ | ||
terminal: null, | ||
}); | ||
export { default as ContainerTerminal } from './container'; | ||
export { default as TtydTerminal } from './ttyd'; | ||
export type { FlowControl, Preferences, Command } from './ttyd'; | ||
export { default as BaseTerminal } from './base'; | ||
export type { ITerminalOptions, ITheme } from '@xterm/xterm'; | ||
export type { | ||
AddonType, | ||
AddonInfo, | ||
ClientOptions, | ||
BaseTerminalOptions, | ||
RendererType, | ||
} from './typing.d.ts'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,390 @@ | ||
import type { ITerminalOptions } from '@xterm/xterm'; | ||
import { addEventListener, getDebugger } from './utils'; | ||
import BaseTerminal from './base.ts'; | ||
import { BaseTerminalOptions, ClientOptions } from './typing'; | ||
import OverlayAddon from '@karmada/xterm-addon-overlay'; | ||
import ZmodemAddon from '@karmada/xterm-addon-zmodem'; | ||
|
||
export type Preferences = ITerminalOptions & ClientOptions; | ||
|
||
const log = getDebugger('TtydTerminal'); | ||
|
||
interface TtydTerminalOptions { | ||
wsUrl: string; | ||
tokenUrl: string; | ||
flowControl: FlowControl; | ||
} | ||
|
||
export interface FlowControl { | ||
limit: number; | ||
highWater: number; | ||
lowWater: number; | ||
} | ||
|
||
export enum Command { | ||
// server side | ||
OUTPUT = '0', | ||
SET_WINDOW_TITLE = '1', | ||
SET_PREFERENCES = '2', | ||
|
||
/* eslint-disable @typescript-eslint/no-duplicate-enum-values */ | ||
// client side | ||
INPUT = '0', | ||
RESIZE_TERMINAL = '1', | ||
PAUSE = '2', | ||
RESUME = '3', | ||
/* eslint-enable @typescript-eslint/no-duplicate-enum-values */ | ||
} | ||
|
||
class TtydTerminal extends BaseTerminal { | ||
private ttydTerminalOptions: TtydTerminalOptions; | ||
private socket!: WebSocket; | ||
private opened = false; | ||
private reconnect = true; | ||
private doReconnect = true; | ||
private token: string = ''; | ||
private textEncoder = new TextEncoder(); | ||
private textDecoder = new TextDecoder(); | ||
|
||
private written = 0; | ||
private pending = 0; | ||
|
||
private title?: string; | ||
private titleFixed?: string; | ||
private resizeOverlay = true; | ||
private sendCb = () => {}; | ||
|
||
constructor(options: BaseTerminalOptions, ttydOptions: TtydTerminalOptions) { | ||
super(options); | ||
this.ttydTerminalOptions = ttydOptions; | ||
this.addons = { | ||
...this.addons, | ||
overlay: { | ||
name: 'overlay', | ||
ctor: new OverlayAddon(), | ||
}, | ||
}; | ||
} | ||
|
||
private writeFunc = (data: ArrayBuffer) => { | ||
return this.writeData(new Uint8Array(data)); | ||
}; | ||
|
||
public writeData = (data: string | Uint8Array) => { | ||
const { terminal, textEncoder } = this; | ||
if (!terminal) return; | ||
const { limit, highWater, lowWater } = this.ttydTerminalOptions.flowControl; | ||
|
||
this.written += data.length; | ||
if (this.written > limit) { | ||
terminal.write(data, () => { | ||
this.pending = Math.max(this.pending - 1, 0); | ||
if (this.pending < lowWater) { | ||
this.socket.send(textEncoder.encode(Command.RESUME)); | ||
} | ||
}); | ||
this.pending++; | ||
this.written = 0; | ||
if (this.pending > highWater) { | ||
this.socket.send(textEncoder.encode(Command.PAUSE)); | ||
} | ||
} else { | ||
terminal.write(data); | ||
} | ||
}; | ||
|
||
public sendData = (data: string | Uint8Array) => { | ||
const { socket, textEncoder } = this; | ||
if (socket.readyState !== WebSocket.OPEN) return; | ||
|
||
if (typeof data === 'string') { | ||
const payload = new Uint8Array(data.length * 3 + 1); | ||
payload[0] = Command.INPUT.charCodeAt(0); | ||
const stats = textEncoder.encodeInto(data, payload.subarray(1)); | ||
socket.send(payload.subarray(0, (stats.written as number) + 1)); | ||
} else { | ||
const payload = new Uint8Array(data.length + 1); | ||
payload[0] = Command.INPUT.charCodeAt(0); | ||
payload.set(data, 1); | ||
socket.send(payload); | ||
} | ||
}; | ||
|
||
public refreshToken = async () => { | ||
try { | ||
const resp = await fetch(this.ttydTerminalOptions.tokenUrl); | ||
if (resp.ok) { | ||
const json = await resp.json(); | ||
this.token = json.token; | ||
} | ||
} catch (e) { | ||
log(`[ttyd] fetch ${this.ttydTerminalOptions.tokenUrl}: `, e); | ||
} | ||
}; | ||
|
||
private parseOptsFromUrlQuery = (query: string): Preferences => { | ||
const { terminal } = this; | ||
const clientOptions = this.baseTerminalOptions | ||
.clientOptions as unknown as Record<string, string>; | ||
const prefs = {} as Record<string, boolean | number | string>; | ||
const queryObj = Array.from( | ||
new URLSearchParams(query) as unknown as Iterable<[string, string]>, | ||
); | ||
|
||
for (const [k, queryVal] of queryObj) { | ||
let v = clientOptions[k]; | ||
if (v === undefined) v = (terminal?.options as Record<string, string>)[k]; | ||
switch (typeof v) { | ||
case 'boolean': | ||
prefs[k] = queryVal === 'true' || queryVal === '1'; | ||
break; | ||
case 'number': | ||
case 'bigint': | ||
prefs[k] = Number.parseInt(queryVal, 10); | ||
break; | ||
case 'string': | ||
prefs[k] = queryVal; | ||
break; | ||
case 'object': | ||
prefs[k] = JSON.parse(queryVal); | ||
break; | ||
default: | ||
console.warn( | ||
`[ttyd] maybe unknown option: ${k}=${queryVal}, treating as string`, | ||
); | ||
prefs[k] = queryVal; | ||
break; | ||
} | ||
} | ||
|
||
return prefs as unknown as Preferences; | ||
}; | ||
|
||
private initTtydListeners = () => { | ||
const { terminal } = this; | ||
this.register( | ||
terminal.onTitleChange((data) => { | ||
if (data && data !== '' && !this.titleFixed) { | ||
document.title = data + ' | ' + this.title; | ||
} | ||
}), | ||
terminal.onData((data) => this.sendData(data)), | ||
terminal.onResize(({ cols, rows }) => { | ||
const msg = JSON.stringify({ columns: cols, rows: rows }); | ||
this.socket.send( | ||
this.textEncoder.encode(Command.RESIZE_TERMINAL + msg), | ||
); | ||
if (this.resizeOverlay) { | ||
this.getAddon<OverlayAddon>('overlay')?.showOverlay( | ||
`${cols}x${rows}`, | ||
300, | ||
); | ||
} | ||
}), | ||
terminal.onSelectionChange(() => { | ||
if (terminal.getSelection() === '') return; | ||
try { | ||
document.execCommand('copy'); | ||
} catch (e) { | ||
return; | ||
} | ||
this.getAddon<OverlayAddon>('overlay')?.showOverlay('\u2702', 200); | ||
}), | ||
addEventListener(window, 'beforeunload', this.onWindowUnload), | ||
); | ||
}; | ||
|
||
public connect = () => { | ||
this.socket = new WebSocket(this.ttydTerminalOptions.wsUrl, ['tty']); | ||
const { socket, register } = this; | ||
socket.binaryType = 'arraybuffer'; | ||
register( | ||
addEventListener(socket, 'open', this.onSocketOpen), | ||
addEventListener(socket, 'message', this.onSocketData as EventListener), | ||
addEventListener(socket, 'close', this.onSocketClose as EventListener), | ||
addEventListener(socket, 'error', () => (this.doReconnect = false)), | ||
); | ||
}; | ||
|
||
private onSocketOpen = () => { | ||
const { terminal, socket, textEncoder } = this; | ||
if (socket.readyState != 1) { | ||
return; | ||
} | ||
log('[ttyd] websocket connection opened', socket.readyState); | ||
|
||
const msg = JSON.stringify({ | ||
AuthToken: this.token, | ||
columns: terminal.cols, | ||
rows: terminal.rows, | ||
}); | ||
socket.send(textEncoder.encode(msg)); | ||
|
||
if (this.opened && terminal) { | ||
terminal.reset(); | ||
terminal.options.disableStdin = false; | ||
} else { | ||
this.opened = true; | ||
} | ||
|
||
this.doReconnect = this.reconnect; | ||
this.initTtydListeners(); | ||
terminal.focus(); | ||
}; | ||
private onSocketClose = (event: CloseEvent) => { | ||
log(`[ttyd] websocket connection closed with code: ${event.code}`); | ||
const { refreshToken, connect, doReconnect, terminal } = this; | ||
this.dispose(); | ||
|
||
// 1000: CLOSE_NORMAL | ||
if (event.code !== 1000 && doReconnect) { | ||
refreshToken().then(connect); | ||
} else { | ||
const keyDispose = terminal.onKey((e) => { | ||
const event = e.domEvent; | ||
if (event.key === 'Enter') { | ||
keyDispose.dispose(); | ||
refreshToken().then(connect); | ||
} | ||
}); | ||
} | ||
}; | ||
private onSocketData = (event: MessageEvent) => { | ||
const { textDecoder } = this; | ||
const rawData = event.data as ArrayBuffer; | ||
const cmd = String.fromCharCode(new Uint8Array(rawData)[0]); | ||
const data = rawData.slice(1); | ||
|
||
switch (cmd) { | ||
case Command.OUTPUT: | ||
this.writeFunc(data); | ||
break; | ||
case Command.SET_WINDOW_TITLE: | ||
this.title = textDecoder.decode(data); | ||
document.title = this.title; | ||
break; | ||
case Command.SET_PREFERENCES: | ||
this.applyPreferences({ | ||
...this.baseTerminalOptions.clientOptions, | ||
...JSON.parse(textDecoder.decode(data)), | ||
...this.parseOptsFromUrlQuery(window.location.search), | ||
} as Preferences); | ||
break; | ||
default: | ||
log(`[ttyd] unknown command: ${cmd}`); | ||
break; | ||
} | ||
}; | ||
private onWindowUnload = (event: BeforeUnloadEvent) => { | ||
event.preventDefault(); | ||
if (this.socket.readyState === WebSocket.OPEN) { | ||
return 'Close terminal? this will also terminate the command.'; | ||
} | ||
return undefined; | ||
}; | ||
|
||
public applyPreferences = (prefs: Preferences) => { | ||
log('preferences', prefs); | ||
const { terminal } = this; | ||
if (prefs.enableZmodem || prefs.enableTrzsz) { | ||
const zmodemAddon = new ZmodemAddon({ | ||
zmodem: prefs.enableZmodem, | ||
trzsz: prefs.enableTrzsz, | ||
windows: prefs.isWindows, | ||
trzszDragInitTimeout: prefs.trzszDragInitTimeout, | ||
onSend: this.sendCb, | ||
sender: this.sendData, | ||
writer: this.writeData, | ||
}); | ||
this.writeFunc = (data) => { | ||
return this.getAddon<ZmodemAddon>('zmodem')?.consume(data); | ||
}; | ||
this.setAddon('zmodem', zmodemAddon); | ||
} | ||
const opts = { | ||
...(terminal?.options as Record<string, unknown>), | ||
}; | ||
delete opts['cols']; | ||
delete opts['rows']; | ||
for (const [key, value] of Object.entries(prefs)) { | ||
switch (key) { | ||
case 'rendererType': | ||
this.setRendererType(value); | ||
break; | ||
// case 'disableLeaveAlert': | ||
// if (value) { | ||
// window.removeEventListener('beforeunload', this.onWindowUnload); | ||
// log('[ttyd] Leave site alert disabled'); | ||
// } | ||
// break; | ||
// case 'disableResizeOverlay': | ||
// if (value) { | ||
// log('[ttyd] Resize overlay disabled'); | ||
// this.resizeOverlay = false; | ||
// } | ||
// break; | ||
// case 'disableReconnect': | ||
// if (value) { | ||
// log('[ttyd] Reconnect disabled'); | ||
// this.reconnect = false; | ||
// this.doReconnect = false; | ||
// } | ||
// break; | ||
case 'enableZmodem': | ||
if (value) log('[ttyd] Zmodem enabled'); | ||
break; | ||
case 'enableTrzsz': | ||
if (value) log('[ttyd] trzsz enabled'); | ||
break; | ||
case 'trzszDragInitTimeout': | ||
if (value) log(`[ttyd] trzsz drag init timeout: ${value}`); | ||
break; | ||
// case 'enableSixel': | ||
// if (value) { | ||
// terminal?.loadAddon(register(new ImageAddon())); | ||
// log('[ttyd] Sixel enabled'); | ||
// } | ||
// break; | ||
// case 'titleFixed': | ||
// if (!value || value === '') return; | ||
// log(`[ttyd] setting fixed title: ${value}`); | ||
// this.titleFixed = value; | ||
// document.title = value; | ||
// break; | ||
case 'isWindows': | ||
if (value) log('[ttyd] is windows'); | ||
break; | ||
// case 'unicodeVersion': | ||
// switch (value) { | ||
// case 6: | ||
// case '6': | ||
// log('[ttyd] setting Unicode version: 6'); | ||
// break; | ||
// case 11: | ||
// case '11': | ||
// default: | ||
// log('[ttyd] setting Unicode version: 11'); | ||
// terminal?.loadAddon(new Unicode11Addon()); | ||
// terminal && (terminal.unicode.activeVersion = '11'); | ||
// break; | ||
// } | ||
// break; | ||
default: | ||
log(`[ttyd] option: ${key}=${JSON.stringify(value)}`); | ||
if (opts[key] instanceof Object) { | ||
opts[key] = Object.assign({}, opts[key], value); | ||
} else { | ||
opts[key] = value; | ||
} | ||
terminal && (terminal.options = opts); | ||
|
||
if (key.indexOf('font') === 0) { | ||
terminal.fit(); | ||
} | ||
break; | ||
} | ||
} | ||
}; | ||
} | ||
|
||
export default TtydTerminal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { ITerminalAddon, type ITerminalOptions, Terminal } from '@xterm/xterm'; | ||
|
||
export interface innerTerminal extends Terminal { | ||
fit(): void; | ||
} | ||
|
||
// AddonType should sync with `cat package.json|grep @xterm/addon-` | ||
export type AddonType = | ||
| 'webgl' | ||
| 'canvas' | ||
| 'fit' | ||
| 'clipboard' | ||
| 'search' | ||
| 'webLinks' | ||
| 'unicode11' | ||
| 'ligatures' | ||
| 'overlay' | ||
| 'zmodem'; | ||
|
||
export interface AddonInfo { | ||
name: string; | ||
ctor: ITerminalAddon; | ||
} | ||
|
||
export interface ClientOptions { | ||
rendererType: RendererType; | ||
disableLeaveAlert: boolean; | ||
disableResizeOverlay: boolean; | ||
enableZmodem: boolean; | ||
enableSixel: boolean; | ||
enableTrzsz: boolean; | ||
trzszDragInitTimeout: number; | ||
isWindows: boolean; | ||
unicodeVersion: string; | ||
} | ||
|
||
export interface BaseTerminalOptions { | ||
clientOptions: ClientOptions; | ||
xtermOptions: ITerminalOptions; | ||
} | ||
|
||
export type RendererType = 'dom' | 'canvas' | 'webgl'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import type { IDisposable } from '@xterm/xterm'; | ||
import debug from 'debug'; | ||
|
||
export function addEventListener( | ||
target: EventTarget, | ||
type: string, | ||
listener: EventListener, | ||
): IDisposable { | ||
target.addEventListener(type, listener); | ||
return toDisposable(() => target.removeEventListener(type, listener)); | ||
} | ||
|
||
export function toDisposable(f: () => void): IDisposable { | ||
return { dispose: f }; | ||
} | ||
|
||
const debuggerStore: Record<string, debug.Debugger> = {}; | ||
|
||
export function getDebugger(name: string) { | ||
if (name in debuggerStore) { | ||
return debuggerStore[name]; | ||
} | ||
debuggerStore[name] = debug(name); | ||
return debuggerStore[name]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "ES2020", | ||
"useDefineForClassFields": true, | ||
"lib": ["ES2020", "DOM", "DOM.Iterable"], | ||
"module": "ESNext", | ||
"skipLibCheck": true, | ||
|
||
/* Bundler mode */ | ||
"moduleResolution": "bundler", | ||
"allowImportingTsExtensions": true, | ||
"resolveJsonModule": true, | ||
"isolatedModules": true, | ||
"noEmit": true, | ||
"jsx": "react-jsx", | ||
|
||
/* Linting */ | ||
"strict": true, | ||
"noUnusedLocals": true, | ||
"noUnusedParameters": true, | ||
"noFallthroughCasesInSwitch": true | ||
}, | ||
"include": ["src", "typings.d.ts"], | ||
"references": [{ "path": "./tsconfig.node.json" }] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"compilerOptions": { | ||
"composite": true, | ||
"skipLibCheck": true, | ||
"module": "ESNext", | ||
"moduleResolution": "bundler", | ||
"allowSyntheticDefaultImports": true, | ||
"strict": true | ||
}, | ||
"include": ["vite.config.ts"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { defineConfig } from 'tsup'; | ||
|
||
export default defineConfig({ | ||
entry: ['src/index.tsx'], | ||
splitting: false, | ||
sourcemap: true, | ||
clean: true, | ||
format: ['cjs', 'esm'], | ||
// external: ['react'], | ||
dts: true, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
declare module 'sockjs-client/dist/sockjs' { | ||
import SockJS from '@types/sockjs-client'; | ||
export = SockJS; | ||
export as namespace SockJS; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
node_modules | ||
build | ||
coverage | ||
dist | ||
pnpm-* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"trailingComma": "all", | ||
"singleQuote": true, | ||
"printWidth": 80, | ||
"tabWidth": 2, | ||
"endOfLine": "auto" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# @karmada/xterm-addon-overlay | ||
|
||
## 1.0.1 | ||
|
||
### Patch Changes | ||
|
||
- update cjs&esm export for xterm-addon-overlay | ||
- add ttyd terminal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"name": "@karmada/xterm-addon-overlay", | ||
"version": "1.0.1", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"description": "", | ||
"exports": { | ||
".": { | ||
"require": "./dist/index.js", | ||
"import": "./dist/index.mjs" | ||
} | ||
}, | ||
"types": "dist/index.d.ts", | ||
"scripts": { | ||
"prepublish": "tsup --config ./tsup.config.ts", | ||
"build": "tsup --config ./tsup.config.ts" | ||
}, | ||
"lint-staged": { | ||
"**/*.{js,jsx,ts,tsx,json,css,md}": [ | ||
"prettier --write" | ||
] | ||
}, | ||
"keywords": [ | ||
"terminal", | ||
"xterm", | ||
"xterm.js" | ||
], | ||
"author": "", | ||
"license": "ISC", | ||
"peerDependencies": { | ||
"@xterm/xterm": "^5.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// ported from hterm.Terminal.prototype.showOverlay | ||
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js | ||
import { ITerminalAddon, Terminal } from '@xterm/xterm'; | ||
|
||
class OverlayAddon implements ITerminalAddon { | ||
private terminal: Terminal | null = null; | ||
private overlayNode: HTMLElement; | ||
private overlayTimeout?: number; | ||
|
||
constructor() { | ||
this.overlayNode = document.createElement('div'); | ||
this.overlayNode.style.cssText = `border-radius: 15px; | ||
font-size: xx-large; | ||
opacity: 0.75; | ||
padding: 0.2em 0.5em 0.2em 0.5em; | ||
position: absolute; | ||
-webkit-user-select: none; | ||
-webkit-transition: opacity 180ms ease-in; | ||
-moz-user-select: none; | ||
-moz-transition: opacity 180ms ease-in;`; | ||
|
||
this.overlayNode.addEventListener( | ||
'mousedown', | ||
(e) => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
}, | ||
true, | ||
); | ||
} | ||
|
||
activate(terminal: Terminal): void { | ||
this.terminal = terminal; | ||
} | ||
|
||
dispose(): void {} | ||
|
||
showOverlay(msg: string, timeout?: number): void { | ||
const { terminal, overlayNode } = this; | ||
if (!terminal || !terminal.element) return; | ||
|
||
overlayNode.style.color = '#101010'; | ||
overlayNode.style.backgroundColor = '#f0f0f0'; | ||
overlayNode.textContent = msg; | ||
overlayNode.style.opacity = '0.75'; | ||
|
||
if (!overlayNode.parentNode) { | ||
terminal.element.appendChild(overlayNode); | ||
} | ||
|
||
const divSize = terminal.element.getBoundingClientRect(); | ||
const overlaySize = overlayNode.getBoundingClientRect(); | ||
|
||
overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px'; | ||
overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px'; | ||
|
||
if (this.overlayTimeout) clearTimeout(this.overlayTimeout); | ||
if (!timeout) return; | ||
|
||
this.overlayTimeout = window.setTimeout(() => { | ||
overlayNode.style.opacity = '0'; | ||
this.overlayTimeout = window.setTimeout(() => { | ||
if (overlayNode.parentNode) { | ||
overlayNode.parentNode.removeChild(overlayNode); | ||
} | ||
this.overlayTimeout = undefined; | ||
overlayNode.style.opacity = '0.75'; | ||
}, 200); | ||
}, timeout || 1500); | ||
} | ||
} | ||
|
||
export default OverlayAddon; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "ES2020", | ||
"useDefineForClassFields": true, | ||
"lib": ["ES2020", "DOM", "DOM.Iterable"], | ||
"module": "ESNext", | ||
"skipLibCheck": true, | ||
|
||
/* Bundler mode */ | ||
"moduleResolution": "bundler", | ||
"allowImportingTsExtensions": true, | ||
"resolveJsonModule": true, | ||
"isolatedModules": true, | ||
"noEmit": true, | ||
"jsx": "react-jsx", | ||
|
||
/* Linting */ | ||
"strict": true, | ||
"noUnusedLocals": true, | ||
"noUnusedParameters": true, | ||
"noFallthroughCasesInSwitch": true | ||
}, | ||
"include": ["src"], | ||
"references": [{ "path": "./tsconfig.node.json" }] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"compilerOptions": { | ||
"composite": true, | ||
"skipLibCheck": true, | ||
"module": "ESNext", | ||
"moduleResolution": "bundler", | ||
"allowSyntheticDefaultImports": true, | ||
"strict": true | ||
}, | ||
"include": ["vite.config.ts"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { defineConfig } from 'tsup'; | ||
|
||
export default defineConfig({ | ||
entry: ['src/index.ts'], | ||
splitting: false, | ||
sourcemap: true, | ||
clean: true, | ||
format: ['cjs', 'esm'], | ||
dts: true, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
node_modules | ||
build | ||
coverage | ||
dist | ||
pnpm-* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"trailingComma": "all", | ||
"singleQuote": true, | ||
"printWidth": 80, | ||
"tabWidth": 2, | ||
"endOfLine": "auto" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# @karmada/xterm-addon-zmodem | ||
|
||
## 1.0.1 | ||
|
||
### Patch Changes | ||
|
||
- update cjs&esm export for xterm-addon-zmodem |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
{ | ||
"name": "@karmada/xterm-addon-zmodem", | ||
"version": "1.0.1", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"description": "", | ||
"exports": { | ||
".": { | ||
"require": "./dist/index.js", | ||
"import": "./dist/index.mjs" | ||
} | ||
}, | ||
"types": "dist/index.d.ts", | ||
"scripts": { | ||
"prepublish": "tsup --config ./tsup.config.ts", | ||
"build": "tsup --config ./tsup.config.ts" | ||
}, | ||
"lint-staged": { | ||
"**/*.{js,jsx,ts,tsx,json,css,md}": [ | ||
"prettier --write" | ||
] | ||
}, | ||
"keywords": [ | ||
"terminal", | ||
"xterm", | ||
"xterm.js" | ||
], | ||
"author": "", | ||
"license": "ISC", | ||
"dependencies": { | ||
"@karmada/xterm-addon-zmodem": "link:", | ||
"file-saver": "^2.0.5", | ||
"trzsz": "^1.1.5", | ||
"zmodem.js": "^0.1.10" | ||
}, | ||
"devDependencies": { | ||
"@types/file-saver": "^2.0.7" | ||
}, | ||
"peerDependencies": { | ||
"@xterm/xterm": "^5.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
import { saveAs } from 'file-saver'; | ||
import { IDisposable, ITerminalAddon, Terminal } from '@xterm/xterm'; | ||
// we've made some patch for the zmodem.js package, but pnpm doesn't support | ||
// sub package patchedDependencies field :https://github.com/pnpm/pnpm/issues/6048 | ||
// so we hoist the patchedDependencies to the root dir of pnpm monorepo: `ui` dir | ||
import * as Zmodem from 'zmodem.js/src/zmodem_browser'; | ||
import { TrzszFilter } from 'trzsz'; | ||
|
||
export interface ZmodemOptions { | ||
zmodem: boolean; | ||
trzsz: boolean; | ||
windows: boolean; | ||
trzszDragInitTimeout: number; | ||
onSend: () => void; | ||
sender: (data: string | Uint8Array) => void; | ||
writer: (data: string | Uint8Array) => void; | ||
} | ||
|
||
class ZmodemAddon implements ITerminalAddon { | ||
private disposables: IDisposable[] = []; | ||
private terminal: Terminal | null = null; | ||
private sentry!: Zmodem.Sentry; | ||
private session: Zmodem.Session; | ||
private denier: () => void = () => {}; | ||
private trzszFilter: TrzszFilter | null = null; | ||
|
||
constructor(private options: ZmodemOptions) {} | ||
|
||
activate(terminal: Terminal) { | ||
this.terminal = terminal; | ||
if (this.options.zmodem) this.zmodemInit(); | ||
if (this.options.trzsz) this.trzszInit(); | ||
} | ||
|
||
dispose() { | ||
for (const d of this.disposables) { | ||
d.dispose(); | ||
} | ||
this.disposables.length = 0; | ||
} | ||
|
||
consume(data: ArrayBuffer) { | ||
try { | ||
if (this.options.trzsz) { | ||
this.trzszFilter?.processServerOutput(data); | ||
} else { | ||
this.sentry.consume(data); | ||
} | ||
} catch (e) { | ||
console.error('[ttyd] zmodem consume: ', e); | ||
this.reset(); | ||
} | ||
} | ||
|
||
private reset() { | ||
if (!this.terminal) return; | ||
this.terminal.options.disableStdin = false; | ||
this.terminal.focus(); | ||
} | ||
|
||
private addDisposableListener( | ||
target: EventTarget, | ||
type: string, | ||
listener: EventListener, | ||
) { | ||
target.addEventListener(type, listener); | ||
this.disposables.push({ | ||
dispose: () => target.removeEventListener(type, listener), | ||
}); | ||
} | ||
|
||
private trzszInit() { | ||
const { terminal } = this; | ||
if (!terminal) return; | ||
const { sender, writer, zmodem } = this.options; | ||
this.trzszFilter = new TrzszFilter({ | ||
writeToTerminal: (data) => { | ||
if (!this.trzszFilter) return; | ||
if (!this.trzszFilter.isTransferringFiles() && zmodem) { | ||
this.sentry.consume(data); | ||
} else { | ||
writer( | ||
typeof data === 'string' | ||
? data | ||
: new Uint8Array(data as ArrayBuffer), | ||
); | ||
} | ||
}, | ||
sendToServer: (data) => sender(data), | ||
terminalColumns: terminal.cols, | ||
isWindowsShell: this.options.windows, | ||
dragInitTimeout: this.options.trzszDragInitTimeout, | ||
}); | ||
const element = terminal.element as EventTarget; | ||
this.addDisposableListener(element, 'dragover', (event) => | ||
event.preventDefault(), | ||
); | ||
this.addDisposableListener(element, 'drop', (event) => { | ||
event.preventDefault(); | ||
if (!this.trzszFilter) return; | ||
this.trzszFilter | ||
.uploadFiles( | ||
(event as DragEvent).dataTransfer?.items as DataTransferItemList, | ||
) | ||
.then(() => console.log('[ttyd] upload success')) | ||
.catch((err) => console.log('[ttyd] upload failed: ' + err)); | ||
}); | ||
this.disposables.push( | ||
terminal.onResize((size) => | ||
this.trzszFilter?.setTerminalColumns(size.cols), | ||
), | ||
); | ||
} | ||
|
||
private zmodemInit() { | ||
const { sender, writer } = this.options; | ||
const { terminal, reset, zmodemDetect } = this; | ||
if (!terminal) return; | ||
this.session = null; | ||
this.sentry = new Zmodem.Sentry({ | ||
to_terminal: (octets: Iterable<number>) => writer(new Uint8Array(octets)), | ||
sender: (octets: Iterable<number>) => sender(new Uint8Array(octets)), | ||
on_retract: () => reset(), | ||
on_detect: (detection: Zmodem.Detection) => zmodemDetect(detection), | ||
}); | ||
this.disposables.push( | ||
terminal.onKey((e) => { | ||
const event = e.domEvent; | ||
if (event.ctrlKey && event.key === 'c') { | ||
if (this.denier) this.denier(); | ||
} | ||
}), | ||
); | ||
} | ||
|
||
private zmodemDetect(detection: Zmodem.Detection): void { | ||
if (!this.terminal) return; | ||
const { terminal, receiveFile } = this; | ||
terminal.options.disableStdin = true; | ||
|
||
this.denier = () => detection.deny(); | ||
this.session = detection.confirm(); | ||
this.session.on('session_end', () => this.reset()); | ||
|
||
if (this.session.type === 'send') { | ||
this.options.onSend(); | ||
} else { | ||
receiveFile(); | ||
} | ||
} | ||
|
||
public sendFile(files: FileList) { | ||
const { session, writeProgress } = this; | ||
Zmodem.Browser.send_files(session, files, { | ||
on_progress: (_: any, offer: Zmodem.Offer) => writeProgress(offer), | ||
}) | ||
.then(() => session.close()) | ||
.catch(() => this.reset()); | ||
} | ||
|
||
private receiveFile() { | ||
const { session, writeProgress } = this; | ||
|
||
session.on('offer', (offer: Zmodem.Offer) => { | ||
offer.on('input', () => writeProgress(offer)); | ||
offer | ||
.accept() | ||
.then((payloads: any) => { | ||
const blob = new Blob(payloads, { type: 'application/octet-stream' }); | ||
saveAs(blob, offer.get_details().name); | ||
}) | ||
.catch(() => this.reset()); | ||
}); | ||
|
||
session.start(); | ||
} | ||
|
||
private writeProgress(offer: Zmodem.Offer) { | ||
const { bytesHuman } = this; | ||
const file = offer.get_details(); | ||
const name = file.name; | ||
const size = file.size; | ||
const offset = offer.get_offset(); | ||
const percent = ((100 * offset) / size).toFixed(2); | ||
|
||
this.options.writer( | ||
`${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r`, | ||
); | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
private bytesHuman(bytes: any, precision: number): string { | ||
if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) { | ||
return '-'; | ||
} | ||
if (bytes === 0) return '0'; | ||
if (typeof precision === 'undefined') precision = 1; | ||
const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; | ||
const num = Math.floor(Math.log(bytes) / Math.log(1024)); | ||
const value = (bytes / Math.pow(1024, Math.floor(num))).toFixed(precision); | ||
return `${value} ${units[num]}`; | ||
} | ||
} | ||
|
||
export default ZmodemAddon; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "ES2020", | ||
"useDefineForClassFields": true, | ||
"lib": ["ES2020", "DOM", "DOM.Iterable"], | ||
"module": "ESNext", | ||
"skipLibCheck": true, | ||
|
||
/* Bundler mode */ | ||
"moduleResolution": "bundler", | ||
"allowImportingTsExtensions": true, | ||
"resolveJsonModule": true, | ||
"isolatedModules": true, | ||
"noEmit": true, | ||
"jsx": "react-jsx", | ||
|
||
/* Linting */ | ||
"strict": true, | ||
"noUnusedLocals": true, | ||
"noUnusedParameters": true, | ||
"noFallthroughCasesInSwitch": true | ||
}, | ||
"include": ["src", "typing.d.ts"], | ||
"references": [{ "path": "./tsconfig.node.json" }] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"compilerOptions": { | ||
"composite": true, | ||
"skipLibCheck": true, | ||
"module": "ESNext", | ||
"moduleResolution": "bundler", | ||
"allowSyntheticDefaultImports": true, | ||
"strict": true | ||
}, | ||
"include": ["vite.config.ts"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { defineConfig } from 'tsup'; | ||
|
||
export default defineConfig({ | ||
entry: ['src/index.ts'], | ||
splitting: false, | ||
sourcemap: true, | ||
clean: true, | ||
format: ['cjs', 'esm'], | ||
dts: true, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
declare module 'zmodem.js/src/zmodem_browser' { | ||
type to_terminal = (octets: Iterable<number>) => void; | ||
|
||
export function sender(octets: Iterable<number>): void; | ||
|
||
export function on_retract(): void; | ||
|
||
export function on_detect(detection: Detection): void; | ||
|
||
export function on_progress(_: any, offer: Zmodem.Offer): void; | ||
|
||
class Sentry { | ||
constructor({ | ||
to_terminal: to_terminal, | ||
sender: sender, | ||
on_retract: on_retract, | ||
on_detect: on_detect, | ||
}); | ||
|
||
consume(data: string | ArrayBuffer | Uint8Array | Blob): void; | ||
} | ||
|
||
type Session = any; | ||
type Detection = any; | ||
type Offer = any; | ||
|
||
type send_files = ( | ||
session: Session, | ||
files: FileList, | ||
{ on_progress: on_progress }, | ||
) => Promise<void>; | ||
const Browser = { | ||
send_files: send_files, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
diff --git a/src/zsession.js b/src/zsession.js | ||
index 5f0b8f9d8afa6fba0acd6dd0477afa186f7aad9a..c7ea98e0f08c97d63d321f784a5dd8bf66888743 100644 | ||
--- a/src/zsession.js | ||
+++ b/src/zsession.js | ||
@@ -548,20 +548,17 @@ Zmodem.Session.Receive = class ZmodemReceiveSession extends Zmodem.Session { | ||
if (this._got_ZFIN) { | ||
if (this._input_buffer.length < 2) return; | ||
|
||
- //if it’s OO, then set this._bytes_after_OO | ||
- if (Zmodem.ZMLIB.find_subarray(this._input_buffer, OVER_AND_OUT) === 0) { | ||
+ if (Zmodem.ZMLIB.find_subarray(this._input_buffer, OVER_AND_OUT) !== 0) { | ||
+ console.warn( "PROTOCOL: Only thing after ZFIN should be “OO” (79,79), not: " + this._input_buffer.join() ); | ||
+ } | ||
|
||
- //This doubles as an indication that the session has ended. | ||
- //We need to set this right away so that handlers like | ||
- //"session_end" will have access to it. | ||
- this._bytes_after_OO = _trim_OO(this._bytes_being_consumed.slice(0)); | ||
- this._on_session_end(); | ||
+ //This doubles as an indication that the session has ended. | ||
+ //We need to set this right away so that handlers like | ||
+ //"session_end" will have access to it. | ||
+ this._bytes_after_OO = _trim_OO(this._bytes_being_consumed.slice(0)); | ||
+ this._on_session_end(); | ||
|
||
- return; | ||
- } | ||
- else { | ||
- throw( "PROTOCOL: Only thing after ZFIN should be “OO” (79,79), not: " + this._input_buffer.join() ); | ||
- } | ||
+ return; | ||
} | ||
|
||
var parsed; |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"$schema": "https://turbo.build/schema.json", | ||
"tasks": { | ||
"build": { | ||
"dependsOn": ["^build"], | ||
"outputs": ["dist/**"] | ||
}, | ||
"dev": { | ||
"persistent": true, | ||
"cache": false, | ||
"dependsOn": ["^build"] | ||
} | ||
} | ||
} |