hacker_term 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +24 -0
- data/LICENSE.txt +22 -0
- data/README.md +18 -0
- data/Rakefile +1 -0
- data/bin/hacker_term +6 -0
- data/data/data.json +304 -0
- data/hacker_term.gemspec +22 -0
- data/lib/hacker_term/page_data.rb +112 -0
- data/lib/hacker_term/ui.rb +152 -0
- data/lib/hacker_term/version.rb +3 -0
- data/lib/hacker_term.rb +93 -0
- data/run.rb +5 -0
- data/spec/page_data_spec.rb +147 -0
- metadata +101 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
hacker_term (0.0.1)
|
|
5
|
+
clipboard
|
|
6
|
+
launchy
|
|
7
|
+
rest-client
|
|
8
|
+
|
|
9
|
+
GEM
|
|
10
|
+
remote: https://rubygems.org/
|
|
11
|
+
specs:
|
|
12
|
+
addressable (2.3.2)
|
|
13
|
+
clipboard (1.0.1)
|
|
14
|
+
launchy (2.1.2)
|
|
15
|
+
addressable (~> 2.3)
|
|
16
|
+
mime-types (1.19)
|
|
17
|
+
rest-client (1.6.7)
|
|
18
|
+
mime-types (>= 1.16)
|
|
19
|
+
|
|
20
|
+
PLATFORMS
|
|
21
|
+
ruby
|
|
22
|
+
|
|
23
|
+
DEPENDENCIES
|
|
24
|
+
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,18 @@
|
|
|
1
|
+
hacker_term
|
|
2
|
+
==========
|
|
3
|
+
|
|
4
|
+
Hacker News on the Terminal.
|
|
5
|
+
|
|
6
|
+
See the front page of HN, use the arrow keys to browse and open particular items in the default system browser.
|
|
7
|
+
|
|
8
|
+
* Uses the Ruby `curses` library to create a terminal UI.
|
|
9
|
+
* Captures keyboard events to allow browsing of the HN front page from the terminal.
|
|
10
|
+
* Tested (and looks colourful) on OSX Mountain Lion, but some functionality may be lost on other flavours of Linux.
|
|
11
|
+
* Ditto the above point if using something other than the basic OSX terminal application.
|
|
12
|
+
* Uses the HN feed available at http://hndroidapi.appspot.com - without that resource this project would not exist.
|
|
13
|
+
* Sorting options included.
|
|
14
|
+
* Some stats included.
|
|
15
|
+
|
|
16
|
+
This project was created to allow me to scratch a particular programming itch after reading about https://github.com/etsy/mctop. It brought me back to my days in college coding in C where everything was a terminal program!
|
|
17
|
+
|
|
18
|
+
Please enjoy/contribute/ignore as you see fit.
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/hacker_term
ADDED
data/data/data.json
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
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
|
+
}
|
data/hacker_term.gemspec
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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 = "hacker_term"
|
|
8
|
+
gem.version = HackerTerm::VERSION
|
|
9
|
+
gem.authors = ["Ciaran Archer"]
|
|
10
|
+
gem.email = ["ciaran.archer@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/ciaranarcher/hacker_term"
|
|
14
|
+
gem.add_dependency('rest-client')
|
|
15
|
+
gem.add_dependency('launchy')
|
|
16
|
+
gem.add_dependency('clipboard')
|
|
17
|
+
|
|
18
|
+
gem.files = `git ls-files`.split($/)
|
|
19
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
20
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
21
|
+
gem.require_paths = ["lib"]
|
|
22
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
|
|
20
|
+
calculate_mean_score
|
|
21
|
+
calculate_median_score
|
|
22
|
+
calculate_mode_score
|
|
23
|
+
|
|
24
|
+
@sorted_by = 'RANK'
|
|
25
|
+
@line_pos = 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sort_on!(mode)
|
|
29
|
+
case mode
|
|
30
|
+
when :score
|
|
31
|
+
@data = @data.sort { |a, b| a['score'].to_f <=> b['score'].to_f }
|
|
32
|
+
when :comments
|
|
33
|
+
@data = @data.sort { |a, b| a['comments'].to_f <=> b['comments'].to_f }
|
|
34
|
+
when :rank
|
|
35
|
+
@data = @data.sort { |a, b| a['rank'].to_f <=> b['rank'].to_f }
|
|
36
|
+
when :title
|
|
37
|
+
@data = @data.sort { |a, b| a['title'].upcase <=> b['title'].upcase } # Convert all to upper case when comparing
|
|
38
|
+
else
|
|
39
|
+
throw "sorting mode #{mode} not supported"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@sorted_by = mode.to_s.upcase
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def change_line_pos(direction)
|
|
46
|
+
if direction == :up
|
|
47
|
+
@line_pos += 1 unless @line_pos == @data.length
|
|
48
|
+
elsif direction == :down
|
|
49
|
+
@line_pos -= 1 unless @line_pos == 1
|
|
50
|
+
elsif direction == :reset
|
|
51
|
+
@line_pos = 1
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def selected_url
|
|
56
|
+
@data[@line_pos - 1]['url']
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def calculate_mode_score
|
|
62
|
+
freq = @data.inject(Hash.new(0)) { |h,v| h[v['score'].to_f] += 1; h }
|
|
63
|
+
# Call sort_by on hash to create an array which each contains two elements, the key and value
|
|
64
|
+
# So we grab the last item, and return the 'key' from our original hash
|
|
65
|
+
@mode_score = freq.sort_by { |k, v| v }.last.first
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def calculate_mean_score
|
|
69
|
+
@mean_score = @data.inject(0.0) { |sum, el| sum + el['score'].to_f } / @data.size
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def calculate_median_score
|
|
73
|
+
# Read our numbers and sort them first
|
|
74
|
+
sorted_scores = @data.map { |el| el['score'].to_f }.sort
|
|
75
|
+
len = sorted_scores.length
|
|
76
|
+
@median_score = len % 2 == 1 ? sorted_scores[len / 2] : (sorted_scores[len / 2 - 1] + sorted_scores[len / 2]).to_f / 2
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def add_missing_keys!
|
|
80
|
+
# Here we're looking to fix nodes with missing/incorrect data
|
|
81
|
+
counter = 1
|
|
82
|
+
@data.each do |item|
|
|
83
|
+
|
|
84
|
+
# Add rank (so we can re-sort in 'natural' order)
|
|
85
|
+
unless item.has_key? 'rank'
|
|
86
|
+
item['rank'] = counter.to_s
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
unless item.has_key? 'score'
|
|
90
|
+
item['score'] = '0'
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
unless item.has_key? 'comments'
|
|
94
|
+
item['comments'] = '0'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
counter += 1
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def format_numbers!
|
|
102
|
+
# Assumption here is a format like '10 comments' or '35 points'
|
|
103
|
+
# Also chucks anything left over that isn't a number
|
|
104
|
+
@data.each do |item|
|
|
105
|
+
item['comments'] = item['comments'].split(' ').first if item['comments'].include? ' '
|
|
106
|
+
item['comments'] = '0' unless item['comments'].is_num?
|
|
107
|
+
item['score'] = item['score'].split(' ').first if item['score'].include? ' '
|
|
108
|
+
item['score'] = '0' unless item['score'].is_num?
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
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, "COMMANDS: Select (Arrows), Open (O), Refresh (A) | Sort by Rank (R), Score (S), Comments (C), Title (T) | Quit (Q)")
|
|
56
|
+
output_divider(next_line_num)
|
|
57
|
+
|
|
58
|
+
# Get width_excl_title, i.e. width of all columns + some extra for |'s and spacing.
|
|
59
|
+
# Once obtained, pad out the title column with the any width remaining
|
|
60
|
+
# A nicer way to do this is always put the title last, and assume last column gets
|
|
61
|
+
# remaining width. That way we can just loop through our cols, rather than hardcoding
|
|
62
|
+
# them as per example below. I'm sticking to this because I want the title listed second.
|
|
63
|
+
width_excl_title = @cols.inject(0) do |width, col|
|
|
64
|
+
width += (3 + col.length)
|
|
65
|
+
end
|
|
66
|
+
attrset color_pair(2)
|
|
67
|
+
@title_width = @total_width - width_excl_title + 'title'.length
|
|
68
|
+
output_line(next_line_num, "RANK | TITLE " + " " * (@total_width - width_excl_title) + "| SCORE | COMMENTS")
|
|
69
|
+
output_divider(next_line_num)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def draw_footer(sorted_by, mean, median, mode)
|
|
73
|
+
output_divider(next_line_num)
|
|
74
|
+
attrset color_pair(1)
|
|
75
|
+
formatted = sprintf("Sorted by: %7s | Scores: Mean: %4.2f | Median: %4.2f | Mode: %4.2f",
|
|
76
|
+
sorted_by, mean, median, mode)
|
|
77
|
+
output_line(next_line_num, formatted)
|
|
78
|
+
output_divider(next_line_num)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def draw_item_line(rank, data, selected)
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
# Truncate if too long
|
|
85
|
+
title = truncate_line! data
|
|
86
|
+
|
|
87
|
+
# Format and output
|
|
88
|
+
if selected
|
|
89
|
+
rank = '> ' + rank
|
|
90
|
+
attrset color_pair(3)
|
|
91
|
+
else
|
|
92
|
+
attrset color_pair(0)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
formatted = sprintf("%4s | %-#{@title_width}s | %5s | %8s", rank, title, data['score'], data['comments'])
|
|
96
|
+
output_line(next_line_num, formatted)
|
|
97
|
+
rescue => ex
|
|
98
|
+
p "error: #{ex.to_s}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def truncate_line!(data)
|
|
103
|
+
return data['title'][0, @title_width - 3] + '...' if data['title'].length >= @title_width
|
|
104
|
+
data['title']
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def show(page_data)
|
|
108
|
+
draw_header
|
|
109
|
+
|
|
110
|
+
page_data.data.each_index do |i|
|
|
111
|
+
line_data = page_data.data.fetch(i)
|
|
112
|
+
draw_item_line(line_data['rank'], line_data, page_data.line_pos == i + 1)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
draw_footer(page_data.sorted_by,
|
|
116
|
+
page_data.mean_score,
|
|
117
|
+
page_data.median_score,
|
|
118
|
+
page_data.mode_score
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def get_char
|
|
123
|
+
interpret_char(getch)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def interpret_char(c)
|
|
127
|
+
case c
|
|
128
|
+
when Curses::Key::UP
|
|
129
|
+
'up'
|
|
130
|
+
when Curses::Key::DOWN
|
|
131
|
+
'down'
|
|
132
|
+
else
|
|
133
|
+
c
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def close
|
|
138
|
+
close_screen
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def clear!
|
|
142
|
+
setpos(0, 0)
|
|
143
|
+
clear
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def defaults
|
|
149
|
+
@options ||= {}
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
data/lib/hacker_term.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
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 "O"
|
|
32
|
+
launch
|
|
33
|
+
|
|
34
|
+
when "A"
|
|
35
|
+
load
|
|
36
|
+
@page.change_line_pos :reset
|
|
37
|
+
|
|
38
|
+
when "S"
|
|
39
|
+
@page.sort_on!(:score)
|
|
40
|
+
|
|
41
|
+
when "R"
|
|
42
|
+
@page.sort_on!(:rank)
|
|
43
|
+
|
|
44
|
+
when "T"
|
|
45
|
+
@page.sort_on!(:title)
|
|
46
|
+
|
|
47
|
+
when "C"
|
|
48
|
+
@page.sort_on!(:comments)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
clear_and_show
|
|
52
|
+
|
|
53
|
+
end while true
|
|
54
|
+
|
|
55
|
+
0 # Zero exit code means everything was OK...
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def launch
|
|
61
|
+
# Attempts to launch a browser; writes URL to clipboard in any case
|
|
62
|
+
begin
|
|
63
|
+
Launchy.open @page.selected_url # May not work in some Linux flavors
|
|
64
|
+
rescue
|
|
65
|
+
ensure
|
|
66
|
+
Clipboard.copy @page.selected_url
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def load
|
|
71
|
+
@page = PageData.new @raw_json
|
|
72
|
+
@ui = UI.new
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def read_json
|
|
76
|
+
local_proxy = get_local_proxy
|
|
77
|
+
RestClient.proxy = local_proxy unless local_proxy.nil?
|
|
78
|
+
RestClient.get 'http://hndroidapi.appspot.com/news/format/json/page/'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def get_local_proxy
|
|
82
|
+
# Cater for both upper and lower case env variables
|
|
83
|
+
local_proxy = ENV['HTTP_PROXY']
|
|
84
|
+
return local_proxy unless local_proxy.nil?
|
|
85
|
+
ENV['http_proxy']
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def clear_and_show
|
|
89
|
+
@ui.clear!
|
|
90
|
+
@ui.show @page
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/run.rb
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
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 == 194.19354838709677
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'provides a median' do
|
|
64
|
+
@page_data.median_score.should == 131
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'provides a mode' do
|
|
68
|
+
@page_data.mode_score.should == 0
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe 'sorting' do
|
|
73
|
+
before(:each) do
|
|
74
|
+
@data =
|
|
75
|
+
'{"items":[
|
|
76
|
+
{
|
|
77
|
+
"title":"First Article",
|
|
78
|
+
"url":"http://google.com",
|
|
79
|
+
"score":"0 points",
|
|
80
|
+
"user":"dumitrue",
|
|
81
|
+
"comments":"100 comments",
|
|
82
|
+
"time":"14 hours ago",
|
|
83
|
+
"item_id":"4923914",
|
|
84
|
+
"description":"260 points by dumitrue 14 hours ago | 122 comments"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"title":"Second Article",
|
|
88
|
+
"url":"http://google.com",
|
|
89
|
+
"score":"50 points",
|
|
90
|
+
"user":"dumitrue",
|
|
91
|
+
"comments":"5 comments",
|
|
92
|
+
"time":"14 hours ago",
|
|
93
|
+
"item_id":"4923914",
|
|
94
|
+
"description":"260 points by dumitrue 14 hours ago | 122 comments"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"title":"Third Article",
|
|
98
|
+
"url":"http://google.com",
|
|
99
|
+
"score":"25 points",
|
|
100
|
+
"user":"dumitrue",
|
|
101
|
+
"comments":"0 comments",
|
|
102
|
+
"time":"14 hours ago",
|
|
103
|
+
"item_id":"4923914",
|
|
104
|
+
"description":"260 points by dumitrue 14 hours ago | 122 comments"
|
|
105
|
+
}
|
|
106
|
+
]}'
|
|
107
|
+
@pd = PageData.new @data
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'preserves natural ordering as default' do
|
|
111
|
+
@pd.data.first['title'].should == 'First Article'
|
|
112
|
+
@pd.data.last['title'].should == 'Third Article'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'sorts by score when requested' do
|
|
116
|
+
@pd.sort_on!(:score)
|
|
117
|
+
@pd.data.first['title'].should == 'First Article'
|
|
118
|
+
@pd.data.last['title'].should == 'Second Article'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'sorts by number of comments when requested' do
|
|
122
|
+
@pd.sort_on!(:comments)
|
|
123
|
+
@pd.data.first['title'].should == 'Third Article'
|
|
124
|
+
@pd.data.last['title'].should == 'First Article'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'sorts by rank when requested' do
|
|
128
|
+
@pd.sort_on!(:rank)
|
|
129
|
+
@pd.data.first['title'].should == 'First Article'
|
|
130
|
+
@pd.data.last['title'].should == 'Third Article'
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'sorts by title when requested' do
|
|
134
|
+
@pd.sort_on!(:title)
|
|
135
|
+
@pd.data.first['title'].should == 'First Article'
|
|
136
|
+
@pd.data.last['title'].should == 'Third Article'
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 're-sorts by rank when requested' do
|
|
140
|
+
@pd.sort_on!(:comments)
|
|
141
|
+
@pd.sort_on!(:rank)
|
|
142
|
+
@pd.data.first['title'].should == 'First Article'
|
|
143
|
+
@pd.data.last['title'].should == 'Third Article'
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hacker_term
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
prerelease:
|
|
5
|
+
version: 0.0.1
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Ciaran Archer
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
|
|
13
|
+
date: 2013-01-01 00:00:00 Z
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: rest-client
|
|
17
|
+
prerelease: false
|
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
|
19
|
+
none: false
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: "0"
|
|
24
|
+
type: :runtime
|
|
25
|
+
version_requirements: *id001
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: launchy
|
|
28
|
+
prerelease: false
|
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
|
30
|
+
none: false
|
|
31
|
+
requirements:
|
|
32
|
+
- - ">="
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: "0"
|
|
35
|
+
type: :runtime
|
|
36
|
+
version_requirements: *id002
|
|
37
|
+
- !ruby/object:Gem::Dependency
|
|
38
|
+
name: clipboard
|
|
39
|
+
prerelease: false
|
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: "0"
|
|
46
|
+
type: :runtime
|
|
47
|
+
version_requirements: *id003
|
|
48
|
+
description: Read Hacker News on the Terminal
|
|
49
|
+
email:
|
|
50
|
+
- ciaran.archer@gmail.com
|
|
51
|
+
executables:
|
|
52
|
+
- hacker_term
|
|
53
|
+
extensions: []
|
|
54
|
+
|
|
55
|
+
extra_rdoc_files: []
|
|
56
|
+
|
|
57
|
+
files:
|
|
58
|
+
- .gitignore
|
|
59
|
+
- Gemfile
|
|
60
|
+
- Gemfile.lock
|
|
61
|
+
- LICENSE.txt
|
|
62
|
+
- README.md
|
|
63
|
+
- Rakefile
|
|
64
|
+
- bin/hacker_term
|
|
65
|
+
- data/data.json
|
|
66
|
+
- hacker_term.gemspec
|
|
67
|
+
- lib/hacker_term.rb
|
|
68
|
+
- lib/hacker_term/page_data.rb
|
|
69
|
+
- lib/hacker_term/ui.rb
|
|
70
|
+
- lib/hacker_term/version.rb
|
|
71
|
+
- run.rb
|
|
72
|
+
- spec/page_data_spec.rb
|
|
73
|
+
homepage: https://github.com/ciaranarcher/hacker_term
|
|
74
|
+
licenses: []
|
|
75
|
+
|
|
76
|
+
post_install_message:
|
|
77
|
+
rdoc_options: []
|
|
78
|
+
|
|
79
|
+
require_paths:
|
|
80
|
+
- lib
|
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
|
+
none: false
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: "0"
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
none: false
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: "0"
|
|
93
|
+
requirements: []
|
|
94
|
+
|
|
95
|
+
rubyforge_project:
|
|
96
|
+
rubygems_version: 1.8.10
|
|
97
|
+
signing_key:
|
|
98
|
+
specification_version: 3
|
|
99
|
+
summary: Allows the reading, sorting and opening of HN articles from the terminal.
|
|
100
|
+
test_files:
|
|
101
|
+
- spec/page_data_spec.rb
|