Class: TuiTui::TextInput

Inherits:
Object
  • Object
show all
Defined in:
lib/tui_tui/text_input.rb

Overview

A single-line, grapheme-aware text buffer with a cursor: the editing model shared by the prompt, search bars, command-palette queries, and editable cells. It is Canvas-agnostic — it knows only graphemes, cursor position, and display columns — so the same model drives any chrome around it.

Editing happens by grapheme cluster, so the cursor never lands inside an emoji modifier or a base+combining-mark pair, and a combining mark typed after its base merges into one grapheme.

The buffer owns only the editing keys (text, Backspace/Delete, arrows, Home/End) via #handle_editing; semantic keys like Enter and Escape belong to the host, which decides what they mean.

Defined Under Namespace

Classes: Viewport

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(value: "") ⇒ TextInput

Returns a new instance of TextInput.



23
24
25
26
# File 'lib/tui_tui/text_input.rb', line 23

def initialize(value: "")
  @graphemes = value.grapheme_clusters
  @cursor = @graphemes.length
end

Instance Attribute Details

#cursorObject

The cursor as a grapheme index in 0..length.



21
22
23
# File 'lib/tui_tui/text_input.rb', line 21

def cursor
  @cursor
end

Instance Method Details

#delete_backObject



72
73
74
75
76
77
78
# File 'lib/tui_tui/text_input.rb', line 72

def delete_back
  return false if @cursor.zero?

  @graphemes.delete_at(@cursor - 1)
  @cursor -= 1
  true
end

#delete_forwardObject



80
81
82
83
84
85
# File 'lib/tui_tui/text_input.rb', line 80

def delete_forward
  return false if @cursor >= @graphemes.length

  @graphemes.delete_at(@cursor)
  true
end

#empty?Boolean

Returns:

  • (Boolean)


29
# File 'lib/tui_tui/text_input.rb', line 29

def empty? = @graphemes.empty?

#handle_editing(key) ⇒ Object

Handle an editing key, returning true if it was consumed. Enter, Escape and any other non-editing key return false so the host can act on them.



39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/tui_tui/text_input.rb', line 39

def handle_editing(key)
  case key
  when KeyCode::BACKSPACE, :backspace then delete_back
  when :delete then delete_forward
  when :left then left
  when :right then right
  when :home then to_home
  when :end then to_end
  when String then return type(key)
  else return false
  end
  true
end

#index_at(rel, from: 0) ⇒ Object

The grapheme index whose left edge is closest to rel columns past the grapheme from — for mapping a mouse click onto the cursor, where from is the left grapheme of the visible viewport (see #viewport).



95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/tui_tui/text_input.rb', line 95

def index_at(rel, from: 0)
  return from if rel <= 0

  width = 0
  @graphemes[from..].each_with_index do |grapheme, i|
    w = DisplayText.new(grapheme).width
    return from + i if rel < width + ((w + 1) / 2)

    width += w
  end
  @graphemes.length
end

#insert(string) ⇒ Object

Insert a string at the cursor, re-clustering across both boundaries so a combining mark merges into its neighbour. The cursor lands after the text.



66
67
68
69
70
# File 'lib/tui_tui/text_input.rb', line 66

def insert(string)
  head = @graphemes[0...@cursor].join
  @graphemes = (head + string + @graphemes[@cursor..].join).grapheme_clusters
  @cursor = (head + string).grapheme_clusters.length
end

#leftObject



87
# File 'lib/tui_tui/text_input.rb', line 87

def left = (@cursor = [@cursor - 1, 0].max)

#lengthObject



30
# File 'lib/tui_tui/text_input.rb', line 30

def length = @graphemes.length

#rightObject



88
# File 'lib/tui_tui/text_input.rb', line 88

def right = (@cursor = [@cursor + 1, @graphemes.length].min)

#to_endObject



90
# File 'lib/tui_tui/text_input.rb', line 90

def to_end = (@cursor = @graphemes.length)

#to_homeObject



89
# File 'lib/tui_tui/text_input.rb', line 89

def to_home = (@cursor = 0)

#type(key) ⇒ Object

Insert key as literal text when it is printable (no control bytes), returning whether it was accepted. The entry point for typed input — hosts that bypass #handle_editing (e.g. append-only queries) call this directly so the printable rule lives in one place rather than at every call site.



57
58
59
60
61
62
# File 'lib/tui_tui/text_input.rb', line 57

def type(key)
  return false unless printable?(key)

  insert(key)
  true
end

#valueObject



28
# File 'lib/tui_tui/text_input.rb', line 28

def value = @graphemes.join

#viewport(cols) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/tui_tui/text_input.rb', line 119

def viewport(cols)
  cols = [cols, 1].max
  # Reserve room for the block cursor: a real grapheme's width, else one
  # column for the trailing space drawn when the cursor sits at the end.
  cell = grapheme_at_cursor ? DisplayText.new(grapheme_at_cursor).width : 1
  # Scroll right one grapheme at a time until the cursor cell fits, shrinking
  # the running leading width instead of recomputing it from scratch.
  start = 0
  lead = width_before_cursor
  while start < @cursor && lead + cell > cols
    lead -= DisplayText.new(@graphemes[start]).width
    start += 1
  end
  Viewport.new(
    text: visible_slice(start, cols),
    cursor_col: lead,
    cursor_grapheme: grapheme_at_cursor,
    start: start
  )
end