rutt 0.3.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/.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
|