hacker-curse 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/bin/hacker.sh ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: hacker.sh
4
+ # Description: download hacker news entries or reddit entries for a subreddit
5
+ # Author: j kepler http://github.com/mare-imbrium/canis/
6
+ # Date: 2014-07-28 - 11:29
7
+ # License: MIT
8
+ # Last update: 2014-07-30 11:46
9
+ # ----------------------------------------------------------------------------- #
10
+ # hacker.sh Copyright (C) 2012-2014 j kepler
11
+ # Last update: 2014-07-30 11:46
12
+
13
+
14
+ set -euo pipefail
15
+
16
+ pages=1
17
+ today=$(date +"%Y-%m-%d-%H%M")
18
+ echo $today
19
+ curdir=$( basename $(pwd))
20
+
21
+ while [[ "$1" = -* ]]; do
22
+ case "$1" in
23
+ -H|--hostname) shift
24
+ hostname=$1
25
+ shift
26
+ ;;
27
+ -p|--pages) shift
28
+ pages=$1
29
+ shift
30
+ ;;
31
+ -o|--outputfile) shift
32
+ outputfile=$1
33
+ shift
34
+ ;;
35
+ -h|--help)
36
+ cat <<!
37
+ $0 Version: 0.0.1 Copyright (C) 2014 jkepler
38
+ This program downloads the latest page from Hacker News or reddit news
39
+ and parses it into a TSV file.
40
+ !
41
+ # no shifting needed here, we'll quit!
42
+ exit
43
+ ;;
44
+ --source)
45
+ echo "this is to edit the source "
46
+ vim $0
47
+ exit
48
+ ;;
49
+ *)
50
+ echo "Error: Unknown option: $1" >&2 # rem _
51
+ echo "Use -h or --help for usage"
52
+ exit 1
53
+ ;;
54
+ esac
55
+ done
56
+
57
+ if [ $# -eq 0 ]
58
+ then
59
+ echo "I got no filename"
60
+ exit 1
61
+ else
62
+ echo "Got $1"
63
+ fi
64
+ subr=${1:-"news"}
65
+ outputfile=${outputfile:-"$subr.tsv"}
66
+ outputhtml=${html:-"$subr.html"}
67
+ outputhtml=$( echo $outputhtml | sed "s/\//__/g" )
68
+ outputfile=$( echo $outputfile | sed "s/\//__/g" )
69
+
70
+ echo "subreddit is: $subr "
71
+
72
+ case "$subr" in
73
+ "news")
74
+ hacker-tsv.rb -H hn -p $pages -s news -w news.html > $outputfile
75
+ ;;
76
+ "newest")
77
+ hacker-tsv.rb -H hn -p $pages -s newest -w newest.html > $outputfile
78
+ ;;
79
+ "ruby")
80
+ hacker-tsv.rb -H rn -p $pages -s ruby -w ruby > $outputfile
81
+ ;;
82
+ "programming")
83
+ hacker-tsv.rb -H rn -p $pages -s programming -w $outputhtml > $outputfile
84
+ ;;
85
+ *)
86
+ hostname=${hostname:-"rn"}
87
+ hacker-tsv.rb -H "$hostname" -p $pages -s "$subr" -w $outputhtml > $outputfile
88
+ ;;
89
+ esac
90
+ ls -ltrh $outputfile
data/bin/redford ADDED
@@ -0,0 +1,946 @@
1
+ #!/usr/bin/env ruby
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: redford.rb
4
+ # Description: curses frontend to hacker-curse which scrapes hnews and reddit mobile
5
+ # Author: j kepler http://github.com/mare-imbrium/canis/
6
+ # Date: 2014-09-09 - 12:35
7
+ # License: MIT
8
+ # Last update: 2014-09-12 20:12
9
+ # ----------------------------------------------------------------------------- #
10
+ # redford.rb Copyright (C) 2014 j kepler
11
+ # encoding: utf-8
12
+ require 'canis/core/util/app'
13
+ require 'canis/core/util/rcommandwindow'
14
+ require 'fileutils'
15
+ require 'pathname'
16
+ require 'open3'
17
+ require 'canis/core/include/defaultfilerenderer'
18
+ require 'canis/core/include/appmethods'
19
+
20
+ # TODO :
21
+ # Using curses part from hackman, but we need to take hacker options and reddit stuff from corvus.
22
+ # including pages etc;
23
+ #
24
+ module HackerCurse
25
+ VERSION="0.0.1"
26
+ CONFIG_FILE="~/.redford.yml"
27
+ # in grey version, cannot see the other links.
28
+ OLDCOLOR_SCHEMES=[
29
+ [20,19,17, 18, :white, :green], # 0 band in header, 1 - menu bgcolor. 2 - bgcolor of main screen, 3 - status, 4 fg color body, detail color (url and comment count)
30
+ [17,19,18, 20, :white, :green], # 0 band in header, 1 - menu bgcolor. 2 - bgcolor of main screen, 3 - status
31
+ [236,236,0, 232,:white, :green], # 0 band in header, 1 - menu bgcolor. 2 - bgcolor of main screen, 3 - status
32
+ [236,236,244, 250, :black, :green] # 0 band in header, 1 - menu bgcolor. 2 - bgcolor of main screen, 3 - status
33
+ ]
34
+ # put all methods and data into this class, so we don't pollute global space. Or get mixed into App's space.
35
+ #
36
+ class Redford
37
+ def initialize app, options
38
+ @app = app
39
+ @options = options
40
+ @form = app.form
41
+ @hash = nil
42
+ @cache_path = "."
43
+ @toggle_titles_only = true
44
+ @toggle_offline = false
45
+ @logger = @app.logger
46
+ @hacker_forums = %w{news newest show jobs ask}
47
+ @long_listing = true
48
+
49
+ @fg = :white
50
+ @_forumlist = %w{ news newest ruby programming scifi science haskell java scala cpp c_programming d_language golang vim emacs unix linux bash zsh commandline vimplugins python }
51
+ @browser_mode = options[:browser_mode] || 'text'
52
+ @browser_text = options[:browser_text] || 'elinks'
53
+ @browser_gui = options[:browser_gui] || 'open'
54
+ @cache_path = options[:cache_path] || "."
55
+ config_file = options[:config_file]
56
+ config_read config_file
57
+ @binding ||= default_bindings
58
+ @color_schemes ||= default_color_schemes
59
+ # we should actually pick the fist, since the name could have changed
60
+ @color_scheme = @color_schemes.values.first
61
+ @forumlist ||= (options[:list] || @_forumlist)
62
+ handle_keys @binding
63
+ @cache_path = File.expand_path(@cache_path)
64
+ end
65
+ def config_read config_file=nil
66
+ config_file ||= CONFIG_FILE
67
+ config_file = File.expand_path(config_file)
68
+ if config_file
69
+ if File.exists? config_file
70
+ #eval(File.open(File.expand_path(config_file)).read)
71
+ obj = YAML::load( File.open( config_file ) )
72
+ #%w{ :binding :forumlist :cache_path}.each do |e|
73
+ #if obj[e]
74
+ obj.keys.each do |e|
75
+ instance_variable_set("@#{e}", obj[e])
76
+ end
77
+ else
78
+ #alert "NOT EXISTS config file #{config_file} "
79
+ end
80
+ end
81
+ end
82
+ # save current config to a yml file, so user can modify it
83
+ # This is included since its a bit difficult to create this file if you don't remember YML format.
84
+ def save_config filename=nil
85
+ unless filename
86
+ filename = get_string "Enter filename to save configuration to:"
87
+ return unless filename
88
+ end
89
+ xx = {}
90
+ [:binding, :forumlist, :browser_gui, :browser_text, :cache_path, :color_schemes, :color_scheme].each do |e|
91
+ xx[e] = instance_variable_get "@#{e}"
92
+ end
93
+ File.open(filename, 'w' ) do |f|
94
+ f << YAML::dump(xx)
95
+ end
96
+ @app.message "Config saved to #{filename} in YML format"
97
+ end
98
+ def default_color_schemes
99
+ @color_schemes={}
100
+ @color_schemes['deep blue'] = { :header_bg => 20, :menu_bg => 19, :body_bg => 17, :status_bg => 18, :body_fg => :white,
101
+ :body_detail => :green }
102
+ @color_schemes['medium blue'] = { :header_bg => 17, :menu_bg => 19, :body_bg => 18, :status_bg => 20, :body_fg => :white,
103
+ :body_detail => :green }
104
+ @color_schemes['black body'] = { :header_bg => 236, :menu_bg => 236, :body_bg => 0, :status_bg => 232, :body_fg => :white,
105
+ :body_detail => :green }
106
+ @color_schemes['grey body'] = { :header_bg => 236, :menu_bg => 236, :body_bg => 244, :status_bg => 250, :body_fg => :black,
107
+ :body_detail => :green }
108
+ return @color_schemes
109
+ end
110
+ def articles
111
+ @hash[:articles]
112
+ end
113
+ # return current color scheme
114
+ def color_scheme
115
+ @color_scheme
116
+ end
117
+ def forumlist
118
+ @forumlist
119
+ end
120
+ def default_bindings
121
+ @binding = {
122
+ "`" => "main_menu",
123
+ "=" => "toggle_menu",
124
+ ">" => "next_forum",
125
+ "<" => "prev_forum",
126
+ "z" => "goto_article",
127
+ "o" => "display_links",
128
+ "<CR>" => "display_links",
129
+ "<C-f>" => "display_links",
130
+ "<F2>" => "choose_forum",
131
+ "<F3>" => "view_properties_as_tree"
132
+ }
133
+ end
134
+
135
+
136
+ # prompt user to select a forum, and fetch data for it.
137
+ def choose_forum
138
+ # scrollable filterable list
139
+ str = display_list @forumlist, :title => "Select a forum"
140
+ return unless str
141
+ return if str == ""
142
+ @current_forum = str
143
+ forum = str
144
+ get_data forum if forum
145
+ end
146
+ # add a forum at runtime, by default this will be a reddit subforum
147
+ def add_forum forum=nil
148
+ unless forum
149
+ forum = get_string "Add a reddit subforum: "
150
+ return if forum.nil? or forum == ""
151
+ end
152
+ @forumlist << forum
153
+ get_data forum
154
+ end
155
+ def remove_forum forum=nil
156
+ unless forum
157
+ forum = display_list @forumlist, :title => "Select a forum"
158
+ return if forum.nil? or forum == ""
159
+ end
160
+ @forumlist.delete forum
161
+ end
162
+ def next_forum
163
+ index = @forumlist.index(@current_forum)
164
+ index = index >= @forumlist.count - 1 ? 0 : index + 1
165
+ get_data @forumlist[index]
166
+ end
167
+ def prev_forum
168
+ index = @forumlist.index(@current_forum)
169
+ index = index == 0? @forumlist.count - 1 : index - 1
170
+ get_data @forumlist[index]
171
+ end
172
+ # if components have some commands, can we find a way of passing the command to them
173
+ # method_missing gave a stack overflow.
174
+ def execute_this(meth, *args)
175
+ alert " #{meth} not found ! "
176
+ $log.debug "app email got #{meth} " if $log.debug?
177
+ cc = @form.get_current_field
178
+ [cc].each do |c|
179
+ if c.respond_to?(meth, true)
180
+ c.send(meth, *args)
181
+ return true
182
+ end
183
+ end
184
+ false
185
+ end
186
+ def open_url url, app
187
+ #shell_out "elinks #{url}"
188
+ shell_out "#{app} #{url}"
189
+ #Window.refresh_all
190
+ end
191
+
192
+ ##
193
+ # Menu creator which displays a menu and executes methods based on keys.
194
+ # In some cases, we call this and then do a case statement on either key or binding.
195
+ # @param String title
196
+ # @param hash of keys and methods to call
197
+ # @return key pressed, and binding (if found, and responded). Can return NIL nil if esc pressed
198
+ #
199
+ def menu title, hash, config={}, &block
200
+ raise ArgumentError, "Nil hash received by menu" unless hash
201
+ list = []
202
+ list << config[:subtitle] if config[:subtitle]
203
+ config.delete(:subtitle)
204
+ hash.each_pair { |k, v| list << " #[fg=yellow, bold] #{k} #[/end] #[fg=green] #{v} #[/end]" }
205
+ # s="#[fg=green]hello there#[fg=yellow, bg=black, dim]"
206
+ config[:title] = title
207
+ config[:width] = hash.values.max_by(&:length).length + 13
208
+ # need to have a proper check, which takes +left+ / column into account
209
+ config[:width] = FFI::NCurses.COLS - 10 if config[:width] > FFI::NCurses.COLS
210
+ ch = padpopup list, config, &block
211
+ return unless ch
212
+ if ch.size > 1
213
+ # could be a string due to pressing enter
214
+ # but what if we format into multiple columns
215
+ ch = ch.strip[0]
216
+ end
217
+
218
+ # if the selection corresponds to a method then execute it.
219
+ # The problem with this is, if you were just giving options and there was a method by that name
220
+ # as in 'show'
221
+ binding = hash[ch]
222
+ binding = hash[ch.to_sym] unless binding
223
+ if binding
224
+ if respond_to?(binding, true)
225
+ send(binding)
226
+ end
227
+ end
228
+ return ch, binding
229
+ end
230
+ # pops up a list, taking a single key and returning if it is in range of 33 and 126
231
+ # Called by menu, print_help, show_marks etc
232
+ # You may pass valid chars or ints so it only returns on pressing those.
233
+ #
234
+ # @param Array of lines to print which may be formatted using :tmux format
235
+ # @return character pressed (ch.chr)
236
+ # @return nil if escape or C-q pressed
237
+ #
238
+ def padpopup list, config={}, &block
239
+ max_visible_items = config[:max_visible_items]
240
+ row = config[:row] || 1
241
+ col = config[:col] || 1
242
+ # format options are :ansi :tmux :none
243
+ fmt = config[:format] || :tmux
244
+ config.delete :format
245
+ relative_to = config[:relative_to]
246
+ if relative_to
247
+ layout = relative_to.form.window.layout
248
+ row += layout[:top]
249
+ col += layout[:left]
250
+ end
251
+ config.delete :relative_to
252
+ # still has the formatting in the string so length is wrong.
253
+ #longest = list.max_by(&:length)
254
+ width = config[:width] || 60
255
+ if config[:title]
256
+ width = config[:title].size + 2 if width < config[:title].size
257
+ end
258
+ height = config[:height]
259
+ height ||= [max_visible_items || 25, list.length+2].min
260
+ #layout(1+height, width+4, row, col)
261
+ layout = { :height => 0+height, :width => 0+width, :top => row, :left => col }
262
+ window = Canis::Window.new(layout)
263
+ form = Canis::Form.new window
264
+
265
+ ## added 2013-03-13 - 18:07 so caller can be more specific on what is to be returned
266
+ valid_keys_int = config.delete :valid_keys_int
267
+ valid_keys_char = config.delete :valid_keys_char
268
+
269
+ listconfig = config[:listconfig] || {}
270
+ #listconfig[:list] = list
271
+ listconfig[:width] = width
272
+ listconfig[:height] = height
273
+ # pass this in config so less dependences
274
+ listconfig[:bgcolor] = @color_scheme[:menu_bg]
275
+ #listconfig[:selection_mode] ||= :single
276
+ listconfig.merge!(config)
277
+ listconfig.delete(:row);
278
+ listconfig.delete(:col);
279
+ #listconfig[:row] = 1
280
+ #listconfig[:col] = 1
281
+ # trying to pass populists block to listbox
282
+ lb = Canis::TextPad.new form, listconfig, &block
283
+ if fmt == :none
284
+ lb.text(list)
285
+ else
286
+ lb.text(list, fmt)
287
+ end
288
+ #
289
+ #window.bkgd(Ncurses.COLOR_PAIR($reversecolor));
290
+ form.repaint
291
+ Ncurses::Panel.update_panels
292
+ if valid_keys_int.nil? && valid_keys_char.nil?
293
+ # changed 32 to 33 so space can scroll list
294
+ valid_keys_int = (33..126)
295
+ end
296
+
297
+ begin
298
+ while((ch = window.getchar()) != 999 )
299
+
300
+ # if a char range or array has been sent, check if the key is in it and send back
301
+ # else just stay here
302
+ if valid_keys_char
303
+ if ch > 32 && ch < 127
304
+ chr = ch.chr
305
+ return chr if valid_keys_char.include? chr
306
+ end
307
+ end
308
+
309
+ # if the user specified an array or range of ints check against that
310
+ # therwise use the range of 33 .. 126
311
+ return ch.chr if valid_keys_int.include? ch
312
+
313
+ case ch
314
+ when ?\C-q.getbyte(0)
315
+ break
316
+ else
317
+ if ch == 13 || ch == 10
318
+ s = lb.current_value.to_s # .strip #if lb.selection_mode != :multiple
319
+ return s
320
+ end
321
+ # close if escape or double escape
322
+ if ch == 27 || ch == 2727
323
+ return nil
324
+ end
325
+ lb.handle_key ch
326
+ form.repaint
327
+ end
328
+ end
329
+ ensure
330
+ window.destroy
331
+ end
332
+ return nil
333
+ end
334
+ # main options, invokable on backtick.
335
+ # TODO add selection of browser
336
+ # r for reload
337
+ # 1,2 a c view article, comments
338
+ def main_menu
339
+ h = {
340
+ :f => :choose_forum,
341
+ :m => :fetch_more,
342
+ :c => :color_scheme_select,
343
+ #:s => :sort_menu,
344
+ #:F => :filter_menu,
345
+ :a => :add_forum,
346
+ :d => :remove_forum,
347
+ :R => :reddit_options,
348
+ :H => :hacker_options,
349
+ :x => :extras
350
+ }
351
+ ch, binding = menu "Main Menu", h
352
+ #alert "Menu got #{ch}, #{binding}" if ch
353
+ end
354
+ # TODO uses text browser t, use gui browser g
355
+ # l - long list (what is currently t)
356
+ def toggle_menu
357
+ h = {
358
+ "t" => :toggle_titles_only,
359
+ "l" => :toggle_long_list,
360
+ "O" => :toggle_offline
361
+ #:x => :extras
362
+ }
363
+ ch, binding = menu "Main Menu", h
364
+ #alert "Menu got #{ch}, #{binding}" if ch
365
+ end
366
+ # fetch next page using the next url.
367
+ # FIXME : since this updates the same cache file, i cannot go back to first page. There is no
368
+ # previous page. or reset.
369
+ def fetch_more
370
+ more_url = @yaml_obj[:next_url]
371
+ #perror "more url is #{more_url} "
372
+ #fetch_data_from_net $subforum, more_url
373
+ file = fetch_data_from_net @current_forum, more_url
374
+ display_yml file if file
375
+ end
376
+ def color_scheme_select ch=nil
377
+ unless ch
378
+ h = {}
379
+ ctr = 0
380
+ @color_schemes.each_pair do |k,v|
381
+ ctr += 1
382
+ h[ctr.to_s] = k
383
+ end
384
+
385
+ h = h.merge({
386
+ #"0" => 'dark blue body',
387
+ #"1" => 'medium blue body',
388
+ #"2" => 'black body',
389
+ #"3" => 'grey body',
390
+ "b" => 'change body color',
391
+ "f" => 'change body fg color',
392
+ "d" => 'change body detail color',
393
+ "c" => 'cycle body color'
394
+ })
395
+ ch, binding = menu "Color Menu", h
396
+ end
397
+ case ch
398
+ when "1", "2", "0", "3","4","5","6"
399
+ @color_scheme = @color_schemes[binding]
400
+ @fg = @color_scheme[:body_fg]
401
+ when "b"
402
+ n = get_string "Enter a number for background color (0..255): "
403
+ unless n =~ /^\d+$/
404
+ n = Canis::ColorMap.colors.index(n.to_sym)
405
+ return unless n
406
+ end
407
+ n = n.to_i
408
+ @color_scheme[:body_bg] = n
409
+ when "f"
410
+ n = get_string "Enter a number for fg color (0..255) : "
411
+ unless n =~ /^\d+$/
412
+ n = Canis::ColorMap.colors.index(n.to_sym)
413
+ return unless n
414
+ end
415
+ @fg = n.to_i
416
+ @color_scheme[:body_fg] = n.to_i
417
+ when "d"
418
+ n = get_string "Enter a number for detail line color (0..255): "
419
+ unless n =~ /^\d+$/
420
+ n = Canis::ColorMap.colors.index(n.to_sym)
421
+ return unless n
422
+ end
423
+ n = n.to_i
424
+ @color_scheme[:body_detail] = n
425
+ when "c"
426
+ # increment bg color
427
+ n = @color_scheme[:body_bg]
428
+ n += 1
429
+ n = 0 if n > 255
430
+ @color_scheme[:body_bg] = n
431
+ when "C"
432
+ # decrement bg color
433
+ n = @color_scheme[:body_bg]
434
+ n -= 1
435
+ n = 255 if n < 0
436
+ @color_scheme[:body_bg] = n
437
+ end
438
+
439
+ h = @form.by_name["header"]
440
+ tv = @form.by_name["tv"]
441
+ sl = @form.by_name["sl"]
442
+ tv.bgcolor = @color_scheme[:body_bg]
443
+ #tv.color = 255
444
+ tv.color = @fg
445
+ sl.color = @color_scheme[:status_bg]
446
+ h.bgcolor = @color_scheme[:header_bg]
447
+ #@app.message "bgcolor is #{@color_scheme[:body_bg]}. :: #{@color_scheme.join(",")}, CP:#{tv.color_pair}=#{tv.color} / #{tv.bgcolor} "
448
+ refresh
449
+ end
450
+ def extras
451
+ h = {
452
+ "s" => :save_config
453
+ }
454
+ ch, binding = menu "Extras ", h
455
+ end
456
+ def refresh
457
+ display_yml @current_file
458
+ end
459
+
460
+ def toggle_titles_only
461
+ @toggle_titles_only = !@toggle_titles_only
462
+ show @current_file
463
+ end
464
+ def toggle_long_list
465
+ @long_listing = !@long_listing
466
+ show @current_file
467
+ end
468
+ def toggle_offline
469
+ @toggle_offline = !@toggle_offline
470
+ end
471
+ # moved from inside App
472
+ #
473
+ def get_item_for_line line
474
+ index = (line - @hash[:first]) / @hash[:diff]
475
+ @hash[:articles][index]
476
+ end
477
+ def title_right text
478
+ w = @form.by_name["header"]
479
+ w.text_right text
480
+ end
481
+ def title text
482
+ w = @form.by_name["header"]
483
+ w.text_center text
484
+ end
485
+ def color_line(fg,bg,attr,text)
486
+ a = "#["
487
+ a = []
488
+ a << "fg=#{fg}" if fg
489
+ a << "bg=#{bg}" if bg
490
+ a << "#{attr}" if attr
491
+ str = "#[" + a.join(",") + "]#{text}#[end]"
492
+ end
493
+ def goto_article n=$multiplier
494
+ i = ((n-1) * @hash[:diff]) + @hash[:first]
495
+ w = @form.by_name["tv"]
496
+ w.goto_line i
497
+ end
498
+ def display_links
499
+ # if multiplier is 0, use current line
500
+ art = self.articles[$multiplier - 1]
501
+ if $multiplier == 0
502
+ tv = @form.by_name["tv"]
503
+ index = tv.current_index
504
+ art = get_item_for_line index
505
+ end
506
+ show_links art
507
+ end
508
+
509
+ # display the given yml file.
510
+ # Converts the yml object to an array for textpad
511
+ def display_yml file
512
+ w = @form.by_name["tv"]
513
+
514
+ obj = YAML::load( File.open( file ) )
515
+ @yaml_obj = obj # needed to get next_url, or should be just store as instance or in @hash
516
+ lines = Array.new
517
+ articles = obj[:articles]
518
+ count = articles.count
519
+ #lines << color_line(:red,COLOR_SCHEME[1],nil,"#{file} #{obj[:page_url]} | #{count} articles | fetched #{obj[:create_time]}")
520
+ #lines << ("-" * lines.last.size )
521
+ @hash = Hash.new
522
+ @hash[:first] = lines.size
523
+ @hash[:articles] = articles
524
+ dc = @color_scheme[:body_detail]
525
+
526
+ articles.each_with_index do |a, i|
527
+ bg = i
528
+ bg = 0 if i > 255
529
+ if @long_listing
530
+ line = "%3s %s %s %s %s " % [i+1 ,a[:age_text], a[:comment_count], a[:points], a[:title] ]
531
+ else
532
+ line = "%3s %s " % [i+1 , a[:title] ]
533
+ end
534
+ #lines << color_line(@fg, bg, nil, line)
535
+ lines << line
536
+ if !@toggle_titles_only
537
+ line1 = []
538
+ line2 = []
539
+ url = a[:article_url] || a[:url]
540
+ line1 << url
541
+ line2 << a[:comments_url] if a[:comments_url]
542
+ if a.key? :comment_count
543
+ line1 << a[:comment_count]
544
+ end
545
+ if a.key? :age
546
+ line2 << Time.at(a[:age]).to_s
547
+ end
548
+ if a.key? :comment_count
549
+ line2 << " #{a[:comment_count]} comments"
550
+ end
551
+ if a.key? :points
552
+ line2 << "#{a[:points]} points"
553
+ end
554
+ #unless detail.empty?
555
+ l = "#[fg=#{dc}]" + " " + line1.join(" | ") + "#[end]"
556
+ lines << l
557
+ l = "#[fg=#{dc}]" + " " + line2.join(" | ") + "#[end]"
558
+ lines << l
559
+ #end
560
+ end
561
+ @hash[:diff] ||= lines.size - @hash[:first]
562
+ end
563
+ w.text(lines, :content_type => :tmux)
564
+ w.title "[ #{file} ]"
565
+
566
+ i = @hash[:first] || 1
567
+ w.goto_line i
568
+ @current_file = file
569
+ #@current_forum = file_to_forum file
570
+ title "#{@current_forum} (#{count} articles) "
571
+ title_right obj[:create_date].to_s
572
+ end
573
+ def file_to_forum filename
574
+ forum = File.basename(filename).sub(File.extname(filename),"").sub("__","/")
575
+ end
576
+ def forum_to_file forum
577
+ file = "#{forum}.yml".sub("/","__")
578
+ file = "#{@cache_path}/#{file}"
579
+ end
580
+ def forum_to_host fo
581
+ if @hacker_forums.include? fo
582
+ return :hn
583
+ end
584
+ return :rn
585
+ end
586
+ alias :show :display_yml
587
+ def get_data forum
588
+ file = forum_to_file forum
589
+ if File.exists? file and fresh?(file)
590
+ else
591
+ ret = fetch_data_from_net forum
592
+ return unless ret
593
+ end
594
+ if File.exists? file
595
+ @current_forum = forum
596
+ display_yml file
597
+ else
598
+ alert "#{file} not created. Check externally. run hacker-yml.rb -y #{file} -h HOST-s #{forum} externally"
599
+ end
600
+ end
601
+
602
+ # get data from net, do not check for file.
603
+ # @param forum String forum name, e.g. ruby, programming
604
+ # @param more_url is the url of the next page
605
+ def fetch_data_from_net forum, more_url=nil
606
+ @num_pages = 1
607
+ host = forum_to_host forum
608
+ file = forum_to_file forum
609
+ m = nil
610
+ if more_url
611
+ m = "-u #{more_url} "
612
+ m = "-u '" + more_url + "'"
613
+ end
614
+ progress_dialog :color_pair => $reversecolor do |sw|
615
+ command = "hacker-yml.rb --pages #{@num_pages} -H #{host} -s #{forum} -y #{file} #{m}"
616
+ sw.print "Fetching #{forum} ..."
617
+ #system("hackercli.rb -y #{file} #{forum}")
618
+ #retval = system("hacker-yml.rb --pages #{$num_pages} -H #{$host} -s #{subforum} -y #{filename} #{m}")
619
+ #o,e,s = Open3.capture3("hackercli.rb -y #{file} #{forum}")
620
+ o,e,s = Open3.capture3(command)
621
+ unless s.success?
622
+ $log.debug " error from capture3 #{e}"
623
+ alert e
624
+ return nil
625
+ end
626
+ end
627
+ return file
628
+ end
629
+ # return true if younger than one hour
630
+ def fresh? file
631
+ return true if @toggle_offline
632
+
633
+ f = File.stat(file)
634
+ now = Time.now
635
+ return (( now - f.mtime) < 3600)
636
+ end
637
+ def show_links art
638
+ return unless art
639
+ links = {}
640
+ keys = %w{a b c d e f}
641
+ i = 0
642
+ art.each_pair do |k, p|
643
+ if p.to_s.index("http") == 0
644
+ links[keys[i]] = p
645
+ i += 1
646
+ end
647
+ end
648
+ ch, binding = menu "Select a link", links, :subtitle => " Enter Upper case letter to open in gui"
649
+ #alert "is #{index}: #{art[:title]} #{ch}:#{binding} "
650
+ app = @browser_text || "elinks"
651
+ unless binding
652
+ return unless ch
653
+ # it must be an upper case for GUI
654
+ return unless ch == ch.upcase
655
+ ch = ch.downcase
656
+ return unless keys.include? ch
657
+ binding = links[ch]
658
+ app = @browser_gui || "open"
659
+ end
660
+ if binding
661
+ open_url binding, app
662
+ end
663
+ end
664
+ # since this does not happen inside form's loop, therefore form is unable to repaint, repaint
665
+ # happens only after a keystroke
666
+ # This allows us to pass in a hash with string names for methods. This hash can be easily updated,
667
+ # or even read in from a config file/yml file. It is assumed here that all the string names
668
+ # correspond to names of methods withing this class, so no class references are required.
669
+ # TODO split the command if there are spaces.
670
+ def handle_keys hash
671
+ @app.keypress do |str|
672
+ binding = hash[str]
673
+ if binding
674
+ binding = binding.to_sym
675
+ if respond_to?(binding, true)
676
+ send(binding)
677
+ else
678
+ #alert "unresponded to #{str}"
679
+ end
680
+ end
681
+ end
682
+ end
683
+
684
+ # Should work on this as a means of binding each element of a hash into forms keymap.
685
+ # FIXME works except that multiplier not working ??
686
+ def form_bind hash
687
+ hash.each_pair do |k, v|
688
+ nk = key_to_i(k)
689
+ desc = "??"
690
+ desc = v if v.is_a? String or v.is_a? Symbol
691
+ @form.bind_key(nk, desc) { self.send(v) }
692
+ end
693
+ end
694
+ # convert a key in the format to an int so it can be mapped using bind_key
695
+ # "[a-zA-Z"] etc a single cahr
696
+ # C-a to C-z
697
+ # M-a to M-z
698
+ # F1 .. F10
699
+ # This does not take complex cases yet. It is a simplistic conversion.
700
+ def key_to_i k
701
+ if k.size == 1
702
+ return k.getbyte(0)
703
+ end
704
+ if k =~ /^<M-/
705
+ ch = k[3]
706
+ return 128 + ch.ord
707
+ elsif k == "<CR>"
708
+ return 13
709
+ elsif k =~ /^<[Cc]/
710
+ ch = k[3]
711
+ x = ch.ord - "a".ord + 1
712
+ elsif k[0,2] == "<F"
713
+ ch = k[2..-2]
714
+ return 264 + ch.to_i
715
+ else
716
+ alert "not able to bind #{k}"
717
+ end
718
+
719
+ end
720
+ # place instance_vars of current or given object into a hash
721
+ # and view in a treedialog.
722
+ def view_properties_as_tree field=self
723
+ alert "Nil field" unless field
724
+ return unless field
725
+ text = []
726
+ tree = {}
727
+ #iv = field.instance_variables.map do |v| v.to_s; end
728
+ field.instance_variables.each do |v|
729
+ val = field.instance_variable_get(v)
730
+ klass = val.class
731
+ if val.is_a? Array
732
+ #tree[v.to_s] = val
733
+ text << { v.to_s => val }
734
+ val = val.size
735
+ elsif val.is_a? Hash
736
+ #tree[v.to_s] = val
737
+ text << { v.to_s => val }
738
+ if val.size <= 5
739
+ val = val.keys
740
+ else
741
+ val = val.keys.size.to_s + " [" + val.keys.first(5).join(", ") + " ...]"
742
+ end
743
+ end
744
+ case val
745
+ when String, Fixnum, Integer, TrueClass, FalseClass, NilClass, Array, Hash, Symbol
746
+ ;
747
+ else
748
+ val = "Not shown"
749
+ end
750
+ text << "%-20s %10s %s" % [v, klass, val]
751
+ end
752
+ tree["Instance Variables"] = text
753
+ pm = field.public_methods(false).map do |v| v.to_s; end
754
+ tree["Public Methods"] = pm
755
+ pm = field.public_methods(true) - field.public_methods(false)
756
+ pm = pm.map do |v| v.to_s; end
757
+ tree["Inherited Methods"] = pm
758
+
759
+ #$log.debug " view_properties #{s.size} , #{s} "
760
+ treedialog tree, :title => "Properties"
761
+ end
762
+ def reddit_options menu_text=nil
763
+ if @hacker_forums.include? @current_forum
764
+ alert "Reddit options invalid inside Hacker News subforum"
765
+ return
766
+ end
767
+ h = {
768
+ :n => :new,
769
+ :r => :rising,
770
+ :c => :controversial,
771
+ :t => :top,
772
+ :h => :hot
773
+ }
774
+ subforum = @current_forum
775
+ unless menu_text
776
+ ch, menu_text = menu "Reddit Options for #{subforum} ", h
777
+ end
778
+ if menu_text
779
+ if menu_text == :hot
780
+ file = fetch_data_from_net "#{subforum}"
781
+ display_yml file if file
782
+ else
783
+ m = menu_text.to_s
784
+ s = "#{subforum}".sub(/\/.*/, '')
785
+ file = fetch_data_from_net "#{s}/#{m}"
786
+ display_yml file if file
787
+ end
788
+ end
789
+ end
790
+ def hacker_options menu_text=nil
791
+
792
+ # there is a method called show already. this is an issue with menu, it executes the option if it finds it
793
+ h = {
794
+ :n => :news,
795
+ :w => :newest,
796
+ # added space before show so does not conflict with 'show' method
797
+ :s => " show",
798
+ :j => :jobs,
799
+ :a => :ask
800
+ }
801
+ # TODO ask article needs host name prepended
802
+ # TODO jobs has no comments, check if nil
803
+ unless menu_text
804
+ ch, menu_text = menu "Hacker Options", h
805
+ end
806
+ if menu_text
807
+ # added the strip due to space before show
808
+ m = menu_text.to_s.strip
809
+ file = fetch_data_from_net m
810
+ display_yml file if file
811
+ end
812
+ end
813
+ end # class
814
+ end # module HackerCurse
815
+ include HackerCurse
816
+
817
+ # http://www.ruby-doc.org/stdlib/libdoc/optparse/rdoc/classes/OptionParser.html
818
+ require 'optparse'
819
+ options = {}
820
+ app = File.basename $0
821
+ OptionParser.new do |opts|
822
+ opts.banner = %Q{
823
+ #{app} version #{VERSION} (YML version)
824
+ Usage: #{app} [options]
825
+ }
826
+
827
+ #opts.on("-m MODE", String,"--mode", "Use 'text' or 'gui' browser") do |v|
828
+ #options[:browser_mode] = v
829
+ #end
830
+ opts.on("-t browser", String,"--text", "browser for text mode, default elinks") do |v|
831
+ options[:browser_text] = v
832
+ end
833
+ opts.on("-g browser", String,"--gui", "browser for gui mode, default open") do |v|
834
+ options[:browser_gui] = v
835
+ end
836
+ opts.on("-c cache dir", String,"--cache-dir", "location to store yml files, default .") do |v|
837
+ options[:cache_path] = File.expand_path(v)
838
+ end
839
+ opts.on("-u config_file", String,"--config-file", "path to load config info from") do |v|
840
+ options[:config_file] = v
841
+ end
842
+ opts.on("--list x,y,z", Array, "Example 'list' of forums: hacker,ruby,programming...") do |list|
843
+ options[:list] = list
844
+ end
845
+ # file age in hours
846
+ # offline mode
847
+ # config file path
848
+ end.parse!
849
+ App.new do
850
+ def logger; return $log; end
851
+ $log = create_logger "hacker.log"
852
+ @h = Redford.new self, options
853
+ @color_scheme = @h.color_scheme
854
+ @header = app_header "redford #{VERSION}", :text_center => "Hacker and Reddit Reader", :name => "header",
855
+ :text_right =>"Menu `", :color => :white, :bgcolor => @color_scheme[:header_bg]
856
+ message "Press F10 (or qq) to exit, F1 Help, ` for Menu "
857
+
858
+
859
+
860
+
861
+ # commands that can be mapped to or executed using M-x
862
+ # however, commands of components aren't yet accessible.
863
+ def get_commands
864
+ %w{ choose_forum next_forum prev_forum }
865
+ end
866
+ # help text for F1, but this needs to be kept consistent with @bindings,
867
+ # if that is changed, then how does this show the change, considering that
868
+ # the config file will be read in Redford, not here.
869
+ def help_text
870
+ <<-eos
871
+ Redford Help
872
+
873
+ F2 - forum selection (interface like Ctrl-P, very minimal)
874
+ F1 - Help
875
+ F10 - Quit application
876
+ qq - Quit application
877
+
878
+ ` (backtick) - Main Menu (add, remove, change forum)
879
+ = (Equal) - Toggle Menu (titles only)
880
+
881
+ o - open url menu for current article (under cursor)
882
+ <n>o - open url menu for <n>th article
883
+ <n>z - goto <n>th article
884
+
885
+ "<" - previous forum in list
886
+ ">" - next forum in list
887
+
888
+ "/" - search within the page (case-sensitive). Append "/i" to ignore case.
889
+
890
+ -----------------------------------------------------------------------
891
+ :n or Alt-n for general help.
892
+ eos
893
+ end
894
+
895
+ #install_help_text help_text
896
+
897
+ def app_menu
898
+ # TODO update and fix this
899
+ require 'canis/core/util/promptmenu'
900
+ menu = PromptMenu.new self do
901
+ item :f, :choose_forum
902
+ item :n, :next_forum
903
+ item :p, :prev_forum
904
+ item :a, :add_forum
905
+ item :d, :remove_forum
906
+ end
907
+ menu.display_new :title => "Menu"
908
+ end
909
+ # BINDING SECTION
910
+ if false
911
+ #@form.bind_key(?:, "App Menu") { app_menu; }
912
+ @form.bind_key(?`, "Main Menu") { @h.main_menu; }
913
+ @form.bind_key(FFI::NCurses::KEY_F2, "Main Menu") { @h.choose_forum; }
914
+ @form.bind_key(FFI::NCurses::KEY_F3, "Cycle bgcolor") { @h.color_scheme_select "c"; }
915
+ @form.bind_key(FFI::NCurses::KEY_F4, "Cycle bgcolor") { @h.color_scheme_select "C"; }
916
+ @form.bind_key($kh_int["S-F3"], "Cycle bgcolor") { @h.color_scheme_select "C"; }
917
+ @form.bind_key(?=, "Toggle Menu") {
918
+ @h.toggle_menu;
919
+ }
920
+ @form.bind_key(?<, "Previous Forum") { @h.prev_forum; }
921
+ @form.bind_key(?>, "Next Forum") { @h.next_forum; }
922
+ end
923
+
924
+ @form.help_manager.help_text = help_text
925
+
926
+ begin
927
+ stack :margin_top => 1, :margin_left => 0, :width => :expand , :height => FFI::NCurses.LINES-2 do
928
+ tv = textpad :height_pc => 100, :width_pc => 100, :name => "tv", :suppress_borders => true,
929
+ :bgcolor => @color_scheme[:body_bg], :color => 255, :attr => NORMAL
930
+ #tv.renderer ruby_renderer
931
+ #tv.bind(:PRESS) {|ev| display_links }
932
+ tv.text_patterns[:articles] = Regexp.new(/^ *\d+ /)
933
+ tv.bind_key(KEY_TAB, "goto article") { tv.next_regex(:articles) }
934
+ end # stack
935
+
936
+ sl = status_line :row => Ncurses.LINES-1, :bgcolor => :yellow, :color => @color_scheme[:status_bg]
937
+ @h.choose_forum
938
+ rescue => ex
939
+ textdialog ["Error in Redford: #{ex} ", *ex.backtrace], :title => "Exception"
940
+ $log.debug( ex) if ex
941
+ $log.debug(ex.backtrace.join("\n")) if ex
942
+ ensure
943
+ p ex if ex
944
+ p(ex.backtrace.join("\n")) if ex
945
+ end
946
+ end # app