vnews 0.0.1

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/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .DS_Store
2
+ *swp
3
+ login.yml
4
+ gmail.yml
5
+ *.log
6
+ pkg/
7
+ .rvmrc
8
+ notes.txt
9
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :rubygems
2
+ gem 'nokogiri'
3
+ gem 'feed_yamlizer', '>= 0.0.3'
4
+ gem 'mysql2'
data/Gemfile.lock ADDED
@@ -0,0 +1,19 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ feed_yamlizer (0.0.2)
5
+ htmlentities
6
+ nokogiri
7
+ sqlite3-ruby
8
+ htmlentities (4.2.4)
9
+ nokogiri (1.4.4)
10
+ sqlite3 (1.3.3)
11
+ sqlite3-ruby (1.3.3)
12
+ sqlite3 (>= 1.3.3)
13
+
14
+ PLATFORMS
15
+ ruby
16
+
17
+ DEPENDENCIES
18
+ feed_yamlizer
19
+ nokogiri
data/MIT-LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2010 Daniel Choi, http://danielchoi.com/software/
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.
21
+
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), 'lib')
2
+ require 'couchrest'
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'vnews'
6
+ require 'rake'
7
+ require 'rake/testtask'
8
+ require 'bundler'
9
+ Bundler::GemHelper.install_tasks
10
+
11
+ desc "Save couchdb views in lib/couchviews.yml"
12
+ task :create_views do
13
+ db = CouchRest.database! "http://localhost:5984/vnews"
14
+ views = YAML::load File.read("lib/couchviews.yml")
15
+ begin
16
+ rev = db.get(views['_id'])['_rev']
17
+ puts db.save_doc(views.merge('_rev' => rev))
18
+ rescue RestClient::ResourceNotFound
19
+ puts db.save_doc(views)
20
+ end
21
+ end
22
+
23
+ desc "List feeds with recent entry titles"
24
+ task :list_feeds do
25
+ vnews = Vnews::Aggregator.new
26
+ puts vnews.list_feeds.inspect
27
+ end
28
+
29
+ desc "Run tests"
30
+ task :test do
31
+ $:.unshift File.expand_path("test")
32
+ MiniTest::Unit.autorun
33
+ end
34
+
35
+ task :default => :test
36
+
37
+
data/bin/vnews-client ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # for dev only; comment out for production
4
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
5
+
6
+ require 'vnews/display'
7
+
8
+ case ARGV.first
9
+ when 'update_feed'
10
+ require 'vnews/feed'
11
+ puts Vnews::Feed.update_feed ARGV[1]
12
+ when 'update_folder'
13
+ require 'vnews/folder'
14
+ puts Vnews::Folder.update_folder ARGV[1]
15
+ else
16
+ puts Vnews::Display.new.send *ARGV
17
+ end
18
+
19
+
data/lib/create.sql ADDED
@@ -0,0 +1,36 @@
1
+ create table feeds (
2
+ title varchar(255),
3
+ feed_url varchar(255),
4
+ link varchar(255),
5
+ num_items_read int default 0, /*a counter of the number of items read from this feed*/
6
+ last_viewed_at datetime,
7
+ primary key (feed_url)
8
+ ) type=MyISAM;
9
+
10
+ create table items (
11
+ guid varchar(255),
12
+ feed varchar(255), /* feed link is the foreign key */
13
+ feed_title varchar(255), /*denorm a little*/
14
+ title varchar(255),
15
+ link varchar(255),
16
+ pub_date datetime,
17
+ author varchar(255),
18
+ text text,
19
+ word_count int unsigned,
20
+ unread bool default true,
21
+ starred bool default false,
22
+ primary key (feed,guid),
23
+ fulltext (title, text)
24
+ ) type=MyISAM;
25
+
26
+ alter table items add index feed (feed(5));
27
+
28
+ create table feeds_folders (
29
+ feed varchar(255), /* the feed link */
30
+ folder varchar(255), /* folder title */
31
+ last_viewed_at datetime,
32
+ unique key (feed, folder)
33
+ ) type=MyISAM;
34
+
35
+ alter table feeds_folders add index folder (folder(5));
36
+
data/lib/vnews.rb ADDED
@@ -0,0 +1,63 @@
1
+ require 'vnews/version'
2
+ require 'vnews/config'
3
+ require 'vnews/feed'
4
+ require 'logger'
5
+ require 'drb'
6
+
7
+ class Vnews
8
+
9
+ def self.start
10
+ puts "Starting vnews #{Vnews::VERSION}"
11
+
12
+ if ! File.exists?(File.expand_path(Vnews::Config::CONFIGPATH))
13
+ puts "Missing #{Vnews::Config::CONFIGPATH}"
14
+ # generate this file
15
+ puts "Generating stub config file at #{Vnews::Config::CONFIGPATH}"
16
+ File.open(Vnews::Config::CONFIGPATH, 'w') {|f| f.write(Config.stub_config)}
17
+ puts "Please edit this file and then run `vnews --create-db` to create your Vnews MySQL database."
18
+ exit
19
+ end
20
+
21
+ if ARGV.first == "--create-db"
22
+ c = File.read(Vnews::Config::CONFIGPATH)
23
+ top, bottom = c.split(/^\s*$/,2)
24
+ dbconfig = YAML::load(top)
25
+ puts "Creating database: #{dbconfig['database']}"
26
+ Vnews::Sql.create_db dbconfig
27
+ puts "OK if everything went ok, you can create your feeds and folders with `vnews -u`."
28
+ exit
29
+ end
30
+
31
+ if ARGV.first == "--opml"
32
+ require 'vnews/opml'
33
+ # opml file must be second arg
34
+ puts "Importing OPML file #{ARGV[1]}"
35
+ Vnews::Opml.import File.read(ARGV[1])
36
+ # rewrite .vnewsrc config
37
+ puts "Rewriting config file #{Vnews::Config::CONFIGPATH} to reflect changes."
38
+ Vnews::Config.rewrite_config
39
+ puts "Done."
40
+ end
41
+
42
+ if ['--update', '-u'].include?(ARGV.first)
43
+ Vnews::Config.update_folders
44
+ end
45
+
46
+ Vnews.sql_client # loads the config
47
+
48
+ vim = ENV['VMAIL_VIM'] || 'vim'
49
+ vimscript = File.join(File.dirname(__FILE__), "vnews.vim")
50
+ vim_command = "#{vim} -S #{vimscript} "
51
+ STDERR.puts vim_command
52
+ system(vim_command)
53
+ if vim == 'mvim'
54
+ DRb.thread.join
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+
61
+ if __FILE__ == $0
62
+ Vnews.start
63
+ end
data/lib/vnews.vim ADDED
@@ -0,0 +1,472 @@
1
+ " Vim script that turns Vim into a feed reader
2
+ " Maintainer: Daniel Choi <dhchoi@gnews.com>
3
+ " License: MIT License (c) 2011 Daniel Choi
4
+
5
+ if exists("g:VnewsLoaded") || &cp || version < 700
6
+ finish
7
+ endif
8
+ let g:VnewsLoaded = 1
9
+
10
+ let mapleader = ','
11
+ highlight VnewsSearchTerm ctermbg=green guibg=green
12
+
13
+
14
+ "let s:client_script = 'vnews-client '
15
+ let s:client_script = 'bin/vnews-client '
16
+ let s:list_folders_command = s:client_script . 'folders '
17
+ let s:list_feeds_command = s:client_script . 'feeds '
18
+ let s:list_folder_items_command = s:client_script . 'folder_items '
19
+ let s:list_feed_items_command = s:client_script . 'feed_items '
20
+ let s:show_item_command = s:client_script . 'show_item '
21
+ let s:star_item_command = s:client_script . 'star_item ' " + guid star(bool)
22
+ let s:unstar_item_command = s:client_script . 'unstar_item ' " + guid star(bool)
23
+ let s:delete_item_command = s:client_script . 'delete_item ' " + guid
24
+ let s:search_items_command = s:client_script . 'search_items '
25
+ let s:cat_items_command = s:client_script . 'cat_items '
26
+
27
+ let s:folder = "All"
28
+ let s:feed = "All"
29
+ function! VnewsStatusLine()
30
+ let end_index = match(s:last_selection, '(\d\+)$')
31
+ let selection = s:last_selection[0:end_index-1]
32
+ return "%<%f\ " . s:selectiontype . " " . selection . "%r%=%-14.(%l,%c%V%)\ %P"
33
+ endfunction
34
+
35
+ func! s:trimString(string)
36
+ let string = substitute(a:string, '\s\+$', '', '')
37
+ return substitute(string, '^\s\+', '', '')
38
+ endfunc
39
+
40
+
41
+ function! s:common_mappings()
42
+ nnoremap <silent> <buffer> <Space> :call <SID>toggle_maximize_window()<cr>
43
+ nnoremap <buffer> <leader>n :call <SID>list_folders()<CR>
44
+ nnoremap <buffer> <leader>m :call <SID>list_feeds(0)<CR>
45
+ nnoremap <buffer> <leader>M :call <SID>list_feeds(1)<CR>
46
+ nnoremap <buffer> <leader>* :call <SID>toggle_star()<CR>
47
+ nnoremap <buffer> <leader>8 :call <SID>toggle_star()<CR>
48
+ nnoremap <buffer> <leader># :call <SID>delete_item()<CR>
49
+ nnoremap <buffer> <leader>3 :call <SID>delete_item()<CR>
50
+ nnoremap <buffer> u :call <SID>update_feed()<CR>
51
+ nnoremap <buffer> <leader>u :call <SID>update_feed()<CR>
52
+ command! -bar -nargs=0 VNUpdateFeed :call <SID>update_feed()
53
+ endfunc
54
+
55
+ function! s:create_list_window()
56
+ new list-window
57
+ wincmd p
58
+ close
59
+ setlocal bufhidden=hide
60
+ setlocal buftype=nofile
61
+ setlocal modifiable
62
+ setlocal textwidth=0
63
+ setlocal nowrap
64
+ setlocal number
65
+ setlocal foldcolumn=0
66
+ setlocal nospell
67
+ " setlocal noreadonly
68
+ " hi CursorLine cterm=NONE ctermbg=darkred ctermfg=white guibg=darkred guifg=white
69
+ setlocal cursorline
70
+ " we need to find the window later
71
+ let s:listbufnr = bufnr('')
72
+ let s:listbufname = bufname('')
73
+ setlocal statusline=%!VnewsStatusLine()
74
+ nnoremap <silent> <buffer> <cr> :call <SID>show_item_under_cursor(1)<CR>
75
+ nnoremap <silent> <buffer> <c-l> :call <SID>show_item_under_cursor(1)<CR>:wincmd p<CR>
76
+ nnoremap <silent> <buffer> <c-j> :call <SID>show_adjacent_item(0, 'list-window')<CR>
77
+ nnoremap <silent> <buffer> <c-k> :call <SID>show_adjacent_item(1, 'list-window')<CR>
78
+ command! -bar -nargs=0 -range VNDelete :<line1>,<line2>call s:delete_item()
79
+ command! -bar -nargs=0 -range VNConcat :<line1>,<line2>call s:cat_items()
80
+ call s:common_mappings()
81
+ if !exists("g:VnewsStarredColor")
82
+ let g:VnewsStarredColor = "ctermfg=green guifg=green guibg=grey"
83
+ endif
84
+ syn match VnewsBufferStarred /^*.*/hs=s
85
+ exec "hi def VnewsBufferStarred " . g:VnewsStarredColor
86
+ endfunction
87
+
88
+ function! s:create_item_window()
89
+ rightbelow split item-window
90
+ setlocal modifiable
91
+ setlocal buftype=nofile
92
+ let s:itembufnr = bufnr('%')
93
+ nnoremap <silent> <buffer> <cr> <C-W>=<C-W>p
94
+ nnoremap <silent> <buffer> <c-j> :call <SID>show_adjacent_item(0, "item-window")<CR>
95
+ nnoremap <silent> <buffer> <c-k> :call <SID>show_adjacent_item(1, "item-window")<CR>
96
+ nnoremap <silent> <buffer> q :call <SID>close_item_window()<cr>
97
+ nnoremap <buffer> <leader>o :call <SID>find_next_href_and_open()<CR>
98
+ " opens the linked item
99
+ nnoremap <buffer> <leader>h :normal Gkk<CR>:call <SID>find_next_href_and_open()<CR>
100
+ call s:common_mappings()
101
+ close
102
+ endfunction
103
+
104
+ function! s:focus_window(target_bufnr)
105
+ if bufwinnr(a:target_bufnr) == winnr()
106
+ return
107
+ end
108
+ let winnr = bufwinnr(a:target_bufnr)
109
+ if winnr == -1
110
+ if a:target_bufnr == s:listbufnr
111
+ leftabove split
112
+ else
113
+ rightbelow split
114
+ endif
115
+ exec "buffer" . a:target_bufnr
116
+ else
117
+ exec winnr . "wincmd w"
118
+ endif
119
+ " set up syntax highlighting
120
+ if has("syntax")
121
+ "
122
+ endif
123
+ endfunction
124
+
125
+ function! s:open_selection_window(selectionlist, buffer_name, prompt)
126
+ let s:selectionlist = a:selectionlist
127
+ call s:focus_window(s:listbufnr)
128
+ exec "leftabove split ".a:buffer_name
129
+ setlocal textwidth=0
130
+ setlocal completefunc=CompleteFunction
131
+ setlocal buftype=nofile
132
+ setlocal noswapfile
133
+ setlocal modifiable
134
+ resize 1
135
+ inoremap <silent> <buffer> <cr> <Esc>:call <SID>select_folder_or_feed()<CR>
136
+ noremap <buffer> q <Esc>:close<cr>
137
+ inoremap <buffer> <Esc> <Esc>:close<cr>
138
+ call setline(1, a:prompt)
139
+ let s:prompt = a:prompt
140
+ normal $
141
+ call feedkeys("a\<c-x>\<c-u>\<c-p>", 't')
142
+ endfunction
143
+
144
+ function! CompleteFunction(findstart, base)
145
+ if a:findstart
146
+ let start = len(s:prompt) + 1
147
+ return start
148
+ else
149
+ let base = s:trimString(a:base)
150
+ if (base == '')
151
+ return s:selectionlist
152
+ else
153
+ let res = []
154
+ for m in s:selectionlist
155
+ if m =~ '\c' . base
156
+ call add(res, m)
157
+ endif
158
+ endfor
159
+ return res
160
+ endif
161
+ endif
162
+ endfun
163
+
164
+ " selection window pick
165
+ function! s:select_folder_or_feed()
166
+ " let folder_or_feed = s:trimString(join(split(getline(line('.')), ":")[1:-1], ":"))
167
+ let folder_or_feed = getline('.')[len(s:prompt):]
168
+ close
169
+ call s:focus_window(s:listbufnr)
170
+ if (folder_or_feed == '') " no selection
171
+ return
172
+ end
173
+ call s:fetch_items(folder_or_feed)
174
+ endfunction
175
+
176
+ func! s:list_folders()
177
+ let folders = split(system(s:list_folders_command), "\n")
178
+ if len(folders) == 0
179
+ echom "There are no folders."
180
+ else
181
+ let s:selectiontype = "folder"
182
+ call s:open_selection_window(folders, 'select-folder', "Select folder: ")
183
+ end
184
+ endfunc
185
+
186
+ func! s:list_feeds(popular_first)
187
+ " default is alphabetical
188
+ " 1 means order by popular_first
189
+ let res = system(s:list_feeds_command . " " . a:popular_first)
190
+ let promptsuffix = a:popular_first ? "(num of item views)" : "(num of unread items)"
191
+ let feeds = split(res, "\n")
192
+ if len(feeds) == 0
193
+ echom "There are no feeds."
194
+ else
195
+ let s:selectiontype = "feed"
196
+ call s:open_selection_window(feeds, 'select-feed', "Select feed ". promptsuffix .": ")
197
+ end
198
+ endfunc
199
+
200
+ func! s:display_items(res)
201
+ setlocal modifiable
202
+ silent! 1,$delete
203
+ silent! put! =a:res
204
+ silent normal Gdd
205
+ setlocal nomodifiable
206
+ normal zz
207
+ endfunc
208
+
209
+ " right now, just does folders
210
+ function! s:fetch_items(selection)
211
+ " take different actions depending on whether a feed or folder?
212
+ call s:focus_window(s:itembufnr)
213
+ call clearmatches()
214
+ call s:focus_window(s:listbufnr)
215
+ call clearmatches()
216
+ if exists("s:selectionlist") && index(s:selectionlist, a:selection) == -1
217
+ return
218
+ end
219
+ if s:selectiontype == "folder"
220
+ let command = s:list_folder_items_command
221
+ else
222
+ let command = s:list_feed_items_command
223
+ endif
224
+ let command .= winwidth(0) . ' ' .shellescape(a:selection)
225
+ let s:last_fetch_command = command " in case user later updates the feed in place
226
+ let s:last_selection = a:selection
227
+ let res = system(command)
228
+ call s:display_items(res)
229
+ normal G
230
+ call s:focus_window(s:itembufnr)
231
+ close
232
+ normal z-
233
+ " call s:show_item_under_cursor(0)
234
+ " call s:focus_window(s:listbufnr)
235
+ endfunction
236
+
237
+ func! s:get_guid(line)
238
+ let line = getline(a:line)
239
+ let s:guid = matchstr(line, '[^|]\+$')
240
+ return s:trimString(s:guid)
241
+ endfunc
242
+
243
+ "------------------------------------------------------------------------
244
+ " SHOW ITEM
245
+ " blank arg is not used yet
246
+ func! s:show_item_under_cursor(inc_read_count)
247
+ let s:guid = s:get_guid(line('.'))
248
+ if s:guid == ""
249
+ return
250
+ end
251
+ " mark as read
252
+ set modifiable
253
+ let newline = substitute(getline('.'), '^+', ' ', '')
254
+ call setline(line('.'), newline)
255
+ set nomodifiable
256
+ let res = system(s:show_item_command . shellescape( s:guid) . ' '. ( a:inc_read_count ? "1" : "" ) )
257
+ call s:focus_window(s:itembufnr)
258
+ set modifiable
259
+ silent 1,$delete
260
+ silent put =res
261
+ silent 1delete
262
+ silent normal 1Gjk
263
+ set nomodifiable
264
+ endfunc
265
+
266
+ " from message window
267
+ function! s:show_adjacent_item(up, focusbufname)
268
+ if (bufwinnr(s:listbufnr) == -1) " we're in full screen item mode
269
+ 3split " make small nav window on top
270
+ exec 'b'. s:listbufnr
271
+ else
272
+ call s:focus_window(s:listbufnr)
273
+ end
274
+ if a:up
275
+ normal k
276
+ else
277
+ normal j
278
+ endif
279
+ normal zz
280
+ call s:show_item_under_cursor(1) " TOD0 is 1 right arg?
281
+ normal zz
282
+ call s:focus_window(bufnr(a:focusbufname))
283
+ redraw
284
+ endfunction
285
+
286
+ func! s:close_item_window()
287
+ if winnr('$') > 1
288
+ close!
289
+ else
290
+ call s:focus_window(s:listbufnr)
291
+ wincmd p
292
+ close!
293
+ normal zz
294
+ endif
295
+ endfunc
296
+
297
+ func! s:toggle_maximize_window()
298
+ if bufwinnr(s:listbufnr) != -1 && bufwinnr(s:itembufnr) != -1
299
+ if bufwinnr(s:listbufnr) == winnr()
300
+ call s:focus_window(s:itembufnr)
301
+ close
302
+ else
303
+ call s:focus_window(s:listbufnr)
304
+ close
305
+ endif
306
+ elseif bufwinnr(s:listbufnr) == winnr()
307
+ call s:show_item_under_cursor(1)
308
+ elseif bufwinnr(s:itembufnr) == winnr()
309
+ call s:focus_window(s:listbufnr)
310
+ wincmd p
311
+ endif
312
+ endfunc
313
+
314
+ "------------------------------------------------------------------------
315
+ let s:http_link_pattern = 'https\?:[^ >)\]]\+'
316
+
317
+ func! s:open_href_under_cursor()
318
+ let href = expand("<cWORD>")
319
+ let command = g:Vnews#browser_command . " '" . href . "' "
320
+ call system(command)
321
+ echom command
322
+ endfunc
323
+
324
+ func! s:find_next_href_and_open()
325
+ let res = search(s:http_link_pattern, 'cw')
326
+ if res != 0
327
+ call s:open_href_under_cursor()
328
+ endif
329
+ endfunc
330
+
331
+ if !exists("g:Vnews#browser_command")
332
+ for cmd in ["gnome-open", "open"]
333
+ if executable(cmd)
334
+ let g:Vnews#browser_command = cmd
335
+ break
336
+ endif
337
+ endfor
338
+ if !exists("g:Vnews#browser_command")
339
+ echom "Can't find the to open your web browser."
340
+ endif
341
+ endif
342
+
343
+ "------------------------------------------------------------------------
344
+ " TOGGLE STAR
345
+
346
+ function! s:toggle_star()
347
+ let original_winnr = winnr()
348
+ call s:focus_window(s:listbufnr)
349
+ let s:guid = s:get_guid(line('.'))
350
+ let flag_symbol = "^*"
351
+ if match(getline('.'), flag_symbol) != -1
352
+ let already_starred = 1
353
+ else
354
+ let already_starred = 0
355
+ end
356
+ if !already_starred
357
+ let command = s:star_item_command
358
+ else
359
+ let command = s:unstar_item_command
360
+ endif
361
+ let command .= shellescape(s:guid )
362
+ let res = system(command)
363
+ setlocal modifiable
364
+ let line = getline('.')
365
+ " toggle * on line
366
+ if !already_starred
367
+ let newline = substitute(line, '^ ', '*', '')
368
+ let newline = substitute(newline, '^+', '*', '')
369
+ else
370
+ let newline = substitute(line, '^*', ' ', '')
371
+ endif
372
+ call setline(line('.'), newline)
373
+ setlocal nomodifiable
374
+ exec original_winnr . "wincmd w"
375
+ redraw
376
+ endfunction
377
+
378
+ "------------------------------------------------------------------------
379
+ " DELETE ITEMS
380
+
381
+ func! s:delete_item()
382
+ call s:focus_window(s:listbufnr)
383
+ let uid = s:get_guid(line('.'))
384
+ let command = s:delete_item_command . shellescape(uid)
385
+ let res = system(command)
386
+ setlocal modifiable
387
+ exec "silent " . line('.') . "delete"
388
+ setlocal nomodifiable
389
+ if winnr("$") > 1
390
+ wincmd p
391
+ close
392
+ end
393
+ redraw
394
+ echom "Item ".uid." deleted"
395
+ endfunc
396
+
397
+ "------------------------------------------------------------------------
398
+ " PRINT ITEMS
399
+
400
+ " must be called from list window
401
+ func! s:cat_items() range
402
+ let lnum = a:firstline
403
+ let items = []
404
+ while lnum <= a:lastline
405
+ let guid = s:get_guid(lnum)
406
+ call add(items, shellescape(guid))
407
+ let lnum += 1
408
+ endwhile
409
+ call s:focus_window(s:itembufnr)
410
+ only
411
+ let command = s:cat_items_command . join(items, ' ')
412
+ let res = system(command)
413
+ setlocal modifiable
414
+ silent 1,$delete
415
+ silent put =res
416
+ silent 1delete
417
+ silent normal 1Gjk
418
+ setlocal nomodifiable
419
+ redraw
420
+ echom "Concatenated ".len(items)."item".(len(items) == 1 ? '' : 's')
421
+ endfunc
422
+
423
+
424
+ "------------------------------------------------------------------------
425
+ " SEARCH
426
+ func! s:search_items(term)
427
+ call clearmatches()
428
+ call s:focus_window(s:listbufnr)
429
+ let command = s:search_items_command . winwidth(0) . ' ' . shellescape(a:term)
430
+ let res = system(command)
431
+ call s:display_items(res)
432
+ " show item for top match
433
+ normal gg
434
+ call s:show_item_under_cursor(0)
435
+ call matchadd("VnewsSearchTerm", '\c' . a:term )
436
+ call s:focus_window(s:listbufnr)
437
+ call matchadd("VnewsSearchTerm", '\c' . a:term )
438
+ endfunc
439
+
440
+ "------------------------------------------------------------------------
441
+ " UPDATE FEED
442
+
443
+ func! s:update_feed()
444
+ call s:focus_window(s:listbufnr)
445
+ if exists("s:last_selection")
446
+ if s:selectiontype == "folder"
447
+ exec ":!vnews-client update_folder ".shellescape(s:last_selection)
448
+ elseif s:selectiontype == "feed"
449
+ exec ":!vnews-client update_feed ".shellescape(s:last_selection)
450
+ end
451
+ endif
452
+ if exists("s:last_fetch_command")
453
+ let res = system(s:last_fetch_command)
454
+ call s:display_items(res)
455
+ end
456
+ redraw!
457
+ normal G
458
+ call s:show_item_under_cursor(0)
459
+ redraw!
460
+ endfunc
461
+
462
+
463
+ command! -bar -nargs=1 VNSearch :call s:search_items(<f-args>)
464
+
465
+
466
+ call s:create_list_window()
467
+ call s:create_item_window()
468
+ call s:focus_window(s:listbufnr)
469
+ let s:selectiontype = "folder"
470
+ call s:fetch_items("All (0)") " number won't show but is assumed by function VnewsStatusLine()
471
+
472
+