diff --git a/README.md b/README.md index ba4fa04..892969a 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ func main() { } // Just pass your tea.Model to the bubbleadapter, and it will render it to the terminal. - win, err := bubbleadapter.Window(1000, 600, fonts, someModel{}, color.Black, tea.WithAltScreen()) + win, _, err := bubbleadapter.Window(1000, 600, fonts, someModel{}, color.Black, tea.WithAltScreen()) if err != nil { panic(err) } diff --git a/bubbletea/bubbletea.go b/bubbletea/bubbletea.go index c945100..3754465 100644 --- a/bubbletea/bubbletea.go +++ b/bubbletea/bubbletea.go @@ -31,7 +31,7 @@ func (f fakeEnviron) Getenv(s string) string { // Window creates a new crt based bubbletea window with the given width, height, fonts, model and default background color. // Additional options can be passed to the bubbletea program. -func Window(width int, height int, fonts crt.Fonts, model tea.Model, defaultBg color.Color, options ...tea.ProgramOption) (*crt.Window, error) { +func Window(width int, height int, fonts crt.Fonts, model tea.Model, defaultBg color.Color, options ...tea.ProgramOption) (*crt.Window, *tea.Program, error) { gameInput := crt.NewConcurrentRW() gameOutput := crt.NewConcurrentRW() @@ -56,5 +56,6 @@ func Window(width int, height int, fonts crt.Fonts, model tea.Model, defaultBg c crt.SysKill() }() - return crt.NewGame(width, height, fonts, gameOutput, NewAdapter(prog), defaultBg) + win, err := crt.NewGame(width, height, fonts, gameOutput, NewAdapter(prog), defaultBg) + return win, prog, err } diff --git a/crt.go b/crt.go index 3e9ede2..1ba8a6f 100644 --- a/crt.go +++ b/crt.go @@ -30,14 +30,17 @@ type Window struct { tty io.Reader // Terminal cursor and color states. - cursorX int - cursorY int - mouseCellX int - mouseCellY int - defaultBg color.Color - curFg color.Color - curBg color.Color - curWeight FontWeight + cursorChar string + cursorColor color.Color + showCursor bool + cursorX int + cursorY int + mouseCellX int + mouseCellY int + defaultBg color.Color + curFg color.Color + curBg color.Color + curWeight FontWeight // Other showTps bool @@ -45,7 +48,6 @@ type Window struct { bgColors *image.RGBA shader []shader.Shader routine sync.Once - tick float64 } // NewGame creates a new terminal game with the given dimensions and font faces. @@ -89,6 +91,8 @@ func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAda grid: grid, tty: tty, bgColors: image.NewRGBA(image.Rect(0, 0, cellsWidth*cellWidth, cellsHeight*cellHeight)), + cursorChar: "█", + cursorColor: color.RGBA{R: 255, G: 255, B: 255, A: 100}, } game.inputAdapter.HandleWindowSize(WindowSize{ @@ -102,6 +106,21 @@ func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAda return game, nil } +// SetShowCursor enables or disables the cursor. +func (g *Window) SetShowCursor(val bool) { + g.showCursor = val +} + +// SetCursorChar sets the character that is used for the cursor. +func (g *Window) SetCursorChar(char string) { + g.cursorChar = char +} + +// SetCursorColor sets the color of the cursor. +func (g *Window) SetCursorColor(color color.Color) { + g.cursorColor = color +} + // SetShader sets a shader that is applied to the whole screen. func (g *Window) SetShader(shader ...shader.Shader) { g.shader = shader @@ -215,6 +234,10 @@ func (g *Window) handleCSI(csi any) { g.SetBgPixels(i, g.cursorY, g.defaultBg) } } + case CursorShowSeq: + g.SetShowCursor(true) + case CursorHideSeq: + g.SetShowCursor(false) case ScrollUpSeq: fmt.Println("UNSUPPORTED: ScrollUpSeq", seq.Count) case ScrollDownSeq: @@ -447,7 +470,10 @@ func (g *Window) Draw(screen *ebiten.Image) { } } - g.tick += 1 / 60.0 + if g.showCursor { + text.Draw(bufferImage, g.cursorChar, g.fonts.Normal, g.cursorX*g.cellWidth, g.cursorY*g.cellHeight+g.cellOffsetY, g.cursorColor) + } + if g.shader != nil { for i := range g.shader { _ = g.shader[i].Apply(screen, bufferImage) diff --git a/csi.go b/csi.go index ee5f793..697a113 100644 --- a/csi.go +++ b/csi.go @@ -72,6 +72,10 @@ type DeleteLineSeq struct { Count int } +type CursorShowSeq struct{} + +type CursorHideSeq struct{} + // extractCSI extracts a CSI sequence from the beginning of a string. // It returns the sequence without any suffix, and a boolean indicating // whether a sequence was found. @@ -105,6 +109,13 @@ func parseCSI(s string) (any, bool) { return nil, false } + switch s { + case termenv.ShowCursorSeq: + return CursorShowSeq{}, true + case termenv.HideCursorSeq: + return CursorHideSeq{}, true + } + switch s[len(s)-1] { case 'A': if count, err := strconv.Atoi(s[:len(s)-1]); err == nil { diff --git a/csi_test.go b/csi_test.go index ba62486..10a3d63 100644 --- a/csi_test.go +++ b/csi_test.go @@ -15,6 +15,7 @@ func TestCSI(t *testing.T) { testString += fmt.Sprintf(termenv.CSI+termenv.CursorPositionSeq, 1, 2) testString += fmt.Sprintf(termenv.CSI+termenv.CursorPositionSeq, 1, 2) testString += "HELLO WORLD" + testString += termenv.CSI + termenv.ShowCursorSeq testString += fmt.Sprintf(termenv.CSI+termenv.CursorPositionSeq, 1, 2) testString += fmt.Sprintf(termenv.CSI+termenv.CursorBackSeq, 5) @@ -34,6 +35,7 @@ func TestCSI(t *testing.T) { EraseDisplaySeq{Type: 20}, CursorPositionSeq{Row: 1, Col: 2}, CursorPositionSeq{Row: 1, Col: 2}, + CursorShowSeq{}, CursorPositionSeq{Row: 1, Col: 2}, CursorBackSeq{Count: 5}, }, sequences) diff --git a/examples/keys/main.go b/examples/keys/main.go index e0db161..034a6dd 100644 --- a/examples/keys/main.go +++ b/examples/keys/main.go @@ -43,7 +43,7 @@ func main() { panic(err) } - win, err := bubbleadapter.Window(Width, Height, fonts, model{}, color.Black) + win, _, err := bubbleadapter.Window(Width, Height, fonts, model{}, color.Black) if err != nil { panic(err) } diff --git a/examples/package-manager/main.go b/examples/package-manager/main.go index 8d73192..607a549 100644 --- a/examples/package-manager/main.go +++ b/examples/package-manager/main.go @@ -192,7 +192,7 @@ func main() { panic(err) } - win, err := bubbleadapter.Window(Width, Height, fonts, newModel(), color.Black) + win, _, err := bubbleadapter.Window(Width, Height, fonts, newModel(), color.Black) if err != nil { panic(err) } diff --git a/examples/shader/main.go b/examples/shader/main.go index 053b114..06705c3 100644 --- a/examples/shader/main.go +++ b/examples/shader/main.go @@ -193,7 +193,7 @@ func main() { panic(err) } - win, err := bubbleadapter.Window(Width, Height, fonts, newModel(), color.Black) + win, _, err := bubbleadapter.Window(Width, Height, fonts, newModel(), color.Black) if err != nil { panic(err) } diff --git a/examples/simple/main.go b/examples/simple/main.go index 14f8b2d..ade1498 100644 --- a/examples/simple/main.go +++ b/examples/simple/main.go @@ -34,11 +34,14 @@ func main() { panic(err) } - win, err := bubbleadapter.Window(Width, Height, fonts, model{}, color.Black, tea.WithAltScreen()) + win, prog, err := bubbleadapter.Window(Width, Height, fonts, model{}, color.Black, tea.WithAltScreen()) if err != nil { panic(err) } + prog.Send(tea.ShowCursor()) + win.SetCursorChar("_") + if err := win.Run("Simple"); err != nil { panic(err) }