From 9061523e325c74c0e46c720933891177383ad4de Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch
Date: Fri, 14 Nov 2025 10:56:01 +0100 Subject: WIP: liustsim --- liust-50/cmd/liustsim/simulator.go | 170 ++++++++++++++++++++++++++----------- 1 file changed, 119 insertions(+), 51 deletions(-) (limited to 'liust-50/cmd/liustsim') diff --git a/liust-50/cmd/liustsim/simulator.go b/liust-50/cmd/liustsim/simulator.go index 2ad69a8..fd26656 100644 --- a/liust-50/cmd/liustsim/simulator.go +++ b/liust-50/cmd/liustsim/simulator.go @@ -4,6 +4,7 @@ import ( "bufio" "image" "image/color" + "log" "os" "strconv" "strings" @@ -11,6 +12,10 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" "janouch.name/desktop-tools/liust-50/charset" ) @@ -31,7 +36,6 @@ const ( ) type Display struct { - image *canvas.Image chars [displayHeight][displayWidth]uint8 charset uint8 cursorX int @@ -67,25 +71,32 @@ func (d *Display) drawCharacter( width, height := bounds.Dx(), bounds.Dy() for dy := 0; dy < height; dy++ { for dx := 0; dx < width; dx++ { - c := charImg.At(bounds.Min.X+dx, bounds.Min.Y+dy) - if r, _, _, _ := c.RGBA(); r >= 0x8000 { + var c color.RGBA + if r, _, _, _ := charImg.At( + bounds.Min.X+dx, bounds.Min.Y+dy).RGBA(); r >= 0x8000 { c = color.RGBA{0x00, 0xFF, 0xC0, 0xFF} } else { c = color.RGBA{0x20, 0x20, 0x20, 0xFF} } - img.Set(1+cx*charWidth+dx, 1+cy*charHeight+dy, c) + img.SetRGBA(1+cx*charWidth+dx, 1+cy*charHeight+dy, c) } } } func (d *Display) Render() image.Image { + // XXX: The VFD display doesn't have rectangular pixels, + // they are rather elongated in a 3:4 ratio. width := 1 + displayWidth*charWidth height := 1 + displayHeight*charHeight + // XXX: Not sure if we rather don't want to provide double buffering, + // meaning we would cycle between two internal buffers. img := image.NewRGBA(image.Rect(0, 0, width, height)) + + black := [4]uint8{0x00, 0x00, 0x00, 0xFF} for y := 0; y < height; y++ { for x := 0; x < width; x++ { - img.Set(x, y, color.Black) + copy(img.Pix[img.PixOffset(x, y):], black[:]) } } @@ -175,36 +186,36 @@ func parseANSI(input string) (command string, params []int) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -type escapeParser struct { +type protocolParser struct { seq strings.Builder inEsc bool inCSI bool display *Display } -func newEscapeParser(d *Display) *escapeParser { - return &escapeParser{display: d} +func newProtocolParser(d *Display) *protocolParser { + return &protocolParser{display: d} } -func (ep *escapeParser) reset() { - ep.inEsc = false - ep.inCSI = false - ep.seq.Reset() +func (pp *protocolParser) reset() { + pp.inEsc = false + pp.inCSI = false + pp.seq.Reset() } -func (ep *escapeParser) handleCSICommand() bool { - cmd, params := parseANSI(ep.seq.String()) +func (pp *protocolParser) handleCSICommand() bool { + cmd, params := parseANSI(pp.seq.String()) switch cmd { case "J": // Clear display - // XXX: No params case is unverified. + // XXX: The no params case is unverified. if len(params) == 0 || params[0] == 2 { - ep.display.Clear() + pp.display.Clear() } case "K": // Delete to end of line - // XXX: No params case is unverified (but it should work). + // XXX: The no params case is unverified (but it should work). if len(params) == 0 || params[0] == 0 { - ep.display.ClearToEnd() + pp.display.ClearToEnd() } case "H": // Cursor position y, x := 0, 0 @@ -214,101 +225,158 @@ func (ep *escapeParser) handleCSICommand() bool { if len(params) >= 2 { x = params[1] - 1 } - ep.display.SetCursor(x, y) + pp.display.SetCursor(x, y) } return true } -func (ep *escapeParser) handleEscapeSequence(b byte) bool { - ep.seq.WriteByte(b) +func (pp *protocolParser) handleEscapeSequence(b byte) bool { + pp.seq.WriteByte(b) - if ep.seq.Len() == 2 && b == '[' { - ep.inCSI = true + if pp.seq.Len() == 2 && b == '[' { + pp.inCSI = true return false } - if ep.seq.Len() == 3 && ep.seq.String()[1] == 'R' { - ep.display.charset = b - ep.reset() + if pp.seq.Len() == 3 && pp.seq.String()[1] == 'R' { + pp.display.charset = b + pp.reset() return true } - if ep.inCSI && (b >= 'A' && b <= 'Z' || b >= 'a' && b <= 'z') { - refresh := ep.handleCSICommand() - ep.reset() + if pp.inCSI && (b >= 'A' && b <= 'Z' || b >= 'a' && b <= 'z') { + refresh := pp.handleCSICommand() + pp.reset() return refresh } - if ep.seq.Len() == 6 && ep.seq.String()[1:5] == "\\?LC" { - ep.display.cursorMode = int(ep.seq.String()[5]) + if pp.seq.Len() == 6 && pp.seq.String()[1:5] == "\\?LC" { + pp.display.cursorMode = int(pp.seq.String()[5]) return true } return false } -func (ep *escapeParser) handleControlChar(b byte) bool { +func (pp *protocolParser) handleCharacter(b byte) bool { switch b { case 0x0A: // LF - ep.display.LineFeed() + pp.display.LineFeed() return true case 0x0D: // CR - ep.display.CarriageReturn() + pp.display.CarriageReturn() return true case 0x08: // BS - ep.display.Backspace() + pp.display.Backspace() return true default: if b >= 0x20 { - ep.display.PutChar(b) + pp.display.PutChar(b) return true } } return false } -func (ep *escapeParser) handleByte(b byte) (needsRefresh bool) { +func (pp *protocolParser) handleByte(b byte) (needsRefresh bool) { if b == 0x1b { // ESC - ep.reset() - ep.inEsc = true - ep.seq.WriteByte(b) + pp.reset() + pp.inEsc = true + pp.seq.WriteByte(b) return false } - - if ep.inEsc { - return ep.handleEscapeSequence(b) + if pp.inEsc { + return pp.handleEscapeSequence(b) } - return ep.handleControlChar(b) + return pp.handleCharacter(b) +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +type DisplayWidget struct { + widget.BaseWidget + display *Display +} + +type DisplayRenderer struct { + image *canvas.Image + label *canvas.Text + + objects []fyne.CanvasObject + displayWidget *DisplayWidget } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - func main() { - myApp := app.New() - window := myApp.NewWindow("Toshiba Tec LIUST-50 Simulator") + a := app.New() + a.Settings().SetTheme(theme.DarkTheme()) + window := a.NewWindow("Toshiba Tec LIUST-50 Simulator") display := NewDisplay() display.Clear() + // TODO(p): It makes the most sense to create a custom widget. + // + // - How does Fyne render text? + // - Do I need to give it a left top position and pixel size, or? + // - canvas.NewText → canvas.Text + // + // - How does the Fyne model work? + // - All Windows have a Canvas. + // - Everything drawn, including Widgets, is a CanvasObject. + // - After CanvasObject updates, call Refresh on them. + // - It seems like this does not work hierarchically, + // and there's a generic canvas.Refresh() function, + // and Refresh() methods generally just invoke that. + // - window.SetContent() takes a CanvasObject. + // - How do WidgetRenderers work? + // - How would a widget that renders itself stretched work? + // + // - What the heck do I need? + // - *canvas.Image, *canvas.Text + // + // - Behaviour: + // - Requesting a minimum size: + // - Return the pixel size of the display, + // and at the bottom add the label. + // - For a character 84 pixels tall, + // the TOSHIBA label is 35 pixels tall. + // That means the TOSHIBA label is 3 pixels tall. + // - Given a size allocation: + // - How does the API work? + // - Rendering: + // - This should really be the work of two CanvasObject subobjects. + // - The bitmap will be rendered pixel-stretched. + // - Below it, the text will be rendered. + // + // _______________________ + // | | + // | VFD 2x20 | + // |______________________| + // |________Label_________| + // + // - + // img := canvas.NewImageFromImage(display.Render()) img.FillMode = canvas.ImageFillOriginal img.ScaleMode = canvas.ImageScalePixels - window.SetContent(img) + c := container.New(layout.NewVBoxLayout(), layout.NewSpacer(), img, layout.NewSpacer()) + + window.SetContent(c) window.Resize(fyne.NewSize(600, 100)) go func() { reader := bufio.NewReader(os.Stdin) - parser := newEscapeParser(display) + parser := newProtocolParser(display) for { b, err := reader.ReadByte() if err != nil { - fyne.DoAndWait(func() { - window.SetTitle(err.Error()) - }) + log.Println(err) return } -- cgit v1.2.3-70-g09d2