490 lines
11 KiB
Go
490 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/tombh/termbox-go"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Import the xzoom C code that creates an X window that zooms
|
|
// and pans the desktop.
|
|
// It's written in C because it borrows from the original xzoom
|
|
// program: http://git.r-36.net/xzoom/
|
|
// NB: The following comments are parsed by `go build` ...
|
|
|
|
// #cgo LDFLAGS: -lXext -lX11 -lXt
|
|
// #include "../xzoom/xzoom.h"
|
|
import "C"
|
|
|
|
var logfile string
|
|
var current string
|
|
var curev termbox.Event
|
|
var lastMouseButton string
|
|
|
|
var hipWidth int
|
|
var hipHeight int
|
|
var envDesktopWidth int
|
|
var envDesktopHeight int
|
|
var desktopWidth float32
|
|
var desktopHeight float32
|
|
var desktopXFloat float32
|
|
var desktopYFloat float32
|
|
var roundedDesktopX int
|
|
var roundedDesktopY int
|
|
|
|
// Channels to control the background xzoom go routine
|
|
var stopXZoomChannel = make(chan struct{})
|
|
var xZoomStoppedChannel = make(chan struct{})
|
|
|
|
var panNeedsSetup bool
|
|
var panStartingX float32
|
|
var panStartingY float32
|
|
|
|
func initialise() {
|
|
setupLogging()
|
|
log("Starting...")
|
|
setupTermbox()
|
|
setupDimensions()
|
|
C.xzoom_init()
|
|
xzoomBackground()
|
|
}
|
|
|
|
func parseENVVar(variable string) int {
|
|
value, err := strconv.Atoi(os.Getenv(variable))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func setupDimensions() {
|
|
hipWidth = parseENVVar("TTY_WIDTH")
|
|
hipHeight = parseENVVar("TTY_HEIGHT")
|
|
envDesktopWidth = parseENVVar("DESKTOP_WIDTH")
|
|
envDesktopHeight = parseENVVar("DESKTOP_HEIGHT")
|
|
C.desktop_width = C.int(envDesktopWidth)
|
|
C.width[C.SRC] = C.desktop_width
|
|
C.width[C.DST] = C.desktop_width
|
|
C.desktop_height = C.int(envDesktopHeight)
|
|
C.height[C.SRC] = C.desktop_height
|
|
C.height[C.DST] = C.desktop_height
|
|
desktopWidth = float32(envDesktopWidth)
|
|
desktopHeight = float32(envDesktopHeight)
|
|
log(fmt.Sprintf("Desktop dimensions: W: %d, H: %d", envDesktopWidth, envDesktopHeight))
|
|
log(fmt.Sprintf("Term dimensions: W: %d, H: %d", hipWidth, hipHeight))
|
|
}
|
|
|
|
func setupTermbox() {
|
|
err := termbox.Init()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
termbox.SetInputMode(termbox.InputMouse)
|
|
}
|
|
|
|
func setupLogging() {
|
|
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
os.Mkdir(filepath.Join(dir, "..", "logs"), os.ModePerm)
|
|
logfile = fmt.Sprintf(filepath.Join(dir, "..", "logs", "input.log"))
|
|
if _, err := os.Stat(logfile); err == nil {
|
|
os.Truncate(logfile, 0)
|
|
}
|
|
}
|
|
|
|
func log(msg string) {
|
|
f, oErr := os.OpenFile(logfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
|
if oErr != nil {
|
|
panic(oErr)
|
|
}
|
|
defer f.Close()
|
|
|
|
msg = msg + "\n"
|
|
if _, wErr := f.WriteString(msg); wErr != nil {
|
|
panic(wErr)
|
|
}
|
|
}
|
|
|
|
func min(a float32, b float32) float32 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func getXGrab() int {
|
|
return int(C.xgrab)
|
|
}
|
|
func getYGrab() int {
|
|
return int(C.ygrab)
|
|
}
|
|
|
|
// Issue an xdotool command to simulate mouse and keyboard input
|
|
func xdotool(args ...string) {
|
|
log(strings.Join(args, " "))
|
|
if args[0] == "noop" {
|
|
return
|
|
}
|
|
if err := exec.Command("xdotool", args...).Run(); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func roundToInt(value32 float32) int {
|
|
var rounded float64
|
|
value := float64(value32)
|
|
if value < 0 {
|
|
rounded = math.Ceil(value - 0.5)
|
|
}
|
|
rounded = math.Floor(value + 0.5)
|
|
return int(rounded)
|
|
}
|
|
|
|
// Whether the current input event includes a depressed CTRL key.
|
|
// Waiting for this PR: https://github.com/nsf/termbox-go/pull/126
|
|
func ctrlPressed() bool {
|
|
return curev.Mod&termbox.ModCtrl != 0
|
|
}
|
|
|
|
// Whether the mouse is moving
|
|
func mouseMotion() bool {
|
|
return curev.Mod&termbox.ModMotion != 0
|
|
}
|
|
|
|
// Convert Termbox symbols to xdotool arguments
|
|
func mouseButtonStr(k termbox.Key) []string {
|
|
switch k {
|
|
case termbox.MouseLeft:
|
|
lastMouseButton = "1"
|
|
return []string{"mousedown", lastMouseButton}
|
|
case termbox.MouseMiddle:
|
|
lastMouseButton = "2"
|
|
return []string{"mousedown", lastMouseButton}
|
|
case termbox.MouseRight:
|
|
lastMouseButton = "3"
|
|
return []string{"mousedown", lastMouseButton}
|
|
case termbox.MouseRelease:
|
|
return []string{"mouseup", lastMouseButton}
|
|
case termbox.MouseWheelUp:
|
|
if ctrlPressed() {
|
|
zoom("in")
|
|
return []string{"noop"}
|
|
}
|
|
return []string{"click", "4"}
|
|
case termbox.MouseWheelDown:
|
|
if ctrlPressed() {
|
|
zoom("out")
|
|
return []string{"noop"}
|
|
}
|
|
return []string{"click", "5"}
|
|
}
|
|
return []string{""}
|
|
}
|
|
|
|
func zoom(direction string) {
|
|
oldZoom := C.magnification
|
|
|
|
// The actual zoom
|
|
if direction == "in" {
|
|
C.magnification++
|
|
} else {
|
|
if C.magnification > 1 {
|
|
C.magnification--
|
|
}
|
|
}
|
|
C.width[C.SRC] = (C.desktop_width + C.magnification - 1) / C.magnification
|
|
C.height[C.SRC] = (C.desktop_height + C.magnification - 1) / C.magnification
|
|
|
|
moveViewportForZoom(oldZoom)
|
|
keepViewportInDesktop()
|
|
}
|
|
|
|
// Move the viewport so that the mouse is still over the same part of
|
|
// the desktop.
|
|
func moveViewportForZoom(oldZoom C.int) {
|
|
factor := float32(oldZoom) / float32(C.magnification)
|
|
magnifiedRelativeX := factor * (desktopXFloat - float32(C.xgrab))
|
|
magnifiedRelativeY := factor * (desktopYFloat - float32(C.ygrab))
|
|
C.xgrab = C.int(desktopXFloat - magnifiedRelativeX)
|
|
C.ygrab = C.int(desktopYFloat - magnifiedRelativeY)
|
|
}
|
|
|
|
func keepViewportInDesktop() {
|
|
manageViewportSize()
|
|
manageViewportPosition()
|
|
}
|
|
|
|
func manageViewportSize() {
|
|
if C.width[C.SRC] < 1 {
|
|
C.width[C.SRC] = 1
|
|
}
|
|
if C.width[C.SRC] > C.desktop_width {
|
|
C.width[C.SRC] = C.desktop_width
|
|
}
|
|
if C.height[C.SRC] < 1 {
|
|
C.height[C.SRC] = 1
|
|
}
|
|
if C.height[C.SRC] > C.desktop_height {
|
|
C.height[C.SRC] = C.desktop_height
|
|
}
|
|
}
|
|
|
|
func manageViewportPosition() {
|
|
if C.xgrab > (C.desktop_width - C.width[C.SRC]) {
|
|
C.xgrab = C.desktop_width - C.width[C.SRC]
|
|
}
|
|
if C.xgrab < 0 {
|
|
C.xgrab = 0
|
|
}
|
|
if C.ygrab > (C.desktop_height - C.height[C.SRC]) {
|
|
C.ygrab = C.desktop_height - C.height[C.SRC]
|
|
}
|
|
if C.ygrab < 0 {
|
|
C.ygrab = 0
|
|
}
|
|
}
|
|
|
|
// Auxillary data. Whether the mouse was moving or a mod key like CTRL
|
|
// is being pressed at the same time.
|
|
func modStr(m termbox.Modifier) string {
|
|
var out []string
|
|
if mouseMotion() {
|
|
out = append(out, "Motion")
|
|
}
|
|
if ctrlPressed() {
|
|
out = append(out, "Ctrl")
|
|
}
|
|
return strings.Join(out, " ")
|
|
}
|
|
|
|
func isPanning() bool {
|
|
return ctrlPressed() && mouseMotion() && lastMouseButton == "1"
|
|
}
|
|
|
|
func mouseEvent() {
|
|
// Figure out where the mouse is on the actual real desktop.
|
|
// Note that the zomming and panning code effectively keep the mouse in the exact same position relative
|
|
// to the desktop, so mousemove *should* have no effect.
|
|
setCurrentDesktopCoords()
|
|
|
|
// Always move the mouse first so that button presses are correct. This is because we're not constantly
|
|
// updating the mouse position, *unless* a drag event is happening. This saves bandwidth. Also, mouse
|
|
// movement isn't supported on all terminals.
|
|
xdotool("mousemove", fmt.Sprintf("%d", roundedDesktopX), fmt.Sprintf("%d", roundedDesktopY))
|
|
|
|
if isPanning() {
|
|
pan()
|
|
} else {
|
|
panNeedsSetup = true
|
|
// Pressing of CTRL indicates that the user is panning or zooming, so there is no need to send
|
|
// button presses.
|
|
// TODO: What about CTRL+leftbutton to open new tab!?
|
|
if !ctrlPressed() {
|
|
xdotool(mouseButtonStr(curev.Key)...)
|
|
}
|
|
}
|
|
}
|
|
|
|
func pan() {
|
|
if panNeedsSetup {
|
|
panStartingX = desktopXFloat
|
|
panStartingY = desktopYFloat
|
|
panNeedsSetup = false
|
|
}
|
|
C.xgrab = C.int(float32(C.xgrab) + panStartingX - desktopXFloat)
|
|
C.ygrab = C.int(float32(C.ygrab) + panStartingY - desktopYFloat)
|
|
keepViewportInDesktop()
|
|
}
|
|
|
|
// Convert terminal coords into desktop coords
|
|
func setCurrentDesktopCoords() {
|
|
hipWidthFloat := float32(hipWidth)
|
|
hipHeightFloat := float32(hipHeight)
|
|
eventX := float32(curev.MouseX)
|
|
eventY := float32(curev.MouseY)
|
|
width := float32(C.width[C.SRC])
|
|
height := float32(C.height[C.SRC])
|
|
xOffset := float32(C.xgrab)
|
|
yOffset := float32(C.ygrab)
|
|
desktopXFloat = (eventX * (width / hipWidthFloat)) + xOffset
|
|
desktopYFloat = (eventY * (height / hipHeightFloat)) + yOffset
|
|
roundedDesktopX = roundToInt(desktopXFloat)
|
|
roundedDesktopY = roundToInt(desktopYFloat)
|
|
log(
|
|
fmt.Sprintf(
|
|
"setCurrentDesktopCoords: tw: %d, th: %d, dx: %d, dy: %d, mag: %d",
|
|
hipHeightFloat, hipWidthFloat, desktopXFloat, desktopYFloat, C.magnification))
|
|
}
|
|
|
|
// Convert a keyboard event into an xdotool command
|
|
// See: http://wiki.linuxquestions.org/wiki/List_of_Keysyms_Recognised_by_Xmodmap
|
|
func keyEvent() {
|
|
var command string
|
|
log(fmt.Sprintf("EventKey: k: %d, c: %c, mod: %s", curev.Key, curev.Ch, modStr(curev.Mod)))
|
|
|
|
key := getSpecialKeyPress()
|
|
|
|
if curev.Key == 0 {
|
|
key = fmt.Sprintf("%c", curev.Ch)
|
|
command = "type"
|
|
} else {
|
|
command = "key"
|
|
}
|
|
|
|
// What is this? It always appears when the program starts :/
|
|
badkey := fmt.Sprintf("%s", curev.Ch) == "%!s(int32=0)" && curev.Key == 0
|
|
|
|
if key == "" || badkey {
|
|
log(fmt.Sprintf("No key found for keycode: %d"))
|
|
return
|
|
}
|
|
|
|
xdotool(command, key)
|
|
}
|
|
|
|
func getSpecialKeyPress() string {
|
|
var key string
|
|
switch curev.Key {
|
|
case termbox.KeyEnter:
|
|
key = "Return"
|
|
case termbox.KeyBackspace, termbox.KeyBackspace2:
|
|
key = "BackSpace"
|
|
case termbox.KeySpace:
|
|
key = "Space"
|
|
case termbox.KeyF1:
|
|
key = "F1"
|
|
case termbox.KeyF2:
|
|
key = "F2"
|
|
case termbox.KeyF3:
|
|
key = "F3"
|
|
case termbox.KeyF4:
|
|
key = "F4"
|
|
case termbox.KeyF5:
|
|
key = "F5"
|
|
case termbox.KeyF6:
|
|
key = "F6"
|
|
case termbox.KeyF7:
|
|
key = "F7"
|
|
case termbox.KeyF8:
|
|
key = "F8"
|
|
case termbox.KeyF9:
|
|
key = "F9"
|
|
case termbox.KeyF10:
|
|
key = "F10"
|
|
case termbox.KeyF11:
|
|
key = "F11"
|
|
case termbox.KeyF12:
|
|
key = "F12"
|
|
case termbox.KeyInsert:
|
|
key = "Insert"
|
|
case termbox.KeyDelete:
|
|
key = "Delete"
|
|
case termbox.KeyHome:
|
|
key = "Home"
|
|
case termbox.KeyEnd:
|
|
key = "End"
|
|
case termbox.KeyPgup:
|
|
key = "Prior"
|
|
case termbox.KeyPgdn:
|
|
key = "Next"
|
|
case termbox.KeyArrowUp:
|
|
key = "Up"
|
|
case termbox.KeyArrowDown:
|
|
key = "Down"
|
|
case termbox.KeyArrowLeft:
|
|
key = "Left"
|
|
case termbox.KeyArrowRight:
|
|
key = "Right"
|
|
case termbox.KeyCtrlL:
|
|
key = "ctrl+l"
|
|
}
|
|
return key
|
|
}
|
|
|
|
func parseInput() {
|
|
switch curev.Type {
|
|
case termbox.EventKey:
|
|
keyEvent()
|
|
case termbox.EventMouse:
|
|
log(
|
|
fmt.Sprintf(
|
|
"EventMouse: x: %d, y: %d, b: %s, mod: %s",
|
|
curev.MouseX, curev.MouseY, mouseButtonStr(curev.Key), modStr(curev.Mod)))
|
|
mouseEvent()
|
|
case termbox.EventNone:
|
|
log("EventNone")
|
|
}
|
|
}
|
|
|
|
// Run the xzoom window in a background go routine
|
|
func xzoomBackground() {
|
|
go func() {
|
|
defer close(xZoomStoppedChannel)
|
|
for {
|
|
select {
|
|
default:
|
|
C.do_iteration()
|
|
time.Sleep(40 * time.Millisecond) // 25fps
|
|
case <-stopXZoomChannel:
|
|
// Gracefully close the xzoom go routine
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func teardown() {
|
|
termbox.Close()
|
|
close(stopXZoomChannel)
|
|
<-xZoomStoppedChannel
|
|
}
|
|
|
|
// I'm afraid I don't understand most of what this does :/
|
|
// TODO: if anyone can shed some light on this. Add some comments, refactor it...
|
|
func mainLoop() {
|
|
data := make([]byte, 0, 64)
|
|
for {
|
|
if cap(data)-len(data) < 32 {
|
|
newdata := make([]byte, len(data), len(data)+32)
|
|
copy(newdata, data)
|
|
data = newdata
|
|
}
|
|
beg := len(data)
|
|
d := data[beg : beg+32]
|
|
switch ev := termbox.PollRawEvent(d); ev.Type {
|
|
case termbox.EventRaw:
|
|
data = data[:beg+ev.N]
|
|
current = fmt.Sprintf("%q", data)
|
|
for {
|
|
ev := termbox.ParseEvent(data)
|
|
if ev.N == 0 {
|
|
break
|
|
}
|
|
curev = ev
|
|
copy(data, data[curev.N:])
|
|
data = data[:len(data)-curev.N]
|
|
}
|
|
case termbox.EventError:
|
|
panic(ev.Err)
|
|
}
|
|
parseInput()
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
initialise()
|
|
defer func() {
|
|
teardown()
|
|
}()
|
|
mainLoop()
|
|
}
|