@@ -1,24 +1,414 @@
graphics . setBackground ( 2 , 1 , 3 ) ;
graphics . resetPalette ( ) ;
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 ) ;
sys . poke ( - 40 , 1 )
}
function getKeyPushed ( keyOrder ) {
return sys . peek ( - 41 - keyOrder ) ;
return sys . peek ( - 41 - keyOrder )
}
let _fsh = { } ;
_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" ;
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
// 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 (
"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 ( ) {
let wp = files . open ( "A:/home/wall.bytes" )
@@ -28,85 +418,85 @@ _fsh.drawWallpaper = function() {
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 ) ;
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 ) ;
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 ( 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 )
}
con . color _pair ( 254 , 255 ) ;
} ;
_fsh . Widget = function ( id , w , h ) {
this . identifier = id ;
this . width = w ;
this . height = h ;
this . identifier = id
this . width = w
this . height = h
if ( ! this . identifier ) {
this . identifier = "" ;
this . identifier = ""
}
//this.update = function() {};
//this.update = function() {}
/**
* Params charXoff and charYoff are ZERO-BASED!
*/
this . draw = function ( charXoff , charYoff ) { } ;
this . draw = function ( charXoff , charYoff ) { }
}
_fsh . widgets = { }
_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 (
"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 . 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 ;
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 ) ;
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 ) ;
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 ] ) ;
} ;
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 )
@@ -171,70 +561,284 @@ calendarWidget.draw = function(charXoff, charYoff) {
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 ( "========== TODO ==========" )
print ( '\u00CD' . repeat ( 10 ) + " TODO " + '\u00CD' . repeat ( 10 ) )
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 ( l ist [ 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 )
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 )
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 )
quickAccessWidget . entries = [
[ "Files" , "/tvdos/bin/explorer .js" ] ,
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" ]
[ "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 ( "====== QUICK ACCESS ======" )
print ( '\u00CD' . repeat ( 6 ) + " QUICK ACCESS " + '\u00CD' . repeat ( 6 ) )
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 ( l ist [ 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 )
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 )
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 ( )
// 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 )
@@ -260,29 +864,126 @@ _fsh.drawWallpaper()
_fsh . drawTitlebar ( )
// TEST
con . move ( 2 , 1 ) ;
print ( "fSh is very much in-dev! Hit backspace to exit" )
// 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"
// TODO update for events: key down (updates some widgets), timer (updates clock and calendar widgets)
while ( true ) {
captureUserInput ( ) ;
if ( getKeyPushed ( 0 ) == 67 ) break ;
captureUserInput ( )
_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 ) ;
// -- 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 --
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 ( )
}
con . move ( 3 , 1 ) ;
con . color _pair ( 201 , 255 ) ;
print ( "cya!" ) ;
let konsht = 3412341241 ;
println ( konsht ) ;
let pppp = graphics . getCursorYX ( ) ;
println ( pppp . toString ( ) ) ;
con . reset _graphics ( )
con . clear ( )