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,27 @@
|
|
|
1
|
+
class HTMLEntities
|
|
2
|
+
class << self
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# Legacy compatibility class method allowing direct encoding of XHTML1 entities.
|
|
6
|
+
# See HTMLEntities#encode for description of parameters.
|
|
7
|
+
#
|
|
8
|
+
def encode_entities(*args)
|
|
9
|
+
xhtml1_entities.encode(*args)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
#
|
|
13
|
+
# Legacy compatibility class method allowing direct decoding of XHTML1 entities.
|
|
14
|
+
# See HTMLEntities#decode for description of parameters.
|
|
15
|
+
#
|
|
16
|
+
def decode_entities(*args)
|
|
17
|
+
xhtml1_entities.decode(*args)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def xhtml1_entities
|
|
23
|
+
@xhtml1_entities ||= new('xhtml1')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'htmlentities'
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# This file extends the String class with methods to allow encoding and decoding of
|
|
5
|
+
# HTML/XML entities from/to their corresponding UTF-8 codepoints.
|
|
6
|
+
#
|
|
7
|
+
class String
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Decode XML and HTML 4.01 entities in a string into their UTF-8
|
|
11
|
+
# equivalents.
|
|
12
|
+
#
|
|
13
|
+
def decode_entities
|
|
14
|
+
return HTMLEntities.decode_entities(self)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Encode codepoints in a string into their corresponding entities. See
|
|
19
|
+
# the documentation of HTMLEntities.encode_entities for a list of possible
|
|
20
|
+
# instructions.
|
|
21
|
+
#
|
|
22
|
+
def encode_entities(*instructions)
|
|
23
|
+
return HTMLEntities.encode_entities(self, *instructions)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
class HTMLEntities
|
|
2
|
+
MAPPINGS = {} unless defined? MAPPINGS
|
|
3
|
+
MAPPINGS['xhtml1'] = {
|
|
4
|
+
'Aacute' => 193,
|
|
5
|
+
'aacute' => 225,
|
|
6
|
+
'Acirc' => 194,
|
|
7
|
+
'acirc' => 226,
|
|
8
|
+
'acute' => 180,
|
|
9
|
+
'AElig' => 198,
|
|
10
|
+
'aelig' => 230,
|
|
11
|
+
'Agrave' => 192,
|
|
12
|
+
'agrave' => 224,
|
|
13
|
+
'alefsym' => 8501,
|
|
14
|
+
'Alpha' => 913,
|
|
15
|
+
'alpha' => 945,
|
|
16
|
+
'amp' => 38,
|
|
17
|
+
'and' => 8743,
|
|
18
|
+
'ang' => 8736,
|
|
19
|
+
'apos' => 39,
|
|
20
|
+
'Aring' => 197,
|
|
21
|
+
'aring' => 229,
|
|
22
|
+
'asymp' => 8776,
|
|
23
|
+
'Atilde' => 195,
|
|
24
|
+
'atilde' => 227,
|
|
25
|
+
'Auml' => 196,
|
|
26
|
+
'auml' => 228,
|
|
27
|
+
'bdquo' => 8222,
|
|
28
|
+
'Beta' => 914,
|
|
29
|
+
'beta' => 946,
|
|
30
|
+
'brvbar' => 166,
|
|
31
|
+
'bull' => 8226,
|
|
32
|
+
'cap' => 8745,
|
|
33
|
+
'Ccedil' => 199,
|
|
34
|
+
'ccedil' => 231,
|
|
35
|
+
'cedil' => 184,
|
|
36
|
+
'cent' => 162,
|
|
37
|
+
'Chi' => 935,
|
|
38
|
+
'chi' => 967,
|
|
39
|
+
'circ' => 710,
|
|
40
|
+
'clubs' => 9827,
|
|
41
|
+
'cong' => 8773,
|
|
42
|
+
'copy' => 169,
|
|
43
|
+
'crarr' => 8629,
|
|
44
|
+
'cup' => 8746,
|
|
45
|
+
'curren' => 164,
|
|
46
|
+
'Dagger' => 8225,
|
|
47
|
+
'dagger' => 8224,
|
|
48
|
+
'dArr' => 8659,
|
|
49
|
+
'darr' => 8595,
|
|
50
|
+
'deg' => 176,
|
|
51
|
+
'Delta' => 916,
|
|
52
|
+
'delta' => 948,
|
|
53
|
+
'diams' => 9830,
|
|
54
|
+
'divide' => 247,
|
|
55
|
+
'Eacute' => 201,
|
|
56
|
+
'eacute' => 233,
|
|
57
|
+
'Ecirc' => 202,
|
|
58
|
+
'ecirc' => 234,
|
|
59
|
+
'Egrave' => 200,
|
|
60
|
+
'egrave' => 232,
|
|
61
|
+
'empty' => 8709,
|
|
62
|
+
'emsp' => 8195,
|
|
63
|
+
'ensp' => 8194,
|
|
64
|
+
'Epsilon' => 917,
|
|
65
|
+
'epsilon' => 949,
|
|
66
|
+
'equiv' => 8801,
|
|
67
|
+
'Eta' => 919,
|
|
68
|
+
'eta' => 951,
|
|
69
|
+
'ETH' => 208,
|
|
70
|
+
'eth' => 240,
|
|
71
|
+
'Euml' => 203,
|
|
72
|
+
'euml' => 235,
|
|
73
|
+
'euro' => 8364,
|
|
74
|
+
'exist' => 8707,
|
|
75
|
+
'fnof' => 402,
|
|
76
|
+
'forall' => 8704,
|
|
77
|
+
'frac12' => 189,
|
|
78
|
+
'frac14' => 188,
|
|
79
|
+
'frac34' => 190,
|
|
80
|
+
'frasl' => 8260,
|
|
81
|
+
'Gamma' => 915,
|
|
82
|
+
'gamma' => 947,
|
|
83
|
+
'ge' => 8805,
|
|
84
|
+
'gt' => 62,
|
|
85
|
+
'hArr' => 8660,
|
|
86
|
+
'harr' => 8596,
|
|
87
|
+
'hearts' => 9829,
|
|
88
|
+
'hellip' => 8230,
|
|
89
|
+
'Iacute' => 205,
|
|
90
|
+
'iacute' => 237,
|
|
91
|
+
'Icirc' => 206,
|
|
92
|
+
'icirc' => 238,
|
|
93
|
+
'iexcl' => 161,
|
|
94
|
+
'Igrave' => 204,
|
|
95
|
+
'igrave' => 236,
|
|
96
|
+
'image' => 8465,
|
|
97
|
+
'infin' => 8734,
|
|
98
|
+
'int' => 8747,
|
|
99
|
+
'Iota' => 921,
|
|
100
|
+
'iota' => 953,
|
|
101
|
+
'iquest' => 191,
|
|
102
|
+
'isin' => 8712,
|
|
103
|
+
'Iuml' => 207,
|
|
104
|
+
'iuml' => 239,
|
|
105
|
+
'Kappa' => 922,
|
|
106
|
+
'kappa' => 954,
|
|
107
|
+
'Lambda' => 923,
|
|
108
|
+
'lambda' => 955,
|
|
109
|
+
'lang' => 9001,
|
|
110
|
+
'laquo' => 171,
|
|
111
|
+
'lArr' => 8656,
|
|
112
|
+
'larr' => 8592,
|
|
113
|
+
'lceil' => 8968,
|
|
114
|
+
'ldquo' => 8220,
|
|
115
|
+
'le' => 8804,
|
|
116
|
+
'lfloor' => 8970,
|
|
117
|
+
'lowast' => 8727,
|
|
118
|
+
'loz' => 9674,
|
|
119
|
+
'lrm' => 8206,
|
|
120
|
+
'lsaquo' => 8249,
|
|
121
|
+
'lsquo' => 8216,
|
|
122
|
+
'lt' => 60,
|
|
123
|
+
'macr' => 175,
|
|
124
|
+
'mdash' => 8212,
|
|
125
|
+
'micro' => 181,
|
|
126
|
+
'middot' => 183,
|
|
127
|
+
'minus' => 8722,
|
|
128
|
+
'Mu' => 924,
|
|
129
|
+
'mu' => 956,
|
|
130
|
+
'nabla' => 8711,
|
|
131
|
+
'nbsp' => 160,
|
|
132
|
+
'ndash' => 8211,
|
|
133
|
+
'ne' => 8800,
|
|
134
|
+
'ni' => 8715,
|
|
135
|
+
'not' => 172,
|
|
136
|
+
'notin' => 8713,
|
|
137
|
+
'nsub' => 8836,
|
|
138
|
+
'Ntilde' => 209,
|
|
139
|
+
'ntilde' => 241,
|
|
140
|
+
'Nu' => 925,
|
|
141
|
+
'nu' => 957,
|
|
142
|
+
'Oacute' => 211,
|
|
143
|
+
'oacute' => 243,
|
|
144
|
+
'Ocirc' => 212,
|
|
145
|
+
'ocirc' => 244,
|
|
146
|
+
'OElig' => 338,
|
|
147
|
+
'oelig' => 339,
|
|
148
|
+
'Ograve' => 210,
|
|
149
|
+
'ograve' => 242,
|
|
150
|
+
'oline' => 8254,
|
|
151
|
+
'Omega' => 937,
|
|
152
|
+
'omega' => 969,
|
|
153
|
+
'Omicron' => 927,
|
|
154
|
+
'omicron' => 959,
|
|
155
|
+
'oplus' => 8853,
|
|
156
|
+
'or' => 8744,
|
|
157
|
+
'ordf' => 170,
|
|
158
|
+
'ordm' => 186,
|
|
159
|
+
'Oslash' => 216,
|
|
160
|
+
'oslash' => 248,
|
|
161
|
+
'Otilde' => 213,
|
|
162
|
+
'otilde' => 245,
|
|
163
|
+
'otimes' => 8855,
|
|
164
|
+
'Ouml' => 214,
|
|
165
|
+
'ouml' => 246,
|
|
166
|
+
'para' => 182,
|
|
167
|
+
'part' => 8706,
|
|
168
|
+
'permil' => 8240,
|
|
169
|
+
'perp' => 8869,
|
|
170
|
+
'Phi' => 934,
|
|
171
|
+
'phi' => 966,
|
|
172
|
+
'Pi' => 928,
|
|
173
|
+
'pi' => 960,
|
|
174
|
+
'piv' => 982,
|
|
175
|
+
'plusmn' => 177,
|
|
176
|
+
'pound' => 163,
|
|
177
|
+
'Prime' => 8243,
|
|
178
|
+
'prime' => 8242,
|
|
179
|
+
'prod' => 8719,
|
|
180
|
+
'prop' => 8733,
|
|
181
|
+
'Psi' => 936,
|
|
182
|
+
'psi' => 968,
|
|
183
|
+
'quot' => 34,
|
|
184
|
+
'radic' => 8730,
|
|
185
|
+
'rang' => 9002,
|
|
186
|
+
'raquo' => 187,
|
|
187
|
+
'rArr' => 8658,
|
|
188
|
+
'rarr' => 8594,
|
|
189
|
+
'rceil' => 8969,
|
|
190
|
+
'rdquo' => 8221,
|
|
191
|
+
'real' => 8476,
|
|
192
|
+
'reg' => 174,
|
|
193
|
+
'rfloor' => 8971,
|
|
194
|
+
'Rho' => 929,
|
|
195
|
+
'rho' => 961,
|
|
196
|
+
'rlm' => 8207,
|
|
197
|
+
'rsaquo' => 8250,
|
|
198
|
+
'rsquo' => 8217,
|
|
199
|
+
'sbquo' => 8218,
|
|
200
|
+
'Scaron' => 352,
|
|
201
|
+
'scaron' => 353,
|
|
202
|
+
'sdot' => 8901,
|
|
203
|
+
'sect' => 167,
|
|
204
|
+
'shy' => 173,
|
|
205
|
+
'Sigma' => 931,
|
|
206
|
+
'sigma' => 963,
|
|
207
|
+
'sigmaf' => 962,
|
|
208
|
+
'sim' => 8764,
|
|
209
|
+
'spades' => 9824,
|
|
210
|
+
'sub' => 8834,
|
|
211
|
+
'sube' => 8838,
|
|
212
|
+
'sum' => 8721,
|
|
213
|
+
'sup' => 8835,
|
|
214
|
+
'sup1' => 185,
|
|
215
|
+
'sup2' => 178,
|
|
216
|
+
'sup3' => 179,
|
|
217
|
+
'supe' => 8839,
|
|
218
|
+
'szlig' => 223,
|
|
219
|
+
'Tau' => 932,
|
|
220
|
+
'tau' => 964,
|
|
221
|
+
'there4' => 8756,
|
|
222
|
+
'Theta' => 920,
|
|
223
|
+
'theta' => 952,
|
|
224
|
+
'thetasym' => 977,
|
|
225
|
+
'thinsp' => 8201,
|
|
226
|
+
'THORN' => 222,
|
|
227
|
+
'thorn' => 254,
|
|
228
|
+
'tilde' => 732,
|
|
229
|
+
'times' => 215,
|
|
230
|
+
'trade' => 8482,
|
|
231
|
+
'Uacute' => 218,
|
|
232
|
+
'uacute' => 250,
|
|
233
|
+
'uArr' => 8657,
|
|
234
|
+
'uarr' => 8593,
|
|
235
|
+
'Ucirc' => 219,
|
|
236
|
+
'ucirc' => 251,
|
|
237
|
+
'Ugrave' => 217,
|
|
238
|
+
'ugrave' => 249,
|
|
239
|
+
'uml' => 168,
|
|
240
|
+
'upsih' => 978,
|
|
241
|
+
'Upsilon' => 933,
|
|
242
|
+
'upsilon' => 965,
|
|
243
|
+
'Uuml' => 220,
|
|
244
|
+
'uuml' => 252,
|
|
245
|
+
'weierp' => 8472,
|
|
246
|
+
'Xi' => 926,
|
|
247
|
+
'xi' => 958,
|
|
248
|
+
'Yacute' => 221,
|
|
249
|
+
'yacute' => 253,
|
|
250
|
+
'yen' => 165,
|
|
251
|
+
'Yuml' => 376,
|
|
252
|
+
'yuml' => 255,
|
|
253
|
+
'Zeta' => 918,
|
|
254
|
+
'zeta' => 950,
|
|
255
|
+
'zwj' => 8205,
|
|
256
|
+
'zwnj' => 8204
|
|
257
|
+
}
|
|
258
|
+
end
|
data/lib/menu_pager.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# This is initialized with the screen area of the window of screen, and then
|
|
2
|
+
# handles showing the visible area of content for menus.
|
|
3
|
+
|
|
4
|
+
class MenuPager
|
|
5
|
+
include CharacterCleaner
|
|
6
|
+
|
|
7
|
+
def initialize(window_height, window_width)
|
|
8
|
+
@window_height = window_height
|
|
9
|
+
@window_width = window_width
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Pass in an array of lines or a string. Returns an array of hashes.
|
|
13
|
+
# Each hash has a key :item that holds the item object and a key :text that
|
|
14
|
+
# holds the text for the item.
|
|
15
|
+
#
|
|
16
|
+
# We don't do anything with the matches parameters yet.
|
|
17
|
+
# May use it later for highlighting. But then we would also need to
|
|
18
|
+
# pass in the search term.
|
|
19
|
+
#
|
|
20
|
+
def create_page(items, current_index, matches=nil, options={})
|
|
21
|
+
out = []
|
|
22
|
+
pages = []
|
|
23
|
+
items.each_slice(@window_height) do |slice|
|
|
24
|
+
pages << slice
|
|
25
|
+
end
|
|
26
|
+
page = calculate_page_for_index( current_index )
|
|
27
|
+
items_on_page = pages[page]
|
|
28
|
+
|
|
29
|
+
items_on_page.each_with_index do |x, i|
|
|
30
|
+
# Space on the left for the arrow
|
|
31
|
+
# Truncate title if it is longer than the width of the window
|
|
32
|
+
|
|
33
|
+
if x.is_a?(Entry)
|
|
34
|
+
title = process_entities_and_utf(x.title).strip
|
|
35
|
+
else
|
|
36
|
+
# Put in a line number if this is a Feed in the Feed Menu
|
|
37
|
+
index = items.index(x)
|
|
38
|
+
title = "%2d " % (index + 1) + x.title
|
|
39
|
+
title = process_entities_and_utf(title)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
title = " " + title
|
|
43
|
+
if x.is_a?(Feed) # put the feed count
|
|
44
|
+
title += " (#{x.entries.count})"
|
|
45
|
+
|
|
46
|
+
elsif x.is_a?(VirtualFeed) # put the feed count
|
|
47
|
+
title += " (#{x.entry_count})"
|
|
48
|
+
end
|
|
49
|
+
# if x.respond_to?(:categories) and !x.categories.empty?
|
|
50
|
+
# title += " - [#{x.categories.join(', ')}]"
|
|
51
|
+
# end
|
|
52
|
+
|
|
53
|
+
maxwidth = @window_width - 18
|
|
54
|
+
full_title = [title]
|
|
55
|
+
full_title << x.feed.title if options[:show_feed_titles]
|
|
56
|
+
full_title << time_ago_in_words(x.date_published) if x.is_a?(Entry)
|
|
57
|
+
excess = full_title.join(" ").length - maxwidth
|
|
58
|
+
if excess > 0
|
|
59
|
+
title = title[0, title.length - excess] + "..."
|
|
60
|
+
end
|
|
61
|
+
out << {:item => x, :text => title}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# page 0 is the first page
|
|
65
|
+
[page, out]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def calculate_page_for_index( index )
|
|
69
|
+
index / @window_height
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/menu_window.rb
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# Wraps a collection of objects into a menu window
|
|
2
|
+
class MenuWindow
|
|
3
|
+
attr_accessor :items, :current_index, :search_string, :matches
|
|
4
|
+
|
|
5
|
+
# Matches are passed in when a search succeeds and certain items
|
|
6
|
+
# need to be indicated as matches
|
|
7
|
+
# TODO This should be changed into a hash parameter with keys
|
|
8
|
+
def initialize(title, scr, items, current_index, matches=nil, options={})
|
|
9
|
+
|
|
10
|
+
@options = options
|
|
11
|
+
@matches = matches
|
|
12
|
+
@height = scr.maxy - 6
|
|
13
|
+
@width = scr.maxx - 2
|
|
14
|
+
scr.clear
|
|
15
|
+
scr.setpos(2, 5)
|
|
16
|
+
scr.refresh
|
|
17
|
+
|
|
18
|
+
@title = title
|
|
19
|
+
@title_box = Curses::Window.new(2, @width, 2, 5)
|
|
20
|
+
|
|
21
|
+
# @window is the main content window
|
|
22
|
+
|
|
23
|
+
@window = Curses::Window.new(@height, @width, 4, 2)
|
|
24
|
+
|
|
25
|
+
@items = items
|
|
26
|
+
if @items.empty?
|
|
27
|
+
@title_box.addstr(@title)
|
|
28
|
+
@title_box.refresh
|
|
29
|
+
@window.clear
|
|
30
|
+
@window.addstr(" No items")
|
|
31
|
+
@window.refresh
|
|
32
|
+
# need to disable some commands in the controller
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
@pager = MenuPager.new(@height, @width)
|
|
36
|
+
@current_index = current_index || 0
|
|
37
|
+
|
|
38
|
+
@window.clear if @window
|
|
39
|
+
|
|
40
|
+
# page_content is an array of hashes with keys :item and :text
|
|
41
|
+
@page_index, @page_content = create_page
|
|
42
|
+
|
|
43
|
+
draw_menu(@page_content)
|
|
44
|
+
|
|
45
|
+
draw_title
|
|
46
|
+
|
|
47
|
+
draw_arrow
|
|
48
|
+
# draw pointer at current selection
|
|
49
|
+
@window.refresh
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create_page
|
|
53
|
+
@pager.create_page(@items, @current_index, @matches, @options)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# TODO put the date range of the items in the title
|
|
57
|
+
def draw_title
|
|
58
|
+
@title_box.clear
|
|
59
|
+
|
|
60
|
+
# This handles a list of feeds, which don't really have a date range
|
|
61
|
+
unless @items.first && @items.first.respond_to?(:date_published)
|
|
62
|
+
@title_box.addstr(@title)
|
|
63
|
+
@title_box.refresh
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get the date range from the items in the page content
|
|
68
|
+
dates = @page_content.map {|x| x[:item]}.map {|i|
|
|
69
|
+
if i.respond_to?(:date_published)
|
|
70
|
+
i.date_published
|
|
71
|
+
elsif i.respond_to?(:last_updated)
|
|
72
|
+
i.last_updated
|
|
73
|
+
end
|
|
74
|
+
}
|
|
75
|
+
most_recent_date = dates.max.strftime('%B %d %Y')
|
|
76
|
+
oldest_date = dates.min.strftime('%B %d %Y')
|
|
77
|
+
|
|
78
|
+
if most_recent_date != oldest_date
|
|
79
|
+
@title_box.addstr(@title + " | " + oldest_date + " - " + most_recent_date)
|
|
80
|
+
else
|
|
81
|
+
@title_box.addstr(@title + " | " + oldest_date)
|
|
82
|
+
end
|
|
83
|
+
@title_box.refresh
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def highlight_matches(matches, search_string)
|
|
87
|
+
LOGGER.debug("setting matches to #{matches.inspect}")
|
|
88
|
+
@search_string = search_string
|
|
89
|
+
@matches = matches
|
|
90
|
+
draw_menu
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def highlight_global_matches(matches, search_string)
|
|
94
|
+
LOGGER.debug("setting matches to #{matches.inspect}")
|
|
95
|
+
@global_search_string = search_string
|
|
96
|
+
@global_matches = matches
|
|
97
|
+
draw_menu
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def cancel_search # Cancels local search only
|
|
101
|
+
@matches = nil
|
|
102
|
+
@search_string = nil
|
|
103
|
+
draw_menu
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Draws the actual menu on the screen, with appropriate color highlighting
|
|
107
|
+
# The color of the highlighting is determined by the model. The item should
|
|
108
|
+
# have a :color attribute that returns the appropriate color as a symbol,
|
|
109
|
+
# which will be looked up on the Curses::Color table.
|
|
110
|
+
def draw_menu(items=@page_content)
|
|
111
|
+
@window.setpos(0, 0)
|
|
112
|
+
items.each do |x|
|
|
113
|
+
draw_line(x[:text], x[:item])
|
|
114
|
+
end
|
|
115
|
+
draw_arrow
|
|
116
|
+
@window.refresh
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def draw_line(text, item)
|
|
120
|
+
if item.is_a?(Entry)
|
|
121
|
+
|
|
122
|
+
color = case
|
|
123
|
+
when item.respond_to?(:flagged) && item.flagged && !@options[:turn_off_flagged_color]
|
|
124
|
+
:red
|
|
125
|
+
# when item.respond_to?(:is_new?) && item.is_new?
|
|
126
|
+
# :green
|
|
127
|
+
|
|
128
|
+
else
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# local matches take precedence
|
|
133
|
+
matches = @matches || @global_matches
|
|
134
|
+
search_string = @search_string || @global_search_string
|
|
135
|
+
|
|
136
|
+
if matches && !matches.empty? && matches.include?( @items.index(item) )
|
|
137
|
+
LOGGER.debug("matching line detected : search string #{search_string} : text: #{text}")
|
|
138
|
+
text = text.gsub(/(#{search_string})/i, '%%%\1%%%')
|
|
139
|
+
text.split(/%%%/).each_with_index do |chunk, index|
|
|
140
|
+
if index % 2 == 0
|
|
141
|
+
@window.addstr(chunk, color)
|
|
142
|
+
else
|
|
143
|
+
@window.addstr(chunk, :yellow)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
else
|
|
147
|
+
@window.addstr(text, color)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if @options[:show_feed_titles] # put the feed title in the title
|
|
151
|
+
@window.addstr(" #{item.feed.title}", :cyan)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
item_date = case
|
|
155
|
+
when @options[:feed_title] == "All Entries"
|
|
156
|
+
item.created_at
|
|
157
|
+
when @options[:feed_title] == "Flagged Entries"
|
|
158
|
+
item.flagged
|
|
159
|
+
else
|
|
160
|
+
item.date_published
|
|
161
|
+
end
|
|
162
|
+
if item_date
|
|
163
|
+
item_date = " #{time_ago_in_words(item_date)} ago"
|
|
164
|
+
else
|
|
165
|
+
item_date = ''
|
|
166
|
+
end
|
|
167
|
+
@window.addstr(item_date, :magenta)
|
|
168
|
+
|
|
169
|
+
@window.addstr("\n")
|
|
170
|
+
|
|
171
|
+
# a feed or virtual feed
|
|
172
|
+
else
|
|
173
|
+
|
|
174
|
+
color = case
|
|
175
|
+
when item.is_a?(VirtualFeed) && text =~ /Flagged Entries/
|
|
176
|
+
:red
|
|
177
|
+
else
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
@window.addstr(text, color)
|
|
181
|
+
|
|
182
|
+
# May not be the case with some virtual feeds
|
|
183
|
+
# Add a column to feed that cached this attribute after an feed
|
|
184
|
+
# update_self
|
|
185
|
+
if item.is_a?(VirtualFeed) && item.title == "All Entries"
|
|
186
|
+
# Use the created_at of the last entry
|
|
187
|
+
entry = Entry.find(:first, :order => "id desc")
|
|
188
|
+
last_updated = entry ? "#{time_ago_in_words(entry.created_at)} ago\n" : "\n"
|
|
189
|
+
|
|
190
|
+
@window.addstr(" " + last_updated, :magenta)
|
|
191
|
+
|
|
192
|
+
elsif item.is_a?(VirtualFeed) && item.title == "Flagged Entries"
|
|
193
|
+
# Use the flagged timetamp of the last entry
|
|
194
|
+
entry = Entry.find(:first, :conditions => "flagged IS NOT NULL", :order => "flagged desc")
|
|
195
|
+
last_updated = entry ? "#{time_ago_in_words(entry.flagged)} ago\n" : "\n"
|
|
196
|
+
@window.addstr(" " + last_updated , :magenta)
|
|
197
|
+
|
|
198
|
+
elsif item.last_updated
|
|
199
|
+
most_recent_entry = item.entries.first
|
|
200
|
+
last_updated = (most_recent_entry && most_recent_entry.date_published) ?
|
|
201
|
+
most_recent_entry.date_published : item.last_updated
|
|
202
|
+
if last_updated
|
|
203
|
+
last_updated = time_ago_in_words(last_updated) + " ago\n"
|
|
204
|
+
else
|
|
205
|
+
last_updated = ''
|
|
206
|
+
end
|
|
207
|
+
@window.addstr(" " + last_updated , :magenta)
|
|
208
|
+
else
|
|
209
|
+
@window.addstr("\n")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def redraw_menu
|
|
215
|
+
LOGGER.debug("Redrawing menu")
|
|
216
|
+
@window.clear
|
|
217
|
+
draw_menu(@page_content)
|
|
218
|
+
draw_arrow
|
|
219
|
+
draw_title
|
|
220
|
+
@window.refresh
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def draw_arrow
|
|
224
|
+
# calculate where arrow should be drawn
|
|
225
|
+
LOGGER.debug("Drawing arrow. Current index: #{@current_index}, Page index: #{@page_index}, Height: #{@height}")
|
|
226
|
+
y = (@current_index - @page_index * @height)
|
|
227
|
+
x = 0
|
|
228
|
+
LOGGER.debug "Drawing arrow at y #{y}"
|
|
229
|
+
@window.setpos(y, x)
|
|
230
|
+
@window.addstr("->")
|
|
231
|
+
# move the cursor
|
|
232
|
+
#@window.setpos(0,0)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# TODO change this to coloring the line
|
|
236
|
+
def toggle_flag
|
|
237
|
+
item = @items[@current_index]
|
|
238
|
+
if item.flagged
|
|
239
|
+
item.update_attribute(:flagged, nil)
|
|
240
|
+
else
|
|
241
|
+
item.update_attribute(:flagged, Time.now)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
redraw_menu
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def unflag_all
|
|
248
|
+
@items.each do |item|
|
|
249
|
+
item.update_attribute(:flagged, nil)
|
|
250
|
+
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def flag_all
|
|
255
|
+
@items.each do |item|
|
|
256
|
+
item.update_attribute(:flagged, Time.now)
|
|
257
|
+
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def erase_arrow
|
|
262
|
+
# calculate where arrow should be drawn
|
|
263
|
+
y = (@current_index - @page_index * @height)
|
|
264
|
+
x = 0
|
|
265
|
+
LOGGER.debug "Erasing arrow at y #{y}"
|
|
266
|
+
@window.setpos(y, x)
|
|
267
|
+
@window << " "
|
|
268
|
+
return
|
|
269
|
+
@window.delch
|
|
270
|
+
@window.delch
|
|
271
|
+
@window.delch
|
|
272
|
+
@window.addstr
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Used to moved in constrainted way among a subset (search matches) of the
|
|
276
|
+
# larger set
|
|
277
|
+
def move_to_item(item_index)
|
|
278
|
+
# move arrow, or change page
|
|
279
|
+
erase_arrow
|
|
280
|
+
@current_index = item_index
|
|
281
|
+
move_arrow_or_page
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# +set+ is a set of indexes to the original larger set
|
|
285
|
+
def move_to_next_item_in_set(set)
|
|
286
|
+
LOGGER.debug("set : #{set}")
|
|
287
|
+
LOGGER.debug("@current_index : #{@current_index}")
|
|
288
|
+
current_index = set.index( @current_index )
|
|
289
|
+
# move arrow, or change page
|
|
290
|
+
return if current_index == set.length - 1
|
|
291
|
+
LOGGER.debug("current_index : #{current_index}")
|
|
292
|
+
new_index = set[current_index + 1]
|
|
293
|
+
move_to_item(new_index)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def move_to_prev_item_in_set(set)
|
|
297
|
+
current_index = set.index( @current_index )
|
|
298
|
+
# move arrow, or change page
|
|
299
|
+
return if current_index == 0
|
|
300
|
+
new_index = set[current_index - 1]
|
|
301
|
+
move_to_item(new_index)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def next_item(multiplier)
|
|
306
|
+
# move arrow, or change page
|
|
307
|
+
new_index = @current_index + multiplier
|
|
308
|
+
LOGGER.debug("New index is #{new_index}")
|
|
309
|
+
if new_index >= @items.size
|
|
310
|
+
new_index = @items.size - 1
|
|
311
|
+
end
|
|
312
|
+
erase_arrow
|
|
313
|
+
@current_index = new_index
|
|
314
|
+
move_arrow_or_page
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def prev_item(multiplier)
|
|
318
|
+
# move arrow, or change page
|
|
319
|
+
new_index = @current_index - multiplier
|
|
320
|
+
if new_index < 0
|
|
321
|
+
new_index = 0
|
|
322
|
+
end
|
|
323
|
+
erase_arrow
|
|
324
|
+
@current_index = new_index
|
|
325
|
+
move_arrow_or_page
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def next_page
|
|
329
|
+
new_index = (@page_index + 1) * @height
|
|
330
|
+
if new_index >= @items.size
|
|
331
|
+
new_index = @items.size - 1
|
|
332
|
+
end
|
|
333
|
+
erase_arrow
|
|
334
|
+
@current_index = new_index
|
|
335
|
+
move_arrow_or_page
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def prev_page
|
|
339
|
+
if @page_index == 0
|
|
340
|
+
top
|
|
341
|
+
return
|
|
342
|
+
end
|
|
343
|
+
new_index = (@page_index - 1) * @height
|
|
344
|
+
if new_index < 0
|
|
345
|
+
new_index = 0
|
|
346
|
+
end
|
|
347
|
+
erase_arrow
|
|
348
|
+
@current_index = new_index
|
|
349
|
+
move_arrow_or_page
|
|
350
|
+
# need to put cursor at bottom unless this is already 1st page
|
|
351
|
+
bottom
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def top
|
|
355
|
+
erase_arrow
|
|
356
|
+
@current_index = (@page_index * @height)
|
|
357
|
+
move_arrow_or_page
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def middle
|
|
361
|
+
new_index = (@page_index + 1) * (@height / 2)
|
|
362
|
+
# Don't want the cursor positioned in a void when the list is short
|
|
363
|
+
if new_index >= @items.size
|
|
364
|
+
new_index = @items.size - 1
|
|
365
|
+
end
|
|
366
|
+
erase_arrow
|
|
367
|
+
@current_index = new_index
|
|
368
|
+
move_arrow_or_page
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def bottom
|
|
372
|
+
new_index = (@page_index + 1) * @height - 1
|
|
373
|
+
if new_index >= @items.size
|
|
374
|
+
new_index = @items.size - 1
|
|
375
|
+
end
|
|
376
|
+
erase_arrow
|
|
377
|
+
@current_index = new_index
|
|
378
|
+
move_arrow_or_page
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def end
|
|
382
|
+
erase_arrow
|
|
383
|
+
@current_index = 0
|
|
384
|
+
move_arrow_or_page
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def beginning
|
|
388
|
+
erase_arrow
|
|
389
|
+
@current_index = @items.size - 1
|
|
390
|
+
move_arrow_or_page
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def move_arrow_or_page
|
|
394
|
+
new_page_index = @pager.calculate_page_for_index(@current_index)
|
|
395
|
+
|
|
396
|
+
LOGGER.debug("Current page: #{@page_index}; New Page number: #{new_page_index}")
|
|
397
|
+
if new_page_index == @page_index
|
|
398
|
+
LOGGER.debug("Moving arrow")
|
|
399
|
+
# move arrow
|
|
400
|
+
draw_arrow
|
|
401
|
+
@window.refresh
|
|
402
|
+
else
|
|
403
|
+
LOGGER.debug("Changing page")
|
|
404
|
+
# change page
|
|
405
|
+
|
|
406
|
+
# TODO change this implementation
|
|
407
|
+
@page_index, @page_content = create_page
|
|
408
|
+
|
|
409
|
+
redraw_menu
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def close
|
|
414
|
+
@window.clear
|
|
415
|
+
@window.refresh
|
|
416
|
+
@window.close
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|