hacker-curse 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4d1c9443f8f9caa4d49b856573fd3fd0ea2aecbb
4
+ data.tar.gz: 80b5976740d8c1edfa17054483081b5411083262
5
+ SHA512:
6
+ metadata.gz: 062ce4af9ae109e51586eb363173fee1e30b76489f7a98939fcefac80eadd925868d28229949251fdd745286b27e13d629c0c64c68d32b6e1250f0972fe35060
7
+ data.tar.gz: 371b91a4ba9ef832e1cf907d9d6323bef878313597fd1d1635db271673f171e108f0cc02afd302954a99ebe5478d900757b51cf24a86e3e43bc8ec1d043f6d6c
data/.gitignore ADDED
@@ -0,0 +1,37 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /lib/bundler/man/
26
+
27
+ # for a library or gem, you might want to ignore these files since the code is
28
+ # intended to run in multiple environments; otherwise, check them in:
29
+ # Gemfile.lock
30
+ # .ruby-version
31
+ # .ruby-gemset
32
+
33
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
34
+ .rvmrc
35
+ todo
36
+ *.tmp
37
+ *.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hacker-curse.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 kepler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Hacker Curse
2
+
3
+ View Hacker News and Reddit.com pages from within your terminal.
4
+ This gem contains several programs/ utilities:
5
+ - a library to parse HN and reddit subforums
6
+ - a program/wrapper that uses the above to generate a tab seperated output (TSV) that may be further used
7
+ as a filter
8
+ - a program/wrapper that uses the library to generate a YML file that can be used by client programs
9
+ - an interactive utility named `redford` that displays stories/articles on the screen (using ncurses)
10
+ - an interactive utility named `corvus` that displays stories/articles on the screen (using ruby but not ncurses).
11
+ `corvus` was used as a prototype for `redford` but may still be used if you are not interested in using
12
+ a curses version.
13
+
14
+ The ncurses version is also based on `hackman` which is a ncurses frontend to view RSS feeds from HN, reddit,
15
+ ars, slashdot.
16
+
17
+ ## Installation
18
+
19
+ $ gem install hacker-curse
20
+
21
+ ## Usage
22
+
23
+ The basic library uses nokogiri to parse hacker news and reddit.com subforums into a similar
24
+ structure so that a single program can render output from both sources.
25
+
26
+ hn = RedditNewsParser.new :subforum => 'ruby'
27
+ or
28
+ hn = HackerNewsParser.new :subforum => 'news'
29
+
30
+ page = hn.get_next_page
31
+ page.each_article do |a|
32
+ print a.title, a.age_text, a.points, a.comment_count, a.article_url, "\n"
33
+ end
34
+ art = page.first
35
+ art.each_comment do |c|
36
+ print c.age_text, c.points, c.submitter, "\n"
37
+ print c.comment_text
38
+ end
39
+
40
+
41
+
42
+ This program comes with several wrappers which display how to use the library and the wrappers may
43
+ themselves be used as-is.
44
+
45
+ For example, `hacker-tsv.rb` prints the articles and its details such as age, points and number of comments,
46
+ url, comments url, submitter etc in a tab delimited format. This may further be used be filtered or sorted
47
+ by other programs.
48
+
49
+ As an example, the following retrieves two pages of stories from Hacker News and save the retrieved HTML file,
50
+ the output goes to the terminal/screen.
51
+
52
+ hacker-tsv.rb -H hn -p 2 -s news -w news.html
53
+
54
+ Retrieve one page of articles from reddit.com/r/ruby printing the tab delimited rows to the screen.
55
+
56
+ hacker-tsv.rb -H rn -s ruby
57
+
58
+ `hacker.rb` is a wrapper over `hacker-tsv.rb` that saves the output of the above into tab delimited files.
59
+ It can guess the host based on the argument.
60
+
61
+ hacker.rb news
62
+ hacker.rb newest
63
+ hacker.rb programming
64
+ hacker.rb --pages 2 ruby
65
+
66
+ `corvus` is an interactive program (non-ncurses) that uses the generated YML files, and displays a selectable list
67
+ of stories which a user may navigate, select and launch the article (or comments) in a gui or text browser.
68
+ User may switch between forums, reload the file, view the articles in a long list or short list, single or
69
+ multiple columns etc. `corvus` requires at least ruby 1.9.3 in order to get single characters.
70
+
71
+ NOTE: The TSV file should be used as a command-line filter, and not in applications or front-ends. Use the YML file for frontends.
72
+
73
+ The YML wrapper has been called as:
74
+
75
+ hacker-yml.rb --pages 1 -H rn -s ruby -y ~/tmp/hacker-curse/ruby.yml
76
+
77
+ The host is "rn" or "hn" (for reddit or hacker news), the subforum is ruby (can be programming or python or java, etc),
78
+ and the YML file location is also given.
79
+
80
+ `redford.rb` can use configuration from a file named `~/.redford.yml`. A sample file named `redford.yml` is in the root folder of this gem.
81
+
82
+
83
+ ## Contributing
84
+
85
+ 1. Fork it ( https://github.com/[my-github-username]/hacker-curse/fork )
86
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
87
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
88
+ 4. Push to the branch (`git push origin my-new-feature`)
89
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/corvus ADDED
@@ -0,0 +1,2320 @@
1
+ #!/usr/bin/env ruby
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: corvus
4
+ # Description: navigate tab-separated output, selecting a row and issuing a command
5
+ #
6
+ # Author: jkepler http://github.com/mare-imbrium//jkepler
7
+ # Date: 2014-07-30
8
+ # License: MIT
9
+ # Last update: 2014-09-11 12:56
10
+ # ----------------------------------------------------------------------------- #
11
+ # corvus.rb Copyright (C) 2014 j kepler
12
+ # == TODO
13
+ # - remove junk options like child dirs else program will crash
14
+ # - don't save html unless specified, also specify location for tsv's so current not cluttered.
15
+ # - config file, save bookmarks and forumlist.
16
+ # - remove file stuff
17
+ # - how to specify format for single list, and large list.
18
+ # - what of comments
19
+ require 'readline'
20
+ require 'io/wait'
21
+ # http://www.ruby-doc.org/stdlib-1.9.3/libdoc/shellwords/rdoc/Shellwords.html
22
+ require 'shellwords'
23
+ require 'fileutils'
24
+ require 'yaml'
25
+ # -- requires 1.9.3 for io/wait
26
+ # -- cannot do with Highline since we need a timeout on wait, not sure if HL can do that
27
+
28
+ ## INSTALLATION
29
+ # copy into PATH
30
+ # alias c=~/bin/corvus.rb
31
+ # c
32
+ VERSION="0.0.2"
33
+ CONFIG_FILE="~/.corvusinfo"
34
+
35
+ $bindings = {
36
+ "`" => "main_menu",
37
+ "=" => "toggle_menu",
38
+ "!" => "command_mode",
39
+ #"@" => "selection_mode_toggle",
40
+ "M-a" => "select_all",
41
+ "M-A" => "unselect_all",
42
+ "+" => "add_to_forum_list",
43
+ "-" => "remove_from_forum_list",
44
+ "<" => "previous_forum",
45
+ ">" => "next_forum",
46
+ ":" => "subcommand",
47
+ "'" => "goto_bookmark",
48
+ "/" => "enter_regex",
49
+ "M-p" => "prev_page",
50
+ "M-n" => "next_page",
51
+ "SPACE" => "next_page",
52
+ "M-f" => "select_visited_files",
53
+ "M-d" => "select_used_dirs",
54
+ "M-b" => "select_bookmarks",
55
+ "M-m" => "create_bookmark",
56
+ "M-M" => "show_marks",
57
+ "C-c" => "escape",
58
+ "ESCAPE" => "escape",
59
+ "TAB" => "views",
60
+ "C-i" => "views",
61
+ #"?" => "dirtree",
62
+ "ENTER" => "select_current",
63
+ "D" => "delete_current_forum",
64
+ #"M" => "forum_actions most",
65
+ "Q" => "quit_command",
66
+ "RIGHT" => "column_next",
67
+ "LEFT" => "column_next 1",
68
+ "C-x" => "forum_actions",
69
+ "M--" => "columns_incdec -1",
70
+ "M-+" => "columns_incdec 1",
71
+ #"S" => "command_file list y ls -lh",
72
+ #"L" => "command_file Page n less",
73
+ "C-d" => "cursor_scroll_dn",
74
+ "C-b" => "cursor_scroll_up",
75
+ "UP" => "cursor_up",
76
+ "DOWN" => "cursor_dn",
77
+ "C-SPACE" => "visual_mode_toggle",
78
+
79
+ "M-?" => "print_help",
80
+ "F1" => "print_help",
81
+ "F2" => "display_forum"
82
+ #"F3" => "dirtree",
83
+ #"F4" => "tree",
84
+ #"S-F1" => "dirtree",
85
+ #"S-F2" => "tree"
86
+
87
+ }
88
+
89
+ ## clean this up a bit, copied from shell program and macro'd
90
+ $kh=Hash.new
91
+ $kh["OP"]="F1"
92
+ $kh[""]="UP"
93
+ $kh["[5~"]="PGUP"
94
+ $kh['']="ESCAPE"
95
+ $kh['']="BACKTAB"
96
+ KEY_PGDN="[6~"
97
+ KEY_PGUP="[5~"
98
+ ## I needed to replace the O with a [ for this to work
99
+ # in Vim Home comes as ^[OH whereas on the command line it is correct as ^[[H
100
+ KEY_HOME=''
101
+ KEY_END=""
102
+ KEY_F1="OP"
103
+ KEY_UP=""
104
+ KEY_DOWN=""
105
+
106
+ $kh[KEY_PGDN]="PgDn"
107
+ $kh[KEY_PGUP]="PgUp"
108
+ $kh[KEY_HOME]="Home"
109
+ $kh[KEY_END]="End"
110
+ $kh[KEY_F1]="F1"
111
+ $kh[KEY_UP]="UP"
112
+ $kh[KEY_DOWN]="DOWN"
113
+ KEY_LEFT=''
114
+ KEY_RIGHT=''
115
+ $kh["OQ"]="F2"
116
+ $kh["OR"]="F3"
117
+ $kh["OS"]="F4"
118
+ $kh[KEY_LEFT] = "LEFT"
119
+ $kh[KEY_RIGHT]= "RIGHT"
120
+ KEY_F5='[15~'
121
+ KEY_F6='[17~'
122
+ KEY_F7='[18~'
123
+ KEY_F8='[19~'
124
+ KEY_F9='[20~'
125
+ KEY_F10='[21~'
126
+ KEY_S_F1=''
127
+ $kh[KEY_F5]="F5"
128
+ $kh[KEY_F6]="F6"
129
+ $kh[KEY_F7]="F7"
130
+ $kh[KEY_F8]="F8"
131
+ $kh[KEY_F9]="F9"
132
+ $kh[KEY_F10]="F10"
133
+ # testing out shift+Function. these are the codes my kb generates
134
+ $kh[KEY_S_F1]="S-F1"
135
+ $kh['']="S-F2"
136
+
137
+ ## get a character from user and return as a string
138
+ # Adapted from:
139
+ #http://stackoverflow.com/questions/174933/how-to-get-a-single-character-without-pressing-enter/8274275#8274275
140
+ # Need to take complex keys and matc against a hash.
141
+ def get_char
142
+ begin
143
+ system("stty raw -echo 2>/dev/null") # turn raw input on
144
+ c = nil
145
+ #if $stdin.ready?
146
+ c = $stdin.getc
147
+ cn=c.ord
148
+ return "ENTER" if cn == 13
149
+ return "BACKSPACE" if cn == 127
150
+ return "C-SPACE" if cn == 0
151
+ return "SPACE" if cn == 32
152
+ # next does not seem to work, you need to bind C-i
153
+ return "TAB" if cn == 8
154
+ if cn >= 0 && cn < 27
155
+ x= cn + 96
156
+ return "C-#{x.chr}"
157
+ end
158
+ if c == ''
159
+ buff=c.chr
160
+ while true
161
+ k = nil
162
+ if $stdin.ready?
163
+ k = $stdin.getc
164
+ #puts "got #{k}"
165
+ buff += k.chr
166
+ else
167
+ x=$kh[buff]
168
+ return x if x
169
+ #puts "returning with #{buff}"
170
+ if buff.size == 2
171
+ ## possibly a meta/alt char
172
+ k = buff[-1]
173
+ return "M-#{k.chr}"
174
+ end
175
+ return buff
176
+ end
177
+ end
178
+ end
179
+ #end
180
+ return c.chr if c
181
+ ensure
182
+ #system "stty -raw echo" # turn raw input off
183
+ system("stty -raw echo 2>/dev/null") # turn raw input on
184
+ end
185
+ end
186
+
187
+ ## GLOBALS
188
+ #$IDX="123456789abcdefghijklmnoprstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
189
+ #$IDX="abcdefghijklmnopqrstuvwxy"
190
+ $IDX=('a'..'y').to_a
191
+ $IDX.concat ('za'..'zz').to_a
192
+ $IDX.concat ('Za'..'Zz').to_a
193
+ $IDX.concat ('ZA'..'ZZ').to_a
194
+ $min_index = 1 # this prevents cursor from moving into zeroth position since this is the title.
195
+ # using min index as 1 works fine but causes some subtle issues like when there are 2 cols
196
+ # and we press right arrow, then first index is this space.
197
+ $min_index = 0 # this is old behavior
198
+ $IDX.unshift(" ") if $min_index > 0
199
+
200
+ #$selected_files = Array.new
201
+ $bookmarks = {}
202
+ $forumlist = {}
203
+ $mode = nil
204
+ $glines=%x(tput lines).to_i
205
+ $gcols=%x(tput cols).to_i
206
+ $grows = $glines - 3
207
+ $pagesize = 60
208
+ # how many columns we will try to show on one page
209
+ $gviscols = 2
210
+ $pagesize = $grows * $gviscols
211
+ $stact = 0
212
+ $editor_mode = true
213
+ $enhanced_mode = false
214
+ $visual_block_start = nil
215
+ $pager_command = {
216
+ :text => 'most',
217
+ :image => 'open',
218
+ :zip => 'tar ztvf %% | most',
219
+ :unknown => 'open'
220
+ }
221
+ $dir_position = {}
222
+ ## CONSTANTS
223
+ GMARK='*'
224
+ DOTMARK='.'
225
+ CURMARK='>'
226
+ MSCROLL = 10
227
+ SPACE=" "
228
+ CLEAR = "\e[0m"
229
+ BOLD = "\e[1m"
230
+ BOLD_OFF = "\e[22m"
231
+ RED = "\e[31m"
232
+ ON_RED = "\e[41m"
233
+ GREEN = "\e[32m"
234
+ YELLOW = "\e[33m"
235
+ BLUE = "\e[1;34m"
236
+
237
+ ON_BLUE = "\e[44m"
238
+ REVERSE = "\e[7m"
239
+ UNDERLINE = "\e[4m"
240
+ CURSOR_COLOR = ON_BLUE
241
+ $patt=nil
242
+ $ignorecase = true
243
+ $quitting = false
244
+ $modified = $writing = false
245
+ $visited_files = []
246
+ ## dir stack for popping
247
+ $visited_dirs = []
248
+ ## dirs where some work has been done, for saving and restoring
249
+ $used_dirs = []
250
+ $sorto = ""
251
+ $viewctr = 0
252
+ $history = []
253
+ $sta = 0
254
+ $cursor = $min_index
255
+ $visual_mode = false
256
+ # these columns ONLY in long_list_columns is base ONE, like in +cut+.
257
+ # These are the columns you want to be shown in a long listing of articles/titles
258
+ $long_list_columns = [1,2,3,4]
259
+ $long_list_columns = [2,3,4,1]
260
+ # sort_columns are zero-based
261
+ $sort_columns = [2,3,6]
262
+ $column_enum = $sort_columns.cycle
263
+ $num_pages = 1
264
+ $logger_array = Array.new
265
+ # history of commands entered in readline for various prompts or keys
266
+ $history = Hash.new
267
+ $commandlist = %w[refresh forum_actions view_log clear_log change_file fetch_data_from_net sort_on display_forum next_forum previous_forum new_articles]
268
+ $selection_allowed = false
269
+ $def_forumlist = %w[ news newest programming ruby python haskell scala java zsh commandline vim vimplugins ]
270
+ $cache_path = "."
271
+ $hint_message = nil
272
+ $hacker_forums = %w{news newest show jobs ask}
273
+ $hints_toggle = true
274
+ $yaml_obj = nil
275
+ # global
276
+
277
+ $help = "#{BOLD}M-?#{BOLD_OFF} Help #{BOLD}`#{BOLD_OFF} Menu #{BOLD}=#{BOLD_OFF} Toggle #{BOLD}Q#{BOLD_OFF} Quit "
278
+
279
+ # Ideally should have only first columns while the others are kept in array
280
+ # so we can get rest of data when needed
281
+ def get_data filename=nil
282
+ unless filename
283
+ filename = $filename
284
+ else
285
+ $filename = filename
286
+ end
287
+ raise "File name not found #{filename} in get_data " if !File.exists?(filename)
288
+ #lines = File.open(filename, "r").read.split("\n")
289
+ #if lines.nil? or lines.empty?
290
+ #perror "Failed trying to read #{filename}. Please delete if empty "
291
+ #lines = ["no data", "no data, please refresh"]
292
+ #end
293
+ obj = YAML::load( File.open( filename ) )
294
+ $yaml_obj = obj
295
+ age = compute_file_age filename
296
+ articles = obj[:articles]
297
+ if articles.empty?
298
+ # this happens when doing More with a stale url
299
+ perror "No articles in this file. Please delete file."
300
+ return
301
+ end
302
+ colh = articles.first.keys
303
+ colh = [:title ,:age_text ,:comment_count ,:points ,:article_url ,:comments_url ,:age ,:submitter ,:submitter_url ,:byline]
304
+ $forum_bgcolor = BOLD
305
+ $forum_bgcolor = ON_RED if age > 8
306
+ # this should only happen if columns are in row 0
307
+ # # FIXME
308
+ $column_headings = colh
309
+ $full_data = Array.new
310
+ # there will be a path also at some time
311
+ #$subforum = filename.sub(File.extname(filename),"").sub("__","/")
312
+ $subforum = file_to_forum(filename)
313
+ $host = forum_to_host $subforum
314
+ titles = Array.new
315
+ articles.each do |a|
316
+ titles << a[:title]
317
+ _cols = []
318
+ $column_headings.each do |c|
319
+ _cols << a[c]
320
+ end
321
+ $full_data << _cols
322
+ end
323
+ perror "No data for #{filename} " if $full_data.empty?
324
+ raise "No data for #{filename} " if $full_data.empty?
325
+ #lines.each_with_index do |l, ix|
326
+ #_cols = l.split("\t")
327
+ #titles << _cols.first
328
+ #$full_data << _cols
329
+ #end
330
+ calc_column_widths
331
+ size_column_headings
332
+ $files = titles
333
+ end
334
+ def TSV_get_data filename=nil # {{{
335
+ unless filename
336
+ filename = $filename
337
+ else
338
+ $filename = filename
339
+ end
340
+ raise "File name not found #{filename} in get_data " if !File.exists?(filename)
341
+ lines = File.open(filename, "r").read.split("\n")
342
+ if lines.nil? or lines.empty?
343
+ perror "Failed trying to read #{filename}. Please delete if empty "
344
+ lines = ["no data", "no data, please refresh"]
345
+ end
346
+ age = compute_file_age filename
347
+ $forum_bgcolor = BOLD
348
+ $forum_bgcolor = ON_RED if age > 8
349
+ # this should only happen if columns are in row 0
350
+ $column_headings = lines.delete_at(0).split("\t")
351
+ $full_data = Array.new
352
+ # there will be a path also at some time
353
+ #$subforum = filename.sub(File.extname(filename),"").sub("__","/")
354
+ $subforum = file_to_forum(filename)
355
+ $host = forum_to_host $subforum
356
+ titles = Array.new
357
+ lines.each_with_index do |l, ix|
358
+ _cols = l.split("\t")
359
+ titles << _cols.first
360
+ $full_data << _cols
361
+ end
362
+ calc_column_widths
363
+ size_column_headings
364
+ $files = titles
365
+ end # }}}
366
+ # reload from internet
367
+ def fetch_data_from_net subforum=nil, more_url=nil
368
+ unless subforum
369
+ print "Enter name of subforum: "
370
+ subforum = Readline::readline('>', true)
371
+ return if subforum == ""
372
+ $host = forum_to_host subforum
373
+ end
374
+ filename = forum_to_file subforum
375
+ print "fetching for #{subforum} ... saving to #{filename}\n"
376
+ m = nil
377
+ if more_url
378
+ m = "-u #{more_url} "
379
+ m = "-u '" + more_url + "'"
380
+ end
381
+ puts "hacker-yml.rb --pages #{$num_pages} -H #{$host} -s #{subforum} -y #{filename} #{m}"
382
+ # put in begin rescue since sometimes no data, as for programming/rising
383
+ #retval = system("hacker.rb --pages #{$num_pages} #{subforum}")
384
+ retval = system("hacker-yml.rb --pages #{$num_pages} -H #{$host} -s #{subforum} -y #{filename} #{m}")
385
+ status = "#{$?}"
386
+ unless retval
387
+ perror "Error occured in hacker-yml.rb. status is #{status}"
388
+ return
389
+ end
390
+
391
+ if File.exists? filename
392
+ get_data filename
393
+ post_cd
394
+ else
395
+ perror "Cannot find file #{filename}. Some error has happened."
396
+ end
397
+ $hint_message = nil
398
+ end
399
+ # this allows us to fetch more items using the NEXT url
400
+ # However, when we read up later, this file will contain a stale next url
401
+ # as well as stale date. We should have avoided stale data.
402
+ def fetch_more
403
+ more_url = $yaml_obj[:next_url]
404
+ #perror "more url is #{more_url} "
405
+ fetch_data_from_net $subforum, more_url
406
+ end
407
+ def file_to_forum filename
408
+ forum = File.basename(filename).sub(File.extname(filename),"").sub("__","/")
409
+ end
410
+ def forum_to_file forum
411
+ file = "#{forum}.yml".sub("/","__")
412
+ file = "#{$cache_path}/#{file}"
413
+ end
414
+ def forum_to_host fo
415
+ if $hacker_forums.include? fo
416
+ return :hn
417
+ end
418
+ return :rn
419
+ end
420
+ def reload
421
+ fetch_data_from_net $subforum
422
+ end
423
+ def calc_column_widths
424
+ $column_widths = []
425
+ row = $full_data.first
426
+ row.each do |r|
427
+ # in some cases like jobs, r may be nil
428
+ #$column_widths << r.strip.size + 1
429
+ # i have padded rjust the integers
430
+ sz = r ? r.size : 1
431
+ $column_widths << sz
432
+ end
433
+ end
434
+ # truncate column headings to file size of column
435
+ def size_column_headings
436
+ $sized_column_headings = []
437
+ if $column_widths
438
+ $column_headings.zip($column_widths) do |h, w|
439
+ $sized_column_headings << h.slice(0, w)
440
+ end
441
+ end
442
+ end
443
+
444
+ ## main loop which calls all other programs
445
+ def run(options)
446
+ $browser_mode = options[:browser_mode] || 'text'
447
+ $browser_text = options[:browser_text] || 'elinks'
448
+ $browser_gui = options[:browser_gui] || 'open'
449
+ $cache_path = options[:cache_path] || "."
450
+ ctr=0
451
+ filename = ARGV[0] || "news.yml"
452
+ config_read
453
+ $open_command = $browser_mode == 'text' ? $browser_text : $browser_gui
454
+ if $bookmarks.empty?
455
+ $bookmarks = { :r => 'ruby', :p => 'programming', :n => 'news', :j => 'java', :c => 'commandline',
456
+ :v => 'vim', :z => 'zsh', :l => 'linux', :a => 'apple', :h => 'haskell' , :s => 'scala',
457
+ :u => 'rust', :y => 'python', :w => 'newest' }
458
+ end
459
+ if $forumlist.empty?
460
+ $forumlist = $def_forumlist
461
+ end
462
+
463
+ # unfortunately this is insisting on a file being present. It should allow use to fetch data if
464
+ # no file.
465
+ unless File.exists? filename
466
+ forum = choose_forum
467
+ if forum
468
+ # fetch data from file, or if not present then internet
469
+ display_forum forum
470
+ filename = data_cached_for forum
471
+ else
472
+ raise "Cannot continue without a forum name"
473
+ end
474
+ end
475
+ $filename = filename
476
+ get_data filename
477
+
478
+ fl=$files.size
479
+
480
+ selectedix = nil
481
+ $patt=""
482
+ $sta = 0
483
+ while true
484
+ i = 0
485
+ if $patt
486
+ if $ignorecase
487
+ $view = $files.grep(/#{$patt}/i)
488
+ else
489
+ $view = $files.grep(/#{$patt}/)
490
+ end
491
+ else
492
+ $view = $files
493
+ end
494
+ fl=$view.size
495
+ $sta = 0 if $sta >= fl || $sta < 0
496
+ $viewport = $view[$sta, $pagesize]
497
+ fin = $sta + $viewport.size
498
+ $title ||= $subforum
499
+ $details ||= "-"
500
+ system("clear")
501
+ # title
502
+ # DETAIL LINE
503
+ # on the top line, lets show some details for the line under the cursor (focussed line)
504
+ if true
505
+ fullrow = $viewport[$cursor]
506
+ # in case of long listings cursor can go one higher than size, as we do not scroll.
507
+ if fullrow
508
+ fullrow = get_full_row(fullrow)
509
+ # this is the byline, we shouldmake it the last column
510
+ $details = "#{YELLOW} >> #{fullrow.last} "
511
+ end
512
+ end
513
+ ## HELP LINE
514
+ _hint = ""
515
+ if $hints_toggle
516
+ _hint = $hint_message || get_hint
517
+ _hint = "#{BOLD}#{_hint}#{CLEAR}"
518
+ end
519
+ print "#{GREEN}#{$help} #{BLUE}corvus #{VERSION}#{CLEAR} : #{_hint}\n"
520
+ #t = "#{$title} #{$sta + 1} to #{fin} of #{fl} #{$sorto} F:#{$filterstr}"
521
+ t = "#{$title}#{CLEAR} #{BOLD}#{$sta + 1} to #{fin} of #{fl} o:#{$sorto}:#{$details} "
522
+ t = t[t.size-$gcols..-1] if t.size >= $gcols
523
+ ## TITLE LINE
524
+ #print "#{BOLD}#{t}#{CLEAR}\n"
525
+ print "#{$forum_bgcolor}#{t}#{CLEAR}\n"
526
+ if $long_listing
527
+ # lets see if we can print the headings
528
+ if $column_headings
529
+ s = get_long_list $sized_column_headings
530
+ print " ", s, "\n"
531
+ end
532
+ end
533
+ ## nilling the title means a superimposed one gets cleared.
534
+ #$title = nil
535
+ # split into 2 procedures so columnate can e clean and reused.
536
+ buff = format $viewport
537
+ buff = columnate buff, $grows
538
+ # needed the next line to see how much extra we were going in padding
539
+ #buff.each {|line| print "#{REVERSE}#{line}#{CLEAR}\n" }
540
+ buff.each {|line| print line, "\n" }
541
+ print
542
+ # prompt
543
+ #print "#{$files.size}, #{view.size} sta=#{sta} (#{patt}): "
544
+ _mm = ""
545
+ _mm = "[#{$mode}] " if $mode
546
+ print "\r #{_mm}#{$patt} >"
547
+ ch = get_char
548
+ #puts
549
+ #break if ch == "q"
550
+ #elsif ch =~ /^[1-9a-zA-Z]$/
551
+ if ch =~ /^[a-zZ]$/
552
+ # hint mode
553
+ select_hint $viewport, ch
554
+ ctr = 0
555
+ elsif ch == "BACKSPACE"
556
+ if $patt && $patt.size > 0
557
+ $patt = $patt[0..-2]
558
+ end
559
+ ctr = 0
560
+ else
561
+ #binding = $bindings[ch]
562
+ x = $bindings[ch]
563
+ x = x.split if x
564
+ if x
565
+ binding = x.shift
566
+ args = x
567
+ send(binding, *args) if binding
568
+ else
569
+ log "No binding for #{ch}, #{ch.ord} "
570
+ end
571
+ #p ch
572
+ end
573
+ break if $quitting
574
+ end
575
+ puts "bye"
576
+ config_write if $writing
577
+ end
578
+ def exec_string x=nil
579
+ unless x
580
+ x = Readline::readline(': ')
581
+ return if x.nil? or x == ""
582
+ end
583
+
584
+ x = x.split
585
+ if x
586
+ binding = x.shift
587
+ args = x
588
+ send(binding, *args) if binding
589
+ end
590
+ end
591
+
592
+ ##
593
+ #
594
+ # print in columns
595
+ # ary - array of data
596
+ # sz - lines in one column
597
+ #
598
+ def columnate ary, sz
599
+ buff=Array.new
600
+ return buff if ary.nil? || ary.size == 0
601
+
602
+ # determine width based on number of files to show
603
+ # if less than sz then 1 col and full width
604
+ #
605
+ wid = 30
606
+ ars = ary.size
607
+ ars = [$pagesize, ary.size].min
608
+ d = 0
609
+ if ars <= sz
610
+ wid = $gcols - d
611
+ else
612
+ tmp = (ars * 1.000/ sz).ceil
613
+ wid = $gcols / tmp - d
614
+ end
615
+ #elsif ars < sz * 2
616
+ #wid = $gcols/2 - d
617
+ #elsif ars < sz * 3
618
+ #wid = $gcols/3 - d
619
+ #else
620
+ #wid = $gcols/$gviscols - d
621
+ #end
622
+
623
+ # ix refers to the index in the complete file list, wherease we only show 60 at a time
624
+ ix=0
625
+ while true
626
+ ## ctr refers to the index in the column
627
+ ctr=0
628
+ while ctr < sz
629
+
630
+ f = ary[ix]
631
+ fsz = f.size
632
+ if fsz > wid
633
+ f = f[0, wid-2]+"$ "
634
+ ## we do the coloring after trunc so ANSI escpe seq does not get get
635
+ if ix + $sta == $cursor
636
+ f = "#{CURSOR_COLOR}#{f}#{CLEAR}"
637
+ end
638
+ else
639
+ ## we do the coloring before padding so the entire line does not get padded, only file name
640
+ if ix + $sta == $cursor
641
+ f = "#{CURSOR_COLOR}#{f}#{CLEAR}"
642
+ end
643
+ #f = f.ljust(wid)
644
+ f << " " * (wid-fsz)
645
+ end
646
+
647
+ if buff[ctr]
648
+ buff[ctr] += f
649
+ else
650
+ buff[ctr] = f
651
+ end
652
+
653
+ ctr+=1
654
+ ix+=1
655
+ break if ix >= ary.size
656
+ end
657
+ break if ix >= ary.size
658
+ end
659
+ return buff
660
+ end
661
+ ## formats the data with number, mark and details
662
+ def format ary
663
+ #buff = Array.new
664
+ buff = Array.new(ary.size)
665
+ return buff if ary.nil? || ary.size == 0
666
+
667
+ # determine width based on number of files to show
668
+ # if less than sz then 1 col and full width
669
+ #
670
+ # ix refers to the index in the complete file list, wherease we only show 60 at a time
671
+ ix=0
672
+ ctr=0
673
+ pad = false
674
+ pad = true if ary.size > 25
675
+ ary.each do |f|
676
+ ## ctr refers to the index in the column
677
+ ind = get_shortcut(ix)
678
+ ind = ind.ljust(2) if pad
679
+ mark=SPACE
680
+ cur=SPACE
681
+ cur = CURMARK if ix + $sta == $cursor
682
+ #mark=GMARK if $selected_files.index(ary[ix])
683
+ # we are not doing selection here
684
+ # but maybe today's articles can be starred
685
+ age_in_hours = article_age_in_hours(f)
686
+ mark=DOTMARK if age_in_hours < 24
687
+ mark=GMARK if age_in_hours < 8
688
+
689
+ if $long_listing
690
+ begin
691
+ f = get_long_list f
692
+ rescue Exception => e
693
+ #f = "%10s %s %s" % ["?", "??????????", f]
694
+ end
695
+ end
696
+
697
+ s = "#{ind}#{mark}#{cur}#{f}"
698
+ # I cannot color the current line since format does the chopping
699
+ # so not only does the next lines alignment get skeweed, but also if the line is truncated
700
+ # then the color overflows.
701
+ #if ix + $sta == $cursor
702
+ #s = "#{RED}#{s}#{CLEAR}"
703
+ #end
704
+
705
+ buff[ctr] = s
706
+
707
+ ctr+=1
708
+ ix+=1
709
+ end
710
+ return buff
711
+ end
712
+ ## select file based on key pressed
713
+ def select_hint view, ch
714
+ # a to y is direct
715
+ # if x or z take a key IF there are those many
716
+ #
717
+ ix = get_index(ch, view.size)
718
+ if ix
719
+ f = view[ix]
720
+ return unless f
721
+ $cursor = $sta + ix
722
+
723
+ if $mode == 'SEL'
724
+ toggle_select f
725
+ elsif $mode == 'COM'
726
+ run_command f
727
+ elsif $mode == "forum"
728
+ display_forum f
729
+ else
730
+ on_enter f
731
+ end
732
+ #selectedix=ix
733
+ end
734
+ end
735
+ ## toggle selection state of file
736
+ def toggle_select f
737
+ return unless $selection_allowed
738
+ if $selected_files.index f
739
+ $selected_files.delete f
740
+ else
741
+ $selected_files.push f
742
+ end
743
+ end
744
+ # returns full row of data for given title
745
+ def get_full_row title=nil
746
+ title = get_current_title unless title
747
+ location = $files.index(title)
748
+ raise "no location for title: #{title}" unless location
749
+ fullrow = $full_data[location]
750
+ return fullrow
751
+ end
752
+ # returns age in hours of given title
753
+ def article_age_in_hours title
754
+ $_time_ ||= Time.now.to_i
755
+ fullrow = get_full_row title
756
+ return 10000 unless fullrow
757
+ age = fullrow[6].to_i
758
+ hours = (($_time_ - age)/3600).to_i
759
+ return hours
760
+ end
761
+ def pad_or_trunc str, wid
762
+ f = str.dup
763
+ fsz = f.size
764
+ if fsz > wid
765
+ f = f[0, wid-2]+"% "
766
+ else
767
+ f << " " * (wid-fsz)
768
+ end
769
+ return f
770
+ end
771
+ # returns a formatted string for the title submitted.
772
+ # In the case of columns headings, we send in the columns array ?
773
+ def get_long_list title
774
+ case title
775
+ when String
776
+ fullrow = get_full_row title
777
+ when Array
778
+ fullrow = title
779
+ end
780
+ s = ""
781
+ # while spacing remember that we add a space after each column, and the index at the start which is about 3 chars
782
+
783
+ #@long_list_column_colors ||= [ BLUE, GREEN, ON_RED, ON_BLUE]
784
+ ctr = 0
785
+ if false
786
+ w3 = (($gcols/6).floor.to_i)-1 # minus 1 since we add space after
787
+ w1 = (($gcols/2).floor.to_i) # title
788
+ col_widths = [w1, w3,w3,w3-4]
789
+ col_widths = [ w3,w3,w3-4, w1]
790
+ fullrow.each_with_index do |e, i|
791
+ if $long_list_columns.include? i+1
792
+ wid = col_widths[ctr] || 10
793
+ #colr = $long_list_column_colors[ctr]
794
+ #ss = "#{colr}#{pad_or_trunc(e, wid)}#{CLEAR}"
795
+ s << pad_or_trunc(e, wid)
796
+ s << " "
797
+ ctr += 1
798
+ end
799
+ end
800
+ end
801
+ $long_list_columns.each do |i|
802
+ e = fullrow[i-1]
803
+ #wid = col_widths[ctr] || 10
804
+ #colr = $long_list_column_colors[ctr]
805
+ #ss = "#{colr}#{pad_or_trunc(e, wid)}#{CLEAR}"
806
+ #s << pad_or_trunc(e, wid)
807
+ s << e
808
+ s << " "
809
+ ctr += 1
810
+ end
811
+ return s
812
+ end
813
+ ## open file or directory
814
+ def open_file f, ch = nil
815
+ location = $files.index(f)
816
+ fullrow = $full_data[location]
817
+ ch = column_menu fullrow unless ch
818
+ return if ch.to_i > fullrow.size
819
+ url = fullrow[ch.to_i]
820
+ system "#{$open_command} #{url}"
821
+ return # 2014-07-25 - 22:43
822
+ return unless f
823
+ end
824
+ def view_article
825
+ title = get_current_title
826
+ return unless title
827
+ open_file title, 4
828
+ end
829
+ def view_comments
830
+ title = get_current_title
831
+ return unless title
832
+ open_file title, 5
833
+ end
834
+ def get_current_title
835
+ title = $view[$cursor] if $view[$cursor]
836
+ end
837
+ def get_current_rowdata
838
+ f = get_current_title
839
+ location = $files.index(f)
840
+ fullrow = $full_data[location]
841
+ end
842
+
843
+ ## run command on given file/s
844
+ # Accepts command from user
845
+ # After putting readline in place of gets, pressing a C-c has a delayed effect. It goes intot
846
+ # exception bloack after executing other commands and still does not do the return !
847
+ def run_command f
848
+ return
849
+ end
850
+
851
+ ## cd to a dir
852
+ def change_dir f, pos=nil
853
+ end
854
+
855
+ ## clear sort order and refresh listing, used typically if you are in some view
856
+ # such as visited dirs or files
857
+ def escape
858
+ $sorto = nil
859
+ $viewctr = 0
860
+ $title = nil
861
+ #$filterstr = "M"
862
+ #visual_block_clear
863
+ refresh
864
+ end
865
+
866
+ ## refresh listing after some change like option change, or toggle
867
+ def refresh
868
+ get_data
869
+ #$filterstr ||= "M"
870
+ $patt=nil
871
+ $title = nil
872
+ end
873
+ #
874
+ ## unselect all files
875
+ def unselect_all
876
+ return unless $selection_allowed
877
+ $selected_files = []
878
+ $visual_mode = nil
879
+ end
880
+
881
+ ## select all files
882
+ def select_all
883
+ return unless $selection_allowed
884
+ $selected_files = $view.dup
885
+ end
886
+
887
+ ## accept dir to goto and change to that ( can be a file too)
888
+ def goto_dir
889
+ return
890
+ end
891
+
892
+ ## toggle mode to selection or not
893
+ # In selection, pressed hotkey selects a file without opening, one can keep selecting
894
+ # (or deselecting).
895
+ #
896
+ def selection_mode_toggle
897
+ return unless $selection_allowed
898
+ if $mode == 'SEL'
899
+ # we seem to be coming out of select mode with some files
900
+ if $selected_files.size > 0
901
+ run_command $selected_files
902
+ end
903
+ $mode = nil
904
+ else
905
+ #$selection_mode = !$selection_mode
906
+ $mode = 'SEL'
907
+ end
908
+ end
909
+ ## toggle command mode
910
+ def command_mode
911
+ if $mode == 'COM'
912
+ $mode = nil
913
+ return
914
+ end
915
+ $mode = 'COM'
916
+ end
917
+ def goto_parent_dir
918
+ #change_dir ".."
919
+ end
920
+ ## This actually filters, in zfm it goes to that entry since we have a cursor there
921
+ #
922
+ def goto_entry_starting_with fc=nil
923
+ unless fc
924
+ print "Entries starting with: "
925
+ fc = get_char
926
+ end
927
+ return if fc.size != 1
928
+ ## this is wrong and duplicates the functionality of /
929
+ # It shoud go to cursor of item starting with fc
930
+ $patt = "^#{fc}"
931
+ end
932
+ def goto_bookmark ch=nil
933
+ perror "No bookmarks" if $bookmarks.empty?
934
+ unless ch
935
+ print "Enter bookmark char: "
936
+ ch = get_char
937
+ end
938
+ if ch =~ /^[0-9a-z]$/
939
+ forum = $bookmarks[ch.to_sym]
940
+ display_forum forum if forum
941
+ end
942
+ end
943
+
944
+
945
+ ## take regex from user, to run on files on screen, user can filter file names
946
+ def enter_regex
947
+ print "Enter (regex) pattern: "
948
+ #$patt = gets().chomp
949
+ $patt = Readline::readline('>', true)
950
+ ctr = 0
951
+ end
952
+ # Ask for a yml file name to display
953
+ # NOT USED replaced by choose_filename
954
+ def accept_file_name # {{{
955
+ print "Enter filename: "
956
+ values = Dir.glob("*.tsv")
957
+ values.each do |e|
958
+ unless Readline::HISTORY.include? e
959
+ Readline::HISTORY.push(e)
960
+ end
961
+ end
962
+ filename = Readline::readline('>', true)
963
+ if File.extname(filename) == ""
964
+ # if no extension, add tsv
965
+ filename << ".tsv"
966
+ end
967
+ Readline::HISTORY.pop if Readline::HISTORY.include? filename
968
+ # we still get dupes when user selects from history
969
+ return filename
970
+ end # }}}
971
+ # returns a forum name (that has a file in the cache).
972
+ # returned file name not forum name
973
+ def choose_filename
974
+ forums = get_cached_forums
975
+ ch, forum = menu "Select a file", forums
976
+ return forum_to_file(forum)
977
+ end
978
+ # TODO we can c=>ose these in the ctrlp fashion so i don't have to worry about first letter
979
+ def choose_forum_old
980
+ values = get_forum_list
981
+ ch, menu_text = menu "Choose forum: ", values
982
+ forum = menu_text
983
+ display_forum forum if forum
984
+ end
985
+ # returns list of forums both cached and in forumlist
986
+ def get_forum_list
987
+ # this assumes they are on the same dir
988
+ forums = get_cached_forums
989
+ f = $forumlist
990
+ forums.push(*f)
991
+ forums = forums.uniq
992
+ return forums
993
+ end
994
+ # if cached, fetch data from cache, else fetch from internet
995
+ def display_forum forum=nil
996
+ forum = choose_forum unless forum
997
+ return unless forum
998
+ $mode = nil
999
+ fn = data_cached_for forum
1000
+ if fn
1001
+ change_file fn
1002
+ else
1003
+ fetch_data_from_net forum
1004
+ end
1005
+ end
1006
+ # returns a list of cached forums with slashes
1007
+ # TODO need to take care of the actual location
1008
+ def get_cached_forums
1009
+ #values = Dir.glob("*.yml").map{|a| a.sub(".yml","").gsub("__","/"); }
1010
+ cp = File.expand_path $cache_path
1011
+ values = Dir.entries(cp).grep(/\.yml$/).map{|a| a.sub(".yml","").gsub("__","/"); }
1012
+ end
1013
+ def next_forum
1014
+ f = get_cached_forums.sort
1015
+ curr = f.index($subforum)
1016
+ log "no index for #{$subforum} " unless curr
1017
+ curr ||= 0
1018
+ fo = curr == f.size-1 ? f.first : f[curr + 1]
1019
+ display_forum fo
1020
+
1021
+ end
1022
+ def previous_forum
1023
+ f = get_cached_forums.sort
1024
+ curr = f.index($subforum)
1025
+ log "previous_forum: no index for #{$subforum} " unless curr
1026
+ curr ||= f.size
1027
+ fo = curr == 0 ? f.last : f[curr - 1]
1028
+ display_forum fo
1029
+ end
1030
+
1031
+ def add_to_forum_list f=nil
1032
+ unless f
1033
+ print "Add to forumlist: "
1034
+ f = Readline::readline('>', false)
1035
+ return if f.nil? or f == ""
1036
+ end
1037
+ $forumlist << f unless $forumlist.include? f
1038
+ log "Added #{f} to forumlist"
1039
+ end
1040
+ def delete_current_forum
1041
+ remove_from_forum_list $subforum
1042
+ end
1043
+ # remove from the forumlist, however if you already have yml files on disk, then they keep getting
1044
+ # read up.
1045
+ def remove_from_forum_list f=nil
1046
+ unless f
1047
+ print "Remove forum from list: "
1048
+ oldhist = Readline::HISTORY
1049
+ Readline::HISTORY.clear
1050
+ Readline::HISTORY.push(*$forumlist)
1051
+ f = Readline::readline('>', false)
1052
+ Readline::HISTORY.clear
1053
+ Readline::HISTORY.push(*oldhist)
1054
+ if f.nil? or f == ""
1055
+ return
1056
+ end
1057
+ end
1058
+ $forumlist.delete(f)
1059
+ log "Removed #{f} from forumlist"
1060
+ fn = data_cached_for f
1061
+ if fn
1062
+ system("mv #{fn} #{fn}.old")
1063
+ end
1064
+ perror "Removed #{f} from forum list and disk. Press a key."
1065
+ end
1066
+
1067
+ # display forum list in ctrlp fashion, selection by ENTER or cancel with ESCAPE C-c
1068
+ # returns selected forum (not filename)
1069
+ def choose_forum
1070
+ values = get_forum_list
1071
+ sel = ctrlp values
1072
+ #return unless sel
1073
+ #display_forum sel
1074
+ return sel
1075
+ end
1076
+ def data_cached_for filename
1077
+ f = "#{filename}.yml".gsub("/","__")
1078
+ f = "#{$cache_path}/#{f}"
1079
+ return f if File.exists? f
1080
+ perror "No data cached for #{f} "
1081
+ return nil
1082
+ end
1083
+ # get data for a cached file
1084
+ def change_file file=nil
1085
+ if file.nil?
1086
+ #fn = accept_file_name
1087
+ file = choose_filename
1088
+ return if file == ""
1089
+ end
1090
+ check_file_age file
1091
+ get_data file
1092
+ $filename = file
1093
+ post_cd
1094
+ end
1095
+ def compute_file_age fn
1096
+ s = File.stat(fn.chomp)
1097
+ mtime = s.mtime
1098
+ now = Time.now
1099
+ age = (now - mtime).to_i
1100
+ return (age/3600).to_i
1101
+ end
1102
+ def check_file_age fn
1103
+ age = compute_file_age fn
1104
+ log "#{fn} is #{age} hours old"
1105
+ if age > 24
1106
+ pbold "#{fn} is very old ( #{age} hours). Please refresh"
1107
+ elsif age > 8
1108
+ pbold "#{fn} is quite old ( #{age} hours). Please refresh"
1109
+ elsif age > 1
1110
+ # display it somewhere
1111
+ end
1112
+ $hint_message = get_hint
1113
+ if age > 1
1114
+ $hint_message = "#{ON_RED}#{age} hrs old. `r to refresh.#{CLEAR}"
1115
+ end
1116
+ return age
1117
+ end
1118
+
1119
+ def next_page
1120
+ $sta += $pagesize
1121
+ end
1122
+ def prev_page
1123
+ $sta -= $pagesize
1124
+ end
1125
+ def print_help
1126
+ system("clear")
1127
+ puts "HELP"
1128
+ puts
1129
+ puts "To act on a title press 1-9 a-z A-Z "
1130
+ #puts "Command Mode: Will prompt for a command to run on a file, after selecting using hotkey"
1131
+ #puts "Selection Mode: Each selection adds to selection list (toggles)"
1132
+ #puts " Upon exiting mode, user is prompted for a command to run on selected files"
1133
+ puts
1134
+ ary = []
1135
+ $bindings.each_pair { |k, v| ary.push "#{k.ljust(7)} => #{v}" }
1136
+ ary = columnate ary, $grows - 7
1137
+ ary.each {|line| print line, "\n" }
1138
+ get_char
1139
+
1140
+ end
1141
+ def show_marks
1142
+ puts
1143
+ puts "Bookmarks: "
1144
+ $bookmarks.each_pair { |k, v| puts "#{k.to_s.ljust(7)} => #{v}" }
1145
+ puts
1146
+ print "Enter bookmark to goto: "
1147
+ ch = get_char
1148
+ goto_bookmark(ch) if ch =~ /^[0-9a-z]$/
1149
+ end
1150
+ # MENU MAIN -- keep consistent with zfm
1151
+ def main_menu
1152
+ h = {
1153
+ "1" => :view_article,
1154
+ "2" => :view_comments,
1155
+ :f => :display_forum,
1156
+ :v => :view_menu,
1157
+ :r => :reload,
1158
+ :m => :fetch_more,
1159
+ :R => :reddit_options,
1160
+ :H => :hacker_options,
1161
+ :s => :sort_menu,
1162
+ :C => :config_menu,
1163
+ :a => :view_article,
1164
+ :c => :view_comments,
1165
+ :x => :extras
1166
+ }
1167
+ =begin
1168
+ :a => :ack,
1169
+ "/" => :ffind,
1170
+ :l => :locate,
1171
+ :v => :viminfo,
1172
+ :z => :z_interface,
1173
+ :d => :child_dirs,
1174
+ :r => :recent_files,
1175
+ :t => :dirtree,
1176
+ "4" => :tree,
1177
+ :F => :filter_menu,
1178
+ :c => :command_menu ,
1179
+ :B => :bindkey_ext_command,
1180
+ :M => :newdir,
1181
+ "%" => :newfile,
1182
+ =end
1183
+
1184
+ menu "Main Menu", h
1185
+ end
1186
+ def hacker_options menu_text=nil
1187
+ h = {
1188
+ :n => :news,
1189
+ :w => :newest,
1190
+ :s => :show,
1191
+ :j => :jobs,
1192
+ :a => :ask
1193
+ }
1194
+ # TODO ask article needs host name prepended
1195
+ # TODO jobs has no comments, check if nil
1196
+ unless menu_text
1197
+ ch, menu_text = menu "Hacker Options", h
1198
+ end
1199
+ if menu_text
1200
+ m = menu_text.to_s
1201
+ fetch_data_from_net m
1202
+ end
1203
+ end
1204
+ def reddit_options menu_text=nil
1205
+ if $hacker_forums.include? $subforum
1206
+ perror "Reddit options invalid inside Hacker News subforum"
1207
+ return
1208
+ end
1209
+ h = {
1210
+ :n => :new,
1211
+ :r => :rising,
1212
+ :c => :controversial,
1213
+ :t => :top,
1214
+ :h => :hot
1215
+ }
1216
+ unless menu_text
1217
+ ch, menu_text = menu "Reddit Options for #{$subforum} ", h
1218
+ end
1219
+ if menu_text
1220
+ if menu_text == :hot
1221
+ fetch_data_from_net "#{$subforum}"
1222
+ else
1223
+ m = menu_text.to_s
1224
+ s = "#{$subforum}".sub(/\/.*/, '')
1225
+ fetch_data_from_net "#{s}/#{m}"
1226
+ end
1227
+ end
1228
+ end
1229
+ def config_menu
1230
+ h = {
1231
+ :i => :increase_number_of_pages,
1232
+ :b => :set_browsers,
1233
+ :o => :open_command
1234
+ }
1235
+ ch, menu_text = menu "Configuration Menu", h
1236
+ case menu_text
1237
+ when :open_command
1238
+ print "Enter open command: "
1239
+ str = Readline::readline('>', true)
1240
+ if str && str != ""
1241
+ $open_command = str
1242
+ end
1243
+ when :increase_number_of_pages
1244
+ $num_pages += 1
1245
+ end
1246
+ end
1247
+ def view_menu
1248
+ h = {
1249
+ :a => :view_article,
1250
+ :c => :view_comments,
1251
+ :l => :view_log
1252
+ }
1253
+ menu "View Menu", h
1254
+ end
1255
+ def set_browsers
1256
+ s = gets_with_values "Enter gui browser", ["open"]
1257
+ $browser_gui = s if s
1258
+ s = gets_with_values "Enter text browser", %w[ elinks links w3m lynx ]
1259
+ $browser_text = s if s
1260
+ end
1261
+ # dispay a menu with numbering, allowing selection by pressing index
1262
+ # Take a char, so use alphabets for index, if items more than 9
1263
+ def menu title, h
1264
+ case h
1265
+ when Hash
1266
+ ; # okay
1267
+ when Array
1268
+ # convert array to a hash using letters for key
1269
+ x = "a"
1270
+ hash = Hash.new
1271
+ h.each do |e|
1272
+ hash[x] = e
1273
+ x.succ!
1274
+ end
1275
+ h = hash
1276
+ end
1277
+ return unless h
1278
+
1279
+ kmax_length = h.keys.max_by(&:length).length
1280
+ vmax_length = h.values.max_by(&:length).length
1281
+ puts "#{ON_BLUE}#{title}#{CLEAR}"
1282
+ h.each_pair { |k, v|
1283
+ d = get_action_desc(v) || ""
1284
+ printf " #{BOLD}%-*s#{CLEAR} %-*s #{BOLD}%s#{CLEAR}\n", kmax_length,k, vmax_length,v, d
1285
+ }
1286
+ ch = get_char
1287
+ binding = h[ch]
1288
+ binding = h[ch.to_sym] unless binding
1289
+ if binding
1290
+ if respond_to?(binding, true)
1291
+ send(binding)
1292
+ end
1293
+ end
1294
+ return ch, binding
1295
+ end
1296
+ # todo : use hidden to hide old entries like older than a day or n hours
1297
+ def toggle_menu menu_text=nil
1298
+ unless menu_text
1299
+ h = {
1300
+ # :h => :toggle_hidden,
1301
+ :c => :toggle_case, :l => :toggle_long_list , "1" => :toggle_columns,
1302
+ :g => :use_gui_browser, :t => :use_text_browser}
1303
+ ch, menu_text = menu "Toggle Menu", h
1304
+ end
1305
+ case menu_text
1306
+ when :toggle_hidden
1307
+ $hidden = $hidden ? nil : "D"
1308
+ refresh
1309
+ when :toggle_case
1310
+ #$ignorecase = $ignorecase ? "" : "i"
1311
+ $ignorecase = !$ignorecase
1312
+ refresh
1313
+ when :toggle_columns
1314
+ $gviscols = 3 if $gviscols == 1
1315
+ $long_listing = false if $gviscols > 1
1316
+ x = $grows * $gviscols
1317
+ $pagesize = $pagesize==x ? $grows : x
1318
+ when :use_gui_browser
1319
+ $open_command = $browser_gui || "open"
1320
+ when :use_text_browser
1321
+ $open_command = $browser_text || "elinks"
1322
+
1323
+ when :toggle_long_list
1324
+ $long_listing = !$long_listing
1325
+ if $long_listing
1326
+ $gviscols = 1
1327
+ $pagesize = $grows
1328
+ else
1329
+ x = $grows * $gviscols
1330
+ $pagesize = $pagesize==x ? $grows : x
1331
+ end
1332
+ refresh
1333
+ end
1334
+ end
1335
+
1336
+ def column_menu row
1337
+ h = {}
1338
+ row.each_with_index do |e, i|
1339
+ h[$IDX[i]] = e
1340
+ end
1341
+ ch, menu_text = menu "Columns Menu", h
1342
+ return $IDX.index(ch)
1343
+ end
1344
+
1345
+ def sort col=nil
1346
+ col = col.to_i if col
1347
+ col = $column_enum.next unless col
1348
+ $full_data = $full_data.sort_by{|x| -x[col].to_i}
1349
+ $files = []
1350
+ $full_data.each do |e|
1351
+ $files << e.first
1352
+ end
1353
+ return
1354
+ end
1355
+ def sort_age ; sort_menu :age; end
1356
+ def sort_points ; sort_menu :points; end
1357
+ def sort_comments ; sort_menu :comments; end
1358
+ def sort_menu menu_text = nil
1359
+ # next line is so user can enter on subcommand
1360
+ menu_text = menu_text.to_sym if menu_text.is_a? String
1361
+
1362
+ lo = nil
1363
+ unless menu_text
1364
+ h = { :a => :age, :c => :comments, :p => :points }
1365
+ ch, menu_text = menu "Sort Menu", h
1366
+ end
1367
+ menu_text = ch unless menu_text
1368
+ case menu_text
1369
+ when :age, "1"
1370
+ sort 6
1371
+ when :comments, "2"
1372
+ sort 2
1373
+ when :points, "3"
1374
+ sort 3
1375
+ end
1376
+ $sorto = menu_text
1377
+ end
1378
+ alias :sort_on :sort_menu
1379
+
1380
+ def command_menu
1381
+ ##
1382
+ # since these involve full paths, we need more space, like only one column
1383
+ #
1384
+ ## in these cases, getting back to the earlier dir, back to earlier listing
1385
+ # since we've basically overlaid the old listing
1386
+ #
1387
+ # should be able to sort THIS listing and not rerun command. But for that I'd need to use
1388
+ # xargs ls -t etc rather than the zsh sort order. But we can run a filter using |.
1389
+ #
1390
+ h = { :t => :today, :D => :default_command , :R => :remove_from_list}
1391
+ if $editor_mode
1392
+ h[:e] = :pager_mode
1393
+ else
1394
+ h[:e] = :editor_mode
1395
+ end
1396
+ ch, menu_text = menu "Command Menu", h
1397
+ case menu_text
1398
+ when :pager_mode
1399
+ $editor_mode = false
1400
+ $default_command = ENV['MANPAGER'] || ENV['PAGER']
1401
+ when :editor_mode
1402
+ $editor_mode = true
1403
+ $default_command = nil
1404
+ when :ffind
1405
+ ffind
1406
+ when :locate
1407
+ locate
1408
+ when :today
1409
+ $files = `zsh -c 'print -rl -- *(#{$hidden}Mm0)'`.split("\n")
1410
+ $title = "Today's files"
1411
+ when :default_command
1412
+ print "Selecting a file usually invokes $EDITOR, what command do you want to use repeatedly on selected files: "
1413
+ $default_command = gets().chomp
1414
+ if $default_command != ""
1415
+ print "Second part of command (maybe blank): "
1416
+ $default_command2 = gets().chomp
1417
+ else
1418
+ print "Cleared default command, will default to $EDITOR"
1419
+ $default_command2 = nil
1420
+ $default_command = nil
1421
+ end
1422
+ end
1423
+ end
1424
+ def extras
1425
+ h = { "1" => :one_column, "2" => :multi_column, :c => :columns, :r => :config_read , :w => :config_write}
1426
+ ch, menu_text = menu "Extras Menu", h
1427
+ case menu_text
1428
+ when :one_column
1429
+ $pagesize = $grows
1430
+ when :multi_column
1431
+ #$pagesize = 60
1432
+ $pagesize = $grows * $gviscols
1433
+ when :columns
1434
+ print "How many columns to show: 1-6 [current #{$gviscols}]? "
1435
+ ch = get_char
1436
+ ch = ch.to_i
1437
+ if ch > 0 && ch < 7
1438
+ $gviscols = ch.to_i
1439
+ $pagesize = $grows * $gviscols
1440
+ end
1441
+ end
1442
+ end
1443
+ def select_used_dirs
1444
+ return
1445
+ $title = "Used Directories"
1446
+ $files = $used_dirs.uniq
1447
+ end
1448
+ def select_visited_files
1449
+ return
1450
+ # not yet a unique list, needs to be unique and have latest pushed to top
1451
+ $title = "Visited Files"
1452
+ $files = $visited_files.uniq
1453
+ end
1454
+ # TODO
1455
+ # This shows bookmarks in article view but the marks are the hints not the keys themselves
1456
+ def select_bookmarks
1457
+ # added changes in both select_current and select_hint
1458
+ # However, the age mark that is show is still for the earlier shown forum based on outdated full_data
1459
+ # So we need to show age mark based on file datw which means a change in display program !!! At least
1460
+ # clear the array full_data
1461
+ $mode = "forum"
1462
+ $title = "Bookmarks"
1463
+ $files = $bookmarks.values
1464
+
1465
+ end
1466
+
1467
+ ## part copied and changed from change_dir since we don't dir going back on top
1468
+ # or we'll be stuck in a cycle
1469
+ def pop_dir
1470
+ return # 2014-07-25 - 22:43
1471
+ # the first time we pop, we need to put the current on stack
1472
+ if !$visited_dirs.index(Dir.pwd)
1473
+ $visited_dirs.push Dir.pwd
1474
+ end
1475
+ ## XXX make sure thre is something to pop
1476
+ d = $visited_dirs.delete_at 0
1477
+ ## XXX make sure the dir exists, cuold have been deleted. can be an error or crash otherwise
1478
+ $visited_dirs.push d
1479
+ Dir.chdir d
1480
+ $filterstr ||= "M"
1481
+ $files = `zsh -c 'print -rl -- *(#{$sorto}#{$hidden}#{$filterstr})'`.split("\n")
1482
+ post_cd
1483
+ end
1484
+ def post_cd
1485
+ $patt=nil
1486
+ $sta = 0
1487
+ $cursor = $min_index
1488
+ $title = nil
1489
+ #if $selected_files.size > 0
1490
+ #$selected_files = []
1491
+ #end
1492
+ $visual_block_start = nil
1493
+ $stact = 0
1494
+ screen_settings
1495
+
1496
+ revert_dir_pos
1497
+ end
1498
+ #
1499
+ ## read dirs and files and bookmarks from file
1500
+ def config_read
1501
+ f = File.expand_path(CONFIG_FILE)
1502
+ if File.readable? f
1503
+ load f
1504
+ # maybe we should check for these existing else crash will happen.
1505
+ #$bookmarks.push(*bookmarks) if bookmarks
1506
+ log "loaded #{CONFIG_FILE} "
1507
+ end
1508
+ end
1509
+ def get_env_paths
1510
+ files = []
1511
+ %w{ GEM_HOME PYTHONHOME}.each do |p|
1512
+ d = ENV[p]
1513
+ files.push d if d
1514
+ end
1515
+ %w{ RUBYLIB RUBYPATH GEM_PATH PYTHONPATH }.each do |p|
1516
+ d = ENV[p]
1517
+ files.concat d.split(":") if d
1518
+ end
1519
+ return files
1520
+ end
1521
+
1522
+ ## save dirs and files and bookmarks to a file
1523
+ def config_write
1524
+ return
1525
+ # Putting it in a format that zfm can also read and write
1526
+ #f1 = File.expand_path("~/.zfminfo")
1527
+ f1 = File.expand_path(CONFIG_FILE)
1528
+ d = $used_dirs.join ":"
1529
+ f = $visited_files.join ":"
1530
+ File.open(f1, 'w+') do |f2|
1531
+ # use "\n" for two lines of text
1532
+ f2.puts "DIRS=\"#{d}\""
1533
+ f2.puts "FILES=\"#{f}\""
1534
+ $bookmarks.each_pair { |k, val|
1535
+ f2.puts "BM_#{k}=\"#{val}\""
1536
+ #f2.puts "BOOKMARKS[\"#{k}\"]=\"#{val}\""
1537
+ }
1538
+ end
1539
+ $writing = $modified = false
1540
+ end
1541
+
1542
+ ## accept a character to save this dir as a bookmark
1543
+ def create_bookmark
1544
+ print "Enter a to z or 0-9 for bookmark: "
1545
+ ch = get_char
1546
+ if ch =~ /^[0-9a-z]$/
1547
+ $bookmarks[ch.sym] = "#{$subforum}"
1548
+ $modified = true
1549
+ else
1550
+ perror "Bookmark must be lower-case character or number."
1551
+ end
1552
+ end
1553
+ # currently just send to file-actions till we think of better stuff
1554
+ def register_command cmd
1555
+ $commandlist << cmd
1556
+ end
1557
+ def gets_with_history prompt, histkey=nil
1558
+ histkey ||= prompt
1559
+ print "#{prompt}: "
1560
+ oldhist = Readline::HISTORY
1561
+ Readline::HISTORY.clear
1562
+ values = $history[:histkey]
1563
+ Readline::HISTORY.push(*values)
1564
+ command = Readline::readline('>', false)
1565
+ Readline::HISTORY.clear
1566
+ Readline::HISTORY.push(*oldhist)
1567
+ if command and command != ""
1568
+ values << command unless values.include? command
1569
+ $history[histkey] = values
1570
+ return command
1571
+ end
1572
+ return false
1573
+ end
1574
+ def gets_with_values prompt, values
1575
+ print "#{prompt}: "
1576
+ oldhist = Readline::HISTORY
1577
+ Readline::HISTORY.clear
1578
+ Readline::HISTORY.push(*values)
1579
+ command = Readline::readline('>', false)
1580
+ Readline::HISTORY.clear
1581
+ Readline::HISTORY.push(*oldhist)
1582
+ if command and command != ""
1583
+ return command
1584
+ end
1585
+ return nil
1586
+ end
1587
+ def subcommand
1588
+ print "Enter command: "
1589
+ oldhist = Readline::HISTORY
1590
+ Readline::HISTORY.clear
1591
+ Readline::HISTORY.push(*$commandlist)
1592
+ command = Readline::readline(':', false)
1593
+ Readline::HISTORY.push(*oldhist)
1594
+ return if command.nil? or command == ""
1595
+ command = "fetch_data_from_net #{$subforum}" if command == "r" #or command == "refresh"
1596
+ command = "next_forum" if command == "n"
1597
+ command = "previous_forum" if command == "p"
1598
+ if $commandlist.include? command
1599
+ send(command)
1600
+ end
1601
+ if command =~ /^\d+$/
1602
+ goto_line command
1603
+ end
1604
+ exec_string(command) if command.index(" ")
1605
+
1606
+ return
1607
+ print "Enter command: "
1608
+ begin
1609
+ #command = gets().chomp
1610
+ command = Readline::readline('>', true)
1611
+ return if command == ""
1612
+ rescue Exception => ex
1613
+ return
1614
+ end
1615
+ if command == "q"
1616
+ if $modified
1617
+ print "Do you want to save bookmarks? (y/n): "
1618
+ ch = get_char
1619
+ if ch == "y"
1620
+ $writing = true
1621
+ $quitting = true
1622
+ elsif ch == "n"
1623
+ $quitting = true
1624
+ print "Quitting without saving bookmarks"
1625
+ else
1626
+ perror "No action taken."
1627
+ end
1628
+ else
1629
+ $quitting = true
1630
+ end
1631
+ elsif command == "wq"
1632
+ $quitting = true
1633
+ $writing = true
1634
+ elsif command == "x"
1635
+ $quitting = true
1636
+ $writing = true if $modified
1637
+ elsif command == "p"
1638
+ system "echo $PWD | pbcopy"
1639
+ puts "Stored PWD in clipboard (using pbcopy)"
1640
+ end
1641
+ end
1642
+ def quit_command
1643
+ if $modified
1644
+ puts "Press w to save bookmarks before quitting " if $modified
1645
+ print "Press another q to quit "
1646
+ ch = get_char
1647
+ else
1648
+ $quitting = true
1649
+ end
1650
+ $quitting = true if ch == "q"
1651
+ $quitting = $writing = true if ch == "w"
1652
+ end
1653
+
1654
+ def views
1655
+ # switch between long list, single row and multi row
1656
+ views=[:toggle_columns , :toggle_long_list, :sort_age, :sort_comments, :sort_points]
1657
+ viewlabels=%w[Dirs Newest Accessed Oldest Largest Smallest Reverse Name]
1658
+ $sorto = nil
1659
+ #$title = viewlabels[$viewctr]
1660
+ $viewctr += 1
1661
+ $viewctr = 0 if $viewctr > views.size
1662
+ option = views[$viewctr] || views.first
1663
+ case option
1664
+ when :toggle_columns, :toggle_long_list
1665
+ toggle_menu option
1666
+ else
1667
+ send(option)
1668
+ end
1669
+
1670
+ return # 2014-07-25 - 22:43
1671
+
1672
+ end
1673
+ # on pressing ENTER key
1674
+ def select_current
1675
+ ## vp is local there, so i can do $vp[0]
1676
+ #open_file $view[$sta] if $view[$sta]
1677
+ if $mode == "forum"
1678
+ display_forum $view[$cursor]
1679
+ else
1680
+ on_enter get_current_title
1681
+ end
1682
+ end
1683
+ def on_enter title
1684
+ fullrow = get_full_row(title)
1685
+ puts "#{BOLD}#{title}#{CLEAR}"
1686
+ puts "#{fullrow[-1]}"
1687
+ article = fullrow[4]
1688
+ comments = fullrow[5]
1689
+ puts " "
1690
+ puts "a ) article #{article}"
1691
+ puts "c ) comments #{comments}"
1692
+ print "> "
1693
+ ch = get_char
1694
+ if ch == "a"
1695
+ system "#{$open_command} #{article}"
1696
+ elsif ch == "c"
1697
+ system "#{$open_command} #{comments}"
1698
+ end
1699
+ end
1700
+
1701
+ ## create a list of dirs in which some action has happened, for saving
1702
+ def push_used_dirs d=Dir.pwd
1703
+ $used_dirs.index(d) || $used_dirs.push(d)
1704
+ end
1705
+ def pbold text
1706
+ puts "#{BOLD}#{text}#{BOLD_OFF}"
1707
+ end
1708
+ def perror text
1709
+ puts "#{RED}#{text}#{CLEAR}"
1710
+ get_char
1711
+ end
1712
+ def pause text=" Press a key ..."
1713
+ print text
1714
+ get_char
1715
+ end
1716
+ ## return shortcut for an index (offset in file array)
1717
+ # use 2 more arrays to make this faster
1718
+ # if z or Z take another key if there are those many in view
1719
+ # Also, display ROWS * COLS so now we are not limited to 60.
1720
+ def get_shortcut ix
1721
+ return "<" if ix < $stact
1722
+ ix -= $stact
1723
+ i = $IDX[ix]
1724
+ return i if i
1725
+ return "->"
1726
+ end
1727
+ ## returns the integer offset in view (file array based on a-y za-zz and Za - Zz
1728
+ # Called when user types a key
1729
+ # should we even ask for a second key if there are not enough rows
1730
+ # What if we want to also trap z with numbers for other purposes
1731
+ def get_index key, vsz=999
1732
+ i = $IDX.index(key)
1733
+ return i+$stact if i
1734
+ #sz = $IDX.size
1735
+ zch = nil
1736
+ if vsz > 25
1737
+ if key == "z" || key == "Z"
1738
+ print key
1739
+ zch = get_char
1740
+ print zch
1741
+ i = $IDX.index("#{key}#{zch}")
1742
+ return i+$stact if i
1743
+ end
1744
+ end
1745
+ return nil
1746
+ end
1747
+
1748
+ ## generic external command program
1749
+ # prompt is the user friendly text of command such as list for ls, or extract for dtrx, page for less
1750
+ # pauseyn is whether to pause after command as in file or ls
1751
+ #
1752
+ def command_file prompt, *command
1753
+ pauseyn = command.shift
1754
+ command = command.join " "
1755
+ print "[#{prompt}] Choose a file [#{$view[$cursor]}]: "
1756
+ file = ask_hint $view[$cursor]
1757
+ #print "#{prompt} :: Enter file shortcut: "
1758
+ #file = ask_hint
1759
+ perror "Command Cancelled" unless file
1760
+ return unless file
1761
+ file = File.expand_path(file)
1762
+ if File.exists? file
1763
+ file = Shellwords.escape(file)
1764
+ pbold "#{command} #{file} (#{pauseyn})"
1765
+ system "#{command} #{file}"
1766
+ pause if pauseyn == "y"
1767
+ refresh
1768
+ else
1769
+ perror "File #{file} not found"
1770
+ end
1771
+ end
1772
+
1773
+ ## prompt user for file shortcut and return file or nil
1774
+ #
1775
+ def ask_hint deflt=nil
1776
+ f = nil
1777
+ ch = get_char
1778
+ if ch == "ENTER"
1779
+ return deflt
1780
+ end
1781
+ ix = get_index(ch, $viewport.size)
1782
+ f = $viewport[ix] if ix
1783
+ return f
1784
+ end
1785
+
1786
+ ## check screen size and accordingly adjust some variables
1787
+ #
1788
+ def screen_settings
1789
+ $glines=%x(tput lines).to_i
1790
+ $gcols=%x(tput cols).to_i
1791
+ $grows = $glines - 3
1792
+ $pagesize = 60
1793
+ #$gviscols = 3
1794
+ $pagesize = $grows * $gviscols
1795
+ end
1796
+ ## moves column offset so we can reach unindexed columns or entries
1797
+ # 0 forward and any other back/prev
1798
+ def column_next dir=0
1799
+ if dir == 0
1800
+ $stact += $grows
1801
+ $stact = 0 if $stact >= $viewport.size
1802
+ else
1803
+ $stact -= $grows
1804
+ $stact = 0 if $stact < 0
1805
+ end
1806
+ end
1807
+ # currently i am only passing the action in from the list there as a key
1808
+ # I should be able to pass in new actions that are external commands
1809
+ # c-x CX cx
1810
+ def forum_actions
1811
+ h = { :r => :reload,
1812
+ :n => :new_forum,
1813
+ :b => :display_forum,
1814
+ 'C-b' => :change_subforum,
1815
+ :N => :new_articles ,
1816
+ :k => :delete_current_forum,
1817
+ :RIGHT => :next_forum,
1818
+ :LEFT => :previous_forum,
1819
+ :l => :view_log,
1820
+ :C => :config_menu,
1821
+ 'C-c' => :quit_app}
1822
+
1823
+ ch, menu_text = menu "Forum Menu ", h
1824
+ menu_text = :quit if ch == "q"
1825
+ return unless menu_text
1826
+ case menu_text.to_sym
1827
+ when :quit
1828
+ when :reload
1829
+ fetch_data_from_net $subforum
1830
+ when :pages
1831
+ $num_pages += 1
1832
+ when :new_articles
1833
+ new_articles
1834
+ when :new_forum
1835
+ fetch_data_from_net
1836
+ when :change_subforum
1837
+ change_file
1838
+ when :quit_app
1839
+ quit_command
1840
+ when :view_log
1841
+ view_log
1842
+ when :open_command
1843
+ print "Enter open command: "
1844
+ str = Readline::readline('>', true)
1845
+ if str && str != ""
1846
+ $open_command = str
1847
+ end
1848
+ end
1849
+ end
1850
+ def new_articles
1851
+ case $host
1852
+ when :hn
1853
+ fetch_data_from_net "newest"
1854
+ else
1855
+ fetch_data_from_net "#{$subforum}/new"
1856
+ end
1857
+ end
1858
+
1859
+ def columns_incdec howmany
1860
+ $gviscols += howmany.to_i
1861
+ $gviscols = 1 if $gviscols < 1
1862
+ $gviscols = 6 if $gviscols > 6
1863
+ $pagesize = $grows * $gviscols
1864
+ end
1865
+
1866
+ # bind a key to an external command wich can be then be used for files
1867
+ def bindkey_ext_command
1868
+ print
1869
+ pbold "Bind a capital letter to an external command"
1870
+ print "Enter a capital letter to bind: "
1871
+ ch = get_char
1872
+ return if ch == "Q"
1873
+ if ch =~ /^[A-Z]$/
1874
+ print "Enter an external command to bind to #{ch}: "
1875
+ com = gets().chomp
1876
+ if com != ""
1877
+ print "Enter prompt for command (blank if same as command): "
1878
+ pro = gets().chomp
1879
+ pro = com if pro == ""
1880
+ end
1881
+ print "Pause after output [y/n]: "
1882
+ yn = get_char
1883
+ $bindings[ch] = "command_file #{pro} #{yn} #{com}"
1884
+ end
1885
+ end
1886
+
1887
+
1888
+ ## some cursor movement functions
1889
+ ##
1890
+ #
1891
+ def cursor_scroll_dn
1892
+ moveto(pos() + MSCROLL)
1893
+ end
1894
+ def cursor_scroll_up
1895
+ moveto(pos() - MSCROLL)
1896
+ end
1897
+ def cursor_dn
1898
+ moveto(pos() + 1)
1899
+ end
1900
+ def cursor_up
1901
+ moveto(pos() - 1)
1902
+ end
1903
+ def pos
1904
+ $cursor
1905
+ end
1906
+
1907
+ def moveto pos
1908
+ orig = $cursor
1909
+ $cursor = pos
1910
+ #$cursor = [$cursor, $view.size - 1].min
1911
+ ## 2014-07-28 - 00:45 trying out viewport instead of view
1912
+ # this prevents cursor going off when we press down arrow on last row esp on multi-page listings
1913
+ # How does that impact the rest of this method, such as visual_mode
1914
+ $cursor = [$cursor, $viewport.size - 1].min
1915
+ $cursor = [$cursor, $min_index].max
1916
+ star = [orig, $cursor].min
1917
+ fin = [orig, $cursor].max
1918
+ if $visual_mode
1919
+ # PWD has to be there in selction
1920
+ if $selected_files.index $view[$cursor]
1921
+ # this depends on the direction
1922
+ $selected_files = $selected_files - $view[star..fin]
1923
+ ## current row remains in selection always.
1924
+ $selected_files.push $view[$cursor]
1925
+ else
1926
+ $selected_files.concat $view[star..fin]
1927
+ end
1928
+ end
1929
+ end
1930
+ def visual_mode_toggle
1931
+ $visual_mode = !$visual_mode
1932
+ if $visual_mode
1933
+ $visual_block_start = $cursor
1934
+ $selected_files.push $view[$cursor]
1935
+ end
1936
+ end
1937
+ def visual_block_clear
1938
+ if $visual_block_start
1939
+ star = [$visual_block_start, $cursor].min
1940
+ fin = [$visual_block_start, $cursor].max
1941
+ $selected_files = $selected_files - $view[star..fin]
1942
+ end
1943
+ $visual_block_start = nil
1944
+ $visual_mode = nil
1945
+ end
1946
+ def file_starting_with fc
1947
+ ix = return_next_match(method(:file_matching?), "^#{fc}")
1948
+ if ix
1949
+ goto_line ix
1950
+ end
1951
+ end
1952
+ def file_matching? file, patt
1953
+ file =~ /#{patt}/
1954
+ end
1955
+
1956
+ ## generic method to take cursor to next position for a given condition
1957
+ def return_next_match binding, *args
1958
+ first = nil
1959
+ ix = 0
1960
+ $view.each_with_index do |elem,ii|
1961
+ if binding.call(elem, *args)
1962
+ first ||= ii
1963
+ if ii > $cursor
1964
+ ix = ii
1965
+ break
1966
+ end
1967
+ end
1968
+ end
1969
+ return first if ix == 0
1970
+ return ix
1971
+ end
1972
+ ##
1973
+ # position cursor on a specific line which could be on a nother page
1974
+ # therefore calculate the correct start offset of the display also.
1975
+ def goto_line pos
1976
+ pos = pos.to_i
1977
+ pages = ((pos * 1.00)/$pagesize).ceil
1978
+ pages -= 1
1979
+ $sta = pages * $pagesize + 1
1980
+ $cursor = pos
1981
+ end
1982
+ def filetype f
1983
+ return nil unless f
1984
+ f = Shellwords.escape(f)
1985
+ s = `file #{f}`
1986
+ if s.index "text"
1987
+ return :text
1988
+ elsif s.index(/[Zz]ip/)
1989
+ return :zip
1990
+ elsif s.index("archive")
1991
+ return :zip
1992
+ elsif s.index "image"
1993
+ return :image
1994
+ elsif s.index "data"
1995
+ return :text
1996
+ end
1997
+ nil
1998
+ end
1999
+
2000
+ def save_dir_pos
2001
+ return if $sta == $min_index && $cursor == $min_index
2002
+ $dir_position[Dir.pwd] = [$sta, $cursor]
2003
+ end
2004
+ def revert_dir_pos
2005
+ $sta = 0
2006
+ $cursor = $min_index
2007
+ a = $dir_position[Dir.pwd]
2008
+ if a
2009
+ $sta = a.first
2010
+ $cursor = a[1]
2011
+ raise "sta is nil for #{Dir.pwd} : #{$dir_position[Dir.pwd]}" unless $sta
2012
+ raise "cursor is nil" unless $cursor
2013
+ end
2014
+ end
2015
+ def newdir
2016
+ print
2017
+ print "Enter directory name: "
2018
+ str = Readline::readline('>', true)
2019
+ return if str == ""
2020
+ if File.exists? str
2021
+ perror "#{str} exists."
2022
+ return
2023
+ end
2024
+ begin
2025
+ FileUtils.mkdir str
2026
+ $used_dirs.insert(0, str) if File.exists?(str)
2027
+ refresh
2028
+ rescue Exception => ex
2029
+ perror "Error in newdir: #{ex}"
2030
+ end
2031
+ end
2032
+ def newfile
2033
+ print
2034
+ print "Enter file name: "
2035
+ str = Readline::readline('>', true)
2036
+ return if str == ""
2037
+ system "$EDITOR #{str}"
2038
+ $visited_files.insert(0, str) if File.exists?(str)
2039
+ refresh
2040
+ end
2041
+
2042
+ ##
2043
+ # Editing of the User Dir List.
2044
+ # remove current entry from used dirs list, since we may not want some entries being there
2045
+ #
2046
+
2047
+ def remove_from_list
2048
+ return unless $selection_allowed
2049
+ if $selected_files.size > 0
2050
+ sz = $selected_files.size
2051
+ print "Remove #{sz} files from used list (y)?: "
2052
+ ch = get_char
2053
+ return if ch != "y"
2054
+ $used_dirs = $used_dirs - $selected_files
2055
+ $visited_files = $visited_files - $selected_files
2056
+ unselect_all
2057
+ $modified = true
2058
+ return
2059
+ end
2060
+ print
2061
+ ## what if selected some rows
2062
+ file = $view[$cursor]
2063
+ print "Remove #{file} from used list (y)?: "
2064
+ ch = get_char
2065
+ return if ch != "y"
2066
+ file = File.expand_path(file)
2067
+ if File.directory? file
2068
+ $used_dirs.delete(file)
2069
+ else
2070
+ $visited_files.delete(file)
2071
+ end
2072
+ refresh
2073
+ $modified = true
2074
+ end
2075
+ #
2076
+ # If there's a short file list, take recently mod and accessed folders and put latest
2077
+ # files from there and insert it here. I take both since recent mod can be binaries / object
2078
+ # files and gems created by a process, and not actually edited files. Recent accessed gives
2079
+ # latest source, but in some cases even this can be misleading since running a program accesses
2080
+ # include files.
2081
+ def enhance_file_list
2082
+ return
2083
+ return unless $enhanced_mode
2084
+ # if only one entry and its a dir
2085
+ # get its children and maybe the recent mod files a few
2086
+
2087
+ if $files.size == 1
2088
+ # its a dir, let give the next level at least
2089
+ if $files.first[-1] == "/"
2090
+ d = $files.first
2091
+ f = `zsh -c 'print -rl -- #{d}*(omM)'`.split("\n")
2092
+ if f && f.size > 0
2093
+ $files.concat f
2094
+ $files.concat get_important_files(d)
2095
+ return
2096
+ end
2097
+ else
2098
+ # just a file, not dirs here
2099
+ return
2100
+ end
2101
+ end
2102
+ #
2103
+ # check if a ruby project dir, although it could be a backup file too,
2104
+ # if so , expand lib and maby bin, put a couple recent files
2105
+ #
2106
+ if $files.index("Gemfile") || $files.grep(/\.gemspec/).size > 0
2107
+ # usually the lib dir has only one file and one dir
2108
+ flg = false
2109
+ $files.concat get_important_files(Dir.pwd)
2110
+ if $files.index("lib/")
2111
+ f = `zsh -c 'print -rl -- lib/*(om[1,5]M)'`.split("\n")
2112
+ if f && f.size() > 0
2113
+ insert_into_list("lib/", f)
2114
+ flg = true
2115
+ end
2116
+ dd = File.basename(Dir.pwd)
2117
+ if f.index("lib/#{dd}/")
2118
+ f = `zsh -c 'print -rl -- lib/#{dd}/*(om[1,5]M)'`.split("\n")
2119
+ if f && f.size() > 0
2120
+ insert_into_list("lib/#{dd}/", f)
2121
+ flg = true
2122
+ end
2123
+ end
2124
+ end
2125
+ if $files.index("bin/")
2126
+ f = `zsh -c 'print -rl -- bin/*(om[1,5]M)'`.split("\n")
2127
+ insert_into_list("bin/", f) if f && f.size() > 0
2128
+ flg = true
2129
+ end
2130
+ return if flg
2131
+
2132
+ # lib has a dir in it with the gem name
2133
+
2134
+ end
2135
+ return if $files.size > 15
2136
+
2137
+ ## first check accessed else modified will change accessed
2138
+ moda = `zsh -c 'print -rn -- *(/oa[1]M)'`
2139
+ if moda && moda != ""
2140
+ modf = `zsh -c 'print -rn -- #{moda}*(oa[1]M)'`
2141
+ if modf && modf != ""
2142
+ insert_into_list moda, modf
2143
+ end
2144
+ modm = `zsh -c 'print -rn -- #{moda}*(om[1]M)'`
2145
+ if modm && modm != "" && modm != modf
2146
+ insert_into_list moda, modm
2147
+ end
2148
+ end
2149
+ ## get last modified dir
2150
+ modm = `zsh -c 'print -rn -- *(/om[1]M)'`
2151
+ if modm != moda
2152
+ modmf = `zsh -c 'print -rn -- #{modm}*(oa[1]M)'`
2153
+ insert_into_list modm, modmf
2154
+ modmf1 = `zsh -c 'print -rn -- #{modm}*(om[1]M)'`
2155
+ insert_into_list(modm, modmf1) if modmf1 != modmf
2156
+ else
2157
+ # if both are same then our options get reduced so we need to get something more
2158
+ # If you access the latest mod dir, then come back you get only one, since mod and accessed
2159
+ # are the same dir, so we need to find the second modified dir
2160
+ end
2161
+ end
2162
+
2163
+ # log messages that may not remain on the screen by user may wish to see
2164
+ # such as age of current file being displayed.
2165
+ def log str
2166
+ $logger_array.unshift str
2167
+ end
2168
+ def view_log
2169
+ page_data $logger_array
2170
+ end
2171
+ def clear_log
2172
+ $logger_array.clear
2173
+ end
2174
+ def page_data array
2175
+ require 'tempfile'
2176
+ f = Tempfile.new("corvuslog")
2177
+ begin
2178
+ path = f.path
2179
+ array.each do |line|
2180
+ f.puts line
2181
+ end
2182
+ f.close
2183
+ system("less #{path}")
2184
+ ensure
2185
+ f.close
2186
+ f.unlink
2187
+ end
2188
+ end
2189
+ # allows user to select from list, returning string if user pressed ENTER
2190
+ # Aborts if user presses Q or C-c or ESCAPE
2191
+ def ctrlp arr
2192
+ patt = nil
2193
+ curr = 0
2194
+ while true
2195
+ system("clear")
2196
+ if patt and patt != ""
2197
+ # need fuzzy match here
2198
+ view = arr.grep(/^#{patt}/)
2199
+ view = view | arr.grep(/#{patt}/)
2200
+ fuzzypatt = patt.split("").join(".*")
2201
+ view = view | arr.grep(/#{fuzzypatt}/)
2202
+ else
2203
+ view = arr
2204
+ end
2205
+ curr = [view.size-1, curr].min
2206
+ # if empty then curr becomes -1
2207
+ curr = 0 if curr < 0
2208
+ view.each_with_index do |a, i|
2209
+ mark = " "
2210
+ mark = ">" if curr == i
2211
+ print "#{mark} #{a} \n"
2212
+ end
2213
+ #puts " "
2214
+ print "\r#{patt} >"
2215
+ ch = get_char
2216
+ if ch =~ /^[a-z]$/
2217
+ patt ||= ""
2218
+ patt << ch
2219
+ elsif ch == "BACKSPACE"
2220
+ if patt && patt.size > 0
2221
+ patt = patt[0..-2]
2222
+ end
2223
+ elsif ch == "Q" or ch == "C-c" or ch == "ESCAPE"
2224
+ break
2225
+ elsif ch == "UP"
2226
+ curr -= 1
2227
+ curr = 0 if curr < 0
2228
+ elsif ch == "DOWN"
2229
+ curr += 1
2230
+ curr = [view.size-1, curr].min
2231
+ # if empty then curr becomes -1
2232
+ curr = 0 if curr < 0
2233
+ elsif ch == "ENTER"
2234
+ return view[curr]
2235
+ else
2236
+ # do right and left arrow
2237
+
2238
+ # get arrow keys here
2239
+ end
2240
+
2241
+ end
2242
+ end
2243
+ def get_hint
2244
+ if $gviscols == 2 and $view.size > 25
2245
+ return "(Toggle) Press =1 to see single column, =l for long listing"
2246
+ end
2247
+ if $long_listing
2248
+ return "(Command) To sort, press :sort_on or `s (backtick s)"
2249
+ end
2250
+ hints = [
2251
+ "(Backtick) Press `1 for article, `2 for comments for current line",
2252
+ "(Colon) Press :n<ENTER> for next forum, :p<ENTER> for previous forum",
2253
+ "Reload current forum with `r or :r<ENTER>",
2254
+ "Get more articles with `m (backtick m)",
2255
+ "(Command) Press : and then up arrow to see available commands",
2256
+ "Press > and < to go to next and prev forums.",
2257
+ "Press TAB to cycle through views"
2258
+ ]
2259
+ return hints[rand(hints.size)] || hints.last
2260
+ end
2261
+ def get_action_desc action
2262
+ h = { :reload => "Reload latest articles for this forum",
2263
+ :new_forum => "Get articles for a new forum",
2264
+ :display_forum => "List forums and select one",
2265
+ :change_subforum => "List forums and select one",
2266
+ :new_articles => "Get latest articles from reddit /new option",
2267
+ :view_menu => "View options: articles, comments, log",
2268
+ :reddit_options => "New, rising, controversial, top for this forum",
2269
+ :hacker_options => "Newest, show, ask, jobs",
2270
+ :fetch_more => "Fetch another page for this forum",
2271
+ :delete_current_forum => "Remove forum from your list",
2272
+ :next_forum => "Cycle through selected forums",
2273
+ :previous_forum => "Cycle through selected forums"
2274
+ }
2275
+ return h[action]
2276
+ end
2277
+
2278
+ # http://www.ruby-doc.org/stdlib/libdoc/optparse/rdoc/classes/OptionParser.html
2279
+ require 'optparse'
2280
+ options = {}
2281
+ app = File.basename $0
2282
+ OptionParser.new do |opts|
2283
+ opts.banner = %Q{
2284
+ #{app} version #{VERSION} (YML version)
2285
+ Usage: #{app} [options]
2286
+ }
2287
+
2288
+ #opts.on("-v", "--[no-]verbose", "Print description also") do |v|
2289
+ #options[:verbose] = v
2290
+ #end
2291
+ #opts.on("-n N", "--limit", Integer, "limit to N stories") do |v|
2292
+ #options[:number] = v
2293
+ #end
2294
+ #opts.on("-u URL", String,"--url", "Get articles from URL/file") do |v|
2295
+ #options[:url] = v
2296
+ #end
2297
+ #opts.on("-H (reddit|hn)", String,"--hostname", "Get articles from HOST") do |v|
2298
+ #host = v
2299
+ #end
2300
+ #opts.on("--save-html", "Save html to file?") do |v|
2301
+ #options[:save_html] = true
2302
+ #end
2303
+ #opts.on("-w PATH", String,"--save-html-path", "Save html to file PATH") do |v|
2304
+ #options[:htmloutfile] = v
2305
+ #options[:save_html] = true
2306
+ #end
2307
+ opts.on("-m MODE", String,"--mode", "Use 'text' or 'gui' browser") do |v|
2308
+ options[:browser_mode] = v
2309
+ end
2310
+ opts.on("-t browser", String,"--text", "browser for text mode") do |v|
2311
+ options[:browser_text] = v
2312
+ end
2313
+ opts.on("-g browser", String,"--gui", "browser for gui mode") do |v|
2314
+ options[:browser_gui] = v
2315
+ end
2316
+ opts.on("-c cache dir", String,"--cache-dir", "location to store yml files") do |v|
2317
+ options[:cache_path] = v
2318
+ end
2319
+ end.parse!
2320
+ run options