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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25.x"
go-version: "1.26.x"
- run: go test ./...
- run: go test ./...
env:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ and Lip Gloss.

## Install

`teleop` currently requires Go 1.25 or newer.
`teleop` currently requires Go 1.26 or newer.

```sh
go get github.com/open-ships/teleop
Expand Down
52 changes: 18 additions & 34 deletions action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package action
import (
"context"
"fmt"
"sort"
"slices"
"sync"
"sync/atomic"

Expand Down Expand Up @@ -45,7 +45,7 @@ func (Event) Kind() teleop.EventKind { return EventKind }
// CloneEvent implements teleop.EventCloner.
func (e Event) CloneEvent() teleop.Event {
e.Meta = e.Meta.Clone()
e.Controls = append([]teleop.ControlID(nil), e.Controls...)
e.Controls = slices.Clone(e.Controls)
if e.Value.Vector != nil {
vector := *e.Value.Vector
e.Value.Vector = &vector
Expand Down Expand Up @@ -148,7 +148,7 @@ type Mapper struct {
// New validates and copies bindings. Invalid bindings panic instead of silently
// matching a different control.
func New(bindings ...Binding) *Mapper {
copied := append([]Binding(nil), bindings...)
copied := slices.Clone(bindings)
for index := range copied {
copied[index].Controls = canonicalControls(copied[index].Controls)
if err := validateBinding(copied[index]); err != nil {
Expand Down Expand Up @@ -203,11 +203,7 @@ func validateBinding(binding Binding) error {
// Process implements teleop.Processor.
func (m *Mapper) Process(input teleop.Event) []teleop.Event {
mapped := m.Map(input)
result := make([]teleop.Event, len(mapped))
for index := range mapped {
result[index] = mapped[index]
}
return result
return asEvents(mapped)
}

// ProcessContext implements teleop.ContextProcessor.
Expand All @@ -217,11 +213,7 @@ func (m *Mapper) ProcessContext(
input teleop.Event,
) ([]teleop.Event, error) {
mapped := m.mapInput(input, processing)
result := make([]teleop.Event, len(mapped))
for index := range mapped {
result[index] = mapped[index]
}
return result, nil
return asEvents(mapped), nil
}

// Map returns strongly typed application actions using an instance-unique
Expand Down Expand Up @@ -285,7 +277,7 @@ func (m *Mapper) mapInput(
Action: binding.Action,
Phase: values.phase,
Control: values.control,
Controls: append([]teleop.ControlID(nil), values.controls...),
Controls: slices.Clone(values.controls),
Value: values.value,
})
}
Expand Down Expand Up @@ -387,28 +379,20 @@ func triggerControl(trigger teleop.TriggerID) (teleop.ControlID, bool) {
}
}

func canonicalControls(values []teleop.ControlID) []teleop.ControlID {
result := append([]teleop.ControlID(nil), values...)
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
write := 0
for _, value := range result {
if value == "" || write > 0 && result[write-1] == value {
continue
}
result[write] = value
write++
// asEvents widens mapped actions to the open teleop.Event interface.
func asEvents[T teleop.Event](values []T) []teleop.Event {
result := make([]teleop.Event, len(values))
for index, value := range values {
result[index] = value
}
return result[:write]
return result
}

func canonicalControls(values []teleop.ControlID) []teleop.ControlID {
sorted := slices.Compact(slices.Sorted(slices.Values(values)))
return slices.DeleteFunc(sorted, func(value teleop.ControlID) bool { return value == "" })
}

func equalControls(left, right []teleop.ControlID) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
return slices.Equal(left, right)
}
2 changes: 1 addition & 1 deletion audit/integrity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,7 @@ func TestManifestBindsSession(t *testing.T) {
func splitLines(t *testing.T, raw []byte) [][]byte {
t.Helper()
var lines [][]byte
for _, line := range strings.Split(strings.TrimRight(string(raw), "\n"), "\n") {
for line := range strings.SplitSeq(strings.TrimRight(string(raw), "\n"), "\n") {
lines = append(lines, []byte(line))
}
return lines
Expand Down
17 changes: 3 additions & 14 deletions audit/provenance.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package audit

import (
"maps"
"os"
"runtime"
"runtime/debug"
Expand Down Expand Up @@ -88,19 +89,7 @@ func CaptureProvenance() Provenance {
// Clone returns a deep copy so a recorder cannot observe later mutation of a
// caller's maps.
func (p Provenance) Clone() Provenance {
if p.Config != nil {
config := make(map[string]any, len(p.Config))
for key, value := range p.Config {
config[key] = value
}
p.Config = config
}
if p.Platform != nil {
platform := make(map[string]string, len(p.Platform))
for key, value := range p.Platform {
platform[key] = value
}
p.Platform = platform
}
p.Config = maps.Clone(p.Config)
p.Platform = maps.Clone(p.Platform)
return p
}
5 changes: 1 addition & 4 deletions clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,7 @@ func (m *clockMonitor) observe(now time.Time) clockSample {
wallDelta := now.Round(0).Sub(m.lastWall.Round(0))
monoDelta := monotonic - m.lastMono
divergence := wallDelta - monoDelta
magnitude := divergence
if magnitude < 0 {
magnitude = -magnitude
}
magnitude := divergence.Abs()
if magnitude >= m.threshold {
m.steps++
sample.Step = divergence
Expand Down
5 changes: 1 addition & 4 deletions cmd/teleop-monitor/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -922,10 +922,7 @@ func keyHint(key, action string) string {
}

func padBetween(left, right string, width int) string {
spaces := width - lipgloss.Width(left) - lipgloss.Width(right)
if spaces < 1 {
spaces = 1
}
spaces := max(width-lipgloss.Width(left)-lipgloss.Width(right), 1)
return left + strings.Repeat(" ", spaces) + right
}

Expand Down
6 changes: 3 additions & 3 deletions cmd/teleop-monitor/tui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ func TestMonitorModelTogglesRumble(t *testing.T) {
if repeated != nil {
t.Fatal("repeated rumble key started a second request")
}
for _, line := range strings.Split(model.renderHaptics(30), "\n") {
for line := range strings.SplitSeq(model.renderHaptics(30), "\n") {
if lipgloss.Width(line) > 30 {
t.Fatalf(
"compact haptic line is %d cells wide, want at most 30",
Expand Down Expand Up @@ -523,7 +523,7 @@ func TestMonitorModelAlternatesRumbleAtConfiguredInterval(t *testing.T) {
t.Fatalf("alternating haptic controls do not contain %q", expected)
}
}
for _, line := range strings.Split(model.renderHaptics(30), "\n") {
for line := range strings.SplitSeq(model.renderHaptics(30), "\n") {
if lipgloss.Width(line) > 30 {
t.Fatalf(
"compact alternating haptic line is %d cells wide, want at most 30",
Expand Down Expand Up @@ -756,7 +756,7 @@ func TestMonitorModelReportsUnavailableAndFailedRumble(t *testing.T) {
t.Fatalf("failed rumble view does not contain %q", expected)
}
}
for _, line := range strings.Split(model.renderHaptics(30), "\n") {
for line := range strings.SplitSeq(model.renderHaptics(30), "\n") {
if lipgloss.Width(line) > 30 {
t.Fatalf(
"compact haptic error line is %d cells wide, want at most 30",
Expand Down
3 changes: 2 additions & 1 deletion command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"time"
)

Expand Down Expand Up @@ -69,7 +70,7 @@ func (c *Controller) RecordCommand(ctx context.Context, command Command) error {
payload: payload,
issuedAt: c.clock.Now(),
}
request.command.Causes = append([]EventID(nil), command.Causes...)
request.command.Causes = slices.Clone(command.Causes)
request.command.Payload = nil

// A caller may use a deferred controller solely for audit-backed command
Expand Down
4 changes: 3 additions & 1 deletion control.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package teleop

import "slices"

// ControllerType identifies a family of game controllers. It is string-backed
// so third-party providers can add controller types without changing teleop.
type ControllerType string
Expand Down Expand Up @@ -89,7 +91,7 @@ var standardButtons = []ControlID{

// StandardButtonIDs returns the standard physical buttons in a stable order.
func StandardButtonIDs() []ControlID {
return append([]ControlID(nil), standardButtons...)
return slices.Clone(standardButtons)
}

// StickID selects one of the two standard analog sticks.
Expand Down
14 changes: 6 additions & 8 deletions controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"math"
"slices"
"sync"
"time"
)
Expand Down Expand Up @@ -522,10 +523,7 @@ func (c *Controller) onTick(now time.Time) error {
if meta.ReceivedAt.IsZero() {
meta.ReceivedAt = c.startedAt
}
age := now.Sub(meta.ReceivedAt)
if age < 0 {
age = 0
}
age := max(now.Sub(meta.ReceivedAt), 0)
stale := c.options.staleAfter > 0 && age >= c.options.staleAfter
if stale && !meta.Stale && c.options.neutralizeOnStale {
if err := c.neutralize(now, "input stale"); err != nil {
Expand Down Expand Up @@ -755,7 +753,7 @@ func (c *Controller) nextHeaderAt(
Monotonic: c.clocks.since(publishedAt),
ReceivedMonotonic: c.clocks.since(receivedAt),
DeviceTimestamp: deviceTimestamp,
Causes: append([]EventID(nil), causes...),
Causes: slices.Clone(causes),
Synthetic: synthetic,
}
}
Expand Down Expand Up @@ -826,7 +824,7 @@ func (c *Controller) publishTerminal(event Event) {
if c.processorDisabled[index] {
continue
}
stageInputs := append([]Event(nil), visible...)
stageInputs := slices.Clone(visible)
var derived []Event
failed := false
for _, input := range stageInputs {
Expand Down Expand Up @@ -865,13 +863,13 @@ func (c *Controller) dispatchTerminal(event Event) (completed bool) {
}

func (c *Controller) processStages(inputs []Event, start int) error {
visible := append([]Event(nil), inputs...)
visible := slices.Clone(inputs)
for index := start; index < len(c.options.processors); index++ {
if c.processorDisabled[index] {
continue
}
processor := c.options.processors[index]
stageInputs := append([]Event(nil), visible...)
stageInputs := slices.Clone(visible)
derived := make([]Event, 0, len(stageInputs))
for _, input := range stageInputs {
output, err := c.callProcessor(processor, input)
Expand Down
10 changes: 5 additions & 5 deletions controller_remediation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,29 +240,29 @@ func TestSubscriptionCloseBroadcastsToAllBlockedReaders(t *testing.T) {
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
for index := 0; index < 2; index++ {
for range 2 {
if _, err := subscription.Next(ctx); err != nil {
t.Fatal(err)
}
}

started := make(chan struct{}, 4)
finished := make(chan error, 4)
for index := 0; index < 4; index++ {
for range 4 {
go func() {
started <- struct{}{}
_, err := subscription.Next(context.Background())
finished <- err
}()
}
for index := 0; index < 4; index++ {
for range 4 {
<-started
}
time.Sleep(10 * time.Millisecond)
if err := subscription.Close(); err != nil {
t.Fatal(err)
}
for index := 0; index < 4; index++ {
for index := range 4 {
select {
case err := <-finished:
if !errors.Is(err, teleop.ErrClosed) {
Expand Down Expand Up @@ -642,7 +642,7 @@ func TestSlowSinkDoesNotDelayCanonicalSnapshot(t *testing.T) {
waitForSnapshot(t, controller, func(_ teleop.State, meta teleop.StateMeta) bool {
return meta.Connected
})
for index := 0; index < 8; index++ {
for index := range 8 {
if err := source.Push(context.Background(), teleop.State{
LeftTrigger: float32(index) / 7,
}); err != nil {
Expand Down
20 changes: 7 additions & 13 deletions device.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package teleop

import (
"context"
"maps"
"slices"
"time"
)

Expand Down Expand Up @@ -69,18 +71,15 @@ func (c Capabilities) Clone() Capabilities {
if !c.AuditGrade.Valid() {
c.AuditGrade = AuditUnavailable
}
c.Controls = append([]ControlDescriptor(nil), c.Controls...)
c.Controls = slices.Clone(c.Controls)
return c
}

// Supports reports whether id appears in the discovered control list.
func (c Capabilities) Supports(id ControlID) bool {
for _, control := range c.Controls {
if control.ID == id {
return true
}
}
return false
return slices.ContainsFunc(c.Controls, func(control ControlDescriptor) bool {
return control.ID == id
})
}

// Descriptor contains the stable metadata and capabilities of one discovered
Expand All @@ -100,12 +99,7 @@ type Descriptor struct {
// Clone returns an isolated copy of the device descriptor.
func (d Descriptor) Clone() Descriptor {
d.Capability = d.Capability.Clone()
if d.Properties != nil {
d.Properties = make(map[string]string, len(d.Properties))
for key, value := range d.Properties {
d.Properties[key] = value
}
}
d.Properties = maps.Clone(d.Properties)
return d
}

Expand Down
Loading
Loading