table_tennis 0.0.6 → 0.0.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b141b15004b90e5acaf6f4ed92d7f71a49fbb8376875fd50cf5ed1d8e555c365
4
- data.tar.gz: f9dd1e36fa97767addcce2abbfca0eb721137d6c87f0556f0ed79ca8677fa15c
3
+ metadata.gz: 85ab2cbb42c928e7231529feaa15416f752b3efa3ca13d0ba54331b79cb41cc0
4
+ data.tar.gz: bbd1d0477437ab63c0302a8796ca058eb94a5084595a190032695188680214ef
5
5
  SHA512:
6
- metadata.gz: eba4146e9b2953bef53d958b7d3bab8d65f25be9c0011309cc511f3ecc254385dc3193f9f84c12c6d768010a9d7b4ae35d49530c6a2756906eb50aeb756d72db
7
- data.tar.gz: f7d7262f3afb54a563a73d5e7279052e6dd2f4fb39a4e916994ee35f966b1f36ff7a45e22eb18619db43fe6cc516f0f34a714bb17614fcd03d1562a20fcd0ede
6
+ metadata.gz: 7729436cacaa34103047f9d039e1a81626ad8b9f86b33062e3e8ab58067aa69bc56242d19e7861ec44284c6e277b3daf32966129c0d41da7b389dce591371ee0
7
+ data.tar.gz: 97165044e352b52af3b31870b1999624a4a3b3f6dd8624cd69a9935d594461a1842568973fb8a101438d4a26b6ddc2df754ff6b88fcc5f61a982894fb33c329c
@@ -16,7 +16,7 @@ jobs:
16
16
  ruby-version: [3.0, 3.4]
17
17
  runs-on: ${{ matrix.os }}-latest
18
18
  steps:
19
- - uses: actions/checkout@v3
19
+ - uses: actions/checkout@v4
20
20
  - uses: taiki-e/install-action@just
21
21
  - uses: ruby/setup-ruby@v1
22
22
  with:
data/README.md CHANGED
@@ -69,32 +69,32 @@ options = {
69
69
 
70
70
  #### Popular Options
71
71
 
72
- | option | default | details |
73
- | ------ | ------- | ------- |
74
- | `color_scales` | ─ | Color code a column of floats, similar to the "conditional formatting" feature in Google Sheets. See [docs below](#color-scales). |
75
- | `columns` | ─ | Manually set which columns to include. Leave unset to show all columns.
76
- | `headers` | ─ | Specify some or all column headers. For example, `{user_id: "Customer"}`. When unset, headers are inferred. |
77
- | `mark` | ─ | `mark` is a way to highlight specific columns with a nice color. For example, use `mark: ->(row) { row[:planet] == "tatooine" }` to highlight those rows. Your lambda can also return a specific bg color or Paint color array.
78
- | `row_numbers` | `false` | Show row numbers in the table. |
79
- | `search` | ─ | String/regex to highlight in output. |
80
- | `separators` | `true` | Include column and header separators in output. |
81
- | `title` | ─ | Add a title line to the table. |
82
- | `titleize` | ─ | Titleize column headers, so `person_id` becomes `Person`. |
83
- | `zebra` | `false` | Turn on zebra stripes. |
72
+ | option | default | details |
73
+ | -------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
74
+ | `color_scales` | ─ | Color code a column of floats, similar to the "conditional formatting" feature in Google Sheets. See [docs below](#color-scales). |
75
+ | `columns` | ─ | Manually set which columns to include. Leave unset to show all columns. |
76
+ | `headers` | ─ | Specify some or all column headers. For example, `{user_id: "Customer"}`. When unset, headers are inferred. |
77
+ | `mark` | ─ | `mark` is a way to highlight specific columns with a nice color. For example, use `mark: ->(row) { row[:planet] == "tatooine" }` to highlight those rows. Your lambda can also return a specific bg color or Paint color array. |
78
+ | `row_numbers` | `false` | Show row numbers in the table. |
79
+ | `search` | ─ | String/regex to highlight in output. |
80
+ | `separators` | `true` | Include column and header separators in output. |
81
+ | `title` | ─ | Add a title line to the table. |
82
+ | `titleize` | ─ | Titleize column headers, so `person_id` becomes `Person`. |
83
+ | `zebra` | `false` | Turn on zebra stripes. |
84
84
 
85
85
  #### More Advanced Options
86
86
 
87
- | option | default | details |
88
- | ------ | ------- | ------- |
89
- | `coerce` | `true` | if true, try to coerce strings into numbers where possible so we can format with `digits`. You may want to disable this if you already have nicely formatted ints/floats and you don't want TableTennis to mess with them. |
90
- | `color` | ─ | Are ANSI colors enabled? Specify `true` or `false`, or leave it as nil to autodetect. Autodetect will turn on color unless redirecting to a file. When using autodetect, you can force it on by setting `ENV["FORCE_COLOR"]`, or off with `ENV["NO_COLOR"]`. |
91
- | `delims` | `true` | Format ints & floats with comma delimiter, like 123,456. |
92
- | `digits` | `3` | Format floats to this number of digits. TableTennis will look for either `Float` cells or string floats. |
93
- | `layout` | `true` | This controls column widths. Leave unset or use `true` for autolayout. Autolayout will shrink the table to fit inside the terminal. `false` turns off layout and columns will be full width. Use an int to fix all columns to a certain width, or a hash to just set a few. |
94
- | `placeholder` | `"—"` | Put this into empty cells. |
95
- | `save` | ─ | If you set this to a file path, TableTennis will save your table as a CSV file too. Useful if you want to do something else with the data. |
96
- | `strftime` | see → | strftime string for formatting Date/Time objects. The default is `"%Y-%m-%d"`, which looks like `2025-04-21` |
97
- | `theme` | ─ | When unset, will be autodetected based on terminal background color. If autodetect fails the theme defaults to :dark. You can also manually specify `:dark`, `:light` or `:ansi`. If colors are turned off this setting has no effect.|
87
+ | option | default | details |
88
+ | ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
89
+ | `coerce` | `true` | if true, try to coerce strings into numbers where possible so we can format with `digits`. You may want to disable this if you already have nicely formatted ints/floats and you don't want TableTennis to mess with them. |
90
+ | `color` | ─ | Are ANSI colors enabled? Specify `true` or `false`, or leave it as nil to autodetect. Autodetect will turn on color unless redirecting to a file. When using autodetect, you can force it on by setting `ENV["FORCE_COLOR"]`, or off with `ENV["NO_COLOR"]`. |
91
+ | `delims` | `true` | Format ints & floats with comma delimiter, like 123,456. |
92
+ | `digits` | `3` | Format floats to this number of digits. TableTennis will look for either `Float` cells or string floats. |
93
+ | `layout` | `true` | This controls column widths. Leave unset or use `true` for autolayout. Autolayout will shrink the table to fit inside the terminal. `false` turns off layout and columns will be full width. Use an int to fix all columns to a certain width, or a hash to just set a few. |
94
+ | `placeholder` | `"—"` | Put this into empty cells. |
95
+ | `save` | ─ | If you set this to a file path, TableTennis will save your table as a CSV file too. Useful if you want to do something else with the data. |
96
+ | `strftime` | see → | strftime string for formatting Date/Time objects. The default is `"%Y-%m-%d"`, which looks like `2025-04-21` |
97
+ | `theme` | ─ | When unset, will be autodetected based on terminal background color. If autodetect fails the theme defaults to :dark. You can also manually specify `:dark`, `:light` or `:ansi`. If colors are turned off this setting has no effect. |
98
98
 
99
99
  ### Color Scales
100
100
 
@@ -120,8 +120,8 @@ puts TableTennis.new(rows, search: /hope.*empire/i })
120
120
  puts TableTennis.new(rows, row_numbers: true, zebra: true)
121
121
  ```
122
122
 
123
- | `:mark` | `:search` | `:row_numbers` and `:zebra` |
124
- | - | - | - |
123
+ | `:mark` | `:search` | `:row_numbers` and `:zebra` |
124
+ | ----------------------------------- | ------------------------------- | --------------------------------------------- |
125
125
  | ![droids](./screenshots/droids.png) | ![hope](./screenshots/hope.png) | ![row numbers](./screenshots/row_numbers.png) |
126
126
 
127
127
  ### Links
@@ -172,6 +172,11 @@ We love CSV tools and use them all the time! Here are a few that we rely on:
172
172
 
173
173
  ### Changelog
174
174
 
175
+ #### 0.0.7 (Aug '25)
176
+
177
+ - handle data that already contains ANSI colors (thx @ronaldtse, #12)
178
+ - don't crash if IO.console is nil (thx @ronaldtse, #14)
179
+
175
180
  #### 0.0.6 (May '25)
176
181
 
177
182
  - added `coerce:` option to disable string => numeric coercion
data/justfile CHANGED
@@ -24,7 +24,7 @@ gem-local:
24
24
  gem-push: check
25
25
  @if rg -g '!justfile' "\bREMIND\b" ; then just _fatal "REMIND found, bailing" ; fi
26
26
  @just _banner rake release...
27
- rake release
27
+ bundle exec rake release
28
28
 
29
29
  # optimize images
30
30
  image_optim:
@@ -30,11 +30,10 @@ module TableTennis
30
30
 
31
31
  SCHEMA = {
32
32
  coerce: :bool,
33
- color_scales: ->(value) do
34
- if (error = Config.magic_validate(value, {Symbol => Symbol}))
35
- error
36
- elsif value.values.any? { !Util::Scale::SCALES.include?(_1) }
37
- "values must be the name of a color scale"
33
+ color_scales: -> do
34
+ Config.magic_validate!(:color_scales, _1, {Symbol => Symbol})
35
+ if !(invalid = _1.values - Util::Scale::SCALES.keys).empty?
36
+ raise ArgumentError, "invalid color scale(s): #{invalid.inspect}"
38
37
  end
39
38
  end,
40
39
  color: :bool,
@@ -45,7 +44,7 @@ module TableTennis
45
44
  headers: {sym: :str},
46
45
  layout: -> do
47
46
  return if _1 == true || _1 == false || _1.is_a?(Integer)
48
- Config.magic_validate(_1, {Symbol => Integer})
47
+ Config.magic_validate!(:layout, _1, {Symbol => Integer})
49
48
  end,
50
49
  mark: :proc,
51
50
  placeholder: :str,
@@ -53,7 +52,7 @@ module TableTennis
53
52
  save: :str,
54
53
  search: -> do
55
54
  if !(_1.is_a?(String) || _1.is_a?(Regexp))
56
- "expected string/regex"
55
+ raise ArgumentError, "expected string/regex"
57
56
  end
58
57
  end,
59
58
  separators: :bool,
@@ -83,6 +82,7 @@ module TableTennis
83
82
  end
84
83
  self[:color_scales] = value
85
84
  end
85
+ alias_method :color_scale=, :color_scales=
86
86
 
87
87
  def placeholder=(value)
88
88
  value = "" if value.nil?
@@ -37,7 +37,7 @@ module TableTennis
37
37
  columns.each { _1.width = _1.measure }
38
38
 
39
39
  # How much space is available, and do we already fit?
40
- screen_width = IO.console.winsize[1]
40
+ screen_width = Util::Console.winsize[1]
41
41
  available = screen_width - chrome_width - FUDGE
42
42
  return if available >= data_width
43
43
 
@@ -50,7 +50,7 @@ module TableTennis
50
50
  title_width = data.table_width - 4
51
51
  title = Util::Strings.truncate(config.title, title_width)
52
52
  title_style = data.get_style(r: :title) || :cell
53
- line = paint(title.center(title_width), title_style || :cell)
53
+ line = paint(Util::Strings.center(title, title_width), title_style || :cell)
54
54
  paint("#{pipe} #{line} #{pipe}", Theme::BG)
55
55
  end
56
56
 
@@ -81,7 +81,7 @@ module TableTennis
81
81
  style ||= :cell
82
82
 
83
83
  # add ansi codes for search
84
- value = value.gsub(search) { paint(_1, :search) } if search
84
+ value = search_cell(value) if search
85
85
 
86
86
  # add ansi codes for links
87
87
  if config.color && (link = data.links[[r, c]])
@@ -155,6 +155,14 @@ module TableTennis
155
155
  end
156
156
  end
157
157
  memo_wise :search
158
+
159
+ # add ansi codes for search
160
+ def search_cell(value)
161
+ return value if !value.match?(search)
162
+ # edge case - we can't gsub a painted cell, it can mess up the escaping
163
+ value = Util::Strings.unpaint(value)
164
+ value.gsub(search) { paint(_1, :search) }
165
+ end
158
166
  end
159
167
  end
160
168
  end
@@ -114,7 +114,7 @@ module TableTennis
114
114
  def debug(str)
115
115
  return if !config&.debug
116
116
  str = "[#{Time.now.strftime("%H:%M:%S")}] #{str}"
117
- str = str.ljust(@debug_width ||= IO.console.winsize[1])
117
+ str = str.ljust(@debug_width ||= Util::Console.winsize[1])
118
118
  puts Paint[str, :white, :green]
119
119
  end
120
120
 
@@ -0,0 +1,23 @@
1
+ module TableTennis
2
+ module Util
3
+ # static wrapper around IO.console to handle the case where IO.console is nil
4
+ module Console
5
+ module_function
6
+
7
+ # supported when IO.console is nil
8
+ def winsize(...)
9
+ IO.console&.winsize(...) || [48, 80]
10
+ end
11
+
12
+ # not supported, don't call these
13
+ %i[fileno getbyte raw syswrite].each do |name|
14
+ define_method(name) do |*args, **kwargs, &block|
15
+ if !IO.console
16
+ raise "IO.console.#{name} not supported when IO.console is nil"
17
+ end
18
+ IO.console.send(name, *args, **kwargs, &block)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -32,7 +32,8 @@
32
32
  # (1) A simple type like :bool, :int, :num, :float, :str or :sym.
33
33
  # (2) An array type like :bools, :ints, :nums, :floats, :strs, or :syms.
34
34
  # (3) A range, regexp or Class.
35
- # (4) A lambda which should return an error string or nil.
35
+ # (4) A custom validation lambda. The lambda should raise an ArgumentError if
36
+ # the value is invalid.
36
37
  # (5) An array of possible values (typically numbers, strings, or symbols). The
37
38
  # value must be one of those possibilities.
38
39
  # (6) A hash with one element { class => class }. This specifies the hash
@@ -135,15 +136,7 @@ module TableTennis
135
136
 
136
137
  def magic_set(name, value)
137
138
  raise ArgumentError, "unknown #{self.class}.#{name}=" if !magic_attributes.key?(name)
138
- type = magic_attributes[name]
139
- value = self.class.magic_coerce(value, type)
140
- if !value.nil? && (error = MagicOptions.magic_validate(value, type))
141
- if !type.is_a?(Proc)
142
- error = "#{error}, got #{value.inspect}"
143
- end
144
- raise ArgumentError, "#{self.class}.#{name}= #{error}"
145
- end
146
- magic_values[name] = value
139
+ magic_values[name] = self.class.magic_validate!(name, value, magic_attributes[name])
147
140
  end
148
141
 
149
142
  # these are part of the public api
@@ -151,39 +144,76 @@ module TableTennis
151
144
  alias_method :[]=, :magic_set
152
145
 
153
146
  #
154
- # magic_validate and static helpers
147
+ # magic_validate! and static helpers
155
148
  #
156
149
 
157
- def self.magic_validate(value, type)
150
+ # validate name=value against type, raise on failure
151
+ def self.magic_validate!(name, value, type)
152
+ # we validate against coerced values, but squirrel away the original
153
+ # uncoerced in case we need to use it inside an error message
154
+ original, value = value, magic_coerce(value, type)
155
+ return if value.nil?
156
+
158
157
  case type
159
- when Array
160
- "expected one of #{type.inspect}" if !type.include?(value)
161
- when Class, :bool
162
- "expected #{magic_pretty(type)}" if !magic_is_a?(value, type)
163
- when Hash
164
- name_klass, value_klass = type.first
165
- valid = value.is_a?(Hash) && value.all? { magic_is_a?(_1, name_klass) && magic_is_a?(_2, value_klass) }
166
- "expected hash of #{magic_pretty(name_klass)} => #{magic_pretty(value_klass)}" if !valid
167
- when Proc
168
- ret = type.call(value)
169
- case ret
170
- when String, false, nil then ret
171
- else
172
- puts "warning: MagicOptions.proc should ONLY return error string or nil/false, not #{ret.inspect}"
173
- ret.to_s
174
- end
175
- when Range
176
- if !value.is_a?(Numeric) || !type.include?(value)
177
- "expected to be in range #{type.inspect}"
178
- end
179
- when Regexp
180
- if !value.is_a?(String) || !value.match?(type)
181
- "expected to be a string matching #{type.inspect}"
182
- end
183
- when :bools, :floats, :ints, :nums, :strs, :syms
184
- klass = magic_resolve(type.to_s[..-2].to_sym)
185
- valid = value.is_a?(Array) && value.all? { magic_is_a?(_1, klass) }
186
- "expected array of #{type}" if !valid
158
+ when Array then validate_any_of(value, type)
159
+ when Class, :bool then validate_class(value, type)
160
+ when Hash then validate_hash(value, type)
161
+ when Proc then type.call(value)
162
+ when Range then validate_range(value, type)
163
+ when Regexp then validate_regexp(value, type)
164
+ when :bools, :floats, :ints, :nums, :strs, :syms then validate_array(value, type)
165
+ else
166
+ raise "impossible"
167
+ end
168
+ value
169
+ rescue ArgumentError => ex
170
+ # add context to msg if necessary
171
+ msg = ex.message
172
+ if !msg.include?("#{name} = #{original.inspect}")
173
+ msg = "#{self}.#{name} = #{original.inspect} failed, #{msg}"
174
+ end
175
+ raise ArgumentError, msg
176
+ end
177
+
178
+ #
179
+ # validators
180
+ #
181
+
182
+ def self.validate_any_of(value, possibilities)
183
+ if !possibilities.include?(value)
184
+ raise ArgumentError, "expected one of #{possibilities.inspect}"
185
+ end
186
+ end
187
+
188
+ def self.validate_class(value, klass)
189
+ if !magic_is_a?(value, klass)
190
+ raise ArgumentError, "expected #{magic_pretty(klass)}"
191
+ end
192
+ end
193
+
194
+ def self.validate_hash(value, hash_type)
195
+ kk, vk = hash_type.first
196
+ if !(value.is_a?(Hash) && value.all? { magic_is_a?(_1, kk) && magic_is_a?(_2, vk) })
197
+ raise ArgumentError, "expected hash of #{magic_pretty(kk)} => #{magic_pretty(vk)}"
198
+ end
199
+ end
200
+
201
+ def self.validate_range(value, range)
202
+ if !value.is_a?(Numeric) || !range.include?(value)
203
+ raise ArgumentError, "expected to be in range #{range.inspect}"
204
+ end
205
+ end
206
+
207
+ def self.validate_regexp(value, regexp)
208
+ if !value.is_a?(String) || !value.match?(regexp)
209
+ raise ArgumentError, "expected to be a string matching #{regexp}"
210
+ end
211
+ end
212
+
213
+ def self.validate_array(value, array_type)
214
+ klass = magic_resolve(array_type.to_s[..-2].to_sym)
215
+ if !(value.is_a?(Array) && value.all? { magic_is_a?(_1, klass) })
216
+ raise ArgumentError, "expected array of #{array_type}"
187
217
  end
188
218
  end
189
219
 
@@ -6,6 +6,9 @@ module TableTennis
6
6
 
7
7
  module_function
8
8
 
9
+ # does this string contain ansi codes?
10
+ def painted?(str) = str.match?(/\e/)
11
+
9
12
  # strip ansi codes
10
13
  def unpaint(str) = str.gsub(/\e\[[0-9;]*m/, "")
11
14
 
@@ -18,8 +21,24 @@ module TableTennis
18
21
  str
19
22
  end
20
23
 
21
- def width(text)
22
- simple?(text) ? text.length : Unicode::DisplayWidth.of(text)
24
+ # measure width of text, with support for emojis, painted/ansi strings, etc
25
+ def width(str)
26
+ if simple?(str)
27
+ str.length
28
+ elsif painted?(str)
29
+ unpaint(str).length
30
+ else
31
+ Unicode::DisplayWidth.of(str)
32
+ end
33
+ end
34
+
35
+ # center text, like String#center but works with painted strings
36
+ def center(str, width)
37
+ # artificially inflate width to include escape codes
38
+ if painted?(str)
39
+ width += str.length - unpaint(str).length
40
+ end
41
+ str.center(width)
23
42
  end
24
43
 
25
44
  def hyperlink(str)
@@ -31,12 +50,26 @@ module TableTennis
31
50
  end
32
51
 
33
52
  # truncate a string based on the display width of the grapheme clusters.
34
- # Should handle emojis and international characters
35
- def truncate(text, stop)
36
- if simple?(text)
37
- return (text.length > stop) ? "#{text[0, stop - 1]}…" : text
53
+ # Should handle emojis and international characters. Painted strings too.
54
+ def truncate(str, stop)
55
+ if simple?(str)
56
+ (str.length > stop) ? "#{str[0, stop - 1]}…" : str
57
+ elsif painted?(str)
58
+ # generate truncated plain version
59
+ plain = truncate0(unpaint(str), stop)
60
+ # make a best effort to apply the colors
61
+ if (opening_codes = str[/\e\[(?:[0-9];?)+m/])
62
+ "#{opening_codes}#{plain}#{Paint::NOTHING}"
63
+ else
64
+ plain
65
+ end
66
+ else
67
+ truncate0(str, stop)
38
68
  end
69
+ end
39
70
 
71
+ # slow, but handles graphemes
72
+ def truncate0(text, stop)
40
73
  # get grapheme clusters, and attach zero width graphemes to the previous grapheme
41
74
  list = [].tap do |accum|
42
75
  text.grapheme_clusters.each do
@@ -61,8 +94,10 @@ module TableTennis
61
94
 
62
95
  text
63
96
  end
97
+ private_class_method :truncate0
64
98
 
65
- SIMPLE = /\A[\x00-\x7F–—…·‘’“”•áéíñóúÓ]*\Z/
99
+ # note that escape \e (0x1b) is excluded
100
+ SIMPLE = /\A[\x00-\x1a\x1c-\x7F–—…·‘’“”•áéíñóúÓ]*\Z/
66
101
 
67
102
  # Is this a "simple" string? (no emojis, etc). Caches results for small
68
103
  # strings for performance reasons.
@@ -54,6 +54,8 @@ module TableTennis
54
54
  "bad TERM"
55
55
  elsif ENV["ZELLIJ"]
56
56
  "zellij"
57
+ elsif !IO.console
58
+ "no console"
57
59
  end
58
60
  if error
59
61
  debug("osc_supported? #{{host:, platform:, term:}} => #{error}")
@@ -75,7 +77,7 @@ module TableTennis
75
77
 
76
78
  debug("osc_query(#{attr})")
77
79
  begin
78
- IO.console.raw do
80
+ Util::Console.raw do
79
81
  logs << " IO.console.raw"
80
82
 
81
83
  # we send two messages - the cursor query is widely supported, so we
@@ -89,7 +91,7 @@ module TableTennis
89
91
  end.join
90
92
 
91
93
  logs << " syswrite #{msg.inspect}"
92
- IO.console.syswrite(msg)
94
+ Util::Console.syswrite(msg)
93
95
 
94
96
  # there should always be at least one response. If this is a response to
95
97
  # the cursor message, the first message didn't work
@@ -116,18 +118,18 @@ module TableTennis
116
118
  def read_term_response
117
119
  # fast forward to ESC
118
120
  loop do
119
- return if !(ch = IO.console.getbyte&.chr)
121
+ return if !(ch = Util::Console.getbyte&.chr)
120
122
  break ch if ch == ESC
121
123
  end
122
124
  # next char should be either [ or ]
123
- return if !(type = IO.console.getbyte&.chr)
125
+ return if !(type = Util::Console.getbyte&.chr)
124
126
  return if !(type == "[" || type == "]")
125
127
 
126
128
  # now read the response. note that the response can end in different ways
127
129
  # and we have to check for all of them
128
130
  buf = "#{ESC}#{type}"
129
131
  loop do
130
- return if !(ch = IO.console.getbyte&.chr)
132
+ return if !(ch = Util::Console.getbyte&.chr)
131
133
  buf << ch
132
134
  break if type == "[" && buf.end_with?("R")
133
135
  break if type == "]" && buf.end_with?(BEL, ST)
@@ -162,7 +164,7 @@ module TableTennis
162
164
  return if !respond_to?(:tcgetpgrp)
163
165
  end
164
166
 
165
- io = IO.console
167
+ io = Util::Console
166
168
  if (ttypgrp = tcgetpgrp(io.fileno)) <= 0
167
169
  debug("tcpgrp(#{io.fileno}) => #{ttypgrp}, errno=#{FFI.errno}")
168
170
  return
@@ -1,3 +1,3 @@
1
1
  module TableTennis
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
data/lib/table_tennis.rb CHANGED
@@ -25,6 +25,7 @@ require "table_tennis/stage/painter"
25
25
  require "table_tennis/stage/render"
26
26
 
27
27
  require "table_tennis/util/colors"
28
+ require "table_tennis/util/console"
28
29
  require "table_tennis/util/identify"
29
30
  require "table_tennis/util/scale"
30
31
  require "table_tennis/util/strings"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: table_tennis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Doppelt
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-01 00:00:00.000000000 Z
10
+ date: 2025-08-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: csv
@@ -106,6 +106,7 @@ files:
106
106
  - lib/table_tennis/table_data.rb
107
107
  - lib/table_tennis/theme.rb
108
108
  - lib/table_tennis/util/colors.rb
109
+ - lib/table_tennis/util/console.rb
109
110
  - lib/table_tennis/util/identify.rb
110
111
  - lib/table_tennis/util/inspectable.rb
111
112
  - lib/table_tennis/util/magic_options.rb