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.
- data/History.txt +6 -0
- data/Manifest.txt +7 -0
- data/README.txt +91 -0
- data/bin/fastreader +55 -0
- data/db/default.sqlite3 +0 -0
- data/lib/autodiscovery.rb +24 -0
- data/lib/character_cleaner.rb +17 -0
- data/lib/command_parser.rb +17 -0
- data/lib/command_window.rb +112 -0
- data/lib/curses_color.rb +1391 -0
- data/lib/curses_controller.rb +176 -0
- data/lib/curses_extensions.rb +161 -0
- data/lib/display.rb +232 -0
- data/lib/entries_controller.rb +270 -0
- data/lib/entry.rb +21 -0
- data/lib/entry_controller.rb +131 -0
- data/lib/entry_window.rb +150 -0
- data/lib/fastreader.rb +283 -0
- data/lib/feed.rb +222 -0
- data/lib/feeds_controller.rb +240 -0
- data/lib/htmlentities.rb +165 -0
- data/lib/htmlentities/html4.rb +257 -0
- data/lib/htmlentities/legacy.rb +27 -0
- data/lib/htmlentities/string.rb +26 -0
- data/lib/htmlentities/xhtml1.rb +258 -0
- data/lib/menu_pager.rb +71 -0
- data/lib/menu_window.rb +419 -0
- data/lib/opml.rb +16 -0
- data/lib/virtual_feed.rb +39 -0
- data/test/test_fastreader.rb +0 -0
- metadata +162 -0
|
@@ -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
|
+
|
data/lib/entry_window.rb
ADDED
|
@@ -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
|
+
|