table_tennis 0.0.5 → 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 +4 -4
- data/.github/workflows/test.yml +3 -4
- data/.rubocop.yml +1 -0
- data/Gemfile +11 -11
- data/README.md +42 -21
- data/justfile +2 -1
- data/lib/table_tennis/column.rb +3 -3
- data/lib/table_tennis/config.rb +58 -168
- data/lib/table_tennis/stage/format.rb +13 -13
- data/lib/table_tennis/stage/layout.rb +1 -1
- data/lib/table_tennis/stage/render.rb +10 -2
- data/lib/table_tennis/table.rb +11 -9
- data/lib/table_tennis/table_data.rb +1 -1
- data/lib/table_tennis/theme.rb +10 -1
- data/lib/table_tennis/util/console.rb +23 -0
- data/lib/table_tennis/util/magic_options.rb +285 -0
- data/lib/table_tennis/util/strings.rb +46 -9
- data/lib/table_tennis/util/termbg.rb +8 -6
- data/lib/table_tennis/version.rb +1 -1
- data/lib/table_tennis.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85ab2cbb42c928e7231529feaa15416f752b3efa3ca13d0ba54331b79cb41cc0
|
4
|
+
data.tar.gz: bbd1d0477437ab63c0302a8796ca058eb94a5084595a190032695188680214ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7729436cacaa34103047f9d039e1a81626ad8b9f86b33062e3e8ab58067aa69bc56242d19e7861ec44284c6e277b3daf32966129c0d41da7b389dce591371ee0
|
7
|
+
data.tar.gz: 97165044e352b52af3b31870b1999624a4a3b3f6dd8624cd69a9935d594461a1842568973fb8a101438d4a26b6ddc2df754ff6b88fcc5f61a982894fb33c329c
|
data/.github/workflows/test.yml
CHANGED
@@ -2,9 +2,8 @@ name: test
|
|
2
2
|
|
3
3
|
on:
|
4
4
|
push:
|
5
|
-
|
6
|
-
|
7
|
-
- 'screenshots/**'
|
5
|
+
branches: [main]
|
6
|
+
paths-ignore: [README.md, "screenshots/**"]
|
8
7
|
pull_request:
|
9
8
|
workflow_dispatch:
|
10
9
|
|
@@ -17,7 +16,7 @@ jobs:
|
|
17
16
|
ruby-version: [3.0, 3.4]
|
18
17
|
runs-on: ${{ matrix.os }}-latest
|
19
18
|
steps:
|
20
|
-
- uses: actions/checkout@
|
19
|
+
- uses: actions/checkout@v4
|
21
20
|
- uses: taiki-e/install-action@just
|
22
21
|
- uses: ruby/setup-ruby@v1
|
23
22
|
with:
|
data/.rubocop.yml
CHANGED
@@ -21,6 +21,7 @@ AllCops:
|
|
21
21
|
|
22
22
|
# we like these, don't remove
|
23
23
|
Bundler/OrderedGems: { Enabled: true } # sort gems in gemfile
|
24
|
+
Bundler/GemVersion: { Enabled: true } # make sure we have versions
|
24
25
|
Layout/EmptyLineBetweenDefs: { AllowAdjacentOneLineDefs: true }
|
25
26
|
Lint/NonLocalExitFromIterator: { Enabled: false }
|
26
27
|
Lint/RedundantDirGlobSort: { Enabled: true } # glob is already sorted
|
data/Gemfile
CHANGED
@@ -2,15 +2,15 @@ source "https://rubygems.org"
|
|
2
2
|
gemspec
|
3
3
|
|
4
4
|
group :development, :test do
|
5
|
-
gem "amazing_print"
|
6
|
-
gem "image_optim"
|
7
|
-
gem "image_optim_pack"
|
8
|
-
gem "minitest"
|
9
|
-
gem "minitest-hooks"
|
10
|
-
gem "mocha"
|
11
|
-
gem "ostruct" # required for Ruby 3.5+
|
12
|
-
gem "pry"
|
13
|
-
gem "rake"
|
14
|
-
gem "simplecov", require: false
|
15
|
-
gem "standard", require: false, platform: :mri
|
5
|
+
gem "amazing_print", "~> 1.7"
|
6
|
+
gem "image_optim", "~> 0.31"
|
7
|
+
gem "image_optim_pack", "~> 0.12"
|
8
|
+
gem "minitest", "~> 5.25"
|
9
|
+
gem "minitest-hooks", "~> 1.5"
|
10
|
+
gem "mocha", "~> 2.7"
|
11
|
+
gem "ostruct", "~> 0.6" # required for Ruby 3.5+
|
12
|
+
gem "pry", "~> 0.15"
|
13
|
+
gem "rake", "~> 13.2"
|
14
|
+
gem "simplecov", "~> 0.22", require: false
|
15
|
+
gem "standard", "~> 1.49", require: false, platform: :mri
|
16
16
|
end
|
data/README.md
CHANGED
@@ -67,25 +67,34 @@ options = {
|
|
67
67
|
}
|
68
68
|
```
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
|
73
|
-
|
|
74
|
-
| `
|
75
|
-
| `
|
76
|
-
| `
|
77
|
-
| `
|
78
|
-
| `
|
79
|
-
| `
|
80
|
-
| `
|
81
|
-
| `
|
82
|
-
| `
|
83
|
-
| `
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
|
88
|
-
|
|
70
|
+
#### Popular Options
|
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. |
|
84
|
+
|
85
|
+
#### More Advanced Options
|
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. |
|
89
98
|
|
90
99
|
### Color Scales
|
91
100
|
|
@@ -111,8 +120,8 @@ puts TableTennis.new(rows, search: /hope.*empire/i })
|
|
111
120
|
puts TableTennis.new(rows, row_numbers: true, zebra: true)
|
112
121
|
```
|
113
122
|
|
114
|
-
| `:mark`
|
115
|
-
|
|
123
|
+
| `:mark` | `:search` | `:row_numbers` and `:zebra` |
|
124
|
+
| ----------------------------------- | ------------------------------- | --------------------------------------------- |
|
116
125
|
|  |  |  |
|
117
126
|
|
118
127
|
### Links
|
@@ -163,6 +172,18 @@ We love CSV tools and use them all the time! Here are a few that we rely on:
|
|
163
172
|
|
164
173
|
### Changelog
|
165
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
|
+
|
180
|
+
#### 0.0.6 (May '25)
|
181
|
+
|
182
|
+
- added `coerce:` option to disable string => numeric coercion
|
183
|
+
- added `headers:` option to manually set column headers
|
184
|
+
- fixed some issues related to links, including `save:`
|
185
|
+
- added MagicOptions
|
186
|
+
|
166
187
|
#### 0.0.5 (April '25)
|
167
188
|
|
168
189
|
- Support for markdown style links in the cells
|
data/justfile
CHANGED
@@ -22,8 +22,9 @@ gem-local:
|
|
22
22
|
|
23
23
|
# this will tag, build and push to rubygems
|
24
24
|
gem-push: check
|
25
|
+
@if rg -g '!justfile' "\bREMIND\b" ; then just _fatal "REMIND found, bailing" ; fi
|
25
26
|
@just _banner rake release...
|
26
|
-
rake release
|
27
|
+
bundle exec rake release
|
27
28
|
|
28
29
|
# optimize images
|
29
30
|
image_optim:
|
data/lib/table_tennis/column.rb
CHANGED
@@ -10,12 +10,12 @@ module TableTennis
|
|
10
10
|
# c is the column index
|
11
11
|
attr_reader :name, :data, :c
|
12
12
|
attr_accessor :header, :width
|
13
|
-
def_delegators :data, *%i[rows]
|
13
|
+
def_delegators :data, *%i[config rows]
|
14
14
|
|
15
15
|
def initialize(data, name, c)
|
16
16
|
@name, @data, @c = name, data, c
|
17
|
-
@header = name.to_s
|
18
|
-
if
|
17
|
+
@header = config&.headers&.dig(name) || name.to_s
|
18
|
+
if config&.titleize?
|
19
19
|
@header = Util::Strings.titleize(@header)
|
20
20
|
end
|
21
21
|
end
|
data/lib/table_tennis/config.rb
CHANGED
@@ -3,15 +3,17 @@ module TableTennis
|
|
3
3
|
attr_accessor :defaults
|
4
4
|
end
|
5
5
|
|
6
|
-
#
|
7
|
-
class Config
|
6
|
+
# Table configuration options, with schema validation courtesy of MagicOptions.
|
7
|
+
class Config < Util::MagicOptions
|
8
8
|
OPTIONS = {
|
9
|
+
coerce: true, # if true, we try to coerce strings into numbers
|
9
10
|
color_scales: nil, # columns => color scale
|
10
11
|
color: nil, # true/false/nil (detect)
|
11
12
|
columns: nil, # array of symbols, or inferred from rows
|
12
13
|
debug: false, # true for debug output
|
13
14
|
delims: true, # true for numeric delimeters
|
14
15
|
digits: 3, # format floats
|
16
|
+
headers: nil, # columns => header strings, or inferred from columns
|
15
17
|
layout: true, # true/false/int or hash of columns -> width. true to infer
|
16
18
|
mark: nil, # lambda returning boolean or symbol to mark rows in output
|
17
19
|
placeholder: "—", # placeholder for empty cells. default is emdash
|
@@ -26,206 +28,94 @@ module TableTennis
|
|
26
28
|
zebra: false, # turn on zebra stripes
|
27
29
|
}.freeze
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
# readers
|
40
|
-
attr_reader(*OPTIONS.keys)
|
41
|
-
|
42
|
-
#
|
43
|
-
# simple writers
|
44
|
-
#
|
45
|
-
|
46
|
-
{
|
31
|
+
SCHEMA = {
|
32
|
+
coerce: :bool,
|
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}"
|
37
|
+
end
|
38
|
+
end,
|
47
39
|
color: :bool,
|
40
|
+
columns: :symbols,
|
48
41
|
debug: :bool,
|
49
42
|
delims: :bool,
|
50
|
-
digits:
|
43
|
+
digits: (0..10),
|
44
|
+
headers: {sym: :str},
|
45
|
+
layout: -> do
|
46
|
+
return if _1 == true || _1 == false || _1.is_a?(Integer)
|
47
|
+
Config.magic_validate!(:layout, _1, {Symbol => Integer})
|
48
|
+
end,
|
51
49
|
mark: :proc,
|
52
50
|
placeholder: :str,
|
53
51
|
row_numbers: :bool,
|
54
52
|
save: :str,
|
53
|
+
search: -> do
|
54
|
+
if !(_1.is_a?(String) || _1.is_a?(Regexp))
|
55
|
+
raise ArgumentError, "expected string/regex"
|
56
|
+
end
|
57
|
+
end,
|
55
58
|
separators: :bool,
|
56
59
|
strftime: :str,
|
60
|
+
theme: %i[dark light ansi],
|
57
61
|
title: :str,
|
58
62
|
titleize: :bool,
|
59
63
|
zebra: :bool,
|
60
|
-
}
|
61
|
-
define_method(:"#{option}=") do |value|
|
62
|
-
instance_variable_set(:"@#{option}", send(:"_#{type}", option, value))
|
63
|
-
end
|
64
|
-
alias_method(:"#{option}?", option) if type == :bool
|
65
|
-
end
|
66
|
-
|
67
|
-
#
|
68
|
-
# helpers
|
69
|
-
#
|
64
|
+
}
|
70
65
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
def self.detect_color?
|
79
|
-
return false if ENV["NO_COLOR"] || ENV["CI"]
|
80
|
-
return true if ENV["FORCE_COLOR"] == "1"
|
81
|
-
return false if !($stdout.tty? && $stderr.tty?)
|
82
|
-
Paint.detect_mode > 0
|
83
|
-
end
|
84
|
-
|
85
|
-
def self.detect_theme
|
86
|
-
case terminal_dark?
|
87
|
-
when true, nil then :dark
|
88
|
-
when false then :light
|
89
|
-
end
|
66
|
+
def initialize(options = {}, &block)
|
67
|
+
# assemble from OPTIONS, defaults and options
|
68
|
+
options = [OPTIONS, TableTennis.defaults, options].reduce { _1.merge(_2 || {}) }
|
69
|
+
options[:color] = Config.detect_color? if options[:color].nil?
|
70
|
+
options[:debug] = true if ENV["TT_DEBUG"]
|
71
|
+
options[:theme] = Config.detect_theme if options[:theme].nil?
|
72
|
+
super(SCHEMA, options, &block)
|
90
73
|
end
|
91
74
|
|
92
75
|
#
|
93
|
-
#
|
76
|
+
# override a few setters to coerce values
|
94
77
|
#
|
95
78
|
|
96
79
|
def color_scales=(value)
|
97
|
-
|
98
|
-
|
99
|
-
when Symbol then value = {value => :g}
|
100
|
-
end
|
101
|
-
value.to_h { [_1, :g] } if value.is_a?(Array)
|
102
|
-
@color_scales = validate(:color_scales, value) do
|
103
|
-
if !value.is_a?(Hash)
|
104
|
-
"expected hash"
|
105
|
-
elsif value.keys.any? { !_1.is_a?(Symbol) }
|
106
|
-
"keys must be symbols"
|
107
|
-
elsif value.values.any? { !_1.is_a?(Symbol) }
|
108
|
-
"values must be symbols"
|
109
|
-
elsif value.values.any? { !Util::Scale::SCALES.include?(_1) }
|
110
|
-
"values must be the name of a color scale"
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
alias_method(:"color_scale=", :"color_scales=")
|
115
|
-
|
116
|
-
def columns=(value)
|
117
|
-
@columns = validate(:columns, value) do
|
118
|
-
if !(value.is_a?(Array) && !value.empty? && value.all? { _1.is_a?(Symbol) })
|
119
|
-
"expected array of symbols"
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def theme=(value)
|
125
|
-
@theme = validate(:theme, value) do
|
126
|
-
if !value.is_a?(Symbol)
|
127
|
-
"expected symbol"
|
128
|
-
elsif !Theme::THEMES.key?(value)
|
129
|
-
"expected one of #{Theme::THEMES.keys.inspect}"
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def search=(value)
|
135
|
-
@search = validate(:search, value) do
|
136
|
-
if !(value.is_a?(String) || value.is_a?(Regexp))
|
137
|
-
"expected string/regex"
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
def layout=(value)
|
143
|
-
@layout = validate(:layout, value) do
|
144
|
-
next if [true, false].include?(value) || value.is_a?(Integer)
|
145
|
-
if !value.is_a?(Hash)
|
146
|
-
"expected boolean, int or hash"
|
147
|
-
elsif value.keys.any? { !_1.is_a?(Symbol) }
|
148
|
-
"keys must be symbols"
|
149
|
-
elsif value.values.any? { !_1.is_a?(Integer) }
|
150
|
-
"values must be ints"
|
151
|
-
end
|
80
|
+
if value.is_a?(Array) || value.is_a?(Symbol)
|
81
|
+
value = Array(value).to_h { [_1, :g] }
|
152
82
|
end
|
83
|
+
self[:color_scales] = value
|
153
84
|
end
|
85
|
+
alias_method :color_scale=, :color_scales=
|
154
86
|
|
155
|
-
def
|
156
|
-
|
157
|
-
|
158
|
-
end
|
159
|
-
|
160
|
-
def []=(key, value)
|
161
|
-
raise ArgumentError, "unknown TableTennis.#{key}=" if !respond_to?(:"#{key}=")
|
162
|
-
send(:"#{key}=", value)
|
163
|
-
end
|
164
|
-
|
165
|
-
def inspect
|
166
|
-
options = to_h.map { "@#{_1}=#{_2.inspect}" }.join(", ")
|
167
|
-
"#<Config #{options}>"
|
87
|
+
def placeholder=(value)
|
88
|
+
value = "" if value.nil?
|
89
|
+
self[:placeholder] = value
|
168
90
|
end
|
169
91
|
|
170
|
-
def
|
171
|
-
|
92
|
+
def title=(value)
|
93
|
+
value = value.to_s if value.is_a?(Symbol)
|
94
|
+
self[:title] = value
|
172
95
|
end
|
173
96
|
|
174
|
-
protected
|
175
|
-
|
176
97
|
#
|
177
|
-
#
|
98
|
+
# helpers
|
178
99
|
#
|
179
100
|
|
180
|
-
def
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
end
|
186
|
-
|
187
|
-
def _bool(option, value)
|
188
|
-
value = case value
|
189
|
-
when true, 1, "1", "true" then true
|
190
|
-
when false, 0, "", "0", "false" then false
|
191
|
-
else; value # this will turn into an error down below
|
192
|
-
end
|
193
|
-
validate(option, value) do
|
194
|
-
"expected boolean" if ![true, false].include?(value)
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
def _int(option, value)
|
199
|
-
validate(option, value) do
|
200
|
-
if !value.is_a?(Integer)
|
201
|
-
"expected int"
|
202
|
-
elsif value < 0
|
203
|
-
"expected positive int"
|
204
|
-
end
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
def _proc(option, value)
|
209
|
-
validate(option, value) do
|
210
|
-
"expected proc" if !value.is_a?(Proc)
|
211
|
-
end
|
101
|
+
def self.detect_color?
|
102
|
+
return false if ENV["NO_COLOR"] || ENV["CI"]
|
103
|
+
return true if ENV["FORCE_COLOR"] == "1"
|
104
|
+
return false if !($stdout.tty? && $stderr.tty?)
|
105
|
+
Paint.detect_mode > 0
|
212
106
|
end
|
213
107
|
|
214
|
-
def
|
215
|
-
case
|
216
|
-
when :
|
217
|
-
|
218
|
-
when :title
|
219
|
-
value = value.to_s if value.is_a?(Symbol)
|
220
|
-
end
|
221
|
-
validate(option, value) do
|
222
|
-
"expected string" if !value.is_a?(String)
|
108
|
+
def self.detect_theme
|
109
|
+
case terminal_dark?
|
110
|
+
when true, nil then :dark
|
111
|
+
when false then :light
|
223
112
|
end
|
224
113
|
end
|
225
114
|
|
226
|
-
|
227
|
-
|
228
|
-
|
115
|
+
# is this a dark terminal?
|
116
|
+
def self.terminal_dark?
|
117
|
+
if (bg = Util::Termbg.bg)
|
118
|
+
Util::Colors.dark?(bg)
|
229
119
|
end
|
230
120
|
end
|
231
121
|
end
|
@@ -27,7 +27,7 @@ module TableTennis
|
|
27
27
|
str ||= fn_default(value) || config.placeholder
|
28
28
|
|
29
29
|
# look for markdown-style links
|
30
|
-
if (link =
|
30
|
+
if (link = Util::Strings.hyperlink(str))
|
31
31
|
str, data.links[[r, c]] = link
|
32
32
|
end
|
33
33
|
|
@@ -46,16 +46,24 @@ module TableTennis
|
|
46
46
|
@_memo_wise[__method__].tap { _1.clear if _1.length > 5000 }
|
47
47
|
|
48
48
|
case value
|
49
|
-
when String
|
50
|
-
|
49
|
+
when String
|
50
|
+
if config.coerce && Util::Identify.number?(value)
|
51
|
+
fmt_number(to_f(value), digits: config.digits)
|
52
|
+
end
|
53
|
+
when Numeric
|
54
|
+
fmt_number(value, digits: config.digits)
|
51
55
|
end
|
52
56
|
end
|
53
57
|
memo_wise :fn_float
|
54
58
|
|
55
59
|
def fn_int(value)
|
56
60
|
case value
|
57
|
-
when String
|
58
|
-
|
61
|
+
when String
|
62
|
+
if config.coerce && Util::Identify.int?(value)
|
63
|
+
fmt_number(to_i(value))
|
64
|
+
end
|
65
|
+
when Integer
|
66
|
+
fmt_number(value)
|
59
67
|
end
|
60
68
|
end
|
61
69
|
|
@@ -100,14 +108,6 @@ module TableTennis
|
|
100
108
|
x
|
101
109
|
end
|
102
110
|
|
103
|
-
def detect_link(str)
|
104
|
-
# fail fast, for speed
|
105
|
-
return unless str.length >= 6 && str[0] == "["
|
106
|
-
if str =~ /^\[([^\]]+)\]\(([^\)]+)\)$/
|
107
|
-
[$1, $2]
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
111
|
# str to_xxx that are resistant to commas
|
112
112
|
def to_f(str) = str.delete(",").to_f
|
113
113
|
def to_i(str) = str.delete(",").to_i
|
@@ -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 =
|
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(
|
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
|
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
|
data/lib/table_tennis/table.rb
CHANGED
@@ -3,10 +3,6 @@
|
|
3
3
|
#
|
4
4
|
# puts TableTennis.new(array_of_hashes, options = {})
|
5
5
|
#
|
6
|
-
# See the README for details on options - _color_scales_, _color_, _columns_,
|
7
|
-
# _debug_, _digits_, _layout_, _mark_, _placeholder_, _row_numbers_, _save_,
|
8
|
-
# _search_, _strftime_, _theme_, _title_, _zebra_
|
9
|
-
#
|
10
6
|
|
11
7
|
module TableTennis
|
12
8
|
# Public API for TableTennis.
|
@@ -41,7 +37,16 @@ module TableTennis
|
|
41
37
|
def save(path)
|
42
38
|
headers = column_names
|
43
39
|
CSV.open(path, "wb", headers:, write_headers: true) do |csv|
|
44
|
-
rows.each
|
40
|
+
rows.each do |row|
|
41
|
+
# strip hyperlinks
|
42
|
+
row = row.map do |value|
|
43
|
+
if value.is_a?(String) && (h = Util::Strings.hyperlink(value))
|
44
|
+
value = h[1]
|
45
|
+
end
|
46
|
+
value
|
47
|
+
end
|
48
|
+
csv << row
|
49
|
+
end
|
45
50
|
end
|
46
51
|
end
|
47
52
|
|
@@ -54,7 +59,7 @@ module TableTennis
|
|
54
59
|
|
55
60
|
# we cnan do a bit more config checking now
|
56
61
|
def sanity!
|
57
|
-
%i[color_scales layout].each do |key|
|
62
|
+
%i[color_scales headers layout].each do |key|
|
58
63
|
next if !config[key].is_a?(Hash)
|
59
64
|
next if rows.empty? # ignore on empty data
|
60
65
|
invalid = config[key].keys - data.column_names
|
@@ -71,9 +76,6 @@ module TableTennis
|
|
71
76
|
#
|
72
77
|
# puts TableTennis.new(array_of_hashes_or_records, options = {})
|
73
78
|
#
|
74
|
-
# See the README for details on options - _color_scales_, _color_,
|
75
|
-
# _columns_, _debug_, _digits_, _layout_, _mark_, _placeholder_,
|
76
|
-
# _row_numbers_, _save_, _search_, _strftime_, _theme_, _title_, _zebra_
|
77
79
|
def new(*args, &block) = Table.new(*args, &block)
|
78
80
|
end
|
79
81
|
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 ||=
|
117
|
+
str = str.ljust(@debug_width ||= Util::Console.winsize[1])
|
118
118
|
puts Paint[str, :white, :green]
|
119
119
|
end
|
120
120
|
|
data/lib/table_tennis/theme.rb
CHANGED
@@ -101,9 +101,18 @@ module TableTennis
|
|
101
101
|
# use osc 8 to create a terminal hyperlink. underline too
|
102
102
|
def link(str, link)
|
103
103
|
linked = "#{OSC_8}#{link}#{ST}#{str}#{OSC_8}#{ST}"
|
104
|
-
|
104
|
+
# underline if aren't on iterm
|
105
|
+
linked = Paint[linked, :underline] if term_program != :iterm
|
106
|
+
linked
|
105
107
|
end
|
106
108
|
|
109
|
+
def term_program
|
110
|
+
if ENV["TERM_PROGRAM"] == "iTerm.app"
|
111
|
+
:iterm
|
112
|
+
end
|
113
|
+
end
|
114
|
+
memo_wise :term_program
|
115
|
+
|
107
116
|
# for debugging, mostly
|
108
117
|
def self.info
|
109
118
|
sample = if !Config.detect_color?
|
@@ -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
|
@@ -0,0 +1,285 @@
|
|
1
|
+
#
|
2
|
+
# Helper class for validated option processing. This is used by Config but could probably be a
|
3
|
+
# custom gem at some point...
|
4
|
+
#
|
5
|
+
# MagicOptions is created with a `schema` defining a list of `attributes`. Each attribute has a
|
6
|
+
# `name` and a `type`. `options` is a hash of values that will be validated against the schema.
|
7
|
+
# MagicOptions adds getters and setters for each attribute, and also supports [] and []=. The
|
8
|
+
# setters perform validation and raise ArgumentError if something is awry. Because setters always
|
9
|
+
# validate, it is not possible to populate MagicOptions with invalid values.
|
10
|
+
#
|
11
|
+
# To use, subclass MagicOptions and construct with a schema:
|
12
|
+
#
|
13
|
+
# class Config < MagicOptions
|
14
|
+
# def initialize
|
15
|
+
# super(
|
16
|
+
# first_name: :str,
|
17
|
+
# colors: :strings,
|
18
|
+
# customer: :bool,
|
19
|
+
# age: (20..90)
|
20
|
+
# )
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Then assign values:
|
25
|
+
#
|
26
|
+
# config = Config.new
|
27
|
+
# config.colors = %w[red white blue]
|
28
|
+
# config.update!(first_name: "john", customer: false)
|
29
|
+
#
|
30
|
+
# Here are the supported attribute types:
|
31
|
+
#
|
32
|
+
# (1) A simple type like :bool, :int, :num, :float, :str or :sym.
|
33
|
+
# (2) An array type like :bools, :ints, :nums, :floats, :strs, or :syms.
|
34
|
+
# (3) A range, regexp or Class.
|
35
|
+
# (4) A custom validation lambda. The lambda should raise an ArgumentError if
|
36
|
+
# the value is invalid.
|
37
|
+
# (5) An array of possible values (typically numbers, strings, or symbols). The
|
38
|
+
# value must be one of those possibilities.
|
39
|
+
# (6) A hash with one element { class => class }. This specifies the hash
|
40
|
+
# signature, and the value must be a hash where the keys and values are
|
41
|
+
# those classes.
|
42
|
+
#
|
43
|
+
# There is a bit of type coercion, but not much. For example, the string "true" or "1" will be
|
44
|
+
# coerced to true for boolean options. Integers can be used when the schema calls for floats.
|
45
|
+
#
|
46
|
+
|
47
|
+
module TableTennis
|
48
|
+
module Util
|
49
|
+
class MagicOptions
|
50
|
+
#
|
51
|
+
# public api (also see [] and []=)
|
52
|
+
#
|
53
|
+
|
54
|
+
attr_accessor :magic_attributes, :magic_values
|
55
|
+
|
56
|
+
def initialize(schema, options = {}, &block)
|
57
|
+
@magic_attributes, @magic_values = {}, {}
|
58
|
+
|
59
|
+
if self.class == MagicOptions # rubocop:disable Style/ClassEqualityComparison
|
60
|
+
raise ArgumentError, "MagicOptions is an abstract class"
|
61
|
+
end
|
62
|
+
|
63
|
+
schema.each { magic_add_attribute(_1, _2) }
|
64
|
+
update!(options) if options
|
65
|
+
yield self if block_given?
|
66
|
+
end
|
67
|
+
|
68
|
+
def update!(hash) = hash.each { send("#{_1}=", _2) }
|
69
|
+
def to_h = magic_values.dup
|
70
|
+
|
71
|
+
def inspect
|
72
|
+
values = magic_values.compact.map { "#{_1}=#{_2.inspect}" }.join(", ")
|
73
|
+
"#<#{self.class} #{values}>"
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# magic_add_attribute
|
78
|
+
#
|
79
|
+
|
80
|
+
def magic_add_attribute(name, type)
|
81
|
+
# resolve :boolean to :bool, :int => Integer class, etc.
|
82
|
+
type = if type.is_a?(Hash)
|
83
|
+
type.to_h { [self.class.magic_resolve(_1), self.class.magic_resolve(_2)] }
|
84
|
+
else
|
85
|
+
self.class.magic_resolve(type)
|
86
|
+
end
|
87
|
+
|
88
|
+
# sanity check for schema errors
|
89
|
+
if (error = magic_sanity(name, type))
|
90
|
+
raise ArgumentError, "MagicOptions schema #{name.inspect} #{error}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# all is well
|
94
|
+
magic_attributes[name] = type
|
95
|
+
if !respond_to?(name)
|
96
|
+
define_singleton_method(name) { self[name] }
|
97
|
+
end
|
98
|
+
if type == :bool && !respond_to?("#{name}?")
|
99
|
+
define_singleton_method("#{name}?") { !!self[name] }
|
100
|
+
end
|
101
|
+
if !respond_to?("#{name}=")
|
102
|
+
define_singleton_method("#{name}=") { |value| self[name] = value }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# sanity check a name/type from the schema
|
107
|
+
def magic_sanity(name, type)
|
108
|
+
if !name.is_a?(Symbol)
|
109
|
+
return "attribute names must be symbols"
|
110
|
+
end
|
111
|
+
if !name.to_s.match?(/\A[a-z_][0-9a-z_]*\z/i)
|
112
|
+
return "attribute names must be valid method names"
|
113
|
+
end
|
114
|
+
|
115
|
+
case type
|
116
|
+
when :bool, :bools, :floats, :ints, :nums, :strs, :syms, Class, Proc, Range, Regexp
|
117
|
+
return
|
118
|
+
when Array
|
119
|
+
"must be an array of possible values" if type.empty?
|
120
|
+
when Hash
|
121
|
+
valid = type.length == 1 && type.first.all? { _1 == :bool || _1.is_a?(Class) }
|
122
|
+
"must be { class => class }" if !valid
|
123
|
+
else
|
124
|
+
"unknown schema type #{type.inspect}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# magic_get/set
|
130
|
+
#
|
131
|
+
|
132
|
+
def magic_get(name)
|
133
|
+
raise ArgumentError, "unknown #{self.class}.#{name}" if !magic_attributes.key?(name)
|
134
|
+
magic_values[name]
|
135
|
+
end
|
136
|
+
|
137
|
+
def magic_set(name, value)
|
138
|
+
raise ArgumentError, "unknown #{self.class}.#{name}=" if !magic_attributes.key?(name)
|
139
|
+
magic_values[name] = self.class.magic_validate!(name, value, magic_attributes[name])
|
140
|
+
end
|
141
|
+
|
142
|
+
# these are part of the public api
|
143
|
+
alias_method :[], :magic_get
|
144
|
+
alias_method :[]=, :magic_set
|
145
|
+
|
146
|
+
#
|
147
|
+
# magic_validate! and static helpers
|
148
|
+
#
|
149
|
+
|
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
|
+
|
157
|
+
case type
|
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}"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# coerce value into type. pretty conservative at the moment
|
221
|
+
def self.magic_coerce(value, type)
|
222
|
+
if type == :bool
|
223
|
+
case value
|
224
|
+
when true, 1, "1", "true" then value = true
|
225
|
+
when false, 0, "", "0", "false" then value = false
|
226
|
+
end
|
227
|
+
end
|
228
|
+
value
|
229
|
+
end
|
230
|
+
|
231
|
+
# like is_a?, but supports :bool and allows ints to be floats
|
232
|
+
def self.magic_is_a?(value, klass)
|
233
|
+
if klass == :bool
|
234
|
+
value == true || value == false
|
235
|
+
elsif klass == Float
|
236
|
+
value.is_a?(klass) || value.is_a?(Integer)
|
237
|
+
else
|
238
|
+
value.is_a?(klass)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
MAGIC_ALIASES = {
|
243
|
+
boolean: :bool,
|
244
|
+
booleans: :bools,
|
245
|
+
bool: :bool,
|
246
|
+
bools: :bools,
|
247
|
+
float: Float,
|
248
|
+
floats: :floats,
|
249
|
+
int: Integer,
|
250
|
+
integer: Integer,
|
251
|
+
integers: :ints,
|
252
|
+
ints: :ints,
|
253
|
+
lambda: Proc,
|
254
|
+
num: Numeric,
|
255
|
+
number: Numeric,
|
256
|
+
numbers: :nums,
|
257
|
+
nums: :nums,
|
258
|
+
proc: Proc,
|
259
|
+
str: String,
|
260
|
+
string: String,
|
261
|
+
strings: :strs,
|
262
|
+
strs: :strs,
|
263
|
+
sym: Symbol,
|
264
|
+
symbol: Symbol,
|
265
|
+
symbols: :syms,
|
266
|
+
syms: :syms,
|
267
|
+
}
|
268
|
+
|
269
|
+
MAGIC_PRETTY = {
|
270
|
+
:bool => "boolean",
|
271
|
+
Float => "float",
|
272
|
+
Integer => "integer",
|
273
|
+
Numeric => "number",
|
274
|
+
String => "string",
|
275
|
+
Symbol => "symbol",
|
276
|
+
}
|
277
|
+
|
278
|
+
# pretty print a class (or :bool)
|
279
|
+
def self.magic_pretty(klass) = MAGIC_PRETTY[klass] || klass.to_s
|
280
|
+
|
281
|
+
# resolve :boolean to :bool, :int => Integer class, etc.
|
282
|
+
def self.magic_resolve(type) = MAGIC_ALIASES[type] || type
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
@@ -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,23 +21,55 @@ module TableTennis
|
|
18
21
|
str
|
19
22
|
end
|
20
23
|
|
21
|
-
|
22
|
-
|
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
|
23
33
|
end
|
24
34
|
|
25
|
-
|
26
|
-
|
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)
|
42
|
+
end
|
43
|
+
|
44
|
+
def hyperlink(str)
|
45
|
+
# fail fast, for speed
|
46
|
+
return unless str.length >= 6 && str[0] == "["
|
47
|
+
if str =~ /^\[(.*)\]\((.*)\)$/
|
27
48
|
[$1, $2]
|
28
49
|
end
|
29
50
|
end
|
30
51
|
|
31
52
|
# truncate a string based on the display width of the grapheme clusters.
|
32
|
-
# Should handle emojis and international characters
|
33
|
-
def truncate(
|
34
|
-
if simple?(
|
35
|
-
|
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)
|
36
68
|
end
|
69
|
+
end
|
37
70
|
|
71
|
+
# slow, but handles graphemes
|
72
|
+
def truncate0(text, stop)
|
38
73
|
# get grapheme clusters, and attach zero width graphemes to the previous grapheme
|
39
74
|
list = [].tap do |accum|
|
40
75
|
text.grapheme_clusters.each do
|
@@ -59,8 +94,10 @@ module TableTennis
|
|
59
94
|
|
60
95
|
text
|
61
96
|
end
|
97
|
+
private_class_method :truncate0
|
62
98
|
|
63
|
-
|
99
|
+
# note that escape \e (0x1b) is excluded
|
100
|
+
SIMPLE = /\A[\x00-\x1a\x1c-\x7F–—…·‘’“”•áéíñóúÓ]*\Z/
|
64
101
|
|
65
102
|
# Is this a "simple" string? (no emojis, etc). Caches results for small
|
66
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
|
-
|
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
|
-
|
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 =
|
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 =
|
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 =
|
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 =
|
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
|
data/lib/table_tennis/version.rb
CHANGED
data/lib/table_tennis.rb
CHANGED
@@ -9,6 +9,7 @@ require "unicode/display_width"
|
|
9
9
|
|
10
10
|
# mixins must be at top
|
11
11
|
require "table_tennis/util/inspectable"
|
12
|
+
require "table_tennis/util/magic_options"
|
12
13
|
|
13
14
|
require "table_tennis/column"
|
14
15
|
require "table_tennis/config"
|
@@ -24,6 +25,7 @@ require "table_tennis/stage/painter"
|
|
24
25
|
require "table_tennis/stage/render"
|
25
26
|
|
26
27
|
require "table_tennis/util/colors"
|
28
|
+
require "table_tennis/util/console"
|
27
29
|
require "table_tennis/util/identify"
|
28
30
|
require "table_tennis/util/scale"
|
29
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.
|
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-
|
10
|
+
date: 2025-08-06 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: csv
|
@@ -106,8 +106,10 @@ 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
|
112
|
+
- lib/table_tennis/util/magic_options.rb
|
111
113
|
- lib/table_tennis/util/scale.rb
|
112
114
|
- lib/table_tennis/util/strings.rb
|
113
115
|
- lib/table_tennis/util/termbg.rb
|