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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hacker_term.gemspec
4
+ gemspec
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
@@ -0,0 +1,3 @@
1
+ require "bundler/gem_tasks"
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
data/bin/hacker_term ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ require 'hacker_term'
4
+
5
+ app = HackerTerm::TerminalApp.new
6
+ exit app.run!
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
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ module HackerTerm
2
+ VERSION = "0.0.1"
3
+ end
@@ -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