rutt 0.3.2 → 0.3.3

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/Rakefile CHANGED
@@ -16,7 +16,7 @@ Jeweler::Tasks.new do |gem|
16
16
  gem.homepage = "http://github.com/abhiyerra/rutt"
17
17
  gem.license = "BSD"
18
18
  gem.summary = %Q{The Mutt of RSS/Atom feeds.}
19
- gem.description = %Q{Read RSS feeds from the commandline }
19
+ gem.description = %Q{The Mutt of RSS/Atom feeds.}
20
20
  gem.email = "abhi@berkeley.edu"
21
21
  gem.authors = ["Abhi Yerra"]
22
22
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.3.3
data/bin/rutt CHANGED
@@ -13,46 +13,63 @@ require 'sqlite3'
13
13
  require 'feedparser'
14
14
  require 'parallel'
15
15
  require 'instapaper'
16
+ require 'oauth'
16
17
 
17
18
  require 'rutt'
18
- require 'instapaper'
19
19
 
20
20
  $db = SQLite3::Database.new('rutt.db')
21
21
  $db.results_as_hash = true
22
22
 
23
+ def start_screen
24
+ stdscr = Ncurses.initscr()
25
+
26
+ Ncurses.start_color();
27
+ Ncurses.cbreak();
28
+ Ncurses.noecho();
29
+ Ncurses.keypad(stdscr, true);
30
+
31
+ stdscr.clear
32
+
33
+ feed_screen = Rutt::FeedScreen.new(stdscr)
34
+ feed_screen.loop
35
+ ensure
36
+ Ncurses.endwin()
37
+ end
38
+
39
+
23
40
  def main
24
41
 
25
- Config::make_table!
26
- Feed::make_table!
27
- Item::make_table!
42
+ Rutt::Config.make_table!
43
+ Rutt::Feed.make_table!
44
+ Rutt::Item.make_table!
28
45
 
29
46
  options = {}
30
47
  OptionParser.new do |opts|
31
48
  opts.banner = "Usage: rutt.rb [options]"
32
49
 
33
50
  opts.on('-a', '--add FEED', "Add a feed") do |url|
34
- Feed::add(url)
51
+ Rutt::Feed::add(url)
35
52
  exit
36
53
  end
37
54
 
38
55
  opts.on('-r', '--refresh', "Refresh feeds.") do
39
- Feed::refresh
56
+ Rutt::Feed::refresh
40
57
  exit
41
58
  end
42
59
 
43
60
  opts.on('-o', '--import-opml FILE', "Import opml") do |file|
44
- urls = Opml::get_urls(file)
61
+ urls = Rutt::Opml::get_urls(file)
45
62
 
46
63
  urls.each do |url|
47
64
  puts "Adding #{url}"
48
- Feed::add(url, false)
65
+ Rutt::Feed::add(url, false)
49
66
  end
50
67
 
51
68
  exit
52
69
  end
53
70
 
54
71
  opts.on('-s', '--set-key', "Set config") do |key, value|
55
- Config.set(ARGV[0], ARGV[1])
72
+ Rutt::Config.set(ARGV[0], ARGV[1])
56
73
  exit
57
74
  end
58
75
 
@@ -60,12 +77,12 @@ def main
60
77
 
61
78
  end.parse!
62
79
 
63
- consumer_key = Config.get("instapaper.consumer-key")
64
- consumer_secret = Config.get("instapaper.consumer-secret")
65
- username = Config.get("instapaper.username")
66
- password = Config.get("instapaper.password")
80
+ consumer_key = Rutt::Config.get("instapaper.consumer-key")
81
+ consumer_secret = Rutt::Config.get("instapaper.consumer-secret")
82
+ username = Rutt::Config.get("instapaper.username")
83
+ password = Rutt::Config.get("instapaper.password")
67
84
 
68
- $instapaper = Instapaper::API.new(consumer_key, consumer_secret)
85
+ $instapaper = Rutt::Instapaper::API.new(consumer_key, consumer_secret)
69
86
  $instapaper.authorize(username, password)
70
87
 
71
88
  start_screen
data/lib/rutt.rb CHANGED
@@ -1,477 +1,9 @@
1
- module Config
2
- extend self
3
-
4
- def make_table!
5
- $db.execute(%{
6
- create table if not exists config (
7
- id integer PRIMARY KEY,
8
- key text,
9
- value text,
10
- UNIQUE(key))
11
- })
12
- end
13
-
14
- def get(key)
15
- $db.get_first_value("select value from config where key = ?", key)
16
- end
17
-
18
- def set(key, value)
19
- $db.execute("insert or replace into config(key, value) values (?, ?)", key, value)
20
- end
21
- end
22
-
23
- module Opml
24
- def self.get_urls(file)
25
- doc = Nokogiri::XML(open(file))
26
-
27
- urls = []
28
-
29
- doc.xpath('opml/body/outline').each do |outline|
30
- if outline['xmlUrl']
31
- urls << outline['xmlUrl']
32
- else
33
- (outline/'outline').each do |outline2|
34
- urls << outline2['xmlUrl']
35
- end
36
- end
37
- end
38
-
39
- return urls
40
- end
41
- end
42
-
43
- module Feed
44
- extend self
45
-
46
- def make_table!
47
- $db.execute(%{
48
- create table if not exists feeds (
49
- id integer PRIMARY KEY,
50
- title text,
51
- url text,
52
- update_interval integer default 3600,
53
- created_at NOT NULL DEFAULT CURRENT_TIMESTAMP,
54
- updated_at NOT NULL DEFAULT CURRENT_TIMESTAMP,
55
- UNIQUE(url))
56
- })
57
- end
58
-
59
- def add(url, refresh=true)
60
- $db.execute("insert or ignore into feeds (url) values ('#{url}')")
61
- $db.execute("select * from feeds where id = (select last_insert_rowid())") do |feed|
62
- refresh_for(feed)
63
- end if refresh
64
- end
65
-
66
- def get(id)
67
-
68
- end
69
-
70
- def delete(feed)
71
- $db.execute("delete from items where feed_id = ?", feed['id'])
72
- $db.execute("delete from feeds where id = ?", feed['id'])
73
- end
74
-
75
- def all(min_limit=0, max_limit=-1)
76
- $db.execute(%{
77
- select f.*,
78
- (select count(*) from items iu where iu.feed_id = f.id) as num_items,
79
- (select count(*) from items ir where ir.read = 0 and ir.feed_id = f.id) as unread
80
- from feeds f where unread > 0 order by id desc limit #{min_limit},#{max_limit}
81
- })
82
- end
83
-
84
- def refresh
85
- feeds = $db.execute("select * from feeds")
86
- results = Parallel.map(feeds, :in_threads => 8) do |feed|
87
- refresh_for(feed)
88
- end
89
- end
90
-
91
- def refresh_for(feed)
92
- content = open(feed['url']).read
93
- rss = FeedParser::Feed::new(content)
94
-
95
- puts rss.title
96
-
97
- $db.execute("update feeds set title = ? where id = ?", rss.title, feed['id'])
98
-
99
- rss.items.each do |item|
100
- $db.execute("insert or ignore into items (feed_id, title, url, published_at) values (?, ?, ?, ?)", feed['id'], item.title, item.link, item.date.to_i)
101
- end
102
- rescue Exception => e
103
- # no-op
104
- end
105
-
106
- def unread(feed_id)
107
- $db.execute("select * from items where feed_id = ? and read = 0", feed)
108
- end
109
-
110
- def mark_as_read(feed)
111
- $db.execute("update items set read = 1 where feed_id = ?", feed['id'])
112
- end
113
- end
114
-
115
-
116
- module Item
117
-
118
- extend self
119
-
120
- def make_table!
121
- $db.execute(%{
122
- create table if not exists items (
123
- id integer PRIMARY KEY,
124
- feed_id integer,
125
- title text,
126
- url text,
127
- description text,
128
- read int default 0,
129
- prioritize int default 0,
130
- published_at DATE NOT NULL DEFAULT (datetime('now','localtime')),
131
- created_at NOT NULL DEFAULT CURRENT_TIMESTAMP,
132
- updated_at NOT NULL DEFAULT CURRENT_TIMESTAMP,
133
- UNIQUE(url),
134
- FOREIGN KEY(feed_id) REFERENCES feeds(id))
135
- })
136
- end
137
-
138
- def all(feed, min_limit=0, max_limit=-1)
139
- $db.execute("select * from items where feed_id = ? order by published_at desc limit #{min_limit},#{max_limit}", feed['id'])
140
- end
141
-
142
- def mark_as_unread(item)
143
- $db.execute("update items set read = 0 where id = #{item['id']}")
144
- end
145
-
146
- def mark_as_read(item)
147
- $db.execute("update items set read = 1 where id = #{item['id']}")
148
- end
149
-
150
- def sent_to_instapaper(item)
151
- $db.execute("update items set read = 2 where id = #{item['id']}")
152
- end
153
- end
154
-
155
- class Screen
156
- def initialize(stdscr)
157
- @stdscr = stdscr
158
-
159
- @min_y = 1
160
- @max_y = @stdscr.getmaxy
161
-
162
- @min_limit = @min_y - 1
163
- @max_limit = @max_y - 5
164
-
165
- @cur_y = 1
166
- @cur_x = 0
167
- end
168
-
169
- def incr_page
170
- @min_limit = @max_limit
171
- @max_limit += (@max_y - 5)
172
- end
173
-
174
- def decr_page
175
- @max_limit = @min_limit
176
- @min_limit -= (@max_y - 5)
177
-
178
- if @max_limit <= 0
179
- @min_limit = @min_y - 1
180
- @max_limit = @max_y - 5
181
- end
182
- end
183
-
184
- def display_menu
185
- @stdscr.clear
186
- @stdscr.move(0, 0)
187
- @stdscr.addstr(" rutt #{@menu}\n")
188
- end
189
-
190
- def move_pointer(pos, move_to=false)
191
- @stdscr.move(@cur_y, 0)
192
- @stdscr.addstr(" ")
193
-
194
- if move_to == true
195
- @cur_y = pos
196
- else
197
- @cur_y += pos
198
- end
199
-
200
- @stdscr.move(@cur_y, 0)
201
- @stdscr.addstr(">")
202
- end
203
- end
204
-
205
-
206
- class FeedScreen < Screen
207
- def initialize(stdscr)
208
- @menu = "q:Quit d:delete r:refresh all"
209
-
210
- super(stdscr)
211
- end
212
-
213
- def display_feeds
214
- @cur_y = @min_y
215
-
216
- @feeds = Feed::all(@min_limit, @max_limit)
217
- @feeds.each do |feed|
218
- # next if feed['unread'] == 0 # This should be configurable: feed.showread
219
-
220
- @stdscr.move(@cur_y, 0)
221
- @stdscr.addstr(" #{feed['unread']}/#{feed['num_items']}\t\t#{feed['title']}\n")
222
-
223
- @cur_y += 1
224
- end
225
-
226
- @cur_y = @min_y
227
- @stdscr.refresh
228
- end
229
-
230
- def window
231
- @stdscr.clear
232
-
233
- display_menu
234
- display_feeds
235
- move_pointer(0)
236
- end
237
-
238
- def loop
239
- window
240
-
241
- while true do
242
- c = @stdscr.getch
243
-
244
- if c > 0 && c < 255
245
- case c.chr
246
- when /q/i
247
- break
248
- when /a/i
249
- # no-op
250
- when /r/i
251
- cur_y = @cur_y
252
-
253
- Feed::refresh
254
-
255
- window
256
- move_pointer(cur_y, move_to=true)
257
- when /d/i
258
- cur_y = @cur_y - 1
259
-
260
- @stdscr.clear
261
- display_menu
262
- feed = @feeds[cur_y]
263
- @stdscr.move(2, 0)
264
- @stdscr.addstr("Are you sure you want to delete #{feed['title']}? ")
265
- d = @stdscr.getch
266
- if d.chr =~ /y/i
267
- Feed::delete(feed)
268
- end
269
- window
270
- move_pointer(cur_y, move_to=true)
271
- when /p/i
272
- decr_page
273
- window
274
- when /n/i
275
- incr_page
276
- window
277
- when / /
278
- cur_y = @cur_y
279
- @stdscr.addstr("#{@feeds[cur_y]}")
280
- item_screen = ItemScreen.new(@stdscr, @feeds[cur_y - 1])
281
- item_screen.loop
282
-
283
- window
284
- move_pointer(cur_y, move_to=true)
285
- end
286
- else
287
- case c
288
- when Ncurses::KEY_UP
289
- move_pointer(-1)
290
- when Ncurses::KEY_DOWN
291
- move_pointer(1)
292
- end
293
- end
294
- end
295
- end
296
- end
297
-
298
- class ItemScreen < Screen
299
- def initialize(stdscr, feed)
300
- @feed = feed
301
- @menu = " i:quit r:refresh m:mark as read u:mark as unread a:mark all as read b:open in browser"
302
-
303
- super(stdscr)
304
- end
305
-
306
- def display_items
307
- @cur_y = @min_y
308
-
309
- @items = Item::all(@feed, @min_limit, @max_limit)
310
- @items.each do |item|
311
- item_status = case item['read'].to_i
312
- when 0 then 'N'
313
- when 1 then ' '
314
- when 2 then 'I'
315
- else ' '
316
- end
317
- @stdscr.addstr(" #{item_status}\t#{Time.at(item['published_at']).strftime("%b %d, %Y %R:%M")}\t#{item['title']}\n")
318
- @cur_y += 1
319
- end
320
-
321
- @cur_y = @min_y
322
- @stdscr.refresh
323
- end
324
-
325
- def window
326
- @stdscr.clear
327
-
328
- display_menu
329
- display_items
330
- move_pointer(0)
331
- end
332
-
333
- def loop
334
- window
335
-
336
- while true do
337
- c = @stdscr.getch
338
-
339
- if c > 0 && c < 255
340
- case c.chr
341
- when /[iq]/i
342
- break
343
- when /s/i
344
- cur_y = @cur_y - 1
345
- $instapaper.request('/api/1/bookmarks/add', {
346
- 'url' => @items[cur_y]['url'],
347
- 'title' => @items[cur_y]['title'],
348
- })
349
- Item::sent_to_instapaper(@items[cur_y])
350
- window
351
- move_pointer(@cur_y, move_to=true)
352
- when /a/i
353
- Feed::mark_as_read(@feed)
354
- window
355
- move_pointer(@cur_y, move_to=true)
356
- when /p/i
357
- decr_page
358
- window
359
- when /n/i
360
- incr_page
361
- window
362
- when /b/i
363
- cur_y = @cur_y - 1
364
- Item::mark_as_read(@items[cur_y])
365
- Launchy.open(@items[cur_y]['url'])
366
- window
367
- move_pointer(@cur_y, move_to=true)
368
- when /m/i
369
- cur_y = @cur_y - 1
370
- Item::mark_as_read(@items[cur_y])
371
- window
372
- move_pointer(cur_y + 1, move_to=true)
373
- when /u/i
374
- cur_y = @cur_y - 1
375
- Item::mark_as_unread(@items[cur_y])
376
- window
377
- move_pointer(cur_y + 1, move_to=true)
378
- when /r/i
379
- Feed::refresh_for(@feed)
380
- window
381
- when / /
382
- content_screen = ContentScreen.new(@stdscr, @items[@cur_y - 1])
383
- content_screen.loop
384
-
385
- window
386
- move_pointer(@cur_y, move_to=true)
387
- end
388
- else
389
- case c
390
- when Ncurses::KEY_UP
391
- move_pointer(-1)
392
- when Ncurses::KEY_DOWN
393
- move_pointer(1)
394
- end
395
- end
396
- end
397
- end
398
- end
399
-
400
-
401
-
402
-
403
-
404
- class ContentScreen < Screen
405
- def initialize(stdscr, item)
406
- @item = item
407
- @menu = "i:back b:open in browser"
408
-
409
- super(stdscr)
410
- end
411
-
412
- def display_content
413
- @content = `elinks -dump -dump-charset ascii -force-html #{@item['url']}`
414
- @content = @content.split("\n")
415
-
416
- @stdscr.addstr(" #{@item['title']} (#{@item['url']})\n\n")
417
-
418
- lines = @content[@min_limit..@max_limit]
419
- lines.each { |line| @stdscr.addstr(" #{line}\n") } if lines
420
-
421
- @stdscr.refresh
422
- end
423
-
424
- def window
425
- @stdscr.clear
426
- display_menu
427
- display_content
428
- end
429
-
430
- def loop
431
- @cur_line = 0
432
-
433
- window
434
-
435
- while true do
436
- c = @stdscr.getch
437
- if c > 0 && c < 255
438
- case c.chr
439
- when /[iq]/i
440
- Item::mark_as_read(@item)
441
- break
442
- when /b/i
443
- Launchy.open(@item['url'])
444
- end
445
- else
446
- case c
447
- when Ncurses::KEY_UP
448
- decr_page
449
- window
450
- when Ncurses::KEY_DOWN
451
- incr_page
452
- window
453
- end
454
- end
455
- end
456
-
457
- end
458
- end
459
-
460
-
461
-
462
- def start_screen
463
- stdscr = Ncurses.initscr()
464
-
465
- Ncurses.start_color();
466
- Ncurses.cbreak();
467
- Ncurses.noecho();
468
- Ncurses.keypad(stdscr, true);
469
-
470
- stdscr.clear
471
-
472
- feed_screen = FeedScreen.new(stdscr)
473
- feed_screen.loop
474
- ensure
475
- Ncurses.endwin()
476
- end
477
-
1
+ require 'rutt/config'
2
+ require 'rutt/opml'
3
+ require 'rutt/instapaper'
4
+ require 'rutt/feed'
5
+ require 'rutt/item'
6
+ require 'rutt/screen'
7
+ require 'rutt/feed_screen'
8
+ require 'rutt/item_screen'
9
+ require 'rutt/content_screen'
@@ -0,0 +1,23 @@
1
+ module Rutt
2
+ module Config
3
+ extend self
4
+
5
+ def make_table!
6
+ $db.execute(%{
7
+ create table if not exists config (
8
+ id integer PRIMARY KEY,
9
+ key text,
10
+ value text,
11
+ UNIQUE(key))
12
+ })
13
+ end
14
+
15
+ def get(key)
16
+ $db.get_first_value("select value from config where key = ?", key)
17
+ end
18
+
19
+ def set(key, value)
20
+ $db.execute("insert or replace into config(key, value) values (?, ?)", key, value)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ module Rutt
2
+ class ContentScreen < Screen
3
+ def initialize(stdscr, item)
4
+ @item = item
5
+ @menu = "i:back b:open in browser"
6
+
7
+ super(stdscr)
8
+ end
9
+
10
+ def display_content
11
+ @content = `elinks -dump -dump-charset ascii -force-html #{@item['url']}`
12
+ @content = @content.split("\n")
13
+
14
+ @stdscr.addstr(" #{@item['title']} (#{@item['url']})\n\n")
15
+
16
+ lines = @content[@min_limit..@max_limit]
17
+ lines.each { |line| @stdscr.addstr(" #{line}\n") } if lines
18
+
19
+ @stdscr.refresh
20
+ end
21
+
22
+ def window
23
+ @stdscr.clear
24
+ display_menu
25
+ display_content
26
+ end
27
+
28
+ def loop
29
+ @cur_line = 0
30
+
31
+ window
32
+
33
+ while true do
34
+ c = @stdscr.getch
35
+ if c > 0 && c < 255
36
+ case c.chr
37
+ when /[iq]/i
38
+ Item::mark_as_read(@item)
39
+ break
40
+ when /b/i
41
+ Launchy.open(@item['url'])
42
+ end
43
+ else
44
+ case c
45
+ when Ncurses::KEY_UP
46
+ decr_page
47
+ window
48
+ when Ncurses::KEY_DOWN
49
+ incr_page
50
+ window
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
data/lib/rutt/feed.rb ADDED
@@ -0,0 +1,74 @@
1
+ module Rutt
2
+
3
+ module Feed
4
+ extend self
5
+
6
+ def make_table!
7
+ $db.execute(%{
8
+ create table if not exists feeds (
9
+ id integer PRIMARY KEY,
10
+ title text,
11
+ url text,
12
+ update_interval integer default 3600,
13
+ created_at NOT NULL DEFAULT CURRENT_TIMESTAMP,
14
+ updated_at NOT NULL DEFAULT CURRENT_TIMESTAMP,
15
+ UNIQUE(url))
16
+ })
17
+ end
18
+
19
+ def add(url, refresh=true)
20
+ $db.execute("insert or ignore into feeds (url) values ('#{url}')")
21
+ $db.execute("select * from feeds where id = (select last_insert_rowid())") do |feed|
22
+ refresh_for(feed)
23
+ end if refresh
24
+ end
25
+
26
+ def get(id)
27
+
28
+ end
29
+
30
+ def delete(feed)
31
+ $db.execute("delete from items where feed_id = ?", feed['id'])
32
+ $db.execute("delete from feeds where id = ?", feed['id'])
33
+ end
34
+
35
+ def all(min_limit=0, max_limit=-1)
36
+ $db.execute(%{
37
+ select f.*,
38
+ (select count(*) from items iu where iu.feed_id = f.id) as num_items,
39
+ (select count(*) from items ir where ir.read = 0 and ir.feed_id = f.id) as unread
40
+ from feeds f where unread > 0 order by id desc limit #{min_limit},#{max_limit}
41
+ })
42
+ end
43
+
44
+ def refresh
45
+ feeds = $db.execute("select * from feeds")
46
+ results = Parallel.map(feeds, :in_threads => 8) do |feed|
47
+ refresh_for(feed)
48
+ end
49
+ end
50
+
51
+ def refresh_for(feed)
52
+ content = open(feed['url']).read
53
+ rss = FeedParser::Feed::new(content)
54
+
55
+ puts rss.title
56
+
57
+ $db.execute("update feeds set title = ? where id = ?", rss.title, feed['id'])
58
+
59
+ rss.items.each do |item|
60
+ $db.execute("insert or ignore into items (feed_id, title, url, published_at) values (?, ?, ?, ?)", feed['id'], item.title, item.link, item.date.to_i)
61
+ end
62
+ rescue Exception => e
63
+ # no-op
64
+ end
65
+
66
+ def unread(feed_id)
67
+ $db.execute("select * from items where feed_id = ? and read = 0", feed)
68
+ end
69
+
70
+ def mark_as_read(feed)
71
+ $db.execute("update items set read = 1 where feed_id = ?", feed['id'])
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,93 @@
1
+ module Rutt
2
+ class FeedScreen < Screen
3
+ def initialize(stdscr)
4
+ @menu = "q:Quit d:delete r:refresh all"
5
+
6
+ super(stdscr)
7
+ end
8
+
9
+ def display_feeds
10
+ @cur_y = @min_y
11
+
12
+ @feeds = Feed::all(@min_limit, @max_limit)
13
+ @feeds.each do |feed|
14
+ # next if feed['unread'] == 0 # This should be configurable: feed.showread
15
+
16
+ @stdscr.move(@cur_y, 0)
17
+ @stdscr.addstr(" #{feed['unread']}/#{feed['num_items']}\t\t#{feed['title']}\n")
18
+
19
+ @cur_y += 1
20
+ end
21
+
22
+ @cur_y = @min_y
23
+ @stdscr.refresh
24
+ end
25
+
26
+ def window
27
+ @stdscr.clear
28
+
29
+ display_menu
30
+ display_feeds
31
+ move_pointer(0)
32
+ end
33
+
34
+ def loop
35
+ window
36
+
37
+ while true do
38
+ c = @stdscr.getch
39
+
40
+ if c > 0 && c < 255
41
+ case c.chr
42
+ when /q/i
43
+ break
44
+ when /a/i
45
+ # no-op
46
+ when /r/i
47
+ cur_y = @cur_y
48
+
49
+ Feed::refresh
50
+
51
+ window
52
+ move_pointer(cur_y, move_to=true)
53
+ when /d/i
54
+ cur_y = @cur_y - 1
55
+
56
+ @stdscr.clear
57
+ display_menu
58
+ feed = @feeds[cur_y]
59
+ @stdscr.move(2, 0)
60
+ @stdscr.addstr("Are you sure you want to delete #{feed['title']}? ")
61
+ d = @stdscr.getch
62
+ if d.chr =~ /y/i
63
+ Feed::delete(feed)
64
+ end
65
+ window
66
+ move_pointer(cur_y, move_to=true)
67
+ when /p/i
68
+ decr_page
69
+ window
70
+ when /n/i
71
+ incr_page
72
+ window
73
+ when / /
74
+ cur_y = @cur_y
75
+ @stdscr.addstr("#{@feeds[cur_y]}")
76
+ item_screen = ItemScreen.new(@stdscr, @feeds[cur_y - 1])
77
+ item_screen.loop
78
+
79
+ window
80
+ move_pointer(cur_y, move_to=true)
81
+ end
82
+ else
83
+ case c
84
+ when Ncurses::KEY_UP
85
+ move_pointer(-1)
86
+ when Ncurses::KEY_DOWN
87
+ move_pointer(1)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,32 @@
1
+ module Rutt
2
+ module Instapaper
3
+ class API
4
+ Url = "http://www.instapaper.com"
5
+
6
+ def initialize(consumer_key, consumer_secret)
7
+ @consumer_key = consumer_key
8
+ @consumer_secret = consumer_secret
9
+ end
10
+
11
+ def authorize(username, password)
12
+ @consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret, {
13
+ :site => "https://www.instapaper.com",
14
+ :access_token_path => "/api/1/oauth/access_token",
15
+ :http_method => :post
16
+ })
17
+
18
+ access_token = @consumer.get_access_token(nil, {}, {
19
+ :x_auth_username => username,
20
+ :x_auth_password => password,
21
+ :x_auth_mode => "client_auth",
22
+ })
23
+
24
+ @access_token = OAuth::AccessToken.new(@consumer, access_token.token, access_token.secret)
25
+ end
26
+
27
+ def request(path, params={})
28
+ @access_token.request(:post, "#{Url}#{path}", params)
29
+ end
30
+ end
31
+ end
32
+ end
data/lib/rutt/item.rb ADDED
@@ -0,0 +1,40 @@
1
+ module Rutt
2
+ module Item
3
+
4
+ extend self
5
+
6
+ def make_table!
7
+ $db.execute(%{
8
+ create table if not exists items (
9
+ id integer PRIMARY KEY,
10
+ feed_id integer,
11
+ title text,
12
+ url text,
13
+ description text,
14
+ read int default 0,
15
+ prioritize int default 0,
16
+ published_at DATE NOT NULL DEFAULT (datetime('now','localtime')),
17
+ created_at NOT NULL DEFAULT CURRENT_TIMESTAMP,
18
+ updated_at NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
+ UNIQUE(url),
20
+ FOREIGN KEY(feed_id) REFERENCES feeds(id))
21
+ })
22
+ end
23
+
24
+ def all(feed, min_limit=0, max_limit=-1)
25
+ $db.execute("select * from items where feed_id = ? order by published_at desc limit #{min_limit},#{max_limit}", feed['id'])
26
+ end
27
+
28
+ def mark_as_unread(item)
29
+ $db.execute("update items set read = 0 where id = #{item['id']}")
30
+ end
31
+
32
+ def mark_as_read(item)
33
+ $db.execute("update items set read = 1 where id = #{item['id']}")
34
+ end
35
+
36
+ def sent_to_instapaper(item)
37
+ $db.execute("update items set read = 2 where id = #{item['id']}")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,103 @@
1
+ module Rutt
2
+ class ItemScreen < Screen
3
+ def initialize(stdscr, feed)
4
+ @feed = feed
5
+ @menu = " i:quit r:refresh m:mark as read u:mark as unread a:mark all as read b:open in browser"
6
+
7
+ super(stdscr)
8
+ end
9
+
10
+ def display_items
11
+ @cur_y = @min_y
12
+
13
+ @items = Item::all(@feed, @min_limit, @max_limit)
14
+ @items.each do |item|
15
+ item_status = case item['read'].to_i
16
+ when 0 then 'N'
17
+ when 1 then ' '
18
+ when 2 then 'I'
19
+ else ' '
20
+ end
21
+ @stdscr.addstr(" #{item_status}\t#{Time.at(item['published_at']).strftime("%b %d, %Y %R:%M")}\t#{item['title']}\n")
22
+ @cur_y += 1
23
+ end
24
+
25
+ @cur_y = @min_y
26
+ @stdscr.refresh
27
+ end
28
+
29
+ def window
30
+ @stdscr.clear
31
+
32
+ display_menu
33
+ display_items
34
+ move_pointer(0)
35
+ end
36
+
37
+ def loop
38
+ window
39
+
40
+ while true do
41
+ c = @stdscr.getch
42
+
43
+ if c > 0 && c < 255
44
+ case c.chr
45
+ when /[iq]/i
46
+ break
47
+ when /s/i
48
+ cur_y = @cur_y - 1
49
+ $instapaper.request('/api/1/bookmarks/add', {
50
+ 'url' => @items[cur_y]['url'],
51
+ 'title' => @items[cur_y]['title'],
52
+ })
53
+ Item::sent_to_instapaper(@items[cur_y])
54
+ window
55
+ move_pointer(@cur_y, move_to=true)
56
+ when /a/i
57
+ Feed::mark_as_read(@feed)
58
+ window
59
+ move_pointer(@cur_y, move_to=true)
60
+ when /p/i
61
+ decr_page
62
+ window
63
+ when /n/i
64
+ incr_page
65
+ window
66
+ when /b/i
67
+ cur_y = @cur_y - 1
68
+ Item::mark_as_read(@items[cur_y])
69
+ Launchy.open(@items[cur_y]['url'])
70
+ window
71
+ move_pointer(@cur_y, move_to=true)
72
+ when /m/i
73
+ cur_y = @cur_y - 1
74
+ Item::mark_as_read(@items[cur_y])
75
+ window
76
+ move_pointer(cur_y + 1, move_to=true)
77
+ when /u/i
78
+ cur_y = @cur_y - 1
79
+ Item::mark_as_unread(@items[cur_y])
80
+ window
81
+ move_pointer(cur_y + 1, move_to=true)
82
+ when /r/i
83
+ Feed::refresh_for(@feed)
84
+ window
85
+ when / /
86
+ content_screen = ContentScreen.new(@stdscr, @items[@cur_y - 1])
87
+ content_screen.loop
88
+
89
+ window
90
+ move_pointer(@cur_y, move_to=true)
91
+ end
92
+ else
93
+ case c
94
+ when Ncurses::KEY_UP
95
+ move_pointer(-1)
96
+ when Ncurses::KEY_DOWN
97
+ move_pointer(1)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
data/lib/rutt/opml.rb ADDED
@@ -0,0 +1,21 @@
1
+ module Rutt
2
+ module Opml
3
+ def self.get_urls(file)
4
+ doc = Nokogiri::XML(open(file))
5
+
6
+ urls = []
7
+
8
+ doc.xpath('opml/body/outline').each do |outline|
9
+ if outline['xmlUrl']
10
+ urls << outline['xmlUrl']
11
+ else
12
+ (outline/'outline').each do |outline2|
13
+ urls << outline2['xmlUrl']
14
+ end
15
+ end
16
+ end
17
+
18
+ return urls
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,51 @@
1
+ module Rutt
2
+ class Screen
3
+ def initialize(stdscr)
4
+ @stdscr = stdscr
5
+
6
+ @min_y = 1
7
+ @max_y = @stdscr.getmaxy
8
+
9
+ @min_limit = @min_y - 1
10
+ @max_limit = @max_y - 5
11
+
12
+ @cur_y = 1
13
+ @cur_x = 0
14
+ end
15
+
16
+ def incr_page
17
+ @min_limit = @max_limit
18
+ @max_limit += (@max_y - 5)
19
+ end
20
+
21
+ def decr_page
22
+ @max_limit = @min_limit
23
+ @min_limit -= (@max_y - 5)
24
+
25
+ if @max_limit <= 0
26
+ @min_limit = @min_y - 1
27
+ @max_limit = @max_y - 5
28
+ end
29
+ end
30
+
31
+ def display_menu
32
+ @stdscr.clear
33
+ @stdscr.move(0, 0)
34
+ @stdscr.addstr(" rutt #{@menu}\n")
35
+ end
36
+
37
+ def move_pointer(pos, move_to=false)
38
+ @stdscr.move(@cur_y, 0)
39
+ @stdscr.addstr(" ")
40
+
41
+ if move_to == true
42
+ @cur_y = pos
43
+ else
44
+ @cur_y += pos
45
+ end
46
+
47
+ @stdscr.move(@cur_y, 0)
48
+ @stdscr.addstr(">")
49
+ end
50
+ end
51
+ end
data/rutt.gemspec CHANGED
@@ -5,13 +5,13 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{rutt}
8
- s.version = "0.3.2"
8
+ s.version = "0.3.3"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Abhi Yerra"]
12
12
  s.date = %q{2011-04-23}
13
13
  s.default_executable = %q{rutt}
14
- s.description = %q{Read RSS feeds from the commandline }
14
+ s.description = %q{The Mutt of RSS/Atom feeds.}
15
15
  s.email = %q{abhi@berkeley.edu}
16
16
  s.executables = ["rutt"]
17
17
  s.extra_rdoc_files = [
@@ -28,8 +28,16 @@ Gem::Specification.new do |s|
28
28
  "Rakefile",
29
29
  "VERSION",
30
30
  "bin/rutt",
31
- "lib/instapaper.rb",
32
31
  "lib/rutt.rb",
32
+ "lib/rutt/config.rb",
33
+ "lib/rutt/content_screen.rb",
34
+ "lib/rutt/feed.rb",
35
+ "lib/rutt/feed_screen.rb",
36
+ "lib/rutt/instapaper.rb",
37
+ "lib/rutt/item.rb",
38
+ "lib/rutt/item_screen.rb",
39
+ "lib/rutt/opml.rb",
40
+ "lib/rutt/screen.rb",
33
41
  "rutt.gemspec",
34
42
  "spec/rutt_spec.rb",
35
43
  "spec/spec_helper.rb"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rutt
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 21
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 3
9
- - 2
10
- version: 0.3.2
9
+ - 3
10
+ version: 0.3.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Abhi Yerra
@@ -178,7 +178,7 @@ dependencies:
178
178
  name: rcov
179
179
  version_requirements: *id011
180
180
  prerelease: false
181
- description: "Read RSS feeds from the commandline "
181
+ description: The Mutt of RSS/Atom feeds.
182
182
  email: abhi@berkeley.edu
183
183
  executables:
184
184
  - rutt
@@ -197,8 +197,16 @@ files:
197
197
  - Rakefile
198
198
  - VERSION
199
199
  - bin/rutt
200
- - lib/instapaper.rb
201
200
  - lib/rutt.rb
201
+ - lib/rutt/config.rb
202
+ - lib/rutt/content_screen.rb
203
+ - lib/rutt/feed.rb
204
+ - lib/rutt/feed_screen.rb
205
+ - lib/rutt/instapaper.rb
206
+ - lib/rutt/item.rb
207
+ - lib/rutt/item_screen.rb
208
+ - lib/rutt/opml.rb
209
+ - lib/rutt/screen.rb
202
210
  - rutt.gemspec
203
211
  - spec/rutt_spec.rb
204
212
  - spec/spec_helper.rb
data/lib/instapaper.rb DELETED
@@ -1,34 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- require 'rubygems'
3
- require 'oauth'
4
-
5
- module Instapaper
6
- class API
7
- Url = "http://www.instapaper.com"
8
-
9
- def initialize(consumer_key, consumer_secret)
10
- @consumer_key = consumer_key
11
- @consumer_secret = consumer_secret
12
- end
13
-
14
- def authorize(username, password)
15
- @consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret, {
16
- :site => "https://www.instapaper.com",
17
- :access_token_path => "/api/1/oauth/access_token",
18
- :http_method => :post
19
- })
20
-
21
- access_token = @consumer.get_access_token(nil, {}, {
22
- :x_auth_username => username,
23
- :x_auth_password => password,
24
- :x_auth_mode => "client_auth",
25
- })
26
-
27
- @access_token = OAuth::AccessToken.new(@consumer, access_token.token, access_token.secret)
28
- end
29
-
30
- def request(path, params={})
31
- @access_token.request(:post, "#{Url}#{path}", params)
32
- end
33
- end
34
- end