cless 0.3.20

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.
@@ -0,0 +1 @@
1
+ # cless
@@ -0,0 +1,287 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ trap("SIGTERM") { exit }
4
+ trap("SIGINT") { exit } # Will be redefined
5
+ trap("SIGQUIT") { exit }
6
+ trap("SIGHUP") { exit }
7
+
8
+ require 'cless/cless'
9
+ require 'cless/version'
10
+ require 'optparse'
11
+ require 'yaml'
12
+
13
+ options = {
14
+ :column => false,
15
+ :col_start => 1,
16
+ :line => false,
17
+ :line_offset => false,
18
+ :col_space => 1,
19
+ :line_highlight => false,
20
+ :line_highlight_period => 2,
21
+ :line_highlight_shift => 0,
22
+ :col_highlight => false,
23
+ :col_highlight_period => 2,
24
+ :col_highlight_shift => 0,
25
+ :col_width => 50,
26
+ :parse_header => true,
27
+ :foreground => "none",
28
+ :background => "none",
29
+ :attribute => "bold",
30
+ :names => nil,
31
+ :profile => nil,
32
+ :formats => [],
33
+ :ignore => [],
34
+ :split_regexp => nil,
35
+ :line_highlight_regexp => nil,
36
+ :hide => [],
37
+ :tmp_dir => Dir.tmpdir,
38
+ :options_db => [],
39
+ :separator => " ",
40
+ :padding => " ",
41
+ :start_line => 1,
42
+ :header_line => nil,
43
+ :history_file => File.expand_path("~/.cless_history")
44
+ }
45
+ onoff = proc { |k| "(" + (options[k] ? "on" : "off") + ")" }
46
+ val = proc { |k| "(" + (options[k] || "none").to_s + ")" }
47
+
48
+ def die(msg, code = 1)
49
+ $stderr.puts(msg)
50
+ $stderr.puts("Use --help for detailed options")
51
+ exit(code)
52
+ end
53
+
54
+ opts = nil
55
+ loop do
56
+ again = false
57
+ opts = OptionParser.new do |opts|
58
+ opts.banner = "Usage: cless [options] [file]\n" +
59
+ "Column oriented less-like pager\n" +
60
+ "Options: (default values in parentheses)"
61
+
62
+ opts.on("--[no-]column", "Display column number #{onoff[:column]}") { |v|
63
+ options[:column] = v
64
+ }
65
+ opts.on("--[no-]line", "Display line number #{onoff[:line]}") { |v|
66
+ options[:line] = v
67
+ }
68
+ opts.on("--[no-]offset", "Display offset instead of line number " +
69
+ onoff[:line_offset]) { |v|
70
+ options[:line_offset] = v
71
+ }
72
+ opts.on("--[no-]line-highlight", "Hilight every other line " +
73
+ onoff[:line_highlight]) { |v|
74
+ options[:line_highlight] = v
75
+ }
76
+ opts.on("--line-period PERIOD", "Hilight period for lines " +
77
+ val[:line_highlight_period]) { |v|
78
+ options[:line_highlight_period] = v.to_i
79
+ }
80
+ opts.on("--line-highlight-regexp REGEXP",
81
+ "Hilight line on regular expression" +
82
+ val[:line_highlight_regexp]) { |v|
83
+ options[:line_highlight_regexp] = Regexp.new(v)
84
+ }
85
+ opts.on("--line-shift SHIFT", "Hilight shift for lines " +
86
+ val[:line_highlight_shift]) { |v|
87
+ options[:line_highlight_shift] = v.to_i
88
+ }
89
+ opts.on("--[no-]column-highlight", "Hilight every other column " +
90
+ onoff[:col_highlight]) { |v|
91
+ options[:col_highlight] = v
92
+ }
93
+ opts.on("--column-period PERIOD", "Hilight period for columns " +
94
+ val[:col_highlight_period]) { |v|
95
+ options[:col_highlight_period] = v.to_i
96
+ }
97
+ opts.on("--column-shift SHIFT", "Hilight shift for columns " +
98
+ val[:col_highlight_shift]) { |v|
99
+ options[:col_highlight_shift] = v.to_i
100
+ }
101
+ opts.on("--[no-]column-names", "Display column names " +
102
+ "#{onoff[:col_names]}") { |v|
103
+ options[:col_names] = v
104
+ }
105
+ opts.on("--[no-]parse-header", "Parse header for options " +
106
+ "#{onoff[:parse_header]}") { |v|
107
+ options[:parse_header] = v
108
+ }
109
+ opts.on("--foreground COLOR", "Foreground color for hilight #{val[:foreground]}") { |v|
110
+ options[:foreground] = v
111
+ }
112
+ opts.on("--background COLOR", "Background color for hilight #{val[:background]}") { |v|
113
+ options[:background] = v
114
+ }
115
+ opts.on("--column-space NB", "Number of spaces between columns " +
116
+ val[:col_space]) { |v|
117
+ options[:col_space] = v.to_i
118
+ }
119
+ opts.on("--attribute ATTR", "Attribute for hilight #{val[:attribute]}") { |v|
120
+ options[:attribute] = v
121
+ }
122
+ opts.on("--hide COLUMNS",
123
+ "Comma separated list of columns to hide") { |v|
124
+ a = v.split(/[,\s]+/).collect { |x| x.to_i }
125
+ a.delete_if { |x| x < 0 }
126
+ options[:hide] += a
127
+ }
128
+ opts.on("--column-start INDEX", Integer,
129
+ "first column index #{val[:col_start]}") { |v|
130
+ options[:col_start] = v
131
+ }
132
+ opts.on("--col-width WIDTH", Integer,
133
+ "default maximum column width") { |v|
134
+ options[:col_width] = [v, 5].max
135
+ }
136
+ opts.on("--start-line LINE", Integer,
137
+ "first line to display #{val[:start_line]}") { |v|
138
+ options[:start_line] = v
139
+ }
140
+ opts.on("--names NAMES", "Comma separated list of column names") { |v|
141
+ options[:names] = v.split_with_quotes("\s,")
142
+ }
143
+ opts.on("--header-line LINE", Integer,
144
+ "use given line as headers") { |v|
145
+ options[:header_line] = v.to_i
146
+ }
147
+ opts.on("--options-db DB", "Path of a options db") { |v|
148
+ options[:options_db] << v
149
+ }
150
+ opts.on("--format FORMAT", "Format for a column") { |v|
151
+ options[:formats] << v
152
+ }
153
+ opts.on("--ignore PATTERN", "Line to ignore") { |v|
154
+ options[:ignore] << v
155
+ }
156
+ opts.on("--split-regexp REGEXP", "Regular expression to split lines") { |v|
157
+ v.gsub!(%r{^/|/$}, '')
158
+ options[:split_regexp] = Regexp.new(v)
159
+ }
160
+ opts.on("--profile PROFILE", "Option profile") { |v|
161
+ options[:profile] = v
162
+ }
163
+ opts.on("-T", "--tmp-dir DIR", "Temporary directory #{val[:tmp_dir]}") { |v|
164
+ options[:tmp_dir] = v
165
+ }
166
+ opts.on("--separator SEP", "Separator caracter between columns " +
167
+ "(#{val[:separator]}") { |v|
168
+ v = v[0, 1]
169
+ options[:separator] = v unless v.empty?
170
+ }
171
+ opts.on("--padding PAD", "Padding caracter (#{val[:padding]})") { |v|
172
+ options[:padding] = v
173
+ }
174
+ opts.on_tail("-v", "--version", "Show version and exit") {
175
+ puts(Version.join('.'))
176
+ exit(0)
177
+ }
178
+ opts.on_tail("-h", "--help", "This message") {
179
+ puts(opts)
180
+ exit(0)
181
+ }
182
+ end
183
+
184
+ if ENV["CLESS"]
185
+ args = ENV["CLESS"].split_with_quotes
186
+ ENV["CLESS"] = nil
187
+ again = true
188
+ else
189
+ args = ARGV
190
+ end
191
+ begin
192
+ opts.parse!(args)
193
+ rescue => e
194
+ die("Error (#{e.class}): #{e.message}")
195
+ end
196
+ break unless again
197
+ end
198
+
199
+ # Move around the file descriptor if not a tty!
200
+ if !$stdout.tty?
201
+ exec("cat", *ARGV)
202
+ end
203
+ if ARGV.empty? && $stdin.tty?
204
+ die("Cannot read from data tty")
205
+ end
206
+ if !$stdin.tty?
207
+ stdin = $stdin.dup
208
+ $stdin.reopen("/dev/fd/1", "r")
209
+ end
210
+
211
+ class KeyboardInterrupt < StandardError; end
212
+
213
+ ptr = if ARGV.empty?
214
+ MappedStream.new(stdin, options)
215
+ else
216
+ first = $have_mmap
217
+ begin
218
+ first ? MappedFile.new(ARGV[0]) :
219
+ MappedStream.new(open(ARGV[0]), options)
220
+ rescue => e
221
+ if first
222
+ first = false
223
+ retry
224
+ else
225
+ die("Error opening file '#{ARGV[0]}': #{e.message}")
226
+ end
227
+ end
228
+ end
229
+ $interrupt = Interrupt.new
230
+
231
+ begin
232
+ # Finish parsing options
233
+ db = OptionsDB.new
234
+ options[:options_db].each do |f|
235
+ begin
236
+ db.parse_file(f)
237
+ rescue => e
238
+ $stderr.puts("Error with db #{f}: #{e.message}")
239
+ end
240
+ end
241
+ if a = db[options[:profile]] || a = db.match(ARGV[0])
242
+ begin
243
+ opts.parse(a)
244
+ rescue => e
245
+ $stderr.puts("Error with options from db: #{e.message}")
246
+ end
247
+ end
248
+ header_l = nil
249
+ if options[:parse_header]
250
+ header_l, a = ptr.parse_header(["profile", "names", "format", "ignore"])
251
+ opts.parse(a)
252
+ opts = nil
253
+ end
254
+
255
+ data = MapData.new(ptr, options[:split_regexp])
256
+ display = LineDisplay.new(data, options)
257
+ manager = Manager.new(data, display, db)
258
+ manager.load_history(options[:history_file]) if options[:history_file]
259
+ trap("SIGINT") { $interrupt.raise }
260
+ data.cache_fill(1)
261
+
262
+ data.highlight_regexp = options[:line_highlight_regexp]
263
+ display.col_headers = options[:names]
264
+ display.col_hide(*options[:hide])
265
+ options[:formats].each { |fmt| manager.column_format_inline(fmt) }
266
+ options[:ignore].each { |pat| manager.ignore_line(pat) }
267
+ manager.ignore_line("1-#{header_l}") if header_l && header_l > 0
268
+ if options[:header_line]
269
+ begin
270
+ manager.change_headers_to_line(options[:header_line])
271
+ rescue => e
272
+ $stderr.puts("--header-line #{options[:header_line]}: #{e.message}")
273
+ end
274
+ end
275
+ data.goto_line(options[:start_line])
276
+
277
+ # Start curses application
278
+ Curses.new do |curses|
279
+ display.initialize_curses
280
+ manager.main_loop
281
+ end
282
+ rescue KeyboardInterrupt
283
+ # Quit normally
284
+ ensure
285
+ ptr.munmap
286
+ manager.save_history(options[:history_file]) unless manager.nil?
287
+ end
@@ -0,0 +1,3 @@
1
+ def assert(msg = "")
2
+ yield or raise "Assert failed: #{msg}"
3
+ end
@@ -0,0 +1,816 @@
1
+ #require 'ncurses'
2
+ require 'rubygems'
3
+ require 'ncursesw'
4
+ begin
5
+ require 'mmap'
6
+ $have_mmap = true
7
+ rescue LoadError
8
+ $have_mmap = false
9
+ end
10
+ require 'tempfile'
11
+
12
+ require 'cless/data'
13
+ require 'cless/display'
14
+ require 'cless/optionsdb'
15
+ require 'cless/export'
16
+ require 'cless/help'
17
+
18
+ # For short :)
19
+ NC = Ncurses
20
+ C = Curses
21
+
22
+ def select_or_cancel(*fds)
23
+ ifds = [$stdin] + fds.dup
24
+ loop {
25
+ ofds = select(ifds)[0]
26
+ if ofds.delete($stdin)
27
+ return nil if Ncurses.getch == C::ESC
28
+ end
29
+ return ofds unless ofds.empty?
30
+ }
31
+ end
32
+
33
+ class String
34
+ def split_with_quotes(sep = '\s', q = '\'"')
35
+ r = / \G(?:^|[#{sep}]) # anchor the match
36
+ (?: [#{q}]((?>[^#{q}]*)(?>""[^#{q}]*)*)[#{q}] # find quoted fields
37
+ | # ... or ...
38
+ ([^#{q}#{sep}]*) # unquoted fields
39
+ )/x
40
+ self.split(r).delete_if { |x| x.empty? }
41
+ end
42
+ end
43
+
44
+ class Interrupt
45
+ def initialize; @raised = false; end
46
+ def raise; @raised = true; end
47
+ def reset; r, @raised = @raised, false; r; end
48
+ end
49
+
50
+ class Manager
51
+ class Error < StandardError; end
52
+
53
+ Commands = {
54
+ "scroll_forward_line" => :scroll_forward_line,
55
+ "scroll_backward_line" => :scroll_backward_line,
56
+ "scroll_forward_half_screen" => :scroll_forward_half_screen,
57
+ "scroll_backward_half_screen" => :scroll_backward_half_screen,
58
+ "scroll_right" => :scroll_right,
59
+ "scroll_left" => :scroll_left,
60
+ "hide_columns" => :hide_columns_prompt,
61
+ "unhide_columns" => :unhide_columns,
62
+ "toggle_hide_ignored" => :toggle_hide_ignored,
63
+ "toggle_headers" => :show_hide_headers,
64
+ "change_headers_to_line" => :change_headers_to_line_content_prompt,
65
+ "toggle_line_highlight" => :toggle_line_highlight,
66
+ "toggle_column_highlight" => :toggle_column_highlight,
67
+ "shift_line_highlight" => :shift_line_highlight,
68
+ "shift_column_highlight" => :shift_column_highlight,
69
+ "regexp_line_highlight" => :regexp_line_highlight_prompt,
70
+ "column_align_left" => :column_align_left,
71
+ "column_align_right" => :column_align_right,
72
+ "column_align_center" => :column_align_center,
73
+ "column_align_auto" => :column_align_auto,
74
+ "right_align_regexp" => :right_align_regexp_prompt,
75
+ "column_width" => :column_width_prompt,
76
+ "forward_search" => :forward_search,
77
+ "backward_search" => :backward_search,
78
+ "repeat_search" => :repeat_search,
79
+ "save_to_file" => :save_file_prompt,
80
+ "goto_position" => :goto_position_prompt,
81
+ "format_column" => :column_format_prompt,
82
+ "column_start_index" => :change_column_start_prompt,
83
+ "ignore_line" => :ignore_line_prompt,
84
+ "remove_ignore_line" => :ignore_line_remove_prompt,
85
+ "split_regexp" => :change_split_pattern_prompt,
86
+ "separator_character" => :change_separator_prompt,
87
+ "separator_padding" => :change_padding_prompt,
88
+ "export" => :export_prompt,
89
+ "help" => :display_help,
90
+ }
91
+
92
+ attr_accessor :max_search_history, :search_history
93
+ def initialize(data, display, db)
94
+ @data = data
95
+ @display = display
96
+ @db = db
97
+ @done = false
98
+ @status = ""
99
+ @prebuff = ""
100
+ @half_screen_lines = nil
101
+ @full_screen_lines = nil
102
+ @scroll_columns = nil
103
+ @interrupt = false
104
+ @search_history = []
105
+ @max_search_history = 100
106
+ end
107
+
108
+ def done; @done = true; end
109
+ def interrupt_set; @interrupt = true; end
110
+ def interrupt_reset; i, @interrupt = @interrupt, false; i; end
111
+
112
+ def ttyname
113
+ [Proc.new { File.readlink("/proc/self/fd/0") },
114
+ Proc.new { `tty`.chomp }].each { |m|
115
+ begin
116
+ return m.call
117
+ rescue Errno::ENOENT
118
+ end
119
+ }
120
+ return "/dev/unknown"
121
+ end
122
+
123
+ def load_history(file)
124
+ tty = ttyname
125
+ history = File.open(file, "r") { |fd|
126
+ fd.flock(File::LOCK_SH)
127
+ YAML::load(fd)
128
+ }
129
+ history = {} unless Hash === history
130
+ history["tty"] = {} unless Hash === history["tty"]
131
+ tty_history = history["tty"][tty] || history["tty"][history["recent_tty"]] || {}
132
+ @search_history = tty_history["search"] || []
133
+ rescue Errno::EACCES
134
+ rescue Errno::ENOENT
135
+ end
136
+
137
+ def save_history(file)
138
+ tty = ttyname
139
+ File.open(file, File::RDWR|File::CREAT, 0644) { |fd|
140
+ fd.flock(File::LOCK_EX)
141
+
142
+ history = YAML::load(fd)
143
+ history = {} unless Hash === history
144
+ history["tty"] = {} unless Hash === history["tty"]
145
+ history["tty"][tty] = {} unless Hash === history["tty"][tty]
146
+ history["tty"][tty]["search"] = @search_history
147
+ history["recent_tty"] = tty
148
+
149
+ fd.rewind
150
+ fd.print(history.to_yaml)
151
+ fd.flush
152
+ fd.truncate(fd.pos)
153
+ }
154
+ rescue Errno::EACCES
155
+ rescue Errno::ENOENT
156
+ end
157
+
158
+ def main_loop
159
+ if @status.empty?
160
+ @status = "Help? Press ~ or F1"
161
+ end
162
+ while !@done do
163
+ @data.cache_fill(@display.nb_lines)
164
+ @display.refresh
165
+ wait_for_key or break
166
+ end
167
+ end
168
+
169
+ def prebuff; @prebuff.empty? ? nil : @prebuff.to_i; end
170
+
171
+ def wait_for_key
172
+ status = nil
173
+ esc = false
174
+ while !@done do
175
+ nc = false # Set to true if no data change
176
+ data_fd = @data.select_fd(@display.nb_lines)
177
+ prompt = data_fd ? "+:" : ":"
178
+ @display.wait_status(@status, prompt + @prebuff)
179
+ if data_fd
180
+ @display.flush
181
+ in_fds = select([$stdin, data_fd])
182
+ if in_fds[0].include?(data_fd)
183
+ status = :more
184
+ nc = true
185
+ break
186
+ end
187
+ end
188
+
189
+ k = Ncurses.getch
190
+ status =
191
+ case k
192
+ when NC::KEY_DOWN, NC::KEY_ENTER, C::CTRL_N, ?e.ord, C::CTRL_E, ?j.ord, C::CTRL_J, ?\n.ord, ?\r.ord
193
+ scroll_forward_line
194
+ when NC::KEY_UP, ?y.ord, C::CTRL_Y, C::CTRL_P, ?k.ord, C::CTRL_K
195
+ scroll_backward_line
196
+ when ?d.ord, C::CTRL_D; scroll_forward_half_screen(true)
197
+ when ?u.ord, C::CTRL_U; scroll_backward_half_screen(true)
198
+ when C::SPACE, NC::KEY_NPAGE, C::CTRL_V, ?f.ord, C::CTRL_F; scroll_forward_full_screen
199
+ when ?z.ord; scroll_forward_full_screen(true)
200
+ when NC::KEY_PPAGE, ?b.ord, C::CTRL_B; scroll_backward_full_screen
201
+ when ?w.ord; scroll_backward_full_screen(true)
202
+ when NC::KEY_HOME, ?g.ord, ?<.ord; goto_line(0)
203
+ when NC::KEY_END, ?G.ord, ?>.ord; goto_line(-1)
204
+ when NC::KEY_LEFT; scroll_left
205
+ when NC::KEY_RIGHT; scroll_right
206
+ when ?+.ord; @display.col_space += 1; true
207
+ when ?-.ord; @display.col_space -= 1; true
208
+ when ?F.ord; goto_position_prompt
209
+ when ?v.ord; column_format_prompt
210
+ when ?i.ord; ignore_line_prompt
211
+ when ?I.ord; ignore_line_remove_prompt
212
+ when ?o.ord; toggle_line_highlight
213
+ when ?O.ord; toggle_column_highlight
214
+ when ?m.ord; shift_line_highlight
215
+ when ?M.ord; shift_column_highlight
216
+ when ?c.ord; @display.column = !@display.column; true
217
+ when ?l.ord; @display.line = !@display.line; true
218
+ when ?L.ord; @display.line_offset = !@display.line_offset; true
219
+ when ?h.ord; hide_columns_prompt
220
+ when ?H.ord; hide_columns_prompt(:show)
221
+ when ?A.ord; column_alignment(:right)
222
+ when ?a.ord; column_alignment(:left)
223
+ when ?`.ord; change_column_start_prompt
224
+ when ?).ord; esc ? scroll_right : column_width_increase
225
+ when ?(.ord; esc ? scroll_left : column_width_decrease
226
+ when ?/.ord; search_prompt(:forward)
227
+ when ??.ord; search_prompt(:backward)
228
+ when ?n.ord; repeat_search
229
+ when ?N.ord; repeat_search(true)
230
+ when ?s.ord; save_file_prompt
231
+ when ?S.ord; change_split_pattern_prompt
232
+ when ?E.ord; export_prompt
233
+ when ?t.ord; show_hide_headers
234
+ when ?p.ord, ?%.ord; goto_percent
235
+ when ?|.ord; change_separator_prompt
236
+ when ?\\.ord; change_padding_prompt
237
+ when ?^.ord; change_headers_to_line_content_prompt
238
+ when ?r.ord, ?R.ord, C::CTRL_R, C::CTRL_L; @data.clear_cache; NC::endwin; NC::doupdate
239
+ when NC::KEY_RESIZE; nc = true # Will break to refresh display
240
+ when NC::KEY_F1, ?~.ord; display_help
241
+ when (?0.ord)..(?9.ord); @prebuff += k.chr; next
242
+ when NC::KEY_BACKSPACE, ?\b.ord; esc ? @prebuff = "" : @prebuff.chop!; next
243
+ when ?:.ord; long_command
244
+ when ?q.ord; return nil
245
+ when C::ESC; esc = true; next
246
+ when NC::KEY_SLEFT, ?[.ord; column_offset_left
247
+ when NC::KEY_SRIGHT, ?].ord; column_offset_right
248
+ when ?{.ord; column_offset_start
249
+ when ?}.ord; column_offset_end
250
+ else next
251
+ end
252
+ break
253
+ end
254
+
255
+ @prebuff = "" unless nc
256
+ @status =
257
+ case status
258
+ when String; status
259
+ when nil; "Cancelled"
260
+ when :more; @status
261
+ else ""
262
+ end
263
+ return true
264
+ end
265
+
266
+ # This is a little odd. Does it belong to display more?
267
+ def long_command
268
+ sub = CommandSubWindow.new(Commands.keys.map { |s| s.size }.max)
269
+ old_prompt_line = ""
270
+ sub.new_list(Commands.keys.sort)
271
+ extra = proc {
272
+ if old_prompt_line != @display.prompt_line
273
+ old_prompt_line = @display.prompt_line.dup
274
+ reg = Regexp.new(Regexp.quote(old_prompt_line))
275
+ sub.new_list(Commands.keys.grep(reg).sort)
276
+ Ncurses.refresh
277
+ end
278
+ }
279
+ other = proc { |ch|
280
+ r = true
281
+ case ch
282
+ when NC::KEY_DOWN, C::CTRL_N; sub.next_item
283
+ when NC::KEY_UP, C::CTRL_P; sub.previous_item
284
+ else r = false
285
+ end
286
+ Ncurses.refresh if r
287
+ }
288
+ s = @display.prompt("Filter: ", :extra => extra, :other => other)
289
+ sub.destroy
290
+ @display.refresh
291
+ self.__send__(Commands[sub.item]) if s
292
+ end
293
+
294
+ def str_to_range(str)
295
+ str.split_with_quotes().map { |r|
296
+ case r
297
+ when /^(\d+)$/
298
+ $1.to_i
299
+ when /^(\d+)(?:\.{2,3}|-)(\d+)$/
300
+ (($1.to_i)..($2.to_i)).to_a
301
+ else raise "Invalid range: #{r}"
302
+ end
303
+ }.flatten
304
+ end
305
+
306
+ def range_prompt(prompt)
307
+ s = @display.prompt(prompt) or return nil
308
+ str_to_range(s)
309
+ end
310
+
311
+ def scroll_forward_line
312
+ @data.scroll(prebuff || 1)
313
+ true
314
+ end
315
+
316
+ def scroll_backward_line
317
+ @data.scroll(-(prebuff || 1))
318
+ true
319
+ end
320
+
321
+ def scroll_forward_half_screen(save = false)
322
+ @half_screen_lines = prebuff if save && prebuff
323
+ @data.scroll(prebuff || @half_screen_lines || (@display.nb_lines / 2))
324
+ true
325
+ end
326
+
327
+ def scroll_backward_half_screen(save = false)
328
+ @half_screen_lines = prebuff if save && prebuff
329
+ @data.scroll(-(prebuff || @half_screen_lines || (@display.nb_lines / 2)))
330
+ true
331
+ end
332
+
333
+ def scroll_forward_full_screen(save = false)
334
+ @full_screen_lines = prebuff if save && prebuff
335
+ @data.scroll(prebuff || @full_screen_lines || (@display.nb_lines - 1))
336
+ true
337
+ end
338
+
339
+ def scroll_backward_full_screen(save = false)
340
+ @full_screen_lines = prebuff if save && prebuff
341
+ @data.scroll(-(prebuff || @full_screen_lines || (@display.nb_lines - 1)))
342
+ true
343
+ end
344
+
345
+ def scroll_sideways(dir)
346
+ @scroll_columns = prebuff if prebuff
347
+ to_scroll = @scroll_columns || 1
348
+ st_col = @display.st_col
349
+ to_scroll.times { |i|
350
+ st_col += dir
351
+ redo if @display.col_hidden(false).index(st_col)
352
+ }
353
+ @display.st_col = st_col
354
+ true
355
+ end
356
+
357
+ def scroll_left; scroll_sideways(-1); end
358
+ def scroll_right; scroll_sideways(1); end
359
+
360
+ def column_offset_sideways(dir)
361
+ if prebuff && prebuff >= @display.col_start
362
+ @offset_column = prebuff - @display.col_start
363
+ end
364
+ return if @offset_column.nil?
365
+ off = @display.col_offsets[@offset_column] || 0
366
+ off = [off + dir, 0].max
367
+ @display.col_offsets[@offset_column] = off
368
+ end
369
+
370
+ def column_offset_right; column_offset_sideways(1); end
371
+ def column_offset_left; column_offset_sideways(-1); end
372
+ def column_offset_start
373
+ if prebuff && prebuff >= @display.col_start
374
+ @offset_column = prebuff - @display.col_start
375
+ end
376
+ return if @offset_column.nil?
377
+ @display.col_offsets[@offset_column] = 0
378
+ end
379
+
380
+ def column_offset_end
381
+ if prebuff && prebuff >= @display.col_start
382
+ @offset_column = prebuff - @display.col_start
383
+ end
384
+ return if @offset_column.nil?
385
+ @display.col_offsets[@offset_column] =
386
+ [0, @data.sizes[@offset_column] - (@display.widths[@offset_column] || @display.col_width)].max
387
+ end
388
+
389
+ def goto_line(l)
390
+ if l == 0
391
+ return @data.goto_start ? "" : "Start of file"
392
+ elsif l < 0
393
+ @display.start_active_status("Skipping to end of file")
394
+ return @data.goto_end ? "" : "End of file"
395
+ else
396
+ @display.start_active_status("Skipping to line #{l}")
397
+ @data.goto_line(prebuff)
398
+ end
399
+ true
400
+ ensure
401
+ @display.end_active_status
402
+ end
403
+
404
+ def unhide_columns; hide_columns_prompt(true); end
405
+ def hide_columns_prompt(show = false)
406
+ i = prebuff
407
+ a = i ? [i] : range_prompt(show ? "Show: " : "Hide: ") or return nil
408
+ if a.empty?
409
+ @display.col_hide_clear
410
+ else
411
+ show ? @display.col_show(*a) : @display.col_hide(*a)
412
+ end
413
+ "Hidden: #{@display.col_hidden.join(" ")}"
414
+ rescue => e
415
+ return e.message
416
+ end
417
+
418
+ def toggle_hide_ignored; @display.hide_ignored = !@display.hide_ignored; end
419
+
420
+ def column_align_left; column_alignment(:left); end
421
+ def column_align_right; column_alignment(:right); end
422
+ def column_align_center; column_alignment(:center); end
423
+ def column_align_auto; column_alignment(nil); end
424
+ def column_alignment(align)
425
+ i = prebuff
426
+ a = i ? [i] : range_prompt("Columns to #{align || "auto"} align: ") or return nil
427
+ return if a.empty?
428
+ @display.col_align(align, a)
429
+ end
430
+
431
+ def column_width_prompt
432
+ i = prebuff
433
+ a = i ? [i] : range_prompt("Width of columns: ") or return nil
434
+ return nil if a.empty?
435
+ s = @display.prompt("Max width: ") or return nil
436
+ s = [s.to_i, 5].max
437
+ a.map { |x| @display.widths[x] = s }
438
+ end
439
+
440
+ def column_width_change(x)
441
+ if prebuff && prebuff >= @display.col_start
442
+ @offset_column = prebuff - @display.col_start
443
+ end
444
+ return if @offset_column.nil?
445
+ w = (@display.widths[@offset_column] || @display.col_width) + x
446
+ w = 5 if w < 5
447
+ @display.widths[@offset_column] = w
448
+ end
449
+
450
+ def column_width_increase; column_width_change(1); end
451
+ def column_width_decrease; column_width_change(-1); end
452
+
453
+ def show_hide_headers
454
+ return "No names defined" if !@display.col_names && !@display.col_headers
455
+ @display.col_names = !@display.col_names
456
+ true
457
+ end
458
+
459
+ def change_headers_to_line_content_prompt
460
+ i = prebuff
461
+ if i.nil?
462
+ i = @data.line + 1
463
+ s = @display.prompt("Header line: ", :init => i.to_s) or return nil
464
+ s.strip!
465
+ return "Bad line number #{s}" unless s =~ /^\d+$/
466
+ i = s.to_i
467
+ end
468
+ begin
469
+ change_headers_to_line(i)
470
+ rescue => e
471
+ return e.message
472
+ end
473
+ true
474
+ end
475
+
476
+ def toggle_line_highlight
477
+ i = prebuff
478
+ @display.line_highlight = !@display.line_highlight
479
+ @display.line_highlight_period = i if i
480
+ true
481
+ end
482
+
483
+ def toggle_column_highlight
484
+ i = prebuff
485
+ @display.col_highlight = !@display.col_highlight
486
+ @display.col_highlight_period = i if i
487
+ true
488
+ end
489
+
490
+ def shift_line_highlight
491
+ if i = prebuff
492
+ @display.line_highlight_shift = i
493
+ else
494
+ @display.line_highlight_shift += 1
495
+ end
496
+ end
497
+
498
+ def shift_column_highlight
499
+ if i = prebuff
500
+ @display.col_highlight_shift = i
501
+ else
502
+ @display.col_highlight_shift += 1
503
+ end
504
+ end
505
+
506
+ def change_headers_to_line(i)
507
+ raise Error, "Bad line number #{i}" if i < 1
508
+ i_bak = @data.line + 1
509
+ @data.goto_line(i)
510
+ @data.cache_fill(1)
511
+ line = nil
512
+ @data.lines(1) { |l| line = l }
513
+ @data.goto_line(i_bak) # Go back
514
+ raise Error, "No such line" unless line
515
+ raise Error, "Ignored line: can't use" if line.kind_of?(IgnoredLine)
516
+ @display.col_headers = line.onl_at(0..-1)
517
+ @display.col_names = true
518
+ true
519
+ end
520
+
521
+ # Return a status if an error occur, otherwise, returns nil
522
+ def forward_search; search_prompt(:forward); end
523
+ def backward_search; search_prompt(:backward); end
524
+ def search_prompt(dir = :forward)
525
+ s = @display.prompt("%s Search: " %
526
+ [(dir == :forward) ? "Forward" : "Backward"],
527
+ { :history => @search_history })
528
+ s or return nil
529
+ if s =~ /^\s*$/
530
+ @data.search_clear
531
+ else
532
+ begin
533
+ @display.start_active_status("Searching '#{s}'")
534
+
535
+ hist_index = @search_history.index(s)
536
+ @search_history.slice!(hist_index) if hist_index
537
+ @search_history.unshift(s)
538
+ @search_history = @search_history[0, @max_search_history]
539
+ begin
540
+ @search_dir = dir
541
+ pattern = Regexp.new(s)
542
+ @data.search(pattern, dir) or return "Pattern not found!"
543
+ rescue RegexpError => e
544
+ return "Bad attr_reader :egexp: #{e.message}"
545
+ end
546
+ ensure
547
+ @display.end_active_status
548
+ end
549
+ end
550
+ true
551
+ end
552
+
553
+ # Return a status if an error occur, otherwise, returns nil
554
+ def repeat_search(reverse = false)
555
+ return "No pattern" if !@data.pattern
556
+ dir = if reverse
557
+ (@search_dir == :forward) ? :backward : :forward
558
+ else
559
+ @search_dir
560
+ end
561
+ @data.repeat_search(dir) or return "Pattern not found!"
562
+ true
563
+ end
564
+
565
+ def color_descr
566
+ descr = @display.attr_names
567
+ "Color: F %s B %s A %s" %
568
+ [descr[:foreground] || "-", descr[:background] || "-", descr[:attribute] || "-"]
569
+ end
570
+
571
+ def save_file_prompt
572
+ s = @display.prompt("Save to: ")
573
+ return nil if !s || s.empty?
574
+ if @data.file_path
575
+ begin
576
+ File.link(@data.file_path, s)
577
+ return "Hard linked"
578
+ rescue Errno::EXDEV => e
579
+ rescue Exception => e
580
+ return "Error: #{e.message}"
581
+ end
582
+ end
583
+
584
+ # Got here, hard link failed. Copy by hand.
585
+ nb_bytes = nil
586
+ begin
587
+ File.open(s, File::WRONLY|File::CREAT|File::EXCL) do |fd|
588
+ nb_bytes = @data.write_to(fd)
589
+ end
590
+ rescue Exception => e
591
+ return "Error: #{e.message}"
592
+ end
593
+ "Wrote #{nb_bytes} bytes"
594
+ end
595
+
596
+ def goto_percent
597
+ percent = prebuff or return true
598
+ @data.goto_percent(percent)
599
+ end
600
+
601
+ def goto_position_prompt
602
+ s = prebuff || @display.prompt("Goto: ") or return nil
603
+ s = s.to_s.strip
604
+
605
+ case s[-1]
606
+ when "p", "%"
607
+ s.slice!(-1)
608
+ f = s.to_f
609
+ return "Invalid percentage" if f <= 0.0 || f > 100.0
610
+ @display.start_active_status("Goto %d%%" % f.round)
611
+ @data.goto_percent(f)
612
+ when "o"
613
+ s.slice!(-1)
614
+ i = s.to_i
615
+ return "Invalid offset" if i < 0
616
+ @display.start_active_status("Goto offset #{i}")
617
+ @data.goto_offset(i)
618
+ else
619
+ i = s.to_i
620
+ return "Invalid line number" if i <= 0
621
+ @display.start_active_status("Goto line #{i}")
622
+ @data.goto_line(i)
623
+ end
624
+ true
625
+ ensure
626
+ @display.end_active_status
627
+ end
628
+
629
+ def column_format_prompt
630
+ i = prebuff
631
+ cols = i ? [i] : range_prompt("Format columns: ") or return nil
632
+ fmt = @display.prompt("Format string: ") or return nil
633
+ fmt.strip!
634
+ column_format(cols, fmt)
635
+ rescue => e
636
+ return e.message
637
+ end
638
+
639
+ def column_format_inline(str)
640
+ cols, fmt = str.split(/:/, 2)
641
+ cols = str_to_range(cols)
642
+ column_format(cols, fmt)
643
+ end
644
+
645
+ def column_format(cols, fmt)
646
+ inc = @display.col_start
647
+ if cols
648
+ cols = cols.map { |x| x.to_i - inc }
649
+ cols.delete_if { |x| x < 0 }
650
+ if fmt && !fmt.empty?
651
+ @data.set_format_column(fmt, *cols)
652
+ else
653
+ cols.each { |c| @data.unset_format_column(c) }
654
+ end
655
+ @data.refresh
656
+ end
657
+ cols = @data.formatted_column_list.sort.collect { |x| x + inc }
658
+ "Formatted: " + cols.join(" ")
659
+ end
660
+
661
+ def change_column_start_prompt
662
+ s = prebuff
663
+ s = @display.prompt("First column: ") unless s
664
+ return nil unless s
665
+ @display.col_start = s.to_i
666
+ true
667
+ end
668
+
669
+ def ignore_line_prompt
670
+ s = @display.prompt("Ignore: ") or return nil
671
+ s.strip!
672
+ ignore_line(s)
673
+ end
674
+
675
+ def ignore_line_list_each(str)
676
+ a = str.split_with_quotes('\s', '\/')
677
+ a.each do |spat|
678
+ opat = case spat
679
+ when /^(\d+)(?:\.{2}|-)(\d+)$/; ($1.to_i - 1)..($2.to_i - 1)
680
+ when /^(\d+)$/; $1.to_i - 1
681
+ else Regexp.new(spat) rescue nil
682
+ end
683
+ yield(spat, opat)
684
+ end
685
+ end
686
+
687
+ def ignore_line_list_display(a)
688
+ a.collect { |x|
689
+ o = case x
690
+ when Range; (x.begin + 1)..(x.end + 1)
691
+ when Integer; x + 1
692
+ else x
693
+ end
694
+ o.inspect
695
+ }
696
+ end
697
+
698
+ def ignore_line(str)
699
+ ignore_line_list_each(str) do |spat, opat|
700
+ if opat.nil? || !@data.add_ignore(opat)
701
+ return "Bad pattern #{spat}"
702
+ end
703
+ end
704
+ @data.refresh
705
+ "Ignored: " + ignore_line_list_display(@data.ignore_pattern_list).join(" ")
706
+ end
707
+
708
+ def ignore_line_remove_prompt
709
+ s = @display.prompt("Remove ignore: ") or return nil
710
+ s.strip!
711
+ ignore_line_remove(s)
712
+ end
713
+
714
+ def ignore_line_remove(str)
715
+ if !str || str.empty?
716
+ @data.remove_ignore(nil)
717
+ else
718
+ ignore_line_list_each(str) do |spat, opat|
719
+ opat && @data.remove_ignore(opat)
720
+ end
721
+ end
722
+ @data.refresh
723
+ "Ignored: " + ignore_line_list_display(@data.ignore_pattern_list).join(" ")
724
+ end
725
+
726
+ def change_split_pattern_prompt
727
+ s = @display.prompt("Split regexp(/#{@data.split_regexp}/): ")
728
+ return "Not changed" if !s
729
+ begin
730
+ s.gsub!(%r{^/|/$}, '')
731
+ regexp = s.empty? ? nil : Regexp.new(s)
732
+ rescue => e
733
+ return "Invalid regexp /#{s}/: #{e.message}"
734
+ end
735
+ @data.split_regexp = regexp
736
+ "New split regexp: /#{regexp}/"
737
+ end
738
+
739
+ def regexp_line_highlight_prompt
740
+ s = @display.prompt("Highlight regexp(/#{@data.split_regexp}/): ")
741
+ s.strip! if s
742
+ if !s || s.empty?
743
+ @data.highlight_regexp = nil
744
+ return "No line highlight by regexp"
745
+ end
746
+ begin
747
+ s.gsub!(%r{^/|/$}, '')
748
+ regexp = s.empty? ? nil : Regexp.new(s)
749
+ rescue => e
750
+ return "Invalid regexp /#{s}/: #{e.message}"
751
+ end
752
+ @data.highlight_regexp = regexp
753
+ "New highlight regexp: /#{regexp}/"
754
+ end
755
+
756
+ def right_align_regexp_prompt
757
+ current = @display.right_align_re == LineDisplay::ISNUM ? "number" : @display.right_align_re.to_s
758
+ s = @display.prompt("Right align regexp(/#{current}/): ")
759
+ s.strip! if s
760
+ if s.nil? || s.empty?
761
+ @display.right_align_re = LineDisplay::ISNUM
762
+ return "Automatic right alignment of numbers"
763
+ elsif s == "//"
764
+ @display.right_align_re = nil
765
+ return "No automatic right alignment"
766
+ end
767
+
768
+ begin
769
+ s.gsub!(%r{^/|/$}, '')
770
+ regexp = Regexp.new(s)
771
+ rescue => e
772
+ return "Invalid regexp /#{s}/: #{e.message}"
773
+ end
774
+ @display.right_align_re = regexp
775
+ "Automatic right alignment regexp: /#{regexp}/"
776
+ end
777
+
778
+ def change_separator_prompt
779
+ s = @display.prompt("Separator: ") or return nil
780
+ @display.separator = s
781
+ true
782
+ end
783
+
784
+ def change_padding_prompt
785
+ s = @display.prompt("Padding: ") or return nil
786
+ @display.padding = s
787
+ true
788
+ end
789
+
790
+ def display_help
791
+ Ncurses.endwin
792
+ Help.display
793
+ true
794
+ rescue => e
795
+ e.message
796
+ ensure
797
+ Ncurses.refresh
798
+ end
799
+
800
+ def export_prompt
801
+ format = @display.prompt("Format: ") or return nil
802
+ s = @display.prompt("Lines: ") or return nil
803
+ ls, le = s.split.map { |x| x.to_i }
804
+ file = @display.prompt("File: ") or return nil
805
+ qs = Export.questions(format)
806
+ opts = {}
807
+ qs && qs.each { |k, pt, init|
808
+ s = @display.prompt(pt + ": ", :init => init) or return nil
809
+ opts[k] = s
810
+ }
811
+ len = Export.export(file, format, ls..le, @data, @display, opts)
812
+ "Wrote #{len} bytes"
813
+ rescue => e
814
+ return "Error: #{e.message}"
815
+ end
816
+ end