diff --git a/Protest/Front/terminal.js b/Protest/Front/terminal.js index c72c27bc..27354bed 100644 --- a/Protest/Front/terminal.js +++ b/Protest/Front/terminal.js @@ -10,23 +10,24 @@ class Terminal extends Window { this.AddCssDependencies("terminal.css"); this.cursor = {x:0, y:0}; - this.chars = {}; + this.screen = {}; this.savedCursorPos = null; this.savedLine = null; this.savedScreen = null; - - this.foreColor = null; - this.backColor = null; - this.isBold = false; - this.isDim = false; - this.isItalic = false; - this.isUnderline = false; - this.isBlinking = false; - this.isFastBlinking = false; - this.isInverse = false; - this.isHidden = false; - this.isStrikethrough = false; + this.bracketedMode = false; + + this.foreColor = null; + this.backColor = null; + this.bold = false; + this.faint = false; + this.italic = false; + this.underline = false; + this.blinking = false; + this.fastBlinking = false; + this.inverse = false; + this.hidden = false; + this.strikethrough = false; this.ws = null; @@ -34,6 +35,7 @@ class Terminal extends Window { this.connectButton = this.AddToolbarButton("Connect", "mono/connect.svg?light"); this.optionsButton = this.AddToolbarButton("Options", "mono/wrench.svg?light"); this.AddToolbarSeparator(); + this.saveText = this.AddToolbarButton("Save text", "mono/floppy.svg?light"); this.pasteButton = this.AddToolbarButton("Paste", "mono/clipboard.svg?light"); this.sendKeyButton = this.AddToolbarButton("Send key", "mono/keyboard.svg?light"); @@ -229,7 +231,12 @@ class Terminal extends Window { if (this.ws === null || this.ws.readyState != 1) { return; } - this.ws.send(keyText.value); + if (this.bracketedMode) { + this.ws.send(`\x1b[200~${keyText.value}\x1b[201~`); + } + else { + this.ws.send(keyText.value); + } }; setTimeout(()=>{ keyText.focus(); }, 200); @@ -306,19 +313,18 @@ class Terminal extends Window { HandleMessage(data) { for (let i=0; i{ this.cursorElement.style.animation = "terminal-blinking 1.2s infinite"; }, 400); break; - //TODO: case "\x08": //backspace or move left this.cursor.x = Math.max(0, this.cursor.x - 1); break; @@ -385,14 +390,14 @@ class Terminal extends Window { if (foreColor) char.style.color = foreColor; if (backColor) char.style.backgroundColor = backColor; - if (this.isBold) char.style.fontWeight = "bold"; - if (this.isDim) char.style.opacity = "0.6"; - if (this.isItalic) char.style.fontStyle = "italic"; - if (this.isUnderline) char.style.textDecoration = "underline"; - if (this.isBlinking) char.style.animation = "terminal-blinking 1s infinite"; - if (this.isFastBlinking) char.style.animation = "terminal-fast-blinking .2s infinite"; - if (this.isHidden) char.style.visibility = "hidden"; - if (this.isStrikethrough) char.style.textDecoration = "line-through"; + if (this.bold) char.style.fontWeight = "bold"; + if (this.faint) char.style.opacity = "0.6"; + if (this.italic) char.style.fontStyle = "italic"; + if (this.underline) char.style.textDecoration = "underline"; + if (this.blinking) char.style.animation = "terminal-blinking 1s infinite"; + if (this.fastBlinking) char.style.animation = "terminal-fast-blinking .2s infinite"; + if (this.hidden) char.style.visibility = "hidden"; + if (this.strikethrough) char.style.textDecoration = "line-through"; this.cursor.x++; break; @@ -410,315 +415,116 @@ class Terminal extends Window { } HandleEscSequence(data, index) { //Control Sequence Introducer - if (data[index+1] === "[" || data[index+1] === "\x9b") { - return this.HandleCSI(data, index); - } - - if (data[index+1] === "P" || data[index+1] === "\x90") { - return this.HandleDCS(data, index); - } - - if (data[index+1] === "]" || data[index+1] === "\x9d") { - return this.HandleOSC(data, index); + if (index + 1 >= data.length) return 1; + + switch (data[index+1]) { + case "[": return this.HandleCSI(data, index); + case "P": return this.HandleDCS(data, index); + case "]": return this.HandleOSC(data, index); + default: + console.warn("Unknown escape sequence: " + data[index+1]); + return 2; } - - console.warn("Unknown escape sequence: " + data[index+1]); - return 1; } HandleCSI(data, index) { //Control Sequence Introducer if (index >= data.length) return 2; - - let symbol = null; - let values = []; - let command = null; - - let i = index + 2; - while (i < data.length) { - if (this.IsLetter(data, i)) { //command - command = data[i++]; - break; - } - - if (!isNaN(data[i])) { //number - let n = parseInt(data[i]); - while (!isNaN(data[++i]) && i < data.length) { - n *= 10; - n += parseInt(data[i]); - } - values.push(n); - continue; - } - - switch (data[i]) { - case ";": symbol = ";"; break; //separator - case "=": symbol = "="; break; //screen mode - case "?": symbol = "?"; break; //private modes - default: - console.warn("Unknown token: " + data[i]); - break; - } + const sequence = data.slice(index + 1); - i++; - } + const match = sequence.match(/^\[([?=]?)(\d*(;\d*)*)?([A-Za-z])/); + if (!match) return 2; - //common private modes - if (symbol === "?") { - if (values.length === 1 && values[0] === 25) { - if (command === "l") { - this.cursorElement.style.visibility = "hidden"; - return 6; - } + const fullSequence = match[0]; + const prefix = match[1] || ""; // ?, = or "" + const paramString = match[2] || ""; + const command = match[4]; - if (command === "h") { - this.cursorElement.style.visibility = "visible"; - return 6; - } - } - } + const params = paramString.split(";").map(param => { + return param === "" ? 0 : parseInt(param, 10); + }); switch (command) { case "A": //cursor up - this.cursor.y = Math.max(0, this.cursor.y - values[0] ?? 1); + this.cursor.y = Math.max(0, this.cursor.y - (params[0] || 1)); break; case "B": //cursor down - this.cursor.y += values[0] ?? 1; + this.cursor.y += params[0] || 1; break; case "C": //cursor right - this.cursor.x += values[0] ?? 1; + this.cursor.x += params[0] || 1; break; case "D": //cursor left - this.cursor.x = Math.max(0, this.cursor.x - values[0] ?? 1); + this.cursor.x = Math.max(0, this.cursor.x - (params[0] || 1)); break; case "E": //cursor to beginning of next line, n lines down this.cursor.x = 0; - this.cursor.y += values[0] ?? 1; + this.cursor.y += params[0] || 1; break; case "F": //cursor to beginning of previous line, n lines up this.cursor.x = 0; - this.cursor.y = Math.max(0, this.cursor.y - values[0] ?? 1); + this.cursor.y = Math.max(0, this.cursor.y - (params[0] || 1)); + break; case "G": //cursor to column n - if (values.length > 0) break; - this.cursor.x = values[0]; + this.cursor.x = Math.max(0, (params[0] || 1) - 1); break; case "f": - case "H": //cursor to home position (0,0) - if (values.length === 0) { - this.cursor.x = 0; - this.cursor.y = 0; - return 3; - } - else if (values.length > 1) { - this.cursor.x = values[1]; - this.cursor.y = values[0]; - } + case "H": //move the cursor to row n, column m + this.cursor.y = Math.max(0, (params[0] || 1) - 1); + this.cursor.x = Math.max(0, (params[1] || 1) - 1); break; case "J": - if (values.length === 0) { //same as J0 - this.EraseFromCursorToEndOfScreen(); - return 3; - } - if (values[0] === 0) { - this.EraseFromCursorToEndOfScreen(); - return 4; - } - else if (values[0] === 1) { - this.EraseFromCursorToBeginningOfScreen(); - return 4; - } - else if (values[0] === 2) { - this.ClearScreen(); - return 4; - } - else if (values[0] === 3) { - this.ClearScreen(); - //TODO: clear screen and buffer - return 4; + switch (params[0]) { + case 0: this.EraseFromCursorToEndOfScreen(); break; + case 1: this.EraseFromCursorToBeginningOfScreen(); break; + case 2: this.ClearScreen(); break; + case 3: this.ClearScreenAndBuffer(); break; + default: + console.log(`Unhandled CSI command: ${params.join(";")}J`); + break; } break; case "K": - if (values.length === 0) { //same as K0 - this.EraseLineFromCursorToEnd(); - return 3; - } - if (values[0] === 0) { - this.EraseLineFromCursorToEnd(); - return 4; - } - if (values[0] === 1) { - this.EraseLineFromBeginningToCursor(); - return 4; - } - if (values[0] === 2) { - this.ClearLine(); - return 4; + switch (params[0]) { + case 0: this.EraseLineFromCursorToEnd(); break; + case 1: this.EraseLineFromBeginningToCursor(); break; + case 2: this.ClearLine(); break; + default: + console.log(`Unhandled CSI command: ${params.join(";")}K`); + break; } break; + case "P":this.DeleteN(params[0] || 1); break; //case "S": break; //not ANSI //case "T": break; //not ANSI - case "P": //delete n chars - if (values.length === 0) break; - - //find last character in current line - let x; - for (x=this.cursor.x; x= data.length) return 2; - console.warn("Unknown DCS: " + data[index+2]); + console.warn(`Unknown DCS: ${data[index+2]}`); return 2; } HandleOSC(data, index) { //Operating System Command if (index >= data.length) return 2; - console.warn("Unknown OCS: " + data[index+2]); + console.warn(`Unknown OCS: ${data[index+2]}`); return 2; } @@ -784,20 +592,159 @@ class Terminal extends Window { return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; } - IsLetter(string, index) { - const code = string.charCodeAt(index); - if (code > 64 && code < 91) return true; - if (code > 96 && code < 123) return true; - return false; + ParseGraphicsModes(params) { + for (let i=0; i c) continue; const key = `${x},${y}`; - if (!this.chars[key]) continue; - this.content.removeChild(this.chars[key]); - delete this.chars[key]; + if (!this.screen[key]) continue; + this.content.removeChild(this.screen[key]); + delete this.screen[key]; } } } ClearScreen() { //2J - this.chars = {}; - this.content.innerHTML = ""; + this.screen = {}; + this.content.textContent = ""; this.content.appendChild(this.cursorElement); } + ClearScreenAndBuffer() { //3J + this.ClearScreen(); + //TODO: clear buffer + } + EraseLineFromCursorToEnd() { //0K const w = this.GetScreenWidth(); for (let i=this.cursor.x; i