Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions server/cmd/api/api/computer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -426,6 +427,85 @@ func (s *ApiService) SetCursor(ctx context.Context, request oapi.SetCursorReques
return oapi.SetCursor200JSONResponse{Ok: true}, nil
}

// parseMousePosition parses xdotool getmouselocation --shell output.
// Expected format:
//
// X=100
// Y=200
// SCREEN=0
// WINDOW=12345
//
// Returns x, y coordinates and an error if parsing fails.
func parseMousePosition(output string) (x, y int, err error) {
outStr := strings.TrimSpace(output)
if outStr == "" {
return 0, 0, fmt.Errorf("empty output")
}

var xParsed, yParsed bool

for line := range strings.SplitSeq(outStr, "\n") {
line = strings.TrimSpace(line)
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key, value := parts[0], parts[1]
switch key {
case "X":
if parsed, parseErr := strconv.Atoi(value); parseErr == nil {
x = parsed
xParsed = true
}
case "Y":
if parsed, parseErr := strconv.Atoi(value); parseErr == nil {
y = parsed
yParsed = true
}
}
// Early exit once both coordinates are found
if xParsed && yParsed {
break
}
}

if !xParsed || !yParsed {
return 0, 0, fmt.Errorf("failed to parse coordinates from output: %q", outStr)
}

return x, y, nil
}

func (s *ApiService) GetMousePosition(ctx context.Context, request oapi.GetMousePositionRequestObject) (oapi.GetMousePositionResponseObject, error) {
log := logger.FromContext(ctx)

// serialize input operations to avoid race conditions with other xdotool commands
s.inputMu.Lock()
defer s.inputMu.Unlock()

// Execute xdotool getmouselocation --shell for parseable output
output, err := defaultXdoTool.Run(ctx, "getmouselocation", "--shell")
if err != nil {
log.Error("xdotool getmouselocation failed", "err", err, "output", string(output))
return oapi.GetMousePosition500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{
Message: "failed to get mouse position"},
}, nil
}

x, y, err := parseMousePosition(string(output))
if err != nil {
log.Error("failed to parse mouse position", "err", err, "output", string(output))
return oapi.GetMousePosition500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{
Message: "failed to parse mouse position from xdotool output"},
}, nil
}

return oapi.GetMousePosition200JSONResponse{
X: x,
Y: y,
}, nil
}

func (s *ApiService) PressKey(ctx context.Context, request oapi.PressKeyRequestObject) (oapi.PressKeyResponseObject, error) {
log := logger.FromContext(ctx)

Expand Down
91 changes: 91 additions & 0 deletions server/cmd/api/api/computer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,94 @@ func TestGenerateRelativeSteps_DiagonalsAndSlopes(t *testing.T) {
require.Equal(t, 5, countSteps(steps), "count mismatch")
}
}

// TestParseMousePosition tests the parseMousePosition helper function
func TestParseMousePosition(t *testing.T) {
tests := []struct {
name string
output string
expectX int
expectY int
expectError bool
}{
{
name: "valid output",
output: "X=100\nY=200\nSCREEN=0\nWINDOW=12345\n",
expectX: 100,
expectY: 200,
expectError: false,
},
{
name: "valid output with extra whitespace",
output: " X=512 \n Y=384 \n SCREEN=0 \n WINDOW=67890 \n",
expectX: 512,
expectY: 384,
expectError: false,
},
{
name: "missing Y coordinate",
output: "X=100\nSCREEN=0\nWINDOW=12345\n",
expectError: true,
},
{
name: "missing X coordinate",
output: "Y=200\nSCREEN=0\nWINDOW=12345\n",
expectError: true,
},
{
name: "empty output",
output: "",
expectError: true,
},
{
name: "whitespace only",
output: " \n \t \n",
expectError: true,
},
{
name: "non-numeric X value",
output: "X=abc\nY=200\nSCREEN=0\nWINDOW=12345\n",
expectError: true,
},
{
name: "non-numeric Y value",
output: "X=100\nY=xyz\nSCREEN=0\nWINDOW=12345\n",
expectError: true,
},
{
name: "zero coordinates",
output: "X=0\nY=0\nSCREEN=0\nWINDOW=12345\n",
expectX: 0,
expectY: 0,
expectError: false,
},
{
name: "negative coordinates",
output: "X=-50\nY=-100\nSCREEN=0\nWINDOW=12345\n",
expectX: -50,
expectY: -100,
expectError: false,
},
{
name: "large coordinates",
output: "X=3840\nY=2160\nSCREEN=0\nWINDOW=12345\n",
expectX: 3840,
expectY: 2160,
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
x, y, err := parseMousePosition(tt.output)

if tt.expectError {
require.Error(t, err, "expected parsing to fail")
} else {
require.NoError(t, err, "expected successful parsing")
require.Equal(t, tt.expectX, x, "X coordinate mismatch")
require.Equal(t, tt.expectY, y, "Y coordinate mismatch")
}
})
}
}
Loading
Loading