hacker-curse 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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