/** * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT * * This file is the entry point for browserify. */ /// import { Terminal } from '../lib/public/Terminal'; import * as attach from '../lib/addons/attach/attach'; import * as fit from '../lib/addons/fit/fit'; import * as fullscreen from '../lib/addons/fullscreen/fullscreen'; import * as search from '../lib/addons/search/search'; import * as webLinks from '../lib/addons/webLinks/webLinks'; import { ISearchOptions } from '../lib/addons/search/Interfaces'; // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module import { Terminal as TerminalType, ITerminalOptions } from 'xterm'; export interface IWindowWithTerminal extends Window { term: TerminalType; } declare let window: IWindowWithTerminal; Terminal.applyAddon(attach); Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(search); Terminal.applyAddon(webLinks); let term; let protocol; let socketURL; let socket; let pid; const terminalContainer = document.getElementById('terminal-container'); const actionElements = { findNext: document.querySelector('#find-next'), findPrevious: document.querySelector('#find-previous') }; const paddingElement = document.getElementById('padding'); function setPadding(): void { term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px'; term.fit(); } function getSearchOptions(): ISearchOptions { return { regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked }; } createTerminal(); const disposeRecreateButtonHandler = () => { // If the terminal exists dispose of it, otherwise recreate it if (term) { term.dispose(); term = null; window.term = null; socket = null; document.getElementById('dispose').innerHTML = 'Recreate Terminal'; } else { createTerminal(); document.getElementById('dispose').innerHTML = 'Dispose terminal'; } }; document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); function createTerminal(): void { // Clean terminal while (terminalContainer.children.length) { terminalContainer.removeChild(terminalContainer.children[0]); } const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; term = new Terminal({ windowsMode: isWindows } as ITerminalOptions); window.term = term; // Expose `term` to window for debugging purposes term.onResize((size: { cols: number, rows: number }) => { if (!pid) { return; } const cols = size.cols; const rows = size.rows; const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; fetch(url, {method: 'POST'}); }); protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; term.open(terminalContainer); term.webLinksInit(); term.fit(); term.focus(); addDomListener(paddingElement, 'change', setPadding); addDomListener(actionElements.findNext, 'keyup', (e) => { const searchOptions = getSearchOptions(); searchOptions.incremental = e.key !== `Enter`; term.findNext(actionElements.findNext.value, searchOptions); }); addDomListener(actionElements.findPrevious, 'keyup', (e) => { if (e.key === `Enter`) { term.findPrevious(actionElements.findPrevious.value, getSearchOptions()); } }); // fit is called within a setTimeout, cols and rows need this. setTimeout(() => { initOptions(term); // TODO: Clean this up, opt-cols/rows doesn't exist anymore (document.getElementById(`opt-cols`)).value = term.cols; (document.getElementById(`opt-rows`)).value = term.rows; paddingElement.value = '0'; // Set terminal size again to set the specific dimensions on the demo updateTerminalSize(); fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then((res) => { res.text().then((processId) => { pid = processId; socketURL += processId; socket = new WebSocket(socketURL); socket.onopen = runRealTerminal; socket.onclose = runFakeTerminal; socket.onerror = runFakeTerminal; }); }); }, 0); } function runRealTerminal(): void { term.attach(socket); term._initialized = true; } function runFakeTerminal(): void { if (term._initialized) { return; } term._initialized = true; term.prompt = () => { term.write('\r\n$ '); }; term.writeln('Welcome to xterm.js'); term.writeln('This is a local terminal emulation, without a real terminal in the back-end.'); term.writeln('Type some keys and commands to play around.'); term.writeln(''); term.prompt(); term.onKey((e: { key: string, domEvent: KeyboardEvent }) => { const ev = e.domEvent; const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey; if (ev.keyCode === 13) { term.prompt(); } else if (ev.keyCode === 8) { // Do not delete the prompt if (term._core.buffer.x > 2) { term.write('\b \b'); } } else if (printable) { term.write(e.key); } }); } function initOptions(term: TerminalType): void { const blacklistedOptions = [ // Internal only options 'cancelEvents', 'convertEol', 'debug', 'handler', 'screenKeys', 'termName', 'useFlowControl', // Complex option 'theme' ]; const stringOptions = { bellSound: null, bellStyle: ['none', 'sound'], cursorStyle: ['block', 'underline', 'bar'], experimentalCharAtlas: ['none', 'static', 'dynamic'], fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], rendererType: ['dom', 'canvas'] }; const options = Object.keys((term)._core.options); const booleanOptions = []; const numberOptions = []; options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => { switch (typeof term.getOption(o)) { case 'boolean': booleanOptions.push(o); break; case 'number': numberOptions.push(o); break; default: if (Object.keys(stringOptions).indexOf(o) === -1) { console.warn(`Unrecognized option: "${o}"`); } } }); let html = ''; html += '
'; booleanOptions.forEach(o => { html += `
`; }); html += '
'; numberOptions.forEach(o => { html += `
`; }); html += '
'; Object.keys(stringOptions).forEach(o => { if (stringOptions[o]) { html += `
`; } else { html += `
`; } }); html += '
'; const container = document.getElementById('options-container'); container.innerHTML = html; // Attach listeners booleanOptions.forEach(o => { const input = document.getElementById(`opt-${o}`); addDomListener(input, 'change', () => { console.log('change', o, input.checked); term.setOption(o, input.checked); }); }); numberOptions.forEach(o => { const input = document.getElementById(`opt-${o}`); addDomListener(input, 'change', () => { console.log('change', o, input.value); if (o === 'cols' || o === 'rows') { updateTerminalSize(); } else { term.setOption(o, o === 'lineHeight' ? parseFloat(input.value) : parseInt(input.value, 10)); } }); }); Object.keys(stringOptions).forEach(o => { const input = document.getElementById(`opt-${o}`); addDomListener(input, 'change', () => { console.log('change', o, input.value); term.setOption(o, input.value); }); }); } function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void { element.addEventListener(type, handler); term._core.register({ dispose: () => element.removeEventListener(type, handler) }); } function updateTerminalSize(): void { const cols = parseInt((document.getElementById(`opt-cols`)).value, 10); const rows = parseInt((document.getElementById(`opt-rows`)).value, 10); const width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px'; const height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px'; terminalContainer.style.width = width; terminalContainer.style.height = height; term.fit(); }