raindown 0.5.0 → 0.6.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/actions.rb +1 -1
- data/lib/raindown/bare_directives.rb +14 -3
- data/lib/raindown/cli.rb +2 -2
- data/lib/raindown/document.rb +18 -7
- data/lib/raindown/endpoint/curl.rb +15 -0
- data/lib/raindown/endpoint/endpoint_runner.rb +1 -1
- data/lib/raindown/node_injector.rb +1 -1
- data/lib/raindown/projects.rb +8 -4
- 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 +35 -0
- data/lib/raindown/tui/file_tree.rb +53 -11
- data/lib/raindown/tui/layout.rb +60 -78
- data/lib/raindown/tui/menu_overlay.rb +59 -0
- data/lib/raindown/tui/navigator/commands.rb +84 -0
- data/lib/raindown/tui/navigator/focus.rb +25 -0
- data/lib/raindown/tui/navigator/navigator.rb +127 -0
- data/lib/raindown/tui/navigator/project_menu.rb +61 -0
- data/lib/raindown/tui/navigator/views.rb +49 -0
- data/lib/raindown/tui/renderers/dashboard_renderer.rb +102 -0
- data/lib/raindown/tui/renderers/endpoint_renderer.rb +77 -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 +15 -0
- data/lib/raindown/tui/row.rb +10 -3
- data/lib/raindown/tui/rows.rb +57 -0
- data/lib/raindown/tui/{input.rb → terminal/input.rb} +10 -11
- 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} +32 -17
- data/lib/raindown/tui/terminal/styles.rb +96 -0
- data/lib/raindown/tui/text.rb +83 -0
- data/lib/raindown/tui/title.rb +29 -0
- data/lib/raindown/tui/views/dashboard_view.rb +145 -0
- data/lib/raindown/tui/views/endpoint_view.rb +75 -0
- data/lib/raindown/tui/{pane.rb → views/list_view.rb} +9 -8
- data/lib/raindown/tui/{outline.rb → views/outline_view.rb} +39 -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 +8 -5
- data/lib/raindown.rb +2 -0
- data/lib/version.rb +1 -1
- metadata +32 -9
- data/lib/raindown/tui/commands.rb +0 -70
- 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: c9a61e7a842dcd5dc4cc5f6d004db388173ef2d5c48af1a50e32fea6c317b678
|
|
4
|
+
data.tar.gz: 7bbae724e020d48a3f095ba1cc8e1c45b9ee5dd483daf0af5f8edd4333d83205
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8f312582ede3900be43490c52c54ce3df6292764c04c177f54510e397a4814c9a9184cbb0710113055bb0dd140f00ced0bcf309f59583d34485a900579ac64b
|
|
7
|
+
data.tar.gz: 932d51981cd38660a402a40bbf9b0015da35baf62bad04152cdfdc262287f9593643daf66d2b60b6282e4df8a3de553a9ebba1793b37bec0972c145ea275c5a0
|
data/lib/raindown/actions.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Raindown
|
|
|
30
30
|
raise NotAnEndpoint, "#{directive.name} on line #{directive.line} isn't an endpoint" unless directive.endpoint?
|
|
31
31
|
|
|
32
32
|
content = File.read(path, encoding: Encoding::UTF_8)
|
|
33
|
-
rewritten = EndpointRunner.new(content:, bare: BareDirectives.
|
|
33
|
+
rewritten = EndpointRunner.new(content:, bare: BareDirectives.enabled?(path:, content:), only: directive.line - 1).rewrite
|
|
34
34
|
return false if rewritten == content
|
|
35
35
|
|
|
36
36
|
File.write(path, rewritten)
|
|
@@ -5,7 +5,8 @@ require_relative 'front_matter'
|
|
|
5
5
|
|
|
6
6
|
module Raindown
|
|
7
7
|
# Raindown files (.rd/.raindown) can write a directive like <{ if: draft }> as a bare if: draft on its own line.
|
|
8
|
-
# Markdown files opt in with
|
|
8
|
+
# Markdown files opt in with "raindown: true" in their front matter, or with the --bare flag.
|
|
9
|
+
# Nodes like toc: and files: are handled by NodeBlocks, before the rest.
|
|
9
10
|
#
|
|
10
11
|
# Bare directives have no end tag. A bare block directive closes itself after the content it applies to,
|
|
11
12
|
# so where a block needs to wrap more than that, write it with delimiters and an end tag instead:
|
|
@@ -24,8 +25,8 @@ module Raindown
|
|
|
24
25
|
|
|
25
26
|
module_function
|
|
26
27
|
|
|
27
|
-
def enabled?(path: nil, bare: false)
|
|
28
|
-
bare || file?(path)
|
|
28
|
+
def enabled?(path: nil, content: nil, bare: false)
|
|
29
|
+
bare || file?(path) || front_matter?(content)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def file?(path)
|
|
@@ -34,6 +35,16 @@ module Raindown
|
|
|
34
35
|
EXTENSIONS.include?(File.extname(path).downcase)
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
# Whether the front matter has "raindown: true". Front matter that isn't valid YAML fails when the file renders,
|
|
39
|
+
# so here it only means no.
|
|
40
|
+
def front_matter?(content)
|
|
41
|
+
return false unless content&.start_with?('---')
|
|
42
|
+
|
|
43
|
+
FrontMatter.parse(content).first[:raindown] == true
|
|
44
|
+
rescue Psych::Exception
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
37
48
|
# Wrap each bare directive in Antlers delimiters and close its block, leaving code and front matter as-is.
|
|
38
49
|
def expand(content)
|
|
39
50
|
lines = content.lines
|
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,13 +29,14 @@ 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:)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
# Raindown files read bare directives
|
|
36
|
-
# like with Nodes.load(path), the same as when the file renders.
|
|
38
|
+
# Raindown files read bare directives, as do Markdown files with "raindown: true" front matter.
|
|
39
|
+
# A project's own nodes are only directives once they're loaded, like with Nodes.load(path), the same as when the file renders.
|
|
37
40
|
def self.read(path)
|
|
38
41
|
new(markdown: File.read(path, encoding: Encoding::UTF_8), bare: BareDirectives.file?(path))
|
|
39
42
|
end
|
|
@@ -41,7 +44,7 @@ module Raindown
|
|
|
41
44
|
def initialize(markdown:, bare: false)
|
|
42
45
|
@markdown = markdown
|
|
43
46
|
@lines = markdown.lines
|
|
44
|
-
@bare = bare
|
|
47
|
+
@bare = bare || BareDirectives.front_matter?(markdown)
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
# The headings in the order they appear, with their source text, like "Notes for {name}" before it's rendered.
|
|
@@ -139,10 +142,18 @@ module Raindown
|
|
|
139
142
|
first..last if first <= last
|
|
140
143
|
end
|
|
141
144
|
|
|
142
|
-
# From a range of indexes to line numbers.
|
|
145
|
+
# From a range of indexes to line numbers. The body is the lines after the directive, up to its end tag,
|
|
146
|
+
# or to the end of a bare block, which has none.
|
|
143
147
|
def directive(name, args, span, output:, writes:)
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
end_tag = span.end > span.begin && @lines[span.end].match?(END_TAG)
|
|
149
|
+
body = trimmed(span.begin + 1, end_tag ? span.end - 1 : span.end)
|
|
150
|
+
|
|
151
|
+
Directive.new(name:, args:, line: span.begin + 1, end_line: span.end + 1, body: lines(body), output: lines(output),
|
|
152
|
+
writes:)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def lines(range)
|
|
156
|
+
(range.begin + 1)..(range.end + 1) if range
|
|
146
157
|
end
|
|
147
158
|
end
|
|
148
159
|
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,20 @@ 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
|
+
# A snippet that sends data is a POST, unless it says otherwise, as with curl.
|
|
70
|
+
def request(snippet)
|
|
71
|
+
args = arguments(snippet)
|
|
72
|
+
url = args.find { it.match?(URL) }
|
|
73
|
+
method = args[args.index('--request') + 1] if args.include?('--request')
|
|
74
|
+
method ||= 'HEAD' if args.include?('--head')
|
|
75
|
+
method ||= 'POST' if args.intersect?(VALUE_CHECKS.keys.grep(/data|json/))
|
|
76
|
+
|
|
77
|
+
Request.new(method: (method || 'GET').upcase, url:) if url
|
|
78
|
+
rescue Error
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
67
82
|
def arguments(snippet)
|
|
68
83
|
words = Shellwords.split(snippet.gsub(/\\\r?\n/, ' ').strip.delete_prefix('$ '))
|
|
69
84
|
raise Error, 'Endpoint snippets must start with curl' unless words.first == 'curl'
|
|
@@ -26,7 +26,7 @@ module Raindown
|
|
|
26
26
|
FETCHED = /\A {0,3}<!-- raindown:fetched [^\n]*? -->[ \t]*$/
|
|
27
27
|
|
|
28
28
|
def self.rewrite(content:, path: nil, bare: false, execute: false, **)
|
|
29
|
-
new(content:, bare: BareDirectives.enabled?(path:, bare:), execute:).rewrite
|
|
29
|
+
new(content:, bare: BareDirectives.enabled?(path:, content:, bare:), execute:).rewrite
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def initialize(content:, bare: false, execute: false, only: nil)
|
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,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,35 @@
|
|
|
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
|
+
# What the Screen does after an event, other than drawing again, or stopping for :quit.
|
|
11
|
+
# Leaves the screen for a command, like the editor, then tells the Navigator it ran.
|
|
12
|
+
Run = Data.define(:command)
|
|
13
|
+
|
|
14
|
+
# Shows the message while the task runs, like an endpoint's request.
|
|
15
|
+
Wait = Data.define(:message, :task)
|
|
16
|
+
|
|
17
|
+
# Steps a row or a view asks of the Navigator, which each takes on it, returning what the Screen does next: to open
|
|
18
|
+
# a view right of the files, or a file in the outline at a directive, to edit a file at a line, or to run an endpoint.
|
|
19
|
+
OpenView = Data.define(:view) do
|
|
20
|
+
def take(navigator) = navigator.open_view(view)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Open = Data.define(:path, :line) do
|
|
24
|
+
def take(navigator) = navigator.open_at(path, line)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Edit = Data.define(:path, :line) do
|
|
28
|
+
def take(navigator) = navigator.edit_at(path, line)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Execute = Data.define(:path, :line, :name) do
|
|
32
|
+
def take(navigator) = navigator.run(path, line, name)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,52 +1,94 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../projects'
|
|
4
|
+
require_relative 'events'
|
|
5
|
+
require_relative 'views/outline_view'
|
|
4
6
|
require_relative 'row'
|
|
5
7
|
|
|
6
8
|
module Raindown
|
|
7
9
|
module TUI
|
|
8
|
-
# The
|
|
10
|
+
# The nav pane's files: a project's Markdown files as a tree, with folders before files,
|
|
11
|
+
# including the files of the projects inside it, whose folders are marked with Raindown's cloud.
|
|
12
|
+
# Folders start folded, and open with Enter, or when a file in them opens.
|
|
13
|
+
# A scope narrows it to one project's own files, from the project's folder, like when it's chosen in the header.
|
|
9
14
|
class FileTree
|
|
10
15
|
attr_reader :root
|
|
16
|
+
# The project the files are narrowed to, or nil for all of them.
|
|
17
|
+
attr_accessor :scope
|
|
11
18
|
|
|
12
|
-
def initialize(root:)
|
|
19
|
+
def initialize(root:, workers:)
|
|
13
20
|
@root = root
|
|
14
|
-
@
|
|
21
|
+
@workers = workers
|
|
22
|
+
@expanded = Set.new
|
|
15
23
|
load
|
|
16
24
|
end
|
|
17
25
|
|
|
26
|
+
# The rain cloud without its emoji variation selector, which every terminal draws one column wide, so it lines up
|
|
27
|
+
# the files with the panes beside them. With the selector, as in the title, some terminals draw it two.
|
|
28
|
+
PROJECT = '🌧'
|
|
29
|
+
|
|
18
30
|
def load
|
|
19
|
-
@files = Projects.files(root)
|
|
31
|
+
@files = Projects.files(root, nested: true)
|
|
32
|
+
@projects = Projects.roots(root).to_set
|
|
20
33
|
end
|
|
21
34
|
|
|
35
|
+
# The folder the tree starts from: the scope's, or the root's.
|
|
36
|
+
def base = scope || root
|
|
37
|
+
|
|
22
38
|
def rows
|
|
23
39
|
tree = {}
|
|
24
|
-
|
|
40
|
+
files.each { |path| relative(path).split('/').reduce(tree) { |node, part| node[part] ||= {} } }
|
|
25
41
|
tree_rows(tree, '', 0)
|
|
26
42
|
end
|
|
27
43
|
|
|
28
44
|
def toggle(folder)
|
|
29
|
-
@
|
|
45
|
+
@expanded.delete?(folder) || @expanded.add(folder)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Opens the folders the file is in, so its row is shown.
|
|
49
|
+
def reveal(path)
|
|
50
|
+
folder = File.dirname(path)
|
|
51
|
+
while folder.start_with?(File.join(root, ''))
|
|
52
|
+
@expanded.add(folder)
|
|
53
|
+
folder = File.dirname(folder)
|
|
54
|
+
end
|
|
30
55
|
end
|
|
31
56
|
|
|
32
57
|
def relative(path)
|
|
33
|
-
path.delete_prefix(File.join(
|
|
58
|
+
path.delete_prefix(File.join(base, ''))
|
|
34
59
|
end
|
|
35
60
|
|
|
36
61
|
private
|
|
37
62
|
|
|
63
|
+
# A scope's own files, without those of the projects inside it, which are projects of their own.
|
|
64
|
+
def files
|
|
65
|
+
return @files unless scope
|
|
66
|
+
|
|
67
|
+
@files.select { |path| owner(path) == scope }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def owner(path)
|
|
71
|
+
[root, *@projects].select { path.start_with?(File.join(it, '')) }.max_by(&:length)
|
|
72
|
+
end
|
|
73
|
+
|
|
38
74
|
def tree_rows(tree, prefix, depth)
|
|
39
75
|
folders, files = tree.partition { |_name, children| children.any? }
|
|
40
76
|
|
|
41
77
|
folders.sort.flat_map { |name, children| folder_rows(name, children, prefix, depth) } +
|
|
42
|
-
files.sort.map { |name, _|
|
|
78
|
+
files.sort.map { |name, _| file_row(name, File.join(base, prefix, name), depth) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def file_row(name, path, depth)
|
|
82
|
+
Row.new(text: name, depth:, kind: :file, target: path, open: OpenView.new(view: OutlineView.new(path, workers: @workers)))
|
|
43
83
|
end
|
|
44
84
|
|
|
45
85
|
# A folder, and the tree inside it unless it's folded.
|
|
46
86
|
def folder_rows(name, children, prefix, depth)
|
|
47
|
-
folder = File.join(
|
|
48
|
-
folded =
|
|
49
|
-
|
|
87
|
+
folder = File.join(base, prefix, name)
|
|
88
|
+
folded = !@expanded.include?(folder)
|
|
89
|
+
project = @projects.include?(folder)
|
|
90
|
+
row = Row.new(text: "#{folded ? '▸' : '▾'} #{"#{PROJECT} " if project}#{name}/", depth:,
|
|
91
|
+
kind: project ? :project : :folder, target: folder, fold: -> { toggle(folder) })
|
|
50
92
|
|
|
51
93
|
[row, *(tree_rows(children, File.join(prefix, name), depth + 1) unless folded)]
|
|
52
94
|
end
|