hacker-curse 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +37 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +89 -0
- data/Rakefile +2 -0
- data/bin/corvus +2320 -0
- data/bin/hacker-comments.rb +182 -0
- data/bin/hacker-tsv.rb +144 -0
- data/bin/hacker-yml.rb +100 -0
- data/bin/hacker.rb +68 -0
- data/bin/hacker.sh +90 -0
- data/bin/redford +946 -0
- data/hacker-curse.gemspec +24 -0
- data/lib/hacker/curse.rb +7 -0
- data/lib/hacker/curse/abstractsiteparser.rb +353 -0
- data/lib/hacker/curse/hackernewsparser.rb +226 -0
- data/lib/hacker/curse/redditnewsparser.rb +241 -0
- data/lib/hacker/curse/version.rb +5 -0
- data/redford.yml +68 -0
- metadata +112 -0
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
|