From 05c45c08927735dc485806eeffe21f49baa172d1 Mon Sep 17 00:00:00 2001 From: nikitar020 <42252263+nikitar020@users.noreply.github.com> Date: Wed, 13 Mar 2019 14:16:32 +0200 Subject: [PATCH] Implement window manipulation CSI sequences (#256) * Implement window manipulation CSI sequences * Fix travis config to make gofmt only for go1.11 builds * remove commented out functions --- .travis.yml | 2 +- glfont/truetype.go | 2 +- gui/gui.go | 17 ++++ gui/windowManipulationImpl.go | 107 +++++++++++++++++++++++ main.go | 2 + terminal/csi.go | 4 - terminal/terminal.go | 16 ++++ terminal/windowManipulation.go | 153 +++++++++++++++++++++++++++++++++ 8 files changed, 297 insertions(+), 6 deletions(-) create mode 100644 gui/windowManipulationImpl.go create mode 100644 terminal/windowManipulation.go diff --git a/.travis.yml b/.travis.yml index c30aaba..64fe1cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ script: - if [[ $TRAVIS_OS_NAME == 'osx' ]]; then make build-darwin-native-travis; fi - if [[ $TRAVIS_OS_NAME == 'linux' ]]; then make build-linux-travis; fi - if [[ $TRAVIS_OS_NAME == 'linux' ]]; then make windows-cross-compile-travis; fi -- if [[ $TRAVIS_OS_NAME == 'linux' ]]; then make check-gofmt; fi +- if [[ $TRAVIS_OS_NAME == 'linux' && $TRAVIS_GO_VERSION =~ ^1\.11\. ]]; then echo 'check-gofmt'; make check-gofmt; fi env: global: - secure: "pdRpTOGQSUgbC9tK37voxUYJHMWDPJEmdMhNBsljpP9VnxxbR6JEFwvOQEmUHGlsYv8jma6a17jE60ngVQk8QP12cPh48i2bdbVgym/zTUOKFawCtPAzs8i7evh0di5eZ3uoyc42kG4skc+ePuVHbXC8jDxwaPpMqSHD7QyQc1/6ckI9LLkyWUqhnJJXkVwhmI74Aa1Im6QhywAWFMeTBRRL02cwr6k7VKSYOn6yrtzJRCALFGpZ/n58lPrpDxN7W8o+HRQP89wIDy8FyNeEPdmqGFNfMHDvI3oJRN4dGC4H9EkKf/iGuNJia1Bs+MgaG9kKlMHsI6Fkh5uw9KNTvC1llx43VRQJzm26cn1CpRxxRtF4F8lqkpY4tHjxxCitV+98ddW8jdmQYyx+LeueC5wqlO9g2M5L3oXsGMqZ++mDRDa8oQoQAVUSVtimeO8ODXFuVNR8TlupP0Cthgucil63VUZfAD8EHc2zpRSFxfYByDH53uMEinn20uovL6W42fqgboC43HOnR6aVfSANPsBFDlcpZFa2BY5RkcKyYdaLkucy0DKJ946UDfhOu6FNm0GPHq5HcgWkLojNF0dEFgG6J+SGQGiPjxTlHP/zoe61qMlWu+fYRXQnKWZN5Kk0T1TbAk6pKSE6wRLG8ddxvMg+eVpGLT+gAvQdrrkMFvs=" diff --git a/glfont/truetype.go b/glfont/truetype.go index c7856a2..57af926 100644 --- a/glfont/truetype.go +++ b/glfont/truetype.go @@ -48,7 +48,7 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale float32) (*Font, error) gl.BindVertexArray(f.vao) gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo) - gl.BufferData(gl.ARRAY_BUFFER, 6*4*4, nil, gl.STATIC_DRAW) + gl.BufferData(gl.ARRAY_BUFFER, 6*4*4, nil, gl.DYNAMIC_DRAW) vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00"))) gl.EnableVertexAttribArray(vertAttrib) diff --git a/gui/gui.go b/gui/gui.go index bc7fbaa..be228f5 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -65,6 +65,8 @@ type GUI struct { mouseMovedAfterSelectionStarted bool internalResize bool selectionRegionMode buffer.SelectionRegionMode + + mainThreadFunc chan func() } func Min(x, y int) int { @@ -167,6 +169,8 @@ func New(config *config.Config, terminal *terminal.Terminal, logger *zap.Sugared keyboardShortcuts: shortcuts, resizeLock: &sync.Mutex{}, internalResize: false, + + mainThreadFunc: make(chan func()), }, nil } @@ -490,6 +494,8 @@ Buffer Size: %d lines gui.resizeToTerminal() case reverse := <-reverseChan: gui.generateDefaultCell(reverse) + case funcForMainThread := <-gui.mainThreadFunc: + funcForMainThread() default: break terminalEvents } @@ -825,3 +831,14 @@ func (gui *GUI) windowPosChangeCallback(w *glfw.Window, xpos int, ypos int) { func (gui *GUI) monitorChangeCallback(monitor *glfw.Monitor, event glfw.MonitorEvent) { gui.SetDPIScale() } + +// Synchronously executes the argument function in the main thread. +// Does not return until f() executed! +func (gui *GUI) executeInMainThread(f func() error) error { + resultChan := make(chan error, 1) + gui.mainThreadFunc <- func() { + resultChan <- f() + } + gui.terminal.NotifyDirty() // wake up the main thread to allow processing + return <-resultChan +} diff --git a/gui/windowManipulationImpl.go b/gui/windowManipulationImpl.go new file mode 100644 index 0000000..e6c6459 --- /dev/null +++ b/gui/windowManipulationImpl.go @@ -0,0 +1,107 @@ +package gui + +import ( + "fmt" + "github.com/go-gl/glfw/v3.2/glfw" + "github.com/liamg/aminal/terminal" +) + +// +// Implementation of the terminal.WindowManipulationInterface +// + +func (gui *GUI) RestoreWindow(term *terminal.Terminal) error { + return gui.executeInMainThread(func() error { + gui.window.Restore() + return nil + }) +} + +func (gui *GUI) IconifyWindow(term *terminal.Terminal) error { + return gui.executeInMainThread(func() error { + return gui.window.Iconify() + }) +} + +func (gui *GUI) MoveWindow(term *terminal.Terminal, pixelX int, pixelY int) error { + return gui.executeInMainThread(func() error { + gui.window.SetPos(pixelX, pixelY) + return nil + }) +} + +func (gui *GUI) ResizeWindowByPixels(term *terminal.Terminal, pixelsHeight int, pixelsWidth int) error { + return gui.executeInMainThread(func() error { + term.Unlock() + gui.window.SetSize(pixelsWidth, pixelsHeight) + term.Lock() + return nil + }) +} + +func (gui *GUI) BringWindowToFront(term *terminal.Terminal) error { + var err error + if gui.window.GetAttrib(glfw.Iconified) != 0 { + err = gui.executeInMainThread(func() error { + return gui.window.Restore() + }) + } + + if err != nil { + err = gui.window.Focus() + } + + return err +} + +func (gui *GUI) ResizeWindowByChars(term *terminal.Terminal, charsHeight int, charsWidth int) error { + return gui.executeInMainThread(func() error { + return term.SetSize(uint(charsWidth), uint(charsHeight)) + }) +} + +func (gui *GUI) MaximizeWindow(term *terminal.Terminal) error { + return gui.executeInMainThread(func() error { + term.Lock() + err := gui.window.Maximize() + term.Unlock() + return err + }) +} + +func (gui *GUI) ReportWindowState(term *terminal.Terminal) error { + // Report xterm window state. If the xterm window is open (non-iconified), it returns CSI 1 t . + // If the xterm window is iconified, it returns CSI 2 t . + if gui.window.GetAttrib(glfw.Iconified) != 0 { + _ = term.Write([]byte("\x1b[2t")) + } else { + _ = term.Write([]byte("\x1b[1t")) + } + + return nil +} + +func (gui *GUI) ReportWindowPosition(term *terminal.Terminal) error { + // Report xterm window position as CSI 3 ; x; yt + x, y := gui.window.GetPos() + + _ = term.Write([]byte(fmt.Sprintf("\x1b[3;%d;%dt", x, y))) + + return nil +} + +func (gui *GUI) ReportWindowSizeInPixels(term *terminal.Terminal) error { + // Report xterm window in pixels as CSI 4 ; height ; width t + _ = term.Write([]byte(fmt.Sprintf("\x1b[4;%d;%dt", gui.height, gui.width))) + + return nil +} + +func (gui *GUI) ReportWindowSizeInChars(term *terminal.Terminal) error { + // Report the size of the text area in characters as CSI 8 ; height ; width t + charsWidth, charsHeight := gui.renderer.GetTermSize() + + _ = term.Write([]byte(fmt.Sprintf("\x1b[8;%d;%dt", charsHeight, charsWidth))) + + return nil +} diff --git a/main.go b/main.go index 0cd8fa0..99dc6a6 100644 --- a/main.go +++ b/main.go @@ -71,6 +71,8 @@ func initialize(unitTestfunc callback) { logger.Fatalf("Cannot start: %s", err) } + terminal.WindowManipulation = g + if unitTestfunc != nil { go unitTestfunc(terminal, g) } else { diff --git a/terminal/csi.go b/terminal/csi.go index 6a0e3f8..63275b2 100644 --- a/terminal/csi.go +++ b/terminal/csi.go @@ -425,10 +425,6 @@ func csiSetModeHandler(params []string, terminal *Terminal) error { return csiSetModes(params, true, terminal) } -func csiWindowManipulation(params []string, terminal *Terminal) error { - return fmt.Errorf("Window manipulation is not yet supported") -} - func csiLinePositionAbsolute(params []string, terminal *Terminal) error { row := 1 if len(params) > 0 { diff --git a/terminal/terminal.go b/terminal/terminal.go index 482cdc5..170031c 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -33,6 +33,20 @@ const ( MouseExtSGR ) +type WindowManipulationInterface interface { + RestoreWindow(term *Terminal) error + IconifyWindow(term *Terminal) error + MoveWindow(term *Terminal, pixelX int, pixelY int) error + ResizeWindowByPixels(term *Terminal, pixelsHeight int, pixelsWidth int) error + BringWindowToFront(term *Terminal) error + ResizeWindowByChars(term *Terminal, charsHeight int, charsWidth int) error + MaximizeWindow(term *Terminal) error + ReportWindowState(term *Terminal) error + ReportWindowPosition(term *Terminal) error + ReportWindowSizeInPixels(term *Terminal) error + ReportWindowSizeInChars(term *Terminal) error +} + type Terminal struct { program uint32 buffers []*buffer.Buffer @@ -56,6 +70,8 @@ type Terminal struct { terminalState *buffer.TerminalState platformDependentSettings platform.PlatformDependentSettings dirty *notifier + + WindowManipulation WindowManipulationInterface } type Modes struct { diff --git a/terminal/windowManipulation.go b/terminal/windowManipulation.go new file mode 100644 index 0000000..d604b07 --- /dev/null +++ b/terminal/windowManipulation.go @@ -0,0 +1,153 @@ +package terminal + +import ( + "fmt" + "strconv" +) + +func getOptionalIntegerParam(params []string, paramNo int, defValue int) (int, error) { + result := defValue + if len(params) >= paramNo+1 { + var err error + result, err = strconv.Atoi(params[paramNo]) + if err != nil { + return defValue, err + } + } + + return result, nil +} + +func getMandatoryIntegerParam(params []string, paramNo int) (int, error) { + if len(params) < paramNo+1 { + return 0, fmt.Errorf("no mandatory parameter") + } + + result, err := strconv.Atoi(params[paramNo]) + if err != nil { + return 0, err + } + + return result, nil +} + +func csiWindowManipulation(params []string, terminal *Terminal) error { + if terminal.WindowManipulation == nil { + return fmt.Errorf("Handler for CSI window manipulation commands is not set") + } + + operation, err := getMandatoryIntegerParam(params, 0) + if err != nil { + return fmt.Errorf("CSI t ignored: %s", err.Error()) + } + + switch operation { + case 1: + terminal.logger.Debug("De-iconify window") + return terminal.WindowManipulation.RestoreWindow(terminal) + + case 2: + terminal.logger.Debug("Iconify window") + return terminal.WindowManipulation.IconifyWindow(terminal) + + case 3: + terminal.logger.Debug("Move window") + { + x, err := getMandatoryIntegerParam(params, 1) + if err != nil { + return err + } + y, err := getMandatoryIntegerParam(params, 2) + if err != nil { + return err + } + + return terminal.WindowManipulation.MoveWindow(terminal, x, y) + } + + case 4: + terminal.logger.Debug("Resize the window in pixels") + { + height, err := getMandatoryIntegerParam(params, 1) + if err != nil { + return err + } + width, err := getMandatoryIntegerParam(params, 2) + if err != nil { + return err + } + + return terminal.WindowManipulation.ResizeWindowByPixels(terminal, height, width) + } + + case 5: + terminal.logger.Debug("Raise the window to the front") + return terminal.WindowManipulation.BringWindowToFront(terminal) + + case 6: + return fmt.Errorf("Lowering the window to the bottom is not implemented") + + case 7: + // NB: On Windows this sequence seem handled by the system + return fmt.Errorf("Refreshing the window is not implemented") + + case 8: + terminal.logger.Debug("Resize the text area in characters") + { + height, err := getMandatoryIntegerParam(params, 1) + if err != nil { + return err + } + width, err := getMandatoryIntegerParam(params, 2) + if err != nil { + return err + } + return terminal.WindowManipulation.ResizeWindowByChars(terminal, height, width) + } + + case 9: + { + p, err := getMandatoryIntegerParam(params, 1) + if err != nil { + return err + } + if p == 0 { + terminal.logger.Debug("Restore maximized window") + return terminal.WindowManipulation.RestoreWindow(terminal) + } else if p == 1 { + terminal.logger.Debug("Maximize window") + return terminal.WindowManipulation.MaximizeWindow(terminal) + } + } + + case 11: + terminal.logger.Debug("Report the window state") + return terminal.WindowManipulation.ReportWindowState(terminal) + + case 13: + terminal.logger.Debug("Report the window position") + return terminal.WindowManipulation.ReportWindowPosition(terminal) + + case 14: + terminal.logger.Debug("Report the window size in pixels") + return terminal.WindowManipulation.ReportWindowSizeInPixels(terminal) + + case 18: + terminal.logger.Debug("Report the window size in characters as CSI 8") + return terminal.WindowManipulation.ReportWindowSizeInChars(terminal) + + case 19: + return fmt.Errorf("Reporting the screen size in characters is not implemented") + + case 20: + return fmt.Errorf("Reporting the window icon label is not implemented") + + case 21: + return fmt.Errorf("Reporting the window title is not implemented") + + default: + return fmt.Errorf("not supported CSI t") + } + + return nil +}