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.
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rich
4
+ # Terminal control codes and escape sequences.
5
+ # Provides constants and methods for cursor movement, screen clearing,
6
+ # and other terminal control operations.
7
+ module Control
8
+ # Control code types for segment rendering
9
+ module ControlType
10
+ BELL = :bell
11
+ CARRIAGE_RETURN = :carriage_return
12
+ HOME = :home
13
+ CLEAR = :clear
14
+ SHOW_CURSOR = :show_cursor
15
+ HIDE_CURSOR = :hide_cursor
16
+ ENABLE_ALT_SCREEN = :enable_alt_screen
17
+ DISABLE_ALT_SCREEN = :disable_alt_screen
18
+ CURSOR_UP = :cursor_up
19
+ CURSOR_DOWN = :cursor_down
20
+ CURSOR_FORWARD = :cursor_forward
21
+ CURSOR_BACKWARD = :cursor_backward
22
+ CURSOR_MOVE_TO_COLUMN = :cursor_move_to_column
23
+ CURSOR_MOVE_TO = :cursor_move_to
24
+ ERASE_IN_LINE = :erase_in_line
25
+ SET_WINDOW_TITLE = :set_window_title
26
+ end
27
+
28
+ # ESC character for ANSI sequences
29
+ ESC = "\e"
30
+
31
+ # Control Sequence Introducer
32
+ CSI = "\e["
33
+
34
+ # Operating System Command
35
+ OSC = "\e]"
36
+
37
+ # String Terminator
38
+ ST = "\e\\"
39
+
40
+ # Bell character
41
+ BEL = "\a"
42
+
43
+ class << self
44
+ # Generate bell/alert
45
+ # @return [String]
46
+ def bell
47
+ BEL
48
+ end
49
+
50
+ # Carriage return (move to column 0)
51
+ # @return [String]
52
+ def carriage_return
53
+ "\r"
54
+ end
55
+
56
+ # Move cursor to home position (1,1)
57
+ # @return [String]
58
+ def home
59
+ "#{CSI}H"
60
+ end
61
+
62
+ # Clear the screen
63
+ # @param mode [Integer] 0=cursor to end, 1=start to cursor, 2=entire screen
64
+ # @return [String]
65
+ def clear(mode = 2)
66
+ "#{CSI}#{mode}J"
67
+ end
68
+
69
+ # Clear entire screen and move to home
70
+ # @return [String]
71
+ def clear_screen
72
+ "#{CSI}2J#{CSI}H"
73
+ end
74
+
75
+ # Show the cursor
76
+ # @return [String]
77
+ def show_cursor
78
+ "#{CSI}?25h"
79
+ end
80
+
81
+ # Hide the cursor
82
+ # @return [String]
83
+ def hide_cursor
84
+ "#{CSI}?25l"
85
+ end
86
+
87
+ # Enable alternative screen buffer
88
+ # @return [String]
89
+ def enable_alt_screen
90
+ "#{CSI}?1049h"
91
+ end
92
+
93
+ # Disable alternative screen buffer
94
+ # @return [String]
95
+ def disable_alt_screen
96
+ "#{CSI}?1049l"
97
+ end
98
+
99
+ # Move cursor up
100
+ # @param count [Integer] Number of rows
101
+ # @return [String]
102
+ def cursor_up(count = 1)
103
+ return "" if count < 1
104
+
105
+ "#{CSI}#{count}A"
106
+ end
107
+
108
+ # Move cursor down
109
+ # @param count [Integer] Number of rows
110
+ # @return [String]
111
+ def cursor_down(count = 1)
112
+ return "" if count < 1
113
+
114
+ "#{CSI}#{count}B"
115
+ end
116
+
117
+ # Move cursor forward (right)
118
+ # @param count [Integer] Number of columns
119
+ # @return [String]
120
+ def cursor_forward(count = 1)
121
+ return "" if count < 1
122
+
123
+ "#{CSI}#{count}C"
124
+ end
125
+
126
+ # Move cursor backward (left)
127
+ # @param count [Integer] Number of columns
128
+ # @return [String]
129
+ def cursor_backward(count = 1)
130
+ return "" if count < 1
131
+
132
+ "#{CSI}#{count}D"
133
+ end
134
+
135
+ # Move cursor to next line
136
+ # @param count [Integer] Number of lines
137
+ # @return [String]
138
+ def cursor_next_line(count = 1)
139
+ return "" if count < 1
140
+
141
+ "#{CSI}#{count}E"
142
+ end
143
+
144
+ # Move cursor to previous line
145
+ # @param count [Integer] Number of lines
146
+ # @return [String]
147
+ def cursor_prev_line(count = 1)
148
+ return "" if count < 1
149
+
150
+ "#{CSI}#{count}F"
151
+ end
152
+
153
+ # Move cursor to column (1-based)
154
+ # @param column [Integer] Column number (1-based)
155
+ # @return [String]
156
+ def cursor_move_to_column(column)
157
+ "#{CSI}#{column}G"
158
+ end
159
+
160
+ # Move cursor to position (1-based coordinates)
161
+ # @param row [Integer] Row (1-based)
162
+ # @param column [Integer] Column (1-based)
163
+ # @return [String]
164
+ def cursor_move_to(row, column)
165
+ "#{CSI}#{row};#{column}H"
166
+ end
167
+
168
+ # Save cursor position
169
+ # @return [String]
170
+ def save_cursor
171
+ "#{CSI}s"
172
+ end
173
+
174
+ # Restore cursor position
175
+ # @return [String]
176
+ def restore_cursor
177
+ "#{CSI}u"
178
+ end
179
+
180
+ # Erase in line
181
+ # @param mode [Integer] 0=cursor to end, 1=start to cursor, 2=entire line
182
+ # @return [String]
183
+ def erase_line(mode = 2)
184
+ "#{CSI}#{mode}K"
185
+ end
186
+
187
+ # Erase from cursor to end of line
188
+ # @return [String]
189
+ def erase_end_of_line
190
+ "#{CSI}0K"
191
+ end
192
+
193
+ # Erase from start of line to cursor
194
+ # @return [String]
195
+ def erase_start_of_line
196
+ "#{CSI}1K"
197
+ end
198
+
199
+ # Set window title
200
+ # @param title [String] Window title
201
+ # @return [String]
202
+ def set_title(title)
203
+ "#{OSC}2;#{title}#{ST}"
204
+ end
205
+
206
+ # Set icon name (some terminals)
207
+ # @param name [String] Icon name
208
+ # @return [String]
209
+ def set_icon_name(name)
210
+ "#{OSC}1;#{name}#{ST}"
211
+ end
212
+
213
+ # Set both icon name and window title
214
+ # @param title [String] Title/name
215
+ # @return [String]
216
+ def set_icon_and_title(title)
217
+ "#{OSC}0;#{title}#{ST}"
218
+ end
219
+
220
+ # Request cursor position (terminal will respond)
221
+ # @return [String]
222
+ def request_cursor_position
223
+ "#{CSI}6n"
224
+ end
225
+
226
+ # Scroll up
227
+ # @param count [Integer] Number of lines
228
+ # @return [String]
229
+ def scroll_up(count = 1)
230
+ "#{CSI}#{count}S"
231
+ end
232
+
233
+ # Scroll down
234
+ # @param count [Integer] Number of lines
235
+ # @return [String]
236
+ def scroll_down(count = 1)
237
+ "#{CSI}#{count}T"
238
+ end
239
+
240
+ # Reset all attributes
241
+ # @return [String]
242
+ def reset
243
+ "#{CSI}0m"
244
+ end
245
+
246
+ # Create a hyperlink
247
+ # @param url [String] URL
248
+ # @param text [String] Link text
249
+ # @param id [String, nil] Optional link ID
250
+ # @return [String]
251
+ def hyperlink(url, text, id: nil)
252
+ params = id ? "id=#{id}" : ""
253
+ "#{OSC}8;#{params};#{url}#{ST}#{text}#{OSC}8;;#{ST}"
254
+ end
255
+
256
+ # Start hyperlink
257
+ # @param url [String] URL
258
+ # @param id [String, nil] Optional link ID
259
+ # @return [String]
260
+ def hyperlink_start(url, id: nil)
261
+ params = id ? "id=#{id}" : ""
262
+ "#{OSC}8;#{params};#{url}#{ST}"
263
+ end
264
+
265
+ # End hyperlink
266
+ # @return [String]
267
+ def hyperlink_end
268
+ "#{OSC}8;;#{ST}"
269
+ end
270
+
271
+ # Strip ANSI escape sequences from text
272
+ # @param text [String] Text to strip
273
+ # @return [String] Text without ANSI sequences
274
+ def strip_ansi(text)
275
+ text.gsub(/\e\[[0-9;]*[A-Za-z]/, "")
276
+ .gsub(/\e\][^\a\e]*(?:\a|\e\\)/, "")
277
+ .gsub(/\e[()][\dAB]/, "")
278
+ end
279
+
280
+ # Check if text contains ANSI escape sequences
281
+ # @param text [String] Text to check
282
+ # @return [Boolean]
283
+ def contains_ansi?(text)
284
+ text.match?(/\e[\[\]()][^\a\e]*/)
285
+ end
286
+
287
+ # Generate control code for a control type
288
+ # @param control_type [Symbol] Control type
289
+ # @param param1 [Object] First parameter
290
+ # @param param2 [Object] Second parameter
291
+ # @return [String]
292
+ def generate(control_type, param1 = nil, param2 = nil)
293
+ case control_type
294
+ when ControlType::BELL
295
+ bell
296
+ when ControlType::CARRIAGE_RETURN
297
+ carriage_return
298
+ when ControlType::HOME
299
+ home
300
+ when ControlType::CLEAR
301
+ clear
302
+ when ControlType::SHOW_CURSOR
303
+ show_cursor
304
+ when ControlType::HIDE_CURSOR
305
+ hide_cursor
306
+ when ControlType::ENABLE_ALT_SCREEN
307
+ enable_alt_screen
308
+ when ControlType::DISABLE_ALT_SCREEN
309
+ disable_alt_screen
310
+ when ControlType::CURSOR_UP
311
+ cursor_up(param1 || 1)
312
+ when ControlType::CURSOR_DOWN
313
+ cursor_down(param1 || 1)
314
+ when ControlType::CURSOR_FORWARD
315
+ cursor_forward(param1 || 1)
316
+ when ControlType::CURSOR_BACKWARD
317
+ cursor_backward(param1 || 1)
318
+ when ControlType::CURSOR_MOVE_TO_COLUMN
319
+ cursor_move_to_column(param1 || 1)
320
+ when ControlType::CURSOR_MOVE_TO
321
+ cursor_move_to(param1 || 1, param2 || 1)
322
+ when ControlType::ERASE_IN_LINE
323
+ erase_line(param1 || 2)
324
+ when ControlType::SET_WINDOW_TITLE
325
+ set_title(param1.to_s)
326
+ else
327
+ ""
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end
data/lib/rich/json.rb ADDED
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "style"
5
+ require_relative "segment"
6
+
7
+ module Rich
8
+ # JSON syntax highlighting and formatting
9
+ module JSON
10
+ # Default styles for JSON elements
11
+ DEFAULT_STYLES = {
12
+ key: "cyan",
13
+ string: "green",
14
+ number: "yellow",
15
+ boolean: "italic magenta",
16
+ null: "dim",
17
+ brace: "bold",
18
+ bracket: "bold",
19
+ comma: "dim",
20
+ colon: "dim"
21
+ }.freeze
22
+
23
+ class << self
24
+ # Render JSON with syntax highlighting
25
+ # @param data [Object] Data to render as JSON
26
+ # @param indent [Integer] Indentation size
27
+ # @param styles [Hash] Style overrides
28
+ # @return [Array<Segment>]
29
+ def render(data, indent: 2, styles: {})
30
+ merged_styles = DEFAULT_STYLES.merge(styles)
31
+ json_str = ::JSON.pretty_generate(data, indent: " " * indent)
32
+
33
+ segments = []
34
+ tokenize(json_str, merged_styles, segments)
35
+ segments
36
+ end
37
+
38
+ # Render to string with ANSI codes
39
+ # @param data [Object] Data to render
40
+ # @param indent [Integer] Indentation
41
+ # @param color_system [Symbol] Color system
42
+ # @return [String]
43
+ def to_s(data, indent: 2, color_system: ColorSystem::TRUECOLOR)
44
+ segments = render(data, indent: indent)
45
+ Segment.render(segments, color_system: color_system)
46
+ end
47
+
48
+ # Parse and render a JSON string
49
+ # @param json_str [String] JSON string
50
+ # @param styles [Hash] Style overrides
51
+ # @return [Array<Segment>]
52
+ def highlight(json_str, styles: {})
53
+ merged_styles = DEFAULT_STYLES.merge(styles)
54
+ segments = []
55
+ tokenize(json_str, merged_styles, segments)
56
+ segments
57
+ end
58
+
59
+ private
60
+
61
+ def tokenize(json_str, styles, segments)
62
+ pos = 0
63
+
64
+ while pos < json_str.length
65
+ char = json_str[pos]
66
+
67
+ case char
68
+ when "{"
69
+ segments << Segment.new("{", style: parse_style(styles[:brace]))
70
+ pos += 1
71
+ when "}"
72
+ segments << Segment.new("}", style: parse_style(styles[:brace]))
73
+ pos += 1
74
+ when "["
75
+ segments << Segment.new("[", style: parse_style(styles[:bracket]))
76
+ pos += 1
77
+ when "]"
78
+ segments << Segment.new("]", style: parse_style(styles[:bracket]))
79
+ pos += 1
80
+ when ","
81
+ segments << Segment.new(",", style: parse_style(styles[:comma]))
82
+ pos += 1
83
+ when ":"
84
+ segments << Segment.new(":", style: parse_style(styles[:colon]))
85
+ pos += 1
86
+ when '"'
87
+ # String - check if it's a key (followed by :)
88
+ str_end = find_string_end(json_str, pos)
89
+ str_content = json_str[pos..str_end]
90
+
91
+ # Look ahead to see if this is a key
92
+ look_ahead = json_str[str_end + 1..].lstrip
93
+ is_key = look_ahead.start_with?(":")
94
+
95
+ style = is_key ? styles[:key] : styles[:string]
96
+ segments << Segment.new(str_content, style: parse_style(style))
97
+ pos = str_end + 1
98
+ when /[0-9\-]/
99
+ # Number
100
+ num_end = pos
101
+ while num_end < json_str.length && json_str[num_end].match?(/[0-9eE.\-+]/)
102
+ num_end += 1
103
+ end
104
+ num_content = json_str[pos...num_end]
105
+ segments << Segment.new(num_content, style: parse_style(styles[:number]))
106
+ pos = num_end
107
+ when /[tfn]/
108
+ # Boolean or null
109
+ if json_str[pos, 4] == "true"
110
+ segments << Segment.new("true", style: parse_style(styles[:boolean]))
111
+ pos += 4
112
+ elsif json_str[pos, 5] == "false"
113
+ segments << Segment.new("false", style: parse_style(styles[:boolean]))
114
+ pos += 5
115
+ elsif json_str[pos, 4] == "null"
116
+ segments << Segment.new("null", style: parse_style(styles[:null]))
117
+ pos += 4
118
+ else
119
+ segments << Segment.new(char)
120
+ pos += 1
121
+ end
122
+ when /\s/
123
+ # Whitespace
124
+ ws_end = pos
125
+ while ws_end < json_str.length && json_str[ws_end].match?(/\s/)
126
+ ws_end += 1
127
+ end
128
+ segments << Segment.new(json_str[pos...ws_end])
129
+ pos = ws_end
130
+ else
131
+ segments << Segment.new(char)
132
+ pos += 1
133
+ end
134
+ end
135
+ end
136
+
137
+ def find_string_end(str, start_pos)
138
+ pos = start_pos + 1
139
+ while pos < str.length
140
+ if str[pos] == '"' && str[pos - 1] != '\\'
141
+ return pos
142
+ end
143
+ pos += 1
144
+ end
145
+ str.length - 1
146
+ end
147
+
148
+ def parse_style(style)
149
+ return nil if style.nil?
150
+ return style if style.is_a?(Style)
151
+
152
+ Style.parse(style)
153
+ end
154
+ end
155
+ end
156
+
157
+ # Pretty printing of Ruby objects
158
+ module Pretty
159
+ class << self
160
+ # Pretty print a Ruby object
161
+ # @param obj [Object] Object to print
162
+ # @param indent [Integer] Indentation
163
+ # @return [Array<Segment>]
164
+ def render(obj, indent: 2)
165
+ segments = []
166
+ render_object(obj, 0, indent, segments)
167
+ segments
168
+ end
169
+
170
+ # Render to string
171
+ # @param obj [Object] Object to render
172
+ # @param color_system [Symbol] Color system
173
+ # @return [String]
174
+ def to_s(obj, color_system: ColorSystem::TRUECOLOR)
175
+ Segment.render(render(obj), color_system: color_system)
176
+ end
177
+
178
+ private
179
+
180
+ def render_object(obj, depth, indent, segments)
181
+ case obj
182
+ when NilClass
183
+ segments << Segment.new("nil", style: Style.parse("dim"))
184
+ when TrueClass, FalseClass
185
+ segments << Segment.new(obj.to_s, style: Style.parse("italic magenta"))
186
+ when Integer, Float
187
+ segments << Segment.new(obj.to_s, style: Style.parse("yellow"))
188
+ when String
189
+ segments << Segment.new(obj.inspect, style: Style.parse("green"))
190
+ when Symbol
191
+ segments << Segment.new(":#{obj}", style: Style.parse("cyan bold"))
192
+ when Array
193
+ render_array(obj, depth, indent, segments)
194
+ when Hash
195
+ render_hash(obj, depth, indent, segments)
196
+ else
197
+ segments << Segment.new(obj.inspect, style: Style.parse("white"))
198
+ end
199
+ end
200
+
201
+ def render_array(arr, depth, indent, segments)
202
+ if arr.empty?
203
+ segments << Segment.new("[]", style: Style.parse("bold"))
204
+ return
205
+ end
206
+
207
+ segments << Segment.new("[", style: Style.parse("bold"))
208
+ segments << Segment.new("\n")
209
+
210
+ arr.each_with_index do |item, index|
211
+ segments << Segment.new(" " * ((depth + 1) * indent))
212
+ render_object(item, depth + 1, indent, segments)
213
+ segments << Segment.new(",") if index < arr.length - 1
214
+ segments << Segment.new("\n")
215
+ end
216
+
217
+ segments << Segment.new(" " * (depth * indent))
218
+ segments << Segment.new("]", style: Style.parse("bold"))
219
+ end
220
+
221
+ def render_hash(hash, depth, indent, segments)
222
+ if hash.empty?
223
+ segments << Segment.new("{}", style: Style.parse("bold"))
224
+ return
225
+ end
226
+
227
+ segments << Segment.new("{", style: Style.parse("bold"))
228
+ segments << Segment.new("\n")
229
+
230
+ entries = hash.to_a
231
+ entries.each_with_index do |(key, value), index|
232
+ segments << Segment.new(" " * ((depth + 1) * indent))
233
+
234
+ # Key
235
+ if key.is_a?(Symbol)
236
+ segments << Segment.new(":#{key}", style: Style.parse("cyan"))
237
+ else
238
+ segments << Segment.new(key.inspect, style: Style.parse("cyan"))
239
+ end
240
+
241
+ segments << Segment.new(" => ", style: Style.parse("dim"))
242
+
243
+ # Value
244
+ render_object(value, depth + 1, indent, segments)
245
+ segments << Segment.new(",") if index < entries.length - 1
246
+ segments << Segment.new("\n")
247
+ end
248
+
249
+ segments << Segment.new(" " * (depth * indent))
250
+ segments << Segment.new("}", style: Style.parse("bold"))
251
+ end
252
+ end
253
+ end
254
+ end