import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; import * as webcontainer from '@webcontainer/api'; import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; declare global { interface HTMLElementTagNameMap { 'dees-terminal': DeesTerminal; } } @customElement('dees-terminal') export class DeesTerminal extends DeesElement { public static demo = () => html` `; // INSTANCE private resizeObserver: ResizeObserver; @property() public setupCommand = `pnpm install @git.zone/tsbuild && clear && echo 'welcome'`; constructor() { super(); this.resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { // Handle the resize event console.log(`Terminal Resized`); this.handleResize(); } }); } public static styles = [ cssManager.defaultStyles, css` :host { padding: 20px; background: #000; position: absolute; height: 100%; width: 100%; } * { box-sizing: border-box; } #container { position: absolute; height: calc(100% - 40px); width: calc(100% - 40px); } /** * Copyright (c) 2014 The xterm.js authors. All rights reserved. * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) * https://github.com/chjj/term.js * @license MIT * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * Originally forked from (with the author's permission): * Fabrice Bellard's javascript vt100 for jslinux: * http://bellard.org/jslinux/ * Copyright (c) 2011 Fabrice Bellard * The original design remains. The terminal itself * has been extended to include xterm CSI codes, among * other features. */ /** * Default styles for xterm.js */ .xterm { font-feature-settings: 'liga' 0; position: relative; user-select: none; -ms-user-select: none; -webkit-user-select: none; } .xterm.focus, .xterm:focus { outline: none; } .xterm .xterm-helpers { position: absolute; top: 0; /** * The z-index of the helpers must be higher than the canvases in order for * IMEs to appear on top. */ z-index: 5; } .xterm .xterm-helper-textarea { padding: 0; border: 0; margin: 0; /* Move textarea out of the screen to the far left, so that the cursor is not visible */ position: absolute; opacity: 0; left: -9999em; top: 0; width: 0; height: 0; z-index: -5; /** Prevent wrapping so the IME appears against the textarea at the correct position */ white-space: nowrap; overflow: hidden; resize: none; } .xterm .composition-view { /* TODO: Composition position got messed up somewhere */ background: #000; color: #fff; display: none; position: absolute; white-space: nowrap; z-index: 1; } .xterm .composition-view.active { display: block; } .xterm .xterm-viewport { /* On OS X this is required in order for the scroll bar to appear fully opaque */ background-color: #000; overflow-y: scroll; cursor: default; position: absolute; right: 0; left: 0; top: 0; bottom: 0; } .xterm .xterm-screen { position: relative; } .xterm .xterm-screen canvas { position: absolute; left: 0; top: 0; } .xterm .xterm-scroll-area { visibility: hidden; } .xterm-char-measure-element { display: inline-block; visibility: hidden; position: absolute; top: 0; left: -9999em; line-height: normal; } .xterm { cursor: text; } .xterm.enable-mouse-events { /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ cursor: default; } .xterm.xterm-cursor-pointer { cursor: pointer; } .xterm.column-select.focus { /* Column selection mode */ cursor: crosshair; } .xterm .xterm-accessibility, .xterm .xterm-message { position: absolute; left: 0; top: 0; bottom: 0; right: 0; z-index: 10; color: transparent; } .xterm .live-region { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; } .xterm-dim { opacity: 0.5; } .xterm-underline { text-decoration: underline; } `, ]; public render(): TemplateResult { return html`
`; } private fitAddon: FitAddon; public async firstUpdated( _changedProperties: Map ): Promise { const domtools = await this.domtoolsPromise; super.firstUpdated(_changedProperties); const container = this.shadowRoot.getElementById('container'); const term = new Terminal({ convertEol: true, cursorBlink: true, }); this.fitAddon = new FitAddon(); term.loadAddon(this.fitAddon); // Open the terminal in #terminal-container term.open(container); // Make the terminal's size and geometry fit the size of #terminal-container this.fitAddon.fit(); term.write(`dees-terminal custom terminal. \r\n$ `); // lets start the webcontainer // Call only once const webcontainerInstance = await webcontainer.WebContainer.boot(); const shellProcess = await webcontainerInstance.spawn('jsh'); shellProcess.output.pipeTo( new WritableStream({ write(data) { term.write(data); }, }) ); const input = shellProcess.input.getWriter(); term.onData((data) => { input.write(data); }); await this.waitForPrompt(term, '~/'); input.write(this.setupCommand); input.write(`\n`); } async connectedCallback(): Promise { await super.connectedCallback(); this.resizeObserver.observe(this); } async disconnectedCallback(): Promise { this.resizeObserver.unobserve(this); await super.disconnectedCallback(); } handleResize() { this.fitAddon.fit(); } private async waitForPrompt(term: Terminal, prompt: string): Promise { return new Promise((resolve) => { const checkPrompt = () => { const lines = term.buffer.active; for (let i = 0; i < lines.length; i++) { const line = lines.getLine(i); if (line && line.translateToString().includes(prompt)) { resolve(); return; } } setTimeout(checkPrompt, 100); // check every 100 ms }; checkPrompt(); }); } }