fastreader 1.0.0

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.
@@ -0,0 +1,176 @@
1
+ require 'curses'
2
+ require 'iconv'
3
+ include Curses
4
+
5
+ class CursesController
6
+ include EntriesController
7
+ include FeedsController
8
+ include EntryController
9
+
10
+ def log(statement)
11
+ LOGGER.debug(statement)
12
+ end
13
+
14
+ def show_help(text)
15
+ @command_window.escape_help_prompt
16
+ @help_window = Curses::Window.new(@scr.maxy - 4, @scr.maxx, 0, 0)
17
+ @text_area = Curses::Window.new(@scr.maxy - 4, @scr.maxx - 4, 2, 5)
18
+ @help_window.clear
19
+ @text_area.addstr(PREAMBLE)
20
+ @text_area.addstr(text)
21
+ @text_area.addstr(FOOTER)
22
+ @help_window.refresh
23
+ @text_area.refresh
24
+ @scr.getch
25
+ @text_area.close
26
+ @help_window.close
27
+ end
28
+
29
+ PREAMBLE = <<END
30
+ FastReader Terminal Feed Reader Command Reference
31
+
32
+ END
33
+
34
+ FOOTER = <<END
35
+
36
+ by Daniel Choi, June 2008
37
+ Cambridge, Massachusetts, USA
38
+ dhchoi@gmail.com
39
+
40
+ License: GPL
41
+ END
42
+
43
+
44
+ FEEDS_MENU_COMMANDS = <<-END
45
+ Feed Menu Navigation
46
+
47
+ The standard Vi bindings h, j, k, l move left, down, up, and right. You can
48
+ also use the arrow keys. Right in this case drills down the hierachy
49
+ (goes from the feed list to the entry list for a feed to an entry), while
50
+ left goes back up the hierarchy.
51
+
52
+ You can prefix a up or down movement command with a number to go that many
53
+ steps in that direction.
54
+
55
+ CTRL-f Page down
56
+ CTRL-b Page up
57
+ H Move to top of screen
58
+ M Move to top middle of screen
59
+ L Move to bottom of screen
60
+ G Move to last item
61
+ 1G Move to first item
62
+ l, RIGHT ARROW, RETURN Select a feed and display its entries
63
+
64
+ You can also type the feed's number and press RETURN or RIGHT ARROW to select
65
+ it.
66
+
67
+ Actions
68
+
69
+ q Quit the program
70
+ u Updates the feed; won't update with last update was within the last hour
71
+ U Forces an update of the feed
72
+ CTRL-u Updates all feeds
73
+ * It's better to update the feeds from the command line because the
74
+ feedback if easier to follow. Use the command 'fastreader update'.
75
+ d Deletes the feed. This will also delete any flagged items from this feed.
76
+ a Adds a feed; enter the url of the webpage with a feed autodiscovery link
77
+ or the URL of the feed itself
78
+ / Start a global search; asks for a search string and will find all entry
79
+ items in the database that match
80
+
81
+ END
82
+
83
+ ENTRIES_MENU_COMMANDS = <<-END
84
+ Feed Items Navigation
85
+
86
+ The standard Vi bindings h, j, k, l move left, down, up, and right. You can
87
+ also use the arrow keys. Right in this case drills down the hierachy
88
+ (goes from the feed list to the item list for a feed to an item), while
89
+ left goes back up the hierarchy.
90
+
91
+ You can prefix a up or down movement command with a number to go that many
92
+ steps in that direction.
93
+
94
+ CTRL-f Page down
95
+ CTRL-b Page up
96
+ H Move to top of screen
97
+ M Move to top middle of screen
98
+ L Move to bottom of screen
99
+ G Move to last item
100
+ 1G Move to first item
101
+ l, RIGHT ARROW, RETURN Select an item and display its content
102
+
103
+ Actions
104
+
105
+ q Quit the program
106
+ / Start a local search of the item titles. This lets you jump to a
107
+ particular item quickly.
108
+ s, * Flag an item. Flagged items appears in a virtual feed called
109
+ "Flagged Entries."
110
+ A Flag all items in the list
111
+ Z Unflag all items in the list
112
+ D Delete all the items from the cursor position downward. Flagged items will
113
+ be preserved.
114
+ u Updates the feed; won't update with last update was within the last hour
115
+ U Forces an update of the feed
116
+ END
117
+
118
+ ENTRY_PAGE_COMMANDS = <<-END
119
+ Feed Item Content Navigation
120
+
121
+ j, DOWN ARROW, CTRL-f Page down (if there is more than one page)
122
+ k, UP ARROW, CTRL-b Page up
123
+ h, LEFT ARROW Go back to entries list
124
+ l, RIGHT ARROW, RETURN Go to the web page linked to this entry.
125
+
126
+ By default, the command to open the web browser is "open {url}." This works out
127
+ of the box on Mac OS X. You can change this command by setting your shell's
128
+ FASTREADER_WEB environmental variable. Currently, in-terminal text browsers like
129
+ elinks do not work with Fastreader. Hopefully this will change.
130
+
131
+
132
+ >, . Go to next entry in the list
133
+ <, , Go to previous entry in the list
134
+
135
+ Actions
136
+
137
+ q Quit the program
138
+ s, * Flag an entry. Flagged entries appears in a virtual feed called
139
+ "Flagged Entries."
140
+ \\ Show raw entry content markup
141
+
142
+ END
143
+
144
+ def initialize(formatter)
145
+ # Formatter is the Display class. Should change the variable names to match the class
146
+ # names or vice versa.
147
+ @formatter = formatter
148
+ @scr = init_screen
149
+ noecho
150
+ cbreak
151
+ Color.init
152
+ curs_set(0)
153
+ @scr.keypad = true
154
+ log "Screen width: #{@scr.maxx} | Screen height: #{@scr.maxy}."
155
+ log "Command window initilized."
156
+ end
157
+
158
+ def key_buffer(initial_char)
159
+ c = initial_char
160
+ buffer = []
161
+ printable = (33..126).to_a
162
+ while printable.include?(c) && c.chr =~ /\d/
163
+ LOGGER.debug("detecting a multiplier: #{c.chr}")
164
+ # TODO This code doesn't work right
165
+ buffer << c.chr.to_i
166
+ c = @scr.getch
167
+ end
168
+ return c, buffer
169
+ end
170
+
171
+ def control_key(character)
172
+ character[0] - 64
173
+ end
174
+
175
+ end
176
+
@@ -0,0 +1,161 @@
1
+ # This code is taken from Takashi Nakamoto's GPLType example program
2
+
3
+ #################################################################
4
+ # Curses::Key
5
+ #################################################################
6
+ module Curses::Key
7
+ BS = 8
8
+ HT = 9
9
+ LF = 10
10
+ CR = 13
11
+ ESC = 27
12
+ SP = 32
13
+ DEL = 127
14
+
15
+ KEY_GROUP = {
16
+ :backspace => [BS, BACKSPACE, DEL],
17
+ :escape => [ESC, BREAK],
18
+ :space => [SP, HT],
19
+ :enter => [ENTER, LF, CR],
20
+ :printable => (33..126).to_a,
21
+ }
22
+
23
+ def match?(key_type, key_code)
24
+ if key_type.is_a?(Array)
25
+ key_type.map{ |type| KEY_GROUP[type].include?(key_code)}.include?(true)
26
+ elsif key_type.is_a?(Symbol)
27
+ KEY_GROUP[key_type].include?(key_code)
28
+ else
29
+ false
30
+ end
31
+ end
32
+ module_function :match?
33
+ end
34
+
35
+ #################################################################
36
+ # Curses::Color
37
+ #################################################################
38
+ module Curses::Color
39
+ RED = Curses::COLOR_RED
40
+ BLUE = Curses::COLOR_BLUE
41
+ GREEN = Curses::COLOR_GREEN
42
+ YELLOW = Curses::COLOR_YELLOW
43
+ MAGENTA = Curses::COLOR_MAGENTA
44
+ CYAN = Curses::COLOR_CYAN
45
+ WHITE = Curses::COLOR_WHITE
46
+ BLACK = Curses::COLOR_BLACK
47
+
48
+ COLOR_STYLES = {
49
+ # :name => [foreground, background],
50
+ :default => [WHITE, BLACK],
51
+ :red => [RED, BLACK],
52
+ :blue => [BLUE, BLACK],
53
+ :green => [GREEN, BLACK],
54
+ :yellow => [YELLOW, BLACK],
55
+ :magenta => [MAGENTA, BLACK],
56
+ :cyan => [CYAN, BLACK],
57
+ :wrong => [RED, BLACK],
58
+ :wrong_space => [RED, RED],
59
+ :correct => [BLUE, BLACK],
60
+ :ok => [CYAN, BLACK],
61
+ :highscore => [GREEN, BLACK],
62
+ :search_string => [WHITE, BLUE],
63
+ :mistype => [RED, BLACK],
64
+ }
65
+
66
+ @@color_pair = {}
67
+ @@use_color = false
68
+
69
+ def init
70
+ @@use_color = Curses.has_colors?
71
+ return if @@use_color == false
72
+
73
+ Curses::start_color # background becomes black
74
+
75
+ n = 1
76
+ defined_pair = {}
77
+
78
+ COLOR_STYLES.each do |name, pair|
79
+ if defined_pair.include?(pair)
80
+ @@color_pair[name] = Curses::color_pair(defined_pair[pair])
81
+ else
82
+ Curses::init_pair(n, pair[0], pair[1])
83
+ @@color_pair[name] = Curses::color_pair(n)
84
+ defined_pair[pair] = n
85
+ n += 1
86
+ end
87
+ end
88
+ end
89
+ module_function :init
90
+
91
+ # Return the result of Curses::color_pair, or nil if the specified
92
+ # name is not defined or the terminal doesn't support color
93
+ def pair(name)
94
+ @@use_color ? @@color_pair[name] : nil
95
+ end
96
+ module_function :pair
97
+ end
98
+
99
+ #################################################################
100
+ # Curses::Window
101
+ #################################################################
102
+ class Curses::Window
103
+ alias :width :maxx
104
+ alias :height :maxy
105
+
106
+ alias :_orig_addstr :addstr
107
+ def addstr(str, color_name = nil, align = nil, ref = false)
108
+ # color style on
109
+ if color_name && Curses::Color.pair(color_name)
110
+ attron(Curses::Color.pair(color_name))
111
+ end
112
+
113
+ # set alignment
114
+ case(align)
115
+ when :left
116
+ setpos(cury, 0)
117
+ when :right
118
+ setpos(cury, width - str.size)
119
+ when :center
120
+ setpos(cury, (width - str.size) / 2)
121
+ end
122
+
123
+ # draw
124
+ _orig_addstr(str)
125
+
126
+ # color style off
127
+ if color_name && Curses::Color.pair(color_name)
128
+ attroff(Curses::Color.pair(color_name))
129
+ end
130
+
131
+ # refresh is required
132
+ refresh if ref
133
+ end
134
+
135
+ def next_line
136
+ setpos(cury + 1, 0)
137
+ end
138
+
139
+ def set_title(title, align = :center)
140
+ return if width <= 6 # too small to show title
141
+
142
+ orig_cur = {:x => curx, :y => cury}
143
+
144
+ # shrink title
145
+ title = title[0...(width-6)] if (title.size + 6) > width
146
+
147
+ case(align)
148
+ when :left
149
+ setpos(0, 2)
150
+ when :right
151
+ setpos(0, width - title.size - 4)
152
+ else # :center
153
+ setpos(0, (width - title.size) / 2 - 1)
154
+ end
155
+ addstr(" #{title} ")
156
+
157
+ setpos(orig_cur[:y], orig_cur[:x])
158
+ end
159
+ end
160
+
161
+
data/lib/display.rb ADDED
@@ -0,0 +1,232 @@
1
+ require File.dirname(__FILE__) + '/htmlentities'
2
+ require 'hpricot'
3
+ require 'highline/import'
4
+ require 'active_support/core_ext/string/unicode'
5
+ # This class formats stuff for human consumption
6
+
7
+ class Display
8
+ include CharacterCleaner
9
+
10
+ attr_reader :width
11
+ # In the future, may allow configuration options here, so that you can
12
+ # customize the Display object.
13
+ def initialize(options={})
14
+ # could opt for :no_links
15
+ @options = options
16
+ @width = options[:width] || 80
17
+ end
18
+
19
+ def list_feeds(feeds)
20
+ if @options[:curses]
21
+ CursesController.new(self).show_feeds(feeds)
22
+ else
23
+ feeds.each {|f| display_feed(f)}
24
+ end
25
+ end
26
+
27
+
28
+ def display_entries(entries)
29
+ if @options[:curses]
30
+ CursesController.new(self).show_entries(entries)
31
+ else
32
+ entries.each do |e|
33
+ puts '-' * @width
34
+ puts display_entry(e)
35
+ end
36
+ end
37
+ end
38
+
39
+ def display_entry(entry, show_title_and_feed = true)
40
+ out = []
41
+ if show_title_and_feed
42
+ out << "#{entry.feed.title} | #{entry.last_updated.strftime('%A, %B %d %Y')}"
43
+ out << wrap_text(html_to_text(entry.title.strip))
44
+ end
45
+
46
+ # description is the summary, so mark it as such
47
+ if entry.description && entry.content && (entry.description.strip.length < entry.content.strip.length)
48
+ out << "Entry Summary:\n\n" + html_to_text(entry.description).strip
49
+ out << divider.strip
50
+ out << "Entry Content:"
51
+ end
52
+
53
+ # If there is no content, just print the description
54
+ if entry.content.nil? || entry.content.strip == ''
55
+ out << html_to_text(entry.description || '').strip
56
+ else
57
+ out << html_to_text(entry.content || '').strip
58
+ end
59
+
60
+ unless @options[:simple]
61
+ out << entry.url
62
+ end
63
+ unless entry.categories.empty?
64
+ out << "Categories: #{entry.categories.join(", ")}"
65
+ end
66
+ out.join("\n\n")
67
+ end
68
+
69
+ def display_title_and_feed(entry)
70
+ out = []
71
+ out << "#{entry.feed.title} | #{entry.last_updated.strftime('%A, %B %d %Y')}"
72
+ out << wrap_text(html_to_text(entry.title.strip))
73
+ out.join("\n\n")
74
+ end
75
+
76
+ def display_title(entry)
77
+ wrap_text(html_to_text(entry.title.strip))
78
+ end
79
+
80
+ def display_feed(entry)
81
+ "#{entry.feed.title} | #{entry.date_published.strftime('%A, %B %d %Y')}"
82
+ end
83
+
84
+ def display_raw_entry_content(entry)
85
+ out = []
86
+ if entry.description
87
+ out << divider + "Entry Summary\n\n" + entry.description
88
+ end
89
+ if entry.content
90
+ out << "Entry Content\n\n" + entry.content
91
+ end
92
+ wrap_text(out.join(divider)).strip
93
+ end
94
+
95
+ def html_to_text(html)
96
+ LOGGER.debug("html_to_text:\n #{html}")
97
+ html.strip!
98
+ # convert utf-8 to ascii
99
+
100
+ html = process_entities_and_utf(html)
101
+
102
+ # if there are no tags, and this is a body, wrap what looks like paragraph
103
+ # in paragarph tags, so it can be processed with the html paragraph rule
104
+ # below.
105
+ if html =~ /\n/ && html !~ /<[^>]+>/ && html !~ /<\/[^>]+>/
106
+ html = html.split("\n\n").collect {|x| "<p>#{x}</p>"}.join("\n")
107
+ end
108
+
109
+ html, *links = links_to_footnotes(html)
110
+ html = tags_to_text(html)
111
+ html = normalize_blank_lines(html)
112
+ html = wrap_text(html)
113
+ html = [html, links.join("\n")].join("\n\n")
114
+ #puts out
115
+ #out = normalize_blank_lines(out)
116
+ html.strip
117
+ end
118
+
119
+ # Make sure there is no more than one blank line anywhere
120
+ def normalize_blank_lines(text)
121
+ # get rid of ms line feeds
122
+ text.gsub(/\r\n/, "\n").
123
+ # compress 3 or more blank lines to one
124
+ gsub(/^\s*$/, "\n").
125
+ split(/\n\n\n*/).join( "\n\n")
126
+ # hflush everything left to begin with
127
+ #gsub(/^\s+(\w)/, '\1')
128
+ end
129
+
130
+ def tags_to_text(html)
131
+ doc = Hpricot(html)
132
+
133
+ doc.search('//comment()').remove
134
+
135
+ doc.search('p, div') do |p|
136
+ p.swap( "\n\n" + p.inner_text.gsub("\n", ' ').squeeze(' ').strip + "\n\n" )
137
+ end
138
+
139
+ doc.search('//blockquote') do |x|
140
+ # compress extra spaces
141
+ text = x.inner_text.squeeze(' ').strip
142
+ # collapse the spacing in the text
143
+ text.gsub!(/\s{2,}/, ' ')
144
+ text = wrap_text(text, @width - 4).gsub(/^/, ' ') # indent 4 spaces
145
+ x.swap("\n\n" + text + "\n\n")
146
+ end
147
+
148
+ doc.search('h1,h2,h3,h4') do |p|
149
+ p.swap( "\n\n= #{p.inner_text}\n\n" )
150
+ end
151
+ doc.search('//img') do |img|
152
+ img.swap( "(img)" )
153
+ end
154
+
155
+ doc.search('object').remove
156
+ doc.search('table').remove
157
+ doc.search('script').remove
158
+
159
+ doc.search('//br') do |p|
160
+ p.swap( "\n" )
161
+ end
162
+
163
+ doc.search('i, b') do |p|
164
+ p.swap( "*#{p.inner_text}*" )
165
+ end
166
+ # anchor tags are processed after real links
167
+ doc.search('a') do |p|
168
+ p.swap( "#{p.inner_text}" )
169
+ end
170
+ doc.search('dt') do |x|
171
+ x.swap( "#{x.inner_html}:\n" )
172
+ end
173
+ # This could be improved to insure an indentation even if there are nested
174
+ # tag elements
175
+ doc.search('dd') do |x|
176
+ x.swap( "#{x.inner_text}\n" )
177
+ end
178
+
179
+
180
+ doc.search('//span') do |s|
181
+ s.swap( s.inner_text )
182
+ end
183
+ doc.search('hr') do |s|
184
+ s.swap( '-' * @width)
185
+ end
186
+ # Do this before erasing the enclosing <ul> or <ol> tags
187
+ doc.search('li') do |s|
188
+ text = s.inner_text.strip
189
+ # wrap the text and indent it 2 spaces
190
+ text = wrap_text(text, @width - 2).gsub(/^/, ' ')
191
+ # don't indent 1st line
192
+ text.lstrip!
193
+ s.swap( "* " + text + "\n" )
194
+ end
195
+ doc.search('ul, ol, dl') do |s|
196
+ s.swap( s.inner_text.strip )
197
+ end
198
+ doc.to_s
199
+ end
200
+
201
+ def links_to_footnotes(html)
202
+ doc = Hpricot(html)
203
+ footnotes = []
204
+ doc.search('//a[@href]') do |link|
205
+ if @options[:no_links]
206
+ link.swap(
207
+ if link.inner_text == ''
208
+ ''
209
+ else
210
+ "[#{link.inner_text}]"
211
+ end
212
+ )
213
+ else
214
+ href = link.attributes['href']
215
+ footnotes << " [#{footnotes.size + 1}] #{href}"
216
+ link.swap(link.inner_text + "[#{footnotes.size}]")
217
+ end
218
+ end
219
+ [doc.to_s, *footnotes]
220
+ end
221
+
222
+ # From
223
+ # http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/
224
+ def wrap_text(txt, col = @width)
225
+ txt.chars.gsub(/(.{1,#{col}})( +|$\n?)|(.{1,#{col}})/, "\\1\\3\n")
226
+ end
227
+
228
+
229
+ def divider
230
+ "\n" + '-' * @width + "\n"
231
+ end
232
+ end