rutt 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +20 -0
- data/README.org +25 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/bin/rutt +74 -0
- data/lib/instapaper.rb +34 -0
- data/lib/rutt.rb +477 -0
- data/spec/rutt_spec.rb +7 -0
- data/spec/spec_helper.rb +12 -0
- metadata +338 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem "launchy"
|
4
|
+
gem "ncurses"
|
5
|
+
gem "nokogiri"
|
6
|
+
gem "parallel"
|
7
|
+
gem "ruby-feedparser"
|
8
|
+
gem "sqlite3"
|
9
|
+
gem "oauth"
|
10
|
+
|
11
|
+
group :development do
|
12
|
+
gem "rspec", "~> 2.3.0"
|
13
|
+
gem "bundler", "~> 1.0.0"
|
14
|
+
gem "jeweler", "~> 1.5.2"
|
15
|
+
gem "rcov", ">= 0"
|
16
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
configuration (1.2.0)
|
5
|
+
diff-lcs (1.1.2)
|
6
|
+
git (1.2.5)
|
7
|
+
jeweler (1.5.2)
|
8
|
+
bundler (~> 1.0.0)
|
9
|
+
git (>= 1.2.5)
|
10
|
+
rake
|
11
|
+
launchy (0.4.0)
|
12
|
+
configuration (>= 0.0.5)
|
13
|
+
rake (>= 0.8.1)
|
14
|
+
ncurses (0.9.1)
|
15
|
+
nokogiri (1.4.4)
|
16
|
+
oauth (0.4.4)
|
17
|
+
parallel (0.5.3)
|
18
|
+
rake (0.8.7)
|
19
|
+
rcov (0.9.9)
|
20
|
+
rspec (2.3.0)
|
21
|
+
rspec-core (~> 2.3.0)
|
22
|
+
rspec-expectations (~> 2.3.0)
|
23
|
+
rspec-mocks (~> 2.3.0)
|
24
|
+
rspec-core (2.3.1)
|
25
|
+
rspec-expectations (2.3.0)
|
26
|
+
diff-lcs (~> 1.1.2)
|
27
|
+
rspec-mocks (2.3.0)
|
28
|
+
ruby-feedparser (0.7)
|
29
|
+
sqlite3 (1.3.3)
|
30
|
+
|
31
|
+
PLATFORMS
|
32
|
+
ruby
|
33
|
+
|
34
|
+
DEPENDENCIES
|
35
|
+
bundler (~> 1.0.0)
|
36
|
+
jeweler (~> 1.5.2)
|
37
|
+
launchy
|
38
|
+
ncurses
|
39
|
+
nokogiri
|
40
|
+
oauth
|
41
|
+
parallel
|
42
|
+
rcov
|
43
|
+
rspec (~> 2.3.0)
|
44
|
+
ruby-feedparser
|
45
|
+
sqlite3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Abhi Yerra
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.org
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
* Rutt
|
2
|
+
|
3
|
+
** Description
|
4
|
+
|
5
|
+
Rutt is the Mutt of news reader. It attempts to be the fastest way
|
6
|
+
to read news feeds.
|
7
|
+
|
8
|
+
Currently rutt is still in heavy development and it is a bit
|
9
|
+
unstable although the main features are largely implemented.
|
10
|
+
It still needs a bit of polishing before it can be considered
|
11
|
+
stable.
|
12
|
+
|
13
|
+
** Dependencies
|
14
|
+
- Ruby 1.8
|
15
|
+
- elinks (For now.)
|
16
|
+
|
17
|
+
** Download & Repository
|
18
|
+
|
19
|
+
Rutt is still in heavy development so please
|
20
|
+
check out the [[https://github.com/abhiyerra/rutt][repository]] to use it.
|
21
|
+
|
22
|
+
- [[https://github.com/abhiyerra/rutt]]
|
23
|
+
|
24
|
+
Downloads are located here:
|
25
|
+
- [[https://github.com/abhiyerra/rutt/downloads]]
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "rutt"
|
16
|
+
gem.homepage = "http://github.com/abhiyerra/rutt"
|
17
|
+
gem.license = "BSD"
|
18
|
+
gem.summary = %Q{The Mutt of RSS/Atom feeds.}
|
19
|
+
gem.description = %Q{Read RSS feeds from the commandline }
|
20
|
+
gem.email = "abhi@berkeley.edu"
|
21
|
+
gem.authors = ["Abhi Yerra"]
|
22
|
+
|
23
|
+
gem.add_runtime_dependency "launchy"
|
24
|
+
gem.add_runtime_dependency "ncurses"
|
25
|
+
gem.add_runtime_dependency "nokogiri"
|
26
|
+
gem.add_runtime_dependency "parallel"
|
27
|
+
gem.add_runtime_dependency "ruby-feedparser"
|
28
|
+
gem.add_runtime_dependency "sqlite3"
|
29
|
+
gem.add_runtime_dependency "oauth"
|
30
|
+
end
|
31
|
+
Jeweler::RubygemsDotOrgTasks.new
|
32
|
+
|
33
|
+
require 'rspec/core'
|
34
|
+
require 'rspec/core/rake_task'
|
35
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
36
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
37
|
+
end
|
38
|
+
|
39
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
40
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
41
|
+
spec.rcov = true
|
42
|
+
end
|
43
|
+
|
44
|
+
task :default => :spec
|
45
|
+
|
46
|
+
require 'rake/rdoctask'
|
47
|
+
Rake::RDocTask.new do |rdoc|
|
48
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
49
|
+
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
51
|
+
rdoc.title = "rutt #{version}"
|
52
|
+
rdoc.rdoc_files.include('README*')
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.0
|
data/bin/rutt
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'launchy'
|
6
|
+
require 'ncurses'
|
7
|
+
require 'nokogiri'
|
8
|
+
require 'open-uri'
|
9
|
+
require 'optparse'
|
10
|
+
require 'rss/1.0'
|
11
|
+
require 'rss/2.0'
|
12
|
+
require 'sqlite3'
|
13
|
+
require 'feedparser'
|
14
|
+
require 'parallel'
|
15
|
+
require 'instapaper'
|
16
|
+
|
17
|
+
require 'rutt'
|
18
|
+
require 'instapaper'
|
19
|
+
|
20
|
+
$db = SQLite3::Database.new('rutt.db')
|
21
|
+
$db.results_as_hash = true
|
22
|
+
|
23
|
+
def main
|
24
|
+
|
25
|
+
Config::make_table!
|
26
|
+
Feed::make_table!
|
27
|
+
Item::make_table!
|
28
|
+
|
29
|
+
options = {}
|
30
|
+
OptionParser.new do |opts|
|
31
|
+
opts.banner = "Usage: rutt.rb [options]"
|
32
|
+
|
33
|
+
opts.on('-a', '--add FEED', "Add a feed") do |url|
|
34
|
+
Feed::add(url)
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on('-r', '--refresh', "Refresh feeds.") do
|
39
|
+
Feed::refresh
|
40
|
+
exit
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('-o', '--import-opml FILE', "Import opml") do |file|
|
44
|
+
urls = Opml::get_urls(file)
|
45
|
+
|
46
|
+
urls.each do |url|
|
47
|
+
puts "Adding #{url}"
|
48
|
+
Feed::add(url, false)
|
49
|
+
end
|
50
|
+
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on('-s', '--set-key', "Set config") do |key, value|
|
55
|
+
Config.set(ARGV[0], ARGV[1])
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
|
59
|
+
# opts.on('-l', '--list-feeds', action='store_true', help="List the feeds")
|
60
|
+
|
61
|
+
end.parse!
|
62
|
+
|
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")
|
67
|
+
|
68
|
+
$instapaper = Instapaper::API.new(consumer_key, consumer_secret)
|
69
|
+
$instapaper.authorize(username, password)
|
70
|
+
|
71
|
+
start_screen
|
72
|
+
end
|
73
|
+
|
74
|
+
main
|
data/lib/instapaper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
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
|
data/lib/rutt.rb
ADDED
@@ -0,0 +1,477 @@
|
|
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
|
+
|
data/spec/rutt_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'rutt'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,338 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rutt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 3
|
9
|
+
- 0
|
10
|
+
version: 0.3.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Abhi Yerra
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-04-23 00:00:00 +00:00
|
19
|
+
default_executable: rutt
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
type: :runtime
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
name: launchy
|
33
|
+
version_requirements: *id001
|
34
|
+
prerelease: false
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
type: :runtime
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
name: ncurses
|
47
|
+
version_requirements: *id002
|
48
|
+
prerelease: false
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
type: :runtime
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
name: nokogiri
|
61
|
+
version_requirements: *id003
|
62
|
+
prerelease: false
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
type: :runtime
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 3
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
name: parallel
|
75
|
+
version_requirements: *id004
|
76
|
+
prerelease: false
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
type: :runtime
|
79
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
hash: 3
|
85
|
+
segments:
|
86
|
+
- 0
|
87
|
+
version: "0"
|
88
|
+
name: ruby-feedparser
|
89
|
+
version_requirements: *id005
|
90
|
+
prerelease: false
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
type: :runtime
|
93
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
hash: 3
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
version: "0"
|
102
|
+
name: sqlite3
|
103
|
+
version_requirements: *id006
|
104
|
+
prerelease: false
|
105
|
+
- !ruby/object:Gem::Dependency
|
106
|
+
type: :runtime
|
107
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
hash: 3
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
version: "0"
|
116
|
+
name: oauth
|
117
|
+
version_requirements: *id007
|
118
|
+
prerelease: false
|
119
|
+
- !ruby/object:Gem::Dependency
|
120
|
+
type: :development
|
121
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ~>
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
hash: 3
|
127
|
+
segments:
|
128
|
+
- 2
|
129
|
+
- 3
|
130
|
+
- 0
|
131
|
+
version: 2.3.0
|
132
|
+
name: rspec
|
133
|
+
version_requirements: *id008
|
134
|
+
prerelease: false
|
135
|
+
- !ruby/object:Gem::Dependency
|
136
|
+
type: :development
|
137
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ~>
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
hash: 23
|
143
|
+
segments:
|
144
|
+
- 1
|
145
|
+
- 0
|
146
|
+
- 0
|
147
|
+
version: 1.0.0
|
148
|
+
name: bundler
|
149
|
+
version_requirements: *id009
|
150
|
+
prerelease: false
|
151
|
+
- !ruby/object:Gem::Dependency
|
152
|
+
type: :development
|
153
|
+
requirement: &id010 !ruby/object:Gem::Requirement
|
154
|
+
none: false
|
155
|
+
requirements:
|
156
|
+
- - ~>
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
hash: 7
|
159
|
+
segments:
|
160
|
+
- 1
|
161
|
+
- 5
|
162
|
+
- 2
|
163
|
+
version: 1.5.2
|
164
|
+
name: jeweler
|
165
|
+
version_requirements: *id010
|
166
|
+
prerelease: false
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
type: :development
|
169
|
+
requirement: &id011 !ruby/object:Gem::Requirement
|
170
|
+
none: false
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
hash: 3
|
175
|
+
segments:
|
176
|
+
- 0
|
177
|
+
version: "0"
|
178
|
+
name: rcov
|
179
|
+
version_requirements: *id011
|
180
|
+
prerelease: false
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
type: :runtime
|
183
|
+
requirement: &id012 !ruby/object:Gem::Requirement
|
184
|
+
none: false
|
185
|
+
requirements:
|
186
|
+
- - ">="
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
hash: 3
|
189
|
+
segments:
|
190
|
+
- 0
|
191
|
+
version: "0"
|
192
|
+
name: launchy
|
193
|
+
version_requirements: *id012
|
194
|
+
prerelease: false
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
type: :runtime
|
197
|
+
requirement: &id013 !ruby/object:Gem::Requirement
|
198
|
+
none: false
|
199
|
+
requirements:
|
200
|
+
- - ">="
|
201
|
+
- !ruby/object:Gem::Version
|
202
|
+
hash: 3
|
203
|
+
segments:
|
204
|
+
- 0
|
205
|
+
version: "0"
|
206
|
+
name: ncurses
|
207
|
+
version_requirements: *id013
|
208
|
+
prerelease: false
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
type: :runtime
|
211
|
+
requirement: &id014 !ruby/object:Gem::Requirement
|
212
|
+
none: false
|
213
|
+
requirements:
|
214
|
+
- - ">="
|
215
|
+
- !ruby/object:Gem::Version
|
216
|
+
hash: 3
|
217
|
+
segments:
|
218
|
+
- 0
|
219
|
+
version: "0"
|
220
|
+
name: nokogiri
|
221
|
+
version_requirements: *id014
|
222
|
+
prerelease: false
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
type: :runtime
|
225
|
+
requirement: &id015 !ruby/object:Gem::Requirement
|
226
|
+
none: false
|
227
|
+
requirements:
|
228
|
+
- - ">="
|
229
|
+
- !ruby/object:Gem::Version
|
230
|
+
hash: 3
|
231
|
+
segments:
|
232
|
+
- 0
|
233
|
+
version: "0"
|
234
|
+
name: parallel
|
235
|
+
version_requirements: *id015
|
236
|
+
prerelease: false
|
237
|
+
- !ruby/object:Gem::Dependency
|
238
|
+
type: :runtime
|
239
|
+
requirement: &id016 !ruby/object:Gem::Requirement
|
240
|
+
none: false
|
241
|
+
requirements:
|
242
|
+
- - ">="
|
243
|
+
- !ruby/object:Gem::Version
|
244
|
+
hash: 3
|
245
|
+
segments:
|
246
|
+
- 0
|
247
|
+
version: "0"
|
248
|
+
name: ruby-feedparser
|
249
|
+
version_requirements: *id016
|
250
|
+
prerelease: false
|
251
|
+
- !ruby/object:Gem::Dependency
|
252
|
+
type: :runtime
|
253
|
+
requirement: &id017 !ruby/object:Gem::Requirement
|
254
|
+
none: false
|
255
|
+
requirements:
|
256
|
+
- - ">="
|
257
|
+
- !ruby/object:Gem::Version
|
258
|
+
hash: 3
|
259
|
+
segments:
|
260
|
+
- 0
|
261
|
+
version: "0"
|
262
|
+
name: sqlite3
|
263
|
+
version_requirements: *id017
|
264
|
+
prerelease: false
|
265
|
+
- !ruby/object:Gem::Dependency
|
266
|
+
type: :runtime
|
267
|
+
requirement: &id018 !ruby/object:Gem::Requirement
|
268
|
+
none: false
|
269
|
+
requirements:
|
270
|
+
- - ">="
|
271
|
+
- !ruby/object:Gem::Version
|
272
|
+
hash: 3
|
273
|
+
segments:
|
274
|
+
- 0
|
275
|
+
version: "0"
|
276
|
+
name: oauth
|
277
|
+
version_requirements: *id018
|
278
|
+
prerelease: false
|
279
|
+
description: "Read RSS feeds from the commandline "
|
280
|
+
email: abhi@berkeley.edu
|
281
|
+
executables:
|
282
|
+
- rutt
|
283
|
+
extensions: []
|
284
|
+
|
285
|
+
extra_rdoc_files:
|
286
|
+
- LICENSE.txt
|
287
|
+
- README.org
|
288
|
+
files:
|
289
|
+
- .document
|
290
|
+
- .rspec
|
291
|
+
- Gemfile
|
292
|
+
- Gemfile.lock
|
293
|
+
- LICENSE.txt
|
294
|
+
- README.org
|
295
|
+
- Rakefile
|
296
|
+
- VERSION
|
297
|
+
- bin/rutt
|
298
|
+
- lib/instapaper.rb
|
299
|
+
- lib/rutt.rb
|
300
|
+
- spec/rutt_spec.rb
|
301
|
+
- spec/spec_helper.rb
|
302
|
+
has_rdoc: true
|
303
|
+
homepage: http://github.com/abhiyerra/rutt
|
304
|
+
licenses:
|
305
|
+
- BSD
|
306
|
+
post_install_message:
|
307
|
+
rdoc_options: []
|
308
|
+
|
309
|
+
require_paths:
|
310
|
+
- lib
|
311
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
312
|
+
none: false
|
313
|
+
requirements:
|
314
|
+
- - ">="
|
315
|
+
- !ruby/object:Gem::Version
|
316
|
+
hash: 3
|
317
|
+
segments:
|
318
|
+
- 0
|
319
|
+
version: "0"
|
320
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
321
|
+
none: false
|
322
|
+
requirements:
|
323
|
+
- - ">="
|
324
|
+
- !ruby/object:Gem::Version
|
325
|
+
hash: 3
|
326
|
+
segments:
|
327
|
+
- 0
|
328
|
+
version: "0"
|
329
|
+
requirements: []
|
330
|
+
|
331
|
+
rubyforge_project:
|
332
|
+
rubygems_version: 1.6.2
|
333
|
+
signing_key:
|
334
|
+
specification_version: 3
|
335
|
+
summary: The Mutt of RSS/Atom feeds.
|
336
|
+
test_files:
|
337
|
+
- spec/rutt_spec.rb
|
338
|
+
- spec/spec_helper.rb
|