asciinema_win 0.1.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 +7 -0
- data/README.md +575 -0
- data/exe/asciinema_win +17 -0
- data/lib/asciinema_win/ansi_parser.rb +437 -0
- data/lib/asciinema_win/asciicast.rb +537 -0
- data/lib/asciinema_win/cli.rb +591 -0
- data/lib/asciinema_win/export.rb +780 -0
- data/lib/asciinema_win/output_organizer.rb +276 -0
- data/lib/asciinema_win/player.rb +348 -0
- data/lib/asciinema_win/recorder.rb +480 -0
- data/lib/asciinema_win/screen_buffer.rb +375 -0
- data/lib/asciinema_win/themes.rb +334 -0
- data/lib/asciinema_win/version.rb +6 -0
- data/lib/asciinema_win.rb +153 -0
- data/lib/rich/_palettes.rb +148 -0
- data/lib/rich/box.rb +342 -0
- data/lib/rich/cells.rb +512 -0
- data/lib/rich/color.rb +628 -0
- data/lib/rich/color_triplet.rb +220 -0
- data/lib/rich/console.rb +549 -0
- data/lib/rich/control.rb +332 -0
- data/lib/rich/json.rb +254 -0
- data/lib/rich/layout.rb +314 -0
- data/lib/rich/markdown.rb +509 -0
- data/lib/rich/markup.rb +175 -0
- data/lib/rich/panel.rb +311 -0
- data/lib/rich/progress.rb +430 -0
- data/lib/rich/segment.rb +387 -0
- data/lib/rich/style.rb +433 -0
- data/lib/rich/syntax.rb +1145 -0
- data/lib/rich/table.rb +525 -0
- data/lib/rich/terminal_theme.rb +126 -0
- data/lib/rich/text.rb +433 -0
- data/lib/rich/tree.rb +220 -0
- data/lib/rich/version.rb +5 -0
- data/lib/rich/win32_console.rb +859 -0
- data/lib/rich.rb +108 -0
- metadata +123 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# AsciinemaWin - Native Windows Terminal Recorder in Pure Ruby
|
|
4
|
+
#
|
|
5
|
+
# A zero-dependency terminal recording and playback system for Windows.
|
|
6
|
+
# Uses Ruby's built-in Fiddle for Win32 Console API access and integrates
|
|
7
|
+
# Rich-Ruby for terminal rendering. Compatible with asciinema's asciicast v2 format.
|
|
8
|
+
#
|
|
9
|
+
# @example Record a terminal session
|
|
10
|
+
# AsciinemaWin.record("session.cast", title: "My Recording")
|
|
11
|
+
#
|
|
12
|
+
# @example Play back a recording
|
|
13
|
+
# AsciinemaWin.play("session.cast", speed: 1.5)
|
|
14
|
+
#
|
|
15
|
+
# @example Get recording info
|
|
16
|
+
# info = AsciinemaWin.info("session.cast")
|
|
17
|
+
# puts "Duration: #{info[:duration]}s"
|
|
18
|
+
|
|
19
|
+
module AsciinemaWin
|
|
20
|
+
# Base error class for all AsciinemaWin errors
|
|
21
|
+
class Error < StandardError; end
|
|
22
|
+
|
|
23
|
+
# Raised when recording fails
|
|
24
|
+
class RecordingError < Error; end
|
|
25
|
+
|
|
26
|
+
# Raised when playback fails
|
|
27
|
+
class PlaybackError < Error; end
|
|
28
|
+
|
|
29
|
+
# Raised when file format is invalid
|
|
30
|
+
class FormatError < Error; end
|
|
31
|
+
|
|
32
|
+
# Raised when platform is not supported
|
|
33
|
+
class PlatformError < Error; end
|
|
34
|
+
|
|
35
|
+
# Raised when export fails
|
|
36
|
+
class ExportError < Error; end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
require_relative "rich"
|
|
40
|
+
require_relative "asciinema_win/version"
|
|
41
|
+
require_relative "asciinema_win/screen_buffer"
|
|
42
|
+
require_relative "asciinema_win/asciicast"
|
|
43
|
+
require_relative "asciinema_win/recorder"
|
|
44
|
+
require_relative "asciinema_win/player"
|
|
45
|
+
require_relative "asciinema_win/themes"
|
|
46
|
+
require_relative "asciinema_win/ansi_parser"
|
|
47
|
+
require_relative "asciinema_win/output_organizer"
|
|
48
|
+
require_relative "asciinema_win/export"
|
|
49
|
+
require_relative "asciinema_win/cli"
|
|
50
|
+
|
|
51
|
+
module AsciinemaWin
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# Record a terminal session to a file
|
|
55
|
+
#
|
|
56
|
+
# @param output_path [String] Path to save the recording
|
|
57
|
+
# @param title [String, nil] Recording title
|
|
58
|
+
# @param command [String, nil] Command to record (runs in subprocess)
|
|
59
|
+
# @param idle_time_limit [Float] Maximum idle time between events
|
|
60
|
+
# @param env_vars [Array<String>] Environment variables to capture
|
|
61
|
+
# @yield [Recorder] Optional block for manual recording control
|
|
62
|
+
# @return [void]
|
|
63
|
+
# @raise [RecordingError] If recording fails
|
|
64
|
+
# @raise [PlatformError] If not running on Windows
|
|
65
|
+
#
|
|
66
|
+
# @example Record interactively
|
|
67
|
+
# AsciinemaWin.record("session.cast", title: "Demo") do |rec|
|
|
68
|
+
# # Recording happens until block exits or user presses Ctrl+D
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# @example Record a command
|
|
72
|
+
# AsciinemaWin.record("session.cast", command: "dir /s")
|
|
73
|
+
def record(output_path, title: nil, command: nil, idle_time_limit: 2.0, env_vars: %w[SHELL TERM], &block)
|
|
74
|
+
ensure_windows!
|
|
75
|
+
|
|
76
|
+
recorder = Recorder.new(
|
|
77
|
+
title: title,
|
|
78
|
+
command: command,
|
|
79
|
+
idle_time_limit: idle_time_limit,
|
|
80
|
+
env_vars: env_vars
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
recorder.record(output_path, &block)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Play back a recording from a file
|
|
87
|
+
#
|
|
88
|
+
# @param input_path [String] Path to the recording file
|
|
89
|
+
# @param speed [Float] Playback speed multiplier (1.0 = normal)
|
|
90
|
+
# @param idle_time_limit [Float, nil] Cap idle time between frames
|
|
91
|
+
# @param pause_on_markers [Boolean] Pause playback at markers
|
|
92
|
+
# @return [void]
|
|
93
|
+
# @raise [PlaybackError] If playback fails
|
|
94
|
+
# @raise [FormatError] If file format is invalid
|
|
95
|
+
#
|
|
96
|
+
# @example Normal playback
|
|
97
|
+
# AsciinemaWin.play("session.cast")
|
|
98
|
+
#
|
|
99
|
+
# @example Fast playback
|
|
100
|
+
# AsciinemaWin.play("session.cast", speed: 2.0)
|
|
101
|
+
def play(input_path, speed: 1.0, idle_time_limit: nil, pause_on_markers: false)
|
|
102
|
+
player = Player.new(
|
|
103
|
+
speed: speed,
|
|
104
|
+
idle_time_limit: idle_time_limit,
|
|
105
|
+
pause_on_markers: pause_on_markers
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
player.play(input_path)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Output recording to stdout without timing (for piping)
|
|
112
|
+
#
|
|
113
|
+
# @param input_path [String] Path to the recording file
|
|
114
|
+
# @return [void]
|
|
115
|
+
# @raise [FormatError] If file format is invalid
|
|
116
|
+
def cat(input_path)
|
|
117
|
+
player = Player.new(speed: Float::INFINITY)
|
|
118
|
+
player.play(input_path)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Get metadata about a recording
|
|
122
|
+
#
|
|
123
|
+
# @param input_path [String] Path to the recording file
|
|
124
|
+
# @return [Hash] Recording metadata including width, height, duration, title
|
|
125
|
+
# @raise [FormatError] If file format is invalid
|
|
126
|
+
#
|
|
127
|
+
# @example
|
|
128
|
+
# info = AsciinemaWin.info("session.cast")
|
|
129
|
+
# puts "Size: #{info[:width]}x#{info[:height]}"
|
|
130
|
+
# puts "Duration: #{info[:duration]}s"
|
|
131
|
+
def info(input_path)
|
|
132
|
+
Asciicast::Reader.info(input_path)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Run the CLI with the given arguments
|
|
136
|
+
#
|
|
137
|
+
# @param args [Array<String>] Command-line arguments
|
|
138
|
+
# @return [Integer] Exit code
|
|
139
|
+
def run(args = ARGV)
|
|
140
|
+
CLI.run(args)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# @raise [PlatformError] If not running on Windows
|
|
146
|
+
# @return [void]
|
|
147
|
+
def ensure_windows!
|
|
148
|
+
return if Gem.win_platform?
|
|
149
|
+
|
|
150
|
+
raise PlatformError, "AsciinemaWin requires Windows. Use the standard asciinema on other platforms."
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "color_triplet"
|
|
4
|
+
|
|
5
|
+
module Rich
|
|
6
|
+
# Color palette definitions for terminal color systems.
|
|
7
|
+
# Provides lookup tables for standard 16-color, 256-color (8-bit),
|
|
8
|
+
# and Windows console color palettes.
|
|
9
|
+
module Palettes
|
|
10
|
+
# Standard 16-color ANSI palette (colors 0-15)
|
|
11
|
+
# These are the typical default colors, but terminals may customize them
|
|
12
|
+
STANDARD_PALETTE = [
|
|
13
|
+
ColorTriplet.new(0, 0, 0), # 0: Black
|
|
14
|
+
ColorTriplet.new(128, 0, 0), # 1: Red
|
|
15
|
+
ColorTriplet.new(0, 128, 0), # 2: Green
|
|
16
|
+
ColorTriplet.new(128, 128, 0), # 3: Yellow
|
|
17
|
+
ColorTriplet.new(0, 0, 128), # 4: Blue
|
|
18
|
+
ColorTriplet.new(128, 0, 128), # 5: Magenta
|
|
19
|
+
ColorTriplet.new(0, 128, 128), # 6: Cyan
|
|
20
|
+
ColorTriplet.new(192, 192, 192), # 7: White
|
|
21
|
+
ColorTriplet.new(128, 128, 128), # 8: Bright Black (Gray)
|
|
22
|
+
ColorTriplet.new(255, 0, 0), # 9: Bright Red
|
|
23
|
+
ColorTriplet.new(0, 255, 0), # 10: Bright Green
|
|
24
|
+
ColorTriplet.new(255, 255, 0), # 11: Bright Yellow
|
|
25
|
+
ColorTriplet.new(0, 0, 255), # 12: Bright Blue
|
|
26
|
+
ColorTriplet.new(255, 0, 255), # 13: Bright Magenta
|
|
27
|
+
ColorTriplet.new(0, 255, 255), # 14: Bright Cyan
|
|
28
|
+
ColorTriplet.new(255, 255, 255) # 15: Bright White
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# Windows Console palette (slightly different from ANSI standard)
|
|
32
|
+
WINDOWS_PALETTE = [
|
|
33
|
+
ColorTriplet.new(12, 12, 12), # 0: Black
|
|
34
|
+
ColorTriplet.new(197, 15, 31), # 1: Red
|
|
35
|
+
ColorTriplet.new(19, 161, 14), # 2: Green
|
|
36
|
+
ColorTriplet.new(193, 156, 0), # 3: Yellow
|
|
37
|
+
ColorTriplet.new(0, 55, 218), # 4: Blue
|
|
38
|
+
ColorTriplet.new(136, 23, 152), # 5: Magenta
|
|
39
|
+
ColorTriplet.new(58, 150, 221), # 6: Cyan
|
|
40
|
+
ColorTriplet.new(204, 204, 204), # 7: White
|
|
41
|
+
ColorTriplet.new(118, 118, 118), # 8: Bright Black (Gray)
|
|
42
|
+
ColorTriplet.new(231, 72, 86), # 9: Bright Red
|
|
43
|
+
ColorTriplet.new(22, 198, 12), # 10: Bright Green
|
|
44
|
+
ColorTriplet.new(249, 241, 165), # 11: Bright Yellow
|
|
45
|
+
ColorTriplet.new(59, 120, 255), # 12: Bright Blue
|
|
46
|
+
ColorTriplet.new(180, 0, 158), # 13: Bright Magenta
|
|
47
|
+
ColorTriplet.new(97, 214, 214), # 14: Bright Cyan
|
|
48
|
+
ColorTriplet.new(242, 242, 242) # 15: Bright White
|
|
49
|
+
].freeze
|
|
50
|
+
|
|
51
|
+
# Generate the 256-color (8-bit) palette
|
|
52
|
+
# Colors 0-15: Standard colors
|
|
53
|
+
# Colors 16-231: 6x6x6 color cube
|
|
54
|
+
# Colors 232-255: Grayscale ramp
|
|
55
|
+
EIGHT_BIT_PALETTE = begin
|
|
56
|
+
palette = []
|
|
57
|
+
|
|
58
|
+
# Colors 0-15: Standard palette
|
|
59
|
+
STANDARD_PALETTE.each { |color| palette << color }
|
|
60
|
+
|
|
61
|
+
# Colors 16-231: 6x6x6 color cube
|
|
62
|
+
# Each component can be 0, 95, 135, 175, 215, or 255
|
|
63
|
+
cube_values = [0, 95, 135, 175, 215, 255]
|
|
64
|
+
(0...6).each do |r|
|
|
65
|
+
(0...6).each do |g|
|
|
66
|
+
(0...6).each do |b|
|
|
67
|
+
palette << ColorTriplet.new(cube_values[r], cube_values[g], cube_values[b])
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Colors 232-255: Grayscale ramp (24 shades, excluding black and white)
|
|
73
|
+
(0...24).each do |i|
|
|
74
|
+
gray = 8 + i * 10
|
|
75
|
+
palette << ColorTriplet.new(gray, gray, gray)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
palette.freeze
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
# Find the closest color in a palette
|
|
83
|
+
# @param triplet [ColorTriplet] Color to match
|
|
84
|
+
# @param palette [Array<ColorTriplet>] Palette to search
|
|
85
|
+
# @param start_index [Integer] Starting index in palette
|
|
86
|
+
# @param end_index [Integer] Ending index in palette (exclusive)
|
|
87
|
+
# @return [Integer] Index of closest matching color
|
|
88
|
+
def match_color(triplet, palette: EIGHT_BIT_PALETTE, start_index: 0, end_index: nil)
|
|
89
|
+
end_index ||= palette.length
|
|
90
|
+
|
|
91
|
+
best_index = start_index
|
|
92
|
+
best_distance = Float::INFINITY
|
|
93
|
+
|
|
94
|
+
(start_index...end_index).each do |i|
|
|
95
|
+
distance = triplet.weighted_distance(palette[i])
|
|
96
|
+
if distance < best_distance
|
|
97
|
+
best_distance = distance
|
|
98
|
+
best_index = i
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
best_index
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Match to standard 16-color palette
|
|
106
|
+
# @param triplet [ColorTriplet] Color to match
|
|
107
|
+
# @return [Integer] Standard color index (0-15)
|
|
108
|
+
def match_standard(triplet)
|
|
109
|
+
match_color(triplet, palette: STANDARD_PALETTE, start_index: 0, end_index: 16)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Match to 8-bit palette (256 colors)
|
|
113
|
+
# @param triplet [ColorTriplet] Color to match
|
|
114
|
+
# @return [Integer] 8-bit color index (0-255)
|
|
115
|
+
def match_eight_bit(triplet)
|
|
116
|
+
match_color(triplet, palette: EIGHT_BIT_PALETTE, start_index: 0, end_index: 256)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Match to Windows console palette
|
|
120
|
+
# @param triplet [ColorTriplet] Color to match
|
|
121
|
+
# @return [Integer] Windows color index (0-15)
|
|
122
|
+
def match_windows(triplet)
|
|
123
|
+
match_color(triplet, palette: WINDOWS_PALETTE, start_index: 0, end_index: 16)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get a color from the 8-bit palette
|
|
127
|
+
# @param index [Integer] Color index (0-255)
|
|
128
|
+
# @return [ColorTriplet]
|
|
129
|
+
def get_eight_bit(index)
|
|
130
|
+
EIGHT_BIT_PALETTE[index.clamp(0, 255)]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get a color from the standard palette
|
|
134
|
+
# @param index [Integer] Color index (0-15)
|
|
135
|
+
# @return [ColorTriplet]
|
|
136
|
+
def get_standard(index)
|
|
137
|
+
STANDARD_PALETTE[index.clamp(0, 15)]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get a color from the Windows palette
|
|
141
|
+
# @param index [Integer] Color index (0-15)
|
|
142
|
+
# @return [ColorTriplet]
|
|
143
|
+
def get_windows(index)
|
|
144
|
+
WINDOWS_PALETTE[index.clamp(0, 15)]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
data/lib/rich/box.rb
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rich
|
|
4
|
+
# Box drawing character sets for borders and tables
|
|
5
|
+
class Box
|
|
6
|
+
# @return [String] Top-left corner
|
|
7
|
+
attr_reader :top_left
|
|
8
|
+
|
|
9
|
+
# @return [String] Top-right corner
|
|
10
|
+
attr_reader :top_right
|
|
11
|
+
|
|
12
|
+
# @return [String] Bottom-left corner
|
|
13
|
+
attr_reader :bottom_left
|
|
14
|
+
|
|
15
|
+
# @return [String] Bottom-right corner
|
|
16
|
+
attr_reader :bottom_right
|
|
17
|
+
|
|
18
|
+
# @return [String] Horizontal line
|
|
19
|
+
attr_reader :horizontal
|
|
20
|
+
|
|
21
|
+
# @return [String] Vertical line
|
|
22
|
+
attr_reader :vertical
|
|
23
|
+
|
|
24
|
+
# @return [String] Left T-junction
|
|
25
|
+
attr_reader :left_t
|
|
26
|
+
|
|
27
|
+
# @return [String] Right T-junction
|
|
28
|
+
attr_reader :right_t
|
|
29
|
+
|
|
30
|
+
# @return [String] Top T-junction
|
|
31
|
+
attr_reader :top_t
|
|
32
|
+
|
|
33
|
+
# @return [String] Bottom T-junction
|
|
34
|
+
attr_reader :bottom_t
|
|
35
|
+
|
|
36
|
+
# @return [String] Cross/plus junction
|
|
37
|
+
attr_reader :cross
|
|
38
|
+
|
|
39
|
+
# @return [String] Thick horizontal (for headers)
|
|
40
|
+
attr_reader :thick_horizontal
|
|
41
|
+
|
|
42
|
+
# @return [String] Thick left T-junction
|
|
43
|
+
attr_reader :thick_left_t
|
|
44
|
+
|
|
45
|
+
# @return [String] Thick right T-junction
|
|
46
|
+
attr_reader :thick_right_t
|
|
47
|
+
|
|
48
|
+
# @return [String] Thick cross
|
|
49
|
+
attr_reader :thick_cross
|
|
50
|
+
|
|
51
|
+
def initialize(
|
|
52
|
+
top_left:,
|
|
53
|
+
top_right:,
|
|
54
|
+
bottom_left:,
|
|
55
|
+
bottom_right:,
|
|
56
|
+
horizontal:,
|
|
57
|
+
vertical:,
|
|
58
|
+
left_t: nil,
|
|
59
|
+
right_t: nil,
|
|
60
|
+
top_t: nil,
|
|
61
|
+
bottom_t: nil,
|
|
62
|
+
cross: nil,
|
|
63
|
+
thick_horizontal: nil,
|
|
64
|
+
thick_left_t: nil,
|
|
65
|
+
thick_right_t: nil,
|
|
66
|
+
thick_cross: nil
|
|
67
|
+
)
|
|
68
|
+
@top_left = top_left
|
|
69
|
+
@top_right = top_right
|
|
70
|
+
@bottom_left = bottom_left
|
|
71
|
+
@bottom_right = bottom_right
|
|
72
|
+
@horizontal = horizontal
|
|
73
|
+
@vertical = vertical
|
|
74
|
+
@left_t = left_t || vertical
|
|
75
|
+
@right_t = right_t || vertical
|
|
76
|
+
@top_t = top_t || horizontal
|
|
77
|
+
@bottom_t = bottom_t || horizontal
|
|
78
|
+
@cross = cross || "+"
|
|
79
|
+
@thick_horizontal = thick_horizontal || horizontal
|
|
80
|
+
@thick_left_t = thick_left_t || @left_t
|
|
81
|
+
@thick_right_t = thick_right_t || @right_t
|
|
82
|
+
@thick_cross = thick_cross || @cross
|
|
83
|
+
freeze
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get the top edge
|
|
87
|
+
# @param width [Integer] Width of content
|
|
88
|
+
# @return [String]
|
|
89
|
+
def top_edge(width)
|
|
90
|
+
"#{@top_left}#{@horizontal * [0, width - 2].max}#{@top_right}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get the bottom edge
|
|
94
|
+
# @param width [Integer] Width of content
|
|
95
|
+
# @return [String]
|
|
96
|
+
def bottom_edge(width)
|
|
97
|
+
"#{@bottom_left}#{@horizontal * [0, width - 2].max}#{@bottom_right}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get the row separator
|
|
101
|
+
# @param width_or_cells [Integer, Array] Total width or array of cell contents
|
|
102
|
+
# @param widths [Array<Integer>, nil] Array of column widths
|
|
103
|
+
# @return [String]
|
|
104
|
+
def row(width_or_cells, widths = nil)
|
|
105
|
+
if widths
|
|
106
|
+
# Table row separator with multiple columns
|
|
107
|
+
parts = widths.map { |w| @horizontal * w }
|
|
108
|
+
"#{@left_t}#{parts.join(@cross)}#{@right_t}"
|
|
109
|
+
else
|
|
110
|
+
# Single column separator
|
|
111
|
+
width = width_or_cells.is_a?(Integer) ? width_or_cells : Cells.cell_len(width_or_cells.to_s)
|
|
112
|
+
"#{@left_t}#{@horizontal * [0, width - 2].max}#{@right_t}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
alias top top_edge
|
|
117
|
+
alias bottom bottom_edge
|
|
118
|
+
|
|
119
|
+
# Get a content row
|
|
120
|
+
# @param content [String] Content
|
|
121
|
+
# @param width [Integer] Width to pad to
|
|
122
|
+
# @param align [Symbol] Alignment (:left, :center, :right)
|
|
123
|
+
# @return [String]
|
|
124
|
+
def content_row(content, width, align: :left)
|
|
125
|
+
content_len = Cells.cell_len(content)
|
|
126
|
+
padding = width - content_len
|
|
127
|
+
|
|
128
|
+
case align
|
|
129
|
+
when :center
|
|
130
|
+
left_pad = padding / 2
|
|
131
|
+
right_pad = padding - left_pad
|
|
132
|
+
"#{@vertical}#{' ' * left_pad}#{content}#{' ' * right_pad}#{@vertical}"
|
|
133
|
+
when :right
|
|
134
|
+
"#{@vertical}#{' ' * padding}#{content}#{@vertical}"
|
|
135
|
+
else # :left
|
|
136
|
+
"#{@vertical}#{content}#{' ' * padding}#{@vertical}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get header separator (thicker line)
|
|
141
|
+
# @param width [Integer] Width
|
|
142
|
+
# @return [String]
|
|
143
|
+
def header_separator(width)
|
|
144
|
+
"#{@thick_left_t}#{@thick_horizontal * width}#{@thick_right_t}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Substitute ASCII characters for box characters
|
|
148
|
+
# @return [Box]
|
|
149
|
+
def to_ascii
|
|
150
|
+
ASCII
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Check if this is the ASCII box
|
|
154
|
+
# @return [Boolean]
|
|
155
|
+
def ascii?
|
|
156
|
+
self == ASCII
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Predefined box styles
|
|
160
|
+
class << self
|
|
161
|
+
# ASCII characters only
|
|
162
|
+
def ascii
|
|
163
|
+
ASCII
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Standard Unicode box drawing
|
|
167
|
+
def square
|
|
168
|
+
SQUARE
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Rounded corners
|
|
172
|
+
def rounded
|
|
173
|
+
ROUNDED
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Heavy/thick lines
|
|
177
|
+
def heavy
|
|
178
|
+
HEAVY
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Double lines
|
|
182
|
+
def double
|
|
183
|
+
DOUBLE
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Minimal (no corners)
|
|
187
|
+
def minimal
|
|
188
|
+
MINIMAL
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Simple horizontal lines only
|
|
192
|
+
def simple
|
|
193
|
+
SIMPLE
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# No border
|
|
197
|
+
def none
|
|
198
|
+
NONE
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# ASCII box (works everywhere)
|
|
203
|
+
ASCII = new(
|
|
204
|
+
top_left: "+",
|
|
205
|
+
top_right: "+",
|
|
206
|
+
bottom_left: "+",
|
|
207
|
+
bottom_right: "+",
|
|
208
|
+
horizontal: "-",
|
|
209
|
+
vertical: "|",
|
|
210
|
+
left_t: "+",
|
|
211
|
+
right_t: "+",
|
|
212
|
+
top_t: "+",
|
|
213
|
+
bottom_t: "+",
|
|
214
|
+
cross: "+",
|
|
215
|
+
thick_horizontal: "=",
|
|
216
|
+
thick_left_t: "+",
|
|
217
|
+
thick_right_t: "+",
|
|
218
|
+
thick_cross: "+"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Standard Unicode box
|
|
222
|
+
SQUARE = new(
|
|
223
|
+
top_left: "┌",
|
|
224
|
+
top_right: "┐",
|
|
225
|
+
bottom_left: "└",
|
|
226
|
+
bottom_right: "┘",
|
|
227
|
+
horizontal: "─",
|
|
228
|
+
vertical: "│",
|
|
229
|
+
left_t: "├",
|
|
230
|
+
right_t: "┤",
|
|
231
|
+
top_t: "┬",
|
|
232
|
+
bottom_t: "┴",
|
|
233
|
+
cross: "┼",
|
|
234
|
+
thick_horizontal: "━",
|
|
235
|
+
thick_left_t: "┝",
|
|
236
|
+
thick_right_t: "┥",
|
|
237
|
+
thick_cross: "┿"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Rounded corners
|
|
241
|
+
ROUNDED = new(
|
|
242
|
+
top_left: "╭",
|
|
243
|
+
top_right: "╮",
|
|
244
|
+
bottom_left: "╰",
|
|
245
|
+
bottom_right: "╯",
|
|
246
|
+
horizontal: "─",
|
|
247
|
+
vertical: "│",
|
|
248
|
+
left_t: "├",
|
|
249
|
+
right_t: "┤",
|
|
250
|
+
top_t: "┬",
|
|
251
|
+
bottom_t: "┴",
|
|
252
|
+
cross: "┼",
|
|
253
|
+
thick_horizontal: "━",
|
|
254
|
+
thick_left_t: "┝",
|
|
255
|
+
thick_right_t: "┥",
|
|
256
|
+
thick_cross: "┿"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Heavy/thick box
|
|
260
|
+
HEAVY = new(
|
|
261
|
+
top_left: "┏",
|
|
262
|
+
top_right: "┓",
|
|
263
|
+
bottom_left: "┗",
|
|
264
|
+
bottom_right: "┛",
|
|
265
|
+
horizontal: "━",
|
|
266
|
+
vertical: "┃",
|
|
267
|
+
left_t: "┣",
|
|
268
|
+
right_t: "┫",
|
|
269
|
+
top_t: "┳",
|
|
270
|
+
bottom_t: "┻",
|
|
271
|
+
cross: "╋",
|
|
272
|
+
thick_horizontal: "━",
|
|
273
|
+
thick_left_t: "┣",
|
|
274
|
+
thick_right_t: "┫",
|
|
275
|
+
thick_cross: "╋"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Double line box
|
|
279
|
+
DOUBLE = new(
|
|
280
|
+
top_left: "╔",
|
|
281
|
+
top_right: "╗",
|
|
282
|
+
bottom_left: "╚",
|
|
283
|
+
bottom_right: "╝",
|
|
284
|
+
horizontal: "═",
|
|
285
|
+
vertical: "║",
|
|
286
|
+
left_t: "╠",
|
|
287
|
+
right_t: "╣",
|
|
288
|
+
top_t: "╦",
|
|
289
|
+
bottom_t: "╩",
|
|
290
|
+
cross: "╬",
|
|
291
|
+
thick_horizontal: "═",
|
|
292
|
+
thick_left_t: "╠",
|
|
293
|
+
thick_right_t: "╣",
|
|
294
|
+
thick_cross: "╬"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Minimal (dashes, no corners)
|
|
298
|
+
MINIMAL = new(
|
|
299
|
+
top_left: " ",
|
|
300
|
+
top_right: " ",
|
|
301
|
+
bottom_left: " ",
|
|
302
|
+
bottom_right: " ",
|
|
303
|
+
horizontal: "─",
|
|
304
|
+
vertical: " ",
|
|
305
|
+
left_t: " ",
|
|
306
|
+
right_t: " ",
|
|
307
|
+
top_t: "─",
|
|
308
|
+
bottom_t: "─",
|
|
309
|
+
cross: "─"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Simple (just horizontal lines)
|
|
313
|
+
SIMPLE = new(
|
|
314
|
+
top_left: "",
|
|
315
|
+
top_right: "",
|
|
316
|
+
bottom_left: "",
|
|
317
|
+
bottom_right: "",
|
|
318
|
+
horizontal: "─",
|
|
319
|
+
vertical: "",
|
|
320
|
+
left_t: "",
|
|
321
|
+
right_t: "",
|
|
322
|
+
top_t: "",
|
|
323
|
+
bottom_t: "",
|
|
324
|
+
cross: ""
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# No border at all
|
|
328
|
+
NONE = new(
|
|
329
|
+
top_left: "",
|
|
330
|
+
top_right: "",
|
|
331
|
+
bottom_left: "",
|
|
332
|
+
bottom_right: "",
|
|
333
|
+
horizontal: "",
|
|
334
|
+
vertical: "",
|
|
335
|
+
left_t: "",
|
|
336
|
+
right_t: "",
|
|
337
|
+
top_t: "",
|
|
338
|
+
bottom_t: "",
|
|
339
|
+
cross: ""
|
|
340
|
+
)
|
|
341
|
+
end
|
|
342
|
+
end
|