marvi 0.1.3 → 0.3.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/.claude/hooks/standardrb-check.sh +23 -0
- data/.claude/settings.json +17 -0
- data/CHANGELOG.md +5 -0
- data/lib/marvi/ansi.rb +7 -7
- data/lib/marvi/ast_walker.rb +93 -21
- data/lib/marvi/renderer/ansi.rb +13 -5
- data/lib/marvi/renderer/curses.rb +107 -48
- data/lib/marvi/version.rb +1 -1
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c5df9bb69ae060cf562d656a78f4aa152223a4ad58178af6b6b18891e19cb525
|
|
4
|
+
data.tar.gz: 1590ce11f925c30f25e256e1e0f0571355a4a4fd7690cc636310d9bf20e349eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1541589372fbdd04e4dbf7fe929de77788f7308c4abdf31ed8fea657712de058d1e38f7a48000c1122405b0c8a44f679b57946937ed8c779dcc81e2e7f4cfd10
|
|
7
|
+
data.tar.gz: 8637f420f782945d396f8dfa97ec3f4b748ffd056bb43baf23c6e6adbd54c6e8d9f8b3dbe896a1540faf46c6affa772cb7637bc929a9c9fe60e626c0420392e3
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PostToolUse hook: run standardrb on the edited file and block the turn
|
|
3
|
+
# when violations remain so Claude must fix them before continuing.
|
|
4
|
+
|
|
5
|
+
set -u
|
|
6
|
+
|
|
7
|
+
payload="$(cat)"
|
|
8
|
+
file_path=$(printf '%s' "$payload" | jq -r '.tool_response.filePath // .tool_input.file_path // empty')
|
|
9
|
+
[ -z "$file_path" ] && exit 0
|
|
10
|
+
|
|
11
|
+
case "$file_path" in
|
|
12
|
+
"$PWD"/*.rb | "$PWD"/*.gemspec | "$PWD"/Rakefile | "$PWD"/Gemfile) ;;
|
|
13
|
+
*) exit 0 ;;
|
|
14
|
+
esac
|
|
15
|
+
|
|
16
|
+
output=$(bundle exec standardrb "$file_path" 2>&1)
|
|
17
|
+
if [ $? -ne 0 ]; then
|
|
18
|
+
reason="standardrb violations in $file_path. Fix manually or run \`bundle exec rake standard:fix\`:
|
|
19
|
+
|
|
20
|
+
$output"
|
|
21
|
+
jq -n --arg reason "$reason" '{decision:"block",reason:$reason}'
|
|
22
|
+
fi
|
|
23
|
+
exit 0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-05-18
|
|
4
|
+
|
|
5
|
+
- Render tables correctly when cells contain East Asian wide characters and emoji (uses `unicode-display_width`).
|
|
6
|
+
- Wrap long table cells to fit the terminal width so borders no longer break across physical lines; curses pager re-flows on window resize.
|
|
7
|
+
|
|
3
8
|
## [0.1.0] - 2026-03-17
|
|
4
9
|
|
|
5
10
|
- Initial release
|
data/lib/marvi/ansi.rb
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Marvi
|
|
4
4
|
module ANSI
|
|
5
|
-
RESET
|
|
6
|
-
BOLD
|
|
7
|
-
ITALIC
|
|
8
|
-
CYAN
|
|
9
|
-
YELLOW
|
|
10
|
-
GREEN
|
|
5
|
+
RESET = "\e[0m"
|
|
6
|
+
BOLD = "\e[1m"
|
|
7
|
+
ITALIC = "\e[3m"
|
|
8
|
+
CYAN = "\e[36m"
|
|
9
|
+
YELLOW = "\e[33m"
|
|
10
|
+
GREEN = "\e[32m"
|
|
11
11
|
MAGENTA = "\e[35m"
|
|
12
|
-
WHITE
|
|
12
|
+
WHITE = "\e[37m"
|
|
13
13
|
BG_DARK = "\e[48;5;236m"
|
|
14
14
|
|
|
15
15
|
HEADER_COLORS = [CYAN, GREEN, YELLOW, MAGENTA, WHITE, WHITE].freeze
|
data/lib/marvi/ast_walker.rb
CHANGED
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require "kramdown"
|
|
4
4
|
require "kramdown-parser-gfm"
|
|
5
|
+
require "unicode/display_width"
|
|
5
6
|
|
|
6
7
|
module Marvi
|
|
7
8
|
class ASTWalker
|
|
8
9
|
HEADER_COLORS = %i[cyan green yellow magenta white white].freeze
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
DEFAULT_MAX_WIDTH = 80
|
|
12
|
+
MIN_COL_WIDTH = 4
|
|
13
|
+
|
|
14
|
+
def walk(markdown, max_width: nil)
|
|
15
|
+
@max_width = max_width || Integer(ENV["COLUMNS"] || DEFAULT_MAX_WIDTH)
|
|
11
16
|
doc = Kramdown::Document.new(markdown, input: "GFM")
|
|
12
17
|
lines = render_block(doc.root)
|
|
13
18
|
lines.pop while lines.last&.plain_text&.empty?
|
|
@@ -52,7 +57,7 @@ module Marvi
|
|
|
52
57
|
def render_header(el)
|
|
53
58
|
level = el.options[:level]
|
|
54
59
|
color = HEADER_COLORS[level - 1]
|
|
55
|
-
src
|
|
60
|
+
src = el.options[:location]
|
|
56
61
|
prefix = Span.new(text: "#" * level + " ", bold: true, color: color)
|
|
57
62
|
content = render_inline_children(el).map do |s|
|
|
58
63
|
Span.new(text: s.text, bold: true, italic: s.italic, color: s.color || color, bg_color: s.bg_color)
|
|
@@ -61,10 +66,10 @@ module Marvi
|
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
def render_li(el, indent:, list_type:, list_index:)
|
|
64
|
-
bullet = list_type == :ol ? "#{list_index}." : "•"
|
|
69
|
+
bullet = (list_type == :ol) ? "#{list_index}." : "•"
|
|
65
70
|
prefix = Span.new(text: "#{" " * indent}#{bullet} ", color: :cyan)
|
|
66
|
-
src
|
|
67
|
-
lines
|
|
71
|
+
src = el.options[:location]
|
|
72
|
+
lines = []
|
|
68
73
|
|
|
69
74
|
el.children.each do |child|
|
|
70
75
|
case child.type
|
|
@@ -79,10 +84,10 @@ module Marvi
|
|
|
79
84
|
lines += render_block(child)
|
|
80
85
|
end
|
|
81
86
|
else
|
|
82
|
-
if lines.empty?
|
|
83
|
-
|
|
87
|
+
lines << if lines.empty?
|
|
88
|
+
RichLine.new([prefix] + render_inline(child), source_line: src)
|
|
84
89
|
else
|
|
85
|
-
|
|
90
|
+
RichLine.new(render_inline(child))
|
|
86
91
|
end
|
|
87
92
|
end
|
|
88
93
|
end
|
|
@@ -90,12 +95,14 @@ module Marvi
|
|
|
90
95
|
end
|
|
91
96
|
|
|
92
97
|
def render_codeblock(el)
|
|
93
|
-
src
|
|
98
|
+
src = el.options[:location]
|
|
94
99
|
lang = el.options[:lang]
|
|
95
100
|
lines = []
|
|
96
101
|
lines << RichLine.new([Span.new(text: lang, color: :yellow)], source_line: src) if lang
|
|
97
102
|
el.value.chomp.split("\n").each_with_index do |line, i|
|
|
98
|
-
line_src =
|
|
103
|
+
line_src = if src
|
|
104
|
+
src + i + (lang ? 1 : 0)
|
|
105
|
+
end
|
|
99
106
|
lines << RichLine.new([Span.new(text: " #{line}", color: :green, bg_color: :dark)], source_line: line_src)
|
|
100
107
|
end
|
|
101
108
|
lines << RichLine.blank
|
|
@@ -103,20 +110,21 @@ module Marvi
|
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
def render_blockquote(el)
|
|
106
|
-
inner
|
|
113
|
+
inner = el.children.flat_map { |child| render_block(child) }
|
|
107
114
|
prefix = Span.new(text: "│ ", color: :cyan)
|
|
108
115
|
# preserve source_line from inner lines
|
|
109
116
|
inner.map { |line| RichLine.new([prefix] + line.spans, source_line: line.source_line) } + [RichLine.blank]
|
|
110
117
|
end
|
|
111
118
|
|
|
112
119
|
def render_table(el)
|
|
113
|
-
src
|
|
120
|
+
src = el.options[:location]
|
|
114
121
|
rows = el.children.flat_map(&:children)
|
|
115
122
|
header_row = el.children.find { |s| s.type == :thead }&.children&.first
|
|
116
123
|
|
|
117
124
|
cell_spans = rows.map { |row| row.children.map { |cell| render_inline_children(cell) } }
|
|
118
|
-
|
|
125
|
+
natural_widths = cell_spans.map { |row| row.map { |spans| spans_display_width(spans) } }
|
|
119
126
|
.transpose.map { |col| col.max }
|
|
127
|
+
col_widths = shrink_col_widths(natural_widths, @max_width)
|
|
120
128
|
|
|
121
129
|
lines = []
|
|
122
130
|
top = col_widths.map { |w| "─" * (w + 2) }.join("┬")
|
|
@@ -124,16 +132,23 @@ module Marvi
|
|
|
124
132
|
|
|
125
133
|
rows.each_with_index do |row, ri|
|
|
126
134
|
is_header = row == header_row
|
|
127
|
-
|
|
128
|
-
row.children.each_with_index do |cell, ci|
|
|
135
|
+
wrapped = row.children.each_with_index.map do |_cell, ci|
|
|
129
136
|
content = cell_spans[ri][ci]
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
styled = is_header ? content.map { |s| Span.new(text: s.text, bold: true, italic: s.italic, color: :cyan, bg_color: s.bg_color) } : content
|
|
138
|
+
wrap_spans(styled, col_widths[ci])
|
|
139
|
+
end
|
|
140
|
+
sub_row_count = wrapped.map(&:size).max
|
|
141
|
+
sub_row_count.times do |j|
|
|
142
|
+
row_spans = []
|
|
143
|
+
wrapped.each_with_index do |cell_lines, ci|
|
|
144
|
+
sub_spans = cell_lines[j] || []
|
|
145
|
+
sub_len = spans_display_width(sub_spans)
|
|
146
|
+
padding = col_widths[ci] - sub_len
|
|
147
|
+
row_spans += [Span.new(text: "│ ", color: :cyan)] + sub_spans + [Span.new(text: " " * (padding + 1))]
|
|
148
|
+
end
|
|
149
|
+
row_spans << Span.new(text: "│", color: :cyan)
|
|
150
|
+
lines << RichLine.new(row_spans)
|
|
134
151
|
end
|
|
135
|
-
row_spans << Span.new(text: "│", color: :cyan)
|
|
136
|
-
lines << RichLine.new(row_spans)
|
|
137
152
|
|
|
138
153
|
if is_header
|
|
139
154
|
sep = col_widths.map { |w| "─" * (w + 2) }.join("┼")
|
|
@@ -147,6 +162,63 @@ module Marvi
|
|
|
147
162
|
lines
|
|
148
163
|
end
|
|
149
164
|
|
|
165
|
+
def shrink_col_widths(widths, max_width)
|
|
166
|
+
budget = max_width - (3 * widths.size + 1)
|
|
167
|
+
total = widths.sum
|
|
168
|
+
return widths if total <= budget
|
|
169
|
+
|
|
170
|
+
shrunk = widths.dup
|
|
171
|
+
while total > budget
|
|
172
|
+
max_w, i = shrunk.each_with_index.max_by { |w, _| w }
|
|
173
|
+
break if max_w <= MIN_COL_WIDTH
|
|
174
|
+
shrunk[i] -= 1
|
|
175
|
+
total -= 1
|
|
176
|
+
end
|
|
177
|
+
shrunk
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def wrap_spans(spans, width)
|
|
181
|
+
lines = [[]]
|
|
182
|
+
current_width = 0
|
|
183
|
+
spans.each do |span|
|
|
184
|
+
text = span.text.dup
|
|
185
|
+
until text.empty?
|
|
186
|
+
remaining = width - current_width
|
|
187
|
+
if remaining <= 0
|
|
188
|
+
lines << []
|
|
189
|
+
current_width = 0
|
|
190
|
+
remaining = width
|
|
191
|
+
end
|
|
192
|
+
taken = 0
|
|
193
|
+
chunk_width = 0
|
|
194
|
+
text.each_char do |c|
|
|
195
|
+
cw = Unicode::DisplayWidth.of(c)
|
|
196
|
+
break if chunk_width + cw > remaining
|
|
197
|
+
chunk_width += cw
|
|
198
|
+
taken += c.bytesize
|
|
199
|
+
end
|
|
200
|
+
if taken.zero?
|
|
201
|
+
first_char = text.each_char.first
|
|
202
|
+
taken = first_char.bytesize
|
|
203
|
+
chunk_width = Unicode::DisplayWidth.of(first_char)
|
|
204
|
+
end
|
|
205
|
+
chunk = text.byteslice(0, taken)
|
|
206
|
+
text = text.byteslice(taken..) || ""
|
|
207
|
+
lines.last << Span.new(text: chunk, bold: span.bold, italic: span.italic, color: span.color, bg_color: span.bg_color)
|
|
208
|
+
current_width += chunk_width
|
|
209
|
+
unless text.empty?
|
|
210
|
+
lines << []
|
|
211
|
+
current_width = 0
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
lines
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def spans_display_width(spans)
|
|
219
|
+
spans.sum { |s| Unicode::DisplayWidth.of(s.text) }
|
|
220
|
+
end
|
|
221
|
+
|
|
150
222
|
def render_inline_children(el)
|
|
151
223
|
el.children.flat_map { |child| render_inline(child) }
|
|
152
224
|
end
|
data/lib/marvi/renderer/ansi.rb
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "io/console"
|
|
4
|
+
|
|
3
5
|
module Marvi
|
|
4
6
|
module Renderer
|
|
5
7
|
class ANSI
|
|
6
8
|
COLOR_MAP = {
|
|
7
|
-
cyan:
|
|
8
|
-
green:
|
|
9
|
-
yellow:
|
|
9
|
+
cyan: Marvi::ANSI::CYAN,
|
|
10
|
+
green: Marvi::ANSI::GREEN,
|
|
11
|
+
yellow: Marvi::ANSI::YELLOW,
|
|
10
12
|
magenta: Marvi::ANSI::MAGENTA,
|
|
11
|
-
white:
|
|
13
|
+
white: Marvi::ANSI::WHITE
|
|
12
14
|
}.freeze
|
|
13
15
|
|
|
14
16
|
def render(markdown)
|
|
15
|
-
lines = ASTWalker.new.walk(markdown)
|
|
17
|
+
lines = ASTWalker.new.walk(markdown, max_width: terminal_width)
|
|
16
18
|
lines.map { |line| render_line(line) }.join("\n") + "\n"
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
private
|
|
20
22
|
|
|
23
|
+
def terminal_width
|
|
24
|
+
IO.console&.winsize&.last || Integer(ENV["COLUMNS"] || ASTWalker::DEFAULT_MAX_WIDTH)
|
|
25
|
+
rescue
|
|
26
|
+
ASTWalker::DEFAULT_MAX_WIDTH
|
|
27
|
+
end
|
|
28
|
+
|
|
21
29
|
def render_line(line)
|
|
22
30
|
line.spans.map { |span| render_span(span) }.join
|
|
23
31
|
end
|
|
@@ -7,34 +7,38 @@ module Marvi
|
|
|
7
7
|
module Renderer
|
|
8
8
|
class Curses
|
|
9
9
|
COLOR_PAIRS = {
|
|
10
|
-
cyan:
|
|
11
|
-
green:
|
|
12
|
-
yellow:
|
|
13
|
-
magenta:
|
|
14
|
-
white:
|
|
10
|
+
cyan: 1,
|
|
11
|
+
green: 2,
|
|
12
|
+
yellow: 3,
|
|
13
|
+
magenta: 4,
|
|
14
|
+
white: 5,
|
|
15
15
|
green_on_dark: 6,
|
|
16
|
-
cyan_on_dark:
|
|
16
|
+
cyan_on_dark: 7
|
|
17
17
|
}.freeze
|
|
18
18
|
|
|
19
19
|
ITALIC_ATTR = (defined?(::Curses::A_ITALIC) ? ::Curses::A_ITALIC : 0)
|
|
20
20
|
|
|
21
|
+
FILE_POLL_INTERVAL_MS = 500
|
|
22
|
+
|
|
21
23
|
def render(markdown, file: nil)
|
|
22
|
-
@file
|
|
24
|
+
@file = file
|
|
23
25
|
@markdown = markdown
|
|
24
|
-
@
|
|
25
|
-
|
|
26
|
+
@scroll = 0
|
|
27
|
+
mark_reloaded
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
::Curses.use_default_colors
|
|
30
|
-
::Curses.noecho
|
|
31
|
-
::Curses.cbreak
|
|
32
|
-
::Curses.stdscr.keypad(true)
|
|
33
|
-
setup_colors
|
|
29
|
+
init_curses_state
|
|
30
|
+
rewalk
|
|
34
31
|
draw
|
|
35
32
|
|
|
36
33
|
catch(:quit) do
|
|
37
|
-
loop
|
|
34
|
+
loop do
|
|
35
|
+
key = ::Curses.getch
|
|
36
|
+
if key.nil? || key == -1
|
|
37
|
+
check_file_updated
|
|
38
|
+
else
|
|
39
|
+
handle_key(key)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
38
42
|
end
|
|
39
43
|
ensure
|
|
40
44
|
::Curses.close_screen
|
|
@@ -80,60 +84,107 @@ module Marvi
|
|
|
80
84
|
|
|
81
85
|
output = IO.popen(["infocmp", "-1", term, err: File::NULL], &:read)
|
|
82
86
|
@infocmp_cache[term] = $?.success? ? output : nil
|
|
83
|
-
rescue
|
|
87
|
+
rescue
|
|
84
88
|
@infocmp_cache[term] = nil
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
def setup_colors
|
|
88
|
-
::Curses.init_pair(COLOR_PAIRS[:cyan],
|
|
89
|
-
::Curses.init_pair(COLOR_PAIRS[:green],
|
|
90
|
-
::Curses.init_pair(COLOR_PAIRS[:yellow],
|
|
91
|
-
::Curses.init_pair(COLOR_PAIRS[:magenta],
|
|
92
|
-
::Curses.init_pair(COLOR_PAIRS[:white],
|
|
93
|
-
::Curses.init_pair(COLOR_PAIRS[:green_on_dark], ::Curses::COLOR_GREEN,
|
|
94
|
-
::Curses.init_pair(COLOR_PAIRS[:cyan_on_dark],
|
|
92
|
+
::Curses.init_pair(COLOR_PAIRS[:cyan], ::Curses::COLOR_CYAN, -1)
|
|
93
|
+
::Curses.init_pair(COLOR_PAIRS[:green], ::Curses::COLOR_GREEN, -1)
|
|
94
|
+
::Curses.init_pair(COLOR_PAIRS[:yellow], ::Curses::COLOR_YELLOW, -1)
|
|
95
|
+
::Curses.init_pair(COLOR_PAIRS[:magenta], ::Curses::COLOR_MAGENTA, -1)
|
|
96
|
+
::Curses.init_pair(COLOR_PAIRS[:white], ::Curses::COLOR_WHITE, -1)
|
|
97
|
+
::Curses.init_pair(COLOR_PAIRS[:green_on_dark], ::Curses::COLOR_GREEN, ::Curses::COLOR_BLACK)
|
|
98
|
+
::Curses.init_pair(COLOR_PAIRS[:cyan_on_dark], ::Curses::COLOR_CYAN, ::Curses::COLOR_BLACK)
|
|
95
99
|
end
|
|
96
100
|
|
|
97
101
|
def handle_key(key)
|
|
98
102
|
case key
|
|
99
|
-
when "q", "Q", 27
|
|
100
|
-
when "j", ::Curses::Key::DOWN
|
|
101
|
-
when "k", ::Curses::Key::UP
|
|
102
|
-
when "d"
|
|
103
|
-
when "u"
|
|
103
|
+
when "q", "Q", 27 then throw :quit
|
|
104
|
+
when "j", ::Curses::Key::DOWN then scroll_by(1)
|
|
105
|
+
when "k", ::Curses::Key::UP then scroll_by(-1)
|
|
106
|
+
when "d" then scroll_by(page_size / 2)
|
|
107
|
+
when "u" then scroll_by(-page_size / 2)
|
|
104
108
|
when "f", " ", ::Curses::Key::NPAGE then scroll_by(page_size)
|
|
105
|
-
when "b", ::Curses::Key::PPAGE
|
|
106
|
-
when "g"
|
|
107
|
-
|
|
108
|
-
when "
|
|
109
|
+
when "b", ::Curses::Key::PPAGE then scroll_by(-page_size)
|
|
110
|
+
when "g" then @scroll = 0
|
|
111
|
+
draw
|
|
112
|
+
when "G" then @scroll = max_scroll
|
|
113
|
+
draw
|
|
114
|
+
when "e" then launch_editor if @file
|
|
115
|
+
when "r", "R" then reload_from_key if @file
|
|
116
|
+
when ::Curses::Key::RESIZE then handle_resize
|
|
109
117
|
end
|
|
110
118
|
end
|
|
111
119
|
|
|
120
|
+
def handle_resize
|
|
121
|
+
rewalk
|
|
122
|
+
@scroll = [@scroll, max_scroll].min
|
|
123
|
+
draw
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def rewalk
|
|
127
|
+
@lines = ASTWalker.new.walk(@markdown, max_width: ::Curses.cols)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def reload_from_key
|
|
131
|
+
reload
|
|
132
|
+
mark_reloaded
|
|
133
|
+
draw
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def check_file_updated
|
|
137
|
+
return unless @file
|
|
138
|
+
mtime = current_mtime
|
|
139
|
+
return if mtime.nil? || mtime == @last_mtime
|
|
140
|
+
|
|
141
|
+
@last_mtime = mtime
|
|
142
|
+
return if @file_updated
|
|
143
|
+
|
|
144
|
+
@file_updated = true
|
|
145
|
+
draw_status_bar
|
|
146
|
+
::Curses.refresh
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def mark_reloaded
|
|
150
|
+
@last_mtime = current_mtime
|
|
151
|
+
@file_updated = false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def current_mtime
|
|
155
|
+
return nil unless @file
|
|
156
|
+
File.mtime(@file)
|
|
157
|
+
rescue SystemCallError
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
112
161
|
def launch_editor
|
|
113
162
|
editor = ENV["EDITOR"] || ENV["VISUAL"] || "vi"
|
|
114
|
-
line
|
|
115
|
-
cmd
|
|
163
|
+
line = current_source_line
|
|
164
|
+
cmd = build_editor_command(editor, @file, line)
|
|
116
165
|
|
|
117
166
|
::Curses.close_screen
|
|
118
167
|
system(cmd)
|
|
168
|
+
init_curses_state
|
|
119
169
|
reload
|
|
120
|
-
|
|
170
|
+
mark_reloaded
|
|
121
171
|
draw
|
|
122
172
|
end
|
|
123
173
|
|
|
124
174
|
def reload
|
|
125
175
|
@markdown = File.read(@file)
|
|
126
|
-
|
|
127
|
-
@scroll
|
|
176
|
+
rewalk
|
|
177
|
+
@scroll = [@scroll, max_scroll].min
|
|
128
178
|
end
|
|
129
179
|
|
|
130
|
-
def
|
|
180
|
+
def init_curses_state
|
|
131
181
|
with_safe_term { ::Curses.init_screen }
|
|
132
182
|
::Curses.start_color
|
|
133
183
|
::Curses.use_default_colors
|
|
134
184
|
::Curses.noecho
|
|
135
185
|
::Curses.cbreak
|
|
136
186
|
::Curses.stdscr.keypad(true)
|
|
187
|
+
::Curses.stdscr.timeout = FILE_POLL_INTERVAL_MS
|
|
137
188
|
setup_colors
|
|
138
189
|
end
|
|
139
190
|
|
|
@@ -170,12 +221,20 @@ module Marvi
|
|
|
170
221
|
|
|
171
222
|
def draw_status_bar
|
|
172
223
|
::Curses.setpos(::Curses.lines - 1, 0)
|
|
224
|
+
top = @scroll + 1
|
|
225
|
+
bottom = [@scroll + page_size, @lines.size].min
|
|
226
|
+
edit_hint = @file ? " e edit" : ""
|
|
227
|
+
status = " #{top}-#{bottom}/#{@lines.size} j/k scroll g/G top/bottom#{edit_hint} q quit"
|
|
228
|
+
updated_hint = @file_updated ? " ● updated (r to reload) " : ""
|
|
229
|
+
available = [::Curses.cols - updated_hint.length, 0].max
|
|
230
|
+
|
|
173
231
|
::Curses.attron(::Curses.color_pair(COLOR_PAIRS[:cyan])) do
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
232
|
+
::Curses.addstr(status.ljust(available)[0, available])
|
|
233
|
+
end
|
|
234
|
+
unless updated_hint.empty?
|
|
235
|
+
::Curses.attron(::Curses.color_pair(COLOR_PAIRS[:yellow]) | ::Curses::A_BOLD) do
|
|
236
|
+
::Curses.addstr(updated_hint)
|
|
237
|
+
end
|
|
179
238
|
end
|
|
180
239
|
end
|
|
181
240
|
|
|
@@ -199,10 +258,10 @@ module Marvi
|
|
|
199
258
|
def build_attr(span)
|
|
200
259
|
attr = 0
|
|
201
260
|
attr |= ::Curses::A_BOLD if span.bold
|
|
202
|
-
attr |= ITALIC_ATTR
|
|
261
|
+
attr |= ITALIC_ATTR if span.italic
|
|
203
262
|
|
|
204
263
|
pair_key = if span.bg_color == :dark
|
|
205
|
-
span.color == :cyan ? :cyan_on_dark : :green_on_dark
|
|
264
|
+
(span.color == :cyan) ? :cyan_on_dark : :green_on_dark
|
|
206
265
|
elsif span.color
|
|
207
266
|
span.color
|
|
208
267
|
end
|
|
@@ -224,7 +283,7 @@ module Marvi
|
|
|
224
283
|
end
|
|
225
284
|
|
|
226
285
|
def scroll_by(delta)
|
|
227
|
-
@scroll =
|
|
286
|
+
@scroll = (@scroll + delta).clamp(0, max_scroll)
|
|
228
287
|
draw
|
|
229
288
|
end
|
|
230
289
|
end
|
data/lib/marvi/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: marvi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mitsutaka Mimura
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '1.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: unicode-display_width
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
54
68
|
description: Renders Markdown with ANSI colors in pipes and an interactive curses
|
|
55
69
|
pager in TTY.
|
|
56
70
|
email:
|
|
@@ -60,6 +74,8 @@ executables:
|
|
|
60
74
|
extensions: []
|
|
61
75
|
extra_rdoc_files: []
|
|
62
76
|
files:
|
|
77
|
+
- ".claude/hooks/standardrb-check.sh"
|
|
78
|
+
- ".claude/settings.json"
|
|
63
79
|
- CHANGELOG.md
|
|
64
80
|
- CODE_OF_CONDUCT.md
|
|
65
81
|
- LICENSE.txt
|