# coding: utf-8 """ StaSh - Pythonista Shell https://github.com/ywangd/stash """ __version__ = '0.7.4' import imp as pyimp # rename to avoid name conflict with objc_util import logging import logging.handlers import os import platform import sys from io import IOBase import six from six import BytesIO, StringIO from six.moves.configparser import ConfigParser # noinspection PyPep8Naming from .system.shcommon import (_EXTERNAL_DIRS, _STASH_CONFIG_FILES, _STASH_ROOT, _SYS_STDOUT, IN_PYTHONISTA, ON_IPAD) from .system.shcommon import Control as ctrl from .system.shcommon import Escape as esc from .system.shcommon import Graphics as graphics from .system.shio import ShIO from .system.shiowrapper import disable as disable_io_wrapper from .system.shiowrapper import enable as enable_io_wrapper from .system.shparsers import ShCompleter, ShExpander, ShParser from .system.shruntime import ShRuntime from .system.shscreens import ShSequentialScreen from .system.shstreams import ShMiniBuffer, ShStream from .system.shui import get_ui_implementation from .system.shuseractionproxy import ShUserActionProxy # Setup logging LOGGER = logging.getLogger('StaSh') # Debugging constants _DEBUG_STREAM = 200 _DEBUG_RENDERER = 201 _DEBUG_MAIN_SCREEN = 202 _DEBUG_MINI_BUFFER = 203 _DEBUG_IO = 204 _DEBUG_UI = 300 _DEBUG_TERMINAL = 301 _DEBUG_TV_DELEGATE = 302 _DEBUG_RUNTIME = 400 _DEBUG_PARSER = 401 _DEBUG_EXPANDER = 402 _DEBUG_COMPLETER = 403 # Default configuration (can be overridden by external configuration file) _DEFAULT_CONFIG = """[system] rcfile=.stashrc py_traceback=0 py_pdb=0 input_encoding_utf8=1 thread_type=ctypes [display] TEXT_FONT_SIZE={font_size} BUTTON_FONT_SIZE=14 BACKGROUND_COLOR=(0.0, 0.0, 0.0) TEXT_COLOR=(1.0, 1.0, 1.0) TINT_COLOR=(0.0, 0.0, 1.0) INDICATOR_STYLE=white BUFFER_MAX=150 AUTO_COMPLETION_MAX=50 VK_SYMBOLS=~/.-*|>$'=!&_"\\?` [style] enable_styles=1 colored_errors=1 [history] ipython_style_history_search=1 allow_double_lines=0 hide_whitespace_lines=1 maxsize=50 """.format( font_size=(14 if ON_IPAD else 12), ) # create directories outside STASH_ROOT # we should do this each time StaSh because some commands may require # this directories for p in _EXTERNAL_DIRS: if not os.path.exists(p): try: os.mkdir(p) except: pass class StaSh(object): """ Main application class. It initialize and wires the components and provide utility interfaces to running scripts. """ PY3 = six.PY3 def __init__(self, debug=(), log_setting=None, no_cfgfile=False, no_rcfile=False, no_historyfile=False, command=None): self.__version__ = __version__ # Intercept IO enable_io_wrapper() self.config = self._load_config(no_cfgfile=no_cfgfile) self.logger = self._config_logging(log_setting) self.enable_styles = self.config.getboolean("style", "enable_styles") self.user_action_proxy = ShUserActionProxy(self) # Tab handler for running scripts self.external_tab_handler = None # Wire the components self.main_screen = ShSequentialScreen( self, nlines_max=self.config.getint('display', 'BUFFER_MAX'), debug=_DEBUG_MAIN_SCREEN in debug ) self.mini_buffer = ShMiniBuffer(self, self.main_screen, debug=_DEBUG_MINI_BUFFER in debug) self.stream = ShStream(self, self.main_screen, debug=_DEBUG_STREAM in debug) self.io = ShIO(self, debug=_DEBUG_IO in debug) ShUI, ShSequentialRenderer = get_ui_implementation() self.terminal = None # will be set during UI initialisation self.ui = ShUI(self, debug=(_DEBUG_UI in debug), debug_terminal=(_DEBUG_TERMINAL in debug)) self.renderer = ShSequentialRenderer(self, self.main_screen, self.terminal, debug=_DEBUG_RENDERER in debug) parser = ShParser(debug=_DEBUG_PARSER in debug) expander = ShExpander(self, debug=_DEBUG_EXPANDER in debug) self.runtime = ShRuntime(self, parser, expander, no_historyfile=no_historyfile, debug=_DEBUG_RUNTIME in debug) self.completer = ShCompleter(self, debug=_DEBUG_COMPLETER in debug) # Navigate to the startup folder if IN_PYTHONISTA: os.chdir(self.runtime.state.environ_get('HOME2')) self.runtime.load_rcfile(no_rcfile=no_rcfile) self.io.write( self.text_style( 'StaSh v%s on python %s\n' % ( self.__version__, platform.python_version(), ), { 'color': 'blue', 'traits': ['bold'] }, always=True, ), ) # warn on py3 if self.PY3: self.io.write( self.text_style( 'Warning: you are running StaSh in python3. Some commands may not work correctly in python3.\n', {'color': 'red'}, always=True, ), ) self.io.write( self.text_style( 'Please help us improving StaSh by reporting bugs on github.\n', { 'color': 'yellow', 'traits': ['italic'] }, always=True, ), ) # Load shared libraries self._load_lib() # run command (this calls script_will_end) if command is None: # show tip of the day command = '$STASH_ROOT/bin/totd.py' if command: # do not run command if command is False (but not None) if self.runtime.debug: self.logger.debug("Running command: {!r}".format(command)) self(command, add_to_history=False, persistent_level=0) def __call__(self, input_, persistent_level=2, *args, **kwargs): """ This function is to be called by external script for executing shell commands """ worker = self.runtime.run(input_, persistent_level=persistent_level, *args, **kwargs) worker.join() return worker @staticmethod def _load_config(no_cfgfile=False): config = ConfigParser() config.optionxform = str # make it preserve case # defaults if not six.PY3: config.readfp(BytesIO(_DEFAULT_CONFIG)) else: config.read_file(StringIO(_DEFAULT_CONFIG)) # update from config file if not no_cfgfile: config.read(os.path.join(_STASH_ROOT, f) for f in _STASH_CONFIG_FILES) return config @staticmethod def _config_logging(log_setting): logger = logging.getLogger('StaSh') _log_setting = { 'level': 'DEBUG', 'stdout': True, } _log_setting.update(log_setting or {}) level = { 'CRITICAL': logging.CRITICAL, 'ERROR': logging.ERROR, 'WARNING': logging.WARNING, 'INFO': logging.INFO, 'DEBUG': logging.DEBUG, 'NOTEST': logging.NOTSET, }.get(_log_setting['level'], logging.DEBUG) logger.setLevel(level) if not logger.handlers: if _log_setting['stdout']: _log_handler = logging.StreamHandler(_SYS_STDOUT) else: _log_handler = logging.handlers.RotatingFileHandler('stash.log', mode='w') _log_handler.setLevel(level) _log_handler.setFormatter( logging.Formatter( '[%(asctime)s] [%(levelname)s] [%(threadName)s] [%(name)s] [%(funcName)s] [%(lineno)d] - %(message)s' ) ) logger.addHandler(_log_handler) return logger def _load_lib(self): """ Load library files as modules and save each of them as attributes """ lib_path = os.path.join(_STASH_ROOT, 'lib') os.environ['STASH_ROOT'] = _STASH_ROOT # libcompleter needs this value try: for f in os.listdir(lib_path): fp = os.path.join(lib_path, f) if f.startswith('lib') and f.endswith('.py') and os.path.isfile(fp): name, _ = os.path.splitext(f) if self.runtime.debug: self.logger.debug("Attempting to load library '{}'...".format(name)) try: self.__dict__[name] = pyimp.load_source(name, fp) except Exception as e: self.write_message('%s: failed to load library file (%s)' % (f, repr(e)), error=True) finally: # do not modify environ permanently os.environ.pop('STASH_ROOT') def write_message(self, s, error=False, prefix="stash: "): """ Write a message to the output. :param s: message to write :type w: str :param error: whether this is an error message :type error: bool """ s = '%s%s\n' % (prefix, s) if error: if self.runtime.debug: self.logger.error(s) if self.runtime.colored_errors: s = self.text_color(s, "red") else: if self.runtime.debug: self.logger.info(s) self.io.write(s) def launch(self, command=None): """ Launch StaSh, presenting the UI. """ self.ui.show() # self.terminal.set_focus() def close(self): """ Quit StaSh. StaSh is based arround the UI, so we delegate this task to the UI, which in turn will call self.on_exit(). """ self.ui.close() def on_exit(self): """ This method will be called when StaSh is about the be closed. """ self.runtime.save_history() self.cleanup() # Clear the stack or the stdout becomes unusable for interactive prompt self.runtime.worker_registry.purge() def cleanup(self): """ Perform cleanup here. """ disable_io_wrapper() def get_workers(self): """ Return a list of all workers.. :return: a list of all workers :rtype: list of [stash.system.shtreads.BaseThread] """ return [worker for worker in self.runtime.worker_registry] # noinspection PyProtectedMember # @staticmethod def text_style(self, s, style, always=False): """ Style the given string with ASCII escapes. :param str s: String to decorate :param dict style: A dictionary of styles :param bool always: If true, style will be applied even for pipes. :return: """ # No color for pipes, files and Pythonista console if not self.enable_styles or (not always and (isinstance(sys.stdout, (StringIO, IOBase)) # or sys.stdout.write.im_self is _SYS_STDOUT or sys.stdout is _SYS_STDOUT)): return s fmt_string = u'%s%%d%s%%s%s%%d%s' % (ctrl.CSI, esc.SGR, ctrl.CSI, esc.SGR) for style_name, style_value in style.items(): if style_name == 'color': color_id = graphics._SGR.get(style_value.lower()) if color_id is not None: s = fmt_string % (color_id, s, graphics._SGR['default']) elif style_name == 'bgcolor': color_id = graphics._SGR.get('bg-' + style_value.lower()) if color_id is not None: s = fmt_string % (color_id, s, graphics._SGR['default']) elif style_name == 'traits': for val in style_value: val = val.lower() if val == 'bold': s = fmt_string % (graphics._SGR['+bold'], s, graphics._SGR['-bold']) elif val == 'italic': s = fmt_string % (graphics._SGR['+italics'], s, graphics._SGR['-italics']) elif val == 'underline': s = fmt_string % (graphics._SGR['+underscore'], s, graphics._SGR['-underscore']) elif val == 'strikethrough': s = fmt_string % (graphics._SGR['+strikethrough'], s, graphics._SGR['-strikethrough']) return s def text_color(self, s, color_name='default', **kwargs): return self.text_style(s, {'color': color_name}, **kwargs) def text_bgcolor(self, s, color_name='default', **kwargs): return self.text_style(s, {'bgcolor': color_name}, **kwargs) def text_bold(self, s, **kwargs): return self.text_style(s, {'traits': ['bold']}, **kwargs) def text_italic(self, s, **kwargs): return self.text_style(s, {'traits': ['italic']}, **kwargs) def text_bold_italic(self, s, **kwargs): return self.text_style(s, {'traits': ['bold', 'italic']}, **kwargs) def text_underline(self, s, **kwargs): return self.text_style(s, {'traits': ['underline']}, **kwargs) def text_strikethrough(self, s, **kwargs): return self.text_style(s, {'traits': ['strikethrough']}, **kwargs)