diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b63e5ab096b..984af9c993a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,13 +29,13 @@ jobs: run: | curl -O -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py python get-poetry.py -y --version 1.0.10 - echo "::set-env name=PATH::$HOME/.poetry/bin:$PATH" + echo "PATH=${HOME}/.poetry/bin:${PATH}" >> $GITHUB_ENV rm get-poetry.py - name: Get poetry cache paths from config run: | - echo ::set-env name=poetry_cache_dir::$(poetry config --list | sed -n 's/.*cache-dir = //p' | sed -e 's/^"//' -e 's/"$//') - echo ::set-env name=poetry_virtualenvs_path::$(poetry config --list | sed -n 's/.*virtualenvs.path = .* # //p' | sed -e 's/^"//' -e 's/"$//') + echo "poetry_cache_dir=$(poetry config --list | sed -n 's/.*cache-dir = //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV + echo "poetry_virtualenvs_path=$(poetry config --list | sed -n 's/.*virtualenvs.path = .* # //p' | sed -e 's/^\"//' -e 's/\"$//')" >> $GITHUB_ENV - name: Configure poetry shell: bash diff --git a/CHANGES b/CHANGES index fcc69453b04..a428aa4b3a5 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,7 @@ Here you can find the recent changes to tmuxp current ------- +- Adding option to dump `load` output to log file - *Insert changes/features/fixes for next release here* tmuxp 1.6.2 (2020-11-08) diff --git a/README.rst b/README.rst index 91a5179e042..56074ba1025 100644 --- a/README.rst +++ b/README.rst @@ -177,9 +177,16 @@ You can auto confirm the prompt. In this case no preview will be shown. $ tmuxp convert --yes filename -Debug Info +Debugging Helpers ---------- +The `load` command provides a way to log output to a log file for debugging +purposes. + +.. code-block:: sh + + $ tmuxp load --log-file . + Collect system info to submit with a Github issue: .. code-block:: sh @@ -192,6 +199,7 @@ Collect system info to submit with a Github issue: # ... so on + Docs / Reading material ----------------------- diff --git a/docs/cli.rst b/docs/cli.rst index 5d9b3fc4bee..87a67a089b6 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -184,12 +184,23 @@ are created, the last session is named from the terminal. $ tmxup load -s ... +The output of the ``load`` command can be logged to a file for +debugging purposes. the log level can be controlled with the global +``--log-level`` option (defaults to INFO). + +.. code-block:: bash + + $ tmuxp load --log-file + $ tmuxp --log-level load --log-file + + .. _cli_debug_info: Debug Info ---------- -Use to collect all relevant information for submitting an issue to the project. +Use to collect all relevant information for submitting an issue to +the project. .. code-block:: bash diff --git a/pyproject.toml b/pyproject.toml index 56bd8985bda..8a8efa679b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ skip-string-normalization = true [tool.poetry] name = "tmuxp" -version = "1.6.2" +version = "1.6.3" description = "tmux session manager" license = "MIT" authors = ["Tony Narlock "] diff --git a/tests/test_cli.py b/tests/test_cli.py index 303370a0165..1e14e51f918 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -408,6 +408,33 @@ def test_load_zsh_autotitle_warning(cli_args, tmpdir, monkeypatch): assert 'Please set' not in result.output +@pytest.mark.parametrize( + "cli_args", + [ + (['load', '.', '--log-file', 'log.txt']), + ], +) +def test_load_log_file(cli_args, tmpdir, monkeypatch): + # create dummy tmuxp yaml that breaks to prevent actually loading tmux + tmpdir.join('.tmuxp.yaml').write( + """ +session_name: hello + """ + ) + tmpdir.join('.oh-my-zsh').ensure(dir=True) + monkeypatch.setenv('HOME', str(tmpdir)) + + with tmpdir.as_cwd(): + print('tmpdir: {0}'.format(tmpdir)) + runner = CliRunner() + + # If autoconfirm (-y) no need to prompt y + input_args = 'y\ny\n' if '-y' not in cli_args else '' + + runner.invoke(cli.cli, cli_args, input=input_args) + assert 'Loading' in tmpdir.join('log.txt').open().read() + + @pytest.mark.parametrize("cli_cmd", ['shell', ('shell', '--pdb')]) @pytest.mark.parametrize( "cli_args,inputs,env,expected_output", diff --git a/tmuxp/__about__.py b/tmuxp/__about__.py index eaa33c135f6..f94e45557bd 100644 --- a/tmuxp/__about__.py +++ b/tmuxp/__about__.py @@ -1,6 +1,6 @@ __title__ = 'tmuxp' __package_name__ = 'tmuxp' -__version__ = '1.6.2' +__version__ = '1.6.3' __description__ = 'tmux session manager' __email__ = 'tony@git-pull.com' __author__ = 'Tony Narlock' diff --git a/tmuxp/cli.py b/tmuxp/cli.py index 72e79ce624e..53271f3f4db 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -42,6 +42,18 @@ def get_cwd(): return os.getcwd() +def tmuxp_echo(message=None, log_level='INFO', style_log=False, **click_kwargs): + """ + Combines logging.log and click.echo + """ + if style_log: + logger.log(log.LOG_LEVELS[log_level], message) + else: + logger.log(log.LOG_LEVELS[log_level], click.unstyle(message)) + + click.echo(message, **click_kwargs) + + def get_config_dir(): """ Return tmuxp configuration directory. @@ -246,8 +258,8 @@ def scan_config_argument(ctx, param, value, config_dir=None): config_dir = config_dir() if not config: - click.echo("Enter at least one CONFIG") - click.echo(ctx.get_help(), color=ctx.color) + tmuxp_echo("Enter at least one CONFIG") + tmuxp_echo(ctx.get_help(), color=ctx.color) ctx.exit() if isinstance(value, string_types): @@ -357,11 +369,14 @@ def scan_config(config, config_dir=None): ] if len(candidates) > 1: - click.secho( - 'Multiple .tmuxp.{yml,yaml,json} configs in %s' % dirname(config), - fg="red", + tmuxp_echo( + click.style( + 'Multiple .tmuxp.{yml,yaml,json} configs in %s' + % dirname(config), + fg="red", + ) ) - click.echo( + tmuxp_echo( click.wrap_text( 'This is undefined behavior, use only one. ' 'Use file names e.g. myproject.json, coolproject.yaml. ' @@ -505,6 +520,11 @@ def load_workspace( # get the canonical path, eliminating any symlinks config_file = os.path.realpath(config_file) + tmuxp_echo( + click.style('[Loading] ', fg='green') + + click.style(config_file, fg='blue', bold=True) + ) + # kaptan allows us to open a yaml or json file as a dict sconfig = kaptan.Kaptan() sconfig = sconfig.import_config(config_file).get() @@ -525,7 +545,7 @@ def load_workspace( try: # load WorkspaceBuilder object for tmuxp config / tmux server builder = WorkspaceBuilder(sconf=sconfig, server=t) except exc.EmptyConfigException: - click.echo('%s is empty or parsed no config data' % config_file, err=True) + tmuxp_echo('%s is empty or parsed no config data' % config_file, err=True) return session_name = sconfig['session_name'] @@ -545,11 +565,6 @@ def load_workspace( return try: - click.echo( - click.style('[Loading] ', fg='green') - + click.style(config_file, fg='blue', bold=True) - ) - builder.build() # load tmux session via workspace builder if 'TMUX' in os.environ: # tmuxp ran from inside tmux @@ -586,8 +601,8 @@ def load_workspace( except exc.TmuxpException as e: import traceback - click.echo(traceback.format_exc(), err=True) - click.echo(e, err=True) + tmuxp_echo(traceback.format_exc(), err=True) + tmuxp_echo(e, err=True) choice = click.prompt( 'Error loading workspace. (k)ill, (a)ttach, (d)etach?', @@ -597,7 +612,7 @@ def load_workspace( if choice == 'k': builder.session.kill_session() - click.echo('Session killed.') + tmuxp_echo('Session killed.') elif choice == 'a': if 'TMUX' in os.environ: builder.session.switch_client() @@ -612,7 +627,7 @@ def load_workspace( @click.group(context_settings={'obj': {}}) @click.version_option(__version__, '-V', '--version', message='%(prog)s %(version)s') @click.option( - '--log_level', + '--log-level', default='INFO', help='Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)', ) @@ -625,12 +640,12 @@ def cli(log_level): try: has_minimum_version() except TmuxCommandNotFound: - click.echo('tmux not found. tmuxp requires you install tmux first.') + tmuxp_echo('tmux not found. tmuxp requires you install tmux first.') sys.exit() except exc.TmuxpException as e: - click.echo(e, err=True) + tmuxp_echo(e, err=True) sys.exit() - setup_logger(level=log_level.upper()) + setup_logger(logger=logger, level=log_level.upper()) def setup_logger(logger=None, level='INFO'): @@ -649,12 +664,12 @@ def setup_logger(logger=None, level='INFO'): logger = logging.getLogger() if not logger.handlers: # setup logger handlers - channel = logging.StreamHandler() - channel.setFormatter(log.DebugLogFormatter()) - + # channel = logging.StreamHandler() + # channel.setFormatter(log.DebugLogFormatter()) # channel.setFormatter(log.LogFormatter()) + logger.setLevel(level) - logger.addHandler(channel) + # logger.addHandler(channel) def startup(config_dir): @@ -875,6 +890,7 @@ def command_freeze(session_name, socket_name, socket_path, force): flag_value=88, help='Like -2, but indicates that the terminal supports 88 colours.', ) +@click.option('--log-file', help='File to log errors/output to') def command_load( ctx, config, @@ -884,6 +900,7 @@ def command_load( answer_yes, detached, colors, + log_file, ): """Load a tmux workspace from each CONFIG. @@ -908,6 +925,10 @@ def command_load( detached mode. """ util.oh_my_zsh_auto_title() + if log_file: + logfile_handler = logging.FileHandler(log_file) + logfile_handler.setFormatter(log.LogFormatter()) + logger.addHandler(logfile_handler) tmux_options = { 'socket_name': socket_name, @@ -919,8 +940,8 @@ def command_load( } if not config: - click.echo("Enter at least one CONFIG") - click.echo(ctx.get_help(), color=ctx.color) + tmuxp_echo("Enter at least one CONFIG") + tmuxp_echo(ctx.get_help(), color=ctx.color) ctx.exit() if isinstance(config, string_types): @@ -962,7 +983,7 @@ def import_config(configfile, importfunc): else: sys.exit('Unknown config format.') - click.echo( + tmuxp_echo( newconfig + '---------------------------------------------------------------' '\n' 'Configuration import does its best to convert files.\n' @@ -984,9 +1005,9 @@ def import_config(configfile, importfunc): buf.write(newconfig) buf.close() - click.echo('Saved to %s.' % dest) + tmuxp_echo('Saved to %s.' % dest) else: - click.echo( + tmuxp_echo( 'tmuxp has examples in JSON and YAML format at ' '\n' 'View tmuxp docs at ' @@ -1125,4 +1146,4 @@ def format_tmux_resp(std_resp): % format_tmux_resp(tmux_cmd('show-window-options', '-g')), ] - click.echo('\n'.join(output)) + tmuxp_echo('\n'.join(output)) diff --git a/tmuxp/log.py b/tmuxp/log.py index 561877835b5..bb9b3d0a45f 100644 --- a/tmuxp/log.py +++ b/tmuxp/log.py @@ -21,8 +21,26 @@ 'CRITICAL': Fore.RED, } +LOG_LEVELS = { + 'CRITICAL': 50, + 'ERROR': 40, + 'WARNING': 30, + 'INFO': 20, + 'DEBUG': 10, + 'NOTSET': 0, +} + + +def set_style( + message, stylized, style_before=None, style_after=None, prefix='', suffix='' +): + if stylized: + return prefix + style_before + message + style_after + suffix + + return prefix + message + suffix -def default_log_template(self, record): + +def default_log_template(self, record, stylized=False): """ Return the prefix for the log message. Template for Formatter. @@ -39,37 +57,34 @@ def default_log_template(self, record): """ reset = Style.RESET_ALL - levelname = ( - LEVEL_COLORS.get(record.levelname) - + Style.BRIGHT - + '(%(levelname)s)' - + Style.RESET_ALL - + ' ' + levelname = set_style( + '(%(levelname)s)', + stylized, + style_before=(LEVEL_COLORS.get(record.levelname) + Style.BRIGHT), + style_after=Style.RESET_ALL, + suffix=' ', ) - asctime = ( - '[' - + Fore.BLACK - + Style.DIM - + Style.BRIGHT - + '%(asctime)s' - + Fore.RESET - + Style.RESET_ALL - + ']' + asctime = set_style( + '%(asctime)s', + stylized, + style_before=(Fore.BLACK + Style.DIM + Style.BRIGHT), + style_after=(Fore.RESET + Style.RESET_ALL), + prefix='[', + suffix=']', ) - name = ( - ' ' - + Fore.WHITE - + Style.DIM - + Style.BRIGHT - + '%(name)s' - + Fore.RESET - + Style.RESET_ALL - + ' ' + name = set_style( + '%(name)s', + stylized, + style_before=(Fore.WHITE + Style.DIM + Style.BRIGHT), + style_after=(Fore.RESET + Style.RESET_ALL), + prefix=' ', + suffix=' ', ) - tpl = reset + levelname + asctime + name + reset + if stylized: + return reset + levelname + asctime + name + reset - return tpl + return levelname + asctime + name class LogFormatter(logging.Formatter): @@ -89,8 +104,9 @@ def format(self, record): prefix = self.template(record) % record.__dict__ + parts = prefix.split(record.message) formatted = prefix + " " + record.message - return formatted.replace("\n", "\n ") + return formatted.replace("\n", "\n" + parts[0] + " ") def debug_log_template(self, record):