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