From c69e7b85e85566ac97dc0a4b5b938d927a5e968a Mon Sep 17 00:00:00 2001 From: Thomas Buckley-Houston Date: Mon, 16 May 2016 00:49:34 +0800 Subject: [PATCH] Hiptext dimensions. Better zooming. Tests. Conforming to hiptext dimensions. Bounding the zoom window so as not to overlap. Some basic tests. --- run.sh | 10 +-- stdin_forward.go | 161 +++++++++++++++++++++++++++++++++--------- stdin_forward_test.go | 121 +++++++++++++++++++++++++++++-- 3 files changed, 248 insertions(+), 44 deletions(-) diff --git a/run.sh b/run.sh index 35311ac..25bf16f 100755 --- a/run.sh +++ b/run.sh @@ -34,15 +34,15 @@ ffmpeg \ sleep 1 # Intercept STDIN (mouse and keypresses) and forward to the X framebuffer via xdotool -(./stdin_forward <&3) 3<&0 +(./stdin_forward <&3 &) 3<&0 # Hiptext renders images and videos into text characters displayable in a terminal # Hiptext complains unless you specify the exact path to the font, seems like a bug to me # TODO: support variable width, ideally dynamic sizing -# hiptext \ -# -font /usr/share/fonts/ttf-dejavu/DejaVuSansMono.ttf \ -# $UDP_URI \ -# 2> hiptext.log +hiptext \ + -font /usr/share/fonts/ttf-dejavu/DejaVuSansMono.ttf \ + $UDP_URI \ + 2> hiptext.log # Kill all the subprocesses created in this script if the script itself exits trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT diff --git a/stdin_forward.go b/stdin_forward.go index 3a9f230..35b9e36 100644 --- a/stdin_forward.go +++ b/stdin_forward.go @@ -2,26 +2,68 @@ package main import ( "fmt" - "github.com/nsf/termbox-go" "strings" "os" "os/exec" + "math" + "github.com/nsf/termbox-go" ) var current string var curev termbox.Event var lastMouseButton string -var desktopX int -var desktopY int -var termWidth, termHeight = termbox.Size() +var desktopWidth float32 = 1600 +var desktopHeight float32 = 1200 +var desktopXFloat float32 +var desktopYFloat float32 +var roundedDesktopX int +var roundedDesktopY int +// Dimensions of hiptext output +var hipWidth int +var hipHeight int // For keeping track of the zoom -var zoomLevel float32 = 1 -var viewport = map[string] float32 { - "xSize": 1600, - "ySize": 1200, - "xOffset": 0, - "yOffset": 0, +// TODO: look at the XFCE code to accurately determine the factor. It may +// even be linear. +var zoomFactor float32 = 0.03 +var maxZoom float32 = 1000000 +var zoomLevel float32 +var viewport map[string] float32 + +func initialise() { + log("Starting...") + calculateHipDimensions() + zoomLevel = 1 + viewport = map[string] float32 { + "xSize": desktopWidth, + "ySize": desktopHeight, + "xOffset": 0, + "yOffset": 0, + } +} + +// Hiptext needs to render the aspect ratio faithfully. So firstly it tries to fill +// the terminal as much as it can. And secondly it treat a row as representing twice +// as much as a column - thus why there are some multiplications/divisions by 2. +func calculateHipDimensions() { + _tw, _th := termbox.Size() + tw := float32(_tw) + th := float32(_th * 2) + ratio := desktopWidth / desktopHeight + bestHeight := min(th, (tw / ratio)) + bestWidth := min(tw, (bestHeight * ratio)) + // Not sure why the +1 and -1 are needed + hipWidth = roundToInt(bestWidth) + 1 + hipHeight = roundToInt(bestHeight / 2) - 1 + log(fmt.Sprintf("Term dimensions: W: %d, H: %d", _tw, _th)) + log(fmt.Sprintf("Hiptext dimensions: W: %d, H: %d", hipWidth, hipHeight)) +} + +func min(a float32, b float32) float32 { + if a < b { + return a + } + return b } func log(msg string) { @@ -47,6 +89,21 @@ func xdotool(args ...string) { } } +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. +func ctrlPressed() bool { + return curev.Mod&termbox.ModCtrl != 0 +} + // Convert Termbox symbols to xdotool arguments func mouseButtonStr(k termbox.Key) []string { switch k { @@ -62,13 +119,13 @@ func mouseButtonStr(k termbox.Key) []string { case termbox.MouseRelease: return []string{"mouseup", lastMouseButton} case termbox.MouseWheelUp: - if curev.Mod&termbox.ModCtrl != 0 { - trackZoom("out") + if ctrlPressed() { + trackZoom("in") } return []string{"click", "4"} case termbox.MouseWheelDown: - if curev.Mod&termbox.ModCtrl != 0 { - trackZoom("in") + if ctrlPressed() { + trackZoom("out") } return []string{"click", "5"} } @@ -100,18 +157,17 @@ func mouseEvent() { curev.MouseX, curev.MouseY, mouseButtonStr(curev.Key), modStr(curev.Mod))) // CTRL allows the user to drag the mouse to pan and zoom the desktop. - if curev.Mod&termbox.ModCtrl != 0 { + if ctrlPressed() { xdotool("keydown", "alt") } else { xdotool("keyup", "alt") } - // Always move the mouse first. This is because we're not constantly updating the mouse position, - // *unless* a drag event is happening. This saves bandwidth and also mouse movement isn't supported + // *unless* a drag event is happening. This saves bandwidth. Also, mouse movement isn't supported // on all terminals. setCurrentDesktopCoords() - xdotool("mousemove", fmt.Sprintf("%d", desktopX), fmt.Sprintf("%d", desktopY)) + xdotool("mousemove", fmt.Sprintf("%d", roundedDesktopX), fmt.Sprintf("%d", roundedDesktopY)) // Send a button press to X. Note that the "Motion" modifier is sent when the user is doing // a drag event and thus mouse reporting will be constantly streamed. @@ -122,35 +178,74 @@ func mouseEvent() { // Convert terminal coords into desktop coords func setCurrentDesktopCoords() { - termWidthFloat := float32(termWidth) - termHeightFloat := float32(termHeight) + hipWidthFloat := float32(hipWidth) + hipHeightFloat := float32(hipHeight) eventX := float32(curev.MouseX) eventY := float32(curev.MouseY) - x := (eventX * (viewport["xSize"] / termWidthFloat)) + viewport["xOffset"] - y := (eventY * (viewport["ySize"] / termHeightFloat)) + viewport["yOffset"] - desktopX = int(x) - desktopY = int(y) + desktopXFloat = (eventX * (viewport["xSize"] / hipWidthFloat)) + viewport["xOffset"] + desktopYFloat = (eventY * (viewport["ySize"] / hipHeightFloat)) + viewport["yOffset"] + log( + fmt.Sprintf( + "setCurrentDesktopCoords: tw: %d, th: %d, dx: %d, dy: %d", + hipHeightFloat, hipWidthFloat, desktopXFloat, desktopYFloat)) + roundedDesktopX = roundToInt(desktopXFloat) + roundedDesktopY = roundToInt(desktopYFloat) } -// XFCE doesn't provide the current zoom, so we need to keep track of it. -// For every zoom level the terminal coords will be mapped differently onto the X desktop. +// XFCE doesn't provide the current zoom, so *we* need to keep track of it. +// For every zoom level, the terminal coords will be mapped differently onto the X desktop. // TODO: support custom desktop sizes. func trackZoom(direction string) { if direction == "in" { - zoomLevel += 0.2 + if zoomLevel <= maxZoom { + zoomLevel += zoomFactor + } else { + return + } } else { - zoomLevel -= 0.2 + if zoomLevel >= 1 { + zoomLevel -= zoomFactor + } else { + return + } } + // Use the existing viewport to get the current coords setCurrentDesktopCoords() - desktopXFloat := float32(desktopX) - desktopYFloat := float32(desktopY) + + // The actual zoom + viewport["xSize"] = desktopWidth / zoomLevel + viewport["ySize"] = desktopHeight / zoomLevel viewport["xOffset"] = desktopXFloat - (viewport["xSize"] / 2) viewport["yOffset"] = desktopYFloat - (viewport["ySize"] / 2) - viewport["xSize"] = 1600 / zoomLevel - viewport["ySize"] = 1200 / zoomLevel + + keepViewportInDesktop() + + log(fmt.Sprintf("zoom: %s", zoomLevel)) log(fmt.Sprintf("viewport: %s", viewport)) } +// When zooming near the edges of the desktop it is possible that the viewport's edges overlap +// the desktop's edges. So just limit the possible movement of the viewport. +func keepViewportInDesktop() { + xLeft := viewport["xOffset"] + xRight := viewport["xOffset"] + viewport["xSize"] + yTop := viewport["yOffset"] + yBottom := viewport["yOffset"] + viewport["ySize"] + + if xLeft < 0 { + viewport["xOffset"] = 0 + } + if xRight > desktopWidth { + viewport["xOffset"] = desktopWidth - viewport["xSize"] + } + if yTop < 0 { + viewport["yOffset"] = 0 + } + if yBottom > desktopHeight { + viewport["yOffset"] = desktopHeight - viewport["ySize"] + } +} + // Convert a keyboard event into an xdotool command func keyEvent() { // I've no idea why this gets picked up by the terminal, or what it refers to. But whatever, we don't @@ -186,6 +281,7 @@ func main() { } defer termbox.Close() termbox.SetInputMode(termbox.InputMouse) + initialise() parseInput() data := make([]byte, 0, 64) @@ -202,6 +298,7 @@ mainloop: case termbox.EventRaw: data = data[:beg+ev.N] current = fmt.Sprintf("%q", data) + // TODO: think of a different way to exit, 'q' will be needed for actual text input. if current == `"q"` { break mainloop } diff --git a/stdin_forward_test.go b/stdin_forward_test.go index d6119a7..39bcde3 100644 --- a/stdin_forward_test.go +++ b/stdin_forward_test.go @@ -3,6 +3,7 @@ package main import ( "testing" "github.com/nsf/termbox-go" + "github.com/stretchr/testify/assert" ) func setup() { @@ -10,19 +11,125 @@ func setup() { if err != nil { panic(err) } + initialise() + hipWidth = 90 + hipHeight = 30 + setCurrentDesktopCoords() } func teardown() { termbox.Close() } -func TestPoint(t *testing.T) { +func TestPointTL(t *testing.T) { + assert := assert.New(t) + curev.MouseX = 30 + curev.MouseY = 10 setup() - curev.MouseX = 11 - curev.MouseY = 11 - termWidth = 90 - termWidth = 30 - setCurrentDesktopCoords() - t.Error(desktopX, desktopY) + + assert.Equal(533, roundedDesktopX, "Mapped X coord") + assert.Equal(400, roundedDesktopY, "Mapped Y coord") + + teardown() +} + +func TestPointMiddle(t *testing.T) { + assert := assert.New(t) + curev.MouseX = 45 + curev.MouseY = 15 + setup() + + assert.Equal(800, roundedDesktopX, "Mapped X coord") + assert.Equal(600, roundedDesktopY, "Mapped Y coord") + + teardown() +} + +func TestPointBR(t *testing.T) { + assert := assert.New(t) + curev.MouseX = 60 + curev.MouseY = 20 + setup() + + assert.Equal(1067, roundedDesktopX, "Mapped X coord") + assert.Equal(800, roundedDesktopY, "Mapped Y coord") + + teardown() +} + + +func TestZoomIn(t *testing.T) { + assert := assert.New(t) + curev.MouseX = 30 + curev.MouseY = 10 + setup() + + trackZoom("in") + setCurrentDesktopCoords() + + assert.Equal(444, roundedDesktopX, "Mapped X coord") + assert.Equal(333, roundedDesktopY, "Mapped Y coord") + + teardown() +} + +func TestZoomInMiddle(t *testing.T) { + assert := assert.New(t) + curev.MouseX = 45 + curev.MouseY = 15 + setup() + + trackZoom("in") + setCurrentDesktopCoords() + + assert.Equal(800, roundedDesktopX, "Mapped X coord") + assert.Equal(600, roundedDesktopY, "Mapped Y coord") + + teardown() +} + +func TestZoomInOut(t *testing.T) { + assert := assert.New(t) + curev.MouseX = 45 + curev.MouseY = 15 + setup() + + trackZoom("in") + trackZoom("out") + setCurrentDesktopCoords() + + assert.Equal(800, roundedDesktopX, "Mapped X coord") + assert.Equal(600, roundedDesktopY, "Mapped Y coord") + + teardown() +} + +func TestZoomInEdgeTL(t *testing.T) { + assert := assert.New(t) + curev.MouseX = 0 + curev.MouseY = 0 + setup() + + trackZoom("in") + setCurrentDesktopCoords() + + assert.Equal(0, roundedDesktopX, "Mapped X coord") + assert.Equal(0, roundedDesktopY, "Mapped Y coord") + + teardown() +} + +func TestZoomInEdgeBR(t *testing.T) { + assert := assert.New(t) + curev.MouseX = hipWidth + curev.MouseY = hipHeight + setup() + + trackZoom("in") + setCurrentDesktopCoords() + + assert.Equal(int(desktopWidth), roundedDesktopX, "Mapped X coord") + assert.Equal(int(desktopHeight), roundedDesktopY, "Mapped Y coord") + teardown() }