zlx_hacker_term 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +26 -0
- data/LICENSE.txt +22 -0
- data/README.md +17 -0
- data/Rakefile +3 -0
- data/bin/hacker_term +6 -0
- data/data/data.json +314 -0
- data/hacker_term.gemspec +23 -0
- data/lib/hacker_term/page_data.rb +125 -0
- data/lib/hacker_term/ui.rb +153 -0
- data/lib/hacker_term/version.rb +3 -0
- data/lib/hacker_term.rb +102 -0
- data/spec/page_data_spec.rb +165 -0
- metadata +125 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
hacker_term (0.0.4)
|
5
|
+
clipboard
|
6
|
+
launchy
|
7
|
+
rest-client
|
8
|
+
socksify
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
addressable (2.3.2)
|
14
|
+
clipboard (1.0.1)
|
15
|
+
launchy (2.1.2)
|
16
|
+
addressable (~> 2.3)
|
17
|
+
mime-types (1.19)
|
18
|
+
rest-client (1.6.7)
|
19
|
+
mime-types (>= 1.16)
|
20
|
+
socksify (1.4.1)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
ruby
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
hacker_term!
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Ciaran Archer
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
hacker_term
|
2
|
+
==========
|
3
|
+
Hacker News on the Terminal.
|
4
|
+
|
5
|
+
![Screenshot](http://blog.zlxstar.me/images/hack_term_snapshot.png)
|
6
|
+
|
7
|
+
Requirements
|
8
|
+
------------
|
9
|
+
* Ruby 1.9.3
|
10
|
+
* socksify
|
11
|
+
* socks proxy
|
12
|
+
|
13
|
+
Installation
|
14
|
+
------------
|
15
|
+
* Install with `gem install hacker_term`
|
16
|
+
* Run using `socksify_ruby 127.0.0.1 1080 hacker_term`
|
17
|
+
* Tests included; I run them using `rspec -fd` in the project directory
|
data/Rakefile
ADDED
data/bin/hacker_term
ADDED
data/data/data.json
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
{"items":[
|
2
|
+
{
|
3
|
+
"title":"PowWow - Collaborative Screen Sharing",
|
4
|
+
"url":"http://powwow.cc/",
|
5
|
+
"score":"124 points",
|
6
|
+
"user":"siong1987",
|
7
|
+
"comments":"43 comments",
|
8
|
+
"time":"7 hours ago",
|
9
|
+
"item_id":"4924763",
|
10
|
+
"description":"124 points by siong1987 7 hours ago | 43 comments"
|
11
|
+
},
|
12
|
+
{
|
13
|
+
"title":"Ray Kurzweil joins Google",
|
14
|
+
"url":"http://www.kurzweilai.net/kurzweil-joins-google-to-work-on-new-projects-involving-machine-learning-and-language-processing?utm_source=twitterfeed&utm_medium=twitter",
|
15
|
+
"score":"260 points",
|
16
|
+
"user":"dumitrue",
|
17
|
+
"comments":"122 comments",
|
18
|
+
"time":"14 hours ago",
|
19
|
+
"item_id":"4923914",
|
20
|
+
"description":"260 points by dumitrue 14 hours ago | 122 comments"
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"title":"The Pinboard Investment Co-Prosperity Cloud",
|
24
|
+
"url":"http://static.pinboard.in/prosperity_cloud.htm",
|
25
|
+
"score":"381 points",
|
26
|
+
"user":"adulau",
|
27
|
+
"comments":"136 comments",
|
28
|
+
"time":"17 hours ago",
|
29
|
+
"item_id":"4923136",
|
30
|
+
"description":"381 points by adulau 17 hours ago | 136 comments"
|
31
|
+
},
|
32
|
+
{
|
33
|
+
"title":"Web Revenue Models - 9 Categories, 80+ variations",
|
34
|
+
"url":"https://hackpad.com/EgXuEtSibE7#Web-And-Mobile-Revenue-Models-%28final%29",
|
35
|
+
"score":"84 points",
|
36
|
+
"user":"stickhandle",
|
37
|
+
"comments":"6 comments",
|
38
|
+
"time":"8 hours ago",
|
39
|
+
"item_id":"4924647",
|
40
|
+
"description":"84 points by stickhandle 8 hours ago | 6 comments"
|
41
|
+
},
|
42
|
+
{
|
43
|
+
"title":"The 2012 Perl 6 Coding Contest",
|
44
|
+
"url":"http://strangelyconsistent.org/blog/the-2012-perl-6-coding-contest",
|
45
|
+
"score":"20 points",
|
46
|
+
"user":"draegtun",
|
47
|
+
"comments":"discuss",
|
48
|
+
"time":"4 hours ago",
|
49
|
+
"item_id":"4925036",
|
50
|
+
"description":"20 points by draegtun 4 hours ago | discuss"
|
51
|
+
},
|
52
|
+
{
|
53
|
+
"title":"Brython - Python to Javascript translator",
|
54
|
+
"url":"http://www.brython.info/index_en.html",
|
55
|
+
"score":"222 points",
|
56
|
+
"user":"toni",
|
57
|
+
"comments":"55 comments",
|
58
|
+
"time":"16 hours ago",
|
59
|
+
"item_id":"4923530",
|
60
|
+
"description":"222 points by toni 16 hours ago | 55 comments"
|
61
|
+
},
|
62
|
+
{
|
63
|
+
"title":"Dear Open Source Project Leader: Quit Being A Jerk",
|
64
|
+
"url":"http://lostechies.com/derickbailey/2012/12/14/dear-open-source-project-leader-quit-being-a-jerk/",
|
65
|
+
"score":"362 points",
|
66
|
+
"user":"derickbailey",
|
67
|
+
"comments":"158 comments",
|
68
|
+
"time":"22 hours ago",
|
69
|
+
"item_id":"4921152",
|
70
|
+
"description":"362 points by derickbailey 22 hours ago | 158 comments"
|
71
|
+
},
|
72
|
+
{
|
73
|
+
"title":"Google Disabling Exchange Sync for Free Accounts",
|
74
|
+
"url":"http://support.google.com/a/bin/answer.py?hl=en&answer=135937",
|
75
|
+
"score":"145 points",
|
76
|
+
"user":"HaloZero",
|
77
|
+
"comments":"115 comments",
|
78
|
+
"time":"14 hours ago",
|
79
|
+
"item_id":"4923832",
|
80
|
+
"description":"145 points by HaloZero 14 hours ago | 115 comments"
|
81
|
+
},
|
82
|
+
{
|
83
|
+
"title":"Want to learn to code? Don't copy and paste, type out other people's code",
|
84
|
+
"url":"http://www.shockoe.com/blog/typingcodeout/",
|
85
|
+
"score":"283 points",
|
86
|
+
"user":"tomasien",
|
87
|
+
"comments":"167 comments",
|
88
|
+
"time":"22 hours ago",
|
89
|
+
"item_id":"4921258",
|
90
|
+
"description":"283 points by tomasien 22 hours ago | 167 comments"
|
91
|
+
},
|
92
|
+
{
|
93
|
+
"title":"Moravec's paradox",
|
94
|
+
"url":"https://en.wikipedia.org/wiki/Moravec%27s_paradox",
|
95
|
+
"score":"144 points",
|
96
|
+
"user":"kristiandupont",
|
97
|
+
"comments":"34 comments",
|
98
|
+
"time":"16 hours ago",
|
99
|
+
"item_id":"4923299",
|
100
|
+
"description":"144 points by kristiandupont 16 hours ago | 34 comments"
|
101
|
+
},
|
102
|
+
{
|
103
|
+
"title":"A Bad UI Pattern",
|
104
|
+
"url":"http://www.kapilkale.com/blog/the-worst-ui-pattern-in-existence/",
|
105
|
+
"score":"91 points",
|
106
|
+
"user":"kapilkale",
|
107
|
+
"comments":"45 comments",
|
108
|
+
"time":"13 hours ago",
|
109
|
+
"item_id":"4923971",
|
110
|
+
"description":"91 points by kapilkale 13 hours ago | 45 comments"
|
111
|
+
},
|
112
|
+
{
|
113
|
+
"title":"I love you, dad",
|
114
|
+
"url":"http://notch.tumblr.com/post/37823268132/i-love-you-dad",
|
115
|
+
"score":"987 points",
|
116
|
+
"user":"kjackson2012",
|
117
|
+
"comments":"204 comments",
|
118
|
+
"time":"1 day ago",
|
119
|
+
"item_id":"4916629",
|
120
|
+
"description":"987 points by kjackson2012 1 day ago | 204 comments"
|
121
|
+
},
|
122
|
+
{
|
123
|
+
"title":"What euros Facebooktrades responsibility when the nation seeks to lynch someone?",
|
124
|
+
"url":"http://pandodaily.com/2012/12/14/whats-facebooks-responsibility-when-the-nation-seeks-to-lynch-someone-on-only-a-name/",
|
125
|
+
"score":"81 points",
|
126
|
+
"user":"muratmutlu",
|
127
|
+
"comments":"71 comments",
|
128
|
+
"time":"11 hours ago",
|
129
|
+
"item_id":"4924361",
|
130
|
+
"description":"81 points by muratmutlu 11 hours ago | 71 comments"
|
131
|
+
},
|
132
|
+
{
|
133
|
+
"title":"A vim interface for gmail: Vmail",
|
134
|
+
"url":"http://www.danielchoi.com/software/vmail.html",
|
135
|
+
"score":"128 points",
|
136
|
+
"user":"ezl",
|
137
|
+
"comments":"67 comments",
|
138
|
+
"time":"18 hours ago",
|
139
|
+
"item_id":"4922542",
|
140
|
+
"description":"128 points by ezl 18 hours ago | 67 comments"
|
141
|
+
},
|
142
|
+
{
|
143
|
+
"title":"E Online has left a Gist url on the top of their site",
|
144
|
+
"url":"http://www.eonline.com/news/371827/newtown-school-shooting-jack-reacher-u-s-premiere-postponed-out-of-respect-for-victims",
|
145
|
+
"score":"39 points",
|
146
|
+
"user":"Moto7451",
|
147
|
+
"comments":"14 comments",
|
148
|
+
"time":"8 hours ago",
|
149
|
+
"item_id":"4924624",
|
150
|
+
"description":"39 points by Moto7451 8 hours ago | 14 comments"
|
151
|
+
},
|
152
|
+
{
|
153
|
+
"title":"The Web We Lost",
|
154
|
+
"url":"http://dashes.com/anil/2012/12/the-web-we-lost.html",
|
155
|
+
"score":"593 points",
|
156
|
+
"user":"kzasada",
|
157
|
+
"comments":"151 comments",
|
158
|
+
"time":"1 day ago",
|
159
|
+
"item_id":"4917828",
|
160
|
+
"description":"593 points by kzasada 1 day ago | 151 comments"
|
161
|
+
},
|
162
|
+
{
|
163
|
+
"title":"NASA Eyes Mission To Icy Jupiter Moon Europa To Gauge Habitability",
|
164
|
+
"url":"http://www.space.com/18901-nasa-mission-jupiter-moon-europa.html",
|
165
|
+
"score":"26 points",
|
166
|
+
"user":"rpm4321",
|
167
|
+
"comments":"5 comments",
|
168
|
+
"time":"9 hours ago",
|
169
|
+
"item_id":"4924607",
|
170
|
+
"description":"26 points by rpm4321 9 hours ago | 5 comments"
|
171
|
+
},
|
172
|
+
{
|
173
|
+
"title":"Tcl the misunderstood (2006)",
|
174
|
+
"url":"http://antirez.com/articoli/tclmisunderstood.html?",
|
175
|
+
"score":"170 points",
|
176
|
+
"user":"zeitg3ist",
|
177
|
+
"comments":"96 comments",
|
178
|
+
"time":"1 day ago",
|
179
|
+
"item_id":"4920831",
|
180
|
+
"description":"170 points by zeitg3ist 1 day ago | 96 comments"
|
181
|
+
},
|
182
|
+
{
|
183
|
+
"title":"EFF: Stop Congress from Reauthorizing the Warrantless Spying Bill",
|
184
|
+
"url":"https://www.eff.org/deeplinks/2012/12/congress-poised-reauthorize-fisa-amendment-act-warrantless-spying-bill",
|
185
|
+
"score":"191 points",
|
186
|
+
"user":"mtgx",
|
187
|
+
"comments":"38 comments",
|
188
|
+
"time":"1 day ago",
|
189
|
+
"item_id":"4920542",
|
190
|
+
"description":"191 points by mtgx 1 day ago | 38 comments"
|
191
|
+
},
|
192
|
+
{
|
193
|
+
"title":"Thank HN: You helped the FreeBSD Foundation raise over $43K in three days",
|
194
|
+
"url":"http://freebsdfoundation.blogspot.com/2012/12/stunning-news-website-fundraising.html#",
|
195
|
+
"score":"154 points",
|
196
|
+
"user":"profquail",
|
197
|
+
"comments":"23 comments",
|
198
|
+
"time":"23 hours ago",
|
199
|
+
"item_id":"4920891",
|
200
|
+
"description":"154 points by profquail 23 hours ago | 23 comments"
|
201
|
+
},
|
202
|
+
{
|
203
|
+
"title":"Meet the World trades Cheapest Venture Capitalist",
|
204
|
+
"url":"http://www.wired.com/business/2012/12/worlds-cheapest-venture-capitalist/",
|
205
|
+
"score":"34 points",
|
206
|
+
"user":"wyclif",
|
207
|
+
"comments":"9 comments",
|
208
|
+
"time":"8 hours ago",
|
209
|
+
"item_id":"4924651",
|
210
|
+
"description":"34 points by wyclif 8 hours ago | 9 comments"
|
211
|
+
},
|
212
|
+
{
|
213
|
+
"title":"Programmer creates 800,000 books algorithmically, starts selling them on Amazon",
|
214
|
+
"url":"http://www.extremetech.com/extreme/143382-programmer-creates-800000-books-algorithmically-starts-selling-them-on-amazon?utm_source=rss&utm_medium=rss&utm_campaign=programmer-creates-800000-books-algorithmically-starts-selling-them-on-amazon",
|
215
|
+
"score":"131 points",
|
216
|
+
"user":"Libertatea",
|
217
|
+
"comments":"119 comments",
|
218
|
+
"time":"17 hours ago",
|
219
|
+
"item_id":"4923208",
|
220
|
+
"description":"131 points by Libertatea 17 hours ago | 119 comments"
|
221
|
+
},
|
222
|
+
{
|
223
|
+
"title":"Internet porn: Automatic block rejected (UK)",
|
224
|
+
"url":"http://www.bbc.co.uk/news/uk-politics-20738746",
|
225
|
+
"score":"6 points",
|
226
|
+
"user":"andrewaylett",
|
227
|
+
"comments":"4 comments",
|
228
|
+
"time":"3 hours ago",
|
229
|
+
"item_id":"4925047",
|
230
|
+
"description":"6 points by andrewaylett 3 hours ago | 4 comments"
|
231
|
+
},
|
232
|
+
{
|
233
|
+
"title":"Working alone sucks",
|
234
|
+
"url":"http://fleetadmiral.tumblr.com/post/37907736486/working-alone-sucks",
|
235
|
+
"score":"139 points",
|
236
|
+
"user":"labaraka",
|
237
|
+
"comments":"108 comments",
|
238
|
+
"time":"23 hours ago",
|
239
|
+
"item_id":"4921047",
|
240
|
+
"description":"139 points by labaraka 23 hours ago | 108 comments"
|
241
|
+
},
|
242
|
+
{
|
243
|
+
"title":"Ole Roemer and the Speed of Light",
|
244
|
+
"url":"http://www.amnh.org/education/resources/rfl/web/essaybooks/cosmic/p_roemer.html",
|
245
|
+
"score":"307 points",
|
246
|
+
"user":"tmoretti",
|
247
|
+
"comments":"66 comments",
|
248
|
+
"time":"1 day ago",
|
249
|
+
"item_id":"4919594",
|
250
|
+
"description":"307 points by tmoretti 1 day ago | 66 comments"
|
251
|
+
},
|
252
|
+
{
|
253
|
+
"title":"Show HN: Introducing KA Lite, an offline version of Khan Academy",
|
254
|
+
"url":"http://jamiealexandre.com/blog/2012/12/12/ka-lite-offline-khan-academy/",
|
255
|
+
"score":"49 points",
|
256
|
+
"user":"jamalex",
|
257
|
+
"comments":"3 comments",
|
258
|
+
"time":"14 hours ago",
|
259
|
+
"item_id":"4923821",
|
260
|
+
"description":"49 points by jamalex 14 hours ago | 3 comments"
|
261
|
+
},
|
262
|
+
{
|
263
|
+
"title":"Google Maps for iOS",
|
264
|
+
"url":"https://itunes.apple.com/us/app/google-maps/id585027354?mt=8",
|
265
|
+
"score":"808 points",
|
266
|
+
"user":"zacharytamas",
|
267
|
+
"comments":"440 comments",
|
268
|
+
"time":"2 days ago",
|
269
|
+
"item_id":"4914089",
|
270
|
+
"description":"808 points by zacharytamas 2 days ago | 440 comments"
|
271
|
+
},
|
272
|
+
{
|
273
|
+
"title":"Perfect Audience (YC S11) seeks full-stack engineer",
|
274
|
+
"url":"item?id=4924955",
|
275
|
+
"time":"5 hours ago",
|
276
|
+
"description":"5 hours ago"
|
277
|
+
},
|
278
|
+
{
|
279
|
+
"title":"AngelList Raising A Big Round, To Be Valued at $150 Million Or More",
|
280
|
+
"url":"http://techcrunch.com/2012/12/14/angellist-to-be-valued-at-150-million-or-more/",
|
281
|
+
"score":"33 points",
|
282
|
+
"user":"zosegal",
|
283
|
+
"comments":"12 comments",
|
284
|
+
"time":"12 hours ago",
|
285
|
+
"item_id":"4924134",
|
286
|
+
"description":"33 points by zosegal 12 hours ago | 12 comments"
|
287
|
+
},
|
288
|
+
{
|
289
|
+
"title":"Data crunching to find the cheapest airline in the world",
|
290
|
+
"url":"http://www.tnooz.com/2012/12/14/news/data-crunching-to-find-the-cheapest-airline-in-the-world/",
|
291
|
+
"score":"28 points",
|
292
|
+
"user":"tomhoward",
|
293
|
+
"comments":"2 comments",
|
294
|
+
"time":"11 hours ago",
|
295
|
+
"item_id":"4924319",
|
296
|
+
"description":"28 points by tomhoward 11 hours ago | 2 comments"
|
297
|
+
},
|
298
|
+
{
|
299
|
+
"title":"NextId",
|
300
|
+
"url":"/news2",
|
301
|
+
"description":"hn next id news2 "
|
302
|
+
},
|
303
|
+
{
|
304
|
+
"title": "Ask HN: Who is hiring? (January 2013)",
|
305
|
+
"url": "item?id=4992617",
|
306
|
+
"score": "175 points",
|
307
|
+
"user": "whoishiring",
|
308
|
+
"comments": "140 comments",
|
309
|
+
"time": "11 hours ago",
|
310
|
+
"item_id": "4992617",
|
311
|
+
"description": "175 points points by whoishiring 11 hours ago | 140 comments"
|
312
|
+
}
|
313
|
+
]
|
314
|
+
}
|
data/hacker_term.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hacker_term/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "zlx_hacker_term"
|
8
|
+
gem.version = HackerTerm::VERSION
|
9
|
+
gem.authors = ["soffolk zhu"]
|
10
|
+
gem.email = ["zlx.star@gmail.com"]
|
11
|
+
gem.description = %q{Read Hacker News on the Terminal}
|
12
|
+
gem.summary = %q{Allows the reading, sorting and opening of HN articles from the terminal.}
|
13
|
+
gem.homepage = "https://github.com/zlx/hacker_term"
|
14
|
+
gem.add_dependency('rest-client')
|
15
|
+
gem.add_dependency('launchy')
|
16
|
+
gem.add_dependency('clipboard')
|
17
|
+
gem.add_dependency('socksify')
|
18
|
+
|
19
|
+
gem.files = `git ls-files`.split($/)
|
20
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
21
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
22
|
+
gem.require_paths = ["lib"]
|
23
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
# Controversial monkeypatch of String class so it can tell us if a string is a number
|
4
|
+
class String
|
5
|
+
def is_num?
|
6
|
+
self =~ /^[-+]?[0-9]*\.?[0-9]+$/
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module HackerTerm
|
11
|
+
class PageData
|
12
|
+
attr_reader :data, :mean_score, :median_score, :mode_score, :sorted_by, :line_pos
|
13
|
+
|
14
|
+
def initialize(data)
|
15
|
+
@data = JSON.parse(data)['items']
|
16
|
+
|
17
|
+
add_missing_keys!
|
18
|
+
format_numbers!
|
19
|
+
format_urls!
|
20
|
+
|
21
|
+
calculate_mean_score
|
22
|
+
calculate_median_score
|
23
|
+
calculate_mode_score
|
24
|
+
|
25
|
+
@sorted_by = 'RANK'
|
26
|
+
@line_pos = 1
|
27
|
+
end
|
28
|
+
|
29
|
+
def sort_on!(mode)
|
30
|
+
case mode
|
31
|
+
when :score
|
32
|
+
@data = @data.sort_by { |a| -a['score'].to_f } # desc
|
33
|
+
when :comments
|
34
|
+
@data = @data.sort_by { |a| -a['comments'].to_f } # desc
|
35
|
+
when :rank
|
36
|
+
@data = @data.sort_by { |a| a['rank'].to_f }
|
37
|
+
when :title
|
38
|
+
@data = @data.sort_by { |a| a['title'].upcase }
|
39
|
+
else
|
40
|
+
throw "Sorting mode #{mode} not supported!"
|
41
|
+
end
|
42
|
+
|
43
|
+
@sorted_by = mode.to_s.upcase
|
44
|
+
end
|
45
|
+
|
46
|
+
def change_line_pos(direction)
|
47
|
+
if direction == :up
|
48
|
+
@line_pos += 1 unless @line_pos == @data.length
|
49
|
+
elsif direction == :down
|
50
|
+
@line_pos -= 1 unless @line_pos == 1
|
51
|
+
elsif direction == :reset
|
52
|
+
@line_pos = 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def selected_url
|
57
|
+
@data[@line_pos - 1]['url']
|
58
|
+
end
|
59
|
+
|
60
|
+
def selected_comments_url
|
61
|
+
"http://news.ycombinator.com/item?id=" + @data[@line_pos - 1]['item_id']
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def calculate_mode_score
|
67
|
+
freq = @data.inject(Hash.new(0)) { |h,v| h[v['score'].to_f] += 1; h }
|
68
|
+
# Call sort_by on hash to create an array which each contains two elements, the key and value
|
69
|
+
# So we grab the last item, and return the 'key' from our original hash
|
70
|
+
@mode_score = freq.sort_by { |k, v| v }.last.first
|
71
|
+
end
|
72
|
+
|
73
|
+
def calculate_mean_score
|
74
|
+
@mean_score = @data.inject(0.0) { |sum, el| sum + el['score'].to_f } / @data.size
|
75
|
+
end
|
76
|
+
|
77
|
+
def calculate_median_score
|
78
|
+
# Read our numbers and sort them first
|
79
|
+
sorted_scores = @data.map { |el| el['score'].to_f }.sort
|
80
|
+
len = sorted_scores.length
|
81
|
+
@median_score = len % 2 == 1 ? sorted_scores[len / 2] : (sorted_scores[len / 2 - 1] + sorted_scores[len / 2]).to_f / 2
|
82
|
+
end
|
83
|
+
|
84
|
+
def add_missing_keys!
|
85
|
+
# Here we're looking to fix nodes with missing/incorrect data
|
86
|
+
counter = 1
|
87
|
+
@data.each do |item|
|
88
|
+
|
89
|
+
# Add rank (so we can re-sort in 'natural' order)
|
90
|
+
unless item.has_key? 'rank'
|
91
|
+
item['rank'] = counter.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
unless item.has_key? 'score'
|
95
|
+
item['score'] = '0'
|
96
|
+
end
|
97
|
+
|
98
|
+
unless item.has_key? 'comments'
|
99
|
+
item['comments'] = '0'
|
100
|
+
end
|
101
|
+
|
102
|
+
counter += 1
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def format_numbers!
|
107
|
+
# Assumption here is a format like '10 comments' or '35 points'
|
108
|
+
# Also chucks anything left over that isn't a number
|
109
|
+
@data.each do |item|
|
110
|
+
item['comments'] = item['comments'].split(' ').first if item['comments'].include? ' '
|
111
|
+
item['comments'] = '0' unless item['comments'].is_num?
|
112
|
+
item['score'] = item['score'].split(' ').first if item['score'].include? ' '
|
113
|
+
item['score'] = '0' unless item['score'].is_num?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def format_urls!
|
118
|
+
# Add HN domain for posts without an external link
|
119
|
+
@data.each do |item|
|
120
|
+
item['url'] = "http://news.ycombinator.com/#{item['url']}" if item['url'] =~ /^item\?id=[0-9]+/
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
module HackerTerm
|
4
|
+
class UI
|
5
|
+
include Curses
|
6
|
+
|
7
|
+
def initialize(opts={})
|
8
|
+
|
9
|
+
opts = defaults.merge(opts) # Ununsed for now
|
10
|
+
|
11
|
+
raw # Intercept everything
|
12
|
+
noecho # Do not echo user input to stdout
|
13
|
+
stdscr.keypad(true) # Enable arrows
|
14
|
+
|
15
|
+
if can_change_color?
|
16
|
+
start_color
|
17
|
+
# foreground / background colours
|
18
|
+
init_pair(0, COLOR_WHITE, COLOR_BLACK)
|
19
|
+
init_pair(1, COLOR_WHITE, COLOR_BLUE)
|
20
|
+
init_pair(2, COLOR_WHITE, COLOR_RED)
|
21
|
+
init_pair(3, COLOR_BLACK, COLOR_GREEN)
|
22
|
+
end
|
23
|
+
|
24
|
+
@total_width = cols
|
25
|
+
@total_height = lines
|
26
|
+
@padding_left = 2
|
27
|
+
@title_width = 0
|
28
|
+
@cols = ['rank', 'title', 'score', 'comments']
|
29
|
+
@line_num = -1
|
30
|
+
|
31
|
+
clear!
|
32
|
+
end
|
33
|
+
|
34
|
+
def next_line_num
|
35
|
+
@line_num += 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def output_line(line_num, data)
|
39
|
+
setpos(line_num, 0)
|
40
|
+
padding_right = @total_width - data.length - @padding_left
|
41
|
+
padding_right = 0 if padding_right < 0
|
42
|
+
addstr((" " * @padding_left) + data + (" " * padding_right))
|
43
|
+
end
|
44
|
+
|
45
|
+
def output_divider(line_num)
|
46
|
+
setpos(line_num, 0)
|
47
|
+
attrset color_pair(0)
|
48
|
+
addstr('-' * @total_width)
|
49
|
+
end
|
50
|
+
|
51
|
+
def draw_header
|
52
|
+
output_divider(next_line_num)
|
53
|
+
attrset color_pair(1)
|
54
|
+
output_line(next_line_num, "HACKER NEWS TERMINAL - thanks to http://hndroidapi.appspot.com")
|
55
|
+
output_line(next_line_num, "CMDS: Select (Arrows), Open Item (O), Open Item Discussion (D), Refresh (A)")
|
56
|
+
output_line(next_line_num, "CMDS CONT: Sort by Rank (R), Score (S), Comments (C), Title (T) | Quit (Q)")
|
57
|
+
output_divider(next_line_num)
|
58
|
+
|
59
|
+
# Get width_excl_title, i.e. width of all columns + some extra for |'s and spacing.
|
60
|
+
# Once obtained, pad out the title column with the any width remaining
|
61
|
+
# A nicer way to do this is always put the title last, and assume last column gets
|
62
|
+
# remaining width. That way we can just loop through our cols, rather than hardcoding
|
63
|
+
# them as per example below. I'm sticking to this because I want the title listed second.
|
64
|
+
width_excl_title = @cols.inject(0) do |width, col|
|
65
|
+
width += (3 + col.length)
|
66
|
+
end
|
67
|
+
attrset color_pair(2)
|
68
|
+
@title_width = @total_width - width_excl_title + 'title'.length
|
69
|
+
output_line(next_line_num, "RANK | TITLE " + " " * (@total_width - width_excl_title) + "| SCORE | COMMENTS")
|
70
|
+
output_divider(next_line_num)
|
71
|
+
end
|
72
|
+
|
73
|
+
def draw_footer(sorted_by, mean, median, mode)
|
74
|
+
output_divider(next_line_num)
|
75
|
+
attrset color_pair(1)
|
76
|
+
formatted = sprintf("Sorted by: %7s | Scores: Mean: %4.2f | Median: %4.2f | Mode: %4.2f",
|
77
|
+
sorted_by, mean, median, mode)
|
78
|
+
output_line(next_line_num, formatted)
|
79
|
+
output_divider(next_line_num)
|
80
|
+
end
|
81
|
+
|
82
|
+
def draw_item_line(rank, data, selected)
|
83
|
+
|
84
|
+
begin
|
85
|
+
# Truncate if too long
|
86
|
+
title = truncate_line! data
|
87
|
+
|
88
|
+
# Format and output
|
89
|
+
if selected
|
90
|
+
rank = '> ' + rank
|
91
|
+
attrset color_pair(3)
|
92
|
+
else
|
93
|
+
attrset color_pair(0)
|
94
|
+
end
|
95
|
+
|
96
|
+
formatted = sprintf("%4s | %-#{@title_width}s | %5s | %8s", rank, title, data['score'], data['comments'])
|
97
|
+
output_line(next_line_num, formatted)
|
98
|
+
rescue => ex
|
99
|
+
p "error: #{ex.to_s}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def truncate_line!(data)
|
104
|
+
return data['title'][0, @title_width - 3] + '...' if data['title'].length >= @title_width
|
105
|
+
data['title']
|
106
|
+
end
|
107
|
+
|
108
|
+
def show(page_data)
|
109
|
+
draw_header
|
110
|
+
|
111
|
+
page_data.data.each_index do |i|
|
112
|
+
line_data = page_data.data.fetch(i)
|
113
|
+
draw_item_line(line_data['rank'], line_data, page_data.line_pos == i + 1)
|
114
|
+
end
|
115
|
+
|
116
|
+
draw_footer(page_data.sorted_by,
|
117
|
+
page_data.mean_score,
|
118
|
+
page_data.median_score,
|
119
|
+
page_data.mode_score
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_char
|
124
|
+
interpret_char(getch)
|
125
|
+
end
|
126
|
+
|
127
|
+
def interpret_char(c)
|
128
|
+
case c
|
129
|
+
when Curses::Key::UP
|
130
|
+
'up'
|
131
|
+
when Curses::Key::DOWN
|
132
|
+
'down'
|
133
|
+
else
|
134
|
+
c
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def close
|
139
|
+
close_screen
|
140
|
+
end
|
141
|
+
|
142
|
+
def clear!
|
143
|
+
setpos(0, 0)
|
144
|
+
clear
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def defaults
|
150
|
+
@options ||= {}
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/hacker_term.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'hacker_term/page_data'
|
2
|
+
require 'hacker_term/ui'
|
3
|
+
require 'rest_client'
|
4
|
+
require 'launchy'
|
5
|
+
require 'clipboard'
|
6
|
+
|
7
|
+
module HackerTerm
|
8
|
+
class TerminalApp
|
9
|
+
def initialize
|
10
|
+
@raw_json = read_json
|
11
|
+
load
|
12
|
+
end
|
13
|
+
|
14
|
+
def run!
|
15
|
+
clear_and_show
|
16
|
+
|
17
|
+
begin
|
18
|
+
char = @ui.get_char
|
19
|
+
|
20
|
+
case char.to_s.upcase.chomp
|
21
|
+
when "Q"
|
22
|
+
@ui.close
|
23
|
+
exit
|
24
|
+
|
25
|
+
when "UP"
|
26
|
+
@page.change_line_pos :down
|
27
|
+
|
28
|
+
when "DOWN"
|
29
|
+
@page.change_line_pos :up
|
30
|
+
|
31
|
+
when "K"
|
32
|
+
@page.change_line_pos :down
|
33
|
+
|
34
|
+
when "J"
|
35
|
+
@page.change_line_pos :up
|
36
|
+
|
37
|
+
when "O"
|
38
|
+
open_link(@page.selected_url)
|
39
|
+
|
40
|
+
when "D"
|
41
|
+
open_link(@page.selected_comments_url)
|
42
|
+
|
43
|
+
when "A"
|
44
|
+
load
|
45
|
+
@page.change_line_pos :reset
|
46
|
+
|
47
|
+
when "S"
|
48
|
+
@page.sort_on!(:score)
|
49
|
+
|
50
|
+
when "R"
|
51
|
+
@page.sort_on!(:rank)
|
52
|
+
|
53
|
+
when "T"
|
54
|
+
@page.sort_on!(:title)
|
55
|
+
|
56
|
+
when "C"
|
57
|
+
@page.sort_on!(:comments)
|
58
|
+
end
|
59
|
+
|
60
|
+
clear_and_show
|
61
|
+
|
62
|
+
end while true
|
63
|
+
|
64
|
+
0 # Zero exit code means everything was OK...
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def open_link(url)
|
70
|
+
# Attempts to launch a browser; writes URL to clipboard in any case
|
71
|
+
begin
|
72
|
+
Launchy.open url # May not work in some Linux flavors
|
73
|
+
rescue
|
74
|
+
ensure
|
75
|
+
Clipboard.copy url
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def load
|
80
|
+
@page = PageData.new @raw_json
|
81
|
+
@ui = UI.new
|
82
|
+
end
|
83
|
+
|
84
|
+
def read_json
|
85
|
+
local_proxy = get_local_proxy
|
86
|
+
#RestClient.proxy = local_proxy unless local_proxy.nil?
|
87
|
+
RestClient.get 'http://hndroidapi.appspot.com/news/format/json/page/'
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_local_proxy
|
91
|
+
# Cater for both upper and lower case env variables
|
92
|
+
local_proxy = ENV['all_proxy']
|
93
|
+
return local_proxy unless local_proxy.nil?
|
94
|
+
ENV['http_proxy']
|
95
|
+
end
|
96
|
+
|
97
|
+
def clear_and_show
|
98
|
+
@ui.clear!
|
99
|
+
@ui.show @page
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'hacker_term/page_data'
|
2
|
+
|
3
|
+
module HackerTerm
|
4
|
+
describe PageData do
|
5
|
+
describe 'replace missing nodes and format numbers' do
|
6
|
+
before(:each) do
|
7
|
+
@data =
|
8
|
+
'{"items":[
|
9
|
+
{
|
10
|
+
"title":"NextId",
|
11
|
+
"url":"/news2",
|
12
|
+
"description":"hn next id news2 "
|
13
|
+
},
|
14
|
+
{
|
15
|
+
"title":"Ray Kurzweil joins Google",
|
16
|
+
"url":"http://www.kurzweilai.net/kurzweil-joins-google-to-work-on-new-projects-involving-machine-learning-and-language-processing?utm_source=twitterfeed&utm_medium=twitter",
|
17
|
+
"score":"260 points",
|
18
|
+
"user":"dumitrue",
|
19
|
+
"comments":"122 comments",
|
20
|
+
"time":"14 hours ago",
|
21
|
+
"item_id":"4923914",
|
22
|
+
"description":"260 points by dumitrue 14 hours ago | 122 comments"
|
23
|
+
}
|
24
|
+
]}'
|
25
|
+
@pd = PageData.new @data
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'adds score node' do
|
29
|
+
@pd.data.first.should have_key 'score'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'adds comments node' do
|
33
|
+
@pd.data.first.should have_key 'comments'
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'formats score node as a number when the node didn\'t exist' do
|
37
|
+
@pd.data.first['score'].should == '0'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'formats score node as a number when text is present' do
|
41
|
+
@pd.data.last['score'].should == '260'
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'formats comments node as a number when the node didn\'t exist' do
|
45
|
+
@pd.data.first['comments'].should == '0'
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'formats comments node as a number when text is present' do
|
49
|
+
@pd.data.last['comments'].should == '122'
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'calculating stats' do
|
55
|
+
before(:each) do
|
56
|
+
@page_data = HackerTerm::PageData.new File.read './data/data.json'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'provides a mean' do
|
60
|
+
@page_data.mean_score.should == 193.59375
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'provides a median' do
|
64
|
+
@page_data.median_score.should == 135.0
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'provides a mode' do
|
68
|
+
@page_data.mode_score.should == 0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'formatting URLs' do
|
73
|
+
before(:each) do
|
74
|
+
@pg = HackerTerm::PageData.new File.read './data/data.json'
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'provides a URL for actual article' do
|
78
|
+
@pg.selected_url.should == "http://powwow.cc/"
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'provides a URL for article comments' do
|
82
|
+
@pg.selected_comments_url.should == "http://news.ycombinator.com/item?id=4924763"
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'links to HN directly if URL is not absolute' do
|
86
|
+
@pg.data.last['url'].should == 'http://news.ycombinator.com/item?id=4992617'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe 'sorting' do
|
91
|
+
before(:each) do
|
92
|
+
@data =
|
93
|
+
'{"items":[
|
94
|
+
{
|
95
|
+
"title":"First Article",
|
96
|
+
"url":"http://google.com",
|
97
|
+
"score":"0 points",
|
98
|
+
"user":"dumitrue",
|
99
|
+
"comments":"100 comments",
|
100
|
+
"time":"14 hours ago",
|
101
|
+
"item_id":"4923914",
|
102
|
+
"description":"260 points by dumitrue 14 hours ago | 122 comments"
|
103
|
+
},
|
104
|
+
{
|
105
|
+
"title":"Second Article",
|
106
|
+
"url":"http://google.com",
|
107
|
+
"score":"50 points",
|
108
|
+
"user":"dumitrue",
|
109
|
+
"comments":"5 comments",
|
110
|
+
"time":"14 hours ago",
|
111
|
+
"item_id":"4923914",
|
112
|
+
"description":"260 points by dumitrue 14 hours ago | 122 comments"
|
113
|
+
},
|
114
|
+
{
|
115
|
+
"title":"Third Article",
|
116
|
+
"url":"http://google.com",
|
117
|
+
"score":"25 points",
|
118
|
+
"user":"dumitrue",
|
119
|
+
"comments":"0 comments",
|
120
|
+
"time":"14 hours ago",
|
121
|
+
"item_id":"4923914",
|
122
|
+
"description":"260 points by dumitrue 14 hours ago | 122 comments"
|
123
|
+
}
|
124
|
+
]}'
|
125
|
+
@pd = PageData.new @data
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'preserves natural ordering as default' do
|
129
|
+
@pd.data.first['title'].should == 'First Article'
|
130
|
+
@pd.data.last['title'].should == 'Third Article'
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'sorts by score when requested' do
|
134
|
+
@pd.sort_on!(:score)
|
135
|
+
@pd.data.first['title'].should == 'Second Article'
|
136
|
+
@pd.data.last['title'].should == 'First Article'
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'sorts by number of comments when requested' do
|
140
|
+
@pd.sort_on!(:comments)
|
141
|
+
@pd.data.first['title'].should == 'First Article'
|
142
|
+
@pd.data.last['title'].should == 'Third Article'
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'sorts by rank when requested' do
|
146
|
+
@pd.sort_on!(:rank)
|
147
|
+
@pd.data.first['title'].should == 'First Article'
|
148
|
+
@pd.data.last['title'].should == 'Third Article'
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'sorts by title when requested' do
|
152
|
+
@pd.sort_on!(:title)
|
153
|
+
@pd.data.first['title'].should == 'First Article'
|
154
|
+
@pd.data.last['title'].should == 'Third Article'
|
155
|
+
end
|
156
|
+
|
157
|
+
it 're-sorts by rank when requested' do
|
158
|
+
@pd.sort_on!(:comments)
|
159
|
+
@pd.sort_on!(:rank)
|
160
|
+
@pd.data.first['title'].should == 'First Article'
|
161
|
+
@pd.data.last['title'].should == 'Third Article'
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zlx_hacker_term
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- soffolk zhu
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
type: :runtime
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ! '>='
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '0'
|
29
|
+
name: rest-client
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
type: :runtime
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
name: launchy
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
type: :runtime
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
prerelease: false
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
name: clipboard
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
type: :runtime
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
name: socksify
|
78
|
+
description: Read Hacker News on the Terminal
|
79
|
+
email:
|
80
|
+
- zlx.star@gmail.com
|
81
|
+
executables:
|
82
|
+
- hacker_term
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files: []
|
85
|
+
files:
|
86
|
+
- .gitignore
|
87
|
+
- Gemfile
|
88
|
+
- Gemfile.lock
|
89
|
+
- LICENSE.txt
|
90
|
+
- README.md
|
91
|
+
- Rakefile
|
92
|
+
- bin/hacker_term
|
93
|
+
- data/data.json
|
94
|
+
- hacker_term.gemspec
|
95
|
+
- lib/hacker_term.rb
|
96
|
+
- lib/hacker_term/page_data.rb
|
97
|
+
- lib/hacker_term/ui.rb
|
98
|
+
- lib/hacker_term/version.rb
|
99
|
+
- spec/page_data_spec.rb
|
100
|
+
homepage: https://github.com/zlx/hacker_term
|
101
|
+
licenses: []
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ! '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 1.8.24
|
121
|
+
signing_key:
|
122
|
+
specification_version: 3
|
123
|
+
summary: Allows the reading, sorting and opening of HN articles from the terminal.
|
124
|
+
test_files:
|
125
|
+
- spec/page_data_spec.rb
|