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.
- checksums.yaml +4 -4
- data/lib/nodes/endpoint_node.rb +3 -2
- data/lib/raindown/actions.rb +5 -3
- data/lib/raindown/bare_directives.rb +14 -27
- data/lib/raindown/blueprints.rb +255 -0
- data/lib/raindown/cli.rb +39 -54
- data/lib/raindown/config.rb +2 -3
- data/lib/raindown/directives.rb +30 -13
- data/lib/raindown/document.rb +21 -15
- data/lib/raindown/endpoint/endpoint_runner.rb +13 -5
- data/lib/raindown/node_blocks.rb +22 -16
- data/lib/raindown/node_injector.rb +7 -10
- data/lib/raindown/pages.rb +1 -1
- data/lib/raindown/pre_renderer.rb +18 -0
- data/lib/raindown/tui/blueprints/blueprints_renderer.rb +93 -0
- data/lib/raindown/tui/blueprints/blueprints_view.rb +146 -0
- data/lib/raindown/tui/blueprints/canvas.rb +58 -0
- data/lib/raindown/tui/blueprints/code_comments.rb +71 -0
- data/lib/raindown/tui/blueprints/filter.rb +77 -0
- data/lib/raindown/tui/blueprints/traced_code.rb +56 -0
- data/lib/raindown/tui/blueprints/tracing.rb +129 -0
- data/lib/raindown/tui/{feelings/code_box.rb → code_box.rb} +44 -30
- data/lib/raindown/tui/endpoints.rb +2 -2
- data/lib/raindown/tui/events.rb +3 -1
- data/lib/raindown/tui/feelings/bins.rb +5 -5
- data/lib/raindown/tui/feelings/{celebration.rb → celebration/celebration.rb} +5 -2
- data/lib/raindown/tui/feelings/{scene.rb → celebration/scene.rb} +4 -1
- data/lib/raindown/tui/feelings/cell_moves.rb +34 -0
- data/lib/raindown/tui/feelings/feelings_code_box.rb +40 -0
- data/lib/raindown/tui/feelings/feelings_log.rb +22 -19
- data/lib/raindown/tui/feelings/feelings_renderer.rb +94 -95
- data/lib/raindown/tui/feelings/feelings_view.rb +28 -32
- data/lib/raindown/tui/feelings/magnifier.rb +68 -0
- data/lib/raindown/tui/feelings/motion.rb +93 -0
- data/lib/raindown/tui/feelings/waffle/baked_waffle.rb +88 -0
- data/lib/raindown/tui/feelings/waffle/balloons.rb +81 -0
- data/lib/raindown/tui/feelings/waffle/confetti.rb +55 -0
- data/lib/raindown/tui/feelings/waffle/sparkles.rb +61 -0
- data/lib/raindown/tui/feelings/waffle/waffle.rb +62 -0
- data/lib/raindown/tui/feelings/weekly_batch.rb +24 -5
- data/lib/raindown/tui/layout.rb +25 -18
- data/lib/raindown/tui/navigator/commands.rb +32 -1
- data/lib/raindown/tui/navigator/mouse.rb +43 -0
- data/lib/raindown/tui/navigator/navigator.rb +40 -44
- data/lib/raindown/tui/navigator/{search.rb → teleport.rb} +14 -12
- data/lib/raindown/tui/navigator/views.rb +2 -3
- data/lib/raindown/tui/{workers/project.rb → project.rb} +33 -22
- data/lib/raindown/tui/{menu_overlay.rb → project_box.rb} +1 -1
- data/lib/raindown/tui/renderers/renderer.rb +1 -0
- data/lib/raindown/tui/saves.rb +22 -0
- data/lib/raindown/tui/{search_overlay.rb → search_box.rb} +27 -14
- data/lib/raindown/tui/terminal/input.rb +58 -7
- data/lib/raindown/tui/terminal/screen.rb +31 -26
- data/lib/raindown/tui/terminal/styles.rb +55 -28
- data/lib/raindown/tui/terminal/terminal_title.rb +31 -0
- data/lib/raindown/tui/text.rb +35 -12
- data/lib/raindown/tui/views/dashboard_view.rb +2 -2
- data/lib/raindown/tui/views/endpoint_view.rb +3 -3
- data/lib/raindown/tui/views/list_view.rb +1 -1
- data/lib/raindown/tui/views/outline_view.rb +5 -4
- data/lib/raindown/tui.rb +17 -7
- data/lib/raindown/watcher/changes.rb +33 -0
- data/lib/raindown/watcher/file_watcher.rb +58 -0
- data/lib/raindown/watcher/project.rb +47 -0
- data/lib/raindown/watcher/supervisor.rb +111 -0
- data/lib/raindown/{tui/workers → workers}/worker.rb +37 -29
- data/lib/raindown/workers/workers.rb +63 -0
- data/lib/raindown.rb +4 -4
- data/lib/version.rb +1 -1
- metadata +37 -17
- data/lib/raindown/project_worker.rb +0 -48
- data/lib/raindown/supervisor.rb +0 -160
- data/lib/raindown/tui/feelings/waffle.rb +0 -43
- data/lib/raindown/tui/workers/workers.rb +0 -53
- data/lib/raindown/watcher.rb +0 -57
- /data/lib/raindown/tui/feelings/{fireworks.rb → celebration/fireworks.rb} +0 -0
- /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
|
|
30
|
-
HEADER = %i[
|
|
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
|
-
|
|
43
|
-
|
|
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.
|
|
56
|
+
# now, to the memories.
|
|
57
57
|
def move_stale_to_memories(methods)
|
|
58
|
-
now = methods.
|
|
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.
|
|
74
|
-
#
|
|
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
|
-
|
|
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) =
|
|
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[:
|
|
97
|
+
def path(row) = row[:location].sub(/:\d+\z/, '')
|
|
97
98
|
|
|
98
|
-
# The row's file, method and checksum,
|
|
99
|
-
def key(row) = [path(row), row[:method], row[:checksum]
|
|
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
|
-
|
|
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 '
|
|
9
|
-
require_relative '
|
|
10
|
-
require_relative '
|
|
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
|
|
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 ────╮
|
|
23
|
-
# │ FROLIC │ │
|
|
24
|
-
# │ ███░ 9 │ │
|
|
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
|
|
29
|
+
include FeelingsCodeBox
|
|
30
|
+
include Magnifier
|
|
31
|
+
include Motion
|
|
29
32
|
include Waffle
|
|
30
33
|
|
|
31
|
-
# The columns
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
80
|
-
#
|
|
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 = [
|
|
99
|
+
rows = [lines - 2, 0].max
|
|
84
100
|
first = first_row(cells, columns, rows)
|
|
85
|
-
grid = magnified([
|
|
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
|
|
136
|
-
# takes rows from the grid, which then scrolls to keep the selected method
|
|
137
|
-
|
|
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
|
|
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,
|
|
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
|
|
158
|
-
#
|
|
159
|
-
def cell_text(owner, index
|
|
160
|
-
return ' ' * CELL if hidden?(owner)
|
|
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
|
-
|
|
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)
|
|
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
|
|
169
|
-
#
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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 '
|
|
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
|
|
14
|
-
# sort how it feels into a bin: frolic,
|
|
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: '
|
|
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
|
|
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, :
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
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
|