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,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+
5
+ module Raindown
6
+ module TUI
7
+ # How Code feelings' digits move in their cells: the restless ones, the heaviest, now and then flicker one lower, and
8
+ # the ones beside the method the mouse is over flee from it, to the far side of their cells, where they tremble often,
9
+ # as if they're trying to escape, while every cell around it is bold.
10
+ module Motion
11
+ # The columns a fleeing digit moves by when it trembles, each for a step of FLEEING_TREMBLE_SECONDS, a little longer
12
+ # than a frame, so none are missed, once every FLEEING_TREMBLE_EVERY of them, about every 0.7 seconds, each cell at its
13
+ # own moment.
14
+ TREMBLE = [1, -1, 1, -1].freeze
15
+ FLEEING_TREMBLE_SECONDS = 0.09
16
+ FLEEING_TREMBLE_EVERY = 8
17
+ # The columns a digit flees by, from the method beside it, from the middle of its cell, where it's still.
18
+ FLEE = 1
19
+ MIDDLE = 1
20
+ # A restless digit flickers one lower for FLICKER_SECONDS, once every FLICKER_EVERY of them, about every 30 seconds.
21
+ FLICKER_SECONDS = 0.5
22
+ FLICKER_EVERY = 60
23
+
24
+ private
25
+
26
+ # The column of the cell's digit in its cell: the middle, or fleeing to its edge, where it trembles often.
27
+ def digit_column(owner, index)
28
+ away = flee(index)
29
+ return MIDDLE if away.zero?
30
+
31
+ fleeing_column(owner, index, away)
32
+ end
33
+
34
+ # At the edge of its cell it's fled to, trembling often and quickly, but never out of the cell.
35
+ def fleeing_column(owner, index, away)
36
+ (MIDDLE + away + tremble(owner, index)).clamp(0, FeelingsRenderer::CELL - 1)
37
+ end
38
+
39
+ # A column either way, during the cell's moment once every FLEEING_TREMBLE_EVERY steps, or else none.
40
+ def tremble(owner, index)
41
+ TREMBLE.fetch(((now / FLEEING_TREMBLE_SECONDS).floor + Zlib.crc32("#{owner}:#{index}")) % FLEEING_TREMBLE_EVERY, 0)
42
+ end
43
+
44
+ # One lower, during the restless cell's moment once every FLICKER_EVERY steps of FLICKER_SECONDS, or else none.
45
+ def flicker(owner, index)
46
+ return 0 if view.code_methods[owner].drift < FeelingsRenderer::RESTLESS
47
+ return 0 unless (((now / FLICKER_SECONDS).floor + Zlib.crc32("#{owner}:#{index}")) % FLICKER_EVERY).zero?
48
+
49
+ -1
50
+ end
51
+
52
+ # The time the frame's drawn at, in seconds, the same for all its cells.
53
+ def now = @now ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
+
55
+ # Away from the method the mouse is over, when it's beside the cell in its row.
56
+ def flee(index)
57
+ away = 0
58
+ away += FLEE if beside_hovered?(index, -1)
59
+ away -= FLEE if beside_hovered?(index, 1)
60
+ away
61
+ end
62
+
63
+ # Whether the cell a step along its row, -1 to the left or 1 to the right, is the method the mouse is over's, and
64
+ # this one isn't.
65
+ def beside_hovered?(index, step)
66
+ ((index % columns) + step).between?(0, columns - 1) && hovered.include?(index + step) && !hovered.include?(index)
67
+ end
68
+
69
+ # Bold around the method the mouse is over, beside it, above it, below it, or diagonally.
70
+ def cell_style(index)
71
+ @nearby ||= hovered.flat_map { around(it) }.to_set - hovered
72
+ return :nearby if @nearby.include?(index)
73
+
74
+ :feelings
75
+ end
76
+
77
+ # The cells around the cell, in the rows above and below it and beside it in its own.
78
+ def around(index)
79
+ column = index % columns
80
+ [-1, 0, 1].product([-1, 0, 1]).filter_map do |down, across|
81
+ next if (down.zero? && across.zero?) || !(column + across).between?(0, columns - 1)
82
+
83
+ index + (down * columns) + across
84
+ end
85
+ end
86
+
87
+ # The cells of the method the mouse is over, by their index.
88
+ def hovered
89
+ @hovered ||= @cells.each_index.select { navigator.hovered == Click.new(pane:, index: @cells[it], action: :focus) }.to_set
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raindown
4
+ module TUI
5
+ # A waffle's rows, as [text, style] parts: a grid of pockets between golden ridges, shaded darker at their tops,
6
+ # under the ridges, so they look deep, with rounded corners, and a pat of butter melting where the middle ridges cross,
7
+ # with a white shine at its top left, and as many of its pockets as have been filled with syrup, nearest the butter
8
+ # first:
9
+ # ▄███████████████████████▄
10
+ # █▓▓▓▓▓█▓▓▓▓▓█▓▓▓▓▓█▓▓▓▓▓█
11
+ # █░░░░░█░░░░░█░░░░░█░░░░░█
12
+ # █████████████████████████
13
+ # █▓▓▓▓▓█▓▓▓▓▓█▓▓▓▓▓█▓▓▓▓▓█
14
+ # █░░░░░█░░░▄▄▄▄▄░░░█░░░░░█
15
+ # ██████████████████████████
16
+ module BakedWaffle
17
+ RIDGE = :code_number
18
+ POCKET = :bright_orange
19
+ BUTTER = :button
20
+ SHINE = :shine
21
+ SYRUP = :syrup
22
+ # The pockets across and down, the columns each takes, and the lines a pocket and a ridge take.
23
+ POCKETS = 4
24
+ POCKET_WIDTH = 5
25
+ ROW_LINES = 3
26
+ # The pat of butter's lines, from the line above the middle ridge, a drip below it, and the shine at its top left, as
27
+ # its lines and columns.
28
+ PAT = %w[▄▄▄▄▄ █████ ▀▀█▀▀].freeze
29
+ DRIP = '▀'
30
+ SHINE_AT = [[0, 0], [0, 1], [1, 0]].freeze
31
+
32
+ module_function
33
+
34
+ def rows(syrup: 0)
35
+ cells = Array.new(down) { |row| Array.new(across) { |column| cell(row, column, across, down) } }
36
+ buttered(sweetened(cells, syrup), down / 2, across / 2).map { parts(it) }
37
+ end
38
+
39
+ def across = 1 + (POCKETS * (POCKET_WIDTH + 1))
40
+ def down = 1 + (POCKETS * ROW_LINES)
41
+
42
+ # The cells with as many pockets filled with syrup, nearest the middle first, keeping their shading.
43
+ def sweetened(cells, filled)
44
+ pockets.first(filled).each do |pocket_row, pocket_column|
45
+ (1...ROW_LINES).to_a.product((1..POCKET_WIDTH).to_a).each do |line, offset|
46
+ cells[(pocket_row * ROW_LINES) + line][(pocket_column * (POCKET_WIDTH + 1)) + offset][1] = SYRUP
47
+ end
48
+ end
49
+ cells
50
+ end
51
+
52
+ # The pockets, by their row and column, nearest the middle first.
53
+ def pockets
54
+ middle = (POCKETS - 1) / 2.0
55
+ (0...POCKETS).to_a.product((0...POCKETS).to_a).sort_by { |row, column| [(row - middle).abs + (column - middle).abs, row, column] }
56
+ end
57
+
58
+ # A ridge, rounded at its corners, or a pocket, darker at its top, under the ridge above it, than below.
59
+ def cell(row, column, across, down)
60
+ edge = [0, down - 1].include?(row)
61
+ return [' ', :blank] if edge && [0, across - 1].include?(column)
62
+ return ['▄', RIDGE] if row.zero? && [1, across - 2].include?(column)
63
+ return ['▀', RIDGE] if edge && [1, across - 2].include?(column)
64
+ return ['█', RIDGE] if (row % ROW_LINES).zero? || (column % (POCKET_WIDTH + 1)).zero?
65
+
66
+ [%w[▓ ░][(row % ROW_LINES) - 1], POCKET]
67
+ end
68
+
69
+ # The cells with the pat of butter on the middle ridges' cross, its drip, and its shine.
70
+ def buttered(cells, row, column)
71
+ PAT.each_with_index do |line, at|
72
+ line.each_char.with_index { |char, offset| cells[row - 1 + at][column - 2 + offset] = [char, BUTTER] }
73
+ end
74
+ cells[row + 2][column] = [DRIP, BUTTER]
75
+ shine(cells, row, column)
76
+ end
77
+
78
+ # The cells with the butter's top left shining.
79
+ def shine(cells, row, column)
80
+ SHINE_AT.each { |line, offset| cells[row - 1 + line][column - 2 + offset][1] = SHINE }
81
+ cells
82
+ end
83
+
84
+ # The row's cells in runs of the same style, as [text, style] parts.
85
+ def parts(row) = row.chunk_while { |cell, after| cell[1] == after[1] }.map { [it.map(&:first).join, it[0][1]] }
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+
5
+ require_relative '../celebration/rain'
6
+
7
+ module Raindown
8
+ module TUI
9
+ # Balloons floating up either side of the waffle, when a batch of code feelings is refined: one for every so many
10
+ # columns, left and right in turn, clear of the waffle in the middle, each in a bright colour, shaded where the light
11
+ # catches it, with a knot and a string that kinks, rising from below the bottom, swaying a column either way, then
12
+ # round again. Their spaces are see-through:
13
+ # ▄███▄
14
+ # █░▒████
15
+ # █▒█████
16
+ # ▀███▀
17
+ # ▼
18
+ # │
19
+ # ╰╮
20
+ # │
21
+ class Balloons
22
+ BALLOON = [' ▄███▄', '█░▒████', '█▒█████', ' ▀███▀', ' ▼'].freeze
23
+ STRING = [' │', ' ╰╮', ' │'].freeze
24
+ # The columns for each balloon, and the frames each row it rises takes.
25
+ COLUMNS_A_BALLOON = 20
26
+ # The columns kept clear in the middle, for the waffle and its text.
27
+ MIDDLE = 39
28
+ RISE_FRAMES = 2.1
29
+ # Where a balloon is, by its left column and its top row, and its colour.
30
+ Balloon = Data.define(:left, :top, :color)
31
+
32
+ attr_reader :width, :height
33
+
34
+ def initialize(width:, height:)
35
+ @width = width
36
+ @height = height
37
+ end
38
+
39
+ # Draws the balloons where they've floated to by the tick into a Scene's cells.
40
+ def draw(cells, tick)
41
+ [width / COLUMNS_A_BALLOON, 2].max.times { balloon(cells, floated(it, Zlib.crc32("balloon #{it}"), tick)) }
42
+ end
43
+
44
+ private
45
+
46
+ # Where a balloon's floated to by the tick, from its seed, a checksum of it: where it rises in its side, when it starts,
47
+ # how it sways, never past an edge, and its colour.
48
+ def floated(number, seed, tick)
49
+ sway = (side(number, seed) + Math.sin((tick + seed) / 6.4).round).clamp(0, [width - BALLOON.map(&:length).max, 0].max)
50
+ Balloon.new(left: sway, top: risen(seed, tick),
51
+ color: Rain::COLORS[(seed >> 16) % Rain::COLORS.length])
52
+ end
53
+
54
+ # A column in the left side for the even balloons, and the right for the odd, either side of the middle.
55
+ def side(number, seed)
56
+ room = [((width - MIDDLE) / 2) - BALLOON.map(&:length).max, 1].max
57
+ column = seed % room
58
+ return column if number.even?
59
+
60
+ width - room + column - BALLOON.map(&:length).max
61
+ end
62
+
63
+ # Its top row, a row higher every RISE_FRAMES, from below the bottom to above the top, then round again.
64
+ def risen(seed, tick) = height - 1 - (((tick / RISE_FRAMES).floor + (seed >> 8)) % (height + BALLOON.length + STRING.length))
65
+
66
+ def balloon(cells, balloon)
67
+ BALLOON.each_with_index { |text, row| put(cells, balloon.top + row, balloon.left, text, balloon.color) }
68
+ STRING.each_with_index { |text, row| put(cells, balloon.top + BALLOON.length + row, balloon.left, text, :border) }
69
+ end
70
+
71
+ # The text's characters from the left column, leaving the cells under its spaces as they are.
72
+ def put(cells, row, left, text, style)
73
+ return unless row.between?(0, height - 1)
74
+
75
+ text.each_char.with_index do |char, at|
76
+ cells[row][left + at] = [char, style] if char != ' ' && (left + at).between?(0, width - 1)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+
5
+ require_relative '../celebration/rain'
6
+ require_relative 'balloons'
7
+
8
+ module Raindown
9
+ module TUI
10
+ # Confetti falling either side of the waffle, behind the balloons, when a batch of code feelings is refined: flecks in
11
+ # bright colours, a row or two lower each frame, as fast as the Rain, wavering a column either way, then round again
12
+ # from the top.
13
+ # ▪ ◆
14
+ # · ▴
15
+ class Confetti
16
+ FLECKS = %w[▪ ◆ ▴ ·].freeze
17
+ # The columns for each fleck, and the frames each column it wavers takes.
18
+ COLUMNS_A_FLECK = 3
19
+ WAVER_FRAMES = 11
20
+
21
+ attr_reader :width, :height
22
+
23
+ def initialize(width:, height:)
24
+ @width = width
25
+ @height = height
26
+ end
27
+
28
+ # Draws the confetti where it's drifted to by the tick into a Scene's cells, clear of the waffle in the middle.
29
+ def draw(cells, tick)
30
+ (width / COLUMNS_A_FLECK).times { fleck(cells, Zlib.crc32("confetti #{it}"), tick) }
31
+ end
32
+
33
+ private
34
+
35
+ def fleck(cells, seed, tick)
36
+ column = drifted(seed, tick)
37
+ return if clear?(column)
38
+
39
+ cells[((tick * speed(seed)) + (seed >> 8)) % height][column] = look(seed)
40
+ end
41
+
42
+ # A row or two a frame, like the Rain's drops.
43
+ def speed(seed) = ((seed >> 12) % 2) + 1
44
+
45
+ # Its column, wavering a column either way as it falls.
46
+ def drifted(seed, tick) = (seed % width) + (((tick / WAVER_FRAMES) + seed) % 3) - 1
47
+
48
+ # Whether the column's in the middle, kept clear for the waffle, or off the screen.
49
+ def clear?(column) = (column - (width / 2)).abs <= Balloons::MIDDLE / 2 || !column.between?(0, width - 1)
50
+
51
+ # Its fleck and colour.
52
+ def look(seed) = [FLECKS[(seed >> 4) % FLECKS.length], Rain::COLORS[(seed >> 16) % Rain::COLORS.length]]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+
5
+ require_relative 'baked_waffle'
6
+
7
+ module Raindown
8
+ module TUI
9
+ # Sparkles twinkling around the waffle, when a batch of code feelings is refined: white, on a ring just clear of it,
10
+ # each growing from a dot to a star and back, then gone for a while, at its own time.
11
+ # · ✦
12
+ # ▄█████████████▄ ✧
13
+ class Sparkles
14
+ TWINKLE = %w[· ✧ ✦ ✧ ·].freeze
15
+ COUNT = 10
16
+ # The frames each of a twinkle's steps takes, and the steps between a sparkle's twinkles.
17
+ TWINKLE_FRAMES = 3
18
+ TWINKLE_EVERY = 16
19
+ # The columns and lines the ring's clear of the waffle by.
20
+ MARGIN = 3
21
+
22
+ attr_reader :width, :height
23
+
24
+ def initialize(width:, height:)
25
+ @width = width
26
+ @height = height
27
+ end
28
+
29
+ # Draws the sparkles twinkling by the tick into a Scene's cells.
30
+ def draw(cells, tick)
31
+ COUNT.times { sparkle(cells, Zlib.crc32("sparkle #{it}"), tick) }
32
+ end
33
+
34
+ private
35
+
36
+ def sparkle(cells, seed, tick)
37
+ char = TWINKLE[((tick / TWINKLE_FRAMES) + seed) % TWINKLE_EVERY]
38
+ row, column = spot(seed)
39
+ cells[row][column] = [char, :shine] if char && row.between?(0, height - 1) && column.between?(0, width - 1)
40
+ end
41
+
42
+ def spot(seed) = ring[seed % ring.length]
43
+
44
+ # The spots on a ring just clear of the waffle, in the middle of the screen, by their row and column: along its top
45
+ # and bottom, and down its sides.
46
+ def ring
47
+ @ring ||= begin
48
+ top, left, bottom, right = corners
49
+ [*(left..right).flat_map { [[top, it], [bottom, it]] }, *((top + 1)...bottom).flat_map { [[it, left], [it, right]] }]
50
+ end
51
+ end
52
+
53
+ # The ring's top row, left column, bottom row and right column, MARGIN clear of the waffle.
54
+ def corners
55
+ left = (width - BakedWaffle.across) / 2 - MARGIN
56
+ top = ((height - BakedWaffle.down) / 2) - MARGIN + 1
57
+ [top, left, top + BakedWaffle.down + MARGIN, left + BakedWaffle.across + (MARGIN * 2) - 1]
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'baked_waffle'
4
+
5
+ module Raindown
6
+ module TUI
7
+ # The waffle after the rain, between balloons floating up, when a batch of code is refined, and the week's work is
8
+ # done: a BakedWaffle, then a blank line, then what's been done. Once it's popped, syrup pours onto its butter, then
9
+ # fills its pockets.
10
+ module Waffle
11
+ TEXT = [[], [['100% refined', :feelings]], [['The work is mysterious and important.', :feelings]]].freeze
12
+ # The row the waffle pops out from, its butter's on the middle ridge, and the frames each row more above and below
13
+ # it takes, half a second at the Screen's 20 frames a second while it celebrates.
14
+ BUTTER_ROW = BakedWaffle::POCKETS * BakedWaffle::ROW_LINES / 2
15
+ POP_FRAMES = 10
16
+ # The frames until it's all popped, and then the frames each pocket takes to fill with syrup, once it's poured.
17
+ POPPED_FRAMES = POP_FRAMES * (BakedWaffle.down + TEXT.length - 1 - BUTTER_ROW)
18
+ FILL_FRAMES = 10
19
+
20
+ private
21
+
22
+ # The waffle over the lines, as it pops, by the frames since the rain stopped: the butter's row first, then a row
23
+ # more above and below it every POP_FRAMES, then its lines of text, and then the syrup.
24
+ def waffle(lines, frame)
25
+ top = waffle_top
26
+ [*BakedWaffle.rows(syrup: filled(frame, top)), *TEXT].each_with_index do |parts, row|
27
+ next unless popped?(parts, row, frame) && lines[top + row]
28
+
29
+ lines[top + row] = over(lines[top + row], parts)
30
+ end
31
+ pour(lines, frame, top)
32
+ end
33
+
34
+ def waffle_top = [(height - BakedWaffle.down - TEXT.length) / 2, 0].max
35
+
36
+ def popped?(parts, row, frame) = parts.any? && (row - BUTTER_ROW).abs <= frame / POP_FRAMES
37
+
38
+ # The syrup's stream, falling a line a frame from the top onto the butter, once it's all popped, until every
39
+ # pocket's filled.
40
+ def pour(lines, frame, top)
41
+ poured = frame - POPPED_FRAMES
42
+ return lines if poured.negative? || filled(frame, top) >= BakedWaffle::POCKETS**2
43
+
44
+ [poured, stream(top)].min.times { lines[it] = overlay_segments(lines[it], [segment('│', :syrup, width: 1)], column: stream_column) }
45
+ lines
46
+ end
47
+
48
+ # The lines from the top down to the butter, and the column of the waffle's middle, which the syrup falls down.
49
+ def stream(top) = top + BUTTER_ROW - 1
50
+ def stream_column = ((width - BakedWaffle.across - 2) / 2) + 1 + (BakedWaffle.across / 2)
51
+
52
+ # The pockets filled with syrup by the frame, one every FILL_FRAMES once the stream's reached the butter.
53
+ def filled(frame, top) = [(frame - POPPED_FRAMES - stream(top)) / FILL_FRAMES, 0].max
54
+
55
+ # The parts over the middle of the line, with a space either side, so they're clear of the balloons behind them.
56
+ def over(line, parts)
57
+ segments = [[' ', :blank], *parts, [' ', :blank]].map { |text, style| segment(text, style, width: display_width(text)) }
58
+ overlay_segments(line, segments, column: [(width - segments.sum { display_width(it.text) }) / 2, 0].max)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -5,8 +5,12 @@ require_relative 'batch'
5
5
  module Raindown
6
6
  module TUI
7
7
  # This week's Batch of a project's code to feel, picked to fill the grid when the view first opens and draws it,
8
- # then kept through reloads, like after a method's edited or fixed.
8
+ # then kept through reloads, like after a method's edited or fixed, until the screen's resized.
9
9
  class WeeklyBatch
10
+ # How long a resized grid keeps its new number of cells before the batch is picked again to fill it, so it isn't
11
+ # picked for every size the screen passes through while it's dragged.
12
+ RESIZE_SECONDS = 2
13
+
10
14
  attr_reader :batch
11
15
 
12
16
  def initialize(log:)
@@ -20,12 +24,27 @@ module Raindown
20
24
  @batch = batch.reading(every)
21
25
  end
22
26
 
23
- # Picks the batch to fill the grid's cells, once they're known.
24
- def fill(grid_cells)
25
- return if @filled
27
+ # Picks the batch to fill the grid's cells, once they're known, and again once a resized grid's kept its new number
28
+ # of them for RESIZE_SECONDS, and returns whether it did.
29
+ def fill(grid_cells, now: Process.clock_gettime(Process::CLOCK_MONOTONIC))
30
+ return refill(grid_cells) unless @filled_for
31
+
32
+ @resizing = nil unless @resizing&.first == grid_cells
33
+ return false if grid_cells == @filled_for
34
+
35
+ @resizing ||= [grid_cells, now]
36
+ return false if now - @resizing.last < RESIZE_SECONDS
37
+
38
+ refill(grid_cells)
39
+ end
40
+
41
+ private
26
42
 
27
- @filled = true
43
+ def refill(grid_cells)
44
+ @filled_for = grid_cells
45
+ @resizing = nil
28
46
  @batch = Batch.pick(@every, log: @log, cells: grid_cells)
47
+ true
29
48
  end
30
49
  end
31
50
  end
@@ -3,8 +3,8 @@
3
3
  require_relative 'events'
4
4
  require_relative 'text'
5
5
  require_relative 'title'
6
- require_relative 'menu_overlay'
7
- require_relative 'search_overlay'
6
+ require_relative 'project_box'
7
+ require_relative 'search_box'
8
8
 
9
9
  module Raindown
10
10
  module TUI
@@ -16,9 +16,9 @@ module Raindown
16
16
  # ┌─ Layout ──────────────────────────────────────────────────┐
17
17
  # │ Raindown ~/docs › guides/setup.md │
18
18
  # │ ┌─ nav pane ──────┐ ┌─ main pane ───────────────────────┐ │
19
- # │ │ Dashboard ▏ # Setup │ │
20
- # │ │ ▾ guides/ ▏ toc: [+3] │ │
21
- # │ │ setup.md ▏ ## Install │ │
19
+ # │ │ Dashboard │ # Setup │ │
20
+ # │ │ ▾ guides/ │ toc: [+3] │ │
21
+ # │ │ setup.md │ ## Install │ │
22
22
  # │ └─ ListRenderer ──┘ └─ view.renderer ───────────────────┘ │
23
23
  # │ Opened guides/setup.md │
24
24
  # │ Tab Pane Enter Open X Run E dit W rite back Q uit │
@@ -29,9 +29,12 @@ module Raindown
29
29
  include Title
30
30
 
31
31
  NAV_WIDTH = (16..32)
32
- # The line between the nav and main panes, at its column's left edge, unlike │ in the middle,
33
- # so a highlighted file's row runs up to it.
34
- DIVIDER = '▏'
32
+ # The line between the nav and main panes, and the line beneath the title, along the line above the panes, in the
33
+ # middle of their rows and columns, joined where they meet, like a box's lines. The divider is never highlighted,
34
+ # so a highlighted row stops at the edge of its column.
35
+ DIVIDER = '│'
36
+ BORDER = '─'
37
+ JOIN = '┬'
35
38
  # The title, status and button lines.
36
39
  CHROME_HEIGHT = 3
37
40
 
@@ -56,10 +59,10 @@ module Raindown
56
59
 
57
60
  private
58
61
 
59
- # The project menu or the search, drawn over the lines while it's open.
62
+ # The project menu or Teleport, drawn over the lines while it's open.
60
63
  def overlaid(lines)
61
- return MenuOverlay.new(navigator, width:, column: display_width(name)).apply(lines) if navigator.menu
62
- return SearchOverlay.new(navigator, width:).apply(lines) if navigator.searching?
64
+ return ProjectBox.new(navigator, width:, column: display_width(name)).apply(lines) if navigator.menu
65
+ return SearchBox.new(navigator, width:).apply(lines) if navigator.teleporting?
63
66
 
64
67
  lines
65
68
  end
@@ -71,16 +74,19 @@ module Raindown
71
74
  nav = renderer_in(:nav, nav_width).lines
72
75
  main = view_renderer.lines
73
76
 
74
- [spacer, *Array.new(body_height - 1) { |index| [*nav[index], divider(main[index]), *main[index]] }]
77
+ [spacer, *Array.new(body_height - 1) { |index| [*nav[index], segment(DIVIDER, :divider, width: 1), *main[index]] }]
75
78
  end
76
79
 
77
- # A line above the panes, for spacing, blank but for what the main pane's renderer puts in its side of it.
78
- def spacer = [segment('', :blank, width: nav_width), divider([]), *view_renderer.spacer]
80
+ # A line above the panes, the border beneath the title, but for what the main pane's renderer puts in its side of it.
81
+ def spacer
82
+ [border(segment('', :blank, width: nav_width)), segment(JOIN, :divider, width: 1), *view_renderer.spacer.map { border(it) }]
83
+ end
84
+
85
+ # A blank segment of the line above the panes as the border, in the divider's colour.
86
+ def border(segment)
87
+ return segment unless segment.style == :blank
79
88
 
80
- # The divider is on the background of the outline's row beside it, when that's highlighted, so they meet.
81
- def divider(main_line)
82
- style = { selected: :selected_divider, cursor: :cursor_divider }.fetch(main_line.first&.style, :divider)
83
- segment(DIVIDER, style, width: 1)
89
+ segment.with(text: segment.text.tr(' ', BORDER), style: :divider)
84
90
  end
85
91
 
86
92
  def view_renderer
@@ -127,5 +133,6 @@ end
127
133
  require_relative 'renderers/list_renderer'
128
134
  require_relative 'renderers/outline_renderer'
129
135
  require_relative 'renderers/dashboard_renderer'
136
+ require_relative 'blueprints/blueprints_renderer'
130
137
  require_relative 'renderers/endpoint_renderer'
131
138
  require_relative 'feelings/feelings_renderer'
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../../actions'
4
+ require_relative '../../watcher/changes'
4
5
 
5
6
  module Raindown
6
7
  module TUI
@@ -77,12 +78,42 @@ module Raindown
77
78
  info('Reloaded')
78
79
  end
79
80
 
80
- # Reads the file again when it's changed, like when it's saved in an editor or a watcher writes back to it.
81
+ # Writes back the files saved since the last tick, like when they're watched, then reads the open file again when
82
+ # it's changed, like when it's saved in an editor or written back to.
81
83
  def refresh
84
+ effect = watched(@saves.take) if @saves
82
85
  view.load if view.changed?
86
+ effect
87
+ end
88
+
89
+ # A change to a project's nodes loads them again, and a saved Markdown file's written back, like when it's watched,
90
+ # and the files and endpoints are read again for the files added, removed or changed. When endpoints run as files
91
+ # are written back, like with -x, the screen says so while they do, as they can take a while.
92
+ def watched(paths)
93
+ changes = Watcher.changes(paths, root:)
94
+ return reload if changes.any?(&:nodes?)
95
+ return if changes.empty?
96
+
97
+ saved = changes.select(&:saved?).map(&:path)
98
+ return write_back_all(saved) unless @runs_endpoints && saved.any?
99
+
100
+ Wait.new(message: "Writing back #{relative(saved.first)}...", task: -> { write_back_all(saved) })
101
+ end
102
+
103
+ def write_back_all(files)
104
+ files.each { write_back_saved(it) }
105
+ @files.load
106
+ @endpoints.load
83
107
  nil
84
108
  end
85
109
 
110
+ # A saved file's written back, saying so only when it's changed, as when it's watched.
111
+ def write_back_saved(file)
112
+ info("Updated #{relative(file)}") if @workers.for(file).call(:write_back, file)
113
+ rescue StandardError => e
114
+ error("#{relative(file)}: #{e.message}")
115
+ end
116
+
86
117
  def info(message)
87
118
  @status = [:info, message]
88
119
  nil
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raindown
4
+ module TUI
5
+ # What a Navigator does with the mouse: a click where it's clicked, the mouse moving over something, which it keeps
6
+ # as what's hovered, and dragging with the button held, which the view takes, like a blueprint's canvas, if it can.
7
+ module Mouse
8
+ # What the mouse is over, which a click there would send, like a Click on a method's cells, or nil.
9
+ attr_reader :hovered
10
+
11
+ private
12
+
13
+ # A click moves the cursor where it's clicked, and does the click's action there, like a key, unless that would
14
+ # leave the terminal UI, like for the editor, so a stray click only moves the cursor.
15
+ # While the project menu or Teleport is open, only its items are clicked.
16
+ def click(click)
17
+ return if menu && click.pane != :menu
18
+ return if teleporting? && click.pane != :teleport
19
+
20
+ point(click)
21
+ effect = route(click.action)
22
+ effect unless effect.is_a?(Run)
23
+ end
24
+
25
+ def point(click)
26
+ return @menu = click.index if click.pane == :menu
27
+ return @match = click.index if click.pane == :teleport
28
+
29
+ content(click.pane).cursor = click.index
30
+ @focus = click.pane
31
+ end
32
+
33
+ def hover(hover)
34
+ @hovered = hover.target
35
+ nil
36
+ end
37
+
38
+ def drag(drag)
39
+ view.drag(drag) if view.respond_to?(:drag)
40
+ end
41
+ end
42
+ end
43
+ end