From bd740df6d7771192df234c5271174e6cf9275952 Mon Sep 17 00:00:00 2001 From: Hiddify <114227601+hiddify-com@users.noreply.github.com> Date: Sun, 21 Jan 2024 13:34:12 +0000 Subject: [PATCH] =?UTF-8?q?release:=20version=201.0.0=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HISTORY.md | 13 --- cli_progress/ANSIWidget.py | 62 +++++++++++ cli_progress/BackgroundWidget.py | 32 ++++++ cli_progress/LogListWidget.py | 24 ++++ cli_progress/VERSION | 2 +- cli_progress/__main__.py | 40 ++++++- cli_progress/base.py | 17 --- cli_progress/cli.py | 28 ----- cli_progress/progress_ui.py | 183 +++++++++++++++++++++++++++++++ requirements.txt | 5 + test.sh | 15 +++ 11 files changed, 360 insertions(+), 61 deletions(-) create mode 100644 cli_progress/ANSIWidget.py create mode 100644 cli_progress/BackgroundWidget.py create mode 100644 cli_progress/LogListWidget.py delete mode 100644 cli_progress/base.py delete mode 100644 cli_progress/cli.py create mode 100644 cli_progress/progress_ui.py create mode 100644 test.sh diff --git a/HISTORY.md b/HISTORY.md index 9bf6ef0..e69de29 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,13 +0,0 @@ -Changelog -========= - - -0.1.2 (2021-08-14) ------------------- -- Fix release, README and windows CI. [Bruno Rocha] -- Release: version 0.1.0. [Bruno Rocha] - - -0.1.0 (2021-08-14) ------------------- -- Add release command. [Bruno Rocha] diff --git a/cli_progress/ANSIWidget.py b/cli_progress/ANSIWidget.py new file mode 100644 index 0000000..c6c31c5 --- /dev/null +++ b/cli_progress/ANSIWidget.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +from __future__ import annotations +from typing import Tuple, List, Optional, Any, Iterable +import urwid +import re + +class ANSICanvas(urwid.canvas.Canvas): + def __init__(self, size: Tuple[int, int], text_lines: List[str]) -> None: + super().__init__() + + self.maxcols, self.maxrows = size + + self.text_lines = text_lines + + def cols(self) -> int: + return self.maxcols + + def rows(self) -> int: + return self.maxrows + + def content( + self, + trim_left: int = 0, + trim_top: int = 0, + cols: Optional[int] = None, + rows: Optional[int] = None, + attr_map: Optional[Any] = None, + ) -> Iterable[List[Tuple[None, str, bytes]]]: + assert cols is not None + assert rows is not None + + for i in range(rows): + if i < len(self.text_lines): + text = self.text_lines[i] + else: + text = b"" + + padding = bytes().rjust(max(0, cols - len(escape_ansi(text)))) + line = [(None, "U", text.encode("utf-8") + padding)] + + yield line + + +class ANSIWidget(urwid.Widget): + _sizing = frozenset([urwid.widget.BOX]) + + def __init__(self, text: str = "") -> None: + self.lines = text.split("\n") + + def set_content(self, lines: List[str]) -> None: + self.lines = lines + self._invalidate() + + def render(self, size: Tuple[int, int], focus: bool = False) -> urwid.canvas.Canvas: + canvas = ANSICanvas(size, self.lines) + + return canvas + +def escape_ansi(line): + ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]') + return ansi_escape.sub('', line) \ No newline at end of file diff --git a/cli_progress/BackgroundWidget.py b/cli_progress/BackgroundWidget.py new file mode 100644 index 0000000..785666c --- /dev/null +++ b/cli_progress/BackgroundWidget.py @@ -0,0 +1,32 @@ +import urwid +class BackgroundView(urwid.WidgetWrap): + def __init__(self,forground,header,footer): + self.forground=forground + self.header_widget=header + self.footer_widget=footer + super().__init__(self.main_window()) + + def main_shadow(self, w): + """Wrap a shadow and background around widget w.""" + bg = urwid.AttrMap(urwid.SolidFill("\u2592"), "screen edge") + shadow = urwid.AttrMap(urwid.SolidFill(" "), "main shadow") + + bg = urwid.Overlay(shadow, bg, ("fixed left", 2), ("fixed right", 1), ("fixed top", 2), ("fixed bottom", 1)) + w = urwid.Overlay(w, bg, ("fixed left", 1), ("fixed right", 2), ("fixed top", 1), ("fixed bottom", 2)) + # bg = urwid.Overlay(shadow, bg, ("fixed left", 0), ("fixed right", 0), ("fixed top", 0), ("fixed bottom", 0)) + # w = urwid.Overlay(w, bg, ("fixed left", 0), ("fixed right", 0), ("fixed top", 0), ("fixed bottom", 0)) + return w + def main_window(self): + + self.forground_wrap = urwid.WidgetWrap(self.forground) + vline = urwid.AttrMap(urwid.SolidFill("\u2502"), "line") + + w = urwid.Padding(self.forground_wrap, ("fixed left", 1), ("fixed right", 0)) + w = urwid.AttrMap(w, "body") + w = urwid.LineBox(w) + w = urwid.AttrMap(w, "line") + w = self.main_shadow(w) + return urwid.Frame( + body=w, header=self.header_widget,footer=self.footer_widget + ) + diff --git a/cli_progress/LogListWidget.py b/cli_progress/LogListWidget.py new file mode 100644 index 0000000..00a5d33 --- /dev/null +++ b/cli_progress/LogListWidget.py @@ -0,0 +1,24 @@ +import urwid +from .ANSIWidget import ANSIWidget + + +class LogListBox(urwid.ListBox): + def __init__(self): + body = urwid.SimpleFocusListWalker([get_line("")]) + super().__init__(body) + + def add_log_line(self, data, err, replace=False): + txt = f"\033[91m{data}\033[0m" if err else data + if replace: + self.body[-1].set_content(txt) + else: + self.body.append(get_line(txt)) + if self.is_focus_end(): + self.focus_position = len(self.body) - 1 + + def is_focus_end(self): + return self.focus_position >= len(self.body) - 2 + + +def get_line(text): + return urwid.BoxAdapter(ANSIWidget(text), 1) diff --git a/cli_progress/VERSION b/cli_progress/VERSION index 6e8bf73..3eefcb9 100644 --- a/cli_progress/VERSION +++ b/cli_progress/VERSION @@ -1 +1 @@ -0.1.0 +1.0.0 diff --git a/cli_progress/__main__.py b/cli_progress/__main__.py index 7a1adb7..e2d425a 100644 --- a/cli_progress/__main__.py +++ b/cli_progress/__main__.py @@ -1,6 +1,42 @@ """Entry point for cli_progress.""" -from cli_progress.cli import main # pragma: no cover -if __name__ == "__main__": # pragma: no cover +import argparse +from .progress_ui import ProgressUI + +def parse_arguments(): + parser = argparse.ArgumentParser(description="Process log file with a command") + + # Add a title to the command-line arguments + title_group = parser.add_argument_group("Arguments") + title_group.add_argument("--log", help="Path to the log file", required=False) + title_group.add_argument( + "--title", default="Title", help="Title for the script", required=False + ) + title_group.add_argument( + "--subtitle", + default="SubTitle", + help="SubTitle for the script. e.x. version", + required=False, + ) + title_group.add_argument( + "--regex", + default=r"####(?P\d+)####(?P.*?)####(?P<subtitle>.*?)####", + help="Regex for capturing the progress", + required=False, + ) + title_group.add_argument("command", nargs="+", help="Command and its arguments") + + return parser.parse_args() + + +def main(): + args = parse_arguments() + + + ui = ProgressUI(args.log, args.command, args.title, args.subtitle, args.regex) + ui.start() + + +if __name__ == "__main__": main() diff --git a/cli_progress/base.py b/cli_progress/base.py deleted file mode 100644 index b04f332..0000000 --- a/cli_progress/base.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -cli_progress base module. - -This is the principal module of the cli_progress project. -here you put your main classes and objects. - -Be creative! do whatever you want! - -If you want to replace this with a Flask application run: - - $ make init - -and then choose `flask` as template. -""" - -# example constant variable -NAME = "cli_progress" diff --git a/cli_progress/cli.py b/cli_progress/cli.py deleted file mode 100644 index aeb78cb..0000000 --- a/cli_progress/cli.py +++ /dev/null @@ -1,28 +0,0 @@ -"""CLI interface for cli_progress project. - -Be creative! do whatever you want! - -- Install click or typer and create a CLI app -- Use builtin argparse -- Start a web application -- Import things from your .base module -""" - - -def main(): # pragma: no cover - """ - The main function executes on commands: - `python -m cli_progress` and `$ cli_progress `. - - This is your program's entry point. - - You can change this function to do whatever you want. - Examples: - * Run a test suite - * Run a server - * Do some other stuff - * Run a command line application (Click, Typer, ArgParse) - * List all available tasks - * Run an application (Flask, FastAPI, Django, etc.) - """ - print("This will do something") diff --git a/cli_progress/progress_ui.py b/cli_progress/progress_ui.py new file mode 100644 index 0000000..54322f3 --- /dev/null +++ b/cli_progress/progress_ui.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python + +from __future__ import annotations +from typing import Tuple, List, Optional, Any, Iterable +import threading +from .BackgroundWidget import BackgroundView +import os +import subprocess +import sys +import signal +import urwid +import re +from twisted.internet import reactor, threads + +from .LogListWidget import LogListBox + + +main_event_loop = urwid.TwistedEventLoop() + +palette = [ + ("screen edge", "light blue", "dark blue"), + ("main shadow", "dark gray", "black"), + ("body", "default", "default"), + ("foot", "dark cyan", "dark blue", "bold"), + ("key", "light cyan", "dark blue", "underline"), + ("footer_foot", "light gray", "black"), + ("footer_key", "light cyan", "black", "underline"), + ("footer_title", "white", "black"), + ("header_title", "black", "white"), + ("header_version", "dark red", "white"), + ("header_back", "black", "white"), + ("progressbar_header", "black", "light gray"), + ("progress_header_title", "white", "black", "bold"), + ("progress_header_descr", "dark blue", "light gray"), + ("progressbar_normal", "light gray", "black"), + ("progressbar_complete", "white", "dark green"), + ("progressbar_error","dark red","dark red") +] + +footer_info = [ + ("footer_title", "KEY"), + " ", + ("footer_key", "Q"), + ", ", + ("footer_key", "CTRL+C"), + " exits ", + ("footer_key", "UP"), + ", ", + ("footer_key", "DOWN"), + ", ", + ("footer_key", "PAGE+UP"), + ", ", + ("footer_key", "PAGE DOWN"), + " move", +] + + +def exit_on_enter(key): + if key in ("q", "Q"): + raise urwid.ExitMainLoop() + + +class ProgressUI: + def __init__( + self, logpath: str, command: str, title: str, subtitle: str, regex: str + ): + self.logpath = logpath or "/dev/null" + self.cmds = command + self.regex = re.compile(regex) + header_text = [ + ("header_title", title), + " ", + ("header_version", subtitle), + ] + + header_widget = urwid.AttrMap(urwid.Text(header_text), "header_back") + self.progressbar = urwid.ProgressBar( + "progressbar_normal", "progressbar_complete", 0, 100 + ) + self.listbox = LogListBox() + self.progressbar_header = urwid.Text("") + footer_footer = urwid.AttrMap(urwid.Text(footer_info), "footer_foot") + + footer = urwid.Pile( + [ + urwid.AttrMap(self.progressbar_header, "progressbar_header"), + self.progressbar, + footer_footer, + ] + ) + + self.loop = urwid.MainLoop( + BackgroundView(urwid.AttrMap(self.listbox, "body"), header_widget, footer), + palette, + unhandled_input=exit_on_enter, + handle_mouse=False, + event_loop=main_event_loop, + ) + self.write_fd = self.loop.watch_pipe(self.received_output) + self.write_fd_err = self.loop.watch_pipe(self.received_err) + + self.std_err_line = ["", ""] + + def received_output(self, data): + self.handle_in(data, False) + + def received_err(self, data): + self.handle_in(data, True) + + def handle_in(self, data, err): + indx = 1 if err else 0 + last_char = "" + # print(self.std_err_line[indx],data) + for c in data.decode(): + if c in ["\r", "\n"]: + self.handle_line(self.std_err_line[indx], err) + self.std_err_line[indx] = "" + # elif '\r' == c: + # listbox.add_log_line(std_err_line[indx], err, replace=True) + # std_err_line[indx] = '' + else: + self.std_err_line[indx] += c + + def handle_line(self, data, err): + self.logfile.writelines([data]) + # data = escape_ansi(data) + progress_match = self.regex.match(data) + if progress_match: + p = int(progress_match.group("progress")) + title = f'{progress_match.group("title")} ' + desc = f'{progress_match.group("subtitle")}' + self.handle_progress(p, title, desc) + + else: + self.listbox.add_log_line(data, err) + + def handle_progress(self, progress: int, title: str, subtitle: str): + self.progressbar.set_completion(progress) + self.progressbar_header.set_text( + [ + ("progress_header_title", title), + (" "), + ("progress_header_descr", subtitle), + ] + ) + + def start(self): + with open(self.logpath, "w") as self.logfile: + proc = subprocess.Popen( + self.cmds, + stdout=self.write_fd, + stderr=self.write_fd_err, + close_fds=True, + ) + d = threads.deferToThread(proc.wait) + d.addCallback(self.exit_loop) + # try: + self.loop.run() + # except: + # pass + try: + proc.send_signal(signal.SIGTERM) + except: + pass + sys.exit(0) + + def exit_loop_finish_proceess(self, exit_code): + # self.handle_line("Process Finished... To Exit Press Q",False) + if exit_code: + self.progressbar.normal="progressbar_error" + self.handle_progress(self.progressbar.current, "An Error Occured...", f"Code {exit_code}") + else: + self.handle_progress(100, "Process Finished...", "To Exit Press Q") + if self.listbox.is_focus_end(): + main_event_loop.alarm(1, self.exit_loop_exception) + + def exit_loop_exception(self): + if self.listbox.is_focus_end(): + raise urwid.ExitMainLoop + + @main_event_loop.handle_exit + def exit_loop(self, exit_code): + main_event_loop.alarm(0, lambda: self.exit_loop_finish_proceess(exit_code)) diff --git a/requirements.txt b/requirements.txt index b05f2a6..87744e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,8 @@ # Add the requirements you need to this file. # or run `make init` to create this file automatically based on the template. # You can also run `make switch-to-poetry` to use the poetry package manager. +urwid==2.4.1 +twisted==23.10.0 +pyOpenSSL==23.3.0 +service_identity==24.1.0 +argparse==1.4.0 \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..210949e --- /dev/null +++ b/test.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +for ((c = 1; c <= 10; c++)); do + echo "factor: $c" + echo "####$c####title: $c####descr: $c#####" + sleep 0.1 +done + +for ((c = 1; c <= 10; c++)); do + echo "factor: $c" >&2 + echo "####$c####title: $c####descr: $c#####" + sleep 0.1 +done + +