ratatat 1.0.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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +201 -0
  3. data/examples/log_tailer.rb +460 -0
  4. data/lib/ratatat/ansi_backend.rb +175 -0
  5. data/lib/ratatat/app.rb +342 -0
  6. data/lib/ratatat/binding.rb +74 -0
  7. data/lib/ratatat/buffer.rb +238 -0
  8. data/lib/ratatat/cell.rb +166 -0
  9. data/lib/ratatat/color.rb +191 -0
  10. data/lib/ratatat/css_parser.rb +192 -0
  11. data/lib/ratatat/dom_query.rb +124 -0
  12. data/lib/ratatat/driver.rb +200 -0
  13. data/lib/ratatat/input.rb +208 -0
  14. data/lib/ratatat/message.rb +147 -0
  15. data/lib/ratatat/reactive.rb +79 -0
  16. data/lib/ratatat/styles.rb +293 -0
  17. data/lib/ratatat/terminal.rb +168 -0
  18. data/lib/ratatat/version.rb +3 -0
  19. data/lib/ratatat/widget.rb +337 -0
  20. data/lib/ratatat/widgets/button.rb +43 -0
  21. data/lib/ratatat/widgets/checkbox.rb +68 -0
  22. data/lib/ratatat/widgets/container.rb +50 -0
  23. data/lib/ratatat/widgets/data_table.rb +123 -0
  24. data/lib/ratatat/widgets/grid.rb +40 -0
  25. data/lib/ratatat/widgets/horizontal.rb +52 -0
  26. data/lib/ratatat/widgets/log.rb +97 -0
  27. data/lib/ratatat/widgets/modal.rb +161 -0
  28. data/lib/ratatat/widgets/progress_bar.rb +80 -0
  29. data/lib/ratatat/widgets/radio_set.rb +91 -0
  30. data/lib/ratatat/widgets/scrollable_container.rb +88 -0
  31. data/lib/ratatat/widgets/select.rb +100 -0
  32. data/lib/ratatat/widgets/sparkline.rb +61 -0
  33. data/lib/ratatat/widgets/spinner.rb +93 -0
  34. data/lib/ratatat/widgets/static.rb +23 -0
  35. data/lib/ratatat/widgets/tabbed_content.rb +114 -0
  36. data/lib/ratatat/widgets/text_area.rb +183 -0
  37. data/lib/ratatat/widgets/text_input.rb +143 -0
  38. data/lib/ratatat/widgets/toast.rb +55 -0
  39. data/lib/ratatat/widgets/tooltip.rb +66 -0
  40. data/lib/ratatat/widgets/tree.rb +172 -0
  41. data/lib/ratatat/widgets/vertical.rb +52 -0
  42. data/lib/ratatat.rb +51 -0
  43. metadata +142 -0
@@ -0,0 +1,147 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Ratatat
7
+ # Base class for all messages in the event system.
8
+ # Messages flow up the widget tree (bubbling) unless stopped.
9
+ class Message
10
+ extend T::Sig
11
+
12
+ sig { returns(T.untyped) }
13
+ attr_reader :sender
14
+
15
+ sig { returns(Time) }
16
+ attr_reader :time
17
+
18
+ sig { returns(T::Boolean) }
19
+ attr_accessor :bubble
20
+
21
+ sig { params(sender: T.untyped, bubble: T::Boolean).void }
22
+ def initialize(sender:, bubble: true)
23
+ @sender = sender
24
+ @time = T.let(Time.now, Time)
25
+ @bubble = bubble
26
+ @stopped = T.let(false, T::Boolean)
27
+ @prevented = T.let(false, T::Boolean)
28
+ end
29
+
30
+ sig { void }
31
+ def stop
32
+ @stopped = true
33
+ end
34
+
35
+ sig { returns(T::Boolean) }
36
+ def stopped?
37
+ @stopped
38
+ end
39
+
40
+ sig { void }
41
+ def prevent_default
42
+ @prevented = true
43
+ end
44
+
45
+ sig { returns(T::Boolean) }
46
+ def prevented?
47
+ @prevented
48
+ end
49
+ end
50
+
51
+ # Keyboard input message
52
+ class Key < Message
53
+ extend T::Sig
54
+
55
+ sig { returns(T.any(Symbol, String)) }
56
+ attr_reader :key
57
+
58
+ sig { returns(T::Set[Symbol]) }
59
+ attr_reader :modifiers
60
+
61
+ sig { params(sender: T.untyped, key: T.any(Symbol, String), modifiers: T::Set[Symbol], bubble: T::Boolean).void }
62
+ def initialize(sender:, key:, modifiers: Set.new, bubble: true)
63
+ super(sender: sender, bubble: bubble)
64
+ @key = key
65
+ @modifiers = modifiers
66
+ end
67
+
68
+ sig { returns(T::Boolean) }
69
+ def ctrl? = @modifiers.include?(:ctrl)
70
+
71
+ sig { returns(T::Boolean) }
72
+ def alt? = @modifiers.include?(:alt)
73
+
74
+ sig { returns(T::Boolean) }
75
+ def shift? = @modifiers.include?(:shift)
76
+ end
77
+
78
+ # Terminal resize message
79
+ class Resize < Message
80
+ extend T::Sig
81
+
82
+ sig { returns(Integer) }
83
+ attr_reader :width, :height
84
+
85
+ sig { params(sender: T.untyped, width: Integer, height: Integer, bubble: T::Boolean).void }
86
+ def initialize(sender:, width:, height:, bubble: true)
87
+ super(sender: sender, bubble: bubble)
88
+ @width = width
89
+ @height = height
90
+ end
91
+ end
92
+
93
+ # Application quit message
94
+ class Quit < Message
95
+ extend T::Sig
96
+
97
+ sig { params(sender: T.untyped).void }
98
+ def initialize(sender:)
99
+ super(sender: sender, bubble: false)
100
+ end
101
+ end
102
+
103
+ # Widget gained focus
104
+ class Focus < Message
105
+ extend T::Sig
106
+
107
+ sig { params(sender: T.untyped).void }
108
+ def initialize(sender:)
109
+ super(sender: sender, bubble: false)
110
+ end
111
+ end
112
+
113
+ # Widget lost focus
114
+ class Blur < Message
115
+ extend T::Sig
116
+
117
+ sig { params(sender: T.untyped).void }
118
+ def initialize(sender:)
119
+ super(sender: sender, bubble: false)
120
+ end
121
+ end
122
+
123
+ # Worker namespace for background task messages
124
+ module Worker
125
+ # Worker completed message
126
+ class Done < Message
127
+ extend T::Sig
128
+
129
+ sig { returns(Symbol) }
130
+ attr_reader :name
131
+
132
+ sig { returns(T.untyped) }
133
+ attr_reader :result
134
+
135
+ sig { returns(T.nilable(Exception)) }
136
+ attr_reader :error
137
+
138
+ sig { params(sender: T.untyped, name: Symbol, result: T.untyped, error: T.nilable(Exception)).void }
139
+ def initialize(sender:, name:, result: nil, error: nil)
140
+ super(sender: sender, bubble: true)
141
+ @name = name
142
+ @result = result
143
+ @error = error
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,79 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Ratatat
7
+ # Provides the reactive property DSL for widgets
8
+ module Reactive
9
+ extend T::Sig
10
+
11
+ # Class methods added when Reactive is extended
12
+ module ClassMethods
13
+ extend T::Sig
14
+
15
+ # Define a reactive property
16
+ sig { params(name: Symbol, default: T.untyped, repaint: T::Boolean).void }
17
+ def reactive(name, default: nil, repaint: true)
18
+ # Store reactive metadata
19
+ @_reactives ||= {}
20
+ @_reactives[name] = { default: default, repaint: repaint }
21
+
22
+ # Define getter
23
+ define_method(name) do
24
+ ivar = :"@#{name}"
25
+ if instance_variable_defined?(ivar)
26
+ instance_variable_get(ivar)
27
+ else
28
+ self.class.reactive_default(name)
29
+ end
30
+ end
31
+
32
+ # Define setter
33
+ define_method(:"#{name}=") do |value|
34
+ ivar = :"@#{name}"
35
+ old_value = send(name)
36
+
37
+ # Run validator if defined
38
+ validator = :"validate_#{name}"
39
+ value = send(validator, value) if respond_to?(validator, true)
40
+
41
+ # Skip if unchanged
42
+ return if old_value == value
43
+
44
+ # Store new value
45
+ instance_variable_set(ivar, value)
46
+
47
+ # Call watcher if defined
48
+ watcher = :"watch_#{name}"
49
+ send(watcher, old_value, value) if respond_to?(watcher, true)
50
+
51
+ # Trigger repaint if configured
52
+ if self.class.reactive_repaint?(name) && respond_to?(:refresh, true)
53
+ send(:refresh)
54
+ end
55
+ end
56
+ end
57
+
58
+ sig { params(name: Symbol).returns(T.untyped) }
59
+ def reactive_default(name)
60
+ @_reactives&.dig(name, :default)
61
+ end
62
+
63
+ sig { params(name: Symbol).returns(T::Boolean) }
64
+ def reactive_repaint?(name)
65
+ @_reactives&.dig(name, :repaint) != false
66
+ end
67
+
68
+ sig { returns(T::Hash[Symbol, T::Hash[Symbol, T.untyped]]) }
69
+ def reactives
70
+ @_reactives || {}
71
+ end
72
+ end
73
+
74
+ sig { params(base: T::Class[T.anything]).void }
75
+ def self.extended(base)
76
+ base.extend(ClassMethods)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,293 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Ratatat
7
+ # Style properties for widgets
8
+ class Styles
9
+ extend T::Sig
10
+
11
+ PROPERTIES = T.let(%i[
12
+ foreground background
13
+ width height min_width max_width min_height max_height
14
+ padding margin
15
+ bold italic underline
16
+ border border_title
17
+ text_align
18
+ ].freeze, T::Array[Symbol])
19
+
20
+ sig { returns(T.nilable(T.any(Symbol, Color::Rgb, Color::Indexed))) }
21
+ attr_accessor :foreground, :background
22
+
23
+ sig { returns(T.nilable(Integer)) }
24
+ attr_accessor :width, :height, :min_width, :max_width, :min_height, :max_height
25
+
26
+ sig { returns(T.nilable(T::Array[Integer])) }
27
+ attr_accessor :padding, :margin
28
+
29
+ sig { returns(T.nilable(T::Boolean)) }
30
+ attr_accessor :bold, :italic, :underline
31
+
32
+ sig { returns(T.nilable(Symbol)) }
33
+ attr_accessor :border, :text_align
34
+
35
+ sig { returns(T.nilable(String)) }
36
+ attr_accessor :border_title
37
+
38
+ sig { params(props: T.untyped).void }
39
+ def initialize(**props)
40
+ props.each do |key, value|
41
+ send(:"#{key}=", value) if respond_to?(:"#{key}=")
42
+ end
43
+ end
44
+
45
+ sig { params(other: Styles).returns(Styles) }
46
+ def merge(other)
47
+ result = Styles.new
48
+ PROPERTIES.each do |prop|
49
+ my_value = send(prop)
50
+ other_value = other.send(prop)
51
+ result.send(:"#{prop}=", other_value.nil? ? my_value : other_value)
52
+ end
53
+ result
54
+ end
55
+
56
+ sig { returns(T::Hash[Symbol, T.untyped]) }
57
+ def to_h
58
+ PROPERTIES.each_with_object({}) do |prop, hash|
59
+ value = send(prop)
60
+ hash[prop] = value unless value.nil?
61
+ end
62
+ end
63
+ end
64
+
65
+ # Manages style rules and computes styles for widgets
66
+ class StyleSheet
67
+ extend T::Sig
68
+
69
+ Rule = T.type_alias { { selector: String, styles: Styles, specificity: Integer } }
70
+
71
+ sig { void }
72
+ def initialize
73
+ @rules = T.let([], T::Array[Rule])
74
+ end
75
+
76
+ sig { params(selector: String, props: T.untyped).void }
77
+ def add_rule(selector, **props)
78
+ @rules << {
79
+ selector: selector,
80
+ styles: Styles.new(**props),
81
+ specificity: compute_specificity(selector)
82
+ }
83
+ end
84
+
85
+ sig { params(widget: Widget).returns(Styles) }
86
+ def compute(widget)
87
+ matching = @rules.select { |rule| matches?(widget, rule[:selector]) }
88
+ sorted = matching.sort_by { |rule| rule[:specificity] }
89
+
90
+ result = Styles.new
91
+ sorted.each do |rule|
92
+ result = result.merge(rule[:styles])
93
+ end
94
+ result
95
+ end
96
+
97
+ private
98
+
99
+ sig { params(selector: String).returns(Integer) }
100
+ def compute_specificity(selector)
101
+ # Handle combinator selectors
102
+ parts = parse_combinator_parts(selector)
103
+ return parts.sum { |part| compute_simple_specificity(part[:selector]) }
104
+ end
105
+
106
+ sig { params(selector: String).returns(Integer) }
107
+ def compute_simple_specificity(selector)
108
+ # Strip :not() for specificity calculation
109
+ selector = selector.gsub(/:not\([^)]+\)/, "")
110
+
111
+ # Parse compound selector parts
112
+ parts = parse_compound_selector(selector)
113
+ specificity = 0
114
+
115
+ parts.each do |part|
116
+ if part.start_with?("#")
117
+ specificity += 100
118
+ elsif part.start_with?(".")
119
+ specificity += 10
120
+ else
121
+ specificity += 1
122
+ end
123
+ end
124
+
125
+ # Add pseudo-class specificity
126
+ specificity += 10 if selector.include?(":")
127
+ specificity
128
+ end
129
+
130
+ sig { params(selector: String).returns(T::Array[{ selector: String, combinator: T.nilable(Symbol) }]) }
131
+ def parse_combinator_parts(selector)
132
+ parts = T.let([], T::Array[{ selector: String, combinator: T.nilable(Symbol) }])
133
+
134
+ # Split by child combinator first
135
+ if selector.include?(" > ")
136
+ segments = selector.split(" > ")
137
+ segments.each_with_index do |seg, i|
138
+ # Each segment might have descendant selectors
139
+ if seg.include?(" ")
140
+ sub_parts = seg.strip.split(/\s+/)
141
+ sub_parts.each_with_index do |sub, j|
142
+ combinator = j < sub_parts.length - 1 ? :descendant : (i < segments.length - 1 ? :child : nil)
143
+ parts << { selector: sub, combinator: combinator }
144
+ end
145
+ else
146
+ parts << { selector: seg.strip, combinator: i < segments.length - 1 ? :child : nil }
147
+ end
148
+ end
149
+ elsif selector.include?(" ")
150
+ # Only descendant combinators
151
+ segments = selector.split(/\s+/)
152
+ segments.each_with_index do |seg, i|
153
+ parts << { selector: seg, combinator: i < segments.length - 1 ? :descendant : nil }
154
+ end
155
+ else
156
+ parts << { selector: selector, combinator: nil }
157
+ end
158
+
159
+ parts
160
+ end
161
+
162
+ sig { params(selector: String).returns(T::Array[String]) }
163
+ def parse_compound_selector(selector)
164
+ # Remove pseudo-classes for parsing
165
+ selector = selector.gsub(/:[a-z-]+(\([^)]*\))?/, "")
166
+
167
+ parts = T.let([], T::Array[String])
168
+
169
+ # Extract ID
170
+ if selector.include?("#")
171
+ id_match = selector.match(/#([a-zA-Z0-9_-]+)/)
172
+ parts << "##{id_match[1]}" if id_match
173
+ end
174
+
175
+ # Extract classes
176
+ selector.scan(/\.([a-zA-Z0-9_-]+)/).each do |match|
177
+ parts << ".#{match[0]}"
178
+ end
179
+
180
+ # Extract type (must be at start, before # or .)
181
+ type_match = selector.match(/^([a-zA-Z][a-zA-Z0-9_]*)/)
182
+ if type_match && !type_match[1].nil?
183
+ type_name = T.must(type_match[1])
184
+ parts.unshift(type_name) unless type_name.empty?
185
+ end
186
+
187
+ parts
188
+ end
189
+
190
+ sig { params(widget: Widget, selector: String).returns(T::Boolean) }
191
+ def matches?(widget, selector)
192
+ parts = parse_combinator_parts(selector)
193
+
194
+ # Single selector (no combinators)
195
+ return matches_simple?(widget, parts[0][:selector]) if parts.length == 1
196
+
197
+ # Combinator selector - check from right to left
198
+ current = widget
199
+ (parts.length - 1).downto(0) do |i|
200
+ part = parts[i]
201
+ return false unless matches_simple?(current, part[:selector])
202
+
203
+ # Move up the tree based on combinator
204
+ if i > 0
205
+ prev_combinator = parts[i - 1][:combinator]
206
+ case prev_combinator
207
+ when :child
208
+ current = current.parent
209
+ return false if current.nil?
210
+ when :descendant
211
+ # Find any ancestor matching the next selector
212
+ found = false
213
+ ancestor = current.parent
214
+ while ancestor
215
+ if matches_simple?(ancestor, parts[i - 1][:selector])
216
+ current = ancestor
217
+ found = true
218
+ break
219
+ end
220
+ ancestor = ancestor.parent
221
+ end
222
+ return false unless found
223
+ next # Skip the normal iteration since we handled it
224
+ end
225
+ end
226
+ end
227
+
228
+ true
229
+ end
230
+
231
+ sig { params(widget: Widget, selector: String).returns(T::Boolean) }
232
+ def matches_simple?(widget, selector)
233
+ # Handle :not() pseudo-class
234
+ not_match = selector.match(/:not\(([^)]+)\)/)
235
+ if not_match
236
+ not_selector = not_match[1]
237
+ return false if matches_simple?(widget, not_selector)
238
+ selector = selector.gsub(/:not\([^)]+\)/, "")
239
+ end
240
+
241
+ # Parse base and pseudo-class
242
+ base, pseudo = parse_simple_selector(selector)
243
+
244
+ # Check compound selector
245
+ base_matches = matches_compound?(widget, base)
246
+ return false unless base_matches
247
+
248
+ # Check pseudo-class if present
249
+ return true unless pseudo
250
+
251
+ case pseudo
252
+ when "focus"
253
+ widget.has_focus?
254
+ when "disabled"
255
+ widget.disabled == true
256
+ when "hover"
257
+ widget.hover == true
258
+ else
259
+ false
260
+ end
261
+ end
262
+
263
+ sig { params(selector: String).returns([String, T.nilable(String)]) }
264
+ def parse_simple_selector(selector)
265
+ # Match pseudo-class but not :not()
266
+ if selector.match?(/:[a-z]+$/)
267
+ parts = selector.split(":")
268
+ [parts[0..-2].join(":"), parts.last]
269
+ else
270
+ [selector, nil]
271
+ end
272
+ end
273
+
274
+ sig { params(widget: Widget, selector: String).returns(T::Boolean) }
275
+ def matches_compound?(widget, selector)
276
+ return true if selector.empty?
277
+
278
+ parts = parse_compound_selector(selector)
279
+ return true if parts.empty?
280
+
281
+ parts.all? do |part|
282
+ if part.start_with?("#")
283
+ widget.id == part[1..]
284
+ elsif part.start_with?(".")
285
+ widget.classes.include?(part[1..])
286
+ else
287
+ # Type selector
288
+ widget.class.name&.split("::")&.last == part
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,168 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "io/console"
5
+ require "sorbet-runtime"
6
+
7
+ module Ratatat
8
+ # Terminal abstraction with double buffering for flicker-free rendering.
9
+ # Manages two buffers (current and previous) and computes diffs.
10
+ class Terminal
11
+ extend T::Sig
12
+
13
+ sig { returns(AnsiBackend) }
14
+ attr_reader :backend
15
+
16
+ sig { returns(Integer) }
17
+ attr_reader :width, :height
18
+
19
+ sig { params(backend: T.nilable(AnsiBackend), io: IO).void }
20
+ def initialize(backend: nil, io: $stdout)
21
+ @backend = T.let(backend || AnsiBackend.new(io: io), AnsiBackend)
22
+ size = detect_size
23
+ @width = T.let(size[0], Integer)
24
+ @height = T.let(size[1], Integer)
25
+ @buffers = T.let([Buffer.new(@width, @height), Buffer.new(@width, @height)], [Buffer, Buffer])
26
+ @current = T.let(0, Integer)
27
+ @cursor_hidden = T.let(false, T::Boolean)
28
+ end
29
+
30
+ # Get current buffer (the one being drawn to)
31
+ sig { returns(Buffer) }
32
+ def current_buffer
33
+ @buffers[@current]
34
+ end
35
+
36
+ # Get previous buffer (last rendered frame)
37
+ sig { returns(Buffer) }
38
+ def previous_buffer
39
+ @buffers[1 - @current]
40
+ end
41
+
42
+ # Draw a frame. Yields the current buffer for drawing.
43
+ # After the block, computes diff and renders to terminal.
44
+ sig { params(blk: T.proc.params(buffer: Buffer).void).void }
45
+ def draw(&blk)
46
+ # Check for resize
47
+ check_resize
48
+
49
+ # Clear current buffer
50
+ current_buffer.clear
51
+
52
+ # Let caller draw to buffer
53
+ blk.call(current_buffer)
54
+
55
+ # Compute diff and render
56
+ flush
57
+ end
58
+
59
+ # Compute diff between buffers and render to terminal
60
+ sig { void }
61
+ def flush
62
+ updates = previous_buffer.diff(current_buffer)
63
+ @backend.draw(updates)
64
+ @backend.flush
65
+ swap_buffers
66
+ end
67
+
68
+ # Force a full redraw (no diffing)
69
+ sig { void }
70
+ def force_redraw
71
+ @backend.clear
72
+ previous_buffer.clear
73
+ flush
74
+ end
75
+
76
+ # Swap current and previous buffers
77
+ sig { void }
78
+ def swap_buffers
79
+ # Copy current to previous for next frame's diff
80
+ previous_buffer.cells.each_with_index do |_, i|
81
+ previous_buffer.cells[i] = T.must(current_buffer.cells[i])
82
+ end
83
+ end
84
+
85
+ # Get current terminal size
86
+ sig { returns([Integer, Integer]) }
87
+ def size
88
+ [@width, @height]
89
+ end
90
+
91
+ # Check if terminal was resized and update buffers
92
+ sig { void }
93
+ def check_resize
94
+ new_width, new_height = detect_size
95
+ return if new_width == @width && new_height == @height
96
+
97
+ @width = new_width
98
+ @height = new_height
99
+ @buffers.each { |buf| buf.resize(@width, @height) }
100
+ end
101
+
102
+ # Enter raw mode and alternate screen
103
+ sig { void }
104
+ def enter
105
+ @backend.enter_alternate_screen
106
+ @backend.hide_cursor
107
+ @backend.clear
108
+ @cursor_hidden = true
109
+ enable_raw_mode
110
+ end
111
+
112
+ # Exit raw mode and restore terminal
113
+ sig { void }
114
+ def exit
115
+ @backend.show_cursor if @cursor_hidden
116
+ @backend.reset_style
117
+ @backend.leave_alternate_screen
118
+ @backend.flush
119
+ disable_raw_mode
120
+ @cursor_hidden = false
121
+ end
122
+
123
+ # Show cursor at position
124
+ sig { params(x: T.nilable(Integer), y: T.nilable(Integer)).void }
125
+ def show_cursor(x = nil, y = nil)
126
+ if x && y
127
+ @backend.io.write(@backend.move_to(x, y))
128
+ end
129
+ @backend.show_cursor
130
+ @cursor_hidden = false
131
+ end
132
+
133
+ # Hide cursor
134
+ sig { void }
135
+ def hide_cursor
136
+ @backend.hide_cursor
137
+ @cursor_hidden = true
138
+ end
139
+
140
+ private
141
+
142
+ sig { returns([Integer, Integer]) }
143
+ def detect_size
144
+ if $stdout.respond_to?(:winsize)
145
+ rows, cols = $stdout.winsize
146
+ [cols || 80, rows || 24]
147
+ else
148
+ [80, 24]
149
+ end
150
+ rescue StandardError
151
+ [80, 24]
152
+ end
153
+
154
+ sig { void }
155
+ def enable_raw_mode
156
+ $stdin.raw! if $stdin.respond_to?(:raw!)
157
+ rescue StandardError
158
+ # Ignore if we can't enter raw mode
159
+ end
160
+
161
+ sig { void }
162
+ def disable_raw_mode
163
+ $stdin.cooked! if $stdin.respond_to?(:cooked!)
164
+ rescue StandardError
165
+ # Ignore if we can't restore mode
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,3 @@
1
+ module Ratatat
2
+ VERSION = "1.0.0"
3
+ end