raindown 0.7.0 → 0.8.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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/lib/nodes/endpoint_node.rb +3 -2
  3. data/lib/raindown/actions.rb +5 -3
  4. data/lib/raindown/bare_directives.rb +14 -27
  5. data/lib/raindown/blueprints.rb +255 -0
  6. data/lib/raindown/cli.rb +39 -54
  7. data/lib/raindown/config.rb +2 -3
  8. data/lib/raindown/directives.rb +30 -13
  9. data/lib/raindown/document.rb +21 -15
  10. data/lib/raindown/endpoint/endpoint_runner.rb +13 -5
  11. data/lib/raindown/node_blocks.rb +22 -16
  12. data/lib/raindown/node_injector.rb +7 -10
  13. data/lib/raindown/pages.rb +1 -1
  14. data/lib/raindown/pre_renderer.rb +18 -0
  15. data/lib/raindown/tui/blueprints/blueprints_renderer.rb +93 -0
  16. data/lib/raindown/tui/blueprints/blueprints_view.rb +146 -0
  17. data/lib/raindown/tui/blueprints/canvas.rb +58 -0
  18. data/lib/raindown/tui/blueprints/code_comments.rb +71 -0
  19. data/lib/raindown/tui/blueprints/filter.rb +77 -0
  20. data/lib/raindown/tui/blueprints/traced_code.rb +56 -0
  21. data/lib/raindown/tui/blueprints/tracing.rb +129 -0
  22. data/lib/raindown/tui/{feelings/code_box.rb → code_box.rb} +44 -30
  23. data/lib/raindown/tui/endpoints.rb +2 -2
  24. data/lib/raindown/tui/events.rb +3 -1
  25. data/lib/raindown/tui/feelings/bins.rb +5 -5
  26. data/lib/raindown/tui/feelings/{celebration.rb → celebration/celebration.rb} +5 -2
  27. data/lib/raindown/tui/feelings/{scene.rb → celebration/scene.rb} +4 -1
  28. data/lib/raindown/tui/feelings/cell_moves.rb +34 -0
  29. data/lib/raindown/tui/feelings/feelings_code_box.rb +40 -0
  30. data/lib/raindown/tui/feelings/feelings_log.rb +22 -19
  31. data/lib/raindown/tui/feelings/feelings_renderer.rb +94 -95
  32. data/lib/raindown/tui/feelings/feelings_view.rb +28 -32
  33. data/lib/raindown/tui/feelings/magnifier.rb +68 -0
  34. data/lib/raindown/tui/feelings/motion.rb +93 -0
  35. data/lib/raindown/tui/feelings/waffle/baked_waffle.rb +88 -0
  36. data/lib/raindown/tui/feelings/waffle/balloons.rb +81 -0
  37. data/lib/raindown/tui/feelings/waffle/confetti.rb +55 -0
  38. data/lib/raindown/tui/feelings/waffle/sparkles.rb +61 -0
  39. data/lib/raindown/tui/feelings/waffle/waffle.rb +62 -0
  40. data/lib/raindown/tui/feelings/weekly_batch.rb +24 -5
  41. data/lib/raindown/tui/layout.rb +25 -18
  42. data/lib/raindown/tui/navigator/commands.rb +32 -1
  43. data/lib/raindown/tui/navigator/mouse.rb +43 -0
  44. data/lib/raindown/tui/navigator/navigator.rb +40 -44
  45. data/lib/raindown/tui/navigator/{search.rb → teleport.rb} +14 -12
  46. data/lib/raindown/tui/navigator/views.rb +2 -3
  47. data/lib/raindown/tui/{workers/project.rb → project.rb} +33 -22
  48. data/lib/raindown/tui/{menu_overlay.rb → project_box.rb} +1 -1
  49. data/lib/raindown/tui/renderers/renderer.rb +1 -0
  50. data/lib/raindown/tui/saves.rb +22 -0
  51. data/lib/raindown/tui/{search_overlay.rb → search_box.rb} +27 -14
  52. data/lib/raindown/tui/terminal/input.rb +58 -7
  53. data/lib/raindown/tui/terminal/screen.rb +31 -26
  54. data/lib/raindown/tui/terminal/styles.rb +55 -28
  55. data/lib/raindown/tui/terminal/terminal_title.rb +31 -0
  56. data/lib/raindown/tui/text.rb +35 -12
  57. data/lib/raindown/tui/views/dashboard_view.rb +2 -2
  58. data/lib/raindown/tui/views/endpoint_view.rb +3 -3
  59. data/lib/raindown/tui/views/list_view.rb +1 -1
  60. data/lib/raindown/tui/views/outline_view.rb +5 -4
  61. data/lib/raindown/tui.rb +17 -7
  62. data/lib/raindown/watcher/changes.rb +33 -0
  63. data/lib/raindown/watcher/file_watcher.rb +58 -0
  64. data/lib/raindown/watcher/project.rb +47 -0
  65. data/lib/raindown/watcher/supervisor.rb +111 -0
  66. data/lib/raindown/{tui/workers → workers}/worker.rb +37 -29
  67. data/lib/raindown/workers/workers.rb +63 -0
  68. data/lib/raindown.rb +4 -4
  69. data/lib/version.rb +1 -1
  70. metadata +37 -17
  71. data/lib/raindown/project_worker.rb +0 -48
  72. data/lib/raindown/supervisor.rb +0 -160
  73. data/lib/raindown/tui/feelings/waffle.rb +0 -43
  74. data/lib/raindown/tui/workers/workers.rb +0 -53
  75. data/lib/raindown/watcher.rb +0 -57
  76. /data/lib/raindown/tui/feelings/{fireworks.rb → celebration/fireworks.rb} +0 -0
  77. /data/lib/raindown/tui/feelings/{rain.rb → celebration/rain.rb} +0 -0
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'csv'
4
- require 'date'
5
4
  require 'fileutils'
6
5
  require 'open3'
6
+ require 'time'
7
7
 
8
8
  module Raindown
9
9
  module TUI
@@ -26,8 +26,8 @@ module Raindown
26
26
  # felt at, which a node can count, like the README's code debt. A feeling about code that's since changed, or gone, moves to
27
27
  # .raindown/memories.csv, so the feelings stay few, and the history's kept.
28
28
  class FeelingsLog
29
- TEMPERS = %w[frolic hope numb woe dread malice].freeze
30
- HEADER = %i[when where method temper note commit checksum].freeze
29
+ TEMPERS = %w[frolic numb woe dread malice].freeze
30
+ HEADER = %i[timestamp commit checksum location method temper note].freeze
31
31
  def initialize(root:)
32
32
  @root = root
33
33
  @csv = File.join(root, '.raindown', 'feelings.csv')
@@ -39,13 +39,13 @@ module Raindown
39
39
  def add(feeling)
40
40
  method = feeling.method
41
41
  row = {
42
- when: Date.today.iso8601,
43
- where: where(method),
42
+ timestamp: Time.now.utc.iso8601,
43
+ commit:,
44
+ checksum: method.checksum,
45
+ location: location(method),
44
46
  method: method.label,
45
47
  temper: feeling.temper,
46
- note: feeling.note,
47
- commit:,
48
- checksum: method.checksum
48
+ note: feeling.note
49
49
  }
50
50
  changed, others = rows.reject { felt_row?(it, method) }.partition { about?(it, method) }
51
51
  append_memories(changed)
@@ -53,9 +53,9 @@ module Raindown
53
53
  end
54
54
 
55
55
  # Moves the feelings about methods that have changed, or gone, since they were felt, from the methods there are
56
- # now, to the memories. A row from before checksums were kept stays while its method's there.
56
+ # now, to the memories.
57
57
  def move_stale_to_memories(methods)
58
- now = methods.flat_map { [[relative(it.path), it.label, it.checksum], [relative(it.path), it.label, '']] }.to_set
58
+ now = methods.to_set { method_key(it) }
59
59
  stale, current = rows.partition { !now.include?(key(it)) }
60
60
  return if stale.empty?
61
61
 
@@ -70,11 +70,12 @@ module Raindown
70
70
  @rows ||= CSV.read(@csv, headers: true, header_converters: :symbol).map(&:to_h)
71
71
  end
72
72
 
73
- # Whether the method's been felt, by its file, name and code's checksum, wherever its line is now. A row from before
74
- # checksums were kept has none, so it counts until the method's felt again.
73
+ # Whether the method's been felt, by its file, name and code's checksum, wherever its line is now. The answer's kept
74
+ # until a feeling's added.
75
75
  def felt?(method)
76
76
  @felt ||= rows.to_set { key(it) }
77
- [method.checksum, ''].any? { @felt.include?([relative(method.path), method.label, it]) }
77
+ @felt_methods ||= {}
78
+ @felt_methods.fetch(method) { @felt_methods[method] = @felt.include?(method_key(method)) }
78
79
  end
79
80
 
80
81
  # How the method last felt, while its code's the same as when it was felt, or nil.
@@ -88,15 +89,16 @@ module Raindown
88
89
 
89
90
  private
90
91
 
91
- def felt_row?(row, method) = about?(row, method) && [method.checksum, ''].include?(row[:checksum].to_s)
92
+ def felt_row?(row, method) = key(row) == method_key(method)
92
93
 
93
94
  def about?(row, method) = path(row) == relative(method.path) && row[:method] == method.label
94
95
 
95
96
  # The row's file, without the line the method was on.
96
- def path(row) = row[:where].sub(/:\d+\z/, '')
97
+ def path(row) = row[:location].sub(/:\d+\z/, '')
97
98
 
98
- # The row's file, method and checksum, which is empty for a row from before checksums were kept.
99
- def key(row) = [path(row), row[:method], row[:checksum].to_s]
99
+ # The row's file, method and checksum, to match a method's.
100
+ def key(row) = [path(row), row[:method], row[:checksum]]
101
+ def method_key(method) = [relative(method.path), method.label, method.checksum]
100
102
 
101
103
  # Adds the rows to the memories, of feelings about code that's since changed or gone.
102
104
  def append_memories(rows)
@@ -115,10 +117,11 @@ module Raindown
115
117
  csv << HEADER
116
118
  rows.each { csv << it.values_at(*HEADER) }
117
119
  end
118
- @rows = @felt = nil
120
+ @rows = @felt = @felt_methods = nil
119
121
  end
120
122
 
121
- def where(method) = "#{relative(method.path)}:#{method.line}"
123
+ # The method's file and line.
124
+ def location(method) = "#{relative(method.path)}:#{method.line}"
122
125
 
123
126
  def relative(path) = path.delete_prefix(File.join(@root, ''))
124
127
 
@@ -2,60 +2,69 @@
2
2
 
3
3
  require 'zlib'
4
4
 
5
- require_relative '../../stat'
6
5
  require_relative '../renderers/renderer'
7
6
  require_relative 'bins'
8
- require_relative 'code_box'
9
- require_relative 'scene'
10
- require_relative 'waffle'
7
+ require_relative 'feelings_code_box'
8
+ require_relative 'magnifier'
9
+ require_relative 'motion'
10
+ require_relative 'celebration/scene'
11
+ require_relative 'waffle/waffle'
11
12
 
12
13
  module Raindown
13
14
  module TUI
14
15
  # Code feelings, right of the files: the methods as clusters of digits in a grid, with the selected one highlighted
15
- # and its code below, then why it felt that way while that's typed, then the six bins and how many each holds.
16
+ # and its code below, then why it felt that way while that's typed, then the five bins and how many each holds.
16
17
  # When the batch is refined, it rains over them, then there's a waffle:
17
18
  # 0 0 2 2 2 5 5 9 9 9 9 1
18
19
  # 1 3 3 7 7 7 6 4 4 0 8 8
19
20
  # ╭─ lib/text.rb:62 · Text#segments_between ──────────── changed 6× ─╮
20
21
  # │ def segments_between(line, from, to) │
21
22
  # ╰───────────────────────────────────────────────────────────────────╯
22
- # ╭─ 01 ────╮ ╭─ 02 ────╮ ╭─ 03 ────╮ ╭─ 04 ────╮ ╭─ 05 ────╮ ╭─ 06 ────╮
23
- # │ FROLIC │ │ HOPE │ │ NUMB │ │ WOE │ │ DREAD │ │ MALICE │
24
- # │ ███░ 9 │ │ ██░░ 5 │ │ ████ 41 │ │ ██░░ 6 │ │ █░░░ 3 │ │ █░░░ 2 │
25
- # ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯
23
+ # ╭─ 01 ────╮ ╭─ 02 ────╮ ╭─ 03 ────╮ ╭─ 04 ────╮ ╭─ 05 ────╮
24
+ # │ FROLIC │ │ NUMB │ │ WOE │ │ DREAD │ │ MALICE │
25
+ # │ ███░ 9 │ │ ████ 41 │ │ ██░░ 6 │ │ █░░░ 3 │ │ █░░░ 2 │
26
+ # ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯
26
27
  class FeelingsRenderer < Renderer
27
28
  include Bins
28
- include CodeBox
29
+ include FeelingsCodeBox
30
+ include Magnifier
31
+ include Motion
29
32
  include Waffle
30
33
 
31
- # The columns and lines a cell takes, its digit small on the first line, with room around it for the digit magnified
32
- # in a stat's block digits, over the line above and the spaces beside it, when its method's selected or the mouse is
33
- # over it.
34
- CELL = 6
35
- CELL_LINES = 2
36
- # The code box's lines of code that a batch is sized for.
37
- FILLED_CODE_LINES = 1
38
- # The drift from which a method's cells flicker, and the ticks, half a second each, between a cell's flickers.
34
+ # The columns a cell takes, on a line of its own, its digit small in the middle, which is magnified in a stat's block
35
+ # digits across the cell and over the cells above and below it when the mouse is over its method.
36
+ CELL = 3
37
+ # The drift from which a method's cells are restless, and flicker now and then.
39
38
  RESTLESS = 7
40
- FLICKER_TICKS = 12
41
- # How far a cell's digit is from its method's drift, by its roll out of NOISE_ROLL, so about three in ten are off.
42
- NOISE = { 0 => -1, 1 => 1, 2 => -1, 3 => 1, 4 => -2, 5 => 2 }.freeze
39
+ # How far a cell's digit is from its method's drift, by its roll out of NOISE_ROLL, so about four in ten are off.
40
+ NOISE = { 0 => -1, 1 => 1, 2 => -1, 3 => 1, 4 => -2, 5 => 2, 6 => -3, 7 => 3 }.freeze
43
41
  NOISE_ROLL = 20
44
- BUTTONS = [['Tab', 'Pane', :tab], ['1-6', 'Feel', nil], ['e', 'Edit', :edit], ['x', 'Fix', :execute],
42
+ # The smallest grid that's worth refining, below which the pane's red, saying the screen's too small, under a
43
+ # window with arrows out from its corners, when there's room for it.
44
+ MIN_COLUMNS = 15
45
+ MIN_ROWS = 5
46
+ TOO_SMALL = 'Screen too small.'
47
+ TOO_SMALL_ART = ['↖ ↗', ' ┌───┐ ', ' └───┘ ', '↙ ↘', ''].freeze
48
+ TITLE = 'Weekly Batch'
49
+ BUTTONS = [['Tab', 'Pane', :tab], ['1-5', 'Feel', nil], ['e', 'Edit', :edit], ['x', 'Fix', :execute],
45
50
  ['/', 'Search', :search], ['q', 'Quit', :quit]].freeze
46
51
 
47
52
  def buttons = BUTTONS
48
53
  def page = 'Code feelings'
49
54
 
50
- # How much of the batch has been refined, on the right of the line above the panes.
55
+ # The pane's title, in the middle of the line above the panes, with the border either side, in its colour.
51
56
  def spacer
52
- progress = "#{view.batch.progress} "
53
- progress_width = display_width(progress)
54
- padded([segment('', :blank, width: [width - progress_width, 0].max), segment(progress, :body, width: progress_width)])
57
+ title = " #{TITLE} "
58
+ title_width = display_width(title)
59
+ left = [(width - title_width) / 2, 0].max
60
+ padded([segment('', :blank, width: left), segment(title, :divider, width: title_width),
61
+ segment('', :blank, width: [width - left - title_width, 0].max)])
55
62
  end
56
63
 
57
- # The cells come first, as the first draw picks the batch to fill them.
64
+ # The cells come first, as the first draw picks the batch to fill them, unless the screen's too small for a grid.
58
65
  def lines
66
+ return too_small if too_small?
67
+
59
68
  cells = view.cells(columns, filled_rows)
60
69
  return instead if view.code_methods.empty? || view.celebration
61
70
 
@@ -67,74 +76,42 @@ module Raindown
67
76
 
68
77
  def view = navigator.view
69
78
 
70
- # Instead of the grid, that there's nothing to feel, or rain and fireworks over the pane when the batch is refined,
71
- # then a waffle.
79
+ # Instead of the grid, that there's nothing to feel, or the celebration.
72
80
  def instead
73
81
  return blank(" No functions or methods in #{File.basename(navigator.root)} to feel") if view.code_methods.empty?
74
- return Scene.new(width:, height:).lines(view.celebration.frame) if view.animating?
75
82
 
76
- waffle
83
+ celebration(view.celebration)
84
+ end
85
+
86
+ # Rain and fireworks over the pane when the batch is refined, then a waffle popping out, among confetti, balloons and
87
+ # sparkles.
88
+ def celebration(celebration)
89
+ return Scene.new(width:, height:).lines(celebration.frame) if celebration.raining?
90
+
91
+ waffle(Scene.new(width:, height:, animations: [Confetti, Balloons, Sparkles]).lines(celebration.popping), celebration.popping)
77
92
  end
78
93
 
79
- # The rows of the grid around the selected method's, each its cells' lines, below a blank line that leaves the first
80
- # row room to be magnified, above the lines below it, and blank lines for any left over.
94
+ # The rows of the grid around the selected method's, between blank lines that leave the first and last rows room to
95
+ # be magnified, above the lines below it, and blank lines for any left over.
81
96
  def grid(cells, below)
97
+ @cells = cells
82
98
  lines = [height - below, 0].max
83
- rows = [(lines - 1) / CELL_LINES, 0].max
99
+ rows = [lines - 2, 0].max
84
100
  first = first_row(cells, columns, rows)
85
- grid = magnified([padded([]), *grid_rows(cells, first, rows)], cells, first, rows)
101
+ grid = magnified([progress_line(cells, first, rows), *grid_rows(cells, first, rows), padded([])], cells, first, rows)
86
102
 
87
- [*grid, *Array.new(lines - grid.length) { padded([]) }].first(lines)
103
+ [*grid, *Array.new([lines - grid.length, 0].max) { padded([]) }].first(lines)
88
104
  end
89
105
 
90
106
  # The cells' lines of the rows from the first.
91
- def grid_rows(cells, first, rows)
92
- Array.new(rows * CELL_LINES) { grid_line(cells, (first + (it / CELL_LINES)) * columns, columns, it % CELL_LINES) }
93
- end
94
-
95
- # The lines with the digits of the selected method, and the one the mouse is over, magnified, over the line above them
96
- # and beside them, each across its whole cell and the line above it.
97
- def magnified(lines, cells, first, rows)
98
- magnified_cells(cells, first, rows).each do |index, click|
99
- magnify_cell(lines, ((index / columns) - first) * CELL_LINES, cells[index], index, click)
100
- end
101
- lines
102
- end
103
-
104
- # The cell's digit magnified over the lines from the top one, across the whole cell.
105
- def magnify_cell(lines, top, owner, index, click)
106
- style = magnified_style(owner)
107
- Stat::DIGITS[digit(owner, index).to_s].each_with_index do |text, line|
108
- magnify(lines, top + line, segment(" #{text} ", style, width: CELL, event: click), 1 + (index % columns * CELL))
109
- end
110
- end
111
-
112
- # The selected method's cells, across the whole of each, on the selection's blue, so they join into one block.
113
- def magnified_style(owner)
114
- return :selected if owner == view.cursor
115
-
116
- :feelings
117
- end
118
-
119
- # The cells of the rows from the first that are the selected method's, or the one the mouse is over's, with its click,
120
- # unless they're blank.
121
- def magnified_cells(cells, first, rows)
122
- (first * columns...(first + rows) * columns).filter_map do |index|
123
- owner = cells[index]
124
- click = Click.new(pane:, index: owner, action: :focus)
125
- [index, click] if owner && (owner == view.cursor || navigator.hovered == click) && !hidden?(owner)
126
- end
127
- end
128
-
129
- def magnify(lines, line, segment, column)
130
- lines[line] = overlay_segments(lines[line], [segment], column:)
131
- end
107
+ def grid_rows(cells, first, rows) = Array.new(rows) { grid_line(cells, (first + it) * columns, columns) }
132
108
 
133
109
  def columns = [(width - 1) / CELL, 1].max
134
110
 
135
- # The grid's rows with a code box of FILLED_CODE_LINES, between its top and bottom, which a batch fills. A taller box
136
- # takes rows from the grid, which then scrolls to keep the selected method in view.
137
- def filled_rows = [(height - BIN_LINES - 3 - FILLED_CODE_LINES) / CELL_LINES, 1].max
111
+ # The grid's rows between its blank lines, above the code box, as tall as it always is until it's expanded, and the
112
+ # bins, which a batch fills. An expanded box takes rows from the grid, which then scrolls to keep the selected method
113
+ # in view.
114
+ def filled_rows = [height - 2 - CODE_LINES - BOX_LINES - BIN_LINES, 1].max
138
115
 
139
116
  # The first row that shows, with the selected method's row in the middle where there's room.
140
117
  def first_row(cells, columns, rows)
@@ -143,37 +120,59 @@ module Raindown
143
120
  end
144
121
 
145
122
  # The cells' line of the row from the index.
146
- def grid_line(cells, from, columns, line)
123
+ def grid_line(cells, from, columns)
147
124
  segments = (from...(from + columns)).filter_map do |index|
148
125
  owner = cells[index]
149
126
  next unless owner
150
127
 
151
128
  click = Click.new(pane:, index: owner, action: :focus)
152
- segment(cell_text(owner, index, line), :feelings, width: CELL, event: click)
129
+ segment(cell_text(owner, index), grid_style(owner, index), width: CELL, event: click)
153
130
  end
154
131
  padded([segment('', :blank, width: 1), *segments])
155
132
  end
156
133
 
157
- # The cell's line: its digit small on the first, or blank once its method's felt, but still there to select, which
158
- # shows it again with its code.
159
- def cell_text(owner, index, line)
160
- return ' ' * CELL if hidden?(owner) || line.nonzero?
134
+ # The cell's digit, small, where it moves, or blank once its method's felt, but still there to select, which shows it
135
+ # again with its code.
136
+ def cell_text(owner, index)
137
+ return ' ' * CELL if hidden?(owner)
138
+
139
+ column = digit_column(owner, index)
140
+ "#{' ' * column}#{digit(owner, index)}#{' ' * (CELL - 1 - column)}"
141
+ end
142
+
143
+ # The selected method's cells, across the whole of each, on the selection's blue, so they join into one block, or on
144
+ # green when it's already been felt, and others as they move.
145
+ def grid_style(owner, index)
146
+ return cell_style(index) unless owner == view.cursor
147
+ return :felt_selected if view.batch.felt?(view.code_methods[owner])
161
148
 
162
- " #{digit(owner, index)} "
149
+ :feelings_selected
163
150
  end
164
151
 
165
- # Whether the method's felt, and not selected, so its cells are blank.
166
- def hidden?(owner) = owner != view.cursor && view.batch.felt?(view.code_methods[owner])
152
+ # Whether the method's felt, and not selected, so its cells are blank, asked once a frame for each method.
153
+ def hidden?(owner)
154
+ @hidden ||= Hash.new { |hidden, felt| hidden[felt] = felt != view.cursor && view.batch.felt?(view.code_methods[felt]) }
155
+ @hidden[owner]
156
+ end
167
157
 
168
- # Near the method's drift, from 0 to 9: a cell's checksum rolls a small chance of it being one or two away.
169
- # A heavy method's cells also flicker one lower now and then, each at its own time, so they look restless.
158
+ # Near the method's drift, from 0 to 9: a cell's checksum rolls a small chance of it being one to three away, and a
159
+ # restless one flickers one lower now and then.
170
160
  def digit(owner, index)
171
161
  drift = view.code_methods[owner].drift
172
- seed = Zlib.crc32("#{owner}:#{index}")
173
- digit = (drift + NOISE.fetch(seed % NOISE_ROLL, 0)).clamp(0, 9)
174
- return digit if drift < RESTLESS || ((view.tick + seed) % FLICKER_TICKS).nonzero?
162
+ (drift + NOISE.fetch(Zlib.crc32("#{owner}:#{index}") % NOISE_ROLL, 0)).clamp(0, 9) + flicker(owner, index)
163
+ end
175
164
 
176
- digit - 1
165
+ def too_small? = columns < MIN_COLUMNS || filled_rows < MIN_ROWS
166
+
167
+ # The pane in red, saying the screen's too small, in the middle, with the cells laid out without picking a batch for
168
+ # so small a grid, so the keys still move between them.
169
+ def too_small
170
+ view.cells(columns)
171
+ shown = [*TOO_SMALL_ART, TOO_SMALL]
172
+ shown = [TOO_SMALL] if shown.length > height
173
+ lines = Array.new(height, '')
174
+ lines[(height - shown.length) / 2, shown.length] = shown
175
+ lines.map { [segment(it.center(width), :too_small)] }
177
176
  end
178
177
 
179
178
  def blank(message)
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../code_box'
3
4
  require_relative '../events'
4
- require_relative 'celebration'
5
+ require_relative 'cell_moves'
6
+ require_relative 'celebration/celebration'
5
7
  require_relative 'code_methods'
6
8
  require_relative 'feelings_log'
7
9
  require_relative 'weekly_batch'
@@ -10,29 +12,31 @@ module Raindown
10
12
  module TUI
11
13
  # Code feelings, like Severance's macrodata refinement: the project's functions and methods, in any language,
12
14
  # as clusters of digits in a grid, where long methods, and ones whose files keep changing, drift. The cursor moves
13
- # between them, the selected one's code shows below the grid, its first lines or with Enter all that fits, and 1 to 6
14
- # sort how it feels into a bin: frolic, hope, numb, woe, dread or malice, which notes it down in the FeelingsLog,
15
+ # between them, the selected one's code shows below the grid, its first lines or with Enter all that fits, and 1 to 5
16
+ # sort how it feels into a bin: frolic, numb, woe, dread or malice, which notes it down in the FeelingsLog,
15
17
  # staying on it. Woe, dread and malice ask why first, typed until Enter, or Esc to leave it out. E opens the method in the
16
18
  # editor, and X asks Claude Code to fix it, and either way it's read again after, to be felt again.
17
19
  class FeelingsView
20
+ include CellMoves
21
+ include ShownCode
22
+
18
23
  # Best to worst, as CodeHealth scores them, so the keys rate the code.
19
- BINS = { bin1: 'frolic', bin2: 'hope', bin3: 'numb', bin4: 'woe', bin5: 'dread', bin6: 'malice' }.freeze
24
+ BINS = { bin1: 'frolic', bin2: 'numb', bin3: 'woe', bin4: 'dread', bin5: 'malice' }.freeze
20
25
  # The tempers that ask why.
21
26
  NOTED = %w[woe dread malice].freeze
22
27
  # Its own keys, besides the bins: Enter and Space show all of the selected method's code, or its first lines again.
23
28
  # Ctrl+T celebrates, to try the rain and the waffle, and isn't shown with the buttons.
24
- KEYS = %i[up down left right edit execute tick enter toggle expand celebrate].freeze
29
+ KEYS = %i[up down left right edit execute enter toggle expand celebrate].freeze
25
30
 
26
31
  # The temper being noted, and the note typed so far, while it's asking why, whether the selected method's code
27
32
  # shows all that fits, until another's selected, and the rain then the waffle, once the batch is refined.
28
- attr_reader :noting, :note, :cursor, :tick, :log, :expanded, :celebration
33
+ attr_reader :noting, :note, :cursor, :log, :celebration
29
34
 
30
35
  def initialize(root:)
31
36
  @root = root
32
37
  @log = FeelingsLog.new(root:)
33
38
  @last_feelings = {}
34
39
  @cursor = 0
35
- @tick = 0
36
40
  @week = WeeklyBatch.new(log: @log)
37
41
  end
38
42
 
@@ -42,8 +46,17 @@ module Raindown
42
46
  def path = nil
43
47
  def changed? = false
44
48
  def typing? = !noting.nil?
45
- def animating? = celebration&.raining? == true
46
- def status(_navigator) = [:info, "This week's code to feel, #{batch.week}"]
49
+
50
+ # As fast as an animation while it celebrates, through the rain and the waffle, and otherwise reactive, so its digits
51
+ # tremble at the same pace whether it's used with the keys or the mouse.
52
+ def pace(_hovered)
53
+ return :animation if celebration
54
+
55
+ :reactive
56
+ end
57
+
58
+ # Opening it says nothing, as its title is above the grid.
59
+ def status(_navigator) = [:info, '']
47
60
 
48
61
  # Reads the code again, keeping the batch, like after a method's edited or fixed, and the selected method, by its
49
62
  # file and name, even when the methods around it change, moving the feelings about code that's changed, or gone, to
@@ -53,6 +66,11 @@ module Raindown
53
66
  every = CodeMethods.list(@root)
54
67
  @log.move_stale_to_memories(every)
55
68
  @week.read(every)
69
+ reselect(was)
70
+ end
71
+
72
+ # Selects the method again, by its file and name, wherever it is now, or keeps the cursor where it was.
73
+ def reselect(was)
56
74
  self.cursor = code_methods.index { [it.path, it.label] == [was&.path, was&.label] } || @cursor
57
75
  end
58
76
 
@@ -68,15 +86,6 @@ module Raindown
68
86
  @cursor = index
69
87
  end
70
88
 
71
- # Each cell's method, by its index, in reading order, in a grid as many cells across as the columns.
72
- # They're kept, so up and down move a row.
73
- # The first time the grid's drawn, with its rows, right after the view opens, the batch is picked to fill them.
74
- def cells(columns, rows = nil)
75
- @week.fill(columns * rows) if rows
76
- @columns = columns
77
- @cells = code_methods.each_with_index.flat_map { |method, index| [index] * method.size }
78
- end
79
-
80
89
  # Every key while it's asking why or celebrating, and otherwise its own, but Tab to the files, or left from the
81
90
  # first method.
82
91
  def next_step?(key)
@@ -88,7 +97,6 @@ module Raindown
88
97
  def next_step(key)
89
98
  return note_step(key) if typing?
90
99
  return @celebration = nil if celebration&.over?(key)
91
- return (@tick += 1).then { nil } if key == :tick
92
100
  return (@celebration = Celebration.new).then { nil } if key == :celebrate
93
101
  return unless selected
94
102
  return feel(BINS[key]) if BINS.key?(key)
@@ -107,25 +115,13 @@ module Raindown
107
115
  when :down then move_row(1)
108
116
  when :edit then Edit.new(path: selected.path, line: selected.line)
109
117
  when :execute then fix
110
- when :enter, :toggle, :expand then (@expanded = !@expanded).then { nil }
118
+ when :enter, :toggle, :expand then toggle_expanded
111
119
  end
112
120
  end
113
121
 
114
122
  # Asks to fix the method, with how it last felt, by its file and name, which stay the same when its lines change.
115
123
  def fix = Fix.new(feeling: @last_feelings[[selected.path, selected.label]] || Feeling.new(method: selected, temper: nil, note: nil))
116
124
 
117
- def move(by)
118
- self.cursor = @cursor + by
119
- nil
120
- end
121
-
122
- # To the method in the cell above or below the selected one's first cell.
123
- def move_row(by)
124
- cell = @cells[@cells.index(@cursor).to_i + (by * @columns)]
125
- self.cursor = cell if cell
126
- nil
127
- end
128
-
129
125
  # Frolic and numb are noted down straight away, while the rest ask why first.
130
126
  def feel(temper)
131
127
  return note_down(temper, nil) unless NOTED.include?(temper)
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../stat'
4
+
5
+ module Raindown
6
+ module TUI
7
+ # Code feelings' magnified digits: the method the mouse is over's, in purple and lavender by turns, so the digits
8
+ # beside and below each other stand apart, each in a stat's block digits across its cell and the cells above and below
9
+ # it, drawn over the grid.
10
+ module Magnifier
11
+ private
12
+
13
+ # The lines with the digits of the method the mouse is over magnified, each across its cell and the cells above and
14
+ # below it.
15
+ def magnified(lines, cells, first, rows)
16
+ magnified_cells(cells, first, rows).each do |index|
17
+ magnify_cell(lines, (index / columns) - first, cells[index], index)
18
+ end
19
+ lines
20
+ end
21
+
22
+ # The cell's digit magnified over its row's line, the top one, and the lines above and below it.
23
+ def magnify_cell(lines, top, owner, index)
24
+ style = magnified_style(index)
25
+ Stat::DIGITS[digit(owner, index).to_s].each_with_index do |text, line|
26
+ magnify(lines, top + line, segment(text, style, width: FeelingsRenderer::CELL), 1 + (index % columns * FeelingsRenderer::CELL))
27
+ end
28
+ end
29
+
30
+ # Purple and lavender in turn, along the cell's row and down its column, like a chessboard's squares.
31
+ def magnified_style(index)
32
+ return :feelings_purple if ((index / columns) + (index % columns)).odd?
33
+
34
+ :feelings
35
+ end
36
+
37
+ # How much of the batch is refined, at the right of the line above the grid, or nothing while a magnified digit
38
+ # reaches it there.
39
+ def progress_line(cells, first, rows)
40
+ progress = "#{view.batch.progress} "
41
+ start = width - display_width(progress)
42
+ return padded([]) if magnified_from?(cells, first, rows, start)
43
+
44
+ padded([segment('', :blank, width: [start, 0].max), segment(progress, :body, width: display_width(progress))])
45
+ end
46
+
47
+ # Whether a magnified digit in the first row reaches the column, or past it.
48
+ def magnified_from?(cells, first, rows, column)
49
+ magnified_cells(cells, first, rows).any? { (it / columns) == first && 1 + (((it % columns) + 1) * FeelingsRenderer::CELL) > column }
50
+ end
51
+
52
+ # The cells of the rows from the first that are the method the mouse is over's, unless they're blank or selected,
53
+ # which keeps its selection's style.
54
+ def magnified_cells(cells, first, rows)
55
+ (first * columns...(first + rows) * columns).select do |index|
56
+ owner = cells[index]
57
+ owner && owner != view.cursor && navigator.hovered == Click.new(pane:, index: owner, action: :focus) && !hidden?(owner)
58
+ end
59
+ end
60
+
61
+ # The segment over the line at the column, clicking whatever cell it covers, so the mouse over it is over that cell.
62
+ def magnify(lines, line, segment, column)
63
+ covered = segments_between(lines[line], column, column + display_width(segment.text)).first
64
+ lines[line] = overlay_segments(lines[line], [segment.with(event: covered&.event)], column:)
65
+ end
66
+ end
67
+ end
68
+ end