terminal_rb 0.20.0 → 1.0.4

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.
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminal
4
+ module Ansi
5
+ # A scrollable text viewer for the terminal.
6
+ #
7
+ # Renders word-wrapped content and supports paging, line-by-line
8
+ # scrolling, and terminal resize. Intended for use with the alternate
9
+ # screen buffer.
10
+ #
11
+ # @example Basic pager
12
+ # viewer = Terminal::Ansi::ScreenViewer.new(lines, footer_rows: 1)
13
+ # Terminal.show_alt_screen
14
+ # viewer.draw
15
+ # Terminal.on_key_event do |event|
16
+ # case event.key
17
+ # when :Up then viewer.up
18
+ # when :Down then viewer.down
19
+ # when :PageUp then viewer.page_up
20
+ # when :PageDown then viewer.page_down
21
+ # when 'q' then break
22
+ # end
23
+ # end
24
+ # Terminal.hide_alt_screen
25
+ #
26
+ class ScreenViewer
27
+ # Total number of terminal rows.
28
+ #
29
+ # @return [Integer]
30
+ attr_reader :rows
31
+
32
+ # @attribute [w] rows
33
+ def rows=(value)
34
+ resize(value, @columns)
35
+ end
36
+
37
+ # Total number of terminal columns.
38
+ #
39
+ # @return [Integer]
40
+ attr_reader :columns
41
+
42
+ # @attribute [w] columns
43
+ def columns=(value)
44
+ resize(@rows, value)
45
+ end
46
+
47
+ # Number of rows available for content (rows minus footer).
48
+ #
49
+ # @return [Integer]
50
+ attr_reader :rows_visible
51
+
52
+ # Total number of lines.
53
+ #
54
+ # @attribute [r] line_count
55
+ # @return [Integer, nil] +nil+ when not already calculated
56
+ def line_count = @buf&.size
57
+
58
+ # Index of the topmost visible line.
59
+ #
60
+ # @return [Integer]
61
+ attr_reader :line_top
62
+
63
+ # Index of the line just below the last visible line.
64
+ #
65
+ # @attribute [r] line_bottom
66
+ # @return [Integer]
67
+ def line_bottom = (@line_top + @rows_visible if @line_top)
68
+
69
+ # Resize to the current terminal dimensions and redraw.
70
+ #
71
+ # @return [self, nil] +nil+ when resize is not required
72
+ def resize_to_screen = _resize(*Terminal.size)
73
+
74
+ # Resize to specific dimensions (clamped to terminal bounds)
75
+ # and redraw.
76
+ #
77
+ # @param rows [Integer] new row count
78
+ # @param columns [Integer] new column count
79
+ # @return [self, nil] +nil+ when resize is not required
80
+ def resize(rows, columns)
81
+ _resize(
82
+ rows.clamp(1, Terminal.rows),
83
+ columns.clamp(1, Terminal.columns)
84
+ )
85
+ end
86
+
87
+ # Render the current viewport to the terminal.
88
+ #
89
+ # @return [self]
90
+ def draw
91
+ recalculate unless @buf
92
+ if @buf.empty?
93
+ Terminal.raw_write(SCREEN_ERASE)
94
+ return self
95
+ end
96
+ Terminal.raw_write(CURSOR_HOME)
97
+ _draw(@line_top, @rows_visible)
98
+ end
99
+
100
+ # Scroll to the beginning of the content.
101
+ #
102
+ # @return [self, nil] +nil+ when already on begin
103
+ def begin
104
+ return draw unless @buf
105
+ return if @line_top == 0
106
+ @line_top = 0
107
+ draw
108
+ end
109
+
110
+ # Scroll to the end of the content.
111
+ #
112
+ # @return [self, nil] +nil+ when already on end
113
+ def end
114
+ return draw unless @buf
115
+ idx = @buf.size - @rows_visible
116
+ return if idx < 0 || @line_top == idx
117
+ @line_top = idx
118
+ draw
119
+ end
120
+
121
+ # Scroll up by the given number of lines.
122
+ #
123
+ # @param count [Integer] number of lines to scroll
124
+ # @return (see #begin)
125
+ def up(count = 1)
126
+ return draw unless @buf
127
+ return if @line_top == 0
128
+ if (nidx = @line_top - count) < 0
129
+ count = @line_top
130
+ @line_top = 0
131
+ else
132
+ @line_top = nidx
133
+ end
134
+ Terminal.raw_write(Ansi.screen_scroll_down(count))
135
+ Terminal.raw_write(CURSOR_HOME)
136
+ _draw(@line_top, count)
137
+ end
138
+
139
+ # Scroll down by the given number of lines.
140
+ #
141
+ # @param count [Integer] number of lines to scroll
142
+ # @return (see #end)
143
+ def down(count = 1)
144
+ return draw unless @buf
145
+ lidx = @line_top + @rows_visible
146
+ return if (count = [count, @buf.size - lidx].min) <= 0
147
+ @line_top += count
148
+ Terminal.raw_write(Ansi.screen_scroll_up(count))
149
+ Terminal.raw_write(Ansi.cursor_pos(@rows_visible - count + 1))
150
+ _draw(lidx, count)
151
+ end
152
+
153
+ # Scroll up by one full page.
154
+ #
155
+ # @return (see #begin)
156
+ def page_up = up(@rows_visible)
157
+
158
+ # Scroll up by half a page.
159
+ #
160
+ # @return (see #begin)
161
+ def half_up = up(@rows_visible / 2)
162
+
163
+ # Scroll down by one full page.
164
+ #
165
+ # @return (see #end)
166
+ def page_down = down(@rows_visible)
167
+
168
+ # Scroll down by half a page.
169
+ #
170
+ # @return (see #end)
171
+ def half_down = down(@rows_visible / 2)
172
+
173
+ # Create a new screen viewer.
174
+ #
175
+ # @param lines [Array<#to_s>] content lines to display
176
+ # @param footer_rows [Integer] number of rows reserved at the bottom
177
+ # (not scrolled with content)
178
+ # @param bbcode [true, false] whether to process BBCode markup
179
+ def initialize(lines, footer_rows: 0, bbcode: true)
180
+ @footer_rows = footer_rows.to_i
181
+ @rows, @columns = Terminal.size
182
+ @rows_visible = @rows - @footer_rows
183
+ @fmt =
184
+ Text::Formatter.new(
185
+ *lines,
186
+ bbcode: bbcode,
187
+ ansi: true,
188
+ eol: true,
189
+ spaces: true
190
+ )
191
+ end
192
+
193
+ private
194
+
195
+ def _resize(rows, columns)
196
+ return if @rows == rows && @columns == columns
197
+ @rows = rows
198
+ @columns = columns
199
+ @rows_visible = rows - @footer_rows
200
+ @buf = nil
201
+ draw
202
+ end
203
+
204
+ def _draw(idx, count)
205
+ idx -= 1
206
+ (count - 1).times do
207
+ line = @buf[idx += 1] and Terminal.raw_write(line)
208
+ Terminal.raw_write(NEXT_LINE)
209
+ end
210
+ line = @buf[idx + 1] and Terminal.raw_write(line)
211
+ Terminal.raw_write(LINE_ERASE_TO_END)
212
+ self
213
+ end
214
+
215
+ NEXT_LINE = -"#{LINE_ERASE_TO_END}#{CURSOR_NEXT_LINE}"
216
+ private_constant :NEXT_LINE
217
+
218
+ def recalculate
219
+ @buf = @fmt.lines(width: @columns)
220
+ @line_top = 0
221
+ end
222
+ end
223
+ end
224
+ end