table_tennis 0.0.6 → 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 +4 -4
- data/.github/workflows/test.yml +2 -2
- data/.justfile +85 -0
- data/.mise.toml +9 -0
- data/AGENTS.md +23 -0
- data/Gemfile +8 -8
- data/README.md +37 -25
- data/lib/table_tennis/config.rb +7 -7
- data/lib/table_tennis/stage/layout.rb +1 -1
- data/lib/table_tennis/stage/render.rb +10 -2
- data/lib/table_tennis/table_data.rb +1 -1
- data/lib/table_tennis/util/colors.rb +12 -0
- data/lib/table_tennis/util/console.rb +23 -0
- data/lib/table_tennis/util/magic_options.rb +70 -40
- data/lib/table_tennis/util/strings.rb +65 -28
- data/lib/table_tennis/util/termbg.rb +16 -7
- data/lib/table_tennis/version.rb +1 -1
- data/lib/table_tennis.rb +1 -0
- data/table_tennis.gemspec +2 -2
- metadata +11 -8
- data/justfile +0 -72
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2dea31d2827226b7c2a9a92414ef69a99eddecafd6dc8bb9865c975fa48a9f2a
|
|
4
|
+
data.tar.gz: 2fe778598f290d8ede809971ab3ddf9bc12c16fa913542dac795708145230759
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 964ac069a29278563120f105ae399b42cb086aa332ba2e47e2816142b5fab6af80acd91a03e7b69b746ca59c85541d64e71c56a3409304e009b33b0ff7887820
|
|
7
|
+
data.tar.gz: 06ab17c03ed9b367b3a668275d3ac5030f3c865222d851c5545ee757dbe525a67cae60f045554775589191168016f3aa4587862b79f28fae2bda0f2f4b7959e5
|
data/.github/workflows/test.yml
CHANGED
|
@@ -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@
|
|
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
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", "~>
|
|
6
|
-
gem "image_optim", "~> 0.
|
|
7
|
-
gem "image_optim_pack", "~> 0.
|
|
8
|
-
gem "minitest", "~> 5.
|
|
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", "~>
|
|
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.
|
|
14
|
-
gem "simplecov", "~> 0
|
|
15
|
-
gem "standard", "~> 1.
|
|
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
|
@@ -69,32 +69,32 @@ options = {
|
|
|
69
69
|
|
|
70
70
|
#### Popular Options
|
|
71
71
|
|
|
72
|
-
| option
|
|
73
|
-
|
|
|
74
|
-
| `color_scales` | ─
|
|
75
|
-
| `columns`
|
|
76
|
-
| `headers`
|
|
77
|
-
| `mark`
|
|
78
|
-
| `row_numbers`
|
|
79
|
-
| `search`
|
|
80
|
-
| `separators`
|
|
81
|
-
| `title`
|
|
82
|
-
| `titleize`
|
|
83
|
-
| `zebra`
|
|
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
|
|
88
|
-
|
|
|
89
|
-
| `coerce`
|
|
90
|
-
| `color`
|
|
91
|
-
| `delims`
|
|
92
|
-
| `digits`
|
|
93
|
-
| `layout`
|
|
94
|
-
| `placeholder` | `"—"`
|
|
95
|
-
| `save`
|
|
96
|
-
| `strftime`
|
|
97
|
-
| `theme`
|
|
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`
|
|
124
|
-
|
|
|
123
|
+
| `:mark` | `:search` | `:row_numbers` and `:zebra` |
|
|
124
|
+
| ----------------------------------- | ------------------------------- | --------------------------------------------- |
|
|
125
125
|
|  |  |  |
|
|
126
126
|
|
|
127
127
|
### Links
|
|
@@ -172,6 +172,18 @@ 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
|
+
|
|
182
|
+
#### 0.0.7 (Aug '25)
|
|
183
|
+
|
|
184
|
+
- handle data that already contains ANSI colors (thx @ronaldtse, #12)
|
|
185
|
+
- don't crash if IO.console is nil (thx @ronaldtse, #14)
|
|
186
|
+
|
|
175
187
|
#### 0.0.6 (May '25)
|
|
176
188
|
|
|
177
189
|
- added `coerce:` option to disable string => numeric coercion
|
data/lib/table_tennis/config.rb
CHANGED
|
@@ -30,11 +30,10 @@ module TableTennis
|
|
|
30
30
|
|
|
31
31
|
SCHEMA = {
|
|
32
32
|
coerce: :bool,
|
|
33
|
-
color_scales: ->
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 =
|
|
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
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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 getch 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
when
|
|
162
|
-
|
|
163
|
-
when
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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,8 +6,13 @@ module TableTennis
|
|
|
6
6
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
|
+
ANSI_CODE = /\e\[[0-9;]*m/
|
|
10
|
+
|
|
11
|
+
# does this string contain ansi codes?
|
|
12
|
+
def painted?(str) = str.match?(/\e/)
|
|
13
|
+
|
|
9
14
|
# strip ansi codes
|
|
10
|
-
def unpaint(str) = str.gsub(
|
|
15
|
+
def unpaint(str) = str.gsub(ANSI_CODE, "")
|
|
11
16
|
|
|
12
17
|
# similar to rails titleize
|
|
13
18
|
def titleize(str)
|
|
@@ -18,8 +23,24 @@ module TableTennis
|
|
|
18
23
|
str
|
|
19
24
|
end
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
# measure width of text, with support for emojis, painted/ansi strings, etc
|
|
27
|
+
def width(str)
|
|
28
|
+
if simple?(str)
|
|
29
|
+
str.length
|
|
30
|
+
elsif painted?(str)
|
|
31
|
+
unpaint(str).length
|
|
32
|
+
else
|
|
33
|
+
Unicode::DisplayWidth.of(str)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# center text, like String#center but works with painted strings
|
|
38
|
+
def center(str, width)
|
|
39
|
+
# artificially inflate width to include escape codes
|
|
40
|
+
if painted?(str)
|
|
41
|
+
width += str.length - unpaint(str).length
|
|
42
|
+
end
|
|
43
|
+
str.center(width)
|
|
23
44
|
end
|
|
24
45
|
|
|
25
46
|
def hyperlink(str)
|
|
@@ -30,39 +51,55 @@ module TableTennis
|
|
|
30
51
|
end
|
|
31
52
|
end
|
|
32
53
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
60
|
+
def truncate(str, stop)
|
|
61
|
+
if str.bytesize <= stop
|
|
62
|
+
str
|
|
63
|
+
elsif simple?(str)
|
|
64
|
+
(str.length <= stop) ? str : "#{str[0, stop - 1]}#{ELLIPSIS}"
|
|
65
|
+
else
|
|
66
|
+
truncate0(str, stop)
|
|
38
67
|
end
|
|
68
|
+
end
|
|
39
69
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
70
|
+
# This is a slower truncate to handle ansi colors and wide characters like
|
|
71
|
+
# emojis. Inspired by piotrmurach/strings-truncation
|
|
72
|
+
def truncate0(text, stop)
|
|
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
|
|
47
81
|
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
82
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
w = Unicode::DisplayWidth.of(list[_1])
|
|
54
|
-
next if (width += w) <= stop
|
|
83
|
+
# what's next?
|
|
84
|
+
ch = scan.getch
|
|
55
85
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
92
|
|
|
62
|
-
|
|
93
|
+
# keep going
|
|
94
|
+
buf << ch
|
|
95
|
+
end
|
|
96
|
+
buf << Paint::NOTHING if painting
|
|
97
|
+
end.join
|
|
63
98
|
end
|
|
99
|
+
private_class_method :truncate0
|
|
64
100
|
|
|
65
|
-
|
|
101
|
+
# note that escape \e (0x1b) is excluded
|
|
102
|
+
SIMPLE = /\A[\x00-\x1a\x1c-\x7F–—…·‘’“”•áéíñóúÓ]*\Z/
|
|
66
103
|
|
|
67
104
|
# Is this a "simple" string? (no emojis, etc). Caches results for small
|
|
68
105
|
# 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 = getch)
|
|
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 = getch)
|
|
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 = getch)
|
|
131
133
|
buf << ch
|
|
132
134
|
break if type == "[" && buf.end_with?("R")
|
|
133
135
|
break if type == "]" && buf.end_with?(BEL, ST)
|
|
@@ -136,6 +138,10 @@ module TableTennis
|
|
|
136
138
|
end
|
|
137
139
|
private_class_method :read_term_response
|
|
138
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
|
+
|
|
139
145
|
#
|
|
140
146
|
# color math
|
|
141
147
|
#
|
|
@@ -162,7 +168,7 @@ module TableTennis
|
|
|
162
168
|
return if !respond_to?(:tcgetpgrp)
|
|
163
169
|
end
|
|
164
170
|
|
|
165
|
-
io =
|
|
171
|
+
io = Util::Console
|
|
166
172
|
if (ttypgrp = tcgetpgrp(io.fileno)) <= 0
|
|
167
173
|
debug("tcpgrp(#{io.fileno}) => #{ttypgrp}, errno=#{FFI.errno}")
|
|
168
174
|
return
|
|
@@ -180,6 +186,7 @@ module TableTennis
|
|
|
180
186
|
def load_ffi!
|
|
181
187
|
module_eval do
|
|
182
188
|
extend FFI::Library
|
|
189
|
+
|
|
183
190
|
ffi_lib "c"
|
|
184
191
|
attach_function :tcgetpgrp, %i[int], :int32
|
|
185
192
|
debug("ffi attach libc.tcgetpgrp => success")
|
|
@@ -202,7 +209,9 @@ module TableTennis
|
|
|
202
209
|
private_class_method :env_colorfgbg
|
|
203
210
|
|
|
204
211
|
def debug(s)
|
|
205
|
-
|
|
212
|
+
return if !ENV["TT_DEBUG"]
|
|
213
|
+
$stderr.write "termbg: #{s}\r\n"
|
|
214
|
+
$stderr.flush
|
|
206
215
|
end
|
|
207
216
|
private_class_method :debug
|
|
208
217
|
end
|
data/lib/table_tennis/version.rb
CHANGED
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"
|
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.
|
|
26
|
+
s.add_dependency "memo_wise", "~> 1.13"
|
|
27
27
|
s.add_dependency "paint", "~> 2.3"
|
|
28
|
-
s.add_dependency "unicode-display_width", "~> 3.
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -106,6 +108,7 @@ files:
|
|
|
106
108
|
- lib/table_tennis/table_data.rb
|
|
107
109
|
- lib/table_tennis/theme.rb
|
|
108
110
|
- lib/table_tennis/util/colors.rb
|
|
111
|
+
- lib/table_tennis/util/console.rb
|
|
109
112
|
- lib/table_tennis/util/identify.rb
|
|
110
113
|
- lib/table_tennis/util/inspectable.rb
|
|
111
114
|
- lib/table_tennis/util/magic_options.rb
|
|
@@ -143,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
143
146
|
- !ruby/object:Gem::Version
|
|
144
147
|
version: '0'
|
|
145
148
|
requirements: []
|
|
146
|
-
rubygems_version: 3.6.
|
|
149
|
+
rubygems_version: 3.6.9
|
|
147
150
|
specification_version: 4
|
|
148
151
|
summary: Stylish tables in your terminal.
|
|
149
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
|
-
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"
|