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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../blueprints'
|
|
4
|
+
require_relative '../feelings/code_methods'
|
|
5
|
+
|
|
6
|
+
module Raindown
|
|
7
|
+
module TUI
|
|
8
|
+
# The blueprints drawn in the comments above a project's classes and methods, like this codebase's renderers', each a
|
|
9
|
+
# run of lines indented further than the comment's prose, described by the prose above it, but not a code block,
|
|
10
|
+
# like an example of a file.
|
|
11
|
+
module CodeComments
|
|
12
|
+
# A comment's line, by what's after its marker and a space, in the languages Code feelings reads.
|
|
13
|
+
COMMENT = %r{\A\s*(?:#|//)(?: |\z)(.*)\z}
|
|
14
|
+
# A line of a comment that's drawn, indented further than its prose.
|
|
15
|
+
DRAWN_LINE = /\A /
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# The blueprints in the comments of the project's code, by file, from the top of each.
|
|
20
|
+
def blueprints(root)
|
|
21
|
+
CodeMethods.files(root).flat_map { |path| in_file(path, path.delete_prefix(File.join(root, ''))) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def in_file(path, relative)
|
|
25
|
+
lines = File.readlines(path, chomp: true)
|
|
26
|
+
comments(lines).flat_map { |start, texts| drawings(texts, start, relative) }
|
|
27
|
+
rescue SystemCallError, ArgumentError
|
|
28
|
+
[]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Each block of comment lines above a class or method, by the index of its first line, with each line's text.
|
|
32
|
+
def comments(lines)
|
|
33
|
+
lines.each_index.filter_map do |index|
|
|
34
|
+
next unless lines[index].match?(COMMENT)
|
|
35
|
+
next if index.positive? && lines[index - 1].match?(COMMENT)
|
|
36
|
+
|
|
37
|
+
texts = lines[index..].take_while { it.match?(COMMENT) }.map { it[COMMENT, 1] }
|
|
38
|
+
[index, texts] if defines?(lines[index + texts.length])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def defines?(line) = line.to_s.match?(CodeMethods::DEFINITION) || line.to_s.match?(CodeMethods::OWNER)
|
|
43
|
+
|
|
44
|
+
# The comment's runs of indented lines that are blueprints, each described by the prose above it, if any.
|
|
45
|
+
def drawings(texts, start, path)
|
|
46
|
+
index = start
|
|
47
|
+
[[], *runs(texts)].each_cons(2).filter_map do |prose, drawn|
|
|
48
|
+
index += prose.length
|
|
49
|
+
next unless drawn.first.match?(DRAWN_LINE)
|
|
50
|
+
|
|
51
|
+
blueprint(drawn, prose.join(' ').strip, path:, line: index + 1)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The comment's lines in runs of prose and of indented lines, by turns.
|
|
56
|
+
def runs(texts) = texts.slice_when { |before, after| before.match?(DRAWN_LINE) != after.match?(DRAWN_LINE) }
|
|
57
|
+
|
|
58
|
+
# The drawn lines, without the indent they share, as a blueprint when they're a drawing.
|
|
59
|
+
def blueprint(lines, prose, path:, line:)
|
|
60
|
+
indent = lines.map { it[/\A */].length }.min
|
|
61
|
+
lines = lines.map { it[indent..] }
|
|
62
|
+
return unless drawing?(lines)
|
|
63
|
+
|
|
64
|
+
Blueprints::Blueprint.new(time: nil, kind: Blueprints.kind(lines), lines:, path:, line:, description: (prose unless prose.empty?))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Whether the lines are drawn, and whole, rather than a code block.
|
|
68
|
+
def drawing?(lines) = lines.none? { it.start_with?('```') } && lines.join.match?(Blueprints::DRAWN) && Blueprints.whole?(lines)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raindown
|
|
4
|
+
module TUI
|
|
5
|
+
# The blueprints filtered by keywords, each a word in one or in its kind, ignoring case, typed into a box like
|
|
6
|
+
# Teleport's, which f or a click on the title opens. Up and down move between all the blueprints that match, as
|
|
7
|
+
# they're shown, with the box listing those around the selected one, Enter or a click keeps the filter, and Esc
|
|
8
|
+
# clears it.
|
|
9
|
+
module Filter
|
|
10
|
+
# The most matches the box lists at once.
|
|
11
|
+
LIMIT = 10
|
|
12
|
+
|
|
13
|
+
# The keywords typed, and whether the box is open, taking what's typed.
|
|
14
|
+
attr_reader :query
|
|
15
|
+
|
|
16
|
+
def typing? = @filtering == true
|
|
17
|
+
|
|
18
|
+
# The blueprints with every keyword, or all of them, filtered again only when the keywords or the blueprints change,
|
|
19
|
+
# as what's drawn asks for the selected one many times a frame.
|
|
20
|
+
def blueprints
|
|
21
|
+
@filtered = [query, @all, filtered(query.to_s.downcase.split)] unless filtered_now?
|
|
22
|
+
@filtered.last
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The box's matches, from the first it lists, each with its text and where it is, and the selected one, which is
|
|
26
|
+
# the one shown.
|
|
27
|
+
def matches = blueprints[first_match, LIMIT].to_a
|
|
28
|
+
def match = @cursor
|
|
29
|
+
|
|
30
|
+
# The first match the box lists, scrolled as little as it takes to list the selected one.
|
|
31
|
+
def first_match
|
|
32
|
+
first = @first_match.to_i.clamp(@cursor - LIMIT + 1, @cursor)
|
|
33
|
+
@first_match = [first, 0].max
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The f key opens the filter here, rather than Code feelings.
|
|
37
|
+
def own_key?(key) = key == :feelings
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def filtered_now? = @filtered && @filtered[0] == query && @filtered[1].equal?(@all)
|
|
42
|
+
|
|
43
|
+
def filtered(keywords)
|
|
44
|
+
return @all if keywords.empty?
|
|
45
|
+
|
|
46
|
+
@all.select { |blueprint| keywords.all? { blueprint.words.downcase.include?(it) || blueprint.kind.include?(it) } }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def open_filter
|
|
50
|
+
@query = query.to_s
|
|
51
|
+
@filtering = true
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def filter_step(key)
|
|
56
|
+
case key
|
|
57
|
+
when Type then filter_to(query + key.text)
|
|
58
|
+
when :backspace then filter_to(query[0...-1])
|
|
59
|
+
when :up, :down then self.cursor = @cursor + self.class::MOVES[key]
|
|
60
|
+
when :enter then @filtering = false
|
|
61
|
+
when :escape then close_filter
|
|
62
|
+
end
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def filter_to(keywords)
|
|
67
|
+
@query = keywords
|
|
68
|
+
self.cursor = 0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def close_filter
|
|
72
|
+
filter_to('')
|
|
73
|
+
@filtering = false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../feelings/code_methods'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
module TUI
|
|
7
|
+
# The code a blueprint's box names, found among the project's methods, read once, when they're first needed, and which
|
|
8
|
+
# of it's shown.
|
|
9
|
+
module TracedCode
|
|
10
|
+
# The code the box names, each once, in the order it names it: by a method, like Watcher.listen or
|
|
11
|
+
# Navigator#next_step, or a class, like Workers, whose first method it is, or a method's name alone that only one
|
|
12
|
+
# method has.
|
|
13
|
+
def codes(box)
|
|
14
|
+
(@codes ||= {})[box] ||= box.text.scan(/[A-Z]\w*(?:::\w+)*(?:[.#][a-z_]\w*[?!]?)?|[a-z_]\w*[?!]?/).filter_map { named(it) }.uniq
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The code shown of what the selected box names, which Space or a click on the box moves on to the next of, or nil.
|
|
18
|
+
def code
|
|
19
|
+
named = codes(boxes[box]) if box
|
|
20
|
+
named[@code % named.length] if named&.any?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# The box at the index, showing the first code it names, from its first lines.
|
|
26
|
+
def select_box(index)
|
|
27
|
+
@box = index
|
|
28
|
+
@code = 0
|
|
29
|
+
@expanded = false
|
|
30
|
+
follow
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def next_code
|
|
35
|
+
@code += 1
|
|
36
|
+
@expanded = false
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The method a name in a box names, by its owner and name, or its owner's first, or its name if only one has it.
|
|
41
|
+
def named(name)
|
|
42
|
+
owner, method = name.split(/[.#]/, 2)
|
|
43
|
+
owner = owner.split('::').last
|
|
44
|
+
return methods.find { it.owner == owner && it.name == method } if method
|
|
45
|
+
return first_method(owner) if owner.match?(/\A[A-Z]/)
|
|
46
|
+
|
|
47
|
+
found = methods.select { it.name == name }
|
|
48
|
+
found.first if found.one?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def first_method(owner) = methods.select { it.owner == owner }.min_by { [it.path, it.line] }
|
|
52
|
+
|
|
53
|
+
def methods = @methods ||= CodeMethods.list(@root)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../code_box'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
module TUI
|
|
7
|
+
# A blueprint traced to its code: its lines and text cornflower blue, the selected box highlighted, on a canvas that
|
|
8
|
+
# scrolls, with a marker at each edge there's more beyond, then the code the selected box names in a box below it,
|
|
9
|
+
# as tall whether or not there's code, so the canvas above stays still.
|
|
10
|
+
module Tracing
|
|
11
|
+
include CodeBox
|
|
12
|
+
|
|
13
|
+
# The rows the canvas keeps when the code box is expanded.
|
|
14
|
+
MIN_CANVAS_ROWS = 2
|
|
15
|
+
# The line above the canvas, and the rows for its up and down markers.
|
|
16
|
+
AROUND_CANVAS = 3
|
|
17
|
+
# The most lines its description wraps onto, above the canvas.
|
|
18
|
+
DESCRIPTION_LINES = 3
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# When it was saved and which one it is, then its description, then the canvas, between the rows for its up and
|
|
23
|
+
# down markers, then the code its selected box names, in a CodeBox, which takes rows from the canvas when it's
|
|
24
|
+
# expanded.
|
|
25
|
+
def traced_lines
|
|
26
|
+
box = code_box
|
|
27
|
+
description = description_lines
|
|
28
|
+
@canvas_height = [height - box.length - description.length - AROUND_CANVAS, 1].max
|
|
29
|
+
canvas = canvas_lines
|
|
30
|
+
[about(view.selected), *description, marker_line(:up, '▲'), *canvas, marker_line(:down, '▼'), *box].first(height)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The paragraph above it in Claude Code's reply, below a blank line, in the text's own colour, wrapped.
|
|
34
|
+
def description_lines
|
|
35
|
+
description = view.selected.description
|
|
36
|
+
return [] unless description
|
|
37
|
+
|
|
38
|
+
lines = wrap(description, width - 2).first(DESCRIPTION_LINES).map { [segment(" #{it.rstrip}", :text)] }
|
|
39
|
+
[[segment('', :blank)], *lines]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The canvas's rows of the drawing, from its offset, which follows the selected box.
|
|
43
|
+
def canvas_lines
|
|
44
|
+
column, row = view.canvas(canvas_width, canvas_height)
|
|
45
|
+
@beyond = view.beyond(canvas_width, canvas_height)
|
|
46
|
+
Array.new(canvas_height) { canvas_line(row + it, column, middle: it == canvas_height / 2) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The pane's width, less a column at each side for the left and right markers.
|
|
50
|
+
def canvas_width = [width - 2, 1].max
|
|
51
|
+
|
|
52
|
+
# The rows between the line above, the markers' rows and the code box.
|
|
53
|
+
attr_reader :canvas_height
|
|
54
|
+
|
|
55
|
+
# A row of the drawing seen through the canvas, from its column, with the left and right markers on the middle row.
|
|
56
|
+
def canvas_line(row, column, middle:)
|
|
57
|
+
segments = drawn_segments(view.drawing[row].to_a, row, column)
|
|
58
|
+
drawn = segments.sum { display_width(it.text) }
|
|
59
|
+
[marker(:left, '◀', middle), *segments, segment('', :blank, width: [canvas_width - drawn, 0].max), marker(:right, '▶', middle)]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The line's columns across the canvas, from its column, in runs of the same style and box.
|
|
63
|
+
def drawn_segments(line, row, column)
|
|
64
|
+
runs = (column...[column + canvas_width, line.length].min).chunk_while { drawn_part(row, _1) == drawn_part(row, _2) }
|
|
65
|
+
runs.map { |run| run_segment(line, run, row) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def marker(side, arrow, shown)
|
|
69
|
+
return segment(arrow, :divider, width: 1) if shown && @beyond[side]
|
|
70
|
+
|
|
71
|
+
segment(' ', :blank, width: 1)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The row above or below the canvas, with its marker in the middle when there's more of the drawing that way.
|
|
75
|
+
def marker_line(side, arrow)
|
|
76
|
+
return [segment('', :blank)] unless @beyond[side]
|
|
77
|
+
|
|
78
|
+
[segment(arrow.center(width), :divider)]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def drawn_part(row, column) = [part_style(row, column), box_click(row, column)]
|
|
82
|
+
|
|
83
|
+
# The line's characters at the run of columns, in their style, which select their box when they're clicked, with half
|
|
84
|
+
# a wide character, cut at the canvas's edge, left blank.
|
|
85
|
+
def run_segment(line, run, row)
|
|
86
|
+
segment(line[run.first..run.last].join, part_style(row, run.first), width: run.size, event: box_click(row, run.first))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def box_click(row, column)
|
|
90
|
+
clicked = view.boxes.find { it.cover?(row, column) }
|
|
91
|
+
Click.new(pane:, index: view.cursor, action: clicked) if clicked
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The selected box highlighted, and the rest cornflower blue.
|
|
95
|
+
def part_style(row, column)
|
|
96
|
+
return :selected if view.box && view.boxes[view.box].cover?(row, column)
|
|
97
|
+
|
|
98
|
+
:divider
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The code box shows the code the selected box names, or that it names none.
|
|
102
|
+
def boxed_method = view.code
|
|
103
|
+
def box_style = :divider
|
|
104
|
+
def box_footer = []
|
|
105
|
+
def box_room = height - AROUND_CANVAS - BOX_LINES - MIN_CANVAS_ROWS
|
|
106
|
+
|
|
107
|
+
def box_title(method)
|
|
108
|
+
return 'No code' unless method
|
|
109
|
+
|
|
110
|
+
"#{where(method)} · #{method.owner}##{method.name}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Which of the code the box names is shown, at the right of the box's top, when it names more than one.
|
|
114
|
+
def box_note(method)
|
|
115
|
+
named = view.codes(view.boxes[view.box]) if method
|
|
116
|
+
return '' unless named && named.length > 1
|
|
117
|
+
|
|
118
|
+
" #{named.index(method) + 1} of #{named.length} ─"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The method's code highlighted, or that the box names no code.
|
|
122
|
+
def box_parts(method)
|
|
123
|
+
return [[['Nothing in this box names a class or method in the code.', :body]]] unless method
|
|
124
|
+
|
|
125
|
+
highlighted(method)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -1,40 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'highlighter'
|
|
4
4
|
|
|
5
5
|
module Raindown
|
|
6
6
|
module TUI
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# A box of a method's code, highlighted, its first lines or, expanded, all that fit, like Code feelings' below its
|
|
8
|
+
# grid, with why it felt that way while that's typed, below a line, or Blueprints' below a blueprint:
|
|
9
9
|
# ╭─ lib/text.rb:62 · Text#segments_between ──────────── changed 6× ─╮
|
|
10
10
|
# │ def segments_between(line, from, to) │
|
|
11
11
|
# │ [12 more lines] │
|
|
12
12
|
# ├───────────────────────────────────────────────────────────────────┤
|
|
13
13
|
# │ Why DREAD? > column maths, a character at a time▏ │
|
|
14
14
|
# ╰───────────────────────────────────────────────────────────────────╯
|
|
15
|
+
# The renderer gives the method shown, as boxed_method, or nil, and box_title, box_note for the top's right, box_style,
|
|
16
|
+
# box_room, the most lines of code that fit expanded, box_parts, the lines it shows, and box_footer, any lines below them.
|
|
17
|
+
# Its view keeps what it shows, as ShownCode.
|
|
15
18
|
module CodeBox
|
|
16
19
|
# The most of a method's code that shows, until it's expanded, and the rows the grid keeps when it is.
|
|
17
|
-
CODE_LINES =
|
|
20
|
+
CODE_LINES = 6
|
|
18
21
|
MIN_GRID_ROWS = 2
|
|
19
22
|
# The box's top and bottom, and the line of how many more lines there are.
|
|
20
23
|
BOX_LINES = 3
|
|
21
24
|
|
|
22
25
|
private
|
|
23
26
|
|
|
24
|
-
# The
|
|
27
|
+
# The method's code in a box, then the lines below it.
|
|
25
28
|
def code_box
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
method = boxed_method
|
|
30
|
+
footer = box_footer
|
|
31
|
+
code = code_lines(box_parts(method), box_room - footer.length)
|
|
32
|
+
top = box_edge("╭─ #{box_title(method)} ", "#{box_note(method)}╮")
|
|
33
|
+
[top, *code, *blank_lines(code.length + footer.length), *footer, box_edge('╰', '╯')]
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
def
|
|
36
|
+
# Blank lines after a short method's code and its footer, so the box is always as tall as it gets until it's expanded,
|
|
37
|
+
# and what's above it stays still.
|
|
38
|
+
def blank_lines(used) = Array.new([CODE_LINES + 1 - used, 0].max) { boxed([]) }
|
|
39
|
+
|
|
40
|
+
# The first lines of the parts, or all that fit in the room when it's expanded, wrapped to fit the box, then how
|
|
41
|
+
# many more there are, clicked to show them, or a line to show fewer again.
|
|
42
|
+
def code_lines(parts, room)
|
|
35
43
|
shown = CODE_LINES
|
|
36
44
|
shown = [room, CODE_LINES].max if view.expanded
|
|
37
|
-
lines =
|
|
45
|
+
lines = parts.map { wrapped(it) }
|
|
38
46
|
fitting = lines.take_while { (shown -= it.length) >= 0 }
|
|
39
47
|
code = fitting.flatten(1).map { boxed(it) }
|
|
40
48
|
return code if lines.sum(&:length) <= CODE_LINES
|
|
@@ -42,13 +50,11 @@ module Raindown
|
|
|
42
50
|
[*code, more_line(lines.length - fitting.length)]
|
|
43
51
|
end
|
|
44
52
|
|
|
45
|
-
def highlighted_code = Highlighter.lines(dedented(view.selected.lines), filename: view.selected.path)
|
|
46
|
-
|
|
47
53
|
# The highlighted line's [text, style] parts in as many lines as it takes to fit the box, like a note's, those
|
|
48
54
|
# after the first carrying on after an ellipsis.
|
|
49
55
|
def wrapped(parts)
|
|
50
56
|
room = width - 6
|
|
51
|
-
lead = [carried_lead(parts, room),
|
|
57
|
+
lead = [carried_lead(parts, room), box_style]
|
|
52
58
|
rest = room - display_width(lead[0])
|
|
53
59
|
return [parts] if rest < 1
|
|
54
60
|
|
|
@@ -91,18 +97,8 @@ module Raindown
|
|
|
91
97
|
boxed([[text, :button]], event: Click.new(pane:, index: view.cursor, action: :expand))
|
|
92
98
|
end
|
|
93
99
|
|
|
94
|
-
#
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
# The text after the lead, wrapped to fit the box, with the lines after the first lined up under the text.
|
|
98
|
-
def hanging(lead, text)
|
|
99
|
-
indent = ' ' * display_width(lead)
|
|
100
|
-
lines = wrap(text, width - 6 - indent.length)
|
|
101
|
-
[lead, *Array.new(lines.length - 1, indent)].zip(lines).map(&:join)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Where the method is, its name, and how many commits changed its file.
|
|
105
|
-
def box_top(method) = box_edge("╭─ #{where(method)} · #{method.label} ", " changed #{method.changes}× ─╮")
|
|
100
|
+
# The method's code highlighted, kept by the view while it's shown.
|
|
101
|
+
def highlighted(method) = view.highlighted(method) { Highlighter.lines(dedented(method.lines), filename: method.path) }
|
|
106
102
|
|
|
107
103
|
# The lines without the indent they share, like a method's in its class.
|
|
108
104
|
def dedented(lines)
|
|
@@ -114,7 +110,7 @@ module Raindown
|
|
|
114
110
|
|
|
115
111
|
# A line of the box, a column in from each side of the pane, of [text, style] parts, like highlighted code's.
|
|
116
112
|
def boxed(parts, event: nil)
|
|
117
|
-
padded([segment(' │ ',
|
|
113
|
+
padded([segment(' │ ', box_style, width: 3), *code_segments(parts, width - 6, event:), segment(' │', box_style, width: 2)])
|
|
118
114
|
end
|
|
119
115
|
|
|
120
116
|
# The box's top or bottom, with the left text cut short, rather than the corner, when there isn't room.
|
|
@@ -122,7 +118,25 @@ module Raindown
|
|
|
122
118
|
room = width - 2 - display_width(right)
|
|
123
119
|
left = "#{fit(left, room - 1)} " if display_width(left) > room
|
|
124
120
|
fill = [room - display_width(left), 0].max
|
|
125
|
-
padded([segment(" #{left}#{'─' * fill}#{right}",
|
|
121
|
+
padded([segment(" #{left}#{'─' * fill}#{right}", box_style, width: width - 1)])
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# What a view keeps of the code its CodeBox shows: whether it's expanded, and its highlighting, from frame to frame
|
|
126
|
+
# while it's the one shown.
|
|
127
|
+
module ShownCode
|
|
128
|
+
attr_reader :expanded
|
|
129
|
+
|
|
130
|
+
def highlighted(method)
|
|
131
|
+
@highlighted = { method => yield } unless @highlighted&.key?(method)
|
|
132
|
+
@highlighted[method]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def toggle_expanded
|
|
138
|
+
@expanded = !@expanded
|
|
139
|
+
nil
|
|
126
140
|
end
|
|
127
141
|
end
|
|
128
142
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require_relative 'endpoint'
|
|
4
4
|
require_relative 'views/endpoint_view'
|
|
5
5
|
require_relative 'row'
|
|
6
|
-
require_relative 'workers/worker'
|
|
6
|
+
require_relative '../workers/worker'
|
|
7
7
|
|
|
8
8
|
module Raindown
|
|
9
9
|
module TUI
|
|
@@ -22,7 +22,7 @@ module Raindown
|
|
|
22
22
|
def load
|
|
23
23
|
@endpoints = @workers.each.flat_map do |worker|
|
|
24
24
|
worker.call(:endpoints)
|
|
25
|
-
rescue Worker::Error
|
|
25
|
+
rescue Workers::Worker::Error
|
|
26
26
|
[]
|
|
27
27
|
end
|
|
28
28
|
end
|
data/lib/raindown/tui/events.rb
CHANGED
|
@@ -9,8 +9,10 @@ module Raindown
|
|
|
9
9
|
|
|
10
10
|
# The mouse moving over what a click there would send, if anything, to show it, like a method's cells in bold.
|
|
11
11
|
Hover = Data.define(:target)
|
|
12
|
+
# The mouse moved with its button held, by columns across and rows down, like to drag a blueprint's canvas.
|
|
13
|
+
Drag = Data.define(:columns, :rows)
|
|
12
14
|
|
|
13
|
-
# A character typed while typing, like in
|
|
15
|
+
# A character typed while typing, like in Teleport, which other keys' events would take, like "j" for :down.
|
|
14
16
|
Type = Data.define(:text)
|
|
15
17
|
|
|
16
18
|
# What the Screen does after an event, other than drawing again, or stopping for :quit.
|
|
@@ -6,17 +6,17 @@ module Raindown
|
|
|
6
6
|
module TUI
|
|
7
7
|
# Code feelings' bins below the code box, one for each temper, best to worst, each a box with its key's number, its
|
|
8
8
|
# temper and a bar of how many feelings it holds, highlighted when it's the chosen temper:
|
|
9
|
-
# ╭─ 01 ────╮ ╭─ 02 ────╮ ╭─ 03 ────╮ ╭─ 04 ────╮ ╭─ 05 ────╮
|
|
10
|
-
# │ FROLIC │ │
|
|
11
|
-
# │ ███░ 9 │ │
|
|
12
|
-
# ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯
|
|
9
|
+
# ╭─ 01 ────╮ ╭─ 02 ────╮ ╭─ 03 ────╮ ╭─ 04 ────╮ ╭─ 05 ────╮
|
|
10
|
+
# │ FROLIC │ │ NUMB │ │ WOE │ │ DREAD │ │ MALICE │
|
|
11
|
+
# │ ███░ 9 │ │ ████ 41 │ │ ██░░ 6 │ │ █░░░ 3 │ │ █░░░ 2 │
|
|
12
|
+
# ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯
|
|
13
13
|
module Bins
|
|
14
14
|
# A bin's lines.
|
|
15
15
|
BIN_LINES = 4
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
-
# The
|
|
19
|
+
# The five bins, each a box with its number, its temper, and a bar of how many feelings it holds.
|
|
20
20
|
def bins
|
|
21
21
|
counts = view.log.counts
|
|
22
22
|
most = [counts.values.max, 1].max
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Raindown
|
|
4
4
|
module TUI
|
|
5
5
|
# When a batch of code feelings is refined, like Severance's waffle party: rain, a frame each tick, for about five
|
|
6
|
-
# seconds of the Screen's
|
|
6
|
+
# seconds of the Screen's 50 ms frames, then a waffle, until a key's pressed.
|
|
7
7
|
class Celebration
|
|
8
|
-
RAIN_FRAMES =
|
|
8
|
+
RAIN_FRAMES = 100
|
|
9
9
|
|
|
10
10
|
attr_reader :frame
|
|
11
11
|
|
|
@@ -15,6 +15,9 @@ module Raindown
|
|
|
15
15
|
|
|
16
16
|
def raining? = frame < RAIN_FRAMES
|
|
17
17
|
|
|
18
|
+
# The frames since the rain stopped, which pop the waffle.
|
|
19
|
+
def popping = frame - RAIN_FRAMES
|
|
20
|
+
|
|
18
21
|
# Each tick's a frame, and any other key puts the waffle away, which is when it's over.
|
|
19
22
|
def over?(key)
|
|
20
23
|
@frame += 1 if key == :tick
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative '../../text'
|
|
4
|
+
require_relative '../waffle/balloons'
|
|
5
|
+
require_relative '../waffle/confetti'
|
|
6
|
+
require_relative '../waffle/sparkles'
|
|
4
7
|
require_relative 'fireworks'
|
|
5
8
|
require_relative 'rain'
|
|
6
9
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raindown
|
|
4
|
+
module TUI
|
|
5
|
+
# The cells Code feelings' methods take in its grid, and how its cursor moves between them, along them and a row up
|
|
6
|
+
# or down.
|
|
7
|
+
module CellMoves
|
|
8
|
+
# Each cell's method, by its index, in reading order, in a grid as many cells across as the columns.
|
|
9
|
+
# They're kept, so up and down move a row.
|
|
10
|
+
# The first time the grid's drawn, with its rows, right after the view opens, the batch is picked to fill them, and
|
|
11
|
+
# again a while after the screen's resized, keeping the selected method where it's still in the batch.
|
|
12
|
+
def cells(columns, rows = nil)
|
|
13
|
+
was = selected
|
|
14
|
+
reselect(was) if rows && @week.fill(columns * rows)
|
|
15
|
+
@columns = columns
|
|
16
|
+
@cells = code_methods.each_with_index.flat_map { |method, index| [index] * method.size }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def move(by)
|
|
22
|
+
self.cursor = @cursor + by
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# To the method in the cell above or below the selected one's first cell.
|
|
27
|
+
def move_row(by)
|
|
28
|
+
cell = @cells[@cells.index(@cursor).to_i + (by * @columns)]
|
|
29
|
+
self.cursor = cell if cell
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../code_box'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
module TUI
|
|
7
|
+
# Code feelings' CodeBox below the grid: the selected method's code, where it is and how many commits changed its
|
|
8
|
+
# file, then why it felt that way while that's typed, below a line.
|
|
9
|
+
module FeelingsCodeBox
|
|
10
|
+
include CodeBox
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def boxed_method = view.selected
|
|
15
|
+
def box_title(method) = "#{where(method)} · #{method.label}"
|
|
16
|
+
def box_note(method) = " changed #{method.changes}× ─"
|
|
17
|
+
def box_style = :feelings
|
|
18
|
+
def box_parts(method) = highlighted(method)
|
|
19
|
+
|
|
20
|
+
# As many lines as fit expanded, with the bins and a few of the grid's rows left above it.
|
|
21
|
+
def box_room = height - Bins::BIN_LINES - BOX_LINES - MIN_GRID_ROWS
|
|
22
|
+
|
|
23
|
+
def box_footer
|
|
24
|
+
return [] unless view.typing?
|
|
25
|
+
|
|
26
|
+
note_lines
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Why it felt that way, as it's typed, below a line, in as many lines as it takes.
|
|
30
|
+
def note_lines = [box_edge('├', '┤'), *hanging("Why #{view.noting.upcase}? > ", "#{view.note}▏").map { boxed([[it, :feelings]]) }]
|
|
31
|
+
|
|
32
|
+
# The text after the lead, wrapped to fit the box, with the lines after the first lined up under the text.
|
|
33
|
+
def hanging(lead, text)
|
|
34
|
+
indent = ' ' * display_width(lead)
|
|
35
|
+
lines = wrap(text, width - 6 - indent.length)
|
|
36
|
+
[lead, *Array.new(lines.length - 1, indent)].zip(lines).map(&:join)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|