raindown 0.5.1 → 0.7.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/raindown/chart/chart.rb +12 -1
- data/lib/raindown/cli.rb +2 -2
- data/lib/raindown/document.rb +16 -9
- data/lib/raindown/endpoint/curl.rb +19 -0
- data/lib/raindown/node_blocks.rb +9 -6
- data/lib/raindown/nodes.rb +1 -0
- data/lib/raindown/projects.rb +8 -4
- data/lib/raindown/stat.rb +53 -0
- data/lib/raindown/tui/card.rb +56 -0
- data/lib/raindown/tui/endpoint.rb +16 -0
- data/lib/raindown/tui/endpoints.rb +40 -0
- data/lib/raindown/tui/events.rb +50 -0
- data/lib/raindown/tui/feelings/batch.rb +47 -0
- data/lib/raindown/tui/feelings/bins.rb +67 -0
- data/lib/raindown/tui/feelings/celebration.rb +25 -0
- data/lib/raindown/tui/feelings/code_box.rb +129 -0
- data/lib/raindown/tui/feelings/code_methods.rb +134 -0
- data/lib/raindown/tui/feelings/feelings_log.rb +133 -0
- data/lib/raindown/tui/feelings/feelings_renderer.rb +184 -0
- data/lib/raindown/tui/feelings/feelings_view.rb +161 -0
- data/lib/raindown/tui/feelings/fireworks.rb +75 -0
- data/lib/raindown/tui/feelings/rain.rb +66 -0
- data/lib/raindown/tui/feelings/scene.rb +37 -0
- data/lib/raindown/tui/feelings/waffle.rb +43 -0
- data/lib/raindown/tui/feelings/weekly_batch.rb +32 -0
- data/lib/raindown/tui/file_tree.rb +55 -12
- data/lib/raindown/tui/highlighter.rb +45 -0
- data/lib/raindown/tui/layout.rb +66 -77
- data/lib/raindown/tui/menu_overlay.rb +59 -0
- data/lib/raindown/tui/navigator/commands.rb +97 -0
- data/lib/raindown/tui/navigator/focus.rb +25 -0
- data/lib/raindown/tui/navigator/navigator.rb +160 -0
- data/lib/raindown/tui/navigator/project_menu.rb +61 -0
- data/lib/raindown/tui/navigator/search.rb +126 -0
- data/lib/raindown/tui/navigator/views.rb +52 -0
- data/lib/raindown/tui/renderers/dashboard_renderer.rb +102 -0
- data/lib/raindown/tui/renderers/endpoint_renderer.rb +81 -0
- data/lib/raindown/tui/renderers/list_renderer.rb +15 -0
- data/lib/raindown/tui/renderers/outline_renderer.rb +21 -0
- data/lib/raindown/tui/renderers/renderer.rb +18 -0
- data/lib/raindown/tui/row.rb +12 -3
- data/lib/raindown/tui/rows.rb +58 -0
- data/lib/raindown/tui/search_overlay.rb +73 -0
- data/lib/raindown/tui/terminal/input.rb +91 -0
- data/lib/raindown/tui/terminal/ncurses_shim.rb +46 -0
- data/lib/raindown/tui/terminal/quiet.rb +32 -0
- data/lib/raindown/tui/{screen.rb → terminal/screen.rb} +42 -24
- data/lib/raindown/tui/terminal/styles.rb +154 -0
- data/lib/raindown/tui/text.rb +110 -0
- data/lib/raindown/tui/title.rb +29 -0
- data/lib/raindown/tui/views/dashboard_view.rb +147 -0
- data/lib/raindown/tui/views/endpoint_view.rb +77 -0
- data/lib/raindown/tui/{pane.rb → views/list_view.rb} +11 -8
- data/lib/raindown/tui/{outline.rb → views/outline_view.rb} +48 -20
- data/lib/raindown/tui/workers/project.rb +122 -0
- data/lib/raindown/tui/workers/worker.rb +124 -0
- data/lib/raindown/tui/workers/workers.rb +53 -0
- data/lib/raindown/tui.rb +10 -5
- data/lib/version.rb +1 -1
- metadata +63 -9
- data/lib/raindown/tui/commands.rb +0 -70
- data/lib/raindown/tui/input.rb +0 -55
- data/lib/raindown/tui/navigator.rb +0 -123
- data/lib/raindown/tui/styles.rb +0 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad2e1df6e9cbfbac50533a9d6dd7f1d8bedb254aa6277a1889fb380871c52ec1
|
|
4
|
+
data.tar.gz: d32bbefdbcae559679033db6b1fc7e1dc4846f6a6356c0944e711cf50699eab0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0dd6247d9f49558b5dc53b9eae6860d841c34c1e03edbaca61ba02ff59ec59a932d647c7f70eff731b1fe195ece4f3d2e999be2a783e0e9f1b57f97a166a5b25
|
|
7
|
+
data.tar.gz: bfc9d3fed994284ae6f0ec94a51e6599a754e1064e648092e595bd80d6224f8f4acc4e258a790de9fe5bbb9e9cfe07ede3d4c6d7bd633c3574ff9f8ec9a3e90a
|
data/lib/raindown/chart/chart.rb
CHANGED
|
@@ -19,7 +19,8 @@ module Raindown
|
|
|
19
19
|
|
|
20
20
|
# Label and value are the columns to chart, by name or index, and minus a column to chart against the values.
|
|
21
21
|
# Width is in characters, for bars and lines, and height in lines, for columns and lines.
|
|
22
|
-
|
|
22
|
+
# Tail keeps only the last points, like the latest months of a file that grows.
|
|
23
|
+
def initialize(data = nil, format: nil, label: nil, value: nil, minus: nil, width: 40, height: 8, tail: nil) # rubocop:disable Metrics/ParameterLists -- each is a prop
|
|
23
24
|
@data = data
|
|
24
25
|
@format = format
|
|
25
26
|
@label = label
|
|
@@ -27,6 +28,7 @@ module Raindown
|
|
|
27
28
|
@minus = minus
|
|
28
29
|
@width = width
|
|
29
30
|
@height = height
|
|
31
|
+
@tail = tail
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# The data is read every time, so the chart follows changes to its file.
|
|
@@ -45,7 +47,16 @@ module Raindown
|
|
|
45
47
|
Points.new(label: @label, value: @value, minus: @minus, root:).from(@data)
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
# The chart's lines, as a card on the terminal UI's Dashboard shows them, or that there's no data.
|
|
51
|
+
def card_lines(metadata: NoMetadata)
|
|
52
|
+
points = points(metadata:)
|
|
53
|
+
return ['No data'] if points.empty?
|
|
54
|
+
|
|
55
|
+
draw(points).map(&:rstrip)
|
|
56
|
+
end
|
|
57
|
+
|
|
48
58
|
def draw(points)
|
|
59
|
+
points = points.last(@tail) if @tail
|
|
49
60
|
format = @format || (points.any?(&:first) ? 'bars' : 'sparkline')
|
|
50
61
|
drawing = FORMATS[format] || raise(Error, "format: is #{FORMATS.keys.join(', ')}, not #{format.inspect}")
|
|
51
62
|
|
data/lib/raindown/cli.rb
CHANGED
|
@@ -23,8 +23,8 @@ module Raindown
|
|
|
23
23
|
|
|
24
24
|
Commands:
|
|
25
25
|
init [DIRECTORY] Create a .raindown folder for the project's config and nodes (default: current directory)
|
|
26
|
-
tui [PATH] Browse a project's files and outlines in the terminal, fold directives, run endpoints
|
|
27
|
-
|
|
26
|
+
tui [PATH] Browse a project's files and outlines in the terminal, fold directives, run endpoints,
|
|
27
|
+
open the editor at a line and see a dashboard of the project's charts
|
|
28
28
|
|
|
29
29
|
Options:
|
|
30
30
|
--html [OUTPUT] Render FILE to HTML once, writing to OUTPUT or else printing to stdout
|
data/lib/raindown/document.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'commonmarker'
|
|
4
4
|
|
|
5
|
+
require_relative '../raindown'
|
|
5
6
|
require_relative 'bare_directives'
|
|
6
7
|
require_relative 'code_blocks'
|
|
7
8
|
require_relative 'endpoint/endpoint_runner'
|
|
@@ -17,9 +18,10 @@ module Raindown
|
|
|
17
18
|
|
|
18
19
|
# A content directive: a node, like toc: or a project's Changelog:, or an endpoint.
|
|
19
20
|
# It spans line to end_line, its end tag, or the last line of a bare block.
|
|
21
|
+
# Body is the lines inside it, between it and its end tag, like an endpoint's request and response.
|
|
20
22
|
# Output is the lines Raindown writes back, like a node's Markdown or an endpoint's response, and is nil
|
|
21
23
|
# when nothing's written yet. A self-closing directive like <{ :toc }> only renders, so it never writes.
|
|
22
|
-
Directive = Data.define(:name, :args, :line, :end_line, :output, :writes) do
|
|
24
|
+
Directive = Data.define(:name, :args, :line, :end_line, :body, :output, :writes) do
|
|
23
25
|
def endpoint? = name == 'endpoint'
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -27,6 +29,7 @@ module Raindown
|
|
|
27
29
|
TEXT_NODES = %i[text code].freeze
|
|
28
30
|
BREAK_NODES = %i[softbreak linebreak].freeze
|
|
29
31
|
ENDPOINT_END_TAG = /\A {0,3}<\{\s*:endpoint\s*\}>[ \t]*$/
|
|
32
|
+
END_TAG = /\A {0,3}<\{\s*:[\w:]+\s*\}>[ \t]*$/
|
|
30
33
|
|
|
31
34
|
def self.parse(markdown, bare: false)
|
|
32
35
|
new(markdown:, bare:)
|
|
@@ -46,7 +49,7 @@ module Raindown
|
|
|
46
49
|
|
|
47
50
|
# The headings in the order they appear, with their source text, like "Notes for {name}" before it's rendered.
|
|
48
51
|
def headings
|
|
49
|
-
@headings ||=
|
|
52
|
+
@headings ||= root.walk.select { |node| node.type == :heading }.map do |node|
|
|
50
53
|
Heading.new(level: node.header_level, text: text(node), line: node.source_position[:start_line])
|
|
51
54
|
end
|
|
52
55
|
end
|
|
@@ -68,10 +71,6 @@ module Raindown
|
|
|
68
71
|
@root ||= Commonmarker.parse(@markdown, options: MARKDOWN_OPTIONS)
|
|
69
72
|
end
|
|
70
73
|
|
|
71
|
-
def nodes(type)
|
|
72
|
-
[].tap { |nodes| root.walk { |node| nodes << node if node.type == type } }
|
|
73
|
-
end
|
|
74
|
-
|
|
75
74
|
def text(heading)
|
|
76
75
|
parts = []
|
|
77
76
|
heading.walk do |node|
|
|
@@ -139,10 +138,18 @@ module Raindown
|
|
|
139
138
|
first..last if first <= last
|
|
140
139
|
end
|
|
141
140
|
|
|
142
|
-
# From a range of indexes to line numbers.
|
|
141
|
+
# From a range of indexes to line numbers. The body is the lines after the directive, up to its end tag,
|
|
142
|
+
# or to the end of a bare block, which has none.
|
|
143
143
|
def directive(name, args, span, output:, writes:)
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
end_tag = span.end > span.begin && @lines[span.end].match?(END_TAG)
|
|
145
|
+
body = trimmed(span.begin + 1, end_tag ? span.end - 1 : span.end)
|
|
146
|
+
|
|
147
|
+
Directive.new(name:, args:, line: span.begin + 1, end_line: span.end + 1, body: lines(body), output: lines(output),
|
|
148
|
+
writes:)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def lines(range)
|
|
152
|
+
(range.begin + 1)..(range.end + 1) if range
|
|
146
153
|
end
|
|
147
154
|
end
|
|
148
155
|
end
|
|
@@ -10,6 +10,7 @@ module Raindown
|
|
|
10
10
|
# are allowed: nothing that reads or writes local files, loads a config, or uses another protocol.
|
|
11
11
|
module Curl
|
|
12
12
|
class Error < StandardError; end
|
|
13
|
+
Request = Data.define(:method, :url)
|
|
13
14
|
|
|
14
15
|
TIMEOUT = '30'
|
|
15
16
|
# --disable must come first and skips ~/.curlrc. The protocol options stop redirects to file:// and the like.
|
|
@@ -64,6 +65,24 @@ module Raindown
|
|
|
64
65
|
raise Error, 'curl is not installed'
|
|
65
66
|
end
|
|
66
67
|
|
|
68
|
+
# The snippet's method and URL, like GET https://httpbin.org/uuid, or nil when it can't be read.
|
|
69
|
+
def request(snippet)
|
|
70
|
+
args = arguments(snippet)
|
|
71
|
+
Request.new(method: request_method(args), url: args.find { it.match?(URL) })
|
|
72
|
+
rescue Error
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A snippet that sends data is a POST, unless it says otherwise, as with curl.
|
|
77
|
+
def request_method(args)
|
|
78
|
+
index = args.index('--request')
|
|
79
|
+
return args[index + 1].upcase if index
|
|
80
|
+
return 'HEAD' if args.include?('--head')
|
|
81
|
+
return 'POST' if args.intersect?(VALUE_CHECKS.keys.grep(/data|json/))
|
|
82
|
+
|
|
83
|
+
'GET'
|
|
84
|
+
end
|
|
85
|
+
|
|
67
86
|
def arguments(snippet)
|
|
68
87
|
words = Shellwords.split(snippet.gsub(/\\\r?\n/, ' ').strip.delete_prefix('$ '))
|
|
69
88
|
raise Error, 'Endpoint snippets must start with curl' unless words.first == 'curl'
|
data/lib/raindown/node_blocks.rb
CHANGED
|
@@ -77,13 +77,16 @@ module Raindown
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def rendered_lines(lines, block, markdown:, metadata:, bare:)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
else "#{Nodes.call(node, :replace, document: markdown, metadata:, bare:).to_s.chomp}\n".lines
|
|
83
|
-
end
|
|
84
|
-
output = [] if output == ["\n"]
|
|
80
|
+
output = node_markdown(block[:node], markdown:, metadata:, bare:).chomp
|
|
81
|
+
close = marker(lines[block[:close]]) if block[:close]
|
|
85
82
|
|
|
86
|
-
[marker(lines[block[:open]]), *output, *
|
|
83
|
+
[marker(lines[block[:open]]), *("#{output}\n".lines unless output.empty?), *close]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def node_markdown(node, markdown:, metadata:, bare:)
|
|
87
|
+
return Nodes.placeholder(node) if Nodes.html?(node)
|
|
88
|
+
|
|
89
|
+
Nodes.call(node, :replace, document: markdown, metadata:, bare:).to_s
|
|
87
90
|
end
|
|
88
91
|
|
|
89
92
|
def marker(line)
|
data/lib/raindown/nodes.rb
CHANGED
data/lib/raindown/projects.rb
CHANGED
|
@@ -35,14 +35,14 @@ module Raindown
|
|
|
35
35
|
folder && File.dirname(folder)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
# The project's Markdown files, sorted
|
|
39
|
-
# since
|
|
40
|
-
def files(root)
|
|
38
|
+
# The project's Markdown files, sorted, leaving out ignored folders. The projects inside it are left out too,
|
|
39
|
+
# since they have nodes of their own, unless they're asked for, like by the terminal UI, which lists them all.
|
|
40
|
+
def files(root, nested: false)
|
|
41
41
|
files = []
|
|
42
42
|
|
|
43
43
|
Find.find(root) do |path|
|
|
44
44
|
if File.directory?(path)
|
|
45
|
-
Find.prune if path != root && (ignored?(File.basename(path)) ||
|
|
45
|
+
Find.prune if path != root && (ignored?(File.basename(path)) || (!nested && project?(path)))
|
|
46
46
|
elsif markdown?(path, root:)
|
|
47
47
|
files << path
|
|
48
48
|
end
|
|
@@ -64,6 +64,10 @@ module Raindown
|
|
|
64
64
|
File.expand_path(path).start_with?(File.join(root, Nodes::FOLDER, '')) && %w[.rb .rbx].include?(File.extname(path))
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
def project?(folder)
|
|
68
|
+
File.directory?(File.join(folder, Nodes::FOLDER))
|
|
69
|
+
end
|
|
70
|
+
|
|
67
71
|
def ignored?(folder)
|
|
68
72
|
folder.start_with?('.') || IGNORED.include?(folder)
|
|
69
73
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'node'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
# The base of a project's stats: one big number in block digits, above its label, written into the Markdown as code,
|
|
7
|
+
# and shown on the terminal UI's Dashboard as a card, like a chart:
|
|
8
|
+
# ▀█ ▀▀█
|
|
9
|
+
# █ █▀▀
|
|
10
|
+
# ▀▀▀ ▀▀▀
|
|
11
|
+
# open issues
|
|
12
|
+
# A stat's a node in .raindown that says its value, and its label:
|
|
13
|
+
# class Issues < Raindown::Stat
|
|
14
|
+
# def value(metadata:) = Issue.open.count
|
|
15
|
+
# def label = 'open issues'
|
|
16
|
+
# end
|
|
17
|
+
class Stat < Node
|
|
18
|
+
# Each character's three lines in block digits, which end on the top half of the third, where a point sits too.
|
|
19
|
+
# Others are left as they are, on the middle line.
|
|
20
|
+
DIGITS = { '0' => ['█▀█', '█ █', '▀▀▀'], '1' => ['▀█ ', ' █ ', '▀▀▀'], '2' => ['▀▀█', '█▀▀', '▀▀▀'],
|
|
21
|
+
'3' => ['▀▀█', ' ▀█', '▀▀▀'], '4' => ['█ █', '▀▀█', ' ▀'], '5' => ['█▀▀', '▀▀█', '▀▀▀'],
|
|
22
|
+
'6' => ['█▀▀', '█▀█', '▀▀▀'], '7' => ['▀▀█', ' █', ' ▀'], '8' => ['█▀█', '█▀█', '▀▀▀'],
|
|
23
|
+
'9' => ['█▀█', '▀▀█', '▀▀▀'], '-' => [' ', '▀▀▀', ' '], '+' => [' ▄ ', '▀█▀', ' '],
|
|
24
|
+
'.' => [' ', ' ', '▀'], ',' => [' ', ' ', '▀'] }.freeze
|
|
25
|
+
|
|
26
|
+
def replace(metadata: NoMetadata) = "```\n#{card_lines(metadata:).join("\n")}\n```"
|
|
27
|
+
|
|
28
|
+
# The number in block digits, then the label, as a card on the Dashboard shows them.
|
|
29
|
+
def card_lines(metadata: NoMetadata)
|
|
30
|
+
characters = number(value(metadata:)).chars.map { digit(it) }
|
|
31
|
+
[*Array.new(3) { |line| characters.map { it[line] }.join(' ').rstrip }, label]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def value(metadata: NoMetadata) = raise(NotImplementedError, "#{self.class} needs a value")
|
|
35
|
+
|
|
36
|
+
def label = ''
|
|
37
|
+
|
|
38
|
+
# How the value's written, which a stat can change, like with a sign.
|
|
39
|
+
def number(value) = value.to_s
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# The character's three lines in block digits, or itself on the middle line.
|
|
44
|
+
def digit(character) = DIGITS.fetch(character) { [' ', character, ' '] }
|
|
45
|
+
|
|
46
|
+
# The project's folder, from the page being rendered, or else the current one.
|
|
47
|
+
def root(metadata)
|
|
48
|
+
return metadata.root if metadata.respond_to?(:root)
|
|
49
|
+
|
|
50
|
+
Dir.pwd
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'unicode/display_width'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
module TUI
|
|
7
|
+
# A chart on the dashboard, drawn from its data by its project's Worker: a title of its file, from its project,
|
|
8
|
+
# and the heading above it, then its directive, then the chart, each line with its Layout style,
|
|
9
|
+
# in a border with rounded corners. It's as wide as the chart, so the title and directive are cut to fit it:
|
|
10
|
+
# ╭───────────────────────────────────────╮
|
|
11
|
+
# │ markdown.md › Charts │
|
|
12
|
+
# │ chart: 'data/downloads.csv' width: 30 │
|
|
13
|
+
# │ Jun ██████▊ 640 │
|
|
14
|
+
# ╰───────────────────────────────────────╯
|
|
15
|
+
class Card < Data.define(:project, :path, :line, :heading, :directive, :chart)
|
|
16
|
+
# The border's columns either side of a card's lines, "│ " and " │", and its lines above and below them.
|
|
17
|
+
BORDER_WIDTH = 4
|
|
18
|
+
BORDER_HEIGHT = 2
|
|
19
|
+
|
|
20
|
+
def title = [path.delete_prefix(File.join(project, '')), heading].compact.join(' › ')
|
|
21
|
+
|
|
22
|
+
def lines = parts.map(&:first)
|
|
23
|
+
|
|
24
|
+
# Each line, with its style.
|
|
25
|
+
def parts = [[title, :heading], [directive, :directive], *chart.map { [it, :chart] }]
|
|
26
|
+
|
|
27
|
+
# As wide as its chart.
|
|
28
|
+
def width = chart.map { Unicode::DisplayWidth.of(it) }.max || 0
|
|
29
|
+
|
|
30
|
+
def height = lines.length
|
|
31
|
+
|
|
32
|
+
# With its border, within a range of widths, like a pane's.
|
|
33
|
+
def box_width(within: nil) = within ? (width + BORDER_WIDTH).clamp(within) : width + BORDER_WIDTH
|
|
34
|
+
def box_height = height + BORDER_HEIGHT
|
|
35
|
+
|
|
36
|
+
# A line of the card in its border, as [text, style, width] parts, for a card the box width wide.
|
|
37
|
+
# The border's top is line 0, and below its bottom, the lines are blank, for a card shorter than its row.
|
|
38
|
+
# The title's style is title_style, like when the card's selected.
|
|
39
|
+
def format_line(line, box_width:, title_style: :heading)
|
|
40
|
+
width = [box_width - BORDER_WIDTH, 0].max
|
|
41
|
+
return [border(width, '╭', '╮')] if line.zero?
|
|
42
|
+
return [border(width, '╰', '╯')] if line == height + 1
|
|
43
|
+
return [['', :blank, width + BORDER_WIDTH]] if line > height + 1
|
|
44
|
+
|
|
45
|
+
text, style = parts[line - 1]
|
|
46
|
+
[['│ ', :border, 2], [text, style == :heading ? title_style : style, width], [' │', :border, 2]]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def border(width, left, right)
|
|
52
|
+
["#{left}#{'─' * (width + 2)}#{right}", :border, width + BORDER_WIDTH]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raindown
|
|
4
|
+
module TUI
|
|
5
|
+
# An endpoint in a project's files, listed in the nav pane by its request, like "GET httpbin.org/uuid",
|
|
6
|
+
# with its method in its style, like GET in :method_get.
|
|
7
|
+
Endpoint = Data.define(:project, :path, :line, :method, :url) do
|
|
8
|
+
def text = "#{method} #{address}"
|
|
9
|
+
|
|
10
|
+
# The URL without its scheme.
|
|
11
|
+
def address = url.sub(%r{\Ahttps?://}i, '')
|
|
12
|
+
|
|
13
|
+
def style = :"method_#{method.downcase}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'endpoint'
|
|
4
|
+
require_relative 'views/endpoint_view'
|
|
5
|
+
require_relative 'row'
|
|
6
|
+
require_relative 'workers/worker'
|
|
7
|
+
|
|
8
|
+
module Raindown
|
|
9
|
+
module TUI
|
|
10
|
+
# Every endpoint in the projects' files, found by each project's Worker, listed below the files.
|
|
11
|
+
# Like the dashboard, it can be narrowed to a project's own.
|
|
12
|
+
class Endpoints
|
|
13
|
+
# The project the endpoints are narrowed to, or nil for every project's.
|
|
14
|
+
attr_accessor :project
|
|
15
|
+
|
|
16
|
+
def initialize(workers:)
|
|
17
|
+
@workers = workers
|
|
18
|
+
load
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Finds the endpoints again, like after a file's changed.
|
|
22
|
+
def load
|
|
23
|
+
@endpoints = @workers.each.flat_map do |worker|
|
|
24
|
+
worker.call(:endpoints)
|
|
25
|
+
rescue Worker::Error
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def all = project ? @endpoints.select { it.project == project } : @endpoints
|
|
31
|
+
|
|
32
|
+
def rows
|
|
33
|
+
all.map do
|
|
34
|
+
Row.new(text: it.address, tag: it, depth: 0, kind: :endpoint, target: it,
|
|
35
|
+
open: OpenView.new(view: EndpointView.new(it, workers: @workers)))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raindown
|
|
4
|
+
module TUI
|
|
5
|
+
# A click on the screen: the cursor moves to the pane's row, or the dashboard's card or the menu's item,
|
|
6
|
+
# then the action's done there, like :enter, or a button's, like :toggle or :execute.
|
|
7
|
+
# Keys are their events' names, like :down or :enter.
|
|
8
|
+
Click = Data.define(:pane, :index, :action)
|
|
9
|
+
|
|
10
|
+
# The mouse moving over what a click there would send, if anything, to show it, like a method's cells in bold.
|
|
11
|
+
Hover = Data.define(:target)
|
|
12
|
+
|
|
13
|
+
# A character typed while typing, like in the search, which other keys' events would take, like "j" for :down.
|
|
14
|
+
Type = Data.define(:text)
|
|
15
|
+
|
|
16
|
+
# What the Screen does after an event, other than drawing again, or stopping for :quit.
|
|
17
|
+
# Leaves the screen for a command, like the editor, then tells the Navigator it ran.
|
|
18
|
+
Run = Data.define(:command)
|
|
19
|
+
|
|
20
|
+
# Shows the message while the task runs, like an endpoint's request.
|
|
21
|
+
Wait = Data.define(:message, :task)
|
|
22
|
+
|
|
23
|
+
# Steps a row or a view asks of the Navigator, which each takes on it, returning what the Screen does next: to open
|
|
24
|
+
# a view right of the files, or a file in the outline at a directive, to edit a file at a line, to run an endpoint,
|
|
25
|
+
# to draw the page a feeling was noted down in, or to ask Claude Code to fix a method.
|
|
26
|
+
OpenView = Data.define(:view) do
|
|
27
|
+
def take(navigator) = navigator.open_view(view)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Open = Data.define(:path, :line) do
|
|
31
|
+
def take(navigator) = navigator.open_at(path, line)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Edit = Data.define(:path, :line) do
|
|
35
|
+
def take(navigator) = navigator.edit_at(path, line)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Execute = Data.define(:path, :line, :name) do
|
|
39
|
+
def take(navigator) = navigator.run(path, line, name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Felt = Data.define(:feeling) do
|
|
43
|
+
def take(navigator) = navigator.felt(feeling)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Fix = Data.define(:feeling) do
|
|
47
|
+
def take(navigator) = navigator.fix_with_agent(feeling)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'zlib'
|
|
5
|
+
|
|
6
|
+
module Raindown
|
|
7
|
+
module TUI
|
|
8
|
+
# Files to feel, like the files Severance's refiners work through: the first in the week's shuffled order, as many as
|
|
9
|
+
# fill the grid's cells, with their methods shuffled together. The order is random, but the same all week, so the
|
|
10
|
+
# grid stays put through reloads, edits and restarts, and once it's refined, that's the week's work done.
|
|
11
|
+
Batch = Data.define(:files, :code_methods, :log, :week) do
|
|
12
|
+
# Like 2026-W44, from the ISO year and week.
|
|
13
|
+
def self.this_week(date = Date.today) = "#{date.cwyear}-W#{date.cweek}"
|
|
14
|
+
|
|
15
|
+
def self.pick(every, log:, cells:, week: this_week)
|
|
16
|
+
files = filling(every.group_by(&:path).sort_by { |path, _| Zlib.crc32("#{week}:#{path}") }, cells)
|
|
17
|
+
|
|
18
|
+
new(files:, code_methods: [], log:, week:).reading(every)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The files, in order, whose methods' cells fit in the grid's, passing over any too big for what's left, and
|
|
22
|
+
# at least the first.
|
|
23
|
+
def self.filling(files, cells)
|
|
24
|
+
used = 0
|
|
25
|
+
files.each_with_object([]) do |(path, methods), picked|
|
|
26
|
+
size = methods.sum(&:size)
|
|
27
|
+
next unless picked.empty? || used + size <= cells
|
|
28
|
+
|
|
29
|
+
picked << path
|
|
30
|
+
used += size
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The same files, with their methods read again, like after one's edited.
|
|
35
|
+
def reading(every)
|
|
36
|
+
with(code_methods: every.select { files.include?(it.path) }.sort_by { Zlib.crc32("#{week}:#{it.path}:#{it.label}") })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def felt?(method) = log.felt?(method)
|
|
40
|
+
def refined? = code_methods.all? { felt?(it) }
|
|
41
|
+
|
|
42
|
+
def progress
|
|
43
|
+
"#{code_methods.count { felt?(it) } * 100 / [code_methods.length, 1].max}% refined"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'feelings_log'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
module TUI
|
|
7
|
+
# Code feelings' bins below the code box, one for each temper, best to worst, each a box with its key's number, its
|
|
8
|
+
# temper and a bar of how many feelings it holds, highlighted when it's the chosen temper:
|
|
9
|
+
# ╭─ 01 ────╮ ╭─ 02 ────╮ ╭─ 03 ────╮ ╭─ 04 ────╮ ╭─ 05 ────╮ ╭─ 06 ────╮
|
|
10
|
+
# │ FROLIC │ │ HOPE │ │ NUMB │ │ WOE │ │ DREAD │ │ MALICE │
|
|
11
|
+
# │ ███░ 9 │ │ ██░░ 5 │ │ ████ 41 │ │ ██░░ 6 │ │ █░░░ 3 │ │ █░░░ 2 │
|
|
12
|
+
# ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯ ╰─────────╯
|
|
13
|
+
module Bins
|
|
14
|
+
# A bin's lines.
|
|
15
|
+
BIN_LINES = 4
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# The six bins, each a box with its number, its temper, and a bar of how many feelings it holds.
|
|
20
|
+
def bins
|
|
21
|
+
counts = view.log.counts
|
|
22
|
+
most = [counts.values.max, 1].max
|
|
23
|
+
chosen = chosen_temper
|
|
24
|
+
boxes = counts.each_with_index.map { |(temper, count), index| bin(index + 1, temper, count, most, chosen) }
|
|
25
|
+
|
|
26
|
+
Array.new(BIN_LINES) do |line|
|
|
27
|
+
padded([segment('', :blank, width: 1), *boxes.flat_map { [it[line], segment(' ', :blank, width: 1)] }])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The temper being noted, or else how the selected method felt, if it's been felt.
|
|
32
|
+
def chosen_temper = view.noting || view.log.temper(view.selected)
|
|
33
|
+
|
|
34
|
+
# As wide as they all fit, which cuts their borders short in a pane too narrow for them.
|
|
35
|
+
def bin_width = [(width - 1 - FeelingsLog::TEMPERS.length) / FeelingsLog::TEMPERS.length, 1].max
|
|
36
|
+
|
|
37
|
+
# A space inside a bin's sides, unless the longest temper would only fit without it.
|
|
38
|
+
def bin_inside
|
|
39
|
+
return '' if bin_width < FeelingsLog::TEMPERS.map(&:length).max + 4
|
|
40
|
+
|
|
41
|
+
' '
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The bin's lines, highlighted when it's the chosen temper: the one being noted, or how the selected method felt.
|
|
45
|
+
def bin(number, temper, count, most, chosen)
|
|
46
|
+
style = :feelings
|
|
47
|
+
style = :selected if temper == chosen
|
|
48
|
+
bin_lines(number, temper, count, most).map { segment(it, style, width: bin_width) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Its key's number on its top line, its temper, the bar of its count, and its bottom.
|
|
52
|
+
def bin_lines(number, temper, count, most)
|
|
53
|
+
inside = bin_inside
|
|
54
|
+
name = temper.upcase.ljust(bin_width - 2 - (2 * inside.length))
|
|
55
|
+
["╭─ 0#{number} #{'─' * [bin_width - 7, 0].max}╮", "│#{inside}#{name}#{inside}│",
|
|
56
|
+
"│#{inside}#{bar(count, most)} #{count}#{inside}│", "╰#{'─' * (bin_width - 2)}╯"]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# As long as there's room beside the count, and filled as far as its share of the fullest bin's.
|
|
60
|
+
def bar(count, most)
|
|
61
|
+
length = bin_width - 3 - (2 * bin_inside.length) - count.to_s.length
|
|
62
|
+
filled = (length * count / most.to_f).round
|
|
63
|
+
"#{'█' * filled}#{'░' * (length - filled)}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raindown
|
|
4
|
+
module TUI
|
|
5
|
+
# When a batch of code feelings is refined, like Severance's waffle party: rain, a frame each tick, for about five
|
|
6
|
+
# seconds of the Screen's 60 ms frames, then a waffle, until a key's pressed.
|
|
7
|
+
class Celebration
|
|
8
|
+
RAIN_FRAMES = 83
|
|
9
|
+
|
|
10
|
+
attr_reader :frame
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@frame = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def raining? = frame < RAIN_FRAMES
|
|
17
|
+
|
|
18
|
+
# Each tick's a frame, and any other key puts the waffle away, which is when it's over.
|
|
19
|
+
def over?(key)
|
|
20
|
+
@frame += 1 if key == :tick
|
|
21
|
+
key != :tick
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|