kv 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.
Files changed (10) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/LICENSE +25 -0
  4. data/README.md +100 -0
  5. data/Rakefile +10 -0
  6. data/exe/kv +11 -0
  7. data/kv.gemspec +31 -0
  8. data/lib/kv/version.rb +3 -0
  9. data/lib/kv.rb +666 -0
  10. metadata +68 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 07f9b8b9be509c1a29acef17374146c9381713514e19c91a8a2942f9c2ba09e9
4
+ data.tar.gz: fb877ac8dbf8b5fac3f9b2c4e6690aa01fc25bca3b11fcf7abf88c52ef41fa23
5
+ SHA512:
6
+ metadata.gz: c8c646ba69a9a977ea232dedaaff65db9f781d054e76d846913763821678c93c5362e7f4e153574d252505181da0a73a01b7865c38ac2c380c4bc9abaadcd6bc
7
+ data.tar.gz: eb9f37454be0ce707fbc02f6caae238541fe1865460e67de6c730a2eb81e2ba90d2fbaefa97f418426f19a8343e651b843f9943c61cd5e49cb2d22d15e5cde32
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2020, Koichi Sasada
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # kv: A page viewer written by Ruby
2
+
3
+ kv is a page viewer designed for streaming data written by Ruby.
4
+
5
+ ## Installation
6
+
7
+ Install it yourself as:
8
+
9
+ $ gem install kv
10
+
11
+ # Usage
12
+
13
+ kv requires Ruby and curses gem.
14
+
15
+ ## Use kv
16
+
17
+ ```
18
+ # View [FILE]
19
+ $ kv [OPTIONS] [FILE]
20
+
21
+ # View results of [CMD]
22
+ $ [CMD] | kv [OPTIONS]
23
+
24
+ # View command help
25
+ $ kv
26
+
27
+ Options:
28
+ -f following mode like "tail -f"
29
+ -n, --line-number LINE goto LINE
30
+ ```
31
+
32
+ ## Command on a pager
33
+
34
+ ```
35
+ kv: A pager by Ruby Command list
36
+
37
+ ?: show the help message
38
+ q: quit
39
+
40
+ # Moving
41
+ k, j, [UP], [DOWN]: move cursor (y)
42
+ h, l, [LEFT], [RIGTH]: move cursor (x)
43
+ Ctrl-U, [PAGE UP]: page up
44
+ Ctrl-D, [PAGE DOWN], [SPACE]: page down
45
+
46
+ g: Goto first line
47
+ G: Goto last line (current last line)
48
+ \d+: Goto specified line
49
+
50
+ # Loading
51
+ You can load a huge file or infinite input from a pipe.
52
+ 10,000 lines ahead current line will be loaded.
53
+ If you want to load further lines the follwoing commands will help you.
54
+
55
+ F: Load remaining data or monitor a specified file and scroll forward.
56
+ Pushing any keys stops loading.
57
+ If a search string (specified by commadn "/") is specified,
58
+ stop scroll if the further input lines contains the string.
59
+
60
+ L: Toggle unlimited input mode
61
+
62
+ # Searching
63
+ /: search
64
+ When you enter a search string, you can choose
65
+ the following mode by Control key combination:
66
+ Ctrl-R: toggle Regexp mode (Ruby's regexp)
67
+ Ctrl-I: toggle ignore case mode
68
+ To clear search string, research with an empty string.
69
+ n: search next
70
+ p: search preview
71
+ f: filter mode (show only matched lines)
72
+
73
+ # Output
74
+ s: Save screen buffer to file
75
+
76
+ # Modes
77
+ N: toggle line mode
78
+ m: toggle mouse mode
79
+ t: terminal (REPL) mode
80
+ v: vi ("vi filename +[LINE]")
81
+ ```
82
+
83
+ `G` is notable feature, `less` doesn't have. This feature jumps to "current" last line even if the pipe source command does not close output (== input for kv). You can refresh the last line by putting any command.
84
+
85
+ ## Mouse mode
86
+
87
+ Not yet.
88
+
89
+ ## Terminal (REPL) mode
90
+
91
+ Not yet.
92
+
93
+ # Contributing
94
+
95
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ko1/kv.
96
+
97
+ # Credit
98
+
99
+ Created by Koichi Sasada at 2020.
100
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/exe/kv ADDED
@@ -0,0 +1,11 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ begin
4
+ require 'kv'
5
+ rescue LoadError
6
+ $:.unshift File.join(__dir__, '../lib')
7
+ require 'kv'
8
+ end
9
+
10
+ kv = KV::KV.new ARGV
11
+ kv.control
data/kv.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ require_relative 'lib/kv/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "kv"
5
+ spec.version = KV::VERSION
6
+ spec.authors = ["Koichi Sasada"]
7
+ spec.email = ["ko1@atdot.net"]
8
+
9
+ spec.summary = %q{kv: A page viewer written by Ruby}
10
+ spec.description = %q{kv is a page viewer designed for streaming data written by Ruby.}
11
+ spec.homepage = "https://github.com/ko1/kv"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/ko1/kv"
19
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_runtime_dependency 'curses', '~> 1.3'
31
+ end
data/lib/kv/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module KV
2
+ VERSION = "0.2.0"
3
+ end
data/lib/kv.rb ADDED
@@ -0,0 +1,666 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kv/version"
4
+ require "curses"
5
+ require 'stringio'
6
+ require 'optparse'
7
+
8
+ module KV
9
+ class KV_PushScreen < Exception
10
+ attr_reader :screen
11
+ def initialize screen
12
+ @screen = screen
13
+ end
14
+ end
15
+
16
+ class KV_PopScreen < Exception
17
+ end
18
+
19
+ class KV_Screen
20
+ RenderStatus = Struct.new(
21
+ :c_cols, :c_lines, :x, :y,
22
+ :search, :goto, :line_mode, :render_full, :last_lineno
23
+ )
24
+ class RenderStatus
25
+ def to_s
26
+ 'kv'
27
+ end
28
+ end
29
+
30
+ def initialize input, lines: [], search: nil, name: nil, following_mode: false, first_line: 0, line_mode: false
31
+ @rs = RenderStatus.new
32
+ @last_rs = nil
33
+ @rs.y = first_line
34
+ @rs.goto = first_line if first_line > 0
35
+ @rs.x = 0
36
+ @rs.last_lineno = 0
37
+ @rs.line_mode = line_mode
38
+ @rs.search = search
39
+
40
+ @name = name
41
+ @filename = @name if @name && File.exist?(@name)
42
+
43
+ @lines = lines
44
+ @mode = :screen
45
+
46
+ @following_mode = following_mode
47
+
48
+ @mouse = false
49
+ @search_ignore_case = false
50
+ @search_regexp = true
51
+ @loading = false
52
+ @buffer_lines = 10_000
53
+ @yq = Queue.new
54
+ @load_unlimited = false
55
+ @prev_render = {}
56
+
57
+ read_async input if input
58
+ end
59
+
60
+ def setup_line line
61
+ line = line.chomp
62
+ line.instance_variable_set(:@lineno, @rs.last_lineno += 1)
63
+ line
64
+ end
65
+
66
+ def read_async input
67
+ @loading = true
68
+ data = input.read_nonblock(4096)
69
+
70
+ lines = data.each_line.to_a
71
+ last_line = nil
72
+ lines.each{|line|
73
+ if line[-1] != "\n"
74
+ last_line = line
75
+ break
76
+ end
77
+ @lines << setup_line(line)
78
+ }
79
+
80
+ Thread.abort_on_exception = true
81
+ @reader_thread = Thread.new do
82
+ while line = input.gets
83
+ if last_line
84
+ line = last_line + line
85
+ last_line = nil
86
+ end
87
+
88
+ @lines << setup_line(line)
89
+ while !@load_unlimited && @lines.size > self.y + @buffer_lines
90
+ @yq.pop; @yq.clear
91
+ end
92
+ @yq.clear
93
+ end
94
+ ensure
95
+ if @filename
96
+ @file_mtime = File.mtime(@filename)
97
+ @file_lastpos = input.tell
98
+ end
99
+ input.close
100
+ @loading = false
101
+ end
102
+ end
103
+
104
+ def y_max
105
+ @lines.size - Curses.lines + 2
106
+ end
107
+
108
+ def y
109
+ @rs.y
110
+ end
111
+
112
+ def y=(y)
113
+ if y > (ym = self.y_max)
114
+ @rs.y = ym
115
+ else
116
+ @rs.y = y
117
+ end
118
+
119
+ @rs.y = 0 if @rs.y < 0
120
+ @yq << nil if @loading
121
+ end
122
+
123
+ attr_reader :x
124
+
125
+ def x=(x)
126
+ @rs.x = x
127
+ @rs.x = 0 if @rs.x < 0
128
+ end
129
+
130
+ def x
131
+ @rs.x
132
+ end
133
+
134
+ def init_screen
135
+ Curses.init_screen
136
+ Curses.stdscr.keypad(true)
137
+
138
+ if @mouse
139
+ Curses.mousemask(Curses::BUTTON1_CLICKED | Curses::BUTTON2_CLICKED |
140
+ Curses::BUTTON3_CLICKED | Curses::BUTTON4_CLICKED)
141
+ else
142
+ Curses.mousemask(0)
143
+ end
144
+ self.y = @rs.y
145
+ end
146
+
147
+
148
+ def standout
149
+ Curses.standout
150
+ yield
151
+ Curses.standend
152
+ end
153
+
154
+ def cattr attr
155
+ Curses.attron attr
156
+ begin
157
+ yield
158
+ ensure
159
+ Curses.attroff attr
160
+ end
161
+ end
162
+
163
+ def ctimeout ms
164
+ Curses.timeout = ms
165
+ begin
166
+ yield
167
+ ensure
168
+ Curses.timeout = -1
169
+ end
170
+ end
171
+
172
+ def screen_status status, post = nil
173
+ Curses.setpos Curses.lines-1, 0
174
+ Curses.addstr ' '.ljust(Curses.cols)
175
+
176
+ standout{
177
+ Curses.setpos Curses.lines-1, 0
178
+ Curses.addstr status
179
+ }
180
+ Curses.addstr post if post
181
+ Curses.standend
182
+ end
183
+
184
+ LINE_ATTR = Curses::A_DIM
185
+
186
+ def render_data
187
+ # check update
188
+ c_lines = Curses.lines
189
+ c_cols = Curses.cols
190
+
191
+ if @rs != @last_rs
192
+ @last_rs = @rs.dup
193
+ else
194
+ return
195
+ end
196
+
197
+ Curses.clear
198
+
199
+ (c_lines-1).times{|i|
200
+ lno = i + self.y
201
+ line = @lines[lno]
202
+ cols = c_cols
203
+
204
+ unless line
205
+ if lno == @lines.size
206
+ Curses.setpos i, 0
207
+ cattr LINE_ATTR do
208
+ Curses.addstr '(END)'
209
+ end
210
+ end
211
+
212
+ break
213
+ end
214
+
215
+ Curses.setpos i, 0
216
+
217
+ if @rs.line_mode
218
+ cattr LINE_ATTR do
219
+ lineno = line.instance_variable_get(:@lineno)
220
+ ln_str = '%5d |' % lineno
221
+ if @rs.goto == lineno - 1 || (@rs.search && (@rs.search === line))
222
+ standout do
223
+ Curses.addstr(ln_str)
224
+ end
225
+ else
226
+ Curses.addstr(ln_str)
227
+ end
228
+ cols -= ln_str.size
229
+ end
230
+ end
231
+
232
+ line = line[self.x, cols] || ''
233
+
234
+ if !@rs.search || !(Regexp === @rs.search)
235
+ Curses.addstr line
236
+ else
237
+ partition(line, @rs.search).each{|(matched, str)|
238
+ if matched == :match
239
+ standout{
240
+ Curses.addstr str
241
+ }
242
+ else
243
+ Curses.addstr str
244
+ end
245
+ }
246
+ end
247
+ }
248
+ end
249
+
250
+ def search_str
251
+ if @rs.search
252
+ if str = @rs.search.instance_variable_get(:@search_str)
253
+ str
254
+ else
255
+ @rs.search.inspect
256
+ end
257
+ else
258
+ nil
259
+ end
260
+ end
261
+
262
+ def render_status
263
+ name = @name ? "<#{@name}>" : ''
264
+ mouse = @mouse ? ' [MOUSE]' : ''
265
+ search = @rs.search ? " search[#{search_str}]" : ''
266
+ loading = @loading ? " (loading...#{@load_unlimited ? '!' : nil}#{@following_mode ? ' following' : ''}) " : ''
267
+ x = self.x > 0 ? " x:#{self.x}" : ''
268
+ screen_status "#{name} lines:#{self.y+1}/#{@lines.size}#{x}#{loading}#{search}#{mouse}"
269
+ end
270
+
271
+ def check_update
272
+ if @loading == false
273
+ if @filename && File.exist?(@filename) && File.mtime(@filename) > @file_mtime
274
+ input = open(@filename)
275
+
276
+ if input.size < @file_lastpos
277
+ screen_status "#{@filename} is truncated. Rewinded."
278
+ pause
279
+ @lineno = 0
280
+ else
281
+ input.seek @file_lastpos
282
+ end
283
+ read_async input
284
+ end
285
+ end
286
+ end
287
+
288
+ def render_screen
289
+ ev = nil
290
+
291
+ ms = @following_mode ? 100 : 500
292
+
293
+ ctimeout ms do
294
+ while ev == nil
295
+ render_data
296
+ render_status
297
+ ev = Curses.getch
298
+ check_update
299
+ if @following_mode
300
+ if @rs.search && search_next_move(self.y + 1)
301
+ break
302
+ end
303
+ self.y = self.y_max
304
+ end
305
+ end
306
+
307
+ if @following_mode
308
+ @following_mode = false
309
+ @load_unlimited = false
310
+ end
311
+
312
+ return ev
313
+ end
314
+ end
315
+
316
+ def search_next_move start
317
+ (start...@lines.size).each{|i|
318
+ if @rs.search === @lines[i]
319
+ self.y = i
320
+ return true
321
+ end
322
+ }
323
+ return false
324
+ end
325
+
326
+ def search_next start
327
+ if search_next_move start
328
+ # OK
329
+ else
330
+ screen_status "not found: [#{self.search_str}]"
331
+ pause
332
+ end
333
+ end
334
+
335
+ def search_prev start
336
+ start.downto(0){|i|
337
+ if @rs.search === @lines[i]
338
+ self.y = i
339
+ return true
340
+ end
341
+ }
342
+ screen_status "not found: [#{self.search_str}]"
343
+ pause
344
+ end
345
+
346
+ def key_name ev
347
+ Curses.constants.grep(/KEY/){|c|
348
+ return c if Curses.const_get(c) == ev
349
+ }
350
+ ev
351
+ end
352
+
353
+ def input_str pattern, str = ''.dup, other_actions: nil
354
+ loop{
355
+ ev = Curses.getch
356
+
357
+ case ev
358
+ when 10
359
+ return str
360
+ when Curses::KEY_BACKSPACE
361
+ str.chop!
362
+ when pattern
363
+ str << ev
364
+ else
365
+ if other_actions && (action = other_actions[ev])
366
+ action.call(ev)
367
+ else
368
+ log "failure: #{key_name ev}"
369
+ return nil
370
+ end
371
+ end
372
+ }
373
+ end
374
+
375
+ def pause
376
+ ev = Curses.getch
377
+ Curses.ungetch ev if ev
378
+ end
379
+
380
+ def control_screen
381
+ ev = render_screen
382
+
383
+ case ev
384
+ when 'q'
385
+ raise KV_PopScreen
386
+
387
+ when Curses::KEY_UP, 'k'
388
+ self.y -= 1
389
+ when Curses::KEY_DOWN, 'j'
390
+ self.y += 1
391
+ when Curses::KEY_LEFT, 'h'
392
+ self.x -= 1
393
+ when Curses::KEY_RIGHT, 'l'
394
+ self.x += 1
395
+ when 'g'
396
+ self.y = 0
397
+ self.x = 0
398
+ when 'G'
399
+ self.y = self.y_max
400
+ self.x = 0
401
+ when ' ', Curses::KEY_NPAGE, Curses::KEY_CTRL_D
402
+ self.y += Curses.lines-1
403
+ when Curses::KEY_PPAGE, Curses::KEY_CTRL_U
404
+ self.y -= Curses.lines-1
405
+
406
+ when /[0-9]/
407
+ screen_status "Goto:", ev
408
+ ystr = input_str(/\d/, ev)
409
+ if ystr && !ystr.empty?
410
+ @rs.goto = ystr.to_i - 1
411
+ self.y = @rs.goto
412
+ end
413
+
414
+ when 'F'
415
+ @following_mode = true
416
+ @load_unlimited = true
417
+ @yq << true
418
+
419
+ when 'L'
420
+ @load_unlimited = !@load_unlimited
421
+ @yq << true
422
+
423
+ when '/'
424
+ search_str = ''.dup
425
+
426
+ update_search_status = -> do
427
+ regexp = @search_regexp ? 'regexp' : 'string'
428
+ ignore = @search_ignore_case ? '/ignore' : ''
429
+ screen_status "Search[#{regexp}#{ignore}]:", search_str
430
+ end
431
+
432
+ update_search_status[]
433
+ input_str(/./, search_str, other_actions: {
434
+ Curses::KEY_CTRL_I => -> ev do
435
+ @search_ignore_case = !@search_ignore_case
436
+ update_search_status[]
437
+ end,
438
+ Curses::KEY_CTRL_R => -> ev do
439
+ @search_regexp = !@search_regexp
440
+ update_search_status[]
441
+ end,
442
+ })
443
+
444
+ if search_str && !search_str.empty?
445
+ ic = @search_ignore_case ? [Regexp::IGNORECASE] : []
446
+ if @search_regexp
447
+ begin
448
+ @rs.search = Regexp.compile(search_str, *ic)
449
+ rescue RegexpError => e
450
+ @rs.search = nil
451
+ screen_status "regexp compile error: #{e.message}"
452
+ pause
453
+ end
454
+ else
455
+ @rs.search = Regexp.compile(Regexp.escape(search_str), *ic)
456
+ end
457
+ else
458
+ @rs.search = nil
459
+ end
460
+ if @rs.search
461
+ @rs.search.instance_variable_set(:@search_str, search_str)
462
+ search_next self.y
463
+ end
464
+ when 'n'
465
+ search_next self.y+1 if @rs.search
466
+ when 'p'
467
+ search_prev self.y-1 if @rs.search
468
+ when 'f'
469
+ if @rs.search
470
+ filter_mode_title = "*filter mode [#{self.search_str}]*"
471
+ if @name != filter_mode_title
472
+ lines = @lines.grep(@rs.search)
473
+ fscr = KV_Screen.new nil, lines: lines, search: @rs.search, name: filter_mode_title
474
+ raise KV_PushScreen.new(fscr)
475
+ end
476
+ end
477
+
478
+ when 's'
479
+ screen_status "Save file:"
480
+ file = input_str /./
481
+ begin
482
+ if file && !file.empty?
483
+ if File.exist? file
484
+ screen_status "#{file.dump} exists. Override? [y/n] "
485
+ yn = input_str(/[yn]/)
486
+ if yn == 'y'
487
+ File.write(file, @lines.join("\n"))
488
+ else
489
+ # do nothing
490
+ end
491
+ else
492
+ File.write(file, @lines.join("\n"))
493
+ end
494
+ end
495
+ rescue SystemCallError
496
+ # TODO: status line
497
+ end
498
+
499
+ when 'v'
500
+ if @filename
501
+ system("vi #{@filename} +#{self.y + 1}")
502
+ @last_rs = nil
503
+ end
504
+
505
+ when 'm'
506
+ @mouse = !@mouse
507
+ Curses.close_screen
508
+ init_screen
509
+ when Curses::KEY_MOUSE
510
+ m = Curses.getmouse
511
+ log m, "mouse ->"
512
+ # log [m.bstate, m.x, m.y, m.z, m.eid]
513
+ log @lines[self.y + m.y]
514
+
515
+ when 'N'
516
+ @rs.line_mode = !@rs.line_mode
517
+ when 't'
518
+ Curses.close_screen
519
+ @mode = :terminal
520
+
521
+ when '?'
522
+ raise KV_PushScreen.new(KV_Screen.new help_io)
523
+
524
+ when nil
525
+ # ignore
526
+
527
+ else
528
+ screen_status "unknown: #{key_name(ev)}"
529
+ pause
530
+ end
531
+ end
532
+
533
+ def control_terminal
534
+ @rs.instance_eval('binding').irb
535
+
536
+ @mode = :screen
537
+ init_screen
538
+ @last_rs = nil
539
+ end
540
+
541
+ def control
542
+ case @mode
543
+ when :screen
544
+ control_screen
545
+ when :terminal
546
+ control_terminal
547
+ else
548
+ raise
549
+ end
550
+ end
551
+ end
552
+
553
+ class KV
554
+ def initialize argv
555
+ @opts = {
556
+ following_mode: false,
557
+ first_line: 0,
558
+ line_mode: false,
559
+ }
560
+
561
+ files = parse_option(argv)
562
+
563
+ @pipe_in = nil
564
+
565
+ if files.empty?
566
+ if STDIN.isatty
567
+ input = help_io
568
+ name = 'HELP'
569
+ else
570
+ input = STDIN.dup
571
+ STDIN.reopen('/dev/tty')
572
+ name = nil
573
+ @pipe_in = input
574
+ end
575
+ else
576
+ name = files.shift
577
+ begin
578
+ input = open(name)
579
+ rescue
580
+ if /(.+):(\d+)/ =~ name
581
+ name = $1
582
+ @first_line = $2.to_i - 1
583
+ retry
584
+ end
585
+ raise
586
+ end
587
+ end
588
+
589
+ trap(:INT){
590
+ log "SIGINT"
591
+ }
592
+
593
+ @screens = [KV_Screen.new(input, name: name, **@opts)]
594
+ end
595
+
596
+ def parse_option argv
597
+ opts = OptionParser.new
598
+ opts.on('-f', 'following mode like "tail -f"'){
599
+ @opts[:following_mode] = true
600
+ }
601
+ opts.on('-n', '--line-number LINE', 'goto LINE'){|n|
602
+ @opts[:first_line] = n.to_i - 1
603
+ }
604
+ opts.on('-N', 'Show lines'){
605
+ @opts[:line_mode] = true
606
+ }
607
+ opts.parse!(argv)
608
+ end
609
+
610
+ def control
611
+ @screens.last.init_screen
612
+ until @screens.empty?
613
+ begin
614
+ @screens.last.control
615
+ rescue KV_PopScreen
616
+ @screens.pop
617
+ rescue KV_PushScreen => e
618
+ @screens.push e.screen
619
+ end
620
+ end
621
+ ensure
622
+ Curses.close_screen
623
+ log "terminate"
624
+ end
625
+ end
626
+ end
627
+
628
+ $debug_log = ENV['KV_DEBUG']
629
+
630
+ def log obj, prefix = ''
631
+ if $debug_log
632
+ File.open($debug_log, 'a'){|f|
633
+ f.puts "#{$$} #{prefix}#{obj.inspect}"
634
+ }
635
+ end
636
+ end
637
+
638
+ def partition str, search
639
+ results = []
640
+ loop{
641
+ r = str.match(search){|m|
642
+ break if m.post_match == str
643
+ results << [:unmatch, m.pre_match]
644
+ results << [:match, m.to_s]
645
+ str = m.post_match
646
+ }
647
+ break unless r
648
+ }
649
+ results << [:unmatch, str]
650
+ end
651
+
652
+ def help_io
653
+ readme = File.read(File.join(__dir__, '../README.md'))
654
+ help = []
655
+ readme.each_line{|line|
656
+ if /kv: A pager by Ruby Command list/ =~ line
657
+ help << line
658
+ elsif /^```/ =~ line && !help.empty?
659
+ break
660
+ elsif !help.empty?
661
+ help << line
662
+ end
663
+ }
664
+
665
+ help_io = StringIO.new(help.join)
666
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kv
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Koichi Sasada
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-02-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: curses
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ description: kv is a page viewer designed for streaming data written by Ruby.
28
+ email:
29
+ - ko1@atdot.net
30
+ executables:
31
+ - kv
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".gitignore"
36
+ - LICENSE
37
+ - README.md
38
+ - Rakefile
39
+ - exe/kv
40
+ - kv.gemspec
41
+ - lib/kv.rb
42
+ - lib/kv/version.rb
43
+ homepage: https://github.com/ko1/kv
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ homepage_uri: https://github.com/ko1/kv
48
+ source_code_uri: https://github.com/ko1/kv
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 2.3.0
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.2.0.pre1
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: 'kv: A page viewer written by Ruby'
68
+ test_files: []