table_tennis 0.0.7 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85ab2cbb42c928e7231529feaa15416f752b3efa3ca13d0ba54331b79cb41cc0
4
- data.tar.gz: bbd1d0477437ab63c0302a8796ca058eb94a5084595a190032695188680214ef
3
+ metadata.gz: 2dea31d2827226b7c2a9a92414ef69a99eddecafd6dc8bb9865c975fa48a9f2a
4
+ data.tar.gz: 2fe778598f290d8ede809971ab3ddf9bc12c16fa913542dac795708145230759
5
5
  SHA512:
6
- metadata.gz: 7729436cacaa34103047f9d039e1a81626ad8b9f86b33062e3e8ab58067aa69bc56242d19e7861ec44284c6e277b3daf32966129c0d41da7b389dce591371ee0
7
- data.tar.gz: 97165044e352b52af3b31870b1999624a4a3b3f6dd8624cd69a9935d594461a1842568973fb8a101438d4a26b6ddc2df754ff6b88fcc5f61a982894fb33c329c
6
+ metadata.gz: 964ac069a29278563120f105ae399b42cb086aa332ba2e47e2816142b5fab6af80acd91a03e7b69b746ca59c85541d64e71c56a3409304e009b33b0ff7887820
7
+ data.tar.gz: 06ab17c03ed9b367b3a668275d3ac5030f3c865222d851c5545ee757dbe525a67cae60f045554775589191168016f3aa4587862b79f28fae2bda0f2f4b7959e5
@@ -13,10 +13,10 @@ jobs:
13
13
  max-parallel: 3
14
14
  matrix:
15
15
  os: [macos, ubuntu, windows]
16
- ruby-version: [3.0, 3.4]
16
+ ruby-version: [3.0, 3.4, 4.0]
17
17
  runs-on: ${{ matrix.os }}-latest
18
18
  steps:
19
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@v7
20
20
  - uses: taiki-e/install-action@just
21
21
  - uses: ruby/setup-ruby@v1
22
22
  with:
data/.justfile ADDED
@@ -0,0 +1,85 @@
1
+ set quiet := true
2
+
3
+ default: test
4
+
5
+ # check repo - lint & test
6
+ check: lint test
7
+
8
+ # for ci. don't bother linting on windows
9
+ ci:
10
+ if [[ "{{os()}}" != "windows" ]]; then just lint ; fi
11
+ just test
12
+
13
+ # check test coverage
14
+ coverage:
15
+ COVERAGE=1 just test
16
+ open /tmp/coverage/index.html
17
+
18
+ # format with rubocop
19
+ fmt: (lint "-a")
20
+
21
+ gem-local:
22
+ just banner rake install:local...
23
+ bundle exec rake install:local
24
+
25
+ # this will tag, build and push to rubygems
26
+ gem-push: check
27
+ if rg -g '!.justfile' "\bREMIND\b" ; then just _fatal "REMIND found, bailing" ; fi
28
+ just banner rake release...
29
+ bundle exec rake release
30
+
31
+ # optimize images
32
+ image_optim:
33
+ # advpng/pngout are slow. consider --verbose as well
34
+ bundle exec image_optim --allow-lossy --svgo-precision=1 --no-advpng --no-pngout -r .
35
+
36
+ # check for outdated deps
37
+ outdated:
38
+ just banner Here are the easy ones:
39
+ bundle outdated --filter-minor || true
40
+ just banner The full list:
41
+ bundle outdated || true
42
+
43
+ # lint with rubocop
44
+ lint *ARGS:
45
+ just banner lint...
46
+ bundle exec rubocop {{ARGS}}
47
+
48
+ # start pry with the lib loaded
49
+ pry:
50
+ bundle exec pry -I lib -r table_tennis.rb
51
+
52
+ # run tennis repeatedly
53
+ tennis-watch *ARGS:
54
+ watchexec --stop-timeout=0 --clear clear tennis {{ARGS}}
55
+
56
+ # run tests
57
+ test *ARGS:
58
+ just banner rake test {{ARGS}}
59
+ bundle exec rake test {{ARGS}}
60
+
61
+ # run tests repeatedly
62
+ test-watch *ARGS:
63
+ watchexec --stop-timeout=0 --clear clear just test "{{ARGS}}"
64
+
65
+ # create sceenshot using vhs
66
+ vhs:
67
+ just banner "running vhs..."
68
+ vhs demo.tape
69
+ magick /tmp/dark.png -crop 1448x1004+18+16 screenshots/dark.png
70
+
71
+ #
72
+ # util
73
+ #
74
+
75
+ TRUWHITE := '\e[38;5;231m'
76
+ GREEN := '\e[48;2;064;160;043m'
77
+ ORANGE := '\e[48;2;251;100;011m'
78
+ RED := '\e[48;2;210;015;057m'
79
+
80
+ banner +ARGS: (_banner GREEN ARGS)
81
+ warning +ARGS: (_banner ORANGE ARGS)
82
+ fatal +ARGS: (_banner RED ARGS)
83
+ exit 1
84
+ _banner BG +ARGS:
85
+ printf '{{BOLD+TRUWHITE+BG}}[%s] %-72s {{NORMAL}}\n' "$(date +%H:%M:%S)" "{{ARGS}}"
data/.mise.toml ADDED
@@ -0,0 +1,9 @@
1
+ [env]
2
+ _.path = ['{{config_root}}/bin']
3
+
4
+ [hooks]
5
+ postinstall = ['bundle']
6
+
7
+ [tools]
8
+ just = "latest"
9
+ ruby = "3.4"
data/AGENTS.md ADDED
@@ -0,0 +1,23 @@
1
+ # Purpose
2
+
3
+ TableTennis is a popular ruby gem for rendering ANSI-colored tables from csv-like rows (hash-ish objects). Beauty > performance, but keep small-table latency snappy.
4
+
5
+ # Key flow
6
+
7
+ - Public API: `TableTennis.new` -> `TableTennis::Table` (`lib/table_tennis/table.rb`).
8
+ - Pipeline: `Format` -> `Layout` -> `Painter` -> `Render` (`lib/table_tennis/stage/*.rb`).
9
+ - Data model: `TableData`, `Column`, `Row` (`lib/table_tennis/table_data.rb`, `lib/table_tennis/column.rb`, `lib/table_tennis/row.rb`).
10
+ - Config/theming: `Config`, `Theme`, `Util::Colors/Console/Strings/Termbg` (`lib/table_tennis/config.rb`, `lib/table_tennis/theme.rb`, `lib/table_tennis/util/*`).
11
+
12
+ # dev (prefer `just`)
13
+
14
+ - `just test`
15
+ - `just lint` / `just format`
16
+ - `just check`
17
+
18
+ # Style/conventions
19
+
20
+ - Defaults and validation live in `Config` (`MagicOptions`).
21
+ - Rendering cost is mostly in `Stage::Render` (ANSI/string work); memoization via `MemoWise` is used in hot paths.
22
+ - Prefer short methods, short variable names, and comment lightly when code is complex
23
+ - Ruby one-liners are find and even preferred for trivial methods
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", "~> 1.7"
6
- gem "image_optim", "~> 0.31"
7
- gem "image_optim_pack", "~> 0.12"
8
- gem "minitest", "~> 5.25"
5
+ gem "amazing_print", "~> 2.0"
6
+ gem "image_optim", "~> 0.32"
7
+ gem "image_optim_pack", "~> 0.13"
8
+ gem "minitest", "~> 5.27"
9
9
  gem "minitest-hooks", "~> 1.5"
10
- gem "mocha", "~> 2.7"
10
+ gem "mocha", "~> 3.1"
11
11
  gem "ostruct", "~> 0.6" # required for Ruby 3.5+
12
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
13
+ gem "rake", "~> 13.4"
14
+ gem "simplecov", "~> 1.0", require: false
15
+ gem "standard", "~> 1.56", require: false, platform: :mri
16
16
  end
data/README.md CHANGED
@@ -172,6 +172,13 @@ 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
+ #### 1.0.0 (Jul '26)
176
+
177
+ - better truncation with ansi colors, inspired by strings-truncation gem
178
+ - set timeout in io/console getch to make OSC query more resilient
179
+ - truncation no longer supports graphemes (breaking)
180
+ - added AGENTS.md and mise.toml
181
+
175
182
  #### 0.0.7 (Aug '25)
176
183
 
177
184
  - handle data that already contains ANSI colors (thx @ronaldtse, #12)
@@ -489,6 +489,7 @@ module TableTennis
489
489
 
490
490
  # print all colors to $stdout, helps with creating themes
491
491
  def spectrum
492
+ # paint goes bg, fg
492
493
  max = NAMED.keys.map(&:length).max
493
494
  fmt = " %-#{max}s %s %0.3f "
494
495
  NAMED.each do |name, color|
@@ -500,6 +501,17 @@ module TableTennis
500
501
  puts "#{str1}#{str2}#{str3}#{str4}"
501
502
  end
502
503
 
504
+ # with 256 - 38/5/fg or 38/5/bg
505
+ fmt = " ansi256 %d"
506
+ (1..256).each do |color|
507
+ str = sprintf(" ansi256 %3d ", color)
508
+ str1 = Paint[str, 38, 5, color, "white"]
509
+ str2 = Paint[str, 38, 5, color, "black"]
510
+ str3 = Paint[" white ", "white", 48, 5, color]
511
+ str4 = Paint[" black ", "black", 48, 5, color]
512
+ puts "#{str1}#{str2}#{str3}#{str4}"
513
+ end
514
+
503
515
  fmt = " ansi %-6s %-8s "
504
516
  colors = Paint::ANSI_COLORS_FOREGROUND.keys
505
517
  puts
@@ -10,7 +10,7 @@ module TableTennis
10
10
  end
11
11
 
12
12
  # not supported, don't call these
13
- %i[fileno getbyte raw syswrite].each do |name|
13
+ %i[fileno getch raw syswrite].each do |name|
14
14
  define_method(name) do |*args, **kwargs, &block|
15
15
  if !IO.console
16
16
  raise "IO.console.#{name} not supported when IO.console is nil"
@@ -6,11 +6,13 @@ module TableTennis
6
6
 
7
7
  module_function
8
8
 
9
+ ANSI_CODE = /\e\[[0-9;]*m/
10
+
9
11
  # does this string contain ansi codes?
10
12
  def painted?(str) = str.match?(/\e/)
11
13
 
12
14
  # strip ansi codes
13
- def unpaint(str) = str.gsub(/\e\[[0-9;]*m/, "")
15
+ def unpaint(str) = str.gsub(ANSI_CODE, "")
14
16
 
15
17
  # similar to rails titleize
16
18
  def titleize(str)
@@ -49,50 +51,50 @@ module TableTennis
49
51
  end
50
52
  end
51
53
 
52
- # truncate a string based on the display width of the grapheme clusters.
53
- # Should handle emojis and international characters. Painted strings too.
54
+ ELLIPSIS = "…"
55
+ INVISIBLE = /\A(\e\[[0-9;]*m|\u200B)*\z/
56
+
57
+ # Truncate a string based on the display width of characters. Does not
58
+ # attempt to handle graphemes. Should handle emojis and international
59
+ # characters. Painted strings too.
54
60
  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
61
+ if str.bytesize <= stop
62
+ str
63
+ elsif simple?(str)
64
+ (str.length <= stop) ? str : "#{str[0, stop - 1]}#{ELLIPSIS}"
66
65
  else
67
66
  truncate0(str, stop)
68
67
  end
69
68
  end
70
69
 
71
- # slow, but handles graphemes
70
+ # This is a slower truncate to handle ansi colors and wide characters like
71
+ # emojis. Inspired by piotrmurach/strings-truncation
72
72
  def truncate0(text, stop)
73
- # get grapheme clusters, and attach zero width graphemes to the previous grapheme
74
- list = [].tap do |accum|
75
- text.grapheme_clusters.each do
76
- if width(_1) == 0 && !accum.empty?
77
- accum[-1] = "#{accum[-1]}#{_1}"
78
- else
79
- accum << _1
73
+ [].tap do |buf|
74
+ scan, len, painting = StringScanner.new(text), 0, false
75
+ until scan.eos?
76
+ # are we looking at an ansi code?
77
+ if scan.scan(ANSI_CODE)
78
+ buf << scan.matched
79
+ painting = scan.matched != Paint::NOTHING
80
+ next
80
81
  end
81
- end
82
- end
83
82
 
84
- width = 0
85
- list.each_index do
86
- w = Unicode::DisplayWidth.of(list[_1])
87
- next if (width += w) <= stop
83
+ # what's next?
84
+ ch = scan.getch
88
85
 
89
- # we've gone too far. do we need to pop for the ellipsis?
90
- text = list[0, _1]
91
- text.pop if width - w == stop
92
- return "#{text.join}…"
93
- end
86
+ # done? append one final char, possible an ELLIPSIS
87
+ len += Unicode::DisplayWidth.of(ch)
88
+ if len >= stop
89
+ buf << (scan.check(INVISIBLE) ? ch : ELLIPSIS)
90
+ break
91
+ end
94
92
 
95
- text
93
+ # keep going
94
+ buf << ch
95
+ end
96
+ buf << Paint::NOTHING if painting
97
+ end.join
96
98
  end
97
99
  private_class_method :truncate0
98
100
 
@@ -118,18 +118,18 @@ module TableTennis
118
118
  def read_term_response
119
119
  # fast forward to ESC
120
120
  loop do
121
- return if !(ch = Util::Console.getbyte&.chr)
121
+ return if !(ch = getch)
122
122
  break ch if ch == ESC
123
123
  end
124
124
  # next char should be either [ or ]
125
- return if !(type = Util::Console.getbyte&.chr)
125
+ return if !(type = getch)
126
126
  return if !(type == "[" || type == "]")
127
127
 
128
128
  # now read the response. note that the response can end in different ways
129
129
  # and we have to check for all of them
130
130
  buf = "#{ESC}#{type}"
131
131
  loop do
132
- return if !(ch = Util::Console.getbyte&.chr)
132
+ return if !(ch = getch)
133
133
  buf << ch
134
134
  break if type == "[" && buf.end_with?("R")
135
135
  break if type == "]" && buf.end_with?(BEL, ST)
@@ -138,6 +138,10 @@ module TableTennis
138
138
  end
139
139
  private_class_method :read_term_response
140
140
 
141
+ # get the next character, with a 100ms timeout
142
+ def getch = Util::Console.getch(min: 0, time: 0.1)
143
+ private_class_method :read_term_response
144
+
141
145
  #
142
146
  # color math
143
147
  #
@@ -182,6 +186,7 @@ module TableTennis
182
186
  def load_ffi!
183
187
  module_eval do
184
188
  extend FFI::Library
189
+
185
190
  ffi_lib "c"
186
191
  attach_function :tcgetpgrp, %i[int], :int32
187
192
  debug("ffi attach libc.tcgetpgrp => success")
@@ -204,7 +209,9 @@ module TableTennis
204
209
  private_class_method :env_colorfgbg
205
210
 
206
211
  def debug(s)
207
- puts "termbg: #{s}" if ENV["TT_DEBUG"]
212
+ return if !ENV["TT_DEBUG"]
213
+ $stderr.write "termbg: #{s}\r\n"
214
+ $stderr.flush
208
215
  end
209
216
  private_class_method :debug
210
217
  end
@@ -1,3 +1,3 @@
1
1
  module TableTennis
2
- VERSION = "0.0.7"
2
+ VERSION = "1.0.0"
3
3
  end
data/table_tennis.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |s|
23
23
  # gem dependencies
24
24
  s.add_dependency "csv", "~> 3.3" # required for Ruby 3.4+
25
25
  s.add_dependency "ffi", "~> 1.17" # required for Ruby 3.2+
26
- s.add_dependency "memo_wise", "~> 1.11"
26
+ s.add_dependency "memo_wise", "~> 1.13"
27
27
  s.add_dependency "paint", "~> 2.3"
28
- s.add_dependency "unicode-display_width", "~> 3.1"
28
+ s.add_dependency "unicode-display_width", "~> 3.2"
29
29
  end
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.7
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Doppelt
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-06 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: csv
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.11'
46
+ version: '1.13'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1.11'
53
+ version: '1.13'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: paint
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '3.1'
74
+ version: '3.2'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '3.1'
81
+ version: '3.2'
82
82
  email: amd@gurge.com
83
83
  executables: []
84
84
  extensions: []
@@ -86,13 +86,15 @@ extra_rdoc_files: []
86
86
  files:
87
87
  - ".github/workflows/test.yml"
88
88
  - ".gitignore"
89
+ - ".justfile"
90
+ - ".mise.toml"
89
91
  - ".rubocop.yml"
92
+ - AGENTS.md
90
93
  - Gemfile
91
94
  - LICENSE
92
95
  - README.md
93
96
  - Rakefile
94
97
  - demo.tape
95
- - justfile
96
98
  - lib/table_tennis.rb
97
99
  - lib/table_tennis/column.rb
98
100
  - lib/table_tennis/config.rb
@@ -144,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
146
  - !ruby/object:Gem::Version
145
147
  version: '0'
146
148
  requirements: []
147
- rubygems_version: 3.6.2
149
+ rubygems_version: 3.6.9
148
150
  specification_version: 4
149
151
  summary: Stylish tables in your terminal.
150
152
  test_files: []
data/justfile DELETED
@@ -1,72 +0,0 @@
1
- default: test
2
-
3
- # check repo - lint & test
4
- check: lint test
5
-
6
- # for ci. don't bother linting on windows
7
- ci:
8
- @if [[ "{{os()}}" != "windows" ]]; then just lint ; fi
9
- @just test
10
-
11
- # check test coverage
12
- coverage:
13
- COVERAGE=1 just test
14
- open /tmp/coverage/index.html
15
-
16
- # format with rubocop
17
- format: (lint "-a")
18
-
19
- gem-local:
20
- @just _banner rake install:local...
21
- bundle exec rake install:local
22
-
23
- # this will tag, build and push to rubygems
24
- gem-push: check
25
- @if rg -g '!justfile' "\bREMIND\b" ; then just _fatal "REMIND found, bailing" ; fi
26
- @just _banner rake release...
27
- bundle exec rake release
28
-
29
- # optimize images
30
- image_optim:
31
- @# advpng/pngout are slow. consider --verbose as well
32
- @bundle exec image_optim --allow-lossy --svgo-precision=1 --no-advpng --no-pngout -r .
33
-
34
- # lint with rubocop
35
- lint *ARGS:
36
- @just _banner lint...
37
- bundle exec rubocop {{ARGS}}
38
-
39
- # start pry with the lib loaded
40
- pry:
41
- bundle exec pry -I lib -r table_tennis.rb
42
-
43
- # run tennis repeatedly
44
- tennis-watch *ARGS:
45
- @watchexec --stop-timeout=0 --clear clear tennis {{ARGS}}
46
-
47
- # run tests
48
- test *ARGS:
49
- @just _banner rake test {{ARGS}}
50
- @bundle exec rake test {{ARGS}}
51
-
52
- # run tests repeatedly
53
- test-watch *ARGS:
54
- watchexec --stop-timeout=0 --clear clear just test "{{ARGS}}"
55
-
56
- # create sceenshot using vhs
57
- vhs:
58
- @just _banner "running vhs..."
59
- vhs demo.tape
60
- magick /tmp/dark.png -crop 1448x1004+18+16 screenshots/dark.png
61
-
62
- #
63
- # util
64
- #
65
-
66
- _banner *ARGS: (_message BG_GREEN ARGS)
67
- _warning *ARGS: (_message BG_YELLOW ARGS)
68
- _fatal *ARGS: (_message BG_RED ARGS)
69
- @exit 1
70
- _message color *ARGS:
71
- @msg=$(printf "[%s] %s" $(date +%H:%M:%S) "{{ARGS}}") ; \
72
- printf "{{color+BOLD+WHITE}}%-72s{{ NORMAL }}\n" "$msg"