zlx_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 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