table_tennis 0.0.1
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 +7 -0
- data/.github/workflows/test.yml +26 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +58 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +122 -0
- data/LICENSE +21 -0
- data/README.md +154 -0
- data/Rakefile +12 -0
- data/justfile +75 -0
- data/lib/table_tennis/column.rb +41 -0
- data/lib/table_tennis/config.rb +221 -0
- data/lib/table_tennis/row.rb +9 -0
- data/lib/table_tennis/stage/base.rb +19 -0
- data/lib/table_tennis/stage/format.rb +68 -0
- data/lib/table_tennis/stage/layout.rb +76 -0
- data/lib/table_tennis/stage/painter.rb +84 -0
- data/lib/table_tennis/stage/render.rb +146 -0
- data/lib/table_tennis/table.rb +79 -0
- data/lib/table_tennis/table_data.rb +161 -0
- data/lib/table_tennis/theme.rb +92 -0
- data/lib/table_tennis/util/colors.rb +524 -0
- data/lib/table_tennis/util/inspectable.rb +25 -0
- data/lib/table_tennis/util/scale.rb +55 -0
- data/lib/table_tennis/util/strings.rb +62 -0
- data/lib/table_tennis/util/termbg.rb +275 -0
- data/lib/table_tennis/version.rb +3 -0
- data/lib/table_tennis.rb +29 -0
- data/screenshots/dark.png +0 -0
- data/screenshots/droids.png +0 -0
- data/screenshots/hope.png +0 -0
- data/screenshots/light.png +0 -0
- data/screenshots/row_numbers.png +0 -0
- data/screenshots/scales.png +0 -0
- data/screenshots/themes.png +0 -0
- data/table_tennis.gemspec +28 -0
- metadata +145 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
module TableTennis
|
2
|
+
# This class stores our data as rows & columns. The initialization is a little
|
3
|
+
# tricky due to memoization and some ordering issues, but basically works like
|
4
|
+
# this:
|
5
|
+
#
|
6
|
+
# 1) Start with input_rows.
|
7
|
+
# 2) Calculate `fat_rows` which is an array of hashes with all keys. This
|
8
|
+
# might be too much data because we haven't taken config.columns into
|
9
|
+
# account.
|
10
|
+
# 3) Use fat_rows to calculate rows & columns.
|
11
|
+
#
|
12
|
+
# Rows are hash tables and column names are symbols.
|
13
|
+
class TableData
|
14
|
+
prepend MemoWise
|
15
|
+
include Util::Inspectable
|
16
|
+
|
17
|
+
attr_accessor :config, :input_rows, :styles
|
18
|
+
|
19
|
+
def initialize(input_rows:, config: nil)
|
20
|
+
@config, @input_rows = config, input_rows
|
21
|
+
|
22
|
+
if !config && !ENV["MINITEST"]
|
23
|
+
raise ArgumentError, "must provide a config"
|
24
|
+
end
|
25
|
+
|
26
|
+
# We leave input_rows untouched so we can pass them back to the user for
|
27
|
+
# `mark`. The only strange case is if the user passed in a plain old Hash.
|
28
|
+
if @input_rows.is_a?(Hash)
|
29
|
+
@input_rows = @input_rows.map { |key, value| {key:, value:} }
|
30
|
+
elsif !@input_rows.is_a?(Enumerable)
|
31
|
+
raise ArgumentError, "input_rows must be an array of hash-like objects, not #{input_rows.class}"
|
32
|
+
end
|
33
|
+
|
34
|
+
@styles = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
# Lazily calculate the list of columns.
|
38
|
+
def columns
|
39
|
+
names = config&.columns
|
40
|
+
names ||= {}.tap do |memo|
|
41
|
+
fat_rows.each { |row| row.each_key { memo[_1] = 1 } }
|
42
|
+
end.keys
|
43
|
+
names.each do |name|
|
44
|
+
if !fat_rows.any? { _1.key?(name) }
|
45
|
+
raise ArgumentError, "specified column `#{name}` not found in any row of input data"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
names.map { Column.new(_1, self) }
|
49
|
+
end
|
50
|
+
memo_wise :columns
|
51
|
+
|
52
|
+
# Lazily calculate column names (always symbols)
|
53
|
+
def column_names = columns.map(&:name)
|
54
|
+
memo_wise :column_names
|
55
|
+
|
56
|
+
# fat_rows is an array of hashes with ALL keys (not just config.columns).
|
57
|
+
# rows is an array of Row objects with just the keys we want. We use
|
58
|
+
# memoization to cache the result of fat_rows, and then we create final rows
|
59
|
+
# with just the columns we want
|
60
|
+
def rows
|
61
|
+
fat_rows.map { Row.new(column_names, _1) }
|
62
|
+
end
|
63
|
+
memo_wise :rows
|
64
|
+
|
65
|
+
# currnet theme
|
66
|
+
def theme = Theme.new(config&.theme)
|
67
|
+
memo_wise :theme
|
68
|
+
|
69
|
+
# Set the style for a cell, row or column. The "style" is a
|
70
|
+
# theme symbol or hex color.
|
71
|
+
def set_style(style:, r: nil, c: nil)
|
72
|
+
styles[[r, c]] = style
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get the style for a cell, row or column.
|
76
|
+
def get_style(r: nil, c: nil)
|
77
|
+
styles[[r, c]]
|
78
|
+
end
|
79
|
+
|
80
|
+
# what is the width of the columns, not including chrome?
|
81
|
+
def data_width = columns.sum(&:width)
|
82
|
+
|
83
|
+
# how wide is the table?
|
84
|
+
def table_width = data_width + chrome_width
|
85
|
+
|
86
|
+
# layout math
|
87
|
+
#
|
88
|
+
# |•xxxx•|•xxxx•|•xxxx•|•xxxx•||•xxxx•|•xxxx•|•xxxx•|•xxxx•|
|
89
|
+
# ↑↑ ↑ ↑
|
90
|
+
# 12 3 <- three chrome chars per column │
|
91
|
+
# │
|
92
|
+
# extra chrome char at the end
|
93
|
+
#
|
94
|
+
def chrome_width = columns.length * 3 + 1
|
95
|
+
|
96
|
+
# this is handy for tests
|
97
|
+
def first_cell = rows.first.values&.first
|
98
|
+
|
99
|
+
# for debugging
|
100
|
+
def debug(str)
|
101
|
+
return if !config&.debug
|
102
|
+
str = "[#{Time.now.strftime("%H:%M:%S")}] #{str}"
|
103
|
+
str = str.ljust(@debug_width ||= IO.console.winsize[1])
|
104
|
+
puts Paint[str, :white, :green]
|
105
|
+
end
|
106
|
+
|
107
|
+
def debug_if_slow(str, &block)
|
108
|
+
tm = Time.now
|
109
|
+
yield.tap do
|
110
|
+
if (elapsed = Time.now - tm) > 0.01
|
111
|
+
debug(sprintf("%-40s [+%0.3fs]", str, elapsed))
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
# fat_rows is an array of hashes with ALL keys (not just config.columns).
|
119
|
+
def fat_rows
|
120
|
+
first_row = input_rows.first
|
121
|
+
array = if first_row.is_a?(Hash)
|
122
|
+
input_rows
|
123
|
+
elsif first_row.is_a?(Array)
|
124
|
+
indexes = first_row.each_index.to_a
|
125
|
+
input_rows.map { indexes.zip(_1).to_h }
|
126
|
+
elsif first_row.respond_to?(:to_h)
|
127
|
+
input_rows.map(&:to_h)
|
128
|
+
elsif first_row.respond_to?(:attributes)
|
129
|
+
input_rows.map(&:attributes)
|
130
|
+
else
|
131
|
+
raise ArgumentError, "unknown row type #{first_row.inspect}"
|
132
|
+
end
|
133
|
+
return [] if array.empty?
|
134
|
+
|
135
|
+
# this is faster with a lookup table
|
136
|
+
quick = array.first.keys.map { [_1, symbolize(_1)] }
|
137
|
+
fat_rows = array.map do |row|
|
138
|
+
row.transform_keys.with_index do |name, ii|
|
139
|
+
if (q = quick[ii]) && (name == q[0])
|
140
|
+
q[1]
|
141
|
+
else
|
142
|
+
symbolize(name)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# add row_numbers
|
148
|
+
if config&.row_numbers?
|
149
|
+
fat_rows = fat_rows.map.with_index do |row, r|
|
150
|
+
row.to_a.unshift([:"#", r + 1]).to_h
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
fat_rows
|
155
|
+
end
|
156
|
+
memo_wise :fat_rows
|
157
|
+
|
158
|
+
# helper for turning something into a symbol
|
159
|
+
def symbolize(obj) = obj.is_a?(Symbol) ? obj : obj.to_s.to_sym
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module TableTennis
|
2
|
+
# This class contains the current theme, as well as the definitions for all
|
3
|
+
# themes.
|
4
|
+
class Theme
|
5
|
+
prepend MemoWise
|
6
|
+
|
7
|
+
RESET = Paint::NOTHING
|
8
|
+
THEMES = {
|
9
|
+
dark: {
|
10
|
+
title: "#7f0",
|
11
|
+
chrome: "gray-500",
|
12
|
+
cell: "gray-200",
|
13
|
+
mark: %w[white blue-500],
|
14
|
+
search: %w[black yellow-300],
|
15
|
+
zebra: %w[white #333],
|
16
|
+
},
|
17
|
+
light: {
|
18
|
+
title: "blue-600",
|
19
|
+
chrome: "#bbb",
|
20
|
+
cell: "gray-800",
|
21
|
+
mark: %w[white blue-500],
|
22
|
+
search: %w[black yellow-300],
|
23
|
+
zebra: %w[black gray-200],
|
24
|
+
},
|
25
|
+
ansi: {
|
26
|
+
title: :green,
|
27
|
+
chrome: %i[faint default],
|
28
|
+
cell: :default,
|
29
|
+
mark: %i[white blue],
|
30
|
+
search: %i[white magenta],
|
31
|
+
zebra: nil, # not supported
|
32
|
+
},
|
33
|
+
}
|
34
|
+
THEME_KEYS = THEMES[:dark].keys
|
35
|
+
BG = [nil, :default]
|
36
|
+
|
37
|
+
attr_reader :name
|
38
|
+
|
39
|
+
def initialize(name)
|
40
|
+
@name = name
|
41
|
+
raise ArgumentError, "unknown theme #{name}, should be one of #{THEMES.keys}" if !THEMES.key?(name)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Value is one of the following:
|
45
|
+
# - theme.symbol (like ":title")
|
46
|
+
# - a color that works with Colors.get (#fff, or :bold, or "steelblue")
|
47
|
+
# - an array of colors
|
48
|
+
def codes(value)
|
49
|
+
# theme key?
|
50
|
+
if value.is_a?(Symbol) && THEME_KEYS.include?(value)
|
51
|
+
value = THEMES[name][value]
|
52
|
+
end
|
53
|
+
# turn value(s) into colors
|
54
|
+
colors = Array(value).map { Util::Colors.get(_1) }
|
55
|
+
return if colors == [] || colors == [nil]
|
56
|
+
|
57
|
+
# turn colors into ansi codes
|
58
|
+
Paint["", *colors].gsub(RESET, "")
|
59
|
+
end
|
60
|
+
memo_wise :codes
|
61
|
+
|
62
|
+
# Apply colors to a string. Value is one of the following:
|
63
|
+
# - theme.symbol (like ":title")
|
64
|
+
# - a color that works with Colors.get (#fff, or :bold, or "steelblue")
|
65
|
+
# - an array of colors
|
66
|
+
def paint(str, value)
|
67
|
+
if (codes = codes(value))
|
68
|
+
str = str.gsub(RESET, "#{RESET}#{codes}")
|
69
|
+
str = "#{codes}#{str}#{RESET}"
|
70
|
+
end
|
71
|
+
str
|
72
|
+
end
|
73
|
+
|
74
|
+
# for debugging, mostly
|
75
|
+
def self.info
|
76
|
+
sample = if !Config.detect_color?
|
77
|
+
"(color is disabled)"
|
78
|
+
elsif Config.detect_theme == :light
|
79
|
+
Paint[" light theme ", "#000", "#eee", :bold]
|
80
|
+
elsif Config.detect_theme == :dark
|
81
|
+
Paint[" dark theme ", "#fff", "#444", :bold]
|
82
|
+
end
|
83
|
+
|
84
|
+
{
|
85
|
+
detect_color?: Config.detect_color?,
|
86
|
+
detect_theme: Config.detect_theme,
|
87
|
+
sample:,
|
88
|
+
terminal_dark?: Config.terminal_dark?,
|
89
|
+
}.merge(Util::Termbg.info)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|