tui_tui 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +60 -0
- data/README.md +51 -14
- data/examples/README.md +28 -0
- data/examples/breakout.rb +225 -0
- data/examples/counter.rb +11 -12
- data/examples/csv_viewer.rb +28 -50
- data/examples/downloads.rb +128 -0
- data/examples/file_browser.rb +64 -55
- data/examples/form.rb +80 -102
- data/examples/life.rb +1 -1
- data/examples/log_viewer.rb +118 -0
- data/examples/todo.rb +43 -64
- data/examples/widgets.rb +4 -4
- data/lib/tui_tui/box_chrome.rb +0 -9
- data/lib/tui_tui/box_chrome_resolver.rb +21 -0
- data/lib/tui_tui/canvas.rb +6 -55
- data/lib/tui_tui/canvas_compositor.rb +12 -10
- data/lib/tui_tui/canvas_renderer.rb +59 -0
- data/lib/tui_tui/cell.rb +4 -3
- data/lib/tui_tui/command.rb +21 -0
- data/lib/tui_tui/event_stream.rb +42 -7
- data/lib/tui_tui/key_reader.rb +1 -1
- data/lib/tui_tui/modal/command_palette.rb +193 -0
- data/lib/tui_tui/modal/confirm.rb +74 -0
- data/lib/tui_tui/modal/help.rb +44 -0
- data/lib/tui_tui/modal/pager.rb +106 -0
- data/lib/tui_tui/modal/prompt.rb +66 -0
- data/lib/tui_tui/modal/select.rb +103 -0
- data/lib/tui_tui/modal.rb +4 -0
- data/lib/tui_tui/modal_host.rb +2 -2
- data/lib/tui_tui/rect.rb +69 -0
- data/lib/tui_tui/runtime.rb +46 -23
- data/lib/tui_tui/screen.rb +7 -3
- data/lib/tui_tui/test_runtime.rb +71 -0
- data/lib/tui_tui/text_input.rb +168 -0
- data/lib/tui_tui/text_sanitizer.rb +3 -10
- data/lib/tui_tui/theme.rb +25 -27
- data/lib/tui_tui/version.rb +1 -1
- data/lib/tui_tui/widget/list.rb +59 -0
- data/lib/tui_tui/widget/progress.rb +33 -0
- data/lib/tui_tui/widget/scroll_list.rb +59 -0
- data/lib/tui_tui/widget/scrollbar.rb +43 -0
- data/lib/tui_tui/widget/spinner.rb +37 -0
- data/lib/tui_tui/widget/status_bar.rb +25 -0
- data/lib/tui_tui/widget/table.rb +81 -0
- data/lib/tui_tui/widget/tabs.rb +67 -0
- data/lib/tui_tui/widget/text_view.rb +47 -0
- data/lib/tui_tui/widget/toast.rb +83 -0
- data/lib/tui_tui.rb +23 -15
- metadata +28 -15
- data/lib/tui_tui/command_palette.rb +0 -190
- data/lib/tui_tui/confirm.rb +0 -74
- data/lib/tui_tui/help.rb +0 -44
- data/lib/tui_tui/list.rb +0 -57
- data/lib/tui_tui/pager.rb +0 -106
- data/lib/tui_tui/prompt.rb +0 -108
- data/lib/tui_tui/scroll_list.rb +0 -57
- data/lib/tui_tui/scrollbar.rb +0 -41
- data/lib/tui_tui/select.rb +0 -103
- data/lib/tui_tui/status_bar.rb +0 -23
- data/lib/tui_tui/text_view.rb +0 -45
- data/lib/tui_tui/toast.rb +0 -83
- /data/lib/tui_tui/{event.rb → events.rb} +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# A simulated download manager. It demonstrates a ticking dashboard built from
|
|
5
|
+
# Widget::Table (name/size/progress/status columns with Span-colored status
|
|
6
|
+
# cells), Widget::Spinner (animating the active row), Widget::Progress (overall
|
|
7
|
+
# bar), and Widget::StatusBar, laid out with Rect#split_rows. Downloads advance
|
|
8
|
+
# on each tick; finished ones promote the next queued item. Japanese file names
|
|
9
|
+
# show the width-safe rendering. Colors come from TuiTui::Theme.auto.
|
|
10
|
+
#
|
|
11
|
+
# ruby examples/downloads.rb
|
|
12
|
+
#
|
|
13
|
+
# Keys: j/k (or ↑/↓) move, Space pause/resume the selected download,
|
|
14
|
+
# q (or Ctrl-C) quit.
|
|
15
|
+
|
|
16
|
+
require_relative "../lib/tui_tui"
|
|
17
|
+
|
|
18
|
+
module DownloadsSample
|
|
19
|
+
Download = Data.define(:name, :size_mb, :ratio, :state)
|
|
20
|
+
|
|
21
|
+
SEED = [
|
|
22
|
+
Download.new("ruby-3.4.tar.gz", 24.8, 0.0, :active),
|
|
23
|
+
Download.new("開発環境つくりかた.pdf", 8.2, 0.0, :queued),
|
|
24
|
+
Download.new("tui_tui-manual.epub", 3.5, 0.0, :queued),
|
|
25
|
+
Download.new("空の絶景写真集.zip", 412.0, 0.0, :queued),
|
|
26
|
+
Download.new("linux-firmware.img", 96.3, 0.0, :queued),
|
|
27
|
+
Download.new("soundtrack.flac", 58.1, 0.0, :queued),
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
class App
|
|
31
|
+
def initialize(downloads = SEED)
|
|
32
|
+
@downloads = downloads.dup
|
|
33
|
+
@theme = TuiTui::Theme.auto
|
|
34
|
+
@spinner = TuiTui::Widget::Spinner.new
|
|
35
|
+
@list = TuiTui::Widget::ScrollList.new(@downloads.size)
|
|
36
|
+
@table = TuiTui::Widget::Table.new(@list)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Keep ticking only while something is downloading.
|
|
40
|
+
def wants_tick? = @downloads.any? { |d| d.state == :active }
|
|
41
|
+
|
|
42
|
+
def update(event)
|
|
43
|
+
case event
|
|
44
|
+
in TuiTui::KeyEvent(key: "q" | TuiTui::KeyCode::CTRL_C) then return :quit
|
|
45
|
+
in TuiTui::KeyEvent(key: "j" | :down) then @list.move(1)
|
|
46
|
+
in TuiTui::KeyEvent(key: "k" | :up) then @list.move(-1)
|
|
47
|
+
in TuiTui::KeyEvent(key: " ") then toggle_pause
|
|
48
|
+
in TuiTui::TickEvent then step
|
|
49
|
+
else nil
|
|
50
|
+
end
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def view(size)
|
|
55
|
+
canvas = TuiTui::Canvas.blank(size)
|
|
56
|
+
table, bar, status = TuiTui::Rect.of(size).split_rows(:rest, 1, 1)
|
|
57
|
+
|
|
58
|
+
@list.ensure_visible([table.rows - 1, 1].max)
|
|
59
|
+
draw_table(canvas, table)
|
|
60
|
+
TuiTui::Widget::Progress.draw(canvas, bar, overall_ratio,
|
|
61
|
+
bar_style: @theme.selection, track_style: @theme.muted)
|
|
62
|
+
TuiTui::Widget::StatusBar.draw(canvas, status, style: @theme.bar,
|
|
63
|
+
left: " #{done_count}/#{@downloads.size} done",
|
|
64
|
+
right: "j/k move Space pause q quit ")
|
|
65
|
+
canvas
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Advance every active download; a finished one promotes the next queued.
|
|
71
|
+
def step
|
|
72
|
+
@spinner.advance
|
|
73
|
+
@downloads.each_with_index do |item, index|
|
|
74
|
+
next unless item.state == :active
|
|
75
|
+
|
|
76
|
+
ratio = item.ratio + rand(0.02..0.09)
|
|
77
|
+
@downloads[index] = ratio >= 1.0 ? item.with(ratio: 1.0, state: :done) : item.with(ratio: ratio)
|
|
78
|
+
promote_next if ratio >= 1.0
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def promote_next
|
|
83
|
+
index = @downloads.index { |d| d.state == :queued }
|
|
84
|
+
@downloads[index] = @downloads[index].with(state: :active) if index
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def toggle_pause
|
|
88
|
+
item = @downloads[@list.cursor]
|
|
89
|
+
case item.state
|
|
90
|
+
when :active then @downloads[@list.cursor] = item.with(state: :paused)
|
|
91
|
+
when :paused then @downloads[@list.cursor] = item.with(state: :active)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def draw_table(canvas, rect)
|
|
96
|
+
return if rect.rows <= 0 || rect.cols <= 0
|
|
97
|
+
|
|
98
|
+
name_width = [rect.cols - 9 - 6 - 10 - 3, 8].max # fixed columns + 3 gaps
|
|
99
|
+
columns = [["name", name_width], ["size", 9], ["prog", 6], ["status", 10]]
|
|
100
|
+
@table.draw(canvas, rect, columns: columns,
|
|
101
|
+
header_style: @theme.bar, highlight: @theme.selection) do |index, _selected|
|
|
102
|
+
item = @downloads[index]
|
|
103
|
+
[item.name, format("%.1f MB", item.size_mb), "#{(item.ratio * 100).round}%", status_span(item)]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Status cells carry their own Span style, so they stay colored even on
|
|
108
|
+
# the highlighted row.
|
|
109
|
+
def status_span(item)
|
|
110
|
+
case item.state
|
|
111
|
+
when :done then TuiTui::Span["done", @theme.success]
|
|
112
|
+
when :active then TuiTui::Span["#{@spinner.glyph} down", @theme.warning]
|
|
113
|
+
when :paused then TuiTui::Span["paused", @theme.danger]
|
|
114
|
+
else TuiTui::Span["queued", @theme.muted]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def overall_ratio
|
|
119
|
+
@downloads.sum(&:ratio) / @downloads.size
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def done_count = @downloads.count { |d| d.state == :done }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if $PROGRAM_NAME == __FILE__
|
|
127
|
+
TuiTui::Runtime.new(DownloadsSample::App.new).run(tick: 0.1)
|
|
128
|
+
end
|
data/examples/file_browser.rb
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
#
|
|
15
15
|
# `/` is an incremental fuzzy finder built on TuiTui::Fuzzy (type to narrow,
|
|
16
16
|
# matched characters highlighted, ↑↓ to navigate, Enter to open, Esc to cancel).
|
|
17
|
-
# The : / m / ? / q modals are TuiTui widgets (CommandPalette, Select, Help,
|
|
17
|
+
# The : / m / ? / q modals are TuiTui widgets (CommandPalette, Select, Help,
|
|
18
|
+
# Confirm), opened and dispatched via TuiTui::ModalHost.
|
|
18
19
|
|
|
19
20
|
require "strscan"
|
|
20
21
|
require_relative "../lib/tui_tui"
|
|
@@ -204,7 +205,7 @@ module FileBrowserSample
|
|
|
204
205
|
class Browser
|
|
205
206
|
def initialize(path)
|
|
206
207
|
@dir = File.expand_path(path)
|
|
207
|
-
@list = TuiTui::ScrollList.new
|
|
208
|
+
@list = TuiTui::Widget::ScrollList.new
|
|
208
209
|
@preview_scroll = 0
|
|
209
210
|
@preview_wrap = false # toggle with "w": wrap long lines vs. clip them
|
|
210
211
|
@hl_path = nil # cache key for syntax-highlighted preview lines
|
|
@@ -219,31 +220,21 @@ module FileBrowserSample
|
|
|
219
220
|
@page = 1
|
|
220
221
|
@preview_page = 1
|
|
221
222
|
@split = 0.5 # divider position as a fraction of the body width (resize-safe)
|
|
222
|
-
@finder = nil # the fuzzy query while finding
|
|
223
|
+
@finder = nil # a TuiTui::TextInput holding the fuzzy query while finding, or nil when off
|
|
223
224
|
@matches = {} # entry name => matched char positions, for highlighting
|
|
224
|
-
@
|
|
225
|
-
@on_result = nil
|
|
226
|
-
@clipboard = nil # a path queued for the clipboard; the Runtime drains it
|
|
225
|
+
@host = TuiTui::ModalHost.new
|
|
227
226
|
load_entries
|
|
228
227
|
end
|
|
229
228
|
|
|
230
|
-
# The Runtime calls this after `update` and copies the returned text (OSC 52),
|
|
231
|
-
# then clears it. Keeping the I/O out of `update` leaves the fold pure.
|
|
232
|
-
def take_clipboard
|
|
233
|
-
text = @clipboard
|
|
234
|
-
@clipboard = nil
|
|
235
|
-
text
|
|
236
|
-
end
|
|
237
|
-
|
|
238
229
|
# Keep ticking only while a toast is showing, so it auto-dismisses.
|
|
239
230
|
def wants_tick? = !@toast.nil?
|
|
240
231
|
|
|
241
232
|
def update(event)
|
|
242
233
|
@toast = nil if @toast&.expired?
|
|
243
234
|
case event
|
|
244
|
-
when TuiTui::MouseEvent then @
|
|
235
|
+
when TuiTui::MouseEvent then @host.open? ? route_modal(event) : handle_mouse(event)
|
|
245
236
|
when TuiTui::KeyEvent
|
|
246
|
-
return route_modal(event
|
|
237
|
+
return route_modal(event) if @host.open?
|
|
247
238
|
return finder_key(event.key) if @finder
|
|
248
239
|
|
|
249
240
|
handle_key(event.key)
|
|
@@ -320,7 +311,7 @@ module FileBrowserSample
|
|
|
320
311
|
draw_preview(canvas, preview_rect) if preview_rect
|
|
321
312
|
draw_status(canvas, status) if status
|
|
322
313
|
@toast&.draw(canvas, size, style: @theme.selection)
|
|
323
|
-
@
|
|
314
|
+
@host.draw(canvas, size) # modal overlay on top of everything
|
|
324
315
|
canvas
|
|
325
316
|
end
|
|
326
317
|
|
|
@@ -329,28 +320,30 @@ module FileBrowserSample
|
|
|
329
320
|
# --- modals ---
|
|
330
321
|
|
|
331
322
|
# Show a modal widget; `on_result` interprets its resolved value (and may
|
|
332
|
-
# return :quit).
|
|
323
|
+
# return :quit). ModalHost routes events and runs on_result on resolve; this
|
|
324
|
+
# wrapper returns self so handle_key branches evaluate to the app model.
|
|
333
325
|
def open_modal(widget, &on_result)
|
|
334
|
-
@
|
|
335
|
-
|
|
326
|
+
@host.open(widget, &on_result)
|
|
327
|
+
self
|
|
336
328
|
end
|
|
337
329
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
330
|
+
# Route one event to the open modal and normalize the outcome for update:
|
|
331
|
+
# nil while it stays open (or when on_result had nothing to say) means self;
|
|
332
|
+
# :quit and [app, *commands] (the palette's "Copy path") pass through.
|
|
333
|
+
def route_modal(event)
|
|
334
|
+
case (outcome = @host.handle(event))
|
|
335
|
+
when :quit then :quit
|
|
336
|
+
when Array then outcome
|
|
337
|
+
else self
|
|
338
|
+
end
|
|
346
339
|
end
|
|
347
340
|
|
|
348
341
|
def confirm_quit
|
|
349
|
-
open_modal(TuiTui::Confirm.new("Quit file browser?", theme: @theme)) { |r| :quit if r == :ok }
|
|
342
|
+
open_modal(TuiTui::Modal::Confirm.new("Quit file browser?", theme: @theme)) { |r| :quit if r == :ok }
|
|
350
343
|
end
|
|
351
344
|
|
|
352
345
|
def open_actions
|
|
353
|
-
open_modal(TuiTui::Select.new("Actions", ACTIONS.map(&:first), theme: @theme)) do |result|
|
|
346
|
+
open_modal(TuiTui::Modal::Select.new("Actions", ACTIONS.map(&:first), theme: @theme)) do |result|
|
|
354
347
|
run_action(result) if result.is_a?(Integer)
|
|
355
348
|
end
|
|
356
349
|
end
|
|
@@ -363,13 +356,13 @@ module FileBrowserSample
|
|
|
363
356
|
end
|
|
364
357
|
end
|
|
365
358
|
|
|
366
|
-
def open_help = open_modal(TuiTui::Help.new("Keys", HELP, theme: @theme)) { nil }
|
|
359
|
+
def open_help = open_modal(TuiTui::Modal::Help.new("Keys", HELP, theme: @theme)) { nil }
|
|
367
360
|
|
|
368
361
|
# The ":" command palette: a fuzzy-filtered list of every command. The palette
|
|
369
362
|
# ranks by the label and resolves to the chosen [label, action] pair (or
|
|
370
363
|
# :cancel on Esc), which run_command dispatches.
|
|
371
364
|
def open_palette
|
|
372
|
-
open_modal(TuiTui::CommandPalette.new(COMMANDS, theme: @theme) { |label, _action| label }) do |chosen|
|
|
365
|
+
open_modal(TuiTui::Modal::CommandPalette.new(COMMANDS, theme: @theme) { |label, _action| label }) do |chosen|
|
|
373
366
|
run_command(chosen.last) if chosen.is_a?(Array)
|
|
374
367
|
end
|
|
375
368
|
end
|
|
@@ -390,6 +383,7 @@ module FileBrowserSample
|
|
|
390
383
|
|
|
391
384
|
# --- input ---
|
|
392
385
|
|
|
386
|
+
# Each branch evaluates to update's return value: self, [self, *commands], or :quit.
|
|
393
387
|
def handle_key(key)
|
|
394
388
|
case key
|
|
395
389
|
when "q", TuiTui::KeyCode::CTRL_C then confirm_quit
|
|
@@ -399,38 +393,50 @@ module FileBrowserSample
|
|
|
399
393
|
when "m" then open_actions
|
|
400
394
|
when "l", "\r", :right then open_entry
|
|
401
395
|
when "h", :left, TuiTui::KeyCode::BACKSPACE then up_dir # h / ← / Backspace
|
|
402
|
-
when "\t" then
|
|
403
|
-
when "<" then
|
|
404
|
-
when ">" then
|
|
396
|
+
when "\t" then switch_focus
|
|
397
|
+
when "<" then nudge_split(-SPLIT_STEP)
|
|
398
|
+
when ">" then nudge_split(SPLIT_STEP)
|
|
405
399
|
when "J" then scroll_preview(1) # always works, whichever pane is focused
|
|
406
400
|
when "K" then scroll_preview(-1)
|
|
407
401
|
when "w" then toggle_preview_wrap
|
|
408
402
|
when "t" then cycle_theme
|
|
409
|
-
when "y" then copy_path
|
|
403
|
+
when "y" then copy_path # [self, Command::Copy] propagates out of update
|
|
410
404
|
else navigate(key) # j/k, arrows, paging and g/G follow the focused pane
|
|
411
405
|
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def switch_focus
|
|
409
|
+
@focus = @focus.next
|
|
412
410
|
self
|
|
413
411
|
end
|
|
414
412
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
413
|
+
def nudge_split(delta)
|
|
414
|
+
@split = (@split + delta).clamp(0.1, 0.9)
|
|
415
|
+
self
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Return a Command::Copy for the selected path (the Runtime writes it to the
|
|
419
|
+
# clipboard via OSC 52) and show a toast so the copy is visibly confirmed.
|
|
419
420
|
def copy_path
|
|
420
|
-
|
|
421
|
-
@toast = TuiTui::Toast.new("copied path to clipboard")
|
|
421
|
+
path = File.expand_path(File.join(@dir, selected.to_s))
|
|
422
|
+
@toast = TuiTui::Widget::Toast.new("copied path to clipboard")
|
|
423
|
+
[self, TuiTui::Command::Copy.new(path)]
|
|
422
424
|
end
|
|
423
425
|
|
|
426
|
+
# Cycle the UI theme (default -> warm -> mono -> ...), rebuilding the chrome
|
|
427
|
+
# palette. Open modals read @theme when created, so the next one matches too.
|
|
424
428
|
def cycle_theme
|
|
425
429
|
@theme_i = (@theme_i + 1) % THEMES.size
|
|
426
430
|
@theme = FileBrowserSample.theme_for(THEMES[@theme_i])
|
|
427
431
|
@styles = FileBrowserSample.palette(@theme)
|
|
432
|
+
self
|
|
428
433
|
end
|
|
429
434
|
|
|
430
435
|
# Move/page keys act on whichever pane Tab has focused: the directory list,
|
|
431
436
|
# or the file preview (scrolling its text back and forth).
|
|
432
437
|
def navigate(key)
|
|
433
438
|
@focus.focused?(:preview) ? navigate_preview(key) : navigate_list(key)
|
|
439
|
+
self
|
|
434
440
|
end
|
|
435
441
|
|
|
436
442
|
def navigate_list(key)
|
|
@@ -457,27 +463,31 @@ module FileBrowserSample
|
|
|
457
463
|
|
|
458
464
|
def scroll_preview(delta)
|
|
459
465
|
@preview_scroll = [@preview_scroll + delta, 0].max # upper bound clamped in draw_preview
|
|
466
|
+
self
|
|
460
467
|
end
|
|
461
468
|
|
|
462
469
|
def toggle_preview_wrap
|
|
463
470
|
@preview_wrap = !@preview_wrap
|
|
464
471
|
@preview_scroll = 0 # the display-line count changes, so start from the top
|
|
472
|
+
self
|
|
465
473
|
end
|
|
466
474
|
|
|
467
475
|
def move(delta)
|
|
468
476
|
@list.move(delta)
|
|
469
477
|
@preview_scroll = 0
|
|
478
|
+
self
|
|
470
479
|
end
|
|
471
480
|
|
|
472
481
|
def go_to(index)
|
|
473
482
|
@list.go_to(index)
|
|
474
483
|
@preview_scroll = 0
|
|
484
|
+
self
|
|
475
485
|
end
|
|
476
486
|
|
|
477
487
|
def open_entry
|
|
478
488
|
name = selected
|
|
479
489
|
return up_dir if name == ".."
|
|
480
|
-
return unless directory?(name)
|
|
490
|
+
return self unless directory?(name)
|
|
481
491
|
|
|
482
492
|
@dir = File.join(@dir, name)
|
|
483
493
|
load_entries
|
|
@@ -486,7 +496,7 @@ module FileBrowserSample
|
|
|
486
496
|
|
|
487
497
|
def up_dir
|
|
488
498
|
parent = File.dirname(@dir)
|
|
489
|
-
return if parent == @dir # already at the filesystem root
|
|
499
|
+
return self if parent == @dir # already at the filesystem root
|
|
490
500
|
|
|
491
501
|
came_from = File.basename(@dir)
|
|
492
502
|
@dir = parent
|
|
@@ -499,8 +509,9 @@ module FileBrowserSample
|
|
|
499
509
|
# --- fuzzy finder (incremental; built on TuiTui::Fuzzy) ---
|
|
500
510
|
|
|
501
511
|
def enter_finder
|
|
502
|
-
@finder =
|
|
512
|
+
@finder = TuiTui::TextInput.new
|
|
503
513
|
refilter
|
|
514
|
+
self
|
|
504
515
|
end
|
|
505
516
|
|
|
506
517
|
def exit_finder
|
|
@@ -524,16 +535,14 @@ module FileBrowserSample
|
|
|
524
535
|
end
|
|
525
536
|
|
|
526
537
|
def type_finder(key)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
@finder += key
|
|
530
|
-
refilter
|
|
538
|
+
# #type ignores control keys; the query is append-only (cursor at the end).
|
|
539
|
+
refilter if @finder.type(key)
|
|
531
540
|
end
|
|
532
541
|
|
|
533
542
|
def backspace_finder
|
|
534
543
|
return if @finder.empty?
|
|
535
544
|
|
|
536
|
-
@finder
|
|
545
|
+
@finder.delete_back # by grapheme, so a multi-codepoint cluster deletes whole
|
|
537
546
|
refilter
|
|
538
547
|
end
|
|
539
548
|
|
|
@@ -562,7 +571,7 @@ module FileBrowserSample
|
|
|
562
571
|
# with matched positions for highlighting), otherwise the full dir-first list.
|
|
563
572
|
def refilter
|
|
564
573
|
if @finder && !@finder.empty?
|
|
565
|
-
ranked = TuiTui::Fuzzy.new(@finder).rank(@all)
|
|
574
|
+
ranked = TuiTui::Fuzzy.new(@finder.value).rank(@all)
|
|
566
575
|
@entries = ranked.map(&:first)
|
|
567
576
|
@matches = ranked.to_h { |name, found| [name, found.positions] }
|
|
568
577
|
else
|
|
@@ -603,7 +612,7 @@ module FileBrowserSample
|
|
|
603
612
|
|
|
604
613
|
def draw_list(canvas, rect)
|
|
605
614
|
highlight = @focus.focused?(:list) ? @styles[:select] : @styles[:select_blur]
|
|
606
|
-
TuiTui::List.new(@list).draw(canvas, rect, highlight: highlight, scrollbar: @theme) do |index, selected|
|
|
615
|
+
TuiTui::Widget::List.new(@list).draw(canvas, rect, highlight: highlight, scrollbar: @theme) do |index, selected|
|
|
607
616
|
name = @entries[index]
|
|
608
617
|
label = directory?(name) && name != ".." ? "#{name}/" : name
|
|
609
618
|
base = selected ? highlight : (directory?(name) ? @styles[:dir] : @styles[:file])
|
|
@@ -635,7 +644,7 @@ module FileBrowserSample
|
|
|
635
644
|
width = rect.split_gutter.first.cols # leave room for the scrollbar gutter when wrapping/truncating
|
|
636
645
|
lines = preview_display(width)
|
|
637
646
|
@preview_scroll = @preview_scroll.clamp(0, [lines.length - 1, 0].max)
|
|
638
|
-
TuiTui::TextView.draw(canvas, rect, lines, top: @preview_scroll, scrollbar: @theme)
|
|
647
|
+
TuiTui::Widget::TextView.draw(canvas, rect, lines, top: @preview_scroll, scrollbar: @theme)
|
|
639
648
|
end
|
|
640
649
|
|
|
641
650
|
# Preview as styled display lines (Line). Wrap mode wraps to the width (plain,
|
|
@@ -671,10 +680,10 @@ module FileBrowserSample
|
|
|
671
680
|
end
|
|
672
681
|
|
|
673
682
|
def draw_status(canvas, rect)
|
|
674
|
-
left = @finder ? " > #{@finder}" : " #{@dir}"
|
|
683
|
+
left = @finder ? " > #{@finder.value}" : " #{@dir}"
|
|
675
684
|
hints = @finder ? "Esc=cancel Enter=open" : "?=help /=find :=cmds m=menu t=#{THEMES[@theme_i]} q=quit"
|
|
676
685
|
right = "#{@list.cursor + 1}/#{@entries.size} #{hints} "
|
|
677
|
-
TuiTui::StatusBar.draw(canvas, rect, left: left, right: right, style: @styles[:bar])
|
|
686
|
+
TuiTui::Widget::StatusBar.draw(canvas, rect, left: left, right: right, style: @styles[:bar])
|
|
678
687
|
end
|
|
679
688
|
|
|
680
689
|
# Lines of the selected file's preview, cached per selection so we do not
|