marvi 0.1.2 → 0.2.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: 0dd503447b94d15e62154562c11505be76de5f380abf17c735aa5f9879ac0ce8
4
- data.tar.gz: 234f6152a1797777ad5129307246dd19ad8c564539d060ae5015c8d5afc2284f
3
+ metadata.gz: 5131e252ac808321286aa802bdd386e5b3bb9b494dda00df1761ef9700088a4e
4
+ data.tar.gz: 2e9bccb531018e22014e43053b6a05d081e60a422c9107966c54739829cf528d
5
5
  SHA512:
6
- metadata.gz: d042ca2e2863425922de5ffc43cef959c0dded6a8a63c901d0073ce2d12813f11ddbf3f53bfa402da92bb9df82ae2cb9894124d7333c3b9f5cd62358b45bbfc0
7
- data.tar.gz: eb80372539222159afffd907a92b37dff0af63be65b72364cbc1e46449b05fd8dbf0490e9c64438a493400a2afefc51dbf01d4c39f07e0dac52298d0cc3876e6
6
+ metadata.gz: d87aa8d7de7acd2414e5301416fd09b09d1f33e48f64c93c86b5d0b7fc833e20102c297e89a77396cdcb4176269493459e835a6ab9df2a677d6d112ad9bbb3ed
7
+ data.tar.gz: 673ca0703a768010c6116fa4cae6b29c723b439a50e7591c3a7ac7f9928e614eda42f2ef2fda7f377ef7453babd37b729786579ef20479b24107f800be453c59
@@ -18,23 +18,27 @@ module Marvi
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
24
  @file = file
23
25
  @markdown = markdown
24
26
  @lines = ASTWalker.new.walk(markdown)
25
27
  @scroll = 0
28
+ mark_reloaded
26
29
 
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
30
+ init_curses_state
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
@@ -42,16 +46,48 @@ module Marvi
42
46
 
43
47
  private
44
48
 
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.
49
+ # ncurses uses the terminfo `rep` capability ("ESC[Nb") to compress runs of
50
+ # identical glyphs, but ghostty mishandles it and drops the run from the
51
+ # screen — table borders and long padding go missing. The bug surfaces both
52
+ # under xterm-ghostty directly and inside multiplexers like cmux that ship a
53
+ # terminfo whose xterm-256color entry advertises `rep`. Detect `rep` in the
54
+ # active terminfo and swap to a known no-rep alternative around initscr only.
55
+ REP_SAFE_TERMS = %w[screen-256color tmux-256color xterm-color xterm].freeze
56
+
47
57
  def with_safe_term
48
58
  original = ENV["TERM"]
49
- ENV["TERM"] = "xterm-256color" if original == "xterm-ghostty"
59
+ replacement = rep_safe_term_for(original)
60
+ ENV["TERM"] = replacement if replacement
50
61
  yield
51
62
  ensure
52
63
  ENV["TERM"] = original
53
64
  end
54
65
 
66
+ def rep_safe_term_for(term)
67
+ return nil if term.nil? || term.empty?
68
+ return nil unless terminfo_has_rep?(term)
69
+
70
+ REP_SAFE_TERMS.find { |candidate| candidate != term && terminfo_exists?(candidate) && !terminfo_has_rep?(candidate) }
71
+ end
72
+
73
+ def terminfo_has_rep?(term)
74
+ infocmp(term)&.include?("rep=") || false
75
+ end
76
+
77
+ def terminfo_exists?(term)
78
+ !infocmp(term).nil?
79
+ end
80
+
81
+ def infocmp(term)
82
+ @infocmp_cache ||= {}
83
+ return @infocmp_cache[term] if @infocmp_cache.key?(term)
84
+
85
+ output = IO.popen(["infocmp", "-1", term, err: File::NULL], &:read)
86
+ @infocmp_cache[term] = $?.success? ? output : nil
87
+ rescue StandardError
88
+ @infocmp_cache[term] = nil
89
+ end
90
+
55
91
  def setup_colors
56
92
  ::Curses.init_pair(COLOR_PAIRS[:cyan], ::Curses::COLOR_CYAN, -1)
57
93
  ::Curses.init_pair(COLOR_PAIRS[:green], ::Curses::COLOR_GREEN, -1)
@@ -74,9 +110,41 @@ module Marvi
74
110
  when "g" then @scroll = 0; draw
75
111
  when "G" then @scroll = max_scroll; draw
76
112
  when "e" then launch_editor if @file
113
+ when "r", "R" then reload_from_key if @file
77
114
  end
78
115
  end
79
116
 
117
+ def reload_from_key
118
+ reload
119
+ mark_reloaded
120
+ draw
121
+ end
122
+
123
+ def check_file_updated
124
+ return unless @file
125
+ mtime = current_mtime
126
+ return if mtime.nil? || mtime == @last_mtime
127
+
128
+ @last_mtime = mtime
129
+ return if @file_updated
130
+
131
+ @file_updated = true
132
+ draw_status_bar
133
+ ::Curses.refresh
134
+ end
135
+
136
+ def mark_reloaded
137
+ @last_mtime = current_mtime
138
+ @file_updated = false
139
+ end
140
+
141
+ def current_mtime
142
+ return nil unless @file
143
+ File.mtime(@file)
144
+ rescue SystemCallError
145
+ nil
146
+ end
147
+
80
148
  def launch_editor
81
149
  editor = ENV["EDITOR"] || ENV["VISUAL"] || "vi"
82
150
  line = current_source_line
@@ -85,7 +153,8 @@ module Marvi
85
153
  ::Curses.close_screen
86
154
  system(cmd)
87
155
  reload
88
- reinit_curses
156
+ mark_reloaded
157
+ init_curses_state
89
158
  draw
90
159
  end
91
160
 
@@ -95,13 +164,14 @@ module Marvi
95
164
  @scroll = [@scroll, max_scroll].min
96
165
  end
97
166
 
98
- def reinit_curses
167
+ def init_curses_state
99
168
  with_safe_term { ::Curses.init_screen }
100
169
  ::Curses.start_color
101
170
  ::Curses.use_default_colors
102
171
  ::Curses.noecho
103
172
  ::Curses.cbreak
104
173
  ::Curses.stdscr.keypad(true)
174
+ ::Curses.stdscr.timeout = FILE_POLL_INTERVAL_MS
105
175
  setup_colors
106
176
  end
107
177
 
@@ -138,12 +208,20 @@ module Marvi
138
208
 
139
209
  def draw_status_bar
140
210
  ::Curses.setpos(::Curses.lines - 1, 0)
211
+ top = @scroll + 1
212
+ bottom = [@scroll + page_size, @lines.size].min
213
+ edit_hint = @file ? " e edit" : ""
214
+ status = " #{top}-#{bottom}/#{@lines.size} j/k scroll g/G top/bottom#{edit_hint} q quit"
215
+ updated_hint = @file_updated ? " ● updated (r to reload) " : ""
216
+ available = [::Curses.cols - updated_hint.length, 0].max
217
+
141
218
  ::Curses.attron(::Curses.color_pair(COLOR_PAIRS[:cyan])) do
142
- top = @scroll + 1
143
- bottom = [@scroll + page_size, @lines.size].min
144
- edit_hint = @file ? " e edit" : ""
145
- status = " #{top}-#{bottom}/#{@lines.size} j/k scroll g/G top/bottom#{edit_hint} q quit"
146
- ::Curses.addstr(status.ljust(::Curses.cols)[0, ::Curses.cols])
219
+ ::Curses.addstr(status.ljust(available)[0, available])
220
+ end
221
+ unless updated_hint.empty?
222
+ ::Curses.attron(::Curses.color_pair(COLOR_PAIRS[:yellow]) | ::Curses::A_BOLD) do
223
+ ::Curses.addstr(updated_hint)
224
+ end
147
225
  end
148
226
  end
149
227
 
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.2"
4
+ VERSION = "0.2.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.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mitsutaka Mimura