├── screenshots ├── vim-hackernews-home.png └── vim-hackernews-item.png ├── plugin └── hackernews.vim ├── CHANGES ├── LICENSE ├── syntax └── hackernews.vim ├── doc └── hackernews.txt ├── README.md ├── ftplugin ├── hackernews.vim └── hackernews.py └── tests.vader /screenshots/vim-hackernews-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelarsq/vim-hackernews/HEAD/screenshots/vim-hackernews-home.png -------------------------------------------------------------------------------- /screenshots/vim-hackernews-item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adelarsq/vim-hackernews/HEAD/screenshots/vim-hackernews-item.png -------------------------------------------------------------------------------- /plugin/hackernews.vim: -------------------------------------------------------------------------------- 1 | " vim-hackernews 2 | " -------------- 3 | " Browse Hacker News (news.ycombinator.com) inside Vim. 4 | " 5 | " Author: ryanss 6 | " Mainteiner: Adelar da Silva Queiróz 7 | " Website: https://github.com/adelarsq/vim-hackernews 8 | " License: MIT (see LICENSE file) 9 | " Version: 0.4-dev 10 | 11 | 12 | " Filetype plugins need to be enabled 13 | filetype plugin on 14 | 15 | " Load ftplugin when opening .hackernews buffer 16 | au! BufRead,BufNewFile *.hackernews set filetype=hackernews 17 | 18 | 19 | " Set required defaults 20 | if !exists("g:hackernews_arg") 21 | let g:hackernews_arg = 'news' 22 | endif 23 | 24 | if !exists("g:hackernews_marks") 25 | let g:hackernews_marks = {} 26 | endif 27 | 28 | 29 | function! HackerNews(...) 30 | if a:0 > 0 31 | let g:hackernews_arg = a:1 32 | else 33 | let g:hackernews_arg = "" 34 | endif 35 | execute "edit .hackernews" 36 | normal! gg 37 | endfunction 38 | 39 | command! -nargs=? HackerNews call HackerNews() 40 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Vim-HackerNews Changelog 2 | ======================== 3 | 4 | 5 | Version 0.2 6 | ----------- 7 | 8 | Released April 26, 2015 9 | 10 | - Add Python 3 support 11 | - Add additional motions to improve browsing of story lists, comments, articles 12 | - Add comment thread folding 13 | - Add ability to display specific HackerNews lists (top, ask, show, etc) 14 | - Handle all HackerNews item types properly (poll, job, etc) 15 | - Save cursor position when moving back/forward 16 | - Add highlighting of OP username in comment titles 17 | - Do not load python code until first :HackerNews command 18 | - Timeout HTTP requests after 5 seconds instead of hanging Vim 19 | - Improve HTTP error information 20 | - Add units tests with Vader.vim 21 | - Add Travis CI integration 22 | - Lots of syntax fixes and improvements 23 | - Dozens of other bug fixes and improvements 24 | 25 | 26 | Version 0.1.1 27 | ------------- 28 | 29 | Released February 7, 2015 30 | 31 | - Fix "job" type items without a `domain` key in API 32 | 33 | 34 | Version 0.1 35 | ----------- 36 | 37 | Released February 7, 2015 38 | 39 | - Initial release 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /syntax/hackernews.vim: -------------------------------------------------------------------------------- 1 | " vim-hackernews 2 | " -------------- 3 | " Browse Hacker News (news.ycombinator.com) inside Vim. 4 | " 5 | " Author: ryanss 6 | " Mainteiner: Adelar da Silva Queiróz 7 | " Website: https://github.com/adelarsq/vim-hackernews 8 | " License: MIT (see LICENSE file) 9 | " Version: 0.4-dev 10 | 11 | 12 | if exists("b:current_syntax") 13 | finish 14 | endif 15 | 16 | 17 | " Hide hacker news item id or url at end of front page lines 18 | syn match Ignore /\s\[[0-9]\{3,}\]$/ 19 | syn match Ignore /\s\[http.\+\] $/ 20 | 21 | " Make sure `Ignore` highlight group is hidden 22 | " Some colorschemes do not hide the `Ignore` group (ex. Solarized) 23 | " An exception will be raised here if ctermfg=NONE which is sometimes set 24 | " when using a transparent terminal so we wrap these commands in try/catch 25 | try 26 | if has('gui_running') 27 | highlight Ignore guifg=bg 28 | else 29 | highlight Ignore ctermfg=bg 30 | endif 31 | catch 32 | endtry 33 | 34 | " Remove emphesis from all components of main page item except title 35 | syn match Comment /^\s*[0-9]\{1,2}\.\s/ 36 | syn match Comment /\s([^\[]\S\+\.\S\+)/ 37 | syn match Comment /^\s\{4}[0-9an]\+\s.\+\sago\s\[/ contains=Ignore 38 | syn match Comment /^\s\{4}[0-9an]\+\spoints.\+\s\s|.*comments/ 39 | syn match Comment /^.*ago\s|.*comments/ contains=Question 40 | syn match Comment /^[0-9an]\+\s.\+\sago$/ 41 | 42 | " Comment titles 43 | syn match Comment /^\s*Comment\sby.\+ago:/ contains=Question 44 | 45 | " Highlight links 46 | syn region Constant start="\[http" end="\]" 47 | 48 | " Italics tags 49 | syn match Italic /\v<_\_.{-}(_>|^$)/ contains=Statement 50 | highlight Italic gui=italic 51 | 52 | " Highlight code blocks 53 | syn region Statement start="^ " end="^ " 54 | 55 | " Highlight Hacker News header orange 56 | syn match Title /^┌.*$/ 57 | syn match Title /^│.*$/ 58 | syn match Title /^└.*$/ 59 | highlight Title ctermfg=208 guifg=#ff6600 60 | 61 | 62 | let b:current_syntax = "hackernews" 63 | -------------------------------------------------------------------------------- /doc/hackernews.txt: -------------------------------------------------------------------------------- 1 | *hackernews.txt* Browse HackerNews inside Vim *hackernews* 2 | 3 | Author: ryanss 4 | Website: https://github.com/ryanss/vim-hackernews 5 | License: MIT (see LICENSE file) 6 | Version: 0.3-dev 7 | 8 | 9 | BASIC USAGE *hackernews-usage* 10 | 11 | * Open the Hacker News front page in Vim by executing the `:HackerNews` command 12 | * The HackerNews command takes an optional parameter to view items other 13 | than the top stories on the front page: 14 | * `:HackerNews ask` 15 | * `:HackerNews show` 16 | * `:HackerNews shownew` 17 | * `:HackerNews jobs` 18 | * `:HackerNews best` 19 | * `:HackerNews active` 20 | * `:HackerNews newest` 21 | * `:HackerNews noobstories` 22 | * Press lowercase `o` to open links in Vim 23 | * Press uppercase `O` to open links in default web browser 24 | * Numbered lines with story titles on the front page link to the story url 25 | * Comment lines on the front page link to the comments url 26 | * Press uppercase `F` to fold current comment thread 27 | * Press lowercase `u` to go back 28 | * Press `Ctrl+r` to go forward 29 | * Execute the `:bd` command to close and remove the Hacker News buffer 30 | 31 | 32 | ENHANCED MOTIONS *hackernews-motions* 33 | 34 | Uppercase `J` and `K` are mapped to helpful new motions based on what type of 35 | content is on the screen: 36 | 37 | * Move to next/prev item when viewing the front page. (If the cursor is on a 38 | numbered line with story title the cursor will move to the next/prev numbered 39 | line with story title. If the cursor is on a comment line it will move to the 40 | next/prev comment line.) 41 | * Move to next/prev comment when viewing comments. 42 | * Move to next/prev paragraph when viewing the text version of articles. 43 | 44 | 45 | COMMANDS *hackernews-commands* 46 | 47 | :HackerNews Open Hacker News front page stories in Vim 48 | 49 | 50 | ABOUT *hackernews-about* 51 | 52 | Grab the latest version or report a bug on GitHub: 53 | 54 | https://github.com/ryanss/vim-hackernews 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vim-hackernews 2 | ============== 3 | 4 | Browse [Hacker News](https://news.ycombinator.com) inside Vim. Fork from [vim-hackernews](https://vim.sourceforge.io/scripts/script.php?script_id=5108) which is no longer maintained. 5 | 6 | ![Hacker News Front Page in Vim](https://github.com/adelarsq/vim-hackernews/raw/master/screenshots/vim-hackernews-home.png) 7 | 8 | ![Hacker News Comments in Vim](https://github.com/adelarsq/vim-hackernews/raw/master/screenshots/vim-hackernews-item.png) 9 | 10 | Uses [cheeaun's Unofficial Hacker News API](https://github.com/cheeaun/node-hnapi) 11 | to retrieve home page stories and comments and 12 | [FUCK YEAH MARKDOWN](http://fuckyeahmarkdown.com) for rendering HTML articles 13 | as text. 14 | 15 | 16 | Basic Usage 17 | ----------- 18 | 19 | * Open the Hacker News front page in Vim by executing the `:HackerNews` command 20 | * The HackerNews command takes an optional parameter to view items other 21 | than the top stories on the front page: `ask`, `show`, `shownew`, `jobs`, 22 | `best`, `active`, `newest`, `noobstories`, ``, or `` 23 | * Press lowercase `o` to open links in Vim 24 | * Press uppercase `O` to open links in default web browser 25 | * Numbered lines with story titles on the front page link to the story url 26 | * Comment lines on the front page link to the comments url 27 | * Press uppercase `F` to fold current comment thread 28 | * Press lowercase `u` to go back 29 | * Press `Ctrl+r` to go forward 30 | * Execute the `:bd` command to close and remove the Hacker News buffer 31 | 32 | 33 | Enhanced Motions 34 | ---------------- 35 | 36 | Uppercase `J` and `K` are mapped to helpful new motions based on what type of 37 | content is on the screen: 38 | 39 | * Move to next/prev item when viewing the front page. (If the cursor is on a 40 | numbered line with story title the cursor will move to the next/prev numbered 41 | line with story title. If the cursor is on a comment line it will move to the 42 | next/prev comment line.) 43 | * Move to next/prev comment when viewing comments. 44 | * Move to next/prev paragraph when viewing the text version of articles. 45 | 46 | 47 | Installation 48 | ------------ 49 | 50 | ##### Pathogen (https://github.com/tpope/vim-pathogen) 51 | ```bash 52 | git clone https://github.com/adelarsq/vim-hackernews ~/.vim/bundle/vim-hackernews 53 | ``` 54 | 55 | ##### Vundle (https://github.com/gmarik/vundle) 56 | ``` 57 | Plugin 'adelarsq/vim-hackernews' 58 | ``` 59 | 60 | ##### NeoBundle (https://github.com/Shougo/neobundle.vim) 61 | ``` 62 | NeoBundle 'adelarsq/vim-hackernews' 63 | ``` 64 | 65 | 66 | Running Tests 67 | ------------- 68 | 69 | ```bash 70 | $ vim -c Vader! tests.vader 71 | ``` 72 | 73 | 74 | Contributions 75 | ------------- 76 | 77 | [Issues](https://github.com/adelarsq/vim-hackernews/issues) and 78 | [Pull Requests](https://github.com/adelarsq/vim-hackernews/pulls) are always 79 | welcome! 80 | 81 | 82 | License 83 | ------- 84 | 85 | Code is available according to the MIT License 86 | (see [LICENSE](https://github.com/adelarsq/vim-hackernews/raw/master/LICENSE)). 87 | -------------------------------------------------------------------------------- /ftplugin/hackernews.vim: -------------------------------------------------------------------------------- 1 | " vim-hackernews 2 | " -------------- 3 | " Browse Hacker News (news.ycombinator.com) inside Vim. 4 | " 5 | " Author: ryanss 6 | " Maintainer: Adelar da Silva Queiróz 7 | " Website: https://github.com/adelarsq/vim-hackernews 8 | " License: MIT (see LICENSE file) 9 | " Version: 0.4-dev 10 | 11 | 12 | if has('python') 13 | command! -nargs=1 Python python3 14 | elseif has('python2') 15 | command! -nargs=1 Python python 16 | elseif has('python3') 17 | command! -nargs=1 Python python3 18 | else 19 | echo "HackerNews.vim Error: Requires Vim compiled with +python or +python3" 20 | finish 21 | endif 22 | 23 | 24 | " Import Python code 25 | execute "Python import sys" 26 | execute "Python sys.path.append(r'" . expand(":p:h") . "')" 27 | 28 | Python << EOF 29 | if 'hackernews' not in sys.modules: 30 | import hackernews 31 | else: 32 | import imp 33 | # Reload python module to avoid errors when updating plugin 34 | hackernews = imp.reload(hackernews) 35 | EOF 36 | 37 | 38 | " Load front page 39 | execute "Python hackernews.main()" 40 | 41 | 42 | noremap o :Python hackernews.link() 43 | noremap O :Python hackernews.link(external=True) 44 | noremap gx :Python hackernews.link(external=True) 45 | noremap u :Python hackernews.save_pos() 46 | \u 47 | \:Python hackernews.recall_pos("undo") 48 | noremap :Python hackernews.save_pos() 49 | \ 50 | \:Python hackernews.recall_pos("redo") 51 | 52 | 53 | " Helper motions to browse front page, comments and articles easier 54 | function! s:Move(backwards) 55 | let dir = a:backwards? '?' : '/' 56 | if match(getline(1), "┌───┐") == 0 57 | " Front Page 58 | if match(getline('.'), '^\s\{4}.\+ago') >= 0 59 | " Move to next/previous comment line 60 | let pattern = '^\s\{4}[0-9]' 61 | else 62 | " Move to next/previous title line 63 | let pattern = '^\s*\d\+\.\s.' 64 | endif 65 | execute 'silent normal! ' . dir . pattern . dir . '\r ' 66 | elseif match(getline(2), '^\d\+\s.\+ago') == 0 67 | " Comment Page 68 | let pattern = '^\s*Comment by' 69 | execute 'silent normal! ' . dir . pattern . dir . '\r zt' 70 | " Do not stop on folded lines 71 | if foldclosed(line('.')) != -1 72 | execute 'silent normal! ' . dir . pattern . dir . '\r zt' 73 | endif 74 | else 75 | " Article 76 | if a:backwards 77 | silent normal! { 78 | else 79 | silent normal! } 80 | endif 81 | endif 82 | endfunction 83 | 84 | noremap J :call Move(0) 85 | noremap K :call Move(1) 86 | 87 | 88 | " Fold comment threads 89 | function! s:FoldComments() 90 | if match(getline(2), '^\d\+\s.\+ago') != 0 91 | " Do not continue if this is not a comments page 92 | return 93 | endif 94 | set nowrapscan 95 | try 96 | execute 'silent normal! ' . 'jj?^\s*Comment by.*:?\r j' 97 | catch 98 | " Nothing to fold 99 | return 100 | endtry 101 | let level = matchstr(getline('.'), '^\s\+') 102 | try 103 | execute 'silent normal! ' . 'zf/\n^\s\{0,' . len(level). '}Comment/\r ' 104 | catch 105 | execute 'silent! normal! ' . 'zf/\n\%$/e\r ' 106 | endtry 107 | set wrapscan 108 | endfunction 109 | 110 | noremap F :call FoldComments() 111 | -------------------------------------------------------------------------------- /tests.vader: -------------------------------------------------------------------------------- 1 | " vim-hackernews 2 | " -------------- 3 | " Browse Hacker News (news.ycombinator.com) inside Vim. 4 | " 5 | " Author: ryanss 6 | " Website: https://github.com/ryanss/vim-hackernews 7 | " License: MIT (see LICENSE file) 8 | " Version: 0.3-dev 9 | 10 | Execute (Test Plugin Loaded): 11 | AssertEqual 1, filereadable('doc/hackernews.txt') 12 | AssertEqual 1, filereadable('ftplugin/hackernews.py') 13 | AssertEqual 1, filereadable('ftplugin/hackernews.vim') 14 | AssertEqual 1, filereadable('plugin/hackernews.vim') 15 | AssertEqual 1, filereadable('syntax/hackernews.vim') 16 | redir @a 17 | au BufRead *.hackernews 18 | redir @b 19 | au BufNewFile *.hackernews 20 | redir @c 21 | command HackerNews 22 | redir END 23 | 24 | Do (Test autocmd BufRead *.hackernews): 25 | "apdd 26 | 27 | Expect (set filetype=hackernews): 28 | --- Auto-Commands --- 29 | BufRead 30 | *.hackernews 31 | set filetype=hackernews 32 | 33 | Do (Test autocmd BufNewFile *.hackernews): 34 | "bpdd 35 | 36 | Expect (set filetype=hackernews): 37 | --- Auto-Commands --- 38 | BufNewFile 39 | *.hackernews 40 | set filetype=hackernews 41 | 42 | Do (Test HackerNews Defined): 43 | "cpdddd 44 | :%s/ \{2,}/ /g\ 45 | 46 | Expect (HackerNews Defined): 47 | HackerNews ? call HackerNews() 48 | 49 | Execute (Test HackerNews Command): 50 | HackerNews 51 | 52 | Then (Test Front Page): 53 | AssertEqual getline(1), '┌───┐' 54 | AssertEqual getline(2), '│ Y │ Hacker News (news.ycombinator.com)' 55 | AssertEqual getline(3), '└───┘' 56 | AssertEqual 'Comment', SyntaxAt(5,2), 'Item number syntax' 57 | AssertEqual 'Comment', SyntaxAt(6,5), 'Point/user/comment line syntax' 58 | AssertEqual '', SyntaxAt(5,5), 'Item title syntax' 59 | 60 | Do (Test Key Mappings): 61 | :HackerNews\ 62 | ggVGd 63 | :redir @a\ 64 | :map o\ 65 | :map O\ 66 | :map gx\ 67 | :map u\ 68 | :map \ 69 | "apdd 70 | :%s/^.*Netrw.*$\n//g\ 71 | 72 | Expect (Keys Mapped): 73 | o *@:Python hackernews.link() 74 | O *@:Python hackernews.link(external=True) 75 | gx *@:Python hackernews.link(external=True) 76 | u *@:Python hackernews.save_pos()u:Python hackernews.recall_pos("undo") 77 | *@:Python hackernews.save_pos():Python hackernews.recall_pos("redo") 78 | 79 | Do (Test opening link item w/ url): 80 | :HackerNews\ 81 | cc[9015621]\o 82 | 83 | Then (Test link w/ url opened): 84 | AssertEqual getline(1), 'Show HN: vim-hackernews (github.com)' 85 | AssertEqual getline(3), '[https://github.com/ryanss/vim-hackernews]' 86 | 87 | Do (Test opening link item w/ content): 88 | :HackerNews\ 89 | cc[1474094]\o 90 | 91 | Then (Test link w/ content opened): 92 | AssertEqual getline(1), 'Ask HN: What were your naivetés in your twenties?' 93 | AssertEqual getline(3), '[http://news.ycombinator.com/item?id=1474094]' 94 | AssertEqual getline(5), 'Oh the wise elders of Hack News,' 95 | 96 | Do (Test opening poll item): 97 | :HackerNews\ 98 | cc[5736367]\o 99 | 100 | Then (Test poll item opened): 101 | AssertEqual getline(1), 'Poll: Which is your primary text editor?' 102 | AssertEqual getline(3), '[http://news.ycombinator.com/item?id=5736367]' 103 | AssertEqual strpart(getline(5), 0, 2), 'Vi' 104 | AssertEqual getline(6), '################################################################################' 105 | 106 | Do (Test opening comment item): 107 | :HackerNews\ 108 | cc[9015621]\o 109 | :36\o 110 | 111 | Then (Test comment item opened): 112 | AssertEqual strpart(getline(1), 0, 17), 'Comment by atmosx' 113 | AssertEqual strpart(getline(7), 0, 21), ' Comment by ryanss' 114 | 115 | Do (Test opening job item): 116 | :HackerNews\ 117 | cc[9250739]\o 118 | 119 | Then (Test job item opened): 120 | AssertEqual getline(1), 'Love Devops? Aptible Is Hiring Senior Platform Engineers' 121 | AssertEqual getline(3), '[http://news.ycombinator.com/item?id=9250739]' 122 | AssertEqual getline(5), '_Brooklyn/Remote_' 123 | -------------------------------------------------------------------------------- /ftplugin/hackernews.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # vim-hackernews 4 | # -------------- 5 | # Browse Hacker News (news.ycombinator.com) inside Vim. 6 | # 7 | # Author: ryanss 8 | # Mainteiner: Adelar da Silva Queiróz 9 | # Website: https://github.com/adelarsq/vim-hackernews 10 | # License: MIT (see LICENSE file) 11 | # Version: 0.4-dev 12 | 13 | 14 | from __future__ import print_function, division 15 | import binascii 16 | import json 17 | import re 18 | import sys 19 | import textwrap 20 | import vim 21 | import webbrowser 22 | from datetime import datetime 23 | if sys.version_info >= (3, 0): 24 | from html.parser import HTMLParser 25 | from urllib.parse import quote_plus, urlparse 26 | from urllib.request import urlopen 27 | from urllib.error import HTTPError 28 | unicode = bytes 29 | unichr = chr 30 | else: 31 | from HTMLParser import HTMLParser 32 | from urllib import quote_plus 33 | from urllib2 import urlopen, HTTPError 34 | from urlparse import urlparse 35 | 36 | 37 | API_URL = "http://node-hnapi.herokuapp.com" 38 | MARKDOWN_URL = "http://fuckyeahmarkdown.com/go/?read=1&u=" 39 | SEARCH = ("https://hn.algolia.com/api/v1/search" + 40 | "?tags=story&hitsPerPage=60&query=") 41 | 42 | html = HTMLParser() 43 | 44 | 45 | def bwrite(s): 46 | b = vim.current.buffer 47 | # Never write more than two blank lines in a row 48 | if not s.strip() and not b[-1].strip() and not b[-2].strip(): 49 | return 50 | 51 | # Vim buffer.append() cannot accept unicode type, 52 | # must first encode to UTF-8 string 53 | if isinstance(s, unicode): 54 | s = s.encode('utf-8', errors='replace') 55 | 56 | # Code block markers for syntax highlighting 57 | cb = unichr(160) 58 | if isinstance(cb, unicode): 59 | cb = cb.encode('utf-8') 60 | if s == cb and not b[-1]: 61 | b[-1] = s 62 | return 63 | 64 | if not b[0]: 65 | b[0] = s 66 | else: 67 | b.append(s) 68 | 69 | 70 | def hex(s): 71 | if sys.version_info >= (3, 0): 72 | return str(binascii.hexlify(bytes(vim.current.buffer[0], 'utf-8'))) 73 | return binascii.hexlify(s) 74 | 75 | 76 | def time_ago(timestamp): 77 | d = datetime.now() - datetime.fromtimestamp(timestamp) 78 | years = d.days // 365 79 | if years > 1: 80 | return "%d years ago" % (years) 81 | elif years == 1: 82 | return "%d year ago" % (years) 83 | months = d.days // 30 84 | if months > 1: 85 | return "%d months ago" % (months) 86 | elif months == 1: 87 | return "%d month ago" % (months) 88 | if d.days > 1: 89 | return "%d days ago" % (d.days) 90 | elif d.days == 1: 91 | return "%d day ago" % (d.days) 92 | hours = d.seconds // 60 // 60 93 | if hours > 1: 94 | return "%d hours ago" % (hours) 95 | elif hours == 1: 96 | return "%d hour ago" % (hours) 97 | minutes = d.seconds // 60 98 | if minutes > 1: 99 | return "%d minutes ago" % (minutes) 100 | elif minutes == 1: 101 | return "%d minute ago" % (minutes) 102 | return "%d seconds ago" % (d.seconds) 103 | 104 | 105 | def main(): 106 | vim.command("edit .hackernews") 107 | vim.command("setlocal noswapfile") 108 | vim.command("setlocal nonumber") 109 | vim.command("setlocal buftype=nofile") 110 | vim.command("setlocal cc=") 111 | 112 | if vim.eval("changenr()") == "1": 113 | vim.command("setlocal undolevels=-1") 114 | 115 | bwrite("┌───┐") 116 | bwrite("│ Y │ Hacker News (news.ycombinator.com)") 117 | bwrite("└───┘") 118 | bwrite("") 119 | 120 | try: 121 | arg = vim.eval("g:hackernews_arg") 122 | if arg in ['newest', 'ask', 'show', 'shownew', 'jobs', 123 | 'best', 'active', 'noobstories']: 124 | items = json.loads(urlopen(API_URL+"/"+arg, timeout=5) 125 | .read().decode('utf-8')) 126 | elif arg.isdigit(): 127 | link(item_id=arg) 128 | return 129 | elif arg[:4] == 'http': 130 | link(url=arg) 131 | return 132 | elif not arg: 133 | news1 = json.loads(urlopen(API_URL+"/news", timeout=5) 134 | .read().decode('utf-8')) 135 | news2 = json.loads(urlopen(API_URL+"/news2", timeout=5) 136 | .read().decode('utf-8')) 137 | items = news1 + news2 138 | else: 139 | items = json.loads(urlopen(SEARCH+quote_plus(arg), timeout=5) 140 | .read().decode('utf-8'))['hits'] 141 | except HTTPError: 142 | print("HackerNews.vim Error: %s" % str(sys.exc_info()[1].reason)) 143 | return 144 | except: 145 | print("HackerNews.vim Error: HTTP Request Timeout") 146 | return 147 | 148 | for i, item in enumerate(items): 149 | # Test if item is a result from search API 150 | if 'objectID' in item: 151 | # Convert search API results into dict similar to regular API 152 | # results so we can use same code to output results 153 | item['id'] = int(item['objectID']) 154 | item['user'] = item['author'] 155 | item['type'] = "link" 156 | item['time_ago'] = time_ago(item['created_at_i']) 157 | item['comments_count'] = int(item['num_comments']) 158 | if item.get('url', False): 159 | item['domain'] = urlparse(item['url']).netloc 160 | 161 | if 'title' not in item: 162 | continue 163 | if 'domain' in item: 164 | line = "%s%d. %s (%s) [%s]%s" 165 | line %= (" " if i+1 < 10 else "", i+1, item['title'], 166 | item['domain'], item['url'], unichr(160)) 167 | bwrite(line) 168 | else: 169 | line = "%s%d. %s [%d]" 170 | line %= (" " if i+1 < 10 else "", i+1, item['title'], item['id']) 171 | bwrite(line) 172 | if item['type'] in ("link", "ask"): 173 | line = "%s%d points by %s %s | %d comments [%s]" 174 | line %= (" "*4, item['points'], item['user'], item['time_ago'], 175 | item['comments_count'], str(item['id'])) 176 | bwrite(line) 177 | elif item['type'] == "job": 178 | line = "%s%s [%d]" 179 | line %= (" "*4, item['time_ago'], item['id']) 180 | bwrite(line) 181 | bwrite("") 182 | vim.command("setlocal undolevels=100") 183 | 184 | 185 | def link(item_id=None, url=None, external=False): 186 | line = vim.current.line 187 | 188 | if not (item_id or url): 189 | # Search for Hacker News [item id] 190 | m = re.search(r"\[([0-9]{3,})\]$", line) 191 | if m: 192 | item_id = m.group(1) 193 | 194 | else: 195 | # Search for [http] link 196 | b = vim.current.buffer 197 | y, x = vim.current.window.cursor 198 | y -= 1 199 | while b[y].find("[http") < 0 and y >= 0: 200 | # The line we were on had no part of a link in it 201 | if b[y-1].find("]") > 0 \ 202 | and b[y-1].find("]") > b[y-1].find("[http"): 203 | return 204 | y -= 1 205 | start = y 206 | loc = max(b[y].find("[http", x, b[y].find("]", x)), 207 | b[y].rfind("[http", 0, x)) 208 | if loc >= 0: 209 | if b[y].find("]", loc) >= 0: 210 | a = loc + 1 211 | e = b[y].find("]", loc) 212 | url = b[y][a:e] 213 | else: 214 | url = b[y][loc+1:] 215 | y += 1 216 | while b[y].find("]") < 0: 217 | if y != start: 218 | url += b[y] 219 | y += 1 220 | if y != start: 221 | url += b[y][:b[y].find("]")] 222 | url = url.replace(" ", "").replace("\n", "") 223 | 224 | if url and url.find("news.ycombinator.com/item?id=") > 0: 225 | item_id = url[url.find("item?id=")+8:] 226 | 227 | if item_id: 228 | if external: 229 | browser = webbrowser.get() 230 | browser.open("https://news.ycombinator.com/item?id="+item_id) 231 | return 232 | try: 233 | item = json.loads(urlopen(API_URL+"/item/"+item_id, 234 | timeout=5).read().decode('utf-8')) 235 | except: 236 | print("HackerNews.vim Error: HTTP Request Timeout") 237 | return 238 | save_pos() 239 | vim.command("set syntax=hackernews") 240 | del vim.current.buffer[:] 241 | if 'title' in item: 242 | if 'domain' in item: 243 | bwrite("%s (%s)" % (item['title'], item['domain'])) 244 | else: 245 | bwrite(item['title']) 246 | if item.get('comments_count', None) is not None \ 247 | and item['type'] != "job": 248 | bwrite("%d points by %s %s | %d comments" 249 | % (item['points'], item['user'], item['time_ago'], 250 | item['comments_count'])) 251 | else: 252 | bwrite(item['time_ago']) 253 | if 'url' in item and item['url'].find(item_id) < 0: 254 | bwrite("[%s]" % item['url']) 255 | else: 256 | bwrite("[http://news.ycombinator.com/item?id=%s]" % item_id) 257 | if 'content' in item: 258 | bwrite("") 259 | print_comments([dict(content=item['content'])]) 260 | if 'poll' in item: 261 | bwrite("") 262 | max_score = max((c['points'] for c in item['poll'])) 263 | for c in item['poll']: 264 | bwrite("%s (%d points)" 265 | % (html.unescape(c['item']), c['points'])) 266 | bar = int(80.0 * c['points'] / max_score) 267 | bwrite("#"*bar) 268 | bwrite("") 269 | bwrite("") 270 | bwrite("") 271 | if item['type'] == "comment": 272 | item['level'] = 0 273 | print_comments([item]) 274 | else: 275 | print_comments(item['comments']) 276 | # Prevent syntax issues in long comment threads with code blocks 277 | vim.command("syntax sync fromstart") 278 | # Highlight OP username in comment titles 279 | if 'level' not in item: 280 | vim.command("syntax clear Question") 281 | vim.command("syntax match Question /%s/ contained" % item['user']) 282 | 283 | elif url: 284 | if external: 285 | browser = webbrowser.get() 286 | browser.open(url) 287 | return 288 | try: 289 | content = urlopen(MARKDOWN_URL+url, timeout=8) 290 | content = content.read().decode('utf-8') 291 | except HTTPError: 292 | print("HackerNews.vim Error: %s" % str(sys.exc_info()[1][0])) 293 | return 294 | except: 295 | print("HackerNews.vim Error: HTTP Request Timeout") 296 | return 297 | content = re.sub(r"(http\S+?)([\<\>\s\n])", "[\g<1>]\g<2>", content) 298 | save_pos() 299 | vim.command("set syntax=markdown") 300 | del vim.current.buffer[:] 301 | for i, line in enumerate(content.split('\n')): 302 | if not line: 303 | bwrite("") 304 | continue 305 | line = textwrap.wrap(line, width=80) 306 | for j, wrap in enumerate(line): 307 | bwrite(wrap) 308 | 309 | 310 | def save_pos(): 311 | marks = vim.eval("g:hackernews_marks") 312 | m = hex(vim.current.buffer[0]) 313 | if not m: 314 | return 315 | marks[m] = list(vim.current.window.cursor) 316 | marks[m].append(vim.eval("&syntax")) 317 | vim.command("let g:hackernews_marks = %s" % str(marks)) 318 | 319 | 320 | def recall_pos(cmd): 321 | marks = vim.eval("g:hackernews_marks") 322 | m = hex(vim.current.buffer[0]) 323 | if not m: 324 | vim.command(cmd) 325 | if m in marks: 326 | mark = marks[m] 327 | vim.current.window.cursor = (int(mark[0]), int(mark[1])) 328 | vim.command("set syntax=%s" % mark[2]) 329 | 330 | 331 | def print_comments(comments, level=0): 332 | for comment in comments: 333 | if 'level' in comment: 334 | # This is a comment (not content) so add comment header 335 | bwrite("%sComment by %s %s: [%s]" 336 | % (" "*level*4, comment.get('user', '???'), 337 | comment['time_ago'], comment['id'])) 338 | if not comment.get('content', False): 339 | bwrite("") 340 | bwrite("") 341 | continue 342 | for p in comment['content'].split("

"): 343 | if not p: 344 | continue 345 | p = html.unescape(p) 346 | p = p.replace("", "_").replace("", "_") 347 | 348 | # Extract code block before textwrap to conserve whitespace 349 | code = None 350 | if p.find("") >= 0: 351 | m = re.search("

([\S\s]*?)
", p) 352 | code = m.group(1) 353 | p = p.replace(m.group(0), "!CODE!") 354 | 355 | # Convert Text tags 356 | # to markdown equivalent: (Text)[http://url/] 357 | s = p.find("a>") 358 | while s > 0: 359 | s += 2 360 | section = p[:s] 361 | m = re.search(r"(.*)", 362 | section) 363 | if m: 364 | # Do not bother with anchor text if it is same as href url 365 | if m.group(1)[:20] == m.group(2)[:20]: 366 | p = p.replace(m.group(0), "[%s]" % m.group(1)) 367 | else: 368 | p = p.replace(m.group(0), 369 | "(%s)[%s]" % (m.group(2), m.group(1))) 370 | s = p.find("a>") 371 | else: 372 | s = p.find("a>", s) 373 | 374 | contents = textwrap.wrap(p, width=80, 375 | initial_indent=" "*4*level, 376 | subsequent_indent=" "*4*level) 377 | for line in contents: 378 | if line.find("!CODE!") >= 0: 379 | bwrite(unichr(160)) 380 | for c in code.split("\n"): 381 | if c.strip(): 382 | bwrite(" "*4*level + c) 383 | bwrite(unichr(160)) 384 | line = " "*4*level + line.replace("!CODE!", "").strip() 385 | if line.strip(): 386 | bwrite(line) 387 | if contents and line.strip(): 388 | bwrite("") 389 | bwrite("") 390 | if 'comments' in comment: 391 | print_comments(comment['comments'], level+1) 392 | --------------------------------------------------------------------------------