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,270 @@
1
+ module EntriesController
2
+
3
+ def show_entries
4
+ begin
5
+ @command_window = CommandWindow.new( @scr )
6
+
7
+ @current_entry_index ||= 0
8
+ @scr.refresh # or else the initial screen is blank, I don't know why yet
9
+
10
+ @current_feed_title = @global_search_string ? "Entries matching '#{@global_search_string}'" : @current_feed.title
11
+ @options = {}
12
+ if (@current_feed.is_a?(VirtualFeed) || @global_search_string)
13
+ @options[:show_feed_titles] = true
14
+ @options[:feed_title] = @current_feed.title
15
+ end
16
+ if @current_feed.is_a?(VirtualFeed)
17
+ #@options[:turn_off_flagged_color] = true
18
+ end
19
+
20
+ @content_area = MenuWindow.new(@current_feed_title, @scr, @current_entries, @current_entry_index, nil, @options)
21
+ if @search_string
22
+ show_matches
23
+ elsif @global_search_string # This is a global search
24
+ show_global_matches
25
+ @command_window.help_prompt
26
+ else
27
+ @command_window.help_prompt
28
+ end
29
+
30
+
31
+ loop do
32
+ parse_entries_list_command
33
+ end
34
+ end
35
+ end
36
+
37
+ def parse_entries_list_command
38
+ LOGGER.debug "Waiting for entries list page command"
39
+ c = @scr.getch
40
+ # store number in buffer, vim style
41
+ c, buffer = key_buffer(c)
42
+ multiplier = buffer.empty? ? 1 : buffer.join.to_i
43
+ LOGGER.debug("Char: #{c}")
44
+ LOGGER.debug("Multiplier: #{multiplier}")
45
+
46
+ case c
47
+ when ?\? # help
48
+ show_help(CursesController::ENTRIES_MENU_COMMANDS)
49
+ # Any key restores the menu
50
+ redraw
51
+
52
+ when ?\q
53
+ exit
54
+
55
+ when ?\u # UPDATE FEEDS
56
+
57
+ if @current_feed && !@current_feed.is_a?(VirtualFeed)
58
+ @command_window.feed_update_progress(@current_feed)
59
+ @content_area.draw_menu
60
+ end
61
+
62
+ when ?\U # UPDATE FEEDS, forcing update even if soon after last update
63
+
64
+ if @current_feed && !@current_feed.is_a?(VirtualFeed)
65
+ @command_window.feed_update_progress(@current_feed, true)
66
+ @content_area.draw_menu
67
+ end
68
+
69
+ # SEARCH
70
+ when ?\/
71
+ search
72
+
73
+ when 27 # escape key
74
+ # Cancel any search
75
+ cancel_search
76
+
77
+ when ?\D # delete everything from the cursor position downward
78
+ items_to_delete = @items[@content_area.current_index..-1]
79
+ num = 0
80
+ items_to_delete.each do |item|
81
+ item.destroy unless item.flagged
82
+ num += 1 unless item.flagged
83
+ end
84
+ remake_window(true, true)
85
+ @command_window.command_line = "#{num} items deleted"
86
+ @command_window.draw
87
+
88
+ when Key::LEFT, ?h, '-'[0] # Go up to feeds list
89
+
90
+ # if this is a search result list, cancel the search
91
+ @search_string = nil
92
+ @global_search_string = nil
93
+ @entries_list_matches = nil
94
+
95
+ # Go to feeds list
96
+ @content_area.close
97
+ @content_area = nil
98
+ show_feeds
99
+
100
+ when Key::UP, ?k, ?p, ?N
101
+ return if @current_entries.empty? # nowhere to go
102
+
103
+ if @entries_list_matches.nil? || @entries_list_matches.empty?
104
+ @content_area.prev_item(multiplier)
105
+ else
106
+ # Constrain movement to matches
107
+ @content_area.move_to_prev_item_in_set(@entries_list_matches)
108
+ @current_entry_index = @content_area.current_index
109
+ end
110
+
111
+ when Key::DOWN, ?j, ?\s, ?n
112
+ return if @current_entries.empty? # nowhere to go
113
+
114
+ if @entries_list_matches.nil? || @entries_list_matches.empty?
115
+ @content_area.next_item(multiplier)
116
+ else
117
+ # Constrain movement to matches
118
+ @content_area.move_to_next_item_in_set(@entries_list_matches)
119
+ @current_entry_index = @content_area.current_index
120
+ end
121
+
122
+ when Key::RESIZE
123
+ remake_window
124
+
125
+ when Key::RIGHT, ?l, 10 # Enter key
126
+ return if @current_entries.empty? # nothing to see
127
+
128
+ # Cancel any search
129
+ # Don't cancel search
130
+ # cancel_search
131
+ LOGGER.debug("Enter key pressed")
132
+ @current_entry = @items[@content_area.current_index]
133
+ @current_entry_index = @content_area.current_index
134
+ LOGGER.debug("Selected entry #{@current_entry.title}")
135
+
136
+ # remove the menu page
137
+
138
+ @content_area.close
139
+ @content_area = nil
140
+ @command_window.close
141
+ @command_window = nil
142
+ # Show the entry content
143
+ show_entry
144
+
145
+ when '*'[0], 's'[0] # Flag the entry
146
+ @content_area.toggle_flag
147
+
148
+ when 'A'[0] # Flag all
149
+ @content_area.flag_all
150
+ remake_window
151
+
152
+ when 'Z'[0] # Unflag all
153
+ @content_area.unflag_all
154
+ remake_window
155
+
156
+ end
157
+
158
+ # These movement keys are grouped together because
159
+ # sometimes the movement need to be disabled as a group
160
+ if @entries_list_matches.nil?
161
+ case c
162
+ when 'M'[0] # middle of screen
163
+ @content_area.middle
164
+ when 'L'[0] # bottom of screen
165
+ @content_area.bottom
166
+ when 'H'[0] # top of screen
167
+ @content_area.top
168
+ when 'G'[0] # beginning or of list
169
+ if buffer.last == 1
170
+ @content_area.end
171
+ else
172
+ @content_area.beginning
173
+ end
174
+ when control_key('F')
175
+ @content_area.next_page
176
+ when control_key('B')
177
+ @content_area.prev_page
178
+ else
179
+ # don't do anything
180
+ end
181
+ end
182
+ end
183
+
184
+ def remake_window(reload_entries=false, put_index_at_end = false)
185
+ if reload_entries
186
+ @items = @current_feed.reload.entries
187
+ @current_index = put_index_at_end ? @items.size - 1 : 0
188
+ sleep 0.4
189
+ else
190
+ @items = @content_area.items
191
+ @current_index = @content_area.current_index
192
+ sleep 0.4
193
+ end
194
+ @content_area.close
195
+ @content_area = MenuWindow.new(@current_feed_title, @scr, @items, @current_index, nil, @options)
196
+ if @global_search_string # This is a global search
197
+ show_global_matches
198
+ end
199
+
200
+ end
201
+
202
+ def find_matches(string)
203
+ matches = []
204
+ @current_entries.each_with_index do |e, i|
205
+ LOGGER.debug("Trying to search, comparing #{string} to #{e.title}")
206
+ if e.title =~ /#{string.strip}/i
207
+ matches << i
208
+ end
209
+ end
210
+ matches
211
+ end
212
+
213
+ def search
214
+ # Search mode: Searches through all the titles in the list and highlights
215
+ # the matches, and moves the cursor to the first match (from top)
216
+ # First, we need to show the search prompt at the bottom.
217
+
218
+ @search_string = @command_window.get_search_string.strip
219
+ if @search_string == '' || @search_string =~ /^\W*$/
220
+ @command_window.command_line = "Found no matches"
221
+ end
222
+ @entries_list_matches = find_matches(@search_string)
223
+ if @entries_list_matches.empty?
224
+ @command_window.command_line = "Found no matches"
225
+ else
226
+ # Matches
227
+ show_matches
228
+
229
+ # Color highlighting is hard, so for now, we'll just constrain the
230
+ # movement up and down to the matches and
231
+ # move the cursor to the first match.
232
+ @content_area.move_to_item(@entries_list_matches[0])
233
+ end
234
+ @command_window.draw
235
+ end
236
+
237
+ def show_matches
238
+ return unless @entries_list_matches && !@entries_list_matches.empty?
239
+ @command_window.command_line = "Found #{pluralize_matches(@entries_list_matches.size)} " +
240
+ "for '#{@search_string}'. You can cancel the search by pressing ESC."
241
+ @command_window.draw
242
+ @content_area.highlight_matches(@entries_list_matches, @search_string)
243
+ end
244
+
245
+ def show_global_matches
246
+ @content_area.highlight_global_matches((0..(@items.length - 1)).to_a, @global_search_string)
247
+ end
248
+
249
+ def cancel_search
250
+ @entries_list_matches = nil
251
+ @search_string = nil
252
+ @content_area.cancel_search
253
+ remake_window
254
+ if @command_window
255
+ @command_window.help_prompt
256
+ end
257
+ end
258
+
259
+ def pluralize_matches(number)
260
+ case number
261
+ when 1
262
+ "1 match"
263
+ when 0
264
+ "no matches"
265
+ else
266
+ "#{number} matches"
267
+ end
268
+ end
269
+
270
+ end
data/lib/entry.rb ADDED
@@ -0,0 +1,21 @@
1
+ class Entry < ActiveRecord::Base
2
+ serialize :urls
3
+ serialize :authors
4
+ serialize :categories
5
+ belongs_to :feed
6
+
7
+ def is_new?
8
+ LOGGER.debug("is_new?")
9
+ LOGGER.debug(self.title)
10
+ LOGGER.debug(self.date_published.to_s)
11
+ LOGGER.debug(self.date_published.class.to_s)
12
+ LOGGER.debug(self.feed.previously_updated_at.to_s)
13
+ LOGGER.debug(self.feed.previously_updated_at.class.to_s)
14
+ LOGGER.debug( (self.date_published > self.feed.previously_updated_at).to_s )
15
+ self.date_published >= self.feed.previously_updated_at
16
+ end
17
+
18
+ def date_published
19
+ self['date_published'] || self.created_at
20
+ end
21
+ end
@@ -0,0 +1,131 @@
1
+ module EntryController
2
+
3
+ def show_entry
4
+ begin
5
+ LOGGER.debug("initializing EntryWindow")
6
+
7
+ @command_window = CommandWindow.new( @scr )
8
+
9
+ # The formatter formats a blog entry but does not paginate it
10
+
11
+
12
+ set_content_area
13
+ @command_window.help_prompt
14
+ begin
15
+ @content_area.draw
16
+ end while parse_entry_page_command != :exit
17
+ end
18
+ end
19
+
20
+ def set_content_area
21
+ @content_area = EntryWindow.new(@scr,
22
+ @formatter.display_feed(@current_entry),
23
+ @formatter.display_title(@current_entry),
24
+ @formatter.display_entry(@current_entry, false),
25
+ @current_entry, @global_search_string)
26
+ end
27
+
28
+
29
+ def parse_entry_page_command
30
+ LOGGER.debug "Waiting for entry page command"
31
+ c = @scr.getch
32
+ LOGGER.debug("Entry Page getch: #{c}")
33
+ c, buffer = key_buffer(c)
34
+ multiplier = buffer.empty? ? 1 : buffer.join.to_i
35
+
36
+ case c
37
+ when "?"[0] # help
38
+ @content_area.close
39
+ show_help(CursesController::ENTRY_PAGE_COMMANDS)
40
+ # Any key restores the menu
41
+
42
+ @scr.clear
43
+ @scr.refresh
44
+ set_content_area
45
+ @content_area.draw
46
+ return
47
+ when ?\q
48
+ exit
49
+
50
+ # go back to entries list
51
+ when '-'[0], Key::LEFT, ?h
52
+ @content_area.close
53
+ show_entries
54
+ return :exit
55
+
56
+ when ?\/
57
+ # search mode
58
+ # TODO
59
+ when Key::UP, ?k, ?p, control_key('B')
60
+ if @content_area.prev_page == :exit
61
+ # go back to entries list
62
+ @content_area.close
63
+ show_entries
64
+ return :exit
65
+ end
66
+ when Key::DOWN, ?j, ?\s, ?n, control_key('F')
67
+ if @content_area.next_page == :exit
68
+ # go back to entries list
69
+ @content_area.close
70
+ show_entries
71
+ return :exit
72
+ end
73
+ when '<'[0], ','[0] # Previous entry
74
+ if @current_entry_index > 0
75
+ @current_entry_index -= 1
76
+ @current_entry = @current_entries[@current_entry_index]
77
+ @content_area.close
78
+ set_content_area
79
+ end
80
+ when '>'[0], '.'[0] # Next entry
81
+ if @current_entry_index < @current_entries.size - 1
82
+ @current_entry_index += 1
83
+ @current_entry = @current_entries[@current_entry_index]
84
+ @content_area.close
85
+ set_content_area
86
+ end
87
+
88
+ when Key::RESIZE
89
+ content = @content_area.content
90
+ @content_area.close
91
+ set_content_area
92
+ @content_area.content = content
93
+
94
+ when '*'[0], 's'[0] # Flag the entry
95
+ @content_area.toggle_flag
96
+
97
+ when '\\'[0]
98
+ unless @raw_content_mode
99
+ # Show raw HTML for entry content
100
+ @content_area.close
101
+ @content_area = EntryWindow.new(@scr,
102
+ @formatter.display_feed(@current_entry),
103
+ @formatter.display_title(@current_entry),
104
+ @formatter.display_raw_entry_content(@current_entry),
105
+ @current_entry, @global_search_string)
106
+ @raw_content_mode = true
107
+ else # go back to normal mode
108
+ @content_area.close
109
+ set_content_area
110
+ @raw_content_mode = false
111
+ end
112
+
113
+ when ?l, Key::RIGHT, 10 # Enter key
114
+ # TODO
115
+ # open web browser
116
+ command = (ENV['FASTREADER_WEB'] || "open") + " #{@current_entry.url.strip}"
117
+ `#{command}`
118
+ when 'G'[0] # beginning or of list
119
+ if buffer.last == 1
120
+ @content_area.end
121
+ else
122
+ @content_area.beginning
123
+ end
124
+ else
125
+ # don't do anything
126
+ end
127
+ end
128
+
129
+
130
+ end
131
+
@@ -0,0 +1,150 @@
1
+ require 'iconv'
2
+
3
+ class EntryWindow
4
+ include CharacterCleaner
5
+
6
+ attr_accessor :content
7
+
8
+ def initialize(scr, feed_title, title, content, item, search_string=nil)
9
+ @item = item
10
+ @height = scr.maxy - 10 # This is the height on the content area. The title box is separate
11
+ @width = scr.maxx - 2
12
+
13
+ @feed_title = feed_title
14
+ @search_string = search_string
15
+
16
+ @title = title
17
+ @title_box = Curses::Window.new(3, @width, 2, 5)
18
+
19
+ @window = Curses::Window.new(@height, @width, 7, 5)
20
+
21
+ @content = content
22
+
23
+ @lines = content.split("\n")
24
+ @page_index = 0
25
+ @num_pages = (@lines.size / @height) + 1
26
+ #
27
+ #scr.clear
28
+ end
29
+
30
+
31
+ # TODO add a page range
32
+ def draw_title
33
+ @title_box.clear
34
+
35
+ @title_box.addstr(@feed_title)
36
+ @title_box.setpos(2, 0)
37
+ color = case
38
+ when @item.flagged
39
+ :red
40
+ else
41
+ nil
42
+ end
43
+
44
+ if @search_string && @title =~ /#{@search_string}/i
45
+ text = @title.gsub(/(#{@search_string})/i, '%%%\1%%%')
46
+ text.split(/%%%/).each_with_index do |chunk, index|
47
+ if index % 2 == 0
48
+ @title_box.addstr(chunk, color)
49
+ else
50
+ @title_box.addstr(chunk, :yellow)
51
+ end
52
+ end
53
+ else
54
+ @title_box.addstr(@title, color)
55
+ end
56
+
57
+ @title_box.addstr(" (%s/%s)" % [@page_index + 1 , @num_pages], :cyan)
58
+ @title_box.refresh
59
+ end
60
+
61
+
62
+ def addstr_with_highlighting(string, highlighted_string)
63
+ text = string
64
+ text = text.gsub(/(#{highlighted_string})/i, '%%%\1%%%')
65
+ text.split(/%%%/).each_with_index do |chunk, index|
66
+ if index % 2 == 0
67
+ @window.addstr(chunk)
68
+ else
69
+ @window.addstr(chunk, :yellow)
70
+ end
71
+ end
72
+
73
+ end
74
+
75
+ def draw
76
+ draw_title
77
+ @window.setpos(0, 0)
78
+ @window.clear
79
+ if @search_string
80
+ addstr_with_highlighting(page_content, @search_string)
81
+ else
82
+ @window.addstr(page_content)
83
+ end
84
+
85
+ @window.refresh
86
+ end
87
+
88
+ def next_page
89
+ LOGGER.debug("Trying to next_page")
90
+ LOGGER.debug("Page index: #{@page_index}; lines: #{@lines.size}; height: #{@height}")
91
+ if (@page_index + 1) * @height >= @lines.size
92
+ LOGGER.debug('exiting')
93
+ # Go back to entries menu
94
+ return :exit
95
+ else
96
+ @page_index += 1
97
+ draw
98
+ end
99
+ end
100
+
101
+ def prev_page
102
+ LOGGER.debug("Trying to prev_page")
103
+ if @page_index == 0
104
+ return :exit
105
+ else
106
+ @page_index -= 1
107
+ draw
108
+ end
109
+ end
110
+
111
+ def close
112
+ @window.close
113
+ end
114
+
115
+ def page_content
116
+ line_offset = @page_index * @height
117
+ @lines[line_offset, @height].join("\n")
118
+ end
119
+
120
+ def toggle_flag
121
+ if @item.flagged
122
+ @item.update_attribute(:flagged, nil)
123
+ else
124
+ @item.update_attribute(:flagged, Time.now)
125
+ end
126
+ end
127
+
128
+ # turned off for now
129
+ def teletype
130
+
131
+ # used to calibrate the speed of typing
132
+ interval = 3.0 / (height * 80)
133
+ sleep 0.4
134
+
135
+ @content.chars.length.times do |x|
136
+ character = @content.chars[x,1]
137
+ if character !~ /\S/
138
+ # add to a buffer, so you output all the spaces at once
139
+ @window.addstr(character)
140
+ else
141
+ @window.addstr(character.chars)
142
+ @window.refresh
143
+ sleep interval
144
+ end
145
+ end
146
+
147
+ end
148
+
149
+ end
150
+