Compare commits

...

17 Commits
v0.0.8 ... main

Author SHA1 Message Date
Daniel Schmidt
7710fdc88d
Merge pull request #1 from obvionaoe/main
Add WindowOption and RunWithOptions method to allow for extra configuration
2024-05-09 19:54:27 +02:00
obvionaoe
d976f61673
Remove test functions for WindowOption 2024-05-09 16:15:19 +01:00
obvionaoe
ea37c58ee8
Add WindowOption and RunWithOptions function to allow for extra configuration 2024-05-09 16:01:48 +01:00
Daniel Schmidt
a3526f42c5 feat: update to new bubbletea mouse handling 2024-01-12 21:00:06 +01:00
Daniel Schmidt
fbecabe083 chore: update bubbletea dep 2023-12-14 13:53:27 +01:00
Daniel Schmidt
902582a274 feat: optimize image allocations 2023-12-11 17:55:50 +01:00
Daniel Schmidt
ac7d8bf4c3 Merge branch 'main' of https://github.com/BigJk/crt 2023-12-10 09:21:40 +01:00
Daniel Schmidt
ee5e2843e2 chore: update ebiten to support newer osx version 2023-12-10 08:49:28 +01:00
Daniel S
f2f0473fdf
Update README.md 2023-05-27 14:19:33 +02:00
Daniel Schmidt
5104c9a225 Added glamour example. 2023-05-27 10:19:22 +02:00
Daniel S
b56ac867a2
Update README.md 2023-05-27 10:00:04 +02:00
Daniel Schmidt
48a7adaf9e Refactor. 2023-05-23 23:30:56 +02:00
Daniel Schmidt
6151ddad2f Fix ctr+key input. 2023-05-23 23:29:05 +02:00
Daniel Schmidt
d9c72af4f2 Fix double space. 2023-05-23 23:21:44 +02:00
Daniel Schmidt
e5f8fb908a Better keyboard input handling. 2023-05-23 23:00:46 +02:00
Daniel Schmidt
52a54cbfc8 Fix missing bg setting for ANSI256 colors. 2023-05-23 23:00:37 +02:00
Daniel Schmidt
2c21fde141 Added high dpi support. 2023-05-23 22:36:08 +02:00
13 changed files with 618 additions and 145 deletions

View File

@ -6,7 +6,7 @@
CRT is a library to provide a simple terminal emulator that can be attached to a ``tea.Program``. It uses ``ebitengine`` to render a terminal. It supports TrueColor, Mouse and Keyboard input. It interprets the CSI escape sequences coming from bubbletea and renders them to the terminal.
This started as a simple proof of concept for the game I'm writing with the help of bubbletea, called [End Of Eden](github.com/BigJk/end_of_eden). I wanted to give people who have no clue about the terminal a simple option to play the game without interacting with the terminal directly. It's also possible to apply shaders to the terminal to give it a more retro look which is a nice side effect.
This started as a simple proof of concept for the game I'm writing with the help of bubbletea, called [End Of Eden](https://github.com/BigJk/end_of_eden). I wanted to give people who have no clue about the terminal a simple option to play the game without interacting with the terminal directly. It's also possible to apply shaders to the terminal to give it a more retro look which is a nice side effect.
## Usage
@ -30,7 +30,7 @@ import (
func main() {
// Load fonts for normal, bold and italic text styles.
fonts, err := crt.LoadFaces("./fonts/SomeFont-Regular.ttf", "./fonts/SomeFont-Bold.ttf", "./fonts/SomeFont-Italic.ttf", 72.0, 16.0)
fonts, err := crt.LoadFaces("./fonts/SomeFont-Regular.ttf", "./fonts/SomeFont-Bold.ttf", "./fonts/SomeFont-Italic.ttf", crt.GetFontDPI(), 16.0)
if err != nil {
panic(err)
}
@ -48,6 +48,8 @@ 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.**
@ -58,4 +60,4 @@ func main() {
## 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

@ -13,80 +13,80 @@ type teaKey struct {
rune []rune
}
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{'\t'}},
ebiten.KeySpace: {tea.KeySpace, []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.Key1: {tea.KeyRunes, []rune{'1'}},
ebiten.Key2: {tea.KeyRunes, []rune{'2'}},
ebiten.Key3: {tea.KeyRunes, []rune{'3'}},
ebiten.Key4: {tea.KeyRunes, []rune{'4'}},
ebiten.Key5: {tea.KeyRunes, []rune{'5'}},
ebiten.Key6: {tea.KeyRunes, []rune{'6'}},
ebiten.Key7: {tea.KeyRunes, []rune{'7'}},
ebiten.Key8: {tea.KeyRunes, []rune{'8'}},
ebiten.Key9: {tea.KeyRunes, []rune{'9'}},
ebiten.Key0: {tea.KeyRunes, []rune{'0'}},
ebiten.KeyA: {tea.KeyRunes, []rune{'a'}},
ebiten.KeyB: {tea.KeyRunes, []rune{'b'}},
ebiten.KeyC: {tea.KeyRunes, []rune{'c'}},
ebiten.KeyD: {tea.KeyRunes, []rune{'d'}},
ebiten.KeyE: {tea.KeyRunes, []rune{'e'}},
ebiten.KeyF: {tea.KeyRunes, []rune{'f'}},
ebiten.KeyG: {tea.KeyRunes, []rune{'g'}},
ebiten.KeyH: {tea.KeyRunes, []rune{'h'}},
ebiten.KeyI: {tea.KeyRunes, []rune{'i'}},
ebiten.KeyJ: {tea.KeyRunes, []rune{'j'}},
ebiten.KeyK: {tea.KeyRunes, []rune{'k'}},
ebiten.KeyL: {tea.KeyRunes, []rune{'l'}},
ebiten.KeyM: {tea.KeyRunes, []rune{'m'}},
ebiten.KeyN: {tea.KeyRunes, []rune{'n'}},
ebiten.KeyO: {tea.KeyRunes, []rune{'o'}},
ebiten.KeyP: {tea.KeyRunes, []rune{'p'}},
ebiten.KeyQ: {tea.KeyRunes, []rune{'q'}},
ebiten.KeyR: {tea.KeyRunes, []rune{'r'}},
ebiten.KeyS: {tea.KeyRunes, []rune{'s'}},
ebiten.KeyT: {tea.KeyRunes, []rune{'t'}},
ebiten.KeyU: {tea.KeyRunes, []rune{'u'}},
ebiten.KeyV: {tea.KeyRunes, []rune{'v'}},
ebiten.KeyW: {tea.KeyRunes, []rune{'w'}},
ebiten.KeyX: {tea.KeyRunes, []rune{'x'}},
ebiten.KeyY: {tea.KeyRunes, []rune{'y'}},
ebiten.KeyZ: {tea.KeyRunes, []rune{'z'}},
ebiten.KeyComma: {tea.KeyRunes, []rune{','}},
ebiten.KeyPeriod: {tea.KeyRunes, []rune{'.'}},
ebiten.KeySlash: {tea.KeyRunes, []rune{'/'}},
ebiten.KeyBackslash: {tea.KeyRunes, []rune{'\\'}},
ebiten.KeySemicolon: {tea.KeyRunes, []rune{';'}},
ebiten.KeyApostrophe: {tea.KeyRunes, []rune{'\''}},
ebiten.KeyGraveAccent: {tea.KeyRunes, []rune{'`'}},
ebiten.KeyEqual: {tea.KeyRunes, []rune{'='}},
ebiten.KeyMinus: {tea.KeyRunes, []rune{'-'}},
ebiten.KeyLeftBracket: {tea.KeyRunes, []rune{'['}},
ebiten.KeyRightBracket: {tea.KeyRunes, []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{}},
ebiten.KeyEnter: {tea.KeyEnter, []rune{'\n'}},
ebiten.KeyTab: {tea.KeyTab, []rune{}},
ebiten.KeyBackspace: {tea.KeyBackspace, []rune{}},
ebiten.KeyDelete: {tea.KeyDelete, []rune{}},
ebiten.KeyHome: {tea.KeyHome, []rune{}},
ebiten.KeyEnd: {tea.KeyEnd, []rune{}},
ebiten.KeyPageUp: {tea.KeyPgUp, []rune{}},
ebiten.KeyArrowUp: {tea.KeyUp, []rune{}},
ebiten.KeyArrowDown: {tea.KeyDown, []rune{}},
ebiten.KeyArrowLeft: {tea.KeyLeft, []rune{}},
ebiten.KeyArrowRight: {tea.KeyRight, []rune{}},
ebiten.KeyEscape: {tea.KeyEscape, []rune{}},
ebiten.KeyF1: {tea.KeyF1, []rune{}},
ebiten.KeyF2: {tea.KeyF2, []rune{}},
ebiten.KeyF3: {tea.KeyF3, []rune{}},
ebiten.KeyF4: {tea.KeyF4, []rune{}},
ebiten.KeyF5: {tea.KeyF5, []rune{}},
ebiten.KeyF6: {tea.KeyF6, []rune{}},
ebiten.KeyF7: {tea.KeyF7, []rune{}},
ebiten.KeyF8: {tea.KeyF8, []rune{}},
ebiten.KeyF9: {tea.KeyF9, []rune{}},
ebiten.KeyF10: {tea.KeyF10, []rune{}},
ebiten.KeyF11: {tea.KeyF11, []rune{}},
ebiten.KeyF12: {tea.KeyF12, []rune{}},
ebiten.KeyShift: {tea.KeyShiftLeft, []rune{}},
}
var ebitenToCtrlKeys = map[ebiten.Key]tea.KeyType{
ebiten.KeyA: tea.KeyCtrlA,
ebiten.KeyB: tea.KeyCtrlB,
ebiten.KeyC: tea.KeyCtrlC,
ebiten.KeyD: tea.KeyCtrlD,
ebiten.KeyE: tea.KeyCtrlE,
ebiten.KeyF: tea.KeyCtrlF,
ebiten.KeyG: tea.KeyCtrlG,
ebiten.KeyH: tea.KeyCtrlH,
ebiten.KeyI: tea.KeyCtrlI,
ebiten.KeyJ: tea.KeyCtrlJ,
ebiten.KeyK: tea.KeyCtrlK,
ebiten.KeyL: tea.KeyCtrlL,
ebiten.KeyM: tea.KeyCtrlM,
ebiten.KeyN: tea.KeyCtrlN,
ebiten.KeyO: tea.KeyCtrlO,
ebiten.KeyP: tea.KeyCtrlP,
ebiten.KeyQ: tea.KeyCtrlQ,
ebiten.KeyR: tea.KeyCtrlR,
ebiten.KeyS: tea.KeyCtrlS,
ebiten.KeyT: tea.KeyCtrlT,
ebiten.KeyU: tea.KeyCtrlU,
ebiten.KeyV: tea.KeyCtrlV,
ebiten.KeyW: tea.KeyCtrlW,
ebiten.KeyX: tea.KeyCtrlX,
ebiten.KeyY: tea.KeyCtrlY,
ebiten.KeyZ: tea.KeyCtrlZ,
ebiten.KeyLeftBracket: tea.KeyCtrlOpenBracket,
ebiten.KeyBackslash: tea.KeyCtrlBackslash,
ebiten.KeyRightBracket: tea.KeyCtrlCloseBracket,
ebiten.KeyApostrophe: tea.KeyCtrlCaret,
}
var ebitenToTeaMouse = map[ebiten.MouseButton]tea.MouseEventType{
@ -95,6 +95,16 @@ var ebitenToTeaMouse = map[ebiten.MouseButton]tea.MouseEventType{
ebiten.MouseButtonRight: tea.MouseRight,
}
var ebitenToTeaMouseNew = map[ebiten.MouseButton]tea.MouseButton{
ebiten.MouseButtonLeft: tea.MouseButtonLeft,
ebiten.MouseButtonMiddle: tea.MouseButtonMiddle,
ebiten.MouseButtonRight: tea.MouseButtonRight,
// TODO: is this right?
ebiten.MouseButton3: tea.MouseButtonBackward,
ebiten.MouseButton4: tea.MouseButtonForward,
}
// Options are used to configure the adapter.
type Options func(*Adapter)
@ -124,11 +134,12 @@ func NewAdapter(prog *tea.Program, options ...Options) *Adapter {
func (b *Adapter) HandleMouseMotion(motion crt.MouseMotion) {
b.prog.Send(tea.MouseMsg{
X: motion.X,
Y: motion.Y,
Alt: false,
Ctrl: false,
Type: tea.MouseMotion,
X: motion.X,
Y: motion.Y,
Alt: false,
Ctrl: false,
Type: tea.MouseMotion,
Action: tea.MouseActionMotion,
})
}
@ -138,13 +149,22 @@ func (b *Adapter) HandleMouseButton(button crt.MouseButton) {
return
}
b.prog.Send(tea.MouseMsg{
X: button.X,
Y: button.Y,
Alt: ebiten.IsKeyPressed(ebiten.KeyAlt),
Ctrl: ebiten.IsKeyPressed(ebiten.KeyControl),
Type: ebitenToTeaMouse[button.Button],
})
msg := tea.MouseMsg{
X: button.X,
Y: button.Y,
Alt: ebiten.IsKeyPressed(ebiten.KeyAlt),
Ctrl: ebiten.IsKeyPressed(ebiten.KeyControl),
Type: ebitenToTeaMouse[button.Button],
Button: ebitenToTeaMouseNew[button.Button],
}
if button.JustReleased {
msg.Action = tea.MouseActionRelease
} else if button.JustPressed {
msg.Action = tea.MouseActionPress
}
b.prog.Send(msg)
}
func (b *Adapter) HandleMouseWheel(wheel crt.MouseWheel) {
@ -168,24 +188,52 @@ 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.AppendJustReleasedKeys(keys)
keys = inpututil.AppendJustPressedKeys(keys)
repeatedBackspace := repeatingKeyPressed(ebiten.KeyBackspace)
if repeatedBackspace {
b.prog.Send(tea.KeyMsg{
Type: tea.KeyBackspace,
Runes: []rune{},
Alt: false,
})
}
for _, k := range keys {
switch k {
case ebiten.KeyC:
if ebiten.IsKeyPressed(ebiten.KeyControl) {
if ebiten.IsKeyPressed(ebiten.KeyControl) {
if tk, ok := ebitenToCtrlKeys[k]; ok {
b.prog.Send(tea.KeyMsg{
Type: tea.KeyCtrlC,
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)

64
crt.go
View File

@ -60,10 +60,14 @@ type Window struct {
bgColors *image.RGBA
shader []shader.Shader
routine sync.Once
shaderByteBuffer []byte
shaderBuffer *ebiten.Image
lastBuffer *ebiten.Image
invalidateBuffer bool
}
type WindowOption func(window *Window)
// NewGame creates a new terminal game with the given dimensions and font faces.
func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAdapter, defaultBg color.Color) (*Window, error) {
if defaultBg == nil {
@ -73,12 +77,12 @@ func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAda
bounds, _, _ := fonts.Normal.GlyphBounds([]rune("█")[0])
size := bounds.Max.Sub(bounds.Min)
cellWidth := size.X.Round()
cellHeight := size.Y.Round()
cellOffsetY := -bounds.Min.Y.Round()
cellWidth := size.X.Ceil()
cellHeight := size.Y.Ceil()
cellOffsetY := -bounds.Min.Y.Ceil()
cellsWidth := width / cellWidth
cellsHeight := height / cellHeight
cellsWidth := int(float64(width)*DeviceScale()) / cellWidth
cellsHeight := int(float64(height)*DeviceScale()) / cellHeight
grid := make([][]GridCell, cellsHeight)
for y := 0; y < cellsHeight; y++ {
@ -105,6 +109,7 @@ func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAda
grid: grid,
tty: tty,
bgColors: image.NewRGBA(image.Rect(0, 0, cellsWidth*cellWidth, cellsHeight*cellHeight)),
lastBuffer: ebiten.NewImage(cellsWidth*cellWidth, cellsHeight*cellHeight),
cursorChar: "█",
cursorColor: color.RGBA{R: 255, G: 255, B: 255, A: 100},
onUpdate: func() {},
@ -342,6 +347,15 @@ func (g *Window) handleSGR(sgr any) {
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
}
}
}
}
@ -541,9 +555,7 @@ func (g *Window) Draw(screen *ebiten.Image) {
bufferImage := g.lastBuffer
// Only draw the buffer if it's invalid
if bufferImage == nil || g.invalidateBuffer {
bufferImage = ebiten.NewImage(g.cellsWidth*g.cellWidth, g.cellsHeight*g.cellHeight)
if g.invalidateBuffer {
// Draw background
bufferImage.WritePixels(g.bgColors.Pix)
@ -576,13 +588,22 @@ func (g *Window) Draw(screen *ebiten.Image) {
// Draw shader
if g.shader != nil {
shaderBuffer := ebiten.NewImageFromImage(bufferImage)
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, shaderBuffer)
_ = g.shader[i].Apply(screen, g.shaderBuffer)
if len(g.shader) > 0 {
shaderBuffer.DrawImage(screen, nil)
g.shaderBuffer.DrawImage(screen, nil)
}
}
} else {
@ -597,14 +618,13 @@ func (g *Window) Draw(screen *ebiten.Image) {
}
func (g *Window) Layout(outsideWidth, outsideHeight int) (int, int) {
return g.cellsWidth * g.cellWidth, g.cellsHeight * g.cellHeight
s := DeviceScale()
return int(float64(outsideWidth) * s), int(float64(outsideHeight) * s)
}
func (g *Window) Run(title string) error {
sw, sh := g.Layout(0, 0)
ebiten.SetScreenFilterEnabled(false)
ebiten.SetWindowSize(sw, sh)
ebiten.SetWindowSize(int(float64(g.cellsWidth*g.cellWidth)/DeviceScale()), int(float64(g.cellsHeight*g.cellHeight)/DeviceScale()))
ebiten.SetWindowTitle(title)
if err := ebiten.RunGame(g); err != nil {
return err
@ -613,6 +633,20 @@ func (g *Window) Run(title string) error {
return nil
}
func (g *Window) RunWithOptions(options ...WindowOption) error {
ebiten.SetWindowSize(int(float64(g.cellsWidth*g.cellWidth)/DeviceScale()), int(float64(g.cellsHeight*g.cellHeight)/DeviceScale()))
for _, opt := range options {
opt(g)
}
if err := ebiten.RunGame(g); err != nil {
return err
}
return nil
}
func (g *Window) Kill() {
SysKill()
}

26
dpi.go Normal file
View File

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

View File

@ -47,7 +47,7 @@ func main() {
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", 72.0, 9.0)
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)
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

29
go.mod
View File

@ -4,37 +4,46 @@ go 1.20
require (
github.com/charmbracelet/bubbles v0.15.0
github.com/charmbracelet/bubbletea v0.24.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.5.4
github.com/hajimehoshi/ebiten/v2 v2.6.3
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
github.com/muesli/termenv v0.15.1
github.com/muesli/termenv v0.15.2
github.com/stretchr/testify v1.8.2
golang.org/x/image v0.7.0
golang.org/x/image v0.12.0
)
require (
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.3.0 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/ebitengine/purego v0.5.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/jezek/xgb v1.1.0 // indirect
github.com/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
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 // indirect
golang.org/x/net v0.6.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

63
go.sum
View File

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