thaum 0.1.0 → 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 +4 -4
- data/README.md +106 -14
- data/examples/checkbox.rb +89 -0
- data/examples/counter.rb +50 -0
- data/examples/hello_world.rb +28 -0
- data/examples/layout_demo.rb +138 -0
- data/examples/modal.rb +76 -0
- data/examples/mouse.rb +60 -0
- data/examples/octagram_picker.rb +224 -0
- data/examples/picker.rb +150 -0
- data/examples/progress_bar.rb +90 -0
- data/examples/scroll_view.rb +64 -0
- data/examples/select.rb +64 -0
- data/examples/spinner.rb +66 -0
- data/examples/status_bar.rb +65 -0
- data/examples/stopwatch.rb +84 -0
- data/examples/table.rb +196 -0
- data/examples/tabs.rb +112 -0
- data/examples/text.rb +101 -0
- data/examples/theme_picker.rb +95 -0
- data/examples/todo.rb +242 -0
- data/lib/thaum/action.rb +30 -0
- data/lib/thaum/app.rb +87 -0
- data/lib/thaum/color.rb +97 -0
- data/lib/thaum/concerns/context_update.rb +40 -0
- data/lib/thaum/concerns/focus.rb +53 -0
- data/lib/thaum/concerns/layout.rb +349 -0
- data/lib/thaum/concerns/modal.rb +102 -0
- data/lib/thaum/concerns/tab_navigation.rb +97 -0
- data/lib/thaum/dispatch.rb +149 -0
- data/lib/thaum/escape_parser.rb +265 -0
- data/lib/thaum/event.rb +13 -0
- data/lib/thaum/events.rb +28 -0
- data/lib/thaum/hit_test.rb +28 -0
- data/lib/thaum/input_reader.rb +46 -0
- data/lib/thaum/key_event.rb +13 -0
- data/lib/thaum/keys.rb +55 -0
- data/lib/thaum/minitest.rb +64 -0
- data/lib/thaum/octagram.rb +76 -0
- data/lib/thaum/painter.rb +49 -0
- data/lib/thaum/rect.rb +5 -0
- data/lib/thaum/rendering/box_drawing.rb +186 -0
- data/lib/thaum/rendering/buffer.rb +84 -0
- data/lib/thaum/rendering/canvas.rb +219 -0
- data/lib/thaum/rendering/cell.rb +11 -0
- data/lib/thaum/rendering/renderer.rb +98 -0
- data/lib/thaum/rendering/style.rb +13 -0
- data/lib/thaum/run_loop.rb +182 -0
- data/lib/thaum/seq.rb +91 -0
- data/lib/thaum/sigil.rb +41 -0
- data/lib/thaum/sigils/button.rb +47 -0
- data/lib/thaum/sigils/checkbox.rb +57 -0
- data/lib/thaum/sigils/progress_bar.rb +65 -0
- data/lib/thaum/sigils/scroll_view.rb +115 -0
- data/lib/thaum/sigils/select.rb +56 -0
- data/lib/thaum/sigils/spinner.rb +39 -0
- data/lib/thaum/sigils/status_bar.rb +89 -0
- data/lib/thaum/sigils/table.rb +156 -0
- data/lib/thaum/sigils/tabs.rb +59 -0
- data/lib/thaum/sigils/text.rb +22 -0
- data/lib/thaum/sigils/text_input.rb +86 -0
- data/lib/thaum/terminal.rb +46 -0
- data/lib/thaum/themes.rb +267 -0
- data/lib/thaum/tree.rb +16 -0
- data/lib/thaum/version.rb +1 -1
- data/lib/thaum.rb +64 -1
- metadata +114 -4
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thaum
|
|
4
|
+
# Horizontal bar of labeled segments separated by a configurable
|
|
5
|
+
# delimiter. Segments are either plain Strings (decorative) or Hashes
|
|
6
|
+
# `{ label:, on_click: }` whose click handler fires when the user
|
|
7
|
+
# left-presses inside the segment's column range. Non-focusable —
|
|
8
|
+
# status bars sit at the bottom of the focus order. Mouse events the
|
|
9
|
+
# bar does not act on are eaten (not propagated) so the bar acts as
|
|
10
|
+
# the floor of the click target.
|
|
11
|
+
class StatusBar
|
|
12
|
+
include Sigil
|
|
13
|
+
|
|
14
|
+
DEFAULT_SEPARATOR = " │ "
|
|
15
|
+
|
|
16
|
+
attr_reader :separator, :segments
|
|
17
|
+
|
|
18
|
+
def initialize(segments:, separator: DEFAULT_SEPARATOR)
|
|
19
|
+
@segments = segments
|
|
20
|
+
@separator = separator
|
|
21
|
+
@ranges = [] # parallel array: [start_col, end_col_exclusive] per segment
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def focusable? = false
|
|
25
|
+
|
|
26
|
+
def segments=(value)
|
|
27
|
+
@segments = value
|
|
28
|
+
request_render
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def on_mouse(event)
|
|
32
|
+
return unless event.action == :press && event.button == :left
|
|
33
|
+
|
|
34
|
+
seg = segment_at(event.x) or return
|
|
35
|
+
handler = on_click(seg) or return
|
|
36
|
+
if handler.arity.zero?
|
|
37
|
+
handler.call
|
|
38
|
+
else
|
|
39
|
+
handler.call(event)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def render(canvas:, theme:)
|
|
44
|
+
canvas.fill(bg: theme.bar_bg)
|
|
45
|
+
@ranges = []
|
|
46
|
+
x = 0
|
|
47
|
+
width = canvas.width
|
|
48
|
+
@segments.each_with_index do |seg, idx|
|
|
49
|
+
x += draw_separator(canvas: canvas, theme: theme, x: x, width: width) if idx.positive?
|
|
50
|
+
break if x >= width
|
|
51
|
+
|
|
52
|
+
label = label(seg)
|
|
53
|
+
label_w = label.display_width
|
|
54
|
+
start = x
|
|
55
|
+
canvas.text(content: label, x: x, fg: theme.fg, bg: theme.bar_bg)
|
|
56
|
+
x += label_w
|
|
57
|
+
finish = [x, width].min
|
|
58
|
+
@ranges << [start, finish]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def draw_separator(canvas:, theme:, x:, width:)
|
|
65
|
+
return 0 if x >= width
|
|
66
|
+
|
|
67
|
+
canvas.text(content: @separator, x: x, fg: theme.dim, bg: theme.bar_bg)
|
|
68
|
+
@separator.display_width
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def label(seg)
|
|
72
|
+
seg.is_a?(Hash) ? seg.fetch(:label).to_s : seg.to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def on_click(seg)
|
|
76
|
+
return nil unless seg.is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
seg[:on_click]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def segment_at(col)
|
|
82
|
+
@segments.each_with_index do |seg, idx|
|
|
83
|
+
range = @ranges[idx] or next
|
|
84
|
+
return seg if col >= range[0] && col < range[1]
|
|
85
|
+
end
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thaum
|
|
4
|
+
class Table
|
|
5
|
+
include Sigil
|
|
6
|
+
|
|
7
|
+
SelectedEvent = Thaum::Event.define(:index, :row)
|
|
8
|
+
|
|
9
|
+
PAGE_STEP = 10
|
|
10
|
+
|
|
11
|
+
attr_reader :headers, :rows, :widths, :cursor, :offset
|
|
12
|
+
|
|
13
|
+
def initialize(headers:, rows:, widths: nil)
|
|
14
|
+
@headers = headers
|
|
15
|
+
@rows = rows
|
|
16
|
+
@widths = widths
|
|
17
|
+
@cursor = 0
|
|
18
|
+
@offset = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on_key(event)
|
|
22
|
+
case event.key
|
|
23
|
+
when :up then @cursor = [@cursor - 1, 0].max
|
|
24
|
+
when :down then @cursor = [@cursor + 1, rows.length - 1].min if rows.any?
|
|
25
|
+
when :home then @cursor = 0
|
|
26
|
+
when :end then @cursor = rows.length - 1 if rows.any?
|
|
27
|
+
when :page_up then @cursor = [@cursor - PAGE_STEP, 0].max
|
|
28
|
+
when :page_down then @cursor = [@cursor + PAGE_STEP, rows.length - 1].min if rows.any?
|
|
29
|
+
when :enter then emit_selected
|
|
30
|
+
else emit event
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render(canvas:, theme:)
|
|
35
|
+
canvas.fill(bg: theme.bg)
|
|
36
|
+
widths = effective_widths(canvas.width)
|
|
37
|
+
visible_offset(canvas)
|
|
38
|
+
|
|
39
|
+
render_header(canvas: canvas, theme: theme, widths: widths)
|
|
40
|
+
render_separator(canvas: canvas, theme: theme)
|
|
41
|
+
render_data_rows(canvas: canvas, theme: theme, widths: widths)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def emit_selected
|
|
47
|
+
return if rows.empty?
|
|
48
|
+
|
|
49
|
+
emit SelectedEvent.new(index: @cursor, row: rows[@cursor])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def render_header(canvas:, theme:, widths:)
|
|
53
|
+
row = canvas.row(0) or return
|
|
54
|
+
|
|
55
|
+
row.fill(bg: theme.bar_bg)
|
|
56
|
+
row.text(content: format_cells(cells: @headers, widths: widths), fg: theme.accent, bg: theme.bar_bg)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render_separator(canvas:, theme:)
|
|
60
|
+
row = canvas.row(1) or return
|
|
61
|
+
|
|
62
|
+
row.fill(bg: theme.bg)
|
|
63
|
+
row.text(content: "─" * canvas.width, fg: theme.border, bg: theme.bg)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render_data_rows(canvas:, theme:, widths:)
|
|
67
|
+
visible_rows = canvas.height - 2
|
|
68
|
+
return if visible_rows <= 0
|
|
69
|
+
|
|
70
|
+
visible_rows.times do |i|
|
|
71
|
+
file_idx = @offset + i
|
|
72
|
+
row_data = rows[file_idx] or break
|
|
73
|
+
row = canvas.row(i + 2) or break
|
|
74
|
+
|
|
75
|
+
selected = file_idx == @cursor
|
|
76
|
+
bg = selected ? theme.selection : theme.bg
|
|
77
|
+
fg = selected ? theme.selection_fg : theme.fg
|
|
78
|
+
row.fill(bg: bg)
|
|
79
|
+
row.text(content: format_cells(cells: row_data, widths: widths), fg: fg, bg: bg)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format_cells(cells:, widths:)
|
|
84
|
+
cells.each_with_index.map { |cell, idx| fit(str: cell.to_s, width: widths[idx] || 0) }.join(" ")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Pad with spaces or truncate to fit exactly `width` display columns.
|
|
88
|
+
def fit(str:, width:)
|
|
89
|
+
return "" if width <= 0
|
|
90
|
+
|
|
91
|
+
w = str.display_width
|
|
92
|
+
return str + (" " * (width - w)) if w <= width
|
|
93
|
+
|
|
94
|
+
truncate_to_width(str: str, width: width)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def truncate_to_width(str:, width:)
|
|
98
|
+
out = +""
|
|
99
|
+
cols = 0
|
|
100
|
+
str.each_char do |c|
|
|
101
|
+
cw = c.display_width
|
|
102
|
+
break if cols + cw > width
|
|
103
|
+
|
|
104
|
+
out << c
|
|
105
|
+
cols += cw
|
|
106
|
+
end
|
|
107
|
+
out << (" " * (width - cols)) if cols < width
|
|
108
|
+
out
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def visible_offset(canvas)
|
|
112
|
+
visible_rows = canvas.height - 2
|
|
113
|
+
return @offset = 0 if visible_rows <= 0
|
|
114
|
+
|
|
115
|
+
if @cursor < @offset
|
|
116
|
+
@offset = @cursor
|
|
117
|
+
elsif @cursor >= @offset + visible_rows
|
|
118
|
+
@offset = @cursor - visible_rows + 1
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
max_offset = [rows.length - visible_rows, 0].max
|
|
122
|
+
@offset = @offset.clamp(0, max_offset)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def effective_widths(canvas_width)
|
|
126
|
+
return widths if widths
|
|
127
|
+
|
|
128
|
+
col_count = headers.length
|
|
129
|
+
return [] if col_count.zero?
|
|
130
|
+
|
|
131
|
+
max_widths = column_max_widths(col_count)
|
|
132
|
+
separators = col_count - 1
|
|
133
|
+
total_max = max_widths.sum
|
|
134
|
+
budget = canvas_width - separators
|
|
135
|
+
|
|
136
|
+
return max_widths if total_max <= budget || budget <= 0
|
|
137
|
+
|
|
138
|
+
scale_widths(max_widths: max_widths, total_max: total_max, budget: budget)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def column_max_widths(col_count)
|
|
142
|
+
Array.new(col_count) do |idx|
|
|
143
|
+
header_w = (headers[idx] || "").to_s.display_width
|
|
144
|
+
row_w = rows.map { |r| (r[idx] || "").to_s.display_width }.max || 0
|
|
145
|
+
[header_w, row_w].max
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def scale_widths(max_widths:, total_max:, budget:)
|
|
150
|
+
scaled = max_widths.map { |w| (budget * w / total_max).floor }
|
|
151
|
+
leftover = budget - scaled.sum
|
|
152
|
+
scaled[-1] += leftover if scaled.any?
|
|
153
|
+
scaled
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thaum
|
|
4
|
+
# Horizontal tab strip. ←/→ cycle the active tab (with wrap). Emits
|
|
5
|
+
# Tabs::ActivatedEvent when the active index changes. The content under each
|
|
6
|
+
# tab is the App's concern — Tabs only owns the strip.
|
|
7
|
+
class Tabs
|
|
8
|
+
include Sigil
|
|
9
|
+
|
|
10
|
+
ActivatedEvent = Thaum::Event.define(:index, :label)
|
|
11
|
+
|
|
12
|
+
attr_reader :labels, :active
|
|
13
|
+
|
|
14
|
+
def initialize(labels:, active: 0)
|
|
15
|
+
raise ArgumentError, "Tabs needs at least one label" if labels.empty?
|
|
16
|
+
|
|
17
|
+
@labels = labels
|
|
18
|
+
@active = active.clamp(0, labels.length - 1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def current = @labels[@active]
|
|
22
|
+
|
|
23
|
+
def on_key(event)
|
|
24
|
+
case event.key
|
|
25
|
+
when :left then move(-1)
|
|
26
|
+
when :right then move(1)
|
|
27
|
+
else emit event
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render(canvas:, theme:)
|
|
32
|
+
canvas.fill(bg: theme.bar_bg)
|
|
33
|
+
x = 0
|
|
34
|
+
@labels.each_with_index do |label, idx|
|
|
35
|
+
x += draw_tab(canvas: canvas, label: label, idx: idx, x: x, theme: theme)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def move(delta)
|
|
42
|
+
new_idx = (@active + delta) % @labels.length
|
|
43
|
+
return if new_idx == @active
|
|
44
|
+
|
|
45
|
+
@active = new_idx
|
|
46
|
+
emit ActivatedEvent.new(index: @active, label: current)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def draw_tab(canvas:, label:, idx:, x:, theme:)
|
|
50
|
+
cell = " #{label} "
|
|
51
|
+
width = cell.display_width
|
|
52
|
+
bg = idx == @active ? theme.selection : theme.bar_bg
|
|
53
|
+
fg = idx == @active ? theme.selection_fg : theme.fg
|
|
54
|
+
canvas.fill(bg: bg, x: x, width: width)
|
|
55
|
+
canvas.text(content: cell, x: x, fg: fg, bg: bg)
|
|
56
|
+
width
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thaum
|
|
4
|
+
class Text
|
|
5
|
+
include Sigil
|
|
6
|
+
|
|
7
|
+
attr_accessor :content
|
|
8
|
+
|
|
9
|
+
def initialize(content:, align: :left, wrap: :none)
|
|
10
|
+
@content = content
|
|
11
|
+
@align = align
|
|
12
|
+
@wrap = wrap
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def focusable? = false
|
|
16
|
+
|
|
17
|
+
def render(canvas:, theme:)
|
|
18
|
+
resolved = content.respond_to?(:call) ? content.call : content
|
|
19
|
+
canvas.text(content: resolved.to_s, fg: theme.fg, align: @align, wrap: @wrap)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thaum
|
|
4
|
+
class TextInput
|
|
5
|
+
include Sigil
|
|
6
|
+
|
|
7
|
+
SubmittedEvent = Thaum::Event.define(:value)
|
|
8
|
+
|
|
9
|
+
attr_reader :value, :cursor
|
|
10
|
+
|
|
11
|
+
def initialize(value: "")
|
|
12
|
+
@value = value.dup
|
|
13
|
+
@cursor = @value.length
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def clear
|
|
17
|
+
@value = +""
|
|
18
|
+
@cursor = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on_key(event)
|
|
22
|
+
key = event.key
|
|
23
|
+
case key
|
|
24
|
+
when String then insert(key) unless event.ctrl? || event.alt?
|
|
25
|
+
when :backspace then backspace
|
|
26
|
+
when :delete then delete_forward
|
|
27
|
+
when :left then @cursor = [@cursor - 1, 0].max
|
|
28
|
+
when :right then @cursor = [@cursor + 1, @value.length].min
|
|
29
|
+
when :home then @cursor = 0
|
|
30
|
+
when :end then @cursor = @value.length
|
|
31
|
+
when :enter then emit SubmittedEvent.new(value: @value)
|
|
32
|
+
else emit event
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def render(canvas:, theme:)
|
|
37
|
+
offset = scroll_offset(canvas.width)
|
|
38
|
+
visible = @value[offset..] || ""
|
|
39
|
+
cursor_x = display_width(@value[offset...@cursor])
|
|
40
|
+
canvas.fill(bg: theme.input_bg)
|
|
41
|
+
canvas.text(content: visible, fg: theme.fg, bg: theme.input_bg)
|
|
42
|
+
canvas.cursor(x: cursor_x, y: 0) if focused?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def insert(char)
|
|
48
|
+
@value = @value[0...@cursor] + char + @value[@cursor..]
|
|
49
|
+
@cursor += char.length
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def backspace
|
|
53
|
+
return if @cursor.zero?
|
|
54
|
+
|
|
55
|
+
@value = @value[0...(@cursor - 1)] + @value[@cursor..]
|
|
56
|
+
@cursor -= 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def delete_forward
|
|
60
|
+
return if @cursor >= @value.length
|
|
61
|
+
|
|
62
|
+
@value = @value[0...@cursor] + @value[(@cursor + 1)..]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Keep the cursor inside the visible window, measured in display columns
|
|
66
|
+
# (not character indices) so wide chars (CJK, emoji) scroll correctly.
|
|
67
|
+
# When the prefix fits, no scroll. Otherwise drop leading chars until the
|
|
68
|
+
# cursor sits at column width-1.
|
|
69
|
+
def scroll_offset(width)
|
|
70
|
+
prefix_cols = display_width(@value[0...@cursor])
|
|
71
|
+
return 0 if prefix_cols < width
|
|
72
|
+
|
|
73
|
+
target = width - 1
|
|
74
|
+
chars = @value.chars
|
|
75
|
+
offset = 0
|
|
76
|
+
cols = prefix_cols
|
|
77
|
+
while cols > target && offset < @cursor
|
|
78
|
+
cols -= chars[offset].display_width
|
|
79
|
+
offset += 1
|
|
80
|
+
end
|
|
81
|
+
offset
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def display_width(str) = (str || "").display_width
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
|
|
5
|
+
module Thaum
|
|
6
|
+
# Manages terminal setup and teardown: alternate screen, cursor, mouse, and bracketed paste.
|
|
7
|
+
class Terminal
|
|
8
|
+
def initialize(input: $stdin, output: $stdout)
|
|
9
|
+
@input = input
|
|
10
|
+
@output = output
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def setup
|
|
14
|
+
@original_stty = stty_save
|
|
15
|
+
@input.raw!
|
|
16
|
+
write Seq::ALT_SCREEN_ON
|
|
17
|
+
write Seq::CURSOR_HIDE
|
|
18
|
+
write Seq::BRACKETED_PASTE_ON
|
|
19
|
+
write Seq::SGR_MOUSE_ON
|
|
20
|
+
write Seq::CELL_MOTION_ON
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def teardown
|
|
24
|
+
write Seq::CELL_MOTION_OFF
|
|
25
|
+
write Seq::SGR_MOUSE_OFF
|
|
26
|
+
write Seq::BRACKETED_PASTE_OFF
|
|
27
|
+
write Seq::CURSOR_SHOW
|
|
28
|
+
write Seq::ALT_SCREEN_OFF
|
|
29
|
+
stty_restore(@original_stty)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def size
|
|
33
|
+
rows, cols = @input.winsize
|
|
34
|
+
[cols, rows]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def write(str) = @output.write(str)
|
|
40
|
+
def stty_save = @input.respond_to?(:tty?) && @input.tty? ? `stty -g 2>/dev/null`.chomp : ""
|
|
41
|
+
|
|
42
|
+
def stty_restore(state)
|
|
43
|
+
system("stty", state) if state && !state.empty?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|