Merge remote-tracking branch 'upstream/master'
This commit is contained in:
160
rtv/terminal.py
160
rtv/terminal.py
@@ -17,10 +17,13 @@ from contextlib import contextmanager
|
||||
|
||||
import six
|
||||
from kitchen.text.display import textual_width_chop
|
||||
from mailcap_fix import mailcap
|
||||
|
||||
from . import exceptions
|
||||
from . import mime_parsers
|
||||
from .objects import LoadScreen, Color
|
||||
|
||||
|
||||
try:
|
||||
# Added in python 3.4+
|
||||
from html import unescape
|
||||
@@ -42,28 +45,29 @@ class Terminal(object):
|
||||
RETURN = 10
|
||||
SPACE = 32
|
||||
|
||||
def __init__(self, stdscr, ascii=False):
|
||||
def __init__(self, stdscr, config):
|
||||
|
||||
self.stdscr = stdscr
|
||||
self.ascii = ascii
|
||||
self.config = config
|
||||
self.loader = LoadScreen(self)
|
||||
self._display = None
|
||||
self._mailcap_dict = mailcap.getcaps()
|
||||
|
||||
@property
|
||||
def up_arrow(self):
|
||||
symbol = '^' if self.ascii else '▲'
|
||||
symbol = '^' if self.config['ascii'] else '▲'
|
||||
attr = curses.A_BOLD | Color.GREEN
|
||||
return symbol, attr
|
||||
|
||||
@property
|
||||
def down_arrow(self):
|
||||
symbol = 'v' if self.ascii else '▼'
|
||||
symbol = 'v' if self.config['ascii'] else '▼'
|
||||
attr = curses.A_BOLD | Color.RED
|
||||
return symbol, attr
|
||||
|
||||
@property
|
||||
def neutral_arrow(self):
|
||||
symbol = 'o' if self.ascii else '•'
|
||||
symbol = 'o' if self.config['ascii'] else '•'
|
||||
attr = curses.A_BOLD
|
||||
return symbol, attr
|
||||
|
||||
@@ -75,7 +79,7 @@ class Terminal(object):
|
||||
|
||||
@property
|
||||
def guilded(self):
|
||||
symbol = '*' if self.ascii else '✪'
|
||||
symbol = '*' if self.config['ascii'] else '✪'
|
||||
attr = curses.A_BOLD | Color.YELLOW
|
||||
return symbol, attr
|
||||
|
||||
@@ -228,7 +232,7 @@ class Terminal(object):
|
||||
if isinstance(string, six.text_type):
|
||||
string = unescape(string)
|
||||
|
||||
if self.ascii:
|
||||
if self.config['ascii']:
|
||||
if isinstance(string, six.binary_type):
|
||||
string = string.decode('utf-8')
|
||||
string = string.encode('ascii', 'replace')
|
||||
@@ -279,7 +283,7 @@ class Terminal(object):
|
||||
"""
|
||||
|
||||
if isinstance(message, six.string_types):
|
||||
message = [message]
|
||||
message = message.splitlines()
|
||||
|
||||
n_rows, n_cols = self.stdscr.getmaxyx()
|
||||
|
||||
@@ -317,6 +321,128 @@ class Terminal(object):
|
||||
|
||||
return ch
|
||||
|
||||
def open_link(self, url):
|
||||
"""
|
||||
Open a media link using the definitions from the user's mailcap file.
|
||||
|
||||
Most urls are parsed using their file extension, but special cases
|
||||
exist for websites that are prevalent on reddit such as Imgur and
|
||||
Gfycat. If there are no valid mailcap definitions, RTV will fall back
|
||||
to using the default webbrowser.
|
||||
|
||||
RTV checks for certain mailcap fields to determine how to open a link:
|
||||
- If ``copiousoutput`` is specified, the curses application will
|
||||
be paused and stdout will be piped to the system pager.
|
||||
- If `needsterminal`` is specified, the curses application will
|
||||
yield terminal control to the subprocess until it has exited.
|
||||
- Otherwise, we assume that the subprocess is meant to open a new
|
||||
x-window, and we swallow all stdout output.
|
||||
|
||||
Examples:
|
||||
Stream youtube videos with VLC
|
||||
Browse images and imgur albums with feh
|
||||
Watch .webm videos through your terminal with mplayer
|
||||
View images directly in your terminal with fbi or w3m
|
||||
Play .mp3 files with sox player
|
||||
Send HTML pages your pager using to html2text
|
||||
...anything is possible!
|
||||
"""
|
||||
|
||||
if not self.config['enable_media']:
|
||||
return self.open_browser(url)
|
||||
|
||||
try:
|
||||
with self.loader('Checking link', catch_exception=False):
|
||||
command, entry = self.get_mailcap_entry(url)
|
||||
except exceptions.MailcapEntryNotFound:
|
||||
return self.open_browser(url)
|
||||
|
||||
_logger.info('Executing command: %s', command)
|
||||
needs_terminal = 'needsterminal' in entry
|
||||
copious_output = 'copiousoutput' in entry
|
||||
|
||||
if needs_terminal or copious_output:
|
||||
# Blocking, pause rtv until the process returns
|
||||
with self.suspend():
|
||||
os.system('clear')
|
||||
p = subprocess.Popen(
|
||||
[command], stderr=subprocess.PIPE,
|
||||
universal_newlines=True, shell=True)
|
||||
code = p.wait()
|
||||
if copious_output:
|
||||
six.moves.input('Press any key to continue')
|
||||
if code != 0:
|
||||
_, stderr = p.communicate()
|
||||
_logger.warning(stderr)
|
||||
self.show_notification(
|
||||
'Program exited with status={0}\n{1}'.format(
|
||||
code, stderr.strip()))
|
||||
|
||||
else:
|
||||
# Non-blocking, open a background process
|
||||
with self.loader('Opening page', delay=0):
|
||||
p = subprocess.Popen(
|
||||
[command], shell=True, universal_newlines=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
# Wait a little while to make sure that the command doesn't
|
||||
# exit with an error. This isn't perfect, but it should be good
|
||||
# enough to catch invalid commands.
|
||||
time.sleep(1.0)
|
||||
code = p.poll()
|
||||
if code is not None and code != 0:
|
||||
_, stderr = p.communicate()
|
||||
raise exceptions.BrowserError(
|
||||
'Program exited with status={0}\n{1}'.format(
|
||||
code, stderr.strip()))
|
||||
|
||||
def get_mailcap_entry(self, url):
|
||||
"""
|
||||
Search through the mime handlers list and attempt to find the
|
||||
appropriate command to open the provided url with.
|
||||
|
||||
Will raise a MailcapEntryNotFound exception if no valid command exists.
|
||||
|
||||
Params:
|
||||
url (text): URL that will be checked
|
||||
|
||||
Returns:
|
||||
command (text): The string of the command that should be executed
|
||||
in a subprocess to open the resource.
|
||||
entry (dict): The full mailcap entry for the corresponding command
|
||||
"""
|
||||
|
||||
for parser in mime_parsers.parsers:
|
||||
if parser.pattern.match(url):
|
||||
# modified_url may be the same as the original url, but it
|
||||
# could also be updated to point to a different page, or it
|
||||
# could refer to the location of a temporary file with the
|
||||
# page's downloaded content.
|
||||
try:
|
||||
modified_url, content_type = parser.get_mimetype(url)
|
||||
except Exception as e:
|
||||
# If Imgur decides to change its html layout, let it fail
|
||||
# silently in the background instead of crashing.
|
||||
_logger.warn('parser %s raised an exception', parser)
|
||||
_logger.exception(e)
|
||||
raise exceptions.MailcapEntryNotFound()
|
||||
if not content_type:
|
||||
_logger.info('Content type could not be determined')
|
||||
raise exceptions.MailcapEntryNotFound()
|
||||
elif content_type == 'text/html':
|
||||
_logger.info('Content type text/html, deferring to browser')
|
||||
raise exceptions.MailcapEntryNotFound()
|
||||
|
||||
command, entry = mailcap.findmatch(
|
||||
self._mailcap_dict, content_type, filename=modified_url)
|
||||
if not entry:
|
||||
_logger.info('Could not find a valid mailcap entry')
|
||||
raise exceptions.MailcapEntryNotFound()
|
||||
|
||||
return command, entry
|
||||
|
||||
# No parsers matched the url
|
||||
raise exceptions.MailcapEntryNotFound()
|
||||
|
||||
def open_browser(self, url):
|
||||
"""
|
||||
Open the given url using the default webbrowser. The preferred browser
|
||||
@@ -359,7 +485,7 @@ class Terminal(object):
|
||||
break # Success
|
||||
elif code is not None:
|
||||
raise exceptions.BrowserError(
|
||||
'Browser exited with status=%s' % code)
|
||||
'Program exited with status=%s' % code)
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
raise exceptions.BrowserError(
|
||||
@@ -453,6 +579,12 @@ class Terminal(object):
|
||||
_logger.info('File deleted: %s', filepath)
|
||||
|
||||
def open_urlview(self, data):
|
||||
"""
|
||||
Pipe a block of text to urlview, which displays a list of urls
|
||||
contained in the text and allows the user to open them with their
|
||||
web browser.
|
||||
"""
|
||||
|
||||
urlview = os.getenv('RTV_URLVIEWER') or 'urlview'
|
||||
try:
|
||||
with self.suspend():
|
||||
@@ -461,6 +593,16 @@ class Terminal(object):
|
||||
p.communicate(input=data.encode('utf-8'))
|
||||
except KeyboardInterrupt:
|
||||
p.terminate()
|
||||
|
||||
code = p.poll()
|
||||
if code == 1:
|
||||
# Clear the "No URLs found." message from stdout
|
||||
sys.stdout.write("\033[F")
|
||||
sys.stdout.flush()
|
||||
|
||||
if code == 1:
|
||||
self.show_notification('No URLs found')
|
||||
|
||||
except OSError:
|
||||
self.show_notification(
|
||||
'Failed to open {0}'.format(urlview))
|
||||
|
||||
Reference in New Issue
Block a user