-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhackernews
executable file
·183 lines (169 loc) · 5.56 KB
/
hackernews
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#!/usr/bin/env ruby
require 'curses'
require 'httparty'
require 'nokogiri'
require "optparse"
require 'launchy'
include Curses
ENTER_KEY = 10
BASE_URL = 'https://news.ycombinator.com/'
MAX_STORIES = 30
STORY_RELOAD_INTERVAL_MILLIS = 60 * 1000
STORIES_PAGE_BOTTOM_BAR = '[q = Quit] [r = Refresh] [a = Launch Story In Browser] [Enter = View Comments]'
COMMENTS_PAGE_BOTTOM_BAR = '[q = Quit] [Left Arrow = Back] [Up Arrow = Scroll Up] [Down Arrow = Scroll Down]'
class HTTPartyProxy
include HTTParty
end
opts_parser = OptionParser.new do |opts|
opts.banner = "Usage: hackernews [--proxy-host <proxy_host>] [--proxy-port <proxy_port>]"
opts.on("-h", "--proxy-host proxy_host", "The proxy host to use") do |proxy_host|
@proxy_host = proxy_host
end
opts.on("-p", "--proxy-port proxy_port", "The proxy port to use") do |proxy_port|
@proxy_port = proxy_port
end
opts.on("-t", "--test", "Whether to run in test mode (with mocked assets)") do |test|
@is_test_mode = true
end
end
opts_parser.parse!
def get_stories_html
return File.open("mockStories.html", "rb").read if @is_test_mode
HTTPartyProxy.http_proxy(@proxy_host, @proxy_port.to_i) if @proxy_host
return HTTPartyProxy.get(BASE_URL).body
end
def get_comments_html(url)
return File.open("mockComments.html", "rb").read if @is_test_mode
return HTTPartyProxy.get(url).body
end
def load_stories_from_server
return if (@last_load_time_millis ||= 0) > (Time.now.to_i - STORY_RELOAD_INTERVAL_MILLIS)
@stories = []
Nokogiri::HTML(get_stories_html).css("td.title a").each do |node|
unless node.text == "More"
comments_url = node.parent.parent.next_sibling.css("a")[1]['href']
points = node.parent.parent.next_sibling.css("span").text.gsub(/ points/, '')
url = node["href"].start_with?('item') ? BASE_URL + node["href"] : node["href"]
@stories << OpenStruct.new(:points => points, :title => node.text, :url => url, :comments_url => BASE_URL + comments_url)
end
break if @stories.size == MAX_STORIES
end
@last_load_time_millis = Time.now.to_i
end
def load_comments_from_server(url)
comments = []
Nokogiri::HTML(get_comments_html(url)).css('span.comment').each do |node|
comments << OpenStruct.new(:comment => node.text, :indent => (node.parent.parent.css('img')[0]['width'].to_i / 40) + 1)
end
@comment_lines = []
comments.each do |comment|
lines = split_comment_into_lines(comment.comment, @screen_width - comment.indent - 2)
@comment_lines << lines.map{ |line| " #{"|" * comment.indent}#{comment.indent > 0 ? " " : ""}#{line}" } << pad('')
end
@comment_lines.flatten!
end
def split_comment_into_lines(comment, line_length)
lines = []
comment.split("\n").each do |newline|
current_line = ""
newline.split(" ").each do |part|
if (current_line + "#{part} ").length > line_length
lines << current_line
current_line = "#{part }"
else
current_line += "#{part} "
end
end
lines << current_line
end
lines
end
def init_screen
Curses.noecho # do not show typed keys
Curses.init_screen
Curses.stdscr.keypad(true) # enable arrow keys
Curses.start_color
Curses.init_pair(COLOR_RED, COLOR_WHITE, COLOR_RED)
Curses::timeout = 60 * 1000 # How often to fetch from the server if no keys have been pressed
@screen_height = Curses.lines
@screen_width = Curses.cols
@cursor_pos = 0
begin
yield
ensure
Curses.close_screen
end
end
def draw_comments
@comment_lines.each_with_index { |line, index|
line_num = index + 2 + @comment_firstline
write(line_num, 0, pad(line)) if line_num < @screen_height - 2 && line_num >= 2
}
end
def draw_stories
index = 2
@stories.each do |story|
if (index - 2 == @cursor_pos)
Curses.attron(color_pair(COLOR_RED)|A_HORIZONTAL){
write(index, 0, pad(' ' + story.title))
}
else
write(index, 0, pad(' ' + story.title))
end
index += 1
end
end
def write(line, column, text)
Curses.setpos(line, column)
Curses.addstr(text);
end
def pad(message)
message + " " * (@screen_width - message.length)
end
def draw_screen
@current_screen == 'comments' ? draw_comments : draw_stories
draw_title_bar
draw_bottom_bar
end
def draw_title_bar
Curses.attron(color_pair(COLOR_RED)|A_HORIZONTAL){ write(0, 0, pad(" Hacker News")) }
end
def draw_bottom_bar
write(@screen_height - 1, 0, @current_screen == 'comments' ? COMMENTS_PAGE_BOTTOM_BAR : STORIES_PAGE_BOTTOM_BAR)
end
def clear_screen
Curses.clear
Curses.refresh
end
init_screen do
loop do
load_stories_from_server
draw_screen
case Curses.getch
when Curses::Key::UP then
if @current_screen == 'comments'
@comment_firstline = @comment_firstline == 0 ? 0 : @comment_firstline + 1
else
@cursor_pos = @cursor_pos - 1 < 0 ? @stories.length - 1 : @cursor_pos - 1
end
when Curses::Key::DOWN then
if @current_screen == 'comments'
@comment_firstline = (@comment_lines.length + 3 - @screen_height + @comment_firstline) < 0 ? @comment_firstline : @comment_firstline - 1
else
@cursor_pos = (@cursor_pos + 1) % @stories.length
end
when Curses::Key::LEFT then
@current_screen = 'stories'
clear_screen
when ENTER_KEY, Curses::Key::RIGHT then
@current_screen = 'comments'
@comment_firstline = 0
clear_screen
load_comments_from_server(@stories[@cursor_pos].comments_url)
when ?a then
Launchy.open(@stories[@cursor_pos].url)
when ?r then load_stories_from_server
when ?q then break
end
end
end