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,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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
data/lib/raindown/tui/layout.rb
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require_relative 'events'
|
|
4
4
|
require_relative 'text'
|
|
5
5
|
require_relative 'title'
|
|
6
|
-
require_relative '
|
|
7
|
-
require_relative '
|
|
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
|
|
20
|
-
# │ │ ▾ guides/
|
|
21
|
-
# │ │ setup.md
|
|
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,
|
|
33
|
-
#
|
|
34
|
-
|
|
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
|
|
62
|
+
# The project menu or Teleport, drawn over the lines while it's open.
|
|
60
63
|
def overlaid(lines)
|
|
61
|
-
return
|
|
62
|
-
return
|
|
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
|
|
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,
|
|
78
|
-
def 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
|
-
|
|
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
|
-
#
|
|
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
|