mirror of
https://github.com/BigJk/crt.git
synced 2026-02-06 18:56:53 +00:00
Compare commits
No commits in common. "main" and "v0.0.6" have entirely different histories.
13
README.md
13
README.md
@ -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.
|
||||
@ -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),
|
||||
})
|
||||
|
||||
@ -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
303
crt.go
@ -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
34
csi.go
@ -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 {
|
||||
|
||||
@ -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
26
dpi.go
@ -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()
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
38
go.mod
@ -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
63
go.sum
@ -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=
|
||||
|
||||
@ -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
46
sgr.go
@ -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
|
||||
}
|
||||
|
||||
54
sgr_test.go
54
sgr_test.go
@ -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)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user