raindown 0.3.0 → 0.5.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.
@@ -0,0 +1,123 @@
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
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../document'
4
+ require_relative 'row'
5
+
6
+ module Raindown
7
+ module TUI
8
+ # The outline pane: a file's headings and directives, read from its Document. Headings fold the section below them,
9
+ # and a directive's output, the lines Raindown wrote back, expands below it. Both start folded away.
10
+ class Outline
11
+ attr_reader :path, :error
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ @folded = Set.new
16
+ @expanded = Set.new
17
+ read
18
+ end
19
+
20
+ # Reads the file again, keeping what's folded.
21
+ def read
22
+ @mtime = File.mtime(path)
23
+ @document = Document.read(path)
24
+ @lines = File.readlines(path, chomp: true)
25
+ @document.directives # Finds an error in the file, like a missing end tag, now rather than while drawing.
26
+ @error = nil
27
+ rescue StandardError => e
28
+ @error = e.message
29
+ end
30
+
31
+ def changed?
32
+ File.exist?(path) && File.mtime(path) != @mtime
33
+ end
34
+
35
+ def fold(heading)
36
+ @folded.delete?(heading.line) || @folded.add(heading.line)
37
+ end
38
+
39
+ def expand(directive)
40
+ @expanded.delete?(directive.line) || @expanded.add(directive.line)
41
+ end
42
+
43
+ def rows
44
+ return [Row.new(text: error, depth: 0, kind: :error, target: 1)] if error
45
+
46
+ sections.flat_map do |heading, items|
47
+ next items.flat_map { directive_rows(it, depth: 0) } unless heading
48
+
49
+ depth = heading.level - min_level + 1
50
+ [heading_row(heading), *(items.flat_map { directive_rows(it, depth:) } unless @folded.include?(heading.line))]
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Each heading with the directives below it, leaving out the headings in a folded heading's section.
57
+ # Directives before the first heading come first, with no heading.
58
+ def sections
59
+ sections = [[nil, []]]
60
+ hidden = nil
61
+
62
+ items.each do |item|
63
+ heading = item.is_a?(Document::Heading)
64
+ next if hidden && !(heading && item.level <= hidden)
65
+
66
+ hidden = (item.level if @folded.include?(item.line)) if heading
67
+ heading ? sections << [item, []] : sections.last.last << item
68
+ end
69
+
70
+ sections
71
+ end
72
+
73
+ # Headings a directive wrote are part of its output, so they show when it's expanded rather than on their own.
74
+ def items
75
+ outputs = @document.directives.filter_map(&:output)
76
+ @document.outline.reject { |item| item.is_a?(Document::Heading) && outputs.any? { it.cover?(item.line) } }
77
+ end
78
+
79
+ def min_level
80
+ @document.headings.map(&:level).min || 1
81
+ end
82
+
83
+ def heading_row(heading)
84
+ folded = @folded.include?(heading.line) ? ' …' : ''
85
+ Row.new(text: "#{'#' * heading.level} #{heading.text}#{folded}", depth: heading.level - min_level, kind: :heading,
86
+ target: heading)
87
+ end
88
+
89
+ def directive_rows(directive, depth:)
90
+ expanded = @expanded.include?(directive.line)
91
+ row = Row.new(text: directive_text(directive), depth:, kind: :directive, target: directive,
92
+ buttons: buttons(directive, expanded:))
93
+ return [row] unless expanded
94
+
95
+ [row, *directive.output.map do |number|
96
+ Row.new(text: @lines[number - 1].to_s, depth: depth + 1, kind: :output, target: number)
97
+ end]
98
+ end
99
+
100
+ def buttons(directive, expanded:)
101
+ buttons = []
102
+ buttons << [expanded ? '[-]' : "[+#{directive.output.size}]", :toggle] if directive.output
103
+ buttons << ['[Run]', :execute] if directive.endpoint?
104
+ buttons
105
+ end
106
+
107
+ def directive_text(directive)
108
+ return ":#{directive.name}" unless directive.writes
109
+
110
+ [directive.name, ':', (" #{directive.args}" unless directive.args.empty?)].join
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raindown
4
+ module TUI
5
+ # A pane's cursor and how far it's scrolled, over rows that can change between draws, like when a folder folds.
6
+ class Pane
7
+ attr_writer :cursor
8
+
9
+ def initialize(&rows)
10
+ @rows = rows
11
+ @cursor = 0
12
+ @offset = 0
13
+ end
14
+
15
+ def rows = @rows.call
16
+ def selected = rows[cursor]
17
+
18
+ # The cursor stays on a row when there are fewer rows than before.
19
+ def cursor
20
+ @cursor.clamp(0, [rows.length - 1, 0].max)
21
+ end
22
+
23
+ def move(by)
24
+ @cursor = (cursor + by).clamp(0, [rows.length - 1, 0].max)
25
+ end
26
+
27
+ def reset
28
+ @cursor = 0
29
+ @offset = 0
30
+ end
31
+
32
+ # The rows that fit in the height, scrolled so the cursor's row is one of them, and the first one's index.
33
+ def visible(height)
34
+ cursor = self.cursor
35
+ @offset = @offset.clamp([cursor - height + 1, 0].max, cursor)
36
+
37
+ [rows[@offset, height] || [], @offset]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raindown
4
+ module TUI
5
+ # A line of a pane. Target is what it's about: a folder or file path, a Heading or Directive, or a line number.
6
+ # Buttons are [label, event] pairs, like ['[Run]', :execute].
7
+ Row = Data.define(:text, :depth, :kind, :target, :buttons) do
8
+ def initialize(buttons: [], **) = super
9
+
10
+ # The line the editor opens at for the row.
11
+ def line
12
+ target.is_a?(Integer) ? target : target.line
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'curses'
4
+
5
+ require_relative 'input'
6
+ require_relative 'layout'
7
+ require_relative 'styles'
8
+
9
+ module Raindown
10
+ module TUI
11
+ # Paints a Navigator's Layout with curses, and sends the Navigator the events of keys and clicks, read with Input.
12
+ # Only this class draws on the terminal, so another library could draw the same Layout.
13
+ class Screen
14
+ # How often, in milliseconds, the open file is checked for changes while no key is pressed.
15
+ TICK = 500
16
+
17
+ def initialize(navigator)
18
+ @navigator = navigator
19
+ @zones = []
20
+ end
21
+
22
+ def run
23
+ start
24
+
25
+ loop do
26
+ draw
27
+ effect = @navigator.handle(read)
28
+ break if effect == :quit
29
+
30
+ perform(*effect) if effect.is_a?(Array)
31
+ end
32
+ rescue Interrupt
33
+ nil
34
+ ensure
35
+ stop
36
+ end
37
+
38
+ private
39
+
40
+ def perform(name, *args)
41
+ case name
42
+ when :run
43
+ suspend { system(*args.first) }
44
+ @navigator.ran
45
+ when :wait
46
+ message, task = args
47
+ show(message)
48
+ task.call
49
+ end
50
+ end
51
+
52
+ def start
53
+ ENV['ESCDELAY'] ||= '25'
54
+ Curses.init_screen
55
+ @styles = Styles.start
56
+ Curses.noecho
57
+ Curses.cbreak
58
+ Curses.nonl
59
+ Curses.curs_set(0)
60
+ Curses.stdscr.keypad(true)
61
+ Curses.stdscr.timeout = TICK
62
+ Curses.mousemask(Curses::BUTTON1_PRESSED | Curses::BUTTON1_CLICKED | Input.wheel.keys.sum)
63
+ Curses.mouseinterval(0)
64
+ end
65
+
66
+ def stop
67
+ Curses.close_screen
68
+ end
69
+
70
+ def draw
71
+ lines = Layout.new(@navigator, width: Curses.cols, height: Curses.lines).lines
72
+ @zones = Input.zones(lines)
73
+ window = Curses.stdscr
74
+ window.erase
75
+
76
+ lines.each_with_index do |segments, row|
77
+ window.setpos(row, 0)
78
+ # Writing the bottom-right corner would scroll the screen, so the last line stops a column short.
79
+ segments = last_line(segments) if row == Curses.lines - 1
80
+ segments.each { |segment| paint(window, segment) }
81
+ end
82
+
83
+ window.refresh
84
+ end
85
+
86
+ def paint(window, segment)
87
+ window.attron(@styles.fetch(segment.style, Curses::A_NORMAL)) { window.addstr(segment.text) }
88
+ end
89
+
90
+ def last_line(segments)
91
+ *rest, last = segments
92
+ [*rest, last.with(text: last.text[0...-1])]
93
+ end
94
+
95
+ # The next event, or :tick when no key is pressed in time, or nil for keys without one, like a resize.
96
+ def read
97
+ key = Curses.stdscr.getch
98
+ return :tick if key.nil?
99
+ return mouse if key == Curses::KEY_MOUSE
100
+
101
+ Curses.stdscr.clear if key == Curses::KEY_RESIZE
102
+ Input.key_event(key)
103
+ end
104
+
105
+ def mouse
106
+ event = Curses.getmouse
107
+ Input.mouse_event(event, @zones) if event
108
+ end
109
+
110
+ # Shows a message in the status line while something slow runs, like an endpoint's request.
111
+ def show(message)
112
+ window = Curses.stdscr
113
+ window.setpos(Curses.lines - 2, 0)
114
+ window.clrtoeol
115
+ window.addstr(" #{message}"[0, Curses.cols])
116
+ window.refresh
117
+ end
118
+
119
+ # Leaves the screen to the command, like the editor, and comes back to it after.
120
+ def suspend
121
+ Curses.def_prog_mode
122
+ Curses.close_screen
123
+ yield
124
+ ensure
125
+ Curses.reset_prog_mode
126
+ Curses.stdscr.clear
127
+ Curses.refresh
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'nodes'
4
+ require_relative 'projects'
5
+ require_relative 'tui/navigator'
6
+
7
+ module Raindown
8
+ # The terminal UI, started with "raindown tui": a project's Markdown files beside the open file's outline,
9
+ # where directives fold, endpoints run and the editor opens at a line. Curses only loads here,
10
+ # so watching and rendering start without it.
11
+ module TUI
12
+ module_function
13
+
14
+ # PATH is a file to open, or a directory. Its project is the one it's in, or else the directory itself.
15
+ def start(path)
16
+ require_relative 'tui/screen'
17
+ # Without a terminal, curses' own error is "Error opening terminal: unknown".
18
+ abort('raindown tui needs a terminal') unless $stdin.tty? && $stdout.tty?
19
+
20
+ path = File.expand_path(path)
21
+ root = Projects.root(path) || (File.directory?(path) ? path : File.dirname(path))
22
+ Nodes.load(File.join(root, Nodes::FOLDER))
23
+
24
+ Screen.new(Navigator.new(root:, path: (path unless File.directory?(path)))).run
25
+ end
26
+ end
27
+ end
@@ -4,12 +4,12 @@ require_relative '../raindown'
4
4
  require_relative 'rewriter'
5
5
 
6
6
  module Raindown
7
+ # Watches one file and writes back to it as it's saved, like its TOC, tables and endpoint responses.
7
8
  class Watcher
8
- attr_reader :path, :output, :bare, :execute
9
+ attr_reader :path, :bare, :execute
9
10
 
10
- def initialize(path:, output: nil, bare: false, execute: false)
11
+ def initialize(path:, bare: false, execute: false)
11
12
  @path = path
12
- @output = output || default_output
13
13
  @bare = bare
14
14
  @execute = execute
15
15
  end
@@ -19,9 +19,8 @@ module Raindown
19
19
 
20
20
  $stdout.sync = true
21
21
 
22
- rewrite
23
- render
24
- puts "Watching #{path} for changes (writing to #{output})..."
22
+ safely { rewrite }
23
+ puts "Watching #{path} for changes..."
25
24
 
26
25
  listener.start
27
26
  sleep
@@ -35,25 +34,24 @@ module Raindown
35
34
 
36
35
  Listen.to(directory) do |modified, added, _removed|
37
36
  next unless (modified + added).any? { |f| File.basename(f) == filename }
38
- # Formatting saves the file again, and that change triggers the render.
39
- next if rewrite
40
37
 
41
- render
42
- puts "Re-rendered #{path} -> #{output}"
38
+ # Writing back saves the file again, and that save changes nothing, so it ends there.
39
+ safely { puts "Updated #{path}" if rewrite }
43
40
  end
44
41
  end
45
42
 
46
- # The files' metadata is loaded again each time, so new and renamed files are listed.
47
- def rewrite
48
- Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path))
49
- end
43
+ # An error in the file is shown rather than stopping the watcher, so it can be fixed and saved again.
44
+ def safely
45
+ yield
46
+ rescue StandardError => e
47
+ raise if ENV['RAINDOWN_DEBUG']
50
48
 
51
- def render
52
- File.write(output, Raindown.render_file(path:, bare:, metadata: Pages.for(path)))
49
+ warn "raindown: #{path}: #{e.message}"
53
50
  end
54
51
 
55
- def default_output
56
- "#{File.dirname(path)}/#{File.basename(path, '.*')}.html"
52
+ # The files' metadata is loaded again each time, so new and renamed files are listed.
53
+ def rewrite
54
+ Rewriter.update(path:, bare:, execute:, metadata: Pages.for(path))
57
55
  end
58
56
  end
59
57
  end
data/lib/raindown.rb CHANGED
@@ -35,9 +35,6 @@ module Raindown
35
35
  def self.list(**) = []
36
36
  end
37
37
 
38
- # Headings with an id, which are the ones a table of contents links to.
39
- HEADINGS = 'h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]'
40
-
41
38
  module_function
42
39
 
43
40
  # Directives show in bold and code is highlighted, unless the file's project changes them in .raindown/config.yml.
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raindown
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.0'
5
5
  end
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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-20 00:00:00.000000000 Z
10
+ date: 2026-10-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: antlers
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: curses
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.7'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.7'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: dirload
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -80,21 +94,21 @@ dependencies:
80
94
  - !ruby/object:Gem::Version
81
95
  version: '0'
82
96
  - !ruby/object:Gem::Dependency
83
- name: lownode
97
+ name: lowkey
84
98
  requirement: !ruby/object:Gem::Requirement
85
99
  requirements:
86
100
  - - ">="
87
101
  - !ruby/object:Gem::Version
88
- version: '0'
102
+ version: 0.7.0
89
103
  type: :runtime
90
104
  prerelease: false
91
105
  version_requirements: !ruby/object:Gem::Requirement
92
106
  requirements:
93
107
  - - ">="
94
108
  - !ruby/object:Gem::Version
95
- version: '0'
109
+ version: 0.7.0
96
110
  - !ruby/object:Gem::Dependency
97
- name: nokogiri
111
+ name: lownode
98
112
  requirement: !ruby/object:Gem::Requirement
99
113
  requirements:
100
114
  - - ">="
@@ -167,6 +181,7 @@ files:
167
181
  - lib/nodes/endpoint_node.rb
168
182
  - lib/nodes/list_node.rb
169
183
  - lib/raindown.rb
184
+ - lib/raindown/actions.rb
170
185
  - lib/raindown/bare_directives.rb
171
186
  - lib/raindown/chart/bars.rb
172
187
  - lib/raindown/chart/chart.rb
@@ -180,6 +195,7 @@ files:
180
195
  - lib/raindown/code_blocks.rb
181
196
  - lib/raindown/config.rb
182
197
  - lib/raindown/directives.rb
198
+ - lib/raindown/document.rb
183
199
  - lib/raindown/endpoint/curl.rb
184
200
  - lib/raindown/endpoint/endpoint_runner.rb
185
201
  - lib/raindown/files.rb
@@ -193,9 +209,23 @@ files:
193
209
  - lib/raindown/page.rb
194
210
  - lib/raindown/pages.rb
195
211
  - lib/raindown/pre_renderer.rb
212
+ - lib/raindown/project_worker.rb
213
+ - lib/raindown/projects.rb
196
214
  - lib/raindown/rewriter.rb
215
+ - lib/raindown/supervisor.rb
197
216
  - lib/raindown/table_formatter.rb
198
217
  - lib/raindown/toc.rb
218
+ - lib/raindown/tui.rb
219
+ - lib/raindown/tui/commands.rb
220
+ - lib/raindown/tui/file_tree.rb
221
+ - lib/raindown/tui/input.rb
222
+ - lib/raindown/tui/layout.rb
223
+ - lib/raindown/tui/navigator.rb
224
+ - lib/raindown/tui/outline.rb
225
+ - lib/raindown/tui/pane.rb
226
+ - lib/raindown/tui/row.rb
227
+ - lib/raindown/tui/screen.rb
228
+ - lib/raindown/tui/styles.rb
199
229
  - lib/raindown/variables.rb
200
230
  - lib/raindown/watcher.rb
201
231
  - lib/version.rb