You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
253 lines
8.9 KiB
253 lines
8.9 KiB
from __future__ import print_function, unicode_literals
|
|
|
|
import os
|
|
import sys
|
|
import shlex
|
|
import errno
|
|
import locale
|
|
import codecs
|
|
import tempfile
|
|
import traceback
|
|
import subprocess
|
|
|
|
from . import color
|
|
from . import config
|
|
from .p3 import _get_raw_stdout, _get_raw_stderr, input, ustr
|
|
from .content import check_file, read_text_file, write_file
|
|
|
|
|
|
DEBUG = False # unhandled exceptions traces are printed
|
|
DEBUG_ALL_TRACES = False # handled exceptions traces are printed
|
|
# package-shared ui that can be accessed using :
|
|
# from uis import get_ui
|
|
# ui = get_ui()
|
|
# you must instanciate ui with a Config instance using init_ui(config)
|
|
_ui = None
|
|
|
|
|
|
def _get_encoding(conf):
|
|
"""Get local terminal encoding or user preference in config."""
|
|
enc = 'utf-8'
|
|
try:
|
|
enc = locale.getdefaultlocale()[1]
|
|
except ValueError:
|
|
pass # Keep default
|
|
if conf is None:
|
|
return enc or 'utf-8'
|
|
return conf.get('terminal-encoding', enc or 'utf-8')
|
|
|
|
|
|
def _get_local_editor():
|
|
"""Get the editor from environment variables.
|
|
|
|
Use vi as a default.
|
|
"""
|
|
return os.environ.get('EDITOR', 'vi')
|
|
|
|
|
|
def get_ui():
|
|
if _ui is None:
|
|
return PrintUI(config.load_default_conf()) # no editor support. (#FIXME?)
|
|
return _ui
|
|
|
|
|
|
def init_ui(conf, force_colors=False):
|
|
global _ui
|
|
_ui = InputUI(conf, force_colors=force_colors)
|
|
|
|
|
|
class PrintUI(object):
|
|
|
|
def __init__(self, conf, force_colors=False):
|
|
"""
|
|
:param conf: if None, conservative default values are used.
|
|
Useful to instanciate the UI before parsing the config file.
|
|
"""
|
|
color.setup(conf, force_colors=force_colors)
|
|
self.encoding = _get_encoding(conf)
|
|
self._stdout = codecs.getwriter(self.encoding)(_get_raw_stdout(),
|
|
errors='replace')
|
|
self._stderr = codecs.getwriter(self.encoding)(_get_raw_stderr(),
|
|
errors='replace')
|
|
self.debug = conf['main'].get('debug', False)
|
|
|
|
def message(self, *messages, **kwargs):
|
|
kwargs['file'] = self._stdout
|
|
print(*messages, **kwargs)
|
|
|
|
def info(self, message, **kwargs):
|
|
kwargs['file'] = self._stdout
|
|
print('{}: {}'.format(color.dye_out('info', 'ok'), message), **kwargs)
|
|
|
|
def warning(self, message, **kwargs):
|
|
kwargs['file'] = self._stderr
|
|
print('{}: {}'.format(color.dye_err('warning', 'warning'), message), **kwargs)
|
|
|
|
def error(self, message, **kwargs):
|
|
kwargs['file'] = self._stderr
|
|
print('{}: {}'.format(color.dye_err('error', 'error'), message), **kwargs)
|
|
|
|
if DEBUG_ALL_TRACES: # if an exception has been raised, print the trace.
|
|
if sys.exc_info()[0] is not None:
|
|
traceback.print_exception(*sys.exc_info())
|
|
|
|
def exit(self, error_code=1):
|
|
sys.exit(error_code)
|
|
|
|
def handle_exception(self, exc):
|
|
"""Attempts to handle exception.
|
|
|
|
:returns: True if exception has been handled (currently never happens)
|
|
"""
|
|
self.error(ustr(exc))
|
|
if DEBUG or self.debug:
|
|
raise
|
|
else:
|
|
self.exit()
|
|
return True # never happens
|
|
|
|
def test_handle_exception(self, exc):
|
|
"""Attempts to handle exception.
|
|
|
|
:returns: True if exception has been handled (currently never happens)
|
|
"""
|
|
self.error(ustr(exc))
|
|
if DEBUG or self.debug:
|
|
raise
|
|
else:
|
|
self.exit()
|
|
return True # never happens
|
|
|
|
|
|
|
|
class InputUI(PrintUI):
|
|
"""UI class. Stores configuration parameters and system information.
|
|
"""
|
|
|
|
def __init__(self, conf, force_colors=False):
|
|
super(InputUI, self).__init__(conf, force_colors=force_colors)
|
|
self.editor = conf['main']['edit_cmd'] or _get_local_editor()
|
|
|
|
def input(self):
|
|
try:
|
|
data = input()
|
|
except EOFError:
|
|
self.error('Standard input ended while waiting for answer.')
|
|
self.exit(1)
|
|
return ustr(data) #.decode('utf-8')
|
|
|
|
def input_choice_ng(self, options, option_chars=None, default=None, question=''):
|
|
"""Ask the user to chose between a set of options. The user is asked
|
|
to input a char corresponding to the option he chooses.
|
|
|
|
:param options: list of strings
|
|
list of options
|
|
:param default: int
|
|
default if no option is accepted, if None answer is required
|
|
:param question: string
|
|
:returns: int
|
|
the index of the chosen option
|
|
"""
|
|
option_chars = [s[0] for s in options]
|
|
displayed_chars = [c.upper() if i == default else c
|
|
for i, c in enumerate(option_chars)]
|
|
|
|
if len(set(option_chars)) != len(option_chars): # duplicate chars, char choices are deactivated. #FIXME: should only deactivate ambiguous chars
|
|
option_chars = []
|
|
|
|
option_str = '/'.join(["{}{}".format(color.dye_out(c, 'bold'), s[1:])
|
|
for c, s in zip(displayed_chars, options)])
|
|
|
|
self.message('{}: {} {}: '.format(color.dye_err('prompt', 'warning'), question, option_str), end='')
|
|
while True:
|
|
answer = self.input()
|
|
if answer is None or answer == '':
|
|
if default is not None:
|
|
return default
|
|
else:
|
|
try:
|
|
return options.index(answer.lower())
|
|
except ValueError:
|
|
try: # FIXME options handling !!!
|
|
return option_chars.index(answer.lower())
|
|
except ValueError:
|
|
pass
|
|
self.message('Incorrect option.', option_str)
|
|
|
|
def input_choice(self, options, option_chars, default=None, question=''):
|
|
"""Ask the user to chose between a set of options. The user is asked
|
|
to input a char corresponding to the option he chooses.
|
|
|
|
:param options: list of strings
|
|
list of options
|
|
:param option_chars: list of chars
|
|
chars used to identify options, should be lowercase and not
|
|
contain duplicates
|
|
:param default: int
|
|
default if no option is accepted, if None answer is required
|
|
:param question: string
|
|
:returns: int
|
|
the index of the chosen option
|
|
"""
|
|
displayed_chars = [s.upper() if i == default else s
|
|
for i, s in enumerate(option_chars)]
|
|
option_str = ', '.join(["[%s]%s" % (color.dye_out(c, 'cyan'), o)
|
|
for c, o in zip(displayed_chars, options)])
|
|
self.message(question, option_str)
|
|
while True:
|
|
answer = self.input()
|
|
if answer is None or answer == '':
|
|
if default is not None:
|
|
return default
|
|
else:
|
|
try: # FIXME options handling !!!
|
|
return option_chars.index(answer.lower())
|
|
except ValueError:
|
|
pass
|
|
self.message('Incorrect option.', option_str)
|
|
|
|
def input_yn(self, question='', default='y'):
|
|
d = 0 if default in (True, 'y', 'yes') else 1
|
|
answer = self.input_choice_ng(['yes', 'no'], default=d, question=question)
|
|
return [True, False][answer]
|
|
|
|
def editor_input(self, initial="", suffix='.tmp'):
|
|
"""Use an editor to get input"""
|
|
str_initial = initial.encode('utf-8') # TODO: make it a configuration item
|
|
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as temp_file:
|
|
tfile_name = temp_file.name
|
|
temp_file.write(str_initial)
|
|
self._call_editor(tfile_name)
|
|
content = read_text_file(tfile_name)
|
|
os.remove(tfile_name)
|
|
return content
|
|
|
|
def edit_file(self, path, temporary):
|
|
if temporary:
|
|
check_file(path, fail=True)
|
|
content = read_text_file(path)
|
|
content = self.editor_input(content)
|
|
write_file(path, content)
|
|
else:
|
|
self._call_editor(path)
|
|
|
|
def _call_editor(self, path):
|
|
"""Call the editor, and checks that no error were raised by the OS"""
|
|
cmd = shlex.split(self.editor) # this enable editor command with option, e.g. gvim -f
|
|
cmd.append(path)
|
|
try:
|
|
subprocess.call(cmd)
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
self.error(("Error while calling editor '{}'. The editor may "
|
|
"not be present. You can change the text editor "
|
|
"that pubs uses by setting the $EDITOR environment "
|
|
"variable, or by running `pubs conf` and setting "
|
|
"the `edit_cmd` field."
|
|
).format(self.editor))
|
|
# handle file not found error.
|
|
self.exit()
|
|
else:
|
|
# Something else went wrong while trying to run `wget`
|
|
self.handle_exception(e)
|