cless 0.3.20

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