From 9cdb7ae020879d56d9444c1ede7dc50686a8a2a1 Mon Sep 17 00:00:00 2001 From: Liam Galvin Date: Mon, 2 Aug 2021 20:46:59 +0100 Subject: [PATCH] Add support for cursor shapes and images --- .gitignore | 1 + README.md | 26 +- cursor.gif | Bin 0 -> 19304 bytes internal/app/darktile/cmd/root.go | 23 ++ internal/app/darktile/config/config.go | 12 +- internal/app/darktile/config/default.go | 7 +- internal/app/darktile/gui/draw.go | 315 +----------------- internal/app/darktile/gui/gui.go | 26 +- internal/app/darktile/gui/input.go | 6 +- internal/app/darktile/gui/options.go | 20 ++ internal/app/darktile/gui/popup/popup.go | 13 + internal/app/darktile/gui/popups.go | 4 +- .../app/darktile/gui/render/annotation.go | 162 +++++++++ internal/app/darktile/gui/render/content.go | 10 + internal/app/darktile/gui/render/cursor.go | 67 ++++ internal/app/darktile/gui/render/ligatures.go | 45 +++ internal/app/darktile/gui/render/popups.go | 42 +++ internal/app/darktile/gui/render/render.go | 97 ++++++ internal/app/darktile/gui/render/row.go | 94 ++++++ internal/app/darktile/gui/render/selection.go | 35 ++ internal/app/darktile/gui/render/sixels.go | 17 + internal/app/darktile/gui/update.go | 3 +- internal/app/darktile/termutil/buffer.go | 24 ++ internal/app/darktile/termutil/csi.go | 50 ++- internal/app/darktile/termutil/options.go | 4 + internal/app/darktile/termutil/selection.go | 68 ++-- 26 files changed, 784 insertions(+), 387 deletions(-) create mode 100644 cursor.gif create mode 100644 internal/app/darktile/gui/popup/popup.go create mode 100644 internal/app/darktile/gui/render/annotation.go create mode 100644 internal/app/darktile/gui/render/content.go create mode 100644 internal/app/darktile/gui/render/cursor.go create mode 100644 internal/app/darktile/gui/render/ligatures.go create mode 100644 internal/app/darktile/gui/render/popups.go create mode 100644 internal/app/darktile/gui/render/render.go create mode 100644 internal/app/darktile/gui/render/row.go create mode 100644 internal/app/darktile/gui/render/selection.go create mode 100644 internal/app/darktile/gui/render/sixels.go diff --git a/.gitignore b/.gitignore index 5ed0bb89..cf7a9a7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea +.vscode /darktile diff --git a/README.md b/README.md index dc51b8e5..22731065 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,19 @@ Darktile is a GPU rendered terminal emulator designed for tiling window managers - GPU rendering - Unicode support +- Variety of themes available (or build your own!) - Compiled-in powerline font -- Configurable/customisable, supports custom themes, fonts etc. -- Hints: Context-aware overlays e.g. hex colour viewer +- Works with your favourite monospaced TTF/OTF fonts +- Font ligatures (turn it off if you're not a ligature fan) +- Hints: Context-aware overlays e.g. hex colour viewer, octal permission annotation - Take screenshots with a single key-binding -- Sixel support -- Transparency +- Sixels +- Window transparency (0-100%) +- Customisable cursor (most popular image formats supported) + +

+ +

## Installation @@ -43,11 +50,14 @@ Darktile will use sensible defaults if no config/theme files are available. The Found in the config directory (see above) inside `config.yaml`. ```yaml -opacity: 1.0 # window opacity: 0.0 is fully transparent, 1.0 is fully opaque +opacity: 1.0 # Window opacity: 0.0 is fully transparent, 1.0 is fully opaque font: - family: "" # Find possible values for this by running 'darktile list-fonts' - size: 16 - dpi: 72 + family: "" # Font family. Find possible values for this by running 'darktile list-fonts' + size: 16 # Font size + dpi: 72 # DPI + ligatures: true # Enable font ligatures e.g. render '≡' instead of '===' +cursor: + image: "" # Path to an image to render as your cursor (defaults to standard rectangular cursor) ``` ### Example Theme diff --git a/cursor.gif b/cursor.gif new file mode 100644 index 0000000000000000000000000000000000000000..ec66f63784b1cb546fa8ac6de08296efce5d6cca GIT binary patch literal 19304 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 DN4I(d literal 0 HcmV?d00001 diff --git a/internal/app/darktile/cmd/root.go b/internal/app/darktile/cmd/root.go index 0df4279f..1ed007eb 100644 --- a/internal/app/darktile/cmd/root.go +++ b/internal/app/darktile/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "errors" "fmt" + "image" "os" "time" @@ -48,6 +49,8 @@ var rootCmd = &cobra.Command{ if _, err := conf.Save(); err != nil { return fmt.Errorf("failed to write config file: %w", err) } + fmt.Println("Config written.") + return nil } var theme *termutil.Theme @@ -91,6 +94,16 @@ var rootCmd = &cobra.Command{ gui.WithFontSize(conf.Font.Size), gui.WithFontFamily(conf.Font.Family), gui.WithOpacity(conf.Opacity), + gui.WithLigatures(conf.Font.Ligatures), + } + + if conf.Cursor.Image != "" { + img, err := getImageFromFilePath(conf.Cursor.Image) + if err != nil { + startupErrors = append(startupErrors, err) + } else { + options = append(options, gui.WithCursorImage(img)) + } } if screenshotAfterMS > 0 { @@ -118,6 +131,16 @@ var rootCmd = &cobra.Command{ }, } +func getImageFromFilePath(filePath string) (image.Image, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + image, _, err := image.Decode(f) + return image, err +} + func Execute() error { rootCmd.Flags().BoolVar(&showVersion, "version", showVersion, "Show darktile version information and exit") rootCmd.Flags().BoolVar(&rewriteConfig, "rewrite-config", rewriteConfig, "Write the resultant config after parsing config files and merging with defauls back to the config file") diff --git a/internal/app/darktile/config/config.go b/internal/app/darktile/config/config.go index 044c29fd..0c7d97ea 100644 --- a/internal/app/darktile/config/config.go +++ b/internal/app/darktile/config/config.go @@ -12,12 +12,18 @@ import ( type Config struct { Opacity float64 Font Font + Cursor Cursor } type Font struct { - Family string - Size float64 - DPI float64 + Family string + Size float64 + DPI float64 + Ligatures bool +} + +type Cursor struct { + Image string } type ErrorFileNotFound struct { diff --git a/internal/app/darktile/config/default.go b/internal/app/darktile/config/default.go index e225c5b8..55d21831 100644 --- a/internal/app/darktile/config/default.go +++ b/internal/app/darktile/config/default.go @@ -11,9 +11,10 @@ import ( var defaultConfig = Config{ Opacity: 1.0, Font: Font{ - Family: "", // internally packed font will be loaded by default - Size: 18.0, - DPI: 72.0, + Family: "", // internally packed font will be loaded by default + Size: 18.0, + DPI: 72.0, + Ligatures: true, }, } diff --git a/internal/app/darktile/gui/draw.go b/internal/app/darktile/gui/draw.go index 1d93780d..48673c9d 100644 --- a/internal/app/darktile/gui/draw.go +++ b/internal/app/darktile/gui/draw.go @@ -1,322 +1,17 @@ package gui import ( - "image/color" - "strings" - "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/ebitenutil" - "github.com/hajimehoshi/ebiten/v2/text" - "github.com/liamg/darktile/internal/app/darktile/termutil" - imagefont "golang.org/x/image/font" + "github.com/liamg/darktile/internal/app/darktile/gui/render" ) // Draw renders the terminal GUI to the ebtien window. Required to implement the ebiten interface. func (g *GUI) Draw(screen *ebiten.Image) { - - tmp := ebiten.NewImage(g.size.X, g.size.Y) - - cellSize := g.fontManager.CharSize() - dotDepth := g.fontManager.DotDepth() - - buffer := g.terminal.GetActiveBuffer() - - regularFace := g.fontManager.RegularFontFace() - boldFace := g.fontManager.BoldFontFace() - italicFace := g.fontManager.ItalicFontFace() - boldItalicFace := g.fontManager.BoldItalicFontFace() - - var useFace imagefont.Face - - defBg := g.terminal.Theme().DefaultBackground() - defFg := g.terminal.Theme().DefaultForeground() - - var colour color.Color - - endX := float64(cellSize.X * int(buffer.ViewWidth())) - endY := float64(cellSize.Y * int(buffer.ViewHeight())) - extraW := float64(g.size.X) - endX - extraH := float64(g.size.Y) - endY - if extraW > 0 { - ebitenutil.DrawRect(tmp, endX, 0, extraW, endY, defBg) - } - if extraH > 0 { - ebitenutil.DrawRect(tmp, 0, endY, float64(g.size.X), extraH, defBg) - } - - var inHighlight bool - var highlightRendered bool - var highlightMin termutil.Position - highlightMin.Col = uint16(g.size.X) - highlightMin.Line = uint64(g.size.Y) - var highlightMax termutil.Position - - for y := int(buffer.ViewHeight() - 1); y >= 0; y-- { - py := cellSize.Y * y - - ebitenutil.DrawRect(tmp, 0, float64(py), float64(g.size.X), float64(cellSize.Y), defBg) - inHighlight = false - for x := uint16(0); x < buffer.ViewWidth(); x++ { - cell := buffer.GetCell(x, uint16(y)) - px := cellSize.X * int(x) - if cell != nil { - colour = cell.Bg() - } else { - colour = defBg - } - isCursor := g.terminal.GetActiveBuffer().IsCursorVisible() && int(buffer.CursorLine()) == y && buffer.CursorColumn() == x - if isCursor { - colour = g.terminal.Theme().CursorBackground() - } else if buffer.InSelection(termutil.Position{ - Line: uint64(y), - Col: x, - }) { - colour = g.terminal.Theme().SelectionBackground() - } else if colour == nil { - colour = defBg - } - - ebitenutil.DrawRect(tmp, float64(px), float64(py), float64(cellSize.X), float64(cellSize.Y), colour) - - if buffer.IsHighlighted(termutil.Position{ - Line: uint64(y), - Col: x, - }) { - - if !inHighlight { - highlightRendered = true - } - - if uint64(y) < highlightMin.Line { - highlightMin.Col = uint16(g.size.X) - highlightMin.Line = uint64(y) - } - if uint64(y) > highlightMax.Line { - highlightMax.Line = uint64(y) - } - if uint64(y) == highlightMax.Line && x > highlightMax.Col { - highlightMax.Col = x - } - if uint64(y) == highlightMin.Line && x < highlightMin.Col { - highlightMin.Col = x - } - - inHighlight = true - - } else if inHighlight { - inHighlight = false - } - - if isCursor && !ebiten.IsFocused() { - ebitenutil.DrawRect(tmp, float64(px)+1, float64(py)+1, float64(cellSize.X)-2, float64(cellSize.Y)-2, g.terminal.Theme().DefaultBackground()) - } - } - for x := uint16(0); x < buffer.ViewWidth(); x++ { - cell := buffer.GetCell(x, uint16(y)) - if cell == nil || cell.Rune().Rune == 0 { - continue - } - - px := cellSize.X * int(x) - colour = cell.Fg() - if g.terminal.GetActiveBuffer().IsCursorVisible() && int(buffer.CursorLine()) == y && buffer.CursorColumn() == x { - colour = g.terminal.Theme().CursorForeground() - } else if buffer.InSelection(termutil.Position{ - Line: uint64(y), - Col: x, - }) { - colour = g.terminal.Theme().SelectionForeground() - } else if colour == nil { - colour = defFg - } - - useFace = regularFace - if cell.Bold() && cell.Italic() { - useFace = boldItalicFace - } else if cell.Bold() { - useFace = boldFace - } else if cell.Italic() { - useFace = italicFace - } - - if cell.Underline() { - uly := float64(py + (dotDepth+cellSize.Y)/2) - ebitenutil.DrawLine(tmp, float64(px), uly, float64(px+cellSize.X), uly, colour) - } - - text.Draw(tmp, string(cell.Rune().Rune), useFace, px, py+dotDepth, colour) - - if cell.Strikethrough() { - ebitenutil.DrawLine(tmp, float64(px), float64(py+(cellSize.Y/2)), float64(px+cellSize.X), float64(py+(cellSize.Y/2)), colour) - } - - } - } - - for _, sixel := range buffer.GetVisibleSixels() { - sx := float64(int(sixel.Sixel.X) * cellSize.X) - sy := float64(sixel.ViewLineOffset * cellSize.Y) - - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(sx, sy) - tmp.DrawImage( - ebiten.NewImageFromImage(sixel.Sixel.Image), - op, - ) - } - - // draw annotations and overlays - if highlightRendered { - if annotation := buffer.GetHighlightAnnotation(); annotation != nil { - - if highlightMin.Col == uint16(g.size.X) { - highlightMin.Col = 0 - } - if highlightMin.Line == uint64(g.size.Y) { - highlightMin.Line = 0 - } - - mx, _ := ebiten.CursorPosition() - padding := float64(cellSize.X) / 2 - lineX := float64(mx) - var lineY float64 - var lineHeight float64 - annotationX := mx - cellSize.X*2 - var annotationY float64 - annotationWidth := float64(cellSize.X) * annotation.Width - var annotationHeight float64 - - if annotationX+int(annotationWidth)+int(padding*2) > g.size.X { - annotationX = g.size.X - (int(annotationWidth) + int(padding*2)) - } - if annotationX < int(padding) { - annotationX = int(padding) - } - - if (highlightMin.Line + (highlightMax.Line-highlightMin.Line)/2) < uint64(buffer.ViewHeight()/2) { - // annotate underneath max - - pixelsUnderHighlight := float64(g.size.Y) - float64((highlightMax.Line+1)*uint64(cellSize.Y)) - // we need to reserve at least one cell height for the label line - pixelsAvailableY := pixelsUnderHighlight - float64(cellSize.Y) - annotationHeight = annotation.Height * float64(cellSize.Y) - if annotationHeight > pixelsAvailableY { - annotationHeight = pixelsAvailableY - } - - lineHeight = pixelsUnderHighlight - padding - annotationHeight - if lineHeight > annotationHeight { - if annotationHeight > float64(cellSize.Y)*3 { - lineHeight = annotationHeight - } else { - lineHeight = float64(cellSize.Y) * 3 - } - } - annotationY = float64((highlightMax.Line+1)*uint64(cellSize.Y)) + lineHeight + float64(padding) - lineY = float64((highlightMax.Line + 1) * uint64(cellSize.Y)) - - } else { - //annotate above min - - pixelsAboveHighlight := float64((highlightMin.Line) * uint64(cellSize.Y)) - // we need to reserve at least one cell height for the label line - pixelsAvailableY := pixelsAboveHighlight - float64(cellSize.Y) - annotationHeight = annotation.Height * float64(cellSize.Y) - if annotationHeight > pixelsAvailableY { - annotationHeight = pixelsAvailableY - } - - lineHeight = pixelsAboveHighlight - annotationHeight - if lineHeight > annotationHeight { - if annotationHeight > float64(cellSize.Y)*3 { - lineHeight = annotationHeight - } else { - lineHeight = float64(cellSize.Y) * 3 - } - } - annotationY = float64((highlightMin.Line)*uint64(cellSize.Y)) - lineHeight - float64(padding*2) - annotationHeight - lineY = annotationY + annotationHeight + +padding - } - - // draw opaque box below and above highlighted line(s) - ebitenutil.DrawRect(tmp, 0, float64(highlightMin.Line*uint64(cellSize.Y)), float64(cellSize.X*int(highlightMin.Col)), float64(cellSize.Y), color.RGBA{A: 0x80}) - ebitenutil.DrawRect(tmp, float64((cellSize.X)*int(highlightMax.Col+1)), float64(highlightMax.Line*uint64(cellSize.Y)), float64(g.size.X), float64(cellSize.Y), color.RGBA{A: 0x80}) - ebitenutil.DrawRect(tmp, 0, 0, float64(g.size.X), float64(highlightMin.Line*uint64(cellSize.Y)), color.RGBA{A: 0x80}) - afterLineY := float64((1 + highlightMax.Line) * uint64(cellSize.Y)) - ebitenutil.DrawRect(tmp, 0, afterLineY, float64(g.size.X), float64(g.size.Y)-afterLineY, color.RGBA{A: 0x80}) - - // annotation border - ebitenutil.DrawRect(tmp, float64(annotationX)-padding, annotationY-padding, float64(annotationWidth)+(padding*2), annotationHeight+(padding*2), g.terminal.Theme().SelectionBackground()) - // annotation background - ebitenutil.DrawRect(tmp, 1+float64(annotationX)-padding, 1+annotationY-padding, float64(annotationWidth)+(padding*2)-2, annotationHeight+(padding*2)-2, g.terminal.Theme().DefaultBackground()) - - // vertical line - ebitenutil.DrawLine(tmp, lineX, float64(lineY), lineX, lineY+lineHeight, g.terminal.Theme().SelectionBackground()) - - var tY int - var tX int - - if annotation.Image != nil { - tY += annotation.Image.Bounds().Dy() + cellSize.Y/2 - - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(float64(annotationX), annotationY) - tmp.DrawImage( - ebiten.NewImageFromImage(annotation.Image), - op, - ) - } - - for _, r := range annotation.Text { - if r == '\n' { - tY += cellSize.Y - tX = 0 - continue - } - text.Draw(tmp, string(r), regularFace, annotationX+tX, int(annotationY)+dotDepth+tY, g.terminal.Theme().DefaultForeground()) - tX += cellSize.X - } - - } - } - - if len(g.popupMessages) > 0 { - pad := cellSize.Y / 2 // horizontal and vertical padding - msgEndY := endY - for _, msg := range g.popupMessages { - - lines := strings.Split(msg.Text, "\n") - - msgX := pad - - msgY := msgEndY - float64(pad*3) - float64(cellSize.Y*len(lines)) - - msgText := msg.Text - - boxWidth := float64(pad*2) + float64(cellSize.X*len(msgText)) - boxHeight := float64(pad*2) + float64(cellSize.Y*len(lines)) - - if boxWidth < endX/8 { - boxWidth = endX / 8 - } - - ebitenutil.DrawRect(tmp, float64(msgX-1), msgY-1, boxWidth+2, boxHeight+2, msg.Foreground) - ebitenutil.DrawRect(tmp, float64(msgX), msgY, boxWidth, boxHeight, msg.Background) - for y, line := range lines { - for x, r := range line { - text.Draw(tmp, string(r), regularFace, msgX+pad+(x*cellSize.X), pad+(y*cellSize.Y)+int(msgY)+dotDepth, msg.Foreground) - } - } - msgEndY = msgEndY - float64(pad*4) - float64(len(lines)*g.CellSize().Y) - } - } + render. + New(screen, g.terminal, g.fontManager, g.popupMessages, g.opacity, g.enableLigatures, g.cursorImage). + Draw() if g.screenshotRequested { - g.takeScreenshot(tmp) + g.takeScreenshot(screen) } - - opt := &ebiten.DrawImageOptions{} - opt.ColorM.Scale(1, 1, 1, g.opacity) - screen.DrawImage(tmp, opt) - tmp.Dispose() } diff --git a/internal/app/darktile/gui/gui.go b/internal/app/darktile/gui/gui.go index 63b436b2..cbc64b77 100644 --- a/internal/app/darktile/gui/gui.go +++ b/internal/app/darktile/gui/gui.go @@ -3,13 +3,13 @@ package gui import ( "fmt" "image" - "image/color" "math/rand" "os" "strings" "time" "github.com/liamg/darktile/internal/app/darktile/font" + "github.com/liamg/darktile/internal/app/darktile/gui/popup" "github.com/liamg/darktile/internal/app/darktile/hinters" "github.com/liamg/darktile/internal/app/darktile/termutil" @@ -34,19 +34,14 @@ type GUI struct { mousePos termutil.Position hinters []hinters.Hinter activeHinter int - popupMessages []PopupMessage + popupMessages []popup.Message screenshotRequested bool screenshotFilename string startupFuncs []func(g *GUI) keyState *keyState opacity float64 -} - -type PopupMessage struct { - Text string - Expiry time.Time - Foreground color.Color - Background color.Color + enableLigatures bool + cursorImage *ebiten.Image } type MouseState uint8 @@ -59,12 +54,13 @@ const ( func New(terminal *termutil.Terminal, options ...Option) (*GUI, error) { g := &GUI{ - terminal: terminal, - size: image.Point{80, 30}, - updateChan: make(chan struct{}), - fontManager: font.NewManager(), - activeHinter: -1, - keyState: newKeyState(), + terminal: terminal, + size: image.Point{80, 30}, + updateChan: make(chan struct{}), + fontManager: font.NewManager(), + activeHinter: -1, + keyState: newKeyState(), + enableLigatures: true, } for _, option := range options { diff --git a/internal/app/darktile/gui/input.go b/internal/app/darktile/gui/input.go index 6cd159c9..7122c5a9 100644 --- a/internal/app/darktile/gui/input.go +++ b/internal/app/darktile/gui/input.go @@ -219,11 +219,7 @@ func (g *GUI) handleInput() error { return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[6%s~", g.getModifierStr()))) default: input := ebiten.AppendInputChars(nil) - for _, runePressed := range input { - if err := g.terminal.WriteToPty([]byte(string(runePressed))); err != nil { - return err - } - } + return g.terminal.WriteToPty([]byte(string(input))) } return nil diff --git a/internal/app/darktile/gui/options.go b/internal/app/darktile/gui/options.go index 7a0fe50d..2f26f937 100644 --- a/internal/app/darktile/gui/options.go +++ b/internal/app/darktile/gui/options.go @@ -1,5 +1,11 @@ package gui +import ( + "image" + + "github.com/hajimehoshi/ebiten/v2" +) + type Option func(g *GUI) error func WithFontFamily(family string) func(g *GUI) error { @@ -29,6 +35,20 @@ func WithFontDPI(dpi float64) func(g *GUI) error { } } +func WithLigatures(enable bool) func(g *GUI) error { + return func(g *GUI) error { + g.enableLigatures = enable + return nil + } +} + +func WithCursorImage(img image.Image) func(g *GUI) error { + return func(g *GUI) error { + g.cursorImage = ebiten.NewImageFromImage(img) + return nil + } +} + func WithStartupFunc(f func(g *GUI)) Option { return func(g *GUI) error { g.startupFuncs = append(g.startupFuncs, f) diff --git a/internal/app/darktile/gui/popup/popup.go b/internal/app/darktile/gui/popup/popup.go new file mode 100644 index 00000000..83ae5f2e --- /dev/null +++ b/internal/app/darktile/gui/popup/popup.go @@ -0,0 +1,13 @@ +package popup + +import ( + "image/color" + "time" +) + +type Message struct { + Text string + Expiry time.Time + Foreground color.Color + Background color.Color +} diff --git a/internal/app/darktile/gui/popups.go b/internal/app/darktile/gui/popups.go index b2c77d6d..2c732d71 100644 --- a/internal/app/darktile/gui/popups.go +++ b/internal/app/darktile/gui/popups.go @@ -4,6 +4,8 @@ import ( "fmt" "image/color" "time" + + "github.com/liamg/darktile/internal/app/darktile/gui/popup" ) const ( @@ -12,7 +14,7 @@ const ( ) func (g *GUI) ShowPopup(msg string, fg color.Color, bg color.Color, duration time.Duration) { - g.popupMessages = append(g.popupMessages, PopupMessage{ + g.popupMessages = append(g.popupMessages, popup.Message{ Text: msg, Expiry: time.Now().Add(duration), Foreground: fg, diff --git a/internal/app/darktile/gui/render/annotation.go b/internal/app/darktile/gui/render/annotation.go new file mode 100644 index 00000000..2a461b8d --- /dev/null +++ b/internal/app/darktile/gui/render/annotation.go @@ -0,0 +1,162 @@ +package render + +import ( + "image/color" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/text" +) + +func (r *Render) drawAnnotation() { + + // 1. check if we have anything to highlight/annotate + highlightStart, highlightEnd, ok := r.buffer.GetViewHighlight() + if !ok { + return + } + + // 2. make everything outside of the highlighted area opaque + dimColour := color.RGBA{A: 0x80} // 50% alpha black overlay to dim non-highlighted area + for line := 0; line < int(r.buffer.ViewHeight()); line++ { + if line < int(highlightStart.Line) || line > int(highlightEnd.Line) { + ebitenutil.DrawRect( + r.frame, + 0, + float64(line*r.font.CellSize.Y), + float64(r.pixelWidth), + float64(r.font.CellSize.Y), + dimColour, // 50% alpha black overlay to dim non-highlighted area + ) + continue + } + + if line == int(highlightStart.Line) && highlightStart.Col > 0 { + // we need to dim some content on this line before the highlight starts + ebitenutil.DrawRect( + r.frame, + 0, + float64(line*r.font.CellSize.Y), + float64(int(highlightStart.Col)*r.font.CellSize.X), + float64(r.font.CellSize.Y), + dimColour, + ) + } + + if line == int(highlightEnd.Line) && highlightEnd.Col < r.buffer.ViewWidth()-2 { + // we need to dim some content on this line after the highlight ends + ebitenutil.DrawRect( + r.frame, + float64(int(highlightEnd.Col+1)*r.font.CellSize.X), + float64(line*r.font.CellSize.Y), + float64(int(r.buffer.ViewWidth()-(highlightEnd.Col+1))*r.font.CellSize.X), + float64(r.font.CellSize.Y), + dimColour, + ) + } + } + + // 3. annotate the highlighted area (if there is an annotation) + annotation := r.buffer.GetHighlightAnnotation() + if annotation == nil { + return + } + + mousePixelX, _ := ebiten.CursorPosition() + padding := float64(r.font.CellSize.X) / 2 + + var lineY float64 + var lineHeight float64 + var annotationY float64 + var annotationHeight float64 + + if (highlightStart.Line + (highlightEnd.Line-highlightStart.Line)/2) < uint64(r.buffer.ViewHeight()/2) { + // annotate underneath max + + pixelsUnderHighlight := float64(r.pixelHeight) - float64((highlightEnd.Line+1)*uint64(r.font.CellSize.Y)) + // we need to reserve at least one cell height for the label line + pixelsAvailableY := pixelsUnderHighlight - float64(r.font.CellSize.Y) + annotationHeight = annotation.Height * float64(r.font.CellSize.Y) + if annotationHeight > pixelsAvailableY { + annotationHeight = pixelsAvailableY + } + + lineHeight = pixelsUnderHighlight - padding - annotationHeight + if lineHeight > annotationHeight { + if annotationHeight > float64(r.font.CellSize.Y)*3 { + lineHeight = annotationHeight + } else { + lineHeight = float64(r.font.CellSize.Y) * 3 + } + } + annotationY = float64((highlightEnd.Line+1)*uint64(r.font.CellSize.Y)) + lineHeight + float64(padding) + lineY = float64((highlightEnd.Line + 1) * uint64(r.font.CellSize.Y)) + + } else { + //annotate above min + + pixelsAboveHighlight := float64((highlightStart.Line) * uint64(r.font.CellSize.Y)) + // we need to reserve at least one cell height for the label line + pixelsAvailableY := pixelsAboveHighlight - float64(r.font.CellSize.Y) + annotationHeight = annotation.Height * float64(r.font.CellSize.Y) + if annotationHeight > pixelsAvailableY { + annotationHeight = pixelsAvailableY + } + + lineHeight = pixelsAboveHighlight - annotationHeight + if lineHeight > annotationHeight { + if annotationHeight > float64(r.font.CellSize.Y)*3 { + lineHeight = annotationHeight + } else { + lineHeight = float64(r.font.CellSize.Y) * 3 + } + } + annotationY = float64((highlightStart.Line)*uint64(r.font.CellSize.Y)) - lineHeight - float64(padding*2) - annotationHeight + lineY = annotationY + annotationHeight + +padding + } + + annotationX := mousePixelX - r.font.CellSize.X*2 + annotationWidth := float64(r.font.CellSize.X) * annotation.Width + + // if the annotation box goes off the right side of the terminal, align it against the right side + if annotationX+int(annotationWidth)+int(padding*2) > r.pixelWidth { + annotationX = r.pixelWidth - (int(annotationWidth) + int(padding*2)) + } + + // if the annotation is too far left, align it against the left side + if annotationX < int(padding) { + annotationX = int(padding) + } + + // annotation border + ebitenutil.DrawRect(r.frame, float64(annotationX)-padding, annotationY-padding, float64(annotationWidth)+(padding*2), annotationHeight+(padding*2), r.theme.SelectionBackground()) + // annotation background + ebitenutil.DrawRect(r.frame, 1+float64(annotationX)-padding, 1+annotationY-padding, float64(annotationWidth)+(padding*2)-2, annotationHeight+(padding*2)-2, r.theme.DefaultBackground()) + + // vertical line + ebitenutil.DrawLine(r.frame, float64(mousePixelX), float64(lineY), float64(mousePixelX), lineY+lineHeight, r.theme.SelectionBackground()) + + var tY int + var tX int + + if annotation.Image != nil { + tY += annotation.Image.Bounds().Dy() + r.font.CellSize.Y/2 + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(annotationX), annotationY) + r.frame.DrawImage( + ebiten.NewImageFromImage(annotation.Image), + op, + ) + } + + for _, ch := range annotation.Text { + if ch == '\n' { + tY += r.font.CellSize.Y + tX = 0 + continue + } + text.Draw(r.frame, string(ch), r.font.Regular, annotationX+tX, int(annotationY)+r.font.DotDepth+tY, r.theme.DefaultForeground()) + tX += r.font.CellSize.X + } + +} diff --git a/internal/app/darktile/gui/render/content.go b/internal/app/darktile/gui/render/content.go new file mode 100644 index 00000000..40490c5e --- /dev/null +++ b/internal/app/darktile/gui/render/content.go @@ -0,0 +1,10 @@ +package render + +func (r *Render) drawContent() { + // draw base content for each row + defBg := r.theme.DefaultBackground() + defFg := r.theme.DefaultForeground() + for viewY := int(r.buffer.ViewHeight() - 1); viewY >= 0; viewY-- { + r.drawRow(viewY, defBg, defFg) + } +} diff --git a/internal/app/darktile/gui/render/cursor.go b/internal/app/darktile/gui/render/cursor.go new file mode 100644 index 00000000..ba66aaf9 --- /dev/null +++ b/internal/app/darktile/gui/render/cursor.go @@ -0,0 +1,67 @@ +package render + +import ( + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/text" + "github.com/liamg/darktile/internal/app/darktile/termutil" +) + +func (r *Render) drawCursor() { + //draw cursor + if !r.buffer.IsCursorVisible() { + return + } + + pixelX := float64(int(r.buffer.CursorColumn()) * r.font.CellSize.X) + pixelY := float64(int(r.buffer.CursorLine()) * r.font.CellSize.Y) + cell := r.buffer.GetCell(r.buffer.CursorColumn(), r.buffer.CursorLine()) + + useFace := r.font.Regular + if cell != nil { + if cell.Bold() && cell.Italic() { + useFace = r.font.BoldItalic + } else if cell.Bold() { + useFace = r.font.Bold + } else if cell.Italic() { + useFace = r.font.Italic + } + } + + pixelW, pixelH := float64(r.font.CellSize.X), float64(r.font.CellSize.Y) + + // empty rect without focus + if !ebiten.IsFocused() { + ebitenutil.DrawRect(r.frame, pixelX, pixelY, pixelW, pixelH, r.theme.CursorBackground()) + ebitenutil.DrawRect(r.frame, pixelX+1, pixelY+1, pixelW-2, pixelH-2, r.theme.CursorForeground()) + return + } + + // draw the cursor shape + switch r.buffer.GetCursorShape() { + case termutil.CursorShapeBlinkingBar, termutil.CursorShapeSteadyBar: + ebitenutil.DrawRect(r.frame, pixelX, pixelY, 2, pixelH, r.theme.CursorBackground()) + case termutil.CursorShapeBlinkingUnderline, termutil.CursorShapeSteadyUnderline: + ebitenutil.DrawRect(r.frame, pixelX, pixelY+pixelH-2, pixelW, 2, r.theme.CursorBackground()) + default: + // draw a custom cursor if we have one and there are no characters in the way + if r.cursorImage != nil && (cell == nil || cell.Rune().Rune == 0) { + opt := &ebiten.DrawImageOptions{} + _, h := r.cursorImage.Size() + ratio := 1 / (float64(h) / float64(r.font.CellSize.Y)) + actualHeight := float64(h) * ratio + offsetY := (float64(r.font.CellSize.Y) - actualHeight) / 2 + opt.GeoM.Scale(ratio, ratio) + opt.GeoM.Translate(pixelX, pixelY+offsetY) + r.frame.DrawImage(r.cursorImage, opt) + return + } + + ebitenutil.DrawRect(r.frame, pixelX, pixelY, pixelW, pixelH, r.theme.CursorBackground()) + + // we've drawn over the cell contents, so we need to draw it again in the cursor colours + if cell != nil && cell.Rune().Rune > 0 { + text.Draw(r.frame, string(cell.Rune().Rune), useFace, int(pixelX), int(pixelY)+r.font.DotDepth, r.theme.CursorForeground()) + } + } +} diff --git a/internal/app/darktile/gui/render/ligatures.go b/internal/app/darktile/gui/render/ligatures.go new file mode 100644 index 00000000..9be18b3f --- /dev/null +++ b/internal/app/darktile/gui/render/ligatures.go @@ -0,0 +1,45 @@ +package render + +import ( + "image/color" + + "github.com/hajimehoshi/ebiten/v2/text" + imagefont "golang.org/x/image/font" +) + +var ligatures = map[string]rune{ + ":=": '≔', + "===": '≡', + "!=": '≠', + "!==": '≢', + "<=": '≤', + ">=": '≥', + "=>": '⇒', + "->": '→', + "<-": '←', + "<>": '≷', +} + +func (r *Render) handleLigatures(sx uint16, sy uint16, face imagefont.Face, colour color.Color) (length int) { + + var candidate string + for x := sx; x <= sx+2; x++ { + cell := r.buffer.GetCell(x, sy) + if cell == nil || cell.Rune().Rune == 0 { + break + } + candidate += string(cell.Rune().Rune) + } + + for len(candidate) > 1 { + if ru, ok := ligatures[candidate]; ok { + // draw ligature + ligX := (int(sx) * r.font.CellSize.X) + (((len(candidate) - 1) * r.font.CellSize.X) / 2) + text.Draw(r.frame, string(ru), face, ligX, (int(sy)*r.font.CellSize.Y)+r.font.DotDepth, colour) + return len(candidate) + } + candidate = candidate[:len(candidate)-1] + } + + return 0 +} diff --git a/internal/app/darktile/gui/render/popups.go b/internal/app/darktile/gui/render/popups.go new file mode 100644 index 00000000..c7225800 --- /dev/null +++ b/internal/app/darktile/gui/render/popups.go @@ -0,0 +1,42 @@ +package render + +import ( + "strings" + + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/text" +) + +func (r *Render) drawPopups() { + + if len(r.popups) == 0 { + return + } + + pad := r.font.CellSize.Y / 2 // horizontal and vertical padding + maxPixelX := float64(r.font.CellSize.X * int(r.buffer.ViewWidth())) + maxPixelY := float64(r.font.CellSize.Y * int(r.buffer.ViewHeight())) + + for _, msg := range r.popups { + + lines := strings.Split(msg.Text, "\n") + msgX := pad + msgY := maxPixelY - float64(pad*3) - float64(r.font.CellSize.Y*len(lines)) + boxWidth := float64(pad*2) + float64(r.font.CellSize.X*len(msg.Text)) + boxHeight := float64(pad*2) + float64(r.font.CellSize.Y*len(lines)) + + if boxWidth < maxPixelX/8 { + boxWidth = maxPixelX / 8 + } + + ebitenutil.DrawRect(r.frame, float64(msgX-1), msgY-1, boxWidth+2, boxHeight+2, msg.Foreground) + ebitenutil.DrawRect(r.frame, float64(msgX), msgY, boxWidth, boxHeight, msg.Background) + for y, line := range lines { + for x, c := range line { + text.Draw(r.frame, string(c), r.font.Regular, msgX+pad+(x*r.font.CellSize.X), pad+(y*r.font.CellSize.Y)+int(msgY)+r.font.DotDepth, msg.Foreground) + } + } + maxPixelY = maxPixelY - float64(pad*4) - float64(len(lines)*r.font.CellSize.Y) + } + +} diff --git a/internal/app/darktile/gui/render/render.go b/internal/app/darktile/gui/render/render.go new file mode 100644 index 00000000..359e0690 --- /dev/null +++ b/internal/app/darktile/gui/render/render.go @@ -0,0 +1,97 @@ +package render + +import ( + "image" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/liamg/darktile/internal/app/darktile/font" + "github.com/liamg/darktile/internal/app/darktile/gui/popup" + "github.com/liamg/darktile/internal/app/darktile/termutil" + imagefont "golang.org/x/image/font" +) + +type Render struct { + frame *ebiten.Image + screen *ebiten.Image + terminal *termutil.Terminal + buffer *termutil.Buffer + theme *termutil.Theme + fontManager *font.Manager + pixelWidth int + pixelHeight int + font Font + opacity float64 + popups []popup.Message + enableLigatures bool + cursorImage *ebiten.Image +} + +type Font struct { + Regular imagefont.Face + Bold imagefont.Face + Italic imagefont.Face + BoldItalic imagefont.Face + CellSize image.Point + DotDepth int +} + +func New(screen *ebiten.Image, terminal *termutil.Terminal, fontManager *font.Manager, popups []popup.Message, opacity float64, enableLigatures bool, cursorImage *ebiten.Image) *Render { + w, h := screen.Size() + return &Render{ + screen: screen, + frame: ebiten.NewImage(w, h), + terminal: terminal, + buffer: terminal.GetActiveBuffer(), + theme: terminal.Theme(), + fontManager: fontManager, + pixelWidth: w, + pixelHeight: h, + font: Font{ + Regular: fontManager.RegularFontFace(), + Bold: fontManager.BoldFontFace(), + Italic: fontManager.ItalicFontFace(), + BoldItalic: fontManager.BoldItalicFontFace(), + CellSize: fontManager.CharSize(), + DotDepth: fontManager.DotDepth(), + }, + opacity: opacity, + popups: popups, + enableLigatures: enableLigatures, + cursorImage: cursorImage, + } +} + +func (r *Render) Draw() { + + // 1. fill frame with default background colour + r.frame.Fill(r.theme.DefaultBackground()) + + // 2. draw content (each row, each cell) + r.drawContent() + + // 3. draw cursor + r.drawCursor() + + // // 4. draw sixels + r.drawSixels() + + // // 5. draw selection + r.drawSelection() + + // // 6. draw highlight/annotations + r.drawAnnotation() + + // // 7. draw popups + r.drawPopups() + + // // 8. apply effects (e.g. transparency) + r.finalise() + +} + +func (r *Render) finalise() { + defer r.frame.Dispose() + opt := &ebiten.DrawImageOptions{} + opt.ColorM.Scale(1, 1, 1, r.opacity) + r.screen.DrawImage(r.frame, opt) +} diff --git a/internal/app/darktile/gui/render/row.go b/internal/app/darktile/gui/render/row.go new file mode 100644 index 00000000..3ddbc14c --- /dev/null +++ b/internal/app/darktile/gui/render/row.go @@ -0,0 +1,94 @@ +package render + +import ( + "image/color" + + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/text" + imagefont "golang.org/x/image/font" +) + +func (r *Render) drawRow(viewY int, defaultBackgroundColour color.Color, defaultForegroundColour color.Color) { + + pixelY := r.font.CellSize.Y * viewY + + // draw a default colour background image across the entire row background + ebitenutil.DrawRect(r.frame, 0, float64(pixelY), float64(r.pixelWidth), float64(r.font.CellSize.Y), defaultBackgroundColour) + + var colour color.Color + + // draw background for each cell in row + for viewX := uint16(0); viewX < r.buffer.ViewWidth(); viewX++ { + cell := r.buffer.GetCell(viewX, uint16(viewY)) + pixelX := r.font.CellSize.X * int(viewX) + if cell != nil { + colour = cell.Bg() + } + if colour == nil { + colour = defaultBackgroundColour + } + + ebitenutil.DrawRect(r.frame, float64(pixelX), float64(pixelY), float64(r.font.CellSize.X), float64(r.font.CellSize.Y), colour) + } + + var useFace imagefont.Face + var skipRunes int + + // draw text content of each cell in row + for viewX := uint16(0); viewX < r.buffer.ViewWidth(); viewX++ { + + cell := r.buffer.GetCell(viewX, uint16(viewY)) + + // we don't need to draw empty cells + if cell == nil || cell.Rune().Rune == 0 { + continue + } + colour = cell.Fg() + if colour == nil { + colour = defaultForegroundColour + } + + // pick a font face for the cell + if !cell.Bold() && !cell.Italic() { + useFace = r.font.Regular + } else if cell.Bold() && cell.Italic() { + useFace = r.font.Italic + } else if cell.Bold() { + useFace = r.font.Bold + } else if cell.Italic() { + useFace = r.font.Italic + } + + pixelX := r.font.CellSize.X * int(viewX) + + // underline the cell content if required + if cell.Underline() { + underlinePixelY := float64(pixelY + (r.font.DotDepth+r.font.CellSize.Y)/2) + ebitenutil.DrawLine(r.frame, float64(pixelX), underlinePixelY, float64(pixelX+r.font.CellSize.X), underlinePixelY, colour) + } + + // strikethrough the cell if required + if cell.Strikethrough() { + ebitenutil.DrawLine( + r.frame, + float64(pixelX), + float64(pixelY+(r.font.CellSize.Y/2)), + float64(pixelX+r.font.CellSize.X), + float64(pixelY+(r.font.CellSize.Y/2)), + colour, + ) + } + + if r.enableLigatures && skipRunes == 0 { + skipRunes = r.handleLigatures(viewX, uint16(viewY), useFace, colour) + } + + if skipRunes > 0 { + skipRunes-- + continue + } + + // draw the text for the cell + text.Draw(r.frame, string(cell.Rune().Rune), useFace, pixelX, pixelY+r.font.DotDepth, colour) + } +} diff --git a/internal/app/darktile/gui/render/selection.go b/internal/app/darktile/gui/render/selection.go new file mode 100644 index 00000000..b7c7b640 --- /dev/null +++ b/internal/app/darktile/gui/render/selection.go @@ -0,0 +1,35 @@ +package render + +import ( + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/text" +) + +func (r *Render) drawSelection() { + _, selection := r.buffer.GetSelection() + if selection == nil { + // nothing selected + return + } + + bg, fg := r.theme.SelectionBackground(), r.theme.SelectionForeground() + + for y := selection.Start.Line; y <= selection.End.Line; y++ { + xStart, xEnd := 0, int(r.buffer.ViewWidth()) + if y == selection.Start.Line { + xStart = int(selection.Start.Col) + } + if y == selection.End.Line { + xEnd = int(selection.End.Col) + } + for x := xStart; x <= xEnd; x++ { + pX, pY := float64(x*r.font.CellSize.X), float64(y*uint64(r.font.CellSize.Y)) + ebitenutil.DrawRect(r.frame, pX, pY, float64(r.font.CellSize.X), float64(r.font.CellSize.Y), bg) + cell := r.buffer.GetCell(uint16(x), uint16(y)) + if cell == nil || cell.Rune().Rune == 0 { + continue + } + text.Draw(r.frame, string(cell.Rune().Rune), r.font.Regular, int(pX), int(pY)+r.font.DotDepth, fg) + } + } +} diff --git a/internal/app/darktile/gui/render/sixels.go b/internal/app/darktile/gui/render/sixels.go new file mode 100644 index 00000000..18c75d5b --- /dev/null +++ b/internal/app/darktile/gui/render/sixels.go @@ -0,0 +1,17 @@ +package render + +import "github.com/hajimehoshi/ebiten/v2" + +func (r *Render) drawSixels() { + for _, sixel := range r.buffer.GetVisibleSixels() { + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate( + float64(int(sixel.Sixel.X)*r.font.CellSize.X), + float64(sixel.ViewLineOffset*r.font.CellSize.Y), + ) + r.frame.DrawImage( + ebiten.NewImageFromImage(sixel.Sixel.Image), + op, + ) + } +} diff --git a/internal/app/darktile/gui/update.go b/internal/app/darktile/gui/update.go index 36ad0194..a546eaac 100644 --- a/internal/app/darktile/gui/update.go +++ b/internal/app/darktile/gui/update.go @@ -4,6 +4,7 @@ import ( "time" "github.com/hajimehoshi/ebiten/v2" + "github.com/liamg/darktile/internal/app/darktile/gui/popup" ) func (g *GUI) getModifierStr() string { @@ -40,7 +41,7 @@ func (g *GUI) Update() error { } func (g *GUI) filterPopupMessages() { - var filtered []PopupMessage + var filtered []popup.Message for _, msg := range g.popupMessages { if time.Since(msg.Expiry) >= 0 { continue diff --git a/internal/app/darktile/termutil/buffer.go b/internal/app/darktile/termutil/buffer.go index 5b38bd53..7c7ed6d7 100644 --- a/internal/app/darktile/termutil/buffer.go +++ b/internal/app/darktile/termutil/buffer.go @@ -3,14 +3,28 @@ package termutil import ( "image" "image/color" + "sync" ) const TabSize = 8 +type CursorShape uint8 + +const ( + CursorShapeBlinkingBlock CursorShape = iota + CursorShapeDefault + CursorShapeSteadyBlock + CursorShapeBlinkingUnderline + CursorShapeSteadyUnderline + CursorShapeBlinkingBar + CursorShapeSteadyBar +) + type Buffer struct { lines []Line savedCursorPos Position savedCursorAttr *CellAttributes + cursorShape CursorShape savedCharsets []*map[rune]rune savedCurrentCharset int topMargin uint // see DECSTBM docs - this is for scrollable regions @@ -31,6 +45,7 @@ type Buffer struct { highlightEnd *Position highlightAnnotation *Annotation sixels []Sixel + selectionMu sync.Mutex } type Annotation struct { @@ -70,10 +85,19 @@ func NewBuffer(width, height uint16, maxLines uint64, fg color.Color, bg color.C ShowCursor: true, SixelScrolling: true, }, + cursorShape: CursorShapeDefault, } return b } +func (buffer *Buffer) SetCursorShape(shape CursorShape) { + buffer.cursorShape = shape +} + +func (buffer *Buffer) GetCursorShape() CursorShape { + return buffer.cursorShape +} + func (buffer *Buffer) IsCursorVisible() bool { return buffer.modes.ShowCursor } diff --git a/internal/app/darktile/termutil/csi.go b/internal/app/darktile/termutil/csi.go index 757b67c6..df2471d7 100644 --- a/internal/app/darktile/termutil/csi.go +++ b/internal/app/darktile/termutil/csi.go @@ -45,13 +45,6 @@ func (t *Terminal) handleCSI(readChan chan MeasuredRune) (renderRequired bool) { t.log("CSI P(%q) I(%q) %c", strings.Join(params, ";"), string(intermediate), final) - for _, b := range intermediate { - t.processRunes(MeasuredRune{ - Rune: b, - Width: 1, - }) - } - switch final { case 'c': return t.csiSendDeviceAttributesHandler(params) @@ -73,6 +66,10 @@ func (t *Terminal) handleCSI(readChan chan MeasuredRune) (renderRequired bool) { return t.csiSetMarginsHandler(params) case 't': return t.csiWindowManipulation(params) + case 'q': + if string(intermediate) == " " { + return t.csiCursorSelection(params) + } case 'A': return t.csiCursorUpHandler(params) case 'B': @@ -112,15 +109,22 @@ func (t *Terminal) handleCSI(readChan chan MeasuredRune) (renderRequired bool) { return t.csiSoftResetHandler(params) } return false - default: - // TODO review this: - // if this is an unknown CSI sequence, write it to stdout as we can't handle it? - //_ = t.writeToRealStdOut(append([]rune{0x1b, '['}, raw...)...) - _ = raw - t.log("UNKNOWN CSI P(%s) I(%s) %c", strings.Join(params, ";"), string(intermediate), final) - return false } + for _, b := range intermediate { + t.processRunes(MeasuredRune{ + Rune: b, + Width: 1, + }) + } + + // TODO review this: + // if this is an unknown CSI sequence, write it to stdout as we can't handle it? + //_ = t.writeToRealStdOut(append([]rune{0x1b, '['}, raw...)...) + _ = raw + t.log("UNKNOWN CSI P(%s) I(%s) %c", strings.Join(params, ";"), string(intermediate), final) + return false + } type WindowState uint8 @@ -963,6 +967,12 @@ func (t *Terminal) sgrSequenceHandler(params []string) bool { } } + x := t.GetActiveBuffer().CursorColumn() + y := t.GetActiveBuffer().CursorLine() + if cell := t.GetActiveBuffer().GetCell(x, y); cell != nil { + cell.attr = t.GetActiveBuffer().cursorAttr + } + return false } @@ -970,3 +980,15 @@ func (t *Terminal) csiSoftResetHandler(params []string) bool { t.reset() return true } + +func (t *Terminal) csiCursorSelection(params []string) (renderRequired bool) { + if len(params) == 0 { + return false + } + i, err := strconv.Atoi(params[0]) + if err != nil { + return false + } + t.GetActiveBuffer().SetCursorShape(CursorShape(i)) + return true +} diff --git a/internal/app/darktile/termutil/options.go b/internal/app/darktile/termutil/options.go index 96671999..501e1a35 100644 --- a/internal/app/darktile/termutil/options.go +++ b/internal/app/darktile/termutil/options.go @@ -8,6 +8,10 @@ type Option func(t *Terminal) func WithLogFile(path string) Option { return func(t *Terminal) { + if path == "-" { + t.logFile = os.Stdout + return + } t.logFile, _ = os.Create(path) } } diff --git a/internal/app/darktile/termutil/selection.go b/internal/app/darktile/termutil/selection.go index 9c0e4bef..c4acbae9 100644 --- a/internal/app/darktile/termutil/selection.go +++ b/internal/app/darktile/termutil/selection.go @@ -1,6 +1,8 @@ package termutil func (buffer *Buffer) ClearSelection() { + buffer.selectionMu.Lock() + defer buffer.selectionMu.Unlock() buffer.selectionStart = nil buffer.selectionEnd = nil } @@ -13,6 +15,9 @@ func (buffer *Buffer) GetBoundedTextAtPosition(pos Position) (start Position, en // if the selection is invalid - e.g. lines are selected that no longer exist in the buffer func (buffer *Buffer) fixSelection() bool { + buffer.selectionMu.Lock() + defer buffer.selectionMu.Unlock() + if buffer.selectionStart == nil || buffer.selectionEnd == nil { return false } @@ -44,6 +49,9 @@ func (buffer *Buffer) ExtendSelectionToEntireLines() { return } + buffer.selectionMu.Lock() + defer buffer.selectionMu.Unlock() + buffer.selectionStart.Col = 0 buffer.selectionEnd.Col = uint16(len(buffer.lines[buffer.selectionEnd.Line].cells)) - 1 } @@ -150,6 +158,8 @@ FORWARD: } func (buffer *Buffer) SetSelectionStart(pos Position) { + buffer.selectionMu.Lock() + defer buffer.selectionMu.Unlock() buffer.selectionStart = &Position{ Col: pos.Col, Line: buffer.convertViewLineToRawLine(uint16(pos.Line)), @@ -157,10 +167,14 @@ func (buffer *Buffer) SetSelectionStart(pos Position) { } func (buffer *Buffer) setRawSelectionStart(pos Position) { + buffer.selectionMu.Lock() + defer buffer.selectionMu.Unlock() buffer.selectionStart = &pos } func (buffer *Buffer) SetSelectionEnd(pos Position) { + buffer.selectionMu.Lock() + defer buffer.selectionMu.Unlock() buffer.selectionEnd = &Position{ Col: pos.Col, Line: buffer.convertViewLineToRawLine(uint16(pos.Line)), @@ -168,6 +182,8 @@ func (buffer *Buffer) SetSelectionEnd(pos Position) { } func (buffer *Buffer) setRawSelectionEnd(pos Position) { + buffer.selectionMu.Lock() + defer buffer.selectionMu.Unlock() buffer.selectionEnd = &pos } @@ -176,6 +192,9 @@ func (buffer *Buffer) GetSelection() (string, *Selection) { return "", nil } + buffer.selectionMu.Lock() + defer buffer.selectionMu.Unlock() + start := *buffer.selectionStart end := *buffer.selectionEnd @@ -187,6 +206,9 @@ func (buffer *Buffer) GetSelection() (string, *Selection) { var text string for y := start.Line; y <= end.Line; y++ { + if y >= uint64(len(buffer.lines)) { + break + } line := buffer.lines[y] startX := 0 endX := len(line.cells) - 1 @@ -200,7 +222,13 @@ func (buffer *Buffer) GetSelection() (string, *Selection) { text += "\n" } for x := startX; x <= endX; x++ { + if x >= len(line.cells) { + break + } mr := line.cells[x].Rune() + if mr.Width == 0 { + continue + } x += mr.Width - 1 text += string(mr.Rune) } @@ -221,6 +249,8 @@ func (buffer *Buffer) InSelection(pos Position) bool { if !buffer.fixSelection() { return false } + buffer.selectionMu.Lock() + defer buffer.selectionMu.Unlock() start := *buffer.selectionStart end := *buffer.selectionEnd @@ -256,31 +286,30 @@ func (buffer *Buffer) GetHighlightAnnotation() *Annotation { return buffer.highlightAnnotation } -// takes view coords -func (buffer *Buffer) IsHighlighted(pos Position) bool { +func (buffer *Buffer) GetViewHighlight() (start Position, end Position, exists bool) { if buffer.highlightStart == nil || buffer.highlightEnd == nil { - return false + return } if buffer.highlightStart.Line >= uint64(len(buffer.lines)) { - return false + return } if buffer.highlightEnd.Line >= uint64(len(buffer.lines)) { - return false + return } if buffer.highlightStart.Col >= uint16(len(buffer.lines[buffer.highlightStart.Line].cells)) { - return false + return } if buffer.highlightEnd.Col >= uint16(len(buffer.lines[buffer.highlightEnd.Line].cells)) { - return false + return } - start := *buffer.highlightStart - end := *buffer.highlightEnd + start = *buffer.highlightStart + end = *buffer.highlightEnd if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) { swap := end @@ -288,23 +317,8 @@ func (buffer *Buffer) IsHighlighted(pos Position) bool { start = swap } - rY := buffer.convertViewLineToRawLine(uint16(pos.Line)) - if rY < start.Line { - return false - } - if rY > end.Line { - return false - } - if rY == start.Line { - if pos.Col < start.Col { - return false - } - } - if rY == end.Line { - if pos.Col > end.Col { - return false - } - } + start.Line = uint64(buffer.convertRawLineToViewLine(start.Line)) + end.Line = uint64(buffer.convertRawLineToViewLine(end.Line)) - return true + return start, end, true }