kv 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/LICENSE +25 -0
- data/README.md +100 -0
- data/Rakefile +10 -0
- data/exe/kv +11 -0
- data/kv.gemspec +31 -0
- data/lib/kv/version.rb +3 -0
- data/lib/kv.rb +666 -0
- 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
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
data/exe/kv
ADDED
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
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: []
|