diff --git a/src/MandoCode.Desktop/Assets/web/terminal/terminal.html b/src/MandoCode.Desktop/Assets/web/terminal/terminal.html new file mode 100644 index 0000000..70e520b --- /dev/null +++ b/src/MandoCode.Desktop/Assets/web/terminal/terminal.html @@ -0,0 +1,24 @@ + + +
+ + + + + + + + + + + + diff --git a/src/MandoCode.Desktop/Assets/web/terminal/terminal.js b/src/MandoCode.Desktop/Assets/web/terminal/terminal.js new file mode 100644 index 0000000..e2c009b --- /dev/null +++ b/src/MandoCode.Desktop/Assets/web/terminal/terminal.js @@ -0,0 +1,136 @@ +// Terminal front-end: hosts one xterm.js instance per shell tab inside a single +// WebView2, and bridges keystrokes/output/resize to the C# TerminalPanel. Output +// bytes arrive base64-encoded (raw VT from ConPTY) and are handed to xterm untouched. +// +// Design note (see the tool-call-pill history in project memory): nothing here may +// repaint indefinitely. cursorBlink is OFF, so xterm only repaints on actual output. +(function () { + "use strict"; + + const TerminalCtor = window.Terminal; + const FitAddonNS = window.FitAddon; + const host = document.getElementById("host"); + const terms = Object.create(null); // id -> { term, fit, el } + + function post(obj) { + try { window.chrome.webview.postMessage(JSON.stringify(obj)); } catch (e) { } + } + + function makeTheme(t) { + return t || { + background: "#0b0b12", + foreground: "#e6e6ef", + cursor: "#f2c14e", + cursorAccent: "#0b0b12", + selectionBackground: "rgba(242,193,78,0.30)", + black: "#1e1e28", red: "#ff6b6b", green: "#7ee787", yellow: "#f2c14e", + blue: "#79c0ff", magenta: "#d2a8ff", cyan: "#56d4dd", white: "#d0d0e0", + brightBlack: "#6a6a80", brightRed: "#ffa198", brightGreen: "#aff5b4", + brightYellow: "#ffd479", brightBlue: "#a5d6ff", brightMagenta: "#e2c5ff", + brightCyan: "#b3f0f5", brightWhite: "#ffffff" + }; + } + + function create(id, cols, rows, theme) { + if (terms[id]) return; + + const el = document.createElement("div"); + el.className = "term"; + el.style.display = "none"; + host.appendChild(el); + + const term = new TerminalCtor({ + cols: cols || 80, + rows: rows || 24, + cursorBlink: false, // no perpetual repaint loop + cursorStyle: "bar", + fontFamily: "Cascadia Mono, Consolas, 'Courier New', monospace", + fontSize: 13, + lineHeight: 1.1, + theme: makeTheme(theme), + scrollback: 5000, + allowProposedApi: true + }); + + const fit = new FitAddonNS.FitAddon(); + term.loadAddon(fit); + term.open(el); + + // Keystrokes / pasted text -> C# -> shell stdin. + term.onData(d => post({ type: "data", id: id, data: d })); + term.onBinary(d => post({ type: "data", id: id, data: d })); + + terms[id] = { term: term, fit: fit, el: el }; + } + + function write(id, b64) { + const t = terms[id]; + if (!t) return; + const bin = atob(b64); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + t.term.write(bytes); + } + + function fit(id) { + const t = terms[id]; + if (!t || t.el.style.display === "none") return; + try { + t.fit.fit(); + post({ type: "resize", id: id, cols: t.term.cols, rows: t.term.rows }); + } catch (e) { } + } + + function show(id) { + for (const k in terms) terms[k].el.style.display = (k === id) ? "block" : "none"; + fit(id); + focus(id); + } + + function focus(id) { + const t = terms[id]; + if (t) setTimeout(() => { try { t.term.focus(); } catch (e) { } }, 0); + } + + function dispose(id) { + const t = terms[id]; + if (!t) return; + try { t.term.dispose(); } catch (e) { } + t.el.remove(); + delete terms[id]; + } + + function exited(id, message) { + const t = terms[id]; + if (!t) return; + t.term.write("\r\n\x1b[38;5;244m" + (message || "[process exited]") + "\x1b[0m\r\n"); + } + + function setTheme(theme) { + for (const k in terms) terms[k].term.options.theme = makeTheme(theme); + } + + // C# -> JS (host.PostWebMessageAsJson -> parsed object on e.data). + window.chrome.webview.addEventListener("message", function (e) { + const m = e.data; + if (!m || !m.type) return; + switch (m.type) { + case "create": create(m.id, m.cols, m.rows, m.theme); break; + case "write": write(m.id, m.data); break; + case "show": show(m.id); break; + case "fit": fit(m.id); break; + case "focus": focus(m.id); break; + case "dispose": dispose(m.id); break; + case "exited": exited(m.id, m.message); break; + case "theme": setTheme(m.theme); break; + } + }); + + // Refit the visible terminal when the panel is resized (fires only on change). + const ro = new ResizeObserver(function () { + for (const k in terms) if (terms[k].el.style.display !== "none") fit(k); + }); + ro.observe(host); + + post({ type: "ready" }); +})(); diff --git a/src/MandoCode.Desktop/Assets/web/terminal/xterm-addon-fit.js b/src/MandoCode.Desktop/Assets/web/terminal/xterm-addon-fit.js new file mode 100644 index 0000000..7cfd9de --- /dev/null +++ b/src/MandoCode.Desktop/Assets/web/terminal/xterm-addon-fit.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})())); +//# sourceMappingURL=xterm-addon-fit.js.map \ No newline at end of file diff --git a/src/MandoCode.Desktop/Assets/web/terminal/xterm.css b/src/MandoCode.Desktop/Assets/web/terminal/xterm.css new file mode 100644 index 0000000..74acc26 --- /dev/null +++ b/src/MandoCode.Desktop/Assets/web/terminal/xterm.css @@ -0,0 +1,209 @@ +/** + * 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 { + cursor: text; + 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.enable-mouse-events { + /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ + cursor: default; +} + +.xterm.xterm-cursor-pointer, +.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; + pointer-events: none; +} + +.xterm .live-region { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; +} + +.xterm-dim { + /* Dim should not apply to background, so the opacity of the foreground color is applied + * explicitly in the generated class and reset to 1 here */ + opacity: 1 !important; +} + +.xterm-underline-1 { text-decoration: underline; } +.xterm-underline-2 { text-decoration: double underline; } +.xterm-underline-3 { text-decoration: wavy underline; } +.xterm-underline-4 { text-decoration: dotted underline; } +.xterm-underline-5 { text-decoration: dashed underline; } + +.xterm-overline { + text-decoration: overline; +} + +.xterm-overline.xterm-underline-1 { text-decoration: overline underline; } +.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; } +.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; } +.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; } +.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; } + +.xterm-strikethrough { + text-decoration: line-through; +} + +.xterm-screen .xterm-decoration-container .xterm-decoration { + z-index: 6; + position: absolute; +} + +.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer { + z-index: 7; +} + +.xterm-decoration-overview-ruler { + z-index: 8; + position: absolute; + top: 0; + right: 0; + pointer-events: none; +} + +.xterm-decoration-top { + z-index: 2; + position: relative; +} diff --git a/src/MandoCode.Desktop/Assets/web/terminal/xterm.js b/src/MandoCode.Desktop/Assets/web/terminal/xterm.js new file mode 100644 index 0000000..a68eae6 --- /dev/null +++ b/src/MandoCode.Desktop/Assets/web/terminal/xterm.js @@ -0,0 +1,2 @@ +!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var i=t();for(var s in i)("object"==typeof exports?exports:e)[s]=i[s]}}(self,(()=>(()=>{"use strict";var e={4567:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.AccessibilityManager=void 0;const n=i(9042),o=i(6114),a=i(9924),h=i(844),c=i(5596),l=i(4725),d=i(3656);let _=t.AccessibilityManager=class extends h.Disposable{constructor(e,t){super(),this._terminal=e,this._renderService=t,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=document.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=document.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let e=0;e