kv 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: []