Add support for ligatures, cursor shapes (and images) (#304)
This commit is contained in:
parent
c18b702b61
commit
765a781055
|
@ -1,2 +1,3 @@
|
|||
.idea
|
||||
.vscode
|
||||
/darktile
|
||||
|
|
26
README.md
26
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)
|
||||
|
||||
<p align="center">
|
||||
<img src="cursor.gif">
|
||||
</p>
|
||||
|
||||
## 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
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
|
@ -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")
|
||||
|
|
|
@ -12,12 +12,18 @@ import (
|
|||
type Config struct {
|
||||
Opacity float64
|
||||
Font Font
|
||||
Cursor Cursor
|
||||
}
|
||||
|
||||
type Font struct {
|
||||
Family string
|
||||
Size float64
|
||||
DPI float64
|
||||
Ligatures bool
|
||||
}
|
||||
|
||||
type Cursor struct {
|
||||
Image string
|
||||
}
|
||||
|
||||
type ErrorFileNotFound struct {
|
||||
|
|
|
@ -14,6 +14,7 @@ var defaultConfig = Config{
|
|||
Family: "", // internally packed font will be loaded by default
|
||||
Size: 18.0,
|
||||
DPI: 72.0,
|
||||
Ligatures: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -65,6 +60,7 @@ func New(terminal *termutil.Terminal, options ...Option) (*GUI, error) {
|
|||
fontManager: font.NewManager(),
|
||||
activeHinter: -1,
|
||||
keyState: newKeyState(),
|
||||
enableLigatures: true,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package popup
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Text string
|
||||
Expiry time.Time
|
||||
Foreground color.Color
|
||||
Background color.Color
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,14 +109,21 @@ func (t *Terminal) handleCSI(readChan chan MeasuredRune) (renderRequired bool) {
|
|||
return t.csiSoftResetHandler(params)
|
||||
}
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue