prettier-console 0.1.0__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.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: prettier_console
3
+ Version: 0.1.0
4
+ Summary: Provide you a automation to construct a prettier interactive console in one call
5
+ Author-email: MrCosine <a95sun@uwaterloo.ca>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mr-cosine/prettier_console
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: keyboard>=0.13.5
13
+
14
+ So yea some shortcuts for you to get a prettier interactive console. Um I think its good for lazy people. If you enjoy you enjoy and if you don't...you don't.
15
+ Will update this readme later on... I guess.
16
+
17
+ What you can do:
18
+ 1. using interactive.menu() to create menus with single selections
19
+ 2. Customize your page by inserting interactive parts using tools such as print_selections and print_yesorno.
20
+ 3. draw ascii-art banners and headers
21
+ 4. get colored outputs
22
+
23
+ dependencies:
24
+ build==1.5.0
25
+ colorama==0.4.6
26
+ keyboard==0.13.5
27
+ packaging==26.2
28
+ pyproject_hooks==1.2.0
@@ -0,0 +1,15 @@
1
+ So yea some shortcuts for you to get a prettier interactive console. Um I think its good for lazy people. If you enjoy you enjoy and if you don't...you don't.
2
+ Will update this readme later on... I guess.
3
+
4
+ What you can do:
5
+ 1. using interactive.menu() to create menus with single selections
6
+ 2. Customize your page by inserting interactive parts using tools such as print_selections and print_yesorno.
7
+ 3. draw ascii-art banners and headers
8
+ 4. get colored outputs
9
+
10
+ dependencies:
11
+ build==1.5.0
12
+ colorama==0.4.6
13
+ keyboard==0.13.5
14
+ packaging==26.2
15
+ pyproject_hooks==1.2.0
@@ -0,0 +1,16 @@
1
+ from .interactive import (
2
+ colored_output,
3
+ default_colored_output,
4
+ display_width,
5
+ safe_input,
6
+ clear_screen,
7
+ select_files,
8
+ select_folder,
9
+ print_selections,
10
+ print_yesorno,
11
+ print_banner,
12
+ print_header,
13
+ menu,
14
+ home_menu,
15
+ quit_program
16
+ )
@@ -0,0 +1,8 @@
1
+ from .ascii_art_font import (
2
+ get_character,
3
+ build_display,
4
+ default_banner,
5
+ default_header,
6
+ parse_font_file,
7
+ set_cset,
8
+ )
@@ -0,0 +1,128 @@
1
+ import json
2
+ import string
3
+ import os
4
+
5
+ def _get_cset(style):
6
+ script_dir = os.path.dirname(os.path.abspath(__file__))
7
+ try:
8
+ cset = json.load(open(os.path.join(script_dir, 'font', f'{style}.json'), 'r', encoding='utf-8'))
9
+ except FileNotFoundError:
10
+ raise Exception(f"Invalid style {style}: not supported by the font library")
11
+ except Exception as e:
12
+ raise Exception(f"Error loading style {style}: {str(e)}")
13
+ return cset
14
+
15
+ def get_character(char, style):
16
+ if len(char) > 1 or not isinstance(char, str): raise Exception("Chracter invalid: must be a character")
17
+ char = char.upper()
18
+
19
+ cset = _get_cset(style)
20
+ output_char = cset.get(char, None)
21
+ if output_char is None:
22
+ missing = 'space' if char == ' ' else char
23
+ raise Exception(f'File error: incomplete ascii character library. Missing character: {missing}')
24
+
25
+ return output_char
26
+
27
+ def build_display(input_string, style, delim=''):
28
+ if len(input_string) < 1: input_string = ' '
29
+
30
+ all_characters = set(input_string.upper())
31
+ cset = {}
32
+ for c in all_characters: cset.update({c: get_character(c, style)})
33
+
34
+ print_string = [c.upper() for c in input_string]
35
+ lines = []
36
+ line = 1
37
+ while all(cset.get(c).get(str(line), None) is not None for c in cset):
38
+ lines.append(delim.join(cset.get(char).get(str(line)) for char in print_string))
39
+ line += 1
40
+
41
+ width = len(lines[0]) if lines else 0
42
+ return '\n'.join(lines), width
43
+
44
+ def parse_font_file(path):
45
+ """
46
+ parse a text file into a character set dictionary for a font style.
47
+
48
+ each line is one row of the glyphs: the 26 letters (A-Z) followed by the
49
+ space character, each segment separated by '/'.
50
+
51
+ :param path: string, path to the font text file
52
+
53
+ :return: dict, {char: {'1': string, '2': string, ...}}
54
+ """
55
+ with open(path, 'r', encoding='utf-8') as f:
56
+ lines = f.read().splitlines()
57
+
58
+ if len(lines) == 0:
59
+ raise Exception('Font file error: file is empty')
60
+
61
+ characters = list(string.ascii_uppercase) + [' ']
62
+ cset = {character: {} for character in characters}
63
+
64
+ for i, line in enumerate(lines):
65
+ segments = line.split('/')
66
+ if len(segments) != len(characters):
67
+ raise Exception(
68
+ f"Font file error: line {i + 1} has {len(segments)} '/'-separated segments, "
69
+ f'expected {len(characters)} (A-Z + space).'
70
+ )
71
+ for character, segment in zip(characters, segments):
72
+ cset[character][str(i + 1)] = segment
73
+
74
+ return cset
75
+
76
+ def set_cset(style, font_path):
77
+ """
78
+ parse a font text file and add or replace a style under ascii_art_font/font/.
79
+
80
+ :param style: string, the style key to save the parsed font under
81
+ :param font_path: string, path to the font text file
82
+ """
83
+ script_dir = os.path.dirname(os.path.abspath(__file__))
84
+ font_dir = os.path.join(script_dir, 'font')
85
+ os.makedirs(font_dir, exist_ok=True)
86
+ json_path = os.path.join(font_dir, f'{style}.json')
87
+
88
+ data = parse_font_file(font_path)
89
+
90
+ with open(json_path, 'w', encoding='utf-8') as f:
91
+ json.dump(data, f, ensure_ascii=False, indent=4)
92
+
93
+ def _build_default_display(input_string, style):
94
+ """
95
+ build the rendered content of a default style, underlined by a separator.
96
+
97
+ :param input_string: string, the text to render, '\\n' splits it into rows
98
+ :param style: string, the font style to render with
99
+
100
+ :return: string, the rendered content, rows joined by '\\n'
101
+ """
102
+ lines = []
103
+ length = 0
104
+ for line in input_string.split('\n'):
105
+ display_content, length = build_display(line, style, delim=' ')
106
+ lines.append(display_content)
107
+ lines.append('='*length)
108
+ return '\n'.join(lines)
109
+
110
+ def default_banner(input_string):
111
+ """
112
+ build the banner content.
113
+
114
+ :param input_string: string, the text to render
115
+
116
+ :return: string, the rendered banner content
117
+ """
118
+ return _build_default_display(input_string, 'banner')
119
+
120
+ def default_header(input_string):
121
+ """
122
+ build the header content.
123
+
124
+ :param input_string: string, the text to render
125
+
126
+ :return: string, the rendered header content
127
+ """
128
+ return _build_default_display(input_string, 'header')
@@ -0,0 +1,218 @@
1
+ {
2
+ "A": {
3
+ "1": " █████╗ ",
4
+ "2": "██╔══██╗",
5
+ "3": "███████║",
6
+ "4": "██╔══██║",
7
+ "5": "██║ ██║",
8
+ "6": "╚═╝ ╚═╝"
9
+ },
10
+ "B": {
11
+ "1": "██████╗ ",
12
+ "2": "██╔══██╗",
13
+ "3": "██████╔╝",
14
+ "4": "██╔══██╗",
15
+ "5": "██████╔╝",
16
+ "6": "╚═════╝ "
17
+ },
18
+ "C": {
19
+ "1": " ██████╗",
20
+ "2": "██╔════╝",
21
+ "3": "██║ ",
22
+ "4": "██║ ",
23
+ "5": "╚██████╗",
24
+ "6": " ╚═════╝"
25
+ },
26
+ "D": {
27
+ "1": "██████╗ ",
28
+ "2": "██╔══██╗",
29
+ "3": "██║ ██║",
30
+ "4": "██║ ██║",
31
+ "5": "██████╔╝",
32
+ "6": "╚═════╝ "
33
+ },
34
+ "E": {
35
+ "1": "███████╗",
36
+ "2": "██╔════╝",
37
+ "3": "█████╗ ",
38
+ "4": "██╔══╝ ",
39
+ "5": "███████╗",
40
+ "6": "╚══════╝"
41
+ },
42
+ "F": {
43
+ "1": "███████╗",
44
+ "2": "██╔════╝",
45
+ "3": "█████╗ ",
46
+ "4": "██╔══╝ ",
47
+ "5": "██║ ",
48
+ "6": "╚═╝ "
49
+ },
50
+ "G": {
51
+ "1": " ██████╗ ",
52
+ "2": "██╔════╝ ",
53
+ "3": "██║ ███╗",
54
+ "4": "██║ ██║",
55
+ "5": "╚██████╔╝",
56
+ "6": " ╚═════╝ "
57
+ },
58
+ "H": {
59
+ "1": "██╗ ██╗",
60
+ "2": "██║ ██║",
61
+ "3": "███████║",
62
+ "4": "██╔══██║",
63
+ "5": "██║ ██║",
64
+ "6": "╚═╝ ╚═╝"
65
+ },
66
+ "I": {
67
+ "1": "████╗",
68
+ "2": "╚██╔╝",
69
+ "3": " ██║ ",
70
+ "4": " ██║ ",
71
+ "5": "████╗",
72
+ "6": "╚═══╝"
73
+ },
74
+ "J": {
75
+ "1": "██╗ ",
76
+ "2": "██║ ",
77
+ "3": "██║ ",
78
+ "4": "██║ ██║",
79
+ "5": "╚█████╔╝",
80
+ "6": " ╚════╝ "
81
+ },
82
+ "K": {
83
+ "1": "██╗ ██╗",
84
+ "2": "██║ ██╔╝",
85
+ "3": "█████╔╝ ",
86
+ "4": "██╔═██╗ ",
87
+ "5": "██║ ╚██╗",
88
+ "6": "╚═╝ ╚═╝"
89
+ },
90
+ "L": {
91
+ "1": "██╗ ",
92
+ "2": "██║ ",
93
+ "3": "██║ ",
94
+ "4": "██║ ",
95
+ "5": "███████╗",
96
+ "6": "╚══════╝"
97
+ },
98
+ "M": {
99
+ "1": "███╗ ███╗",
100
+ "2": "████╗ ████║",
101
+ "3": "██╔████╔██║",
102
+ "4": "██║╚██╔╝██║",
103
+ "5": "██║ ╚═╝ ██║",
104
+ "6": "╚═╝ ╚═╝"
105
+ },
106
+ "N": {
107
+ "1": "███╗ ██╗",
108
+ "2": "████╗ ██║",
109
+ "3": "██╔██╗ ██║",
110
+ "4": "██║╚██╗██║",
111
+ "5": "██║ ╚████║",
112
+ "6": "╚═╝ ╚═══╝"
113
+ },
114
+ "O": {
115
+ "1": " ██████╗ ",
116
+ "2": "██╔═══██╗",
117
+ "3": "██║ ██║",
118
+ "4": "██║ ██║",
119
+ "5": "╚██████╔╝",
120
+ "6": " ╚═════╝ "
121
+ },
122
+ "P": {
123
+ "1": "██████╗ ",
124
+ "2": "██╔══██╗",
125
+ "3": "██████╔╝",
126
+ "4": "██╔═══╝ ",
127
+ "5": "██║ ",
128
+ "6": "╚═╝ "
129
+ },
130
+ "Q": {
131
+ "1": " ██████╗ ",
132
+ "2": "██╔═══██╗",
133
+ "3": "██║ ██║",
134
+ "4": "██║▄▄ ██║",
135
+ "5": "╚██████╔╝",
136
+ "6": " ╚══▀▀═╝ "
137
+ },
138
+ "R": {
139
+ "1": "██████╗ ",
140
+ "2": "██╔══██╗",
141
+ "3": "██████╔╝",
142
+ "4": "██╔══██╗",
143
+ "5": "██║ ██║",
144
+ "6": "╚═╝ ╚═╝"
145
+ },
146
+ "S": {
147
+ "1": "███████╗",
148
+ "2": "██╔════╝",
149
+ "3": "███████╗",
150
+ "4": "╚════██║",
151
+ "5": "███████║",
152
+ "6": "╚══════╝"
153
+ },
154
+ "T": {
155
+ "1": "████████╗",
156
+ "2": "╚══██╔══╝",
157
+ "3": " ██║ ",
158
+ "4": " ██║ ",
159
+ "5": " ██║ ",
160
+ "6": " ╚═╝ "
161
+ },
162
+ "U": {
163
+ "1": "██╗ ██╗",
164
+ "2": "██║ ██║",
165
+ "3": "██║ ██║",
166
+ "4": "██║ ██║",
167
+ "5": "╚██████╔╝",
168
+ "6": " ╚═════╝ "
169
+ },
170
+ "V": {
171
+ "1": "██╗ ██╗",
172
+ "2": "██║ ██║",
173
+ "3": "██║ ██║",
174
+ "4": "╚██╗ ██╔╝",
175
+ "5": " ╚████╔╝ ",
176
+ "6": " ╚═══╝ "
177
+ },
178
+ "W": {
179
+ "1": "██╗ ██╗",
180
+ "2": "██║ ██║",
181
+ "3": "██║ █╗ ██║",
182
+ "4": "██║███╗██║",
183
+ "5": "╚███╔███╔╝",
184
+ "6": " ╚══╝╚══╝ "
185
+ },
186
+ "X": {
187
+ "1": "██╗ ██╗",
188
+ "2": "╚██╗██╔╝",
189
+ "3": " ╚███╔╝ ",
190
+ "4": " ██╔██╗ ",
191
+ "5": "██╔╝ ██╗",
192
+ "6": "╚═╝ ╚═╝"
193
+ },
194
+ "Y": {
195
+ "1": "██╗ ██╗",
196
+ "2": "╚██╗ ██╔╝",
197
+ "3": " ╚████╔╝ ",
198
+ "4": " ╚██╔╝ ",
199
+ "5": " ██║ ",
200
+ "6": " ╚═╝ "
201
+ },
202
+ "Z": {
203
+ "1": "███████╗",
204
+ "2": "╚══███╔╝",
205
+ "3": " ███╔╝ ",
206
+ "4": " ███╔╝ ",
207
+ "5": "███████╗",
208
+ "6": "╚══════╝"
209
+ },
210
+ " ": {
211
+ "1": " ",
212
+ "2": " ",
213
+ "3": " ",
214
+ "4": " ",
215
+ "5": " ",
216
+ "6": " "
217
+ }
218
+ }
@@ -0,0 +1,137 @@
1
+ {
2
+ "A": {
3
+ "1": "┏━┓",
4
+ "2": "┣━┫",
5
+ "3": "┛ ┗"
6
+ },
7
+ "B": {
8
+ "1": "┳━┓",
9
+ "2": "┣━┫",
10
+ "3": "┻━┛"
11
+ },
12
+ "C": {
13
+ "1": "┏━┓",
14
+ "2": "┃ ",
15
+ "3": "┗━┛"
16
+ },
17
+ "D": {
18
+ "1": "┳━┓",
19
+ "2": "┃ ┃",
20
+ "3": "┻━┛"
21
+ },
22
+ "E": {
23
+ "1": "┏━┓",
24
+ "2": "┣━ ",
25
+ "3": "┗━┛"
26
+ },
27
+ "F": {
28
+ "1": "┏━┓",
29
+ "2": "┣━ ",
30
+ "3": "┻ "
31
+ },
32
+ "G": {
33
+ "1": "┏━┓",
34
+ "2": "┃┏┓",
35
+ "3": "┗━┛"
36
+ },
37
+ "H": {
38
+ "1": "┓ ┏",
39
+ "2": "┣━┫",
40
+ "3": "┛ ┗"
41
+ },
42
+ "I": {
43
+ "1": " ┳ ",
44
+ "2": " ┃ ",
45
+ "3": " ┻ "
46
+ },
47
+ "J": {
48
+ "1": " ┏┳",
49
+ "2": " ┃",
50
+ "3": "┗━┛"
51
+ },
52
+ "K": {
53
+ "1": "┓┏┓",
54
+ "2": "┣┫ ",
55
+ "3": "┛┗┛"
56
+ },
57
+ "L": {
58
+ "1": "┓ ",
59
+ "2": "┃ ",
60
+ "3": "┗━┛"
61
+ },
62
+ "M": {
63
+ "1": "┳┓┓",
64
+ "2": "┃┃┃",
65
+ "3": "┛ ┗"
66
+ },
67
+ "N": {
68
+ "1": "┳━┓",
69
+ "2": "┃ ┃",
70
+ "3": "┛ ┗"
71
+ },
72
+ "O": {
73
+ "1": "┏━┓",
74
+ "2": "┃ ┃",
75
+ "3": "┗━┛"
76
+ },
77
+ "P": {
78
+ "1": "┳━┓",
79
+ "2": "┣━┛",
80
+ "3": "┻ "
81
+ },
82
+ "Q": {
83
+ "1": "┏━┓",
84
+ "2": "┃ ┃",
85
+ "3": "┗━┻"
86
+ },
87
+ "R": {
88
+ "1": "┳━┓",
89
+ "2": "┣┳┛",
90
+ "3": "┛┗┛"
91
+ },
92
+ "S": {
93
+ "1": "┏━┓",
94
+ "2": "┗━┓",
95
+ "3": "┗━┛"
96
+ },
97
+ "T": {
98
+ "1": "┏┳┓",
99
+ "2": " ┃ ",
100
+ "3": " ┻ "
101
+ },
102
+ "U": {
103
+ "1": "┳ ┳",
104
+ "2": "┃ ┃",
105
+ "3": "┗━┛"
106
+ },
107
+ "V": {
108
+ "1": "┓ ┏",
109
+ "2": "┃┏┛",
110
+ "3": "┗┛ "
111
+ },
112
+ "W": {
113
+ "1": "┓ ┏",
114
+ "2": "┃┃┃",
115
+ "3": "┗┻┛"
116
+ },
117
+ "X": {
118
+ "1": "┏┓┏━",
119
+ "2": " ┣┫ ",
120
+ "3": "━┛┗┛"
121
+ },
122
+ "Y": {
123
+ "1": "┓ ┏",
124
+ "2": "┗━┫",
125
+ "3": "┗━┛"
126
+ },
127
+ "Z": {
128
+ "1": "━━┓",
129
+ "2": "┏┛ ",
130
+ "3": "┗━┛"
131
+ },
132
+ " ": {
133
+ "1": " ",
134
+ "2": " ",
135
+ "3": " "
136
+ }
137
+ }
@@ -0,0 +1,408 @@
1
+ """
2
+ dedicated tools to create interactive UI in command line window
3
+ """
4
+
5
+ import tkinter
6
+ import tkinter.filedialog
7
+ import subprocess
8
+ import os
9
+ import sys
10
+ import keyboard
11
+ from .ascii_art_font import ascii_art_font as ascii_art
12
+
13
+ #----------------------------------------------------------------------------------------------------------------------------------
14
+ class colored_output:
15
+ """
16
+ Colors: black, red, green, yellow, blue, magenta, cyan, white.
17
+ """
18
+
19
+ _NORMAL_FG = {
20
+ 'BLACK': 30, 'RED': 31, 'GREEN': 32, 'YELLOW': 33,
21
+ 'BLUE': 34, 'MAGENTA': 35, 'CYAN': 36, 'WHITE': 37
22
+ }
23
+ _NORMAL_BG = {
24
+ 'BLACK': 40, 'RED': 41, 'GREEN': 42, 'YELLOW': 43,
25
+ 'BLUE': 44, 'MAGENTA': 45, 'CYAN': 46, 'WHITE': 47
26
+ }
27
+
28
+ _BRIGHT_FG = {
29
+ 'BLACK': 90, 'RED': 91, 'GREEN': 92, 'YELLOW': 93,
30
+ 'BLUE': 94, 'MAGENTA': 95, 'CYAN': 96, 'WHITE': 97
31
+ }
32
+ _BRIGHT_BG = {
33
+ 'BLACK': 100, 'RED': 101, 'GREEN': 102, 'YELLOW': 103,
34
+ 'BLUE': 104, 'MAGENTA': 105, 'CYAN': 106, 'WHITE': 107
35
+ }
36
+
37
+ def __init__(self, bright=False):
38
+ """
39
+ :param bright: boolean, True = bright text
40
+ """
41
+ self._bright = bright
42
+ if bright:
43
+ self._fg_map = self._BRIGHT_FG
44
+ self._bg_map = self._BRIGHT_BG
45
+ else:
46
+ self._fg_map = self._NORMAL_FG
47
+ self._bg_map = self._NORMAL_BG
48
+
49
+ self._RESET = "\033[0m"
50
+
51
+ def _get_escape(self, fg_color=None, bg_color=None):
52
+ """
53
+ return ANSI color code
54
+ """
55
+ codes = []
56
+ if fg_color and fg_color.upper() in self._fg_map:
57
+ codes.append(str(self._fg_map[fg_color.upper()]))
58
+ if bg_color and bg_color.upper() in self._bg_map:
59
+ codes.append(str(self._bg_map[bg_color.upper()]))
60
+
61
+ if not codes: return ""
62
+ return f"\033[{';'.join(codes)}m"
63
+
64
+ def print(self, *objects, color="white", background=None, sep=' ', end='\n', file=None, flush=False):
65
+ """
66
+ print out colored content
67
+
68
+ :param color: string, text color
69
+ :param objects: any, printing objects
70
+ :param background: string, background color
71
+ :param sep: string, delimintor between printing objects
72
+ :param end: string, ending style
73
+ :param file: any, output stream
74
+ :param flush: boolean, flush buffer zone
75
+ """
76
+ text = sep.join(str(obj) for obj in objects)
77
+ colored_text = f"{self._get_escape(color, background)}{text}{self._RESET}"
78
+ print(colored_text, end=end, file=file, flush=flush)
79
+
80
+ def get_print_string_text(self, *objects, sep=' ', color=None, background=None):
81
+ """
82
+ return colored text string for print
83
+
84
+ :param color: string, text color
85
+ :param objects: any, printing objects
86
+ :param background: string, background color
87
+ :param sep: string, delimintor between printing objects
88
+ """
89
+ text = sep.join(str(obj) for obj in objects)
90
+ escape = self._get_escape(color, background)
91
+ return f"{escape}{text}{self._RESET}"
92
+
93
+ # shared instance used for every output of this module
94
+ default_colored_output = colored_output(bright=False)
95
+
96
+ #----------------------------------------------------------------------------------------------------------------------------------
97
+ def display_width(text):
98
+ """
99
+ get consistent displayed width for latin + chinese characters string
100
+
101
+ :param text: string, the text for getting length of display
102
+ """
103
+ width = 0
104
+ for ch in str(text):
105
+ if '\u4e00' <= ch <= '\u9fff' or '\u3000' <= ch <= '\u303f' or '\uff00' <= ch <= '\uffef':
106
+ width += 2
107
+ else:
108
+ width += 1
109
+ return width
110
+
111
+ #----------------------------------------------------------------------------------------------------------------------------------
112
+ def safe_input(prompt=""):
113
+ """
114
+ input with preventions of previous input buffer overflow
115
+
116
+ :param prompt: string, text displayed before asking for input
117
+ """
118
+ if prompt: default_colored_output.print(prompt, color='white', end='', flush=True)
119
+
120
+ result = []
121
+
122
+ while True:
123
+ try:
124
+ event = keyboard.read_event(suppress=True)
125
+
126
+ if event.event_type == keyboard.KEY_DOWN:
127
+ if event.name == 'enter':
128
+ print()
129
+ return ''.join(result)
130
+ elif event.name == 'backspace':
131
+ if result:
132
+ result.pop()
133
+ print('\b \b', end='', flush=True)
134
+ elif event.name == 'space':
135
+ result.append(' ')
136
+ default_colored_output.print(' ', color='white', end='', flush=True)
137
+ elif len(event.name) == 1:
138
+ result.append(event.name)
139
+ default_colored_output.print(event.name, color='white', end='', flush=True)
140
+ except:
141
+ return input()
142
+
143
+ #----------------------------------------------------------------------------------------------------------------------------------
144
+
145
+ def clear_screen():
146
+ """
147
+ flush content on the console
148
+ """
149
+ command = 'cls' if os.name == 'nt' else 'clear'
150
+ subprocess.run(command, shell=True, check=False)
151
+
152
+ #----------------------------------------------------------------------------------------------------------------------------------
153
+
154
+ def print_banner(input_string, color='white'):
155
+ """
156
+ print a text rendered in the banner ascii-art style.
157
+
158
+ :param input_string: string, the text to render, '\\n' splits it into rows
159
+ :param color: string, text color (default: white)
160
+ """
161
+ default_colored_output.print(ascii_art.default_banner(input_string), color=color, background=None)
162
+
163
+ def print_header(input_string, color='white'):
164
+ """
165
+ print a text rendered in the header ascii-art style.
166
+
167
+ :param input_string: string, the text to render, '\\n' splits it into rows
168
+ :param color: string, text color (default: white)
169
+ """
170
+
171
+ default_colored_output.print(ascii_art.default_header(input_string), color=color, background=None)
172
+
173
+ #----------------------------------------------------------------------------------------------------------------------------------
174
+
175
+ def select_files(file_types):
176
+ """
177
+ opens file selection dialog.
178
+
179
+ :param file_types: [string...], list of wanted types, eg. ['jpg', 'pdf', 'txt]
180
+ :param
181
+
182
+ :return: the selected file location, if not selected return None
183
+ """
184
+ root = tkinter.Tk()
185
+ root.withdraw()
186
+
187
+ print()
188
+
189
+ if isinstance(file_types, str):
190
+ file_types = [file_types]
191
+
192
+ filetypes = []
193
+ for ft in file_types:
194
+ ext = ft.lstrip('*.')
195
+ desc = ext.lower() + ' File'
196
+ filetypes.append((desc, ft))
197
+ filetypes.append(('All Files', '*.*'))
198
+
199
+ files = tkinter.filedialog.askopenfilenames(
200
+ title='File selection',
201
+ filetypes=filetypes
202
+ )
203
+
204
+ root.destroy()
205
+ return files if files else None
206
+
207
+ #----------------------------------------------------------------------------------------------------------------------------------
208
+
209
+ def select_folder():
210
+ """
211
+ opens file selection dialog.
212
+
213
+ :return: the selected folder path, if not selected return None
214
+ """
215
+ root = tkinter.Tk()
216
+ root.withdraw()
217
+ folder = tkinter.filedialog.askdirectory(title='Folder selection')
218
+ root.destroy()
219
+ return folder if folder else None;
220
+
221
+ #----------------------------------------------------------------------------------------------------------------------------------
222
+
223
+ def print_yesorno(prompt):
224
+ """
225
+ print yes or no choice to let user select.
226
+
227
+ :param prompt: string/func, the prompt displayed before asking.
228
+
229
+ :return: string, 'y' or 'n'
230
+ """
231
+ options = [
232
+ {
233
+ 'text': 'Yes',
234
+ 'color': 'green',
235
+ 'id': 'y'
236
+ },
237
+ {
238
+ 'text': 'No',
239
+ 'color': 'red',
240
+ 'id': 'n'
241
+ }
242
+ ]
243
+ return print_selections(prompt, options)
244
+
245
+ def print_selections(prompt, options):
246
+
247
+ """
248
+ print menu options could be operated by up and down arrow to select. returns the id of the option.
249
+
250
+ :param prompt" string/func, the prompt string or prompt builder function(need to return string)
251
+ :param options: dict, the options for choose
252
+ {
253
+ text: string, the option text displayed
254
+ color: string, color of the option displayed(default: white)
255
+ id: string, unique identifier for each option
256
+ }
257
+ """
258
+ def hide_cursor(): print('\033[?25l', end='', flush=True)
259
+ def show_cursor(): print('\033[?25h', end='', flush=True)
260
+
261
+ active_index = 0
262
+
263
+ # Calculate max length once for lining up entries
264
+ target_w = max(display_width(str(option['text'])) for option in options)
265
+ target_w = 10 if target_w < 10 else target_w
266
+
267
+ if not callable(prompt): default_colored_output.print(prompt, color='white')
268
+ else: default_colored_output.print(prompt(), color='white')
269
+
270
+ def print_options():
271
+ for idx, option in enumerate(options):
272
+ lead_cursor = ">·" if idx == active_index else " "
273
+ lag_cursor = "·<" if idx == active_index else " "
274
+
275
+ text = option['text']
276
+ color = option.get('color') or 'white'
277
+
278
+ current_w = display_width(text)
279
+ filler = ('·' if active_index == idx else " ") * (max((target_w - current_w), 0) + 4 - display_width(lead_cursor))
280
+
281
+ default_colored_output.print(' '*(4 - display_width(lead_cursor)), color='white', end='', flush=True)
282
+ default_colored_output.print(f"{lead_cursor}", color='white', end='', flush=True)
283
+ default_colored_output.print(f"{idx+1}.{text}", color=color, end='', flush=True)
284
+ default_colored_output.print(f"{filler}{lag_cursor}", color='white', flush=True)
285
+
286
+ hide_cursor()
287
+ # Print menu
288
+ print_options()
289
+
290
+ # Main loop for arrow key navigation
291
+ while True:
292
+ # Move cursor up to redraw menu
293
+ print(f"\033[{len(options) + 1}A", flush=True)
294
+ print_options()
295
+
296
+ # Get key press
297
+ key = keyboard.read_event(suppress=True)
298
+
299
+ if key.event_type == keyboard.KEY_DOWN:
300
+ if key.name == 'up':
301
+ active_index = (active_index - 1) % len(options)
302
+ elif key.name == 'down':
303
+ active_index = (active_index + 1) % len(options)
304
+ elif key.name == 'enter':
305
+ show_cursor()
306
+ return options[active_index]['id']
307
+ else: continue
308
+
309
+ def menu(name, prompt, options, home=False):
310
+ """
311
+ open up a menu with single selection toward other menu
312
+
313
+ :param name: string, the name of the panel
314
+ :param prompt: string/func, the prompt pr a prompt builder function(need to return string)
315
+ :param options: [dict], the options informations
316
+ [
317
+ {
318
+ text: string,
319
+ color: string,
320
+ id: string,
321
+ func: {
322
+ body: function,
323
+ param: [string...],
324
+ }
325
+ }
326
+ ...
327
+ ]
328
+ """
329
+ # gather the necessary information for print_selections(options)
330
+ menu_options = [{'text': option['text'], 'color': option.get('color') or None, 'id': option['id']} for option in options]
331
+
332
+ # add back option to allow to return to previous menu
333
+ if home: menu_options.append({'text': 'quit program', 'color': '', 'id': 'quit'})
334
+ else: menu_options.append({'text': 'return to previous', 'color': '', 'id': 'back'})
335
+ # check for any duplicated id to prevent ambiguation in searching the options
336
+ if len([d.get('id') for d in menu_options]) != len(set([d.get('id') for d in menu_options])):
337
+ raise Exception('duplicated id for options')
338
+
339
+ while True:
340
+ clear_screen()
341
+ if not home:
342
+ print_header(name)
343
+ else:
344
+ print_banner(name)
345
+ if not callable(prompt): default_colored_output.print(prompt, color='white')
346
+ else: default_colored_output.print(prompt(), color='white')
347
+ print()
348
+
349
+ chosen_id = print_selections(
350
+ f'Choose from the following {len(menu_options)} options:',
351
+ menu_options
352
+ )
353
+
354
+ if chosen_id in ['back', 'quit']:
355
+ return chosen_id
356
+
357
+ selected = None
358
+ for opt in options:
359
+ if opt['id'] == chosen_id:
360
+ selected = opt
361
+ break
362
+
363
+ if not selected: raise Exception(f'no defined action for an option: {selected}.')
364
+
365
+ # If there's a 'func', execute it
366
+ if 'func' in selected:
367
+ func = selected['func']
368
+ if callable(func):
369
+ result = func()
370
+ elif isinstance(func, dict) and 'body' in func:
371
+ body = func['body']
372
+ params = func.get('param', [])
373
+ result = body(*params)
374
+ else: raise Exception(f'action provided for an option: {selected} is not callable.')
375
+ if result == 'quit':
376
+ return result
377
+
378
+ def home_menu(name, prompt, options):
379
+ """
380
+ open a root menu with single selection toward other menu
381
+
382
+ :param name: string, the name of the panel
383
+ :param prompt: string, the prompt
384
+ :param options: [dict...], the options informations
385
+ [
386
+ {
387
+ text: string,
388
+ color: string,
389
+ id: string,
390
+ func: {
391
+ body: function,
392
+ param: [string...],
393
+ }
394
+ }
395
+ ...
396
+ ]
397
+ """
398
+ result = menu(name, prompt, options, home=True)
399
+ if result == 'quit': quit_program(0)
400
+
401
+ def quit_program(code: int = 0):
402
+ clear_screen()
403
+ default_colored_output.print("Exiting program.", color='white')
404
+ default_colored_output.print('='*30, color='white')
405
+
406
+ try: keyboard.unhook_all()
407
+ except: pass
408
+ finally: sys.exit(code)
@@ -0,0 +1,63 @@
1
+ """
2
+ command line interface for prettier_console
3
+ """
4
+
5
+ import zipfile
6
+ import hashlib
7
+ from datetime import datetime
8
+ import argparse
9
+ import os
10
+ import sys
11
+
12
+ from .ascii_art_font import ascii_art_font
13
+
14
+
15
+ def _cmd_updatefont(args):
16
+ if args.banner: style = 'banner'
17
+ elif args.header: style = 'header'
18
+ else: style = args.style if args.style else os.path.splitext(os.path.basename(args.change_font))[0]
19
+
20
+ try:
21
+ font_folder_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ascii_art_font', 'font')
22
+ target_path = os.path.join(font_folder_path, f'{style}.json')
23
+
24
+ if os.path.exists(target_path):
25
+ randID = hashlib.md5(f'{datetime.now().timestamp()}'.encode()).hexdigest()
26
+ zip_name = f'legacyfont-{style}-{randID}.zip'
27
+ os.makedirs(os.path.join(font_folder_path, "legacy_fonts"), exist_ok=True)
28
+ zip_path = os.path.join(font_folder_path, "legacy_fonts", zip_name)
29
+
30
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
31
+ zipf.write(target_path, arcname=f'{style}.json')
32
+
33
+ ascii_art_font.set_cset(style, args.change_font)
34
+ print(f'Font updated.')
35
+ except Exception as e:
36
+ print(f'Error: {e}', file=sys.stderr)
37
+ sys.exit(1)
38
+
39
+
40
+ def main():
41
+ parser = argparse.ArgumentParser(prog='prettier_console')
42
+ subparsers = parser.add_subparsers(dest='command', required=True)
43
+
44
+ updatefont = subparsers.add_parser(
45
+ 'updatefont',
46
+ help='add or replace an ascii-art font style from a text file'
47
+ )
48
+ updatefont.add_argument(
49
+ 'change_font',
50
+ help="path to a text file with A-Z + space glyphs, one row per line, each character separated by '/'"
51
+ )
52
+ target = updatefont.add_mutually_exclusive_group()
53
+ target.add_argument('--banner', action='store_true', help='save as the banner style')
54
+ target.add_argument('--header', action='store_true', help='save as the header style')
55
+ target.add_argument('--style', help='style name to save as (default: the file name without extension)')
56
+ updatefont.set_defaults(func=_cmd_updatefont)
57
+
58
+ args = parser.parse_args()
59
+ args.func(args)
60
+
61
+
62
+ if __name__ == '__main__':
63
+ main()
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: prettier_console
3
+ Version: 0.1.0
4
+ Summary: Provide you a automation to construct a prettier interactive console in one call
5
+ Author-email: MrCosine <a95sun@uwaterloo.ca>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mr-cosine/prettier_console
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: keyboard>=0.13.5
13
+
14
+ So yea some shortcuts for you to get a prettier interactive console. Um I think its good for lazy people. If you enjoy you enjoy and if you don't...you don't.
15
+ Will update this readme later on... I guess.
16
+
17
+ What you can do:
18
+ 1. using interactive.menu() to create menus with single selections
19
+ 2. Customize your page by inserting interactive parts using tools such as print_selections and print_yesorno.
20
+ 3. draw ascii-art banners and headers
21
+ 4. get colored outputs
22
+
23
+ dependencies:
24
+ build==1.5.0
25
+ colorama==0.4.6
26
+ keyboard==0.13.5
27
+ packaging==26.2
28
+ pyproject_hooks==1.2.0
@@ -0,0 +1,17 @@
1
+ README.md
2
+ pyproject.toml
3
+ prettier_console/__init__.py
4
+ prettier_console/interactive.py
5
+ prettier_console/manage.py
6
+ prettier_console.egg-info/PKG-INFO
7
+ prettier_console.egg-info/SOURCES.txt
8
+ prettier_console.egg-info/dependency_links.txt
9
+ prettier_console.egg-info/entry_points.txt
10
+ prettier_console.egg-info/requires.txt
11
+ prettier_console.egg-info/top_level.txt
12
+ prettier_console/ascii_art_font/__init__.py
13
+ prettier_console/ascii_art_font/ascii_art_font.py
14
+ prettier_console/ascii_art_font/font/banner.json
15
+ prettier_console/ascii_art_font/font/header.json
16
+ tests/ascii-art-test.py
17
+ tests/testcases.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ prettier_console = prettier_console.manage:main
@@ -0,0 +1 @@
1
+ keyboard>=0.13.5
@@ -0,0 +1,3 @@
1
+ dist
2
+ prettier_console
3
+ tests
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "prettier_console"
7
+ version = "0.1.0"
8
+ description = "Provide you a automation to construct a prettier interactive console in one call"
9
+ authors = [{name = "MrCosine", email = "a95sun@uwaterloo.ca"}]
10
+ license = {text = "MIT"}
11
+ readme = "README.md"
12
+ requires-python = ">=3.8"
13
+ dependencies = [
14
+ "keyboard>=0.13.5",
15
+ ]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/mr-cosine/prettier_console"
23
+
24
+ [project.scripts]
25
+ prettier_console = "prettier_console.manage:main"
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["."]
29
+
30
+ [tool.setuptools.package-data]
31
+ prettier_console = ["ascii_art_font/font/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ import prettier_console as pc
2
+
3
+ pc.print_banner('quick brown fox\njumps over the\nlazy dog', color="white")
4
+ pc.print_header('quick brown fox jumps over the lazy dog', color="white")
5
+
6
+ pc.print_banner('quick brown fox\njumps over the\nlazy dog', color="red")
7
+ pc.print_header('quick brown fox jumps over the lazy dog', color="blue")
@@ -0,0 +1,49 @@
1
+ from prettier_console import interactive
2
+
3
+ if __name__ == "__main__":
4
+ #==============================================================================================
5
+ def sub(*args):
6
+ def helloworld():
7
+ options = []
8
+ return interactive.menu(f'hello world', f"hello!", options)
9
+ #==========================================================================================
10
+
11
+ num = args
12
+ options = [
13
+ {
14
+ 'text': "say hello",
15
+ 'color': 'red',
16
+ 'id': 'hello',
17
+ 'func':
18
+ {
19
+ 'body': helloworld
20
+ }
21
+ },
22
+ ]
23
+ return interactive.menu('sub panel', f"this is panel {num}", options)
24
+ #==============================================================================================
25
+
26
+ welcome_text = 'welcome'
27
+ options = [
28
+ {
29
+ 'text': "open panel 1",
30
+ 'color': 'red',
31
+ 'id': 'sub1',
32
+ 'func':
33
+ {
34
+ 'body': sub,
35
+ 'param': [1] # positional argument
36
+ }
37
+ },
38
+ {
39
+ 'text': "open panel 2",
40
+ 'color': 'blue',
41
+ 'id': 'sub2',
42
+ 'func':
43
+ {
44
+ 'body': sub,
45
+ 'param': [2] # positional argument
46
+ }
47
+ }
48
+ ]
49
+ interactive.home_menu('HOME', welcome_text, options)