mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
more fshell
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
[EXEC_FUNS]
|
|
||||||
nes,A:/home/tvnes/tvnes.js {0}
|
|
||||||
|
|
||||||
[COL_HL_EXT]
|
|
||||||
nes,156
|
|
||||||
@@ -1,24 +1,414 @@
|
|||||||
graphics.setBackground(2,1,3);
|
graphics.setBackground(2,1,3)
|
||||||
graphics.resetPalette();
|
graphics.resetPalette()
|
||||||
|
const GL = require("gl")
|
||||||
|
const win = require("wintex")
|
||||||
|
const keysym = require("keysym")
|
||||||
|
|
||||||
function captureUserInput() {
|
function captureUserInput() {
|
||||||
sys.poke(-40, 1);
|
sys.poke(-40, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKeyPushed(keyOrder) {
|
function getKeyPushed(keyOrder) {
|
||||||
return sys.peek(-41 - keyOrder);
|
return sys.peek(-41 - keyOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
let _fsh = {};
|
function readMousePos() {
|
||||||
_fsh.titlebarTex = new GL.Texture(2, 14, base64.atob("/u/+/v3+/f39/f39/f39/f39/P39/Pz8/Pv7+w=="));
|
let lx = sys.peek(-33) & 0xFF
|
||||||
_fsh.scrdim = con.getmaxyx();
|
let hx = sys.peek(-34) & 0xFF
|
||||||
_fsh.scrwidth = _fsh.scrdim[1];
|
let ly = sys.peek(-35) & 0xFF
|
||||||
_fsh.scrheight = _fsh.scrdim[0];
|
let hy = sys.peek(-36) & 0xFF
|
||||||
_fsh.brandName = "f\xb3Sh";
|
return [(hx << 8) | lx, (hy << 8) | ly]
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMouseButtons() {
|
||||||
|
return sys.peek(-37) & 0xFF
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if any of the eight key event buffer slots holds keycode `kc`.
|
||||||
|
function isKeyDown(kc) {
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
if ((sys.peek(-41 - i) & 0xFF) === kc) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let _fsh = {}
|
||||||
|
|
||||||
|
// Config file path
|
||||||
|
_fsh.CONFIG_PATH = "A:/home/config/fshrc"
|
||||||
|
|
||||||
|
// Widget row caps (must match the loop bounds in draw())
|
||||||
|
_fsh.TODO_MAX_ROWS = 13 // todoWidget draws i = 0..12
|
||||||
|
_fsh.QA_MAX_ROWS = 22 // quickAccessWidget draws i = 0..21
|
||||||
|
_fsh.TODO_TEXT_WIDTH = 24 // visible characters per todo row
|
||||||
|
_fsh.QA_LABEL_WIDTH = 24 // visible characters per QA label
|
||||||
|
_fsh.QA_CMD_WIDTH = 60 // command path field width in dialog
|
||||||
|
|
||||||
|
// Highlight foreground for keyboard focus on widget lists. The background
|
||||||
|
// stays transparent (255) so the wallpaper continues to show through.
|
||||||
|
_fsh.HL_FG = 230
|
||||||
|
_fsh.HL_BG = 255
|
||||||
|
|
||||||
|
// Dialog colour pair. Background MUST be opaque (bg 255 is transparent
|
||||||
|
// in TSVM and lets the pixel-layer wallpaper bleed through dialog cells).
|
||||||
|
_fsh.DIALOG_FG = 254
|
||||||
|
_fsh.DIALOG_BG = 242
|
||||||
|
_fsh.FIELD_BG = 240
|
||||||
|
_fsh.DIALOG_DIM_FG = 249
|
||||||
|
|
||||||
|
// Default Quick Access entries when fshrc is missing or empty
|
||||||
|
_fsh.DEFAULT_QA = [
|
||||||
|
["Files", "/tvdos/bin/zsh.js"],
|
||||||
|
["Editor", "/tvdos/bin/edit.js"],
|
||||||
|
["BASIC", "/tbas/basic.js"],
|
||||||
|
["DOS Shell", "/tvdos/bin/command.js /fancy"]
|
||||||
|
]
|
||||||
|
|
||||||
|
// Mouse button bits (MMIO[36] layout per IOSpace.kt)
|
||||||
|
_fsh.MB_LEFT = 1
|
||||||
|
_fsh.MB_RIGHT = 2
|
||||||
|
|
||||||
|
// Current focus: null or {widgetId: string, index: number}.
|
||||||
|
// Index uses the same convention as hitTest: 0..length-1 are entries,
|
||||||
|
// `length` is the "+ Click to add" row.
|
||||||
|
_fsh.focus = null
|
||||||
|
|
||||||
|
// Parse fshrc text into {todos: [[text, done], ...], qa: [[label, cmd], ...]}.
|
||||||
|
// Returns null for both arrays when input is empty/whitespace.
|
||||||
|
_fsh.parseConfig = function(text) {
|
||||||
|
let todos = []
|
||||||
|
let qa = []
|
||||||
|
let section = null
|
||||||
|
if (!text) return {todos: todos, qa: qa}
|
||||||
|
let lines = text.split("\n")
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let line = lines[i]
|
||||||
|
// strip trailing \r if any
|
||||||
|
if (line.length && line.charCodeAt(line.length - 1) === 13) {
|
||||||
|
line = line.substring(0, line.length - 1)
|
||||||
|
}
|
||||||
|
if (line.length === 0) continue
|
||||||
|
if (line.charAt(0) === "[") {
|
||||||
|
let close = line.indexOf("]")
|
||||||
|
if (close > 0) {
|
||||||
|
let name = line.substring(1, close).trim().toUpperCase()
|
||||||
|
if (name === "TODO" || name === "QUICK_ACCESS") section = name
|
||||||
|
else section = null // unknown section: ignore until next header
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (section === "TODO") {
|
||||||
|
if (line.length < 2) continue
|
||||||
|
let marker = line.charAt(0)
|
||||||
|
if ((marker === "+" || marker === "-") && line.charAt(1) === " ") {
|
||||||
|
todos.push([line.substring(2), marker === "+"])
|
||||||
|
}
|
||||||
|
} else if (section === "QUICK_ACCESS") {
|
||||||
|
let comma = line.indexOf(",")
|
||||||
|
if (comma <= 0) continue // need a non-empty label
|
||||||
|
let label = line.substring(0, comma)
|
||||||
|
let cmd = line.substring(comma + 1)
|
||||||
|
qa.push([label, cmd])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {todos: todos, qa: qa}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build fshrc text from in-memory model. Inverse of parseConfig.
|
||||||
|
_fsh.serializeConfig = function(todos, qa) {
|
||||||
|
let out = "[TODO]\n"
|
||||||
|
for (let i = 0; i < todos.length; i++) {
|
||||||
|
let t = todos[i]
|
||||||
|
out += (t[1] ? "+ " : "- ") + t[0] + "\n"
|
||||||
|
}
|
||||||
|
out += "\n[QUICK_ACCESS]\n"
|
||||||
|
for (let i = 0; i < qa.length; i++) {
|
||||||
|
out += qa[i][0] + "," + qa[i][1] + "\n"
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read fshrc; populate todoWidget.todoList and quickAccessWidget.entries.
|
||||||
|
// Falls back to defaults on missing/empty/malformed file.
|
||||||
|
_fsh.loadConfig = function() {
|
||||||
|
let f = files.open(_fsh.CONFIG_PATH)
|
||||||
|
let parsed = {todos: [], qa: []}
|
||||||
|
if (f.exists) {
|
||||||
|
try {
|
||||||
|
parsed = _fsh.parseConfig(f.sread())
|
||||||
|
} catch (e) {
|
||||||
|
serial.printerr("fsh.loadConfig: parse failed: " + e)
|
||||||
|
parsed = {todos: [], qa: []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
todoWidget.todoList = parsed.todos
|
||||||
|
quickAccessWidget.entries = (parsed.qa.length > 0)
|
||||||
|
? parsed.qa
|
||||||
|
: _fsh.DEFAULT_QA.slice() // copy so saves don't mutate the constant
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist the current in-memory todos + QA entries to fshrc.
|
||||||
|
_fsh.saveConfig = function() {
|
||||||
|
try {
|
||||||
|
let f = files.open(_fsh.CONFIG_PATH)
|
||||||
|
if (!f.exists) f.mkFile()
|
||||||
|
f.swrite(_fsh.serializeConfig(todoWidget.todoList, quickAccessWidget.entries))
|
||||||
|
} catch (e) {
|
||||||
|
serial.printerr("fsh.saveConfig: write failed: " + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the bordered popup background. (row, col) is the top-left, (h, w)
|
||||||
|
// the size. Paints an opaque interior first (otherwise the wallpaper bleeds
|
||||||
|
// through cells with bg 255), then delegates frame drawing to wintex so the
|
||||||
|
// corner/edge glyphs always connect correctly.
|
||||||
|
_fsh.drawDialogFrame = function(row, col, h, w, title) {
|
||||||
|
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
con.move(row + y, col)
|
||||||
|
print(' '.repeat(w))
|
||||||
|
}
|
||||||
|
let wo = new win.WindowObject(col, row, w, h, function(){}, function(){}, title)
|
||||||
|
wo.isHighlighted = true
|
||||||
|
wo.titleBack = _fsh.DIALOG_BG
|
||||||
|
wo.drawFrame()
|
||||||
|
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slide the visible window so the caret stays inside (cursor at the
|
||||||
|
// rightmost column once it passes the field width).
|
||||||
|
_fsh.fieldScroll = function(cursor, width) {
|
||||||
|
return cursor < width ? 0 : cursor - width + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a single-line bordered input field at (row, col) with given width.
|
||||||
|
// content is the current text; cursor is the caret offset within content
|
||||||
|
// focused brightens the border colour.
|
||||||
|
_fsh.drawDialogField = function(row, col, width, content, cursor, focused) {
|
||||||
|
let frameFg = focused ? _fsh.DIALOG_FG : _fsh.DIALOG_DIM_FG
|
||||||
|
// Clear the field area (3 rows × width+2 cols) with FIELD_BG first so any
|
||||||
|
// stale chars from a previous render are wiped before we draw on top.
|
||||||
|
con.color_pair(_fsh.DIALOG_FG, _fsh.FIELD_BG)
|
||||||
|
con.move(row + 1, col + 1)
|
||||||
|
print(' '.repeat(width))
|
||||||
|
// Top border
|
||||||
|
con.color_pair(frameFg, _fsh.DIALOG_BG)
|
||||||
|
con.move(row, col)
|
||||||
|
print('\u00DA') // ┌
|
||||||
|
print('\u00C4'.repeat(width)) // ─
|
||||||
|
print('\u00BF') // ┐
|
||||||
|
// Vertical borders + content
|
||||||
|
con.move(row + 1, col)
|
||||||
|
print('\u00B3') // │
|
||||||
|
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
||||||
|
let scroll = _fsh.fieldScroll(cursor, width)
|
||||||
|
let visible = content.substring(scroll, scroll + width)
|
||||||
|
print(visible)
|
||||||
|
con.color_pair(frameFg, _fsh.DIALOG_BG)
|
||||||
|
con.move(row + 1, col + width + 1)
|
||||||
|
print('\u00B3') // │
|
||||||
|
// Bottom border
|
||||||
|
con.move(row + 2, col)
|
||||||
|
print('\u00C0') // └
|
||||||
|
print('\u00C4'.repeat(width)) // ─
|
||||||
|
print('\u00D9') // ┘
|
||||||
|
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a button as "[ Label ]" at the given position; highlights when focused.
|
||||||
|
_fsh.drawDialogButton = function(row, col, label, focused) {
|
||||||
|
if (focused) con.color_pair(_fsh.HL_FG, _fsh.DIALOG_BG)
|
||||||
|
else con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
||||||
|
con.move(row, col)
|
||||||
|
print("[ " + label + " ]")
|
||||||
|
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal dialog. opts = {
|
||||||
|
// title: string,
|
||||||
|
// fields: [{label, initial, width}, ...],
|
||||||
|
// allowDelete: bool,
|
||||||
|
// }
|
||||||
|
// Returns {action: "ok"|"cancel"|"delete", values: [string, ...]}.
|
||||||
|
_fsh.showDialog = function(opts) {
|
||||||
|
let fields = opts.fields
|
||||||
|
let values = fields.map(function(f) { return f.initial || "" })
|
||||||
|
// Caret position per field. Start at end of any pre-filled initial text.
|
||||||
|
let cursors = values.map(function(v) { return v.length })
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
let maxFieldW = fields.reduce(function(m, f) { return Math.max(m, f.width) }, 16)
|
||||||
|
let titleW = (opts.title ? opts.title.length : 0) + 4
|
||||||
|
let w = Math.max(maxFieldW + 6, titleW + 4, 24)
|
||||||
|
let buttonsRow = 2 + fields.length * 4 + 1 // 1 label + 3 field rows per field
|
||||||
|
let h = buttonsRow + 2
|
||||||
|
let screen = con.getmaxyx()
|
||||||
|
let row = Math.max(2, Math.floor((screen[0] - h) / 2))
|
||||||
|
let col = Math.max(2, Math.floor((screen[1] - w) / 2))
|
||||||
|
|
||||||
|
// Buttons list: indices follow Tab order after the last field
|
||||||
|
let buttons = [{label: "OK", action: "ok"}, {label: "Cancel", action: "cancel"}]
|
||||||
|
if (opts.allowDelete) buttons.splice(1, 0, {label: "Delete", action: "delete"})
|
||||||
|
|
||||||
|
let focusIdx = 0 // 0..fields.length-1 = field; then buttons
|
||||||
|
let totalFocus = fields.length + buttons.length
|
||||||
|
let done = null // {action, values} when set
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
_fsh.drawDialogFrame(row, col, h, w, opts.title)
|
||||||
|
// Fields
|
||||||
|
for (let i = 0; i < fields.length; i++) {
|
||||||
|
let labelRow = row + 1 + i * 4
|
||||||
|
let fieldRow = labelRow + 1
|
||||||
|
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
|
||||||
|
con.move(labelRow, col + 2)
|
||||||
|
print(fields[i].label + ":")
|
||||||
|
_fsh.drawDialogField(fieldRow, col + 2, fields[i].width,
|
||||||
|
values[i], cursors[i], i === focusIdx)
|
||||||
|
}
|
||||||
|
// Buttons centred on buttonsRow
|
||||||
|
let totalBtnW = buttons.reduce(function(s, b) { return s + b.label.length + 5 }, 0) - 1
|
||||||
|
let bx = col + Math.floor((w - totalBtnW) / 2)
|
||||||
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
|
let bIdx = fields.length + i
|
||||||
|
_fsh.drawDialogButton(row + buttonsRow, bx, buttons[i].label, bIdx === focusIdx)
|
||||||
|
bx += buttons[i].label.length + 5
|
||||||
|
}
|
||||||
|
// Position the visible caret. Inside a field: place it on the content
|
||||||
|
// row at the cursor offset (corrected for horizontal scroll). On a
|
||||||
|
// button: hide the caret entirely.
|
||||||
|
if (focusIdx < fields.length) {
|
||||||
|
let fldWidth = fields[focusIdx].width
|
||||||
|
let scroll = _fsh.fieldScroll(cursors[focusIdx], fldWidth)
|
||||||
|
let contentRow = row + 1 + focusIdx * 4 + 2
|
||||||
|
let contentCol = col + 2 + 1 + (cursors[focusIdx] - scroll)
|
||||||
|
con.move(contentRow, contentCol)
|
||||||
|
con.curs_set(1)
|
||||||
|
} else {
|
||||||
|
con.curs_set(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
|
||||||
|
// Note: con.getch() returns TSVM scancodes (defined in JS_INIT.js as
|
||||||
|
// con.KEY_UP=200, KEY_DOWN=208, KEY_LEFT=203, KEY_RIGHT=205,
|
||||||
|
// con.KEY_BACKSPACE=8, KEY_TAB=9, KEY_RETURN=10). Esc isn't in JS_INIT's
|
||||||
|
// map — it arrives as ASCII 27 via keyTyped().
|
||||||
|
while (done === null) {
|
||||||
|
let k = con.getch()
|
||||||
|
|
||||||
|
if (k === 27) { // Esc
|
||||||
|
done = {action: "cancel", values: values}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (k === con.KEY_TAB) {
|
||||||
|
focusIdx = (focusIdx + 1) % totalFocus
|
||||||
|
render()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Up/Down always cycles focus across fields/buttons.
|
||||||
|
if (k === con.KEY_UP) {
|
||||||
|
focusIdx = (focusIdx - 1 + totalFocus) % totalFocus
|
||||||
|
render()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (k === con.KEY_DOWN) {
|
||||||
|
focusIdx = (focusIdx + 1) % totalFocus
|
||||||
|
render()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Left/Right moves the caret inside a field; on a button it cycles.
|
||||||
|
if (k === con.KEY_LEFT) {
|
||||||
|
if (focusIdx < fields.length) {
|
||||||
|
if (cursors[focusIdx] > 0) {
|
||||||
|
cursors[focusIdx] -= 1
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
focusIdx = (focusIdx - 1 + totalFocus) % totalFocus
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (k === con.KEY_RIGHT) {
|
||||||
|
if (focusIdx < fields.length) {
|
||||||
|
if (cursors[focusIdx] < values[focusIdx].length) {
|
||||||
|
cursors[focusIdx] += 1
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
focusIdx = (focusIdx + 1) % totalFocus
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// On a field
|
||||||
|
if (focusIdx < fields.length) {
|
||||||
|
if (k === con.KEY_RETURN) {
|
||||||
|
if (focusIdx < fields.length - 1) {
|
||||||
|
focusIdx += 1
|
||||||
|
} else {
|
||||||
|
focusIdx = fields.length // move to OK button
|
||||||
|
}
|
||||||
|
render()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (k === con.KEY_BACKSPACE) {
|
||||||
|
let c = cursors[focusIdx]
|
||||||
|
if (c > 0) {
|
||||||
|
let v = values[focusIdx]
|
||||||
|
values[focusIdx] = v.substring(0, c - 1) + v.substring(c)
|
||||||
|
cursors[focusIdx] = c - 1
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Printable: insert at the caret.
|
||||||
|
if (k >= 32 && k < 256 && values[focusIdx].length < fields[focusIdx].width * 4) {
|
||||||
|
let v = values[focusIdx]
|
||||||
|
let c = cursors[focusIdx]
|
||||||
|
values[focusIdx] = v.substring(0, c) + String.fromCharCode(k) + v.substring(c)
|
||||||
|
cursors[focusIdx] = c + 1
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// On a button
|
||||||
|
if (k === con.KEY_RETURN || k === 32) {
|
||||||
|
done = {action: buttons[focusIdx - fields.length].action, values: values}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
con.curs_set(0)
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map (mouse char x, mouse char y) to a row index for a widget drawn at
|
||||||
|
// (xoff, yoff) with `length` existing entries and `maxRows` total rows.
|
||||||
|
// Returns null / {kind:"add"} / {kind:"item", index: i}.
|
||||||
|
_fsh.hitTestList = function(charX, charY, xoff, yoff, textWidth, length, maxRows) {
|
||||||
|
// Each row sits at (yoff + i + 2, xoff..xoff + textWidth + 1).
|
||||||
|
// Column range: icon at xoff, text at xoff+2 .. xoff+1+textWidth.
|
||||||
|
// Allow clicks anywhere on the row's char cells (icon + text region).
|
||||||
|
let relY = charY - yoff - 2
|
||||||
|
if (relY < 0 || relY >= maxRows) return null
|
||||||
|
if (charX < xoff || charX > xoff + 1 + textWidth) return null
|
||||||
|
if (relY < length) return {kind: "item", index: relY}
|
||||||
|
if (relY === length) return {kind: "add"}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
_fsh.titlebarTex = new GL.Texture(2, 14, base64.atob("/u/+/v3+/f39/f39/f39/f39/P39/Pz8/Pv7+w=="))
|
||||||
|
_fsh.scrdim = con.getmaxyx()
|
||||||
|
_fsh.scrwidth = _fsh.scrdim[1]
|
||||||
|
_fsh.scrheight = _fsh.scrdim[0]
|
||||||
|
_fsh.brandName = "f\xb3Sh"
|
||||||
_fsh.brandLogoTexSmall = new GL.Texture(24, 14, gzip.decomp(base64.atob(
|
_fsh.brandLogoTexSmall = new GL.Texture(24, 14, gzip.decomp(base64.atob(
|
||||||
"H4sIAAAAAAAAAPv/Hy/4Qbz458+fIeILQQBIwoSh6qECuMVBukCmIJkDVQ+RQNgLE0MX/w+1lyhxqIUwTLJ/sQMAcIXsbVABAAA="
|
"H4sIAAAAAAAAAPv/Hy/4Qbz458+fIeILQQBIwoSh6qECuMVBukCmIJkDVQ+RQNgLE0MX/w+1lyhxqIUwTLJ/sQMAcIXsbVABAAA="
|
||||||
)));
|
)))
|
||||||
_fsh.scrlayout = ["com.fsh.clock","com.fsh.calendar","com.fsh.todo_list", "com.fsh.quick_access"];
|
_fsh.scrlayout = ["com.fsh.clock","com.fsh.calendar","com.fsh.todo_list", "com.fsh.quick_access"]
|
||||||
|
|
||||||
_fsh.drawWallpaper = function() {
|
_fsh.drawWallpaper = function() {
|
||||||
let wp = files.open("A:/home/wall.bytes")
|
let wp = files.open("A:/home/wall.bytes")
|
||||||
@@ -28,85 +418,85 @@ _fsh.drawWallpaper = function() {
|
|||||||
wp.pread(b, 250880, 0)
|
wp.pread(b, 250880, 0)
|
||||||
dma.ramToFrame(b, 0, 250880)
|
dma.ramToFrame(b, 0, 250880)
|
||||||
sys.free(b)
|
sys.free(b)
|
||||||
};
|
}
|
||||||
|
|
||||||
_fsh.drawTitlebar = function(titletext) {
|
_fsh.drawTitlebar = function(titletext) {
|
||||||
GL.drawTexPattern(_fsh.titlebarTex, 0, 0, 560, 14);
|
GL.drawTexPattern(_fsh.titlebarTex, 0, 0, 560, 14)
|
||||||
if (titletext === undefined || titletext.length == 0) {
|
if (titletext === undefined || titletext.length == 0) {
|
||||||
con.move(1,1);
|
con.move(1,1)
|
||||||
print(" ".repeat(_fsh.scrwidth));
|
print(" ".repeat(_fsh.scrwidth))
|
||||||
GL.drawTexImageOver(_fsh.brandLogoTexSmall, 268, 0);
|
GL.drawTexImageOver(_fsh.brandLogoTexSmall, 268, 0)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
con.color_pair(240, 255);
|
con.color_pair(240, 255)
|
||||||
GL.drawTexPattern(_fsh.titlebarTex, 268, 0, 24, 14);
|
GL.drawTexPattern(_fsh.titlebarTex, 268, 0, 24, 14)
|
||||||
con.move(1, 1 + (_fsh.scrwidth - titletext.length) / 2);
|
con.move(1, 1 + (_fsh.scrwidth - titletext.length) / 2)
|
||||||
print(titletext);
|
print(titletext)
|
||||||
|
}
|
||||||
|
con.color_pair(254, 255)
|
||||||
}
|
}
|
||||||
con.color_pair(254, 255);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
_fsh.Widget = function(id, w, h) {
|
_fsh.Widget = function(id, w, h) {
|
||||||
this.identifier = id;
|
this.identifier = id
|
||||||
this.width = w;
|
this.width = w
|
||||||
this.height = h;
|
this.height = h
|
||||||
|
|
||||||
if (!this.identifier) {
|
if (!this.identifier) {
|
||||||
this.identifier = "";
|
this.identifier = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
//this.update = function() {};
|
//this.update = function() {}
|
||||||
/**
|
/**
|
||||||
* Params charXoff and charYoff are ZERO-BASED!
|
* Params charXoff and charYoff are ZERO-BASED!
|
||||||
*/
|
*/
|
||||||
this.draw = function(charXoff, charYoff) {};
|
this.draw = function(charXoff, charYoff) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
_fsh.widgets = {}
|
_fsh.widgets = {}
|
||||||
_fsh.registerNewWidget = function(widget) {
|
_fsh.registerNewWidget = function(widget) {
|
||||||
_fsh.widgets[widget.identifier] = widget;
|
_fsh.widgets[widget.identifier] = widget
|
||||||
}
|
}
|
||||||
|
|
||||||
let clockWidget = new _fsh.Widget("com.fsh.clock", _fsh.scrwidth - 8, 7*2);
|
let clockWidget = new _fsh.Widget("com.fsh.clock", _fsh.scrwidth - 8, 7*2)
|
||||||
clockWidget.numberSheet = new GL.SpriteSheet(19, 22, new GL.Texture(190, 22, gzip.decomp(base64.atob(
|
clockWidget.numberSheet = new GL.SpriteSheet(19, 22, new GL.Texture(190, 22, gzip.decomp(base64.atob(
|
||||||
"H4sIAAAAAAAAAMWVW3LEMAgE739aHcFJJV5ZMD2I9ToVfcl4GBr80HF8r/FaR1ozMuIyoUu87lEXI0al5qVR5AebSwchSaNE6Nyo1Nw5HXF3SfPT4Bshl"+
|
"H4sIAAAAAAAAAMWVW3LEMAgE739aHcFJJV5ZMD2I9ToVfcl4GBr80HF8r/FaR1ozMuIyoUu87lEXI0al5qVR5AebSwchSaNE6Nyo1Nw5HXF3SfPT4Bshl"+
|
||||||
"EycA8RD96mLlHbuhTgOrfLnUDZspafbSQWk56WEGvQEtWaWwgb8iz7a8AOXhsraO/q9Qw2/GnXovfVN+q2wM/p/oddn2cjF239GX3y11+SWCtc6FTHC1v"+
|
"EycA8RD96mLlHbuhTgOrfLnUDZspafbSQWk56WEGvQEtWaWwgb8iz7a8AOXhsraO/q9Qw2/GnXovfVN+q2wM/p/oddn2cjF239GX3y11+SWCtc6FTHC1v"+
|
||||||
"TVPkDPWWn0w+DDz93UX9v9mF5KIsQ6OdN2KJoB4ui1bXXr0AMp0YfiQo//4XhpK8555dsNehAqVS5uhb5iHn3Kko769J59KmLBe/TSR7hcsd+hr+HnrwR"+
|
"TVPkDPWWn0w+DDz93UX9v9mF5KIsQ6OdN2KJoB4ui1bXXr0AMp0YfiQo//4XhpK8555dsNehAqVS5uhb5iHn3Kko769J59KmLBe/TSR7hcsd+hr+HnrwR"+
|
||||||
"9uvRF9+D3MP14gN7lqx+8OuNT+uqt3NFX3SN9fTbeeHNq+C29pRWzX5+Rcm7SZyjOKJ/2hkSPqul4xN279DrSYvCrNu2NI7ZMp1ouBxK3KBVVnEeAUWbK"+
|
"9uvRF9+D3MP14gN7lqx+8OuNT+uqt3NFX3SN9fTbeeHNq+C29pRWzX5+Rcm7SZyjOKJ/2hkSPqul4xN279DrSYvCrNu2NI7ZMp1ouBxK3KBVVnEeAUWbK"+
|
||||||
"MUDn5DPsPxmUqHZQjGpy2hergM3EVBAAAA=="
|
"MUDn5DPsPxmUqHZQjGpy2hergM3EVBAAAA=="
|
||||||
))));
|
))))
|
||||||
|
|
||||||
clockWidget.clockColon = new GL.Texture(4, 3, base64.atob("7+/v7+/v7+/v7+/v"));
|
clockWidget.clockColon = new GL.Texture(4, 3, base64.atob("7+/v7+/v7+/v7+/v"))
|
||||||
clockWidget.monthNames = ["Spring", "Summer", "Autumn", "Winter"];
|
clockWidget.monthNames = ["Spring", "Summer", "Autumn", "Winter"]
|
||||||
clockWidget.dayNames = ["Mondag ", "Tysdag ", "Midtveke", "Torsdag ", "Fredag ", "Laurdag ", "Sundag ", "Verddag "];
|
clockWidget.dayNames = ["Mondag ", "Tysdag ", "Midtveke", "Torsdag ", "Fredag ", "Laurdag ", "Sundag ", "Verddag "]
|
||||||
clockWidget.draw = function(charXoff, charYoff) {
|
clockWidget.draw = function(charXoff, charYoff) {
|
||||||
con.color_pair(254, 255);
|
con.color_pair(254, 255)
|
||||||
let xoff = charXoff * 7;
|
let xoff = charXoff * 7
|
||||||
let yoff = charYoff * 14 + 3;
|
let yoff = charYoff * 14 + 3
|
||||||
let timeInMinutes = ((sys.currentTimeInMills() / 60000)|0);
|
let timeInMinutes = ((sys.currentTimeInMills() / 60000)|0)
|
||||||
let mins = timeInMinutes % 60;
|
let mins = timeInMinutes % 60
|
||||||
let hours = ((timeInMinutes / 60)|0) % 24;
|
let hours = ((timeInMinutes / 60)|0) % 24
|
||||||
let ordinalDay = ((timeInMinutes / (60*24))|0) % 120;
|
let ordinalDay = ((timeInMinutes / (60*24))|0) % 120
|
||||||
let visualDay = (ordinalDay % 30) + 1;
|
let visualDay = (ordinalDay % 30) + 1
|
||||||
let months = ((timeInMinutes / (60*24*30))|0) % 4;
|
let months = ((timeInMinutes / (60*24*30))|0) % 4
|
||||||
let dayName = ordinalDay % 7; // 0 for Mondag
|
let dayName = ordinalDay % 7 // 0 for Mondag
|
||||||
if (ordinalDay == 119) dayName = 7; // Verddag
|
if (ordinalDay == 119) dayName = 7 // Verddag
|
||||||
let years = ((timeInMinutes / (60*24*30*120))|0) + 125;
|
let years = ((timeInMinutes / (60*24*30*120))|0) + 125
|
||||||
// draw timepiece
|
// draw timepiece
|
||||||
GL.drawSprite(clockWidget.numberSheet, (hours / 10)|0, 0, xoff, yoff, 1);
|
GL.drawSprite(clockWidget.numberSheet, (hours / 10)|0, 0, xoff, yoff, 1)
|
||||||
GL.drawSprite(clockWidget.numberSheet, hours % 10, 0, xoff + 24, yoff, 1);
|
GL.drawSprite(clockWidget.numberSheet, hours % 10, 0, xoff + 24, yoff, 1)
|
||||||
GL.drawTexImage(clockWidget.clockColon, xoff + 48, yoff + 5, 1);
|
GL.drawTexImage(clockWidget.clockColon, xoff + 48, yoff + 5, 1)
|
||||||
GL.drawTexImage(clockWidget.clockColon, xoff + 48, yoff + 14, 1);
|
GL.drawTexImage(clockWidget.clockColon, xoff + 48, yoff + 14, 1)
|
||||||
GL.drawSprite(clockWidget.numberSheet, (mins / 10)|0, 0, xoff + 57, yoff, 1);
|
GL.drawSprite(clockWidget.numberSheet, (mins / 10)|0, 0, xoff + 57, yoff, 1)
|
||||||
GL.drawSprite(clockWidget.numberSheet, mins % 10, 0, xoff + 81, yoff, 1);
|
GL.drawSprite(clockWidget.numberSheet, mins % 10, 0, xoff + 81, yoff, 1)
|
||||||
// print month and date
|
// print month and date
|
||||||
con.move(1 + charYoff, 17 + charXoff);
|
con.move(1 + charYoff, 17 + charXoff)
|
||||||
print(clockWidget.monthNames[months]+" "+visualDay);
|
print(clockWidget.monthNames[months]+" "+visualDay)
|
||||||
// print year and dayname
|
// print year and dayname
|
||||||
con.move(2 + charYoff, 17 + charXoff);
|
con.move(2 + charYoff, 17 + charXoff)
|
||||||
print("\xE7"+years+" "+clockWidget.dayNames[dayName]);
|
print("\xE7"+years+" "+clockWidget.dayNames[dayName])
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
let calendarWidget = new _fsh.Widget("com.fsh.calendar", (_fsh.scrwidth - 8) / 2, 7*6)
|
let calendarWidget = new _fsh.Widget("com.fsh.calendar", (_fsh.scrwidth - 8) / 2, 7*6)
|
||||||
@@ -171,70 +561,281 @@ calendarWidget.draw = function(charXoff, charYoff) {
|
|||||||
let todoWidget = new _fsh.Widget("com.fsh.todo_list", (_fsh.scrwidth - 8) / 2, 7*10)
|
let todoWidget = new _fsh.Widget("com.fsh.todo_list", (_fsh.scrwidth - 8) / 2, 7*10)
|
||||||
todoWidget.todoList = [["Hello, world!", true]]
|
todoWidget.todoList = [["Hello, world!", true]]
|
||||||
todoWidget.draw = function(charXoff, charYoff) {
|
todoWidget.draw = function(charXoff, charYoff) {
|
||||||
|
let focusIndex = (_fsh.focus && _fsh.focus.widgetId === todoWidget.identifier)
|
||||||
|
? _fsh.focus.index : -1
|
||||||
|
|
||||||
con.color_pair(254, 255)
|
con.color_pair(254, 255)
|
||||||
let xoff = charXoff * 7
|
let xoff = charXoff * 7
|
||||||
let yoff = charYoff * 14 + 3
|
let yoff = charYoff * 14 + 3
|
||||||
|
|
||||||
con.move(charYoff, charXoff)
|
con.move(charYoff, charXoff)
|
||||||
print("========== TODO ==========")
|
print('\u00CD'.repeat(10)+" TODO "+'\u00CD'.repeat(10))
|
||||||
|
|
||||||
for (let i = 0; i <= 12; i++) {
|
for (let i = 0; i <= 12; i++) {
|
||||||
let list = todoWidget.todoList[i] || ["Click to add", null]
|
let list = todoWidget.todoList[i] || ["Click to add"+" ".repeat(_fsh.TODO_TEXT_WIDTH - 12), null]
|
||||||
|
let isFocused = (i === focusIndex)
|
||||||
|
|
||||||
if (list[1] === null) con.color_pair(249, 255)
|
if (isFocused) con.color_pair(_fsh.HL_FG, _fsh.HL_BG)
|
||||||
|
else if (list[1] === null) con.color_pair(249, 255)
|
||||||
else con.color_pair(254, 255)
|
else con.color_pair(254, 255)
|
||||||
|
|
||||||
con.move(charYoff + i + 2, charXoff)
|
con.move(charYoff + i + 2, charXoff)
|
||||||
con.addch((list[1] === null) ? 43 : (list[1]) ? 0x9F : 0x9E)
|
con.addch((list[1] === null) ? 43 : (list[1]) ? 0x9F : 0x9E)
|
||||||
|
|
||||||
if (i > todoWidget.todoList.length) {
|
if (i > todoWidget.todoList.length) {
|
||||||
|
// Filler row \u2014 keep underscores but don't highlight (can't focus here)
|
||||||
|
con.color_pair(254, 255)
|
||||||
for (let k = 0; k < 24; k++) {
|
for (let k = 0; k < 24; k++) {
|
||||||
con.mvaddch(charYoff + i + 2, charXoff + 2 + k, 95)
|
con.mvaddch(charYoff + i + 2, charXoff + 2 + k, 95)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
con.move(charYoff + i + 2, charXoff + 2)
|
con.move(charYoff + i + 2, charXoff + 2)
|
||||||
print(`${list[0]}`)
|
// Pad text to TODO_TEXT_WIDTH so the highlight bar covers full row
|
||||||
|
let text = `${list[0]}`
|
||||||
|
if (text.length > _fsh.TODO_TEXT_WIDTH) text = text.substring(0, _fsh.TODO_TEXT_WIDTH)
|
||||||
|
if (isFocused) text = text + " ".repeat(_fsh.TODO_TEXT_WIDTH - text.length)
|
||||||
|
print(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let quickAccessWidget = new _fsh.Widget("com.fsh.quick_access", (_fsh.scrwidth - 8) / 2, 7*20)
|
let quickAccessWidget = new _fsh.Widget("com.fsh.quick_access", (_fsh.scrwidth - 8) / 2, 7*20)
|
||||||
quickAccessWidget.entries = [
|
quickAccessWidget.entries = [ // TODO read from /home/config/fshrc
|
||||||
["Files", "/tvdos/bin/explorer.js"],
|
["Files", "/tvdos/bin/zfm.js"],
|
||||||
["Editor", "/tvdos/bin/edit.js"],
|
["Editor", "/tvdos/bin/edit.js"],
|
||||||
["BASIC", "/tbas/basic.js"],
|
["BASIC", "/tbas/basic.js"],
|
||||||
["DOS Shell", "/tvdos/bin/command.js /fancy"]
|
["DOS Shell", "/tvdos/bin/command.js -fancy"]
|
||||||
]
|
]
|
||||||
quickAccessWidget.draw = function(charXoff, charYoff) {
|
quickAccessWidget.draw = function(charXoff, charYoff) {
|
||||||
|
let focusIndex = (_fsh.focus && _fsh.focus.widgetId === quickAccessWidget.identifier)
|
||||||
|
? _fsh.focus.index : -1
|
||||||
|
|
||||||
con.color_pair(254, 255)
|
con.color_pair(254, 255)
|
||||||
let xoff = charXoff * 7
|
let xoff = charXoff * 7
|
||||||
let yoff = charYoff * 14 + 3
|
let yoff = charYoff * 14 + 3
|
||||||
|
|
||||||
con.move(charYoff, charXoff)
|
con.move(charYoff, charXoff)
|
||||||
print("====== QUICK ACCESS ======")
|
print('\u00CD'.repeat(6)+" QUICK ACCESS "+'\u00CD'.repeat(6))
|
||||||
|
|
||||||
for (let i = 0; i <= 21; i++) {
|
for (let i = 0; i <= 21; i++) {
|
||||||
let list = quickAccessWidget.entries[i] || ["Click to add", null]
|
let list = quickAccessWidget.entries[i] || ["Click to add"+" ".repeat(_fsh.QA_LABEL_WIDTH - 12), null]
|
||||||
|
let isFocused = (i === focusIndex)
|
||||||
|
|
||||||
if (list[1] === null) con.color_pair(249, 255)
|
if (isFocused) con.color_pair(_fsh.HL_FG, _fsh.HL_BG)
|
||||||
|
else if (list[1] === null) con.color_pair(249, 255)
|
||||||
else con.color_pair(254, 255)
|
else con.color_pair(254, 255)
|
||||||
|
|
||||||
con.move(charYoff + i + 2, charXoff)
|
con.move(charYoff + i + 2, charXoff)
|
||||||
con.addch((list[1] === null) ? 0xF9 : (list[1]) ? 7 : 0x7F)
|
con.addch((list[1] === null) ? 0xF9 : (list[1]) ? 7 : 0x7F)
|
||||||
|
|
||||||
if (i > quickAccessWidget.entries.length) {
|
if (i > quickAccessWidget.entries.length) {
|
||||||
|
con.color_pair(254, 255)
|
||||||
for (let k = 0; k < 24; k++) {
|
for (let k = 0; k < 24; k++) {
|
||||||
con.mvaddch(charYoff + i + 2, charXoff + 2 + k, 95)
|
con.mvaddch(charYoff + i + 2, charXoff + 2 + k, 95)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
con.move(charYoff + i + 2, charXoff + 2)
|
con.move(charYoff + i + 2, charXoff + 2)
|
||||||
print(`${list[0]}`)
|
let text = `${list[0]}`
|
||||||
|
if (text.length > _fsh.QA_LABEL_WIDTH) text = text.substring(0, _fsh.QA_LABEL_WIDTH)
|
||||||
|
if (isFocused) text = text + " ".repeat(_fsh.QA_LABEL_WIDTH - text.length)
|
||||||
|
print(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
todoWidget.hitTest = function(charX, charY, xoff, yoff) {
|
||||||
|
return _fsh.hitTestList(charX, charY, xoff, yoff,
|
||||||
|
_fsh.TODO_TEXT_WIDTH, todoWidget.todoList.length, _fsh.TODO_MAX_ROWS)
|
||||||
|
}
|
||||||
|
|
||||||
|
quickAccessWidget.hitTest = function(charX, charY, xoff, yoff) {
|
||||||
|
return _fsh.hitTestList(charX, charY, xoff, yoff,
|
||||||
|
_fsh.QA_LABEL_WIDTH, quickAccessWidget.entries.length, _fsh.QA_MAX_ROWS)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Re-render the whole shell. Use after a dialog closes (which clobbered
|
||||||
|
// the underlying char cells) or after execApp returns.
|
||||||
|
_fsh.redrawAll = function() {
|
||||||
|
con.color_pair(254, 255)
|
||||||
|
con.clear()
|
||||||
|
graphics.clearPixels(255)
|
||||||
|
graphics.clearPixels2(255)
|
||||||
|
graphics.setFramebufferScroll(0, 0)
|
||||||
|
_fsh.drawWallpaper()
|
||||||
|
_fsh.drawTitlebar()
|
||||||
|
_fsh.widgets["com.fsh.clock"].draw(25, 3)
|
||||||
|
_fsh.widgets["com.fsh.calendar"].draw(12, 8)
|
||||||
|
_fsh.widgets["com.fsh.todo_list"].draw(10, 17)
|
||||||
|
_fsh.widgets["com.fsh.quick_access"].draw(47, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
_fsh.openAddTodoDialog = function() {
|
||||||
|
let res = _fsh.showDialog({
|
||||||
|
title: "New Todo",
|
||||||
|
fields: [{label: "Text", initial: "", width: _fsh.TODO_TEXT_WIDTH}],
|
||||||
|
allowDelete: false
|
||||||
|
})
|
||||||
|
_fsh.redrawAll()
|
||||||
|
if (res.action !== "ok") return
|
||||||
|
let text = res.values[0].trim()
|
||||||
|
if (text.length === 0) return
|
||||||
|
if (todoWidget.todoList.length >= _fsh.TODO_MAX_ROWS) return
|
||||||
|
todoWidget.todoList.push([text, false])
|
||||||
|
_fsh.saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
_fsh.openEditTodoDialog = function(index) {
|
||||||
|
let entry = todoWidget.todoList[index]
|
||||||
|
if (!entry) return
|
||||||
|
let res = _fsh.showDialog({
|
||||||
|
title: "Edit Todo",
|
||||||
|
fields: [{label: "Text", initial: entry[0], width: _fsh.TODO_TEXT_WIDTH}],
|
||||||
|
allowDelete: true
|
||||||
|
})
|
||||||
|
_fsh.redrawAll()
|
||||||
|
if (res.action === "cancel") return
|
||||||
|
if (res.action === "delete") {
|
||||||
|
todoWidget.todoList.splice(index, 1)
|
||||||
|
_fsh.saveConfig()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let text = res.values[0].trim()
|
||||||
|
if (text.length === 0) return
|
||||||
|
todoWidget.todoList[index] = [text, entry[1]]
|
||||||
|
_fsh.saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
_fsh.openAddQaDialog = function() {
|
||||||
|
let res = _fsh.showDialog({
|
||||||
|
title: "New Quick Access",
|
||||||
|
fields: [
|
||||||
|
{label: "Label", initial: "", width: _fsh.QA_LABEL_WIDTH},
|
||||||
|
{label: "Command", initial: "", width: _fsh.QA_CMD_WIDTH}
|
||||||
|
],
|
||||||
|
allowDelete: false
|
||||||
|
})
|
||||||
|
_fsh.redrawAll()
|
||||||
|
if (res.action !== "ok") return
|
||||||
|
let label = res.values[0].trim()
|
||||||
|
let cmd = res.values[1].trim()
|
||||||
|
if (label.length === 0 || cmd.length === 0) return
|
||||||
|
if (quickAccessWidget.entries.length >= _fsh.QA_MAX_ROWS) return
|
||||||
|
quickAccessWidget.entries.push([label, cmd])
|
||||||
|
_fsh.saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
_fsh.openEditQaDialog = function(index) {
|
||||||
|
let entry = quickAccessWidget.entries[index]
|
||||||
|
if (!entry) return
|
||||||
|
let res = _fsh.showDialog({
|
||||||
|
title: "Edit Quick Access",
|
||||||
|
fields: [
|
||||||
|
{label: "Label", initial: entry[0], width: _fsh.QA_LABEL_WIDTH},
|
||||||
|
{label: "Command", initial: entry[1], width: _fsh.QA_CMD_WIDTH}
|
||||||
|
],
|
||||||
|
allowDelete: true
|
||||||
|
})
|
||||||
|
_fsh.redrawAll()
|
||||||
|
if (res.action === "cancel") return
|
||||||
|
if (res.action === "delete") {
|
||||||
|
quickAccessWidget.entries.splice(index, 1)
|
||||||
|
_fsh.saveConfig()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let label = res.values[0].trim()
|
||||||
|
let cmd = res.values[1].trim()
|
||||||
|
if (label.length === 0 || cmd.length === 0) return
|
||||||
|
quickAccessWidget.entries[index] = [label, cmd]
|
||||||
|
_fsh.saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
_fsh.toggleTodoDone = function(index) {
|
||||||
|
let entry = todoWidget.todoList[index]
|
||||||
|
if (!entry) return
|
||||||
|
entry[1] = !entry[1]
|
||||||
|
_fsh.saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch a Quick Access entry. cmd is the verbatim string the user typed.
|
||||||
|
// We split on first space to derive a program path + args; if the path
|
||||||
|
// has no leading "/", we treat it as relative to the current drive.
|
||||||
|
_fsh.launchEntry = function(label, cmd) {
|
||||||
|
let firstSpace = cmd.indexOf(" ")
|
||||||
|
let progPath = (firstSpace >= 0) ? cmd.substring(0, firstSpace) : cmd
|
||||||
|
let argTail = (firstSpace >= 0) ? cmd.substring(firstSpace + 1) : ""
|
||||||
|
let fullPath = progPath.startsWith("/") ? ("A:" + progPath) : progPath
|
||||||
|
|
||||||
|
try {
|
||||||
|
let f = files.open(fullPath)
|
||||||
|
if (!f.exists) {
|
||||||
|
serial.printerr("fsh.launchEntry: not found: " + fullPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let code = f.sread()
|
||||||
|
let tokens = [progPath].concat(argTail.length ? argTail.split(" ") : [])
|
||||||
|
|
||||||
|
// erase all pixels and draw wallpaper
|
||||||
|
con.reset_graphics()
|
||||||
|
con.clear()
|
||||||
|
graphics.clearPixels(255)
|
||||||
|
graphics.clearPixels2(255)
|
||||||
|
_fsh.drawWallpaper()
|
||||||
|
con.curs_set(1)
|
||||||
|
|
||||||
|
execApp(code, tokens)
|
||||||
|
} catch (e) {
|
||||||
|
serial.printerr("fsh.launchEntry: " + label + " failed: " + e)
|
||||||
|
}
|
||||||
|
con.curs_set(0)
|
||||||
|
graphics.setBackground(2,1,3)
|
||||||
|
graphics.resetPalette()
|
||||||
|
_fsh.redrawAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout map: widget positions hard-coded to match the draw calls below.
|
||||||
|
_fsh.layouts = {
|
||||||
|
"com.fsh.todo_list": {xoff: 10, yoff: 17, widget: null},
|
||||||
|
"com.fsh.quick_access": {xoff: 47, yoff: 8, widget: null}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find which widget (if any) was hit by (charX, charY). Returns
|
||||||
|
// {widgetId, hit} or null.
|
||||||
|
_fsh.findHit = function(charX, charY) {
|
||||||
|
let ids = ["com.fsh.todo_list", "com.fsh.quick_access"]
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
let id = ids[i]
|
||||||
|
let layout = _fsh.layouts[id]
|
||||||
|
let widget = _fsh.widgets[id]
|
||||||
|
let hit = widget.hitTest(charX, charY, layout.xoff, layout.yoff)
|
||||||
|
if (hit) return {widgetId: id, hit: hit}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
_fsh.dispatchLeft = function(widgetId, hit) {
|
||||||
|
if (hit.kind === "add") {
|
||||||
|
if (widgetId === "com.fsh.todo_list") _fsh.openAddTodoDialog()
|
||||||
|
else _fsh.openAddQaDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// hit.kind === "item"
|
||||||
|
if (widgetId === "com.fsh.todo_list") {
|
||||||
|
_fsh.toggleTodoDone(hit.index)
|
||||||
|
} else {
|
||||||
|
let entry = quickAccessWidget.entries[hit.index]
|
||||||
|
if (entry) _fsh.launchEntry(entry[0], entry[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_fsh.dispatchRight = function(widgetId, hit) {
|
||||||
|
if (hit.kind !== "item") return
|
||||||
|
if (widgetId === "com.fsh.todo_list") _fsh.openEditTodoDialog(hit.index)
|
||||||
|
else _fsh.openEditQaDialog(hit.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// change graphics mode and check if it's supported
|
// change graphics mode and check if it's supported
|
||||||
graphics.setGraphicsMode(3)
|
graphics.setGraphicsMode(3)
|
||||||
@@ -260,29 +861,126 @@ _fsh.drawWallpaper()
|
|||||||
_fsh.drawTitlebar()
|
_fsh.drawTitlebar()
|
||||||
|
|
||||||
|
|
||||||
// TEST
|
// Load persisted state before the first draw
|
||||||
con.move(2,1);
|
_fsh.loadConfig();
|
||||||
print("fSh is very much in-dev! Hit backspace to exit")
|
|
||||||
|
// keyEventBuffers (read via sys.peek(-41-i)) holds *raw libGDX keycodes*,
|
||||||
|
// not the cooked TSVM scancodes that con.getch() returns. Existing fsh.js
|
||||||
|
// already uses 67 for Backspace (libGDX DEL); follow the same scheme here.
|
||||||
|
const KEY_ESC = keysym.ESCAPE
|
||||||
|
const KEY_ENTER = keysym.ENTER
|
||||||
|
const KEY_UP = keysym.UP
|
||||||
|
const KEY_DOWN = keysym.DOWN
|
||||||
|
const KEY_LEFT = keysym.LEFT
|
||||||
|
const KEY_RIGHT = keysym.RIGHT
|
||||||
|
const KEY_LSHIFT = keysym.SHIFT_LEFT
|
||||||
|
const KEY_RSHIFT = keysym.SHIFT_RIGHT
|
||||||
|
|
||||||
|
let prevButtons = 0
|
||||||
|
let prevMouseCharX = -1
|
||||||
|
let prevMouseCharY = -1
|
||||||
|
let keyLatch = {} // {keycode: true} while the key is held — debounces "just pressed"
|
||||||
|
|
||||||
// TODO update for events: key down (updates some widgets), timer (updates clock and calendar widgets)
|
|
||||||
while (true) {
|
while (true) {
|
||||||
captureUserInput();
|
captureUserInput()
|
||||||
if (getKeyPushed(0) == 67) break;
|
|
||||||
|
|
||||||
_fsh.widgets["com.fsh.clock"].draw(25, 3);
|
// -- keyboard --
|
||||||
_fsh.widgets["com.fsh.calendar"].draw(12, 8);
|
if (isKeyDown(KEY_ESC)) break;
|
||||||
_fsh.widgets["com.fsh.todo_list"].draw(10, 17);
|
|
||||||
_fsh.widgets["com.fsh.quick_access"].draw(47, 8);
|
let shiftDown = isKeyDown(KEY_LSHIFT) || isKeyDown(KEY_RSHIFT)
|
||||||
|
let enterPressed = false
|
||||||
|
|
||||||
|
// Edge-detect each navigation key
|
||||||
|
function edge(kc) {
|
||||||
|
let down = isKeyDown(kc)
|
||||||
|
let was = !!keyLatch[kc]
|
||||||
|
keyLatch[kc] = down
|
||||||
|
return down && !was
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edge(KEY_ENTER)) enterPressed = true;
|
||||||
|
let navUp = edge(KEY_UP)
|
||||||
|
let navDown = edge(KEY_DOWN)
|
||||||
|
let navLeft = edge(KEY_LEFT)
|
||||||
|
let navRight = edge(KEY_RIGHT)
|
||||||
|
|
||||||
|
// -- mouse --
|
||||||
|
let pos = readMousePos()
|
||||||
|
let charX = (pos[0] / 7) | 0
|
||||||
|
let charY = (pos[1] / 14) | 0
|
||||||
|
let mouseMoved = (charX !== prevMouseCharX || charY !== prevMouseCharY)
|
||||||
|
prevMouseCharX = charX
|
||||||
|
prevMouseCharY = charY
|
||||||
|
|
||||||
|
let buttons = readMouseButtons()
|
||||||
|
let leftEdge = ((buttons & _fsh.MB_LEFT) !== 0) && ((prevButtons & _fsh.MB_LEFT) === 0)
|
||||||
|
let rightEdge = ((buttons & _fsh.MB_RIGHT) !== 0) && ((prevButtons & _fsh.MB_RIGHT) === 0)
|
||||||
|
prevButtons = buttons
|
||||||
|
|
||||||
|
// -- focus update --
|
||||||
|
if (navUp || navDown || navLeft || navRight) {
|
||||||
|
if (!_fsh.focus) _fsh.focus = {widgetId: "com.fsh.todo_list", index: 0}
|
||||||
|
if (navUp || navDown) {
|
||||||
|
let layout = _fsh.layouts[_fsh.focus.widgetId]
|
||||||
|
let maxRows = (_fsh.focus.widgetId === "com.fsh.todo_list")
|
||||||
|
? _fsh.TODO_MAX_ROWS : _fsh.QA_MAX_ROWS
|
||||||
|
let length = (_fsh.focus.widgetId === "com.fsh.todo_list")
|
||||||
|
? todoWidget.todoList.length : quickAccessWidget.entries.length
|
||||||
|
let maxIdx = Math.min(length, maxRows - 1)
|
||||||
|
let next = _fsh.focus.index + (navDown ? 1 : -1)
|
||||||
|
if (next < 0) next = 0
|
||||||
|
if (next > maxIdx) next = maxIdx
|
||||||
|
_fsh.focus.index = next
|
||||||
|
} else {
|
||||||
|
// Left/right switches widget
|
||||||
|
let other = (_fsh.focus.widgetId === "com.fsh.todo_list")
|
||||||
|
? "com.fsh.quick_access" : "com.fsh.todo_list"
|
||||||
|
let otherLength = (other === "com.fsh.todo_list")
|
||||||
|
? todoWidget.todoList.length : quickAccessWidget.entries.length
|
||||||
|
let otherMaxRows = (other === "com.fsh.todo_list")
|
||||||
|
? _fsh.TODO_MAX_ROWS : _fsh.QA_MAX_ROWS
|
||||||
|
let otherMaxIdx = Math.min(otherLength, otherMaxRows - 1)
|
||||||
|
_fsh.focus = {widgetId: other, index: Math.min(_fsh.focus.index, otherMaxIdx)}
|
||||||
|
}
|
||||||
|
} else if (mouseMoved) {
|
||||||
|
let h = _fsh.findHit(charX, charY)
|
||||||
|
_fsh.focus = h ? {widgetId: h.widgetId, index: h.hit.kind === "add"
|
||||||
|
? ((h.widgetId === "com.fsh.todo_list")
|
||||||
|
? todoWidget.todoList.length
|
||||||
|
: quickAccessWidget.entries.length)
|
||||||
|
: h.hit.index} : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- mouse click dispatch --
|
||||||
|
if (leftEdge) {
|
||||||
|
let h = _fsh.findHit(charX, charY)
|
||||||
|
if (h) _fsh.dispatchLeft(h.widgetId, h.hit)
|
||||||
|
} else if (rightEdge) {
|
||||||
|
let h = _fsh.findHit(charX, charY)
|
||||||
|
if (h) _fsh.dispatchRight(h.widgetId, h.hit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- keyboard dispatch (synthesise click at focus) --
|
||||||
|
if (enterPressed && _fsh.focus) {
|
||||||
|
let length = (_fsh.focus.widgetId === "com.fsh.todo_list")
|
||||||
|
? todoWidget.todoList.length : quickAccessWidget.entries.length
|
||||||
|
let hit = (_fsh.focus.index < length)
|
||||||
|
? {kind: "item", index: _fsh.focus.index}
|
||||||
|
: (_fsh.focus.index === length ? {kind: "add"} : null)
|
||||||
|
if (hit) {
|
||||||
|
if (shiftDown) _fsh.dispatchRight(_fsh.focus.widgetId, hit)
|
||||||
|
else _fsh.dispatchLeft(_fsh.focus.widgetId, hit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- redraw --
|
||||||
|
_fsh.widgets["com.fsh.clock"].draw(25, 3)
|
||||||
|
_fsh.widgets["com.fsh.calendar"].draw(12, 8)
|
||||||
|
_fsh.widgets["com.fsh.todo_list"].draw(10, 17)
|
||||||
|
_fsh.widgets["com.fsh.quick_access"].draw(47, 8)
|
||||||
|
|
||||||
sys.spin(); sys.spin()
|
sys.spin(); sys.spin()
|
||||||
}
|
}
|
||||||
|
|
||||||
con.move(3,1);
|
con.reset_graphics()
|
||||||
con.color_pair(201,255);
|
con.clear()
|
||||||
print("cya!");
|
|
||||||
|
|
||||||
let konsht = 3412341241;
|
|
||||||
println(konsht);
|
|
||||||
|
|
||||||
let pppp = graphics.getCursorYX();
|
|
||||||
println(pppp.toString());
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,329 +0,0 @@
|
|||||||
# fSh Interactive Widgets — Design
|
|
||||||
|
|
||||||
**Date**: 2026-05-24
|
|
||||||
**Scope**: Make `com.fsh.todo_list` and `com.fsh.quick_access` widgets in
|
|
||||||
`assets/disk0/home/fsh.js` functional. Persist state to
|
|
||||||
`assets/disk0/home/config/fshrc`. Add a single `IOSpace.kt` change to expose
|
|
||||||
the right mouse button.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
1. Click "Click to add" → modal popup that adds a new entry.
|
|
||||||
2. Click an existing todo → toggle done. Click an existing QA entry → launch
|
|
||||||
its program via `execApp`, then return to fsh.
|
|
||||||
3. Right-click any existing entry → modal popup for edit / delete.
|
|
||||||
4. Hover (or keyboard focus) highlights the row under the pointer.
|
|
||||||
5. Keyboard navigation: arrows move focus; Enter = left-click; Shift+Enter =
|
|
||||||
right-click; Esc exits fsh.
|
|
||||||
6. State persists across runs via `A:/home/config/fshrc`.
|
|
||||||
|
|
||||||
## Non-goals
|
|
||||||
|
|
||||||
- Drag-and-drop reordering of items.
|
|
||||||
- Multi-line todos.
|
|
||||||
- Validation or autocomplete on the QA "Command" field — whatever the user
|
|
||||||
types is stored verbatim and passed to `execApp`.
|
|
||||||
- Any UI for resolving errors in a malformed `fshrc`: invalid lines are
|
|
||||||
silently dropped on load. fsh is the only writer.
|
|
||||||
- Right-click support exposed via any new dedicated MMIO range; we just
|
|
||||||
promote the existing single-bit `mouseDown` byte to a two-bit field.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Source files touched
|
|
||||||
|
|
||||||
| File | Change |
|
|
||||||
|-----------------------------------------------------------------|------------------------------------------------------------------------|
|
|
||||||
| `assets/disk0/home/fsh.js` | Widget interaction, dialog primitive, config I/O, new main loop. |
|
|
||||||
| `tsvm_core/src/net/torvald/tsvm/peripheral/IOSpace.kt` | MMIO[36] becomes a button bitfield (bit 0 = left, bit 1 = right). |
|
|
||||||
|
|
||||||
No new files. `assets/disk0/home/config/fshrc` is created lazily on first
|
|
||||||
save.
|
|
||||||
|
|
||||||
### High-level units in fsh.js
|
|
||||||
|
|
||||||
1. **Input polling layer** — reads mouse position (MMIO 32–35), mouse buttons
|
|
||||||
(MMIO 36), and keyboard events (existing `captureUserInput()` /
|
|
||||||
`getKeyPushed()`). Provides edge-detected click events and a per-frame
|
|
||||||
"cursor moved?" signal.
|
|
||||||
2. **Focus state** — single `_fsh.focus = {widgetId, index}` driven by
|
|
||||||
whichever input device moved last. Cleared when neither mouse nor keyboard
|
|
||||||
selects anything actionable.
|
|
||||||
3. **Widget hit-test + draw** — each interactive widget gains a
|
|
||||||
`hitTest(charX, charY)` returning `{kind: "add"|"item", index}` or `null`,
|
|
||||||
and its `draw()` accepts the focus state to invert the highlighted row.
|
|
||||||
4. **Modal dialog primitive** — `_fsh.showDialog(opts)` blocks input until
|
|
||||||
the user submits or cancels. Returns a tagged result.
|
|
||||||
5. **Config I/O** — `_fsh.loadConfig()` runs once at startup;
|
|
||||||
`_fsh.saveConfig()` runs after every mutation.
|
|
||||||
6. **Dispatcher** — translates click / keyboard events into widget mutations,
|
|
||||||
`execApp` invocations, or dialog opens.
|
|
||||||
|
|
||||||
## Detailed behaviour
|
|
||||||
|
|
||||||
### Input polling
|
|
||||||
|
|
||||||
```
|
|
||||||
Mouse X = (sys.peek(-33) & 0xFF) | ((sys.peek(-34) & 0xFF) << 8)
|
|
||||||
Mouse Y = (sys.peek(-35) & 0xFF) | ((sys.peek(-36) & 0xFF) << 8)
|
|
||||||
Buttons = sys.peek(-37) & 0xFF // bit 0 = left, bit 1 = right
|
|
||||||
```
|
|
||||||
|
|
||||||
Mouse pixel → char-grid conversion: `charX = mouseX / 7`, `charY = mouseY / 14`
|
|
||||||
(matching the existing widget coordinate system).
|
|
||||||
|
|
||||||
Each frame the loop computes `(prevButtons, currButtons)` and emits at most
|
|
||||||
one event:
|
|
||||||
|
|
||||||
- left-pressed edge (`!(prev & 1) && (curr & 1)`) → `leftClick(charX, charY)`
|
|
||||||
- right-pressed edge (`!(prev & 2) && (curr & 2)`) → `rightClick(charX, charY)`
|
|
||||||
|
|
||||||
Keyboard events use the existing `captureUserInput() / getKeyPushed(k)` mechanism
|
|
||||||
the file already uses. We don't need `con.getch()` in the main loop because the
|
|
||||||
dialog handles its own text input.
|
|
||||||
|
|
||||||
### Focus state
|
|
||||||
|
|
||||||
- `_fsh.focus = null | {widgetId: string, index: number}`.
|
|
||||||
- After each frame's input poll, focus is reassigned by the most recent input:
|
|
||||||
- If mouse moved since last frame: focus = hit-test under cursor (or `null`).
|
|
||||||
- If a nav key was pressed: focus = computed from previous focus + key.
|
|
||||||
- Drawing always honours `_fsh.focus`; widgets that don't match `widgetId` draw
|
|
||||||
normally.
|
|
||||||
|
|
||||||
Keyboard nav rules:
|
|
||||||
|
|
||||||
- Indexing convention (matches the existing draw): for a list of length `N`,
|
|
||||||
indices `0..N-1` are existing entries and index `N` is the `+ Click to
|
|
||||||
add` row. Indices past `N` are not focusable.
|
|
||||||
- `↑` / `↓`: move `index` ± 1, clamped to `[0, min(N, maxRows-1)]` for the
|
|
||||||
current widget. No wrap.
|
|
||||||
- `←` / `→`: switch `widgetId` between `com.fsh.todo_list` and
|
|
||||||
`com.fsh.quick_access`, with `index` clamped to the target widget's range.
|
|
||||||
- If focus is `null` on key press, default to `{widgetId: "com.fsh.todo_list",
|
|
||||||
index: 0}`.
|
|
||||||
|
|
||||||
### Hit-testing
|
|
||||||
|
|
||||||
Each interactive widget exports:
|
|
||||||
|
|
||||||
```
|
|
||||||
widget.hitTest(charX, charY) → null
|
|
||||||
| {kind: "add"}
|
|
||||||
| {kind: "item", index: i} // i is the 0-based model index
|
|
||||||
```
|
|
||||||
|
|
||||||
The hit region is the widget's rendered row range. For the Todo widget that's
|
|
||||||
charY in `[charYoff + 2, charYoff + 14]` for rows 0..12; charX in
|
|
||||||
`[charXoff, charXoff + 26)`. Same shape for QA, with its own `charYoff`,
|
|
||||||
`charXoff`, and 22 rows. The widget owns these magic numbers because they
|
|
||||||
already live in its `draw()`.
|
|
||||||
|
|
||||||
The clicked row index maps to:
|
|
||||||
|
|
||||||
- `0..N-1` (existing entries) → `{kind: "item", index}`.
|
|
||||||
- `N` (the row that draws "Click to add") → `{kind: "add"}`.
|
|
||||||
- `> N` (the underscore filler rows) → `null`.
|
|
||||||
|
|
||||||
### Dispatcher
|
|
||||||
|
|
||||||
```
|
|
||||||
on leftClick(cx, cy):
|
|
||||||
hit = widget.hitTest(cx, cy)
|
|
||||||
if hit is null: return
|
|
||||||
if hit.kind == "add":
|
|
||||||
openAddDialog(widget)
|
|
||||||
elif widget == todo:
|
|
||||||
toggleDone(hit.index); saveConfig()
|
|
||||||
elif widget == qa:
|
|
||||||
launchEntry(qa.entries[hit.index])
|
|
||||||
|
|
||||||
on rightClick(cx, cy):
|
|
||||||
hit = widget.hitTest(cx, cy)
|
|
||||||
if hit is null or hit.kind == "add": return
|
|
||||||
openEditDialog(widget, hit.index)
|
|
||||||
|
|
||||||
on Enter: leftClick at focus
|
|
||||||
on Shift+Enter: rightClick at focus
|
|
||||||
```
|
|
||||||
|
|
||||||
`launchEntry({label, cmd})`:
|
|
||||||
|
|
||||||
1. Read the file at `cmd` (using the existing path-resolution pattern from
|
|
||||||
`command.js`).
|
|
||||||
2. `execApp(programCode, [cmd])`.
|
|
||||||
3. On return, redraw wallpaper + titlebar + all widgets.
|
|
||||||
4. Errors from `execApp` are caught and logged via `serial.printerr`; fsh
|
|
||||||
continues running. No bulletin shown (out of scope).
|
|
||||||
|
|
||||||
### Modal dialog
|
|
||||||
|
|
||||||
```
|
|
||||||
_fsh.showDialog({
|
|
||||||
title: "New Todo",
|
|
||||||
fields: [{label: "Text", initial: "", width: 24}],
|
|
||||||
allowDelete: false, // adds [Delete] button when true
|
|
||||||
}) → {action: "ok"|"delete"|"cancel", values: [string, ...]}
|
|
||||||
```
|
|
||||||
|
|
||||||
Render:
|
|
||||||
|
|
||||||
- Centred on a `_fsh.scrwidth × _fsh.scrheight` grid. Width = max(title length
|
|
||||||
+ 4, longest field width + 6, 16). Height = 4 + 3 × fields.length + 1.
|
|
||||||
- Frame: `╔═╗ ║ ╚═╝` (double-line). Inner field box: `┌─┐ │ └─┘`.
|
|
||||||
- Saves a snapshot of the underlying char cells via `con.peekch`-style reads
|
|
||||||
(if available) or simply redraws wallpaper + widgets after close. The
|
|
||||||
simpler "redraw everything" approach is acceptable given the small screen
|
|
||||||
budget.
|
|
||||||
|
|
||||||
Input loop inside the dialog (separate from the main loop):
|
|
||||||
|
|
||||||
- Uses `con.getch()` for character entry, matching `command.js` line 505.
|
|
||||||
- Printable ASCII (32..126) and the TSVM extended chars append to the active
|
|
||||||
field.
|
|
||||||
- Backspace deletes one char.
|
|
||||||
- Tab cycles fields (forward).
|
|
||||||
- Enter: if active field is not last, advance to next field; if last, submit.
|
|
||||||
- Esc: cancel.
|
|
||||||
- Mouse: re-uses the main-loop hit-tester logic to detect clicks on `[OK]`,
|
|
||||||
`[Cancel]`, or `[Delete]` buttons, and to focus a field when clicked.
|
|
||||||
|
|
||||||
The dialog drives its own input loop. The main loop is **not** running while a
|
|
||||||
dialog is open. This avoids race conditions on shared input state.
|
|
||||||
|
|
||||||
### Config (fshrc)
|
|
||||||
|
|
||||||
Path: `A:/home/config/fshrc`.
|
|
||||||
|
|
||||||
Format (re-stated for the spec):
|
|
||||||
|
|
||||||
```
|
|
||||||
[TODO]
|
|
||||||
+ Buy groceries
|
|
||||||
- Read CLAUDE.md
|
|
||||||
+ Take out trash
|
|
||||||
|
|
||||||
[QUICK_ACCESS]
|
|
||||||
Files,/tvdos/bin/zsh.js
|
|
||||||
Editor,/tvdos/bin/edit.js
|
|
||||||
BASIC,/tbas/basic.js
|
|
||||||
```
|
|
||||||
|
|
||||||
Parse rules:
|
|
||||||
|
|
||||||
- Lines starting with `[` open a new section. Recognised names: `TODO`,
|
|
||||||
`QUICK_ACCESS`. Unknown sections cause subsequent lines to be ignored until
|
|
||||||
the next header.
|
|
||||||
- Inside `[TODO]`: line must match `^[+-] (.*)$`. `+` → done; `-` → not done.
|
|
||||||
Whitespace-only lines skipped.
|
|
||||||
- Inside `[QUICK_ACCESS]`: split on the **first** comma. Label = left side
|
|
||||||
(trimmed); cmd = right side (verbatim, no trim — leading space may be
|
|
||||||
intentional). Lines without a comma are skipped.
|
|
||||||
- Blank lines anywhere are ignored.
|
|
||||||
- Trailing newline tolerated.
|
|
||||||
|
|
||||||
Load behaviour:
|
|
||||||
|
|
||||||
- If file does not exist: todoList stays `[]`, QA falls back to the hardcoded
|
|
||||||
default entries (`Files / Editor / BASIC / DOS Shell`). On first save, the
|
|
||||||
file is created and defaults are written out.
|
|
||||||
- If file exists but is empty or only contains unknown sections: same as
|
|
||||||
above (defaults for QA, empty todo).
|
|
||||||
|
|
||||||
Save behaviour:
|
|
||||||
|
|
||||||
- Whole file rewrite via `file.swrite(serialized)` on every mutation.
|
|
||||||
- Order in file matches in-memory order; in-memory order matches click order
|
|
||||||
(newest at the bottom for new adds).
|
|
||||||
|
|
||||||
### Engine change (`IOSpace.kt`)
|
|
||||||
|
|
||||||
Convert MMIO[36] from a single boolean to a two-bit field. Touch points
|
|
||||||
(all within `IOSpace.kt`):
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// ~line 283: rename and retype
|
|
||||||
private var mouseButtons: Int = 0 // bit 0 = LEFT, bit 1 = RIGHT
|
|
||||||
|
|
||||||
// ~line 101: change the read
|
|
||||||
36L -> mouseButtons.toByte()
|
|
||||||
|
|
||||||
// ~line 302: set both bits in the touched branch
|
|
||||||
mouseButtons = (if (Gdx.input.isButtonPressed(Buttons.LEFT)) 1 else 0) or
|
|
||||||
(if (Gdx.input.isButtonPressed(Buttons.RIGHT)) 2 else 0)
|
|
||||||
|
|
||||||
// ~line 316: clear when no touch
|
|
||||||
mouseButtons = 0
|
|
||||||
```
|
|
||||||
|
|
||||||
Backwards compatibility: existing JS does `sys.peek(-37)` and treats non-zero
|
|
||||||
as "pressed." Since LEFT (the only previously available button) is bit 0,
|
|
||||||
non-zero is preserved for left-click. No JS callers currently inspect the
|
|
||||||
high bits, so no callers break.
|
|
||||||
|
|
||||||
## Data flow
|
|
||||||
|
|
||||||
```
|
|
||||||
startup
|
|
||||||
└─ loadConfig() → populates todoWidget.todoList and quickAccessWidget.entries
|
|
||||||
└─ registerNewWidget(...)
|
|
||||||
└─ enter main loop
|
|
||||||
|
|
||||||
main loop, per frame
|
|
||||||
├─ poll mouse pos + buttons + keyboard
|
|
||||||
├─ update _fsh.focus
|
|
||||||
├─ if leftClick edge: dispatchLeftClick()
|
|
||||||
├─ if rightClick edge: dispatchRightClick()
|
|
||||||
├─ if Enter / Shift+Enter: synthesize click at focus
|
|
||||||
├─ if Esc: break
|
|
||||||
└─ redraw widgets (each receives _fsh.focus)
|
|
||||||
|
|
||||||
dispatch
|
|
||||||
├─ openAddDialog → showDialog → mutate model → saveConfig() → redraw all
|
|
||||||
├─ openEditDialog → showDialog → mutate model → saveConfig() → redraw all
|
|
||||||
├─ toggleDone → mutate model → saveConfig() → no full redraw needed
|
|
||||||
└─ launchEntry → execApp → redraw all on return
|
|
||||||
|
|
||||||
shutdown (Esc)
|
|
||||||
└─ con.reset_graphics(); con.clear()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error handling
|
|
||||||
|
|
||||||
- `loadConfig`: any parse failure on a single line → drop the line, keep
|
|
||||||
parsing. No user-visible error.
|
|
||||||
- `saveConfig`: file open failure → log via `serial.printerr`, continue.
|
|
||||||
In-memory state is still correct for the session.
|
|
||||||
- `execApp` throws → caught, logged via `serial.printerr`, fsh continues.
|
|
||||||
- Dialog cancel → model untouched, no save, redraw.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Manual verification path (the project doesn't have a JS test harness for
|
|
||||||
fsh):
|
|
||||||
|
|
||||||
1. **First run**: delete `fshrc`, launch fsh — expect default QA entries and
|
|
||||||
empty todo list with a single `+ Click to add` row.
|
|
||||||
2. **Add todo**: left-click `+ Click to add` on todo widget → dialog appears →
|
|
||||||
type "Buy groceries" → Enter. Row added. Restart fsh — row persists.
|
|
||||||
3. **Toggle done**: left-click an existing todo → checkbox flips. Restart →
|
|
||||||
state preserved.
|
|
||||||
4. **Edit todo**: right-click an existing todo → dialog opens pre-filled. OK
|
|
||||||
saves edit; Delete removes; Cancel discards.
|
|
||||||
5. **Add QA**: left-click `+ Click to add` on QA widget → dialog with two
|
|
||||||
fields (Label, Command). Submit.
|
|
||||||
6. **Launch QA**: left-click `Editor` → `edit.js` runs. Quit edit → fsh
|
|
||||||
redraws.
|
|
||||||
7. **Edit/Delete QA**: right-click an entry → edit dialog (with Delete button)
|
|
||||||
appears.
|
|
||||||
8. **Keyboard nav**: cursor not over any item — press ↓ — first todo
|
|
||||||
highlights. Use arrows to traverse, ← / → to switch widgets, Enter to
|
|
||||||
activate.
|
|
||||||
9. **Hover highlight**: move mouse over items — row inverts under cursor.
|
|
||||||
10. **Esc**: exits fsh cleanly.
|
|
||||||
11. **Malformed fshrc**: hand-edit the file to contain garbage — fsh should
|
|
||||||
start with defaults and not crash.
|
|
||||||
|
|
||||||
## Open questions
|
|
||||||
|
|
||||||
None — all design decisions are settled. Implementation can begin.
|
|
||||||
@@ -49,7 +49,7 @@ MMIO
|
|||||||
0..31 RO: Raw Keyboard Buffer read. Won't shift the key buffer
|
0..31 RO: Raw Keyboard Buffer read. Won't shift the key buffer
|
||||||
32..33 RO: Mouse X pos
|
32..33 RO: Mouse X pos
|
||||||
34..35 RO: Mouse Y pos
|
34..35 RO: Mouse Y pos
|
||||||
36 RO: Mouse down? (1 for TRUE, 0 for FALSE)
|
36 RO: Mouse down? (1 for LEFT, 2 for RIGHT, 3 for BOTH)
|
||||||
37 RW: Read/Write single key input. Key buffer will be shifted. Manual writing is
|
37 RW: Read/Write single key input. Key buffer will be shifted. Manual writing is
|
||||||
usually unnecessary as such action must be automatically managed via LibGDX
|
usually unnecessary as such action must be automatically managed via LibGDX
|
||||||
input processing.
|
input processing.
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
in 0..31 -> keyboardBuffer[(addr.toInt())] ?: -1
|
in 0..31 -> keyboardBuffer[(addr.toInt())] ?: -1
|
||||||
in 32..33 -> (mouseX.toInt() shr (adi - 32).times(8)).toByte()
|
in 32..33 -> (mouseX.toInt() shr (adi - 32).times(8)).toByte()
|
||||||
in 34..35 -> (mouseY.toInt() shr (adi - 34).times(8)).toByte()
|
in 34..35 -> (mouseY.toInt() shr (adi - 34).times(8)).toByte()
|
||||||
36L -> mouseDown.toInt().toByte()
|
36L -> mouseButtons.toByte() // only bits 0..1 used; higher bits intentionally truncated
|
||||||
37L -> {
|
37L -> {
|
||||||
val key = keyboardBuffer.removeTail() ?: -1
|
val key = keyboardBuffer.removeTail() ?: -1
|
||||||
keyPushed = !keyboardBuffer.isEmpty // Clear flag when buffer becomes empty
|
keyPushed = !keyboardBuffer.isEmpty // Clear flag when buffer becomes empty
|
||||||
@@ -280,7 +280,7 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
|
|
||||||
private var mouseX: Short = 0
|
private var mouseX: Short = 0
|
||||||
private var mouseY: Short = 0
|
private var mouseY: Short = 0
|
||||||
private var mouseDown = false
|
private var mouseButtons: Int = 0 // bit 0 = LEFT, bit 1 = RIGHT
|
||||||
private var systemUptime = 0L
|
private var systemUptime = 0L
|
||||||
private var rtc = 0L
|
private var rtc = 0L
|
||||||
|
|
||||||
@@ -299,7 +299,8 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
// store mouse info
|
// store mouse info
|
||||||
mouseX = (Gdx.input.x + guiPosX).toShort()
|
mouseX = (Gdx.input.x + guiPosX).toShort()
|
||||||
mouseY = (Gdx.input.y + guiPosY).toShort()
|
mouseY = (Gdx.input.y + guiPosY).toShort()
|
||||||
mouseDown = Gdx.input.isTouched
|
mouseButtons = (if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) 1 else 0) or
|
||||||
|
(if (Gdx.input.isButtonPressed(Input.Buttons.RIGHT)) 2 else 0)
|
||||||
|
|
||||||
// strobe keys to fill the key read buffer
|
// strobe keys to fill the key read buffer
|
||||||
var keysPushed = 0
|
var keysPushed = 0
|
||||||
@@ -313,7 +314,7 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
mouseDown = false
|
mouseButtons = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user