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.
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