diff --git a/dvc/dagascii.py b/dvc/dagascii.py index 6f324b865b..bf23d88c09 100644 --- a/dvc/dagascii.py +++ b/dvc/dagascii.py @@ -2,7 +2,10 @@ from __future__ import print_function from __future__ import unicode_literals +import logging import math +import os +import pydoc import sys from grandalf.graphs import Edge @@ -12,6 +15,42 @@ from grandalf.routing import EdgeViewer from grandalf.routing import route_with_lines +from dvc.env import DVC_PAGER +from dvc.utils import boxify, is_exec_found + + +logger = logging.getLogger(__name__) + + +DEFAULT_PAGER = "less" +DEFAULT_PAGER_FORMATTED = "{} --chop-long-lines --clear-screen".format( + DEFAULT_PAGER +) +DVC_PAGER_INFO = """\ +Less command line tool is missing. +Install & add less tool to your PATH env. var. to automatically send output \ +to pager. +Also, you can override default pager via {} env. var. +""".format( + DVC_PAGER +) + + +def find_pager(): + if not sys.stdout.isatty(): + return pydoc.plainpager + + if is_exec_found(DEFAULT_PAGER): + pager_cmd = os.getenv(DVC_PAGER, DEFAULT_PAGER_FORMATTED) + + def less_pager(text): + return pydoc.tempfilepager(pydoc.plain(text), pager_cmd) + + return less_pager + + logger.info(boxify(DVC_PAGER_INFO, border_color="yellow")) + return pydoc.plainpager + class VertexViewer(object): """Class to define vertex box boundaries that will be accounted for during @@ -60,99 +99,10 @@ def __init__(self, cols, lines): def draw(self): """Draws ASCII canvas on the screen.""" - if sys.stdout.isatty(): # pragma: no cover - from asciimatics.screen import Screen - - Screen.wrapper(self._do_draw) - else: - for line in self.canvas: - print("".join(line)) - - def _do_draw(self, screen): # pragma: no cover - # pylint: disable=too-many-locals - # pylint: disable=too-many-branches, too-many-statements - from dvc.system import System - from asciimatics.event import KeyboardEvent - - offset_x = 0 - offset_y = 0 - smaxrow, smaxcol = screen.dimensions - assert smaxrow > 1 - assert smaxcol > 1 - smaxrow -= 1 - smaxcol -= 1 - - if self.lines + 1 > smaxrow: - max_y = self.lines + 1 - smaxrow - else: - max_y = 0 - - if self.cols + 1 > smaxcol: - max_x = self.cols + 1 - smaxcol - else: - max_x = 0 - - while True: - for y in range(smaxrow + 1): - y_index = offset_y + y - line = [] - for x in range(smaxcol + 1): - x_index = offset_x + x - if ( - len(self.canvas) > y_index - and len(self.canvas[y_index]) > x_index - ): - line.append(self.canvas[y_index][x_index]) - else: - line.append(" ") - assert len(line) == (smaxcol + 1) - screen.print_at("".join(line), 0, y) - - screen.refresh() - - # NOTE: get_event() doesn't block by itself, - # so we have to do the blocking ourselves. - # - # NOTE: using this workaround while waiting for PR [1] - # to get merged and released. After that need to adjust - # asciimatics version requirements. - # - # [1] https://github.com/peterbrittain/asciimatics/pull/188 - System.wait_for_input(self.TIMEOUT) - - event = screen.get_event() - if not isinstance(event, KeyboardEvent): - continue - - k = event.key_code - if k == screen.KEY_DOWN or k == ord("s"): - offset_y += 1 - elif k == screen.KEY_PAGE_DOWN or k == ord("S"): - offset_y += smaxrow - elif k == screen.KEY_UP or k == ord("w"): - offset_y -= 1 - elif k == screen.KEY_PAGE_UP or k == ord("W"): - offset_y -= smaxrow - elif k == screen.KEY_RIGHT or k == ord("d"): - offset_x += 1 - elif k == ord("D"): - offset_x += smaxcol - elif k == screen.KEY_LEFT or k == ord("a"): - offset_x -= 1 - elif k == ord("A"): - offset_x -= smaxcol - elif k == ord("q") or k == ord("Q"): - break - - if offset_y > max_y: - offset_y = max_y - elif offset_y < 0: - offset_y = 0 - - if offset_x > max_x: - offset_x = max_x - elif offset_x < 0: - offset_x = 0 + pager = find_pager() + lines = map("".join, self.canvas) + joined_lines = os.linesep.join(lines) + pager(joined_lines) def point(self, x, y, char): """Create a point on ASCII canvas. diff --git a/dvc/env.py b/dvc/env.py index be49679127..9b5ea67a80 100644 --- a/dvc/env.py +++ b/dvc/env.py @@ -1 +1,2 @@ DVC_DAEMON = "DVC_DAEMON" +DVC_PAGER = "DVC_PAGER" diff --git a/dvc/system.py b/dvc/system.py index 1a8dbef70b..2195008337 100644 --- a/dvc/system.py +++ b/dvc/system.py @@ -218,41 +218,6 @@ def inode(path): assert inode < 2 ** 64 return inode - @staticmethod - def _wait_for_input_windows(timeout): - import sys - import ctypes - import msvcrt - from ctypes.wintypes import DWORD, HANDLE - - # https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-waitforsingleobject - from win32event import WAIT_OBJECT_0, WAIT_TIMEOUT - - func = ctypes.windll.kernel32.WaitForSingleObject - func.argtypes = [HANDLE, DWORD] - func.restype = DWORD - - rc = func(msvcrt.get_osfhandle(sys.stdin.fileno()), timeout * 1000) - if rc not in [WAIT_OBJECT_0, WAIT_TIMEOUT]: - raise RuntimeError(rc) - - @staticmethod - def _wait_for_input_posix(timeout): - import sys - import select - - try: - select.select([sys.stdin], [], [], timeout) - except select.error: - pass - - @staticmethod - def wait_for_input(timeout): - if System.is_unix(): - return System._wait_for_input_posix(timeout) - else: - return System._wait_for_input_windows(timeout) - @staticmethod def is_symlink(path): path = fspath(path) diff --git a/dvc/utils/__init__.py b/dvc/utils/__init__.py index 68ba418778..4a186bfea9 100644 --- a/dvc/utils/__init__.py +++ b/dvc/utils/__init__.py @@ -420,3 +420,8 @@ def resolve_output(inp, out): if os.path.isdir(out): return os.path.join(out, name) return out + + +def is_exec_found(exec_name): + cmd = "({}) 2>/dev/null".format(exec_name) + return hasattr(os, "system") and os.system(cmd) == 0 diff --git a/setup.py b/setup.py index b9b16f014d..c3903c6be9 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ def run(self): "jsonpath-ng>=1.4.3", "requests>=2.22.0", "grandalf==0.6", - "asciimatics>=1.10.0", "distro>=1.3.0", "appdirs>=1.4.3", "treelib>=1.5.5", diff --git a/tests/func/test_utils.py b/tests/func/test_utils.py index 594fdc332d..40c575e4f4 100644 --- a/tests/func/test_utils.py +++ b/tests/func/test_utils.py @@ -88,3 +88,13 @@ def test_makedirs_permissions(tmpdir): assert stat.S_IMODE(os.stat(test_dir).st_mode) == dir_mode assert stat.S_IMODE(os.stat(intermediate_dir).st_mode) == dir_mode + + +def test_is_exec_found_returns_true_when_program_exists(): + result = utils.is_exec_found("python") + assert result == True + + +def test_is_exec_found_returns_false_when_program_is_missing(): + result = utils.is_exec_found("some-missing-program") + assert result == False