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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'project'
|
|
4
|
+
|
|
5
|
+
module Raindown
|
|
6
|
+
module TUI
|
|
7
|
+
# A Project in a process of its own, forked so it loads the project's nodes apart from other projects', like watching.
|
|
8
|
+
# Requests and answers go through two pipes, as Marshal data, one at a time. Without fork, like on Windows,
|
|
9
|
+
# or with fork off, like in specs, the Project answers in this process instead.
|
|
10
|
+
# The process ends with exit!, since it's forked mid-call, so the terminal UI's ensure doesn't run in it.
|
|
11
|
+
class Worker
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
attr_writer :fork
|
|
16
|
+
|
|
17
|
+
def fork?
|
|
18
|
+
@fork.nil? ? Process.respond_to?(:fork) : @fork
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Every worker's pipes, which a new worker closes the others of, since a worker holding another's pipe open
|
|
22
|
+
# would keep that worker from seeing it close.
|
|
23
|
+
def pipes
|
|
24
|
+
@pipes ||= []
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :root
|
|
29
|
+
|
|
30
|
+
def initialize(root:, fork: self.class.fork?)
|
|
31
|
+
@root = root
|
|
32
|
+
@fork = fork
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The Project's answer, or a Worker::Error with its error's message. A worker that stopped starts again.
|
|
36
|
+
def call(name, *args, **options)
|
|
37
|
+
return local(name, args, options) unless @fork
|
|
38
|
+
|
|
39
|
+
start unless alive?
|
|
40
|
+
write(@requests, [name, args, options])
|
|
41
|
+
status, value = read(@answers) || raise(Error, "The worker for #{File.basename(root)} stopped")
|
|
42
|
+
status == :ok ? value : raise(Error, value)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def stop
|
|
46
|
+
[@requests, @answers].compact.each { it.close unless it.closed? }
|
|
47
|
+
return unless @pid
|
|
48
|
+
|
|
49
|
+
Process.kill('TERM', @pid)
|
|
50
|
+
Process.wait(@pid)
|
|
51
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
52
|
+
nil
|
|
53
|
+
ensure
|
|
54
|
+
@pid = nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def local(name, args, options)
|
|
60
|
+
@project ||= Project.new(root:)
|
|
61
|
+
@project.public_send(name, *args, **options)
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
raise Error, e.message
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def start
|
|
67
|
+
stop
|
|
68
|
+
requests, @requests = IO.pipe
|
|
69
|
+
@answers, answers = IO.pipe
|
|
70
|
+
self.class.pipes.reject!(&:closed?)
|
|
71
|
+
self.class.pipes.push(@requests, @answers)
|
|
72
|
+
@pid = fork do
|
|
73
|
+
self.class.pipes.each { it.close unless it.closed? }
|
|
74
|
+
serve(requests, answers)
|
|
75
|
+
end
|
|
76
|
+
requests.close
|
|
77
|
+
answers.close
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Answers until the terminal UI closes its end. The screen is the terminal UI's, so nothing is written to it,
|
|
81
|
+
# and Ctrl+C, which reaches every process in the terminal, is the terminal UI's to handle.
|
|
82
|
+
def serve(requests, answers)
|
|
83
|
+
trap('INT', 'IGNORE')
|
|
84
|
+
$stdout.reopen(File::NULL)
|
|
85
|
+
$stderr.reopen(File::NULL)
|
|
86
|
+
project = Project.new(root:)
|
|
87
|
+
|
|
88
|
+
while (request = read(requests))
|
|
89
|
+
write(answers, answer(project, *request))
|
|
90
|
+
end
|
|
91
|
+
ensure
|
|
92
|
+
exit!(0)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def answer(project, name, args, options)
|
|
96
|
+
[:ok, project.public_send(name, *args, **options)]
|
|
97
|
+
rescue StandardError, ScriptError => e
|
|
98
|
+
[:error, e.message]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def alive?
|
|
102
|
+
@pid && Process.waitpid(@pid, Process::WNOHANG).nil?
|
|
103
|
+
rescue Errno::ECHILD
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A message is its length, then its Marshal data.
|
|
108
|
+
def write(io, message)
|
|
109
|
+
data = Marshal.dump(message)
|
|
110
|
+
io.write([data.bytesize].pack('N'), data)
|
|
111
|
+
io.flush
|
|
112
|
+
rescue Errno::EPIPE, IOError
|
|
113
|
+
raise Error, "The worker for #{File.basename(root)} stopped"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def read(io)
|
|
117
|
+
length = io.read(4)&.unpack1('N')
|
|
118
|
+
Marshal.load(io.read(length)) if length # rubocop:disable Security/MarshalLoad -- from this process's own fork
|
|
119
|
+
rescue IOError
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../projects'
|
|
4
|
+
require_relative 'worker'
|
|
5
|
+
|
|
6
|
+
module Raindown
|
|
7
|
+
module TUI
|
|
8
|
+
# A Worker for each project in the terminal UI's folder: the folder's own, and every project inside it.
|
|
9
|
+
# A file's requests go to the worker of the project it's in, the nearest one above it, even when it's new.
|
|
10
|
+
# Workers start when they're first asked something.
|
|
11
|
+
class Workers
|
|
12
|
+
attr_reader :root, :roots
|
|
13
|
+
|
|
14
|
+
def initialize(root:, fork: Worker.fork?)
|
|
15
|
+
@root = root
|
|
16
|
+
@fork = fork
|
|
17
|
+
@workers = {}
|
|
18
|
+
find_roots
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def for(path)
|
|
22
|
+
worker(Projects.root(path) || root)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A worker for each project, as they were found when the workers last started.
|
|
26
|
+
def each(&)
|
|
27
|
+
roots.map { worker(it) }.each(&)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Stops the workers, so the next requests start them again, loading their projects' nodes again,
|
|
31
|
+
# and finds the projects again, like ones added since.
|
|
32
|
+
def restart
|
|
33
|
+
stop
|
|
34
|
+
@workers.clear
|
|
35
|
+
find_roots
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def stop
|
|
39
|
+
@workers.each_value(&:stop)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def worker(project)
|
|
45
|
+
@workers[project] ||= Worker.new(root: project, fork: @fork)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def find_roots
|
|
49
|
+
@roots = [root, *Projects.roots(root)].uniq.sort
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/raindown/tui.rb
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'nodes'
|
|
4
3
|
require_relative 'projects'
|
|
5
|
-
require_relative 'tui/navigator'
|
|
4
|
+
require_relative 'tui/navigator/navigator'
|
|
6
5
|
|
|
7
6
|
module Raindown
|
|
8
7
|
# The terminal UI, started with "raindown tui": a project's Markdown files beside the open file's outline,
|
|
@@ -13,15 +12,19 @@ module Raindown
|
|
|
13
12
|
|
|
14
13
|
# PATH is a file to open, or a directory. Its project is the one it's in, or else the directory itself.
|
|
15
14
|
def start(path)
|
|
16
|
-
require_relative 'tui/screen'
|
|
15
|
+
require_relative 'tui/terminal/screen'
|
|
17
16
|
# Without a terminal, curses' own error is "Error opening terminal: unknown".
|
|
18
17
|
abort('raindown tui needs a terminal') unless $stdin.tty? && $stdout.tty?
|
|
19
18
|
|
|
20
19
|
path = File.expand_path(path)
|
|
21
20
|
root = Projects.root(path) || (File.directory?(path) ? path : File.dirname(path))
|
|
22
|
-
|
|
21
|
+
# The projects inside it are shown too. Each loads its own nodes in its worker's process, like when watching,
|
|
22
|
+
# so two projects' nodes of the same name stay apart.
|
|
23
|
+
workers = Workers.new(root:)
|
|
23
24
|
|
|
24
|
-
Screen.new(Navigator.new(root:, path: (path unless File.directory?(path)))).run
|
|
25
|
+
Screen.new(Navigator.new(root:, path: (path unless File.directory?(path)), workers:)).run
|
|
26
|
+
ensure
|
|
27
|
+
workers&.stop
|
|
25
28
|
end
|
|
26
29
|
end
|
|
27
30
|
end
|
data/lib/raindown.rb
CHANGED
|
@@ -47,6 +47,8 @@ module Raindown
|
|
|
47
47
|
# Directives like "toc:" export to HTML as bold or comments depending on hide_directives config.
|
|
48
48
|
# Code blocks are highlighted, unless highlight_code is false for a site that highlights code in the browser.
|
|
49
49
|
def render(markdown:, metadata: NoMetadata, bare: false, hide_directives: false, highlight_code: true)
|
|
50
|
+
bare ||= BareDirectives.front_matter?(markdown)
|
|
51
|
+
|
|
50
52
|
highlighting(highlight_code) do
|
|
51
53
|
Fragments.collect do
|
|
52
54
|
# Components' HTML goes back in, then their headings get unique ids, before nodes like the TOC see it.
|
data/lib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: raindown
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-27 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: antlers
|
|
@@ -216,16 +216,39 @@ files:
|
|
|
216
216
|
- lib/raindown/table_formatter.rb
|
|
217
217
|
- lib/raindown/toc.rb
|
|
218
218
|
- lib/raindown/tui.rb
|
|
219
|
-
- lib/raindown/tui/
|
|
219
|
+
- lib/raindown/tui/card.rb
|
|
220
|
+
- lib/raindown/tui/endpoint.rb
|
|
221
|
+
- lib/raindown/tui/endpoints.rb
|
|
222
|
+
- lib/raindown/tui/events.rb
|
|
220
223
|
- lib/raindown/tui/file_tree.rb
|
|
221
|
-
- lib/raindown/tui/input.rb
|
|
222
224
|
- lib/raindown/tui/layout.rb
|
|
223
|
-
- lib/raindown/tui/
|
|
224
|
-
- lib/raindown/tui/
|
|
225
|
-
- lib/raindown/tui/
|
|
225
|
+
- lib/raindown/tui/menu_overlay.rb
|
|
226
|
+
- lib/raindown/tui/navigator/commands.rb
|
|
227
|
+
- lib/raindown/tui/navigator/focus.rb
|
|
228
|
+
- lib/raindown/tui/navigator/navigator.rb
|
|
229
|
+
- lib/raindown/tui/navigator/project_menu.rb
|
|
230
|
+
- lib/raindown/tui/navigator/views.rb
|
|
231
|
+
- lib/raindown/tui/renderers/dashboard_renderer.rb
|
|
232
|
+
- lib/raindown/tui/renderers/endpoint_renderer.rb
|
|
233
|
+
- lib/raindown/tui/renderers/list_renderer.rb
|
|
234
|
+
- lib/raindown/tui/renderers/outline_renderer.rb
|
|
235
|
+
- lib/raindown/tui/renderers/renderer.rb
|
|
226
236
|
- lib/raindown/tui/row.rb
|
|
227
|
-
- lib/raindown/tui/
|
|
228
|
-
- lib/raindown/tui/
|
|
237
|
+
- lib/raindown/tui/rows.rb
|
|
238
|
+
- lib/raindown/tui/terminal/input.rb
|
|
239
|
+
- lib/raindown/tui/terminal/ncurses_shim.rb
|
|
240
|
+
- lib/raindown/tui/terminal/quiet.rb
|
|
241
|
+
- lib/raindown/tui/terminal/screen.rb
|
|
242
|
+
- lib/raindown/tui/terminal/styles.rb
|
|
243
|
+
- lib/raindown/tui/text.rb
|
|
244
|
+
- lib/raindown/tui/title.rb
|
|
245
|
+
- lib/raindown/tui/views/dashboard_view.rb
|
|
246
|
+
- lib/raindown/tui/views/endpoint_view.rb
|
|
247
|
+
- lib/raindown/tui/views/list_view.rb
|
|
248
|
+
- lib/raindown/tui/views/outline_view.rb
|
|
249
|
+
- lib/raindown/tui/workers/project.rb
|
|
250
|
+
- lib/raindown/tui/workers/worker.rb
|
|
251
|
+
- lib/raindown/tui/workers/workers.rb
|
|
229
252
|
- lib/raindown/variables.rb
|
|
230
253
|
- lib/raindown/watcher.rb
|
|
231
254
|
- lib/version.rb
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../actions'
|
|
4
|
-
|
|
5
|
-
module Raindown
|
|
6
|
-
module TUI
|
|
7
|
-
# What a Navigator does to the open file: run an endpoint, open the editor, write back and read it again.
|
|
8
|
-
# Each sets the status, so the Screen shows how it went, and returns what the Screen should do next, if anything.
|
|
9
|
-
module Commands
|
|
10
|
-
private
|
|
11
|
-
|
|
12
|
-
def execute
|
|
13
|
-
directive = selected(:outline)&.target if @focus == :outline
|
|
14
|
-
return error('Select an endpoint to run') unless directive.is_a?(Document::Directive) && directive.endpoint?
|
|
15
|
-
|
|
16
|
-
[:wait, "Running the endpoint on line #{directive.line}...", -> { run_endpoint(directive) }]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def run_endpoint(directive)
|
|
20
|
-
changed = Actions.execute(path, directive)
|
|
21
|
-
@outline.read
|
|
22
|
-
info(changed ? "Ran the endpoint on line #{directive.line}" : 'The response is the same')
|
|
23
|
-
rescue StandardError => e
|
|
24
|
-
error(e.message)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# In the files pane, the selected file opens at its top, and in the outline, the open file at the selected line.
|
|
28
|
-
def edit
|
|
29
|
-
row = selected
|
|
30
|
-
show(row.target) if @focus == :files && row&.kind == :file
|
|
31
|
-
return error('Open a file to edit') unless path
|
|
32
|
-
|
|
33
|
-
line = @focus == :outline && row ? row.line : 1
|
|
34
|
-
[:run, Actions.edit_command(path, line:)]
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def write_back
|
|
38
|
-
return error('Open a file to write back') unless path
|
|
39
|
-
|
|
40
|
-
changed = Actions.write_back(path)
|
|
41
|
-
@outline.read
|
|
42
|
-
info(changed ? "Updated #{relative(path)}" : "#{relative(path)} is up to date")
|
|
43
|
-
rescue StandardError => e
|
|
44
|
-
error(e.message)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def reload
|
|
48
|
-
@files.load
|
|
49
|
-
@outline&.read if path && File.exist?(path)
|
|
50
|
-
info('Reloaded')
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Reads the file again when it's changed, like when it's saved in an editor or a watcher writes back to it.
|
|
54
|
-
def refresh
|
|
55
|
-
@outline.read if @outline&.changed?
|
|
56
|
-
nil
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def info(message)
|
|
60
|
-
@status = [:info, message]
|
|
61
|
-
nil
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def error(message)
|
|
65
|
-
@status = [:error, message]
|
|
66
|
-
nil
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'file_tree'
|
|
4
|
-
require_relative 'commands'
|
|
5
|
-
require_relative 'outline'
|
|
6
|
-
require_relative 'pane'
|
|
7
|
-
|
|
8
|
-
module Raindown
|
|
9
|
-
module TUI
|
|
10
|
-
# What the terminal UI shows and does, in plain Ruby, with no terminal: a FileTree of the project's Markdown files,
|
|
11
|
-
# beside the open file's Outline. The Screen draws it and sends it events, like :down or
|
|
12
|
-
# [:press, :outline, 3, :execute], and handle returns what the Screen should do next:
|
|
13
|
-
# nil draw again
|
|
14
|
-
# :quit stop
|
|
15
|
-
# [:run, command] leave the screen to run a command, like the editor, then call ran
|
|
16
|
-
# [:wait, message, task] show the message while the task runs, like an endpoint's request
|
|
17
|
-
class Navigator
|
|
18
|
-
include Commands
|
|
19
|
-
|
|
20
|
-
# Keys of the events that move the cursor or the focus, and those that act on what's selected.
|
|
21
|
-
MOVES = { up: -1, down: 1 }.freeze
|
|
22
|
-
PANES = { tab: nil, left: :files, right: :outline }.freeze
|
|
23
|
-
ACTIONS = { execute: :execute, edit: :edit, write_back: :write_back, reload: :reload, tick: :refresh }.freeze
|
|
24
|
-
|
|
25
|
-
attr_reader :focus, :status
|
|
26
|
-
|
|
27
|
-
def initialize(root:, path: nil)
|
|
28
|
-
@files = FileTree.new(root:)
|
|
29
|
-
@focus = :files
|
|
30
|
-
@panes = { files: Pane.new { @files.rows }, outline: Pane.new { @outline ? @outline.rows : [] } }
|
|
31
|
-
@status = [:info, 'Tab switches panes, Enter opens and folds, q quits']
|
|
32
|
-
first = @files.rows.find { it.kind == :file }&.target
|
|
33
|
-
show(path || first) if path || first
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def handle(event)
|
|
37
|
-
name, *args = event
|
|
38
|
-
return :quit if name == :quit
|
|
39
|
-
return move(MOVES[name]) if MOVES.key?(name)
|
|
40
|
-
return switch(PANES[name] || other_pane) if PANES.key?(name)
|
|
41
|
-
return select(*args) if name == :select
|
|
42
|
-
return press(*args) if name == :press
|
|
43
|
-
return activate(name) if %i[enter toggle].include?(name)
|
|
44
|
-
|
|
45
|
-
send(ACTIONS[name]) if ACTIONS.key?(name)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# After the editor closes, the file is written back, like a save while it's watched, and read again.
|
|
49
|
-
def ran
|
|
50
|
-
write_back
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def root = @files.root
|
|
54
|
-
def path = @outline&.path
|
|
55
|
-
def relative(path) = @files.relative(path)
|
|
56
|
-
|
|
57
|
-
def rows(pane) = @panes[pane].rows
|
|
58
|
-
def cursor(pane) = @panes[pane].cursor
|
|
59
|
-
def visible(pane, height) = @panes[pane].visible(height)
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def show(path)
|
|
64
|
-
@outline = Outline.new(File.expand_path(path))
|
|
65
|
-
@panes[:outline].reset
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def other_pane
|
|
69
|
-
@focus == :files ? :outline : :files
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def move(by)
|
|
73
|
-
@panes[@focus].move(by)
|
|
74
|
-
nil
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def switch(pane)
|
|
78
|
-
@focus = pane
|
|
79
|
-
nil
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def select(pane, index)
|
|
83
|
-
@focus = pane
|
|
84
|
-
@panes[pane].cursor = index
|
|
85
|
-
nil
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def press(pane, index, event)
|
|
89
|
-
select(pane, index)
|
|
90
|
-
handle(event)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def selected(pane = @focus)
|
|
94
|
-
@panes[pane].selected
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Enter opens what's selected, or folds it, and Space only folds.
|
|
98
|
-
# A line of output, or a directive without any, opens in the editor.
|
|
99
|
-
def activate(name)
|
|
100
|
-
row = selected
|
|
101
|
-
return edit if name == :enter && editable?(row)
|
|
102
|
-
|
|
103
|
-
case row&.kind
|
|
104
|
-
when :folder then @files.toggle(row.target)
|
|
105
|
-
when :file then open_file(row.target) if name == :enter
|
|
106
|
-
when :heading then @outline.fold(row.target)
|
|
107
|
-
when :directive then @outline.expand(row.target) if row.target.output
|
|
108
|
-
end
|
|
109
|
-
nil
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def editable?(row)
|
|
113
|
-
row&.kind == :output || (row&.kind == :directive && !row.target.output)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def open_file(path)
|
|
117
|
-
show(path)
|
|
118
|
-
@focus = :outline
|
|
119
|
-
info("Opened #{relative(path)}")
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
data/lib/raindown/tui/styles.rb
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'curses'
|
|
4
|
-
|
|
5
|
-
module Raindown
|
|
6
|
-
module TUI
|
|
7
|
-
# The curses attributes of a Layout's styles, in colour on the terminal's own background when it has colours.
|
|
8
|
-
module Styles
|
|
9
|
-
COLORS = { directive: Curses::COLOR_CYAN, button: Curses::COLOR_YELLOW, error: Curses::COLOR_RED,
|
|
10
|
-
folder: Curses::COLOR_BLUE }.freeze
|
|
11
|
-
|
|
12
|
-
module_function
|
|
13
|
-
|
|
14
|
-
# Sets up the colours, once the screen has started, and returns the attribute of each style.
|
|
15
|
-
def start
|
|
16
|
-
colors = Curses.has_colors?
|
|
17
|
-
start_colors if colors
|
|
18
|
-
color = ->(name) { colors ? Curses.color_pair(COLORS.keys.index(name) + 1) : Curses::A_NORMAL }
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
title: Curses::A_REVERSE | Curses::A_BOLD, selected: Curses::A_REVERSE, cursor: Curses::A_UNDERLINE,
|
|
22
|
-
heading: Curses::A_BOLD, directive: color[:directive], output: Curses::A_DIM,
|
|
23
|
-
error: color[:error] | Curses::A_BOLD, button: color[:button] | Curses::A_BOLD,
|
|
24
|
-
selected_button: color[:button] | Curses::A_REVERSE, folder: color[:folder] | Curses::A_BOLD,
|
|
25
|
-
key: Curses::A_REVERSE, border: Curses::A_DIM
|
|
26
|
-
}
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def start_colors
|
|
30
|
-
Curses.start_color
|
|
31
|
-
Curses.use_default_colors
|
|
32
|
-
COLORS.each_value.with_index(1) { |color, pair| Curses.init_pair(pair, color, -1) }
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|