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,275 @@ | |
| 1 | 
            +
            module TableTennis
         | 
| 2 | 
            +
              module Util
         | 
| 3 | 
            +
                # Very complicated module for determining the terminal background color,
         | 
| 4 | 
            +
                # used to select the default color theme.
         | 
| 5 | 
            +
                module Termbg
         | 
| 6 | 
            +
                  prepend MemoWise
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  module_function
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  # get fg color as "#RRGGBB", or nil if we can't tel
         | 
| 11 | 
            +
                  def fg = osc_query(10) || env_colorfgbg&.fetch(0)
         | 
| 12 | 
            +
                  memo_wise self: :fg
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  # get bg color as "#RRGGBB", or nil if we can't tell
         | 
| 15 | 
            +
                  def bg = osc_query(11) || env_colorfgbg&.fetch(1)
         | 
| 16 | 
            +
                  memo_wise self: :bg
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  # mostly for debugging
         | 
| 19 | 
            +
                  def info
         | 
| 20 | 
            +
                    {
         | 
| 21 | 
            +
                      fg:,
         | 
| 22 | 
            +
                      bg:,
         | 
| 23 | 
            +
                      bg_luma: bg ? Colors.luma(bg) : nil,
         | 
| 24 | 
            +
                      tty?: "#{$stdin.tty?}/#{$stdout.tty?}/#{$stderr.tty?}",
         | 
| 25 | 
            +
                      in_foreground?: in_foreground?,
         | 
| 26 | 
            +
                      osc_supported?: osc_supported?,
         | 
| 27 | 
            +
                      "$COLORFGBG": ENV["COLORFGBG"],
         | 
| 28 | 
            +
                      "$TERM": ENV["TERM"],
         | 
| 29 | 
            +
                      colorfgbg: env_colorfgbg,
         | 
| 30 | 
            +
                    }
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  #
         | 
| 34 | 
            +
                  # osc_query
         | 
| 35 | 
            +
                  #
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # escape chars
         | 
| 38 | 
            +
                  ESC, BEL, ST, = "\e", "\a", "\e\\"
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  # Operating System Control queries
         | 
| 41 | 
            +
                  OSC_FG, OSC_BG = 10, 11
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def osc_supported?
         | 
| 44 | 
            +
                    host, platform, term = [
         | 
| 45 | 
            +
                      RbConfig::CONFIG["host_os"],
         | 
| 46 | 
            +
                      RbConfig::CONFIG["platform"],
         | 
| 47 | 
            +
                      ENV["TERM"],
         | 
| 48 | 
            +
                    ]
         | 
| 49 | 
            +
                    error = if host !~ /darwin|freebsd|linux|netbsd|openbsd/
         | 
| 50 | 
            +
                      "bad host"
         | 
| 51 | 
            +
                    elsif platform !~ /^(arm64|x86_64)/
         | 
| 52 | 
            +
                      "bad platform"
         | 
| 53 | 
            +
                    elsif term =~ /^(screen|tmux|dumb)/i
         | 
| 54 | 
            +
                      "bad TERM"
         | 
| 55 | 
            +
                    elsif ENV["ZELLIJ"]
         | 
| 56 | 
            +
                      "zellij"
         | 
| 57 | 
            +
                    end
         | 
| 58 | 
            +
                    if error
         | 
| 59 | 
            +
                      debug("osc_supported? #{{host:, platform:, term:}} => #{error}")
         | 
| 60 | 
            +
                      return false
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
                    debug("osc_supported? #{{host:, platform:, term:}} => success")
         | 
| 63 | 
            +
                    true
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  def osc_query(attr)
         | 
| 67 | 
            +
                    # let's be conservative
         | 
| 68 | 
            +
                    return if !osc_supported?
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    # mucking with the tty will hang if we are not in the foreground
         | 
| 71 | 
            +
                    return if !in_foreground?
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    # we can't touch stdout inside IO.console.raw, so save these for later
         | 
| 74 | 
            +
                    logs = []
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                    debug("osc_query(#{attr})")
         | 
| 77 | 
            +
                    begin
         | 
| 78 | 
            +
                      IO.console.raw do
         | 
| 79 | 
            +
                        logs << "  IO.console.raw"
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                        # we send two messages - the cursor query is widely supported, so we
         | 
| 82 | 
            +
                        # always end with that. if the first message is ignored we will still
         | 
| 83 | 
            +
                        # get an answer to the second so we know when to stop reading from stdin
         | 
| 84 | 
            +
                        msg = [].tap do
         | 
| 85 | 
            +
                          # operating system control with Ps=attr
         | 
| 86 | 
            +
                          _1 << "\e]#{attr};?\a"
         | 
| 87 | 
            +
                          # device status report with Ps = 6 (cursor position)
         | 
| 88 | 
            +
                          _1 << "\e[6n"
         | 
| 89 | 
            +
                        end.join
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                        logs << "  syswrite #{msg.inspect}"
         | 
| 92 | 
            +
                        IO.console.syswrite(msg)
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                        # there should always be at least one response. If this is a response to
         | 
| 95 | 
            +
                        # the cursor message, the first message didn't work
         | 
| 96 | 
            +
                        response1 = read_term_response.tap do
         | 
| 97 | 
            +
                          logs << "  got #{_1.inspect}"
         | 
| 98 | 
            +
                          if !(_1 && _1[1] == "]")
         | 
| 99 | 
            +
                            logs << "  not OSC, bailing"
         | 
| 100 | 
            +
                            return
         | 
| 101 | 
            +
                          end
         | 
| 102 | 
            +
                          response2 = read_term_response # skip cursor response
         | 
| 103 | 
            +
                          logs << "  got #{response2.inspect}"
         | 
| 104 | 
            +
                        end
         | 
| 105 | 
            +
                        decoded = decode_osc_response(response1)
         | 
| 106 | 
            +
                        logs << "=> #{decoded}"
         | 
| 107 | 
            +
                        decoded
         | 
| 108 | 
            +
                      end
         | 
| 109 | 
            +
                    ensure
         | 
| 110 | 
            +
                      logs.each { debug(_1) }
         | 
| 111 | 
            +
                    end
         | 
| 112 | 
            +
                  end
         | 
| 113 | 
            +
                  private_class_method :osc_query
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  # read a response, which could be either an OSC or cursor response
         | 
| 116 | 
            +
                  def read_term_response
         | 
| 117 | 
            +
                    # fast forward to ESC
         | 
| 118 | 
            +
                    loop do
         | 
| 119 | 
            +
                      return if !(ch = IO.console.getbyte&.chr)
         | 
| 120 | 
            +
                      break ch if ch == ESC
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
                    # next char should be either [ or ]
         | 
| 123 | 
            +
                    return if !(type = IO.console.getbyte&.chr)
         | 
| 124 | 
            +
                    return if !(type == "[" || type == "]")
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                    # now read the response. note that the response can end in different ways
         | 
| 127 | 
            +
                    # and we have to check for all of them
         | 
| 128 | 
            +
                    buf = "#{ESC}#{type}"
         | 
| 129 | 
            +
                    loop do
         | 
| 130 | 
            +
                      return if !(ch = IO.console.getbyte&.chr)
         | 
| 131 | 
            +
                      buf << ch
         | 
| 132 | 
            +
                      break if type == "[" && buf.end_with?("R")
         | 
| 133 | 
            +
                      break if type == "]" && buf.end_with?(BEL, ST)
         | 
| 134 | 
            +
                    end
         | 
| 135 | 
            +
                    buf
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
                  private_class_method :read_term_response
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                  #
         | 
| 140 | 
            +
                  # color math
         | 
| 141 | 
            +
                  #
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                  def decode_osc_response(response)
         | 
| 144 | 
            +
                    if response =~ %r{;rgb:([0-9a-f/]+)}i
         | 
| 145 | 
            +
                      rgb = $1.split("/")
         | 
| 146 | 
            +
                      return if rgb.length != 3
         | 
| 147 | 
            +
                      hex = rgb.join
         | 
| 148 | 
            +
                      return if hex.length % 3 != 0
         | 
| 149 | 
            +
                      Colors.to_hex(Colors.to_rgb(hex))
         | 
| 150 | 
            +
                    end
         | 
| 151 | 
            +
                  end
         | 
| 152 | 
            +
                  private_class_method :decode_osc_response
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                  #
         | 
| 155 | 
            +
                  # in_foreground?
         | 
| 156 | 
            +
                  #
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                  # returns true/false or nil (if unknown)
         | 
| 159 | 
            +
                  def in_foreground?
         | 
| 160 | 
            +
                    if !respond_to?(:tcgetpgrp)
         | 
| 161 | 
            +
                      load_ffi!
         | 
| 162 | 
            +
                      return if !respond_to?(:tcgetpgrp)
         | 
| 163 | 
            +
                    end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                    io = IO.console
         | 
| 166 | 
            +
                    if (ttypgrp = tcgetpgrp(io.fileno)) <= 0
         | 
| 167 | 
            +
                      debug("tcpgrp(#{io.fileno}) => #{ttypgrp}, errno=#{FFI.errno}")
         | 
| 168 | 
            +
                      return
         | 
| 169 | 
            +
                    end
         | 
| 170 | 
            +
                    debug("tcpgrp(#{io.fileno}) => #{ttypgrp}")
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                    # now compare against our process group
         | 
| 173 | 
            +
                    infg = Process.getpgrp == ttypgrp
         | 
| 174 | 
            +
                    debug("Process.getpgrp => #{Process.getpgrp}, in_foreground? #{infg}")
         | 
| 175 | 
            +
                    infg
         | 
| 176 | 
            +
                  end
         | 
| 177 | 
            +
                  private_class_method :in_foreground?
         | 
| 178 | 
            +
                  memo_wise self: :in_foreground?
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                  def load_ffi!
         | 
| 181 | 
            +
                    module_eval do
         | 
| 182 | 
            +
                      extend FFI::Library
         | 
| 183 | 
            +
                      ffi_lib "c"
         | 
| 184 | 
            +
                      attach_function :tcgetpgrp, %i[int], :int32
         | 
| 185 | 
            +
                      debug("ffi attach libc.tcgetpgrp => success")
         | 
| 186 | 
            +
                    end
         | 
| 187 | 
            +
                  rescue LoadError => ex
         | 
| 188 | 
            +
                    debug("ffi attach libc.tcgetpgrp => failed #{ex.message}")
         | 
| 189 | 
            +
                  end
         | 
| 190 | 
            +
                  private_class_method :load_ffi!
         | 
| 191 | 
            +
                  memo_wise self: :load_ffi!
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                  def env_colorfgbg(env = ENV["COLORFGBG"])
         | 
| 194 | 
            +
                    if env !~ /^\d+;\d+$/
         | 
| 195 | 
            +
                      debug("env_colorfgbg: COLORFGBG '#{env.inspect}'") if env
         | 
| 196 | 
            +
                      return
         | 
| 197 | 
            +
                    end
         | 
| 198 | 
            +
                    colors = env.split(";").map { Colors.ansi_color_to_hex(_1.to_i) }
         | 
| 199 | 
            +
                    debug("env_colorfgbg: #{env.inspect}' => #{colors.inspect}")
         | 
| 200 | 
            +
                    colors
         | 
| 201 | 
            +
                  end
         | 
| 202 | 
            +
                  private_class_method :env_colorfgbg
         | 
| 203 | 
            +
             | 
| 204 | 
            +
                  def debug(s)
         | 
| 205 | 
            +
                    puts "termbg: #{s}" if ENV["TM_DEBUG"]
         | 
| 206 | 
            +
                  end
         | 
| 207 | 
            +
                  private_class_method :debug
         | 
| 208 | 
            +
                end
         | 
| 209 | 
            +
              end
         | 
| 210 | 
            +
            end
         | 
| 211 | 
            +
             | 
| 212 | 
            +
            #
         | 
| 213 | 
            +
            # This comment is down here to avoid polluting ruby-lsp hover.
         | 
| 214 | 
            +
            #
         | 
| 215 | 
            +
            # Is the terminal dark or light? To answer this simple question, we need to
         | 
| 216 | 
            +
            # query the terminal to get the current background color.
         | 
| 217 | 
            +
            #
         | 
| 218 | 
            +
            #    https://github.com/dalance/termbg
         | 
| 219 | 
            +
            #    https://github.com/muesli/termenv
         | 
| 220 | 
            +
            #    https://github.com/rocky/shell-term-background
         | 
| 221 | 
            +
            #
         | 
| 222 | 
            +
            # This is absurdly difficult, so here is our approach:
         | 
| 223 | 
            +
            #
         | 
| 224 | 
            +
            # 1. Use OSC 11 to query the bgcolor using a magical escape sequence. We write
         | 
| 225 | 
            +
            #    the escape sequence to stdout and read the response from stdin. Not all
         | 
| 226 | 
            +
            #    terminals support this. Also, the terminal must be in "raw" mode for this
         | 
| 227 | 
            +
            #    to work. Raw mode means disable echo and disable line buffering.
         | 
| 228 | 
            +
            #
         | 
| 229 | 
            +
            #    https://en.wikipedia.org/wiki/ANSI_escape_code
         | 
| 230 | 
            +
            #    https://github.com/ruby/io-console/blob/master/lib/ffi/io/console/common.rb
         | 
| 231 | 
            +
            #    https://www.xfree86.org/4.8.0/ctlseqs.html
         | 
| 232 | 
            +
            #    https://stackoverflow.com/questions/2507337/
         | 
| 233 | 
            +
            #
         | 
| 234 | 
            +
            # 2. Because not all terminals support OSC 11, we actually send two magic escape
         | 
| 235 | 
            +
            #    sequences - OSC 11 and a "where is the cursor" message. Because the second
         | 
| 236 | 
            +
            #    query is universally supported we always get a response. That's how we
         | 
| 237 | 
            +
            #    avoid breaking stdin by over/under reading.
         | 
| 238 | 
            +
            #
         | 
| 239 | 
            +
            # 3. Mucking with the tty can hang (!) under some circumstances, which is a poor
         | 
| 240 | 
            +
            #    outcome for a fun ruby library like this. Only try this if we are "in the
         | 
| 241 | 
            +
            #    foreground". You can easily try this with the following command:
         | 
| 242 | 
            +
            #
         | 
| 243 | 
            +
            #    $ watchexec stty sane                         # this hangs
         | 
| 244 | 
            +
            #    $ watchexec --wrap-process=none stty sane     # this works fine
         | 
| 245 | 
            +
            #
         | 
| 246 | 
            +
            #    https://github.com/watchexec/watchexec/issues/874
         | 
| 247 | 
            +
            #    https://github.com/ruby/io-console
         | 
| 248 | 
            +
            #
         | 
| 249 | 
            +
            # 4. To detect if we are in the foreground, compare the process group against
         | 
| 250 | 
            +
            #    the group the process group that owns stdin. If they match, we are good to
         | 
| 251 | 
            +
            #    go.
         | 
| 252 | 
            +
            #
         | 
| 253 | 
            +
            #    https://unix.stackexchange.com/questions/736821/
         | 
| 254 | 
            +
            #
         | 
| 255 | 
            +
            # 5. Sadly, ruby does not have an easy way to get the process group of stdin.
         | 
| 256 | 
            +
            #    Instead, we have to use ruby-termios, ffi or $stdin.ioctl using a magic
         | 
| 257 | 
            +
            #    ioctl number (this magic number differs across platforms). I went with
         | 
| 258 | 
            +
            #    ffi.
         | 
| 259 | 
            +
            #
         | 
| 260 | 
            +
            #    https://github.com/arika/ruby-termios
         | 
| 261 | 
            +
            #    https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/ioctls.h
         | 
| 262 | 
            +
            #    https://github.com/swiftlang/swift/blob/main/stdlib/public/Platform/TiocConstants.swift
         | 
| 263 | 
            +
            #
         | 
| 264 | 
            +
            # 6. If the foreground is lighter than the background, the background is dark.
         | 
| 265 | 
            +
            #
         | 
| 266 | 
            +
            # 7. As a fallback to OSC 11, we also support $COLORFGBG. Terminals can set this
         | 
| 267 | 
            +
            #    environment variable to communicate colors to apps. Support is spotty,
         | 
| 268 | 
            +
            #    unfortunately. Even when COLORFGBG is set, it is not updated after the
         | 
| 269 | 
            +
            #    terminal is started. So it can be out of date if the user mucks with
         | 
| 270 | 
            +
            #    colors.
         | 
| 271 | 
            +
            #
         | 
| 272 | 
            +
            #    https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
         | 
| 273 | 
            +
            #    https://unix.stackexchange.com/questions/245378/
         | 
| 274 | 
            +
            #    https://www.xfree86.org/4.8.0/XLookupColor.3.html#toc4
         | 
| 275 | 
            +
            #
         | 
    
        data/lib/table_tennis.rb
    ADDED
    
    | @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            # gems
         | 
| 2 | 
            +
            require "csv"
         | 
| 3 | 
            +
            require "ffi"
         | 
| 4 | 
            +
            require "forwardable"
         | 
| 5 | 
            +
            require "io/console"
         | 
| 6 | 
            +
            require "memo_wise"
         | 
| 7 | 
            +
            require "paint"
         | 
| 8 | 
            +
            require "unicode/display_width"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            # mixins must be at top
         | 
| 11 | 
            +
            require "table_tennis/util/inspectable"
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            require "table_tennis/column"
         | 
| 14 | 
            +
            require "table_tennis/config"
         | 
| 15 | 
            +
            require "table_tennis/row"
         | 
| 16 | 
            +
            require "table_tennis/table_data"
         | 
| 17 | 
            +
            require "table_tennis/table"
         | 
| 18 | 
            +
            require "table_tennis/theme"
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            require "table_tennis/stage/base"
         | 
| 21 | 
            +
            require "table_tennis/stage/format"
         | 
| 22 | 
            +
            require "table_tennis/stage/layout"
         | 
| 23 | 
            +
            require "table_tennis/stage/painter"
         | 
| 24 | 
            +
            require "table_tennis/stage/render"
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            require "table_tennis/util/colors"
         | 
| 27 | 
            +
            require "table_tennis/util/scale"
         | 
| 28 | 
            +
            require "table_tennis/util/strings"
         | 
| 29 | 
            +
            require "table_tennis/util/termbg"
         | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            require_relative "lib/table_tennis/version"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Gem::Specification.new do |s|
         | 
| 4 | 
            +
              s.name = "table_tennis"
         | 
| 5 | 
            +
              s.version = TableTennis::VERSION
         | 
| 6 | 
            +
              s.authors = ["Adam Doppelt"]
         | 
| 7 | 
            +
              s.email = "amd@gurge.com"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              s.summary = "Stylish tables in your terminal."
         | 
| 10 | 
            +
              s.homepage = "http://github.com/gurgeous/table_tennis"
         | 
| 11 | 
            +
              s.license = "MIT"
         | 
| 12 | 
            +
              s.required_ruby_version = ">= 3.0.0"
         | 
| 13 | 
            +
              s.metadata = {
         | 
| 14 | 
            +
                "rubygems_mfa_required" => "true",
         | 
| 15 | 
            +
                "source_code_uri" => s.homepage,
         | 
| 16 | 
            +
              }
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              # what's in the gem?
         | 
| 19 | 
            +
              s.files = `git ls-files`.split("\n").grep_v(%r{^(bin|samples|test)/})
         | 
| 20 | 
            +
              s.require_paths = ["lib"]
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              # gem dependencies
         | 
| 23 | 
            +
              s.add_dependency "csv", "~> 3.3" # required for Ruby 3.4+
         | 
| 24 | 
            +
              s.add_dependency "ffi", "~> 1.17" # required for Ruby 3.2+
         | 
| 25 | 
            +
              s.add_dependency "memo_wise", "~> 1.11"
         | 
| 26 | 
            +
              s.add_dependency "paint", "~> 2.3"
         | 
| 27 | 
            +
              s.add_dependency "unicode-display_width", "~> 3.1"
         | 
| 28 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,145 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: table_tennis
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.0.1
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Adam Doppelt
         | 
| 8 | 
            +
            bindir: bin
         | 
| 9 | 
            +
            cert_chain: []
         | 
| 10 | 
            +
            date: 2025-04-11 00:00:00.000000000 Z
         | 
| 11 | 
            +
            dependencies:
         | 
| 12 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 13 | 
            +
              name: csv
         | 
| 14 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 15 | 
            +
                requirements:
         | 
| 16 | 
            +
                - - "~>"
         | 
| 17 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 18 | 
            +
                    version: '3.3'
         | 
| 19 | 
            +
              type: :runtime
         | 
| 20 | 
            +
              prerelease: false
         | 
| 21 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 22 | 
            +
                requirements:
         | 
| 23 | 
            +
                - - "~>"
         | 
| 24 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 25 | 
            +
                    version: '3.3'
         | 
| 26 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 27 | 
            +
              name: ffi
         | 
| 28 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 29 | 
            +
                requirements:
         | 
| 30 | 
            +
                - - "~>"
         | 
| 31 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 32 | 
            +
                    version: '1.17'
         | 
| 33 | 
            +
              type: :runtime
         | 
| 34 | 
            +
              prerelease: false
         | 
| 35 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 36 | 
            +
                requirements:
         | 
| 37 | 
            +
                - - "~>"
         | 
| 38 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 39 | 
            +
                    version: '1.17'
         | 
| 40 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 41 | 
            +
              name: memo_wise
         | 
| 42 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 43 | 
            +
                requirements:
         | 
| 44 | 
            +
                - - "~>"
         | 
| 45 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 46 | 
            +
                    version: '1.11'
         | 
| 47 | 
            +
              type: :runtime
         | 
| 48 | 
            +
              prerelease: false
         | 
| 49 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 50 | 
            +
                requirements:
         | 
| 51 | 
            +
                - - "~>"
         | 
| 52 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 53 | 
            +
                    version: '1.11'
         | 
| 54 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 55 | 
            +
              name: paint
         | 
| 56 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 57 | 
            +
                requirements:
         | 
| 58 | 
            +
                - - "~>"
         | 
| 59 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 60 | 
            +
                    version: '2.3'
         | 
| 61 | 
            +
              type: :runtime
         | 
| 62 | 
            +
              prerelease: false
         | 
| 63 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 64 | 
            +
                requirements:
         | 
| 65 | 
            +
                - - "~>"
         | 
| 66 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 67 | 
            +
                    version: '2.3'
         | 
| 68 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 69 | 
            +
              name: unicode-display_width
         | 
| 70 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 71 | 
            +
                requirements:
         | 
| 72 | 
            +
                - - "~>"
         | 
| 73 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 74 | 
            +
                    version: '3.1'
         | 
| 75 | 
            +
              type: :runtime
         | 
| 76 | 
            +
              prerelease: false
         | 
| 77 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 78 | 
            +
                requirements:
         | 
| 79 | 
            +
                - - "~>"
         | 
| 80 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 81 | 
            +
                    version: '3.1'
         | 
| 82 | 
            +
            email: amd@gurge.com
         | 
| 83 | 
            +
            executables: []
         | 
| 84 | 
            +
            extensions: []
         | 
| 85 | 
            +
            extra_rdoc_files: []
         | 
| 86 | 
            +
            files:
         | 
| 87 | 
            +
            - ".github/workflows/test.yml"
         | 
| 88 | 
            +
            - ".gitignore"
         | 
| 89 | 
            +
            - ".rubocop.yml"
         | 
| 90 | 
            +
            - Gemfile
         | 
| 91 | 
            +
            - Gemfile.lock
         | 
| 92 | 
            +
            - LICENSE
         | 
| 93 | 
            +
            - README.md
         | 
| 94 | 
            +
            - Rakefile
         | 
| 95 | 
            +
            - justfile
         | 
| 96 | 
            +
            - lib/table_tennis.rb
         | 
| 97 | 
            +
            - lib/table_tennis/column.rb
         | 
| 98 | 
            +
            - lib/table_tennis/config.rb
         | 
| 99 | 
            +
            - lib/table_tennis/row.rb
         | 
| 100 | 
            +
            - lib/table_tennis/stage/base.rb
         | 
| 101 | 
            +
            - lib/table_tennis/stage/format.rb
         | 
| 102 | 
            +
            - lib/table_tennis/stage/layout.rb
         | 
| 103 | 
            +
            - lib/table_tennis/stage/painter.rb
         | 
| 104 | 
            +
            - lib/table_tennis/stage/render.rb
         | 
| 105 | 
            +
            - lib/table_tennis/table.rb
         | 
| 106 | 
            +
            - lib/table_tennis/table_data.rb
         | 
| 107 | 
            +
            - lib/table_tennis/theme.rb
         | 
| 108 | 
            +
            - lib/table_tennis/util/colors.rb
         | 
| 109 | 
            +
            - lib/table_tennis/util/inspectable.rb
         | 
| 110 | 
            +
            - lib/table_tennis/util/scale.rb
         | 
| 111 | 
            +
            - lib/table_tennis/util/strings.rb
         | 
| 112 | 
            +
            - lib/table_tennis/util/termbg.rb
         | 
| 113 | 
            +
            - lib/table_tennis/version.rb
         | 
| 114 | 
            +
            - screenshots/dark.png
         | 
| 115 | 
            +
            - screenshots/droids.png
         | 
| 116 | 
            +
            - screenshots/hope.png
         | 
| 117 | 
            +
            - screenshots/light.png
         | 
| 118 | 
            +
            - screenshots/row_numbers.png
         | 
| 119 | 
            +
            - screenshots/scales.png
         | 
| 120 | 
            +
            - screenshots/themes.png
         | 
| 121 | 
            +
            - table_tennis.gemspec
         | 
| 122 | 
            +
            homepage: http://github.com/gurgeous/table_tennis
         | 
| 123 | 
            +
            licenses:
         | 
| 124 | 
            +
            - MIT
         | 
| 125 | 
            +
            metadata:
         | 
| 126 | 
            +
              rubygems_mfa_required: 'true'
         | 
| 127 | 
            +
              source_code_uri: http://github.com/gurgeous/table_tennis
         | 
| 128 | 
            +
            rdoc_options: []
         | 
| 129 | 
            +
            require_paths:
         | 
| 130 | 
            +
            - lib
         | 
| 131 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 132 | 
            +
              requirements:
         | 
| 133 | 
            +
              - - ">="
         | 
| 134 | 
            +
                - !ruby/object:Gem::Version
         | 
| 135 | 
            +
                  version: 3.0.0
         | 
| 136 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 137 | 
            +
              requirements:
         | 
| 138 | 
            +
              - - ">="
         | 
| 139 | 
            +
                - !ruby/object:Gem::Version
         | 
| 140 | 
            +
                  version: '0'
         | 
| 141 | 
            +
            requirements: []
         | 
| 142 | 
            +
            rubygems_version: 3.6.2
         | 
| 143 | 
            +
            specification_version: 4
         | 
| 144 | 
            +
            summary: Stylish tables in your terminal.
         | 
| 145 | 
            +
            test_files: []
         |