add hinting

This commit is contained in:
Liam Galvin 2018-10-29 20:35:49 +00:00
parent 2907bb69eb
commit 1c96cb83d5
14 changed files with 499 additions and 78 deletions

4
Gopkg.lock generated
View File

@ -88,12 +88,12 @@
[[projects]]
branch = "master"
name = "golang.org/x/image"
packages = ["font","math/fixed"]
packages = ["bmp","font","math/fixed"]
revision = "991ec62608f3c0da01d400756917825d1e2fd528"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "62428a3f67dc7a8a89ecb5666b6d8a6641788f89c61d0b0b4dd99982923bb0c6"
inputs-digest = "6f1eff05a1c5ca969d1f54eb8002e3213fda92b99a459e6729098b2497de5b8c"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -10,7 +10,11 @@ The project is experimental at the moment, so you probably won't want to rely on
Ensure you have your latest graphics card drivers installed before use.
Sixels are now supported.
## Contextual Hints
![Example hint](hint.png)
## Sixel Support
![Example sixel](sixel.png)
@ -55,13 +59,18 @@ Sixels are now supported.
## Keyboard Shortcuts
| Operation | Key(s) |
| ------------------ | ---------------- |
| Copy | ctrl + shift + c |
| Paste | ctrl + shift + v |
| Toggle slomo | ctrl + shift + ; |
| Interrupt (SIGINT) | ctrl + c |
| Operation | Key(s) |
| --------------------- | -------------------- |
| Select text | click + drag |
| Select word | double click |
| Select line | triple click |
| Copy | ctrl + shift + c |
| Paste | ctrl + shift + v |
| Google selected text | ctrl + shift + g |
| Report bug in aminal | ctrl + shift + r |
| Explain text | ctrl + shift + click |
| Toggle slomo | ctrl + shift + ; |
## Configuration
Aminal looks for a config file in `~/.aminal.toml`, and will write one there the first time it runs, if it doesn't already exist.

49
buffer/hint.go Normal file
View File

@ -0,0 +1,49 @@
package buffer
import (
"fmt"
"strings"
"github.com/liamg/aminal/hints"
)
func (buffer *Buffer) GetHintAtPosition(col uint16, row uint16) *hints.Hint {
cell := buffer.GetCell(col, row)
if cell == nil || cell.Rune() == 0x00 {
return nil
}
candidate := ""
for i := col; i >= 0; i-- {
cell := buffer.GetCell(i, row)
if cell == nil {
break
}
if isRuneWordSelectionMarker(cell.Rune()) {
break
}
candidate = fmt.Sprintf("%c%s", cell.Rune(), candidate)
}
trimmed := strings.TrimLeft(candidate, " ")
sx := col - uint16(len(trimmed)-1)
for i := col + 1; i < buffer.viewWidth; i++ {
cell := buffer.GetCell(i, row)
if cell == nil {
break
}
if isRuneWordSelectionMarker(cell.Rune()) {
break
}
candidate = fmt.Sprintf("%s%c", candidate, cell.Rune())
}
line := buffer.lines[buffer.convertViewLineToRawLine(row)]
return hints.Get(strings.Trim(candidate, " "), line.String(), sx, row)
}

46
gui/explain.go Normal file
View File

@ -0,0 +1,46 @@
package gui
import (
"github.com/go-gl/gl/all-core/gl"
"github.com/liamg/aminal/hints"
)
type annotation struct {
hint *hints.Hint
}
func newAnnotation(it *hints.Hint) *annotation {
return &annotation{
hint: it,
}
}
func (a *annotation) render(gui *GUI) {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
lines := gui.terminal.GetVisibleLines()
for y := 0; y < len(lines); y++ {
cells := lines[y].Cells()
for x := 0; x < len(cells); x++ {
if int(x) >= len(cells) {
break
}
cell := cells[x]
var colour *[3]float32
var alpha float32 = 0.6
if y == int(a.hint.StartY) {
if x >= int(a.hint.StartX) && x <= int(a.hint.StartX+uint16(len(a.hint.Word))) {
colour = &[3]float32{0.2, 1.0, 0.2}
alpha = 1.0
}
}
gui.renderer.DrawCellText(cell, uint(x), uint(y), alpha, colour)
}
}
gui.textbox(a.hint.StartX+1, a.hint.StartY+3, a.hint.Description)
}

View File

@ -3,6 +3,7 @@ package gui
import (
"bytes"
"fmt"
"os/exec"
"runtime"
"time"
@ -18,29 +19,32 @@ import (
)
type GUI struct {
window *glfw.Window
logger *zap.SugaredLogger
config *config.Config
terminal *terminal.Terminal
width int //window width in pixels
height int //window height in pixels
font *glfont.Font
boldFont *glfont.Font
fontScale float32
renderer *OpenGLRenderer
colourAttr uint32
mouseDown bool
window *glfw.Window
logger *zap.SugaredLogger
config *config.Config
terminal *terminal.Terminal
width int //window width in pixels
height int //window height in pixels
font *glfont.Font
boldFont *glfont.Font
fontScale float32
renderer *OpenGLRenderer
colourAttr uint32
mouseDown bool
overlay overlay
terminalAlpha float32
}
func New(config *config.Config, terminal *terminal.Terminal, logger *zap.SugaredLogger) *GUI {
return &GUI{
config: config,
logger: logger,
width: 800,
height: 600,
terminal: terminal,
fontScale: 14.0,
config: config,
logger: logger,
width: 800,
height: 600,
terminal: terminal,
fontScale: 14.0,
terminalAlpha: 1,
}
}
@ -87,14 +91,6 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
}
func (gui *GUI) glfwScrollCallback(w *glfw.Window, xoff float64, yoff float64) {
if yoff > 0 {
gui.terminal.ScrollUp(1)
} else {
gui.terminal.ScrollDown(1)
}
}
func (gui *GUI) getTermSize() (uint, uint) {
if gui.renderer == nil {
return 0, 0
@ -242,9 +238,9 @@ func (gui *GUI) Render() error {
colour = &gui.config.ColourScheme.Selection
}
gui.renderer.DrawCellBg(cell, uint(x), uint(y), cursor, colour)
gui.renderer.DrawCellBg(cell, uint(x), uint(y), cursor, colour, false)
if hasText {
gui.renderer.DrawCellText(cell, uint(x), uint(y), nil)
gui.renderer.DrawCellText(cell, uint(x), uint(y), 1.0, nil)
}
gui.renderer.DrawCellImage(cell, uint(x), uint(y))
@ -252,6 +248,8 @@ func (gui *GUI) Render() error {
}
}
gui.renderOverlay()
gui.window.SwapBuffers()
}
@ -340,3 +338,19 @@ func (gui *GUI) createProgram() (uint32, error) {
return prog, nil
}
func (gui *GUI) launchTarget(target string) {
cmd := "xdg-open"
switch runtime.GOOS {
case "darwin":
cmd = "open"
case "windows":
cmd = "start"
}
if err := exec.Command(cmd, target).Run(); err != nil {
gui.logger.Errorf("Failed to launch external command %s: %s", cmd, err)
}
}

View File

@ -2,6 +2,7 @@ package gui
import (
"fmt"
"net/url"
"github.com/go-gl/glfw/v3.2/glfw"
)
@ -22,8 +23,15 @@ func modsPressed(pressed glfw.ModifierKey, mods ...glfw.ModifierKey) bool {
}
func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
if action == glfw.Repeat || action == glfw.Press {
if gui.overlay != nil {
if key == glfw.KeyEscape {
gui.setOverlay(nil)
}
}
gui.logger.Debugf("KEY PRESS: key=0x%X scan=0x%X", key, scancode)
modStr := ""
@ -39,6 +47,13 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti
case glfw.KeyC:
gui.window.SetClipboardString(gui.terminal.ActiveBuffer().GetSelectedText())
return
case glfw.KeyG:
keywords := gui.terminal.ActiveBuffer().GetSelectedText()
if keywords != "" {
gui.launchTarget(fmt.Sprintf("https://www.google.com/search?q=%s", url.QueryEscape(keywords)))
}
case glfw.KeyR:
gui.launchTarget("https://github.com/liamg/aminal/issues/new/choose")
case glfw.KeyV:
if s, err := gui.window.GetClipboardString(); err == nil {
_ = gui.terminal.Paste([]byte(s))

View File

@ -3,20 +3,39 @@ package gui
import (
"fmt"
"math"
"os/exec"
"runtime"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/liamg/aminal/terminal"
)
func (gui *GUI) glfwScrollCallback(w *glfw.Window, xoff float64, yoff float64) {
if yoff > 0 {
gui.terminal.ScrollUp(1)
} else {
gui.terminal.ScrollDown(1)
}
}
func (gui *GUI) mouseMoveCallback(w *glfw.Window, xpos float64, ypos float64) {
px, py := w.GetCursorPos()
x := uint16(math.Floor((px - float64(gui.renderer.areaX)) / float64(gui.renderer.CellWidth())))
y := uint16(math.Floor((py - float64(gui.renderer.areaY)) / float64(gui.renderer.CellHeight())))
if gui.mouseDown {
gui.terminal.ActiveBuffer().EndSelection(x, y, false)
} else {
if gui.terminal.UsingMainBuffer() {
hint := gui.terminal.ActiveBuffer().GetHintAtPosition(x, y)
if hint != nil {
gui.setOverlay(newAnnotation(hint))
} else {
gui.setOverlay(nil)
}
}
}
if url := gui.terminal.ActiveBuffer().GetURLAtPosition(x, y); url != "" {
@ -26,24 +45,15 @@ func (gui *GUI) mouseMoveCallback(w *glfw.Window, xpos float64, ypos float64) {
}
}
func (gui *GUI) launchTarget(target string) {
cmd := "xdg-open"
switch runtime.GOOS {
case "darwin":
cmd = "open"
case "windows":
cmd = "start"
}
if err := exec.Command(cmd, target).Run(); err != nil {
gui.logger.Errorf("Failed to launch external command %s: %s", cmd, err)
}
}
func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
if gui.overlay != nil {
if button == glfw.MouseButtonRight && action == glfw.Release {
gui.setOverlay(nil)
}
return
}
// before we forward clicks on (below), we need to handle them locally for url clicking, text highlighting etc.
px, py := w.GetCursorPos()
x := uint16(math.Floor((px - float64(gui.renderer.areaX)) / float64(gui.renderer.CellWidth())))
@ -51,14 +61,17 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act
tx := int(x) + 1 // vt100 is 1 indexed
ty := int(y) + 1
if action == glfw.Press {
gui.mouseDown = true
gui.terminal.ActiveBuffer().StartSelection(x, y)
} else if action == glfw.Release {
gui.mouseDown = false
gui.terminal.ActiveBuffer().EndSelection(x, y, true)
if url := gui.terminal.ActiveBuffer().GetURLAtPosition(x, y); url != "" {
go gui.launchTarget(url)
if button == glfw.MouseButtonLeft {
if action == glfw.Press {
gui.mouseDown = true
gui.terminal.ActiveBuffer().StartSelection(x, y)
} else if action == glfw.Release {
gui.mouseDown = false
gui.terminal.ActiveBuffer().EndSelection(x, y, true)
if url := gui.terminal.ActiveBuffer().GetURLAtPosition(x, y); url != "" {
go gui.launchTarget(url)
}
}
}
// https://www.xfree86.org/4.8.0/ctlseqs.html

18
gui/overlays.go Normal file
View File

@ -0,0 +1,18 @@
package gui
type overlay interface {
render(gui *GUI)
}
func (gui *GUI) setOverlay(m overlay) {
defer gui.terminal.SetDirty()
gui.overlay = m
}
func (gui *GUI) renderOverlay() {
if gui.overlay == nil || !gui.terminal.UsingMainBuffer() {
return
}
gui.overlay.render(gui)
}

View File

@ -195,7 +195,7 @@ func (r *OpenGLRenderer) DrawCursor(col uint, row uint, colour config.Colour) {
rect.Draw()
}
func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor bool, colour *config.Colour) {
func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor bool, colour *config.Colour, force bool) {
var bg [3]float32
@ -212,7 +212,7 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor
}
}
if bg != r.config.ColourScheme.Background {
if bg != r.config.ColourScheme.Background || force {
rect := r.getRectangle(col, row)
rect.setColour(bg)
rect.Draw()
@ -220,23 +220,20 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor
}
func (r *OpenGLRenderer) DrawCellText(cell buffer.Cell, col uint, row uint, colour *config.Colour) {
func (r *OpenGLRenderer) DrawCellText(cell buffer.Cell, col uint, row uint, alpha float32, colour *[3]float32) {
var fg [3]float32
if colour != nil {
fg = *colour
} else if cell.Attr().Reverse {
fg = cell.Bg()
} else {
if cell.Attr().Reverse {
fg = cell.Bg()
} else {
fg = cell.Fg()
}
fg = cell.Fg()
}
var alpha float32 = 1
if cell.Attr().Dim {
alpha = 0.5
alpha = 0.5 * alpha
}
r.font.SetColor(fg[0], fg[1], fg[2], alpha)

97
gui/textbox.go Normal file
View File

@ -0,0 +1,97 @@
package gui
import (
"fmt"
"github.com/liamg/aminal/buffer"
)
func (gui *GUI) textbox(col uint16, row uint16, text string) {
lines := []string{}
line := ""
word := ""
maxWidth := int(gui.terminal.ActiveBuffer().ViewWidth()) - 4
maxHeight := (int(gui.terminal.ActiveBuffer().ViewHeight()) / 2) - 2
if maxHeight < 1 {
return
}
longestLine := 0
addWord := func() {
if len(line)+len(word) <= maxWidth {
line = fmt.Sprintf("%s%s", line, word)
if len(line) < maxWidth {
line = fmt.Sprintf("%s ", line)
} else {
lines = append(lines, line)
line = ""
}
} else {
lines = append(lines, line)
line = word
for len(line) > maxWidth {
// break word into bits
}
}
word = ""
}
addLine := func() bool {
addWord()
if len(line) > longestLine {
longestLine = len(line)
}
lines = append(lines, line)
if len(lines) >= maxHeight-1 {
lines = append(lines, "...")
return true
}
line = ""
return false
}
var done = false
DONE:
for _, c := range text {
switch c {
case 0x0d:
continue
case 0x0a:
if done = addLine(); done {
break DONE
}
case ' ':
addWord()
default:
word = fmt.Sprintf("%s%c", word, c)
}
}
if word != "" {
addWord()
}
if line != "" && !done {
addLine()
}
for hx := col; hx < col+uint16(longestLine)+2; hx++ {
for hy := row - 1; hy < row+uint16(len(lines))+1; hy++ {
gui.renderer.DrawCellBg(buffer.NewBackgroundCell([3]float32{0, 0, 0}), uint(hx), uint(hy), false, nil, true)
}
}
x := (float32(col) * gui.renderer.cellWidth)
gui.font.SetColor(0.2, 1, 0.2, 1)
for i, line := range lines {
y := (float32(row+1+uint16(i)) * gui.renderer.cellHeight) - (gui.font.LinePadding())
gui.font.Print(x, y, fmt.Sprintf(" %s", line))
}
}

BIN
hint.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

22
hints/hint.go Normal file
View File

@ -0,0 +1,22 @@
package hints
type Hint struct {
Word string
StartX uint16
StartY uint16
Line string
Description string
}
type hinter func(word string, context string, wordX uint16, wordY uint16) *Hint
var hinters = []hinter{}
func Get(word string, context string, wordX uint16, wordY uint16) *Hint {
for _, exp := range hinters {
if h := exp(word, context, wordX, wordY); h != nil {
return h
}
}
return nil
}

122
hints/perms.go Normal file
View File

@ -0,0 +1,122 @@
package hints
import (
"fmt"
"regexp"
"strings"
)
type perms struct {
IsDirectory bool
Owner access
Group access
World access
}
type access struct {
Read bool
Write bool
Execute bool
}
func (p perms) Numeric() string {
return p.Owner.Numeric() + p.Group.Numeric() + p.World.Numeric()
}
func (a access) Nice() string {
all := []string{}
if a.Read {
all = append(all, "read")
}
if a.Write {
all = append(all, "write")
}
if a.Execute {
all = append(all, "execute")
}
return strings.Join(all, ", ")
}
func (a access) Numeric() string {
var n uint8
if a.Read {
n += 4
}
if a.Write {
n += 2
}
if a.Execute {
n++
}
return fmt.Sprintf("%d", n)
}
func parsePermissionString(s string) (perms, error) {
if !isPermString(s) {
return perms{}, fmt.Errorf("Invalid permission string")
}
p := perms{}
p.IsDirectory = s[0] == 'd'
p.Owner.Read = s[1] == 'r'
p.Owner.Write = s[2] == 'w'
p.Owner.Execute = s[3] == 'x'
p.Group.Read = s[4] == 'r'
p.Group.Write = s[5] == 'w'
p.Group.Execute = s[6] == 'x'
p.World.Read = s[7] == 'r'
p.World.Write = s[8] == 'w'
p.World.Execute = s[9] == 'x'
return p, nil
}
func init() {
hinters = append(hinters, hintPerms)
}
func hintPerms(word string, context string, wordX uint16, wordY uint16) *Hint {
item := &Hint{
Line: context,
Word: word,
StartX: wordX,
StartY: wordY,
}
if wordX == 0 {
p, err := parsePermissionString(word)
if err != nil {
return nil
}
typ := "file"
if p.IsDirectory {
typ = "directory"
}
item.Description = fmt.Sprintf(`Permissions:
Type: %s
Numeric: %s
Owner: %s
Group: %s
World: %s`,
typ,
p.Numeric(),
p.Owner.Nice(),
p.Group.Nice(),
p.World.Nice(),
)
return item
}
return nil
}
func isPermString(s string) bool {
re := regexp.MustCompile("[dl\\-sS]{1}[sSrwx\\-]{9}")
return re.MatchString(s)
}

View File

@ -16,8 +16,9 @@ import (
)
const (
MainBuffer uint8 = 0
AltBuffer uint8 = 1
MainBuffer uint8 = 0
AltBuffer uint8 = 1
InternalBuffer uint8 = 2
)
type MouseMode uint
@ -50,6 +51,7 @@ type Terminal struct {
isDirty bool
charWidth float32
charHeight float32
lastBuffer uint8
}
type Modes struct {
@ -77,6 +79,10 @@ func New(pty *os.File, logger *zap.SugaredLogger, config *config.Config) *Termin
FgColour: config.ColourScheme.Foreground,
BgColour: config.ColourScheme.Background,
}),
buffer.NewBuffer(1, 1, buffer.CellAttributes{
FgColour: config.ColourScheme.Foreground,
BgColour: config.ColourScheme.Background,
}),
},
pty: pty,
logger: logger,
@ -131,16 +137,30 @@ func (terminal *Terminal) UseAltBuffer() {
terminal.SetSize(uint(terminal.size.Width), uint(terminal.size.Height))
}
func (terminal *Terminal) UseInternalBuffer() {
terminal.pauseChan <- true
terminal.activeBufferIndex = InternalBuffer
terminal.SetSize(uint(terminal.size.Width), uint(terminal.size.Height))
}
func (terminal *Terminal) ExitInternalBuffer() {
terminal.activeBufferIndex = terminal.lastBuffer
terminal.resumeChan <- true
}
func (terminal *Terminal) ActiveBuffer() *buffer.Buffer {
return terminal.buffers[terminal.activeBufferIndex]
}
func (terminal *Terminal) UsingMainBuffer() bool {
return terminal.activeBufferIndex == MainBuffer
}
func (terminal *Terminal) GetScrollOffset() uint {
return terminal.ActiveBuffer().GetScrollOffset()
}
func (terminal *Terminal) ScrollDown(lines uint16) {
terminal.logger.Infof("Scrolling down %d", lines)
terminal.ActiveBuffer().ScrollDown(lines)
}
@ -151,7 +171,6 @@ func (terminal *Terminal) SetCharSize(w float32, h float32) {
}
func (terminal *Terminal) ScrollUp(lines uint16) {
terminal.logger.Infof("Scrolling up %d", lines)
terminal.ActiveBuffer().ScrollUp(lines)
}