From a87fa43ea70745d3f24086264087a263b9e39839 Mon Sep 17 00:00:00 2001 From: havocesp <10012416+havocesp@users.noreply.github.com> Date: Wed, 22 Mar 2023 18:48:24 +0000 Subject: [PATCH] Code restructured and many other changes. --- .gitignore | 10 +- MANIFEST.in | 1 + README.md | 86 +++++----- README.rst | 331 ------------------------------------- pyterm.jpg | Bin 34495 -> 0 bytes pyterm/__init__.py | 24 +++ pyterm/constants.py | 119 ++++++++++++++ pyterm/core.py | 391 ++++++++++++++++++++++++++++++++++++++++++++ pyterm/model.py | 31 ++++ pyterm/utils.py | 15 ++ setup.py | 89 +++++----- term.py | 184 --------------------- 12 files changed, 675 insertions(+), 606 deletions(-) delete mode 100644 README.rst delete mode 100644 pyterm.jpg create mode 100644 pyterm/__init__.py create mode 100644 pyterm/constants.py create mode 100644 pyterm/core.py create mode 100644 pyterm/model.py create mode 100644 pyterm/utils.py delete mode 100644 term.py diff --git a/.gitignore b/.gitignore index 65caf74..06304ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.pyc test.py -build/ -dist/ -py_term.egg-info -py-term-0.1/ +build/** +dist/** +pyterm.egg-info/** +pyterm-0.1/ +.idea/** +/Code_restructured_and_many_other_changes_.patch diff --git a/MANIFEST.in b/MANIFEST.in index bb3ec5f..d8bab85 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include README.md +LICENSE diff --git a/README.md b/README.md index dff7ec9..a356664 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,44 @@ -# py-term -Python module to style terminal output, moving and positioning the cursor. +# pyterm -**Python 2 and 3 compatible** - -![alt text](pyterm.jpg "See? amazing!") +Python 3 package to easily handle style terminal output, moving and positioning the cursor and some other features. +This project is heavily based on [gravmatt/py-term](https://github.com/gravmatt/py-term]github.com) project. ## Installation Install through **pip**. -``` -$ pip install py-term +```shell +$ pip3 install git+https://github.com/havocesp/pyterm ``` or get from source -``` -$ git clone https://github.com/gravmatt/py-term.git -$ cd py-term -$ python setup.py install +```shell +$ git clone https://github.com/havocesp/pyterm +$ cd pyterm +$ python3 setup.py install ``` ## Import module Import the module into your python project. -``` -import term +```python3 +from pyterm import Term as Tm ``` ## Usage -``` -term.write('Hello, ') -term.write('Python!') +```python +from pyterm import Term as Tm + +Tm.write('Hello, ') +Tm.write('Python!') > Hello, Python! -term.writeLine('Hello') -term.writeLine('Python!') +Tm.write_line('Hello') +Tm.write_line('Python!') > Hello > Python! @@ -48,23 +48,25 @@ term.writeLine('Python!') The first argument is the text output and all following arguments are for styling. -``` -term.writeLine(text, *style) +```python3 +from pyterm import Term as Tm -Or +Tm.write_line('text', Tm.BOLD) -text = term.format(text, *style) -term.writeLine(text) +text = Tm.style('text', Tm.DIM) +Tm.write_line(text) ``` ### Usage -``` -term.writeLine('Hello, Python!') +```python +from pyterm import Term as Tm + +Tm.write_line('Hello, Python!') -term.writeLine('This text line will be green', term.green) +Tm.write_line('This text line will be green', Tm.GREEN) -term.writeLine('Reverse the green color', term.green, term.reverse) +Tm.write_line('Reverse the green color', Tm.GREEN, Tm.REVERSE) ``` Or @@ -72,7 +74,7 @@ Or ``` output = term.format('Hello, ', term.green) + term.format('Python!', term.blue, term.bold) -term.writeLine(output) +term.write_line(output) term.write(term.format('All in one line', term.reverse)) ``` @@ -84,15 +86,15 @@ term.write(term.format('All in one line', term.reverse)) ``` # term.textCenter(text) -term.writeLine(term.textCenter('Super Python!')) +term.write_line(term.textCenter('Super Python!')) ``` **Right align** ``` -# term.textRight(text) +# term.text_right(text) -term.writeLine(term.textRight('Rene Tanczos (@gravmatt)')) +term.write_line(term.text_right('Rene Tanczos (@gravmatt)')) ( Function term.right() to align text is depricated because of naming conflicts! ) ``` @@ -172,21 +174,21 @@ array_of_positions (index 2) = array of tuples with start and stop points of the ## Set title ``` -term.setTitle('Hello Terminal') +term.set_title('Hello Terminal') # or clear it -term.clearTitle() +term.clear_title() ``` ## Set tab name ``` -term.setTab('Hello Tab') +term.set_tab('Hello Tab') # or clear it -term.clearTab() +term.clear_tab() ``` ## Cursor position @@ -201,9 +203,9 @@ term.pos(2, 15) Get the size of the terminal (lines and columns) ``` -(30, 100) = term.getSize() +(30, 100) = term.get_size() -# (lines, colums) = term.getSize() +# (lines, colums) = term.get_size() ``` Move the cursor to the home position (1, 1). @@ -224,13 +226,13 @@ term.right(value=1) Saves the current cursor position. ``` -term.saveCursor() +term.save_cursor() ``` Restore the previously stored cursor position. ``` -term.restoreCursor() +term.restore_cursor() ``` Clear the terminal screen. @@ -242,19 +244,19 @@ term.clear() Clear the entire line on the current cursor position. ``` -term.clearLine() +term.clear_line() ``` Clear line from the current cursor position to the end. ``` -term.clearLineFromPos() +term.clear_line_from_pos() ``` Clear line from begin to current cursor position. ``` -term.clearLineToPos() +term.clear_line_to_pos() ``` ## Licence diff --git a/README.rst b/README.rst deleted file mode 100644 index 3f52ac2..0000000 --- a/README.rst +++ /dev/null @@ -1,331 +0,0 @@ -py-term -======= - -Python module to style terminal output, moving and positioning the -cursor. - -**Python 2 and 3 compatible** - -Installation ------------- - -Install through **pip**. - -:: - - $ pip install py-term - -or get from source - -:: - - $ git clone https://github.com/gravmatt/py-term.git - $ cd py-term - $ python setup.py install - -Import module -------------- - -Import the module into your python project. - -:: - - import term - -Usage ------ - -:: - - term.write('Hello, ') - term.write('Python!') - - > Hello, Python! - - term.writeLine('Hello') - term.writeLine('Python!') - - > Hello - > Python! - -Style output ------------- - -The first argument is the text output and all following arguments are -for styling. - -:: - - term.writeLine(text, *style) - - Or - - text = term.format(text, *style) - term.writeLine(text) - -Usage -~~~~~ - -:: - - term.writeLine('Hello, Python!') - - term.writeLine('This text line will be green', term.green) - - term.writeLine('Reverse the green color', term.green, term.reverse) - -Or - -:: - - output = term.format('Hello, ', term.green) + term.format('Python!', term.blue, term.bold) - - term.writeLine(output) - - term.write(term.format('All in one line', term.reverse)) - -Text align -^^^^^^^^^^ - -**Center align** - -:: - - # term.textCenter(text) - - term.writeLine(term.textCenter('Super Python!')) - -**Right align** - -:: - - # term.textRight(text) - - term.writeLine(term.textRight('Rene Tanczos (@gravmatt)')) - - ( Function term.right() to align text is depricated because of naming conflicts! ) - -Style attributes -'''''''''''''''' - -+-------------------+----------------------------------------+ -| Code | Description | -+===================+========================================+ -| term.off | All attributes off | -+-------------------+----------------------------------------+ -| term.bold | Bold | -+-------------------+----------------------------------------+ -| term.dim | Dim | -+-------------------+----------------------------------------+ -| term.underscore | Underscore (monochrome display only) | -+-------------------+----------------------------------------+ -| term.blink | Blink | -+-------------------+----------------------------------------+ -| term.reverse | Reverse | -+-------------------+----------------------------------------+ -| term.hide | Hide | -+-------------------+----------------------------------------+ - -Text color -'''''''''' - -+----------------+-----------+ -| Code | Color | -+================+===========+ -| term.black | Black | -+----------------+-----------+ -| term.red | Red | -+----------------+-----------+ -| term.green | Green | -+----------------+-----------+ -| term.yellow | Yellow | -+----------------+-----------+ -| term.blue | Blue | -+----------------+-----------+ -| term.magenta | Magenta | -+----------------+-----------+ -| term.cyan | Cyan | -+----------------+-----------+ -| term.white | White | -+----------------+-----------+ - -Background color -'''''''''''''''' - -+------------------+-----------+ -| Code | Color | -+==================+===========+ -| term.bgblack | Black | -+------------------+-----------+ -| term.bgred | Red | -+------------------+-----------+ -| term.bggreen | Green | -+------------------+-----------+ -| term.bgyellow | Yellow | -+------------------+-----------+ -| term.bgblue | Blue | -+------------------+-----------+ -| term.bgMagenta | Magenta | -+------------------+-----------+ -| term.bgcyan | Cyan | -+------------------+-----------+ -| term.bgwhite | White | -+------------------+-----------+ - -Remove style attributes ------------------------ - -Removes style characters. - -(Good to call before you count a string) - -:: - - term.strip(formatted_text) - - hello = term.red + 'hello, world' + term.off - print hello - # '\x1b[31mhello, world\x1b[0m\x1b[27m' - - print term.strip(hello) - # hello, world - -Highlighting text ------------------ - -Simple highlighting of unformatted text strings - -:: - - def callback(matching_text): - return term.blue + matching_text + term.off - - output, match_count, array_of_positions = term.highlight(regex_pattern, text, callback) - -Return a tuple. - -output (index 0) = formatted text output - -match\_count (index 1) = match count of the pattern - -array\_of\_positions (index 2) = array of tuples with start and stop -points of the matches [(4, 6), (9, 11), ..] - -Set title ---------- - -:: - - term.setTitle('Hello Terminal') - - # or clear it - - term.clearTitle() - -Set tab name ------------- - -:: - - term.setTab('Hello Tab') - - # or clear it - - term.clearTab() - -Cursor position ---------------- - -Move the cursor to a specific position. - -:: - - term.pos(line, column) - - term.pos(2, 15) - -Get the size of the terminal (lines and columns) - -:: - - (30, 100) = term.getSize() - - # (lines, colums) = term.getSize() - -Move the cursor to the home position (1, 1). - -:: - - term.homePos() - -Moves the current cursor position up, down, left or right by the -specified value. - -:: - - term.up(value=1) - term.down(value=1) - term.left(value=1) - term.right(value=1) - -Saves the current cursor position. - -:: - - term.saveCursor() - -Restore the previously stored cursor position. - -:: - - term.restoreCursor() - -Clear the terminal screen. - -:: - - term.clear() - -Clear the entire line on the current cursor position. - -:: - - term.clearLine() - -Clear line from the current cursor position to the end. - -:: - - term.clearLineFromPos() - -Clear line from begin to current cursor position. - -:: - - term.clearLineToPos() - -Licence -------- - -The MIT License (MIT) - -Copyright (c) 2015-2021 René Tanczos - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pyterm.jpg b/pyterm.jpg deleted file mode 100644 index 73dc0349113655b2f6c471f84649cd6d003c127d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34495 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 diff --git a/pyterm/__init__.py b/pyterm/__init__.py new file mode 100644 index 0000000..dc00062 --- /dev/null +++ b/pyterm/__init__.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pyterm + + - File: pyterm/__init__.py + - Author: Havocesp + - Created: 2023-03-22 + - +""" +from pyterm.constants import __all_constants__ +from pyterm.core import Term +from pyterm.model import Color + +Tm = Term + +__version__ = '0.7.2' + +__all__ = [ + 'Term', + 'Tm', + 'Color', + '__version__', + *__all_constants__ +] diff --git a/pyterm/constants.py b/pyterm/constants.py new file mode 100644 index 0000000..0b9c5da --- /dev/null +++ b/pyterm/constants.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pyterm + + - File: pyterm/constants.py + - Author: Havocesp + - Created: 2023-03-22 + - +""" +BLINK = '\033[5m' +BOLD = '\033[1m' +DIM = '\033[2m' +HIDE = '\033[8m' +OFF = '\033[0m\033[27m' +REVERSE = ' \033[7m' +UNDERSCORE = '\033[4m' +UNDERLINE = UNDERSCORE + +BLACK = '\033[30m' +BLUE = '\033[34m' +CYAN = '\033[36m' +GREEN = '\033[32m' +MAGENTA = '\033[35m' +RED = '\033[31m' +WHITE = '\033[37m' +YELLOW = '\033[33m' + +BGBLACK = '\033[40m' +BGBLUE = '\033[44m' +BGCYAN = '\033[46m' +BGGREEN = '\033[42m' +BGMAGENTA = '\033[45m' +BGRED = '\033[41m' +BGWHITE = '\033[47m' +BGYELLOW = '\033[43m' + +# aliases +BK = BLACK +BL = BLUE +CY = CYAN +GN = GREEN +MG = MAGENTA +RD = RED +WH = WHITE +YL = YELLOW + +BGK = BGBLACK +BGL = BGBLUE +BGC = BGCYAN +BGG = BGGREEN +BGM = BGMAGENTA +BGR = BGRED +BGW = BGWHITE +BGY = BGYELLOW + +BLK = BLINK +BD = BOLD +DN = DIM +HD = HIDE +OF = OFF +RV = REVERSE +US = UNDERSCORE +UL = UNDERLINE + +__all_constants__ = [] + +__all__ = [ + 'BD', + 'BGBLACK', + 'BGBLUE', + 'BGC', + 'BGCYAN', + 'BGG', + 'BGGREEN', + 'BGK', + 'BGL', + 'BGM', + 'BGMAGENTA', + 'BGR', + 'BGRED', + 'BGW', + 'BGWHITE', + 'BGY', + 'BGYELLOW', + 'BK', + 'BL', + 'BLACK', + 'BLINK', + 'BLK', + 'BLUE', + 'BOLD', + 'CY', + 'CYAN', + 'DIM', + 'DN', + 'GN', + 'GREEN', + 'HD', + 'HIDE', + 'MAGENTA', + 'MG', + 'OF', + 'OFF', + 'RD', + 'RED', + 'REVERSE', + 'RV', + 'UNDERLINE', + 'UNDERSCORE', + 'US', + 'WH', + 'WHITE', + 'YELLOW', + 'YL', + 'UL', + '__all_constants__' +] + +__all_constants__.extend(__all__) diff --git a/pyterm/core.py b/pyterm/core.py new file mode 100644 index 0000000..192a74c --- /dev/null +++ b/pyterm/core.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pyterm + + - File: pyterm/core.py + - Author: Havocesp + - Created: 2023-03-22 + - +""" + +import shutil +import sys +from typing import NoReturn, Text, Tuple + +import regex as re + +from pyterm.model import Color + + +class Term(Color): + """Term class for controlling the termina feature like output color, cursor position, etc.""" + + @classmethod + def _send(cls, cmd) -> NoReturn: + sys.stdout.write(cmd) + sys.stdout.flush() + + @classmethod + def pos(cls, line: int, column: int) -> NoReturn: + """ Set the cursor position to the given line and column. + + :param line: terminal line position. 1 is the first line. + :param column: terminal column position. 1 is the first column. + """ + cls._send(f'\033[{line};{column}f') + + @classmethod + def home_pos(cls) -> NoReturn: + """ Set the cursor position to the home position (1,1). """ + cls._send('\033[H') + + @classmethod + def up(cls, value: int = 1) -> NoReturn: + """ Move the cursor up the given number of lines. + + :param value: number of lines to move the cursor up. 1 is the default value. + """ + cls._send(f'\033[{value}A') + + @classmethod + def down(cls, value=1) -> NoReturn: + """ Move the cursor down the given number of lines. + + :param value: number of lines to move the cursor down. 1 is the default value. + """ + cls._send(f'\033[{value}B') + + @classmethod + def right(cls, value=1) -> NoReturn: + """ Move the cursor right the given number of columns. + + :param value: number of columns to move the cursor right. 1 is the default value. + """ + cls._send(f'\033[{value}C') + + @classmethod + def left(cls, value=1) -> NoReturn: + cls._send(f'\033[{value}D') + + @classmethod + def save_cursor(cls) -> NoReturn: + """ Save the current cursor position. """ + cls._send('\0337') + # _send('\033[s') + + @classmethod + def restore_cursor(cls) -> NoReturn: + cls._send('\0338') + # _send('\033[u') + + @classmethod + def clear(cls) -> NoReturn: + cls._send('\033[2J') + + @classmethod + def clear_line_from_pos(cls) -> NoReturn: + cls._send('\033[K') + + @classmethod + def clear_line_to_pos(cls) -> NoReturn: + cls._send('\033[1K') + + @classmethod + def clear_line(cls) -> NoReturn: + """ Clear the current line.""" + cls._send('\033[2K') + + @classmethod + def write(cls, text: Text, *style) -> NoReturn: + """ Write text to the terminal. + + :param text: text to write to the terminal. + :param Iterable[Color] style: style to apply to the text. + """ + cls._send(cls.style(text, *style)) + + @classmethod + def write_line(cls, text: Text, *style) -> NoReturn: + """ Write a line of text to the terminal. + + :param text: text to write to the terminal. + :param Iterable[Color] style: style to apply to the text. + """ + cls.write(f'{text}\n', *style) + + @classmethod + def set_title(cls, name: Text) -> NoReturn: + """ Set the terminal title. + + :param name: title to set. + """ + cls._send(f'\033]2;{name}\007') + + @classmethod + def clear_title(cls) -> NoReturn: + cls.set_title('') + + @classmethod + def set_tab(cls, name: str) -> NoReturn: + cls._send(f'\033]1;{name}\007') + + @classmethod + def clear_tab(cls) -> NoReturn: + cls.set_tab('') + + @classmethod + def strip(cls, text) -> NoReturn: + return re.sub(r'\x1b\[[0-9]{1,2}m', '', text) + + @classmethod + def center(cls, text) -> NoReturn: + ''' + DEPRICATED: Use textCenter() instead! + Will be removed in later verions! + ''' + return ' ' * (int(cls.get_size()[1] / 2) - int(len(cls.strip(text)) / 2)) + text + + @classmethod + def text_right(cls, text: Text) -> Text: + """Align text to the right of the terminal. + + :param text: text to align + :return: aligned text + """ + _diff = cls.term_cols() - len(cls.strip(text)) + return str(' ' * _diff) + text + + # @classmethod + # def get_size(cls): + # + # os_sys = platform.system() + # if os_sys in ['Linux', 'Darwin'] or os_sys.startswith('CYGWIN'): + # try: + # def _get_unix_terminal_size(fd): + # import fcntl, termios, struct + # return struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'rene')) + # + # cr = _get_unix_terminal_size(0) or _get_unix_terminal_size(1) or _get_unix_terminal_size(2) + # if not cr: + # fd = os.open(os.ctermid(), os.O_RDONLY) + # cr = _get_unix_terminal_size(fd) + # os.close(fd) + # return cr + # except: + # pass + # else: + # raise Exception('operating system not supported') + + @classmethod + def term_cols(cls) -> int: + return shutil.get_terminal_size().columns + + @classmethod + def term_rows(cls) -> int: + return shutil.get_terminal_size().lines + + @classmethod + def get_size(cls) -> Tuple: + return cls.term_rows(), cls.term_cols() + + @classmethod + def style(cls, text: Text, *style) -> Text: + return f'{"".join(style)}{text}{cls.OFF}' if style else text + + @classmethod + def highligth(cls, pattern, text, *style) -> Text: + """Apply a style to all matches of a regular expression pattern in a text. + + :param pattern: regular expression pattern to style to. + :param text: text to apply the style to words matching the pattern. + :param style: style to apply to the words that match the pattern. + :return: text with text words matching the pattern styled. + """ + positions = [] + for m in re.finditer(pattern, text, flags=re.MULTILINE): + match = m + if m: + positions = [match.span()] + + if positions: + for pos in positions: + styled_text_range = cls.style(text[pos[0]:pos[1]], *style) + text = text[:pos[0]] + styled_text_range + text[pos[1]:] + + return text + + @classmethod + def color(cls, text: Text, *color: Color) -> Text: + return cls.style(text, *color) + + # @classmethod + # def colorize(cls, text: Text, color: str, *style) -> Text: + # return(cls.style(text, color, *style)) + # + + @classmethod + def red(cls, text: Text) -> Text: + """Format with color to a given text. + + :param text: text to apply the color to. + :return: colored text. + """ + return cls.color(text, cls.RED) + + @classmethod + def cyan(cls, text: Text) -> Text: + """Format with color to a given text. + + :param text: text to apply the color to. + :return: colored text. + """ + return cls.color(text, cls.CYAN) + + @classmethod + def yellow(cls, text: Text) -> Text: + """Format with color to a given text. + + :param text: text to apply the color to. + :return: colored text. + """ + return cls.color(text, cls.YELLOW) + + @classmethod + def magenta(cls, text: Text) -> Text: + """Format with color to a given text. + + :param text: text to apply the color to. + :return: colored text. + """ + return cls.color(text, cls.MAGENTA) + + @classmethod + def white(cls, text: Text) -> Text: + """Format with color to a given text. + + :param text: text to apply the color to. + :return: colored text. + """ + return cls.color(text, cls.WHITE) + + @classmethod + def black(cls, text: Text) -> Text: + """Format with color to a given text. + + :param text: text to apply the color to. + :return: colored text. + """ + return cls.color(text, cls.BLACK) + + @classmethod + def blue(cls, text: Text) -> Text: + """Format with color to a given text. + + :param text: text to apply the color to. + :return: colored text. + """ + return cls.color(text, cls.BLUE) + + @classmethod + def green(cls, text: Text) -> Text: + """Format with color to a given text. + + :param text: text to apply the color to. + :return: colored text. + """ + return cls.color(text, cls.GREEN) + + @classmethod + def dim(cls, text: Text) -> Text: + """Format with color to a given text. + + :param text: text to apply the color to. + :return: colored text. + """ + return cls.color(text, cls.DIM) + + @classmethod + def bold(cls, text: Text) -> Text: + """Format with color to a given text. + + :param text: text to apply the color to. + :return: colored text. + """ + return cls.color(text, cls.BOLD) + + def clear_lines(self, num: int = 1) -> NoReturn: + """Clear the current line.""" + for n in range(num): + self._send('\033[2K') + self.up() + + def save_pos(self) -> NoReturn: + """Save the current cursor position.""" + self._send('\033[s') + + def restore_pos(self) -> NoReturn: + """Restore the last saved cursor position.""" + self._send('\033[u') + + def home(self) -> NoReturn: + """Move the cursor to the home position.""" + self._send('\033[H') + + def hide_cursor(self) -> NoReturn: + """Hide the cursor.""" + self._send('\033[?25l') + + def show_cursor(self) -> NoReturn: + """Show the cursor.""" + self._send('\033[?25h') + + def underline(self, text: Text) -> Text: + """Underline the given text. + + :param text: text to underline. + :return: underlined text. + """ + return self.style(text, self.UNDERLINE) + + def blink(self, text: Text) -> Text: + """Blink the given text. + + :param text: text to blink. + :return: blinked text. + """ + return self.style(text, self.BLINK) + + def reverse(self, text: Text) -> Text: + """Reverse the given text. + + :param text: text to reverse. + :return: reversed text. + """ + return self.style(text, self.REVERSE) + + def set_icon(self, icon: Text) -> NoReturn: + """Set the terminal icon. + + :param icon: icon to set. + """ + self._send(f'\033]1;{icon}\007') + + def clear_icon(self) -> NoReturn: + """Clear the terminal icon.""" + self._send('\033]1;\007') + + def clear_to_end(self) -> NoReturn: + """Clear the screen from the current position to the end.""" + self._send('\033[0J') + + def clear_from_start(self) -> NoReturn: + """Clear the screen from the current position to the start.""" + self._send('\033[1J') + + def clear_line_to_end(self) -> NoReturn: + """Clear the current line from the current position to the end.""" + self._send('\033[0K') + + def clear_line_from_start(self) -> NoReturn: + """Clear the current line from the current position to the start.""" + self._send('\033[1K') diff --git a/pyterm/model.py b/pyterm/model.py new file mode 100644 index 0000000..a5950ab --- /dev/null +++ b/pyterm/model.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pyterm + + - File: pyterm/model.py + - Author: Havocesp + - Created: 2023-03-22 + - +""" + +from pyterm.constants import * + + +class Color: + """Color class for controlling the terminal color.""" + + BLACK = BLACK + RED = RED + GREEN = GREEN + YELLOW = YELLOW + BLUE = BLUE + MAGENTA = MAGENTA + CYAN = CYAN + WHITE = WHITE + + OFF = OFF + BOLD = BOLD + UNDERLINE = UNDERLINE + BLINK = BLINK + REVERSE = REVERSE + DIM = DIM diff --git a/pyterm/utils.py b/pyterm/utils.py new file mode 100644 index 0000000..6676d9a --- /dev/null +++ b/pyterm/utils.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pyterm + + - File: pyterm/utils.py + - Author: Havocesp + - Created: 2023-03-26 + - +""" + + +def get_caller_name() -> str: + """Get the caller name.""" + import inspect + return inspect.stack()[2][3] diff --git a/setup.py b/setup.py index 20910e3..e102323 100644 --- a/setup.py +++ b/setup.py @@ -1,53 +1,52 @@ -#!/usr/bin/env python - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- import sys +from pathlib import Path + try: - from setuptools import setup + from setuptools import setup, find_packages except ImportError: from distutils.core import setup -if sys.version_info < (2, 6): - raise NotImplementedError("Sorry, you need at least Python 2.6 or Python 3.2+ to use py-term.") +if sys.version_info < (3, 6): + raise NotImplementedError("Sorry, you need at least Python 3.6 to use py-term.") + +import pyterm -import term +readme_file = Path(__file__).parent / 'README.md' -with open('README.md', 'r') as f: - longDesc = f.read() +if readme_file.is_file(): + long_desc = readme_file.read_text() +else: + long_desc = pyterm.__doc__ -setup(name='py-term', - version=term.__version__, - description='Python module to style terminal output, moving and positioning the cursor.', - long_description=longDesc, - long_description_content_type='text/markdown', - author='Rene Tanczos', - author_email='gravmatt@gmail.com', - url='https://github.com/gravmatt/py-term', - py_modules=['term'], - scripts=['term.py'], - license='MIT', - platforms=['MacOSX', 'UNIX/Linux'], - classifiers=['Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2.5', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.1', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Intended Audience :: Developers', - 'Programming Language :: Python', - 'Development Status :: 5 - Production/Stable', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: POSIX', - 'Topic :: Terminals', - 'Environment :: Console' - ], - ) +setup( + name='pyterm', + version=pyterm.__version__, + description='Python module to style terminal output, moving and positioning the cursor.', + long_description=long_desc, + long_description_content_type='text/markdown', + author='Havocesp', + packages=find_packages(exclude=['tests', 'tests.*', 'examples', 'examples.*', 'docs', 'docs.*', 'venv', 'venv.*', 'build', 'build.*', 'dist', 'dist.*']), + url='https://github.com/havocesp/pyterm', + py_modules=['pyterm'], + scripts=['pyterm'], + license='MIT', + platforms=['MacOSX', 'UNIX/Linux'], + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Intended Audience :: Developers', + 'Programming Language :: Python', + 'Development Status :: 5 - Production/Stable', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: POSIX', + 'Topic :: Terminals', + 'Environment :: Console' + ], +) diff --git a/term.py b/term.py deleted file mode 100644 index d64dab0..0000000 --- a/term.py +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Copyright (c) 2015-2016, René Tanczos (Twitter @gravmatt) -The MIT License (MIT) - -pyterm helps positioning the cursor and styling output inside the terminal. - -Project on github https://github.com/gravmatt/py-term -""" - -__author__ = 'Rene Tanczos' -__version__ = '0.7' -__license__ = 'MIT' - -import sys -import re -import os -from subprocess import Popen, PIPE - - -off = '\033[0m\033[27m' -bold = '\033[1m' -dim = '\033[2m' -underscore = '\033[4m' -blink = '\033[5m' -reverse = ' \033[7m' -hide = '\033[8m' - -black = '\033[30m' -red = '\033[31m' -green = '\033[32m' -yellow = '\033[33m' -blue = '\033[34m' -magenta = '\033[35m' -cyan = '\033[36m' -white = '\033[37m' - -bgblack = '\033[40m' -bgred = '\033[41m' -bggreen = '\033[42m' -bgyellow = '\033[43m' -bgblue = '\033[44m' -bgmagenta = '\033[45m' -bgcyan = '\033[46m' -bgwhite = '\033[47m' - - -def send(cmd): - sys.stdout.write(cmd) - sys.stdout.flush() - - -def pos(line, column): - send('\033[%s;%sf' % (line, column)) - - -def homePos(): - send('\033[H') - - -def up(value=1): - send('\033[%sA' % value) - - -def down(value=1): - send('\033[%sB' % value) - - -def right(value=1): - send('\033[%sC' % value) - - -def left(value=1): - send('\033[%sD' % value) - - -def saveCursor(): - send('\0337') - # send('\033[s') - - -def restoreCursor(): - send('\0338') - # send('\033[u') - - -def clear(): - send('\033[2J') - - -def clearLineFromPos(): - send('\033[K') - - -def clearLineToPos(): - send('\033[1K') - - -def clearLine(): - send('\033[2K') - - -def write(text='', *style): - send(format(text, *style)) - - -def writeLine(text='', *style): - write(str(text) + '\n', *style) - - -def setTitle(name): - send('\033]2;%s\007' % name) - - -def clearTitle(): - setTitle('') - - -def setTab(name): - send('\033]1;%s\007' % name) - - -def clearTab(): - setTab('') - - -def strip(text): - return re.sub('\x1b\[[0-9]{1,2}m', '', text) - - -def textCenter(text): - return ' ' * (int(getSize()[1] / 2) - int(len(strip(text)) / 2)) + text - - -def center(text): - ''' - DEPRICATED: Use textCenter() instead! - Will be removed in later verions! - ''' - textCenter(text) - - -def textRight(text): - return ' ' * (getSize()[1] - len(strip(text))) + text - - -def getSize(): - import platform - os_sys = platform.system() - if(os_sys in ['Linux', 'Darwin'] or os_sys.startswith('CYGWIN')): - try: - def __get_unix_terminal_size(fd): - import fcntl, termios, struct - return struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'rene')) - cr = __get_unix_terminal_size(0) or __get_unix_terminal_size(1) or __get_unix_terminal_size(2) - if(not cr): - fd = os.open(os.ctermid(), os.O_RDONLY) - cr = __get_unix_terminal_size(fd) - os.close(fd) - return cr - except: - pass - else: - raise Exception('operating system not supported') - - -def format(text, *style): - if(style): - return '%s%s%s' % (''.join(style), text, off) - else: - return text - - -def highlight(pattern, text, func): - output = '' - idx = 0 - matches = [(m.start(), m.end()) for m in re.finditer(pattern, text)] - for p in matches: - output += text[idx:p[0]] - output += func(text[p[0]:p[1]]) - idx = p[1] - output += text[idx:] - return (output, len(matches), matches)