hackmac 1.11.0 → 2.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.
data/lib/hackmac/graph.rb DELETED
@@ -1,457 +0,0 @@
1
- require 'term/ansicolor'
2
- require 'tins'
3
- require 'digest/md5'
4
-
5
- require 'hackmac/graph/formatters.rb'
6
-
7
- # A class that provides graphical display functionality for terminal-based data
8
- # visualization
9
- #
10
- # The Graph class enables the creation of dynamic, real-time visualizations of
11
- # data values within a terminal environment. It manages the rendering of
12
- # graphical representations such as line charts or graphs, updating them
13
- # continuously based on provided data sources. The class handles terminal
14
- # control operations, including cursor positioning, color management, and
15
- # screen clearing to ensure smooth visual updates. It also supports
16
- # configuration of display parameters like title, formatting strategies for
17
- # values, update intervals, and color schemes for different data series.
18
- #
19
- # @example
20
- # graph = Hackmac::Graph.new(
21
- # title: 'CPU Usage',
22
- # value: ->(i) { rand(100) },
23
- # format_value: :as_percent,
24
- # sleep: 1,
25
- # color: 33
26
- # )
27
- # graph.start
28
- # # Starts the graphical display loop
29
- #
30
- # @example
31
- # graph = Hackmac::Graph.new(
32
- # title: 'Memory Usage',
33
- # value: ->(i) { `vm_stat`.match(/Pages free: (\d+)/)[1].to_i },
34
- # format_value: :as_bytes,
35
- # sleep: 2
36
- # )
37
- # graph.start
38
- # # Starts a memory usage graph with custom data source and formatting
39
- class Hackmac::Graph
40
- include Term::ANSIColor
41
- include Hackmac::Graph::Formatters
42
-
43
- # The initialize method sets up a Graph instance by configuring its display parameters and internal state.
44
- #
45
- # This method configures the graph visualization with title, value provider,
46
- # formatting options, update interval, and color settings. It initializes
47
- # internal data structures for storing historical values and manages
48
- # synchronization through a mutex for thread-safe operations.
49
- #
50
- # @param title [ String ] the title to display at the bottom of the graph
51
- # @param value [ Proc ] a proc that takes an index and returns a numeric
52
- # value for plotting
53
- # @param format_value [ Proc, Symbol, nil ] formatting strategy for
54
- # displaying values
55
- # @param sleep [ Numeric ] time in seconds between updates
56
- # @param color [ Object, Proc, nil ] color or proc or nil. nil
57
- # determineicolor dynamically from the title
58
- # @param color_secondary [ Object, nil ] secondary color or nil
59
- # for enhanced visuals, nil derives secondary color from (primary) color
60
- # @param adjust_brightness [ Symbol ] the method to call on the color for
61
- # brightness adjustment (:lighten or :darken), defaults to :lighten
62
- # @param adjust_brightness_percentage [ Float ] the percentage value to use
63
- # for the brightness adjustment
64
- # @param foreground_color [ Symbol ] the default text color for the display
65
- # @param background_color [ Symbol ] the default background color for the
66
- # display
67
- # @param resolution [ Symbol ] the resolution mode (:single or :double) for
68
- # the graph display, defaults to :double.
69
- #
70
- # @raise [ ArgumentError ] if the sleep parameter is negative
71
- # @raise [ TypeError ] if the sleep parameter is not numeric
72
- # @raise [ ArgumentError ] if the resolution parameter is not :single or :double
73
- def initialize(
74
- title:,
75
- value: -> i { 0 },
76
- format_value: nil,
77
- sleep: 5,
78
- color: nil,
79
- color_secondary: nil,
80
- adjust_brightness: :lighten,
81
- adjust_brightness_percentage: 15,
82
- foreground_color: :white,
83
- background_color: :black,
84
- resolution: :double
85
- )
86
- @title = title
87
- @value = value
88
- @format_value = format_value
89
- sleep = Float(sleep)
90
- sleep >= 0 or raise ArgumentError, 'sleep has to be >= 0'
91
- @sleep = sleep
92
- @continue = false
93
- @data = []
94
- @color = color
95
- @color_secondary = color_secondary
96
- adjust_brightness = adjust_brightness.to_sym
97
- if %i[ lighten darken ].include? adjust_brightness
98
- @adjust_brightness = adjust_brightness
99
- else
100
- raise ArgumentError, 'adjust_brightness required to be either :lighten or :darken'
101
- end
102
- @adjust_brightness_percentage = Float(adjust_brightness_percentage)
103
- @foreground_color = foreground_color
104
- @background_color = background_color
105
- resolution = resolution.to_sym
106
- if %i[ single double ].include? resolution
107
- @resolution = resolution
108
- else
109
- raise ArgumentError, 'resolution required to be either :single or :double'
110
- end
111
- @mutex = Mutex.new
112
- end
113
-
114
- # The start method initiates the graphical display process by setting up
115
- # signal handlers, performing an initial terminal reset, and entering the
116
- # main update loop
117
- #
118
- # This method serves as the entry point for starting the graph visualization
119
- # functionality. It configures the necessary signal handlers for graceful
120
- # shutdown and terminal resizing, performs an initial full reset of the
121
- # display state, and then begins the continuous loop that updates and renders
122
- # graphical data.
123
- def start
124
- install_handlers
125
- full_reset
126
- start_loop
127
- end
128
-
129
- # The stop method terminates the graphical display process by performing a
130
- # full reset and setting the continue flag to false
131
- #
132
- # This method serves as the shutdown mechanism for the graph visualization
133
- # functionality. It ensures that all display resources are properly cleaned
134
- # up and the terminal state is restored to its original condition before
135
- # stopping the continuous update loop.
136
- def stop
137
- full_reset
138
- @continue = false
139
- end
140
-
141
- private
142
-
143
- # Draws the graphical representation of the data on the display.
144
- #
145
- # This method renders the data as a graph using Unicode block characters (▀)
146
- # to achieve 2px vertical resolution in terminal graphics. Each data point is
147
- # plotted with appropriate color blending for visual appeal.
148
- def draw_graph
149
- y_width = data_range
150
- color = pick_color
151
- color_secondary = pick_secondary_color(
152
- color,
153
- adjust_brightness: @adjust_brightness,
154
- adjust_brightness_percentage: @adjust_brightness_percentage
155
- )
156
- data.each_with_index do |value, i|
157
- x = 1 + i + columns - data.size
158
- y0 = ((value - data.min) * lines / y_width.to_f)
159
- y = lines - y0.round + 1
160
- y.upto(lines) do |iy|
161
- if iy > y
162
- @display.at(iy, x).on_color(color_secondary).write(' ')
163
- else
164
- case @resolution
165
- when :double
166
- fract = 1 - (y0 - y0.floor).abs
167
- case
168
- when (0...0.5) === fract
169
- @display.at(iy, x).on_color(@background_color).color(color).write(?▄)
170
- else
171
- @display.at(iy, x).on_color(color).color(color_secondary).write(?▄)
172
- end
173
- else
174
- @display.at(iy, x).on_color(color).color(color_secondary).write(' ')
175
- end
176
- end
177
- end
178
- end
179
- end
180
-
181
- # The data_range method calculates the range of data values by computing the
182
- # difference between the maximum and minimum values in the data set and
183
- # converting the result to a float
184
- #
185
- # @return [ Float ] the calculated range of the data values as a float
186
- def data_range
187
- (data.max - data.min).abs.to_f
188
- end
189
-
190
- # The start_loop method executes a continuous loop to update and display
191
- # graphical data
192
- #
193
- # This method manages the main execution loop for rendering graphical
194
- # representations of data values over time. It initializes display state,
195
- # processes incoming data, calculates visual representations, and handles
196
- # terminal updates while respecting configured timing intervals.
197
- #
198
- # It continuously updates the display and handles data processing in a loop
199
- # until explicitly stopped.
200
- def start_loop
201
- full_reset
202
- @counter = -1
203
- @continue = true
204
- while @continue
205
- @start = Time.now
206
- @full_reset and full_reset
207
- perform hide_cursor
208
-
209
- @data << @value.(@counter += 1)
210
- @data = data.last(columns)
211
-
212
- if data_range.zero?
213
- @display.reset.bottom.styled(:bold).
214
- write_centered("#@title / #{sleep_duration}").
215
- reset.centered.styled(:italic).write_centered("no data")
216
- perform_display_diff
217
- sleep_now
218
- next
219
- end
220
-
221
- @display.reset
222
- draw_graph
223
-
224
- @display.reset.bottom.styled(:bold).
225
- write_centered("#@title #{format_value(data.last)} / #{sleep_duration}")
226
- @display.reset.styled(:bold).
227
- left.top.write(format_value(data.max)).
228
- left.bottom.write(format_value(data.min))
229
-
230
- perform_display_diff
231
- sleep_now
232
- end
233
- rescue Interrupt
234
- ensure
235
- stop
236
- end
237
-
238
- def perform(*a)
239
- print(*a)
240
- end
241
-
242
- # The columns method returns the number of columns in the display
243
- #
244
- # This method provides access to the horizontal dimension of the graphical
245
- # display by returning the total number of columns available for rendering
246
- # content
247
- #
248
- # @return [ Integer ] the number of columns (characters per line) in the display object
249
- def columns
250
- @display.columns
251
- end
252
-
253
- # The lines method returns the number of lines in the display
254
- #
255
- # This method provides access to the vertical dimension of the graphical
256
- # display by returning the total number of rows available for rendering
257
- # content
258
- #
259
- # @return [ Integer ] the number of lines (rows) in the display object
260
- def lines
261
- @display.lines
262
- end
263
-
264
- # The data reader method provides access to the data attribute that was set
265
- # during object initialization.
266
- #
267
- # This method returns the value of the data instance variable, which
268
- # typically contains structured information that has been processed or
269
- # collected by the object.
270
- #
271
- # @return [ Array<Object>, Hash, nil ] the data value stored in the instance variable, or nil if not set
272
- attr_reader :data
273
-
274
- # The sleep_duration method returns a string representation of the configured
275
- # sleep interval with the 's' suffix appended to indicate seconds.
276
- #
277
- # @return [ String ] a formatted string containing the sleep duration in seconds
278
- def sleep_duration
279
- "#{@sleep}s"
280
- end
281
-
282
- # The format_value method processes a given value using the configured
283
- # formatting strategy
284
- #
285
- # This method applies the appropriate formatting to a value based on the
286
- # \@format_value instance variable configuration It supports different
287
- # formatting approaches including custom Proc objects, Symbol-based method
288
- # calls, and default formatting
289
- #
290
- # @param value [ Object ] the value to be formatted according to the configured strategy
291
- #
292
- # @return [ String ] the formatted string representation of the input value
293
- def format_value(value)
294
- case @format_value
295
- when Proc
296
- @format_value.(value)
297
- when Symbol
298
- send(@format_value, value)
299
- else
300
- send(:as_default, value)
301
- end
302
- end
303
-
304
- # The pick_color method determines and returns an ANSI color attribute based
305
- # on the configured color setting
306
- #
307
- # This method evaluates the @color instance variable to decide how to select
308
- # a color attribute. If @color is a Proc, it invokes the proc with the @title
309
- # to determine the color. If @color is nil, it derives a color from the title
310
- # string. Otherwise, it uses the @color value directly as an index into the
311
- # ANSI color attributes.
312
- #
313
- # @return [ Term::ANSIColor::Attribute ] the selected color attribute object
314
- def pick_color
315
- Term::ANSIColor::Attribute[
316
- case @color
317
- when Proc
318
- @color.(@title)
319
- when nil
320
- derive_color_from_string(@title)
321
- else
322
- @color
323
- end
324
- ]
325
- end
326
-
327
- # The pick_secondary_color method determines a secondary color based on a
328
- # primary color and brightness adjustment parameters It returns the
329
- # pre-configured secondary color if one exists, otherwise
330
- # calculates a new color by adjusting the brightness of the primary color
331
- #
332
- # @param color [ Term::ANSIColor::Attribute ] the primary color attribute to
333
- # be used as a base for calculation
334
- # @param adjust_brightness [ Symbol ] the method to call on the color for
335
- # brightness adjustment
336
- # @param adjust_brightness_percentage [ Integer ] the percentage value to use
337
- # for the brightness adjustment
338
- # @return [ Term::ANSIColor::Attribute ] the secondary color attribute,
339
- # either pre-configured or calculated from the primary color
340
- def pick_secondary_color(color, adjust_brightness:, adjust_brightness_percentage:)
341
- @color_secondary and return @color_secondary
342
- color_primary = color.to_rgb_triple.to_hsl_triple
343
- color_primary.send(adjust_brightness, adjust_brightness_percentage) rescue color
344
- end
345
-
346
- # The sleep_now method calculates and executes a sleep duration based on the
347
- # configured sleep time and elapsed time since start
348
- #
349
- # This method determines how long to sleep by calculating the difference
350
- # between the configured sleep interval and the time elapsed since the last
351
- # operation started. If no start time is recorded, it uses the full
352
- # configured sleep duration. The method ensures that negative sleep durations
353
- # are not used by taking the maximum of the calculated duration and zero.
354
- def sleep_now
355
- duration = if @start
356
- [ @sleep - (Time.now - @start).to_f, 0 ].max
357
- else
358
- @sleep
359
- end
360
- sleep duration
361
- end
362
-
363
- # The perform_display_diff method calculates and displays the difference
364
- # between the current and previous display states to update only the changed
365
- # portions of the terminal output
366
- #
367
- # This method synchronizes access to shared display resources using a mutex,
368
- # then compares the current display with the previous state to determine what
369
- # needs updating. It handles dimension mismatches by resetting the old
370
- # display, computes the visual difference, and outputs only the modified
371
- # portions to reduce terminal update overhead
372
- #
373
- # When the DEBUG_BYTESIZE environment variable is set, it also outputs
374
- # debugging information about the size of the diff and the time elapsed since
375
- # the last debug output
376
- def perform_display_diff
377
- @mutex.synchronize do
378
- unless @old_display && @old_display.dimensions == @display.dimensions
379
- @old_display = @display.dup.clear
380
- end
381
- diff = @display - @old_display
382
- if ENV['DEBUG_BYTESIZE']
383
- unless @last
384
- STDERR.puts %w[ size duration ] * ?\t
385
- else
386
- STDERR.puts [ diff.size, (Time.now - @last).to_f ] * ?\t
387
- end
388
- @last = Time.now
389
- end
390
- perform diff
391
- @display, @old_display = @old_display.clear, @display
392
- perform move_to(lines, columns)
393
- end
394
- end
395
-
396
-
397
- # The normalize_value method converts a value to its appropriate numeric
398
- # representation
399
- #
400
- # This method takes an input value and normalizes it to either a Float or
401
- # Integer type depending on its original form. If the value is already a
402
- # Float, it is returned as-is. For all other types, the method attempts to
403
- # convert the value to an integer using to_i
404
- #
405
- # @param value [ Object ] the value to be normalized
406
- #
407
- # @return [ Float, Integer ] the normalized numeric value as either a Float
408
- # or Integer
409
- def normalize_value(value)
410
- case value
411
- when Float
412
- value
413
- else
414
- value.to_i
415
- end
416
- end
417
-
418
- # The full_reset method performs a complete reset of the display and terminal
419
- # state
420
- #
421
- # This method synchronizes access to shared resources using a mutex, then
422
- # executes a series of terminal control operations to reset the terminal
423
- # state, clear the screen, move the cursor to the home position, and make the
424
- # cursor visible. It also initializes new display objects with the current
425
- # terminal dimensions and updates the internal
426
- # display state.
427
- def full_reset
428
- @mutex.synchronize do
429
- perform reset, clear_screen, move_home, show_cursor
430
- winsize = Tins::Terminal.winsize
431
- opts = {
432
- color: @foreground_color,
433
- on_color: @background_color,
434
- }
435
- @display = Hackmac::Graph::Display.new(*winsize, **opts)
436
- @old_display = Hackmac::Graph::Display.new(*winsize, **opts)
437
- perform @display
438
- @full_reset = false
439
- end
440
- end
441
-
442
- # The install_handlers method sets up signal handlers for graceful shutdown
443
- # and terminal resize handling
444
- #
445
- # This method configures two signal handlers: one for the exit hook that
446
- # performs a full reset, and another for the SIGWINCH signal that handles
447
- # terminal resize events by setting a flag and displaying a sleeping message
448
- def install_handlers
449
- at_exit { full_reset }
450
- trap(:SIGWINCH) do
451
- @full_reset = true
452
- perform reset, clear_screen, move_home, 'Zzz…'
453
- end
454
- end
455
- end
456
-
457
- require 'hackmac/graph/display'