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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38191038ed0255015b0184643cc52e36bc35b8973803c75f3f5636e227fddba6
4
- data.tar.gz: 83a3fe56615300c99bcc9858750926d5afab8a4e7a78c279e7b74ceef81ff074
3
+ metadata.gz: c5df9bb69ae060cf562d656a78f4aa152223a4ad58178af6b6b18891e19cb525
4
+ data.tar.gz: 1590ce11f925c30f25e256e1e0f0571355a4a4fd7690cc636310d9bf20e349eb
5
5
  SHA512:
6
- metadata.gz: 0dce42f6d88ceac30fa7cd2a75ad9318ab383f71164ad383ea1a95835601129ac8df125c403e634b5882a68802d24e05b4db9dd57496b4b91ab3a9a3bc176cca
7
- data.tar.gz: 4f41145dc7639d8034cdff69b750dd0c92856b23b830b3e25542de48685e11f70a610cca7130a4ca54b04e829d5bd79bdcc5322f94fea939c6e6c64db81bbb08
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
@@ -0,0 +1,17 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Write|Edit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": ".claude/hooks/standardrb-check.sh",
10
+ "timeout": 30,
11
+ "statusMessage": "Running standardrb"
12
+ }
13
+ ]
14
+ }
15
+ ]
16
+ }
17
+ }
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 = "\e[0m"
6
- BOLD = "\e[1m"
7
- ITALIC = "\e[3m"
8
- CYAN = "\e[36m"
9
- YELLOW = "\e[33m"
10
- GREEN = "\e[32m"
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 = "\e[37m"
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
@@ -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
- def walk(markdown)
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 = el.options[:location]
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 = el.options[:location]
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
- lines << RichLine.new([prefix] + render_inline(child), source_line: src)
87
+ lines << if lines.empty?
88
+ RichLine.new([prefix] + render_inline(child), source_line: src)
84
89
  else
85
- lines << RichLine.new(render_inline(child))
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 = el.options[:location]
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 = src ? src + i + (lang ? 1 : 0) : nil
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 = el.children.flat_map { |child| render_block(child) }
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 = el.options[:location]
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
- col_widths = cell_spans.map { |row| row.map { |spans| spans.sum { |s| s.text.length } } }
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
- row_spans = []
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
- plain_len = content.sum { |s| s.text.length }
131
- padding = col_widths[ci] - plain_len
132
- styled = is_header ? content.map { |s| Span.new(text: s.text, bold: true, color: :cyan) } : content
133
- row_spans += [Span.new(text: "│ ", color: :cyan)] + styled + [Span.new(text: " " * (padding + 1))]
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
@@ -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: Marvi::ANSI::CYAN,
8
- green: Marvi::ANSI::GREEN,
9
- yellow: Marvi::ANSI::YELLOW,
9
+ cyan: Marvi::ANSI::CYAN,
10
+ green: Marvi::ANSI::GREEN,
11
+ yellow: Marvi::ANSI::YELLOW,
10
12
  magenta: Marvi::ANSI::MAGENTA,
11
- white: Marvi::ANSI::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: 1,
11
- green: 2,
12
- yellow: 3,
13
- magenta: 4,
14
- white: 5,
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: 7
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 = file
24
+ @file = file
23
25
  @markdown = markdown
24
- @lines = ASTWalker.new.walk(markdown)
25
- @scroll = 0
26
+ @scroll = 0
27
+ mark_reloaded
26
28
 
27
- with_safe_term { ::Curses.init_screen }
28
- ::Curses.start_color
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 { handle_key(::Curses.getch) }
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 StandardError
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], ::Curses::COLOR_CYAN, -1)
89
- ::Curses.init_pair(COLOR_PAIRS[:green], ::Curses::COLOR_GREEN, -1)
90
- ::Curses.init_pair(COLOR_PAIRS[:yellow], ::Curses::COLOR_YELLOW, -1)
91
- ::Curses.init_pair(COLOR_PAIRS[:magenta], ::Curses::COLOR_MAGENTA, -1)
92
- ::Curses.init_pair(COLOR_PAIRS[:white], ::Curses::COLOR_WHITE, -1)
93
- ::Curses.init_pair(COLOR_PAIRS[:green_on_dark], ::Curses::COLOR_GREEN, ::Curses::COLOR_BLACK)
94
- ::Curses.init_pair(COLOR_PAIRS[:cyan_on_dark], ::Curses::COLOR_CYAN, ::Curses::COLOR_BLACK)
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 then throw :quit
100
- when "j", ::Curses::Key::DOWN then scroll_by(1)
101
- when "k", ::Curses::Key::UP then scroll_by(-1)
102
- when "d" then scroll_by(page_size / 2)
103
- when "u" then scroll_by(-page_size / 2)
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 then scroll_by(-page_size)
106
- when "g" then @scroll = 0; draw
107
- when "G" then @scroll = max_scroll; draw
108
- when "e" then launch_editor if @file
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 = current_source_line
115
- cmd = build_editor_command(editor, @file, line)
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
- reinit_curses
170
+ mark_reloaded
121
171
  draw
122
172
  end
123
173
 
124
174
  def reload
125
175
  @markdown = File.read(@file)
126
- @lines = ASTWalker.new.walk(@markdown)
127
- @scroll = [@scroll, max_scroll].min
176
+ rewalk
177
+ @scroll = [@scroll, max_scroll].min
128
178
  end
129
179
 
130
- def reinit_curses
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
- top = @scroll + 1
175
- bottom = [@scroll + page_size, @lines.size].min
176
- edit_hint = @file ? " e edit" : ""
177
- status = " #{top}-#{bottom}/#{@lines.size} j/k scroll g/G top/bottom#{edit_hint} q quit"
178
- ::Curses.addstr(status.ljust(::Curses.cols)[0, ::Curses.cols])
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 if span.italic
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 = [[@scroll + delta, 0].max, max_scroll].min
286
+ @scroll = (@scroll + delta).clamp(0, max_scroll)
228
287
  draw
229
288
  end
230
289
  end
data/lib/marvi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Marvi
4
- VERSION = "0.1.3"
4
+ VERSION = "0.3.0"
5
5
  end
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.1.3
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