diff --git a/webfiles/beep.wav b/webfiles/canvas/beep.wav
similarity index 100%
rename from webfiles/beep.wav
rename to webfiles/canvas/beep.wav
diff --git a/webfiles/canvas/tcell.html b/webfiles/canvas/tcell.html
new file mode 100644
index 00000000..f7867ea8
--- /dev/null
+++ b/webfiles/canvas/tcell.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Tcell
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webfiles/canvas/tcell.js b/webfiles/canvas/tcell.js
new file mode 100644
index 00000000..aa3b33cd
--- /dev/null
+++ b/webfiles/canvas/tcell.js
@@ -0,0 +1,227 @@
+// Copyright 2023 The TCell Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use file except in compliance with the License.
+// You may obtain a copy of the license at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const wasmFilePath = "main.wasm"
+const fontclass = "16px \"Menlo\", \"Andale Mono\", \"Courier New\", Monospace"
+const fontcolor = "#FFFFFF"
+const fontback = "#000000"
+const dimcolor = "#7F7F7F"
+const dimback = "#000000"
+const fitwindow = true
+
+/** @type HTMLCanvasElement */
+const termElement = document.getElementById("terminal")
+const term = termElement.getContext("2d")
+term.font = fontclass
+const fontSize = term.measureText("X")
+// Results are less than ideal if these are not integers.
+const fontwidth = Math.round(fontSize.width)
+const fontheight = Math.round(fontSize.fontBoundingBoxAscent + fontSize.fontBoundingBoxDescent)
+const vertoffset = Math.round(fontSize.fontBoundingBoxAscent)
+
+var width = 80; var height = 24
+if (fitwindow) {
+ document.documentElement.style.overflow = 'hidden';
+ width = Math.floor(document.documentElement.clientWidth / fontwidth)
+ height = Math.floor(document.documentElement.clientHeight / fontheight)
+}
+
+const beepAudio = new Audio("beep.wav");
+
+var cursorClass = "cursor-blinking-block"
+
+var blinkState = false
+var blinkCells = []
+
+var blinkId = setInterval(function () {
+ blinkState = !blinkState
+ blinkCells.forEach(cell => {
+ drawCell(cell.x, cell.y, cell.mainc, cell.combc, cell.fg, cell.bg, cell.attrs, true)
+ })
+}, 500);
+
+function initialize() {
+ resize(width, height) // initialize content
+}
+
+function resize(w, h) {
+ width = w
+ height = h
+ termElement.width = fontwidth * width
+ termElement.height = fontheight * height
+ clearScreen()
+}
+
+function clearScreen(fg, bg) {
+ if (fg) { fontcolor = intToHex(fg) }
+ if (bg) { fontback = intToHex(bg) }
+
+ term.fillStyle = fontback
+ term.fillRect(0, 0, termElement.width, termElement.height)
+ blinkCells = []
+}
+
+function invert(data) {
+ for (var idx = 0; idx < data.data.byteLength; idx++) {
+ if (idx % 4 != 3) { // don't invert the alpha channel
+ data.data[idx] = 255 - data.data[idx]
+ }
+ }
+}
+
+function drawCell(x, y, mainc, combc, fg, bg, attrs, blink) {
+ if (!blink) {
+ if ((attrs & ((1 << 1) | (1 << 8))) != 0) {
+ // include x and y so we don't have to deconvert the id
+ blinkCells[y * width + x] = { x: x, y: y, mainc: mainc, combc: combc, fg: fg, bg: bg, attrs: attrs }
+ } else {
+ delete blinkCells[y * width + x]
+ }
+ }
+
+ var combString = String.fromCharCode(mainc)
+ combc.forEach(char => { combString += String.fromCharCode(char) });
+
+ let fgc = fontcolor
+ let bgc = fontback
+ if ((attrs & (1 << 4)) == 0) {
+ if (fg != -1) { fgc = intToHex(fg) }
+ if (bg != -1) { bgc = intToHex(bg) }
+ } else { // dim
+ fgc = dimcolor
+ if (fg != -1) { fgc = intToHex((fg & 0xFEFEFE) >> 1) }
+ bgc = dimback
+ if (bg != -1) { bgc = intToHex((bg & 0xFEFEFE) >> 1) }
+ }
+
+ if (((attrs & (1 << 1)) != 0) && !blinkState) { // blink off. Just blank the cell and return.
+ term.fillStyle = bgc
+ term.fillRect(x * fontwidth, y * fontheight, fontwidth, fontheight)
+ return
+ }
+
+ if ((attrs & (1 << 2)) != 0) { // reverse video
+ var temp = bgc
+ bgc = fgc
+ fgc = temp
+ }
+
+ term.fillStyle = bgc
+ term.fillRect(x * fontwidth, y * fontheight, fontwidth, fontheight)
+
+ term.fillStyle = fgc
+
+ let fc = fontclass
+
+ if (attrs != 0) {
+ if ((attrs & 1) != 0) { fc = "bold " + fc }
+ if ((attrs & (1 << 3)) != 0) { // underscore
+ term.fillRect(x * fontwidth, y * fontheight + fontheight - 2, fontwidth, 2)
+ }
+ if ((attrs & (1 << 5)) != 0) { fc = "italic " + fc }
+ if ((attrs & (1 << 6)) != 0) { // strikethrough
+ term.fillRect(x * fontwidth, y * fontheight + fontheight / 2 - 1, fontwidth, 2)
+ }
+ }
+
+ term.font = fc
+ term.fillText(combString, x * fontwidth, y * fontheight + vertoffset)
+
+ if ((attrs & (1 << 8)) != 0) { // cursor: invert the cursor area
+ var data
+ switch (cursorClass) {
+ case "cursor-blinking-block":
+ if (!blinkState) break;
+ case "cursor-steady-block":
+ data = term.getImageData(x * fontwidth, y * fontheight, fontwidth, fontheight)
+ invert(data)
+ term.putImageData(data, x * fontwidth, y * fontheight)
+ case "cursor-blinking-underline":
+ if (!blinkState) break;
+ case "cursor-steady-underline":
+ data = term.getImageData(x * fontwidth, y * fontheight + fontheight - 2, fontwidth, 2)
+ invert(data)
+ term.putImageData(data, x * fontwidth, y * fontheight + fontheight - 2)
+ break;
+ case "cursor-blinking-bar":
+ if (!blinkState) break;
+ case "cursor-steady-bar":
+ data = term.getImageData(x * fontwidth, y * fontheight, 2, fontheight)
+ invert(data)
+ term.putImageData(data, x * fontwidth, y * fontheight)
+ break;
+ }
+ }
+}
+
+function setCursorStyle(newClass) {
+ cursorClass = newClass
+}
+
+function beep() {
+ beepAudio.currentTime = 0;
+ beepAudio.play();
+}
+
+function intToHex(n) {
+ return "#" + n.toString(16).padStart(6, '0')
+}
+
+initialize()
+
+document.addEventListener("keydown", e => {
+ onKeyEvent(e.key, e.shiftKey, e.altKey, e.ctrlKey, e.metaKey)
+})
+
+termElement.addEventListener("click", e => {
+ onMouseClick(Math.min((e.offsetX / fontwidth) | 0, width - 1), Math.min((e.offsetY / fontheight) | 0, height - 1), e.which, e.shiftKey, e.altKey, e.ctrlKey)
+})
+
+termElement.addEventListener("mousemove", e => {
+ onMouseMove(Math.min((e.offsetX / fontwidth) | 0, width - 1), Math.min((e.offsetY / fontheight) | 0, height - 1), e.which, e.shiftKey, e.altKey, e.ctrlKey)
+})
+
+termElement.addEventListener("focus", e => {
+ onFocus(true)
+})
+
+termElement.addEventListener("blur", e => {
+ onFocus(false)
+})
+term.tabIndex = 0
+
+document.addEventListener("paste", e => {
+ e.preventDefault();
+ var text = (e.originalEvent || e).clipboardData.getData('text/plain');
+ onPaste(true)
+ for (let i = 0; i < text.length; i++) {
+ onKeyEvent(text.charAt(i), false, false, false, false)
+ }
+ onPaste(false)
+});
+
+if (fitwindow) {
+ document.defaultView.addEventListener("resize", e => {
+ const charWidth = Math.floor(document.documentElement.clientWidth / fontwidth)
+ const charHeight = Math.floor(document.documentElement.clientHeight / fontheight)
+ onResizeEvent(charWidth, charHeight)
+ })
+}
+
+const go = new Go();
+go.env.LINES = height.toString();
+go.env.COLUMNS = width.toString();
+WebAssembly.instantiateStreaming(fetch(wasmFilePath), go.importObject).then((result) => {
+ go.run(result.instance);
+});
diff --git a/webfiles/dom/beep.wav b/webfiles/dom/beep.wav
new file mode 100644
index 00000000..96cefd44
Binary files /dev/null and b/webfiles/dom/beep.wav differ
diff --git a/webfiles/tcell.html b/webfiles/dom/tcell.html
similarity index 100%
rename from webfiles/tcell.html
rename to webfiles/dom/tcell.html
diff --git a/webfiles/tcell.js b/webfiles/dom/tcell.js
similarity index 50%
rename from webfiles/tcell.js
rename to webfiles/dom/tcell.js
index 010ef6b2..8eb111ba 100644
--- a/webfiles/tcell.js
+++ b/webfiles/dom/tcell.js
@@ -13,6 +13,7 @@
// limitations under the License.
const wasmFilePath = "main.wasm"
+const fitwindow = true
const term = document.getElementById("terminal")
var width = 80; var height = 24
const beepAudio = new Audio("beep.wav");
@@ -20,25 +21,13 @@ const beepAudio = new Audio("beep.wav");
var cx = -1; var cy = -1
var cursorClass = "cursor-blinking-block"
-var content // {data: row[height], dirty: bool}
-// row = {data: element[width], previous: span}
-// dirty/[previous being null] indicates if previous (or entire terminal) needs to be recalculated.
-// dirty is true/null if terminal/previous need to be re-calculated/shown
-
function initialize() {
resize(width, height) // initialize content
- show() // then show the screen
}
function resize(w, h) {
-
width = w
height = h
- content = {data: new Array(height), dirty: true}
- for (let i = 0; i < height; i++) {
- content.data[i] = {data: new Array(width), previous: null}
- }
-
clearScreen()
}
@@ -46,23 +35,25 @@ function clearScreen(fg, bg) {
if (fg) { term.style.color = intToHex(fg) }
if (bg) { term.style.backgroundColor = intToHex(bg) }
- content.dirty = true
+ term.innerHTML = ""
for (let i = 0; i < height; i++) {
- content.data[i].previous = null // we set the row to be recalculated later
+ row = document.createElement("span")
for (let j = 0; j < width; j++) {
- content.data[i].data[j] = document.createTextNode(" ") // set the entire row to spaces.
+ row.appendChild(document.createTextNode(" "))
}
+ row.appendChild(document.createTextNode("\n"))
+ term.appendChild(row)
}
}
function drawCell(x, y, mainc, combc, fg, bg, attrs) {
var combString = String.fromCharCode(mainc)
- combc.forEach(char => {combString += String.fromCharCode(char)});
+ combc.forEach(char => { combString += String.fromCharCode(char) });
var span = document.createElement("span")
var use = false
- if ((attrs & (1<<2)) != 0) { // reverse video
+ if ((attrs & (1 << 2)) != 0) { // reverse video
var temp = bg
bg = fg
fg = temp
@@ -71,74 +62,26 @@ function drawCell(x, y, mainc, combc, fg, bg, attrs) {
if (fg != -1) { span.style.color = intToHex(fg); use = true }
if (bg != -1) { span.style.backgroundColor = intToHex(bg); use = true }
+ if ((x == cx) && (y == cy) && ((attrs & (1 << 8)) == 0)) {
+ cx = -1
+ cy = -1
+ }
+
if (attrs != 0) {
use = true
if ((attrs & 1) != 0) { span.classList.add("bold") }
- if ((attrs & (1<<1)) != 0) { span.classList.add("blink") }
- if ((attrs & (1<<3)) != 0) { span.classList.add("underline") }
- if ((attrs & (1<<4)) != 0) { span.classList.add("dim") }
- if ((attrs & (1<<5)) != 0) { span.classList.add("italic") }
- if ((attrs & (1<<6)) != 0) { span.classList.add("strikethrough") }
+ if ((attrs & (1 << 1)) != 0) { span.classList.add("blink") }
+ if ((attrs & (1 << 3)) != 0) { span.classList.add("underline") }
+ if ((attrs & (1 << 4)) != 0) { span.classList.add("dim") }
+ if ((attrs & (1 << 5)) != 0) { span.classList.add("italic") }
+ if ((attrs & (1 << 6)) != 0) { span.classList.add("strikethrough") }
+ if ((attrs & (1 << 8)) != 0) { span.classList.add(cursorClass); cx = x; cy = y }
}
var textnode = document.createTextNode(combString)
span.appendChild(textnode)
- content.dirty = true // invalidate terminal- new cell
- content.data[y].previous = null // invalidate row- new row
- content.data[y].data[x] = use ? span : textnode
-}
-
-function show() {
- if (!content.dirty) {
- return // no new draws; no need to update
- }
-
- displayCursor()
-
- term.innerHTML = ""
- content.data.forEach(row => {
- if (row.previous == null) {
- row.previous = document.createElement("span")
- row.data.forEach(c => {
- row.previous.appendChild(c)
- })
- row.previous.appendChild(document.createTextNode("\n"))
- }
- term.appendChild(row.previous)
- })
-
- content.dirty = false
-}
-
-function showCursor(x, y) {
- content.dirty = true
-
- if (!(cx < 0 || cy < 0)) { // if original position is a valid cursor position
- content.data[cy].previous = null;
- if (content.data[cy].data[cx].classList) {
- content.data[cy].data[cx].classList.remove(cursorClass)
- }
- }
-
- cx = x
- cy = y
-}
-
-function displayCursor() {
- content.dirty = true
-
- if (!(cx < 0 || cy < 0)) { // if new position is a valid cursor position
- content.data[cy].previous = null;
-
- if (!content.data[cy].data[cx].classList) {
- var span = document.createElement("span")
- span.appendChild(content.data[cy].data[cx])
- content.data[cy].data[cx] = span
- }
-
- content.data[cy].data[cx].classList.add(cursorClass)
- }
+ term.childNodes[y].childNodes[x].replaceWith(use ? span : textnode)
}
function setCursorStyle(newClass) {
@@ -147,15 +90,16 @@ function setCursorStyle(newClass) {
}
if (!(cx < 0 || cy < 0)) {
- // mark cursor row as dirty; new class has been applied to (cx, cy)
content.dirty = true
content.data[cy].previous = null
- if (content.data[cy].data[cx].classList) {
- content.data[cy].data[cx].classList.remove(cursorClass)
+ if (!term.childNodes[cy].childNodes[cx].classList) {
+ var span = document.createElement("span")
+ span.appendChild(term.childNodes[cy].childNodes[cx])
+ term.childNodes[cy].childNodes[cx].replaceWith(span)
}
-
- // adding the new class will be dealt with when displayCursor() is called
+ term.childNodes[cy].childNodes[cx].classList.remove(cursorClass)
+ term.childNodes[cy].childNodes[cx].classList.add(newClass)
}
cursorClass = newClass
@@ -175,16 +119,21 @@ initialize()
let fontwidth = term.clientWidth / width
let fontheight = term.clientHeight / height
+if (fitwindow) {
+ document.documentElement.style.overflow = 'hidden';
+ resize(Math.floor(document.documentElement.clientWidth / fontwidth), Math.floor(document.documentElement.clientHeight / fontheight))
+}
+
document.addEventListener("keydown", e => {
onKeyEvent(e.key, e.shiftKey, e.altKey, e.ctrlKey, e.metaKey)
})
term.addEventListener("click", e => {
- onMouseClick(Math.min((e.offsetX / fontwidth) | 0, width-1), Math.min((e.offsetY / fontheight) | 0, height-1), e.which, e.shiftKey, e.altKey, e.ctrlKey)
+ onMouseClick(Math.min((e.offsetX / fontwidth) | 0, width - 1), Math.min((e.offsetY / fontheight) | 0, height - 1), e.which, e.shiftKey, e.altKey, e.ctrlKey)
})
term.addEventListener("mousemove", e => {
- onMouseMove(Math.min((e.offsetX / fontwidth) | 0, width-1), Math.min((e.offsetY / fontheight) | 0, height-1), e.which, e.shiftKey, e.altKey, e.ctrlKey)
+ onMouseMove(Math.min((e.offsetX / fontwidth) | 0, width - 1), Math.min((e.offsetY / fontheight) | 0, height - 1), e.which, e.shiftKey, e.altKey, e.ctrlKey)
})
term.addEventListener("focus", e => {
@@ -207,7 +156,17 @@ document.addEventListener("paste", e => {
onPaste(false)
});
+if (fitwindow) {
+ document.defaultView.addEventListener("resize", e => {
+ const charWidth = Math.floor(document.documentElement.clientWidth / fontwidth)
+ const charHeight = Math.floor(document.documentElement.clientHeight / fontheight)
+ onResizeEvent(charWidth, charHeight)
+ })
+}
+
const go = new Go();
+go.env.LINES = height.toString();
+go.env.COLUMNS = width.toString();
WebAssembly.instantiateStreaming(fetch(wasmFilePath), go.importObject).then((result) => {
go.run(result.instance);
});
diff --git a/webfiles/termstyle.css b/webfiles/dom/termstyle.css
similarity index 100%
rename from webfiles/termstyle.css
rename to webfiles/dom/termstyle.css
diff --git a/wscreen.go b/wscreen.go
index bfdce438..44f807eb 100644
--- a/wscreen.go
+++ b/wscreen.go
@@ -19,11 +19,19 @@ package tcell
import (
"errors"
- "github.com/gdamore/tcell/v2/terminfo"
+ "os"
+ "strconv"
"strings"
"sync"
"syscall/js"
"unicode/utf8"
+
+ "github.com/gdamore/tcell/v2/terminfo"
+)
+
+// Extra attributes for web display
+const (
+ attrCursor AttrMask = AttrInvalid << 1
)
func NewTerminfoScreen() (Screen, error) {
@@ -33,6 +41,11 @@ func NewTerminfoScreen() (Screen, error) {
return t, nil
}
+type resized struct {
+ w int
+ h int
+}
+
type wScreen struct {
w, h int
style Style
@@ -44,19 +57,31 @@ type wScreen struct {
pasteEnabled bool
mouseFlags MouseFlags
+ cx int
+ cy int
cursorStyle CursorStyle
quit chan struct{}
evch chan Event
+ rsch chan resized
fallback map[rune]string
sync.Mutex
}
func (t *wScreen) Init() error {
- t.w, t.h = 80, 24 // default for html as of now
+ t.w, t.h = 80, 24
+ if i, _ := strconv.Atoi(os.Getenv("LINES")); i != 0 {
+ t.h = i
+ }
+ if i, _ := strconv.Atoi(os.Getenv("COLUMNS")); i != 0 {
+ t.w = i
+ }
+ t.cx, t.cy = -1, -1
+
t.evch = make(chan Event, 10)
t.quit = make(chan struct{})
+ t.rsch = make(chan resized, 1)
t.Lock()
t.running = true
@@ -65,6 +90,8 @@ func (t *wScreen) Init() error {
t.Unlock()
js.Global().Set("onKeyEvent", js.FuncOf(t.onKeyEvent))
+ js.Global().Set("onResizeEvent", js.FuncOf(t.onResizeEvent))
+ go t.watchResize()
return nil
}
@@ -91,6 +118,9 @@ func (t *wScreen) Fill(r rune, style Style) {
func (t *wScreen) SetContent(x, y int, mainc rune, combc []rune, style Style) {
t.Lock()
+ if x == t.cx && y == t.cy {
+ style.attrs |= attrCursor
+ }
t.cells.SetContent(x, y, mainc, combc, style)
t.Unlock()
}
@@ -98,6 +128,7 @@ func (t *wScreen) SetContent(x, y int, mainc rune, combc []rune, style Style) {
func (t *wScreen) GetContent(x, y int) (rune, []rune, Style, int) {
t.Lock()
mainc, combc, style, width := t.cells.GetContent(x, y)
+ style.attrs &= ^attrCursor
t.Unlock()
return mainc, combc, style, width
}
@@ -175,7 +206,17 @@ func (t *wScreen) drawCell(x, y int) int {
func (t *wScreen) ShowCursor(x, y int) {
t.Lock()
- js.Global().Call("showCursor", x, y)
+ if t.cx > -1 && t.cy > -1 {
+ mainc, combc, style, _ := t.cells.GetContent(t.cx, t.cy)
+ style.attrs &= ^attrCursor
+ t.cells.SetContent(t.cx, t.cy, mainc, combc, style)
+ }
+ t.cx, t.cy = x, y
+ if t.cx > -1 && t.cy > -1 {
+ mainc, combc, style, _ := t.cells.GetContent(t.cx, t.cy)
+ style.attrs |= attrCursor
+ t.cells.SetContent(t.cx, t.cy, mainc, combc, style)
+ }
t.Unlock()
}
@@ -212,8 +253,6 @@ func (t *wScreen) draw() {
x += width - 1
}
}
-
- js.Global().Call("show")
}
func (t *wScreen) EnableMouse(flags ...MouseFlags) {
@@ -366,6 +405,22 @@ func (t *wScreen) clip(x, y int) (int, int) {
return x, y
}
+func (t *wScreen) watchResize() {
+ for {
+ select {
+ case <-t.quit:
+ break
+ case rs := <-t.rsch:
+ t.SetSize(rs.w, rs.h)
+ }
+ }
+}
+
+func (t *wScreen) onResizeEvent(this js.Value, args []js.Value) interface{} {
+ t.rsch <- resized{w: args[0].Int(), h: args[1].Int()}
+ return nil
+}
+
func (t *wScreen) onMouseEvent(this js.Value, args []js.Value) interface{} {
mod := ModNone
button := ButtonNone