mirror of https://github.com/liamg/aminal.git
updates
This commit is contained in:
parent
f237dd0384
commit
cf2e585c91
122
gui/gui.go
122
gui/gui.go
|
@ -5,6 +5,8 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/4ydx/gltext"
|
"github.com/4ydx/gltext"
|
||||||
v41 "github.com/4ydx/gltext/v4.1"
|
v41 "github.com/4ydx/gltext/v4.1"
|
||||||
|
@ -18,13 +20,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type GUI struct {
|
type GUI struct {
|
||||||
window *glfw.Window
|
window *glfw.Window
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
config config.Config
|
config config.Config
|
||||||
font *v41.Font
|
font *v41.Font
|
||||||
terminal *terminal.Terminal
|
terminal *terminal.Terminal
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
charWidth float32
|
||||||
|
charHeight float32
|
||||||
|
texts [][]*v41.Text
|
||||||
|
textLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config config.Config, terminal *terminal.Terminal, logger *zap.SugaredLogger) *GUI {
|
func New(config config.Config, terminal *terminal.Terminal, logger *zap.SugaredLogger) *GUI {
|
||||||
|
@ -36,6 +42,7 @@ func New(config config.Config, terminal *terminal.Terminal, logger *zap.SugaredL
|
||||||
width: 600,
|
width: 600,
|
||||||
height: 300,
|
height: 300,
|
||||||
terminal: terminal,
|
terminal: terminal,
|
||||||
|
texts: [][]*v41.Text{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,14 +64,16 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
|
||||||
scaleMin, scaleMax := float32(1.0), float32(1.1)
|
scaleMin, scaleMax := float32(1.0), float32(1.1)
|
||||||
text := v41.NewText(gui.font, scaleMin, scaleMax)
|
text := v41.NewText(gui.font, scaleMin, scaleMax)
|
||||||
text.SetString("A")
|
text.SetString("A")
|
||||||
cw, ch := text.Width(), text.Height()
|
gui.charWidth, gui.charHeight = text.Width(), text.Height()
|
||||||
|
|
||||||
cols := int(math.Floor(float64(float32(width) / cw)))
|
cols := int(math.Floor(float64(float32(width) / gui.charWidth)))
|
||||||
rows := int(math.Floor(float64(float32(height) / ch)))
|
rows := int(math.Floor(float64(float32(height) / gui.charHeight)))
|
||||||
|
|
||||||
if err := gui.terminal.SetSize(cols, rows); err != nil {
|
if err := gui.terminal.SetSize(cols, rows); err != nil {
|
||||||
gui.logger.Errorf("Failed to resize terminal to %d cols, %d rows: %s", cols, rows, err)
|
gui.logger.Errorf("Failed to resize terminal to %d cols, %d rows: %s", cols, rows, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gui.createTexts()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *GUI) getTermSize() (int, int) {
|
func (gui *GUI) getTermSize() (int, int) {
|
||||||
|
@ -75,16 +84,64 @@ func (gui *GUI) getTermSize() (int, int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if the terminals cells have been updated, and updates the text objects if needed
|
// checks if the terminals cells have been updated, and updates the text objects if needed
|
||||||
func (gui *GUI) updateCells() {
|
func (gui *GUI) updateTexts() {
|
||||||
|
gui.textLock.Lock()
|
||||||
|
defer gui.textLock.Unlock()
|
||||||
|
|
||||||
|
cols, rows := gui.getTermSize()
|
||||||
|
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
for col := 0; col < cols; col++ {
|
||||||
|
|
||||||
|
r, err := gui.terminal.GetRuneAtPos(terminal.Position{Row: row, Col: col})
|
||||||
|
if err != nil {
|
||||||
|
gui.logger.Errorf("Failed to read rune: %s", err)
|
||||||
|
}
|
||||||
|
if r > 0 {
|
||||||
|
gui.texts[row][col].SetString(string(r))
|
||||||
|
gui.texts[row][col].SetColor(mgl32.Vec3{1, 1, 1})
|
||||||
|
// @todo set colour
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// builds text objects
|
// builds text objects
|
||||||
func (gui *GUI) createCells() {
|
func (gui *GUI) createTexts() {
|
||||||
|
gui.textLock.Lock()
|
||||||
|
defer gui.textLock.Unlock()
|
||||||
scaleMin, scaleMax := float32(1.0), float32(1.1)
|
scaleMin, scaleMax := float32(1.0), float32(1.1)
|
||||||
text := v41.NewText(gui.font, scaleMin, scaleMax)
|
|
||||||
text.SetString("hello")
|
cols, rows := gui.getTermSize()
|
||||||
text.SetColor(mgl32.Vec3{1, 1, 1})
|
|
||||||
text.SetPosition(mgl32.Vec2{-float32(gui.width) / 2, -float32(gui.height) / 2})
|
texts := [][]*v41.Text{}
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
|
||||||
|
if len(texts) <= row {
|
||||||
|
texts = append(texts, []*v41.Text{})
|
||||||
|
}
|
||||||
|
for col := 0; col < cols; col++ {
|
||||||
|
if len(texts[row]) <= col {
|
||||||
|
text := v41.NewText(gui.font, scaleMin, scaleMax)
|
||||||
|
|
||||||
|
if row < len(gui.texts) {
|
||||||
|
if col < len(gui.texts[row]) {
|
||||||
|
text.SetString(gui.texts[row][col].String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text.SetColor(mgl32.Vec3{1, 1, 1})
|
||||||
|
|
||||||
|
x := ((float32(col) * gui.charWidth) - (float32(gui.width) / 2)) + (gui.charWidth / 2)
|
||||||
|
y := -(((float32(row) * gui.charHeight) - (float32(gui.height) / 2)) + (gui.charHeight / 2))
|
||||||
|
|
||||||
|
text.SetPosition(mgl32.Vec2{x, y})
|
||||||
|
texts[row] = append(texts[row], text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.texts = texts
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,13 +176,22 @@ func (gui *GUI) Render() error {
|
||||||
w, h := gui.window.GetFramebufferSize()
|
w, h := gui.window.GetFramebufferSize()
|
||||||
gl.Viewport(0, 0, int32(w), int32(h))
|
gl.Viewport(0, 0, int32(w), int32(h))
|
||||||
|
|
||||||
|
gui.logger.Debugf("Starting pty read handling...")
|
||||||
|
gui.terminal.OnUpdate(func() {
|
||||||
|
gui.updateTexts()
|
||||||
|
})
|
||||||
|
go gui.terminal.Read()
|
||||||
|
|
||||||
scaleMin, scaleMax := float32(1.0), float32(1.1)
|
scaleMin, scaleMax := float32(1.0), float32(1.1)
|
||||||
text := v41.NewText(gui.font, scaleMin, scaleMax)
|
text := v41.NewText(gui.font, scaleMin, scaleMax)
|
||||||
text.SetString("hello")
|
text.SetString("")
|
||||||
text.SetColor(mgl32.Vec3{1, 1, 1})
|
text.SetColor(mgl32.Vec3{1, 0, 0})
|
||||||
text.SetPosition(mgl32.Vec2{-float32(gui.width) / 2, -float32(gui.height) / 2})
|
|
||||||
text.SetPosition(mgl32.Vec2{0, 0})
|
text.SetPosition(mgl32.Vec2{0, 0})
|
||||||
|
|
||||||
|
frames := 0
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
//gl.Disable(gl.MULTISAMPLE)
|
//gl.Disable(gl.MULTISAMPLE)
|
||||||
// stop smoothing fonts
|
// stop smoothing fonts
|
||||||
gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
||||||
|
@ -133,17 +199,35 @@ func (gui *GUI) Render() error {
|
||||||
gui.logger.Debugf("Starting render...")
|
gui.logger.Debugf("Starting render...")
|
||||||
for !gui.window.ShouldClose() {
|
for !gui.window.ShouldClose() {
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
text.SetString(fmt.Sprintf("%d fps | %d, %d", frames, gui.terminal.GetPosition().Row, gui.terminal.GetPosition().Col))
|
||||||
|
frames = 0
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
gl.UseProgram(program)
|
gl.UseProgram(program)
|
||||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
||||||
|
|
||||||
// Render the string.
|
// Render the string.
|
||||||
gui.window.SetTitle(gui.terminal.GetTitle())
|
gui.window.SetTitle(gui.terminal.GetTitle())
|
||||||
|
|
||||||
|
gui.textLock.Lock()
|
||||||
|
cols, rows := gui.getTermSize()
|
||||||
|
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
for col := 0; col < cols; col++ {
|
||||||
|
gui.texts[row][col].Draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gui.textLock.Unlock()
|
||||||
|
|
||||||
text.Draw()
|
text.Draw()
|
||||||
|
|
||||||
glfw.PollEvents()
|
glfw.PollEvents()
|
||||||
gui.window.SwapBuffers()
|
gui.window.SwapBuffers()
|
||||||
|
|
||||||
|
frames++
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.logger.Debugf("Stopping render...")
|
gui.logger.Debugf("Stopping render...")
|
||||||
|
|
2
main.go
2
main.go
|
@ -40,8 +40,6 @@ func main() {
|
||||||
sugaredLogger.Infof("Creating terminal...")
|
sugaredLogger.Infof("Creating terminal...")
|
||||||
terminal := terminal.New(pty, sugaredLogger)
|
terminal := terminal.New(pty, sugaredLogger)
|
||||||
|
|
||||||
go terminal.Read()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(time.Second * 5)
|
time.Sleep(time.Second * 5)
|
||||||
terminal.Write([]byte("tput cols && tput lines\n"))
|
terminal.Write([]byte("tput cols && tput lines\n"))
|
||||||
|
|
|
@ -17,6 +17,7 @@ type Terminal struct {
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
title string
|
title string
|
||||||
position Position
|
position Position
|
||||||
|
onUpdate []func()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Winsize struct {
|
type Winsize struct {
|
||||||
|
@ -33,9 +34,20 @@ type Position struct {
|
||||||
|
|
||||||
func New(pty *os.File, logger *zap.SugaredLogger) *Terminal {
|
func New(pty *os.File, logger *zap.SugaredLogger) *Terminal {
|
||||||
return &Terminal{
|
return &Terminal{
|
||||||
cells: [][]Cell{},
|
cells: [][]Cell{},
|
||||||
pty: pty,
|
pty: pty,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
onUpdate: []func(){},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) OnUpdate(handler func()) {
|
||||||
|
terminal.onUpdate = append(terminal.onUpdate, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) triggerOnUpdate() {
|
||||||
|
for _, handler := range terminal.onUpdate {
|
||||||
|
go handler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +61,15 @@ func (terminal *Terminal) Write(data []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) ClearToEndOfLine() error {
|
||||||
|
w, _ := terminal.GetSize()
|
||||||
|
for i := terminal.position.Col; i < w; i++ {
|
||||||
|
// @todo handle errors?
|
||||||
|
terminal.setRuneAtPos(Position{Row: terminal.position.Row, Col: i}, 0)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Read needs to be run on a goroutine, as it continually reads output to set on the terminal
|
// Read needs to be run on a goroutine, as it continually reads output to set on the terminal
|
||||||
func (terminal *Terminal) Read() error {
|
func (terminal *Terminal) Read() error {
|
||||||
|
|
||||||
|
@ -63,20 +84,21 @@ func (terminal *Terminal) Read() error {
|
||||||
|
|
||||||
if b == 0x1b { // if the byte is an escape character, read the next byte to determine which one
|
if b == 0x1b { // if the byte is an escape character, read the next byte to determine which one
|
||||||
b = <-buffer
|
b = <-buffer
|
||||||
terminal.logger.Debugf("Escape: 0x%X", b)
|
|
||||||
switch b {
|
switch b {
|
||||||
case 0x5b: // CSI: Control Sequence Introducer
|
case 0x5b: // CSI: Control Sequence Introducer ]
|
||||||
b = <-buffer
|
b = <-buffer
|
||||||
switch b {
|
switch b {
|
||||||
default:
|
case 0x4b: // K - EOL - Erase to end of line
|
||||||
terminal.logger.Debugf("Unknown CSI control sequence: 0x%X", b)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
default:
|
||||||
|
terminal.logger.Debugf("Unknown CSI control sequence: 0x%02X (%s)", b, string([]byte{b}))
|
||||||
|
}
|
||||||
case 0x5d: // OSC: Operating System Command
|
case 0x5d: // OSC: Operating System Command
|
||||||
b = <-buffer
|
b = <-buffer
|
||||||
switch b {
|
switch b {
|
||||||
case byte('0'):
|
case byte('0'):
|
||||||
if <-buffer == byte(';') {
|
b = <-buffer
|
||||||
|
if b == byte(';') {
|
||||||
title := []byte{}
|
title := []byte{}
|
||||||
for {
|
for {
|
||||||
b = <-buffer
|
b = <-buffer
|
||||||
|
@ -88,17 +110,27 @@ func (terminal *Terminal) Read() error {
|
||||||
terminal.logger.Debugf("Terminal title set to: %s", string(title))
|
terminal.logger.Debugf("Terminal title set to: %s", string(title))
|
||||||
terminal.title = string(title)
|
terminal.title = string(title)
|
||||||
} else {
|
} else {
|
||||||
terminal.logger.Debugf("Invalid OSC 0 control sequence")
|
terminal.logger.Debugf("Invalid OSC 0 control sequence: 0x%02X", b)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
terminal.logger.Debugf("Unknown OSC control sequence: 0x%X", b)
|
terminal.logger.Debugf("Unknown OSC control sequence: 0x%02X", b)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
terminal.logger.Debugf("Unknown control sequence: 0x%X", b)
|
terminal.logger.Debugf("Unknown control sequence: 0x%02X", b)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// render character at current location
|
|
||||||
terminal.writeRune([]rune(string([]byte{b}))[0])
|
switch b {
|
||||||
|
case 0x0a:
|
||||||
|
terminal.newLine()
|
||||||
|
case 0x0d:
|
||||||
|
terminal.position.Col = 0
|
||||||
|
default:
|
||||||
|
// render character at current location
|
||||||
|
// fmt.Printf("%s\n", string([]byte{b}))
|
||||||
|
terminal.writeRune([]rune(string([]byte{b}))[0])
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -112,15 +144,17 @@ func (terminal *Terminal) Read() error {
|
||||||
}
|
}
|
||||||
if len(readBytes) > 0 {
|
if len(readBytes) > 0 {
|
||||||
readBytes = readBytes[:n]
|
readBytes = readBytes[:n]
|
||||||
|
fmt.Printf("Data in: %q\n", string(readBytes))
|
||||||
for _, x := range readBytes {
|
for _, x := range readBytes {
|
||||||
buffer <- x
|
buffer <- x
|
||||||
}
|
}
|
||||||
|
terminal.triggerOnUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) writeRune(r rune) error {
|
func (terminal *Terminal) writeRune(r rune) error {
|
||||||
fmt.Println(string(r))
|
|
||||||
err := terminal.setRuneAtPos(terminal.position, r)
|
err := terminal.setRuneAtPos(terminal.position, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -139,15 +173,53 @@ func (terminal *Terminal) writeRune(r rune) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) newLine() {
|
||||||
|
_, h := terminal.GetSize()
|
||||||
|
terminal.position.Col = 0
|
||||||
|
if terminal.position.Row <= h-1 {
|
||||||
|
terminal.position.Row++
|
||||||
|
} else {
|
||||||
|
panic(fmt.Errorf("Not implemented - need to shuffle all rows up one"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) Clear() {
|
||||||
|
for y := range terminal.cells {
|
||||||
|
for x := range terminal.cells[y] {
|
||||||
|
terminal.cells[y][x].rune = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminal.position = Position{0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) GetPosition() Position {
|
||||||
|
return terminal.position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) GetRuneAtPos(pos Position) (rune, error) {
|
||||||
|
if len(terminal.cells) <= pos.Row {
|
||||||
|
return 0, fmt.Errorf("Row %d does not exist", pos.Row)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(terminal.cells) < 1 || len(terminal.cells[0]) <= pos.Col {
|
||||||
|
return 0, fmt.Errorf("Col %d does not exist", pos.Col)
|
||||||
|
}
|
||||||
|
|
||||||
|
return terminal.cells[pos.Row][pos.Col].rune, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) setRuneAtPos(pos Position, r rune) error {
|
func (terminal *Terminal) setRuneAtPos(pos Position, r rune) error {
|
||||||
|
|
||||||
if len(terminal.cells) <= pos.Col {
|
if len(terminal.cells) <= pos.Row {
|
||||||
|
return fmt.Errorf("Row %d does not exist", pos.Row)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(terminal.cells) < 1 || len(terminal.cells[0]) <= pos.Col {
|
||||||
return fmt.Errorf("Col %d does not exist", pos.Col)
|
return fmt.Errorf("Col %d does not exist", pos.Col)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(terminal.cells) < 1 || len(terminal.cells[0]) <= pos.Row {
|
//fmt.Printf("%d %d %s\n", pos.Row, pos.Col, string(r))
|
||||||
return fmt.Errorf("Row %d does not exist", pos.Row)
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.cells[pos.Row][pos.Col].rune = r
|
terminal.cells[pos.Row][pos.Col].rune = r
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Reference in New Issue