parent
731aecd556
commit
12c0894079
@ -0,0 +1,95 @@ |
|||||||
|
/** |
||||||
|
* Copyright (c) 2017 The xterm.js authors. All rights reserved. |
||||||
|
* @license MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
import { Terminal, ITerminalAddon } from 'xterm'; |
||||||
|
|
||||||
|
|
||||||
|
interface ITerminalDimensions { |
||||||
|
/** |
||||||
|
* The number of rows in the terminal. |
||||||
|
*/ |
||||||
|
rows: number; |
||||||
|
|
||||||
|
/** |
||||||
|
* The number of columns in the terminal. |
||||||
|
*/ |
||||||
|
cols: number; |
||||||
|
} |
||||||
|
|
||||||
|
const MINIMUM_COLS = 2; |
||||||
|
const MINIMUM_ROWS = 1; |
||||||
|
|
||||||
|
export class FitAddon implements ITerminalAddon { |
||||||
|
private _terminal: Terminal | undefined; |
||||||
|
|
||||||
|
constructor() {} |
||||||
|
|
||||||
|
public activate(terminal: Terminal): void { |
||||||
|
console.log('FitAddon activate') |
||||||
|
this._terminal = terminal; |
||||||
|
} |
||||||
|
|
||||||
|
public dispose(): void {} |
||||||
|
|
||||||
|
public fit(): void { |
||||||
|
const dims = this.proposeDimensions(); |
||||||
|
console.log('FitAddon fit', dims) |
||||||
|
if (!dims || !this._terminal || isNaN(dims.cols) || isNaN(dims.rows)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Remove reliance on private API
|
||||||
|
const core = (this._terminal as any)._core; |
||||||
|
|
||||||
|
// Force a full render
|
||||||
|
if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) { |
||||||
|
core._renderService.clear(); |
||||||
|
this._terminal.resize(dims.cols, dims.rows); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public proposeDimensions(): ITerminalDimensions | undefined { |
||||||
|
if (!this._terminal) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
|
||||||
|
if (!this._terminal.element || !this._terminal.element.parentElement) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Remove reliance on private API
|
||||||
|
const core = (this._terminal as any)._core; |
||||||
|
const dims = core._renderService.dimensions; |
||||||
|
|
||||||
|
if (dims.css.cell.width === 0 || dims.css.cell.height === 0) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
|
||||||
|
const scrollbarWidth = this._terminal.options.scrollback === 0 ? |
||||||
|
0 : core.viewport.scrollBarWidth; |
||||||
|
|
||||||
|
console.log('parentElementStyle', this._terminal.element.parentElement) |
||||||
|
|
||||||
|
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement); |
||||||
|
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); |
||||||
|
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'))); |
||||||
|
const elementStyle = window.getComputedStyle(this._terminal.element); |
||||||
|
const elementPadding = { |
||||||
|
top: parseInt(elementStyle.getPropertyValue('padding-top')), |
||||||
|
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')), |
||||||
|
right: parseInt(elementStyle.getPropertyValue('padding-right')), |
||||||
|
left: parseInt(elementStyle.getPropertyValue('padding-left')) |
||||||
|
}; |
||||||
|
const elementPaddingVer = elementPadding.top + elementPadding.bottom; |
||||||
|
const elementPaddingHor = elementPadding.right + elementPadding.left; |
||||||
|
const availableHeight = parentElementHeight - elementPaddingVer; |
||||||
|
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth; |
||||||
|
const geometry = { |
||||||
|
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)), |
||||||
|
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height)) |
||||||
|
}; |
||||||
|
return geometry; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,240 @@ |
|||||||
|
import * as React from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
|
||||||
|
import 'xterm/css/xterm.css' |
||||||
|
|
||||||
|
// We are using these as types.
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Terminal, ITerminalOptions, ITerminalAddon } from 'xterm' |
||||||
|
|
||||||
|
interface IProps { |
||||||
|
/** |
||||||
|
* Class name to add to the terminal container. |
||||||
|
*/ |
||||||
|
className?: string |
||||||
|
|
||||||
|
/** |
||||||
|
* Options to initialize the terminal with. |
||||||
|
*/ |
||||||
|
options?: ITerminalOptions |
||||||
|
|
||||||
|
/** |
||||||
|
* An array of XTerm addons to load along with the terminal. |
||||||
|
*/ |
||||||
|
addons?: Array<ITerminalAddon> |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds an event listener for when a binary event fires. This is used to |
||||||
|
* enable non UTF-8 conformant binary messages to be sent to the backend. |
||||||
|
* Currently this is only used for a certain type of mouse reports that |
||||||
|
* happen to be not UTF-8 compatible. |
||||||
|
* The event value is a JS string, pass it to the underlying pty as |
||||||
|
* binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`. |
||||||
|
*/ |
||||||
|
onBinary?(data: string): void |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds an event listener for the cursor moves. |
||||||
|
*/ |
||||||
|
onCursorMove?(): void |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds an event listener for when a data event fires. This happens for |
||||||
|
* example when the user types or pastes into the terminal. The event value |
||||||
|
* is whatever `string` results, in a typical setup, this should be passed |
||||||
|
* on to the backing pty. |
||||||
|
*/ |
||||||
|
onData?(data: string): void |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds an event listener for when a key is pressed. The event value contains the |
||||||
|
* string that will be sent in the data event as well as the DOM event that |
||||||
|
* triggered it. |
||||||
|
*/ |
||||||
|
onKey?(event: { key: string; domEvent: KeyboardEvent }): void |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds an event listener for when a line feed is added. |
||||||
|
*/ |
||||||
|
onLineFeed?(): void |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds an event listener for when a scroll occurs. The event value is the |
||||||
|
* new position of the viewport. |
||||||
|
* @returns an `IDisposable` to stop listening. |
||||||
|
*/ |
||||||
|
onScroll?(newPosition: number): void |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds an event listener for when a selection change occurs. |
||||||
|
*/ |
||||||
|
onSelectionChange?(): void |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds an event listener for when rows are rendered. The event value |
||||||
|
* contains the start row and end rows of the rendered area (ranges from `0` |
||||||
|
* to `Terminal.rows - 1`). |
||||||
|
*/ |
||||||
|
onRender?(event: { start: number; end: number }): void |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds an event listener for when the terminal is resized. The event value |
||||||
|
* contains the new size. |
||||||
|
*/ |
||||||
|
onResize?(event: { cols: number; rows: number }): void |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds an event listener for when an OSC 0 or OSC 2 title change occurs. |
||||||
|
* The event value is the new title. |
||||||
|
*/ |
||||||
|
onTitleChange?(newTitle: string): void |
||||||
|
|
||||||
|
/** |
||||||
|
* Attaches a custom key event handler which is run before keys are |
||||||
|
* processed, giving consumers of xterm.js ultimate control as to what keys |
||||||
|
* should be processed by the terminal and what keys should not. |
||||||
|
* |
||||||
|
* @param event The custom KeyboardEvent handler to attach. |
||||||
|
* This is a function that takes a KeyboardEvent, allowing consumers to stop |
||||||
|
* propagation and/or prevent the default action. The function returns |
||||||
|
* whether the event should be processed by xterm.js. |
||||||
|
*/ |
||||||
|
customKeyEventHandler?(event: KeyboardEvent): boolean |
||||||
|
} |
||||||
|
|
||||||
|
export class Xterm extends React.Component<IProps> { |
||||||
|
/** |
||||||
|
* The ref for the containing element. |
||||||
|
*/ |
||||||
|
terminalRef: React.RefObject<HTMLDivElement> |
||||||
|
|
||||||
|
/** |
||||||
|
* XTerm.js Terminal object. |
||||||
|
*/ |
||||||
|
terminal!: Terminal // This is assigned in the setupTerminal() which is called from the constructor
|
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
className: PropTypes.string, |
||||||
|
options: PropTypes.object, |
||||||
|
addons: PropTypes.array, |
||||||
|
onBinary: PropTypes.func, |
||||||
|
onCursorMove: PropTypes.func, |
||||||
|
onData: PropTypes.func, |
||||||
|
onKey: PropTypes.func, |
||||||
|
onLineFeed: PropTypes.func, |
||||||
|
onScroll: PropTypes.func, |
||||||
|
onSelectionChange: PropTypes.func, |
||||||
|
onRender: PropTypes.func, |
||||||
|
onResize: PropTypes.func, |
||||||
|
onTitleChange: PropTypes.func, |
||||||
|
customKeyEventHandler: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
constructor(props: IProps) { |
||||||
|
super(props) |
||||||
|
|
||||||
|
console.log('Xterm constructor') |
||||||
|
|
||||||
|
this.terminalRef = React.createRef() |
||||||
|
|
||||||
|
// Bind Methods
|
||||||
|
this.onData = this.onData.bind(this) |
||||||
|
this.onCursorMove = this.onCursorMove.bind(this) |
||||||
|
this.onKey = this.onKey.bind(this) |
||||||
|
this.onBinary = this.onBinary.bind(this) |
||||||
|
this.onLineFeed = this.onLineFeed.bind(this) |
||||||
|
this.onScroll = this.onScroll.bind(this) |
||||||
|
this.onSelectionChange = this.onSelectionChange.bind(this) |
||||||
|
this.onRender = this.onRender.bind(this) |
||||||
|
this.onResize = this.onResize.bind(this) |
||||||
|
this.onTitleChange = this.onTitleChange.bind(this) |
||||||
|
|
||||||
|
this.setupTerminal() |
||||||
|
} |
||||||
|
|
||||||
|
setupTerminal() { |
||||||
|
// Setup the XTerm terminal.
|
||||||
|
this.terminal = new Terminal(this.props.options) |
||||||
|
|
||||||
|
// Load addons if the prop exists.
|
||||||
|
if (this.props.addons) { |
||||||
|
this.props.addons.forEach((addon) => { |
||||||
|
console.log('addon', addon) |
||||||
|
this.terminal.loadAddon(addon) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Create Listeners
|
||||||
|
this.terminal.onBinary(this.onBinary) |
||||||
|
this.terminal.onCursorMove(this.onCursorMove) |
||||||
|
this.terminal.onData(this.onData) |
||||||
|
this.terminal.onKey(this.onKey) |
||||||
|
this.terminal.onLineFeed(this.onLineFeed) |
||||||
|
this.terminal.onScroll(this.onScroll) |
||||||
|
this.terminal.onSelectionChange(this.onSelectionChange) |
||||||
|
this.terminal.onRender(this.onRender) |
||||||
|
this.terminal.onResize(this.onResize) |
||||||
|
this.terminal.onTitleChange(this.onTitleChange) |
||||||
|
|
||||||
|
// Add Custom Key Event Handler
|
||||||
|
if (this.props.customKeyEventHandler) { |
||||||
|
this.terminal.attachCustomKeyEventHandler(this.props.customKeyEventHandler) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
componentDidMount() { |
||||||
|
if (this.terminalRef.current) { |
||||||
|
// Creates the terminal within the container element.
|
||||||
|
this.terminal.open(this.terminalRef.current) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
componentWillUnmount() { |
||||||
|
// When the component unmounts dispose of the terminal and all of its listeners.
|
||||||
|
this.terminal.dispose() |
||||||
|
} |
||||||
|
|
||||||
|
private onBinary(data: string) { |
||||||
|
if (this.props.onBinary) this.props.onBinary(data) |
||||||
|
} |
||||||
|
|
||||||
|
private onCursorMove() { |
||||||
|
if (this.props.onCursorMove) this.props.onCursorMove() |
||||||
|
} |
||||||
|
|
||||||
|
private onData(data: string) { |
||||||
|
if (this.props.onData) this.props.onData(data) |
||||||
|
} |
||||||
|
|
||||||
|
private onKey(event: { key: string; domEvent: KeyboardEvent }) { |
||||||
|
if (this.props.onKey) this.props.onKey(event) |
||||||
|
} |
||||||
|
|
||||||
|
private onLineFeed() { |
||||||
|
if (this.props.onLineFeed) this.props.onLineFeed() |
||||||
|
} |
||||||
|
|
||||||
|
private onScroll(newPosition: number) { |
||||||
|
if (this.props.onScroll) this.props.onScroll(newPosition) |
||||||
|
} |
||||||
|
|
||||||
|
private onSelectionChange() { |
||||||
|
if (this.props.onSelectionChange) this.props.onSelectionChange() |
||||||
|
} |
||||||
|
|
||||||
|
private onRender(event: { start: number; end: number }) { |
||||||
|
if (this.props.onRender) this.props.onRender(event) |
||||||
|
} |
||||||
|
|
||||||
|
private onResize(event: { cols: number; rows: number }) { |
||||||
|
if (this.props.onResize) this.props.onResize(event) |
||||||
|
} |
||||||
|
|
||||||
|
private onTitleChange(newTitle: string) { |
||||||
|
if (this.props.onTitleChange) this.props.onTitleChange(newTitle) |
||||||
|
} |
||||||
|
|
||||||
|
render() { |
||||||
|
return <div className={this.props.className} ref={this.terminalRef} /> |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue