diff --git a/TODO.md b/TODO.md --- a/TODO.md +++ b/TODO.md @@ -27,16 +27,7 @@ - [x] Focus encoding (`focus.h`) - [x] Kitty graphics (`kitty_graphics.h`) - [x] Allocator (`allocator.h` — `ghostty_alloc`, `ghostty_free`) - [x] Terminal selection helpers (`selection.h`) -- [ ] Selection gesture APIs (`selection.h`) - - `ghostty_selection_gesture_new()` - - `ghostty_selection_gesture_free()` - - `ghostty_selection_gesture_reset()` - - `ghostty_selection_gesture_event_new()` - - `ghostty_selection_gesture_event_free()` - - `ghostty_selection_gesture_event_set()` - - `ghostty_selection_gesture_event()` - - `ghostty_selection_gesture_get()` - - `ghostty_selection_gesture_get_multi()` +- [x] Selection gesture APIs (`selection.h`) - [x] Render-state row selection (`GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION`) - [x] Build info (`build_info.h`) diff --git a/point.go b/point.go --- a/point.go +++ b/point.go @@ -7,6 +7,28 @@ import "C" import "unsafe" +// PointCoordinate is an untagged coordinate in the terminal grid. +// +// It is used by APIs that already define which coordinate system applies, +// such as selection gesture autoscroll viewport positions. Use [Point] when +// an API needs the coordinate system tag alongside X and Y. +// C: GhosttyPointCoordinate +type PointCoordinate struct { + // X is the column (0-indexed). + X uint16 + + // Y is the row (0-indexed). May exceed page size for screen/history tags. + Y uint32 +} + +// toC converts a Go PointCoordinate to a C GhosttyPointCoordinate. +func (p PointCoordinate) toC() C.GhosttyPointCoordinate { + return C.GhosttyPointCoordinate{ + x: C.uint16_t(p.X), + y: C.uint32_t(p.Y), + } +} + // PointTag determines which coordinate system a point uses. // C: GhosttyPointTag type PointTag int diff --git a/selection.go b/selection.go --- a/selection.go +++ b/selection.go @@ -108,6 +108,211 @@ // SelectionAdjustEndOfLine moves to the right edge of the current line. SelectionAdjustEndOfLine SelectionAdjust = C.GHOSTTY_SELECTION_ADJUST_END_OF_LINE ) +// SelectionGesture is an opaque handle to mutable state for interpreting +// pointer gestures as terminal selection snapshots. It is not safe for +// concurrent use and should be serialized with the terminal it is used with. +// C: GhosttySelectionGesture +type SelectionGesture struct { + ptr C.GhosttySelectionGesture +} + +// SelectionGestureEvent is an opaque, reusable input event for a selection +// gesture. Its event type is fixed at creation time, while its option values +// may be changed between calls. +// C: GhosttySelectionGestureEvent +type SelectionGestureEvent struct { + ptr C.GhosttySelectionGestureEvent +} + +// SelectionGestureBehavior is the selection behavior chosen for a gesture's +// click sequence. +// C: GhosttySelectionGestureBehavior +type SelectionGestureBehavior int + +const ( + // SelectionGestureBehaviorCell performs cell-granular drag selection. + SelectionGestureBehaviorCell SelectionGestureBehavior = C.GHOSTTY_SELECTION_GESTURE_BEHAVIOR_CELL + + // SelectionGestureBehaviorWord performs word selection on press and + // word-granular drag selection. + SelectionGestureBehaviorWord SelectionGestureBehavior = C.GHOSTTY_SELECTION_GESTURE_BEHAVIOR_WORD + + // SelectionGestureBehaviorLine performs line selection on press and + // line-granular drag selection. + SelectionGestureBehaviorLine SelectionGestureBehavior = C.GHOSTTY_SELECTION_GESTURE_BEHAVIOR_LINE + + // SelectionGestureBehaviorOutput performs semantic command-output selection + // on press and drag. + SelectionGestureBehaviorOutput SelectionGestureBehavior = C.GHOSTTY_SELECTION_GESTURE_BEHAVIOR_OUTPUT +) + +// SelectionGestureBehaviors configures the behavior selected for single-, +// double-, and triple-click gesture sequences. +// C: GhosttySelectionGestureBehaviors +type SelectionGestureBehaviors struct { + // SingleClick is the behavior for single-click selection gestures. + SingleClick SelectionGestureBehavior + + // DoubleClick is the behavior for double-click selection gestures. + DoubleClick SelectionGestureBehavior + + // TripleClick is the behavior for triple-click selection gestures. + TripleClick SelectionGestureBehavior +} + +// toC converts Go selection gesture behaviors to C. +func (b SelectionGestureBehaviors) toC() C.GhosttySelectionGestureBehaviors { + return C.GhosttySelectionGestureBehaviors{ + single_click: C.GhosttySelectionGestureBehavior(b.SingleClick), + double_click: C.GhosttySelectionGestureBehavior(b.DoubleClick), + triple_click: C.GhosttySelectionGestureBehavior(b.TripleClick), + } +} + +// SelectionGestureGeometry is the display geometry used to interpret drag and +// autoscroll selection gesture events. +// C: GhosttySelectionGestureGeometry +type SelectionGestureGeometry struct { + // Columns is the number of columns in the rendered terminal grid. It must + // be non-zero. + Columns uint32 + + // CellWidth is the width of one terminal cell in surface pixels. It must be + // non-zero. + CellWidth uint32 + + // PaddingLeft is the left padding before the terminal grid begins in surface + // pixels. + PaddingLeft uint32 + + // ScreenHeight is the height of the rendered terminal surface in surface + // pixels. It must be non-zero. + ScreenHeight uint32 +} + +// toC converts Go selection gesture geometry to C. +func (g SelectionGestureGeometry) toC() C.GhosttySelectionGestureGeometry { + return C.GhosttySelectionGestureGeometry{ + columns: C.uint32_t(g.Columns), + cell_width: C.uint32_t(g.CellWidth), + padding_left: C.uint32_t(g.PaddingLeft), + screen_height: C.uint32_t(g.ScreenHeight), + } +} + +// SelectionGestureAutoscroll is the current autoscroll direction for an active +// selection drag gesture. +// C: GhosttySelectionGestureAutoscroll +type SelectionGestureAutoscroll int + +const ( + // SelectionGestureAutoscrollNone means no selection autoscroll is requested. + SelectionGestureAutoscrollNone SelectionGestureAutoscroll = C.GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_NONE + + // SelectionGestureAutoscrollUp means selection dragging should autoscroll + // the viewport upward. + SelectionGestureAutoscrollUp SelectionGestureAutoscroll = C.GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_UP + + // SelectionGestureAutoscrollDown means selection dragging should autoscroll + // the viewport downward. + SelectionGestureAutoscrollDown SelectionGestureAutoscroll = C.GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_DOWN +) + +// SelectionGestureData identifies data fields readable from a selection +// gesture. +// C: GhosttySelectionGestureData +type SelectionGestureData int + +const ( + // SelectionGestureDataClickCount reads the current click count as *uint8. + SelectionGestureDataClickCount SelectionGestureData = C.GHOSTTY_SELECTION_GESTURE_DATA_CLICK_COUNT + + // SelectionGestureDataDragged reads whether the current or last left-click + // gesture dragged as *bool. + SelectionGestureDataDragged SelectionGestureData = C.GHOSTTY_SELECTION_GESTURE_DATA_DRAGGED + + // SelectionGestureDataAutoscroll reads the current autoscroll request as a + // *GhosttySelectionGestureAutoscroll-compatible value. + SelectionGestureDataAutoscroll SelectionGestureData = C.GHOSTTY_SELECTION_GESTURE_DATA_AUTOSCROLL + + // SelectionGestureDataBehavior reads the current gesture behavior as a + // *GhosttySelectionGestureBehavior-compatible value. + SelectionGestureDataBehavior SelectionGestureData = C.GHOSTTY_SELECTION_GESTURE_DATA_BEHAVIOR + + // SelectionGestureDataAnchor reads the current left-click anchor as a + // *GhosttyGridRef-compatible value. It returns ResultNoValue when there is + // no valid active anchor. + SelectionGestureDataAnchor SelectionGestureData = C.GHOSTTY_SELECTION_GESTURE_DATA_ANCHOR +) + +// SelectionGestureEventType is the fixed type of a selection gesture event. +// C: GhosttySelectionGestureEventType +type SelectionGestureEventType int + +const ( + // SelectionGestureEventTypePress is a press event. + SelectionGestureEventTypePress SelectionGestureEventType = C.GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS + + // SelectionGestureEventTypeRelease is a release event. + SelectionGestureEventTypeRelease SelectionGestureEventType = C.GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_RELEASE + + // SelectionGestureEventTypeDrag is a drag event. + SelectionGestureEventTypeDrag SelectionGestureEventType = C.GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DRAG + + // SelectionGestureEventTypeAutoscrollTick is an autoscroll tick event. + SelectionGestureEventTypeAutoscrollTick SelectionGestureEventType = C.GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_AUTOSCROLL_TICK + + // SelectionGestureEventTypeDeepPress is a deep press event. + SelectionGestureEventTypeDeepPress SelectionGestureEventType = C.GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DEEP_PRESS +) + +// SelectionGestureEventOption identifies an option stored on a reusable +// selection gesture event. +// C: GhosttySelectionGestureEventOption +type SelectionGestureEventOption int + +const ( + // SelectionGestureEventOptionRef stores a *GridRef pointer under the + // pointer. It is required for press and drag events and optional for release. + SelectionGestureEventOptionRef SelectionGestureEventOption = C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF + + // SelectionGestureEventOptionPosition stores a *SurfacePosition pointer + // position. + SelectionGestureEventOptionPosition SelectionGestureEventOption = C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_POSITION + + // SelectionGestureEventOptionRepeatDistance stores a *float64 maximum + // repeat-click distance in pixels. + SelectionGestureEventOptionRepeatDistance SelectionGestureEventOption = C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_DISTANCE + + // SelectionGestureEventOptionTimeNS stores a *uint64 monotonic event time in + // nanoseconds. + SelectionGestureEventOptionTimeNS SelectionGestureEventOption = C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_TIME_NS + + // SelectionGestureEventOptionRepeatIntervalNS stores a *uint64 maximum + // interval between repeat clicks in nanoseconds. + SelectionGestureEventOptionRepeatIntervalNS SelectionGestureEventOption = C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_INTERVAL_NS + + // SelectionGestureEventOptionWordBoundaryCodepoints stores borrowed word + // boundary codepoints that libghostty copies into the event. + SelectionGestureEventOptionWordBoundaryCodepoints SelectionGestureEventOption = C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_WORD_BOUNDARY_CODEPOINTS + + // SelectionGestureEventOptionBehaviors stores a *SelectionGestureBehaviors + // behavior table. + SelectionGestureEventOptionBehaviors SelectionGestureEventOption = C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_BEHAVIORS + + // SelectionGestureEventOptionRectangle stores a *bool indicating whether a + // drag or autoscroll tick should produce a rectangular selection. + SelectionGestureEventOptionRectangle SelectionGestureEventOption = C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_RECTANGLE + + // SelectionGestureEventOptionGeometry stores a *SelectionGestureGeometry + // drag display geometry. + SelectionGestureEventOptionGeometry SelectionGestureEventOption = C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_GEOMETRY + + // SelectionGestureEventOptionViewport stores a *PointCoordinate viewport + // coordinate for an autoscroll tick. + SelectionGestureEventOptionViewport SelectionGestureEventOption = C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_VIEWPORT +) + // SelectWordOptions configures [Terminal.SelectWord]. // C: GhosttyTerminalSelectWordOptions type SelectWordOptions struct { @@ -216,6 +421,341 @@ if errors.As(err, &ge) && ge.Result == ResultNoValue { return nil, nil } return nil, err +} + +// NewSelectionGesture creates a selection gesture state machine. The returned +// gesture must be freed with [SelectionGesture.Close] when no longer needed. +func NewSelectionGesture() (*SelectionGesture, error) { + var ptr C.GhosttySelectionGesture + if err := resultError(C.ghostty_selection_gesture_new(nil, &ptr)); err != nil { + return nil, err + } + return &SelectionGesture{ptr: ptr}, nil +} + +// Close frees the selection gesture. If the terminal most recently used with +// the gesture is still alive, pass it so libghostty can release tracked +// terminal references. Pass nil only after the terminal has already been freed. +func (g *SelectionGesture) Close(t *Terminal) { + var terminal C.GhosttyTerminal + if t != nil { + terminal = t.ptr + } + C.ghostty_selection_gesture_free(g.ptr, terminal) +} + +// Reset cancels active selection gesture state and releases tracked terminal +// references without freeing the gesture. +func (g *SelectionGesture) Reset(t *Terminal) { + var terminal C.GhosttyTerminal + if t != nil { + terminal = t.ptr + } + C.ghostty_selection_gesture_reset(g.ptr, terminal) +} + +// Event applies a selection gesture event to the terminal and returns the +// resulting selection snapshot. It returns nil when the event updates gesture +// state but does not currently produce a selection. +func (g *SelectionGesture) Event(t *Terminal, event *SelectionGestureEvent) (*Selection, error) { + cs := initCSelection() + if err := resultError(C.ghostty_selection_gesture_event(g.ptr, t.ptr, event.ptr, &cs)); err != nil { + return noValueNilSelection(err) + } + + sel := selectionFromC(cs) + return &sel, nil +} + +// Get reads a raw data field from the selection gesture into value. The value +// pointer type must match the documented type for the data key; prefer the +// typed helpers such as [SelectionGesture.ClickCount] and +// [SelectionGesture.Anchor] when possible. +// C: ghostty_selection_gesture_get +func (g *SelectionGesture) Get(t *Terminal, data SelectionGestureData, value unsafe.Pointer) error { + return resultError(C.ghostty_selection_gesture_get( + g.ptr, + t.ptr, + C.GhosttySelectionGestureData(data), + value, + )) +} + +// GetMulti reads multiple raw data fields from the selection gesture in one C +// call. Each element in values must point to storage with the type documented +// by the corresponding key. On partial failure, written is the failing index. +// C: ghostty_selection_gesture_get_multi +func (g *SelectionGesture) GetMulti(t *Terminal, keys []SelectionGestureData, values []unsafe.Pointer) (written int, err error) { + if len(keys) != len(values) { + return 0, errors.New("libghostty: keys and values must have the same length") + } + if len(keys) == 0 { + return 0, nil + } + + cKeys := make([]C.GhosttySelectionGestureData, len(keys)) + for i, key := range keys { + cKeys[i] = C.GhosttySelectionGestureData(key) + } + cVals, cValsSize := cValuesArray(values) + defer Free(unsafe.Pointer(cVals), cValsSize) + var cWritten C.size_t + err = resultError(C.ghostty_selection_gesture_get_multi( + g.ptr, + t.ptr, + C.size_t(len(keys)), + (*C.GhosttySelectionGestureData)(unsafe.Pointer(&cKeys[0])), + cVals, + &cWritten, + )) + return int(cWritten), err +} + +// ClickCount returns the current gesture click count. Zero means inactive. +func (g *SelectionGesture) ClickCount(t *Terminal) (uint8, error) { + var v C.uint8_t + if err := g.Get(t, SelectionGestureDataClickCount, unsafe.Pointer(&v)); err != nil { + return 0, err + } + return uint8(v), nil +} + +// Dragged reports whether the current or last left-click gesture dragged. +func (g *SelectionGesture) Dragged(t *Terminal) (bool, error) { + var v C.bool + if err := g.Get(t, SelectionGestureDataDragged, unsafe.Pointer(&v)); err != nil { + return false, err + } + return bool(v), nil +} + +// Autoscroll returns the current autoscroll request for the gesture. +func (g *SelectionGesture) Autoscroll(t *Terminal) (SelectionGestureAutoscroll, error) { + var v C.GhosttySelectionGestureAutoscroll + if err := g.Get(t, SelectionGestureDataAutoscroll, unsafe.Pointer(&v)); err != nil { + return 0, err + } + return SelectionGestureAutoscroll(v), nil +} + +// Behavior returns the current gesture behavior. +func (g *SelectionGesture) Behavior(t *Terminal) (SelectionGestureBehavior, error) { + var v C.GhosttySelectionGestureBehavior + if err := g.Get(t, SelectionGestureDataBehavior, unsafe.Pointer(&v)); err != nil { + return 0, err + } + return SelectionGestureBehavior(v), nil +} + +// Anchor returns the current left-click anchor as an untracked grid reference +// snapshot. It returns nil when there is no valid active anchor. +func (g *SelectionGesture) Anchor(t *Terminal) (*GridRef, error) { + ref := initCGridRef() + if err := g.Get(t, SelectionGestureDataAnchor, unsafe.Pointer(&ref)); err != nil { + var ge *Error + if errors.As(err, &ge) && ge.Result == ResultNoValue { + return nil, nil + } + return nil, err + } + return &GridRef{ref: ref}, nil +} + +// NewSelectionGestureEvent creates a reusable gesture event of a fixed type. +// The returned event must be freed with [SelectionGestureEvent.Close]. +func NewSelectionGestureEvent(eventType SelectionGestureEventType) (*SelectionGestureEvent, error) { + var ptr C.GhosttySelectionGestureEvent + if err := resultError(C.ghostty_selection_gesture_event_new( + nil, + &ptr, + C.GhosttySelectionGestureEventType(eventType), + )); err != nil { + return nil, err + } + return &SelectionGestureEvent{ptr: ptr}, nil +} + +// Close frees the selection gesture event. After this call, the event must not +// be used. +func (e *SelectionGestureEvent) Close() { + C.ghostty_selection_gesture_event_free(e.ptr) +} + +// ClearOption clears a selection gesture event option. +func (e *SelectionGestureEvent) ClearOption(option SelectionGestureEventOption) error { + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GhosttySelectionGestureEventOption(option), + nil, + )) +} + +// SetRef sets the grid reference under the pointer for a press, drag, or +// release event. +func (e *SelectionGestureEvent) SetRef(ref *GridRef) error { + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF, + unsafe.Pointer(&ref.ref), + )) +} + +// ClearRef clears the optional grid reference under the pointer. +func (e *SelectionGestureEvent) ClearRef() error { + return e.ClearOption(SelectionGestureEventOptionRef) +} + +// SetPosition sets the surface-space pointer position for a press, drag, or +// autoscroll tick event. +func (e *SelectionGestureEvent) SetPosition(position SurfacePosition) error { + cp := position.toC() + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_POSITION, + unsafe.Pointer(&cp), + )) +} + +// ClearPosition clears the surface-space pointer position. +func (e *SelectionGestureEvent) ClearPosition() error { + return e.ClearOption(SelectionGestureEventOptionPosition) +} + +// SetRepeatDistance sets the maximum repeat-click distance in pixels. +func (e *SelectionGestureEvent) SetRepeatDistance(distance float64) error { + v := C.double(distance) + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_DISTANCE, + unsafe.Pointer(&v), + )) +} + +// ClearRepeatDistance clears the maximum repeat-click distance. +func (e *SelectionGestureEvent) ClearRepeatDistance() error { + return e.ClearOption(SelectionGestureEventOptionRepeatDistance) +} + +// SetTimeNS sets the optional monotonic event time in nanoseconds. +func (e *SelectionGestureEvent) SetTimeNS(ns uint64) error { + v := C.uint64_t(ns) + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_TIME_NS, + unsafe.Pointer(&v), + )) +} + +// ClearTimeNS clears the optional monotonic event time. +func (e *SelectionGestureEvent) ClearTimeNS() error { + return e.ClearOption(SelectionGestureEventOptionTimeNS) +} + +// SetRepeatIntervalNS sets the maximum interval between repeat clicks in +// nanoseconds. +func (e *SelectionGestureEvent) SetRepeatIntervalNS(ns uint64) error { + v := C.uint64_t(ns) + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REPEAT_INTERVAL_NS, + unsafe.Pointer(&v), + )) +} + +// ClearRepeatIntervalNS clears the maximum interval between repeat clicks. +func (e *SelectionGestureEvent) ClearRepeatIntervalNS() error { + return e.ClearOption(SelectionGestureEventOptionRepeatIntervalNS) +} + +// SetWordBoundaryCodepoints sets custom word-boundary codepoints for events +// that derive word selections. Libghostty copies the values into the event. +func (e *SelectionGestureEvent) SetWordBoundaryCodepoints(codepoints []uint32) error { + ptr, size := cUint32Slice(codepoints) + if len(codepoints) > 0 && ptr == nil { + return &Error{Result: ResultOutOfMemory} + } + if size > 0 { + defer Free(unsafe.Pointer(ptr), size) + } + + cp := C.GhosttyCodepoints{ + ptr: (*C.uint32_t)(ptr), + len: C.size_t(len(codepoints)), + } + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_WORD_BOUNDARY_CODEPOINTS, + unsafe.Pointer(&cp), + )) +} + +// ClearWordBoundaryCodepoints clears custom word-boundary codepoints so the +// event uses Ghostty's defaults again. +func (e *SelectionGestureEvent) ClearWordBoundaryCodepoints() error { + return e.ClearOption(SelectionGestureEventOptionWordBoundaryCodepoints) +} + +// SetBehaviors sets the behavior table for a press event. +func (e *SelectionGestureEvent) SetBehaviors(behaviors SelectionGestureBehaviors) error { + cb := behaviors.toC() + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_BEHAVIORS, + unsafe.Pointer(&cb), + )) +} + +// ClearBehaviors clears custom press behaviors so Ghostty's default behavior +// table is used again. +func (e *SelectionGestureEvent) ClearBehaviors() error { + return e.ClearOption(SelectionGestureEventOptionBehaviors) +} + +// SetRectangle sets whether a drag or autoscroll tick should produce a +// rectangular selection. +func (e *SelectionGestureEvent) SetRectangle(rectangle bool) error { + v := C.bool(rectangle) + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_RECTANGLE, + unsafe.Pointer(&v), + )) +} + +// ClearRectangle clears the rectangular-selection setting. +func (e *SelectionGestureEvent) ClearRectangle() error { + return e.ClearOption(SelectionGestureEventOptionRectangle) +} + +// SetGeometry sets the display geometry required by drag and autoscroll tick +// events. +func (e *SelectionGestureEvent) SetGeometry(geometry SelectionGestureGeometry) error { + cg := geometry.toC() + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_GEOMETRY, + unsafe.Pointer(&cg), + )) +} + +// ClearGeometry clears the display geometry. +func (e *SelectionGestureEvent) ClearGeometry() error { + return e.ClearOption(SelectionGestureEventOptionGeometry) +} + +// SetViewport sets the viewport coordinate required by autoscroll tick events. +func (e *SelectionGestureEvent) SetViewport(viewport PointCoordinate) error { + cp := viewport.toC() + return resultError(C.ghostty_selection_gesture_event_set( + e.ptr, + C.GHOSTTY_SELECTION_GESTURE_EVENT_OPT_VIEWPORT, + unsafe.Pointer(&cp), + )) +} + +// ClearViewport clears the autoscroll tick viewport coordinate. +func (e *SelectionGestureEvent) ClearViewport() error { + return e.ClearOption(SelectionGestureEventOptionViewport) } // cUint32Slice copies codepoints into C-owned memory for the duration of a diff --git a/selection_test.go b/selection_test.go --- a/selection_test.go +++ b/selection_test.go @@ -3,6 +3,7 @@ import ( "errors" "testing" + "unsafe" ) func TestTerminalSelectionAPIs(t *testing.T) { @@ -199,3 +200,198 @@ if formatted != "hello" { t.Fatalf("expected active selection format %q, got %q", "hello", formatted) } } + +func TestSelectionGestureAPIs(t *testing.T) { + term, err := NewTerminal(WithSize(20, 4), WithMaxScrollback(100)) + if err != nil { + t.Fatal(err) + } + defer term.Close() + + term.VTWrite([]byte("hello world\r\nsecond line")) + + gesture, err := NewSelectionGesture() + if err != nil { + t.Fatal(err) + } + defer gesture.Close(term) + + press, err := NewSelectionGestureEvent(SelectionGestureEventTypePress) + if err != nil { + t.Fatal(err) + } + defer press.Close() + + drag, err := NewSelectionGestureEvent(SelectionGestureEventTypeDrag) + if err != nil { + t.Fatal(err) + } + defer drag.Close() + + release, err := NewSelectionGestureEvent(SelectionGestureEventTypeRelease) + if err != nil { + t.Fatal(err) + } + defer release.Close() + + deepPress, err := NewSelectionGestureEvent(SelectionGestureEventTypeDeepPress) + if err != nil { + t.Fatal(err) + } + defer deepPress.Close() + + geometry := SelectionGestureGeometry{ + Columns: 20, + CellWidth: 10, + ScreenHeight: 40, + } + + pressRef, err := term.GridRef(Point{Tag: PointTagActive, X: 0, Y: 0}) + if err != nil { + t.Fatal(err) + } + if err := press.SetRef(pressRef); err != nil { + t.Fatal(err) + } + if err := press.SetPosition(SurfacePosition{X: 2, Y: 8}); err != nil { + t.Fatal(err) + } + if err := press.SetTimeNS(1); err != nil { + t.Fatal(err) + } + if err := press.SetRepeatDistance(4); err != nil { + t.Fatal(err) + } + if err := press.SetRepeatIntervalNS(500); err != nil { + t.Fatal(err) + } + + selection, err := gesture.Event(term, press) + if err != nil { + t.Fatal(err) + } + if selection != nil { + t.Fatal("expected press not to produce a selection") + } + + anchor, err := gesture.Anchor(term) + if err != nil { + t.Fatal(err) + } + if anchor == nil { + t.Fatal("expected press to record an anchor") + } + + dragRef, err := term.GridRef(Point{Tag: PointTagActive, X: 4, Y: 0}) + if err != nil { + t.Fatal(err) + } + if err := drag.SetRef(dragRef); err != nil { + t.Fatal(err) + } + if err := drag.SetPosition(SurfacePosition{X: 46, Y: 8}); err != nil { + t.Fatal(err) + } + if err := drag.SetGeometry(geometry); err != nil { + t.Fatal(err) + } + if err := drag.SetRectangle(false); err != nil { + t.Fatal(err) + } + + selection, err = gesture.Event(term, drag) + if err != nil { + t.Fatal(err) + } + if selection == nil { + t.Fatal("expected drag selection") + } + formatted, err := term.SelectionFormatString(WithSelection(selection)) + if err != nil { + t.Fatal(err) + } + if formatted != "hello" { + t.Fatalf("expected drag selection %q, got %q", "hello", formatted) + } + + dragged, err := gesture.Dragged(term) + if err != nil { + t.Fatal(err) + } + if !dragged { + t.Fatal("expected dragged state") + } + clickCount, err := gesture.ClickCount(term) + if err != nil { + t.Fatal(err) + } + if clickCount != 1 { + t.Fatalf("expected click count 1, got %d", clickCount) + } + behavior, err := gesture.Behavior(term) + if err != nil { + t.Fatal(err) + } + if behavior != SelectionGestureBehaviorCell { + t.Fatalf("expected cell behavior, got %d", behavior) + } + + var multiClickCount uint8 + var multiDragged bool + written, err := gesture.GetMulti( + term, + []SelectionGestureData{SelectionGestureDataClickCount, SelectionGestureDataDragged}, + []unsafe.Pointer{unsafe.Pointer(&multiClickCount), unsafe.Pointer(&multiDragged)}, + ) + if err != nil { + t.Fatal(err) + } + if written != 2 || multiClickCount != 1 || !multiDragged { + t.Fatalf("unexpected get_multi result: written=%d click_count=%d dragged=%t", written, multiClickCount, multiDragged) + } + + if err := release.SetRef(dragRef); err != nil { + t.Fatal(err) + } + selection, err = gesture.Event(term, release) + if err != nil { + t.Fatal(err) + } + if selection != nil { + t.Fatal("expected release not to produce a selection") + } + + gesture.Reset(term) + worldRef, err := term.GridRef(Point{Tag: PointTagActive, X: 6, Y: 0}) + if err != nil { + t.Fatal(err) + } + if err := press.SetRef(worldRef); err != nil { + t.Fatal(err) + } + if err := press.ClearTimeNS(); err != nil { + t.Fatal(err) + } + selection, err = gesture.Event(term, press) + if err != nil { + t.Fatal(err) + } + if selection != nil { + t.Fatal("expected second press not to produce a selection") + } + + selection, err = gesture.Event(term, deepPress) + if err != nil { + t.Fatal(err) + } + if selection == nil { + t.Fatal("expected deep press selection") + } + formatted, err = term.SelectionFormatString(WithSelection(selection)) + if err != nil { + t.Fatal(err) + } + if formatted != "world" { + t.Fatalf("expected deep press selection %q, got %q", "world", formatted) + } +} diff --git a/types.go b/types.go new file mode 100644 --- /dev/null +++ b/types.go @@ -0,0 +1,27 @@ +package libghostty + +/* +#include +*/ +import "C" + +// SurfacePosition is an x/y position in rendered surface pixel space. +// +// This is not a terminal grid coordinate. The origin is the top-left of the +// rendered terminal surface. +// C: GhosttySurfacePosition +type SurfacePosition struct { + // X is the horizontal position in surface pixels. + X float64 + + // Y is the vertical position in surface pixels. + Y float64 +} + +// toC converts a Go SurfacePosition to a C GhosttySurfacePosition. +func (p SurfacePosition) toC() C.GhosttySurfacePosition { + return C.GhosttySurfacePosition{ + x: C.double(p.X), + y: C.double(p.Y), + } +}