Compare commits

..

No commits in common. "main" and "v0.0.6" have entirely different histories.
main ... v0.0.6

19 changed files with 241 additions and 1055 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](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.
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.
## 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", crt.GetFontDPI(), 16.0)
fonts, err := crt.LoadFaces("./fonts/SomeFont-Regular.ttf", "./fonts/SomeFont-Bold.ttf", "./fonts/SomeFont-Italic.ttf", 72.0, 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,16 +48,13 @@ 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")``)~~ **Now supported.**
- Only supports TrueColor at the moment (no 256 color support) so you need to use TrueColor colors in lipgloss (e.g. ``lipgloss.Color("#ff0000")``)
- 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,88 +5,50 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"unicode"
"strings"
)
type teaKey struct {
key tea.KeyType
rune []rune
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,
}
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 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: {']'},
}
var ebitenToTeaMouse = map[ebiten.MouseButton]tea.MouseEventType{
@ -95,16 +57,6 @@ 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)
@ -134,12 +86,11 @@ 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,
Action: tea.MouseActionMotion,
X: motion.X,
Y: motion.Y,
Alt: false,
Ctrl: false,
Type: tea.MouseMotion,
})
}
@ -149,22 +100,13 @@ func (b *Adapter) HandleMouseButton(button crt.MouseButton) {
return
}
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)
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],
})
}
func (b *Adapter) HandleMouseWheel(wheel crt.MouseWheel) {
@ -188,64 +130,24 @@ func (b *Adapter) HandleMouseWheel(wheel crt.MouseWheel) {
}
func (b *Adapter) HandleKeyPress() {
newInputs := ebiten.AppendInputChars([]rune{})
for _, v := range newInputs {
switch v {
case ' ':
b.prog.Send(tea.KeyMsg{
Type: tea.KeySpace,
Runes: []rune{v},
Alt: ebiten.IsKeyPressed(ebiten.KeyAlt),
})
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,
})
}
keys = inpututil.AppendJustReleasedKeys(keys)
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])
}
}
runes := []rune(strings.ToLower(k.String()))
b.prog.Send(tea.KeyMsg{
Type: val.key,
Type: val,
Runes: runes,
Alt: ebiten.IsKeyPressed(ebiten.KeyAlt),
})
} else {
runes := []rune(strings.ToLower(k.String()))
if val, ok := ebitenToTeaRunes[k]; ok {
runes = val
}
b.prog.Send(tea.KeyMsg{
Type: tea.KeyRunes,
Runes: runes,
Alt: ebiten.IsKeyPressed(ebiten.KeyAlt),
})

View File

@ -13,9 +13,25 @@ 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, *tea.Program, error) {
func Window(width int, height int, fonts crt.Fonts, model tea.Model, defaultBg color.Color, options ...tea.ProgramOption) (*crt.Window, error) {
gameInput := crt.NewConcurrentRW()
gameOutput := crt.NewConcurrentRW()
@ -27,7 +43,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(gameOutput),
tea.WithOutput(termenv.NewOutput(gameOutput, termenv.WithEnvironment(fakeEnviron{}), termenv.WithTTY(true), termenv.WithProfile(termenv.TrueColor), termenv.WithColorCache(true))),
tea.WithANSICompressor(),
}, options...)...,
)
@ -40,6 +56,5 @@ func Window(width int, height int, fonts crt.Fonts, model tea.Model, defaultBg c
crt.SysKill()
}()
win, err := crt.NewGame(width, height, fonts, gameOutput, NewAdapter(prog), defaultBg)
return win, prog, err
return crt.NewGame(width, height, fonts, gameOutput, NewAdapter(prog), defaultBg)
}

303
crt.go
View File

@ -7,19 +7,13 @@ 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
@ -36,38 +30,24 @@ type Window struct {
tty io.Reader
// Terminal cursor and color states.
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)
cursorX int
cursorY int
mouseCellX int
mouseCellY int
defaultBg color.Color
curFg color.Color
curBg color.Color
curWeight FontWeight
// Other
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
showTps bool
fonts Fonts
bgColors *image.RGBA
shader []shader.Shader
routine sync.Once
tick float64
}
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 {
@ -77,12 +57,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.Ceil()
cellHeight := size.Y.Ceil()
cellOffsetY := -bounds.Min.Y.Ceil()
cellWidth := size.X.Round()
cellHeight := size.Y.Round()
cellOffsetY := -bounds.Min.Y.Round()
cellsWidth := int(float64(width)*DeviceScale()) / cellWidth
cellsHeight := int(float64(height)*DeviceScale()) / cellHeight
cellsWidth := width / cellWidth
cellsHeight := height / cellHeight
grid := make([][]GridCell, cellsHeight)
for y := 0; y < cellsHeight; y++ {
@ -98,25 +78,17 @@ 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)),
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),
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)),
}
game.inputAdapter.HandleWindowSize(WindowSize{
@ -130,54 +102,16 @@ 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
@ -192,29 +126,6 @@ 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) {
@ -286,25 +197,24 @@ 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.SetBg(g.cursorX+i, g.cursorY, g.defaultBg)
g.grid[g.cursorY][g.cursorX+i].Bg = g.defaultBg
g.SetBgPixels(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.SetBg(i, g.cursorY, g.defaultBg)
g.grid[g.cursorY][i].Bg = g.defaultBg
g.SetBgPixels(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.SetBg(i, g.cursorY, g.defaultBg)
g.grid[g.cursorY][i].Bg = g.defaultBg
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:
@ -335,69 +245,40 @@ func (g *Window) handleSGR(sgr any) {
case SGRUnsetItalic:
g.curWeight = FontWeightNormal
case SGRFgTrueColor:
g.curFg = color.RGBA{R: seq.R, G: seq.G, B: seq.B, A: 255}
g.curFg = color.RGBA{seq.R, seq.G, seq.B, 255}
case SGRBgTrueColor:
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
}
}
g.curBg = color.RGBA{seq.R, seq.G, seq.B, 255}
}
}
func (g *Window) parseSequences(str string, printExtra bool) int {
runes := []rune(str)
lastFound := 0
for i := 0; i < len(str); i++ {
if sgr, ok := extractSGR(str[i:]); ok {
for i := 0; i < len(runes); i++ {
if sgr, ok := extractSGR(string(runes[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(str[i:]); ok {
} else if csi, ok := extractCSI(string(runes[i:])); ok {
i += len(csi) - 1
if csi, ok := parseCSI(csi); ok {
lastFound = i
g.handleCSI(csi)
g.InvalidateBuffer()
}
} else if printExtra {
if r, size := utf8.DecodeRuneInString(str[i:]); r != utf8.RuneError {
g.PrintChar(r, g.curFg, g.curBg, g.curWeight)
i += size - 1
}
g.PrintChar(runes[i], g.curFg, g.curBg, g.curWeight)
}
}
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++ {
@ -452,8 +333,6 @@ func (g *Window) PrintChar(r rune, fg, bg color.Color, weight FontWeight) {
// Move the cursor.
g.cursorX++
g.InvalidateBuffer()
}
func (g *Window) Update() error {
@ -473,7 +352,8 @@ func (g *Window) Update() error {
g.Lock()
{
g.seqBuffer = append(g.seqBuffer, buf[:n]...)
line := string(buf[:n])
g.parseSequences(line, true)
}
g.Unlock()
}
@ -535,8 +415,6 @@ func (g *Window) Update() error {
// Keyboard.
g.inputAdapter.HandleKeyPress()
g.onUpdate()
return nil
}
@ -544,66 +422,38 @@ 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)
// Get current buffer
bufferImage := g.lastBuffer
bufferImage := ebiten.NewImage(g.cellsWidth*g.cellWidth, g.cellsHeight*g.cellHeight)
// Only draw the buffer if it's invalid
if g.invalidateBuffer {
// Draw background
bufferImage.WritePixels(g.bgColors.Pix)
// 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
}
// Draw shader
g.tick += 1 / 60.0
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, g.shaderBuffer)
_ = g.shader[i].Apply(screen, bufferImage)
if len(g.shader) > 0 {
g.shaderBuffer.DrawImage(screen, nil)
bufferImage.DrawImage(screen, nil)
}
}
} else {
@ -613,18 +463,17 @@ 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) {
s := DeviceScale()
return int(float64(outsideWidth) * s), int(float64(outsideHeight) * s)
return g.cellsWidth * g.cellWidth, g.cellsHeight * g.cellHeight
}
func (g *Window) Run(title string) error {
sw, sh := g.Layout(0, 0)
ebiten.SetScreenFilterEnabled(false)
ebiten.SetWindowSize(int(float64(g.cellsWidth*g.cellWidth)/DeviceScale()), int(float64(g.cellsHeight*g.cellHeight)/DeviceScale()))
ebiten.SetWindowSize(sw, sh)
ebiten.SetWindowTitle(title)
if err := ebiten.RunGame(g); err != nil {
return err
@ -633,20 +482,6 @@ 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,12 +4,8 @@ import (
"github.com/muesli/termenv"
"strconv"
"strings"
"sync"
)
var csiMtx = &sync.Mutex{}
var csiCache = map[string]any{}
type CursorUpSeq struct {
Count int
}
@ -76,10 +72,6 @@ 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.
@ -113,32 +105,6 @@ 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,7 +15,6 @@ 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)
@ -35,7 +34,6 @@ 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
View File

@ -1,26 +0,0 @@
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

@ -1,97 +0,0 @@
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)
}
}

View File

@ -1,113 +0,0 @@
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", crt.GetFontDPI(), 16.0)
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 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("211"))
currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#e7c6ff"))
doneStyle = lipgloss.NewStyle().Margin(1, 2)
checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓")
checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("#8ac926")).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", crt.GetFontDPI(), 16.0)
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 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", crt.GetFontDPI(), 16.0)
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 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,19 +29,16 @@ func (m model) View() string {
}
func main() {
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", crt.GetFontDPI(), 16.0)
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 16.0)
if err != nil {
panic(err)
}
win, prog, err := bubbleadapter.Window(Width, Height, fonts, model{}, color.Black, tea.WithAltScreen())
win, 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

@ -1,218 +0,0 @@
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,47 +3,37 @@ module github.com/BigJk/crt
go 1.20
require (
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/charmbracelet/bubbletea v0.24.0
github.com/hajimehoshi/ebiten/v2 v2.5.4
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
github.com/muesli/termenv v0.15.2
github.com/stretchr/testify v1.8.2
golang.org/x/image v0.12.0
github.com/muesli/termenv v0.15.1
golang.org/x/image v0.7.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/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/bubbles v0.15.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/dlclark/regexp2 v1.4.0 // indirect
github.com/ebitengine/purego v0.5.0 // indirect
github.com/gorilla/css v1.0.0 // 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/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/yuin/goldmark v1.5.2 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
github.com/stretchr/testify v1.8.2 // indirect
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // 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/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/term v0.6.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

63
go.sum
View File

@ -1,20 +1,13 @@
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.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/bubbletea v0.24.0 h1:l8PHrft/GIeikDPCUhQe53AJrDD8xGSn0Agirh8xbe8=
github.com/charmbracelet/bubbletea v0.24.0/go.mod h1:rK3g/2+T8vOSEkNHvtq40umJpeVYDn6bLaqbgzhL/hg=
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=
@ -26,15 +19,12 @@ 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/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/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/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=
@ -46,14 +36,11 @@ 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=
@ -64,10 +51,8 @@ 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.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/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
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=
@ -77,28 +62,22 @@ 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.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
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/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
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/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4=
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
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=
@ -107,14 +86,11 @@ 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=
@ -124,13 +100,11 @@ 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=
@ -140,15 +114,14 @@ 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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/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,9 +1,20 @@
package crt
import (
"os"
"syscall"
)
func SysKill() {
os.Exit(1)
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
}
}

46
sgr.go
View File

@ -4,12 +4,8 @@ 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 {
@ -21,10 +17,6 @@ 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
}
@ -51,14 +43,6 @@ 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) {
@ -70,15 +54,6 @@ 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
}
@ -108,6 +83,7 @@ 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)
@ -124,22 +100,6 @@ 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
}
}
}
}
@ -151,9 +111,5 @@ 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...)
}
}
}
assert.Equal(t, []any{
SGRBold{},
SGRFgTrueColor{R: 255, G: 0, B: 255},
SGRReset{},
SGRItalic{},
SGRBgTrueColor{R: 255, G: 0, B: 255},
SGRReset{},
}, sequences)
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)
}