diff --git a/.gitignore b/.gitignore
index 5ed0bb8..cf7a9a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.idea
+.vscode
/darktile
diff --git a/README.md b/README.md
index dc51b8e..2273106 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 0000000..ec66f63
Binary files /dev/null and b/cursor.gif differ
diff --git a/internal/app/darktile/cmd/root.go b/internal/app/darktile/cmd/root.go
index 0df4279..1ed007e 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 044c29f..0c7d97e 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 e225c5b..55d2183 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 1d93780..48673c9 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 63b436b..cbc64b7 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 6cd159c..7122c5a 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 7a0fe50..2f26f93 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 0000000..83ae5f2
--- /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 b2c77d6..2c732d7 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 0000000..2a461b8
--- /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 0000000..40490c5
--- /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 0000000..ba66aaf
--- /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 0000000..9be18b3
--- /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 0000000..c722580
--- /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 0000000..359e069
--- /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 0000000..3ddbc14
--- /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 0000000..b7c7b64
--- /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 0000000..18c75d5
--- /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 36ad019..a546eaa 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 5b38bd5..7c7ed6d 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 757b67c..df2471d 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 9667199..501e1a3 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 9c0e4be..c4acbae 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
}