echoes 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CLAUDE.md +33 -0
- data/Echoes.app/Contents/Info.plist +16 -0
- data/Echoes.app/Contents/MacOS/Echoes +50 -0
- data/EchoesEmbed.app/Contents/Info.plist +16 -0
- data/EchoesEmbed.app/Contents/MacOS/EchoesEmbed +41 -0
- data/LICENSE.txt +21 -0
- data/README.md +133 -0
- data/Rakefile +45 -0
- data/exe/echoes +15 -0
- data/lib/echoes/cell.rb +54 -0
- data/lib/echoes/client.rb +96 -0
- data/lib/echoes/configuration.rb +135 -0
- data/lib/echoes/copy_mode.rb +545 -0
- data/lib/echoes/cursor.rb +18 -0
- data/lib/echoes/editor.rb +225 -0
- data/lib/echoes/embedded_shell.rb +360 -0
- data/lib/echoes/embedded_shell_helper.rb +265 -0
- data/lib/echoes/gui.rb +2861 -0
- data/lib/echoes/installer.rb +95 -0
- data/lib/echoes/objc.rb +188 -0
- data/lib/echoes/pane.rb +1122 -0
- data/lib/echoes/pane_tree.rb +194 -0
- data/lib/echoes/parser.rb +821 -0
- data/lib/echoes/preferences.rb +45 -0
- data/lib/echoes/screen.rb +1468 -0
- data/lib/echoes/sixel_decoder.rb +221 -0
- data/lib/echoes/tab.rb +152 -0
- data/lib/echoes/terminal.rb +124 -0
- data/lib/echoes/version.rb +5 -0
- data/lib/echoes.rb +37 -0
- data/sig/echoes.rbs +4 -0
- metadata +123 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Echoes
|
|
4
|
+
class CopyMode
|
|
5
|
+
attr_reader :active, :cursor_row, :cursor_col
|
|
6
|
+
attr_reader :selection_start, :selection_end
|
|
7
|
+
|
|
8
|
+
def initialize(screen)
|
|
9
|
+
@screen = screen
|
|
10
|
+
@active = false
|
|
11
|
+
@cursor_row = 0
|
|
12
|
+
@cursor_col = 0
|
|
13
|
+
@selection_start = nil
|
|
14
|
+
@selection_end = nil
|
|
15
|
+
@search_query = nil
|
|
16
|
+
@search_direction = :forward
|
|
17
|
+
@pending_find = nil # :f, :F, :t, :T
|
|
18
|
+
@last_find = nil # [direction, char] for ; and ,
|
|
19
|
+
@line_selection = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def enter
|
|
23
|
+
@active = true
|
|
24
|
+
@cursor_row = @screen.cursor.row
|
|
25
|
+
@cursor_col = @screen.cursor.col
|
|
26
|
+
@selection_start = nil
|
|
27
|
+
@selection_end = nil
|
|
28
|
+
@search_query = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def exit
|
|
32
|
+
@active = false
|
|
33
|
+
@selection_start = nil
|
|
34
|
+
@selection_end = nil
|
|
35
|
+
@line_selection = false
|
|
36
|
+
@search_query = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def selecting?
|
|
40
|
+
@selection_start != nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Handle a key in copy mode. Returns :exit if copy mode should end,
|
|
44
|
+
# :yank if text was yanked, nil otherwise.
|
|
45
|
+
def handle_key(key)
|
|
46
|
+
# Pending f/F/t/T — next key is the target character
|
|
47
|
+
if @pending_find
|
|
48
|
+
execute_find(@pending_find, key)
|
|
49
|
+
@pending_find = nil
|
|
50
|
+
update_selection_end if selecting?
|
|
51
|
+
return nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
case key
|
|
55
|
+
# Basic movement
|
|
56
|
+
when 'h'
|
|
57
|
+
move_cursor(0, -1)
|
|
58
|
+
when 'j'
|
|
59
|
+
move_cursor(1, 0)
|
|
60
|
+
when 'k'
|
|
61
|
+
move_cursor(-1, 0)
|
|
62
|
+
when 'l'
|
|
63
|
+
move_cursor(0, 1)
|
|
64
|
+
|
|
65
|
+
# Line positions
|
|
66
|
+
when '0'
|
|
67
|
+
@cursor_col = 0
|
|
68
|
+
when '^'
|
|
69
|
+
move_first_non_blank
|
|
70
|
+
when '$'
|
|
71
|
+
move_end_of_line
|
|
72
|
+
|
|
73
|
+
# Word motions (vim-style: word = keyword chars)
|
|
74
|
+
when 'w'
|
|
75
|
+
move_word_forward
|
|
76
|
+
when 'e'
|
|
77
|
+
move_word_end_forward
|
|
78
|
+
when 'b'
|
|
79
|
+
move_word_backward
|
|
80
|
+
|
|
81
|
+
# WORD motions (whitespace-delimited)
|
|
82
|
+
when 'W'
|
|
83
|
+
move_bigword_forward
|
|
84
|
+
when 'E'
|
|
85
|
+
move_bigword_end_forward
|
|
86
|
+
when 'B'
|
|
87
|
+
move_bigword_backward
|
|
88
|
+
|
|
89
|
+
# Find in line
|
|
90
|
+
when 'f'
|
|
91
|
+
@pending_find = :f
|
|
92
|
+
when 'F'
|
|
93
|
+
@pending_find = :F
|
|
94
|
+
when 't'
|
|
95
|
+
@pending_find = :t
|
|
96
|
+
when 'T'
|
|
97
|
+
@pending_find = :T
|
|
98
|
+
when ';'
|
|
99
|
+
repeat_find(:same)
|
|
100
|
+
when ','
|
|
101
|
+
repeat_find(:reverse)
|
|
102
|
+
|
|
103
|
+
# Screen-relative jumps
|
|
104
|
+
when 'H'
|
|
105
|
+
@cursor_row = visible_top_row
|
|
106
|
+
when 'M'
|
|
107
|
+
@cursor_row = visible_top_row + @screen.rows / 2
|
|
108
|
+
when 'L'
|
|
109
|
+
@cursor_row = visible_top_row + @screen.rows - 1
|
|
110
|
+
|
|
111
|
+
# Document jumps
|
|
112
|
+
when 'g'
|
|
113
|
+
@cursor_row = -@screen.scrollback.size
|
|
114
|
+
@cursor_col = 0
|
|
115
|
+
when 'G'
|
|
116
|
+
@cursor_row = @screen.rows - 1
|
|
117
|
+
@cursor_col = 0
|
|
118
|
+
|
|
119
|
+
# Paragraph motions
|
|
120
|
+
when '{'
|
|
121
|
+
move_paragraph_backward
|
|
122
|
+
when '}'
|
|
123
|
+
move_paragraph_forward
|
|
124
|
+
|
|
125
|
+
# Scrolling
|
|
126
|
+
when "\x15" # Ctrl-U
|
|
127
|
+
move_cursor(-(@screen.rows / 2), 0)
|
|
128
|
+
when "\x04" # Ctrl-D
|
|
129
|
+
move_cursor(@screen.rows / 2, 0)
|
|
130
|
+
when "\x02" # Ctrl-B
|
|
131
|
+
move_cursor(-@screen.rows, 0)
|
|
132
|
+
when "\x06" # Ctrl-F
|
|
133
|
+
move_cursor(@screen.rows, 0)
|
|
134
|
+
|
|
135
|
+
# Search
|
|
136
|
+
when '/'
|
|
137
|
+
@search_query = +""
|
|
138
|
+
@search_direction = :forward
|
|
139
|
+
when '?'
|
|
140
|
+
@search_query = +""
|
|
141
|
+
@search_direction = :backward
|
|
142
|
+
when 'n'
|
|
143
|
+
search_next
|
|
144
|
+
when 'N'
|
|
145
|
+
search_prev
|
|
146
|
+
|
|
147
|
+
# Selection & yank
|
|
148
|
+
when 'v'
|
|
149
|
+
start_selection
|
|
150
|
+
when 'V'
|
|
151
|
+
start_line_selection
|
|
152
|
+
when 'y'
|
|
153
|
+
return :yank if selecting?
|
|
154
|
+
when 'q', "\e"
|
|
155
|
+
self.exit
|
|
156
|
+
return :exit
|
|
157
|
+
end
|
|
158
|
+
update_selection_end if selecting?
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Extract text from selection range
|
|
163
|
+
def selected_text
|
|
164
|
+
return "" unless @selection_start && @selection_end
|
|
165
|
+
|
|
166
|
+
start_pos, end_pos = [@selection_start, @selection_end].sort_by { |p| [p[0], p[1]] }
|
|
167
|
+
sr, sc = start_pos
|
|
168
|
+
er, ec = end_pos
|
|
169
|
+
|
|
170
|
+
lines = []
|
|
171
|
+
(sr..er).each do |row|
|
|
172
|
+
line = row_text(row)
|
|
173
|
+
if row == sr && row == er
|
|
174
|
+
lines << line[sc..ec]
|
|
175
|
+
elsif row == sr
|
|
176
|
+
lines << line[sc..]
|
|
177
|
+
elsif row == er
|
|
178
|
+
lines << line[0..ec]
|
|
179
|
+
else
|
|
180
|
+
lines << line
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
lines.join("\n")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Scroll offset needed to keep cursor visible
|
|
187
|
+
def scroll_offset_for_cursor
|
|
188
|
+
if @cursor_row < 0
|
|
189
|
+
@cursor_row.abs
|
|
190
|
+
else
|
|
191
|
+
0
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Absolute row in the combined scrollback+grid buffer
|
|
196
|
+
def absolute_cursor_row
|
|
197
|
+
@screen.scrollback.size + @cursor_row
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
def move_cursor(dr, dc)
|
|
203
|
+
new_row = @cursor_row + dr
|
|
204
|
+
new_col = @cursor_col + dc
|
|
205
|
+
|
|
206
|
+
min_row = -@screen.scrollback.size
|
|
207
|
+
max_row = @screen.rows - 1
|
|
208
|
+
|
|
209
|
+
@cursor_row = new_row.clamp(min_row, max_row)
|
|
210
|
+
@cursor_col = new_col.clamp(0, @screen.cols - 1)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# --- Character classification ---
|
|
214
|
+
|
|
215
|
+
def char_class(c)
|
|
216
|
+
if c.nil? || c.empty? || c == ' '
|
|
217
|
+
:space
|
|
218
|
+
elsif word_separators.include?(c)
|
|
219
|
+
:separator
|
|
220
|
+
else
|
|
221
|
+
:word
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def word_separators
|
|
226
|
+
Echoes.config.word_separators
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# --- Line positions ---
|
|
230
|
+
|
|
231
|
+
def move_first_non_blank
|
|
232
|
+
line = row_text(@cursor_row)
|
|
233
|
+
col = 0
|
|
234
|
+
col += 1 while col < @screen.cols && line[col] == ' '
|
|
235
|
+
@cursor_col = col.clamp(0, @screen.cols - 1)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def move_end_of_line
|
|
239
|
+
line = row_text(@cursor_row)
|
|
240
|
+
col = line.rstrip.length - 1
|
|
241
|
+
@cursor_col = col.clamp(0, @screen.cols - 1)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# --- Small word motions (vim 'word': same-class sequences) ---
|
|
245
|
+
|
|
246
|
+
def move_word_forward
|
|
247
|
+
line = row_text(@cursor_row)
|
|
248
|
+
col = @cursor_col
|
|
249
|
+
max = @screen.cols - 1
|
|
250
|
+
|
|
251
|
+
if col < max
|
|
252
|
+
cls = char_class(line[col])
|
|
253
|
+
# Skip rest of current word/separator run
|
|
254
|
+
col += 1 while col < max && char_class(line[col]) == cls
|
|
255
|
+
# Skip whitespace
|
|
256
|
+
col += 1 while col < max && char_class(line[col]) == :space
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
if col >= max && @cursor_row < @screen.rows - 1
|
|
260
|
+
# Wrap to next line's first non-blank
|
|
261
|
+
@cursor_row += 1
|
|
262
|
+
line = row_text(@cursor_row)
|
|
263
|
+
col = 0
|
|
264
|
+
col += 1 while col < max && char_class(line[col]) == :space
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
@cursor_col = col.clamp(0, max)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def move_word_end_forward
|
|
271
|
+
line = row_text(@cursor_row)
|
|
272
|
+
col = @cursor_col
|
|
273
|
+
max = @screen.cols - 1
|
|
274
|
+
|
|
275
|
+
# Move at least one position
|
|
276
|
+
col += 1 if col < max
|
|
277
|
+
line = row_text(@cursor_row)
|
|
278
|
+
|
|
279
|
+
# Skip whitespace
|
|
280
|
+
col += 1 while col < max && char_class(line[col]) == :space
|
|
281
|
+
|
|
282
|
+
# Move to end of word
|
|
283
|
+
cls = char_class(line[col])
|
|
284
|
+
col += 1 while col < max && char_class(line[col + 1]) == cls
|
|
285
|
+
|
|
286
|
+
@cursor_col = col.clamp(0, max)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def move_word_backward
|
|
290
|
+
line = row_text(@cursor_row)
|
|
291
|
+
col = @cursor_col
|
|
292
|
+
|
|
293
|
+
if col > 0
|
|
294
|
+
col -= 1
|
|
295
|
+
# Skip whitespace
|
|
296
|
+
col -= 1 while col > 0 && char_class(line[col]) == :space
|
|
297
|
+
# Skip to beginning of word
|
|
298
|
+
cls = char_class(line[col])
|
|
299
|
+
col -= 1 while col > 0 && char_class(line[col - 1]) == cls
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
if col <= 0 && @cursor_row > -@screen.scrollback.size
|
|
303
|
+
# Wrap to end of previous line
|
|
304
|
+
@cursor_row -= 1
|
|
305
|
+
line = row_text(@cursor_row)
|
|
306
|
+
col = line.rstrip.length - 1
|
|
307
|
+
col = 0 if col < 0
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
@cursor_col = col.clamp(0, @screen.cols - 1)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# --- Big WORD motions (whitespace-delimited) ---
|
|
314
|
+
|
|
315
|
+
def move_bigword_forward
|
|
316
|
+
line = row_text(@cursor_row)
|
|
317
|
+
col = @cursor_col
|
|
318
|
+
content_end = line.rstrip.length
|
|
319
|
+
|
|
320
|
+
if col < content_end
|
|
321
|
+
# Skip non-space
|
|
322
|
+
col += 1 while col < content_end && line[col] != ' '
|
|
323
|
+
# Skip space
|
|
324
|
+
col += 1 while col < content_end && line[col] == ' '
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
@cursor_col = col.clamp(0, @screen.cols - 1)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def move_bigword_end_forward
|
|
331
|
+
line = row_text(@cursor_row)
|
|
332
|
+
col = @cursor_col
|
|
333
|
+
max = @screen.cols - 1
|
|
334
|
+
|
|
335
|
+
col += 1 if col < max
|
|
336
|
+
# Skip space
|
|
337
|
+
col += 1 while col < max && line[col] == ' '
|
|
338
|
+
# Move to end of WORD
|
|
339
|
+
col += 1 while col < max && line[col + 1] && line[col + 1] != ' '
|
|
340
|
+
|
|
341
|
+
@cursor_col = col.clamp(0, max)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def move_bigword_backward
|
|
345
|
+
line = row_text(@cursor_row)
|
|
346
|
+
col = @cursor_col
|
|
347
|
+
|
|
348
|
+
col -= 1 if col > 0
|
|
349
|
+
# Skip space
|
|
350
|
+
col -= 1 while col > 0 && line[col] == ' '
|
|
351
|
+
# Skip to beginning of WORD
|
|
352
|
+
col -= 1 while col > 0 && line[col - 1] && line[col - 1] != ' '
|
|
353
|
+
|
|
354
|
+
@cursor_col = col.clamp(0, @screen.cols - 1)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# --- Find in line (f/F/t/T) ---
|
|
358
|
+
|
|
359
|
+
def execute_find(direction, char)
|
|
360
|
+
@last_find = [direction, char]
|
|
361
|
+
line = row_text(@cursor_row)
|
|
362
|
+
|
|
363
|
+
case direction
|
|
364
|
+
when :f
|
|
365
|
+
idx = line.index(char, @cursor_col + 1)
|
|
366
|
+
@cursor_col = idx if idx
|
|
367
|
+
when :F
|
|
368
|
+
idx = line.rindex(char, [@cursor_col - 1, 0].max)
|
|
369
|
+
@cursor_col = idx if idx && idx < @cursor_col
|
|
370
|
+
when :t
|
|
371
|
+
idx = line.index(char, @cursor_col + 1)
|
|
372
|
+
@cursor_col = idx - 1 if idx && idx > @cursor_col + 1
|
|
373
|
+
when :T
|
|
374
|
+
idx = line.rindex(char, [@cursor_col - 1, 0].max)
|
|
375
|
+
@cursor_col = idx + 1 if idx && idx + 1 < @cursor_col
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def repeat_find(mode)
|
|
380
|
+
return unless @last_find
|
|
381
|
+
|
|
382
|
+
direction, char = @last_find
|
|
383
|
+
if mode == :reverse
|
|
384
|
+
direction = {:f => :F, :F => :f, :t => :T, :T => :t}[direction]
|
|
385
|
+
end
|
|
386
|
+
execute_find(direction, char)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# --- Paragraph motions ---
|
|
390
|
+
|
|
391
|
+
def move_paragraph_backward
|
|
392
|
+
row = @cursor_row - 1
|
|
393
|
+
min_row = -@screen.scrollback.size
|
|
394
|
+
# Skip non-blank lines
|
|
395
|
+
row -= 1 while row > min_row && !blank_row?(row)
|
|
396
|
+
# Skip blank lines
|
|
397
|
+
row -= 1 while row > min_row && blank_row?(row)
|
|
398
|
+
@cursor_row = row.clamp(min_row, @screen.rows - 1)
|
|
399
|
+
@cursor_col = 0
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def move_paragraph_forward
|
|
403
|
+
row = @cursor_row + 1
|
|
404
|
+
max_row = @screen.rows - 1
|
|
405
|
+
# Skip non-blank lines
|
|
406
|
+
row += 1 while row < max_row && !blank_row?(row)
|
|
407
|
+
# Skip blank lines
|
|
408
|
+
row += 1 while row < max_row && blank_row?(row)
|
|
409
|
+
@cursor_row = row.clamp(-@screen.scrollback.size, max_row)
|
|
410
|
+
@cursor_col = 0
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def blank_row?(row)
|
|
414
|
+
row_text(row).strip.empty?
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# --- Search ---
|
|
418
|
+
|
|
419
|
+
def search_next
|
|
420
|
+
return unless @search_query && !@search_query.empty?
|
|
421
|
+
if @search_direction == :forward
|
|
422
|
+
search_forward
|
|
423
|
+
else
|
|
424
|
+
search_backward
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def search_prev
|
|
429
|
+
return unless @search_query && !@search_query.empty?
|
|
430
|
+
if @search_direction == :forward
|
|
431
|
+
search_backward
|
|
432
|
+
else
|
|
433
|
+
search_forward
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def search_forward
|
|
438
|
+
min_row = -@screen.scrollback.size
|
|
439
|
+
max_row = @screen.rows - 1
|
|
440
|
+
|
|
441
|
+
# Search from current position forward
|
|
442
|
+
((@cursor_row)..max_row).each do |row|
|
|
443
|
+
line = row_text(row)
|
|
444
|
+
start = (row == @cursor_row) ? @cursor_col + 1 : 0
|
|
445
|
+
idx = line.index(@search_query, start)
|
|
446
|
+
if idx
|
|
447
|
+
@cursor_row = row
|
|
448
|
+
@cursor_col = idx
|
|
449
|
+
return
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Wrap around from top
|
|
454
|
+
(min_row...@cursor_row).each do |row|
|
|
455
|
+
line = row_text(row)
|
|
456
|
+
idx = line.index(@search_query)
|
|
457
|
+
if idx
|
|
458
|
+
@cursor_row = row
|
|
459
|
+
@cursor_col = idx
|
|
460
|
+
return
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def search_backward
|
|
466
|
+
min_row = -@screen.scrollback.size
|
|
467
|
+
max_row = @screen.rows - 1
|
|
468
|
+
|
|
469
|
+
# Search from current position backward
|
|
470
|
+
@cursor_row.downto(min_row) do |row|
|
|
471
|
+
line = row_text(row)
|
|
472
|
+
limit = (row == @cursor_row) ? [@cursor_col - 1, 0].max : line.length
|
|
473
|
+
idx = line.rindex(@search_query, limit)
|
|
474
|
+
if idx && (row != @cursor_row || idx < @cursor_col)
|
|
475
|
+
@cursor_row = row
|
|
476
|
+
@cursor_col = idx
|
|
477
|
+
return
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Wrap around from bottom
|
|
482
|
+
max_row.downto(@cursor_row + 1) do |row|
|
|
483
|
+
line = row_text(row)
|
|
484
|
+
idx = line.rindex(@search_query)
|
|
485
|
+
if idx
|
|
486
|
+
@cursor_row = row
|
|
487
|
+
@cursor_col = idx
|
|
488
|
+
return
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# --- Screen-relative ---
|
|
494
|
+
|
|
495
|
+
def visible_top_row
|
|
496
|
+
@cursor_row - @cursor_row.clamp(0, @screen.rows - 1)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# --- Selection ---
|
|
500
|
+
|
|
501
|
+
def start_selection
|
|
502
|
+
if selecting?
|
|
503
|
+
@selection_start = nil
|
|
504
|
+
@selection_end = nil
|
|
505
|
+
@line_selection = false
|
|
506
|
+
else
|
|
507
|
+
@selection_start = [@cursor_row, @cursor_col]
|
|
508
|
+
@selection_end = [@cursor_row, @cursor_col]
|
|
509
|
+
@line_selection = false
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def start_line_selection
|
|
514
|
+
if selecting?
|
|
515
|
+
@selection_start = nil
|
|
516
|
+
@selection_end = nil
|
|
517
|
+
@line_selection = false
|
|
518
|
+
else
|
|
519
|
+
@selection_start = [@cursor_row, 0]
|
|
520
|
+
@selection_end = [@cursor_row, @screen.cols - 1]
|
|
521
|
+
@line_selection = true
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def update_selection_end
|
|
526
|
+
if @line_selection
|
|
527
|
+
@selection_start = [[@selection_start[0], @cursor_row].min, 0]
|
|
528
|
+
@selection_end = [[@selection_end[0], @cursor_row].max, @screen.cols - 1]
|
|
529
|
+
else
|
|
530
|
+
@selection_end = [@cursor_row, @cursor_col]
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def row_text(row)
|
|
535
|
+
if row < 0
|
|
536
|
+
sb_index = @screen.scrollback.size + row
|
|
537
|
+
return "" if sb_index < 0 || sb_index >= @screen.scrollback.size
|
|
538
|
+
@screen.scrollback[sb_index].map(&:char).join
|
|
539
|
+
else
|
|
540
|
+
return "" if row >= @screen.rows
|
|
541
|
+
@screen.grid[row].map(&:char).join
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Echoes
|
|
4
|
+
class Cursor
|
|
5
|
+
attr_accessor :row, :col, :visible
|
|
6
|
+
|
|
7
|
+
def initialize(row: 0, col: 0)
|
|
8
|
+
@row = row
|
|
9
|
+
@col = col
|
|
10
|
+
@visible = true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def move_to(row, col)
|
|
14
|
+
@row = row
|
|
15
|
+
@col = col
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|