marvi 0.1.0 → 0.1.2

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: 81a8d64fd49b33e6d17b7d1b2bf898bca77b6abb347ec5d4f6844f6b2969b647
4
- data.tar.gz: b8020fc1f24ab9806f1dc3d8b94baf4061e6ab329a0dc54598db747465e5c5d7
3
+ metadata.gz: 0dd503447b94d15e62154562c11505be76de5f380abf17c735aa5f9879ac0ce8
4
+ data.tar.gz: 234f6152a1797777ad5129307246dd19ad8c564539d060ae5015c8d5afc2284f
5
5
  SHA512:
6
- metadata.gz: decbb5c0688e364d432aced0e156a9b0cdd1d33f3aeaa6c9bba5f7761f1bfd5182e740cc023b956926838261e1d66fa8a90f0666ded886d72dd397010c793ac7
7
- data.tar.gz: 5f969855ab9c5d49801bc4675ecd19d4c815a8e49e7532559bc6f46ffc6d76f3ae6f735e6f73c495bee7406ccbfcd091305127f29c00e0d0c859179c3447543b
6
+ metadata.gz: d042ca2e2863425922de5ffc43cef959c0dded6a8a63c901d0073ce2d12813f11ddbf3f53bfa402da92bb9df82ae2cb9894124d7333c3b9f5cd62358b45bbfc0
7
+ data.tar.gz: eb80372539222159afffd907a92b37dff0af63be65b72364cbc1e46449b05fd8dbf0490e9c64438a493400a2afefc51dbf01d4c39f07e0dac52298d0cc3876e6
data/exe/marvi CHANGED
@@ -23,7 +23,7 @@ else
23
23
  end
24
24
 
25
25
  if $stdout.tty?
26
- Marvi::Renderer::Curses.new.render(markdown)
26
+ Marvi::Renderer::Curses.new.render(markdown, file: ARGV[0])
27
27
  else
28
28
  print Marvi::Renderer::ANSI.new.render(markdown)
29
29
  end
@@ -23,7 +23,8 @@ module Marvi
23
23
  when :header
24
24
  render_header(el)
25
25
  when :p
26
- [RichLine.new(render_inline_children(el)), RichLine.blank]
26
+ src = el.options[:location]
27
+ [RichLine.new(render_inline_children(el), source_line: src), RichLine.blank]
27
28
  when :ul
28
29
  el.children.flat_map { |child| render_block(child, indent: indent, list_type: :ul) } + [RichLine.blank]
29
30
  when :ol
@@ -37,7 +38,8 @@ module Marvi
37
38
  when :blockquote
38
39
  render_blockquote(el)
39
40
  when :hr
40
- [RichLine.new([Span.new(text: "─" * 60, color: :cyan)]), RichLine.blank]
41
+ src = el.options[:location]
42
+ [RichLine.new([Span.new(text: "─" * 60, color: :cyan)], source_line: src), RichLine.blank]
41
43
  when :table
42
44
  render_table(el)
43
45
  when :blank
@@ -50,17 +52,19 @@ module Marvi
50
52
  def render_header(el)
51
53
  level = el.options[:level]
52
54
  color = HEADER_COLORS[level - 1]
55
+ src = el.options[:location]
53
56
  prefix = Span.new(text: "#" * level + " ", bold: true, color: color)
54
57
  content = render_inline_children(el).map do |s|
55
58
  Span.new(text: s.text, bold: true, italic: s.italic, color: s.color || color, bg_color: s.bg_color)
56
59
  end
57
- [RichLine.new([prefix] + content), RichLine.blank]
60
+ [RichLine.new([prefix] + content, source_line: src), RichLine.blank]
58
61
  end
59
62
 
60
63
  def render_li(el, indent:, list_type:, list_index:)
61
64
  bullet = list_type == :ol ? "#{list_index}." : "•"
62
65
  prefix = Span.new(text: "#{" " * indent}#{bullet} ", color: :cyan)
63
- lines = []
66
+ src = el.options[:location]
67
+ lines = []
64
68
 
65
69
  el.children.each do |child|
66
70
  case child.type
@@ -70,13 +74,13 @@ module Marvi
70
74
  lines += nested
71
75
  when :p
72
76
  if lines.empty?
73
- lines << RichLine.new([prefix] + render_inline_children(child))
77
+ lines << RichLine.new([prefix] + render_inline_children(child), source_line: src)
74
78
  else
75
79
  lines += render_block(child)
76
80
  end
77
81
  else
78
82
  if lines.empty?
79
- lines << RichLine.new([prefix] + render_inline(child))
83
+ lines << RichLine.new([prefix] + render_inline(child), source_line: src)
80
84
  else
81
85
  lines << RichLine.new(render_inline(child))
82
86
  end
@@ -86,23 +90,27 @@ module Marvi
86
90
  end
87
91
 
88
92
  def render_codeblock(el)
93
+ src = el.options[:location]
89
94
  lang = el.options[:lang]
90
95
  lines = []
91
- lines << RichLine.new([Span.new(text: lang, color: :yellow)]) if lang
92
- el.value.chomp.split("\n").each do |line|
93
- lines << RichLine.new([Span.new(text: " #{line}", color: :green, bg_color: :dark)])
96
+ lines << RichLine.new([Span.new(text: lang, color: :yellow)], source_line: src) if lang
97
+ el.value.chomp.split("\n").each_with_index do |line, i|
98
+ line_src = src ? src + i + (lang ? 1 : 0) : nil
99
+ lines << RichLine.new([Span.new(text: " #{line}", color: :green, bg_color: :dark)], source_line: line_src)
94
100
  end
95
101
  lines << RichLine.blank
96
102
  lines
97
103
  end
98
104
 
99
105
  def render_blockquote(el)
100
- inner = el.children.flat_map { |child| render_block(child) }
106
+ inner = el.children.flat_map { |child| render_block(child) }
101
107
  prefix = Span.new(text: "│ ", color: :cyan)
102
- inner.map { |line| RichLine.new([prefix] + line.spans) } + [RichLine.blank]
108
+ # preserve source_line from inner lines
109
+ inner.map { |line| RichLine.new([prefix] + line.spans, source_line: line.source_line) } + [RichLine.blank]
103
110
  end
104
111
 
105
112
  def render_table(el)
113
+ src = el.options[:location]
106
114
  rows = el.children.flat_map(&:children)
107
115
  header_row = el.children.find { |s| s.type == :thead }&.children&.first
108
116
 
@@ -112,7 +120,7 @@ module Marvi
112
120
 
113
121
  lines = []
114
122
  top = col_widths.map { |w| "─" * (w + 2) }.join("┬")
115
- lines << RichLine.new([Span.new(text: "┌#{top}┐", color: :cyan)])
123
+ lines << RichLine.new([Span.new(text: "┌#{top}┐", color: :cyan)], source_line: src)
116
124
 
117
125
  rows.each_with_index do |row, ri|
118
126
  is_header = row == header_row
@@ -14,10 +14,11 @@ module Marvi
14
14
  end
15
15
 
16
16
  class RichLine
17
- attr_reader :spans
17
+ attr_reader :spans, :source_line
18
18
 
19
- def initialize(spans = [])
19
+ def initialize(spans = [], source_line: nil)
20
20
  @spans = spans
21
+ @source_line = source_line
21
22
  end
22
23
 
23
24
  def plain_text
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "curses"
4
+ require "shellwords"
4
5
 
5
6
  module Marvi
6
7
  module Renderer
@@ -17,11 +18,13 @@ module Marvi
17
18
 
18
19
  ITALIC_ATTR = (defined?(::Curses::A_ITALIC) ? ::Curses::A_ITALIC : 0)
19
20
 
20
- def render(markdown)
21
- @lines = ASTWalker.new.walk(markdown)
22
- @scroll = 0
21
+ def render(markdown, file: nil)
22
+ @file = file
23
+ @markdown = markdown
24
+ @lines = ASTWalker.new.walk(markdown)
25
+ @scroll = 0
23
26
 
24
- ::Curses.init_screen
27
+ with_safe_term { ::Curses.init_screen }
25
28
  ::Curses.start_color
26
29
  ::Curses.use_default_colors
27
30
  ::Curses.noecho
@@ -39,6 +42,16 @@ module Marvi
39
42
 
40
43
  private
41
44
 
45
+ # xterm-ghostty's `rep` capability mishandles long runs of identical glyphs,
46
+ # so swap to xterm-256color around initscr to disable that ncurses optimization.
47
+ def with_safe_term
48
+ original = ENV["TERM"]
49
+ ENV["TERM"] = "xterm-256color" if original == "xterm-ghostty"
50
+ yield
51
+ ensure
52
+ ENV["TERM"] = original
53
+ end
54
+
42
55
  def setup_colors
43
56
  ::Curses.init_pair(COLOR_PAIRS[:cyan], ::Curses::COLOR_CYAN, -1)
44
57
  ::Curses.init_pair(COLOR_PAIRS[:green], ::Curses::COLOR_GREEN, -1)
@@ -51,18 +64,68 @@ module Marvi
51
64
 
52
65
  def handle_key(key)
53
66
  case key
54
- when "q", "Q", 27 then throw :quit
55
- when "j", ::Curses::Key::DOWN then scroll_by(1)
56
- when "k", ::Curses::Key::UP then scroll_by(-1)
57
- when "d" then scroll_by(page_size / 2)
58
- when "u" then scroll_by(-page_size / 2)
67
+ when "q", "Q", 27 then throw :quit
68
+ when "j", ::Curses::Key::DOWN then scroll_by(1)
69
+ when "k", ::Curses::Key::UP then scroll_by(-1)
70
+ when "d" then scroll_by(page_size / 2)
71
+ when "u" then scroll_by(-page_size / 2)
59
72
  when "f", " ", ::Curses::Key::NPAGE then scroll_by(page_size)
60
- when "b", ::Curses::Key::PPAGE then scroll_by(-page_size)
61
- when "g" then @scroll = 0; draw
62
- when "G" then @scroll = max_scroll; draw
73
+ when "b", ::Curses::Key::PPAGE then scroll_by(-page_size)
74
+ when "g" then @scroll = 0; draw
75
+ when "G" then @scroll = max_scroll; draw
76
+ when "e" then launch_editor if @file
77
+ end
78
+ end
79
+
80
+ def launch_editor
81
+ editor = ENV["EDITOR"] || ENV["VISUAL"] || "vi"
82
+ line = current_source_line
83
+ cmd = build_editor_command(editor, @file, line)
84
+
85
+ ::Curses.close_screen
86
+ system(cmd)
87
+ reload
88
+ reinit_curses
89
+ draw
90
+ end
91
+
92
+ def reload
93
+ @markdown = File.read(@file)
94
+ @lines = ASTWalker.new.walk(@markdown)
95
+ @scroll = [@scroll, max_scroll].min
96
+ end
97
+
98
+ def reinit_curses
99
+ with_safe_term { ::Curses.init_screen }
100
+ ::Curses.start_color
101
+ ::Curses.use_default_colors
102
+ ::Curses.noecho
103
+ ::Curses.cbreak
104
+ ::Curses.stdscr.keypad(true)
105
+ setup_colors
106
+ end
107
+
108
+ def build_editor_command(editor, file, line)
109
+ base = File.basename(editor.split.first)
110
+ escaped = Shellwords.escape(file)
111
+ case base
112
+ when "code"
113
+ "#{editor} --goto #{escaped}:#{line}"
114
+ when "subl", "sublime_text"
115
+ "#{editor} #{escaped}:#{line}"
116
+ else
117
+ # vim, nvim, nano, emacs, micro, etc.
118
+ "#{editor} +#{line} #{escaped}"
63
119
  end
64
120
  end
65
121
 
122
+ def current_source_line
123
+ visible_lines.each { |line| return line.source_line if line.source_line }
124
+ # fall back to searching upward from scroll position
125
+ @scroll.downto(0) { |i| return @lines[i].source_line if @lines[i]&.source_line }
126
+ 1
127
+ end
128
+
66
129
  def draw
67
130
  ::Curses.clear
68
131
  visible_lines.each_with_index do |line, row|
@@ -78,7 +141,8 @@ module Marvi
78
141
  ::Curses.attron(::Curses.color_pair(COLOR_PAIRS[:cyan])) do
79
142
  top = @scroll + 1
80
143
  bottom = [@scroll + page_size, @lines.size].min
81
- status = " #{top}-#{bottom}/#{@lines.size} j/k scroll g/G top/bottom q quit"
144
+ edit_hint = @file ? " e edit" : ""
145
+ status = " #{top}-#{bottom}/#{@lines.size} j/k scroll g/G top/bottom#{edit_hint} q quit"
82
146
  ::Curses.addstr(status.ljust(::Curses.cols)[0, ::Curses.cols])
83
147
  end
84
148
  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.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/marvi-0.1.0.gem ADDED
Binary file
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.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mitsutaka Mimura
@@ -73,6 +73,7 @@ files:
73
73
  - lib/marvi/renderer/ansi.rb
74
74
  - lib/marvi/renderer/curses.rb
75
75
  - lib/marvi/version.rb
76
+ - marvi-0.1.0.gem
76
77
  - sig/marvi.rbs
77
78
  homepage: https://github.com/takkanm/marvi
78
79
  licenses: