mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
760 lines
27 KiB
JavaScript
760 lines
27 KiB
JavaScript
graphics.setBackground(2,1,3)
|
|
graphics.resetPalette()
|
|
const GL = require("gl")
|
|
const win = require("wintex")
|
|
const keysym = require("keysym")
|
|
|
|
function captureUserInput() {
|
|
sys.poke(-40, 1)
|
|
}
|
|
|
|
function getKeyPushed(keyOrder) {
|
|
return sys.peek(-41 - keyOrder)
|
|
}
|
|
|
|
function readMousePos() {
|
|
let lx = sys.peek(-33) & 0xFF
|
|
let hx = sys.peek(-34) & 0xFF
|
|
let ly = sys.peek(-35) & 0xFF
|
|
let hy = sys.peek(-36) & 0xFF
|
|
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
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
|
|
// 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(
|
|
"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.drawWallpaper = function() {
|
|
let wp = files.open("A:/home/wall.bytes")
|
|
// filesystem.open("A", "/tvdos/wall.bytes", "R")
|
|
let b = sys.malloc(250880)
|
|
// dma.comToRam(0, 0, b, 250880)
|
|
wp.pread(b, 250880, 0)
|
|
dma.ramToFrame(b, 0, 250880)
|
|
sys.free(b)
|
|
}
|
|
|
|
_fsh.drawTitlebar = function(titletext) {
|
|
GL.drawTexPattern(_fsh.titlebarTex, 0, 0, 560, 14)
|
|
if (titletext === undefined || titletext.length == 0) {
|
|
con.move(1,1)
|
|
print(" ".repeat(_fsh.scrwidth))
|
|
GL.drawTexImageOver(_fsh.brandLogoTexSmall, 268, 0)
|
|
}
|
|
else {
|
|
con.color_pair(240, 255)
|
|
GL.drawTexPattern(_fsh.titlebarTex, 268, 0, 24, 14)
|
|
con.move(1, 1 + (_fsh.scrwidth - titletext.length) / 2)
|
|
print(titletext)
|
|
}
|
|
con.color_pair(254, 255)
|
|
}
|
|
|
|
|
|
_fsh.Widget = function(id, w, h) {
|
|
this.identifier = id
|
|
this.width = w
|
|
this.height = h
|
|
|
|
if (!this.identifier) {
|
|
this.identifier = ""
|
|
}
|
|
|
|
//this.update = function() {}
|
|
/**
|
|
* Params charXoff and charYoff are ZERO-BASED!
|
|
*/
|
|
this.draw = function(charXoff, charYoff) {}
|
|
}
|
|
|
|
_fsh.widgets = {}
|
|
_fsh.registerNewWidget = function(widget) {
|
|
_fsh.widgets[widget.identifier] = widget
|
|
}
|
|
|
|
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(
|
|
"H4sIAAAAAAAAAMWVW3LEMAgE739aHcFJJV5ZMD2I9ToVfcl4GBr80HF8r/FaR1ozMuIyoUu87lEXI0al5qVR5AebSwchSaNE6Nyo1Nw5HXF3SfPT4Bshl"+
|
|
"EycA8RD96mLlHbuhTgOrfLnUDZspafbSQWk56WEGvQEtWaWwgb8iz7a8AOXhsraO/q9Qw2/GnXovfVN+q2wM/p/oddn2cjF239GX3y11+SWCtc6FTHC1v"+
|
|
"TVPkDPWWn0w+DDz93UX9v9mF5KIsQ6OdN2KJoB4ui1bXXr0AMp0YfiQo//4XhpK8555dsNehAqVS5uhb5iHn3Kko769J59KmLBe/TSR7hcsd+hr+HnrwR"+
|
|
"9uvRF9+D3MP14gN7lqx+8OuNT+uqt3NFX3SN9fTbeeHNq+C29pRWzX5+Rcm7SZyjOKJ/2hkSPqul4xN279DrSYvCrNu2NI7ZMp1ouBxK3KBVVnEeAUWbK"+
|
|
"MUDn5DPsPxmUqHZQjGpy2hergM3EVBAAAA=="
|
|
))))
|
|
|
|
clockWidget.clockColon = new GL.Texture(4, 3, base64.atob("7+/v7+/v7+/v7+/v"))
|
|
clockWidget.monthNames = ["Spring", "Summer", "Autumn", "Winter"]
|
|
clockWidget.dayNames = ["Mondag ", "Tysdag ", "Midtveke", "Torsdag ", "Fredag ", "Laurdag ", "Sundag ", "Verddag "]
|
|
clockWidget.draw = function(charXoff, charYoff) {
|
|
con.color_pair(254, 255)
|
|
let xoff = charXoff * 7
|
|
let yoff = charYoff * 14 + 3
|
|
let timeInMinutes = ((sys.currentTimeInMills() / 60000)|0)
|
|
let mins = timeInMinutes % 60
|
|
let hours = ((timeInMinutes / 60)|0) % 24
|
|
let ordinalDay = ((timeInMinutes / (60*24))|0) % 120
|
|
let visualDay = (ordinalDay % 30) + 1
|
|
let months = ((timeInMinutes / (60*24*30))|0) % 4
|
|
let dayName = ordinalDay % 7 // 0 for Mondag
|
|
if (ordinalDay == 119) dayName = 7 // Verddag
|
|
let years = ((timeInMinutes / (60*24*30*120))|0) + 125
|
|
// draw timepiece
|
|
GL.drawSprite(clockWidget.numberSheet, (hours / 10)|0, 0, xoff, 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 + 14, 1)
|
|
GL.drawSprite(clockWidget.numberSheet, (mins / 10)|0, 0, xoff + 57, yoff, 1)
|
|
GL.drawSprite(clockWidget.numberSheet, mins % 10, 0, xoff + 81, yoff, 1)
|
|
// print month and date
|
|
con.move(1 + charYoff, 17 + charXoff)
|
|
print(clockWidget.monthNames[months]+" "+visualDay)
|
|
// print year and dayname
|
|
con.move(2 + charYoff, 17 + charXoff)
|
|
print("\xE7"+years+" "+clockWidget.dayNames[dayName])
|
|
}
|
|
|
|
|
|
let calendarWidget = new _fsh.Widget("com.fsh.calendar", (_fsh.scrwidth - 8) / 2, 7*6)
|
|
calendarWidget.dayLabels = [
|
|
" 1 2 3 4 5 6 7 \xFA\xFA",
|
|
" 8 9 10 11 12 13 14 \xFA\xFA",
|
|
"15 16 17 18 19 20 21 \xFA\xFA",
|
|
"22 23 24 25 26 27 28 \xFA\xFA",
|
|
"29 30 1 2 3 4 5 \xFA\xFA",
|
|
" 6 7 8 9 10 11 12 \xFA\xFA",
|
|
"13 14 15 16 17 18 19 \xFA\xFA",
|
|
"20 21 22 23 24 25 26 \xFA\xFA",
|
|
"27 28 29 30 1 2 3 \xFA\xFA",
|
|
" 4 5 6 7 8 9 10 \xFA\xFA",
|
|
"11 12 13 14 15 16 17 \xFA\xFA",
|
|
"18 19 20 21 22 23 24 \xFA\xFA",
|
|
"25 26 27 28 29 30 1 \xFA\xFA",
|
|
" 2 3 4 5 6 7 8 \xFA\xFA",
|
|
" 9 10 11 12 13 14 15 \xFA\xFA",
|
|
"16 17 18 19 20 21 22 \xFA\xFA",
|
|
"23 24 25 26 27 28 29 30"
|
|
]
|
|
calendarWidget.seasonCols = [229,39,215,239,253]
|
|
calendarWidget.draw = function(charXoff, charYoff) {
|
|
con.color_pair(254, 255)
|
|
let xoff = charXoff * 7
|
|
let yoff = charYoff * 14 + 3
|
|
|
|
let timeInMinutes = ((sys.currentTimeInMills() / 60000)|0)
|
|
let ordinalDay = ((timeInMinutes / (60*24))|0) % 120
|
|
let offset = (119 == ordinalDay) ? 16 : (ordinalDay / 7)|0
|
|
|
|
|
|
con.move(charYoff, charXoff)
|
|
print("Mo Ty Mi To Fr La Su Ve")
|
|
|
|
for (let i = -3; i <= 3; i++) {
|
|
let lineOff = (offset + i + 17) % 17 // adding 17 to prevent mod-ing on negative number
|
|
let line = calendarWidget.dayLabels[lineOff]
|
|
let textCol = 0
|
|
|
|
con.move(charYoff + 4 + i, charXoff)
|
|
|
|
for (let x = 0; x <= 23; x++) {
|
|
let paintingDayOrd = lineOff*7 + ((x/3)|0)
|
|
if (x >= 21 && lineOff != 16) textCol = calendarWidget.seasonCols[4]
|
|
else textCol = calendarWidget.seasonCols[(paintingDayOrd / 30)|0]
|
|
|
|
// special colour for spaces between numbers
|
|
if (x % 3 == 2) con.color_pair(255,255)
|
|
// mark today
|
|
else if (paintingDayOrd == ordinalDay && x < 21 || paintingDayOrd == 119 && ordinalDay == 119) con.color_pair(0,textCol)
|
|
// paint normal day number with seasonal colour
|
|
else con.color_pair(textCol,255)
|
|
|
|
con.addch(line.charCodeAt(x))
|
|
con.curs_right()
|
|
}
|
|
}
|
|
}
|
|
|
|
let todoWidget = new _fsh.Widget("com.fsh.todo_list", (_fsh.scrwidth - 8) / 2, 7*10)
|
|
todoWidget.todoList = [["Hello, world!", true]]
|
|
todoWidget.draw = function(charXoff, charYoff) {
|
|
let focusIndex = (_fsh.focus && _fsh.focus.widgetId === todoWidget.identifier)
|
|
? _fsh.focus.index : -1
|
|
|
|
con.color_pair(254, 255)
|
|
let xoff = charXoff * 7
|
|
let yoff = charYoff * 14 + 3
|
|
|
|
con.move(charYoff, charXoff)
|
|
print('\u00CD'.repeat(10)+" TODO "+'\u00CD'.repeat(10))
|
|
|
|
for (let i = 0; i <= 12; i++) {
|
|
let list = todoWidget.todoList[i] || ["Click to add"+" ".repeat(_fsh.TODO_TEXT_WIDTH - 12), null]
|
|
let isFocused = (i === focusIndex)
|
|
|
|
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)
|
|
|
|
con.move(charYoff + i + 2, charXoff)
|
|
con.addch((list[1] === null) ? 43 : (list[1]) ? 0x9F : 0x9E)
|
|
|
|
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++) {
|
|
con.mvaddch(charYoff + i + 2, charXoff + 2 + k, 95)
|
|
}
|
|
}
|
|
else {
|
|
con.move(charYoff + i + 2, charXoff + 2)
|
|
// 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)
|
|
quickAccessWidget.entries = [ // TODO read from /home/config/fshrc
|
|
["Files", "/tvdos/bin/zfm.js"],
|
|
["Editor", "/tvdos/bin/edit.js"],
|
|
["BASIC", "/tbas/basic.js"],
|
|
["DOS Shell", "/tvdos/bin/command.js -fancy"]
|
|
]
|
|
quickAccessWidget.draw = function(charXoff, charYoff) {
|
|
let focusIndex = (_fsh.focus && _fsh.focus.widgetId === quickAccessWidget.identifier)
|
|
? _fsh.focus.index : -1
|
|
|
|
con.color_pair(254, 255)
|
|
let xoff = charXoff * 7
|
|
let yoff = charYoff * 14 + 3
|
|
|
|
con.move(charYoff, charXoff)
|
|
print('\u00CD'.repeat(6)+" QUICK ACCESS "+'\u00CD'.repeat(6))
|
|
|
|
for (let i = 0; i <= 21; i++) {
|
|
let list = quickAccessWidget.entries[i] || ["Click to add"+" ".repeat(_fsh.QA_LABEL_WIDTH - 12), null]
|
|
let isFocused = (i === focusIndex)
|
|
|
|
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)
|
|
|
|
con.move(charYoff + i + 2, charXoff)
|
|
con.addch((list[1] === null) ? 0xF9 : (list[1]) ? 7 : 0x7F)
|
|
|
|
if (i > quickAccessWidget.entries.length) {
|
|
con.color_pair(254, 255)
|
|
for (let k = 0; k < 24; k++) {
|
|
con.mvaddch(charYoff + i + 2, charXoff + 2 + k, 95)
|
|
}
|
|
}
|
|
else {
|
|
con.move(charYoff + i + 2, charXoff + 2)
|
|
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 = win.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 = win.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 = win.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 = win.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()
|
|
// Apps (e.g. zfm) may switch to graphics mode 0; restore mode 3 so the
|
|
// clock widget on framebuffer 2 is composited again.
|
|
graphics.setGraphicsMode(3)
|
|
_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
|
|
graphics.setGraphicsMode(3)
|
|
if (graphics.getGraphicsMode() == 0) {
|
|
printerrln("Insufficient VRAM")
|
|
return 1
|
|
}
|
|
|
|
// register widgets
|
|
_fsh.registerNewWidget(clockWidget)
|
|
_fsh.registerNewWidget(calendarWidget)
|
|
_fsh.registerNewWidget(todoWidget)
|
|
_fsh.registerNewWidget(quickAccessWidget)
|
|
|
|
// screen init
|
|
con.color_pair(254, 255)
|
|
con.clear()
|
|
con.curs_set(0)
|
|
graphics.clearPixels(255)
|
|
graphics.clearPixels2(255)
|
|
graphics.setFramebufferScroll(0,0)
|
|
_fsh.drawWallpaper()
|
|
_fsh.drawTitlebar()
|
|
|
|
|
|
// Load persisted state before the first draw
|
|
_fsh.loadConfig();
|
|
|
|
// 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"
|
|
|
|
while (true) {
|
|
captureUserInput()
|
|
|
|
// -- keyboard --
|
|
if (isKeyDown(KEY_ESC)) break;
|
|
|
|
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 --
|
|
// MMIO returns VM-screen pixel coords (origin at the top-left of the framebuffer).
|
|
// Widget xoff/yoff are passed straight into con.move(y, x), which is 1-indexed, so
|
|
// we offset by +1 here. Without this the click registers one cell up-and-left from
|
|
// where the user's pointer is, because pixel 0 = con.move(1, 1).
|
|
let pos = readMousePos()
|
|
let charX = (pos[0] / 7 | 0) + 1
|
|
let charY = (pos[1] / 14 | 0) + 1
|
|
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()
|
|
}
|
|
|
|
con.reset_graphics()
|
|
con.clear() |