A terminal panel beside an agent chat
A real shell on the chat's own connection, opened and closed like the inspector and never beside it. The modes govern the model; what a person types is theirs, since they hold the credential and could open the same shell with an ssh client. The model cannot see the panel -- a button copies the output you choose into the composer. The session outlives the socket: closing the panel leaves a build running, and coming back reattaches with the scrollback. Two tabs share one shell and the smaller window decides the size. It ends on an idle timeout, on deleting the chat, on disabling, moving or deleting the connection, and on a restart -- which says why rather than quietly opening a fresh shell that has lost the working directory. The nginx template's `Connection ""` is right for SSE and fails every WebSocket handshake, so `location /` now uses a `map $http_upgrade`; update.sh grows a drift check for it, because the only symptom on a stale vhost is a panel that cannot connect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -510,6 +510,85 @@ button, input, textarea, select {
|
||||
}
|
||||
}
|
||||
|
||||
/* The terminal panel: a fourth child of .shell, to the left of the inspector.
|
||||
Built beside it rather than in chat.css because the shell layout lives here,
|
||||
and the two are the same shape -- a fixed-width column that hides with the
|
||||
`hidden` attribute. */
|
||||
.terminal {
|
||||
width: var(--terminal-width);
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: var(--bg-sunken);
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.terminal__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
height: var(--header-height);
|
||||
flex: none;
|
||||
padding: 0 var(--sp-3);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.terminal__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.terminal__where {
|
||||
font-weight: 400;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--ink-faint);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* The element xterm renders into. It measures itself from this box, so it must
|
||||
have a size of its own -- min-height: 0 on a flex child is what stops the
|
||||
terminal growing the panel instead of scrolling inside it. */
|
||||
.terminal__screen {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: var(--sp-2);
|
||||
background: var(--code-bg);
|
||||
}
|
||||
.terminal__screen .xterm { height: 100%; }
|
||||
|
||||
.terminal__status {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--ink-faint);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
.terminal__status strong { color: var(--ink-muted); font-weight: 600; }
|
||||
.terminal__message { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
||||
.terminal__message--error { color: var(--danger); }
|
||||
|
||||
@media (max-width: 64rem) {
|
||||
.terminal {
|
||||
position: fixed;
|
||||
inset: 0 0 0 auto;
|
||||
width: min(var(--terminal-width), 100vw);
|
||||
z-index: 40;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
/* --- Layout ----------------------------------------------------------- */
|
||||
--sidebar-width: 17.5rem;
|
||||
--inspector-width: 24rem;
|
||||
/* Wider than the inspector because the content is not prose: eighty columns
|
||||
of --font-mono do not fit in 24rem, and a terminal narrower than eighty
|
||||
re-wraps everything a program prints. */
|
||||
--terminal-width: 34rem;
|
||||
--thread-max-width: 48rem;
|
||||
--header-height: 3.5rem;
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
: "Switch to Moria (dark)");
|
||||
});
|
||||
|
||||
/* For anything holding colours as values rather than reading them from a
|
||||
variable. The terminal is the only such thing: xterm copies its palette
|
||||
at construction, so switching to Shire would otherwise leave a black
|
||||
rectangle in a light interface. */
|
||||
document.dispatchEvent(new CustomEvent("lembas:theme", { detail: { theme: name } }));
|
||||
|
||||
if (document.body.dataset.authenticated === "true") {
|
||||
fetch("/api/preferences/theme", {
|
||||
method: "POST",
|
||||
@@ -357,7 +363,48 @@
|
||||
revealInstall(false);
|
||||
});
|
||||
|
||||
/* --- Panels ------------------------------------------------------------- */
|
||||
/* A panel can be opened or closed by more than one control -- the button in
|
||||
the topbar and the panel's own Close -- and it can now also be closed by
|
||||
something nobody clicked, because two panels sharing the right-hand side
|
||||
of the screen must not both be open. So the state is applied to the panel
|
||||
and then *every* toggle pointing at it is brought in line. Setting
|
||||
aria-expanded on the clicked button alone left the other one lying. */
|
||||
function syncToggles(selector, open) {
|
||||
var toggles = document.querySelectorAll('[data-toggle="' + selector + '"]');
|
||||
for (var i = 0; i < toggles.length; i++) {
|
||||
toggles[i].setAttribute("aria-expanded", open ? "true" : "false");
|
||||
toggles[i].classList.toggle("is-active", open);
|
||||
}
|
||||
}
|
||||
|
||||
function setPanel(selector, open, group) {
|
||||
var panel = document.querySelector(selector);
|
||||
if (!panel) return;
|
||||
|
||||
/* One at a time down the right-hand side. Not only a narrow-screen
|
||||
concern: a 1280px window with the sidebar, the inspector and the
|
||||
terminal all open leaves the conversation about seventy pixels wide. */
|
||||
if (open && group) {
|
||||
var others = document.querySelectorAll('[data-toggle-group="' + group + '"]');
|
||||
for (var i = 0; i < others.length; i++) {
|
||||
var other = others[i].dataset.toggle;
|
||||
if (other && other !== selector) setPanel(other, false);
|
||||
}
|
||||
}
|
||||
|
||||
panel.toggleAttribute("hidden", !open);
|
||||
syncToggles(selector, open);
|
||||
/* What a panel needs to know it is visible. The terminal listens for this:
|
||||
xterm cannot measure itself inside a hidden element, so it has to be
|
||||
told rather than left to discover. */
|
||||
panel.dispatchEvent(
|
||||
new CustomEvent("lembas:toggle", { bubbles: true, detail: { open: open } })
|
||||
);
|
||||
}
|
||||
|
||||
window.lembas = {
|
||||
setPanel: setPanel,
|
||||
applyTheme: applyTheme,
|
||||
toggleTheme: toggleTheme,
|
||||
copyText: copyText,
|
||||
@@ -410,10 +457,7 @@
|
||||
event.preventDefault();
|
||||
var panel = document.querySelector(toggle.dataset.toggle);
|
||||
if (!panel) return;
|
||||
var nowOpen = panel.hasAttribute("hidden");
|
||||
panel.toggleAttribute("hidden");
|
||||
toggle.setAttribute("aria-expanded", nowOpen ? "true" : "false");
|
||||
toggle.classList.toggle("is-active", nowOpen);
|
||||
setPanel(toggle.dataset.toggle, panel.hasAttribute("hidden"), toggle.dataset.toggleGroup);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ var SHELL = [
|
||||
"/static/js/app.js",
|
||||
"/static/js/ui.js",
|
||||
"/static/js/audio.js",
|
||||
"/static/js/terminal.js",
|
||||
// Deliberately not the three xterm files below it: ~300KB precached on every
|
||||
// install, for a panel most people never open, to spare one fetch from the
|
||||
// people who do. The runtime branch caches them the first time it is opened.
|
||||
"/static/vendor/htmx.min.js",
|
||||
"/static/vendor/htmx-ext-sse.js",
|
||||
"/static/vendor/alpine.min.js",
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
The terminal panel.
|
||||
|
||||
Loaded only on a chat that can actually open a shell -- see the head and
|
||||
scripts blocks in chat/index.html -- because xterm is nearly three times the
|
||||
size of everything else vendored here. The Terminal object itself is built on
|
||||
the first *open* rather than on load, so even here nothing is parsed for
|
||||
somebody who never presses the button.
|
||||
|
||||
Three things about xterm that are easy to get wrong, and cost an afternoon
|
||||
each:
|
||||
|
||||
* `fit()` measures `offsetWidth`, which is 0 inside a `[hidden]` ancestor, so
|
||||
fitting while closed silently does nothing and leaves an 80-column terminal
|
||||
in a 34rem panel. Everything below is arranged so a fit only ever happens
|
||||
after the panel is visible.
|
||||
* A window `resize` event does not fire when the sidebar is toggled or a panel
|
||||
opens beside this one, which is by far the commonest way the panel changes
|
||||
size. Hence the ResizeObserver.
|
||||
* xterm does not read CSS variables. The theme is built from the computed
|
||||
style at open time and rebuilt when the theme changes, or switching to
|
||||
`shire` leaves a black rectangle in a light interface.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var panel = null;
|
||||
var term = null;
|
||||
var fit = null;
|
||||
var socket = null;
|
||||
var screen = null;
|
||||
var messageEl = null;
|
||||
var observer = null;
|
||||
var closedOnPurpose = false;
|
||||
|
||||
function say(text, isError) {
|
||||
if (!messageEl) return;
|
||||
messageEl.textContent = text;
|
||||
messageEl.classList.toggle("terminal__message--error", !!isError);
|
||||
}
|
||||
|
||||
/* --- Theme -------------------------------------------------------------- */
|
||||
function readTheme() {
|
||||
var style = getComputedStyle(document.documentElement);
|
||||
function token(name, fallback) {
|
||||
return (style.getPropertyValue(name) || "").trim() || fallback;
|
||||
}
|
||||
return {
|
||||
background: token("--code-bg", "#0C0F13"),
|
||||
foreground: token("--ink", "#E8E2D4"),
|
||||
cursor: token("--accent", "#C9A227"),
|
||||
cursorAccent: token("--code-bg", "#0C0F13"),
|
||||
selectionBackground: token("--accent-soft", "rgba(201, 162, 39, 0.3)")
|
||||
};
|
||||
}
|
||||
|
||||
/* --- Sizing ------------------------------------------------------------- */
|
||||
function visible() {
|
||||
return panel && !panel.hasAttribute("hidden") && panel.offsetWidth > 0;
|
||||
}
|
||||
|
||||
function refit() {
|
||||
if (!term || !fit || !visible()) return;
|
||||
try {
|
||||
fit.fit();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
send({ t: "resize", cols: term.cols, rows: term.rows });
|
||||
}
|
||||
|
||||
/* --- The socket --------------------------------------------------------- */
|
||||
function send(payload) {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify(payload));
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (socket) return;
|
||||
closedOnPurpose = false;
|
||||
|
||||
var base = location.protocol === "https:" ? "wss://" : "ws://";
|
||||
var url =
|
||||
base + location.host + panel.dataset.url +
|
||||
"?cols=" + (term.cols || 80) + "&rows=" + (term.rows || 24);
|
||||
|
||||
say("Connecting…");
|
||||
socket = new WebSocket(url);
|
||||
socket.binaryType = "arraybuffer";
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
if (typeof event.data === "string") return control(event.data);
|
||||
/* Written straight through as bytes. xterm's decoder is stateful across
|
||||
calls, so a multi-byte character split across two frames still lands
|
||||
correctly -- which is exactly why the server never decodes either. */
|
||||
term.write(new Uint8Array(event.data));
|
||||
};
|
||||
|
||||
socket.onclose = function () {
|
||||
socket = null;
|
||||
if (!closedOnPurpose) say("Disconnected. Close and reopen to reconnect.");
|
||||
};
|
||||
|
||||
socket.onerror = function () {
|
||||
/* A failed handshake gives the page nothing: no status, no reason. So
|
||||
this is a guess, and it names the likeliest cause rather than
|
||||
pretending to know. */
|
||||
say("Could not connect. If this instance is behind a proxy, it may not " +
|
||||
"be passing WebSocket upgrades through.", true);
|
||||
};
|
||||
}
|
||||
|
||||
function control(raw) {
|
||||
var payload;
|
||||
try {
|
||||
payload = JSON.parse(raw);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.t === "ready") {
|
||||
say(payload.shared
|
||||
? "Connected. This shell is also open in another tab, and they share a size."
|
||||
: "Connected.");
|
||||
if (payload.dir) {
|
||||
var where = panel.querySelector("[data-terminal-where]");
|
||||
if (where) where.textContent = payload.dir;
|
||||
}
|
||||
/* The server may have opened the shell at a size chosen by whoever got
|
||||
here first, so ask for ours now that there is something to ask. */
|
||||
refit();
|
||||
term.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.t === "behind") {
|
||||
/* This window stopped reading and was disconnected so the others kept
|
||||
up. Reconnecting costs nothing: the scrollback is the state. */
|
||||
say(payload.message || "Reconnecting…");
|
||||
closedOnPurpose = true;
|
||||
if (socket) socket.close();
|
||||
socket = null;
|
||||
term.reset();
|
||||
connect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.t === "closed" || payload.t === "error") {
|
||||
say(payload.message || "This terminal closed.", payload.t === "error");
|
||||
closedOnPurpose = true;
|
||||
/* Deliberately no reconnect. A new shell has lost the working directory,
|
||||
the environment and the half-typed command, and quietly substituting
|
||||
one is worse than saying the connection went. */
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Building it -------------------------------------------------------- */
|
||||
function build() {
|
||||
if (term) return true;
|
||||
if (typeof Terminal === "undefined" || typeof FitAddon === "undefined") {
|
||||
say("The terminal could not be loaded.", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
term = new Terminal({
|
||||
allowProposedApi: true,
|
||||
convertEol: false,
|
||||
cursorBlink: true,
|
||||
fontFamily: getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--font-mono").trim() || "monospace",
|
||||
fontSize: 13,
|
||||
scrollback: 5000,
|
||||
theme: readTheme()
|
||||
});
|
||||
/* The module namespace is the UMD global, so the class is a property of
|
||||
it. `new FitAddon()` is the mistake that reads correctly. */
|
||||
fit = new FitAddon.FitAddon();
|
||||
term.loadAddon(fit);
|
||||
term.open(screen);
|
||||
|
||||
term.onData(function (data) {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(new TextEncoder().encode(data));
|
||||
}
|
||||
});
|
||||
|
||||
/* Ctrl+C is interrupt here, which is correct and will still surprise
|
||||
somebody. Copy and paste are the shifted pair, as in every terminal. */
|
||||
term.attachCustomKeyEventHandler(function (event) {
|
||||
if (!event.ctrlKey || !event.shiftKey || event.type !== "keydown") return true;
|
||||
var key = event.key.toLowerCase();
|
||||
if (key === "c") {
|
||||
var selection = term.getSelection();
|
||||
if (selection) navigator.clipboard.writeText(selection);
|
||||
return false;
|
||||
}
|
||||
if (key === "v") {
|
||||
navigator.clipboard.readText().then(function (text) {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(new TextEncoder().encode(text));
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
/* The panel changes size when the sidebar is toggled or the window is
|
||||
resized, and only the second of those fires a `resize` event. */
|
||||
if (window.ResizeObserver) {
|
||||
observer = new ResizeObserver(function () {
|
||||
refit();
|
||||
});
|
||||
observer.observe(panel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!build()) return;
|
||||
/* Next frame: the panel has just had `hidden` removed and has no measured
|
||||
width yet, so fitting now would be the silent no-op this file exists to
|
||||
avoid. */
|
||||
requestAnimationFrame(function () {
|
||||
refit();
|
||||
connect();
|
||||
term.focus();
|
||||
});
|
||||
}
|
||||
|
||||
/* --- Send to chat ------------------------------------------------------- */
|
||||
/* Into the composer, never sent. What a machine printed is exactly the sort
|
||||
of text somebody should read before a model does, and the box is where
|
||||
that happens. */
|
||||
function sendToChat() {
|
||||
if (!term) return;
|
||||
var text = term.getSelection();
|
||||
if (!text) {
|
||||
var lines = [];
|
||||
var buffer = term.buffer.active;
|
||||
var last = buffer.baseY + buffer.cursorY;
|
||||
for (var y = Math.max(0, last - 40); y <= last; y++) {
|
||||
var line = buffer.getLine(y);
|
||||
if (line) lines.push(line.translateToString(true));
|
||||
}
|
||||
text = lines.join("\n").replace(/\n+$/, "");
|
||||
}
|
||||
if (!text.trim()) {
|
||||
say("Nothing to send: select some output first.");
|
||||
return;
|
||||
}
|
||||
|
||||
var input = document.querySelector("[data-composer-input]");
|
||||
if (!input) return;
|
||||
var fence = "```\n" + text + "\n```\n";
|
||||
input.value = input.value ? input.value.replace(/\s*$/, "\n\n") + fence : fence;
|
||||
if (window.lembas && window.lembas.autosize) window.lembas.autosize(input);
|
||||
input.focus();
|
||||
/* "As it appeared" and not "as it was written": the buffer holds what is on
|
||||
screen, hard-wrapped at the terminal's width, with no way to tell a wrap
|
||||
from a newline. */
|
||||
say("Copied into the message box as it appeared on screen.");
|
||||
}
|
||||
|
||||
/* --- Wiring ------------------------------------------------------------- */
|
||||
function start() {
|
||||
panel = document.querySelector("[data-terminal]");
|
||||
if (!panel) return;
|
||||
screen = panel.querySelector("[data-terminal-screen]");
|
||||
messageEl = panel.querySelector("[data-terminal-message]");
|
||||
|
||||
panel.addEventListener("lembas:toggle", function (event) {
|
||||
if (event.detail && event.detail.open) open();
|
||||
/* Closing leaves the Terminal object and the socket alone. `write()` is
|
||||
internally queued, so disposing mid-output drops it, and keeping the
|
||||
object is what makes reopening instant. The session on the far side
|
||||
outlives this panel by design. */
|
||||
});
|
||||
|
||||
panel.addEventListener("click", function (event) {
|
||||
if (event.target.closest("[data-terminal-send]")) {
|
||||
event.preventDefault();
|
||||
sendToChat();
|
||||
}
|
||||
});
|
||||
|
||||
/* xterm holds colours as values, not as variables, so a theme change has
|
||||
to be pushed into it. */
|
||||
document.addEventListener("lembas:theme", function () {
|
||||
if (term) term.options.theme = readTheme();
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", start);
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
})();
|
||||
@@ -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=addon-fit.js.map
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 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:not(.debug),
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree {
|
||||
user-select: text;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
+2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user