From 000e763a61cd36a896d5eea8adc2079fa90207e5 Mon Sep 17 00:00:00 2001 From: nikitar020 <42252263+nikitar020@users.noreply.github.com> Date: Mon, 18 Mar 2019 17:49:37 +0200 Subject: [PATCH 1/6] Make shells running on Windows (#260) --- platform/winpty.go | 43 ++----------------------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/platform/winpty.go b/platform/winpty.go index 4bc1dc2..386ec79 100644 --- a/platform/winpty.go +++ b/platform/winpty.go @@ -4,12 +4,8 @@ package platform import ( "errors" - "syscall" - "time" - - "fmt" - "github.com/MaxRis/w32" + "syscall" ) // #include "windows.h" @@ -144,42 +140,7 @@ func (pty *winConPty) CreateGuestProcess(imagePath string) (Process, error) { pty.processID = process.processID - err = setupChildConsole(C.DWORD(process.processID), C.STD_OUTPUT_HANDLE, C.ENABLE_PROCESSED_OUTPUT|C.ENABLE_WRAP_AT_EOL_OUTPUT) - if err != nil { - process.Close() - return nil, err - } - - return process, err -} - -func setupChildConsole(processID C.DWORD, nStdHandle C.DWORD, mode uint) error { - C.FreeConsole() - defer C.AttachConsole(^C.DWORD(0)) // attach to parent process console - - // process may not be ready so we'll do retries - const maxWaitMilliSeconds = 5000 - const waitStepMilliSeconds = 200 - count := maxWaitMilliSeconds / waitStepMilliSeconds - - for { - if r := C.AttachConsole(processID); r != 0 { - break // success - } - lastError := C.GetLastError() - if lastError != C.ERROR_GEN_FAILURE || count <= 0 { - return fmt.Errorf("Was not able to attach to the child prosess' console") - } - - time.Sleep(time.Millisecond * time.Duration(waitStepMilliSeconds)) - count-- - } - - h := C.GetStdHandle(nStdHandle) - C.SetConsoleMode(h, C.DWORD(mode)) - C.FreeConsole() - - return nil + return process, nil } func (pty *winConPty) Resize(x, y int) error { From c2a7be2aeb726a706c600b649b3657bcd996c47a Mon Sep 17 00:00:00 2001 From: nikitar020 <42252263+nikitar020@users.noreply.github.com> Date: Tue, 19 Mar 2019 19:57:17 +0200 Subject: [PATCH 2/6] Implementation of the vertical scrollbar (#229) * Implementation of the vertical scrollbar (also enable gofmt checks only for go1.11.x builds) --- README.md | 33 +-- buffer/buffer.go | 15 +- config/config.go | 1 + config/defaults.go | 1 + gui/fonts.go | 10 +- gui/gui.go | 125 +++++++--- gui/mouse.go | 95 ++++++- gui/rectangleRenderer.go | 157 ++++++++++++ gui/renderer.go | 150 ++--------- gui/scrollbar.go | 519 +++++++++++++++++++++++++++++++++++++++ main.go | 1 + main_test.go | 3 + 12 files changed, 923 insertions(+), 187 deletions(-) create mode 100644 gui/rectangleRenderer.go create mode 100644 gui/scrollbar.go diff --git a/README.md b/README.md index bfe5e8a..26d14ff 100644 --- a/README.md +++ b/README.md @@ -114,28 +114,29 @@ shell = "/bin/bash" # The shell to run for the terminal session. Default search_url = "https://www.google.com/search?q=$QUERY" # The search engine to use for the "search selected text" action. Defaults to google. Set this to your own search url using $QUERY as the keywords to replace when searching. max_lines = 1000 # Maximum number of lines in the terminal buffer. copy_and_paste_with_mouse = true # Text selected with the mouse is copied to the clipboard on end selection, and is pasted on right mouse button click. +show_vertical_scrollbar = true # Whether to show the vertical scrollbar dpi-scale = 0.0 # Override DPI scale. Defaults to 0.0 (let Aminal determine the DPI scale itself). [colours] cursor = "#e8dfd6" foreground = "#e8dfd6" background = "#021b21" - black = "#032c36" - red = "#c2454e" - green = "#7cbf9e" - yellow = "#8a7a63" - blue = "#065f73" - magenta = "#ff5879" - cyan = "#44b5b1" - light_grey = "#f2f1b9" - dark_grey = "#3e4360" - light_red = "#ef5847" - light_green = "#a2db91" - light_yellow = "#beb090" - light_blue = "#61778d" - light_magenta = "#ff99a1" - light_cyan = "#9ed9d8" - white = "#f6f6c9" + black = "#000000" + red = "#800000" + green = "#008000" + yellow = "#808000" + blue = "#000080" + magenta = "#800080" + cyan = "#008080" + light_grey = "#f2f2f2" + dark_grey = "#808080" + light_red = "#ff0000" + light_green = "#00ff00" + light_yellow = "#ffff00" + light_blue = "#0000ff" + light_magenta = "#ff00ff" + light_cyan = "#00ffff" + white = "#ffffff" selection = "#333366" # Mouse selection background colour [keys] diff --git a/buffer/buffer.go b/buffer/buffer.go index 201931a..7ec9f93 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -518,6 +518,15 @@ func (buffer *Buffer) convertRawLineToViewLine(rawLine uint64) uint16 { return uint16(int(rawLine) - (rawHeight - int(buffer.terminalState.viewHeight))) } +func (buffer *Buffer) GetVPosition() int { + result := int(uint(buffer.Height()) - uint(buffer.ViewHeight()) - buffer.terminalState.scrollLinesFromBottom) + if result < 0 { + result = 0 + } + + return result +} + // Width returns the width of the buffer in columns func (buffer *Buffer) Width() uint16 { return buffer.terminalState.viewWidth @@ -545,7 +554,7 @@ func (buffer *Buffer) insertLine() { if !buffer.InScrollableRegion() { pos := buffer.RawLine() - maxLines := buffer.getMaxLines() + maxLines := buffer.GetMaxLines() newLineCount := uint64(len(buffer.lines) + 1) if newLineCount > maxLines { newLineCount = maxLines @@ -641,7 +650,7 @@ func (buffer *Buffer) Index() { if buffer.terminalState.cursorY >= buffer.ViewHeight()-1 { buffer.lines = append(buffer.lines, newLine()) - maxLines := buffer.getMaxLines() + maxLines := buffer.GetMaxLines() if uint64(len(buffer.lines)) > maxLines { copy(buffer.lines, buffer.lines[uint64(len(buffer.lines))-maxLines:]) buffer.lines = buffer.lines[:maxLines] @@ -1115,7 +1124,7 @@ func (buffer *Buffer) ResizeView(width uint16, height uint16) { buffer.terminalState.ResetVerticalMargins() } -func (buffer *Buffer) getMaxLines() uint64 { +func (buffer *Buffer) GetMaxLines() uint64 { result := buffer.terminalState.maxLines if result < uint64(buffer.terminalState.viewHeight) { result = uint64(buffer.terminalState.viewHeight) diff --git a/config/config.go b/config/config.go index 11add72..dff0785 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ type Config struct { SearchURL string `toml:"search_url"` MaxLines uint64 `toml:"max_lines"` CopyAndPasteWithMouse bool `toml:"copy_and_paste_with_mouse"` + ShowVerticalScrollbar bool `toml:"show_vertical_scrollbar"` // Developer options. DebugMode bool `toml:"debug"` diff --git a/config/defaults.go b/config/defaults.go index 31eb279..6a62c78 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -39,6 +39,7 @@ func DefaultConfig() *Config { SearchURL: "https://www.google.com/search?q=$QUERY", MaxLines: 1000, CopyAndPasteWithMouse: true, + ShowVerticalScrollbar: true, } } diff --git a/gui/fonts.go b/gui/fonts.go index 3e0aaf6..911be0e 100644 --- a/gui/fonts.go +++ b/gui/fonts.go @@ -8,14 +8,14 @@ import ( "github.com/liamg/aminal/glfont" ) -func (gui *GUI) getPackedFont(name string) (*glfont.Font, error) { +func (gui *GUI) getPackedFont(name string, actualWidth int, actualHeight int) (*glfont.Font, error) { box := packr.NewBox("./packed-fonts") fontBytes, err := box.Find(name) if err != nil { return nil, fmt.Errorf("packaged font '%s' could not be read: %s", name, err) } - font, err := glfont.LoadFont(bytes.NewReader(fontBytes), gui.fontScale*gui.dpiScale/gui.scale(), gui.width, gui.height) + font, err := glfont.LoadFont(bytes.NewReader(fontBytes), gui.fontScale*gui.dpiScale/gui.scale(), actualWidth, actualHeight) if err != nil { return nil, fmt.Errorf("font '%s' failed to load: %v", name, err) } @@ -23,16 +23,16 @@ func (gui *GUI) getPackedFont(name string) (*glfont.Font, error) { return font, nil } -func (gui *GUI) loadFonts() error { +func (gui *GUI) loadFonts(actualWidth int, actualHeight int) error { // from https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/Hack - defaultFont, err := gui.getPackedFont("Hack Regular Nerd Font Complete.ttf") + defaultFont, err := gui.getPackedFont("Hack Regular Nerd Font Complete.ttf", actualWidth, actualHeight) if err != nil { return err } - boldFont, err := gui.getPackedFont("Hack Bold Nerd Font Complete.ttf") + boldFont, err := gui.getPackedFont("Hack Bold Nerd Font Complete.ttf", actualWidth, actualHeight) if err != nil { return err } diff --git a/gui/gui.go b/gui/gui.go index be228f5..7c6b9af 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -30,6 +30,18 @@ import ( const wakePeriod = time.Second / 120 const halfWakePeriod = wakePeriod / 2 +const ( + DefaultWindowWidth = 800 + DefaultWindowHeight = 600 +) + +type mouseEventsHandler interface { + mouseMoveCallback(g *GUI, px float64, py float64) + mouseButtonCallback(g *GUI, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey, mouseX float64, mouseY float64) + cursorEnterCallback(g *GUI, enter bool) + isMouseInside(px float64, py float64) bool +} + type GUI struct { window *glfw.Window logger *zap.SugaredLogger @@ -63,8 +75,15 @@ type GUI struct { leftClickTime time.Time leftClickCount int // number of clicks in a serie - single click, double click, or triple click mouseMovedAfterSelectionStarted bool - internalResize bool - selectionRegionMode buffer.SelectionRegionMode + + catchedMouseHandler mouseEventsHandler + mouseCatchedOnButton glfw.MouseButton + prevMouseEventHandler mouseEventsHandler + + internalResize bool + selectionRegionMode buffer.SelectionRegionMode + + vScrollbar *scrollbar mainThreadFunc chan func() } @@ -156,24 +175,33 @@ func New(config *config.Config, terminal *terminal.Terminal, logger *zap.Sugared } return &GUI{ - config: config, - logger: logger, - width: 800, - height: 600, - appliedWidth: 0, - appliedHeight: 0, - dpiScale: 1, - terminal: terminal, - fontScale: 10.0, - terminalAlpha: 1, - keyboardShortcuts: shortcuts, - resizeLock: &sync.Mutex{}, - internalResize: false, + config: config, + logger: logger, + width: DefaultWindowWidth, + height: DefaultWindowHeight, + appliedWidth: 0, + appliedHeight: 0, + dpiScale: 1, + terminal: terminal, + fontScale: 10.0, + terminalAlpha: 1, + keyboardShortcuts: shortcuts, + resizeLock: &sync.Mutex{}, + internalResize: false, + vScrollbar: nil, + catchedMouseHandler: nil, mainThreadFunc: make(chan func()), }, nil } +func (gui *GUI) Free() { + if gui.vScrollbar != nil { + gui.vScrollbar.Free() + gui.vScrollbar = nil + } +} + // inspired by https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-1-hello-opengl func (gui *GUI) scale() float32 { @@ -208,15 +236,20 @@ func (gui *GUI) resizeToTerminal() { gui.logger.Debugf("Initiating GUI resize to columns=%d rows=%d", newCols, newRows) gui.logger.Debugf("Calculating size...") - width, height := gui.renderer.GetRectangleSize(newCols, newRows) + width, height := gui.renderer.ConvertCoordinates(newCols, newRows) roundedWidth := int(math.Ceil(float64(width))) roundedHeight := int(math.Ceil(float64(height))) + if gui.vScrollbar != nil { + roundedWidth += int(gui.vScrollbar.position.width()) + } + gui.resizeCache = &ResizeCache{roundedWidth, roundedHeight, newCols, newRows} gui.logger.Debugf("Resizing window to %dx%d", roundedWidth, roundedHeight) gui.internalResize = true + gui.window.SetSize(roundedWidth, roundedHeight) // will trigger resize() gui.internalResize = false } @@ -278,14 +311,20 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) { gui.width = width gui.height = height - gui.appliedWidth = width - gui.appliedHeight = height + gui.appliedWidth = gui.width + gui.appliedHeight = gui.height + + vScrollbarWidth := 0 + if gui.vScrollbar != nil { + gui.vScrollbar.resize(gui) + vScrollbarWidth = int(gui.vScrollbar.position.width()) + } gui.logger.Debugf("Updating font resolutions...") - gui.loadFonts() + gui.loadFonts(gui.width, gui.height) gui.logger.Debugf("Setting renderer area...") - gui.renderer.SetArea(0, 0, gui.width, gui.height) + gui.renderer.SetArea(0, 0, gui.width-vScrollbarWidth, gui.height) if gui.resizeCache != nil && gui.resizeCache.Width == width && gui.resizeCache.Height == height { gui.logger.Debugf("No need to resize internal terminal!") @@ -338,14 +377,14 @@ func (gui *GUI) Render() error { gui.logger.Debugf("Creating window...") var err error gui.window, err = gui.createWindow() - gui.SetDPIScale() - gui.window.SetSize(int(float32(gui.width)*gui.dpiScale), - int(float32(gui.height)*gui.dpiScale)) if err != nil { return fmt.Errorf("Failed to create window: %s", err) } defer glfw.Terminate() + gui.SetDPIScale() + gui.window.SetSize(int(float32(gui.width)*gui.dpiScale), int(float32(gui.height)*gui.dpiScale)) + gui.logger.Debugf("Initialising OpenGL and creating program...") program, err := gui.createProgram() if err != nil { @@ -355,8 +394,19 @@ func (gui *GUI) Render() error { gui.colourAttr = uint32(gl.GetAttribLocation(program, gl.Str("inColour\x00"))) gl.BindFragDataLocation(program, 0, gl.Str("outColour\x00")) + vScrollbarWidth := 0 + if gui.config.ShowVerticalScrollbar { + vScrollbar, err := newScrollbar() + if err != nil { + return err + } + gui.vScrollbar = vScrollbar + gui.vScrollbar.resize(gui) + vScrollbarWidth = int(gui.vScrollbar.position.width()) + } + gui.logger.Debugf("Loading font...") - if err := gui.loadFonts(); err != nil { + if err := gui.loadFonts(gui.width, gui.height); err != nil { return fmt.Errorf("Failed to load font: %s", err) } @@ -364,14 +414,18 @@ func (gui *GUI) Render() error { resizeChan := make(chan bool, 1) reverseChan := make(chan bool, 1) - gui.renderer = NewOpenGLRenderer(gui.config, gui.fontMap, 0, 0, gui.width, gui.height, gui.colourAttr, program) + gui.renderer, err = NewOpenGLRenderer(gui.config, gui.fontMap, 0, 0, gui.width-vScrollbarWidth, gui.height, gui.colourAttr, program) + if err != nil { + return err + } gui.window.SetFramebufferSizeCallback(gui.resize) gui.window.SetKeyCallback(gui.key) gui.window.SetCharCallback(gui.char) gui.window.SetScrollCallback(gui.glfwScrollCallback) - gui.window.SetMouseButtonCallback(gui.mouseButtonCallback) - gui.window.SetCursorPosCallback(gui.mouseMoveCallback) + gui.window.SetMouseButtonCallback(gui.globalMouseButtonCallback) + gui.window.SetCursorPosCallback(gui.globalMouseMoveCallback) + gui.window.SetCursorEnterCallback(gui.globalCursorEnterCallback) gui.window.SetRefreshCallback(func(w *glfw.Window) { gui.terminal.NotifyDirty() }) @@ -404,6 +458,10 @@ func (gui *GUI) Render() error { gui.Close() }() + if gui.vScrollbar != nil { + gui.vScrollbar.resize(gui) + } + gui.logger.Debugf("Starting render...") gl.UseProgram(program) @@ -677,8 +735,9 @@ func (gui *GUI) renderTerminalData(shouldLock bool) { gui.renderer.DrawUnderline(span, uint(x-span), uint(y), colour) } } - } + + gui.renderScrollbar() } func (gui *GUI) redraw(shouldLock bool) { @@ -832,6 +891,16 @@ func (gui *GUI) monitorChangeCallback(monitor *glfw.Monitor, event glfw.MonitorE gui.SetDPIScale() } +func (gui *GUI) renderScrollbar() { + if gui.vScrollbar != nil { + position := gui.terminal.ActiveBuffer().GetVPosition() + maxPosition := int(gui.terminal.ActiveBuffer().GetMaxLines()) - int(gui.terminal.ActiveBuffer().ViewHeight()) + + gui.vScrollbar.setPosition(maxPosition, position) + gui.vScrollbar.render(gui) + } +} + // Synchronously executes the argument function in the main thread. // Does not return until f() executed! func (gui *GUI) executeInMainThread(f func() error) error { diff --git a/gui/mouse.go b/gui/mouse.go index 14c71c5..19703e9 100644 --- a/gui/mouse.go +++ b/gui/mouse.go @@ -36,7 +36,86 @@ func (gui *GUI) getArrowCursor() *glfw.Cursor { return gui.arrowCursor } -func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) { +func (gui *GUI) scaleMouseCoordinates(px float64, py float64) (float64, float64) { + scale := float64(gui.scale()) + px = px / scale + py = py / scale + + return px, py +} + +func (gui *GUI) globalMouseMoveCallback(w *glfw.Window, px float64, py float64) { + px, py = gui.scaleMouseCoordinates(px, py) + + if gui.catchedMouseHandler != nil { + gui.catchedMouseHandler.mouseMoveCallback(gui, px, py) + } else { + if gui.isMouseInside(px, py) { + if gui.prevMouseEventHandler != gui { + if gui.prevMouseEventHandler != nil { + gui.prevMouseEventHandler.cursorEnterCallback(gui, false) + } + gui.cursorEnterCallback(gui, true) + } + gui.mouseMoveCallback(gui, px, py) + gui.prevMouseEventHandler = gui + } else if gui.vScrollbar != nil && gui.vScrollbar.isMouseInside(px, py) { + if gui.prevMouseEventHandler != gui.vScrollbar { + if gui.prevMouseEventHandler != nil { + gui.prevMouseEventHandler.cursorEnterCallback(gui, false) + } + gui.vScrollbar.cursorEnterCallback(gui, true) + } + gui.vScrollbar.mouseMoveCallback(gui, px, py) + gui.prevMouseEventHandler = gui.vScrollbar + } + } +} + +func (gui *GUI) globalMouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) { + mouseX, mouseY := gui.scaleMouseCoordinates(w.GetCursorPos()) + + if gui.catchedMouseHandler != nil { + gui.catchedMouseHandler.mouseButtonCallback(gui, button, action, mod, mouseX, mouseY) + if action == glfw.Release && button == gui.mouseCatchedOnButton { + gui.catchMouse(nil, 0) + } + } else { + + if gui.isMouseInside(mouseX, mouseY) { + if action == glfw.Press { + gui.catchMouse(gui, button) + } + gui.mouseButtonCallback(gui, button, action, mod, mouseX, mouseY) + } else if gui.vScrollbar != nil && gui.vScrollbar.isMouseInside(mouseX, mouseY) { + if action == glfw.Press { + gui.catchMouse(gui.vScrollbar, button) + } + gui.vScrollbar.mouseButtonCallback(gui, button, action, mod, mouseX, mouseY) + } + } +} + +func (gui *GUI) globalCursorEnterCallback(w *glfw.Window, entered bool) { + if !entered { + if gui.prevMouseEventHandler != nil { + gui.prevMouseEventHandler.cursorEnterCallback(gui, false) + gui.prevMouseEventHandler = nil + } + } +} + +func (gui *GUI) catchMouse(newHandler mouseEventsHandler, button glfw.MouseButton) { + gui.catchedMouseHandler = newHandler + gui.mouseCatchedOnButton = button +} + +func (gui *GUI) isMouseInside(px float64, py float64) bool { + return px >= float64(gui.renderer.areaX) && px < float64(gui.renderer.areaX+gui.renderer.areaWidth) && + py >= float64(gui.renderer.areaY) && py < float64(gui.renderer.areaY+gui.renderer.areaHeight) +} + +func (gui *GUI) mouseMoveCallback(g *GUI, px float64, py float64) { x, y := gui.convertMouseCoordinates(px, py) @@ -59,16 +138,13 @@ func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) { } if url := gui.terminal.ActiveBuffer().GetURLAtPosition(x, y); url != "" { - w.SetCursor(gui.getHandCursor()) + gui.window.SetCursor(gui.getHandCursor()) } else { - w.SetCursor(gui.getArrowCursor()) + gui.window.SetCursor(gui.getArrowCursor()) } } func (gui *GUI) convertMouseCoordinates(px float64, py float64) (uint16, uint16) { - scale := gui.scale() - px = px / float64(scale) - py = py / float64(scale) 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()))) @@ -122,7 +198,7 @@ func btnCode(button glfw.MouseButton, release bool, mod glfw.ModifierKey) (b byt return b, true } -func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) { +func (gui *GUI) mouseButtonCallback(g *GUI, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey, mouseX float64, mouseY float64) { if gui.overlay != nil { if button == glfw.MouseButtonRight && action == glfw.Release { @@ -132,7 +208,7 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act } // before we forward clicks on (below), we need to handle them locally for url clicking, text highlighting etc. - x, y := gui.convertMouseCoordinates(w.GetCursorPos()) + x, y := gui.convertMouseCoordinates(mouseX, mouseY) tx := int(x) + 1 // vt100 is 1 indexed ty := int(y) + 1 @@ -252,7 +328,10 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act default: panic("Unsupported mouse mode") } +} +func (gui *GUI) cursorEnterCallback(g *GUI, entered bool) { + // empty, just to conform to the mouseEventsHandler interface } func (gui *GUI) handleSelectionButtonPress(x uint16, y uint16, mod glfw.ModifierKey) { diff --git a/gui/rectangleRenderer.go b/gui/rectangleRenderer.go new file mode 100644 index 0000000..484fe51 --- /dev/null +++ b/gui/rectangleRenderer.go @@ -0,0 +1,157 @@ +package gui + +import ( + "github.com/go-gl/gl/all-core/gl" + "github.com/liamg/aminal/config" +) + +const ( + rectangleRendererVertexShaderSource = ` + #version 330 core + layout (location = 0) in vec2 position; + uniform vec2 resolution; + + void main() { + // convert from window coordinates to GL coordinates + vec2 glCoordinates = ((position / resolution) * 2.0 - 1.0) * vec2(1, -1); + + gl_Position = vec4(glCoordinates, 0.0, 1.0); + }` + "\x00" + + rectangleRendererFragmentShaderSource = ` + #version 330 core + uniform vec4 inColor; + out vec4 outColor; + void main() { + outColor = inColor; + }` + "\x00" +) + +type rectangleRenderer struct { + program uint32 + vbo uint32 + vao uint32 + ibo uint32 + uniformLocationResolution int32 + uniformLocationInColor int32 +} + +func createRectangleRendererProgram() (uint32, error) { + vertexShader, err := compileShader(rectangleRendererVertexShaderSource, gl.VERTEX_SHADER) + if err != nil { + return 0, err + } + defer gl.DeleteShader(vertexShader) + + fragmentShader, err := compileShader(rectangleRendererFragmentShaderSource, gl.FRAGMENT_SHADER) + if err != nil { + return 0, err + } + defer gl.DeleteShader(fragmentShader) + + prog := gl.CreateProgram() + gl.AttachShader(prog, vertexShader) + gl.AttachShader(prog, fragmentShader) + gl.LinkProgram(prog) + + return prog, nil +} + +func newRectangleRenderer() (*rectangleRenderer, error) { + prog, err := createRectangleRendererProgram() + if err != nil { + return nil, err + } + + var vbo uint32 + var vao uint32 + var ibo uint32 + + gl.GenBuffers(1, &vbo) + gl.GenVertexArrays(1, &vao) + gl.GenBuffers(1, &ibo) + + indices := [...]uint32{ + 0, 1, 2, + 2, 3, 0, + } + + gl.BindVertexArray(vao) + + gl.BindBuffer(gl.ARRAY_BUFFER, vbo) + gl.BufferData(gl.ARRAY_BUFFER, 8*4, nil, gl.DYNAMIC_DRAW) // just reserve data for the buffer + + gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo) + gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(indices)*4, gl.Ptr(&indices[0]), gl.DYNAMIC_DRAW) + + gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil) + gl.EnableVertexAttribArray(0) + + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + + return &rectangleRenderer{ + program: prog, + vbo: vbo, + vao: vao, + ibo: ibo, + uniformLocationResolution: gl.GetUniformLocation(prog, gl.Str("resolution\x00")), + uniformLocationInColor: gl.GetUniformLocation(prog, gl.Str("inColor\x00")), + }, nil +} + +func (rr *rectangleRenderer) Free() { + if rr.program != 0 { + gl.DeleteProgram(rr.program) + rr.program = 0 + } + + if rr.vbo != 0 { + gl.DeleteBuffers(1, &rr.vbo) + rr.vbo = 0 + } + + if rr.vao != 0 { + gl.DeleteBuffers(1, &rr.vao) + rr.vao = 0 + } + + if rr.ibo != 0 { + gl.DeleteBuffers(1, &rr.ibo) + rr.ibo = 0 + } +} + +func (rr *rectangleRenderer) render(left float32, top float32, width float32, height float32, colour config.Colour) { + var savedProgram int32 + gl.GetIntegerv(gl.CURRENT_PROGRAM, &savedProgram) + defer gl.UseProgram(uint32(savedProgram)) + + currentViewport := [4]int32{} + gl.GetIntegerv(gl.VIEWPORT, ¤tViewport[0]) + + gl.UseProgram(rr.program) + gl.Uniform2f(rr.uniformLocationResolution, float32(currentViewport[2]), float32(currentViewport[3])) + + gl.Uniform4f(rr.uniformLocationInColor, colour[0], colour[1], colour[2], 1.0) + + vertices := [...]float32{ + left, top, + left + width, top, + left + width, top + height, + left, top + height, + } + + /* + gl.NamedBufferSubData(rr.vbo, 0, len(vertices)*4, gl.Ptr(&vertices[0])) + /*/ + gl.BindBuffer(gl.ARRAY_BUFFER, rr.vbo) + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(vertices)*4, gl.Ptr(&vertices[0])) + //*/ + + gl.BindVertexArray(rr.vao) + + gl.DrawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, gl.PtrOffset(0)) + + gl.BindVertexArray(0) +} diff --git a/gui/renderer.go b/gui/renderer.go index b5aee10..9b9e6ab 100644 --- a/gui/renderer.go +++ b/gui/renderer.go @@ -1,13 +1,12 @@ package gui import ( - "image" - "math" - "github.com/go-gl/gl/all-core/gl" "github.com/liamg/aminal/buffer" "github.com/liamg/aminal/config" "github.com/liamg/aminal/glfont" + "image" + "math" ) type OpenGLRenderer struct { @@ -26,16 +25,8 @@ type OpenGLRenderer struct { textureMap map[*image.RGBA]uint32 fontMap *FontMap backgroundColour [3]float32 -} -type rectangle struct { - vao uint32 - vbo uint32 - cv uint32 - colourAttr uint32 - colour [3]float32 - points [18]float32 - prog uint32 + rectRenderer *rectangleRenderer } func (r *OpenGLRenderer) CellWidth() float32 { @@ -46,95 +37,12 @@ func (r *OpenGLRenderer) CellHeight() float32 { return r.cellHeight } -func (r *OpenGLRenderer) newRectangleEx(x float32, y float32, width float32, height float32, colourAttr uint32) *rectangle { - - rect := &rectangle{} - - halfAreaWidth := float32(r.areaWidth / 2) - halfAreaHeight := float32(r.areaHeight / 2) - - x = (x - halfAreaWidth) / halfAreaWidth - y = -(y - (halfAreaHeight)) / halfAreaHeight - w := width / halfAreaWidth - h := height / halfAreaHeight - - rect.points = [18]float32{ - x, y, 0, - x, y + h, 0, - x + w, y + h, 0, - - x + w, y, 0, - x, y, 0, - x + w, y + h, 0, +func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY int, areaWidth int, areaHeight int, colourAttr uint32, program uint32) (*OpenGLRenderer, error) { + rectRenderer, err := newRectangleRenderer() + if err != nil { + return nil, err } - rect.colourAttr = colourAttr - rect.prog = r.program - - // SHAPE - gl.GenBuffers(1, &rect.vbo) - gl.BindBuffer(gl.ARRAY_BUFFER, rect.vbo) - gl.BufferData(gl.ARRAY_BUFFER, 4*len(rect.points), gl.Ptr(&rect.points[0]), gl.STATIC_DRAW) - - gl.GenVertexArrays(1, &rect.vao) - gl.BindVertexArray(rect.vao) - gl.EnableVertexAttribArray(0) - - gl.BindBuffer(gl.ARRAY_BUFFER, rect.vbo) - gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil) - - // colour - gl.GenBuffers(1, &rect.cv) - - rect.setColour([3]float32{0, 1, 0}) - - return rect -} - -func (r *OpenGLRenderer) newRectangle(x float32, y float32, colourAttr uint32) *rectangle { - return r.newRectangleEx(x, y, r.cellWidth, r.cellHeight, colourAttr) -} - -func (rect *rectangle) Draw() { - gl.UseProgram(rect.prog) - gl.BindVertexArray(rect.vao) - gl.DrawArrays(gl.TRIANGLES, 0, 6) -} - -func (rect *rectangle) setColour(colour [3]float32) { - if rect.colour == colour { - return - } - - c := []float32{ - colour[0], colour[1], colour[2], - colour[0], colour[1], colour[2], - colour[0], colour[1], colour[2], - colour[0], colour[1], colour[2], - colour[0], colour[1], colour[2], - colour[0], colour[1], colour[2], - } - - gl.UseProgram(rect.prog) - gl.BindBuffer(gl.ARRAY_BUFFER, rect.cv) - gl.BufferData(gl.ARRAY_BUFFER, len(c)*4, gl.Ptr(c), gl.STATIC_DRAW) - gl.EnableVertexAttribArray(rect.colourAttr) - gl.VertexAttribPointer(rect.colourAttr, 3, gl.FLOAT, false, 0, gl.PtrOffset(0)) - - rect.colour = colour -} - -func (rect *rectangle) Free() { - gl.DeleteVertexArrays(1, &rect.vao) - gl.DeleteBuffers(1, &rect.vbo) - gl.DeleteBuffers(1, &rect.cv) - - rect.vao = 0 - rect.vbo = 0 - rect.cv = 0 -} - -func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY int, areaWidth int, areaHeight int, colourAttr uint32, program uint32) *OpenGLRenderer { r := &OpenGLRenderer{ areaWidth: areaWidth, areaHeight: areaHeight, @@ -146,9 +54,10 @@ func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY program: program, textureMap: map[*image.RGBA]uint32{}, fontMap: fontMap, + rectRenderer: rectRenderer, } r.SetArea(areaX, areaY, areaWidth, areaHeight) - return r + return r, nil } // This method ensures that all OpenGL resources are deleted correctly @@ -162,6 +71,11 @@ func (r *OpenGLRenderer) Free() { gl.DeleteProgram(r.program) r.program = 0 + + if r.rectRenderer != nil { + r.rectRenderer.Free() + r.rectRenderer = nil + } } func (r *OpenGLRenderer) GetTermSize() (uint, uint) { @@ -181,26 +95,16 @@ func (r *OpenGLRenderer) SetArea(areaX int, areaY int, areaWidth int, areaHeight r.termRows = uint(math.Floor(float64(float32(r.areaHeight) / r.cellHeight))) } -func (r *OpenGLRenderer) GetRectangleSize(col uint, row uint) (float32, float32) { - x := float32(float32(col) * r.cellWidth) - y := float32(float32(row) * r.cellHeight) +func (r *OpenGLRenderer) ConvertCoordinates(col uint, row uint) (float32, float32) { + left := float32(float32(col) * r.cellWidth) + top := float32(float32(row) * r.cellHeight) - return x, y -} - -func (r *OpenGLRenderer) getRectangle(col uint, row uint) *rectangle { - x := float32(float32(col) * r.cellWidth) - y := float32(float32(row)*r.cellHeight) + r.cellHeight - - return r.newRectangle(x, y, r.colourAttr) + return left, top } func (r *OpenGLRenderer) DrawCursor(col uint, row uint, colour config.Colour) { - rect := r.getRectangle(col, row) - rect.setColour(colour) - rect.Draw() - - rect.Free() + left, top := r.ConvertCoordinates(col, row) + r.rectRenderer.render(left, top, r.cellWidth, r.cellHeight, colour) } func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, colour *config.Colour, force bool) { @@ -214,13 +118,9 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, colour } if bg != r.backgroundColour || force { - rect := r.getRectangle(col, row) - rect.setColour(bg) - rect.Draw() - - rect.Free() + left, top := r.ConvertCoordinates(col, row) + r.rectRenderer.render(left, top, r.cellWidth, r.cellHeight, bg) } - } // DrawUnderline draws a line under 'span' characters starting at (col, row) @@ -233,12 +133,8 @@ func (r *OpenGLRenderer) DrawUnderline(span int, col uint, row uint, colour [3]f if thickness < 1 { thickness = 1 } - rect := r.newRectangleEx(x, y, r.cellWidth*float32(span), thickness, r.colourAttr) - rect.setColour(colour) - rect.Draw() - - rect.Free() + r.rectRenderer.render(x, y, r.cellWidth*float32(span), thickness, colour) } func (r *OpenGLRenderer) DrawCellText(text string, col uint, row uint, alpha float32, colour [3]float32, bold bool) { diff --git a/gui/scrollbar.go b/gui/scrollbar.go new file mode 100644 index 0000000..a8b5c54 --- /dev/null +++ b/gui/scrollbar.go @@ -0,0 +1,519 @@ +package gui + +import ( + "github.com/go-gl/gl/all-core/gl" + "github.com/go-gl/glfw/v3.2/glfw" +) + +const ( + scrollbarVertexShaderSource = ` + #version 330 core + layout (location = 0) in vec2 position; + uniform vec2 resolution; + + void main() { + // convert from window coordinates to GL coordinates + vec2 glCoordinates = ((position / resolution) * 2.0 - 1.0) * vec2(1, -1); + + gl_Position = vec4(glCoordinates, 0.0, 1.0); + }` + "\x00" + + scrollbarFragmentShaderSource = ` + #version 330 core + uniform vec4 inColor; + out vec4 outColor; + void main() { + outColor = inColor; + }` + "\x00" + + NumberOfVertexValues = 100 +) + +var ( + scrollbarColor_Bg = [3]float32{float32(241) / float32(255), float32(241) / float32(255), float32(241) / float32(255)} + scrollbarColor_ThumbNormal = [3]float32{float32(193) / float32(255), float32(193) / float32(255), float32(193) / float32(255)} + scrollbarColor_ThumbHover = [3]float32{float32(168) / float32(255), float32(168) / float32(255), float32(168) / float32(255)} + scrollbarColor_ThumbClicked = [3]float32{float32(120) / float32(255), float32(120) / float32(255), float32(120) / float32(255)} + + scrollbarColor_ButtonNormalBg = [3]float32{float32(241) / float32(255), float32(241) / float32(255), float32(241) / float32(255)} + scrollbarColor_ButtonNormalFg = [3]float32{float32(80) / float32(255), float32(80) / float32(255), float32(80) / float32(255)} + + scrollbarColor_ButtonHoverBg = [3]float32{float32(210) / float32(255), float32(210) / float32(255), float32(210) / float32(255)} + scrollbarColor_ButtonHoverFg = [3]float32{float32(80) / float32(255), float32(80) / float32(255), float32(80) / float32(255)} + + scrollbarColor_ButtonDisabledBg = [3]float32{float32(241) / float32(255), float32(241) / float32(255), float32(241) / float32(255)} + scrollbarColor_ButtonDisabledFg = [3]float32{float32(163) / float32(255), float32(163) / float32(255), float32(163) / float32(255)} + + scrollbarColor_ButtonClickedBg = [3]float32{float32(120) / float32(255), float32(120) / float32(255), float32(120) / float32(255)} + scrollbarColor_ButtonClickedFg = [3]float32{float32(255) / float32(255), float32(255) / float32(255), float32(255) / float32(255)} +) + +type scrollbarPart int + +const ( + None scrollbarPart = iota + UpperArrow + UpperSpace // the space between upper arrow and thumb + Thumb + BottomSpace // the space between thumb and bottom arrow + BottomArrow +) + +type ScreenRectangle struct { + left, top float32 // upper left corner in pixels relative to the window (in pixels) + right, bottom float32 +} + +func (sr *ScreenRectangle) width() float32 { + return sr.right - sr.left +} + +func (sr *ScreenRectangle) height() float32 { + return sr.bottom - sr.top +} + +func (sr *ScreenRectangle) isInside(x float32, y float32) bool { + return x >= sr.left && x < sr.right && + y >= sr.top && y < sr.bottom +} + +type scrollbar struct { + program uint32 + vbo uint32 + vao uint32 + uniformLocationResolution int32 + uniformLocationInColor int32 + + position ScreenRectangle // relative to the window's top left corner, in pixels + positionUpperArrow ScreenRectangle // relative to the control's top left corner + positionBottomArrow ScreenRectangle + positionThumb ScreenRectangle + + scrollPosition int + maxScrollPosition int + + thumbIsDragging bool + startedDraggingAtPosition int // scrollPosition when the dragging was started + startedDraggingAtThumbTop float32 // sb.positionThumb.top when the dragging was started + offsetInThumbY float32 // y offset inside the thumb of the dragging point + scrollPositionDelta int + + upperArrowIsDown bool + bottomArrowIsDown bool + + upperArrowFg []float32 + upperArrowBg []float32 + bottomArrowFg []float32 + bottomArrowBg []float32 + thumbColor []float32 +} + +// Returns the vertical scrollbar width in pixels +func getDefaultScrollbarWidth() int { + return 13 +} + +func createScrollbarProgram() (uint32, error) { + vertexShader, err := compileShader(scrollbarVertexShaderSource, gl.VERTEX_SHADER) + if err != nil { + return 0, err + } + defer gl.DeleteShader(vertexShader) + + fragmentShader, err := compileShader(scrollbarFragmentShaderSource, gl.FRAGMENT_SHADER) + if err != nil { + return 0, err + } + defer gl.DeleteShader(fragmentShader) + + prog := gl.CreateProgram() + gl.AttachShader(prog, vertexShader) + gl.AttachShader(prog, fragmentShader) + gl.LinkProgram(prog) + + return prog, nil +} + +func newScrollbar() (*scrollbar, error) { + prog, err := createScrollbarProgram() + if err != nil { + return nil, err + } + + var vbo uint32 + var vao uint32 + + gl.GenBuffers(1, &vbo) + gl.GenVertexArrays(1, &vao) + + gl.BindVertexArray(vao) + + gl.BindBuffer(gl.ARRAY_BUFFER, vbo) + gl.BufferData(gl.ARRAY_BUFFER, NumberOfVertexValues*4, nil, gl.DYNAMIC_DRAW) // only reserve the space + + gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil) + gl.EnableVertexAttribArray(0) + + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + + result := &scrollbar{ + program: prog, + vbo: vbo, + vao: vao, + uniformLocationResolution: gl.GetUniformLocation(prog, gl.Str("resolution\x00")), + uniformLocationInColor: gl.GetUniformLocation(prog, gl.Str("inColor\x00")), + + position: ScreenRectangle{ + right: 0, + bottom: 0, + left: 0, + top: 0, + }, + + scrollPosition: 0, + maxScrollPosition: 0, + + thumbIsDragging: false, + upperArrowIsDown: false, + bottomArrowIsDown: false, + } + + result.recalcElementPositions() + result.resetElementColors(-1, -1) // (-1, -1) ensures that no part is hovered by the mouse + + return result, nil +} + +func (sb *scrollbar) Free() { + if sb.program != 0 { + gl.DeleteProgram(sb.program) + sb.program = 0 + } + + if sb.vbo != 0 { + gl.DeleteBuffers(1, &sb.vbo) + sb.vbo = 0 + } + + if sb.vao != 0 { + gl.DeleteBuffers(1, &sb.vao) + sb.vao = 0 + } +} + +// Recalc positions of the scrollbar elements according to current +func (sb *scrollbar) recalcElementPositions() { + arrowHeight := sb.position.width() + + sb.positionUpperArrow = ScreenRectangle{ + left: 0, + top: 0, + right: sb.position.width(), + bottom: arrowHeight, + } + + sb.positionBottomArrow = ScreenRectangle{ + left: sb.positionUpperArrow.left, + top: sb.position.height() - arrowHeight, + right: sb.positionUpperArrow.right, + bottom: sb.position.height(), + } + thumbHeight := sb.position.width() + thumbTop := arrowHeight + if sb.maxScrollPosition != 0 { + thumbTop += (float32(sb.scrollPosition) * (sb.position.height() - thumbHeight - arrowHeight*2)) / float32(sb.maxScrollPosition) + } + + sb.positionThumb = ScreenRectangle{ + left: 2, + top: thumbTop, + right: sb.position.width() - 2, + bottom: thumbTop + thumbHeight, + } +} + +func (sb *scrollbar) resize(gui *GUI) { + sb.position.left = float32(gui.width) - float32(getDefaultScrollbarWidth())*gui.dpiScale + sb.position.top = float32(0.0) + sb.position.right = float32(gui.width) + sb.position.bottom = float32(gui.height - 1) + + sb.recalcElementPositions() + gui.terminal.NotifyDirty() +} + +func (sb *scrollbar) render(gui *GUI) { + var savedProgram int32 + gl.GetIntegerv(gl.CURRENT_PROGRAM, &savedProgram) + defer gl.UseProgram(uint32(savedProgram)) + + gl.UseProgram(sb.program) + gl.Uniform2f(sb.uniformLocationResolution, float32(gui.width), float32(gui.height)) + gl.BindVertexArray(sb.vao) + gl.BindBuffer(gl.ARRAY_BUFFER, sb.vbo) + defer func() { + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + }() + + // Draw background + gl.Uniform4f(sb.uniformLocationInColor, scrollbarColor_Bg[0], scrollbarColor_Bg[1], scrollbarColor_Bg[2], 1.0) + borderVertices := [...]float32{ + sb.position.left, sb.position.top, + sb.position.right, sb.position.top, + sb.position.right, sb.position.bottom, + + sb.position.right, sb.position.bottom, + sb.position.left, sb.position.bottom, + sb.position.left, sb.position.top, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(borderVertices)*4, gl.Ptr(&borderVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(borderVertices)/2)) + + // Draw upper arrow + // Upper arrow background + gl.Uniform4f(sb.uniformLocationInColor, sb.upperArrowBg[0], sb.upperArrowBg[1], sb.upperArrowBg[2], 1.0) + upperArrowBgVertices := [...]float32{ + sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.top, + sb.position.left + sb.positionUpperArrow.right, sb.position.top + sb.positionUpperArrow.top, + sb.position.left + sb.positionUpperArrow.right, sb.position.top + sb.positionUpperArrow.bottom, + + sb.position.left + sb.positionUpperArrow.right, sb.position.top + sb.positionUpperArrow.bottom, + sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.bottom, + sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.top, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(upperArrowBgVertices)*4, gl.Ptr(&upperArrowBgVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(upperArrowBgVertices)/2)) + + // Upper arrow foreground + gl.Uniform4f(sb.uniformLocationInColor, sb.upperArrowFg[0], sb.upperArrowFg[1], sb.upperArrowFg[2], 1.0) + upperArrowFgVertices := [...]float32{ + sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()/2.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/3.0, + sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()*2.0/3.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/2.0, + sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()/3.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/2.0, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(upperArrowFgVertices)*4, gl.Ptr(&upperArrowFgVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(upperArrowFgVertices)/2)) + + // Draw bottom arrow + // Bottom arrow background + gl.Uniform4f(sb.uniformLocationInColor, sb.bottomArrowBg[0], sb.bottomArrowBg[1], sb.bottomArrowBg[2], 1.0) + bottomArrowBgVertices := [...]float32{ + sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.top, + sb.position.left + sb.positionBottomArrow.right, sb.position.top + sb.positionBottomArrow.top, + sb.position.left + sb.positionBottomArrow.right, sb.position.top + sb.positionBottomArrow.bottom, + + sb.position.left + sb.positionBottomArrow.right, sb.position.top + sb.positionBottomArrow.bottom, + sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.bottom, + sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.top, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(bottomArrowBgVertices)*4, gl.Ptr(&bottomArrowBgVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(bottomArrowBgVertices)/2)) + + // Bottom arrow foreground + gl.Uniform4f(sb.uniformLocationInColor, sb.bottomArrowFg[0], sb.bottomArrowFg[1], sb.bottomArrowFg[2], 1.0) + bottomArrowFgVertices := [...]float32{ + sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()/3.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()/2.0, + sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()*2.0/3.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()/2.0, + sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()/2.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()*2.0/3.0, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(bottomArrowFgVertices)*4, gl.Ptr(&bottomArrowFgVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(bottomArrowFgVertices)/2)) + + // Draw thumb + gl.Uniform4f(sb.uniformLocationInColor, sb.thumbColor[0], sb.thumbColor[1], sb.thumbColor[2], 1.0) + thumbVertices := [...]float32{ + sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.top, + sb.position.left + sb.positionThumb.right, sb.position.top + sb.positionThumb.top, + sb.position.left + sb.positionThumb.right, sb.position.top + sb.positionThumb.bottom, + + sb.position.left + sb.positionThumb.right, sb.position.top + sb.positionThumb.bottom, + sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.bottom, + sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.top, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(thumbVertices)*4, gl.Ptr(&thumbVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(thumbVertices)/2)) + + gui.terminal.NotifyDirty() +} + +func (sb *scrollbar) setPosition(max int, position int) { + if max <= 0 { + max = position + } + + if position > max { + position = max + } + + sb.maxScrollPosition = max + sb.scrollPosition = position + + sb.recalcElementPositions() +} + +func (sb *scrollbar) mouseHitTest(px float64, py float64) scrollbarPart { + // convert to local coordinates + mouseX := float32(px - float64(sb.position.left)) + mouseY := float32(py - float64(sb.position.top)) + + result := None + + if sb.positionUpperArrow.isInside(mouseX, mouseY) { + result = UpperArrow + } else if sb.positionBottomArrow.isInside(mouseX, mouseY) { + result = BottomArrow + } else if sb.positionThumb.isInside(mouseX, mouseY) { + result = Thumb + } else { + // construct UpperSpace + pos := ScreenRectangle{ + left: sb.positionThumb.left, + top: sb.positionUpperArrow.bottom, + right: sb.positionThumb.right, + bottom: sb.positionThumb.top, + } + + if pos.isInside(mouseX, mouseY) { + result = UpperSpace + } + + // now update it to be BottomSpace + pos.top = sb.positionThumb.bottom + pos.bottom = sb.positionBottomArrow.top + if pos.isInside(mouseX, mouseY) { + result = BottomSpace + } + } + + return result +} + +func (sb *scrollbar) isMouseInside(px float64, py float64) bool { + return sb.position.isInside(float32(px), float32(py)) +} + +func (sb *scrollbar) mouseButtonCallback(g *GUI, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey, mouseX float64, mouseY float64) { + if button == glfw.MouseButtonLeft { + if action == glfw.Press { + switch sb.mouseHitTest(mouseX, mouseY) { + case UpperArrow: + sb.upperArrowIsDown = true + g.terminal.ScreenScrollUp(1) + + case UpperSpace: + g.terminal.ScrollPageUp() + + case Thumb: + sb.thumbIsDragging = true + sb.startedDraggingAtPosition = sb.scrollPosition + sb.startedDraggingAtThumbTop = sb.positionThumb.top + sb.offsetInThumbY = float32(mouseY) - sb.position.top - sb.positionThumb.top + sb.scrollPositionDelta = 0 + + case BottomSpace: + g.terminal.ScrollPageDown() + + case BottomArrow: + sb.bottomArrowIsDown = true + g.terminal.ScreenScrollDown(1) + } + } else if action == glfw.Release { + if sb.thumbIsDragging { + sb.thumbIsDragging = false + } + + if sb.upperArrowIsDown { + sb.upperArrowIsDown = false + } + + if sb.bottomArrowIsDown { + sb.bottomArrowIsDown = false + } + } + + g.terminal.NotifyDirty() + } + + sb.resetElementColors(mouseX, mouseY) +} + +func (sb *scrollbar) mouseMoveCallback(g *GUI, px float64, py float64) { + sb.resetElementColors(px, py) + + if sb.thumbIsDragging { + py -= float64(sb.position.top) + + minThumbTop := sb.positionUpperArrow.bottom + maxThumbTop := sb.positionBottomArrow.top - sb.positionThumb.height() + + newThumbTop := float32(py) - sb.offsetInThumbY + + newPositionDelta := int((float32(sb.maxScrollPosition) * (newThumbTop - minThumbTop - sb.startedDraggingAtThumbTop)) / (maxThumbTop - minThumbTop)) + + if newPositionDelta > sb.scrollPositionDelta { + scrollLines := newPositionDelta - sb.scrollPositionDelta + g.logger.Debugf("old position: %d, new position delta: %d, scroll down %d lines", sb.scrollPosition, newPositionDelta, scrollLines) + g.terminal.ScreenScrollDown(uint16(scrollLines)) + sb.scrollPositionDelta = newPositionDelta + } else if newPositionDelta < sb.scrollPositionDelta { + scrollLines := sb.scrollPositionDelta - newPositionDelta + g.logger.Debugf("old position: %d, new position delta: %d, scroll up %d lines", sb.scrollPosition, newPositionDelta, scrollLines) + g.terminal.ScreenScrollUp(uint16(scrollLines)) + sb.scrollPositionDelta = newPositionDelta + } + + sb.recalcElementPositions() + g.logger.Debugf("new thumbTop: %f, fact thumbTop: %f, position: %d", newThumbTop, sb.positionThumb.top, sb.scrollPosition) + } + + g.terminal.NotifyDirty() +} + +func (sb *scrollbar) resetElementColors(mouseX float64, mouseY float64) { + part := sb.mouseHitTest(mouseX, mouseY) + + if sb.scrollPosition == 0 { + sb.upperArrowBg = scrollbarColor_ButtonDisabledBg[:] + sb.upperArrowFg = scrollbarColor_ButtonDisabledFg[:] + } else if sb.upperArrowIsDown { + sb.upperArrowFg = scrollbarColor_ButtonClickedFg[:] + sb.upperArrowBg = scrollbarColor_ButtonClickedBg[:] + } else if part == UpperArrow { + sb.upperArrowFg = scrollbarColor_ButtonHoverFg[:] + sb.upperArrowBg = scrollbarColor_ButtonHoverBg[:] + } else { + sb.upperArrowFg = scrollbarColor_ButtonNormalFg[:] + sb.upperArrowBg = scrollbarColor_ButtonNormalBg[:] + } + + if sb.scrollPosition == sb.maxScrollPosition { + sb.bottomArrowBg = scrollbarColor_ButtonDisabledBg[:] + sb.bottomArrowFg = scrollbarColor_ButtonDisabledFg[:] + } else if sb.bottomArrowIsDown { + sb.bottomArrowFg = scrollbarColor_ButtonClickedFg[:] + sb.bottomArrowBg = scrollbarColor_ButtonClickedBg[:] + } else if part == BottomArrow { + sb.bottomArrowFg = scrollbarColor_ButtonHoverFg[:] + sb.bottomArrowBg = scrollbarColor_ButtonHoverBg[:] + } else { + sb.bottomArrowFg = scrollbarColor_ButtonNormalFg[:] + sb.bottomArrowBg = scrollbarColor_ButtonNormalBg[:] + } + + if sb.thumbIsDragging { + sb.thumbColor = scrollbarColor_ThumbClicked[:] + } else if part == Thumb { + sb.thumbColor = scrollbarColor_ThumbHover[:] + } else { + sb.thumbColor = scrollbarColor_ThumbNormal[:] + } +} + +func (sb *scrollbar) cursorEnterCallback(g *GUI, entered bool) { + if !entered { + sb.resetElementColors(-1, -1) // (-1, -1) ensures that no part is hovered by the mouse + g.terminal.NotifyDirty() + } +} diff --git a/main.go b/main.go index 1b4114c..b0b4eab 100644 --- a/main.go +++ b/main.go @@ -72,6 +72,7 @@ func initialize(unitTestfunc callback, configOverride *config.Config) { if err != nil { logger.Fatalf("Cannot start: %s", err) } + defer g.Free() terminal.WindowManipulation = g diff --git a/main_test.go b/main_test.go index ab91005..1e833c7 100644 --- a/main_test.go +++ b/main_test.go @@ -198,6 +198,9 @@ func TestExit(t *testing.T) { func testConfig() *config.Config { c := config.DefaultConfig() + // Force the scrollbar not showing when running unit tests + c.ShowVerticalScrollbar = false + // Use a vanilla shell on POSIX to help ensure consistency. if runtime.GOOS != "windows" { c.Shell = "/bin/sh" From 070f29ad13f509587d761078e5d30747687cac02 Mon Sep 17 00:00:00 2001 From: nikitar020 <42252263+nikitar020@users.noreply.github.com> Date: Wed, 20 Mar 2019 04:45:43 +0200 Subject: [PATCH 3/6] Solve intermittent build errors (#262) * Solve intermittent build errors * Eliminate `panic()` in `gui.Screenshot()` function --- gui/gui.go | 47 +++++++++++++---------- main.go | 15 +++++++- main_test.go | 106 ++++++++++++++++++++++++++++++++++----------------- 3 files changed, 112 insertions(+), 56 deletions(-) diff --git a/gui/gui.go b/gui/gui.go index 7c6b9af..b35a8b8 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -373,7 +373,6 @@ func (gui *GUI) Close() { } func (gui *GUI) Render() error { - gui.logger.Debugf("Creating window...") var err error gui.window, err = gui.createWindow() @@ -470,16 +469,6 @@ func (gui *GUI) Render() error { gl.Disable(gl.DEPTH_TEST) gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - - go func() { - for { - <-ticker.C - gui.logger.Sync() - } - }() - gui.terminal.SetProgram(program) latestVersion := "" @@ -496,7 +485,9 @@ func (gui *GUI) Render() error { showMessage := true stop := make(chan struct{}) - go gui.waker(stop) + var waitForWaker sync.WaitGroup + waitForWaker.Add(1) + go gui.waker(stop, &waitForWaker) for !gui.window.ShouldClose() { gui.redraw(true) @@ -560,9 +551,13 @@ Buffer Size: %d lines } } - close(stop) // Tell waker to end. + gui.logger.Debug("Stopping render...") + + close(stop) // Tell waker to end... + waitForWaker.Wait() // ...and wait it to end + + gui.logger.Debug("Render stopped") - gui.logger.Debugf("Stopping render...") return nil } @@ -571,10 +566,13 @@ Buffer Size: %d lines // waking up the main thread when the GUI needs to be // redrawn. Limiting is applied on wakeups to avoid excessive CPU // usage when the terminal is being updated rapidly. -func (gui *GUI) waker(stop <-chan struct{}) { +func (gui *GUI) waker(stop <-chan struct{}, wg *sync.WaitGroup) { + defer wg.Done() + dirty := gui.terminal.Dirty() var nextWake <-chan time.Time var last time.Time +forLoop: for { select { case <-dirty: @@ -598,7 +596,7 @@ func (gui *GUI) waker(stop <-chan struct{}) { glfw.PostEmptyEvent() nextWake = nil case <-stop: - return + break forLoop } } } @@ -869,18 +867,27 @@ func (gui *GUI) SwapBuffers() { gui.window.SwapBuffers() } -func (gui *GUI) Screenshot(path string) { +func (gui *GUI) Screenshot(path string) error { x, y := gui.window.GetPos() w, h := gui.window.GetSize() img, err := screenshot.CaptureRect(image.Rectangle{Min: image.Point{X: x, Y: y}, Max: image.Point{X: x + w, Y: y + h}}) if err != nil { - panic(err) + return err + } + file, err := os.Create(path) + if err != nil { + return err } - file, _ := os.Create(path) defer file.Close() - png.Encode(file, img) + err = png.Encode(file, img) + if err != nil { + os.Remove(path) + return err + } + + return nil } func (gui *GUI) windowPosChangeCallback(w *glfw.Window, xpos int, ypos int) { diff --git a/main.go b/main.go index b0b4eab..63d6a69 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "github.com/liamg/aminal/platform" "github.com/liamg/aminal/terminal" "github.com/riywo/loginshell" + "time" ) type callback func(terminal *terminal.Terminal, g *gui.GUI) @@ -32,7 +33,17 @@ func initialize(unitTestfunc callback, configOverride *config.Config) { fmt.Printf("Failed to create logger: %s\n", err) os.Exit(1) } - defer logger.Sync() + ticker := time.NewTicker(time.Second) + go func() { + for { + <-ticker.C + logger.Sync() + } + }() + defer func() { + ticker.Stop() + logger.Sync() + }() if conf.CPUProfile != "" { logger.Infof("Starting CPU profiling...") @@ -61,7 +72,7 @@ func initialize(unitTestfunc callback, configOverride *config.Config) { guestProcess, err := pty.CreateGuestProcess(shellStr) if err != nil { pty.Close() - logger.Fatalf("Failed to start your shell: %s", err) + logger.Fatalf("Failed to start your shell %q: %s", shellStr, err) } defer guestProcess.Close() diff --git a/main_test.go b/main_test.go index 1e833c7..e82de2d 100644 --- a/main_test.go +++ b/main_test.go @@ -11,11 +11,10 @@ import ( "testing" "time" + "github.com/carlogit/phash" "github.com/liamg/aminal/config" "github.com/liamg/aminal/gui" "github.com/liamg/aminal/terminal" - - "github.com/carlogit/phash" ) var termRef *terminal.Terminal @@ -47,10 +46,14 @@ func hash(path string) string { return imageHash } +func imagesAreEqual(expected string, actual string) int { + expectedHash := hash(expected) + actualHash := hash(actual) + return phash.GetDistance(expectedHash, actualHash) +} + func compareImages(expected string, actual string) { - template := hash(expected) - screen := hash(actual) - distance := phash.GetDistance(template, screen) + distance := imagesAreEqual(expected, actual) if distance != 0 { os.Exit(terminate(fmt.Sprintf("Screenshot \"%s\" doesn't match expected image \"%s\". Distance of hashes difference: %d\n", actual, expected, distance))) @@ -58,19 +61,55 @@ func compareImages(expected string, actual string) { } func send(terminal *terminal.Terminal, cmd string) { - terminal.Write([]byte(cmd)) + err := terminal.Write([]byte(cmd)) + if err != nil { + panic(err) + } } func enter(terminal *terminal.Terminal) { - terminal.Write([]byte("\n")) + err := terminal.Write([]byte("\n")) + if err != nil { + panic(err) + } } -func validateScreen(img string) { - guiRef.Screenshot(img) +func validateScreen(img string, waitForChange bool) { + fmt.Printf("taking screenshot: %s and comparing...", img) + + err := guiRef.Screenshot(img) + if err != nil { + panic(err) + } + compareImages(strings.Join([]string{"vttest/", img}, ""), img) + fmt.Printf("compare OK\n") + enter(termRef) - sleep() + + if waitForChange { + fmt.Print("Waiting for screen change...") + attempts := 10 + for { + sleep() + actualScren := "temp.png" + err = guiRef.Screenshot(actualScren) + if err != nil { + panic(err) + } + distance := imagesAreEqual(actualScren, img) + if distance != 0 { + break + } + fmt.Printf(" %d", attempts) + attempts-- + if attempts <= 0 { + break + } + } + fmt.Print("done\n") + } } func TestMain(m *testing.M) { @@ -115,12 +154,12 @@ func TestCursorMovement(t *testing.T) { os.Exit(terminate(fmt.Sprintf("ActiveBuffer doesn't match vttest template vttest/test-cursor-movement-1"))) } - validateScreen("test-cursor-movement-1.png") - validateScreen("test-cursor-movement-2.png") - validateScreen("test-cursor-movement-3.png") - validateScreen("test-cursor-movement-4.png") - validateScreen("test-cursor-movement-5.png") - validateScreen("test-cursor-movement-6.png") + validateScreen("test-cursor-movement-1.png", true) + validateScreen("test-cursor-movement-2.png", true) + validateScreen("test-cursor-movement-3.png", true) + validateScreen("test-cursor-movement-4.png", true) + validateScreen("test-cursor-movement-5.png", true) + validateScreen("test-cursor-movement-6.png", false) g.Close() } @@ -142,21 +181,21 @@ func TestScreenFeatures(t *testing.T) { send(term, "2\n") sleep() - validateScreen("test-screen-features-1.png") - validateScreen("test-screen-features-2.png") - validateScreen("test-screen-features-3.png") - validateScreen("test-screen-features-4.png") - validateScreen("test-screen-features-5.png") - validateScreen("test-screen-features-6.png") - validateScreen("test-screen-features-7.png") - validateScreen("test-screen-features-8.png") - validateScreen("test-screen-features-9.png") - validateScreen("test-screen-features-10.png") - validateScreen("test-screen-features-11.png") - validateScreen("test-screen-features-12.png") - validateScreen("test-screen-features-13.png") - validateScreen("test-screen-features-14.png") - validateScreen("test-screen-features-15.png") + validateScreen("test-screen-features-1.png", true) + validateScreen("test-screen-features-2.png", true) + validateScreen("test-screen-features-3.png", true) + validateScreen("test-screen-features-4.png", true) + validateScreen("test-screen-features-5.png", true) + validateScreen("test-screen-features-6.png", true) + validateScreen("test-screen-features-7.png", true) + validateScreen("test-screen-features-8.png", true) + validateScreen("test-screen-features-9.png", true) + validateScreen("test-screen-features-10.png", true) + validateScreen("test-screen-features-11.png", true) + validateScreen("test-screen-features-12.png", true) + validateScreen("test-screen-features-13.png", true) + validateScreen("test-screen-features-14.png", true) + validateScreen("test-screen-features-15.png", false) g.Close() } @@ -178,10 +217,9 @@ func TestSixel(t *testing.T) { send(term, "clear\n") sleep() send(term, "cat example.sixel\n") - sleep(4) + sleep(10) // Displaying SIXEL graphics *sometimes* takes long time. Excessive synchronization??? - guiRef.Screenshot("test-sixel.png") - validateScreen("test-sixel.png") + validateScreen("test-sixel.png", false) g.Close() } From 68953464ba04a71c9a5bd622354b6cb2166e0dbf Mon Sep 17 00:00:00 2001 From: rrrooommmaaa Date: Wed, 20 Mar 2019 12:51:42 +0300 Subject: [PATCH 4/6] #249 no newlines when copying (#261) --- buffer/buffer.go | 3 ++- buffer/line.go | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/buffer/buffer.go b/buffer/buffer.go index 7ec9f93..a362a69 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -215,7 +215,7 @@ func (buffer *Buffer) GetSelectedText(selectionRegionMode SelectionRegionMode) s maxX := int(buffer.terminalState.viewWidth) - 1 if row == start.Line { minX = start.Col - } else if !line.wrapped { + } else if !line.wrapped && !line.nobreak { builder.WriteString("\n") } if row == end.Line { @@ -704,6 +704,7 @@ func (buffer *Buffer) Write(runes ...rune) { buffer.NewLineEx(true) newLine := buffer.getCurrentLine() + newLine.setNoBreak(true) if len(newLine.cells) == 0 { newLine.Append(buffer.terminalState.DefaultCell(true)) } diff --git a/buffer/line.go b/buffer/line.go index 270b26f..3919d44 100644 --- a/buffer/line.go +++ b/buffer/line.go @@ -6,12 +6,14 @@ import ( type Line struct { wrapped bool // whether line was wrapped onto from the previous one + nobreak bool // true if no line break at the beginning of the line cells []Cell } func newLine() Line { return Line{ wrapped: false, + nobreak: false, cells: []Cell{}, } } @@ -45,6 +47,10 @@ func (line *Line) setWrapped(wrapped bool) { line.wrapped = wrapped } +func (line *Line) setNoBreak(nobreak bool) { + line.nobreak = nobreak +} + func (line *Line) String() string { runes := []rune{} for _, cell := range line.cells { From efb31fd15e5acd8ceffefefe667d23143de86537 Mon Sep 17 00:00:00 2001 From: Michael Herrmann Date: Fri, 22 Mar 2019 17:14:14 +0100 Subject: [PATCH 5/6] Fix windows launcher (#255) * Add Windows launcher The launcher looks at directory "Versions" next to its executable. It finds the latest version and runs the executable in that directory with the same name as itself. For instance: Aminal.exe <- the launcher Versions/ 1.0.0/ Aminal.exe 1.0.1/ Aminal.exe In this example, running the top-level Aminal.exe (the launcher) starts Versions/1.0.1/Aminal.exe. Having a launcher allows Aminal to be updated while it is running. For example, version 1.0.1 could be downloaded without disturbing running instances of Aminal 1.0.0. * Implement a command-line installer for Windows It can be generated with the Make target installer-windows. It requires that you ran Make target launcher-windows before. * Implement Uninstaller for Windows * Codesign Windows auto-update executables * Don't require Admin privileges for Win uninstaller * Remove references to fman * Explain automatic updates in windows.md * Limit installer go packages compilation only to Windows platform * Fix Windows launcher * gofmt for launcher.go --- Makefile | 3 ++- windows/launcher/launcher.go | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 6de5891..deb15d5 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,8 @@ launcher-windows: build-windows if exist "bin\windows\Aminal" rmdir /S /Q "bin\windows\Aminal" mkdir "bin\windows\Aminal\Versions\${VERSION}" go build -o "bin\windows\Aminal\${BINARY}.exe" -ldflags "-H windowsgui" "${GEN_SRC_DIR}\launcher" - copy ${BINARY}-windows-amd64.exe "bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe" /Y + windres -o aminal.syso aminal.rc + go build -o "bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe" -ldflags "-H windowsgui" IF "${WINDOWS_CODESIGNING_CERT_PW}"=="" ECHO Environment variable WINDOWS_CODESIGNING_CERT_PW is not defined. & exit 1 signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\Aminal\${BINARY}.exe signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\Aminal\${BINARY}.exe diff --git a/windows/launcher/launcher.go b/windows/launcher/launcher.go index cd5cc5f..73098d8 100644 --- a/windows/launcher/launcher.go +++ b/windows/launcher/launcher.go @@ -23,10 +23,12 @@ import ( "io/ioutil" "os" "os/exec" + "os/user" "path/filepath" "sort" "strconv" "strings" + "syscall" ) type Version struct { @@ -42,8 +44,23 @@ func main() { versionsDir := filepath.Join(executableDir, "Versions") latestVersion, err := getLatestVersion(versionsDir) check(err) - target := filepath.Join(versionsDir, latestVersion, executableName) - cmd := exec.Command(target, os.Args[1:]...) + usr, err := user.Current() + check(err) + cmd := exec.Command("C:\\Windows\\System32\\cmd.exe", "/C", "start", "Aminal", "/B", executableName) + cmd.Dir = usr.HomeDir + latestVersionDir := filepath.Join(versionsDir, latestVersion) + path, pathSet := os.LookupEnv("PATH") + if pathSet { + path += ";" + latestVersionDir + } else { + path = latestVersionDir + } + cmd.Env = append(os.Environ(), "PATH="+path) + const CREATE_NO_WINDOW = 0x08000000 + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + CreationFlags: CREATE_NO_WINDOW, + } check(cmd.Start()) } From 7ed70154997b5b216bebdc77e5fc4c611a42f564 Mon Sep 17 00:00:00 2001 From: rrrooommmaaa Date: Sat, 23 Mar 2019 01:30:34 +0300 Subject: [PATCH 6/6] correct handling (ignoring) Privacy Message sequence (ESC ^) (#264) --- terminal/ansi.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/terminal/ansi.go b/terminal/ansi.go index e56f697..ee181ac 100644 --- a/terminal/ansi.go +++ b/terminal/ansi.go @@ -17,6 +17,7 @@ var ansiSequenceMap = map[rune]escapeSequenceHandler{ 'P': sixelHandler, 'c': risHandler, //RIS '#': screenStateHandler, + '^': privacyMessageHandler, '(': scs0Handler, // select character set into G0 ')': scs1Handler, // select character set into G1 '*': swallowHandler(1), // character set bullshit @@ -104,3 +105,24 @@ func tabSetHandler(pty chan rune, terminal *Terminal) error { terminal.terminalState.TabSetAtCursor() return nil } + +func privacyMessageHandler(pty chan rune, terminal *Terminal) error { + // Handler should lock the terminal if there will be write operations to any data read by the renderer + // terminal.Lock() + // defer terminal.Unlock() + + isEscaped := false + for { + b := <-pty + if b == 0x18 /*CAN*/ || b == 0x1a /*SUB*/ || (b == 0x5c /*backslash*/ && isEscaped) { + break + } + if isEscaped { + isEscaped = false + } else if b == 0x1b { + isEscaped = true + continue + } + } + return nil +}