tui_tui 0.2.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 +80 -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 +106 -56
- 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/box_prober.rb +3 -4
- 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/clock.rb +11 -0
- data/lib/tui_tui/command.rb +21 -0
- data/lib/tui_tui/event_stream.rb +42 -7
- data/lib/tui_tui/key_code.rb +3 -0
- data/lib/tui_tui/key_reader.rb +1 -1
- data/lib/tui_tui/line.rb +11 -0
- 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 +52 -20
- data/lib/tui_tui/screen.rb +15 -3
- data/lib/tui_tui/terminal_session.rb +16 -1
- 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 -2
- 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 +24 -14
- metadata +28 -13
- data/lib/tui_tui/confirm.rb +0 -74
- data/lib/tui_tui/help.rb +0 -44
- data/lib/tui_tui/list.rb +0 -59
- data/lib/tui_tui/pager.rb +0 -94
- data/lib/tui_tui/prompt.rb +0 -111
- data/lib/tui_tui/scroll_list.rb +0 -57
- data/lib/tui_tui/scrollbar.rb +0 -41
- data/lib/tui_tui/select.rb +0 -104
- data/lib/tui_tui/status_bar.rb +0 -23
- data/lib/tui_tui/text_view.rb +0 -56
- data/lib/tui_tui/toast.rb +0 -82
- /data/lib/tui_tui/{event.rb → events.rb} +0 -0
data/lib/tui_tui/prompt.rb
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "display_text"
|
|
4
|
-
require_relative "style"
|
|
5
|
-
require_relative "modal"
|
|
6
|
-
require_relative "key_code"
|
|
7
|
-
|
|
8
|
-
module TuiTui
|
|
9
|
-
# Single-line text input modal with terminal-column-aware cursor placement.
|
|
10
|
-
class Prompt < Modal
|
|
11
|
-
MIN_INNER = 24
|
|
12
|
-
|
|
13
|
-
def initialize(label, value: "", theme: Theme::DEFAULT)
|
|
14
|
-
@label = DisplayText.new(label)
|
|
15
|
-
@graphemes = value.grapheme_clusters
|
|
16
|
-
@pos = @graphemes.length
|
|
17
|
-
@theme = theme
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def value = @graphemes.join
|
|
21
|
-
|
|
22
|
-
def handle(key)
|
|
23
|
-
case key
|
|
24
|
-
when "\r"
|
|
25
|
-
[:ok, value]
|
|
26
|
-
when :escape, KeyCode::CTRL_C
|
|
27
|
-
:cancel
|
|
28
|
-
when KeyCode::BACKSPACE, :backspace
|
|
29
|
-
edit { delete_back }
|
|
30
|
-
when :delete
|
|
31
|
-
edit { delete_forward }
|
|
32
|
-
when :left
|
|
33
|
-
edit { @pos = [@pos - 1, 0].max }
|
|
34
|
-
when :right
|
|
35
|
-
edit { @pos = [@pos + 1, @graphemes.length].min }
|
|
36
|
-
when :home
|
|
37
|
-
edit { @pos = 0 }
|
|
38
|
-
when :end
|
|
39
|
-
edit { @pos = @graphemes.length }
|
|
40
|
-
when String
|
|
41
|
-
edit { insert(key) if printable?(key) }
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def handle_mouse(event)
|
|
46
|
-
return nil unless event.action == :press && @text_row == event.row
|
|
47
|
-
|
|
48
|
-
edit { @pos = index_at(event.col - @text_col) }
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def draw(canvas, size)
|
|
52
|
-
inner = [MIN_INNER, @label.width + 1 + DisplayText.new(value).width].max
|
|
53
|
-
rect, col = panel(canvas, inner: inner, body_rows: 1)
|
|
54
|
-
|
|
55
|
-
canvas.text(rect.row + 1, col, @label, theme.title)
|
|
56
|
-
@text_row = rect.row + 1
|
|
57
|
-
@text_col = col + @label.width + 1
|
|
58
|
-
canvas.text(@text_row, @text_col, value, theme.text)
|
|
59
|
-
draw_cursor(canvas, @text_row, @text_col)
|
|
60
|
-
canvas
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
def edit
|
|
66
|
-
yield
|
|
67
|
-
nil
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Grapheme index whose left edge is closest to `rel` columns into the value.
|
|
71
|
-
def index_at(rel)
|
|
72
|
-
return 0 if rel <= 0
|
|
73
|
-
|
|
74
|
-
width = 0
|
|
75
|
-
@graphemes.each_with_index do |grapheme, i|
|
|
76
|
-
w = DisplayText.new(grapheme).width
|
|
77
|
-
return i if rel < width + ((w + 1) / 2)
|
|
78
|
-
|
|
79
|
-
width += w
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
@graphemes.length
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def draw_cursor(canvas, row, text_col)
|
|
86
|
-
cursor_col = text_col + DisplayText.new(@graphemes[0...@pos].join).width
|
|
87
|
-
canvas.text(row, cursor_col, @graphemes[@pos] || " ", theme.cursor)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def insert(string)
|
|
91
|
-
head = @graphemes[0...@pos].join
|
|
92
|
-
@graphemes = (head + string + @graphemes[@pos..].join).grapheme_clusters
|
|
93
|
-
@pos = (head + string).grapheme_clusters.length
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def delete_back
|
|
97
|
-
return if @pos.zero?
|
|
98
|
-
|
|
99
|
-
@graphemes.delete_at(@pos - 1)
|
|
100
|
-
@pos -= 1
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def delete_forward
|
|
104
|
-
@graphemes.delete_at(@pos) if @pos < @graphemes.length
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def printable?(string)
|
|
108
|
-
string.bytes.all? { |byte| byte >= 0x20 && byte != 0x7F }
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
data/lib/tui_tui/scroll_list.rb
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module TuiTui
|
|
4
|
-
# Cursor and viewport arithmetic shared by list-like widgets.
|
|
5
|
-
class ScrollList
|
|
6
|
-
attr_reader :cursor, :top
|
|
7
|
-
|
|
8
|
-
def initialize(count = 0)
|
|
9
|
-
@count = count
|
|
10
|
-
@cursor = 0
|
|
11
|
-
@top = 0
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def count=(value)
|
|
15
|
-
@count = [value, 0].max
|
|
16
|
-
@cursor = @cursor.clamp(0, last)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
attr_reader :count
|
|
20
|
-
|
|
21
|
-
def empty? = @count.zero?
|
|
22
|
-
def last = [@count - 1, 0].max
|
|
23
|
-
def at_end? = @cursor == last
|
|
24
|
-
|
|
25
|
-
def move(delta) = go_to(@cursor + delta)
|
|
26
|
-
def page(height) = move(height)
|
|
27
|
-
def to_top = go_to(0)
|
|
28
|
-
def to_end = go_to(last)
|
|
29
|
-
|
|
30
|
-
def go_to(index)
|
|
31
|
-
@cursor = index.clamp(0, last)
|
|
32
|
-
self
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def ensure_visible(height)
|
|
36
|
-
return self if height <= 0
|
|
37
|
-
|
|
38
|
-
@top = @cursor if @cursor < @top
|
|
39
|
-
@top = @cursor - height + 1 if @cursor >= @top + height
|
|
40
|
-
@top = 0 if @top.negative?
|
|
41
|
-
self
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def each_visible(height)
|
|
45
|
-
return enum_for(:each_visible, height) unless block_given?
|
|
46
|
-
|
|
47
|
-
height.times do |offset|
|
|
48
|
-
index = @top + offset
|
|
49
|
-
break if index >= @count
|
|
50
|
-
|
|
51
|
-
yield index, offset
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
self
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
data/lib/tui_tui/scrollbar.rb
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "rect"
|
|
4
|
-
require_relative "style"
|
|
5
|
-
require_relative "theme"
|
|
6
|
-
|
|
7
|
-
module TuiTui
|
|
8
|
-
# A vertical scroll indicator for a 1-column gutter.
|
|
9
|
-
# It sizes a thumb from top/visible/total and draws ASCII-only chrome.
|
|
10
|
-
module Scrollbar
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
TRACK = Theme::DEFAULT.scroll_track
|
|
14
|
-
THUMB = Theme::DEFAULT.scroll_thumb
|
|
15
|
-
|
|
16
|
-
def draw(canvas, rect, top:, visible:, total:, track: nil, thumb: " ", track_style: TRACK, thumb_style: THUMB)
|
|
17
|
-
return canvas if rect.rows <= 0
|
|
18
|
-
|
|
19
|
-
track ||= canvas.chrome.track
|
|
20
|
-
length, offset = geometry(rect.rows, top, visible, total)
|
|
21
|
-
rect.rows.times do |i|
|
|
22
|
-
in_thumb = i >= offset && i < offset + length
|
|
23
|
-
canvas.text(rect.row + i, rect.col, in_thumb ? thumb : track, in_thumb ? thumb_style : track_style)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
canvas
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# The thumb's [length, offset] in rows for a `height`-row track. Returns
|
|
30
|
-
# [0, 0] (no thumb, track only) when everything fits — nothing to scroll.
|
|
31
|
-
def geometry(height, top, visible, total)
|
|
32
|
-
visible = [visible, 1].max
|
|
33
|
-
total = [total, visible].max
|
|
34
|
-
return [0, 0] if total <= visible
|
|
35
|
-
|
|
36
|
-
length = [(height * visible / total.to_f).round, 1].max.clamp(1, height)
|
|
37
|
-
offset = ((height - length) * top.to_f / (total - visible)).round
|
|
38
|
-
[length, offset.clamp(0, height - length)]
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
data/lib/tui_tui/select.rb
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "display_text"
|
|
4
|
-
require_relative "style"
|
|
5
|
-
require_relative "scroll_list"
|
|
6
|
-
require_relative "list"
|
|
7
|
-
require_relative "line"
|
|
8
|
-
require_relative "span"
|
|
9
|
-
require_relative "rect"
|
|
10
|
-
require_relative "modal"
|
|
11
|
-
require_relative "key_intent"
|
|
12
|
-
|
|
13
|
-
module TuiTui
|
|
14
|
-
# Scrollable list picker modal.
|
|
15
|
-
class Select < Modal
|
|
16
|
-
MAX_ROWS = 12
|
|
17
|
-
MIN_INNER = 16
|
|
18
|
-
WHEEL = 3
|
|
19
|
-
|
|
20
|
-
def initialize(title, items, default: 0, theme: Theme::DEFAULT)
|
|
21
|
-
@title = DisplayText.new(title)
|
|
22
|
-
@items = items.map { |item| DisplayText.new(item) }
|
|
23
|
-
@list = ScrollList.new(@items.size)
|
|
24
|
-
@list.go_to(default)
|
|
25
|
-
@theme = theme
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def cursor = @list.cursor
|
|
29
|
-
|
|
30
|
-
def handle(key)
|
|
31
|
-
case KeyIntent.for(key)
|
|
32
|
-
when :up
|
|
33
|
-
nudge(-1)
|
|
34
|
-
when :down
|
|
35
|
-
nudge(1)
|
|
36
|
-
when :top
|
|
37
|
-
nudge_to(0)
|
|
38
|
-
when :bottom
|
|
39
|
-
nudge_to(@list.last)
|
|
40
|
-
when :cancel
|
|
41
|
-
:cancel
|
|
42
|
-
else
|
|
43
|
-
@list.cursor if ["\r", " "].include?(key)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Wheel moves the highlighted item; a click on an item picks it. Returns the
|
|
48
|
-
# chosen index on a click, otherwise nil (stay open).
|
|
49
|
-
def handle_mouse(event)
|
|
50
|
-
case event.action
|
|
51
|
-
when :wheel
|
|
52
|
-
nudge(event.button == :wheel_up ? -WHEEL : WHEEL)
|
|
53
|
-
when :press
|
|
54
|
-
click(event)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def draw(canvas, size)
|
|
59
|
-
rows = visible_rows(size)
|
|
60
|
-
inner = [MIN_INNER, @title.width, *@items.map(&:width)].max
|
|
61
|
-
rect, text_col = panel(canvas, inner: inner, body_rows: rows + 2)
|
|
62
|
-
|
|
63
|
-
canvas.text(rect.row + 1, text_col, @title.truncate(inner), theme.title)
|
|
64
|
-
draw_items(canvas, rect.row + 3, text_col, inner, rows)
|
|
65
|
-
canvas
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
private
|
|
69
|
-
|
|
70
|
-
def nudge(delta)
|
|
71
|
-
@list.move(delta)
|
|
72
|
-
nil
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def nudge_to(index)
|
|
76
|
-
@list.go_to(index)
|
|
77
|
-
nil
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def visible_rows(size)
|
|
81
|
-
room = [size.rows - 4, 1].max
|
|
82
|
-
[@items.size, MAX_ROWS, room].min
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def draw_items(canvas, row, col, inner, rows)
|
|
86
|
-
@items_rect = Rect.new(row: row, col: col, rows: rows, cols: inner)
|
|
87
|
-
List.new(@list).draw(canvas, @items_rect) do |index, focused|
|
|
88
|
-
Line[Span[@items[index].to_s, focused ? theme.selection : theme.text]]
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# The item under a click, picked, or nil if the click missed the list.
|
|
93
|
-
def click(event)
|
|
94
|
-
rect = @items_rect
|
|
95
|
-
return nil unless rect && event.col.between?(rect.col, rect.col + rect.cols - 1)
|
|
96
|
-
|
|
97
|
-
index = @list.top + (event.row - rect.row)
|
|
98
|
-
return nil unless (event.row - rect.row).between?(0, rect.rows - 1) && !@list.empty? && index <= @list.last
|
|
99
|
-
|
|
100
|
-
@list.go_to(index)
|
|
101
|
-
@list.cursor
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
data/lib/tui_tui/status_bar.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "display_text"
|
|
4
|
-
|
|
5
|
-
module TuiTui
|
|
6
|
-
# A one-row status/footer bar.
|
|
7
|
-
# It draws left text from the start and optional right text flush right.
|
|
8
|
-
module StatusBar
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
def draw(canvas, rect, left: "", right: nil, style: nil)
|
|
12
|
-
canvas.fill(rect, style)
|
|
13
|
-
|
|
14
|
-
right_width = right ? DisplayText.new(right).width : 0
|
|
15
|
-
fits_right = right && right_width < rect.cols
|
|
16
|
-
left_max = fits_right ? rect.cols - right_width : rect.cols
|
|
17
|
-
|
|
18
|
-
canvas.text(rect.row, rect.col, DisplayText.new(left).truncate(left_max), style)
|
|
19
|
-
canvas.text(rect.row, rect.col + rect.cols - right_width, right, style) if fits_right
|
|
20
|
-
canvas
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
data/lib/tui_tui/text_view.rb
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "line"
|
|
4
|
-
require_relative "span"
|
|
5
|
-
require_relative "scrollbar"
|
|
6
|
-
|
|
7
|
-
module TuiTui
|
|
8
|
-
# A scrolled read-only text window.
|
|
9
|
-
# Lines may be Strings, Lines, or Span arrays, supplied eagerly or lazily.
|
|
10
|
-
module TextView
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
def draw(canvas, rect, lines = nil, top: 0, style: nil, scrollbar: nil, total: nil, auto: false)
|
|
14
|
-
# With auto:, reserve the gutter only when the content overflows the rect.
|
|
15
|
-
# When the total is unknown (lazy block, no total:), the bar is kept.
|
|
16
|
-
count = total || lines&.length
|
|
17
|
-
show_bar = scrollbar && !(auto && count && count <= rect.rows)
|
|
18
|
-
body, gutter = show_bar ? rect.split_gutter : [rect, nil]
|
|
19
|
-
body.rows.times do |offset|
|
|
20
|
-
index = top + offset
|
|
21
|
-
content = lines ? lines[index] : yield(index)
|
|
22
|
-
next if content.nil?
|
|
23
|
-
|
|
24
|
-
canvas.line(body.row + offset, body.col, as_line(content, style).truncate(body.cols))
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
draw_scrollbar(canvas, gutter, top, total || lines&.length, body.rows, scrollbar) if gutter
|
|
28
|
-
canvas
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def draw_scrollbar(canvas, gutter, top, total, visible, theme)
|
|
32
|
-
return unless total
|
|
33
|
-
|
|
34
|
-
Scrollbar.draw(
|
|
35
|
-
canvas,
|
|
36
|
-
gutter,
|
|
37
|
-
top: top,
|
|
38
|
-
visible: visible,
|
|
39
|
-
total: total,
|
|
40
|
-
track_style: theme.scroll_track,
|
|
41
|
-
thumb_style: theme.scroll_thumb
|
|
42
|
-
)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def as_line(content, style)
|
|
46
|
-
case content
|
|
47
|
-
when Line
|
|
48
|
-
content
|
|
49
|
-
when Array
|
|
50
|
-
Line.new(content)
|
|
51
|
-
else
|
|
52
|
-
Line[Span[content.to_s, style]]
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
data/lib/tui_tui/toast.rb
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "display_text"
|
|
4
|
-
require_relative "style"
|
|
5
|
-
|
|
6
|
-
module TuiTui
|
|
7
|
-
# A transient notification overlay.
|
|
8
|
-
# The clock is injectable so expiry is testable without sleeping.
|
|
9
|
-
class Toast
|
|
10
|
-
DEFAULT_SECONDS = 2.0
|
|
11
|
-
DEFAULT_STYLE = Style.new(attrs: [:reverse])
|
|
12
|
-
DEFAULT_POSITION = :bottom_center
|
|
13
|
-
POSITIONS = {
|
|
14
|
-
top_left: [:top, :left],
|
|
15
|
-
top_center: [:top, :center],
|
|
16
|
-
top_right: [:top, :right],
|
|
17
|
-
middle_left: [:middle, :left],
|
|
18
|
-
middle_center: [:middle, :center],
|
|
19
|
-
middle_right: [:middle, :right],
|
|
20
|
-
bottom_left: [:bottom, :left],
|
|
21
|
-
bottom_center: [:bottom, :center],
|
|
22
|
-
bottom_right: [:bottom, :right],
|
|
23
|
-
center: [:middle, :center]
|
|
24
|
-
}.freeze
|
|
25
|
-
MONOTONIC = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
26
|
-
|
|
27
|
-
def initialize(message, seconds: DEFAULT_SECONDS, position: DEFAULT_POSITION, clock: MONOTONIC)
|
|
28
|
-
@message = DisplayText.new(message)
|
|
29
|
-
@position = position
|
|
30
|
-
@clock = clock
|
|
31
|
-
@expires_at = clock.call + seconds
|
|
32
|
-
validate_position!
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def expired? = @clock.call >= @expires_at
|
|
36
|
-
|
|
37
|
-
def draw(canvas, size, style: DEFAULT_STYLE, position: @position)
|
|
38
|
-
return canvas if expired?
|
|
39
|
-
|
|
40
|
-
label = DisplayText.new(" #{@message} ").truncate(size.cols)
|
|
41
|
-
vertical, horizontal = position_parts(position)
|
|
42
|
-
row = row_for(size, vertical)
|
|
43
|
-
col = col_for(size, label.width, horizontal)
|
|
44
|
-
canvas.text(row, col, label, style)
|
|
45
|
-
canvas
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def validate_position!
|
|
51
|
-
position_parts(@position)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def position_parts(position)
|
|
55
|
-
POSITIONS.fetch(position) do
|
|
56
|
-
raise ArgumentError, "unknown toast position: #{position.inspect}"
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def row_for(size, vertical)
|
|
61
|
-
case vertical
|
|
62
|
-
when :top
|
|
63
|
-
1
|
|
64
|
-
when :middle
|
|
65
|
-
((size.rows + 1) / 2).clamp(1, size.rows)
|
|
66
|
-
when :bottom
|
|
67
|
-
[size.rows - 2, 1].max
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def col_for(size, width, horizontal)
|
|
72
|
-
case horizontal
|
|
73
|
-
when :left
|
|
74
|
-
1
|
|
75
|
-
when :center
|
|
76
|
-
[((size.cols - width) / 2) + 1, 1].max
|
|
77
|
-
when :right
|
|
78
|
-
[size.cols - width + 1, 1].max
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
File without changes
|