wraith/internal/rdp/mock_backend.go
Vantz Stockwell 8a096d7f7b
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
Wraith v0.1.0 — Desktop SSH + RDP + SFTP Client
Go + Wails v3 + Vue 3 + SQLite + FreeRDP3 (purego)
183 tests, 76 source files, 9,910 lines of code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:19:29 -04:00

314 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rdp
import (
"fmt"
"math"
"sync"
"time"
)
// MockBackend implements the RDPBackend interface for development and testing.
// Instead of connecting to a real RDP server, it generates animated test frames
// with a gradient pattern, moving shapes, and a timestamp overlay.
type MockBackend struct {
connected bool
config RDPConfig
buffer *PixelBuffer
mu sync.Mutex
startTime time.Time
clipboard string
}
// NewMockBackend creates a new MockBackend instance.
func NewMockBackend() *MockBackend {
return &MockBackend{}
}
// Connect initializes the mock backend with the given configuration and starts
// generating test frames.
func (m *MockBackend) Connect(config RDPConfig) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.connected {
return fmt.Errorf("already connected")
}
width := config.Width
height := config.Height
if width <= 0 {
width = 1920
}
if height <= 0 {
height = 1080
}
m.config = config
m.config.Width = width
m.config.Height = height
m.buffer = NewPixelBuffer(width, height)
m.connected = true
m.startTime = time.Now()
// Generate the initial test frame
m.generateFrame()
return nil
}
// Disconnect shuts down the mock backend.
func (m *MockBackend) Disconnect() error {
m.mu.Lock()
defer m.mu.Unlock()
if !m.connected {
return fmt.Errorf("not connected")
}
m.connected = false
m.buffer = nil
return nil
}
// SendMouseEvent records a mouse event (no-op for mock).
func (m *MockBackend) SendMouseEvent(x, y int, flags uint32) error {
m.mu.Lock()
defer m.mu.Unlock()
if !m.connected {
return fmt.Errorf("not connected")
}
// In the mock, we draw a cursor indicator at the mouse position
if m.buffer != nil && flags&MouseFlagMove != 0 {
m.drawCursor(x, y)
}
return nil
}
// SendKeyEvent records a key event (no-op for mock).
func (m *MockBackend) SendKeyEvent(scancode uint32, pressed bool) error {
m.mu.Lock()
defer m.mu.Unlock()
if !m.connected {
return fmt.Errorf("not connected")
}
return nil
}
// SendClipboard stores clipboard text (mock implementation).
func (m *MockBackend) SendClipboard(data string) error {
m.mu.Lock()
defer m.mu.Unlock()
if !m.connected {
return fmt.Errorf("not connected")
}
m.clipboard = data
return nil
}
// GetFrame returns the current test frame. Each call regenerates the frame
// with an updated animation state so the renderer can verify dynamic updates.
func (m *MockBackend) GetFrame() ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
if !m.connected {
return nil, fmt.Errorf("not connected")
}
m.generateFrame()
return m.buffer.GetFrame(), nil
}
// IsConnected reports whether the mock backend is connected.
func (m *MockBackend) IsConnected() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.connected
}
// generateFrame creates a visually interesting test frame with:
// - A blue-to-purple diagonal gradient background
// - An animated bouncing rectangle
// - A grid overlay for alignment verification
// - Session info text area
func (m *MockBackend) generateFrame() {
w := m.config.Width
h := m.config.Height
elapsed := time.Since(m.startTime).Seconds()
data := make([]byte, w*h*4)
// ── Background: diagonal gradient from dark blue to dark purple ──
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
offset := (y*w + x) * 4
// Normalized coordinates
nx := float64(x) / float64(w)
ny := float64(y) / float64(h)
diag := (nx + ny) / 2.0
r := uint8(20 + diag*40) // 2060
g := uint8(25 + (1.0-diag)*30) // 2555
b := uint8(80 + diag*100) // 80180
data[offset+0] = r
data[offset+1] = g
data[offset+2] = b
data[offset+3] = 255
}
}
// ── Grid overlay: subtle lines every 100 pixels ──
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
if x%100 == 0 || y%100 == 0 {
offset := (y*w + x) * 4
data[offset+0] = min8(data[offset+0]+20, 255)
data[offset+1] = min8(data[offset+1]+20, 255)
data[offset+2] = min8(data[offset+2]+20, 255)
}
}
}
// ── Animated bouncing rectangle ──
rectW, rectH := 200, 120
// Bounce horizontally and vertically using sine/cosine
cx := int(float64(w-rectW) * (0.5 + 0.4*math.Sin(elapsed*0.7)))
cy := int(float64(h-rectH) * (0.5 + 0.4*math.Cos(elapsed*0.5)))
// Color cycles through hues
hue := math.Mod(elapsed*30, 360)
rr, gg, bb := hueToRGB(hue)
for ry := 0; ry < rectH; ry++ {
for rx := 0; rx < rectW; rx++ {
px := cx + rx
py := cy + ry
if px >= 0 && px < w && py >= 0 && py < h {
offset := (py*w + px) * 4
// Border: 3px white outline
if rx < 3 || rx >= rectW-3 || ry < 3 || ry >= rectH-3 {
data[offset+0] = 255
data[offset+1] = 255
data[offset+2] = 255
data[offset+3] = 255
} else {
data[offset+0] = rr
data[offset+1] = gg
data[offset+2] = bb
data[offset+3] = 220
}
}
}
}
// ── Info panel in top-left corner ──
panelW, panelH := 320, 80
for py := 10; py < 10+panelH && py < h; py++ {
for px := 10; px < 10+panelW && px < w; px++ {
offset := (py*w + px) * 4
data[offset+0] = 0
data[offset+1] = 0
data[offset+2] = 0
data[offset+3] = 180
}
}
// ── Secondary animated element: pulsing circle ──
circleX := w / 2
circleY := h / 2
radius := 40.0 + 20.0*math.Sin(elapsed*2.0)
for dy := -70; dy <= 70; dy++ {
for dx := -70; dx <= 70; dx++ {
dist := math.Sqrt(float64(dx*dx + dy*dy))
if dist <= radius && dist >= radius-4 {
px := circleX + dx
py := circleY + dy
if px >= 0 && px < w && py >= 0 && py < h {
offset := (py*w + px) * 4
data[offset+0] = 88
data[offset+1] = 166
data[offset+2] = 255
data[offset+3] = 255
}
}
}
}
// Apply as a full-frame update
m.buffer.Update(0, 0, w, h, data)
}
// drawCursor draws a small crosshair at the given position.
func (m *MockBackend) drawCursor(cx, cy int) {
size := 10
w := m.config.Width
h := m.config.Height
cursorData := make([]byte, (size*2+1)*(size*2+1)*4)
idx := 0
for dy := -size; dy <= size; dy++ {
for dx := -size; dx <= size; dx++ {
if dx == 0 || dy == 0 {
cursorData[idx+0] = 255
cursorData[idx+1] = 255
cursorData[idx+2] = 0
cursorData[idx+3] = 200
}
idx += 4
}
}
startX := cx - size
startY := cy - size
if startX < 0 {
startX = 0
}
if startY < 0 {
startY = 0
}
_ = w
_ = h
m.buffer.Update(startX, startY, size*2+1, size*2+1, cursorData)
}
// hueToRGB converts a hue angle (0-360) to RGB values.
func hueToRGB(hue float64) (uint8, uint8, uint8) {
h := math.Mod(hue, 360) / 60
c := 200.0 // chroma
x := c * (1 - math.Abs(math.Mod(h, 2)-1))
var r, g, b float64
switch {
case h < 1:
r, g, b = c, x, 0
case h < 2:
r, g, b = x, c, 0
case h < 3:
r, g, b = 0, c, x
case h < 4:
r, g, b = 0, x, c
case h < 5:
r, g, b = x, 0, c
default:
r, g, b = c, 0, x
}
return uint8(r + 55), uint8(g + 55), uint8(b + 55)
}
// min8 returns the smaller of two uint8 values.
func min8(a, b uint8) uint8 {
if a < b {
return a
}
return b
}