raindown 0.6.0 → 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/document.rb +1 -5
- data/lib/raindown/endpoint/curl.rb +11 -7
- data/lib/raindown/node_blocks.rb +9 -6
- data/lib/raindown/nodes.rb +1 -0
- data/lib/raindown/stat.rb +53 -0
- data/lib/raindown/tui/events.rb +16 -1
- data/lib/raindown/tui/feelings/batch.rb +47 -0
- data/lib/raindown/tui/feelings/bins.rb +67 -0
- data/lib/raindown/tui/feelings/celebration.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 +6 -5
- data/lib/raindown/tui/highlighter.rb +45 -0
- data/lib/raindown/tui/layout.rb +14 -7
- data/lib/raindown/tui/menu_overlay.rb +1 -1
- data/lib/raindown/tui/navigator/commands.rb +14 -1
- data/lib/raindown/tui/navigator/navigator.rb +42 -9
- data/lib/raindown/tui/navigator/search.rb +126 -0
- data/lib/raindown/tui/navigator/views.rb +6 -3
- data/lib/raindown/tui/renderers/dashboard_renderer.rb +4 -4
- data/lib/raindown/tui/renderers/endpoint_renderer.rb +14 -10
- data/lib/raindown/tui/renderers/outline_renderer.rb +4 -4
- data/lib/raindown/tui/renderers/renderer.rb +3 -0
- data/lib/raindown/tui/row.rb +4 -2
- data/lib/raindown/tui/rows.rb +2 -1
- data/lib/raindown/tui/search_overlay.rb +73 -0
- data/lib/raindown/tui/terminal/input.rb +42 -5
- data/lib/raindown/tui/terminal/screen.rb +12 -9
- data/lib/raindown/tui/terminal/styles.rb +78 -20
- data/lib/raindown/tui/text.rb +27 -0
- data/lib/raindown/tui/title.rb +1 -1
- data/lib/raindown/tui/views/dashboard_view.rb +6 -4
- data/lib/raindown/tui/views/endpoint_view.rb +2 -0
- data/lib/raindown/tui/views/list_view.rb +2 -0
- data/lib/raindown/tui/views/outline_view.rb +11 -2
- data/lib/raindown/tui/workers/project.rb +6 -6
- data/lib/raindown/tui.rb +2 -0
- data/lib/version.rb +1 -1
- metadata +33 -2
data/lib/raindown/tui/row.rb
CHANGED
|
@@ -5,8 +5,10 @@ module Raindown
|
|
|
5
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
6
|
# Buttons are [label, event] pairs, like ['[Run]', :execute], and a tag is an Endpoint, whose method is before its text.
|
|
7
7
|
# Open is the Navigator's next step on Enter, like an Open of a file, and fold, a block that folds or expands the row.
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# Code is the text highlighted, as [text, style] parts, for a line of code, like a directive's body.
|
|
9
|
+
# Each optional field has a default, which is why initialize takes so many.
|
|
10
|
+
Row = Data.define(:text, :depth, :kind, :target, :buttons, :tag, :open, :fold, :code) do
|
|
11
|
+
def initialize(buttons: [], tag: nil, open: nil, fold: nil, code: nil, **) = super # rubocop:disable Metrics/ParameterLists
|
|
10
12
|
|
|
11
13
|
# A separator is space between a pane's sections, which the cursor skips and clicks don't reach.
|
|
12
14
|
def selectable? = kind != :separator
|
data/lib/raindown/tui/rows.rb
CHANGED
|
@@ -29,9 +29,10 @@ module Raindown
|
|
|
29
29
|
parts.zip(spread(parts.map(&:first), width)).map { |(text, style), part| segment(text, style, width: part, event:) }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
# The row's text, after its tag in the tag's style, unless the row's selected.
|
|
32
|
+
# The row's text, after its tag in the tag's style, or highlighted when it's code, unless the row's selected.
|
|
33
33
|
def row_parts(row, style, selected)
|
|
34
34
|
lead = " #{Text::INDENT * row.depth}"
|
|
35
|
+
return [[lead, style], *row.code] if row.code && !selected
|
|
35
36
|
return [["#{lead}#{row.text}", style]] unless row.tag
|
|
36
37
|
|
|
37
38
|
[["#{lead}#{row.tag.method} ", selected ? style : row.tag.style], [row.text, style]]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'events'
|
|
4
|
+
require_relative 'text'
|
|
5
|
+
|
|
6
|
+
module Raindown
|
|
7
|
+
module TUI
|
|
8
|
+
# The search, drawn over the screen's lines in a box below the title: what's typed, then the places that match it,
|
|
9
|
+
# each with where it is, and the selected one highlighted:
|
|
10
|
+
# ┌─ Teleport ───────────────────────────────┐
|
|
11
|
+
# │ / stp▏ │
|
|
12
|
+
# │ ## Setup guides/setup.md:3 │
|
|
13
|
+
# │ ## Steps notes.md:12 │
|
|
14
|
+
# └──────────────────────────────────────────┘
|
|
15
|
+
# Each place is clicked to open it.
|
|
16
|
+
class SearchOverlay
|
|
17
|
+
include Text
|
|
18
|
+
|
|
19
|
+
# The widest the box is, with its border.
|
|
20
|
+
MAX_WIDTH = 72
|
|
21
|
+
|
|
22
|
+
def initialize(navigator, width:)
|
|
23
|
+
@navigator = navigator
|
|
24
|
+
@box_width = [width - 4, MAX_WIDTH].min
|
|
25
|
+
@column = (width - @box_width) / 2
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The lines with the box drawn over them, from the line below the title.
|
|
29
|
+
def apply(lines)
|
|
30
|
+
box.each_with_index do |segments, offset|
|
|
31
|
+
row = offset + 1
|
|
32
|
+
lines[row] = overlay_segments(lines[row], segments, column: @column) if lines[row]
|
|
33
|
+
end
|
|
34
|
+
lines
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# The columns inside the border, "│ " and " │".
|
|
40
|
+
def inner = @box_width - 4
|
|
41
|
+
|
|
42
|
+
def box
|
|
43
|
+
[edge('┌─ Teleport ', '┐'), boxed(segment("/ #{@navigator.query}▏", :menu, width: inner)), *results, edge('└', '┘')]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def results
|
|
47
|
+
matches = @navigator.matches
|
|
48
|
+
return [boxed(segment('No matches', :info, width: inner))] if matches.empty?
|
|
49
|
+
|
|
50
|
+
matches.each_with_index.map { |place, index| place_line(place, index) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The place's text, then where it is, on the right, both highlighted when it's selected.
|
|
54
|
+
def place_line(place, index)
|
|
55
|
+
text_style = :menu
|
|
56
|
+
where_style = :body
|
|
57
|
+
text_style = where_style = :selected if @navigator.match == index
|
|
58
|
+
event = Click.new(pane: :search, index:, action: :enter)
|
|
59
|
+
where = " #{place.where}"
|
|
60
|
+
where_width = [display_width(where), inner / 2].min
|
|
61
|
+
|
|
62
|
+
boxed(segment(place.text, text_style, width: inner - where_width, event:),
|
|
63
|
+
segment(where, where_style, width: where_width, event:))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def boxed(*segments) = [segment('│ ', :border, width: 2), *segments, segment(' │', :border, width: 2)]
|
|
67
|
+
|
|
68
|
+
def edge(left, right)
|
|
69
|
+
[segment("#{left}#{'─' * (@box_width - display_width(left) - 1)}#{right}", :border, width: @box_width)]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'curses'
|
|
4
4
|
require 'unicode/display_width'
|
|
5
5
|
|
|
6
|
+
require_relative '../events'
|
|
7
|
+
|
|
6
8
|
module Raindown
|
|
7
9
|
module TUI
|
|
8
10
|
# Turns curses' keys and clicks into a Navigator's events.
|
|
@@ -12,25 +14,60 @@ module Raindown
|
|
|
12
14
|
Curses::KEY_LEFT => :left, 'h' => :left, Curses::KEY_RIGHT => :right, 'l' => :right,
|
|
13
15
|
"\t" => :tab, Curses::KEY_BTAB => :tab, "\r" => :enter, "\n" => :enter, Curses::KEY_ENTER => :enter,
|
|
14
16
|
' ' => :toggle, 'x' => :execute, 'e' => :edit, 'w' => :write_back, 'r' => :reload, 'd' => :dashboard,
|
|
15
|
-
'p' => :projects, "\
|
|
17
|
+
'p' => :projects, 'f' => :feelings, "\x14" => :celebrate, '/' => :search,
|
|
18
|
+
'1' => :bin1, '2' => :bin2, '3' => :bin3, '4' => :bin4, '5' => :bin5, '6' => :bin6, "\e" => :escape, 'q' => :quit
|
|
19
|
+
}.freeze
|
|
20
|
+
# While typing, like in the search, printable characters are typed, and only these keys do anything else.
|
|
21
|
+
TYPING_KEYS = {
|
|
22
|
+
Curses::KEY_UP => :up, Curses::KEY_DOWN => :down, "\r" => :enter, "\n" => :enter, Curses::KEY_ENTER => :enter,
|
|
23
|
+
"\e" => :escape, Curses::KEY_BACKSPACE => :backspace, "\x7F" => :backspace, "\b" => :backspace
|
|
16
24
|
}.freeze
|
|
17
25
|
|
|
18
26
|
module_function
|
|
19
27
|
|
|
20
28
|
# A key from getch, which is a string for a printable character and otherwise a number.
|
|
21
|
-
# Letters are the same with Shift, as the buttons show them as capitals.
|
|
22
|
-
def key_event(key)
|
|
29
|
+
# Letters are the same with Shift, as the buttons show them as capitals, unless they're being typed.
|
|
30
|
+
def key_event(key, typing: false)
|
|
31
|
+
return typing_event(key) if typing
|
|
32
|
+
|
|
23
33
|
key = key.chr if key.is_a?(Integer) && key < 256
|
|
24
34
|
KEYS[key.is_a?(String) ? key.downcase : key]
|
|
25
35
|
end
|
|
26
36
|
|
|
37
|
+
def typing_event(key)
|
|
38
|
+
key = key.chr if key.is_a?(Integer) && key < 256
|
|
39
|
+
TYPING_KEYS.fetch(key) { Type.new(text: key) if key.is_a?(String) && key.match?(/\A[[:print:]]\z/) }
|
|
40
|
+
end
|
|
41
|
+
|
|
27
42
|
# The mouse buttons that click, which are the left button's. Scrolling does nothing, and since older ncurses,
|
|
28
43
|
# like macOS's, has no button for scrolling down, it can't be mistaken for a click.
|
|
29
44
|
CLICKS = Curses::BUTTON1_PRESSED | Curses::BUTTON1_CLICKED
|
|
30
45
|
|
|
31
|
-
#
|
|
46
|
+
# Moving the mouse, which terminals report once they're asked to, with any motion tracking.
|
|
47
|
+
MOTION = Curses::REPORT_MOUSE_POSITION
|
|
48
|
+
ANY_MOTION = "\e[?1003h"
|
|
49
|
+
NO_MOTION = "\e[?1003l"
|
|
50
|
+
|
|
51
|
+
# The terminal sends the app its mouse events, clicks and moves, so scrolling doesn't scroll the terminal either.
|
|
52
|
+
# ncurses only asks some terminals to report every move, so it's asked directly too, until untrack_mouse.
|
|
53
|
+
def track_mouse
|
|
54
|
+
Curses.mousemask(CLICKS | MOTION)
|
|
55
|
+
Curses.mouseinterval(0)
|
|
56
|
+
$stdout.write(ANY_MOTION)
|
|
57
|
+
$stdout.flush
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def untrack_mouse
|
|
61
|
+
$stdout.write(NO_MOTION)
|
|
62
|
+
$stdout.flush
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A click sends the event of what's under it, and moving the mouse a Hover of it.
|
|
32
66
|
def mouse_event(event, zones)
|
|
33
|
-
click(zones, column: event.x, row: event.y)
|
|
67
|
+
target = click(zones, column: event.x, row: event.y)
|
|
68
|
+
return target if event.bstate.anybits?(CLICKS)
|
|
69
|
+
|
|
70
|
+
Hover.new(target:) if event.bstate.anybits?(MOTION)
|
|
34
71
|
end
|
|
35
72
|
|
|
36
73
|
# Where each of a Layout's segments with an event is on the screen, as [row, columns, event].
|
|
@@ -13,8 +13,9 @@ module Raindown
|
|
|
13
13
|
# Paints a Navigator's Layout with curses, and sends the Navigator the events of keys and clicks, read with Input.
|
|
14
14
|
# Only this class draws on the terminal, so another library could draw the same Layout.
|
|
15
15
|
class Screen
|
|
16
|
-
# How often, in milliseconds, the open file is checked for changes while no key is pressed
|
|
17
|
-
|
|
16
|
+
# How often, in milliseconds, the open file is checked for changes while no key is pressed, or, while the view
|
|
17
|
+
# animates, like Code feelings' rain, it's drawn.
|
|
18
|
+
TICKS = { false => 500, true => 60 }.freeze
|
|
18
19
|
|
|
19
20
|
def initialize(navigator)
|
|
20
21
|
@navigator = navigator
|
|
@@ -61,13 +62,11 @@ module Raindown
|
|
|
61
62
|
Curses.nonl
|
|
62
63
|
Curses.curs_set(0)
|
|
63
64
|
Curses.stdscr.keypad(true)
|
|
64
|
-
|
|
65
|
-
# The terminal sends the app its mouse events, so scrolling doesn't scroll the terminal either.
|
|
66
|
-
Curses.mousemask(Input::CLICKS)
|
|
67
|
-
Curses.mouseinterval(0)
|
|
65
|
+
Input.track_mouse
|
|
68
66
|
end
|
|
69
67
|
|
|
70
68
|
def stop
|
|
69
|
+
Input.untrack_mouse
|
|
71
70
|
Curses.close_screen
|
|
72
71
|
end
|
|
73
72
|
|
|
@@ -98,7 +97,7 @@ module Raindown
|
|
|
98
97
|
end
|
|
99
98
|
|
|
100
99
|
def paint(window, segment)
|
|
101
|
-
window.attron(@styles
|
|
100
|
+
window.attron(@styles[segment.style]) { window.addstr(segment.text) }
|
|
102
101
|
end
|
|
103
102
|
|
|
104
103
|
def last_line(segments)
|
|
@@ -108,12 +107,13 @@ module Raindown
|
|
|
108
107
|
|
|
109
108
|
# The next event, or :tick when no key is pressed in time, or nil for keys without one, like a resize.
|
|
110
109
|
def read
|
|
110
|
+
Curses.stdscr.timeout = TICKS.fetch(@navigator.animating?)
|
|
111
111
|
key = Curses.stdscr.getch
|
|
112
112
|
return :tick if key.nil?
|
|
113
113
|
return mouse if key == Curses::KEY_MOUSE
|
|
114
114
|
|
|
115
115
|
Curses.stdscr.clear if key == Curses::KEY_RESIZE
|
|
116
|
-
Input.key_event(key)
|
|
116
|
+
Input.key_event(key, typing: @navigator.typing?)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
# The curses gem warns the first time it makes a MouseEvent, that it's "undefining the allocator" of the class.
|
|
@@ -131,13 +131,16 @@ module Raindown
|
|
|
131
131
|
window.refresh
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
# Leaves the screen to the command, like the editor, and comes back to it after.
|
|
134
|
+
# Leaves the screen to the command, like the editor, and comes back to it after. The command isn't sent the
|
|
135
|
+
# mouse's moves meanwhile.
|
|
135
136
|
def suspend
|
|
136
137
|
Curses.def_prog_mode
|
|
138
|
+
Input.untrack_mouse
|
|
137
139
|
Curses.close_screen
|
|
138
140
|
yield
|
|
139
141
|
ensure
|
|
140
142
|
Curses.reset_prog_mode
|
|
143
|
+
Input.track_mouse
|
|
141
144
|
Curses.stdscr.clear
|
|
142
145
|
Curses.refresh
|
|
143
146
|
end
|
|
@@ -4,22 +4,47 @@ require 'curses'
|
|
|
4
4
|
|
|
5
5
|
module Raindown
|
|
6
6
|
module TUI
|
|
7
|
-
# The curses attributes of a Layout's styles.
|
|
8
|
-
#
|
|
7
|
+
# The curses attributes of a Layout's styles. Text is periwinkle where there are 256 colours, and the terminal's own
|
|
8
|
+
# colour otherwise, like the line between the panes, while the title and headings are cornflower blue. The selected
|
|
9
|
+
# row is white on blue, or on dark grey in the panes without the focus, while other colours are on the terminal's own
|
|
10
|
+
# background.
|
|
9
11
|
# With only 8 colours, there's no dark grey, so those rows are underlined, and without colours,
|
|
10
12
|
# the rest are in reverse.
|
|
11
13
|
module Styles
|
|
12
|
-
COLORS = {
|
|
13
|
-
|
|
14
|
+
COLORS = { button: Curses::COLOR_YELLOW, error: Curses::COLOR_RED }.freeze
|
|
15
|
+
# Directives, like charts' on the Dashboard, in purple, and the folders, projects and pages in the files, the title,
|
|
16
|
+
# and headings, like the outline's and the Dashboard cards', in cornflower blue, from the 256 colours most terminals
|
|
17
|
+
# have, or magenta and blue with 8.
|
|
18
|
+
ACCENTS = { directive: 141, folder: 111, title: 111, heading: 111 }.freeze
|
|
19
|
+
BASIC_ACCENTS = { directive: Curses::COLOR_MAGENTA, folder: Curses::COLOR_BLUE, title: Curses::COLOR_BLUE,
|
|
20
|
+
heading: Curses::COLOR_BLUE }.freeze
|
|
14
21
|
# The colours of endpoints' HTTP methods, like GET in green, from the 256 colours most terminals have,
|
|
15
22
|
# which are the same in every theme, unlike the 8 they all do, where green can look yellow.
|
|
16
23
|
METHODS = { get: 40, post: 33, delete: 196, put: 220, patch: 171, head: 44, options: 44 }.freeze
|
|
17
24
|
BASIC_METHODS = { get: Curses::COLOR_GREEN, post: Curses::COLOR_BLUE, delete: Curses::COLOR_RED,
|
|
18
25
|
put: Curses::COLOR_YELLOW, patch: Curses::COLOR_MAGENTA, head: Curses::COLOR_CYAN,
|
|
19
26
|
options: Curses::COLOR_CYAN }.freeze
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
# Highlighted code's colours, by the kind of token, from the 256 colours most terminals have, or the 8 they all do,
|
|
28
|
+
# where comments are dim instead of grey.
|
|
29
|
+
CODE = { code_keyword: 204, code_string: 114, code_number: 179, code_function: 75, code_constant: 117,
|
|
30
|
+
code_comment: 244 }.freeze
|
|
31
|
+
BASIC_CODE = { code_keyword: Curses::COLOR_MAGENTA, code_string: Curses::COLOR_GREEN, code_number: Curses::COLOR_YELLOW,
|
|
32
|
+
code_function: Curses::COLOR_BLUE, code_constant: Curses::COLOR_CYAN, code_comment: Curses::COLOR_WHITE }.freeze
|
|
33
|
+
# Code feelings' bright colours, for its rain and fireworks, from the 256 colours most terminals have, or
|
|
34
|
+
# the 8 they all do.
|
|
35
|
+
BRIGHT = { bright_red: 203, bright_orange: 215, bright_yellow: 227, bright_green: 120, bright_cyan: 87,
|
|
36
|
+
bright_blue: 75, bright_purple: 177, bright_pink: 213 }.freeze
|
|
37
|
+
BASIC_BRIGHT = { bright_red: Curses::COLOR_RED, bright_orange: Curses::COLOR_YELLOW, bright_yellow: Curses::COLOR_YELLOW,
|
|
38
|
+
bright_green: Curses::COLOR_GREEN, bright_cyan: Curses::COLOR_CYAN, bright_blue: Curses::COLOR_BLUE,
|
|
39
|
+
bright_purple: Curses::COLOR_MAGENTA, bright_pink: Curses::COLOR_MAGENTA }.freeze
|
|
40
|
+
# The selected row's blue, from the 256 colours most terminals have, or the 8 they all do.
|
|
41
|
+
BLUES = { selected: 33 }.freeze
|
|
42
|
+
BASIC_BLUES = { selected: Curses::COLOR_CYAN }.freeze
|
|
43
|
+
# Text that has no colour of its own, from the 256 colours most terminals have, or the terminal's own with 8.
|
|
44
|
+
PERIWINKLE = 147
|
|
45
|
+
# Code feelings' text and lines, the Dashboard cards' charts, and code blocks, in lavender, or magenta in 8 colours.
|
|
46
|
+
FEELINGS = { feelings: 183 }.freeze
|
|
47
|
+
BASIC_FEELINGS = { feelings: Curses::COLOR_MAGENTA }.freeze
|
|
23
48
|
BRIGHT_WHITE = 15
|
|
24
49
|
DARK_GREY = 238
|
|
25
50
|
|
|
@@ -33,26 +58,57 @@ module Raindown
|
|
|
33
58
|
colored
|
|
34
59
|
end
|
|
35
60
|
|
|
61
|
+
# Without colours, a style that isn't here is the terminal's own.
|
|
36
62
|
def plain
|
|
37
|
-
|
|
63
|
+
Hash.new(Curses::A_NORMAL).merge(
|
|
64
|
+
title: Curses::A_REVERSE | Curses::A_BOLD, selected: Curses::A_REVERSE, selected_button: Curses::A_REVERSE,
|
|
38
65
|
cursor: Curses::A_UNDERLINE, cursor_button: Curses::A_UNDERLINE | Curses::A_BOLD, heading: Curses::A_BOLD,
|
|
39
|
-
body: Curses::A_DIM, error: Curses::A_BOLD, button: Curses::A_BOLD, folder: Curses::A_BOLD, project: Curses::A_BOLD,
|
|
40
|
-
key: Curses::A_REVERSE, border: Curses::A_DIM,
|
|
41
|
-
selected_divider: Curses::A_REVERSE, cursor_divider: Curses::A_BOLD
|
|
66
|
+
body: Curses::A_DIM, error: Curses::A_BOLD, button: Curses::A_BOLD, folder: Curses::A_BOLD, project: Curses::A_BOLD, page: Curses::A_BOLD,
|
|
67
|
+
key: Curses::A_REVERSE, border: Curses::A_DIM, divider: Curses::A_BOLD,
|
|
68
|
+
selected_divider: Curses::A_REVERSE, cursor_divider: Curses::A_BOLD,
|
|
69
|
+
code_keyword: Curses::A_BOLD, code_comment: Curses::A_DIM
|
|
70
|
+
)
|
|
42
71
|
end
|
|
43
72
|
|
|
44
73
|
def colored
|
|
45
|
-
|
|
74
|
+
in_text_color.merge(
|
|
46
75
|
title: pair(:title) | Curses::A_BOLD, selected: pair(:selected) | white_bold,
|
|
47
76
|
selected_button: pair(:selected_button) | Curses::A_BOLD, directive: pair(:directive),
|
|
48
|
-
error: pair(:error) | Curses::A_BOLD, button: pair(:button) | Curses::A_BOLD,
|
|
49
|
-
|
|
77
|
+
error: pair(:error) | Curses::A_BOLD, button: pair(:button) | Curses::A_BOLD, **emphasis,
|
|
78
|
+
**dividers, **code_tokens, **bright, **files, **unfocused
|
|
50
79
|
)
|
|
51
80
|
end
|
|
52
81
|
|
|
82
|
+
# The styles without a colour of their own, or that are only bold, dim or the like, in the text's colour, which is
|
|
83
|
+
# also any style that isn't here.
|
|
84
|
+
def in_text_color
|
|
85
|
+
text = pair(:text)
|
|
86
|
+
Hash.new(text).merge(plain.transform_values { it | text })
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Headings, and Code feelings' text and lines, whose lavender the Dashboard cards' charts and code
|
|
90
|
+
# blocks are too, apart from highlighted code's keywords, strings and the like.
|
|
91
|
+
def emphasis
|
|
92
|
+
lavender = pair(:feelings)
|
|
93
|
+
{ heading: pair(:heading) | Curses::A_BOLD, feelings: lavender, chart: lavender, code: lavender }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Highlighted code's tokens, like keywords and comments, with comments dim in 8 colours, where there's no grey.
|
|
97
|
+
def code_tokens
|
|
98
|
+
tokens = CODE.keys.to_h { [it, pair(it)] }
|
|
99
|
+
tokens[:code_comment] |= Curses::A_DIM if Curses.colors < 256
|
|
100
|
+
tokens
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The bright colours, bold, so they're bright in 8 colours too.
|
|
104
|
+
def bright = BRIGHT.keys.to_h { [it, pair(it) | Curses::A_BOLD] }
|
|
105
|
+
|
|
106
|
+
# The line between the panes, and where it meets a selected row.
|
|
107
|
+
def dividers = { divider: pair(:divider), selected_divider: pair(:selected_divider) }
|
|
108
|
+
|
|
53
109
|
def files
|
|
54
|
-
{ folder: pair(:folder) | Curses::A_BOLD, project: pair(:
|
|
55
|
-
|
|
110
|
+
{ folder: pair(:folder) | Curses::A_BOLD, project: pair(:folder) | Curses::A_BOLD,
|
|
111
|
+
page: pair(:folder) | Curses::A_BOLD, **METHODS.keys.to_h { [:"method_#{it}", pair(:"method_#{it}") | Curses::A_BOLD] } }
|
|
56
112
|
end
|
|
57
113
|
|
|
58
114
|
# The selected rows of the panes without the focus, on dark grey where there's one.
|
|
@@ -67,14 +123,16 @@ module Raindown
|
|
|
67
123
|
many = Curses.colors >= 256
|
|
68
124
|
blues = many ? BLUES : BASIC_BLUES
|
|
69
125
|
white = many ? BRIGHT_WHITE : Curses::COLOR_WHITE
|
|
126
|
+
text = many ? PERIWINKLE : -1
|
|
70
127
|
|
|
71
128
|
[*COLORS.map { |name, color| [name, color, -1] },
|
|
72
129
|
*(many ? METHODS : BASIC_METHODS).map { |name, color| [:"method_#{name}", color, -1] },
|
|
73
|
-
|
|
130
|
+
*(many ? CODE.merge(BRIGHT, FEELINGS, ACCENTS) : BASIC_CODE.merge(BASIC_BRIGHT, BASIC_FEELINGS, BASIC_ACCENTS))
|
|
131
|
+
.map { |name, color| [name, color, -1] },
|
|
132
|
+
[:text, text, -1],
|
|
74
133
|
[:selected, white, blues[:selected]], [:selected_button, Curses::COLOR_YELLOW, blues[:selected]],
|
|
75
|
-
[:cursor, white, DARK_GREY], [:cursor_button, Curses::COLOR_YELLOW, DARK_GREY], [:divider,
|
|
76
|
-
[:selected_divider,
|
|
77
|
-
[:run, white, Curses::COLOR_GREEN]]
|
|
134
|
+
[:cursor, white, DARK_GREY], [:cursor_button, Curses::COLOR_YELLOW, DARK_GREY], [:divider, text, -1],
|
|
135
|
+
[:selected_divider, text, blues[:selected]], [:cursor_divider, text, DARK_GREY]]
|
|
78
136
|
end
|
|
79
137
|
|
|
80
138
|
def pair(name)
|
data/lib/raindown/tui/text.rb
CHANGED
|
@@ -50,6 +50,33 @@ module Raindown
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# A line of highlighted code's [text, style] parts as segments, cut or padded to the width.
|
|
54
|
+
def code_segments(parts, width, event: nil)
|
|
55
|
+
segments = segments_between(parts.map { |text, style| Segment.new(text: printable(text), style:, event:) }, 0, width)
|
|
56
|
+
[*segments, segment('', :code, width: width - segments.sum { display_width(it.text) }, event:)]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The text in lines that fit the width, broken at spaces where it can, and inside a word too long for a line.
|
|
60
|
+
def wrap(text, width)
|
|
61
|
+
return [text] unless width.positive?
|
|
62
|
+
|
|
63
|
+
text.scan(/\S+|\s+/).each_with_object([+'']) do |part, lines|
|
|
64
|
+
next lines.last << part if display_width(lines.last + part) <= width
|
|
65
|
+
next lines << +'' if part.strip.empty?
|
|
66
|
+
|
|
67
|
+
lines << +'' unless lines.last.empty?
|
|
68
|
+
break_word(part, width, lines)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Adds a word too long for a line to the lines, a character at a time, starting a new line when one's full.
|
|
73
|
+
def break_word(word, width, lines)
|
|
74
|
+
word.each_char do |char|
|
|
75
|
+
lines << +'' if display_width(lines.last + char) > width
|
|
76
|
+
lines.last << char
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
53
80
|
# The line's segments, with others drawn over its columns from the column on, like a box over the screen.
|
|
54
81
|
def overlay_segments(line, segments, column:)
|
|
55
82
|
width = segments.sum { display_width(it.text) }
|
data/lib/raindown/tui/title.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Raindown
|
|
|
6
6
|
module Title
|
|
7
7
|
# The rain cloud without the emoji variation selector, like the nav's projects, which terminals draw one column wide,
|
|
8
8
|
# as it's measured, so the title line reaches the edge.
|
|
9
|
-
NAME = '🌧
|
|
9
|
+
NAME = '🌧 Raindown'
|
|
10
10
|
|
|
11
11
|
private
|
|
12
12
|
|
|
@@ -5,9 +5,9 @@ require_relative '../events'
|
|
|
5
5
|
module Raindown
|
|
6
6
|
module TUI
|
|
7
7
|
# Every chart in the projects' Markdown files, drawn from its data by its project's Worker, as cards in a grid
|
|
8
|
-
# with a cursor. A chart is a chart: directive, or a project's own node that's a Raindown::Chart,
|
|
9
|
-
#
|
|
10
|
-
# like a CSV file, without a save.
|
|
8
|
+
# with a cursor. A chart is a chart: directive, or a project's own node that's a Raindown::Chart, like
|
|
9
|
+
# "Sales < Raindown::Chart", or a Raindown::Stat, one big number, like "CodeDebt < Raindown::Stat". Charts are drawn
|
|
10
|
+
# again every few seconds, so they follow changes to their data, like a CSV file, without a save.
|
|
11
11
|
# A key's next step, for the Navigator, is nil, or an Open or Edit of a chart's directive.
|
|
12
12
|
class DashboardView
|
|
13
13
|
# Seconds between drawing the charts again.
|
|
@@ -31,6 +31,8 @@ module Raindown
|
|
|
31
31
|
def target = self
|
|
32
32
|
def path = nil
|
|
33
33
|
def changed? = false
|
|
34
|
+
def typing? = false
|
|
35
|
+
def animating? = false
|
|
34
36
|
|
|
35
37
|
def status(_navigator)
|
|
36
38
|
found = "#{charts.length} #{charts.length == 1 ? 'chart' : 'charts'} in #{File.basename(project || root)}"
|
|
@@ -45,7 +47,7 @@ module Raindown
|
|
|
45
47
|
|
|
46
48
|
# The charts, as each project's Worker drew them, of the project they're narrowed to, if any.
|
|
47
49
|
def charts
|
|
48
|
-
project ? @cards.
|
|
50
|
+
project ? @cards.filter { it.project == project } : @cards
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
# Narrows the charts to a project's, or with nil, shows every project's.
|
|
@@ -26,6 +26,8 @@ module Raindown
|
|
|
26
26
|
def path = endpoint.path
|
|
27
27
|
def status(_navigator) = [:info, endpoint.text]
|
|
28
28
|
def changed? = false
|
|
29
|
+
def typing? = false
|
|
30
|
+
def animating? = false
|
|
29
31
|
def next_step?(key) = %i[up down enter edit execute].include?(key)
|
|
30
32
|
|
|
31
33
|
# Clicks on the response only move the focus to it.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative '../../document'
|
|
4
4
|
require_relative '../events'
|
|
5
5
|
require_relative 'list_view'
|
|
6
|
+
require_relative '../highlighter'
|
|
6
7
|
require_relative '../row'
|
|
7
8
|
require_relative '../workers/worker'
|
|
8
9
|
|
|
@@ -34,6 +35,7 @@ module Raindown
|
|
|
34
35
|
@lines = outline.lines
|
|
35
36
|
@headings = outline.headings
|
|
36
37
|
@directives = outline.directives
|
|
38
|
+
@highlighted = {}
|
|
37
39
|
@error = nil
|
|
38
40
|
rescue Worker::Error, SystemCallError => e
|
|
39
41
|
@error = e.message
|
|
@@ -104,11 +106,18 @@ module Raindown
|
|
|
104
106
|
buttons: buttons(directive, expanded:), **directive_action(directive))
|
|
105
107
|
return [row] unless expanded
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
code = highlighted(directive)
|
|
110
|
+
[row, *directive.body.each_with_index.map do |number, index|
|
|
111
|
+
Row.new(text: @lines[number - 1].to_s, depth: depth + 1, kind: :body, target: number, open: Edit.new(path:, line: number),
|
|
112
|
+
code: code[index])
|
|
109
113
|
end]
|
|
110
114
|
end
|
|
111
115
|
|
|
116
|
+
# The directive's body highlighted as Markdown, which highlights its code blocks in their languages, once a read.
|
|
117
|
+
def highlighted(directive)
|
|
118
|
+
@highlighted[directive.line] ||= Highlighter.lines(directive.body.map { @lines[it - 1].to_s }, language: 'markdown')
|
|
119
|
+
end
|
|
120
|
+
|
|
112
121
|
# A directive with a body expands it, and one without opens in the editor, like a line of its body.
|
|
113
122
|
def directive_action(directive)
|
|
114
123
|
if directive.body then { fold: -> { expand(directive) } }
|
|
@@ -9,6 +9,7 @@ require_relative '../../node_blocks'
|
|
|
9
9
|
require_relative '../../nodes'
|
|
10
10
|
require_relative '../../pages'
|
|
11
11
|
require_relative '../../projects'
|
|
12
|
+
require_relative '../../stat'
|
|
12
13
|
require_relative '../card'
|
|
13
14
|
require_relative '../endpoint'
|
|
14
15
|
require_relative '../../code_blocks'
|
|
@@ -39,7 +40,8 @@ module Raindown
|
|
|
39
40
|
Outline.new(lines: File.readlines(path, chomp: true), headings: document.headings, directives: document.directives)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
# Every chart in the project's files, drawn from its data, and the files
|
|
43
|
+
# Every chart and stat in the project's files, or any node that draws a card, drawn from its data, and the files
|
|
44
|
+
# that couldn't be read.
|
|
43
45
|
# The charts are found again when asked, and otherwise drawn again, so they follow changes to their data.
|
|
44
46
|
def charts(find: false)
|
|
45
47
|
if find || @charts.nil?
|
|
@@ -87,7 +89,7 @@ module Raindown
|
|
|
87
89
|
headings = Document.parse(content, bare:).headings
|
|
88
90
|
|
|
89
91
|
NodeBlocks.find(lines, front_matter: FrontMatter.parse(content).first, bare:)
|
|
90
|
-
.select {
|
|
92
|
+
.select { it[:node].respond_to?(:card_lines) }
|
|
91
93
|
.map { |block| chart(block, path:, lines:, headings:) }
|
|
92
94
|
rescue StandardError => e
|
|
93
95
|
@file_errors << "#{path.delete_prefix(File.join(root, ''))}: #{e.message}"
|
|
@@ -109,11 +111,9 @@ module Raindown
|
|
|
109
111
|
{ path:, line:, heading: headings.select { it.line < line }.last&.text, directive:, node: block[:node] }
|
|
110
112
|
end
|
|
111
113
|
|
|
112
|
-
# Draws the chart as its Markdown would be written back, with the files its project lists.
|
|
114
|
+
# Draws the chart or stat as its Markdown would be written back, with the files its project lists.
|
|
113
115
|
def lines(chart, metadata)
|
|
114
|
-
|
|
115
|
-
points = node.points(metadata:)
|
|
116
|
-
points.empty? ? ['No data'] : node.draw(points).map(&:rstrip)
|
|
116
|
+
chart[:node].card_lines(metadata:)
|
|
117
117
|
rescue StandardError => e
|
|
118
118
|
["Can't chart: #{e.message}"]
|
|
119
119
|
end
|
data/lib/raindown/tui.rb
CHANGED
|
@@ -22,6 +22,8 @@ module Raindown
|
|
|
22
22
|
# so two projects' nodes of the same name stay apart.
|
|
23
23
|
workers = Workers.new(root:)
|
|
24
24
|
|
|
25
|
+
# Rouge loads here, rather than with Raindown, which doesn't highlight code outside the TUI.
|
|
26
|
+
require 'rouge'
|
|
25
27
|
Screen.new(Navigator.new(root:, path: (path unless File.directory?(path)), workers:)).run
|
|
26
28
|
ensure
|
|
27
29
|
workers&.stop
|
data/lib/version.rb
CHANGED