raindown 0.6.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/chart/chart.rb +12 -1
- 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 +22 -20
- data/lib/raindown/endpoint/curl.rb +11 -7
- data/lib/raindown/endpoint/endpoint_runner.rb +13 -5
- data/lib/raindown/node_blocks.rb +31 -22
- data/lib/raindown/node_injector.rb +7 -10
- data/lib/raindown/nodes.rb +1 -0
- data/lib/raindown/pages.rb +1 -1
- data/lib/raindown/pre_renderer.rb +18 -0
- data/lib/raindown/stat.rb +53 -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/code_box.rb +143 -0
- data/lib/raindown/tui/endpoints.rb +2 -2
- data/lib/raindown/tui/events.rb +18 -1
- data/lib/raindown/tui/feelings/batch.rb +47 -0
- data/lib/raindown/tui/feelings/bins.rb +67 -0
- data/lib/raindown/tui/feelings/celebration/celebration.rb +28 -0
- data/lib/raindown/tui/feelings/celebration/fireworks.rb +75 -0
- data/lib/raindown/tui/feelings/celebration/rain.rb +66 -0
- data/lib/raindown/tui/feelings/celebration/scene.rb +40 -0
- data/lib/raindown/tui/feelings/cell_moves.rb +34 -0
- data/lib/raindown/tui/feelings/code_methods.rb +134 -0
- data/lib/raindown/tui/feelings/feelings_code_box.rb +40 -0
- data/lib/raindown/tui/feelings/feelings_log.rb +136 -0
- data/lib/raindown/tui/feelings/feelings_renderer.rb +183 -0
- data/lib/raindown/tui/feelings/feelings_view.rb +157 -0
- 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 +51 -0
- data/lib/raindown/tui/file_tree.rb +6 -5
- data/lib/raindown/tui/highlighter.rb +45 -0
- data/lib/raindown/tui/layout.rb +32 -18
- data/lib/raindown/tui/navigator/commands.rb +46 -2
- data/lib/raindown/tui/navigator/mouse.rb +43 -0
- data/lib/raindown/tui/navigator/navigator.rb +54 -25
- data/lib/raindown/tui/navigator/teleport.rb +128 -0
- data/lib/raindown/tui/navigator/views.rb +5 -3
- data/lib/raindown/tui/{workers/project.rb → project.rb} +38 -27
- data/lib/raindown/tui/{menu_overlay.rb → project_box.rb} +2 -2
- data/lib/raindown/tui/renderers/dashboard_renderer.rb +4 -4
- data/lib/raindown/tui/renderers/endpoint_renderer.rb +14 -10
- data/lib/raindown/tui/renderers/outline_renderer.rb +4 -4
- data/lib/raindown/tui/renderers/renderer.rb +4 -0
- data/lib/raindown/tui/row.rb +4 -2
- data/lib/raindown/tui/rows.rb +2 -1
- data/lib/raindown/tui/saves.rb +22 -0
- data/lib/raindown/tui/search_box.rb +86 -0
- data/lib/raindown/tui/terminal/input.rb +95 -7
- data/lib/raindown/tui/terminal/screen.rb +37 -29
- data/lib/raindown/tui/terminal/styles.rb +111 -26
- data/lib/raindown/tui/terminal/terminal_title.rb +31 -0
- data/lib/raindown/tui/text.rb +62 -12
- data/lib/raindown/tui/title.rb +1 -1
- data/lib/raindown/tui/views/dashboard_view.rb +7 -5
- data/lib/raindown/tui/views/endpoint_view.rb +4 -2
- data/lib/raindown/tui/views/list_view.rb +2 -0
- data/lib/raindown/tui/views/outline_view.rb +16 -6
- data/lib/raindown/tui.rb +19 -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 +60 -9
- data/lib/raindown/project_worker.rb +0 -48
- data/lib/raindown/supervisor.rb +0 -160
- data/lib/raindown/tui/workers/workers.rb +0 -53
- data/lib/raindown/watcher.rb +0 -57
|
@@ -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
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'batch'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
module TUI
|
|
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, until the screen's resized.
|
|
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
|
+
|
|
14
|
+
attr_reader :batch
|
|
15
|
+
|
|
16
|
+
def initialize(log:)
|
|
17
|
+
@log = log
|
|
18
|
+
@batch = Batch.new(files: [], code_methods: [], log:, week: Batch.this_week)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The methods read again, keeping the batch's files.
|
|
22
|
+
def read(every)
|
|
23
|
+
@every = every
|
|
24
|
+
@batch = batch.reading(every)
|
|
25
|
+
end
|
|
26
|
+
|
|
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
|
|
42
|
+
|
|
43
|
+
def refill(grid_cells)
|
|
44
|
+
@filled_for = grid_cells
|
|
45
|
+
@resizing = nil
|
|
46
|
+
@batch = Batch.pick(@every, log: @log, cells: grid_cells)
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -58,8 +58,6 @@ module Raindown
|
|
|
58
58
|
path.delete_prefix(File.join(base, ''))
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
private
|
|
62
|
-
|
|
63
61
|
# A scope's own files, without those of the projects inside it, which are projects of their own.
|
|
64
62
|
def files
|
|
65
63
|
return @files unless scope
|
|
@@ -67,6 +65,8 @@ module Raindown
|
|
|
67
65
|
@files.select { |path| owner(path) == scope }
|
|
68
66
|
end
|
|
69
67
|
|
|
68
|
+
private
|
|
69
|
+
|
|
70
70
|
def owner(path)
|
|
71
71
|
[root, *@projects].select { path.start_with?(File.join(it, '')) }.max_by(&:length)
|
|
72
72
|
end
|
|
@@ -85,12 +85,13 @@ module Raindown
|
|
|
85
85
|
# A folder, and the tree inside it unless it's folded.
|
|
86
86
|
def folder_rows(name, children, prefix, depth)
|
|
87
87
|
folder = File.join(base, prefix, name)
|
|
88
|
-
|
|
88
|
+
expanded = @expanded.include?(folder)
|
|
89
89
|
project = @projects.include?(folder)
|
|
90
|
-
row = Row.new(text:
|
|
90
|
+
row = Row.new(text: [expanded ? '▾' : '▸', (PROJECT if project), "#{name}/"].compact.join(' '), depth:,
|
|
91
91
|
kind: project ? :project : :folder, target: folder, fold: -> { toggle(folder) })
|
|
92
|
+
return [row] unless expanded
|
|
92
93
|
|
|
93
|
-
[row, *
|
|
94
|
+
[row, *tree_rows(children, File.join(prefix, name), depth + 1)]
|
|
94
95
|
end
|
|
95
96
|
end
|
|
96
97
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raindown
|
|
4
|
+
module TUI
|
|
5
|
+
# Code coloured with Rouge, which knows over 200 languages, as each line's [text, style] parts. Each part's style is
|
|
6
|
+
# the kind of token it is, like a keyword or a string, from the few code styles Styles has, so they work in any
|
|
7
|
+
# terminal. Code in a language Rouge doesn't know is a plain part a line.
|
|
8
|
+
module Highlighter
|
|
9
|
+
# Each kind of token's style, by the start of its name, like Literal.String.Double for a string, most specific first.
|
|
10
|
+
STYLES = [['Name.Function', :code_function], ['Name.Class', :code_constant], ['Name.Constant', :code_constant],
|
|
11
|
+
['Name.Builtin', :code_constant], ['Name.Label', :code_constant], ['Keyword', :code_keyword],
|
|
12
|
+
['Literal.String', :code_string], ['Literal.Number', :code_number], ['Comment', :code_comment]].freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# The lines highlighted for a file's name, like shapes.rb, or a code fence's language, like json. Markdown
|
|
17
|
+
# highlights the code in its fences in their own languages, like an endpoint's request and response.
|
|
18
|
+
def lines(lines, filename: nil, language: nil)
|
|
19
|
+
lexer = lexer(filename:, language:)
|
|
20
|
+
return lines.map { [[it, :code]] } unless lexer
|
|
21
|
+
|
|
22
|
+
lexer.lex(lines.join("\n")).each_with_object([[]]) do |(token, text), highlighted|
|
|
23
|
+
text.split(/(\n)/).each do |piece|
|
|
24
|
+
next highlighted << [] if piece == "\n"
|
|
25
|
+
|
|
26
|
+
highlighted.last << [piece, style(token)] unless piece.empty?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Rouge takes about a third of a second to load, so the TUI loads it when it starts, rather than with its code.
|
|
32
|
+
def lexer(filename:, language:)
|
|
33
|
+
require 'rouge'
|
|
34
|
+
lexer = Rouge::Lexer.find_fancy(language) unless language.to_s.empty?
|
|
35
|
+
lexer ||= Rouge::Lexer.guess_by_filename(filename).new if filename
|
|
36
|
+
lexer unless lexer.is_a?(Rouge::Lexers::PlainText)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def style(token)
|
|
40
|
+
name = token.qualname
|
|
41
|
+
STYLES.find { |prefix, _| name.start_with?(prefix) }&.last || :code
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/raindown/tui/layout.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative 'events'
|
|
4
4
|
require_relative 'text'
|
|
5
5
|
require_relative 'title'
|
|
6
|
+
require_relative 'project_box'
|
|
7
|
+
require_relative 'search_box'
|
|
6
8
|
|
|
7
9
|
module Raindown
|
|
8
10
|
module TUI
|
|
@@ -14,9 +16,9 @@ module Raindown
|
|
|
14
16
|
# ┌─ Layout ──────────────────────────────────────────────────┐
|
|
15
17
|
# │ Raindown ~/docs › guides/setup.md │
|
|
16
18
|
# │ ┌─ nav pane ──────┐ ┌─ main pane ───────────────────────┐ │
|
|
17
|
-
# │ │ Dashboard
|
|
18
|
-
# │ │ ▾ guides/
|
|
19
|
-
# │ │ setup.md
|
|
19
|
+
# │ │ Dashboard │ # Setup │ │
|
|
20
|
+
# │ │ ▾ guides/ │ toc: [+3] │ │
|
|
21
|
+
# │ │ setup.md │ ## Install │ │
|
|
20
22
|
# │ └─ ListRenderer ──┘ └─ view.renderer ───────────────────┘ │
|
|
21
23
|
# │ Opened guides/setup.md │
|
|
22
24
|
# │ Tab Pane Enter Open X Run E dit W rite back Q uit │
|
|
@@ -27,9 +29,12 @@ module Raindown
|
|
|
27
29
|
include Title
|
|
28
30
|
|
|
29
31
|
NAV_WIDTH = (16..32)
|
|
30
|
-
# The line between the nav and main panes,
|
|
31
|
-
#
|
|
32
|
-
|
|
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 = '┬'
|
|
33
38
|
# The title, status and button lines.
|
|
34
39
|
CHROME_HEIGHT = 3
|
|
35
40
|
|
|
@@ -44,11 +49,7 @@ module Raindown
|
|
|
44
49
|
def lines
|
|
45
50
|
return [status].first(height) if height < CHROME_HEIGHT + 1
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
return lines unless navigator.menu
|
|
49
|
-
|
|
50
|
-
require_relative 'menu_overlay'
|
|
51
|
-
MenuOverlay.new(navigator, width:, column: display_width(name)).apply(lines)
|
|
52
|
+
overlaid([title, *panes, status, button_bar])
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
# A third of the width, within NAV_WIDTH, and never more than half of a narrow screen.
|
|
@@ -58,6 +59,14 @@ module Raindown
|
|
|
58
59
|
|
|
59
60
|
private
|
|
60
61
|
|
|
62
|
+
# The project menu or Teleport, drawn over the lines while it's open.
|
|
63
|
+
def overlaid(lines)
|
|
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?
|
|
66
|
+
|
|
67
|
+
lines
|
|
68
|
+
end
|
|
69
|
+
|
|
61
70
|
def body_height = height - CHROME_HEIGHT
|
|
62
71
|
|
|
63
72
|
# Below a blank line for spacing, the nav pane beside the main pane, each drawn by the renderer of what's in it.
|
|
@@ -65,16 +74,19 @@ module Raindown
|
|
|
65
74
|
nav = renderer_in(:nav, nav_width).lines
|
|
66
75
|
main = view_renderer.lines
|
|
67
76
|
|
|
68
|
-
[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]] }]
|
|
78
|
+
end
|
|
79
|
+
|
|
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) }]
|
|
69
83
|
end
|
|
70
84
|
|
|
71
|
-
# A blank line above the panes,
|
|
72
|
-
def
|
|
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
|
|
73
88
|
|
|
74
|
-
|
|
75
|
-
def divider(main_line)
|
|
76
|
-
style = { selected: :selected_divider, cursor: :cursor_divider }.fetch(main_line.first&.style, :divider)
|
|
77
|
-
segment(DIVIDER, style, width: 1)
|
|
89
|
+
segment.with(text: segment.text.tr(' ', BORDER), style: :divider)
|
|
78
90
|
end
|
|
79
91
|
|
|
80
92
|
def view_renderer
|
|
@@ -121,4 +133,6 @@ end
|
|
|
121
133
|
require_relative 'renderers/list_renderer'
|
|
122
134
|
require_relative 'renderers/outline_renderer'
|
|
123
135
|
require_relative 'renderers/dashboard_renderer'
|
|
136
|
+
require_relative 'blueprints/blueprints_renderer'
|
|
124
137
|
require_relative 'renderers/endpoint_renderer'
|
|
138
|
+
require_relative 'feelings/feelings_renderer'
|
|
@@ -1,10 +1,12 @@
|
|
|
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
|
|
7
|
-
# What a Navigator does to the open file: run an endpoint, open the editor, write back and read it again
|
|
8
|
+
# What a Navigator does to the open file: run an endpoint, open the editor, write back and read it again,
|
|
9
|
+
# and to a feeling about some code: draw the page it was noted down in, or ask Claude Code to fix the code.
|
|
8
10
|
# Each sets the status, so the Screen shows how it went, and returns what the Screen should do next, if anything.
|
|
9
11
|
module Commands
|
|
10
12
|
def run(file, line, name) = Wait.new(message: "Running #{name}...", task: -> { run_endpoint(file, line) })
|
|
@@ -15,8 +17,20 @@ module Raindown
|
|
|
15
17
|
Run.new(command: Actions.edit_command(file, line:))
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
def felt(feeling) = info("Felt #{feeling.temper} about #{feeling.method.label}")
|
|
21
|
+
|
|
22
|
+
# Leaves the screen for Claude Code, asked to fix the method, and the view reads the code again after.
|
|
23
|
+
def fix_with_agent(feeling)
|
|
24
|
+
return error('Install Claude Code, the claude command, to fix code with X') unless claude?
|
|
25
|
+
|
|
26
|
+
@edited = feeling.method.path
|
|
27
|
+
Run.new(command: ['claude', feeling.prompt])
|
|
28
|
+
end
|
|
29
|
+
|
|
18
30
|
private
|
|
19
31
|
|
|
32
|
+
def claude? = ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? { File.executable?(File.join(it, 'claude')) }
|
|
33
|
+
|
|
20
34
|
# Runs the endpoint selected in the outline, or listed below the files. One opened beside them runs itself.
|
|
21
35
|
def execute
|
|
22
36
|
target = selected&.target
|
|
@@ -64,12 +78,42 @@ module Raindown
|
|
|
64
78
|
info('Reloaded')
|
|
65
79
|
end
|
|
66
80
|
|
|
67
|
-
#
|
|
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.
|
|
68
83
|
def refresh
|
|
84
|
+
effect = watched(@saves.take) if @saves
|
|
69
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
|
|
70
107
|
nil
|
|
71
108
|
end
|
|
72
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
|
+
|
|
73
117
|
def info(message)
|
|
74
118
|
@status = [:info, message]
|
|
75
119
|
nil
|