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,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
@@ -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
+