├── .gitignore ├── docs ├── demo.gif └── demo.cast ├── bkp ├── __main__.py ├── config.py ├── resources.py ├── exceptions.py ├── timestamp.py ├── fsop.py ├── terminal.py ├── action_restore.py ├── action_info.py ├── tarop.py ├── __init__.py └── action_backup.py ├── tests ├── test_info.py ├── test_restore.py └── test_backup.py ├── .github └── workflows │ └── tests.yml ├── pyproject.toml ├── README.md └── poetry.lock /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | venv/ 5 | *.egg-info/ -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gergelyk/bkp/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /bkp/__main__.py: -------------------------------------------------------------------------------- 1 | from bkp import main 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /bkp/config.py: -------------------------------------------------------------------------------- 1 | class Config: 2 | index_from = 1 3 | suffix_format = '.b{i:02}' 4 | suffix_regexp = '\.b(\d+)$' 5 | time_format = '{t.Y}-{t.m}-{t.d} {t.H}:{t.M}:{t.S} {t.Z_}' 6 | 7 | cfg = Config() 8 | -------------------------------------------------------------------------------- /bkp/resources.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | from pathlib import Path 4 | APP_VERSION = importlib.metadata.version('bkp') 5 | BACKUP_DATA_NAME = 'DATA' 6 | BACKUP_INFO_NAME = 'INFO' 7 | BACKUP_META_NAME = 'META' 8 | -------------------------------------------------------------------------------- /bkp/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class ExpectedError(RuntimeError): 3 | exit_code = 255 4 | 5 | class InvalidInput(ExpectedError): 6 | exit_code = 2 7 | 8 | class InvalidFile(ExpectedError): 9 | exit_code = 3 10 | 11 | class AccessDenied(ExpectedError): 12 | exit_code = 4 13 | -------------------------------------------------------------------------------- /bkp/timestamp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime as dt 2 | 3 | class Timestamp: 4 | def __init__(self, timestamp): 5 | self._ts = timestamp 6 | 7 | @property 8 | def Z_(self): 9 | tz = self._ts.astimezone() 10 | return tz.tzname() 11 | 12 | def __getattr__(self, key): 13 | return self._ts.strftime('%{}'.format(key)) 14 | 15 | timestamp = Timestamp(dt.now()) 16 | -------------------------------------------------------------------------------- /tests/test_info.py: -------------------------------------------------------------------------------- 1 | from plumbum import local as sh 2 | 3 | 4 | def test_read_message(): 5 | with sh.tempdir() as tmp: 6 | with sh.cwd(tmp): 7 | (sh['echo']['abc'] >> 'myfile.txt')() 8 | 9 | sh['bkp']('-am', 'Foo', 'myfile.txt') 10 | sh['bkp']('-am', 'Bar', 'myfile.txt') 11 | sh['bkp']('-am', 'Baz', 'myfile.txt') 12 | 13 | (sh['bkp']['-i', 'myfile.txt.b01'] | sh['grep']['Foo'] )() 14 | (sh['bkp']['-i', 'myfile.txt.b02'] | sh['grep']['Bar'] )() 15 | (sh['bkp']['-i', 'myfile.txt.b03'] | sh['grep']['Baz'] )() 16 | -------------------------------------------------------------------------------- /bkp/fsop.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | 5 | from bkp.resources import BACKUP_DATA_NAME, BACKUP_INFO_NAME, BACKUP_META_NAME 6 | from bkp.config import cfg 7 | from bkp.timestamp import timestamp 8 | 9 | 10 | def is_backup(path): 11 | return bool(re.match(cfg.suffix_regexp, path.suffix)) 12 | 13 | 14 | def copy(src, dst): 15 | if src.is_dir(): 16 | shutil.copytree(src, dst) 17 | else: 18 | shutil.copyfile(src, dst) 19 | 20 | 21 | def move(src, dst): 22 | shutil.move(src, dst) 23 | 24 | 25 | def remove(path): 26 | if path.is_dir(): 27 | shutil.rmtree(path) 28 | else: 29 | os.remove(path) 30 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 10 | poetry-version: ["1.8.3"] 11 | os: [ubuntu-22.04, ubuntu-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install poetry 20 | uses: abatilo/actions-poetry@v2 21 | with: 22 | poetry-version: ${{ matrix.poetry-version }} 23 | - name: Install dependencies 24 | run: poetry install 25 | - name: Run tests 26 | run: poetry run pytest 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "bkp" 3 | version = "1.0.0" 4 | description = "Creates backups of your files and directories." 5 | authors = ["Grzegorz Krason "] 6 | homepage = "https://github.com/gergelyk/bkp" 7 | repository = "https://github.com/gergelyk/bkp" 8 | license = "MIT" 9 | readme = "README.md" 10 | keywords = ["backup"] 11 | classifiers = [ 12 | "Programming Language :: Python", 13 | "Topic :: Desktop Environment", 14 | "Topic :: Office/Business", 15 | "Topic :: System", 16 | "Topic :: Utilities", 17 | "Operating System :: POSIX", 18 | "Operating System :: Unix" 19 | ] 20 | 21 | [tool.poetry.dependencies] 22 | python = "^3.8" 23 | click = "^8.1.7" 24 | 25 | 26 | [tool.poetry.group.dev.dependencies] 27 | plumbum = "^1.9.0" 28 | pytest = "^8.3.4" 29 | 30 | [build-system] 31 | requires = ["poetry-core"] 32 | build-backend = "poetry.core.masonry.api" 33 | 34 | [tool.poetry.scripts] 35 | bkp = 'bkp:main' 36 | -------------------------------------------------------------------------------- /bkp/terminal.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | import click 3 | 4 | FILE_INDENT = 2 5 | 6 | def _indent(text, cols): 7 | return textwrap.indent(text, prefix=' ' * cols) 8 | 9 | def _preproc(text, indent): 10 | text = str(text) 11 | text = _indent(text, indent) 12 | return text 13 | 14 | def _echo(text, indent, fg, **kwargs): 15 | text = _preproc(text, indent) 16 | click.secho(text, fg=fg, **kwargs) 17 | 18 | def echo_path(text, indent=0, highlight=False, **kwargs): 19 | fg='green' 20 | _echo(text, indent, fg, **kwargs) 21 | 22 | def echo_inf(text, indent=FILE_INDENT, highlight=False, **kwargs): 23 | fg = [None, 'yellow'][highlight] 24 | _echo(text, indent, fg, **kwargs) 25 | 26 | def echo_wrn(text, indent=FILE_INDENT, highlight=False, **kwargs): 27 | fg='magenta' 28 | _echo(text, indent, fg, **kwargs) 29 | 30 | def echo_err(text, indent=FILE_INDENT, highlight=False, **kwargs): 31 | fg='red' 32 | _echo(text, indent, fg, **kwargs) 33 | 34 | def confirm(text, indent=FILE_INDENT, highlight=False, **kwargs): 35 | return click.confirm(_preproc(text, indent), **kwargs) 36 | -------------------------------------------------------------------------------- /bkp/action_restore.py: -------------------------------------------------------------------------------- 1 | import tarfile 2 | from click import confirm 3 | from bkp.terminal import echo_path, echo_wrn, echo_inf 4 | 5 | import bkp.fsop as fsop 6 | import bkp.tarop as tarop 7 | from bkp.exceptions import AccessDenied 8 | from bkp.resources import BACKUP_DATA_NAME 9 | 10 | 11 | def restore(path, delete, yes): 12 | 13 | echo_path(path) 14 | 15 | try: 16 | 17 | if not fsop.is_backup(path): 18 | echo_wrn('Not a backup') 19 | return 20 | 21 | is_archive = tarop.is_archive(path) 22 | 23 | except AccessDenied as exc: 24 | echo_wrn(exc) 25 | 26 | dst = path.with_suffix('') 27 | 28 | if dst.exists(): 29 | if yes or confirm(f'{dst.name!r} already exists, overwrite?'): 30 | fsop.remove(dst) 31 | echo_inf(f'Deleted: {dst}') 32 | else: 33 | echo_inf('Skipped') 34 | return 35 | 36 | src = path 37 | if is_archive: 38 | tarop.extract_member(src, dst, BACKUP_DATA_NAME) 39 | echo_inf(f'Restored: {dst}') 40 | if delete: 41 | fsop.remove(src) 42 | echo_inf(f'Deleted: {src}') 43 | else: 44 | if delete: 45 | fsop.move(src, dst) 46 | echo_inf(f'Restored: {dst}') 47 | echo_inf(f'Deleted: {src}') 48 | else: 49 | fsop.copy(src, dst) 50 | echo_inf(f'Restored: {dst}') 51 | -------------------------------------------------------------------------------- /bkp/action_info.py: -------------------------------------------------------------------------------- 1 | import tarfile 2 | from click import secho 3 | 4 | import bkp.fsop as fsop 5 | import bkp.tarop as tarop 6 | from bkp.terminal import echo_path, echo_wrn, echo_inf 7 | from bkp.resources import BACKUP_INFO_NAME, BACKUP_META_NAME 8 | from bkp.exceptions import InvalidFile, AccessDenied 9 | 10 | 11 | def info(path): 12 | echo_path(path) 13 | 14 | try: 15 | if not fsop.is_backup(path): 16 | echo_wrn('Not a backup') 17 | return 18 | 19 | if not tarop.is_archive(path): 20 | echo_wrn('Not an archive') 21 | return 22 | 23 | info_str = tarop.extract_text(path, BACKUP_INFO_NAME) 24 | info_dict = eval(info_str) 25 | max_key_len = max(map(len, info_dict.keys())) 26 | highlighted = ['message'] 27 | sep = " : " 28 | 29 | def echo_val(text, highlighted): 30 | lines = text.splitlines() 31 | first_line = ''.join(lines[:1]) 32 | other_lines = '\n'.join(lines[1:]) 33 | echo_inf(first_line, indent=0, highlight=highlighted) 34 | if other_lines: 35 | echo_inf(other_lines, indent=max_key_len + len(sep), highlight=highlighted) 36 | 37 | for key, val in info_dict.items(): 38 | echo_inf(f"{key:<{max_key_len}}" + sep, nl=False) 39 | echo_val(val, key in highlighted) 40 | 41 | except AccessDenied as exc: 42 | echo_wrn(exc) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bkp 2 | [![Github - Tests](https://github.com/gergelyk/bkp/actions/workflows/tests.yml/badge.svg)](https://github.com/gergelyk/bkp/actions) 3 | [![PyPI - Version](https://img.shields.io/pypi/v/bkp)](https://pypi.org/project/bkp/) 4 | [![PyPI - License](https://img.shields.io/pypi/l/bkp)](https://opensource.org/license/mit) 5 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/bkp)](https://pypistats.org/packages/bkp) 6 | 7 | 8 | Simple utility that makes backups of your files/directories. 9 | 10 | ![](docs/demo.gif) 11 | 12 | ## Features 13 | 14 | * Work with separate files or entire directories. 15 | * Creates simple copy or TAR archive. 16 | * Optionally append metadata: author, creation time, commit message (applies only to TAR archives). 17 | 18 | ## Installation 19 | 20 | ```python 21 | pip install bkp 22 | ``` 23 | 24 | ## Compatibility 25 | 26 | * This software is expected to work with Python 3.6, 3.7 and compatible. 27 | * It has never been tested under operating systems other than Linux. 28 | * For editing messages interactively (``-M`` switch) you need either have `vi` installed, or set ``EDITOR`` system variable to relevant value. 29 | 30 | ## Usage 31 | 32 | ### Creating Backups & Restoring 33 | 34 | Provided that we have a file or directory: `foo/bar/baz` 35 | 36 | ```sh 37 | # Create a copy: 38 | bkp foo/bar/baz 39 | 40 | # Following copy will be created: foo/bar/baz.b01 41 | # Invoking command again will create foo/bar/baz.b02 etc. 42 | 43 | # Restore your file or directory: 44 | bkp -r foo/bar/baz.01 45 | 46 | # This will create/overwrite original file/directory: foo/bar/baz 47 | ``` 48 | 49 | Note that multiple files can be specified in the command line. Output files are always created in the same directory where corresponding input files are located, no matter what CWD at the time. 50 | 51 | ### Working With Archives 52 | 53 | Alternatively `-a` flag can be used to create tar archive instead of a simple copy. Also `-m` can be used to add comments. 54 | 55 | ``` 56 | # Create an archive 57 | bkp -am "initial version" foo/bar/baz 58 | 59 | # Comment and other details can be obtained by invoking: 60 | bkp -i foo/bar/baz.b03 61 | ``` 62 | 63 | For more options and explanations invoke `bkp --help`. 64 | 65 | ## Disclaimer 66 | 67 | Author doesn't take any responsibility for loss or damage caused by this utility. You are using it on your own risk. 68 | -------------------------------------------------------------------------------- /tests/test_restore.py: -------------------------------------------------------------------------------- 1 | from plumbum import local as sh 2 | 3 | 4 | def test_restore_file_from_backup(): 5 | with sh.tempdir() as tmp: 6 | with sh.cwd(tmp): 7 | (sh['echo']['abc'] > 'myfile.txt')() 8 | sh['bkp']('myfile.txt') 9 | 10 | (sh['echo']['def'] > 'myfile.txt')() 11 | sh['bkp']('myfile.txt') 12 | 13 | sh['bkp']('-ry', 'myfile.txt.b01') 14 | sh['grep']('abc', 'myfile.txt') 15 | 16 | sh['bkp']('-ry', 'myfile.txt.b02') 17 | sh['grep']('def', 'myfile.txt') 18 | 19 | def test_restore_dir_from_backup(): 20 | with sh.tempdir() as tmp: 21 | with sh.cwd(tmp): 22 | sh['mkdir']('mydir') 23 | sh['touch']('mydir/abc') 24 | sh['touch']('mydir/def') 25 | sh['touch']('mydir/ghi') 26 | 27 | sh['bkp']('mydir') 28 | 29 | (sh['ls']['-1', 'mydir'] | sh['grep']['abc'] )() 30 | (sh['ls']['-1', 'mydir'] | sh['grep']['def'] )() 31 | (sh['ls']['-1', 'mydir'] | sh['grep']['ghi'] )() 32 | 33 | sh['rm']('mydir/abc') 34 | sh['rm']('mydir/def') 35 | sh['rm']('mydir/ghi') 36 | 37 | sh['bkp']('-ry', 'mydir.b01') 38 | 39 | (sh['ls']['-1', 'mydir'] | sh['grep']['abc'] )() 40 | (sh['ls']['-1', 'mydir'] | sh['grep']['def'] )() 41 | (sh['ls']['-1', 'mydir'] | sh['grep']['ghi'] )() 42 | 43 | def test_restore_file_from_archive(): 44 | with sh.tempdir() as tmp: 45 | with sh.cwd(tmp): 46 | (sh['echo']['abc'] > 'myfile.txt')() 47 | sh['bkp']('-a', 'myfile.txt') 48 | 49 | (sh['echo']['def'] > 'myfile.txt')() 50 | sh['bkp']('-a', 'myfile.txt') 51 | 52 | sh['bkp']('-ry', 'myfile.txt.b01') 53 | sh['grep']('abc', 'myfile.txt') 54 | 55 | sh['bkp']('-ry', 'myfile.txt.b02') 56 | sh['grep']('def', 'myfile.txt') 57 | 58 | def test_restore_dir_from_archive(): 59 | with sh.tempdir() as tmp: 60 | with sh.cwd(tmp): 61 | sh['mkdir']('mydir') 62 | sh['touch']('mydir/abc') 63 | sh['touch']('mydir/def') 64 | sh['touch']('mydir/ghi') 65 | 66 | sh['bkp']('-a', 'mydir') 67 | 68 | (sh['ls']['-1', 'mydir'] | sh['grep']['abc'] )() 69 | (sh['ls']['-1', 'mydir'] | sh['grep']['def'] )() 70 | (sh['ls']['-1', 'mydir'] | sh['grep']['ghi'] )() 71 | 72 | sh['rm']('mydir/abc') 73 | sh['rm']('mydir/def') 74 | sh['rm']('mydir/ghi') 75 | 76 | sh['bkp']('-ry', 'mydir.b01') 77 | 78 | (sh['ls']['-1', 'mydir'] | sh['grep']['abc'] )() 79 | (sh['ls']['-1', 'mydir'] | sh['grep']['def'] )() 80 | (sh['ls']['-1', 'mydir'] | sh['grep']['ghi'] )() 81 | -------------------------------------------------------------------------------- /bkp/tarop.py: -------------------------------------------------------------------------------- 1 | import getpass 2 | import tarfile 3 | from pathlib import Path 4 | from io import BytesIO 5 | from bkp.config import cfg 6 | from bkp.timestamp import timestamp 7 | from bkp.exceptions import InvalidFile, AccessDenied 8 | from bkp.resources import BACKUP_META_NAME, BACKUP_DATA_NAME, BACKUP_INFO_NAME 9 | 10 | def _process_tar(handler, path, mode, *args, **kwargs): 11 | try: 12 | tar_ctx = tarfile.open(name=path, mode=mode) 13 | except Exception as exc: 14 | raise InvalidFile('File format not supported') 15 | else: 16 | with tar_ctx as tar: 17 | return handler(tar, *args, **kwargs) 18 | 19 | def archive(src, dst, message): 20 | 21 | def handler(tar, src, message): 22 | 23 | def get_info_str(message): 24 | user = getpass.getuser() 25 | info = dict(author=user, 26 | time=cfg.time_format.format(t=timestamp), 27 | message=message) 28 | return repr(info) 29 | 30 | def get_meta_str(): 31 | meta = dict(file_type="backup", file_version="1.0.0") 32 | return repr(meta) 33 | 34 | def add_text(tar, text, name): 35 | buf = BytesIO(text.encode()) 36 | tar_info = tarfile.TarInfo(name) 37 | tar_info.size = len(buf.getvalue()) 38 | tar.addfile(tar_info, fileobj=buf) 39 | 40 | tar.add(src, arcname=BACKUP_DATA_NAME) 41 | info_str = get_info_str(message) 42 | meta_str = get_meta_str() 43 | add_text(tar, info_str, BACKUP_INFO_NAME) 44 | add_text(tar, meta_str, BACKUP_META_NAME) 45 | 46 | return _process_tar(handler, dst, 'w:', src, message) 47 | 48 | 49 | def extract_text(path, name): 50 | 51 | def handler(tar, name): 52 | try: 53 | buf = tar.extractfile(name) 54 | except KeyError: 55 | raise InvalidFile('File format not supported') 56 | else: 57 | return buf.read().decode() 58 | 59 | return _process_tar(handler, path, 'r:', name) 60 | 61 | 62 | def extract_member(path, name, name_arc): 63 | 64 | def handler(tar, name, name_arc): 65 | for m in tar.getmembers(): 66 | m_name_arc = Path(m.name) 67 | if m_name_arc.parts[0] == name_arc: 68 | m.name = str(Path(name) / Path(*m_name_arc.parts[1:])) 69 | tar.extract(m) 70 | 71 | return _process_tar(handler, path, 'r:', name, name_arc) 72 | 73 | def is_archive(path): 74 | 75 | try: 76 | if not path.is_file(): 77 | return False 78 | except PermissionError: 79 | # this should be handled by click package anyway 80 | raise AccessDenied("File not available") 81 | 82 | try: 83 | meta_str = extract_text(path, BACKUP_META_NAME) 84 | except InvalidFile: 85 | return False 86 | 87 | try: 88 | meta_dict = eval(meta_str) 89 | if meta_dict['file_type'] != "backup": 90 | return False 91 | if meta_dict['file_version'] != "1.0.0": 92 | return False 93 | except Exception: 94 | return False 95 | 96 | return True 97 | -------------------------------------------------------------------------------- /bkp/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import click 3 | import inspect 4 | from pathlib import Path 5 | from types import SimpleNamespace 6 | 7 | from bkp.action_backup import backup 8 | from bkp.action_restore import restore 9 | from bkp.action_info import info 10 | from bkp.exceptions import ExpectedError, InvalidInput 11 | from bkp.terminal import echo_err, echo_inf 12 | from bkp.resources import APP_VERSION 13 | 14 | def check_invalid_options(command, params, kwargs): 15 | cmd_name = command.__name__ 16 | cmd_args = inspect.getfullargspec(command).args 17 | 18 | all_options = filter(lambda p: isinstance(p, click.core.Option), params) 19 | provided_options = list(p.name for p in all_options if p.default != kwargs.get(p.name, p.default)) 20 | if cmd_name in provided_options: 21 | provided_options.remove(cmd_name) 22 | 23 | for opt in provided_options: 24 | if opt not in cmd_args: 25 | raise InvalidInput(f"Option {opt!r} cannot be combined with {cmd_name!r} action") 26 | 27 | 28 | def ensure_at_least_one_item(ctx, param, value): 29 | if not value: 30 | raise click.BadParameter("At least one path needs to be provided.") 31 | return value 32 | 33 | 34 | def print_version(ctx, param, value): 35 | if not value or ctx.resilient_parsing: 36 | return 37 | click.echo(APP_VERSION) 38 | ctx.exit() 39 | 40 | def app(**kwargs): 41 | kw = SimpleNamespace(**kwargs) 42 | 43 | if kw.restore: 44 | check_invalid_options(restore, main.params, kwargs) 45 | action = lambda path: restore(path, kw.delete, kw.yes) 46 | elif kw.info: 47 | check_invalid_options(info, main.params, kwargs) 48 | action = lambda path: info(path) 49 | else: 50 | # backup 51 | check_invalid_options(backup, main.params, kwargs) 52 | action = lambda path: backup(path, kw.delete, kw.yes, kw.archive, kw.message, kw.message_edit) 53 | 54 | last_idx = len(kw.paths) - 1 55 | for idx, path in enumerate(kw.paths): 56 | action(Path(path)) 57 | if idx != last_idx: 58 | echo_inf('') 59 | 60 | 61 | @click.command() 62 | @click.argument('paths', nargs=-1, type=click.Path(exists=True), callback=ensure_at_least_one_item) 63 | @click.option('-r', '--restore', default=False, is_flag=True, help="Restore resources from backup(s).") 64 | @click.option('-d', '--delete', default=False, is_flag=True, help="Delete source file/directory.") 65 | @click.option('-a', '--archive', default=False, is_flag=True, help="Create an archive.") 66 | @click.option('-y', '--yes', default=False, is_flag=True, help="Answer 'yes' to all the questions.") 67 | @click.option('-m', '--message', help="Message to be included.") 68 | @click.option('-M', '--message-edit', default=False, is_flag=True, help="The same as '--message' but opens text editor.") 69 | @click.option('-i', '--info', default=False, is_flag=True, help="Read metadata.") 70 | @click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True, help="Print version.") 71 | def main(**kwargs): 72 | """Create/restore backups of your files/directories.""" 73 | try: 74 | app(**kwargs) 75 | except ExpectedError as exc: 76 | echo_err(exc) 77 | exit(exc.exit_code) 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /bkp/action_backup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import tempfile 4 | from pathlib import Path 5 | from functools import partial, lru_cache 6 | 7 | import bkp.fsop as fsop 8 | import bkp.tarop as tarop 9 | from bkp.config import cfg 10 | from bkp.exceptions import InvalidInput 11 | from bkp.timestamp import timestamp 12 | from bkp.terminal import echo_path, echo_inf, confirm 13 | 14 | 15 | @lru_cache() 16 | def message_from_editor(initial): 17 | initial = initial or "" 18 | with tempfile.NamedTemporaryFile(prefix='message-') as fp: 19 | fp.write(initial.encode() + b'\n\n# Write your message above.') 20 | fp.flush() 21 | editor = os.getenv('EDITOR', 'vi') 22 | os.system(f'{editor} {fp.name}') 23 | fp.seek(0) 24 | text = fp.read().decode() 25 | lines = text.splitlines() 26 | content = filter(lambda line: not line.strip().startswith('#'), lines) 27 | return '\n'.join(content).strip() 28 | 29 | 30 | def generate_index(path): 31 | items_all = path.parent.iterdir() 32 | backups = filter(lambda p: p.with_suffix('').name == path.name, items_all) 33 | suffixes = map(lambda p: p.suffix, backups) 34 | suffix_re = re.compile(cfg.suffix_regexp) 35 | matches = filter(None, map(suffix_re.match, suffixes)) 36 | indices = map(lambda m: int(m.groups()[0]), matches) 37 | 38 | try: 39 | return max(indices) + 1 40 | except ValueError: 41 | return cfg.index_from 42 | 43 | 44 | def backup_copy(src, dst): 45 | fsop.copy(src, dst) 46 | echo_inf(f"Created: {dst}") 47 | 48 | 49 | def backup_move(src, dst): 50 | fsop.move(src, dst) 51 | echo_inf(f'Created: {dst}') 52 | echo_inf(f'Deleted: {src}') 53 | 54 | 55 | def backup_tar(src, dst, message): 56 | tarop.archive(src, dst, message) 57 | echo_inf(f"Created: {dst}") 58 | 59 | 60 | def backup_tar_rm(src, dst, message): 61 | backup_tar(src, dst, message) 62 | fsop.remove(src) 63 | echo_inf(f'Deleted: {src}') 64 | 65 | 66 | def backup(path, delete, yes, archive, message, message_edit): 67 | 68 | if message_edit: 69 | if not archive: 70 | raise InvalidInput(f"Option '--message-edit' is allowed only in conjunction with '--archive'") 71 | message = message_from_editor(message) 72 | elif message is not None: 73 | if not archive: 74 | raise InvalidInput(f"Option '--message' is allowed only in conjunction with '--archive'") 75 | 76 | echo_path(path) 77 | 78 | if not yes and fsop.is_backup(path): 79 | ans = confirm(f'Are you sure you would like to backup another backup?') 80 | if not ans: 81 | echo_inf('Skipping') 82 | return 83 | 84 | message = message or "" 85 | 86 | backup_tar_p = partial(backup_tar, message=message) 87 | backup_tar_rm_p = partial(backup_tar_rm, message=message) 88 | 89 | backup_funcs = ((backup_copy, backup_move), (backup_tar_p, backup_tar_rm_p)) 90 | backup_func = backup_funcs[archive][delete] 91 | 92 | index = generate_index(path) 93 | env = dict(i=index, t=timestamp) 94 | suffix = cfg.suffix_format.format(**env) 95 | 96 | src = path 97 | dst = Path(str(path) + suffix) 98 | 99 | backup_func(src, dst) 100 | -------------------------------------------------------------------------------- /tests/test_backup.py: -------------------------------------------------------------------------------- 1 | from plumbum import local as sh 2 | 3 | 4 | def test_backup_file(): 5 | with sh.tempdir() as tmp: 6 | with sh.cwd(tmp): 7 | (sh['echo']['abc'] >> 'myfile.txt')() 8 | 9 | dir_content = sh['ls']('-1').splitlines() 10 | assert dir_content == ['myfile.txt'] 11 | 12 | sh['bkp']('myfile.txt') 13 | sh['bkp']('myfile.txt') 14 | sh['bkp']('myfile.txt') 15 | 16 | (sh['echo']['cba'] >> 'myfile.txt')() 17 | sh['bkp']('myfile.txt') 18 | 19 | sh['rm']('myfile.txt.b02') 20 | sh['bkp']('myfile.txt') 21 | 22 | dir_content = sh['ls']('-1').splitlines() 23 | assert set(dir_content) == {'myfile.txt', 24 | 'myfile.txt.b01', 25 | 'myfile.txt.b03', 26 | 'myfile.txt.b04', 27 | 'myfile.txt.b05'} 28 | 29 | sh['diff']('myfile.txt.b01', 'myfile.txt.b03') 30 | sh['diff']('myfile.txt', 'myfile.txt.b04') 31 | sh['diff']('myfile.txt', 'myfile.txt.b05') 32 | 33 | 34 | def test_backup_multiple_files(): 35 | with sh.tempdir() as tmp: 36 | with sh.cwd(tmp): 37 | (sh['echo']['abc'] >> 'myfile1.txt')() 38 | (sh['echo']['abc'] >> 'myfile2.txt')() 39 | 40 | dir_content = sh['ls']('-1').splitlines() 41 | assert dir_content == ['myfile1.txt', 42 | 'myfile2.txt'] 43 | 44 | sh['bkp']('myfile1.txt', 'myfile2.txt') 45 | 46 | dir_content = sh['ls']('-1').splitlines() 47 | assert set(dir_content) == {'myfile1.txt', 48 | 'myfile2.txt', 49 | 'myfile1.txt.b01', 50 | 'myfile2.txt.b01'} 51 | 52 | def test_backup_dir(): 53 | with sh.tempdir() as tmp: 54 | with sh.cwd(tmp): 55 | sh['mkdir']('mydir') 56 | sh['touch']('mydir/abc') 57 | sh['touch']('mydir/def') 58 | sh['touch']('mydir/ghi') 59 | 60 | dir_content = sh['ls']('-1').splitlines() 61 | assert set(dir_content) == {'mydir'} 62 | 63 | dir_content = sh['ls']('-1', 'mydir').splitlines() 64 | assert set(dir_content) == {'abc', 'def', 'ghi'} 65 | 66 | sh['bkp']('mydir') 67 | 68 | dir_content = sh['ls']('-1').splitlines() 69 | assert set(dir_content) == {'mydir', 'mydir.b01'} 70 | 71 | dir_content = sh['ls']('-1', 'mydir.b01').splitlines() 72 | assert set(dir_content) == {'abc', 'def', 'ghi'} 73 | 74 | 75 | def test_archive_file(): 76 | with sh.tempdir() as tmp: 77 | with sh.cwd(tmp): 78 | (sh['echo']['abc'] >> 'myfile.txt')() 79 | sh['bkp']('-a', 'myfile.txt') 80 | tar_content = sh['tar']('-t', '-f', 'myfile.txt.b01').splitlines() 81 | assert set(tar_content) == {'DATA', 'INFO', 'META'} 82 | sh['tar']('xf', 'myfile.txt.b01') 83 | assert sh['cat']('DATA') == 'abc\n' 84 | assert eval(sh['cat']('META')) == {'file_type': 'backup', 'file_version': '1.0.0'} 85 | 86 | 87 | def test_archive_dir(): 88 | with sh.tempdir() as tmp: 89 | with sh.cwd(tmp): 90 | sh['mkdir']('mydir') 91 | sh['touch']('mydir/abc') 92 | sh['touch']('mydir/def') 93 | sh['touch']('mydir/ghi') 94 | sh['bkp']('-a', 'mydir') 95 | tar_content = sh['tar']('-t', '-f', 'mydir.b01').splitlines() 96 | assert set(tar_content) == {'DATA/', 'INFO', 'META', 97 | 'DATA/abc', 'DATA/def', 'DATA/ghi'} 98 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "click" 5 | version = "8.1.7" 6 | description = "Composable command line interface toolkit" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 11 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 12 | ] 13 | 14 | [package.dependencies] 15 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 16 | 17 | [[package]] 18 | name = "colorama" 19 | version = "0.4.6" 20 | description = "Cross-platform colored terminal text." 21 | optional = false 22 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 23 | files = [ 24 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 25 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 26 | ] 27 | 28 | [[package]] 29 | name = "exceptiongroup" 30 | version = "1.2.2" 31 | description = "Backport of PEP 654 (exception groups)" 32 | optional = false 33 | python-versions = ">=3.7" 34 | files = [ 35 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 36 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 37 | ] 38 | 39 | [package.extras] 40 | test = ["pytest (>=6)"] 41 | 42 | [[package]] 43 | name = "importlib-resources" 44 | version = "6.4.5" 45 | description = "Read resources from Python packages" 46 | optional = false 47 | python-versions = ">=3.8" 48 | files = [ 49 | {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, 50 | {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, 51 | ] 52 | 53 | [package.dependencies] 54 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 55 | 56 | [package.extras] 57 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 58 | cover = ["pytest-cov"] 59 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 60 | enabler = ["pytest-enabler (>=2.2)"] 61 | test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] 62 | type = ["pytest-mypy"] 63 | 64 | [[package]] 65 | name = "iniconfig" 66 | version = "2.0.0" 67 | description = "brain-dead simple config-ini parsing" 68 | optional = false 69 | python-versions = ">=3.7" 70 | files = [ 71 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 72 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 73 | ] 74 | 75 | [[package]] 76 | name = "packaging" 77 | version = "24.2" 78 | description = "Core utilities for Python packages" 79 | optional = false 80 | python-versions = ">=3.8" 81 | files = [ 82 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 83 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 84 | ] 85 | 86 | [[package]] 87 | name = "pluggy" 88 | version = "1.5.0" 89 | description = "plugin and hook calling mechanisms for python" 90 | optional = false 91 | python-versions = ">=3.8" 92 | files = [ 93 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 94 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 95 | ] 96 | 97 | [package.extras] 98 | dev = ["pre-commit", "tox"] 99 | testing = ["pytest", "pytest-benchmark"] 100 | 101 | [[package]] 102 | name = "plumbum" 103 | version = "1.9.0" 104 | description = "Plumbum: shell combinators library" 105 | optional = false 106 | python-versions = ">=3.8" 107 | files = [ 108 | {file = "plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5"}, 109 | {file = "plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219"}, 110 | ] 111 | 112 | [package.dependencies] 113 | importlib-resources = {version = "*", markers = "python_version < \"3.9\""} 114 | pywin32 = {version = "*", markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""} 115 | 116 | [package.extras] 117 | dev = ["coverage[toml]", "paramiko", "psutil", "pytest (>=6.0)", "pytest-cov", "pytest-mock", "pytest-timeout"] 118 | docs = ["sphinx (>=4.0.0)", "sphinx-rtd-theme (>=1.0.0)"] 119 | ssh = ["paramiko"] 120 | test = ["coverage[toml]", "paramiko", "psutil", "pytest (>=6.0)", "pytest-cov", "pytest-mock", "pytest-timeout"] 121 | 122 | [[package]] 123 | name = "pytest" 124 | version = "8.3.4" 125 | description = "pytest: simple powerful testing with Python" 126 | optional = false 127 | python-versions = ">=3.8" 128 | files = [ 129 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, 130 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, 131 | ] 132 | 133 | [package.dependencies] 134 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 135 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 136 | iniconfig = "*" 137 | packaging = "*" 138 | pluggy = ">=1.5,<2" 139 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 140 | 141 | [package.extras] 142 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 143 | 144 | [[package]] 145 | name = "pywin32" 146 | version = "308" 147 | description = "Python for Window Extensions" 148 | optional = false 149 | python-versions = "*" 150 | files = [ 151 | {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, 152 | {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, 153 | {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, 154 | {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, 155 | {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, 156 | {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, 157 | {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, 158 | {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, 159 | {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, 160 | {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, 161 | {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, 162 | {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, 163 | {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, 164 | {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, 165 | {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, 166 | {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, 167 | {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, 168 | {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, 169 | ] 170 | 171 | [[package]] 172 | name = "tomli" 173 | version = "2.2.1" 174 | description = "A lil' TOML parser" 175 | optional = false 176 | python-versions = ">=3.8" 177 | files = [ 178 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 179 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 180 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 181 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 182 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 183 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 184 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 185 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 186 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 187 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 188 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 189 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 190 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 191 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 192 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 193 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 194 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 195 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 196 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 197 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 198 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 199 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 200 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 201 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 202 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 203 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 204 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 205 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 206 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 207 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 208 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 209 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 210 | ] 211 | 212 | [[package]] 213 | name = "zipp" 214 | version = "3.20.2" 215 | description = "Backport of pathlib-compatible object wrapper for zip files" 216 | optional = false 217 | python-versions = ">=3.8" 218 | files = [ 219 | {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, 220 | {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, 221 | ] 222 | 223 | [package.extras] 224 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 225 | cover = ["pytest-cov"] 226 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 227 | enabler = ["pytest-enabler (>=2.2)"] 228 | test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] 229 | type = ["pytest-mypy"] 230 | 231 | [metadata] 232 | lock-version = "2.0" 233 | python-versions = "^3.8" 234 | content-hash = "038cd71beca6b00ee391263f7d05b2bc3b291fd2563a027fc93eef4edd115ac1" 235 | -------------------------------------------------------------------------------- /docs/demo.cast: -------------------------------------------------------------------------------- 1 | {"version":2,"width":75,"height":19,"timestamp":1712852083,"env":{"TERM":"xterm-256color","SHELL":"/usr/bin/elvish"},"theme":{"fg":"#ebece6","bg":"#1e1f29","palette":"#000000:#fc4346:#50fb7c:#f0fb8c:#49baff:#fc4cb4:#8be9fe:#ededec:#555555:#fc4346:#50fb7c:#f0fb8c:#49baff:#fc4cb4:#8be9fe:#ededec"}} 2 | [0.043182, "o", "Deprecation: \u001b[31;1mthe \"eawk\" command is deprecated; use \"re:awk\" instead\u001b[m\r\n /home/gkrason/.local/share/elvish/lib/github.com/zzamboni/elvish-completions/git.elv:106:35-38: -run-git help -a --no-verbose | \u001b[1;4meawk\u001b[m {|line @f| if (re:match '^ [a-z]' $line) { put $@f } } | each {|c|\r\n"] 3 | [0.048725, "o", "Deprecation: \u001b[31;1mthe \"eawk\" command is deprecated; use \"re:awk\" instead\u001b[m\r\n /home/gkrason/.local/share/elvish/lib/github.com/muesli/elvish-libs/git.elv:56:35-38: var is-ok = ?($git-status-cmd | \u001b[1;4meawk\u001b[m {|line @f|\r\n"] 4 | [0.097982, "o", "\u001b[H\u001b[2J\u001b[3J"] 5 | [0.108711, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 6 | [0.108841, "o", "\u001b[?25l\r???> ???> \r\u001b[5C\u001b[?25h"] 7 | [0.108974, "o", "\u001b[?25l\r\u001b[5C\u001b[K\r\u001b[5C\u001b[?25h"] 8 | [0.110237, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] 9 | [0.111633, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] 10 | [0.113759, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] 11 | [0.113816, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] 12 | [0.11427, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] 13 | [0.118159, "o", "\u001b[?25l\r\u001b[K\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h"] 14 | [0.647715, "o", "\u001b[?25l\r\u001b[15C\u001b[0;31me\u001b[0;m\r\u001b[16C\u001b[?25h"] 15 | [0.647831, "o", "\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 16 | [0.657835, "o", "\u001b[?25l\r\r\u001b[16C\u001b[?25h\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 17 | [0.657932, "o", "\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 18 | [0.658158, "o", "\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 19 | [0.658281, "o", "\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 20 | [0.840007, "o", "\u001b[?25l\r\u001b[16C\u001b[0;31mc\u001b[0;m\r\u001b[17C\u001b[?25h"] 21 | [0.903164, "o", "\u001b[?25l\r\u001b[17C\u001b[0;31mh\u001b[0;m\r\u001b[18C\u001b[?25h\u001b[?25l\r\r\u001b[18C\u001b[?25h"] 22 | [0.96577, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mecho\u001b[0;m\r\u001b[19C\u001b[?25h\u001b[?25l\r\r\u001b[19C\u001b[?25h"] 23 | [1.08706, "o", "\u001b[?25l\r\u001b[19C \r\u001b[20C\u001b[?25h\u001b[?25l\r\r\u001b[20C\u001b[?25h"] 24 | [1.255094, "o", "\u001b[?25l\r\u001b[20Cv\r\u001b[21C\u001b[?25h\u001b[?25l\r\r\u001b[21C\u001b[?25h"] 25 | [1.335489, "o", "\u001b[?25l\r\u001b[21Ce\r\u001b[22C\u001b[?25h"] 26 | [1.335508, "o", "\u001b[?25l\r\r\u001b[22C\u001b[?25h"] 27 | [1.495807, "o", "\u001b[?25l\r\u001b[22Cr\r\u001b[23C\u001b[?25h\u001b[?25l\r\r\u001b[23C\u001b[?25h"] 28 | [1.631178, "o", "\u001b[?25l\r\u001b[23Cs\r\u001b[24C\u001b[?25h\u001b[?25l\r\r\u001b[24C\u001b[?25h"] 29 | [1.751671, "o", "\u001b[?25l\r\u001b[24Ci\r\u001b[25C\u001b[?25h\u001b[?25l\r\r\u001b[25C\u001b[?25h"] 30 | [1.767039, "o", "\u001b[?25l\r\u001b[25Co\r\u001b[26C\u001b[?25h\u001b[?25l\r\r\u001b[26C\u001b[?25h"] 31 | [1.847316, "o", "\u001b[?25l\r\u001b[26Cn\r\u001b[27C\u001b[?25h\u001b[?25l\r\r\u001b[27C\u001b[?25h"] 32 | [2.295509, "o", "\u001b[?25l\r\u001b[27C1\r\u001b[28C\u001b[?25h\u001b[?25l\r\r\u001b[28C\u001b[?25h"] 33 | [2.493742, "o", "\u001b[?25l\r\u001b[28C \r\u001b[29C\u001b[?25h\u001b[?25l\r\r\u001b[29C\u001b[?25h"] 34 | [3.23939, "o", "\u001b[?25l\r\u001b[29C\u001b[0;32m>\u001b[0;m\r\u001b[30C\u001b[?25h\u001b[?25l\r\r\u001b[30C\u001b[?25h"] 35 | [3.335796, "o", "\u001b[?25l\r\u001b[30C \r\u001b[31C\u001b[?25h\u001b[?25l\r\r\u001b[31C\u001b[?25h"] 36 | [3.64732, "o", "\u001b[?25l\r\u001b[31Cf\r\u001b[32C\u001b[?25h\u001b[?25l\r\r\u001b[32C\u001b[?25h"] 37 | [3.807084, "o", "\u001b[?25l\r\u001b[32Co\r\u001b[33C\u001b[?25h\u001b[?25l\r\r\u001b[33C\u001b[?25h"] 38 | [3.94306, "o", "\u001b[?25l\r\u001b[33Co\r\u001b[34C\u001b[?25h"] 39 | [3.943117, "o", "\u001b[?25l\r\r\u001b[34C\u001b[?25h"] 40 | [3.943159, "o", "\u001b[?25l\r\r\u001b[34C\u001b[?25h"] 41 | [4.183212, "o", "\u001b[?25l\r\u001b[34C.\r\u001b[35C\u001b[?25h\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 42 | [4.551518, "o", "\u001b[?25l\r\u001b[35Ct\r\u001b[36C\u001b[?25h\u001b[?25l\r\r\u001b[36C\u001b[?25h"] 43 | [4.647053, "o", "\u001b[?25l\r\u001b[36Cx\r\u001b[37C\u001b[?25h\u001b[?25l\r\r\u001b[37C\u001b[?25h"] 44 | [4.726951, "o", "\u001b[?25l\r\u001b[37Ct\r\u001b[38C\u001b[?25h\u001b[?25l\r\r\u001b[38C\u001b[?25h"] 45 | [6.183248, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 46 | [6.190911, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 47 | [6.191231, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 48 | [6.191596, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 49 | [6.200568, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 50 | [7.623316, "o", "\u001b[?25l\r\u001b[15C\u001b[0;32mb\u001b[0;m\r\u001b[16C\u001b[?25h\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 51 | [7.800629, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;31mbk\u001b[0;m\r\u001b[17C\u001b[?25h\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 52 | [7.802383, "o", "\u001b[?25l\r\r\u001b[17C\u001b[?25h\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 53 | [7.802421, "o", "\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 54 | [7.84684, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mbkp\u001b[0;m\r\u001b[18C\u001b[?25h\u001b[?25l\r\r\u001b[18C\u001b[?25h"] 55 | [7.950861, "o", "\u001b[?25l\r\u001b[18C \r\u001b[19C\u001b[?25h"] 56 | [7.950908, "o", "\u001b[?25l\r\r\u001b[19C\u001b[?25h"] 57 | [8.141375, "o", "\u001b[?25l\r\u001b[19Cf\r\u001b[20C\u001b[?25h\u001b[?25l\r\r\u001b[20C\u001b[?25h"] 58 | [8.263235, "o", "\u001b[?25l\r\u001b[20Co\r\u001b[21C\u001b[?25h\u001b[?25l\r\r\u001b[21C\u001b[?25h"] 59 | [8.366471, "o", "\u001b[?25l\r\u001b[21Co\r\u001b[22C\u001b[?25h\u001b[?25l\r\r\u001b[22C\u001b[?25h"] 60 | [8.496033, "o", "\u001b[?25l\r\u001b[22C.txt \r\u001b[27C\u001b[?25h"] 61 | [9.831433, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 62 | [9.911164, "o", "\u001b[32mfoo.txt\u001b[0m\r\n"] 63 | [9.911296, "o", " Created: foo.txt.b01\u001b[0m\r\n"] 64 | [9.918125, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 65 | [9.918202, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h"] 66 | [9.918204, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 67 | [9.918402, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 68 | [9.921787, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 69 | [11.088341, "o", "\u001b[?25l\r\u001b[15C\u001b[0;31ml\u001b[0;m\r\u001b[16C\u001b[?25h\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 70 | [11.247153, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mls\u001b[0;m\r\u001b[17C\u001b[?25h\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 71 | [11.390169, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 72 | [11.398051, "o", "foo.txt foo.txt.b01\r\n"] 73 | [11.39846, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 74 | [11.398855, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h"] 75 | [11.398865, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 76 | [11.399452, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 77 | [11.400058, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 78 | [11.400072, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 79 | [11.402809, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 80 | [11.408295, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 81 | [13.391788, "o", "\u001b[?25l\r\u001b[15C\u001b[0;31me\u001b[0;m\r\u001b[16C\u001b[?25h"] 82 | [13.590516, "o", "\u001b[?25l\r\u001b[16C\u001b[0;31mc\u001b[0;m\r\u001b[17C\u001b[?25h"] 83 | [13.590524, "o", "\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 84 | [13.639747, "o", "\u001b[?25l\r\u001b[17C\u001b[0;31mh\u001b[0;m\r\u001b[18C\u001b[?25h\u001b[?25l\r\r\u001b[18C\u001b[?25h"] 85 | [13.734995, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mecho\u001b[0;m\r\u001b[19C\u001b[?25h\u001b[?25l\r\r\u001b[19C\u001b[?25h"] 86 | [13.822956, "o", "\u001b[?25l\r\u001b[19C \r\u001b[20C\u001b[?25h\u001b[?25l\r\r\u001b[20C\u001b[?25h"] 87 | [14.023371, "o", "\u001b[?25l\r\u001b[20Cv\r\u001b[21C\u001b[?25h"] 88 | [14.02339, "o", "\u001b[?25l\r\r\u001b[21C\u001b[?25h"] 89 | [14.078749, "o", "\u001b[?25l\r\u001b[21Ce\r\u001b[22C\u001b[?25h\u001b[?25l\r\r\u001b[22C\u001b[?25h"] 90 | [14.207061, "o", "\u001b[?25l\r\u001b[22Cr\r\u001b[23C\u001b[?25h\u001b[?25l\r\r\u001b[23C\u001b[?25h"] 91 | [14.350793, "o", "\u001b[?25l\r\u001b[23Cs\r\u001b[24C\u001b[?25h\u001b[?25l\r\r\u001b[24C\u001b[?25h"] 92 | [14.487353, "o", "\u001b[?25l\r\u001b[24Co\r\u001b[25C\u001b[?25h\u001b[?25l\r\r\u001b[25C\u001b[?25h"] 93 | [14.487862, "o", "\u001b[?25l\r\u001b[25Ci\r\u001b[26C\u001b[?25h"] 94 | [14.550988, "o", "\u001b[?25l\r\u001b[26Cn\r\u001b[27C\u001b[?25h\u001b[?25l\r\r\u001b[27C\u001b[?25h"] 95 | [14.823315, "o", "\u001b[?25l\r\u001b[27C2\r\u001b[28C\u001b[?25h\u001b[?25l\r\r\u001b[28C\u001b[?25h"] 96 | [15.015153, "o", "\u001b[?25l\r\u001b[28C \r\u001b[29C\u001b[?25h"] 97 | [15.431559, "o", "\u001b[?25l\r\u001b[29C\u001b[0;32m>\u001b[0;m\r\u001b[30C\u001b[?25h\u001b[?25l\r\r\u001b[30C\u001b[?25h"] 98 | [15.527364, "o", "\u001b[?25l\r\u001b[30C \r\u001b[31C\u001b[?25h"] 99 | [15.527383, "o", "\u001b[?25l\r\r\u001b[31C\u001b[?25h"] 100 | [15.79043, "o", "\u001b[?25l\r\u001b[31Cf\r\u001b[32C\u001b[?25h\u001b[?25l\r\r\u001b[32C\u001b[?25h"] 101 | [15.919062, "o", "\u001b[?25l\r\u001b[32Co\r\u001b[33C\u001b[?25h"] 102 | [16.031307, "o", "\u001b[?25l\r\u001b[33Co.txt\r\u001b[38C\u001b[?25h"] 103 | [16.910348, "o", "\u001b[?25l\r\r\n\r\u001b[?25h"] 104 | [16.910359, "o", "\u001b[?7h\u001b[?2004l\r"] 105 | [16.916938, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 106 | [16.917091, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 107 | [16.917293, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 108 | [16.919016, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 109 | [16.920166, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 110 | [16.920199, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 111 | [16.920235, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 112 | [16.920611, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 113 | [16.920621, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 114 | [16.923915, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 115 | [18.119857, "o", "\u001b[?25l\r\u001b[15C\u001b[0;32mb\u001b[0;m\r\u001b[16C\u001b[?25h\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 116 | [18.255198, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;31mbk\u001b[0;m\r\u001b[17C\u001b[?25h\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 117 | [18.310637, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mbkp\u001b[0;m\r\u001b[18C\u001b[?25h\u001b[?25l\r\r\u001b[18C\u001b[?25h"] 118 | [18.414007, "o", "\u001b[?25l\r\u001b[18C \r\u001b[19C\u001b[?25h\u001b[?25l\r\r\u001b[19C\u001b[?25h"] 119 | [18.647139, "o", "\u001b[?25l\r\u001b[19Cf\r\u001b[20C\u001b[?25h"] 120 | [18.647184, "o", "\u001b[?25l\r\r\u001b[20C\u001b[?25h"] 121 | [18.815281, "o", "\u001b[?25l\r\u001b[20Co\r\u001b[21C\u001b[?25h\u001b[?25l\r\r\u001b[21C\u001b[?25h"] 122 | [18.983473, "o", "\u001b[?25l\r\u001b[21Co.txt\r\u001b[26C\u001b[?25h"] 123 | [20.31926, "o", "\u001b[?25l\r\r\n\r\u001b[?25h"] 124 | [20.319381, "o", "\u001b[?7h\u001b[?2004l\r"] 125 | [20.378362, "o", "\u001b[32mfoo.txt\u001b[0m\r\n"] 126 | [20.378687, "o", " Created: foo.txt.b02\u001b[0m\r\n"] 127 | [20.385023, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 128 | [20.385132, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h"] 129 | [20.385138, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 130 | [20.385331, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 131 | [20.389493, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 132 | [20.389516, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 133 | [20.389543, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 134 | [20.389694, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 135 | [20.389926, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 136 | [21.05599, "o", "\u001b[?25l\r\u001b[15C\u001b[0;31ml\u001b[0;m\r\u001b[16C\u001b[?25h"] 137 | [21.157967, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mls\u001b[0;m\r\u001b[17C\u001b[?25h\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 138 | [21.319425, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 139 | [21.329955, "o", "foo.txt foo.txt.b01 foo.txt.b02\r\n"] 140 | [21.330631, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 141 | [21.33096, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 142 | [21.331237, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 143 | [21.338354, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 144 | [24.215985, "o", "\u001b[?25l\r\u001b[15C\u001b[0;31me\u001b[0;m\r\u001b[16C\u001b[?25h\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 145 | [25.126328, "o", "\u001b[?25l\r\u001b[16C\u001b[0;31mc\u001b[0;m\r\u001b[17C\u001b[?25h\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 146 | [25.215467, "o", "\u001b[?25l\r\u001b[17C\u001b[0;31mh\u001b[0;m\r\u001b[18C\u001b[?25h\u001b[?25l\r\r\u001b[18C\u001b[?25h"] 147 | [25.303002, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mecho\u001b[0;m\r\u001b[19C\u001b[?25h\u001b[?25l\r\r\u001b[19C\u001b[?25h"] 148 | [25.543032, "o", "\u001b[?25l\r\u001b[19C \r\u001b[20C\u001b[?25h\u001b[?25l\r\r\u001b[20C\u001b[?25h"] 149 | [25.799096, "o", "\u001b[?25l\r\u001b[20Cv\r\u001b[21C\u001b[?25h\u001b[?25l\r\r\u001b[21C\u001b[?25h"] 150 | [25.894824, "o", "\u001b[?25l\r\u001b[21Ce\r\u001b[22C\u001b[?25h\u001b[?25l\r\r\u001b[22C\u001b[?25h"] 151 | [25.990616, "o", "\u001b[?25l\r\u001b[22Cr\r\u001b[23C\u001b[?25h\u001b[?25l\r\r\u001b[23C\u001b[?25h"] 152 | [26.142877, "o", "\u001b[?25l\r\u001b[23Cs\r\u001b[24C\u001b[?25h\u001b[?25l\r\r\u001b[24C\u001b[?25h"] 153 | [26.229946, "o", "\u001b[?25l\r\u001b[24Ci\r\u001b[25C\u001b[?25h"] 154 | [26.262963, "o", "\u001b[?25l\r\u001b[25Co\r\u001b[26C\u001b[?25h\u001b[?25l\r\r\u001b[26C\u001b[?25h"] 155 | [26.326935, "o", "\u001b[?25l\r\u001b[26Cn\r\u001b[27C\u001b[?25h\u001b[?25l\r\r\u001b[27C\u001b[?25h"] 156 | [26.567131, "o", "\u001b[?25l\r\u001b[27C3\r\u001b[28C\u001b[?25h\u001b[?25l\r\r\u001b[28C\u001b[?25h"] 157 | [26.695257, "o", "\u001b[?25l\r\u001b[28C \r\u001b[29C\u001b[?25h\u001b[?25l\r\r\u001b[29C\u001b[?25h"] 158 | [27.631208, "o", "\u001b[?25l\r\u001b[29C\u001b[0;32m>\u001b[0;m\r\u001b[30C\u001b[?25h\u001b[?25l\r\r\u001b[30C\u001b[?25h"] 159 | [27.678717, "o", "\u001b[?25l\r\u001b[30C \r\u001b[31C\u001b[?25h\u001b[?25l\r\r\u001b[31C\u001b[?25h"] 160 | [27.894985, "o", "\u001b[?25l\r\u001b[31Cf\r\u001b[32C\u001b[?25h"] 161 | [27.895001, "o", "\u001b[?25l\r\r\u001b[32C\u001b[?25h"] 162 | [28.023024, "o", "\u001b[?25l\r\u001b[32Co\r\u001b[33C\u001b[?25h\u001b[?25l\r\r\u001b[33C\u001b[?25h"] 163 | [28.159031, "o", "\u001b[?25l\r\u001b[33Co\r\u001b[34C\u001b[?25h\u001b[?25l\r\r\u001b[34C\u001b[?25h"] 164 | [28.991151, "o", "\u001b[?25l\r\u001b[34C.\r\u001b[35C\u001b[?25h"] 165 | [28.991172, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 166 | [29.317755, "o", "\u001b[?25l\r\u001b[35Ct\r\u001b[36C\u001b[?25h"] 167 | [29.317769, "o", "\u001b[?25l\r\r\u001b[36C\u001b[?25h"] 168 | [29.479229, "o", "\u001b[?25l\r\u001b[36Cx\r\u001b[37C\u001b[?25h"] 169 | [29.479242, "o", "\u001b[?25l\r\r\u001b[37C\u001b[?25h"] 170 | [30.279464, "o", "\u001b[?25l\r\u001b[37Ct\r\u001b[38C\u001b[?25h\u001b[?25l\r\r\u001b[38C\u001b[?25h"] 171 | [30.478975, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 172 | [30.487694, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 173 | [30.488037, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h"] 174 | [30.488074, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 175 | [30.489064, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 176 | [30.489291, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 177 | [30.491178, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 178 | [30.491273, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 179 | [30.491407, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 180 | [30.491749, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 181 | [30.491793, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 182 | [30.498966, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 183 | [31.983853, "o", "\u001b[?25l\r\u001b[15C\u001b[0;31ml\u001b[0;m\r\u001b[16C\u001b[?25h"] 184 | [32.045749, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mls\u001b[0;m\r\u001b[17C\u001b[?25h\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 185 | [32.879643, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 186 | [32.890071, "o", "foo.txt foo.txt.b01 foo.txt.b02\r\n"] 187 | [32.890472, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 188 | [32.890789, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h"] 189 | [32.8908, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 190 | [32.891306, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 191 | [32.898705, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 192 | [34.93577, "o", "\u001b[?25l\r\u001b[15C\u001b[0;32mb\u001b[0;m\r\u001b[16C\u001b[?25h"] 193 | [34.935787, "o", "\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 194 | [34.937767, "o", "\u001b[?25l\r\r\u001b[16C\u001b[?25h\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 195 | [34.938245, "o", "\u001b[?25l\r\r\u001b[16C\u001b[?25h\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 196 | [35.087939, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;31mbk\u001b[0;m\r\u001b[17C\u001b[?25h"] 197 | [35.142889, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mbkp\u001b[0;m\r\u001b[18C\u001b[?25h\u001b[?25l\r\r\u001b[18C\u001b[?25h"] 198 | [35.255229, "o", "\u001b[?25l\r\u001b[18C \r\u001b[19C\u001b[?25h\u001b[?25l\r\r\u001b[19C\u001b[?25h"] 199 | [35.573722, "o", "\u001b[?25l\r\u001b[19C-\r\u001b[20C\u001b[?25h\u001b[?25l\r\r\u001b[20C\u001b[?25h"] 200 | [35.694883, "o", "\u001b[?25l\r\u001b[20Cr\r\u001b[21C\u001b[?25h"] 201 | [35.694894, "o", "\u001b[?25l\r\r\u001b[21C\u001b[?25h"] 202 | [35.791103, "o", "\u001b[?25l\r\u001b[21C \r\u001b[22C\u001b[?25h\u001b[?25l\r\r\u001b[22C\u001b[?25h"] 203 | [36.519629, "o", "\u001b[?25l\r\u001b[22Cf\r\u001b[23C\u001b[?25h"] 204 | [36.519667, "o", "\u001b[?25l\r\r\u001b[23C\u001b[?25h"] 205 | [36.646352, "o", "\u001b[?25l\r\u001b[23Co\r\u001b[24C\u001b[?25h\u001b[?25l\r\r\u001b[24C\u001b[?25h"] 206 | [36.823627, "o", "\u001b[?25l\r\u001b[24Co.txt\r\u001b[29C\u001b[?25h"] 207 | [40.480093, "o", "\u001b[?25l\r\u001b[29C.\r\u001b[30C\u001b[?25h\u001b[?25l\r\r\u001b[30C\u001b[?25h"] 208 | [40.871967, "o", "\u001b[?25l\r\u001b[30Cb0\r\u001b[32C\u001b[?25h"] 209 | [41.303662, "o", "\u001b[?25l\r\u001b[32C1\r\u001b[33C\u001b[?25h\u001b[?25l\r\r\u001b[33C\u001b[?25h"] 210 | [42.279473, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 211 | [42.339522, "o", "\u001b[32mfoo.txt.b01\u001b[0m\r\n"] 212 | [42.339695, "o", "'foo.txt' already exists, overwrite? [y/N]: "] 213 | [43.934761, "o", "y"] 214 | [44.734679, "o", "\r\n"] 215 | [44.735351, "o", " Deleted: foo.txt\u001b[0m\r\n"] 216 | [44.73593, "o", " Restored: foo.txt\u001b[0m\r\n"] 217 | [44.746211, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 218 | [44.746421, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 219 | [44.746582, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 220 | [44.75113, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 221 | [47.904414, "o", "\u001b[?25l\r\u001b[15C\u001b[0;31mc\u001b[0;m\r\u001b[16C\u001b[?25h\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 222 | [48.000157, "o", "\u001b[?25l\r\u001b[16C\u001b[0;31ma\u001b[0;m\r\u001b[17C\u001b[?25h\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 223 | [48.095901, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mcat\u001b[0;m\r\u001b[18C\u001b[?25h\u001b[?25l\r\r\u001b[18C\u001b[?25h"] 224 | [48.190954, "o", "\u001b[?25l\r\u001b[18C \r\u001b[19C\u001b[?25h\u001b[?25l\r\r\u001b[19C\u001b[?25h"] 225 | [48.365112, "o", "\u001b[?25l\r\u001b[19Cf\r\u001b[20C\u001b[?25h"] 226 | [48.365124, "o", "\u001b[?25l\r\r\u001b[20C\u001b[?25h"] 227 | [48.520053, "o", "\u001b[?25l\r\u001b[20Co\r\u001b[21C\u001b[?25h\u001b[?25l\r\r\u001b[21C\u001b[?25h"] 228 | [48.646132, "o", "\u001b[?25l\r\u001b[21Co.txt\r\u001b[26C\u001b[?25h"] 229 | [49.759487, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 230 | [49.768196, "o", "version1\r\n"] 231 | [49.768735, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 232 | [49.769071, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h"] 233 | [49.769117, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 234 | [49.769653, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 235 | [49.77604, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 236 | [52.925314, "o", "\u001b[?25l\r\u001b[15C\u001b[0;32mb\u001b[0;m\r\u001b[16C\u001b[?25h"] 237 | [53.175131, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;31mbk\u001b[0;m\r\u001b[17C\u001b[?25h"] 238 | [53.175163, "o", "\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 239 | [53.22296, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mbkp\u001b[0;m\r\u001b[18C\u001b[?25h\u001b[?25l\r\r\u001b[18C\u001b[?25h"] 240 | [53.311225, "o", "\u001b[?25l\r\u001b[18C \r\u001b[19C\u001b[?25h\u001b[?25l\r\r\u001b[19C\u001b[?25h"] 241 | [54.142566, "o", "\u001b[?25l\r\u001b[19Cf\r\u001b[20C\u001b[?25h\u001b[?25l\r\r\u001b[20C\u001b[?25h"] 242 | [54.287191, "o", "\u001b[?25l\r\u001b[20Co\r\u001b[21C\u001b[?25h"] 243 | [54.287277, "o", "\u001b[?25l\r\r\u001b[21C\u001b[?25h"] 244 | [54.431765, "o", "\u001b[?25l\r\u001b[21Co.txt\r\u001b[26C\u001b[?25h"] 245 | [55.583209, "o", "\u001b[?25l\r\u001b[26C \r\u001b[27C\u001b[?25h"] 246 | [55.58349, "o", "\u001b[?25l\r\r\u001b[27C\u001b[?25h"] 247 | [55.5852, "o", "\u001b[?25l\r\r\u001b[27C\u001b[?25h"] 248 | [55.585546, "o", "\u001b[?25l\r\r\u001b[27C\u001b[?25h"] 249 | [55.935194, "o", "\u001b[?25l\r\u001b[27C-\r\u001b[28C\u001b[?25h\u001b[?25l\r\r\u001b[28C\u001b[?25h"] 250 | [56.182806, "o", "\u001b[?25l\r\u001b[28Ca\r\u001b[29C\u001b[?25h\u001b[?25l\r\r\u001b[29C\u001b[?25h"] 251 | [56.695399, "o", "\u001b[?25l\r\u001b[29Cm\r\u001b[30C\u001b[?25h\u001b[?25l\r\r\u001b[30C\u001b[?25h"] 252 | [56.838485, "o", "\u001b[?25l\r\u001b[30C \r\u001b[31C\u001b[?25h\u001b[?25l\r\r\u001b[31C\u001b[?25h"] 253 | [57.830233, "o", "\u001b[?25l\r\u001b[31C\u001b[0;33m\"\u001b[0;m\r\u001b[32C\u001b[?25h\u001b[?25l\r\r\u001b[32C\u001b[?25h"] 254 | [58.239202, "o", "\u001b[?25l\r\u001b[32C\u001b[0;33mb\u001b[0;m\r\u001b[33C\u001b[?25h\u001b[?25l\r\r\u001b[33C\u001b[?25h"] 255 | [58.373369, "o", "\u001b[?25l\r\u001b[33C\u001b[0;33me\u001b[0;m\r\u001b[34C\u001b[?25h"] 256 | [58.373382, "o", "\u001b[?25l\r\r\u001b[34C\u001b[?25h"] 257 | [58.607331, "o", "\u001b[?25l\r\u001b[34C\u001b[0;33ms\u001b[0;m\r\u001b[35C\u001b[?25h\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 258 | [58.719433, "o", "\u001b[?25l\r\u001b[35C\u001b[0;33mt\u001b[0;m\r\u001b[36C\u001b[?25h\u001b[?25l\r\r\u001b[36C\u001b[?25h"] 259 | [58.798583, "o", "\u001b[?25l\r\u001b[36C\u001b[0;33m \u001b[0;m\r\u001b[37C\u001b[?25h\u001b[?25l\r\r\u001b[37C\u001b[?25h"] 260 | [59.087567, "o", "\u001b[?25l\r\u001b[37C\u001b[0;33mv\u001b[0;m\r\u001b[38C\u001b[?25h\u001b[?25l\r\r\u001b[38C\u001b[?25h"] 261 | [59.183691, "o", "\u001b[?25l\r\u001b[38C\u001b[0;33me\u001b[0;m\r\u001b[39C\u001b[?25h\u001b[?25l\r\r\u001b[39C\u001b[?25h"] 262 | [59.303459, "o", "\u001b[?25l\r\u001b[39C\u001b[0;33mr\u001b[0;m\r\u001b[40C\u001b[?25h\u001b[?25l\r\r\u001b[40C\u001b[?25h"] 263 | [59.414725, "o", "\u001b[?25l\r\u001b[40C\u001b[0;33ms\u001b[0;m\r\u001b[41C\u001b[?25h\u001b[?25l\r\r\u001b[41C\u001b[?25h"] 264 | [59.526242, "o", "\u001b[?25l\r\u001b[41C\u001b[0;33mi\u001b[0;m\r\u001b[42C\u001b[?25h\u001b[?25l\r\r\u001b[42C\u001b[?25h"] 265 | [59.56621, "o", "\u001b[?25l\r\u001b[42C\u001b[0;33mo\u001b[0;m\r\u001b[43C\u001b[?25h"] 266 | [59.566293, "o", "\u001b[?25l\r\r\u001b[43C\u001b[?25h"] 267 | [59.647335, "o", "\u001b[?25l\r\u001b[43C\u001b[0;33mn\u001b[0;m\r\u001b[44C\u001b[?25h\u001b[?25l\r\r\u001b[44C\u001b[?25h"] 268 | [60.920016, "o", "\u001b[?25l\r\u001b[44C\u001b[0;33m\"\u001b[0;m\r\u001b[45C\u001b[?25h\u001b[?25l\r\r\u001b[45C\u001b[?25h"] 269 | [61.351715, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 270 | [61.4313, "o", "\u001b[32mfoo.txt\u001b[0m\r\n"] 271 | [61.432163, "o", " Created: foo.txt.b03\u001b[0m\r\n"] 272 | [61.438404, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 273 | [61.438515, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h"] 274 | [61.438525, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 275 | [61.438711, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 276 | [61.44222, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 277 | [63.767776, "o", "\u001b[?25l\r\u001b[15C\u001b[0;31ml\u001b[0;m\r\u001b[16C\u001b[?25h"] 278 | [63.767795, "o", "\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 279 | [63.878556, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mls\u001b[0;m\r\u001b[17C\u001b[?25h"] 280 | [64.095231, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 281 | [64.105409, "o", "foo.txt foo.txt.b01 foo.txt.b02 foo.txt.b03\r\n"] 282 | [64.106091, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 283 | [64.106397, "o", "\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 284 | [64.106791, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 285 | [64.115431, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 286 | [70.823406, "o", "\u001b[?25l\r\u001b[15C\u001b[0;32mb\u001b[0;m\r\u001b[16C\u001b[?25h\u001b[?25l\r\r\u001b[16C\u001b[?25h"] 287 | [71.206648, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;31mbk\u001b[0;m\r\u001b[17C\u001b[?25h\u001b[?25l\r\r\u001b[17C\u001b[?25h"] 288 | [71.279336, "o", "\u001b[?25l\r\u001b[15C\u001b[K\u001b[0;32mbkp\u001b[0;m\r\u001b[18C\u001b[?25h\u001b[?25l\r\r\u001b[18C\u001b[?25h"] 289 | [71.391704, "o", "\u001b[?25l\r\u001b[18C \r\u001b[19C\u001b[?25h\u001b[?25l\r\r\u001b[19C\u001b[?25h"] 290 | [71.637783, "o", "\u001b[?25l\r\u001b[19C-\r\u001b[20C\u001b[?25h\u001b[?25l\r\r\u001b[20C\u001b[?25h"] 291 | [71.879684, "o", "\u001b[?25l\r\u001b[20Ci\r\u001b[21C\u001b[?25h\u001b[?25l\r\r\u001b[21C\u001b[?25h"] 292 | [71.989535, "o", "\u001b[?25l\r\u001b[21C \r\u001b[22C\u001b[?25h\u001b[?25l\r\r\u001b[22C\u001b[?25h"] 293 | [72.655644, "o", "\u001b[?25l\r\u001b[22Cf\r\u001b[23C\u001b[?25h\u001b[?25l\r\r\u001b[23C\u001b[?25h"] 294 | [72.783363, "o", "\u001b[?25l\r\u001b[23Co\r\u001b[24C\u001b[?25h\u001b[?25l\r\r\u001b[24C\u001b[?25h"] 295 | [72.950312, "o", "\u001b[?25l\r\u001b[24Co.txt\r\u001b[29C\u001b[?25h"] 296 | [73.783703, "o", "\u001b[?25l\r\u001b[29C.\r\u001b[30C\u001b[?25h\u001b[?25l\r\r\u001b[30C\u001b[?25h"] 297 | [74.271098, "o", "\u001b[?25l\r\u001b[30Cb\r\u001b[31C\u001b[?25h\u001b[?25l\r\r\u001b[31C\u001b[?25h"] 298 | [74.535788, "o", "\u001b[?25l\r\u001b[31C0\r\u001b[32C\u001b[?25h"] 299 | [75.469664, "o", "\u001b[?25l\r\u001b[32C3\r\u001b[33C\u001b[?25h\u001b[?25l\r\r\u001b[33C\u001b[?25h"] 300 | [76.167149, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 301 | [76.225887, "o", "\u001b[32mfoo.txt.b03\u001b[0m\r\n"] 302 | [76.226659, "o", " author : \u001b[0mgkrason\u001b[0m\r\n time : \u001b[0m2024-04-11 18:15:44 CEST\u001b[0m\r\n message : \u001b[0m\u001b[33mbest version\u001b[0m\r\n"] 303 | [76.233497, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h\u001b[?25l\r\u001b[0;36m[~/tmp/demo]\u001b[0;m─\u001b[0;32m> \u001b[0;m\r\u001b[15C\u001b[?25h\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 304 | [76.233678, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 305 | [76.234869, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 306 | [76.234884, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 307 | [76.235115, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 308 | [76.235123, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 309 | [76.23784, "o", "\u001b[?25l\r\r\u001b[15C\u001b[?25h"] 310 | [81.647368, "o", "\u001b[?25l\r\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 311 | --------------------------------------------------------------------------------