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
@@ -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 '../highlighter'
3
+ require_relative 'highlighter'
4
4
 
5
5
  module Raindown
6
6
  module TUI
7
- # Code feelings' box below the grid: the selected method's code, highlighted, its first lines or, expanded, all that
8
- # fit, then why it felt that way while that's typed, below a line:
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 = 8
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 selected method's code in a box, then why it felt that way while that's typed, below a line.
27
+ # The method's code in a box, then the lines below it.
25
28
  def code_box
26
- notes = []
27
- notes = note_lines if view.typing?
28
- [box_top(view.selected), *code_lines(height - Bins::BIN_LINES - BOX_LINES - MIN_GRID_ROWS - notes.length), *notes,
29
- box_edge('╰', '╯')]
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
- # The method's first lines, or all that fit in the room when it's expanded, highlighted and wrapped to fit the box,
33
- # then how many more there are, clicked to show them, or a line to show fewer again.
34
- def code_lines(room)
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 = highlighted_code.map { wrapped(it) }
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), :feelings]
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
- # Why it felt that way, as it's typed, below a line, in as many lines as it takes.
95
- def note_lines = [box_edge('├', '┤'), *hanging("Why #{view.noting.upcase}? > ", "#{view.note}▏").map { boxed([[it, :feelings]]) }]
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(' │ ', :feelings, width: 3), *code_segments(parts, width - 6, event:), segment(' │', :feelings, width: 2)])
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}", :feelings, width: width - 1)])
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
@@ -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 the search, which other keys' events would take, like "j" for :down.
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 ────╮ ╭─ 06 ────╮
10
- # │ FROLIC │ │ HOPE │ │ NUMB │ │ WOE │ │ DREAD │ │ MALICE │
11
- # │ ███░ 9 │ │ ██░░ 5 │ │ ████ 41 │ │ ██░░ 6 │ │ █░░░ 3 │ │ █░░░ 2 │
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 six bins, each a box with its number, its temper, and a bar of how many feelings it holds.
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 60 ms frames, then a waffle, until a key's pressed.
6
+ # seconds of the Screen's 50 ms frames, then a waffle, until a key's pressed.
7
7
  class Celebration
8
- RAIN_FRAMES = 83
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 '../text'
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