marvi 0.2.0 → 0.4.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 +9 -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 +53 -37
- 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: b0110473458c01229add5274f0ab0df77dad55587286ee01c426612fa0505707
|
|
4
|
+
data.tar.gz: 8e59b8386b4fb283613a2fc81d2841473ccd78eaa216a4608e10c17ae62c61b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3abaf2190555600e5e04cd0a04d7176d6ef5b8384afc902cff76389ec782a448bd99609cf2474d7f62827bc24750514b7517cf780b61739c327a665bb3cc9821
|
|
7
|
+
data.tar.gz: 2caddfb8de48b8fc5f6c894f839d06239b57845993a67f576f8cde2cb6591509d3167dbe35d11636e1c03f5cb140e2c16cfeec945bf8204eeb4095f3a31c8bcb
|
|
@@ -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,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-05-18
|
|
4
|
+
|
|
5
|
+
- Bind `Ctrl-D` / `Ctrl-U` for vim-style half-page scrolling in the curses pager.
|
|
6
|
+
|
|
7
|
+
## [0.3.0] - 2026-05-18
|
|
8
|
+
|
|
9
|
+
- Render tables correctly when cells contain East Asian wide characters and emoji (uses `unicode-display_width`).
|
|
10
|
+
- Wrap long table cells to fit the terminal width so borders no longer break across physical lines; curses pager re-flows on window resize.
|
|
11
|
+
|
|
3
12
|
## [0.1.0] - 2026-03-17
|
|
4
13
|
|
|
5
14
|
- 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,27 +7,30 @@ 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
21
|
FILE_POLL_INTERVAL_MS = 500
|
|
22
22
|
|
|
23
|
+
CTRL_D = 4
|
|
24
|
+
CTRL_U = 21
|
|
25
|
+
|
|
23
26
|
def render(markdown, file: nil)
|
|
24
|
-
@file
|
|
27
|
+
@file = file
|
|
25
28
|
@markdown = markdown
|
|
26
|
-
@
|
|
27
|
-
@scroll = 0
|
|
29
|
+
@scroll = 0
|
|
28
30
|
mark_reloaded
|
|
29
31
|
|
|
30
32
|
init_curses_state
|
|
33
|
+
rewalk
|
|
31
34
|
draw
|
|
32
35
|
|
|
33
36
|
catch(:quit) do
|
|
@@ -84,36 +87,49 @@ module Marvi
|
|
|
84
87
|
|
|
85
88
|
output = IO.popen(["infocmp", "-1", term, err: File::NULL], &:read)
|
|
86
89
|
@infocmp_cache[term] = $?.success? ? output : nil
|
|
87
|
-
rescue
|
|
90
|
+
rescue
|
|
88
91
|
@infocmp_cache[term] = nil
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
def setup_colors
|
|
92
|
-
::Curses.init_pair(COLOR_PAIRS[:cyan],
|
|
93
|
-
::Curses.init_pair(COLOR_PAIRS[:green],
|
|
94
|
-
::Curses.init_pair(COLOR_PAIRS[:yellow],
|
|
95
|
-
::Curses.init_pair(COLOR_PAIRS[:magenta],
|
|
96
|
-
::Curses.init_pair(COLOR_PAIRS[:white],
|
|
97
|
-
::Curses.init_pair(COLOR_PAIRS[:green_on_dark], ::Curses::COLOR_GREEN,
|
|
98
|
-
::Curses.init_pair(COLOR_PAIRS[:cyan_on_dark],
|
|
95
|
+
::Curses.init_pair(COLOR_PAIRS[:cyan], ::Curses::COLOR_CYAN, -1)
|
|
96
|
+
::Curses.init_pair(COLOR_PAIRS[:green], ::Curses::COLOR_GREEN, -1)
|
|
97
|
+
::Curses.init_pair(COLOR_PAIRS[:yellow], ::Curses::COLOR_YELLOW, -1)
|
|
98
|
+
::Curses.init_pair(COLOR_PAIRS[:magenta], ::Curses::COLOR_MAGENTA, -1)
|
|
99
|
+
::Curses.init_pair(COLOR_PAIRS[:white], ::Curses::COLOR_WHITE, -1)
|
|
100
|
+
::Curses.init_pair(COLOR_PAIRS[:green_on_dark], ::Curses::COLOR_GREEN, ::Curses::COLOR_BLACK)
|
|
101
|
+
::Curses.init_pair(COLOR_PAIRS[:cyan_on_dark], ::Curses::COLOR_CYAN, ::Curses::COLOR_BLACK)
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
def handle_key(key)
|
|
102
105
|
case key
|
|
103
|
-
when "q", "Q", 27
|
|
104
|
-
when "j", ::Curses::Key::DOWN
|
|
105
|
-
when "k", ::Curses::Key::UP
|
|
106
|
-
when "d"
|
|
107
|
-
when "u"
|
|
106
|
+
when "q", "Q", 27 then throw :quit
|
|
107
|
+
when "j", ::Curses::Key::DOWN then scroll_by(1)
|
|
108
|
+
when "k", ::Curses::Key::UP then scroll_by(-1)
|
|
109
|
+
when "d", CTRL_D then scroll_by(page_size / 2)
|
|
110
|
+
when "u", CTRL_U then scroll_by(-page_size / 2)
|
|
108
111
|
when "f", " ", ::Curses::Key::NPAGE then scroll_by(page_size)
|
|
109
|
-
when "b", ::Curses::Key::PPAGE
|
|
110
|
-
when "g"
|
|
111
|
-
|
|
112
|
-
when "
|
|
113
|
-
|
|
112
|
+
when "b", ::Curses::Key::PPAGE then scroll_by(-page_size)
|
|
113
|
+
when "g" then @scroll = 0
|
|
114
|
+
draw
|
|
115
|
+
when "G" then @scroll = max_scroll
|
|
116
|
+
draw
|
|
117
|
+
when "e" then launch_editor if @file
|
|
118
|
+
when "r", "R" then reload_from_key if @file
|
|
119
|
+
when ::Curses::Key::RESIZE then handle_resize
|
|
114
120
|
end
|
|
115
121
|
end
|
|
116
122
|
|
|
123
|
+
def handle_resize
|
|
124
|
+
rewalk
|
|
125
|
+
@scroll = [@scroll, max_scroll].min
|
|
126
|
+
draw
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def rewalk
|
|
130
|
+
@lines = ASTWalker.new.walk(@markdown, max_width: ::Curses.cols)
|
|
131
|
+
end
|
|
132
|
+
|
|
117
133
|
def reload_from_key
|
|
118
134
|
reload
|
|
119
135
|
mark_reloaded
|
|
@@ -134,7 +150,7 @@ module Marvi
|
|
|
134
150
|
end
|
|
135
151
|
|
|
136
152
|
def mark_reloaded
|
|
137
|
-
@last_mtime
|
|
153
|
+
@last_mtime = current_mtime
|
|
138
154
|
@file_updated = false
|
|
139
155
|
end
|
|
140
156
|
|
|
@@ -147,21 +163,21 @@ module Marvi
|
|
|
147
163
|
|
|
148
164
|
def launch_editor
|
|
149
165
|
editor = ENV["EDITOR"] || ENV["VISUAL"] || "vi"
|
|
150
|
-
line
|
|
151
|
-
cmd
|
|
166
|
+
line = current_source_line
|
|
167
|
+
cmd = build_editor_command(editor, @file, line)
|
|
152
168
|
|
|
153
169
|
::Curses.close_screen
|
|
154
170
|
system(cmd)
|
|
171
|
+
init_curses_state
|
|
155
172
|
reload
|
|
156
173
|
mark_reloaded
|
|
157
|
-
init_curses_state
|
|
158
174
|
draw
|
|
159
175
|
end
|
|
160
176
|
|
|
161
177
|
def reload
|
|
162
178
|
@markdown = File.read(@file)
|
|
163
|
-
|
|
164
|
-
@scroll
|
|
179
|
+
rewalk
|
|
180
|
+
@scroll = [@scroll, max_scroll].min
|
|
165
181
|
end
|
|
166
182
|
|
|
167
183
|
def init_curses_state
|
|
@@ -208,7 +224,7 @@ module Marvi
|
|
|
208
224
|
|
|
209
225
|
def draw_status_bar
|
|
210
226
|
::Curses.setpos(::Curses.lines - 1, 0)
|
|
211
|
-
top
|
|
227
|
+
top = @scroll + 1
|
|
212
228
|
bottom = [@scroll + page_size, @lines.size].min
|
|
213
229
|
edit_hint = @file ? " e edit" : ""
|
|
214
230
|
status = " #{top}-#{bottom}/#{@lines.size} j/k scroll g/G top/bottom#{edit_hint} q quit"
|
|
@@ -245,10 +261,10 @@ module Marvi
|
|
|
245
261
|
def build_attr(span)
|
|
246
262
|
attr = 0
|
|
247
263
|
attr |= ::Curses::A_BOLD if span.bold
|
|
248
|
-
attr |= ITALIC_ATTR
|
|
264
|
+
attr |= ITALIC_ATTR if span.italic
|
|
249
265
|
|
|
250
266
|
pair_key = if span.bg_color == :dark
|
|
251
|
-
span.color == :cyan ? :cyan_on_dark : :green_on_dark
|
|
267
|
+
(span.color == :cyan) ? :cyan_on_dark : :green_on_dark
|
|
252
268
|
elsif span.color
|
|
253
269
|
span.color
|
|
254
270
|
end
|
|
@@ -270,7 +286,7 @@ module Marvi
|
|
|
270
286
|
end
|
|
271
287
|
|
|
272
288
|
def scroll_by(delta)
|
|
273
|
-
@scroll =
|
|
289
|
+
@scroll = (@scroll + delta).clamp(0, max_scroll)
|
|
274
290
|
draw
|
|
275
291
|
end
|
|
276
292
|
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.4.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
|