| 
									
										
										
										
											2024-01-22 17:12:58 +01:00
										 |  |  | 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 { | 
					
						
							| 
									
										
										
										
											2025-01-20 03:43:20 +01:00
										 |  |  |   public static demo = () => html` <dees-terminal
 | 
					
						
							|  |  |  |     .environment=${{ | 
					
						
							|  |  |  |       NODE_ENV: 'development', | 
					
						
							|  |  |  |       PORT: '3000', | 
					
						
							|  |  |  |     }} | 
					
						
							|  |  |  |   ></dees-terminal> `; | 
					
						
							| 
									
										
										
										
											2024-01-22 17:12:58 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-22 19:23:22 +01:00
										 |  |  |   // INSTANCE
 | 
					
						
							|  |  |  |   private resizeObserver: ResizeObserver; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-24 00:59:11 +01:00
										 |  |  |   @property() | 
					
						
							| 
									
										
										
										
											2025-01-20 03:43:20 +01:00
										 |  |  |   public setupCommand = `pnpm install @serve.zone/cli && servezone cli\n`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @property() | 
					
						
							|  |  |  |   environment: {[key: string]: string} = {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // exposing webcontainer
 | 
					
						
							|  |  |  |   private webcontainerDeferred = new domtools.plugins.smartpromise.Deferred<webcontainer.WebContainer>(); | 
					
						
							|  |  |  |   public webcontainerPromise = this.webcontainerDeferred.promise; | 
					
						
							| 
									
										
										
										
											2024-01-24 00:59:11 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-22 17:12:58 +01:00
										 |  |  |   constructor() { | 
					
						
							|  |  |  |     super(); | 
					
						
							| 
									
										
										
										
											2024-01-22 19:23:22 +01:00
										 |  |  |     this.resizeObserver = new ResizeObserver((entries) => { | 
					
						
							|  |  |  |       for (const entry of entries) { | 
					
						
							|  |  |  |         // Handle the resize event
 | 
					
						
							|  |  |  |         console.log(`Terminal Resized`); | 
					
						
							|  |  |  |         this.handleResize(); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2024-01-22 17:12:58 +01:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public static styles = [ | 
					
						
							| 
									
										
										
										
											2024-02-05 13:13:02 +01:00
										 |  |  |     cssManager.defaultStyles, | 
					
						
							| 
									
										
										
										
											2024-01-22 17:12:58 +01:00
										 |  |  |     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`
 | 
					
						
							|  |  |  |       <div class="mainbox"> | 
					
						
							|  |  |  |         <div id="container"></div> | 
					
						
							|  |  |  |       </div> | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-22 19:23:22 +01:00
										 |  |  |   private fitAddon: FitAddon; | 
					
						
							| 
									
										
										
										
											2024-01-22 17:12:58 +01:00
										 |  |  |   public async firstUpdated( | 
					
						
							|  |  |  |     _changedProperties: Map<string | number | symbol, unknown> | 
					
						
							|  |  |  |   ): Promise<void> { | 
					
						
							|  |  |  |     const domtools = await this.domtoolsPromise; | 
					
						
							|  |  |  |     super.firstUpdated(_changedProperties); | 
					
						
							|  |  |  |     const container = this.shadowRoot.getElementById('container'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const term = new Terminal({ | 
					
						
							|  |  |  |       convertEol: true, | 
					
						
							|  |  |  |       cursorBlink: true, | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2024-01-22 19:23:22 +01:00
										 |  |  |     this.fitAddon = new FitAddon(); | 
					
						
							|  |  |  |     term.loadAddon(this.fitAddon); | 
					
						
							| 
									
										
										
										
											2024-01-22 17:12:58 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Open the terminal in #terminal-container
 | 
					
						
							|  |  |  |     term.open(container); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Make the terminal's size and geometry fit the size of #terminal-container
 | 
					
						
							| 
									
										
										
										
											2024-01-22 19:23:22 +01:00
										 |  |  |     this.fitAddon.fit(); | 
					
						
							| 
									
										
										
										
											2024-01-22 17:12:58 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2024-01-24 00:59:11 +01:00
										 |  |  |     await this.waitForPrompt(term, '~/'); | 
					
						
							| 
									
										
										
										
											2025-01-20 03:43:20 +01:00
										 |  |  |     // lets set the environment variables
 | 
					
						
							|  |  |  |     await this.setEnvironmentVariables(this.environment, webcontainerInstance); | 
					
						
							|  |  |  |     input.write(`source source.env\n`); | 
					
						
							|  |  |  |     await this.waitForPrompt(term, '~/'); | 
					
						
							|  |  |  |     // lets run the setup command
 | 
					
						
							| 
									
										
										
										
											2024-01-24 00:59:11 +01:00
										 |  |  |     input.write(this.setupCommand); | 
					
						
							| 
									
										
										
										
											2025-01-20 03:43:20 +01:00
										 |  |  |     await this.waitForPrompt(term, '~/'); | 
					
						
							|  |  |  |     input.write(`clear && echo 'welcome'\n`); | 
					
						
							|  |  |  |     this.webcontainerDeferred.resolve(webcontainerInstance); | 
					
						
							| 
									
										
										
										
											2024-01-22 17:12:58 +01:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-01-22 19:23:22 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |   async connectedCallback(): Promise<void> { | 
					
						
							|  |  |  |     await super.connectedCallback(); | 
					
						
							|  |  |  |     this.resizeObserver.observe(this); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async disconnectedCallback(): Promise<void> { | 
					
						
							|  |  |  |     this.resizeObserver.unobserve(this); | 
					
						
							|  |  |  |     await super.disconnectedCallback(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   handleResize() { | 
					
						
							|  |  |  |     this.fitAddon.fit(); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-01-24 00:59:11 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-20 03:43:20 +01:00
										 |  |  |   public async waitForPrompt(term: Terminal, prompt: string): Promise<void> { | 
					
						
							| 
									
										
										
										
											2024-01-24 00:59:11 +01:00
										 |  |  |     return new Promise<void>((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)) { | 
					
						
							| 
									
										
										
										
											2025-01-20 03:43:20 +01:00
										 |  |  |             setTimeout(() => { | 
					
						
							|  |  |  |               resolve(); | 
					
						
							|  |  |  |             }, 100); | 
					
						
							| 
									
										
										
										
											2024-01-24 00:59:11 +01:00
										 |  |  |             return; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         setTimeout(checkPrompt, 100); // check every 100 ms
 | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |    | 
					
						
							|  |  |  |       checkPrompt(); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-01-20 03:43:20 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |   public async setEnvironmentVariables(envArg: {[key: string]: string}, webcontainerInstanceArg?: webcontainer.WebContainer) { | 
					
						
							|  |  |  |     const webcontainerInstance = webcontainerInstanceArg ||await this.webcontainerPromise; | 
					
						
							|  |  |  |     let envFile = `` | 
					
						
							|  |  |  |     for (const key in envArg) {  | 
					
						
							|  |  |  |       envFile += `export ${key}="${envArg[key]}"\n`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     await webcontainerInstance.mount({'source.env': { | 
					
						
							|  |  |  |       file: { | 
					
						
							|  |  |  |         contents: envFile, | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }}); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-01-22 17:12:58 +01:00
										 |  |  | } |