Compare commits

..

28 Commits
v0.0.6 ... main

Author SHA1 Message Date
Daniel Schmidt
7710fdc88d
Merge pull request #1 from obvionaoe/main
Add WindowOption and RunWithOptions method to allow for extra configuration
2024-05-09 19:54:27 +02:00
obvionaoe
d976f61673
Remove test functions for WindowOption 2024-05-09 16:15:19 +01:00
obvionaoe
ea37c58ee8
Add WindowOption and RunWithOptions function to allow for extra configuration 2024-05-09 16:01:48 +01:00
Daniel Schmidt
a3526f42c5 feat: update to new bubbletea mouse handling 2024-01-12 21:00:06 +01:00
Daniel Schmidt
fbecabe083 chore: update bubbletea dep 2023-12-14 13:53:27 +01:00
Daniel Schmidt
902582a274 feat: optimize image allocations 2023-12-11 17:55:50 +01:00
Daniel Schmidt
ac7d8bf4c3 Merge branch 'main' of https://github.com/BigJk/crt 2023-12-10 09:21:40 +01:00
Daniel Schmidt
ee5e2843e2 chore: update ebiten to support newer osx version 2023-12-10 08:49:28 +01:00
Daniel S
f2f0473fdf
Update README.md 2023-05-27 14:19:33 +02:00
Daniel Schmidt
5104c9a225 Added glamour example. 2023-05-27 10:19:22 +02:00
Daniel S
b56ac867a2
Update README.md 2023-05-27 10:00:04 +02:00
Daniel Schmidt
48a7adaf9e Refactor. 2023-05-23 23:30:56 +02:00
Daniel Schmidt
6151ddad2f Fix ctr+key input. 2023-05-23 23:29:05 +02:00
Daniel Schmidt
d9c72af4f2 Fix double space. 2023-05-23 23:21:44 +02:00
Daniel Schmidt
e5f8fb908a Better keyboard input handling. 2023-05-23 23:00:46 +02:00
Daniel Schmidt
52a54cbfc8 Fix missing bg setting for ANSI256 colors. 2023-05-23 23:00:37 +02:00
Daniel Schmidt
2c21fde141 Added high dpi support. 2023-05-23 22:36:08 +02:00
Daniel Schmidt
032bbda893 Performance optimization. 2023-05-23 20:53:49 +02:00
Daniel Schmidt
c18e3badb7 Fix problem with uppercase transformation. 2023-05-19 20:23:17 +02:00
Daniel Schmidt
227f55ac55 Updated README.md. 2023-05-19 20:09:52 +02:00
Daniel Schmidt
d3252b9dd6 Better key handling. 2023-05-19 19:57:38 +02:00
Daniel Schmidt
7b3a48d9d5 Fix SGR passing edge case. 2023-05-19 19:57:17 +02:00
Daniel Schmidt
4127d81de6 Updated README.md. 2023-05-19 19:28:47 +02:00
Daniel Schmidt
4c1bcbdaef Add ANSI256 color support. 2023-05-19 19:27:55 +02:00
Daniel Schmidt
a303a3a4dd Add optimization to only redraw if change happened. 2023-05-19 19:18:18 +02:00
Daniel Schmidt
87fafa02e6 Fix exit on windows. 2023-05-19 19:14:46 +02:00
Daniel Schmidt
a2a2705a42 Update rune passing. 2023-05-18 18:34:29 +02:00
Daniel Schmidt
19757d9b62 Added visible cursor support. 2023-05-18 18:23:38 +02:00
19 changed files with 1054 additions and 240 deletions

View File

@ -6,7 +6,7 @@
CRT is a library to provide a simple terminal emulator that can be attached to a ``tea.Program``. It uses ``ebitengine`` to render a terminal. It supports TrueColor, Mouse and Keyboard input. It interprets the CSI escape sequences coming from bubbletea and renders them to the terminal.
This started as a simple proof of concept for the game I'm writing with the help of bubbletea, called [End Of Eden](github.com/BigJk/end_of_eden). I wanted to give people who have no clue about the terminal a simple option to play the game without interacting with the terminal directly. It's also possible to apply shaders to the terminal to give it a more retro look which is a nice side effect.
This started as a simple proof of concept for the game I'm writing with the help of bubbletea, called [End Of Eden](https://github.com/BigJk/end_of_eden). I wanted to give people who have no clue about the terminal a simple option to play the game without interacting with the terminal directly. It's also possible to apply shaders to the terminal to give it a more retro look which is a nice side effect.
## Usage
@ -30,13 +30,13 @@ import (
func main() {
// Load fonts for normal, bold and italic text styles.
fonts, err := crt.LoadFaces("./fonts/SomeFont-Regular.ttf", "./fonts/SomeFont-Bold.ttf", "./fonts/SomeFont-Italic.ttf", 72.0, 16.0)
fonts, err := crt.LoadFaces("./fonts/SomeFont-Regular.ttf", "./fonts/SomeFont-Bold.ttf", "./fonts/SomeFont-Italic.ttf", crt.GetFontDPI(), 16.0)
if err != nil {
panic(err)
}
// 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)
}
@ -48,13 +48,16 @@ func main() {
}
```
See more examples in the ``/examples`` folder!
## Limitations
- Only supports TrueColor at the moment (no 256 color support) so you need to use TrueColor colors in lipgloss (e.g. ``lipgloss.Color("#ff0000")``)
- ~~Only supports TrueColor at the moment (no 256 color support) so you need to use TrueColor colors in lipgloss (e.g. ``lipgloss.Color("#ff0000")``)~~ **Now supported.**
- Not all CSI escape sequences are implemented but the ones that are used by bubbletea are implemented
- Key handling is a bit quirky atm. Ebiten to bubbletea key mapping is not perfect and some keys are not handled correctly yet.
- A lot of testing still needs to be done and there are probably edge cases that are not handled correctly yet
## Credits
- Basic CRT Shader ``./shader/crt_basic``: https://quasilyte.dev/blog/post/ebitengine-shaders/
- Lottes CRT Shader ``./shader/crt_lotte``: Elias Daler https://github.com/eliasdaler/crten and Timothy Lottes.
- Lottes CRT Shader ``./shader/crt_lotte``: Elias Daler https://github.com/eliasdaler/crten and Timothy Lottes.

View File

@ -5,50 +5,88 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"strings"
"unicode"
)
var ebitenToTeaKeys = map[ebiten.Key]tea.KeyType{
ebiten.KeyEnter: tea.KeyEnter,
ebiten.KeyTab: tea.KeyTab,
ebiten.KeySpace: tea.KeySpace,
ebiten.KeyBackspace: tea.KeyBackspace,
ebiten.KeyDelete: tea.KeyDelete,
ebiten.KeyHome: tea.KeyHome,
ebiten.KeyEnd: tea.KeyEnd,
ebiten.KeyPageUp: tea.KeyPgUp,
ebiten.KeyArrowUp: tea.KeyUp,
ebiten.KeyArrowDown: tea.KeyDown,
ebiten.KeyArrowLeft: tea.KeyLeft,
ebiten.KeyArrowRight: tea.KeyRight,
ebiten.KeyEscape: tea.KeyEscape,
type teaKey struct {
key tea.KeyType
rune []rune
}
var ebitenToTeaRunes = map[ebiten.Key][]rune{
ebiten.Key1: {'1'},
ebiten.Key2: {'2'},
ebiten.Key3: {'3'},
ebiten.Key4: {'4'},
ebiten.Key5: {'5'},
ebiten.Key6: {'6'},
ebiten.Key7: {'7'},
ebiten.Key8: {'8'},
ebiten.Key9: {'9'},
ebiten.Key0: {'0'},
ebiten.KeyEnter: {'\n'},
ebiten.KeyTab: {'\t'},
ebiten.KeySpace: {' '},
ebiten.KeyPeriod: {'.'},
ebiten.KeySlash: {'/'},
ebiten.KeyBackslash: {'\\'},
ebiten.KeyMinus: {'-'},
ebiten.KeyEqual: {'='},
ebiten.KeySemicolon: {';'},
ebiten.KeyApostrophe: {'\''},
ebiten.KeyGraveAccent: {'`'},
ebiten.KeyComma: {','},
ebiten.KeyLeftBracket: {'['},
ebiten.KeyRightBracket: {']'},
func repeatingKeyPressed(key ebiten.Key) bool {
const (
delay = 30
interval = 3
)
d := inpututil.KeyPressDuration(key)
if d == 1 {
return true
}
if d >= delay && (d-delay)%interval == 0 {
return true
}
return false
}
var ebitenToTeaKeys = map[ebiten.Key]teaKey{
ebiten.KeyEnter: {tea.KeyEnter, []rune{'\n'}},
ebiten.KeyTab: {tea.KeyTab, []rune{}},
ebiten.KeyBackspace: {tea.KeyBackspace, []rune{}},
ebiten.KeyDelete: {tea.KeyDelete, []rune{}},
ebiten.KeyHome: {tea.KeyHome, []rune{}},
ebiten.KeyEnd: {tea.KeyEnd, []rune{}},
ebiten.KeyPageUp: {tea.KeyPgUp, []rune{}},
ebiten.KeyArrowUp: {tea.KeyUp, []rune{}},
ebiten.KeyArrowDown: {tea.KeyDown, []rune{}},
ebiten.KeyArrowLeft: {tea.KeyLeft, []rune{}},
ebiten.KeyArrowRight: {tea.KeyRight, []rune{}},
ebiten.KeyEscape: {tea.KeyEscape, []rune{}},
ebiten.KeyF1: {tea.KeyF1, []rune{}},
ebiten.KeyF2: {tea.KeyF2, []rune{}},
ebiten.KeyF3: {tea.KeyF3, []rune{}},
ebiten.KeyF4: {tea.KeyF4, []rune{}},
ebiten.KeyF5: {tea.KeyF5, []rune{}},
ebiten.KeyF6: {tea.KeyF6, []rune{}},
ebiten.KeyF7: {tea.KeyF7, []rune{}},
ebiten.KeyF8: {tea.KeyF8, []rune{}},
ebiten.KeyF9: {tea.KeyF9, []rune{}},
ebiten.KeyF10: {tea.KeyF10, []rune{}},
ebiten.KeyF11: {tea.KeyF11, []rune{}},
ebiten.KeyF12: {tea.KeyF12, []rune{}},
ebiten.KeyShift: {tea.KeyShiftLeft, []rune{}},
}
var ebitenToCtrlKeys = map[ebiten.Key]tea.KeyType{
ebiten.KeyA: tea.KeyCtrlA,
ebiten.KeyB: tea.KeyCtrlB,
ebiten.KeyC: tea.KeyCtrlC,
ebiten.KeyD: tea.KeyCtrlD,
ebiten.KeyE: tea.KeyCtrlE,
ebiten.KeyF: tea.KeyCtrlF,
ebiten.KeyG: tea.KeyCtrlG,
ebiten.KeyH: tea.KeyCtrlH,
ebiten.KeyI: tea.KeyCtrlI,
ebiten.KeyJ: tea.KeyCtrlJ,
ebiten.KeyK: tea.KeyCtrlK,
ebiten.KeyL: tea.KeyCtrlL,
ebiten.KeyM: tea.KeyCtrlM,
ebiten.KeyN: tea.KeyCtrlN,
ebiten.KeyO: tea.KeyCtrlO,
ebiten.KeyP: tea.KeyCtrlP,
ebiten.KeyQ: tea.KeyCtrlQ,
ebiten.KeyR: tea.KeyCtrlR,
ebiten.KeyS: tea.KeyCtrlS,
ebiten.KeyT: tea.KeyCtrlT,
ebiten.KeyU: tea.KeyCtrlU,
ebiten.KeyV: tea.KeyCtrlV,
ebiten.KeyW: tea.KeyCtrlW,
ebiten.KeyX: tea.KeyCtrlX,
ebiten.KeyY: tea.KeyCtrlY,
ebiten.KeyZ: tea.KeyCtrlZ,
ebiten.KeyLeftBracket: tea.KeyCtrlOpenBracket,
ebiten.KeyBackslash: tea.KeyCtrlBackslash,
ebiten.KeyRightBracket: tea.KeyCtrlCloseBracket,
ebiten.KeyApostrophe: tea.KeyCtrlCaret,
}
var ebitenToTeaMouse = map[ebiten.MouseButton]tea.MouseEventType{
@ -57,6 +95,16 @@ var ebitenToTeaMouse = map[ebiten.MouseButton]tea.MouseEventType{
ebiten.MouseButtonRight: tea.MouseRight,
}
var ebitenToTeaMouseNew = map[ebiten.MouseButton]tea.MouseButton{
ebiten.MouseButtonLeft: tea.MouseButtonLeft,
ebiten.MouseButtonMiddle: tea.MouseButtonMiddle,
ebiten.MouseButtonRight: tea.MouseButtonRight,
// TODO: is this right?
ebiten.MouseButton3: tea.MouseButtonBackward,
ebiten.MouseButton4: tea.MouseButtonForward,
}
// Options are used to configure the adapter.
type Options func(*Adapter)
@ -86,11 +134,12 @@ func NewAdapter(prog *tea.Program, options ...Options) *Adapter {
func (b *Adapter) HandleMouseMotion(motion crt.MouseMotion) {
b.prog.Send(tea.MouseMsg{
X: motion.X,
Y: motion.Y,
Alt: false,
Ctrl: false,
Type: tea.MouseMotion,
X: motion.X,
Y: motion.Y,
Alt: false,
Ctrl: false,
Type: tea.MouseMotion,
Action: tea.MouseActionMotion,
})
}
@ -100,13 +149,22 @@ func (b *Adapter) HandleMouseButton(button crt.MouseButton) {
return
}
b.prog.Send(tea.MouseMsg{
X: button.X,
Y: button.Y,
Alt: ebiten.IsKeyPressed(ebiten.KeyAlt),
Ctrl: ebiten.IsKeyPressed(ebiten.KeyControl),
Type: ebitenToTeaMouse[button.Button],
})
msg := tea.MouseMsg{
X: button.X,
Y: button.Y,
Alt: ebiten.IsKeyPressed(ebiten.KeyAlt),
Ctrl: ebiten.IsKeyPressed(ebiten.KeyControl),
Type: ebitenToTeaMouse[button.Button],
Button: ebitenToTeaMouseNew[button.Button],
}
if button.JustReleased {
msg.Action = tea.MouseActionRelease
} else if button.JustPressed {
msg.Action = tea.MouseActionPress
}
b.prog.Send(msg)
}
func (b *Adapter) HandleMouseWheel(wheel crt.MouseWheel) {
@ -130,24 +188,64 @@ func (b *Adapter) HandleMouseWheel(wheel crt.MouseWheel) {
}
func (b *Adapter) HandleKeyPress() {
var keys []ebiten.Key
keys = inpututil.AppendJustReleasedKeys(keys)
for _, k := range keys {
if val, ok := ebitenToTeaKeys[k]; ok {
runes := []rune(strings.ToLower(k.String()))
newInputs := ebiten.AppendInputChars([]rune{})
for _, v := range newInputs {
switch v {
case ' ':
b.prog.Send(tea.KeyMsg{
Type: val,
Runes: runes,
Type: tea.KeySpace,
Runes: []rune{v},
Alt: ebiten.IsKeyPressed(ebiten.KeyAlt),
})
} else {
runes := []rune(strings.ToLower(k.String()))
if val, ok := ebitenToTeaRunes[k]; ok {
runes = val
}
default:
b.prog.Send(tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune{v},
Alt: ebiten.IsKeyPressed(ebiten.KeyAlt),
})
}
}
var keys []ebiten.Key
keys = inpututil.AppendJustPressedKeys(keys)
repeatedBackspace := repeatingKeyPressed(ebiten.KeyBackspace)
if repeatedBackspace {
b.prog.Send(tea.KeyMsg{
Type: tea.KeyBackspace,
Runes: []rune{},
Alt: false,
})
}
for _, k := range keys {
if ebiten.IsKeyPressed(ebiten.KeyControl) {
if tk, ok := ebitenToCtrlKeys[k]; ok {
b.prog.Send(tea.KeyMsg{
Type: tk,
Runes: []rune{},
Alt: false,
})
continue
}
}
if repeatedBackspace && k == ebiten.KeyBackspace {
continue
}
if val, ok := ebitenToTeaKeys[k]; ok {
runes := make([]rune, len(val.rune))
copy(runes, val.rune)
if ebiten.IsKeyPressed(ebiten.KeyShift) {
for i := range runes {
runes[i] = unicode.ToUpper(runes[i])
}
}
b.prog.Send(tea.KeyMsg{
Type: val.key,
Runes: runes,
Alt: ebiten.IsKeyPressed(ebiten.KeyAlt),
})

View File

@ -13,25 +13,9 @@ func init() {
lipgloss.SetColorProfile(termenv.TrueColor)
}
type fakeEnviron struct{}
func (f fakeEnviron) Environ() []string {
return []string{"TERM", "COLORTERM"}
}
func (f fakeEnviron) Getenv(s string) string {
switch s {
case "TERM":
return "xterm-256color"
case "COLORTERM":
return "truecolor"
}
return ""
}
// 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()
@ -43,7 +27,7 @@ func Window(width int, height int, fonts crt.Fonts, model tea.Model, defaultBg c
append([]tea.ProgramOption{
tea.WithMouseAllMotion(),
tea.WithInput(gameInput),
tea.WithOutput(termenv.NewOutput(gameOutput, termenv.WithEnvironment(fakeEnviron{}), termenv.WithTTY(true), termenv.WithProfile(termenv.TrueColor), termenv.WithColorCache(true))),
tea.WithOutput(gameOutput),
tea.WithANSICompressor(),
}, options...)...,
)
@ -56,5 +40,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
}

303
crt.go
View File

@ -7,13 +7,19 @@ import (
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/lucasb-eyer/go-colorful"
"github.com/muesli/ansi"
"github.com/muesli/termenv"
"image"
"image/color"
"io"
"sync"
"unicode/utf8"
)
// colorCache is the ansi color cache.
var colorCache = map[int]color.Color{}
type Window struct {
sync.Mutex
@ -30,24 +36,38 @@ 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
// Callbacks
onUpdate func()
onPreDraw func(screen *ebiten.Image)
onPostDraw func(screen *ebiten.Image)
// Other
showTps bool
fonts Fonts
bgColors *image.RGBA
shader []shader.Shader
routine sync.Once
tick float64
seqBuffer []byte
showTps bool
fonts Fonts
bgColors *image.RGBA
shader []shader.Shader
routine sync.Once
shaderByteBuffer []byte
shaderBuffer *ebiten.Image
lastBuffer *ebiten.Image
invalidateBuffer bool
}
type WindowOption func(window *Window)
// NewGame creates a new terminal game with the given dimensions and font faces.
func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAdapter, defaultBg color.Color) (*Window, error) {
if defaultBg == nil {
@ -57,12 +77,12 @@ func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAda
bounds, _, _ := fonts.Normal.GlyphBounds([]rune("█")[0])
size := bounds.Max.Sub(bounds.Min)
cellWidth := size.X.Round()
cellHeight := size.Y.Round()
cellOffsetY := -bounds.Min.Y.Round()
cellWidth := size.X.Ceil()
cellHeight := size.Y.Ceil()
cellOffsetY := -bounds.Min.Y.Ceil()
cellsWidth := width / cellWidth
cellsHeight := height / cellHeight
cellsWidth := int(float64(width)*DeviceScale()) / cellWidth
cellsHeight := int(float64(height)*DeviceScale()) / cellHeight
grid := make([][]GridCell, cellsHeight)
for y := 0; y < cellsHeight; y++ {
@ -78,17 +98,25 @@ func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAda
}
game := &Window{
inputAdapter: adapter,
cellsWidth: cellsWidth,
cellsHeight: cellsHeight,
cellWidth: cellWidth,
cellHeight: cellHeight,
cellOffsetY: cellOffsetY,
fonts: fonts,
defaultBg: defaultBg,
grid: grid,
tty: tty,
bgColors: image.NewRGBA(image.Rect(0, 0, cellsWidth*cellWidth, cellsHeight*cellHeight)),
inputAdapter: adapter,
cellsWidth: cellsWidth,
cellsHeight: cellsHeight,
cellWidth: cellWidth,
cellHeight: cellHeight,
cellOffsetY: cellOffsetY,
fonts: fonts,
defaultBg: defaultBg,
grid: grid,
tty: tty,
bgColors: image.NewRGBA(image.Rect(0, 0, cellsWidth*cellWidth, cellsHeight*cellHeight)),
lastBuffer: ebiten.NewImage(cellsWidth*cellWidth, cellsHeight*cellHeight),
cursorChar: "█",
cursorColor: color.RGBA{R: 255, G: 255, B: 255, A: 100},
onUpdate: func() {},
onPreDraw: func(screen *ebiten.Image) {},
onPostDraw: func(screen *ebiten.Image) {},
invalidateBuffer: true,
seqBuffer: make([]byte, 0, 2^12),
}
game.inputAdapter.HandleWindowSize(WindowSize{
@ -102,16 +130,54 @@ 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
g.InvalidateBuffer()
}
// SetCursorChar sets the character that is used for the cursor.
func (g *Window) SetCursorChar(char string) {
g.cursorChar = char
g.InvalidateBuffer()
}
// SetCursorColor sets the color of the cursor.
func (g *Window) SetCursorColor(color color.Color) {
g.cursorColor = color
g.InvalidateBuffer()
}
// SetShader sets a shader that is applied to the whole screen.
func (g *Window) SetShader(shader ...shader.Shader) {
g.shader = shader
}
// SetOnUpdate sets a function that is called every frame.
func (g *Window) SetOnUpdate(fn func()) {
g.onUpdate = fn
}
// SetOnPreDraw sets a function that is called before the screen is drawn.
func (g *Window) SetOnPreDraw(fn func(screen *ebiten.Image)) {
g.onPreDraw = fn
}
// SetOnPostDraw sets a function that is called after the screen is drawn.
func (g *Window) SetOnPostDraw(fn func(screen *ebiten.Image)) {
g.onPostDraw = fn
}
// ShowTPS enables or disables the TPS counter on the top left.
func (g *Window) ShowTPS(val bool) {
g.showTps = val
}
// InvalidateBuffer forces the buffer to be redrawn.
func (g *Window) InvalidateBuffer() {
g.invalidateBuffer = true
}
// ResetSGR resets the SGR attributes to their default values.
func (g *Window) ResetSGR() {
g.curFg = color.White
@ -126,6 +192,29 @@ func (g *Window) SetBgPixels(x, y int, c color.Color) {
g.bgColors.Set(x*g.cellWidth+i, y*g.cellHeight+j, c)
}
}
g.InvalidateBuffer()
}
// SetBg sets the background color of a cell and checks if it needs to be redrawn.
func (g *Window) SetBg(x, y int, c color.Color) {
ra, rg, rb, _ := g.grid[y][x].Bg.RGBA()
ca, cg, cb, _ := c.RGBA()
if ra == ca && rg == cg && rb == cb {
return
}
g.SetBgPixels(x, y, c)
g.grid[y][x].Bg = c
}
// GetCellsWidth returns the number of cells in the x direction.
func (g *Window) GetCellsWidth() int {
return g.cellsWidth
}
// GetCellsHeight returns the number of cells in the y direction.
func (g *Window) GetCellsHeight() int {
return g.cellsHeight
}
func (g *Window) handleCSI(csi any) {
@ -197,24 +286,25 @@ func (g *Window) handleCSI(csi any) {
for i := g.cursorX; i < g.cellsWidth-g.cursorX; i++ {
g.grid[g.cursorY][g.cursorX+i].Char = ' '
g.grid[g.cursorY][g.cursorX+i].Fg = color.White
g.grid[g.cursorY][g.cursorX+i].Bg = g.defaultBg
g.SetBgPixels(g.cursorX+i, g.cursorY, g.defaultBg)
g.SetBg(g.cursorX+i, g.cursorY, g.defaultBg)
}
case 1: // erase from start of line to cursor
for i := 0; i < g.cursorX; i++ {
g.grid[g.cursorY][i].Char = ' '
g.grid[g.cursorY][i].Fg = color.White
g.grid[g.cursorY][i].Bg = g.defaultBg
g.SetBgPixels(i, g.cursorY, g.defaultBg)
g.SetBg(i, g.cursorY, g.defaultBg)
}
case 2: // erase entire line
for i := 0; i < g.cellsWidth; i++ {
g.grid[g.cursorY][i].Char = ' '
g.grid[g.cursorY][i].Fg = color.White
g.grid[g.cursorY][i].Bg = g.defaultBg
g.SetBgPixels(i, g.cursorY, g.defaultBg)
g.SetBg(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:
@ -245,40 +335,69 @@ func (g *Window) handleSGR(sgr any) {
case SGRUnsetItalic:
g.curWeight = FontWeightNormal
case SGRFgTrueColor:
g.curFg = color.RGBA{seq.R, seq.G, seq.B, 255}
g.curFg = color.RGBA{R: seq.R, G: seq.G, B: seq.B, A: 255}
case SGRBgTrueColor:
g.curBg = color.RGBA{seq.R, seq.G, seq.B, 255}
g.curBg = color.RGBA{R: seq.R, G: seq.G, B: seq.B, A: 255}
case SGRFgColor:
if val, ok := colorCache[seq.Id]; ok {
g.curFg = val
} else {
if col, err := colorful.Hex(termenv.ANSI256Color(seq.Id).String()); err == nil {
g.curFg = col
colorCache[seq.Id] = col
}
}
case SGRBgColor:
if val, ok := colorCache[seq.Id]; ok {
g.curBg = val
} else {
if col, err := colorful.Hex(termenv.ANSI256Color(seq.Id).String()); err == nil {
g.curBg = col
colorCache[seq.Id] = col
}
}
}
}
func (g *Window) parseSequences(str string, printExtra bool) int {
runes := []rune(str)
lastFound := 0
for i := 0; i < len(runes); i++ {
if sgr, ok := extractSGR(string(runes[i:])); ok {
for i := 0; i < len(str); i++ {
if sgr, ok := extractSGR(str[i:]); ok {
i += len(sgr) - 1
if sgr, ok := parseSGR(sgr); ok {
lastFound = i
for i := range sgr {
g.handleSGR(sgr[i])
g.InvalidateBuffer()
}
}
} else if csi, ok := extractCSI(string(runes[i:])); ok {
} else if csi, ok := extractCSI(str[i:]); ok {
i += len(csi) - 1
if csi, ok := parseCSI(csi); ok {
lastFound = i
g.handleCSI(csi)
g.InvalidateBuffer()
}
} else if printExtra {
g.PrintChar(runes[i], g.curFg, g.curBg, g.curWeight)
if r, size := utf8.DecodeRuneInString(str[i:]); r != utf8.RuneError {
g.PrintChar(r, g.curFg, g.curBg, g.curWeight)
i += size - 1
}
}
}
return lastFound
}
func (g *Window) drainSequence() {
if len(g.seqBuffer) > 0 {
g.parseSequences(string(g.seqBuffer), true)
g.seqBuffer = g.seqBuffer[:0]
}
}
// RecalculateBackgrounds syncs the background colors to the background pixels.
func (g *Window) RecalculateBackgrounds() {
for i := 0; i < g.cellsWidth; i++ {
@ -333,6 +452,8 @@ func (g *Window) PrintChar(r rune, fg, bg color.Color, weight FontWeight) {
// Move the cursor.
g.cursorX++
g.InvalidateBuffer()
}
func (g *Window) Update() error {
@ -352,8 +473,7 @@ func (g *Window) Update() error {
g.Lock()
{
line := string(buf[:n])
g.parseSequences(line, true)
g.seqBuffer = append(g.seqBuffer, buf[:n]...)
}
g.Unlock()
}
@ -415,6 +535,8 @@ func (g *Window) Update() error {
// Keyboard.
g.inputAdapter.HandleKeyPress()
g.onUpdate()
return nil
}
@ -422,38 +544,66 @@ func (g *Window) Draw(screen *ebiten.Image) {
g.Lock()
defer g.Unlock()
g.onPreDraw(screen)
// We process the sequence buffer here so that we don't get flickering
g.drainSequence()
screen.Fill(g.defaultBg)
bufferImage := ebiten.NewImage(g.cellsWidth*g.cellWidth, g.cellsHeight*g.cellHeight)
// Get current buffer
bufferImage := g.lastBuffer
// Draw background
bufferImage.WritePixels(g.bgColors.Pix)
// Only draw the buffer if it's invalid
if g.invalidateBuffer {
// Draw background
bufferImage.WritePixels(g.bgColors.Pix)
// Draw text
for y := 0; y < g.cellsHeight; y++ {
for x := 0; x < g.cellsWidth; x++ {
if g.grid[y][x].Char == ' ' {
continue
}
// Draw text
for y := 0; y < g.cellsHeight; y++ {
for x := 0; x < g.cellsWidth; x++ {
if g.grid[y][x].Char == ' ' {
continue
}
switch g.grid[y][x].Weight {
case FontWeightNormal:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Normal, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
case FontWeightBold:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Bold, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
case FontWeightItalic:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Italic, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
switch g.grid[y][x].Weight {
case FontWeightNormal:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Normal, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
case FontWeightBold:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Bold, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
case FontWeightItalic:
text.Draw(bufferImage, string(g.grid[y][x].Char), g.fonts.Italic, x*g.cellWidth, y*g.cellHeight+g.cellOffsetY, g.grid[y][x].Fg)
}
}
}
// Draw cursor
if g.showCursor {
text.Draw(bufferImage, g.cursorChar, g.fonts.Normal, g.cursorX*g.cellWidth, g.cursorY*g.cellHeight+g.cellOffsetY, g.cursorColor)
}
g.lastBuffer = bufferImage
g.invalidateBuffer = false
}
g.tick += 1 / 60.0
// Draw shader
if g.shader != nil {
if g.shaderBuffer == nil {
g.shaderBuffer = ebiten.NewImageFromImage(bufferImage)
} else {
bounds := g.shaderBuffer.Bounds()
if len(g.shaderByteBuffer) < 4*bounds.Dx()*bounds.Dy() {
g.shaderByteBuffer = make([]byte, 4*bounds.Dx()*bounds.Dy())
}
bufferImage.ReadPixels(g.shaderByteBuffer)
g.shaderBuffer.WritePixels(g.shaderByteBuffer)
}
for i := range g.shader {
_ = g.shader[i].Apply(screen, bufferImage)
_ = g.shader[i].Apply(screen, g.shaderBuffer)
if len(g.shader) > 0 {
bufferImage.DrawImage(screen, nil)
g.shaderBuffer.DrawImage(screen, nil)
}
}
} else {
@ -463,17 +613,18 @@ func (g *Window) Draw(screen *ebiten.Image) {
if g.showTps {
ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.CurrentTPS()))
}
g.onPostDraw(screen)
}
func (g *Window) Layout(outsideWidth, outsideHeight int) (int, int) {
return g.cellsWidth * g.cellWidth, g.cellsHeight * g.cellHeight
s := DeviceScale()
return int(float64(outsideWidth) * s), int(float64(outsideHeight) * s)
}
func (g *Window) Run(title string) error {
sw, sh := g.Layout(0, 0)
ebiten.SetScreenFilterEnabled(false)
ebiten.SetWindowSize(sw, sh)
ebiten.SetWindowSize(int(float64(g.cellsWidth*g.cellWidth)/DeviceScale()), int(float64(g.cellsHeight*g.cellHeight)/DeviceScale()))
ebiten.SetWindowTitle(title)
if err := ebiten.RunGame(g); err != nil {
return err
@ -482,6 +633,20 @@ func (g *Window) Run(title string) error {
return nil
}
func (g *Window) RunWithOptions(options ...WindowOption) error {
ebiten.SetWindowSize(int(float64(g.cellsWidth*g.cellWidth)/DeviceScale()), int(float64(g.cellsHeight*g.cellHeight)/DeviceScale()))
for _, opt := range options {
opt(g)
}
if err := ebiten.RunGame(g); err != nil {
return err
}
return nil
}
func (g *Window) Kill() {
SysKill()
}

34
csi.go
View File

@ -4,8 +4,12 @@ import (
"github.com/muesli/termenv"
"strconv"
"strings"
"sync"
)
var csiMtx = &sync.Mutex{}
var csiCache = map[string]any{}
type CursorUpSeq struct {
Count int
}
@ -72,6 +76,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 +113,32 @@ func parseCSI(s string) (any, bool) {
return nil, false
}
csiMtx.Lock()
if cached, ok := csiCache[s]; ok {
csiMtx.Unlock()
return cached, true
}
csiMtx.Unlock()
if val, ok := parseCSIStruct(s); ok {
csiMtx.Lock()
csiCache[s] = val
csiMtx.Unlock()
return val, true
}
return nil, false
}
func parseCSIStruct(s string) (any, bool) {
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 {

View File

@ -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)

26
dpi.go Normal file
View File

@ -0,0 +1,26 @@
package crt
import (
"github.com/hajimehoshi/ebiten/v2"
"os"
"strconv"
)
// DeviceScale returns the current device scale factor.
//
// If the environment variable CRT_DEVICE_SCALE is set, it will be used instead.
func DeviceScale() float64 {
if os.Getenv("CRT_DEVICE_SCALE") != "" {
s, err := strconv.ParseFloat(os.Getenv("CRT_DEVICE_SCALE"), 64)
if err == nil {
return s
}
}
return ebiten.DeviceScaleFactor()
}
// GetFontDPI returns the recommended font DPI for the current device.
func GetFontDPI() float64 {
return 72.0 * DeviceScale()
}

View File

@ -0,0 +1,97 @@
package main
import (
"flag"
"fmt"
"github.com/BigJk/crt"
bubbleadapter "github.com/BigJk/crt/bubbletea"
"github.com/BigJk/crt/shader"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/hajimehoshi/ebiten/v2"
"image/color"
"math/rand"
"net/http"
_ "net/http/pprof"
"time"
)
const (
Width = 1000
Height = 600
)
type model struct {
X, Y int
}
func (m *model) Init() tea.Cmd {
return nil
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *model) View() string {
return lipgloss.NewStyle().Margin(m.X, 0, 0, m.Y).Padding(5).Border(lipgloss.ThickBorder(), true).Background(lipgloss.Color("#fc2022")).Foreground(lipgloss.Color("#ff00ff")).Render("Hello World!")
}
func main() {
go func() {
fmt.Println(http.ListenAndServe("localhost:6060", nil))
}()
rand.Seed(0)
enableShader := flag.Bool("shader", false, "Enable shader")
flag.Parse()
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", crt.GetFontDPI(), 9.0)
if err != nil {
panic(err)
}
mod := &model{}
win, prog, err := bubbleadapter.Window(Width, Height, fonts, mod, color.Black, tea.WithAltScreen())
if err != nil {
panic(err)
}
go func() {
for {
mod.X = rand.Intn(win.GetCellsWidth())
mod.Y = rand.Intn(win.GetCellsHeight())
prog.Send(time.Now())
time.Sleep(time.Second)
}
}()
var lastStart int64
win.SetOnPreDraw(func(screen *ebiten.Image) {
lastStart = time.Now().UnixMicro()
})
win.SetOnPostDraw(func(screen *ebiten.Image) {
elapsed := time.Now().UnixMicro() - lastStart
if (1000 / (float64(elapsed) * 0.001)) > 500 {
return
}
fmt.Printf("Frame took %d micro seconds FPS=%.2f\n", elapsed, 1000/(float64(elapsed)*0.001))
})
if *enableShader {
lotte, err := shader.NewCrtLotte()
if err != nil {
panic(err)
}
win.SetShader(lotte)
}
win.ShowTPS(true)
if err := win.Run("Simple"); err != nil {
panic(err)
}
}

113
examples/glamour/main.go Normal file
View File

@ -0,0 +1,113 @@
package main
import (
"github.com/BigJk/crt"
bubbleadapter "github.com/BigJk/crt/bubbletea"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
"image/color"
"os"
"strings"
)
const (
Width = 700
Height = 900
)
var (
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render
readme = ""
)
type example struct {
viewport viewport.Model
}
func newExample() *example {
vp := viewport.New(20, 20)
vp.Style = lipgloss.NewStyle().
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("62")).
PaddingRight(2)
return &example{
viewport: vp,
}
}
func (e example) Init() tea.Cmd {
return nil
}
func (e example) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
return e, tea.Quit
default:
var cmd tea.Cmd
e.viewport, cmd = e.viewport.Update(msg)
return e, cmd
}
case tea.WindowSizeMsg:
e.viewport.Width = msg.Width
e.viewport.Height = msg.Height - 3
renderer, err := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithWordWrap(msg.Width),
)
if err != nil {
panic(err)
}
str, err := renderer.Render(readme)
if err != nil {
panic(err)
}
e.viewport.SetContent(str)
}
return e, nil
}
func (e example) View() string {
return e.viewport.View() + e.helpView()
}
func (e example) helpView() string {
return helpStyle("\n ↑/↓: Navigate • q: Quit\n")
}
func main() {
// Read the readme from repo
f, err := os.ReadFile("./README.md")
if err != nil {
panic(err)
}
readme = string(f)
readme = strings.Replace(readme, "\t", " ", -1)
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", crt.GetFontDPI(), 12.0)
if err != nil {
panic(err)
}
win, _, err := bubbleadapter.Window(Width, Height, fonts, newExample(), color.RGBA{
R: 30,
G: 30,
B: 30,
A: 255,
}, tea.WithAltScreen())
if err != nil {
panic(err)
}
if err := win.Run("Glamour Markdown"); err != nil {
panic(err)
}
}

View File

@ -38,12 +38,12 @@ func (m model) View() string {
}
func main() {
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 16.0)
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", crt.GetFontDPI(), 16.0)
if err != nil {
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)
}

View File

@ -78,9 +78,9 @@ type model struct {
}
var (
currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#e7c6ff"))
currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211"))
doneStyle = lipgloss.NewStyle().Margin(1, 2)
checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("#8ac926")).SetString("✓")
checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓")
)
func newModel() model {
@ -187,12 +187,12 @@ func max(a, b int) int {
func main() {
rand.Seed(time.Now().Unix())
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 16.0)
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", crt.GetFontDPI(), 16.0)
if err != nil {
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)
}

View File

@ -188,12 +188,12 @@ func max(a, b int) int {
func main() {
rand.Seed(time.Now().Unix())
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 16.0)
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", crt.GetFontDPI(), 16.0)
if err != nil {
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)
}

View File

@ -29,16 +29,19 @@ func (m model) View() string {
}
func main() {
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 16.0)
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", crt.GetFontDPI(), 16.0)
if err != nil {
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)
}

View File

@ -0,0 +1,218 @@
package main
import (
"github.com/BigJk/crt"
bubbleadapter "github.com/BigJk/crt/bubbletea"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"image/color"
)
const (
Width = 1000
Height = 600
)
const (
initialInputs = 2
maxInputs = 6
minInputs = 1
helpHeight = 5
)
var (
cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
cursorLineStyle = lipgloss.NewStyle().
Background(lipgloss.Color("57")).
Foreground(lipgloss.Color("230"))
placeholderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("238"))
endOfBufferStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("235"))
focusedPlaceholderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("99"))
focusedBorderStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("238"))
blurredBorderStyle = lipgloss.NewStyle().
Border(lipgloss.HiddenBorder())
)
type keymap = struct {
next, prev, add, remove, quit key.Binding
}
func newTextarea() textarea.Model {
t := textarea.New()
t.Prompt = ""
t.Placeholder = "Type something"
t.ShowLineNumbers = true
t.Cursor.Style = cursorStyle
t.FocusedStyle.Placeholder = focusedPlaceholderStyle
t.BlurredStyle.Placeholder = placeholderStyle
t.FocusedStyle.CursorLine = cursorLineStyle
t.FocusedStyle.Base = focusedBorderStyle
t.BlurredStyle.Base = blurredBorderStyle
t.FocusedStyle.EndOfBuffer = endOfBufferStyle
t.BlurredStyle.EndOfBuffer = endOfBufferStyle
t.KeyMap.DeleteWordBackward.SetEnabled(false)
t.KeyMap.LineNext = key.NewBinding(key.WithKeys("down"))
t.KeyMap.LinePrevious = key.NewBinding(key.WithKeys("up"))
t.Blur()
return t
}
type model struct {
width int
height int
keymap keymap
help help.Model
inputs []textarea.Model
focus int
}
func newModel() model {
m := model{
inputs: make([]textarea.Model, initialInputs),
help: help.New(),
keymap: keymap{
next: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "next"),
),
prev: key.NewBinding(
key.WithKeys("shift+tab"),
key.WithHelp("shift+tab", "prev"),
),
add: key.NewBinding(
key.WithKeys("ctrl+n"),
key.WithHelp("ctrl+n", "add an editor"),
),
remove: key.NewBinding(
key.WithKeys("ctrl+w"),
key.WithHelp("ctrl+w", "remove an editor"),
),
quit: key.NewBinding(
key.WithKeys("esc", "ctrl+c"),
key.WithHelp("esc", "quit"),
),
},
}
for i := 0; i < initialInputs; i++ {
m.inputs[i] = newTextarea()
}
m.inputs[m.focus].Focus()
m.updateKeybindings()
return m
}
func (m model) Init() tea.Cmd {
return textarea.Blink
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keymap.quit):
for i := range m.inputs {
m.inputs[i].Blur()
}
return m, tea.Quit
case key.Matches(msg, m.keymap.next):
m.inputs[m.focus].Blur()
m.focus++
if m.focus > len(m.inputs)-1 {
m.focus = 0
}
cmd := m.inputs[m.focus].Focus()
cmds = append(cmds, cmd)
case key.Matches(msg, m.keymap.prev):
m.inputs[m.focus].Blur()
m.focus--
if m.focus < 0 {
m.focus = len(m.inputs) - 1
}
cmd := m.inputs[m.focus].Focus()
cmds = append(cmds, cmd)
case key.Matches(msg, m.keymap.add):
m.inputs = append(m.inputs, newTextarea())
case key.Matches(msg, m.keymap.remove):
m.inputs = m.inputs[:len(m.inputs)-1]
if m.focus > len(m.inputs)-1 {
m.focus = len(m.inputs) - 1
}
}
case tea.WindowSizeMsg:
m.height = msg.Height
m.width = msg.Width
}
m.updateKeybindings()
m.sizeInputs()
// Update all textareas
for i := range m.inputs {
newModel, cmd := m.inputs[i].Update(msg)
m.inputs[i] = newModel
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
}
func (m *model) sizeInputs() {
for i := range m.inputs {
m.inputs[i].SetWidth(m.width / len(m.inputs))
m.inputs[i].SetHeight(m.height - helpHeight)
}
}
func (m *model) updateKeybindings() {
m.keymap.add.SetEnabled(len(m.inputs) < maxInputs)
m.keymap.remove.SetEnabled(len(m.inputs) > minInputs)
}
func (m model) View() string {
help := m.help.ShortHelpView([]key.Binding{
m.keymap.next,
m.keymap.prev,
m.keymap.add,
m.keymap.remove,
m.keymap.quit,
})
var views []string
for i := range m.inputs {
views = append(views, m.inputs[i].View())
}
return lipgloss.JoinHorizontal(lipgloss.Top, views...) + "\n\n" + help
}
func main() {
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", crt.GetFontDPI(), 12.0)
if err != nil {
panic(err)
}
win, _, err := bubbleadapter.Window(Width, Height, fonts, newModel(), color.Black, tea.WithAltScreen())
if err != nil {
panic(err)
}
if err := win.Run("Split Editor"); err != nil {
panic(err)
}
}

38
go.mod
View File

@ -3,37 +3,47 @@ module github.com/BigJk/crt
go 1.20
require (
github.com/charmbracelet/bubbletea v0.24.0
github.com/hajimehoshi/ebiten/v2 v2.5.4
github.com/charmbracelet/bubbles v0.15.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/glamour v0.6.0
github.com/charmbracelet/lipgloss v0.7.1
github.com/hajimehoshi/ebiten/v2 v2.6.3
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
github.com/muesli/termenv v0.15.1
golang.org/x/image v0.7.0
github.com/muesli/termenv v0.15.2
github.com/stretchr/testify v1.8.2
golang.org/x/image v0.12.0
)
require (
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v0.15.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/lipgloss v0.7.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.3.0 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/ebitengine/purego v0.5.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/jezek/xgb v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/microcosm-cc/bluemonday v1.0.21 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/stretchr/testify v1.8.2 // indirect
github.com/yuin/goldmark v1.5.2 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 // indirect
golang.org/x/net v0.6.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

63
go.sum
View File

@ -1,13 +1,20 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI=
github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
github.com/charmbracelet/bubbletea v0.24.0 h1:l8PHrft/GIeikDPCUhQe53AJrDD8xGSn0Agirh8xbe8=
github.com/charmbracelet/bubbletea v0.24.0/go.mod h1:rK3g/2+T8vOSEkNHvtq40umJpeVYDn6bLaqbgzhL/hg=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
@ -19,12 +26,15 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:Yyn
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.3.0 h1:BDv9pD98k6AuGNQf3IF41dDppGBOe0F4AofvhFtBXF4=
github.com/ebitengine/purego v0.3.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/hajimehoshi/ebiten/v2 v2.5.4 h1:NvUU6LvVc6oc+u+rD9KfHMjruRdpNwbpalVUINNXufU=
github.com/hajimehoshi/ebiten/v2 v2.5.4/go.mod h1:mnHSOVysTr/nUZrN1lBTRqhK4NG+T9NR3JsJP2rCppk=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/ebitengine/purego v0.5.0 h1:JrMGKfRIAM4/QVKaesIIT7m/UVjTj5GYhRSQYwfVdpo=
github.com/ebitengine/purego v0.5.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4=
github.com/hajimehoshi/ebiten/v2 v2.6.3 h1:xJ5klESxhflZbPUx3GdIPoITzgPgamsyv8aZCVguXGI=
github.com/hajimehoshi/ebiten/v2 v2.6.3/go.mod h1:TZtorL713an00UW4LyvMeKD8uXWnuIuCPtlH11b0pgI=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
@ -36,11 +46,14 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
@ -51,8 +64,10 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@ -62,22 +77,28 @@ github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4=
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 h1:Q6NT8ckDYNcwmi/bmxe+XbiDMXqMRW1xFBtJ+bIpie4=
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57/go.mod h1:wEyOn6VvNW7tcf+bW/wBz1sehi2s2BZ4TimyR7qZen4=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@ -86,11 +107,14 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -100,11 +124,13 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -114,14 +140,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -1,20 +1,9 @@
package crt
import (
"syscall"
"os"
)
func SysKill() {
d, err := syscall.LoadDLL("kernel32.dll")
if err != nil {
return
}
p, err := d.FindProc("GenerateConsoleCtrlEvent")
if err != nil {
return
}
r, _, err := p.Call(syscall.CTRL_BREAK_EVENT, uintptr(syscall.Getpid()))
if r == 0 {
return
}
os.Exit(1)
}

46
sgr.go
View File

@ -4,8 +4,12 @@ import (
"fmt"
"github.com/muesli/termenv"
"strings"
"sync"
)
var sgrMtx = &sync.Mutex{}
var sgrCache = map[string][]any{}
// extractSGR extracts an SGR ansi sequence from the beginning of the string.
func extractSGR(s string) (string, bool) {
if len(s) < 2 {
@ -17,6 +21,10 @@ func extractSGR(s string) (string, bool) {
}
for i := 2; i < len(s); i++ {
if s[i] == ' ' || s[i] == termenv.CSI[0] {
return "", false
}
if s[i] == 'm' {
return s[:i+1], true
}
@ -43,6 +51,14 @@ type SGRBgTrueColor struct {
R, G, B byte
}
type SGRFgColor struct {
Id int
}
type SGRBgColor struct {
Id int
}
// parseSGR parses a single SGR ansi sequence and returns a struct representing the sequence.
func parseSGR(s string) ([]any, bool) {
if !strings.HasPrefix(s, termenv.CSI) {
@ -54,6 +70,15 @@ func parseSGR(s string) ([]any, bool) {
return nil, false
}
sgrMtx.Lock()
if cached, ok := sgrCache[s]; ok {
sgrMtx.Unlock()
return cached, true
}
sgrMtx.Unlock()
full := s
if !strings.HasSuffix(s, "m") {
return nil, false
}
@ -83,7 +108,6 @@ func parseSGR(s string) ([]any, bool) {
case "23":
res = append(res, SGRUnsetItalic{})
default:
// TODO: Only true color is supported for now.
if strings.HasPrefix(s, "38;2;") {
var r, g, b byte
_, err := fmt.Sscanf(s, "38;2;%d;%d;%d", &r, &g, &b)
@ -100,6 +124,22 @@ func parseSGR(s string) ([]any, bool) {
res = append(res, SGRBgTrueColor{r, g, b})
continue
}
} else if strings.HasPrefix(s, "38;5;") {
var id int
_, err := fmt.Sscanf(s, "38;5;%d", &id)
if err == nil {
skips = 2
res = append(res, SGRFgColor{id})
continue
}
} else if strings.HasPrefix(s, "48;5;") {
var id int
_, err := fmt.Sscanf(s, "48;5;%d", &id)
if err == nil {
skips = 2
res = append(res, SGRBgColor{id})
continue
}
}
}
}
@ -111,5 +151,9 @@ func parseSGR(s string) ([]any, bool) {
s = s[len(code)+1:]
}
sgrMtx.Lock()
sgrCache[full] = res
sgrMtx.Unlock()
return res, len(res) > 0
}

View File

@ -1,36 +1,36 @@
package crt
import (
"bytes"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/stretchr/testify/assert"
"testing"
"bytes"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSGR(t *testing.T) {
buf := &bytes.Buffer{}
lip := lipgloss.NewRenderer(buf, termenv.WithProfile(termenv.TrueColor))
testString := lip.NewStyle().Bold(true).Foreground(lipgloss.Color("#ff00ff")).Render("Hello World") + "asdasdasdasdasd" + lip.NewStyle().Italic(true).Background(lipgloss.Color("#ff00ff")).Render("Hello World")
buf := &bytes.Buffer{}
lip := lipgloss.NewRenderer(buf, termenv.WithProfile(termenv.TrueColor))
testString := lip.NewStyle().Bold(true).Foreground(lipgloss.Color("#ff00ff")).Render("Hello World") + "asdasdasdasdasd" + lip.NewStyle().Italic(true).Background(lipgloss.Color("#ff00ff")).Render("Hello World")
var sequences []any
for i := 0; i < len(testString); i++ {
sgr, ok := extractSGR(testString[i:])
if ok {
i += len(sgr) - 1
var sequences []any
for i := 0; i < len(testString); i++ {
sgr, ok := extractSGR(testString[i:])
if ok {
i += len(sgr) - 1
if res, ok := parseSGR(sgr); ok {
sequences = append(sequences, res...)
}
}
}
if res, ok := parseSGR(sgr); ok {
sequences = append(sequences, res...)
}
}
}
assert.Equal(t, []any{
SGRBold{},
SGRFgTrueColor{R: 255, G: 0, B: 255},
SGRReset{},
SGRItalic{},
SGRBgTrueColor{R: 255, G: 0, B: 255},
SGRReset{},
}, sequences)
assert.Equal(t, []any{
SGRBold{},
SGRFgTrueColor{R: 255, G: 0, B: 255},
SGRReset{},
SGRItalic{},
SGRBgTrueColor{R: 255, G: 0, B: 255},
SGRReset{},
}, sequences)
}