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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4d1c9443f8f9caa4d49b856573fd3fd0ea2aecbb
|
4
|
+
data.tar.gz: 80b5976740d8c1edfa17054483081b5411083262
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 062ce4af9ae109e51586eb363173fee1e30b76489f7a98939fcefac80eadd925868d28229949251fdd745286b27e13d629c0c64c68d32b6e1250f0972fe35060
|
7
|
+
data.tar.gz: 371b91a4ba9ef832e1cf907d9d6323bef878313597fd1d1635db271673f171e108f0cc02afd302954a99ebe5478d900757b51cf24a86e3e43bc8ec1d043f6d6c
|
data/.gitignore
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
|
12
|
+
## Specific to RubyMotion:
|
13
|
+
.dat*
|
14
|
+
.repl_history
|
15
|
+
build/
|
16
|
+
|
17
|
+
## Documentation cache and generated files:
|
18
|
+
/.yardoc/
|
19
|
+
/_yardoc/
|
20
|
+
/doc/
|
21
|
+
/rdoc/
|
22
|
+
|
23
|
+
## Environment normalisation:
|
24
|
+
/.bundle/
|
25
|
+
/lib/bundler/man/
|
26
|
+
|
27
|
+
# for a library or gem, you might want to ignore these files since the code is
|
28
|
+
# intended to run in multiple environments; otherwise, check them in:
|
29
|
+
# Gemfile.lock
|
30
|
+
# .ruby-version
|
31
|
+
# .ruby-gemset
|
32
|
+
|
33
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
34
|
+
.rvmrc
|
35
|
+
todo
|
36
|
+
*.tmp
|
37
|
+
*.1
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 kepler
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Hacker Curse
|
2
|
+
|
3
|
+
View Hacker News and Reddit.com pages from within your terminal.
|
4
|
+
This gem contains several programs/ utilities:
|
5
|
+
- a library to parse HN and reddit subforums
|
6
|
+
- a program/wrapper that uses the above to generate a tab seperated output (TSV) that may be further used
|
7
|
+
as a filter
|
8
|
+
- a program/wrapper that uses the library to generate a YML file that can be used by client programs
|
9
|
+
- an interactive utility named `redford` that displays stories/articles on the screen (using ncurses)
|
10
|
+
- an interactive utility named `corvus` that displays stories/articles on the screen (using ruby but not ncurses).
|
11
|
+
`corvus` was used as a prototype for `redford` but may still be used if you are not interested in using
|
12
|
+
a curses version.
|
13
|
+
|
14
|
+
The ncurses version is also based on `hackman` which is a ncurses frontend to view RSS feeds from HN, reddit,
|
15
|
+
ars, slashdot.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
$ gem install hacker-curse
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
The basic library uses nokogiri to parse hacker news and reddit.com subforums into a similar
|
24
|
+
structure so that a single program can render output from both sources.
|
25
|
+
|
26
|
+
hn = RedditNewsParser.new :subforum => 'ruby'
|
27
|
+
or
|
28
|
+
hn = HackerNewsParser.new :subforum => 'news'
|
29
|
+
|
30
|
+
page = hn.get_next_page
|
31
|
+
page.each_article do |a|
|
32
|
+
print a.title, a.age_text, a.points, a.comment_count, a.article_url, "\n"
|
33
|
+
end
|
34
|
+
art = page.first
|
35
|
+
art.each_comment do |c|
|
36
|
+
print c.age_text, c.points, c.submitter, "\n"
|
37
|
+
print c.comment_text
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
This program comes with several wrappers which display how to use the library and the wrappers may
|
43
|
+
themselves be used as-is.
|
44
|
+
|
45
|
+
For example, `hacker-tsv.rb` prints the articles and its details such as age, points and number of comments,
|
46
|
+
url, comments url, submitter etc in a tab delimited format. This may further be used be filtered or sorted
|
47
|
+
by other programs.
|
48
|
+
|
49
|
+
As an example, the following retrieves two pages of stories from Hacker News and save the retrieved HTML file,
|
50
|
+
the output goes to the terminal/screen.
|
51
|
+
|
52
|
+
hacker-tsv.rb -H hn -p 2 -s news -w news.html
|
53
|
+
|
54
|
+
Retrieve one page of articles from reddit.com/r/ruby printing the tab delimited rows to the screen.
|
55
|
+
|
56
|
+
hacker-tsv.rb -H rn -s ruby
|
57
|
+
|
58
|
+
`hacker.rb` is a wrapper over `hacker-tsv.rb` that saves the output of the above into tab delimited files.
|
59
|
+
It can guess the host based on the argument.
|
60
|
+
|
61
|
+
hacker.rb news
|
62
|
+
hacker.rb newest
|
63
|
+
hacker.rb programming
|
64
|
+
hacker.rb --pages 2 ruby
|
65
|
+
|
66
|
+
`corvus` is an interactive program (non-ncurses) that uses the generated YML files, and displays a selectable list
|
67
|
+
of stories which a user may navigate, select and launch the article (or comments) in a gui or text browser.
|
68
|
+
User may switch between forums, reload the file, view the articles in a long list or short list, single or
|
69
|
+
multiple columns etc. `corvus` requires at least ruby 1.9.3 in order to get single characters.
|
70
|
+
|
71
|
+
NOTE: The TSV file should be used as a command-line filter, and not in applications or front-ends. Use the YML file for frontends.
|
72
|
+
|
73
|
+
The YML wrapper has been called as:
|
74
|
+
|
75
|
+
hacker-yml.rb --pages 1 -H rn -s ruby -y ~/tmp/hacker-curse/ruby.yml
|
76
|
+
|
77
|
+
The host is "rn" or "hn" (for reddit or hacker news), the subforum is ruby (can be programming or python or java, etc),
|
78
|
+
and the YML file location is also given.
|
79
|
+
|
80
|
+
`redford.rb` can use configuration from a file named `~/.redford.yml`. A sample file named `redford.yml` is in the root folder of this gem.
|
81
|
+
|
82
|
+
|
83
|
+
## Contributing
|
84
|
+
|
85
|
+
1. Fork it ( https://github.com/[my-github-username]/hacker-curse/fork )
|
86
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
87
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
88
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
89
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/corvus
ADDED
@@ -0,0 +1,2320 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# ----------------------------------------------------------------------------- #
|
3
|
+
# File: corvus
|
4
|
+
# Description: navigate tab-separated output, selecting a row and issuing a command
|
5
|
+
#
|
6
|
+
# Author: jkepler http://github.com/mare-imbrium//jkepler
|
7
|
+
# Date: 2014-07-30
|
8
|
+
# License: MIT
|
9
|
+
# Last update: 2014-09-11 12:56
|
10
|
+
# ----------------------------------------------------------------------------- #
|
11
|
+
# corvus.rb Copyright (C) 2014 j kepler
|
12
|
+
# == TODO
|
13
|
+
# - remove junk options like child dirs else program will crash
|
14
|
+
# - don't save html unless specified, also specify location for tsv's so current not cluttered.
|
15
|
+
# - config file, save bookmarks and forumlist.
|
16
|
+
# - remove file stuff
|
17
|
+
# - how to specify format for single list, and large list.
|
18
|
+
# - what of comments
|
19
|
+
require 'readline'
|
20
|
+
require 'io/wait'
|
21
|
+
# http://www.ruby-doc.org/stdlib-1.9.3/libdoc/shellwords/rdoc/Shellwords.html
|
22
|
+
require 'shellwords'
|
23
|
+
require 'fileutils'
|
24
|
+
require 'yaml'
|
25
|
+
# -- requires 1.9.3 for io/wait
|
26
|
+
# -- cannot do with Highline since we need a timeout on wait, not sure if HL can do that
|
27
|
+
|
28
|
+
## INSTALLATION
|
29
|
+
# copy into PATH
|
30
|
+
# alias c=~/bin/corvus.rb
|
31
|
+
# c
|
32
|
+
VERSION="0.0.2"
|
33
|
+
CONFIG_FILE="~/.corvusinfo"
|
34
|
+
|
35
|
+
$bindings = {
|
36
|
+
"`" => "main_menu",
|
37
|
+
"=" => "toggle_menu",
|
38
|
+
"!" => "command_mode",
|
39
|
+
#"@" => "selection_mode_toggle",
|
40
|
+
"M-a" => "select_all",
|
41
|
+
"M-A" => "unselect_all",
|
42
|
+
"+" => "add_to_forum_list",
|
43
|
+
"-" => "remove_from_forum_list",
|
44
|
+
"<" => "previous_forum",
|
45
|
+
">" => "next_forum",
|
46
|
+
":" => "subcommand",
|
47
|
+
"'" => "goto_bookmark",
|
48
|
+
"/" => "enter_regex",
|
49
|
+
"M-p" => "prev_page",
|
50
|
+
"M-n" => "next_page",
|
51
|
+
"SPACE" => "next_page",
|
52
|
+
"M-f" => "select_visited_files",
|
53
|
+
"M-d" => "select_used_dirs",
|
54
|
+
"M-b" => "select_bookmarks",
|
55
|
+
"M-m" => "create_bookmark",
|
56
|
+
"M-M" => "show_marks",
|
57
|
+
"C-c" => "escape",
|
58
|
+
"ESCAPE" => "escape",
|
59
|
+
"TAB" => "views",
|
60
|
+
"C-i" => "views",
|
61
|
+
#"?" => "dirtree",
|
62
|
+
"ENTER" => "select_current",
|
63
|
+
"D" => "delete_current_forum",
|
64
|
+
#"M" => "forum_actions most",
|
65
|
+
"Q" => "quit_command",
|
66
|
+
"RIGHT" => "column_next",
|
67
|
+
"LEFT" => "column_next 1",
|
68
|
+
"C-x" => "forum_actions",
|
69
|
+
"M--" => "columns_incdec -1",
|
70
|
+
"M-+" => "columns_incdec 1",
|
71
|
+
#"S" => "command_file list y ls -lh",
|
72
|
+
#"L" => "command_file Page n less",
|
73
|
+
"C-d" => "cursor_scroll_dn",
|
74
|
+
"C-b" => "cursor_scroll_up",
|
75
|
+
"UP" => "cursor_up",
|
76
|
+
"DOWN" => "cursor_dn",
|
77
|
+
"C-SPACE" => "visual_mode_toggle",
|
78
|
+
|
79
|
+
"M-?" => "print_help",
|
80
|
+
"F1" => "print_help",
|
81
|
+
"F2" => "display_forum"
|
82
|
+
#"F3" => "dirtree",
|
83
|
+
#"F4" => "tree",
|
84
|
+
#"S-F1" => "dirtree",
|
85
|
+
#"S-F2" => "tree"
|
86
|
+
|
87
|
+
}
|
88
|
+
|
89
|
+
## clean this up a bit, copied from shell program and macro'd
|
90
|
+
$kh=Hash.new
|
91
|
+
$kh["OP"]="F1"
|
92
|
+
$kh["[A"]="UP"
|
93
|
+
$kh["[5~"]="PGUP"
|
94
|
+
$kh['']="ESCAPE"
|
95
|
+
$kh['[Z']="BACKTAB"
|
96
|
+
KEY_PGDN="[6~"
|
97
|
+
KEY_PGUP="[5~"
|
98
|
+
## I needed to replace the O with a [ for this to work
|
99
|
+
# in Vim Home comes as ^[OH whereas on the command line it is correct as ^[[H
|
100
|
+
KEY_HOME='[H'
|
101
|
+
KEY_END="[F"
|
102
|
+
KEY_F1="OP"
|
103
|
+
KEY_UP="[A"
|
104
|
+
KEY_DOWN="[B"
|
105
|
+
|
106
|
+
$kh[KEY_PGDN]="PgDn"
|
107
|
+
$kh[KEY_PGUP]="PgUp"
|
108
|
+
$kh[KEY_HOME]="Home"
|
109
|
+
$kh[KEY_END]="End"
|
110
|
+
$kh[KEY_F1]="F1"
|
111
|
+
$kh[KEY_UP]="UP"
|
112
|
+
$kh[KEY_DOWN]="DOWN"
|
113
|
+
KEY_LEFT='[D'
|
114
|
+
KEY_RIGHT='[C'
|
115
|
+
$kh["OQ"]="F2"
|
116
|
+
$kh["OR"]="F3"
|
117
|
+
$kh["OS"]="F4"
|
118
|
+
$kh[KEY_LEFT] = "LEFT"
|
119
|
+
$kh[KEY_RIGHT]= "RIGHT"
|
120
|
+
KEY_F5='[15~'
|
121
|
+
KEY_F6='[17~'
|
122
|
+
KEY_F7='[18~'
|
123
|
+
KEY_F8='[19~'
|
124
|
+
KEY_F9='[20~'
|
125
|
+
KEY_F10='[21~'
|
126
|
+
KEY_S_F1='[1;2P'
|
127
|
+
$kh[KEY_F5]="F5"
|
128
|
+
$kh[KEY_F6]="F6"
|
129
|
+
$kh[KEY_F7]="F7"
|
130
|
+
$kh[KEY_F8]="F8"
|
131
|
+
$kh[KEY_F9]="F9"
|
132
|
+
$kh[KEY_F10]="F10"
|
133
|
+
# testing out shift+Function. these are the codes my kb generates
|
134
|
+
$kh[KEY_S_F1]="S-F1"
|
135
|
+
$kh['[1;2Q']="S-F2"
|
136
|
+
|
137
|
+
## get a character from user and return as a string
|
138
|
+
# Adapted from:
|
139
|
+
#http://stackoverflow.com/questions/174933/how-to-get-a-single-character-without-pressing-enter/8274275#8274275
|
140
|
+
# Need to take complex keys and matc against a hash.
|
141
|
+
def get_char
|
142
|
+
begin
|
143
|
+
system("stty raw -echo 2>/dev/null") # turn raw input on
|
144
|
+
c = nil
|
145
|
+
#if $stdin.ready?
|
146
|
+
c = $stdin.getc
|
147
|
+
cn=c.ord
|
148
|
+
return "ENTER" if cn == 13
|
149
|
+
return "BACKSPACE" if cn == 127
|
150
|
+
return "C-SPACE" if cn == 0
|
151
|
+
return "SPACE" if cn == 32
|
152
|
+
# next does not seem to work, you need to bind C-i
|
153
|
+
return "TAB" if cn == 8
|
154
|
+
if cn >= 0 && cn < 27
|
155
|
+
x= cn + 96
|
156
|
+
return "C-#{x.chr}"
|
157
|
+
end
|
158
|
+
if c == ''
|
159
|
+
buff=c.chr
|
160
|
+
while true
|
161
|
+
k = nil
|
162
|
+
if $stdin.ready?
|
163
|
+
k = $stdin.getc
|
164
|
+
#puts "got #{k}"
|
165
|
+
buff += k.chr
|
166
|
+
else
|
167
|
+
x=$kh[buff]
|
168
|
+
return x if x
|
169
|
+
#puts "returning with #{buff}"
|
170
|
+
if buff.size == 2
|
171
|
+
## possibly a meta/alt char
|
172
|
+
k = buff[-1]
|
173
|
+
return "M-#{k.chr}"
|
174
|
+
end
|
175
|
+
return buff
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
#end
|
180
|
+
return c.chr if c
|
181
|
+
ensure
|
182
|
+
#system "stty -raw echo" # turn raw input off
|
183
|
+
system("stty -raw echo 2>/dev/null") # turn raw input on
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
## GLOBALS
|
188
|
+
#$IDX="123456789abcdefghijklmnoprstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
189
|
+
#$IDX="abcdefghijklmnopqrstuvwxy"
|
190
|
+
$IDX=('a'..'y').to_a
|
191
|
+
$IDX.concat ('za'..'zz').to_a
|
192
|
+
$IDX.concat ('Za'..'Zz').to_a
|
193
|
+
$IDX.concat ('ZA'..'ZZ').to_a
|
194
|
+
$min_index = 1 # this prevents cursor from moving into zeroth position since this is the title.
|
195
|
+
# using min index as 1 works fine but causes some subtle issues like when there are 2 cols
|
196
|
+
# and we press right arrow, then first index is this space.
|
197
|
+
$min_index = 0 # this is old behavior
|
198
|
+
$IDX.unshift(" ") if $min_index > 0
|
199
|
+
|
200
|
+
#$selected_files = Array.new
|
201
|
+
$bookmarks = {}
|
202
|
+
$forumlist = {}
|
203
|
+
$mode = nil
|
204
|
+
$glines=%x(tput lines).to_i
|
205
|
+
$gcols=%x(tput cols).to_i
|
206
|
+
$grows = $glines - 3
|
207
|
+
$pagesize = 60
|
208
|
+
# how many columns we will try to show on one page
|
209
|
+
$gviscols = 2
|
210
|
+
$pagesize = $grows * $gviscols
|
211
|
+
$stact = 0
|
212
|
+
$editor_mode = true
|
213
|
+
$enhanced_mode = false
|
214
|
+
$visual_block_start = nil
|
215
|
+
$pager_command = {
|
216
|
+
:text => 'most',
|
217
|
+
:image => 'open',
|
218
|
+
:zip => 'tar ztvf %% | most',
|
219
|
+
:unknown => 'open'
|
220
|
+
}
|
221
|
+
$dir_position = {}
|
222
|
+
## CONSTANTS
|
223
|
+
GMARK='*'
|
224
|
+
DOTMARK='.'
|
225
|
+
CURMARK='>'
|
226
|
+
MSCROLL = 10
|
227
|
+
SPACE=" "
|
228
|
+
CLEAR = "\e[0m"
|
229
|
+
BOLD = "\e[1m"
|
230
|
+
BOLD_OFF = "\e[22m"
|
231
|
+
RED = "\e[31m"
|
232
|
+
ON_RED = "\e[41m"
|
233
|
+
GREEN = "\e[32m"
|
234
|
+
YELLOW = "\e[33m"
|
235
|
+
BLUE = "\e[1;34m"
|
236
|
+
|
237
|
+
ON_BLUE = "\e[44m"
|
238
|
+
REVERSE = "\e[7m"
|
239
|
+
UNDERLINE = "\e[4m"
|
240
|
+
CURSOR_COLOR = ON_BLUE
|
241
|
+
$patt=nil
|
242
|
+
$ignorecase = true
|
243
|
+
$quitting = false
|
244
|
+
$modified = $writing = false
|
245
|
+
$visited_files = []
|
246
|
+
## dir stack for popping
|
247
|
+
$visited_dirs = []
|
248
|
+
## dirs where some work has been done, for saving and restoring
|
249
|
+
$used_dirs = []
|
250
|
+
$sorto = ""
|
251
|
+
$viewctr = 0
|
252
|
+
$history = []
|
253
|
+
$sta = 0
|
254
|
+
$cursor = $min_index
|
255
|
+
$visual_mode = false
|
256
|
+
# these columns ONLY in long_list_columns is base ONE, like in +cut+.
|
257
|
+
# These are the columns you want to be shown in a long listing of articles/titles
|
258
|
+
$long_list_columns = [1,2,3,4]
|
259
|
+
$long_list_columns = [2,3,4,1]
|
260
|
+
# sort_columns are zero-based
|
261
|
+
$sort_columns = [2,3,6]
|
262
|
+
$column_enum = $sort_columns.cycle
|
263
|
+
$num_pages = 1
|
264
|
+
$logger_array = Array.new
|
265
|
+
# history of commands entered in readline for various prompts or keys
|
266
|
+
$history = Hash.new
|
267
|
+
$commandlist = %w[refresh forum_actions view_log clear_log change_file fetch_data_from_net sort_on display_forum next_forum previous_forum new_articles]
|
268
|
+
$selection_allowed = false
|
269
|
+
$def_forumlist = %w[ news newest programming ruby python haskell scala java zsh commandline vim vimplugins ]
|
270
|
+
$cache_path = "."
|
271
|
+
$hint_message = nil
|
272
|
+
$hacker_forums = %w{news newest show jobs ask}
|
273
|
+
$hints_toggle = true
|
274
|
+
$yaml_obj = nil
|
275
|
+
# global
|
276
|
+
|
277
|
+
$help = "#{BOLD}M-?#{BOLD_OFF} Help #{BOLD}`#{BOLD_OFF} Menu #{BOLD}=#{BOLD_OFF} Toggle #{BOLD}Q#{BOLD_OFF} Quit "
|
278
|
+
|
279
|
+
# Ideally should have only first columns while the others are kept in array
|
280
|
+
# so we can get rest of data when needed
|
281
|
+
def get_data filename=nil
|
282
|
+
unless filename
|
283
|
+
filename = $filename
|
284
|
+
else
|
285
|
+
$filename = filename
|
286
|
+
end
|
287
|
+
raise "File name not found #{filename} in get_data " if !File.exists?(filename)
|
288
|
+
#lines = File.open(filename, "r").read.split("\n")
|
289
|
+
#if lines.nil? or lines.empty?
|
290
|
+
#perror "Failed trying to read #{filename}. Please delete if empty "
|
291
|
+
#lines = ["no data", "no data, please refresh"]
|
292
|
+
#end
|
293
|
+
obj = YAML::load( File.open( filename ) )
|
294
|
+
$yaml_obj = obj
|
295
|
+
age = compute_file_age filename
|
296
|
+
articles = obj[:articles]
|
297
|
+
if articles.empty?
|
298
|
+
# this happens when doing More with a stale url
|
299
|
+
perror "No articles in this file. Please delete file."
|
300
|
+
return
|
301
|
+
end
|
302
|
+
colh = articles.first.keys
|
303
|
+
colh = [:title ,:age_text ,:comment_count ,:points ,:article_url ,:comments_url ,:age ,:submitter ,:submitter_url ,:byline]
|
304
|
+
$forum_bgcolor = BOLD
|
305
|
+
$forum_bgcolor = ON_RED if age > 8
|
306
|
+
# this should only happen if columns are in row 0
|
307
|
+
# # FIXME
|
308
|
+
$column_headings = colh
|
309
|
+
$full_data = Array.new
|
310
|
+
# there will be a path also at some time
|
311
|
+
#$subforum = filename.sub(File.extname(filename),"").sub("__","/")
|
312
|
+
$subforum = file_to_forum(filename)
|
313
|
+
$host = forum_to_host $subforum
|
314
|
+
titles = Array.new
|
315
|
+
articles.each do |a|
|
316
|
+
titles << a[:title]
|
317
|
+
_cols = []
|
318
|
+
$column_headings.each do |c|
|
319
|
+
_cols << a[c]
|
320
|
+
end
|
321
|
+
$full_data << _cols
|
322
|
+
end
|
323
|
+
perror "No data for #{filename} " if $full_data.empty?
|
324
|
+
raise "No data for #{filename} " if $full_data.empty?
|
325
|
+
#lines.each_with_index do |l, ix|
|
326
|
+
#_cols = l.split("\t")
|
327
|
+
#titles << _cols.first
|
328
|
+
#$full_data << _cols
|
329
|
+
#end
|
330
|
+
calc_column_widths
|
331
|
+
size_column_headings
|
332
|
+
$files = titles
|
333
|
+
end
|
334
|
+
def TSV_get_data filename=nil # {{{
|
335
|
+
unless filename
|
336
|
+
filename = $filename
|
337
|
+
else
|
338
|
+
$filename = filename
|
339
|
+
end
|
340
|
+
raise "File name not found #{filename} in get_data " if !File.exists?(filename)
|
341
|
+
lines = File.open(filename, "r").read.split("\n")
|
342
|
+
if lines.nil? or lines.empty?
|
343
|
+
perror "Failed trying to read #{filename}. Please delete if empty "
|
344
|
+
lines = ["no data", "no data, please refresh"]
|
345
|
+
end
|
346
|
+
age = compute_file_age filename
|
347
|
+
$forum_bgcolor = BOLD
|
348
|
+
$forum_bgcolor = ON_RED if age > 8
|
349
|
+
# this should only happen if columns are in row 0
|
350
|
+
$column_headings = lines.delete_at(0).split("\t")
|
351
|
+
$full_data = Array.new
|
352
|
+
# there will be a path also at some time
|
353
|
+
#$subforum = filename.sub(File.extname(filename),"").sub("__","/")
|
354
|
+
$subforum = file_to_forum(filename)
|
355
|
+
$host = forum_to_host $subforum
|
356
|
+
titles = Array.new
|
357
|
+
lines.each_with_index do |l, ix|
|
358
|
+
_cols = l.split("\t")
|
359
|
+
titles << _cols.first
|
360
|
+
$full_data << _cols
|
361
|
+
end
|
362
|
+
calc_column_widths
|
363
|
+
size_column_headings
|
364
|
+
$files = titles
|
365
|
+
end # }}}
|
366
|
+
# reload from internet
|
367
|
+
def fetch_data_from_net subforum=nil, more_url=nil
|
368
|
+
unless subforum
|
369
|
+
print "Enter name of subforum: "
|
370
|
+
subforum = Readline::readline('>', true)
|
371
|
+
return if subforum == ""
|
372
|
+
$host = forum_to_host subforum
|
373
|
+
end
|
374
|
+
filename = forum_to_file subforum
|
375
|
+
print "fetching for #{subforum} ... saving to #{filename}\n"
|
376
|
+
m = nil
|
377
|
+
if more_url
|
378
|
+
m = "-u #{more_url} "
|
379
|
+
m = "-u '" + more_url + "'"
|
380
|
+
end
|
381
|
+
puts "hacker-yml.rb --pages #{$num_pages} -H #{$host} -s #{subforum} -y #{filename} #{m}"
|
382
|
+
# put in begin rescue since sometimes no data, as for programming/rising
|
383
|
+
#retval = system("hacker.rb --pages #{$num_pages} #{subforum}")
|
384
|
+
retval = system("hacker-yml.rb --pages #{$num_pages} -H #{$host} -s #{subforum} -y #{filename} #{m}")
|
385
|
+
status = "#{$?}"
|
386
|
+
unless retval
|
387
|
+
perror "Error occured in hacker-yml.rb. status is #{status}"
|
388
|
+
return
|
389
|
+
end
|
390
|
+
|
391
|
+
if File.exists? filename
|
392
|
+
get_data filename
|
393
|
+
post_cd
|
394
|
+
else
|
395
|
+
perror "Cannot find file #{filename}. Some error has happened."
|
396
|
+
end
|
397
|
+
$hint_message = nil
|
398
|
+
end
|
399
|
+
# this allows us to fetch more items using the NEXT url
|
400
|
+
# However, when we read up later, this file will contain a stale next url
|
401
|
+
# as well as stale date. We should have avoided stale data.
|
402
|
+
def fetch_more
|
403
|
+
more_url = $yaml_obj[:next_url]
|
404
|
+
#perror "more url is #{more_url} "
|
405
|
+
fetch_data_from_net $subforum, more_url
|
406
|
+
end
|
407
|
+
def file_to_forum filename
|
408
|
+
forum = File.basename(filename).sub(File.extname(filename),"").sub("__","/")
|
409
|
+
end
|
410
|
+
def forum_to_file forum
|
411
|
+
file = "#{forum}.yml".sub("/","__")
|
412
|
+
file = "#{$cache_path}/#{file}"
|
413
|
+
end
|
414
|
+
def forum_to_host fo
|
415
|
+
if $hacker_forums.include? fo
|
416
|
+
return :hn
|
417
|
+
end
|
418
|
+
return :rn
|
419
|
+
end
|
420
|
+
def reload
|
421
|
+
fetch_data_from_net $subforum
|
422
|
+
end
|
423
|
+
def calc_column_widths
|
424
|
+
$column_widths = []
|
425
|
+
row = $full_data.first
|
426
|
+
row.each do |r|
|
427
|
+
# in some cases like jobs, r may be nil
|
428
|
+
#$column_widths << r.strip.size + 1
|
429
|
+
# i have padded rjust the integers
|
430
|
+
sz = r ? r.size : 1
|
431
|
+
$column_widths << sz
|
432
|
+
end
|
433
|
+
end
|
434
|
+
# truncate column headings to file size of column
|
435
|
+
def size_column_headings
|
436
|
+
$sized_column_headings = []
|
437
|
+
if $column_widths
|
438
|
+
$column_headings.zip($column_widths) do |h, w|
|
439
|
+
$sized_column_headings << h.slice(0, w)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
## main loop which calls all other programs
|
445
|
+
def run(options)
|
446
|
+
$browser_mode = options[:browser_mode] || 'text'
|
447
|
+
$browser_text = options[:browser_text] || 'elinks'
|
448
|
+
$browser_gui = options[:browser_gui] || 'open'
|
449
|
+
$cache_path = options[:cache_path] || "."
|
450
|
+
ctr=0
|
451
|
+
filename = ARGV[0] || "news.yml"
|
452
|
+
config_read
|
453
|
+
$open_command = $browser_mode == 'text' ? $browser_text : $browser_gui
|
454
|
+
if $bookmarks.empty?
|
455
|
+
$bookmarks = { :r => 'ruby', :p => 'programming', :n => 'news', :j => 'java', :c => 'commandline',
|
456
|
+
:v => 'vim', :z => 'zsh', :l => 'linux', :a => 'apple', :h => 'haskell' , :s => 'scala',
|
457
|
+
:u => 'rust', :y => 'python', :w => 'newest' }
|
458
|
+
end
|
459
|
+
if $forumlist.empty?
|
460
|
+
$forumlist = $def_forumlist
|
461
|
+
end
|
462
|
+
|
463
|
+
# unfortunately this is insisting on a file being present. It should allow use to fetch data if
|
464
|
+
# no file.
|
465
|
+
unless File.exists? filename
|
466
|
+
forum = choose_forum
|
467
|
+
if forum
|
468
|
+
# fetch data from file, or if not present then internet
|
469
|
+
display_forum forum
|
470
|
+
filename = data_cached_for forum
|
471
|
+
else
|
472
|
+
raise "Cannot continue without a forum name"
|
473
|
+
end
|
474
|
+
end
|
475
|
+
$filename = filename
|
476
|
+
get_data filename
|
477
|
+
|
478
|
+
fl=$files.size
|
479
|
+
|
480
|
+
selectedix = nil
|
481
|
+
$patt=""
|
482
|
+
$sta = 0
|
483
|
+
while true
|
484
|
+
i = 0
|
485
|
+
if $patt
|
486
|
+
if $ignorecase
|
487
|
+
$view = $files.grep(/#{$patt}/i)
|
488
|
+
else
|
489
|
+
$view = $files.grep(/#{$patt}/)
|
490
|
+
end
|
491
|
+
else
|
492
|
+
$view = $files
|
493
|
+
end
|
494
|
+
fl=$view.size
|
495
|
+
$sta = 0 if $sta >= fl || $sta < 0
|
496
|
+
$viewport = $view[$sta, $pagesize]
|
497
|
+
fin = $sta + $viewport.size
|
498
|
+
$title ||= $subforum
|
499
|
+
$details ||= "-"
|
500
|
+
system("clear")
|
501
|
+
# title
|
502
|
+
# DETAIL LINE
|
503
|
+
# on the top line, lets show some details for the line under the cursor (focussed line)
|
504
|
+
if true
|
505
|
+
fullrow = $viewport[$cursor]
|
506
|
+
# in case of long listings cursor can go one higher than size, as we do not scroll.
|
507
|
+
if fullrow
|
508
|
+
fullrow = get_full_row(fullrow)
|
509
|
+
# this is the byline, we shouldmake it the last column
|
510
|
+
$details = "#{YELLOW} >> #{fullrow.last} "
|
511
|
+
end
|
512
|
+
end
|
513
|
+
## HELP LINE
|
514
|
+
_hint = ""
|
515
|
+
if $hints_toggle
|
516
|
+
_hint = $hint_message || get_hint
|
517
|
+
_hint = "#{BOLD}#{_hint}#{CLEAR}"
|
518
|
+
end
|
519
|
+
print "#{GREEN}#{$help} #{BLUE}corvus #{VERSION}#{CLEAR} : #{_hint}\n"
|
520
|
+
#t = "#{$title} #{$sta + 1} to #{fin} of #{fl} #{$sorto} F:#{$filterstr}"
|
521
|
+
t = "#{$title}#{CLEAR} #{BOLD}#{$sta + 1} to #{fin} of #{fl} o:#{$sorto}:#{$details} "
|
522
|
+
t = t[t.size-$gcols..-1] if t.size >= $gcols
|
523
|
+
## TITLE LINE
|
524
|
+
#print "#{BOLD}#{t}#{CLEAR}\n"
|
525
|
+
print "#{$forum_bgcolor}#{t}#{CLEAR}\n"
|
526
|
+
if $long_listing
|
527
|
+
# lets see if we can print the headings
|
528
|
+
if $column_headings
|
529
|
+
s = get_long_list $sized_column_headings
|
530
|
+
print " ", s, "\n"
|
531
|
+
end
|
532
|
+
end
|
533
|
+
## nilling the title means a superimposed one gets cleared.
|
534
|
+
#$title = nil
|
535
|
+
# split into 2 procedures so columnate can e clean and reused.
|
536
|
+
buff = format $viewport
|
537
|
+
buff = columnate buff, $grows
|
538
|
+
# needed the next line to see how much extra we were going in padding
|
539
|
+
#buff.each {|line| print "#{REVERSE}#{line}#{CLEAR}\n" }
|
540
|
+
buff.each {|line| print line, "\n" }
|
541
|
+
print
|
542
|
+
# prompt
|
543
|
+
#print "#{$files.size}, #{view.size} sta=#{sta} (#{patt}): "
|
544
|
+
_mm = ""
|
545
|
+
_mm = "[#{$mode}] " if $mode
|
546
|
+
print "\r #{_mm}#{$patt} >"
|
547
|
+
ch = get_char
|
548
|
+
#puts
|
549
|
+
#break if ch == "q"
|
550
|
+
#elsif ch =~ /^[1-9a-zA-Z]$/
|
551
|
+
if ch =~ /^[a-zZ]$/
|
552
|
+
# hint mode
|
553
|
+
select_hint $viewport, ch
|
554
|
+
ctr = 0
|
555
|
+
elsif ch == "BACKSPACE"
|
556
|
+
if $patt && $patt.size > 0
|
557
|
+
$patt = $patt[0..-2]
|
558
|
+
end
|
559
|
+
ctr = 0
|
560
|
+
else
|
561
|
+
#binding = $bindings[ch]
|
562
|
+
x = $bindings[ch]
|
563
|
+
x = x.split if x
|
564
|
+
if x
|
565
|
+
binding = x.shift
|
566
|
+
args = x
|
567
|
+
send(binding, *args) if binding
|
568
|
+
else
|
569
|
+
log "No binding for #{ch}, #{ch.ord} "
|
570
|
+
end
|
571
|
+
#p ch
|
572
|
+
end
|
573
|
+
break if $quitting
|
574
|
+
end
|
575
|
+
puts "bye"
|
576
|
+
config_write if $writing
|
577
|
+
end
|
578
|
+
def exec_string x=nil
|
579
|
+
unless x
|
580
|
+
x = Readline::readline(': ')
|
581
|
+
return if x.nil? or x == ""
|
582
|
+
end
|
583
|
+
|
584
|
+
x = x.split
|
585
|
+
if x
|
586
|
+
binding = x.shift
|
587
|
+
args = x
|
588
|
+
send(binding, *args) if binding
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
##
|
593
|
+
#
|
594
|
+
# print in columns
|
595
|
+
# ary - array of data
|
596
|
+
# sz - lines in one column
|
597
|
+
#
|
598
|
+
def columnate ary, sz
|
599
|
+
buff=Array.new
|
600
|
+
return buff if ary.nil? || ary.size == 0
|
601
|
+
|
602
|
+
# determine width based on number of files to show
|
603
|
+
# if less than sz then 1 col and full width
|
604
|
+
#
|
605
|
+
wid = 30
|
606
|
+
ars = ary.size
|
607
|
+
ars = [$pagesize, ary.size].min
|
608
|
+
d = 0
|
609
|
+
if ars <= sz
|
610
|
+
wid = $gcols - d
|
611
|
+
else
|
612
|
+
tmp = (ars * 1.000/ sz).ceil
|
613
|
+
wid = $gcols / tmp - d
|
614
|
+
end
|
615
|
+
#elsif ars < sz * 2
|
616
|
+
#wid = $gcols/2 - d
|
617
|
+
#elsif ars < sz * 3
|
618
|
+
#wid = $gcols/3 - d
|
619
|
+
#else
|
620
|
+
#wid = $gcols/$gviscols - d
|
621
|
+
#end
|
622
|
+
|
623
|
+
# ix refers to the index in the complete file list, wherease we only show 60 at a time
|
624
|
+
ix=0
|
625
|
+
while true
|
626
|
+
## ctr refers to the index in the column
|
627
|
+
ctr=0
|
628
|
+
while ctr < sz
|
629
|
+
|
630
|
+
f = ary[ix]
|
631
|
+
fsz = f.size
|
632
|
+
if fsz > wid
|
633
|
+
f = f[0, wid-2]+"$ "
|
634
|
+
## we do the coloring after trunc so ANSI escpe seq does not get get
|
635
|
+
if ix + $sta == $cursor
|
636
|
+
f = "#{CURSOR_COLOR}#{f}#{CLEAR}"
|
637
|
+
end
|
638
|
+
else
|
639
|
+
## we do the coloring before padding so the entire line does not get padded, only file name
|
640
|
+
if ix + $sta == $cursor
|
641
|
+
f = "#{CURSOR_COLOR}#{f}#{CLEAR}"
|
642
|
+
end
|
643
|
+
#f = f.ljust(wid)
|
644
|
+
f << " " * (wid-fsz)
|
645
|
+
end
|
646
|
+
|
647
|
+
if buff[ctr]
|
648
|
+
buff[ctr] += f
|
649
|
+
else
|
650
|
+
buff[ctr] = f
|
651
|
+
end
|
652
|
+
|
653
|
+
ctr+=1
|
654
|
+
ix+=1
|
655
|
+
break if ix >= ary.size
|
656
|
+
end
|
657
|
+
break if ix >= ary.size
|
658
|
+
end
|
659
|
+
return buff
|
660
|
+
end
|
661
|
+
## formats the data with number, mark and details
|
662
|
+
def format ary
|
663
|
+
#buff = Array.new
|
664
|
+
buff = Array.new(ary.size)
|
665
|
+
return buff if ary.nil? || ary.size == 0
|
666
|
+
|
667
|
+
# determine width based on number of files to show
|
668
|
+
# if less than sz then 1 col and full width
|
669
|
+
#
|
670
|
+
# ix refers to the index in the complete file list, wherease we only show 60 at a time
|
671
|
+
ix=0
|
672
|
+
ctr=0
|
673
|
+
pad = false
|
674
|
+
pad = true if ary.size > 25
|
675
|
+
ary.each do |f|
|
676
|
+
## ctr refers to the index in the column
|
677
|
+
ind = get_shortcut(ix)
|
678
|
+
ind = ind.ljust(2) if pad
|
679
|
+
mark=SPACE
|
680
|
+
cur=SPACE
|
681
|
+
cur = CURMARK if ix + $sta == $cursor
|
682
|
+
#mark=GMARK if $selected_files.index(ary[ix])
|
683
|
+
# we are not doing selection here
|
684
|
+
# but maybe today's articles can be starred
|
685
|
+
age_in_hours = article_age_in_hours(f)
|
686
|
+
mark=DOTMARK if age_in_hours < 24
|
687
|
+
mark=GMARK if age_in_hours < 8
|
688
|
+
|
689
|
+
if $long_listing
|
690
|
+
begin
|
691
|
+
f = get_long_list f
|
692
|
+
rescue Exception => e
|
693
|
+
#f = "%10s %s %s" % ["?", "??????????", f]
|
694
|
+
end
|
695
|
+
end
|
696
|
+
|
697
|
+
s = "#{ind}#{mark}#{cur}#{f}"
|
698
|
+
# I cannot color the current line since format does the chopping
|
699
|
+
# so not only does the next lines alignment get skeweed, but also if the line is truncated
|
700
|
+
# then the color overflows.
|
701
|
+
#if ix + $sta == $cursor
|
702
|
+
#s = "#{RED}#{s}#{CLEAR}"
|
703
|
+
#end
|
704
|
+
|
705
|
+
buff[ctr] = s
|
706
|
+
|
707
|
+
ctr+=1
|
708
|
+
ix+=1
|
709
|
+
end
|
710
|
+
return buff
|
711
|
+
end
|
712
|
+
## select file based on key pressed
|
713
|
+
def select_hint view, ch
|
714
|
+
# a to y is direct
|
715
|
+
# if x or z take a key IF there are those many
|
716
|
+
#
|
717
|
+
ix = get_index(ch, view.size)
|
718
|
+
if ix
|
719
|
+
f = view[ix]
|
720
|
+
return unless f
|
721
|
+
$cursor = $sta + ix
|
722
|
+
|
723
|
+
if $mode == 'SEL'
|
724
|
+
toggle_select f
|
725
|
+
elsif $mode == 'COM'
|
726
|
+
run_command f
|
727
|
+
elsif $mode == "forum"
|
728
|
+
display_forum f
|
729
|
+
else
|
730
|
+
on_enter f
|
731
|
+
end
|
732
|
+
#selectedix=ix
|
733
|
+
end
|
734
|
+
end
|
735
|
+
## toggle selection state of file
|
736
|
+
def toggle_select f
|
737
|
+
return unless $selection_allowed
|
738
|
+
if $selected_files.index f
|
739
|
+
$selected_files.delete f
|
740
|
+
else
|
741
|
+
$selected_files.push f
|
742
|
+
end
|
743
|
+
end
|
744
|
+
# returns full row of data for given title
|
745
|
+
def get_full_row title=nil
|
746
|
+
title = get_current_title unless title
|
747
|
+
location = $files.index(title)
|
748
|
+
raise "no location for title: #{title}" unless location
|
749
|
+
fullrow = $full_data[location]
|
750
|
+
return fullrow
|
751
|
+
end
|
752
|
+
# returns age in hours of given title
|
753
|
+
def article_age_in_hours title
|
754
|
+
$_time_ ||= Time.now.to_i
|
755
|
+
fullrow = get_full_row title
|
756
|
+
return 10000 unless fullrow
|
757
|
+
age = fullrow[6].to_i
|
758
|
+
hours = (($_time_ - age)/3600).to_i
|
759
|
+
return hours
|
760
|
+
end
|
761
|
+
def pad_or_trunc str, wid
|
762
|
+
f = str.dup
|
763
|
+
fsz = f.size
|
764
|
+
if fsz > wid
|
765
|
+
f = f[0, wid-2]+"% "
|
766
|
+
else
|
767
|
+
f << " " * (wid-fsz)
|
768
|
+
end
|
769
|
+
return f
|
770
|
+
end
|
771
|
+
# returns a formatted string for the title submitted.
|
772
|
+
# In the case of columns headings, we send in the columns array ?
|
773
|
+
def get_long_list title
|
774
|
+
case title
|
775
|
+
when String
|
776
|
+
fullrow = get_full_row title
|
777
|
+
when Array
|
778
|
+
fullrow = title
|
779
|
+
end
|
780
|
+
s = ""
|
781
|
+
# while spacing remember that we add a space after each column, and the index at the start which is about 3 chars
|
782
|
+
|
783
|
+
#@long_list_column_colors ||= [ BLUE, GREEN, ON_RED, ON_BLUE]
|
784
|
+
ctr = 0
|
785
|
+
if false
|
786
|
+
w3 = (($gcols/6).floor.to_i)-1 # minus 1 since we add space after
|
787
|
+
w1 = (($gcols/2).floor.to_i) # title
|
788
|
+
col_widths = [w1, w3,w3,w3-4]
|
789
|
+
col_widths = [ w3,w3,w3-4, w1]
|
790
|
+
fullrow.each_with_index do |e, i|
|
791
|
+
if $long_list_columns.include? i+1
|
792
|
+
wid = col_widths[ctr] || 10
|
793
|
+
#colr = $long_list_column_colors[ctr]
|
794
|
+
#ss = "#{colr}#{pad_or_trunc(e, wid)}#{CLEAR}"
|
795
|
+
s << pad_or_trunc(e, wid)
|
796
|
+
s << " "
|
797
|
+
ctr += 1
|
798
|
+
end
|
799
|
+
end
|
800
|
+
end
|
801
|
+
$long_list_columns.each do |i|
|
802
|
+
e = fullrow[i-1]
|
803
|
+
#wid = col_widths[ctr] || 10
|
804
|
+
#colr = $long_list_column_colors[ctr]
|
805
|
+
#ss = "#{colr}#{pad_or_trunc(e, wid)}#{CLEAR}"
|
806
|
+
#s << pad_or_trunc(e, wid)
|
807
|
+
s << e
|
808
|
+
s << " "
|
809
|
+
ctr += 1
|
810
|
+
end
|
811
|
+
return s
|
812
|
+
end
|
813
|
+
## open file or directory
|
814
|
+
def open_file f, ch = nil
|
815
|
+
location = $files.index(f)
|
816
|
+
fullrow = $full_data[location]
|
817
|
+
ch = column_menu fullrow unless ch
|
818
|
+
return if ch.to_i > fullrow.size
|
819
|
+
url = fullrow[ch.to_i]
|
820
|
+
system "#{$open_command} #{url}"
|
821
|
+
return # 2014-07-25 - 22:43
|
822
|
+
return unless f
|
823
|
+
end
|
824
|
+
def view_article
|
825
|
+
title = get_current_title
|
826
|
+
return unless title
|
827
|
+
open_file title, 4
|
828
|
+
end
|
829
|
+
def view_comments
|
830
|
+
title = get_current_title
|
831
|
+
return unless title
|
832
|
+
open_file title, 5
|
833
|
+
end
|
834
|
+
def get_current_title
|
835
|
+
title = $view[$cursor] if $view[$cursor]
|
836
|
+
end
|
837
|
+
def get_current_rowdata
|
838
|
+
f = get_current_title
|
839
|
+
location = $files.index(f)
|
840
|
+
fullrow = $full_data[location]
|
841
|
+
end
|
842
|
+
|
843
|
+
## run command on given file/s
|
844
|
+
# Accepts command from user
|
845
|
+
# After putting readline in place of gets, pressing a C-c has a delayed effect. It goes intot
|
846
|
+
# exception bloack after executing other commands and still does not do the return !
|
847
|
+
def run_command f
|
848
|
+
return
|
849
|
+
end
|
850
|
+
|
851
|
+
## cd to a dir
|
852
|
+
def change_dir f, pos=nil
|
853
|
+
end
|
854
|
+
|
855
|
+
## clear sort order and refresh listing, used typically if you are in some view
|
856
|
+
# such as visited dirs or files
|
857
|
+
def escape
|
858
|
+
$sorto = nil
|
859
|
+
$viewctr = 0
|
860
|
+
$title = nil
|
861
|
+
#$filterstr = "M"
|
862
|
+
#visual_block_clear
|
863
|
+
refresh
|
864
|
+
end
|
865
|
+
|
866
|
+
## refresh listing after some change like option change, or toggle
|
867
|
+
def refresh
|
868
|
+
get_data
|
869
|
+
#$filterstr ||= "M"
|
870
|
+
$patt=nil
|
871
|
+
$title = nil
|
872
|
+
end
|
873
|
+
#
|
874
|
+
## unselect all files
|
875
|
+
def unselect_all
|
876
|
+
return unless $selection_allowed
|
877
|
+
$selected_files = []
|
878
|
+
$visual_mode = nil
|
879
|
+
end
|
880
|
+
|
881
|
+
## select all files
|
882
|
+
def select_all
|
883
|
+
return unless $selection_allowed
|
884
|
+
$selected_files = $view.dup
|
885
|
+
end
|
886
|
+
|
887
|
+
## accept dir to goto and change to that ( can be a file too)
|
888
|
+
def goto_dir
|
889
|
+
return
|
890
|
+
end
|
891
|
+
|
892
|
+
## toggle mode to selection or not
|
893
|
+
# In selection, pressed hotkey selects a file without opening, one can keep selecting
|
894
|
+
# (or deselecting).
|
895
|
+
#
|
896
|
+
def selection_mode_toggle
|
897
|
+
return unless $selection_allowed
|
898
|
+
if $mode == 'SEL'
|
899
|
+
# we seem to be coming out of select mode with some files
|
900
|
+
if $selected_files.size > 0
|
901
|
+
run_command $selected_files
|
902
|
+
end
|
903
|
+
$mode = nil
|
904
|
+
else
|
905
|
+
#$selection_mode = !$selection_mode
|
906
|
+
$mode = 'SEL'
|
907
|
+
end
|
908
|
+
end
|
909
|
+
## toggle command mode
|
910
|
+
def command_mode
|
911
|
+
if $mode == 'COM'
|
912
|
+
$mode = nil
|
913
|
+
return
|
914
|
+
end
|
915
|
+
$mode = 'COM'
|
916
|
+
end
|
917
|
+
def goto_parent_dir
|
918
|
+
#change_dir ".."
|
919
|
+
end
|
920
|
+
## This actually filters, in zfm it goes to that entry since we have a cursor there
|
921
|
+
#
|
922
|
+
def goto_entry_starting_with fc=nil
|
923
|
+
unless fc
|
924
|
+
print "Entries starting with: "
|
925
|
+
fc = get_char
|
926
|
+
end
|
927
|
+
return if fc.size != 1
|
928
|
+
## this is wrong and duplicates the functionality of /
|
929
|
+
# It shoud go to cursor of item starting with fc
|
930
|
+
$patt = "^#{fc}"
|
931
|
+
end
|
932
|
+
def goto_bookmark ch=nil
|
933
|
+
perror "No bookmarks" if $bookmarks.empty?
|
934
|
+
unless ch
|
935
|
+
print "Enter bookmark char: "
|
936
|
+
ch = get_char
|
937
|
+
end
|
938
|
+
if ch =~ /^[0-9a-z]$/
|
939
|
+
forum = $bookmarks[ch.to_sym]
|
940
|
+
display_forum forum if forum
|
941
|
+
end
|
942
|
+
end
|
943
|
+
|
944
|
+
|
945
|
+
## take regex from user, to run on files on screen, user can filter file names
|
946
|
+
def enter_regex
|
947
|
+
print "Enter (regex) pattern: "
|
948
|
+
#$patt = gets().chomp
|
949
|
+
$patt = Readline::readline('>', true)
|
950
|
+
ctr = 0
|
951
|
+
end
|
952
|
+
# Ask for a yml file name to display
|
953
|
+
# NOT USED replaced by choose_filename
|
954
|
+
def accept_file_name # {{{
|
955
|
+
print "Enter filename: "
|
956
|
+
values = Dir.glob("*.tsv")
|
957
|
+
values.each do |e|
|
958
|
+
unless Readline::HISTORY.include? e
|
959
|
+
Readline::HISTORY.push(e)
|
960
|
+
end
|
961
|
+
end
|
962
|
+
filename = Readline::readline('>', true)
|
963
|
+
if File.extname(filename) == ""
|
964
|
+
# if no extension, add tsv
|
965
|
+
filename << ".tsv"
|
966
|
+
end
|
967
|
+
Readline::HISTORY.pop if Readline::HISTORY.include? filename
|
968
|
+
# we still get dupes when user selects from history
|
969
|
+
return filename
|
970
|
+
end # }}}
|
971
|
+
# returns a forum name (that has a file in the cache).
|
972
|
+
# returned file name not forum name
|
973
|
+
def choose_filename
|
974
|
+
forums = get_cached_forums
|
975
|
+
ch, forum = menu "Select a file", forums
|
976
|
+
return forum_to_file(forum)
|
977
|
+
end
|
978
|
+
# TODO we can c=>ose these in the ctrlp fashion so i don't have to worry about first letter
|
979
|
+
def choose_forum_old
|
980
|
+
values = get_forum_list
|
981
|
+
ch, menu_text = menu "Choose forum: ", values
|
982
|
+
forum = menu_text
|
983
|
+
display_forum forum if forum
|
984
|
+
end
|
985
|
+
# returns list of forums both cached and in forumlist
|
986
|
+
def get_forum_list
|
987
|
+
# this assumes they are on the same dir
|
988
|
+
forums = get_cached_forums
|
989
|
+
f = $forumlist
|
990
|
+
forums.push(*f)
|
991
|
+
forums = forums.uniq
|
992
|
+
return forums
|
993
|
+
end
|
994
|
+
# if cached, fetch data from cache, else fetch from internet
|
995
|
+
def display_forum forum=nil
|
996
|
+
forum = choose_forum unless forum
|
997
|
+
return unless forum
|
998
|
+
$mode = nil
|
999
|
+
fn = data_cached_for forum
|
1000
|
+
if fn
|
1001
|
+
change_file fn
|
1002
|
+
else
|
1003
|
+
fetch_data_from_net forum
|
1004
|
+
end
|
1005
|
+
end
|
1006
|
+
# returns a list of cached forums with slashes
|
1007
|
+
# TODO need to take care of the actual location
|
1008
|
+
def get_cached_forums
|
1009
|
+
#values = Dir.glob("*.yml").map{|a| a.sub(".yml","").gsub("__","/"); }
|
1010
|
+
cp = File.expand_path $cache_path
|
1011
|
+
values = Dir.entries(cp).grep(/\.yml$/).map{|a| a.sub(".yml","").gsub("__","/"); }
|
1012
|
+
end
|
1013
|
+
def next_forum
|
1014
|
+
f = get_cached_forums.sort
|
1015
|
+
curr = f.index($subforum)
|
1016
|
+
log "no index for #{$subforum} " unless curr
|
1017
|
+
curr ||= 0
|
1018
|
+
fo = curr == f.size-1 ? f.first : f[curr + 1]
|
1019
|
+
display_forum fo
|
1020
|
+
|
1021
|
+
end
|
1022
|
+
def previous_forum
|
1023
|
+
f = get_cached_forums.sort
|
1024
|
+
curr = f.index($subforum)
|
1025
|
+
log "previous_forum: no index for #{$subforum} " unless curr
|
1026
|
+
curr ||= f.size
|
1027
|
+
fo = curr == 0 ? f.last : f[curr - 1]
|
1028
|
+
display_forum fo
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
def add_to_forum_list f=nil
|
1032
|
+
unless f
|
1033
|
+
print "Add to forumlist: "
|
1034
|
+
f = Readline::readline('>', false)
|
1035
|
+
return if f.nil? or f == ""
|
1036
|
+
end
|
1037
|
+
$forumlist << f unless $forumlist.include? f
|
1038
|
+
log "Added #{f} to forumlist"
|
1039
|
+
end
|
1040
|
+
def delete_current_forum
|
1041
|
+
remove_from_forum_list $subforum
|
1042
|
+
end
|
1043
|
+
# remove from the forumlist, however if you already have yml files on disk, then they keep getting
|
1044
|
+
# read up.
|
1045
|
+
def remove_from_forum_list f=nil
|
1046
|
+
unless f
|
1047
|
+
print "Remove forum from list: "
|
1048
|
+
oldhist = Readline::HISTORY
|
1049
|
+
Readline::HISTORY.clear
|
1050
|
+
Readline::HISTORY.push(*$forumlist)
|
1051
|
+
f = Readline::readline('>', false)
|
1052
|
+
Readline::HISTORY.clear
|
1053
|
+
Readline::HISTORY.push(*oldhist)
|
1054
|
+
if f.nil? or f == ""
|
1055
|
+
return
|
1056
|
+
end
|
1057
|
+
end
|
1058
|
+
$forumlist.delete(f)
|
1059
|
+
log "Removed #{f} from forumlist"
|
1060
|
+
fn = data_cached_for f
|
1061
|
+
if fn
|
1062
|
+
system("mv #{fn} #{fn}.old")
|
1063
|
+
end
|
1064
|
+
perror "Removed #{f} from forum list and disk. Press a key."
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
# display forum list in ctrlp fashion, selection by ENTER or cancel with ESCAPE C-c
|
1068
|
+
# returns selected forum (not filename)
|
1069
|
+
def choose_forum
|
1070
|
+
values = get_forum_list
|
1071
|
+
sel = ctrlp values
|
1072
|
+
#return unless sel
|
1073
|
+
#display_forum sel
|
1074
|
+
return sel
|
1075
|
+
end
|
1076
|
+
def data_cached_for filename
|
1077
|
+
f = "#{filename}.yml".gsub("/","__")
|
1078
|
+
f = "#{$cache_path}/#{f}"
|
1079
|
+
return f if File.exists? f
|
1080
|
+
perror "No data cached for #{f} "
|
1081
|
+
return nil
|
1082
|
+
end
|
1083
|
+
# get data for a cached file
|
1084
|
+
def change_file file=nil
|
1085
|
+
if file.nil?
|
1086
|
+
#fn = accept_file_name
|
1087
|
+
file = choose_filename
|
1088
|
+
return if file == ""
|
1089
|
+
end
|
1090
|
+
check_file_age file
|
1091
|
+
get_data file
|
1092
|
+
$filename = file
|
1093
|
+
post_cd
|
1094
|
+
end
|
1095
|
+
def compute_file_age fn
|
1096
|
+
s = File.stat(fn.chomp)
|
1097
|
+
mtime = s.mtime
|
1098
|
+
now = Time.now
|
1099
|
+
age = (now - mtime).to_i
|
1100
|
+
return (age/3600).to_i
|
1101
|
+
end
|
1102
|
+
def check_file_age fn
|
1103
|
+
age = compute_file_age fn
|
1104
|
+
log "#{fn} is #{age} hours old"
|
1105
|
+
if age > 24
|
1106
|
+
pbold "#{fn} is very old ( #{age} hours). Please refresh"
|
1107
|
+
elsif age > 8
|
1108
|
+
pbold "#{fn} is quite old ( #{age} hours). Please refresh"
|
1109
|
+
elsif age > 1
|
1110
|
+
# display it somewhere
|
1111
|
+
end
|
1112
|
+
$hint_message = get_hint
|
1113
|
+
if age > 1
|
1114
|
+
$hint_message = "#{ON_RED}#{age} hrs old. `r to refresh.#{CLEAR}"
|
1115
|
+
end
|
1116
|
+
return age
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
def next_page
|
1120
|
+
$sta += $pagesize
|
1121
|
+
end
|
1122
|
+
def prev_page
|
1123
|
+
$sta -= $pagesize
|
1124
|
+
end
|
1125
|
+
def print_help
|
1126
|
+
system("clear")
|
1127
|
+
puts "HELP"
|
1128
|
+
puts
|
1129
|
+
puts "To act on a title press 1-9 a-z A-Z "
|
1130
|
+
#puts "Command Mode: Will prompt for a command to run on a file, after selecting using hotkey"
|
1131
|
+
#puts "Selection Mode: Each selection adds to selection list (toggles)"
|
1132
|
+
#puts " Upon exiting mode, user is prompted for a command to run on selected files"
|
1133
|
+
puts
|
1134
|
+
ary = []
|
1135
|
+
$bindings.each_pair { |k, v| ary.push "#{k.ljust(7)} => #{v}" }
|
1136
|
+
ary = columnate ary, $grows - 7
|
1137
|
+
ary.each {|line| print line, "\n" }
|
1138
|
+
get_char
|
1139
|
+
|
1140
|
+
end
|
1141
|
+
def show_marks
|
1142
|
+
puts
|
1143
|
+
puts "Bookmarks: "
|
1144
|
+
$bookmarks.each_pair { |k, v| puts "#{k.to_s.ljust(7)} => #{v}" }
|
1145
|
+
puts
|
1146
|
+
print "Enter bookmark to goto: "
|
1147
|
+
ch = get_char
|
1148
|
+
goto_bookmark(ch) if ch =~ /^[0-9a-z]$/
|
1149
|
+
end
|
1150
|
+
# MENU MAIN -- keep consistent with zfm
|
1151
|
+
def main_menu
|
1152
|
+
h = {
|
1153
|
+
"1" => :view_article,
|
1154
|
+
"2" => :view_comments,
|
1155
|
+
:f => :display_forum,
|
1156
|
+
:v => :view_menu,
|
1157
|
+
:r => :reload,
|
1158
|
+
:m => :fetch_more,
|
1159
|
+
:R => :reddit_options,
|
1160
|
+
:H => :hacker_options,
|
1161
|
+
:s => :sort_menu,
|
1162
|
+
:C => :config_menu,
|
1163
|
+
:a => :view_article,
|
1164
|
+
:c => :view_comments,
|
1165
|
+
:x => :extras
|
1166
|
+
}
|
1167
|
+
=begin
|
1168
|
+
:a => :ack,
|
1169
|
+
"/" => :ffind,
|
1170
|
+
:l => :locate,
|
1171
|
+
:v => :viminfo,
|
1172
|
+
:z => :z_interface,
|
1173
|
+
:d => :child_dirs,
|
1174
|
+
:r => :recent_files,
|
1175
|
+
:t => :dirtree,
|
1176
|
+
"4" => :tree,
|
1177
|
+
:F => :filter_menu,
|
1178
|
+
:c => :command_menu ,
|
1179
|
+
:B => :bindkey_ext_command,
|
1180
|
+
:M => :newdir,
|
1181
|
+
"%" => :newfile,
|
1182
|
+
=end
|
1183
|
+
|
1184
|
+
menu "Main Menu", h
|
1185
|
+
end
|
1186
|
+
def hacker_options menu_text=nil
|
1187
|
+
h = {
|
1188
|
+
:n => :news,
|
1189
|
+
:w => :newest,
|
1190
|
+
:s => :show,
|
1191
|
+
:j => :jobs,
|
1192
|
+
:a => :ask
|
1193
|
+
}
|
1194
|
+
# TODO ask article needs host name prepended
|
1195
|
+
# TODO jobs has no comments, check if nil
|
1196
|
+
unless menu_text
|
1197
|
+
ch, menu_text = menu "Hacker Options", h
|
1198
|
+
end
|
1199
|
+
if menu_text
|
1200
|
+
m = menu_text.to_s
|
1201
|
+
fetch_data_from_net m
|
1202
|
+
end
|
1203
|
+
end
|
1204
|
+
def reddit_options menu_text=nil
|
1205
|
+
if $hacker_forums.include? $subforum
|
1206
|
+
perror "Reddit options invalid inside Hacker News subforum"
|
1207
|
+
return
|
1208
|
+
end
|
1209
|
+
h = {
|
1210
|
+
:n => :new,
|
1211
|
+
:r => :rising,
|
1212
|
+
:c => :controversial,
|
1213
|
+
:t => :top,
|
1214
|
+
:h => :hot
|
1215
|
+
}
|
1216
|
+
unless menu_text
|
1217
|
+
ch, menu_text = menu "Reddit Options for #{$subforum} ", h
|
1218
|
+
end
|
1219
|
+
if menu_text
|
1220
|
+
if menu_text == :hot
|
1221
|
+
fetch_data_from_net "#{$subforum}"
|
1222
|
+
else
|
1223
|
+
m = menu_text.to_s
|
1224
|
+
s = "#{$subforum}".sub(/\/.*/, '')
|
1225
|
+
fetch_data_from_net "#{s}/#{m}"
|
1226
|
+
end
|
1227
|
+
end
|
1228
|
+
end
|
1229
|
+
def config_menu
|
1230
|
+
h = {
|
1231
|
+
:i => :increase_number_of_pages,
|
1232
|
+
:b => :set_browsers,
|
1233
|
+
:o => :open_command
|
1234
|
+
}
|
1235
|
+
ch, menu_text = menu "Configuration Menu", h
|
1236
|
+
case menu_text
|
1237
|
+
when :open_command
|
1238
|
+
print "Enter open command: "
|
1239
|
+
str = Readline::readline('>', true)
|
1240
|
+
if str && str != ""
|
1241
|
+
$open_command = str
|
1242
|
+
end
|
1243
|
+
when :increase_number_of_pages
|
1244
|
+
$num_pages += 1
|
1245
|
+
end
|
1246
|
+
end
|
1247
|
+
def view_menu
|
1248
|
+
h = {
|
1249
|
+
:a => :view_article,
|
1250
|
+
:c => :view_comments,
|
1251
|
+
:l => :view_log
|
1252
|
+
}
|
1253
|
+
menu "View Menu", h
|
1254
|
+
end
|
1255
|
+
def set_browsers
|
1256
|
+
s = gets_with_values "Enter gui browser", ["open"]
|
1257
|
+
$browser_gui = s if s
|
1258
|
+
s = gets_with_values "Enter text browser", %w[ elinks links w3m lynx ]
|
1259
|
+
$browser_text = s if s
|
1260
|
+
end
|
1261
|
+
# dispay a menu with numbering, allowing selection by pressing index
|
1262
|
+
# Take a char, so use alphabets for index, if items more than 9
|
1263
|
+
def menu title, h
|
1264
|
+
case h
|
1265
|
+
when Hash
|
1266
|
+
; # okay
|
1267
|
+
when Array
|
1268
|
+
# convert array to a hash using letters for key
|
1269
|
+
x = "a"
|
1270
|
+
hash = Hash.new
|
1271
|
+
h.each do |e|
|
1272
|
+
hash[x] = e
|
1273
|
+
x.succ!
|
1274
|
+
end
|
1275
|
+
h = hash
|
1276
|
+
end
|
1277
|
+
return unless h
|
1278
|
+
|
1279
|
+
kmax_length = h.keys.max_by(&:length).length
|
1280
|
+
vmax_length = h.values.max_by(&:length).length
|
1281
|
+
puts "#{ON_BLUE}#{title}#{CLEAR}"
|
1282
|
+
h.each_pair { |k, v|
|
1283
|
+
d = get_action_desc(v) || ""
|
1284
|
+
printf " #{BOLD}%-*s#{CLEAR} %-*s #{BOLD}%s#{CLEAR}\n", kmax_length,k, vmax_length,v, d
|
1285
|
+
}
|
1286
|
+
ch = get_char
|
1287
|
+
binding = h[ch]
|
1288
|
+
binding = h[ch.to_sym] unless binding
|
1289
|
+
if binding
|
1290
|
+
if respond_to?(binding, true)
|
1291
|
+
send(binding)
|
1292
|
+
end
|
1293
|
+
end
|
1294
|
+
return ch, binding
|
1295
|
+
end
|
1296
|
+
# todo : use hidden to hide old entries like older than a day or n hours
|
1297
|
+
def toggle_menu menu_text=nil
|
1298
|
+
unless menu_text
|
1299
|
+
h = {
|
1300
|
+
# :h => :toggle_hidden,
|
1301
|
+
:c => :toggle_case, :l => :toggle_long_list , "1" => :toggle_columns,
|
1302
|
+
:g => :use_gui_browser, :t => :use_text_browser}
|
1303
|
+
ch, menu_text = menu "Toggle Menu", h
|
1304
|
+
end
|
1305
|
+
case menu_text
|
1306
|
+
when :toggle_hidden
|
1307
|
+
$hidden = $hidden ? nil : "D"
|
1308
|
+
refresh
|
1309
|
+
when :toggle_case
|
1310
|
+
#$ignorecase = $ignorecase ? "" : "i"
|
1311
|
+
$ignorecase = !$ignorecase
|
1312
|
+
refresh
|
1313
|
+
when :toggle_columns
|
1314
|
+
$gviscols = 3 if $gviscols == 1
|
1315
|
+
$long_listing = false if $gviscols > 1
|
1316
|
+
x = $grows * $gviscols
|
1317
|
+
$pagesize = $pagesize==x ? $grows : x
|
1318
|
+
when :use_gui_browser
|
1319
|
+
$open_command = $browser_gui || "open"
|
1320
|
+
when :use_text_browser
|
1321
|
+
$open_command = $browser_text || "elinks"
|
1322
|
+
|
1323
|
+
when :toggle_long_list
|
1324
|
+
$long_listing = !$long_listing
|
1325
|
+
if $long_listing
|
1326
|
+
$gviscols = 1
|
1327
|
+
$pagesize = $grows
|
1328
|
+
else
|
1329
|
+
x = $grows * $gviscols
|
1330
|
+
$pagesize = $pagesize==x ? $grows : x
|
1331
|
+
end
|
1332
|
+
refresh
|
1333
|
+
end
|
1334
|
+
end
|
1335
|
+
|
1336
|
+
def column_menu row
|
1337
|
+
h = {}
|
1338
|
+
row.each_with_index do |e, i|
|
1339
|
+
h[$IDX[i]] = e
|
1340
|
+
end
|
1341
|
+
ch, menu_text = menu "Columns Menu", h
|
1342
|
+
return $IDX.index(ch)
|
1343
|
+
end
|
1344
|
+
|
1345
|
+
def sort col=nil
|
1346
|
+
col = col.to_i if col
|
1347
|
+
col = $column_enum.next unless col
|
1348
|
+
$full_data = $full_data.sort_by{|x| -x[col].to_i}
|
1349
|
+
$files = []
|
1350
|
+
$full_data.each do |e|
|
1351
|
+
$files << e.first
|
1352
|
+
end
|
1353
|
+
return
|
1354
|
+
end
|
1355
|
+
def sort_age ; sort_menu :age; end
|
1356
|
+
def sort_points ; sort_menu :points; end
|
1357
|
+
def sort_comments ; sort_menu :comments; end
|
1358
|
+
def sort_menu menu_text = nil
|
1359
|
+
# next line is so user can enter on subcommand
|
1360
|
+
menu_text = menu_text.to_sym if menu_text.is_a? String
|
1361
|
+
|
1362
|
+
lo = nil
|
1363
|
+
unless menu_text
|
1364
|
+
h = { :a => :age, :c => :comments, :p => :points }
|
1365
|
+
ch, menu_text = menu "Sort Menu", h
|
1366
|
+
end
|
1367
|
+
menu_text = ch unless menu_text
|
1368
|
+
case menu_text
|
1369
|
+
when :age, "1"
|
1370
|
+
sort 6
|
1371
|
+
when :comments, "2"
|
1372
|
+
sort 2
|
1373
|
+
when :points, "3"
|
1374
|
+
sort 3
|
1375
|
+
end
|
1376
|
+
$sorto = menu_text
|
1377
|
+
end
|
1378
|
+
alias :sort_on :sort_menu
|
1379
|
+
|
1380
|
+
def command_menu
|
1381
|
+
##
|
1382
|
+
# since these involve full paths, we need more space, like only one column
|
1383
|
+
#
|
1384
|
+
## in these cases, getting back to the earlier dir, back to earlier listing
|
1385
|
+
# since we've basically overlaid the old listing
|
1386
|
+
#
|
1387
|
+
# should be able to sort THIS listing and not rerun command. But for that I'd need to use
|
1388
|
+
# xargs ls -t etc rather than the zsh sort order. But we can run a filter using |.
|
1389
|
+
#
|
1390
|
+
h = { :t => :today, :D => :default_command , :R => :remove_from_list}
|
1391
|
+
if $editor_mode
|
1392
|
+
h[:e] = :pager_mode
|
1393
|
+
else
|
1394
|
+
h[:e] = :editor_mode
|
1395
|
+
end
|
1396
|
+
ch, menu_text = menu "Command Menu", h
|
1397
|
+
case menu_text
|
1398
|
+
when :pager_mode
|
1399
|
+
$editor_mode = false
|
1400
|
+
$default_command = ENV['MANPAGER'] || ENV['PAGER']
|
1401
|
+
when :editor_mode
|
1402
|
+
$editor_mode = true
|
1403
|
+
$default_command = nil
|
1404
|
+
when :ffind
|
1405
|
+
ffind
|
1406
|
+
when :locate
|
1407
|
+
locate
|
1408
|
+
when :today
|
1409
|
+
$files = `zsh -c 'print -rl -- *(#{$hidden}Mm0)'`.split("\n")
|
1410
|
+
$title = "Today's files"
|
1411
|
+
when :default_command
|
1412
|
+
print "Selecting a file usually invokes $EDITOR, what command do you want to use repeatedly on selected files: "
|
1413
|
+
$default_command = gets().chomp
|
1414
|
+
if $default_command != ""
|
1415
|
+
print "Second part of command (maybe blank): "
|
1416
|
+
$default_command2 = gets().chomp
|
1417
|
+
else
|
1418
|
+
print "Cleared default command, will default to $EDITOR"
|
1419
|
+
$default_command2 = nil
|
1420
|
+
$default_command = nil
|
1421
|
+
end
|
1422
|
+
end
|
1423
|
+
end
|
1424
|
+
def extras
|
1425
|
+
h = { "1" => :one_column, "2" => :multi_column, :c => :columns, :r => :config_read , :w => :config_write}
|
1426
|
+
ch, menu_text = menu "Extras Menu", h
|
1427
|
+
case menu_text
|
1428
|
+
when :one_column
|
1429
|
+
$pagesize = $grows
|
1430
|
+
when :multi_column
|
1431
|
+
#$pagesize = 60
|
1432
|
+
$pagesize = $grows * $gviscols
|
1433
|
+
when :columns
|
1434
|
+
print "How many columns to show: 1-6 [current #{$gviscols}]? "
|
1435
|
+
ch = get_char
|
1436
|
+
ch = ch.to_i
|
1437
|
+
if ch > 0 && ch < 7
|
1438
|
+
$gviscols = ch.to_i
|
1439
|
+
$pagesize = $grows * $gviscols
|
1440
|
+
end
|
1441
|
+
end
|
1442
|
+
end
|
1443
|
+
def select_used_dirs
|
1444
|
+
return
|
1445
|
+
$title = "Used Directories"
|
1446
|
+
$files = $used_dirs.uniq
|
1447
|
+
end
|
1448
|
+
def select_visited_files
|
1449
|
+
return
|
1450
|
+
# not yet a unique list, needs to be unique and have latest pushed to top
|
1451
|
+
$title = "Visited Files"
|
1452
|
+
$files = $visited_files.uniq
|
1453
|
+
end
|
1454
|
+
# TODO
|
1455
|
+
# This shows bookmarks in article view but the marks are the hints not the keys themselves
|
1456
|
+
def select_bookmarks
|
1457
|
+
# added changes in both select_current and select_hint
|
1458
|
+
# However, the age mark that is show is still for the earlier shown forum based on outdated full_data
|
1459
|
+
# So we need to show age mark based on file datw which means a change in display program !!! At least
|
1460
|
+
# clear the array full_data
|
1461
|
+
$mode = "forum"
|
1462
|
+
$title = "Bookmarks"
|
1463
|
+
$files = $bookmarks.values
|
1464
|
+
|
1465
|
+
end
|
1466
|
+
|
1467
|
+
## part copied and changed from change_dir since we don't dir going back on top
|
1468
|
+
# or we'll be stuck in a cycle
|
1469
|
+
def pop_dir
|
1470
|
+
return # 2014-07-25 - 22:43
|
1471
|
+
# the first time we pop, we need to put the current on stack
|
1472
|
+
if !$visited_dirs.index(Dir.pwd)
|
1473
|
+
$visited_dirs.push Dir.pwd
|
1474
|
+
end
|
1475
|
+
## XXX make sure thre is something to pop
|
1476
|
+
d = $visited_dirs.delete_at 0
|
1477
|
+
## XXX make sure the dir exists, cuold have been deleted. can be an error or crash otherwise
|
1478
|
+
$visited_dirs.push d
|
1479
|
+
Dir.chdir d
|
1480
|
+
$filterstr ||= "M"
|
1481
|
+
$files = `zsh -c 'print -rl -- *(#{$sorto}#{$hidden}#{$filterstr})'`.split("\n")
|
1482
|
+
post_cd
|
1483
|
+
end
|
1484
|
+
def post_cd
|
1485
|
+
$patt=nil
|
1486
|
+
$sta = 0
|
1487
|
+
$cursor = $min_index
|
1488
|
+
$title = nil
|
1489
|
+
#if $selected_files.size > 0
|
1490
|
+
#$selected_files = []
|
1491
|
+
#end
|
1492
|
+
$visual_block_start = nil
|
1493
|
+
$stact = 0
|
1494
|
+
screen_settings
|
1495
|
+
|
1496
|
+
revert_dir_pos
|
1497
|
+
end
|
1498
|
+
#
|
1499
|
+
## read dirs and files and bookmarks from file
|
1500
|
+
def config_read
|
1501
|
+
f = File.expand_path(CONFIG_FILE)
|
1502
|
+
if File.readable? f
|
1503
|
+
load f
|
1504
|
+
# maybe we should check for these existing else crash will happen.
|
1505
|
+
#$bookmarks.push(*bookmarks) if bookmarks
|
1506
|
+
log "loaded #{CONFIG_FILE} "
|
1507
|
+
end
|
1508
|
+
end
|
1509
|
+
def get_env_paths
|
1510
|
+
files = []
|
1511
|
+
%w{ GEM_HOME PYTHONHOME}.each do |p|
|
1512
|
+
d = ENV[p]
|
1513
|
+
files.push d if d
|
1514
|
+
end
|
1515
|
+
%w{ RUBYLIB RUBYPATH GEM_PATH PYTHONPATH }.each do |p|
|
1516
|
+
d = ENV[p]
|
1517
|
+
files.concat d.split(":") if d
|
1518
|
+
end
|
1519
|
+
return files
|
1520
|
+
end
|
1521
|
+
|
1522
|
+
## save dirs and files and bookmarks to a file
|
1523
|
+
def config_write
|
1524
|
+
return
|
1525
|
+
# Putting it in a format that zfm can also read and write
|
1526
|
+
#f1 = File.expand_path("~/.zfminfo")
|
1527
|
+
f1 = File.expand_path(CONFIG_FILE)
|
1528
|
+
d = $used_dirs.join ":"
|
1529
|
+
f = $visited_files.join ":"
|
1530
|
+
File.open(f1, 'w+') do |f2|
|
1531
|
+
# use "\n" for two lines of text
|
1532
|
+
f2.puts "DIRS=\"#{d}\""
|
1533
|
+
f2.puts "FILES=\"#{f}\""
|
1534
|
+
$bookmarks.each_pair { |k, val|
|
1535
|
+
f2.puts "BM_#{k}=\"#{val}\""
|
1536
|
+
#f2.puts "BOOKMARKS[\"#{k}\"]=\"#{val}\""
|
1537
|
+
}
|
1538
|
+
end
|
1539
|
+
$writing = $modified = false
|
1540
|
+
end
|
1541
|
+
|
1542
|
+
## accept a character to save this dir as a bookmark
|
1543
|
+
def create_bookmark
|
1544
|
+
print "Enter a to z or 0-9 for bookmark: "
|
1545
|
+
ch = get_char
|
1546
|
+
if ch =~ /^[0-9a-z]$/
|
1547
|
+
$bookmarks[ch.sym] = "#{$subforum}"
|
1548
|
+
$modified = true
|
1549
|
+
else
|
1550
|
+
perror "Bookmark must be lower-case character or number."
|
1551
|
+
end
|
1552
|
+
end
|
1553
|
+
# currently just send to file-actions till we think of better stuff
|
1554
|
+
def register_command cmd
|
1555
|
+
$commandlist << cmd
|
1556
|
+
end
|
1557
|
+
def gets_with_history prompt, histkey=nil
|
1558
|
+
histkey ||= prompt
|
1559
|
+
print "#{prompt}: "
|
1560
|
+
oldhist = Readline::HISTORY
|
1561
|
+
Readline::HISTORY.clear
|
1562
|
+
values = $history[:histkey]
|
1563
|
+
Readline::HISTORY.push(*values)
|
1564
|
+
command = Readline::readline('>', false)
|
1565
|
+
Readline::HISTORY.clear
|
1566
|
+
Readline::HISTORY.push(*oldhist)
|
1567
|
+
if command and command != ""
|
1568
|
+
values << command unless values.include? command
|
1569
|
+
$history[histkey] = values
|
1570
|
+
return command
|
1571
|
+
end
|
1572
|
+
return false
|
1573
|
+
end
|
1574
|
+
def gets_with_values prompt, values
|
1575
|
+
print "#{prompt}: "
|
1576
|
+
oldhist = Readline::HISTORY
|
1577
|
+
Readline::HISTORY.clear
|
1578
|
+
Readline::HISTORY.push(*values)
|
1579
|
+
command = Readline::readline('>', false)
|
1580
|
+
Readline::HISTORY.clear
|
1581
|
+
Readline::HISTORY.push(*oldhist)
|
1582
|
+
if command and command != ""
|
1583
|
+
return command
|
1584
|
+
end
|
1585
|
+
return nil
|
1586
|
+
end
|
1587
|
+
def subcommand
|
1588
|
+
print "Enter command: "
|
1589
|
+
oldhist = Readline::HISTORY
|
1590
|
+
Readline::HISTORY.clear
|
1591
|
+
Readline::HISTORY.push(*$commandlist)
|
1592
|
+
command = Readline::readline(':', false)
|
1593
|
+
Readline::HISTORY.push(*oldhist)
|
1594
|
+
return if command.nil? or command == ""
|
1595
|
+
command = "fetch_data_from_net #{$subforum}" if command == "r" #or command == "refresh"
|
1596
|
+
command = "next_forum" if command == "n"
|
1597
|
+
command = "previous_forum" if command == "p"
|
1598
|
+
if $commandlist.include? command
|
1599
|
+
send(command)
|
1600
|
+
end
|
1601
|
+
if command =~ /^\d+$/
|
1602
|
+
goto_line command
|
1603
|
+
end
|
1604
|
+
exec_string(command) if command.index(" ")
|
1605
|
+
|
1606
|
+
return
|
1607
|
+
print "Enter command: "
|
1608
|
+
begin
|
1609
|
+
#command = gets().chomp
|
1610
|
+
command = Readline::readline('>', true)
|
1611
|
+
return if command == ""
|
1612
|
+
rescue Exception => ex
|
1613
|
+
return
|
1614
|
+
end
|
1615
|
+
if command == "q"
|
1616
|
+
if $modified
|
1617
|
+
print "Do you want to save bookmarks? (y/n): "
|
1618
|
+
ch = get_char
|
1619
|
+
if ch == "y"
|
1620
|
+
$writing = true
|
1621
|
+
$quitting = true
|
1622
|
+
elsif ch == "n"
|
1623
|
+
$quitting = true
|
1624
|
+
print "Quitting without saving bookmarks"
|
1625
|
+
else
|
1626
|
+
perror "No action taken."
|
1627
|
+
end
|
1628
|
+
else
|
1629
|
+
$quitting = true
|
1630
|
+
end
|
1631
|
+
elsif command == "wq"
|
1632
|
+
$quitting = true
|
1633
|
+
$writing = true
|
1634
|
+
elsif command == "x"
|
1635
|
+
$quitting = true
|
1636
|
+
$writing = true if $modified
|
1637
|
+
elsif command == "p"
|
1638
|
+
system "echo $PWD | pbcopy"
|
1639
|
+
puts "Stored PWD in clipboard (using pbcopy)"
|
1640
|
+
end
|
1641
|
+
end
|
1642
|
+
def quit_command
|
1643
|
+
if $modified
|
1644
|
+
puts "Press w to save bookmarks before quitting " if $modified
|
1645
|
+
print "Press another q to quit "
|
1646
|
+
ch = get_char
|
1647
|
+
else
|
1648
|
+
$quitting = true
|
1649
|
+
end
|
1650
|
+
$quitting = true if ch == "q"
|
1651
|
+
$quitting = $writing = true if ch == "w"
|
1652
|
+
end
|
1653
|
+
|
1654
|
+
def views
|
1655
|
+
# switch between long list, single row and multi row
|
1656
|
+
views=[:toggle_columns , :toggle_long_list, :sort_age, :sort_comments, :sort_points]
|
1657
|
+
viewlabels=%w[Dirs Newest Accessed Oldest Largest Smallest Reverse Name]
|
1658
|
+
$sorto = nil
|
1659
|
+
#$title = viewlabels[$viewctr]
|
1660
|
+
$viewctr += 1
|
1661
|
+
$viewctr = 0 if $viewctr > views.size
|
1662
|
+
option = views[$viewctr] || views.first
|
1663
|
+
case option
|
1664
|
+
when :toggle_columns, :toggle_long_list
|
1665
|
+
toggle_menu option
|
1666
|
+
else
|
1667
|
+
send(option)
|
1668
|
+
end
|
1669
|
+
|
1670
|
+
return # 2014-07-25 - 22:43
|
1671
|
+
|
1672
|
+
end
|
1673
|
+
# on pressing ENTER key
|
1674
|
+
def select_current
|
1675
|
+
## vp is local there, so i can do $vp[0]
|
1676
|
+
#open_file $view[$sta] if $view[$sta]
|
1677
|
+
if $mode == "forum"
|
1678
|
+
display_forum $view[$cursor]
|
1679
|
+
else
|
1680
|
+
on_enter get_current_title
|
1681
|
+
end
|
1682
|
+
end
|
1683
|
+
def on_enter title
|
1684
|
+
fullrow = get_full_row(title)
|
1685
|
+
puts "#{BOLD}#{title}#{CLEAR}"
|
1686
|
+
puts "#{fullrow[-1]}"
|
1687
|
+
article = fullrow[4]
|
1688
|
+
comments = fullrow[5]
|
1689
|
+
puts " "
|
1690
|
+
puts "a ) article #{article}"
|
1691
|
+
puts "c ) comments #{comments}"
|
1692
|
+
print "> "
|
1693
|
+
ch = get_char
|
1694
|
+
if ch == "a"
|
1695
|
+
system "#{$open_command} #{article}"
|
1696
|
+
elsif ch == "c"
|
1697
|
+
system "#{$open_command} #{comments}"
|
1698
|
+
end
|
1699
|
+
end
|
1700
|
+
|
1701
|
+
## create a list of dirs in which some action has happened, for saving
|
1702
|
+
def push_used_dirs d=Dir.pwd
|
1703
|
+
$used_dirs.index(d) || $used_dirs.push(d)
|
1704
|
+
end
|
1705
|
+
def pbold text
|
1706
|
+
puts "#{BOLD}#{text}#{BOLD_OFF}"
|
1707
|
+
end
|
1708
|
+
def perror text
|
1709
|
+
puts "#{RED}#{text}#{CLEAR}"
|
1710
|
+
get_char
|
1711
|
+
end
|
1712
|
+
def pause text=" Press a key ..."
|
1713
|
+
print text
|
1714
|
+
get_char
|
1715
|
+
end
|
1716
|
+
## return shortcut for an index (offset in file array)
|
1717
|
+
# use 2 more arrays to make this faster
|
1718
|
+
# if z or Z take another key if there are those many in view
|
1719
|
+
# Also, display ROWS * COLS so now we are not limited to 60.
|
1720
|
+
def get_shortcut ix
|
1721
|
+
return "<" if ix < $stact
|
1722
|
+
ix -= $stact
|
1723
|
+
i = $IDX[ix]
|
1724
|
+
return i if i
|
1725
|
+
return "->"
|
1726
|
+
end
|
1727
|
+
## returns the integer offset in view (file array based on a-y za-zz and Za - Zz
|
1728
|
+
# Called when user types a key
|
1729
|
+
# should we even ask for a second key if there are not enough rows
|
1730
|
+
# What if we want to also trap z with numbers for other purposes
|
1731
|
+
def get_index key, vsz=999
|
1732
|
+
i = $IDX.index(key)
|
1733
|
+
return i+$stact if i
|
1734
|
+
#sz = $IDX.size
|
1735
|
+
zch = nil
|
1736
|
+
if vsz > 25
|
1737
|
+
if key == "z" || key == "Z"
|
1738
|
+
print key
|
1739
|
+
zch = get_char
|
1740
|
+
print zch
|
1741
|
+
i = $IDX.index("#{key}#{zch}")
|
1742
|
+
return i+$stact if i
|
1743
|
+
end
|
1744
|
+
end
|
1745
|
+
return nil
|
1746
|
+
end
|
1747
|
+
|
1748
|
+
## generic external command program
|
1749
|
+
# prompt is the user friendly text of command such as list for ls, or extract for dtrx, page for less
|
1750
|
+
# pauseyn is whether to pause after command as in file or ls
|
1751
|
+
#
|
1752
|
+
def command_file prompt, *command
|
1753
|
+
pauseyn = command.shift
|
1754
|
+
command = command.join " "
|
1755
|
+
print "[#{prompt}] Choose a file [#{$view[$cursor]}]: "
|
1756
|
+
file = ask_hint $view[$cursor]
|
1757
|
+
#print "#{prompt} :: Enter file shortcut: "
|
1758
|
+
#file = ask_hint
|
1759
|
+
perror "Command Cancelled" unless file
|
1760
|
+
return unless file
|
1761
|
+
file = File.expand_path(file)
|
1762
|
+
if File.exists? file
|
1763
|
+
file = Shellwords.escape(file)
|
1764
|
+
pbold "#{command} #{file} (#{pauseyn})"
|
1765
|
+
system "#{command} #{file}"
|
1766
|
+
pause if pauseyn == "y"
|
1767
|
+
refresh
|
1768
|
+
else
|
1769
|
+
perror "File #{file} not found"
|
1770
|
+
end
|
1771
|
+
end
|
1772
|
+
|
1773
|
+
## prompt user for file shortcut and return file or nil
|
1774
|
+
#
|
1775
|
+
def ask_hint deflt=nil
|
1776
|
+
f = nil
|
1777
|
+
ch = get_char
|
1778
|
+
if ch == "ENTER"
|
1779
|
+
return deflt
|
1780
|
+
end
|
1781
|
+
ix = get_index(ch, $viewport.size)
|
1782
|
+
f = $viewport[ix] if ix
|
1783
|
+
return f
|
1784
|
+
end
|
1785
|
+
|
1786
|
+
## check screen size and accordingly adjust some variables
|
1787
|
+
#
|
1788
|
+
def screen_settings
|
1789
|
+
$glines=%x(tput lines).to_i
|
1790
|
+
$gcols=%x(tput cols).to_i
|
1791
|
+
$grows = $glines - 3
|
1792
|
+
$pagesize = 60
|
1793
|
+
#$gviscols = 3
|
1794
|
+
$pagesize = $grows * $gviscols
|
1795
|
+
end
|
1796
|
+
## moves column offset so we can reach unindexed columns or entries
|
1797
|
+
# 0 forward and any other back/prev
|
1798
|
+
def column_next dir=0
|
1799
|
+
if dir == 0
|
1800
|
+
$stact += $grows
|
1801
|
+
$stact = 0 if $stact >= $viewport.size
|
1802
|
+
else
|
1803
|
+
$stact -= $grows
|
1804
|
+
$stact = 0 if $stact < 0
|
1805
|
+
end
|
1806
|
+
end
|
1807
|
+
# currently i am only passing the action in from the list there as a key
|
1808
|
+
# I should be able to pass in new actions that are external commands
|
1809
|
+
# c-x CX cx
|
1810
|
+
def forum_actions
|
1811
|
+
h = { :r => :reload,
|
1812
|
+
:n => :new_forum,
|
1813
|
+
:b => :display_forum,
|
1814
|
+
'C-b' => :change_subforum,
|
1815
|
+
:N => :new_articles ,
|
1816
|
+
:k => :delete_current_forum,
|
1817
|
+
:RIGHT => :next_forum,
|
1818
|
+
:LEFT => :previous_forum,
|
1819
|
+
:l => :view_log,
|
1820
|
+
:C => :config_menu,
|
1821
|
+
'C-c' => :quit_app}
|
1822
|
+
|
1823
|
+
ch, menu_text = menu "Forum Menu ", h
|
1824
|
+
menu_text = :quit if ch == "q"
|
1825
|
+
return unless menu_text
|
1826
|
+
case menu_text.to_sym
|
1827
|
+
when :quit
|
1828
|
+
when :reload
|
1829
|
+
fetch_data_from_net $subforum
|
1830
|
+
when :pages
|
1831
|
+
$num_pages += 1
|
1832
|
+
when :new_articles
|
1833
|
+
new_articles
|
1834
|
+
when :new_forum
|
1835
|
+
fetch_data_from_net
|
1836
|
+
when :change_subforum
|
1837
|
+
change_file
|
1838
|
+
when :quit_app
|
1839
|
+
quit_command
|
1840
|
+
when :view_log
|
1841
|
+
view_log
|
1842
|
+
when :open_command
|
1843
|
+
print "Enter open command: "
|
1844
|
+
str = Readline::readline('>', true)
|
1845
|
+
if str && str != ""
|
1846
|
+
$open_command = str
|
1847
|
+
end
|
1848
|
+
end
|
1849
|
+
end
|
1850
|
+
def new_articles
|
1851
|
+
case $host
|
1852
|
+
when :hn
|
1853
|
+
fetch_data_from_net "newest"
|
1854
|
+
else
|
1855
|
+
fetch_data_from_net "#{$subforum}/new"
|
1856
|
+
end
|
1857
|
+
end
|
1858
|
+
|
1859
|
+
def columns_incdec howmany
|
1860
|
+
$gviscols += howmany.to_i
|
1861
|
+
$gviscols = 1 if $gviscols < 1
|
1862
|
+
$gviscols = 6 if $gviscols > 6
|
1863
|
+
$pagesize = $grows * $gviscols
|
1864
|
+
end
|
1865
|
+
|
1866
|
+
# bind a key to an external command wich can be then be used for files
|
1867
|
+
def bindkey_ext_command
|
1868
|
+
print
|
1869
|
+
pbold "Bind a capital letter to an external command"
|
1870
|
+
print "Enter a capital letter to bind: "
|
1871
|
+
ch = get_char
|
1872
|
+
return if ch == "Q"
|
1873
|
+
if ch =~ /^[A-Z]$/
|
1874
|
+
print "Enter an external command to bind to #{ch}: "
|
1875
|
+
com = gets().chomp
|
1876
|
+
if com != ""
|
1877
|
+
print "Enter prompt for command (blank if same as command): "
|
1878
|
+
pro = gets().chomp
|
1879
|
+
pro = com if pro == ""
|
1880
|
+
end
|
1881
|
+
print "Pause after output [y/n]: "
|
1882
|
+
yn = get_char
|
1883
|
+
$bindings[ch] = "command_file #{pro} #{yn} #{com}"
|
1884
|
+
end
|
1885
|
+
end
|
1886
|
+
|
1887
|
+
|
1888
|
+
## some cursor movement functions
|
1889
|
+
##
|
1890
|
+
#
|
1891
|
+
def cursor_scroll_dn
|
1892
|
+
moveto(pos() + MSCROLL)
|
1893
|
+
end
|
1894
|
+
def cursor_scroll_up
|
1895
|
+
moveto(pos() - MSCROLL)
|
1896
|
+
end
|
1897
|
+
def cursor_dn
|
1898
|
+
moveto(pos() + 1)
|
1899
|
+
end
|
1900
|
+
def cursor_up
|
1901
|
+
moveto(pos() - 1)
|
1902
|
+
end
|
1903
|
+
def pos
|
1904
|
+
$cursor
|
1905
|
+
end
|
1906
|
+
|
1907
|
+
def moveto pos
|
1908
|
+
orig = $cursor
|
1909
|
+
$cursor = pos
|
1910
|
+
#$cursor = [$cursor, $view.size - 1].min
|
1911
|
+
## 2014-07-28 - 00:45 trying out viewport instead of view
|
1912
|
+
# this prevents cursor going off when we press down arrow on last row esp on multi-page listings
|
1913
|
+
# How does that impact the rest of this method, such as visual_mode
|
1914
|
+
$cursor = [$cursor, $viewport.size - 1].min
|
1915
|
+
$cursor = [$cursor, $min_index].max
|
1916
|
+
star = [orig, $cursor].min
|
1917
|
+
fin = [orig, $cursor].max
|
1918
|
+
if $visual_mode
|
1919
|
+
# PWD has to be there in selction
|
1920
|
+
if $selected_files.index $view[$cursor]
|
1921
|
+
# this depends on the direction
|
1922
|
+
$selected_files = $selected_files - $view[star..fin]
|
1923
|
+
## current row remains in selection always.
|
1924
|
+
$selected_files.push $view[$cursor]
|
1925
|
+
else
|
1926
|
+
$selected_files.concat $view[star..fin]
|
1927
|
+
end
|
1928
|
+
end
|
1929
|
+
end
|
1930
|
+
def visual_mode_toggle
|
1931
|
+
$visual_mode = !$visual_mode
|
1932
|
+
if $visual_mode
|
1933
|
+
$visual_block_start = $cursor
|
1934
|
+
$selected_files.push $view[$cursor]
|
1935
|
+
end
|
1936
|
+
end
|
1937
|
+
def visual_block_clear
|
1938
|
+
if $visual_block_start
|
1939
|
+
star = [$visual_block_start, $cursor].min
|
1940
|
+
fin = [$visual_block_start, $cursor].max
|
1941
|
+
$selected_files = $selected_files - $view[star..fin]
|
1942
|
+
end
|
1943
|
+
$visual_block_start = nil
|
1944
|
+
$visual_mode = nil
|
1945
|
+
end
|
1946
|
+
def file_starting_with fc
|
1947
|
+
ix = return_next_match(method(:file_matching?), "^#{fc}")
|
1948
|
+
if ix
|
1949
|
+
goto_line ix
|
1950
|
+
end
|
1951
|
+
end
|
1952
|
+
def file_matching? file, patt
|
1953
|
+
file =~ /#{patt}/
|
1954
|
+
end
|
1955
|
+
|
1956
|
+
## generic method to take cursor to next position for a given condition
|
1957
|
+
def return_next_match binding, *args
|
1958
|
+
first = nil
|
1959
|
+
ix = 0
|
1960
|
+
$view.each_with_index do |elem,ii|
|
1961
|
+
if binding.call(elem, *args)
|
1962
|
+
first ||= ii
|
1963
|
+
if ii > $cursor
|
1964
|
+
ix = ii
|
1965
|
+
break
|
1966
|
+
end
|
1967
|
+
end
|
1968
|
+
end
|
1969
|
+
return first if ix == 0
|
1970
|
+
return ix
|
1971
|
+
end
|
1972
|
+
##
|
1973
|
+
# position cursor on a specific line which could be on a nother page
|
1974
|
+
# therefore calculate the correct start offset of the display also.
|
1975
|
+
def goto_line pos
|
1976
|
+
pos = pos.to_i
|
1977
|
+
pages = ((pos * 1.00)/$pagesize).ceil
|
1978
|
+
pages -= 1
|
1979
|
+
$sta = pages * $pagesize + 1
|
1980
|
+
$cursor = pos
|
1981
|
+
end
|
1982
|
+
def filetype f
|
1983
|
+
return nil unless f
|
1984
|
+
f = Shellwords.escape(f)
|
1985
|
+
s = `file #{f}`
|
1986
|
+
if s.index "text"
|
1987
|
+
return :text
|
1988
|
+
elsif s.index(/[Zz]ip/)
|
1989
|
+
return :zip
|
1990
|
+
elsif s.index("archive")
|
1991
|
+
return :zip
|
1992
|
+
elsif s.index "image"
|
1993
|
+
return :image
|
1994
|
+
elsif s.index "data"
|
1995
|
+
return :text
|
1996
|
+
end
|
1997
|
+
nil
|
1998
|
+
end
|
1999
|
+
|
2000
|
+
def save_dir_pos
|
2001
|
+
return if $sta == $min_index && $cursor == $min_index
|
2002
|
+
$dir_position[Dir.pwd] = [$sta, $cursor]
|
2003
|
+
end
|
2004
|
+
def revert_dir_pos
|
2005
|
+
$sta = 0
|
2006
|
+
$cursor = $min_index
|
2007
|
+
a = $dir_position[Dir.pwd]
|
2008
|
+
if a
|
2009
|
+
$sta = a.first
|
2010
|
+
$cursor = a[1]
|
2011
|
+
raise "sta is nil for #{Dir.pwd} : #{$dir_position[Dir.pwd]}" unless $sta
|
2012
|
+
raise "cursor is nil" unless $cursor
|
2013
|
+
end
|
2014
|
+
end
|
2015
|
+
def newdir
|
2016
|
+
print
|
2017
|
+
print "Enter directory name: "
|
2018
|
+
str = Readline::readline('>', true)
|
2019
|
+
return if str == ""
|
2020
|
+
if File.exists? str
|
2021
|
+
perror "#{str} exists."
|
2022
|
+
return
|
2023
|
+
end
|
2024
|
+
begin
|
2025
|
+
FileUtils.mkdir str
|
2026
|
+
$used_dirs.insert(0, str) if File.exists?(str)
|
2027
|
+
refresh
|
2028
|
+
rescue Exception => ex
|
2029
|
+
perror "Error in newdir: #{ex}"
|
2030
|
+
end
|
2031
|
+
end
|
2032
|
+
def newfile
|
2033
|
+
print
|
2034
|
+
print "Enter file name: "
|
2035
|
+
str = Readline::readline('>', true)
|
2036
|
+
return if str == ""
|
2037
|
+
system "$EDITOR #{str}"
|
2038
|
+
$visited_files.insert(0, str) if File.exists?(str)
|
2039
|
+
refresh
|
2040
|
+
end
|
2041
|
+
|
2042
|
+
##
|
2043
|
+
# Editing of the User Dir List.
|
2044
|
+
# remove current entry from used dirs list, since we may not want some entries being there
|
2045
|
+
#
|
2046
|
+
|
2047
|
+
def remove_from_list
|
2048
|
+
return unless $selection_allowed
|
2049
|
+
if $selected_files.size > 0
|
2050
|
+
sz = $selected_files.size
|
2051
|
+
print "Remove #{sz} files from used list (y)?: "
|
2052
|
+
ch = get_char
|
2053
|
+
return if ch != "y"
|
2054
|
+
$used_dirs = $used_dirs - $selected_files
|
2055
|
+
$visited_files = $visited_files - $selected_files
|
2056
|
+
unselect_all
|
2057
|
+
$modified = true
|
2058
|
+
return
|
2059
|
+
end
|
2060
|
+
print
|
2061
|
+
## what if selected some rows
|
2062
|
+
file = $view[$cursor]
|
2063
|
+
print "Remove #{file} from used list (y)?: "
|
2064
|
+
ch = get_char
|
2065
|
+
return if ch != "y"
|
2066
|
+
file = File.expand_path(file)
|
2067
|
+
if File.directory? file
|
2068
|
+
$used_dirs.delete(file)
|
2069
|
+
else
|
2070
|
+
$visited_files.delete(file)
|
2071
|
+
end
|
2072
|
+
refresh
|
2073
|
+
$modified = true
|
2074
|
+
end
|
2075
|
+
#
|
2076
|
+
# If there's a short file list, take recently mod and accessed folders and put latest
|
2077
|
+
# files from there and insert it here. I take both since recent mod can be binaries / object
|
2078
|
+
# files and gems created by a process, and not actually edited files. Recent accessed gives
|
2079
|
+
# latest source, but in some cases even this can be misleading since running a program accesses
|
2080
|
+
# include files.
|
2081
|
+
def enhance_file_list
|
2082
|
+
return
|
2083
|
+
return unless $enhanced_mode
|
2084
|
+
# if only one entry and its a dir
|
2085
|
+
# get its children and maybe the recent mod files a few
|
2086
|
+
|
2087
|
+
if $files.size == 1
|
2088
|
+
# its a dir, let give the next level at least
|
2089
|
+
if $files.first[-1] == "/"
|
2090
|
+
d = $files.first
|
2091
|
+
f = `zsh -c 'print -rl -- #{d}*(omM)'`.split("\n")
|
2092
|
+
if f && f.size > 0
|
2093
|
+
$files.concat f
|
2094
|
+
$files.concat get_important_files(d)
|
2095
|
+
return
|
2096
|
+
end
|
2097
|
+
else
|
2098
|
+
# just a file, not dirs here
|
2099
|
+
return
|
2100
|
+
end
|
2101
|
+
end
|
2102
|
+
#
|
2103
|
+
# check if a ruby project dir, although it could be a backup file too,
|
2104
|
+
# if so , expand lib and maby bin, put a couple recent files
|
2105
|
+
#
|
2106
|
+
if $files.index("Gemfile") || $files.grep(/\.gemspec/).size > 0
|
2107
|
+
# usually the lib dir has only one file and one dir
|
2108
|
+
flg = false
|
2109
|
+
$files.concat get_important_files(Dir.pwd)
|
2110
|
+
if $files.index("lib/")
|
2111
|
+
f = `zsh -c 'print -rl -- lib/*(om[1,5]M)'`.split("\n")
|
2112
|
+
if f && f.size() > 0
|
2113
|
+
insert_into_list("lib/", f)
|
2114
|
+
flg = true
|
2115
|
+
end
|
2116
|
+
dd = File.basename(Dir.pwd)
|
2117
|
+
if f.index("lib/#{dd}/")
|
2118
|
+
f = `zsh -c 'print -rl -- lib/#{dd}/*(om[1,5]M)'`.split("\n")
|
2119
|
+
if f && f.size() > 0
|
2120
|
+
insert_into_list("lib/#{dd}/", f)
|
2121
|
+
flg = true
|
2122
|
+
end
|
2123
|
+
end
|
2124
|
+
end
|
2125
|
+
if $files.index("bin/")
|
2126
|
+
f = `zsh -c 'print -rl -- bin/*(om[1,5]M)'`.split("\n")
|
2127
|
+
insert_into_list("bin/", f) if f && f.size() > 0
|
2128
|
+
flg = true
|
2129
|
+
end
|
2130
|
+
return if flg
|
2131
|
+
|
2132
|
+
# lib has a dir in it with the gem name
|
2133
|
+
|
2134
|
+
end
|
2135
|
+
return if $files.size > 15
|
2136
|
+
|
2137
|
+
## first check accessed else modified will change accessed
|
2138
|
+
moda = `zsh -c 'print -rn -- *(/oa[1]M)'`
|
2139
|
+
if moda && moda != ""
|
2140
|
+
modf = `zsh -c 'print -rn -- #{moda}*(oa[1]M)'`
|
2141
|
+
if modf && modf != ""
|
2142
|
+
insert_into_list moda, modf
|
2143
|
+
end
|
2144
|
+
modm = `zsh -c 'print -rn -- #{moda}*(om[1]M)'`
|
2145
|
+
if modm && modm != "" && modm != modf
|
2146
|
+
insert_into_list moda, modm
|
2147
|
+
end
|
2148
|
+
end
|
2149
|
+
## get last modified dir
|
2150
|
+
modm = `zsh -c 'print -rn -- *(/om[1]M)'`
|
2151
|
+
if modm != moda
|
2152
|
+
modmf = `zsh -c 'print -rn -- #{modm}*(oa[1]M)'`
|
2153
|
+
insert_into_list modm, modmf
|
2154
|
+
modmf1 = `zsh -c 'print -rn -- #{modm}*(om[1]M)'`
|
2155
|
+
insert_into_list(modm, modmf1) if modmf1 != modmf
|
2156
|
+
else
|
2157
|
+
# if both are same then our options get reduced so we need to get something more
|
2158
|
+
# If you access the latest mod dir, then come back you get only one, since mod and accessed
|
2159
|
+
# are the same dir, so we need to find the second modified dir
|
2160
|
+
end
|
2161
|
+
end
|
2162
|
+
|
2163
|
+
# log messages that may not remain on the screen by user may wish to see
|
2164
|
+
# such as age of current file being displayed.
|
2165
|
+
def log str
|
2166
|
+
$logger_array.unshift str
|
2167
|
+
end
|
2168
|
+
def view_log
|
2169
|
+
page_data $logger_array
|
2170
|
+
end
|
2171
|
+
def clear_log
|
2172
|
+
$logger_array.clear
|
2173
|
+
end
|
2174
|
+
def page_data array
|
2175
|
+
require 'tempfile'
|
2176
|
+
f = Tempfile.new("corvuslog")
|
2177
|
+
begin
|
2178
|
+
path = f.path
|
2179
|
+
array.each do |line|
|
2180
|
+
f.puts line
|
2181
|
+
end
|
2182
|
+
f.close
|
2183
|
+
system("less #{path}")
|
2184
|
+
ensure
|
2185
|
+
f.close
|
2186
|
+
f.unlink
|
2187
|
+
end
|
2188
|
+
end
|
2189
|
+
# allows user to select from list, returning string if user pressed ENTER
|
2190
|
+
# Aborts if user presses Q or C-c or ESCAPE
|
2191
|
+
def ctrlp arr
|
2192
|
+
patt = nil
|
2193
|
+
curr = 0
|
2194
|
+
while true
|
2195
|
+
system("clear")
|
2196
|
+
if patt and patt != ""
|
2197
|
+
# need fuzzy match here
|
2198
|
+
view = arr.grep(/^#{patt}/)
|
2199
|
+
view = view | arr.grep(/#{patt}/)
|
2200
|
+
fuzzypatt = patt.split("").join(".*")
|
2201
|
+
view = view | arr.grep(/#{fuzzypatt}/)
|
2202
|
+
else
|
2203
|
+
view = arr
|
2204
|
+
end
|
2205
|
+
curr = [view.size-1, curr].min
|
2206
|
+
# if empty then curr becomes -1
|
2207
|
+
curr = 0 if curr < 0
|
2208
|
+
view.each_with_index do |a, i|
|
2209
|
+
mark = " "
|
2210
|
+
mark = ">" if curr == i
|
2211
|
+
print "#{mark} #{a} \n"
|
2212
|
+
end
|
2213
|
+
#puts " "
|
2214
|
+
print "\r#{patt} >"
|
2215
|
+
ch = get_char
|
2216
|
+
if ch =~ /^[a-z]$/
|
2217
|
+
patt ||= ""
|
2218
|
+
patt << ch
|
2219
|
+
elsif ch == "BACKSPACE"
|
2220
|
+
if patt && patt.size > 0
|
2221
|
+
patt = patt[0..-2]
|
2222
|
+
end
|
2223
|
+
elsif ch == "Q" or ch == "C-c" or ch == "ESCAPE"
|
2224
|
+
break
|
2225
|
+
elsif ch == "UP"
|
2226
|
+
curr -= 1
|
2227
|
+
curr = 0 if curr < 0
|
2228
|
+
elsif ch == "DOWN"
|
2229
|
+
curr += 1
|
2230
|
+
curr = [view.size-1, curr].min
|
2231
|
+
# if empty then curr becomes -1
|
2232
|
+
curr = 0 if curr < 0
|
2233
|
+
elsif ch == "ENTER"
|
2234
|
+
return view[curr]
|
2235
|
+
else
|
2236
|
+
# do right and left arrow
|
2237
|
+
|
2238
|
+
# get arrow keys here
|
2239
|
+
end
|
2240
|
+
|
2241
|
+
end
|
2242
|
+
end
|
2243
|
+
def get_hint
|
2244
|
+
if $gviscols == 2 and $view.size > 25
|
2245
|
+
return "(Toggle) Press =1 to see single column, =l for long listing"
|
2246
|
+
end
|
2247
|
+
if $long_listing
|
2248
|
+
return "(Command) To sort, press :sort_on or `s (backtick s)"
|
2249
|
+
end
|
2250
|
+
hints = [
|
2251
|
+
"(Backtick) Press `1 for article, `2 for comments for current line",
|
2252
|
+
"(Colon) Press :n<ENTER> for next forum, :p<ENTER> for previous forum",
|
2253
|
+
"Reload current forum with `r or :r<ENTER>",
|
2254
|
+
"Get more articles with `m (backtick m)",
|
2255
|
+
"(Command) Press : and then up arrow to see available commands",
|
2256
|
+
"Press > and < to go to next and prev forums.",
|
2257
|
+
"Press TAB to cycle through views"
|
2258
|
+
]
|
2259
|
+
return hints[rand(hints.size)] || hints.last
|
2260
|
+
end
|
2261
|
+
def get_action_desc action
|
2262
|
+
h = { :reload => "Reload latest articles for this forum",
|
2263
|
+
:new_forum => "Get articles for a new forum",
|
2264
|
+
:display_forum => "List forums and select one",
|
2265
|
+
:change_subforum => "List forums and select one",
|
2266
|
+
:new_articles => "Get latest articles from reddit /new option",
|
2267
|
+
:view_menu => "View options: articles, comments, log",
|
2268
|
+
:reddit_options => "New, rising, controversial, top for this forum",
|
2269
|
+
:hacker_options => "Newest, show, ask, jobs",
|
2270
|
+
:fetch_more => "Fetch another page for this forum",
|
2271
|
+
:delete_current_forum => "Remove forum from your list",
|
2272
|
+
:next_forum => "Cycle through selected forums",
|
2273
|
+
:previous_forum => "Cycle through selected forums"
|
2274
|
+
}
|
2275
|
+
return h[action]
|
2276
|
+
end
|
2277
|
+
|
2278
|
+
# http://www.ruby-doc.org/stdlib/libdoc/optparse/rdoc/classes/OptionParser.html
|
2279
|
+
require 'optparse'
|
2280
|
+
options = {}
|
2281
|
+
app = File.basename $0
|
2282
|
+
OptionParser.new do |opts|
|
2283
|
+
opts.banner = %Q{
|
2284
|
+
#{app} version #{VERSION} (YML version)
|
2285
|
+
Usage: #{app} [options]
|
2286
|
+
}
|
2287
|
+
|
2288
|
+
#opts.on("-v", "--[no-]verbose", "Print description also") do |v|
|
2289
|
+
#options[:verbose] = v
|
2290
|
+
#end
|
2291
|
+
#opts.on("-n N", "--limit", Integer, "limit to N stories") do |v|
|
2292
|
+
#options[:number] = v
|
2293
|
+
#end
|
2294
|
+
#opts.on("-u URL", String,"--url", "Get articles from URL/file") do |v|
|
2295
|
+
#options[:url] = v
|
2296
|
+
#end
|
2297
|
+
#opts.on("-H (reddit|hn)", String,"--hostname", "Get articles from HOST") do |v|
|
2298
|
+
#host = v
|
2299
|
+
#end
|
2300
|
+
#opts.on("--save-html", "Save html to file?") do |v|
|
2301
|
+
#options[:save_html] = true
|
2302
|
+
#end
|
2303
|
+
#opts.on("-w PATH", String,"--save-html-path", "Save html to file PATH") do |v|
|
2304
|
+
#options[:htmloutfile] = v
|
2305
|
+
#options[:save_html] = true
|
2306
|
+
#end
|
2307
|
+
opts.on("-m MODE", String,"--mode", "Use 'text' or 'gui' browser") do |v|
|
2308
|
+
options[:browser_mode] = v
|
2309
|
+
end
|
2310
|
+
opts.on("-t browser", String,"--text", "browser for text mode") do |v|
|
2311
|
+
options[:browser_text] = v
|
2312
|
+
end
|
2313
|
+
opts.on("-g browser", String,"--gui", "browser for gui mode") do |v|
|
2314
|
+
options[:browser_gui] = v
|
2315
|
+
end
|
2316
|
+
opts.on("-c cache dir", String,"--cache-dir", "location to store yml files") do |v|
|
2317
|
+
options[:cache_path] = v
|
2318
|
+
end
|
2319
|
+
end.parse!
|
2320
|
+
run options
|