ttykit 0.2.2__tar.gz → 0.3.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ttykit
3
- Version: 0.2.2
3
+ Version: 0.3.5
4
4
  Summary: Helping made beatiful out in terminal
5
5
  Author: He-STALIN
6
6
  Author-email: hene.stalin@gmail.com
@@ -32,6 +32,7 @@ Keywords: tty,terminal,formatting
32
32
  Requires-Python: >=3.13
33
33
  Description-Content-Type: text/markdown
34
34
  License-File: LICENSE
35
+ Requires-Dist: keyboard
35
36
  Provides-Extra: test
36
37
  Requires-Dist: pytest; extra == "test"
37
38
  Dynamic: license-file
@@ -47,18 +48,23 @@ Dynamic: license-file
47
48
  [![Repository](https://img.shields.io/badge/Repository-GitHub-4b93ff?style=flat&logo=github&logoColor=white)](https://github.com/He-STALIN/ttykit)
48
49
 
49
50
  </div>
51
+
50
52
  ---
51
53
 
52
54
  ## 📦 For Start
55
+ ### Manually:
53
56
 
54
57
  ```bash
55
58
  git clone https://github.com/He-STALIN/ttykit.git
56
59
  ```
57
60
 
58
61
  - Change dir and exec
62
+
59
63
  ```bash
60
64
  pip install .
61
65
  ```
66
+ ### Auto
67
+ - exec `pip install ttykit`
62
68
 
63
69
  - And use in your projects!
64
70
 
@@ -9,18 +9,23 @@
9
9
  [![Repository](https://img.shields.io/badge/Repository-GitHub-4b93ff?style=flat&logo=github&logoColor=white)](https://github.com/He-STALIN/ttykit)
10
10
 
11
11
  </div>
12
+
12
13
  ---
13
14
 
14
15
  ## 📦 For Start
16
+ ### Manually:
15
17
 
16
18
  ```bash
17
19
  git clone https://github.com/He-STALIN/ttykit.git
18
20
  ```
19
21
 
20
22
  - Change dir and exec
23
+
21
24
  ```bash
22
25
  pip install .
23
26
  ```
27
+ ### Auto
28
+ - exec `pip install ttykit`
24
29
 
25
30
  - And use in your projects!
26
31
 
@@ -8,17 +8,17 @@ include = ["ttykit*"]
8
8
 
9
9
  [project]
10
10
  name = "ttykit"
11
- version = "0.2.2"
11
+ version = "0.3.5"
12
12
  description = "Helping made beatiful out in terminal"
13
13
  requires-python = ">=3.13"
14
14
  authors = [
15
- { name = "He-STALIN"},
15
+ {name = "He-STALIN"},
16
16
  {email = "hene.stalin@gmail.com"}
17
17
  ]
18
18
  license = {file = "LICENSE"}
19
19
  readme = "README.md"
20
20
  keywords = ['tty', 'terminal', 'formatting']
21
- dependencies = []
21
+ dependencies = ["keyboard"]
22
22
 
23
23
  [project.scripts]
24
24
  ttykit-spinner = "ttykit.spinner:main"
@@ -0,0 +1,69 @@
1
+ """Helping made beatiful out in terminal"""
2
+
3
+ from .progress import Progress
4
+ from .status import Status
5
+ from ._state import TaskState, Colors, Styles, RESET
6
+ from .console.console import Console
7
+ from .console.TUI import TUI
8
+ import traceback
9
+ import sys
10
+
11
+ __colors__ = [
12
+ Colors,
13
+ Styles,
14
+ RESET
15
+ ]
16
+
17
+ __all__ = [
18
+ 'Progress',
19
+ 'Status',
20
+ 'TaskState',
21
+ 'Console',
22
+ 'TUI',
23
+ 'get_console',
24
+ 'set_custom_hook'
25
+ ]
26
+
27
+ def get_console() -> 'Console':
28
+ """Return a instance of `Console`"""
29
+ console = Console()
30
+ return console
31
+
32
+ def custom_excepthook(exc_type, exc_value, exc_tb):
33
+ tb_lines = traceback.format_exception(exc_type, exc_value, exc_tb)
34
+
35
+ # Самая длинная строка
36
+ error_line = f"Error: {exc_type.__name__}"
37
+ msg_line = f"Message: {exc_value}"
38
+ max_len = max(len(error_line), len(msg_line), 40)
39
+
40
+ # Рамка
41
+ print(f"╔{'═' * (max_len + 4)}╗")
42
+ print(f"║ {Colors.RED}{error_line.ljust(max_len)}{RESET} ║")
43
+ print(f"║ {Colors.RED}{msg_line.ljust(max_len)}{RESET} ║")
44
+ print(f"╠{'═' * (max_len + 4)}╣")
45
+
46
+ for line in tb_lines[-3:]:
47
+ clean = line.strip()
48
+ if len(clean) > max_len:
49
+ clean = clean[:max_len-3] + "..."
50
+ print(f"║ {clean.ljust(max_len)} ║")
51
+
52
+ print(f"╚{'═' * (max_len + 4)}╝")
53
+
54
+ def set_custom_hook(Traceback: bool= False) -> None:
55
+ """
56
+ Set a custom methods
57
+
58
+ Args:
59
+ Traceback (bool): replace Traceback on custom or not. Default `False`
60
+ """
61
+
62
+ if Traceback:
63
+ print('set custom excepthook...')
64
+ sys.excepthook = custom_excepthook
65
+
66
+ __name__ = 'ttykit'
67
+ __author__ = 'He-STALIN'
68
+ __version__ = '0.3.5'
69
+ __license__ = 'MIT'
@@ -0,0 +1,148 @@
1
+ import keyboard as kb
2
+ import os, sys
3
+
4
+
5
+ class TUI:
6
+ """
7
+ Create TUI (Text User Interface) in terminal with user menus
8
+
9
+ Args:
10
+ title (str): title of the `TUI`
11
+
12
+ ### Methods:
13
+ `addMenu(name, callback)`: add menu to UI render.
14
+ `Run()`: exec and start render the UI.
15
+ `UpdateUI()`: forced update UI Layer.
16
+
17
+ ### Examples
18
+
19
+ ```python
20
+ from ttykit import TUI
21
+
22
+ ui = TUI("this example UI!")
23
+
24
+ def someAction():
25
+ # do something
26
+
27
+ ui.addMenu("Us menu", someAction) # adding menu in UI
28
+
29
+ ui.Run() # and start render UI
30
+ ```
31
+ Out in Terminal
32
+ ```
33
+ ===[ this example UI! ]===
34
+
35
+ > Us menu
36
+
37
+ > Exit
38
+
39
+ For control use arrows Up/Down and Enter to select
40
+ ```
41
+ """
42
+ def __init__(self, title: str):
43
+ super().__init__()
44
+ self.TITLE: str = title if title else "Text UI"
45
+ self.FOOTER: str = "For control use arrows Up/Down and Enter to select"
46
+ self.DEFAULT_SPACE = " "
47
+ self._requestClosing: bool = False
48
+ self.action: int = 1
49
+ self._max_menus: int = 0
50
+ self._menus: list = []
51
+ kb.add_hotkey("Up", self.UpMenu)
52
+ kb.add_hotkey("Down", self.DownMenu)
53
+ kb.add_hotkey("enter", self._selectAction)
54
+
55
+ def _clearTerminal(self):
56
+ os.system('cls' if os.name == 'nt' else 'clear')
57
+
58
+ def _selectAction(self):
59
+ if self.action == self._max_menus:
60
+ self._requestClosing = True
61
+ return
62
+
63
+ menu = self._menus[self.action - 1]
64
+ menu["callback"]()
65
+
66
+ def _UpMenu(self):
67
+ self.action -= 1
68
+
69
+ if self.action < 1:
70
+ self.action = self._max_menus
71
+
72
+ self.UpdateUI()
73
+
74
+ def _DownMenu(self):
75
+ self.action += 1
76
+
77
+ if self.action > self._max_menus:
78
+ self.action = 1
79
+
80
+ self.UpdateUI()
81
+
82
+ def UpdateUI(self):
83
+ """Update UI Layer in Terminal"""
84
+ TUI = ""
85
+ TUI += f"===[ {self.TITLE} ]==="
86
+ TUI += "\n\n"
87
+
88
+ _current_menu = 1
89
+ for menu in self._menus:
90
+ if self.action == _current_menu:
91
+ TUI += f"{self.DEFAULT_SPACE}\033[34m> {menu["name"]}\033[0m\n"
92
+ else:
93
+ TUI += f"{self.DEFAULT_SPACE}> {menu["name"]}\033[0m\n"
94
+
95
+ TUI += "\n"
96
+ _current_menu += 1
97
+
98
+ if len(self._menus) == self._max_menus:
99
+ self._max_menus += 1 #? add Exit menu in UI
100
+
101
+ if self.action == self._max_menus: #? exit menu always is last
102
+ TUI += f"{self.DEFAULT_SPACE}\033[31m> Exit\033[0m\n"
103
+ else:
104
+ TUI += f"{self.DEFAULT_SPACE}> Exit\033[0m\n"
105
+
106
+ TUI += "\n" + self.FOOTER + "\n"
107
+
108
+ sys.stdout.write("\033[H") #? set cursor to pos (0, 0)
109
+ sys.stdout.write(TUI)
110
+ sys.stdout.flush()
111
+
112
+ def addMenu(self, name: str, callback):
113
+ """
114
+ Adding menu to UI.
115
+
116
+ Args:
117
+ name (str): name of the menu
118
+ callback: what need to call, when menu selected
119
+ """
120
+ try:
121
+ self._menus.append({
122
+ "name": name,
123
+ "callback": callback
124
+ })
125
+ self._max_menus += 1
126
+ except Exception as e:
127
+ raise Exception(e)
128
+
129
+ def Run(self):
130
+ if len(self._menus) < 1:
131
+ raise ValueError("UI can't has been created without menus")
132
+
133
+ try:
134
+ self._clearTerminal()
135
+ self.UpdateUI()
136
+ while True:
137
+ if self._requestClosing:
138
+ sys.exit()
139
+ break
140
+ else:
141
+ pass
142
+
143
+ except KeyboardInterrupt:
144
+ sys.stdout.write("[!] Exited by user\n")
145
+ sys.stdout.flush()
146
+
147
+ except Exception as e:
148
+ raise Exception(e)
@@ -0,0 +1,235 @@
1
+ import sys
2
+ import os
3
+ from typing import Optional, Literal, Mapping, Any
4
+ from ttykit import Colors, Styles
5
+
6
+ from .const import ColorSystem, WINDOWS
7
+
8
+ class Console:
9
+ """
10
+ Class for controling terminal events
11
+
12
+ Args:
13
+ color_system (str, Optional): Color system of the terminal. Either `standard`, `256` or `truecolor`. Leave as `auto` to autodetect.
14
+ width (int): width of the terminal. If state is `None`, calculates it automatically.
15
+ height (int): height of the terminal. If state is `None`, calculates it automatically.
16
+ stderr (bool): use `stderr` for errors instead of stdout. Default `False`.
17
+ no_color (bool): if `True`, the terminal be without colors, only monochrome (white and black). Default `False`.
18
+ force_terminal (bool): if `True` use terminal control codes in any situation.
19
+ """
20
+
21
+ _environ: Mapping[str, str] = os.environ
22
+
23
+ def __init__(self,
24
+ color_system: Optional[Literal["auto", "standard", "256", "truecolor", "windows"]] = "auto",
25
+ width: int = None,
26
+ height: int = None,
27
+ stderr: bool = False,
28
+ no_color: bool = False,
29
+ force_terminal: Optional[bool] = False
30
+ ):
31
+ super().__init__()
32
+
33
+ #? preparing to avoid any errors
34
+ self._color_system = Optional[ColorSystem]
35
+ self._force_terminal = None
36
+ self.stderr = stderr
37
+ self.no_color = no_color
38
+ self.height = None
39
+ self.width = None
40
+
41
+ if color_system is None:
42
+ self._color_system = None
43
+ elif color_system == "auto":
44
+ self._color_system = self._detect_colorSystem()
45
+
46
+ if force_terminal is not None:
47
+ self._force_terminal = force_terminal
48
+
49
+ if width is None:
50
+ try:
51
+ size = os.get_terminal_size()
52
+ width = size.columns
53
+ except OSError:
54
+ print('Error calculating width of the terminal. Use default value (80)')
55
+ width = 80
56
+
57
+ if height is None:
58
+ try:
59
+ size = os.get_terminal_size()
60
+ height = size.lines
61
+ except OSError:
62
+ print('Error calculating height of the terminal. Use default value (24)')
63
+ height
64
+
65
+ def _print_error(self, msg: str=None):
66
+ if msg is None:
67
+ return
68
+
69
+ if self.stderr:
70
+ sys.stderr.write(msg)
71
+
72
+ @property
73
+ def isTerminal(self) -> bool:
74
+ """
75
+ Check if the console writing to a terminal
76
+
77
+ Returns:
78
+ state (bool): return `True` if console is understanding escape sequences, otherwise `False`
79
+ """
80
+ if self._force_terminal is not None:
81
+ return True
82
+
83
+ if hasattr(sys.stdin, "__module__") and sys.stdin.__module__.startswith("idlelib"):
84
+ return False #? return False for idle which claims to be a tty but can't handle ANSI codes
85
+
86
+ ttyCompatyble = self._environ.get('TTY_COMPATIBLE', '')
87
+ if ttyCompatyble == '0': #? 0 = device is not tty compatible
88
+ return False
89
+ elif ttyCompatyble =='1': #? 1 = device is tty compatible
90
+ return True
91
+
92
+ if not sys.stdout.isatty():
93
+ return False
94
+
95
+
96
+ @property
97
+ def is_dumb_terminal(self) -> bool:
98
+ """Detect dumb terminal.
99
+
100
+ Returns:
101
+ state (bool): `True` if writing to a dumb terminal, otherwise `False`.
102
+
103
+ """
104
+ is_dumb = self._environ.get("TERM", "").lower() in ("dumb", "unknown")
105
+ return self.isTerminal and is_dumb
106
+
107
+
108
+ @property
109
+ def color_system(self):
110
+ return self._detect_colorSystem()
111
+
112
+
113
+ def _detect_colorSystem(self) -> Optional[ColorSystem]:
114
+ """Autodetect the supported color theme fo the terminal"""
115
+
116
+ if self.no_color:
117
+ return None
118
+
119
+ if WINDOWS:
120
+ # TODO: add check for legacy windows
121
+
122
+ if self._environ.get('COLORTERM') in ('truecolor', '24bit'):
123
+ return ColorSystem.TRUECOLOR
124
+ return ColorSystem.WINDOWS
125
+ else:
126
+ if self._environ.get('COLORTERM') in ('truecolor', '24bit'):
127
+ return ColorSystem.TRUECOLOR
128
+
129
+ if self._environ.get("TERM") in ("xterm-256color", "screen-256color"):
130
+ return ColorSystem.EIGHT_BIT
131
+ return ColorSystem.STANDARD
132
+
133
+ def _get_style_code(self, style: str) -> str:
134
+ styles = {
135
+ 'bold': Styles.BOLD,
136
+ 'dim': Styles.DIM,
137
+ 'italic': Styles.ITALIC,
138
+ 'underline': Styles.UNDERLINE,
139
+ 'blink': Styles.BLINK,
140
+ 'rapid_blink': Styles.RAPID_BLINK,
141
+ 'reverse': Styles.REVERSE
142
+ }
143
+ return styles.get(style, '')
144
+
145
+ def _get_color_code(self, color: str) -> str:
146
+ colors = {
147
+ "black": Colors.BLACK,
148
+ "red": Colors.RED,
149
+ "green": Colors.GREEN,
150
+ "yellow": Colors.YELLOW,
151
+ "blue": Colors.BLUE,
152
+ "purple": Colors.PURPLE,
153
+ "cyan": Colors.CYAN,
154
+ "white": Colors.WHITE,
155
+ "dark_grey": Colors.DARK_GREY,
156
+ "light_red": Colors.LIGHT_RED,
157
+ "light_green": Colors.LIGHT_GREEN,
158
+ "light_yellow": Colors.LIGHT_YELLOW,
159
+ "light_blue": Colors.LIGHT_BLUE,
160
+ "light_purple": Colors.LIGHT_PURPLE,
161
+ "light_cyan": Colors.LIGHT_CYAN,
162
+ "light_white": Colors.LIGHT_WHITE
163
+ }
164
+ return colors.get(color, "")
165
+
166
+ def bell(self):
167
+ sys.stdout.write("\a")
168
+ sys.stdout.flush()
169
+
170
+ def print(self,
171
+ *args,
172
+ sep: str | None=" ",
173
+ end: str | None ="\n",
174
+ style: str | None=None,
175
+ color: str | None=None) -> None:
176
+ """
177
+ Print the args to a stream
178
+
179
+ Args:
180
+ *args (Any): args to write to a stream
181
+ sep (str | None): string inserted between values, default a space.
182
+ end (str | None): string appended after the last value, default a newline.
183
+ style: style of args write to a steam, default `None`.
184
+ color: color of args write to a stream, default `None`.
185
+ Returns:
186
+ None: write in `stdout`
187
+ """
188
+ if not self.isTerminal or self.no_color:
189
+ #? Just out, without colors or styles
190
+ sys.stdout.write(sep.join(str(a) for a in args) + end)
191
+ return
192
+
193
+ #TODO: realize print logic with color style replace
194
+
195
+ #? Apply the style with color to args
196
+ text = sep.join(str(a) for a in args)
197
+ if color and self.color_system:
198
+ color_code = self._get_color_code(color.lower())
199
+ text = f"{color_code}{text}{Colors.RESET}"
200
+
201
+ if style:
202
+ style_code = self._get_style_code(style.lower())
203
+ text = f"{style_code}{text}"
204
+
205
+ sys.stdout.write(text + end)
206
+ sys.stdout.flush()
207
+
208
+
209
+ def input(self,
210
+ prompt = '',
211
+ *,
212
+ style: str | None = None,
213
+ color: str | None = None,
214
+ password: bool = False) -> str:
215
+ """
216
+ Displays a prompt if have and waits for input from user.
217
+
218
+ Args:
219
+ prompt: text to render in prompt
220
+ style (str | None): style for prompt render. Required prompt
221
+ color (str | None): color for prompt render. Required prompt
222
+ password (bool): if `True`, hide typed text. Default `False`
223
+ Returns:
224
+ result (str): typed text from user from stdin
225
+ """
226
+
227
+ if prompt:
228
+ self.print(prompt, style=style, color=color, end='')
229
+
230
+ if password:
231
+ # TODO: realize text hidding
232
+ Warning("This attribute in developing. Please, set 'password=False'")
233
+ else:
234
+ result = input()
235
+ return result
@@ -0,0 +1,18 @@
1
+ from enum import IntEnum
2
+ import sys
3
+
4
+ WINDOWS = sys.platform == "win32"
5
+
6
+ class ColorSystem(IntEnum):
7
+ """One of the 3 color system supported by terminals."""
8
+
9
+ STANDARD = 1
10
+ EIGHT_BIT = 2
11
+ TRUECOLOR = 3
12
+ WINDOWS = 4
13
+
14
+ def __repr__(self) -> str:
15
+ return f"ColorSystem.{self.name}"
16
+
17
+ def __str__(self) -> str:
18
+ return repr(self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ttykit
3
- Version: 0.2.2
3
+ Version: 0.3.5
4
4
  Summary: Helping made beatiful out in terminal
5
5
  Author: He-STALIN
6
6
  Author-email: hene.stalin@gmail.com
@@ -32,6 +32,7 @@ Keywords: tty,terminal,formatting
32
32
  Requires-Python: >=3.13
33
33
  Description-Content-Type: text/markdown
34
34
  License-File: LICENSE
35
+ Requires-Dist: keyboard
35
36
  Provides-Extra: test
36
37
  Requires-Dist: pytest; extra == "test"
37
38
  Dynamic: license-file
@@ -47,18 +48,23 @@ Dynamic: license-file
47
48
  [![Repository](https://img.shields.io/badge/Repository-GitHub-4b93ff?style=flat&logo=github&logoColor=white)](https://github.com/He-STALIN/ttykit)
48
49
 
49
50
  </div>
51
+
50
52
  ---
51
53
 
52
54
  ## 📦 For Start
55
+ ### Manually:
53
56
 
54
57
  ```bash
55
58
  git clone https://github.com/He-STALIN/ttykit.git
56
59
  ```
57
60
 
58
61
  - Change dir and exec
62
+
59
63
  ```bash
60
64
  pip install .
61
65
  ```
66
+ ### Auto
67
+ - exec `pip install ttykit`
62
68
 
63
69
  - And use in your projects!
64
70
 
@@ -11,4 +11,7 @@ src/ttykit.egg-info/SOURCES.txt
11
11
  src/ttykit.egg-info/dependency_links.txt
12
12
  src/ttykit.egg-info/entry_points.txt
13
13
  src/ttykit.egg-info/requires.txt
14
- src/ttykit.egg-info/top_level.txt
14
+ src/ttykit.egg-info/top_level.txt
15
+ src/ttykit/console/TUI.py
16
+ src/ttykit/console/console.py
17
+ src/ttykit/console/const.py
@@ -1,3 +1,4 @@
1
+ keyboard
1
2
 
2
3
  [test]
3
4
  pytest
@@ -1,22 +0,0 @@
1
- """Helping made beatiful out in terminal"""
2
-
3
- from .progress import Progress
4
- from .status import Status
5
- from ._state import TaskState, Colors, Styles, RESET
6
-
7
- __colors__ = [
8
- Colors,
9
- Styles,
10
- RESET
11
- ]
12
-
13
- __all__ = [
14
- 'Progress',
15
- 'Status',
16
- 'TaskState'
17
- ]
18
-
19
- __name__ = 'ttykit'
20
- __author__ = 'He-STALIN'
21
- __version__ = '0.2.2'
22
- __license__ = 'MIT'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes