diff --git a/kittens/transfer/main.go b/kittens/transfer/main.go index 408c230ba..2a1242bbb 100644 --- a/kittens/transfer/main.go +++ b/kittens/transfer/main.go @@ -58,7 +58,7 @@ func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) { case "send", "download": err, rc = send_main(opts, args) default: - err = receive_main(opts, args) + err, rc = receive_main(opts, args) } if err != nil { rc = 1 diff --git a/kittens/transfer/receive.go b/kittens/transfer/receive.go index ac1ae9fef..ccc34b033 100644 --- a/kittens/transfer/receive.go +++ b/kittens/transfer/receive.go @@ -4,11 +4,47 @@ package transfer import ( "fmt" + + "kitty/tools/tui/loop" ) var _ = fmt.Print -func receive_main(opts *Options, args []string) (err error) { +func receive_loop(opts *Options, spec []string, dest string) (err error, rc int) { + lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors) + if err != nil { + return err, 1 + } + err = lp.Run() + if err != nil { + return err, 1 + } + if lp.DeathSignalName() != "" { + lp.KillIfSignalled() + return + } + + if lp.ExitCode() != 0 { + rc = lp.ExitCode() + } return } + +func receive_main(opts *Options, args []string) (err error, rc int) { + spec := args + var dest string + switch opts.Mode { + case "mirror": + if len(args) < 1 { + return fmt.Errorf("Must specify at least one file to transfer"), 1 + } + case "normal": + if len(args) < 2 { + return fmt.Errorf("Must specify at least one source and a destination file to transfer"), 1 + } + dest = args[len(args)-1] + spec = args[:len(args)-1] + } + return receive_loop(opts, spec, dest) +} diff --git a/kittens/transfer/send.go b/kittens/transfer/send.go index 6716fea14..a212c712e 100644 --- a/kittens/transfer/send.go +++ b/kittens/transfer/send.go @@ -1243,6 +1243,18 @@ func send_loop(opts *Options, files []*File) (err error, rc int) { lp.KillIfSignalled() return } + p := handler.manager.progress_tracker + if handler.manager.has_rsync && p.total_transferred+int64(p.signature_bytes) > 0 { + var tsf int64 + for _, f := range files { + if f.ttype == TransmissionType_rsync { + tsf += f.file_size + } + } + if tsf > 0 { + print_rsync_stats(tsf, p.total_transferred, int64(p.signature_bytes)) + } + } if len(handler.failed_files) > 0 { fmt.Fprintf(os.Stderr, "Transfer of %d out of %d files failed\n", len(handler.failed_files), len(handler.manager.files)) for _, f := range handler.failed_files { diff --git a/kittens/transfer/utils.go b/kittens/transfer/utils.go index 2cf81c488..40cad3eac 100644 --- a/kittens/transfer/utils.go +++ b/kittens/transfer/utils.go @@ -12,6 +12,7 @@ import ( "kitty/tools/crypto" "kitty/tools/utils" + "kitty/tools/utils/humanize" ) var _ = fmt.Print @@ -104,3 +105,10 @@ func should_be_compressed(path, strategy string) bool { } return true } + +func print_rsync_stats(total_bytes, delta_bytes, signature_bytes int64) { + fmt.Println("Rsync stats:") + fmt.Printf(" Delta size: %s Signature size: %s\n", humanize.Size(delta_bytes), humanize.Size(signature_bytes)) + frac := float64(delta_bytes+signature_bytes) / float64(utils.Max(1, total_bytes)) + fmt.Printf(" Transmitted: %s of a total of %s (%.1f%%)\n", humanize.Size(delta_bytes+signature_bytes), humanize.Size(total_bytes), frac*100) +} diff --git a/kittens/transfer/utils.py b/kittens/transfer/utils.py index f7b027e87..55203f19b 100644 --- a/kittens/transfer/utils.py +++ b/kittens/transfer/utils.py @@ -2,136 +2,12 @@ # License: GPLv3 Copyright: 2021, Kovid Goyal import os -import secrets from contextlib import contextmanager -from datetime import timedelta -from typing import Generator, Union - -from kitty.fast_data_types import truncate_point_for_length, wcswidth -from kitty.guess_mime_type import guess_type - -from ..tui.operations import styled -from ..tui.progress import render_progress_bar -from ..tui.utils import format_number, human_size +from typing import Generator _cwd = _home = '' -def safe_divide(numerator: Union[int, float], denominator: Union[int, float], zero_val: float = 0.) -> float: - return numerator / denominator if denominator else zero_val - - -def reduce_to_single_grapheme(text: str) -> str: - limit = len(text) - if limit < 2: - return text - x = 1 - while x < limit: - pos = truncate_point_for_length(text, x) - if pos > 0: - return text[:pos] - x += 1 - return text - - -def render_path_in_width(path: str, width: int) -> str: - if os.altsep: - path = path.replace(os.altsep, os.sep) - if wcswidth(path) <= width: - return path - parts = path.split(os.sep) - reduced = os.sep.join(map(reduce_to_single_grapheme, parts[:-1])) - path = os.path.join(reduced, parts[-1]) - if wcswidth(path) <= width: - return path - x = truncate_point_for_length(path, width - 1) - return f'{path[:x]}…' - - -def render_seconds(val: float) -> str: - ans = str(timedelta(seconds=int(val))) - if ',' in ans: - days = int(ans.split(' ')[0]) - if days > 99: - ans = '∞' - else: - ans = f'>{days} days' - elif len(ans) == 7: - ans = '0' + ans - return ans.rjust(8) - - -def ljust(text: str, width: int) -> str: - w = wcswidth(text) - if w < width: - text += ' ' * (width - w) - return text - - -def rjust(text: str, width: int) -> str: - w = wcswidth(text) - if w < width: - text = ' ' * (width - w) + text - return text - - -def render_progress_in_width( - path: str, - max_path_length: int = 80, - spinner_char: str = '⠋', - bytes_per_sec: float = 1024, - secs_so_far: float = 100., - bytes_so_far: int = 33070, - total_bytes: int = 50000, - width: int = 80, - is_complete: bool = False, -) -> str: - unit_style = styled('|', dim=True) - sep, trail = unit_style.split('|') - if is_complete or bytes_so_far >= total_bytes: - ratio = human_size(total_bytes, sep=sep) - rate = human_size(int(safe_divide(total_bytes, secs_so_far)), sep=sep) + '/s' - eta = styled(render_seconds(secs_so_far), fg='green') - else: - tb = human_size(total_bytes, sep=' ', max_num_of_decimals=1) - val = float(tb.split(' ', 1)[0]) - ratio = format_number(val * safe_divide(bytes_so_far, total_bytes), max_num_of_decimals=1) + '/' + tb.replace(' ', sep) - rate = human_size(int(bytes_per_sec), sep=sep) + '/s' - bytes_left = total_bytes - bytes_so_far - eta_seconds = safe_divide(bytes_left, bytes_per_sec) - eta = render_seconds(eta_seconds) - lft = f'{spinner_char} ' - max_space_for_path = width // 2 - wcswidth(lft) - w = min(max_path_length, max_space_for_path) - p = lft + render_path_in_width(path, w) - w += wcswidth(lft) - p = ljust(p, w) - q = f'{ratio}{trail}{styled(" @ ", fg="yellow")}{rate}{trail}' - q = rjust(q, 25) + ' ' - eta = ' ' + eta - extra = width - w - wcswidth(q) - wcswidth(eta) - if extra > 4: - q += render_progress_bar(safe_divide(bytes_so_far, total_bytes), extra) + eta - else: - q += eta.strip() - return p + q - - -def should_be_compressed(path: str) -> bool: - ext = path.rpartition(os.extsep)[-1].lower() - if ext in ('zip', 'odt', 'odp', 'pptx', 'docx', 'gz', 'bz2', 'xz', 'svgz'): - return False - mt = guess_type(path) or '' - if mt: - if mt.endswith('+zip'): - return False - if mt.startswith('image/') and mt not in ('image/svg+xml',): - return False - if mt.startswith('video/'): - return False - return True - - def abspath(path: str, use_home: bool = False) -> str: base = home_path() if use_home else (_cwd or os.getcwd()) return os.path.normpath(os.path.join(base, path)) @@ -151,12 +27,6 @@ def expand_home(path: str) -> str: return path -def random_id() -> str: - ans = hex(os.getpid())[2:] - x = secrets.token_hex(2) - return ans + x - - @contextmanager def set_paths(cwd: str = '', home: str = '') -> Generator[None, None, None]: global _cwd, _home @@ -188,10 +58,3 @@ class ZlibCompressor: def flush(self) -> bytes: return self.c.flush() - - -def print_rsync_stats(total_bytes: int, delta_bytes: int, signature_bytes: int) -> None: - print('Rsync stats:') - print(f' Delta size: {human_size(delta_bytes)} Signature size: {human_size(signature_bytes)}') - frac = (delta_bytes + signature_bytes) / max(1, total_bytes) - print(f' Transmitted: {human_size(delta_bytes + signature_bytes)} of a total of {human_size(total_bytes)} ({frac:.1%})')