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