menataur 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.
menataur-0.1.0/LICENSE ADDED
@@ -0,0 +1,41 @@
1
+ Menataur (main)
2
+ ---------------------------------------------------------------------------------------------
3
+ MIT License
4
+
5
+ Copyright (c) 2026 Gratonic (https://github.com/Gratonic/Minotaur) and FailurePoint (https://github.com/FailurePoint)
6
+
7
+ 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:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10
+
11
+ 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.
12
+
13
+ Colorama
14
+ ---------------------------------------------------------------------------------------------
15
+ Copyright (c) 2010 Jonathan Hartley
16
+ All rights reserved.
17
+
18
+ Redistribution and use in source and binary forms, with or without
19
+ modification, are permitted provided that the following conditions are met:
20
+
21
+ * Redistributions of source code must retain the above copyright notice, this
22
+ list of conditions and the following disclaimer.
23
+
24
+ * Redistributions in binary form must reproduce the above copyright notice,
25
+ this list of conditions and the following disclaimer in the documentation
26
+ and/or other materials provided with the distribution.
27
+
28
+ * Neither the name of the copyright holder nor the names of its contributors
29
+ may be used to endorse or promote products derived from this software without
30
+ specific prior written permission.
31
+
32
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
33
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
34
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
35
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
36
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
37
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
38
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
40
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
41
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: menataur
3
+ Version: 0.1.0
4
+ Summary: ...
5
+ Requires-Python: >=3.10
6
+ License-File: LICENSE
7
+ Requires-Dist: colorama>=0.4.6
8
+ Dynamic: license-file
@@ -0,0 +1,242 @@
1
+ # What is Menataur?
2
+
3
+ Menataur is a menu interface builder module for Python3.10.x. It is super easy to use and makes
4
+ constructing menu interfaces a breeze, turning hours, days, or even months of work into just a few minutes.
5
+
6
+ # How do you construct an awesome menu interface with Menataur? Here's a tested example...
7
+
8
+ ```bash
9
+ pip3 install menataur
10
+ ```
11
+
12
+ ```python
13
+ import menataur
14
+
15
+ # [=== Test Menu 1 ===] #
16
+
17
+ # create Banner
18
+ banner_1 = menataur.Banner()
19
+
20
+ banner_1.title = r"""
21
+ _____ _ _
22
+ |_ _| | | | |
23
+ | | _ __ ___| |_ _ __ _ _ _ __ ___ ___ _ __ | |_ ___
24
+ | || '_ \/ __| __| '__| | | | '_ ` _ \ / _ \ '_ \| __/ __|
25
+ _| || | | \__ \ |_| | | |_| | | | | | | __/ | | | |_\__ \
26
+ \___/_| |_|___/\__|_| \__,_|_| |_| |_|\___|_| |_|\__|___/"""
27
+ banner_1.title_colors = ["cyan", "yellow", "green", "magenta"]
28
+ banner_1.title_bar = "_______________________________________________________________/"
29
+ banner_1.title_bar_color = "blue"
30
+ banner_1.program_name = "Menataur"
31
+ banner_1.program_name_color = "blue"
32
+ banner_1.program_version_number = 1.0
33
+ banner_1.program_version_number_color = "yellow"
34
+ banner_1.operating_system_support_message = "This Program Supports Linux, MacOS, and Windows"
35
+ banner_1.operating_system_support_message_color = "light_green"
36
+
37
+ # create Options
38
+ options_1 = menataur.Options()
39
+
40
+ options_1.option_description_color = "blue"
41
+ options_1.option_number_color = "yellow"
42
+ options_1.option_name_color = "magenta"
43
+ options_1.navigation_option_color = "gray"
44
+
45
+ options_1.add_option("A string instrument is a musical instrument that produces sound through the vibration of strings", 1, "Stringed")
46
+ options_1.add_option("A keyboard instrument is a musical instrument played using a keyboard, a row of levers pressed by the fingers", 2, "Keyboards")
47
+ options_1.add_option("A percussion instrument is a musical instrument that makes sound by being struck or scraped by a beater", 3, "Percussion")
48
+
49
+ # create Prompt
50
+ prompt_1 = menataur.Prompt()
51
+
52
+ prompt_1.prompt_message_color = "light_magenta"
53
+ prompt_1.prompt_message = "Select An Option From The Menu [Ex: 3]: "
54
+
55
+ # create Menu
56
+ menu_1 = menataur.Menu()
57
+
58
+ menu_1.name = "main_menu"
59
+ menu_1.main_menu = True
60
+
61
+ menu_1.add_banner(banner_1)
62
+ menu_1.add_options(options_1)
63
+ menu_1.add_prompt(prompt_1)
64
+
65
+ # [=== Test Menu 2 ===] #
66
+
67
+ # create Banner
68
+ banner_2 = menataur.Banner()
69
+
70
+ banner_2.title = r"""
71
+ _____ _ _ _
72
+ / ___| | (_) | |
73
+ \ `--.| |_ _ __ _ _ __ __ _ ___ __| |
74
+ `--. \ __| '__| | '_ \ / _` |/ _ \/ _` |
75
+ /\__/ / |_| | | | | | | (_| | __/ (_| |
76
+ \____/ \__|_| |_|_| |_|\__, |\___|\__,_|
77
+ __/ |
78
+ |___/ """
79
+ banner_2.title_colors = ["cyan", "yellow", "green", "magenta"]
80
+ banner_2.title_bar = "_________________________________________________/"
81
+ banner_2.title_bar_color = "blue"
82
+ banner_2.program_name = "Stringed"
83
+ banner_2.program_name_color = "blue"
84
+ banner_2.program_version_number = 1.0
85
+ banner_2.program_version_number_color = "yellow"
86
+ banner_2.operating_system_support_message = "This Program Supports Linux, MacOS, and Windows"
87
+ banner_2.operating_system_support_message_color = "light_green"
88
+
89
+ # create Options
90
+ options_2 = menataur.Options()
91
+
92
+ options_2.option_description_color = "blue"
93
+ options_2.option_number_color = "yellow"
94
+ options_2.option_name_color = "magenta"
95
+ options_2.navigation_option_color = "gray"
96
+
97
+ options_2.add_option("An string instrument played by strumming or plucking its strings", 1, "Guitar")
98
+ options_2.add_option("A small, high-pitched string instrument usually played with a bow", 2, "Violin")
99
+ options_2.add_option("A small, four-string instrument played by strumming or plucking", 3, "Ukulele")
100
+
101
+ # create Prompt
102
+ prompt_2 = menataur.Prompt()
103
+
104
+ prompt_2.prompt_message_color = "light_magenta"
105
+ prompt_2.prompt_message = "Select An Option From The Menu [Ex: 3]: "
106
+
107
+ # create Menu
108
+ menu_2 = menataur.Menu()
109
+
110
+ menu_2.name = "stringed_menu"
111
+
112
+ menu_2.add_banner(banner_2)
113
+ menu_2.add_options(options_2)
114
+ menu_2.add_prompt(prompt_2)
115
+
116
+ menu_2.final_menu = True
117
+
118
+ # [=== Test Menu 3 ===] #
119
+
120
+ # create Banner
121
+ banner_3 = menataur.Banner()
122
+
123
+ banner_3.title = r"""
124
+ _ __ _ _
125
+ | | / / | | | |
126
+ | |/ / ___ _ _| |__ ___ __ _ _ __ __| |___
127
+ | \ / _ \ | | | '_ \ / _ \ / _` | '__/ _` / __|
128
+ | |\ \ __/ |_| | |_) | (_) | (_| | | | (_| \__ \
129
+ \_| \_/\___|\__, |_.__/ \___/ \__,_|_| \__,_|___/
130
+ __/ |
131
+ |___/ """
132
+ banner_3.title_colors = ["cyan", "yellow", "green", "magenta"]
133
+ banner_3.title_bar = "_________________________________________________/"
134
+ banner_3.title_bar_color = "blue"
135
+ banner_3.program_name = "Keyboards"
136
+ banner_3.program_name_color = "blue"
137
+ banner_3.program_version_number = 1.0
138
+ banner_3.program_version_number_color = "yellow"
139
+ banner_3.operating_system_support_message = "This Program Supports Linux, MacOS, and Windows"
140
+ banner_3.operating_system_support_message_color = "light_green"
141
+
142
+ # create Options
143
+ options_3 = menataur.Options()
144
+
145
+ options_3.option_description_color = "blue"
146
+ options_3.option_number_color = "yellow"
147
+ options_3.option_name_color = "magenta"
148
+ options_3.navigation_option_color = "gray"
149
+
150
+ options_3.add_option("A keyboard instrument that makes sound when hammers strike its strings", 1, "Piano")
151
+ options_3.add_option("A keyboard instrument that produces sound by sending air through pipes or electronic tone generators", 2, "Organ")
152
+ options_3.add_option("A keyboard instrument that produces a delicate, bell-like sound when hammers strike metal bars", 3, "Celeste")
153
+
154
+ # create Prompt
155
+ prompt_3 = menataur.Prompt()
156
+
157
+ prompt_3.prompt_message_color = "light_magenta"
158
+ prompt_3.prompt_message = "Select An Option From The Menu [Ex: 3]: "
159
+
160
+ # create Menu
161
+ menu_3 = menataur.Menu()
162
+
163
+ menu_3.name = "keyboards_menu"
164
+
165
+ menu_3.add_banner(banner_3)
166
+ menu_3.add_options(options_3)
167
+ menu_3.add_prompt(prompt_3)
168
+
169
+ menu_3.final_menu = True
170
+
171
+ # [=== Test Menu 4 ===] #
172
+
173
+ # create Banner
174
+ banner_4 = menataur.Banner()
175
+
176
+ banner_4.title = r"""
177
+ ______ _
178
+ | ___ \ (_)
179
+ | |_/ /__ _ __ ___ _ _ ___ ___ _ ___ _ __
180
+ | __/ _ \ '__/ __| | | / __/ __| |/ _ \| '_ \
181
+ | | | __/ | | (__| |_| \__ \__ \ | (_) | | | |
182
+ \_| \___|_| \___|\__,_|___/___/_|\___/|_| |_|"""
183
+ banner_4.title_colors = ["cyan", "yellow", "green", "magenta"]
184
+ banner_4.title_bar = "_________________________________________________/"
185
+ banner_4.title_bar_color = "blue"
186
+ banner_4.program_name = "Percussion"
187
+ banner_4.program_name_color = "blue"
188
+ banner_4.program_version_number = 1.0
189
+ banner_4.program_version_number_color = "yellow"
190
+ banner_4.operating_system_support_message = "This Program Supports Linux, MacOS, and Windows"
191
+ banner_4.operating_system_support_message_color = "light_green"
192
+
193
+ # create Options
194
+ options_4 = menataur.Options()
195
+
196
+ options_4.option_description_color = "blue"
197
+ options_4.option_number_color = "yellow"
198
+ options_4.option_name_color = "magenta"
199
+ options_4.navigation_option_color = "gray"
200
+
201
+ options_4.add_option("A large drum that produces a deep, low sound when struck", 1, "Bass Drum")
202
+ options_4.add_option("A metal percussion instrument that makes a bright crash or ringing sound when struck", 2, "Cymbal")
203
+ options_4.add_option("Tall, single-headed hand drums played with the hands, often in pairs", 3, "Congas")
204
+
205
+ # create Prompt
206
+ prompt_4 = menataur.Prompt()
207
+
208
+ prompt_4.prompt_message_color = "light_magenta"
209
+ prompt_4.prompt_message = "Select An Option From The Menu [Ex: 3]: "
210
+
211
+ # create Menu
212
+ menu_4 = menataur.Menu()
213
+
214
+ menu_4.name = "percussion_menu"
215
+
216
+ menu_4.add_banner(banner_4)
217
+ menu_4.add_options(options_4)
218
+ menu_4.add_prompt(prompt_4)
219
+
220
+ menu_4.final_menu = True
221
+
222
+ # [=== Datamapping === ] #
223
+
224
+ main_menu_datamap = {
225
+ 1: menu_2,
226
+ 2: menu_3,
227
+ 3: menu_4
228
+ }
229
+
230
+ menu_1.datamap = main_menu_datamap
231
+
232
+ # [=== Menataur (menu interface) ===] #
233
+
234
+ # create a Menataur (menu interface)
235
+ menu_interface = menataur.Menataur()
236
+
237
+ # specify the start Menu
238
+ menu_interface.start_menu = menu_1
239
+
240
+ # run the Menataur (menu interface)
241
+ user_choices = menu_interface.run()
242
+ ```
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: menataur
3
+ Version: 0.1.0
4
+ Summary: ...
5
+ Requires-Python: >=3.10
6
+ License-File: LICENSE
7
+ Requires-Dist: colorama>=0.4.6
8
+ Dynamic: license-file
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ menataur.py
4
+ pyproject.toml
5
+ menataur.egg-info/PKG-INFO
6
+ menataur.egg-info/SOURCES.txt
7
+ menataur.egg-info/dependency_links.txt
8
+ menataur.egg-info/requires.txt
9
+ menataur.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ colorama>=0.4.6
@@ -0,0 +1 @@
1
+ menataur
@@ -0,0 +1,683 @@
1
+ r"""
2
+ # :: Author Information and Program Details :: #
3
+
4
+ File Name: menataur.py
5
+ Author(s): Gratonic (https://github.com/Gratonic) and FailurePoint (https://github.com/FailurePoint)
6
+ Language: Python 3.13.5
7
+ Dependencie(s): colorama
8
+ Last Modified: Septemeber 26, 2026
9
+
10
+ # :: Description :: #
11
+
12
+ This program was designed to make writing command-line menu interfaces easier.
13
+ It splits each menu into blocks represented by classes. These block classes are combined to create
14
+ a Menu instance, which can then be combined with other Menu instances within the Menataur class.
15
+ The Menataur class organizes the menu components and uses a navigation algorithm to allow
16
+ users to move between them.
17
+
18
+ note:
19
+ When reading the menu navigation algorithm code in this program (split between Menu and Menataur), think about a Binary Tree
20
+ and it will begin to make a lot more sense. I believe this is the simplest way to make sb ense
21
+ of the navigation algorithm.
22
+
23
+ # :: Example Usage :: #
24
+
25
+ # create the banner
26
+ banner = Banner()
27
+
28
+ banner.title =
29
+ ___ ___ _
30
+ | \/ | | |
31
+ | . . | ___ _ __ __ _| |_ __ _ _ _ _ __
32
+ | |\/| |/ _ \ '_ \ / _` | __/ _` | | | | '__|
33
+ | | | | __/ | | | (_| | || (_| | |_| | |
34
+ \_| |_/\___|_| |_|\__,_|\__\__,_|\__,_|_|
35
+ banner.title_colors = ["cyan", "yellow", "green", "magenta"]
36
+ banner.title_bar = "_________________________________________________/"
37
+ banner.title_bar_color = "blue"
38
+ banner.program_name = "Menataur"
39
+ banner.program_name_color = "blue"
40
+ banner.program_version_number = 1.0
41
+ banner.program_version_number_color = "yellow"
42
+ banner.operating_system_support_message = "This Program Supports Linux, MacOS, and Windows"
43
+ banner.operating_system_support_message_color = "light_green"
44
+
45
+ # create the options
46
+ options = Options()
47
+
48
+ options.option_description_color = "white"
49
+ options.option_number_color = "light_yellow"
50
+ options.option_name_color = "cyan"
51
+ options.navigation_option_color = "light_red"
52
+
53
+ options.add_option("View Profile", 1, "Profile")
54
+ options.add_option("Settings", 2, "Settings")
55
+ options.add_option("Help", 3, "Help")
56
+
57
+ # create the prompt
58
+ prompt = Prompt()
59
+
60
+ prompt.prompt_message_color = "light_magenta"
61
+ prompt.prompt_message = "Select an option: "
62
+
63
+ # create and run the menu
64
+ menu = Menu()
65
+
66
+ menu.name = "main"
67
+ menu.main_menu = True
68
+
69
+ menu.add_banner(banner)
70
+ menu.add_options(options)
71
+ menu.add_prompt(prompt)
72
+
73
+ menu.run()
74
+
75
+ # you can only simply display it (excludes the input prompt)
76
+ menu.display()
77
+ """
78
+
79
+ # [=== Imports ===] #
80
+
81
+ from colorama import Fore, Back
82
+ from functools import partial
83
+ import subprocess
84
+ import random
85
+ import sys
86
+
87
+ # [=== Global Variables ===]
88
+
89
+ # used to ensure the user does not give two or more Menu's the same name
90
+ _used_menu_name: list[str] = list()
91
+
92
+ # [=== Functions ===] #
93
+
94
+ def clear_terminal():
95
+ # for Linux, MacOS, and Unix-Like operating systems
96
+ if sys.platform in ("linux", "darwin"):
97
+ subprocess.run("clear", shell=True)
98
+ else:
99
+ # for Windows and other obsolete operating systems
100
+ subprocess.run("cls", shell=True)
101
+
102
+ def validate_foreground_color(fore_color: str) -> str:
103
+ foreground_colors = {
104
+ "blue": Fore.BLUE, "light_blue": Fore.LIGHTBLUE_EX,
105
+ "cyan": Fore.CYAN, "light_cyan": Fore.LIGHTCYAN_EX,
106
+ "red": Fore.RED, "light_red": Fore.LIGHTRED_EX,
107
+ "green": Fore.GREEN, "light_green": Fore.LIGHTGREEN_EX,
108
+ "yellow": Fore.YELLOW, "light_yellow": Fore.LIGHTYELLOW_EX,
109
+ "magenta": Fore.MAGENTA, "light_magenta": Fore.LIGHTMAGENTA_EX,
110
+ "black": Fore.BLACK, "white": Fore.WHITE,
111
+ "gray": Fore.LIGHTBLACK_EX, "grey": Fore.LIGHTBLACK_EX
112
+ }
113
+
114
+ color = fore_color.lower()
115
+ if color in foreground_colors:
116
+ return foreground_colors[color]
117
+ else:
118
+ print(f"{Fore.RED}[!] Error: {Fore.YELLOW}{color}{Fore.RED} is not a valid/supported color in this program.{Fore.RESET}")
119
+ print(f"{Fore.GREEN}The following colors are supported:{Fore.RESET}")
120
+ for color_key in foreground_colors.keys():
121
+ if color_key != "gray" or color_key != "grey":
122
+ print(f"{Fore.BLUE}{color_key}{Fore.RESET}")
123
+ else:
124
+ print(f"{Fore.BLUE}gray/grey{Fore.RESET}")
125
+
126
+ exit(1)
127
+
128
+ # note: although this function is currently not in use it may be later
129
+ def validate_background_color(back_color: str) -> str:
130
+ background_colors = {
131
+ "blue": Back.BLUE, "light_blue": Back.LIGHTBLUE_EX,
132
+ "cyan": Back.CYAN, "light_cyan": Back.LIGHTCYAN_EX,
133
+ "red": Back.RED, "light_red": Back.LIGHTRED_EX,
134
+ "green": Back.GREEN, "light_green": Back.LIGHTGREEN_EX,
135
+ "yellow": Back.YELLOW, "light_yellow": Back.LIGHTYELLOW_EX,
136
+ "magenta": Back.MAGENTA, "light_magenta": Back.LIGHTMAGENTA_EX,
137
+ "black": Back.BLACK, "white": Back.WHITE,
138
+ "gray": Back.LIGHTBLACK_EX, "grey": Back.LIGHTBLACK_EX
139
+ }
140
+
141
+ color = back_color.lower()
142
+ if color in background_colors:
143
+ return background_colors[color]
144
+ else:
145
+ print(f"{Fore.RED}[!] Error: {Fore.YELLOW}{color}{Fore.RED} is not a valid/supported color in this program.{Fore.RESET}")
146
+ print(f"{Fore.GREEN}The following colors are supported:{Fore.RESET}")
147
+ for color_key in background_colors.keys():
148
+ if color_key != "gray" or color_key != "grey":
149
+ print(f"{Fore.BLUE}{color_key}{Fore.RESET}")
150
+ else:
151
+ print(f"{Fore.BLUE}gray/grey{Fore.RESET}")
152
+
153
+ exit(1)
154
+
155
+ """
156
+ note:
157
+ the reason for the complex approach in this function is to avoid large amounts of color repeats,
158
+ which was a major issue with a previous one-liner approach
159
+ """
160
+ def paint_text(text: str, colors: list) -> str:
161
+ # avoids waisting compute time with an algorithm if there is only one color provided
162
+ if len(colors) == 1:
163
+ return f"{validate_foreground_color(colors[0])}{text}{Fore.RESET}"
164
+
165
+ # validates the colors and retrieves their actual color values
166
+ colors = [validate_foreground_color(fore_color=color) for color in colors]
167
+ # creates a list of every character in the given text
168
+ characters = list(text)
169
+ # keeps track of the colors list index numbers that can temporarily not be used
170
+ banned_indexes = set()
171
+
172
+ # len(colors) %2 == 0: even, else: odd
173
+ if len(colors) % 2 == 0:
174
+ # needed colors list index(s)
175
+ middle_index = len(colors) // 2
176
+
177
+ # note: used to determine the random index number
178
+ low = middle_index
179
+ high = middle_index
180
+
181
+ for char in range(len(text)):
182
+ # avoids waisting compute time on non-printable characters (with the needed exceptions)
183
+ if characters[char] == " ":
184
+ continue
185
+ elif characters[char].isprintable() == True or characters[char] == "\\" or characters[char] == "\'" or characters[char] == '\"':
186
+ pass
187
+ else:
188
+ continue
189
+
190
+ # note: loops until a valid index number has been selected
191
+ while True:
192
+ selected_index = random.randint(low, high)
193
+
194
+ if selected_index not in banned_indexes:
195
+ break
196
+
197
+ # add the color code for the current character
198
+ characters[char] = colors[selected_index] + characters[char]
199
+
200
+ if low != 0 and high != len(colors) - 1:
201
+ # ban the current low and high indexes temporarily
202
+ banned_indexes.add(low)
203
+ banned_indexes.add(high)
204
+
205
+ # fetch the new low and high indexes; note: center -> ends
206
+ low -= 1
207
+ high += 1
208
+ else:
209
+ # reset the low and high indexes values along with the banned_indexes set
210
+ low = middle_index
211
+ high = middle_index
212
+
213
+ banned_indexes = set()
214
+
215
+ # create the new text string
216
+ text = "".join(characters)
217
+
218
+ return f"{text}{Fore.RESET}"
219
+ else:
220
+ # needed colors list index(s)
221
+ middle_index = ((len(colors) - 1) // 2) + 1
222
+
223
+ # note: used to determine the random index number
224
+ low = 0
225
+ high = len(colors) - 1
226
+
227
+ for char in range(len(text)):
228
+ # avoids waisting compute time on non-printable characters (with the needed exceptions)
229
+ if characters[char] == " ":
230
+ continue
231
+ elif characters[char].isprintable() == True or characters[char] == "\\" or characters[char] == "\'" or characters[char] == '\"':
232
+ pass
233
+ else:
234
+ continue
235
+
236
+ # note: loops until a valid index number has been selected
237
+ while True:
238
+ selected_index = random.randint(low, high)
239
+
240
+ if selected_index not in banned_indexes:
241
+ break
242
+
243
+ # add the color code for the current character
244
+ characters[char] = colors[selected_index] + characters[char]
245
+
246
+ if low == middle_index or high == middle_index:
247
+ # reset the low and high indexes values along with the banned_indexes set
248
+ low = 0
249
+ high = len(colors) - 1
250
+
251
+ banned_indexes = set()
252
+ else:
253
+ # ban the current low and high indexes temporarily
254
+ banned_indexes.add(low)
255
+ banned_indexes.add(high)
256
+
257
+ # fetch the new low and high indexes; note: ends -> center
258
+ low += 1
259
+ high -= 1
260
+
261
+ # create the new text string
262
+ text = "".join(characters)
263
+
264
+ return f"{text}{Fore.RESET}"
265
+
266
+ def input_prompt(prompt_message) -> str:
267
+ try:
268
+ user_choice = input(prompt_message)
269
+
270
+ return user_choice
271
+ except KeyboardInterrupt:
272
+ print("\n")
273
+ exit(0)
274
+
275
+ # [=== Classes ===] #
276
+
277
+ class Banner:
278
+ def __init__(self):
279
+ # elements
280
+ self.title = str()
281
+ self.title_bar = str()
282
+ self.program_name = str()
283
+ self.program_version_number = float()
284
+ self.title_bar = str()
285
+ self.operating_system_support_message = "This Program Supports: Linux, MacOS, and Windows"
286
+
287
+ # element colors
288
+ self._title_colors: list[str] = list()
289
+ self._title_bar_color = str()
290
+ self._program_name_color = str()
291
+ self._program_version_number_color = str()
292
+ self._operating_system_support_message_color = str()
293
+
294
+ # --- getter/setter properties --- #
295
+
296
+ @property
297
+ def title_colors(self):
298
+ return self._title_colors
299
+
300
+ @title_colors.setter
301
+ def title_colors(self, colors: list):
302
+ # note: the paint function will handle the color validation for the colors list
303
+ self._title_colors = colors
304
+
305
+ @property
306
+ def title_bar_color(self):
307
+ return self._title_bar_color
308
+
309
+ @title_bar_color.setter
310
+ def title_bar_color(self, color: str):
311
+ self._title_bar_color = validate_foreground_color(color)
312
+
313
+ @property
314
+ def program_name_color(self):
315
+ return self._program_name_color
316
+
317
+ @program_name_color.setter
318
+ def program_name_color(self, color: str):
319
+ self._program_name_color = validate_foreground_color(color)
320
+
321
+ @property
322
+ def program_version_number_color(self):
323
+ return self._program_version_number_color
324
+
325
+ @program_version_number_color.setter
326
+ def program_version_number_color(self, color: str):
327
+ self._program_version_number_color = validate_foreground_color(color)
328
+
329
+ @property
330
+ def operating_system_support_message_color(self):
331
+ return self._operating_system_support_message_color
332
+
333
+ @operating_system_support_message_color.setter
334
+ def operating_system_support_message_color(self, color: str):
335
+ self._operating_system_support_message_color = validate_foreground_color(color)
336
+
337
+ class Options:
338
+ def __init__(self):
339
+ # elements #
340
+ self.options: dict[int, str] = dict()
341
+
342
+ # element colors
343
+ self._option_description_color = str()
344
+ self._option_number_color = str()
345
+ self._option_name_color = str()
346
+
347
+ self._navigation_option_color = Fore.LIGHTBLACK_EX
348
+
349
+ # --- getter/setter properties --- #
350
+
351
+ @property
352
+ def option_description_color(self):
353
+ return self._option_description_color
354
+
355
+ @option_description_color.setter
356
+ def option_description_color(self, color: str):
357
+ self._option_description_color = validate_foreground_color(color)
358
+
359
+ @property
360
+ def option_number_color(self):
361
+ return self._option_number_color
362
+
363
+ @option_number_color.setter
364
+ def option_number_color(self, color: str):
365
+ self._option_number_color = validate_foreground_color(color)
366
+
367
+ @property
368
+ def option_name_color(self):
369
+ return self._option_name_color
370
+
371
+ @option_name_color.setter
372
+ def option_name_color(self, color: str):
373
+ self._option_name_color = validate_foreground_color(color)
374
+
375
+ @property
376
+ def navigation_option_color(self):
377
+ return self._navigation_option_color
378
+
379
+ @navigation_option_color.setter
380
+ def navigation_option_color(self, color: str):
381
+ self._navigation_option_color = validate_foreground_color(color)
382
+
383
+ # --- methods --- #
384
+
385
+ def add_option(self, option_description: str, option_number: int, option_name: str) -> None:
386
+ # validation
387
+ if option_number == 0:
388
+ print(f"{Fore.RED}[!] Error: The {Fore.YELLOW}option_number 0{Fore.RED} is reserved for the exit option (automatically included).{Fore.RESET}")
389
+ sys.exit(1)
390
+
391
+ if option_number < 0:
392
+ print(f"{Fore.RED}[!] Error: The chosen {Fore.YELLOW}option_number{Fore.RED} can not be negative.")
393
+
394
+ if option_number in self.options.keys():
395
+ print(f"{Fore.RED}[!] Error: The {Fore.YELLOW}option_number {option_number}{Fore.RED} is already in use.{Fore.RESET}")
396
+
397
+ # construct and add the option to the options dictionary; note: color reset is handled when the element is added to a Menu
398
+ option = f"{self._option_description_color}{option_description}\n{self.option_number_color}{option_number}) {self._option_name_color}{option_name}"
399
+
400
+ self.options[option_number] = option
401
+
402
+ class Prompt:
403
+ def __int__(self):
404
+ # element(s)
405
+ self.prompt_message = "Choose An Option From The Menu [ex: 0]: "
406
+
407
+ # element color(s)
408
+ self._prompt_message_color = str()
409
+
410
+ # used to determine wether or not the user decided to color the prompt message using the module
411
+ # note: they may have chosen not to because they wanted to use multiple colors, so instead they added the color codes themself
412
+ self.prompt_colored = False
413
+
414
+ # --- getter/setter properties --- #
415
+
416
+ @property
417
+ def prompt_message_color(self):
418
+ return self._prompt_message_color
419
+
420
+ @prompt_message_color.setter
421
+ def prompt_message_color(self, color: str):
422
+ self._prompt_message_color = validate_foreground_color(color)
423
+
424
+ self.prompt_colored = True
425
+
426
+ class Menu:
427
+ def __init__(self):
428
+ # note: these strings are formated to construct menu elements (in the technical sense they are the elements)
429
+ self._menu = str()
430
+ self._banner = "{title}\n{title_bar_color}{title_bar}\n{program_name_color}{program_name}{program_version_number_color}v{program_version_number}\n{operating_system_support_message_color}{operating_system_support_message}\n{reset}"
431
+ self._option = "{option}{reset}"
432
+
433
+ # this variable will a hold a callable address for the input_prompt() function loaded with its parameter already
434
+ self._prompt = None
435
+
436
+ # used to make it easier to sort through the final Menataur (menu interface) data
437
+ self._name = str()
438
+ # used to determine wether or not this is a/the main menu
439
+ self.main_menu = False
440
+ # used to determine wether or not this is the final menu in a menu path
441
+ self._final_menu = False
442
+ # used to store the option numbers for basic input validation in the run_menu method
443
+ self._option_numbers: set[int] = set()
444
+ # used to store the Option to Menu mappings - required if the user want's to use this Menu in a Menataur
445
+ self._datamap: dict[int, Menu | str] = dict()
446
+
447
+ # --- getter/setter properties --- #
448
+
449
+ @property
450
+ def name(self):
451
+ return self._name
452
+
453
+ @name.setter
454
+ def name(self, menu_name: str):
455
+ if menu_name in _used_menu_name:
456
+ print(f"{Fore.RED}[!] Error: Every Menu must be assigned a unique {Fore.YELLOW}name{Fore.RED}.{Fore.RESET}")
457
+ exit(1)
458
+
459
+ _used_menu_name.append(menu_name)
460
+
461
+ self._name = menu_name
462
+
463
+ @property
464
+ def final_menu(self):
465
+ return self._final_menu
466
+
467
+ @final_menu.setter
468
+ def final_menu(self, value: bool):
469
+ if value == True:
470
+ # used to create a special data map for the final menu
471
+ _final_menu_datamap: dict[int, str] = dict()
472
+ # used to know when to stop looping; note: 1 is not subtracted for previous menu because of the way the range() function works
473
+ _last_option = max(self._option_numbers)
474
+
475
+ _final_menu_datamap[0] = "exit"
476
+
477
+ for opt in range(1, _last_option):
478
+ _final_menu_datamap[opt] = "final_menu"
479
+
480
+ _final_menu_datamap[_last_option] = "previous_menu"
481
+
482
+ self._datamap = _final_menu_datamap
483
+
484
+ # set the _final_menu tracker variable to the user given bool (True or False)
485
+ self._final_menu = value
486
+
487
+ @property
488
+ def datamap(self):
489
+ return self._datamap
490
+
491
+ @datamap.setter
492
+ def datamap(self, map: dict):
493
+ # add the exit and previous menu (if applicable) options
494
+ map[0] = "exit"
495
+
496
+ # ensure this is not a final menu because final Menus do not need a user configured datamap
497
+ if self.final_menu == True:
498
+ print(f"{Fore.RED}[!] Error: This is a {Fore.YELLOW}Final Menu{Fore.RED}, it should not have a datamap.{Fore.RESET}")
499
+ exit(1)
500
+
501
+ if self.main_menu != True:
502
+ map[max(self._option_numbers)] = "previous_menu"
503
+
504
+ self._datamap = map
505
+
506
+ # ensure the user has provided map keys that match the option numbers and store the map if so
507
+ if map.keys() != self._datamap.keys():
508
+ print(f"{Fore.RED}[!] Error: The {Fore.YELLOW}map keys{Fore.RED} do not match the {Fore.YELLOW}option numbers{Fore.RED}.{Fore.RESET}")
509
+ print(map.keys())
510
+ print(self._datamap.keys())
511
+ exit(1)
512
+
513
+ # --- methods --- #
514
+
515
+ # formally adds an element to the menu
516
+ def _add_element(self, menu_element) -> None:
517
+ self._menu = self._menu + f"{menu_element}\n"
518
+
519
+ def add_banner(self, banner: Banner) -> None:
520
+ self._add_element(menu_element=self._banner.format(
521
+ title=paint_text(banner.title, banner.title_colors),
522
+ title_bar_color=banner.title_bar_color,
523
+ title_bar=banner.title_bar,
524
+ program_name_color=banner.program_name_color,
525
+ program_name=banner.program_name,
526
+ program_version_number_color=banner.program_version_number_color,
527
+ program_version_number=banner.program_version_number,
528
+ operating_system_support_message_color=banner.operating_system_support_message_color,
529
+ operating_system_support_message=banner.operating_system_support_message,
530
+ reset=Fore.RESET
531
+ ))
532
+
533
+ def add_options(self, options: Options) -> None:
534
+ # add the exit option
535
+ exit_option = f"{options.navigation_option_color}0) Exit"
536
+
537
+ self._add_element(self._option.format(
538
+ option=exit_option,
539
+ reset=Fore.RESET
540
+ ))
541
+
542
+ for option_key in options.options.keys():
543
+ self._add_element(self._option.format(
544
+ option=options.options[option_key],
545
+ reset=Fore.RESET
546
+ ))
547
+
548
+ # add the previous menu option if necessary
549
+ if self.main_menu != True:
550
+ previous_menu_option_number = list(options.options.keys())[-1] + 1
551
+ previous_menu_option = f"{options.navigation_option_color}{previous_menu_option_number}) Previous Menu"
552
+
553
+ self._add_element(self._option.format(
554
+ option=previous_menu_option,
555
+ reset=Fore.RESET
556
+ ))
557
+
558
+ # add the previous_menu_option with its respective option number to the options dictionary to keep a record of it
559
+ options.options[previous_menu_option_number] = previous_menu_option
560
+
561
+ # add the exit_option with it's respective option number to the options dictionary to keep a record of it
562
+ options.options[0] = exit_option
563
+
564
+ # store the option numbers to reference for basic input validation in the run_method function
565
+ self._option_numbers = options.options.keys()
566
+
567
+ def add_prompt(self, prompt: Prompt) -> None:
568
+ if prompt.prompt_colored == True:
569
+ prompt_message = f"{prompt.prompt_message_color}{prompt.prompt_message}{Fore.RESET}"
570
+ else:
571
+ # note: the Fore.RESET is added in case the user has forgotten to include the reset code (in the case that they manually added the color codes in their prompt_message)
572
+ prompt_message = f"{prompt.prompt_message}{Fore.RESET}"
573
+
574
+ # creates a callable function object with the parameter; think of it like a loaded gun ready to fire
575
+ self._prompt = partial(input_prompt, prompt_message)
576
+
577
+ def display(self) -> None:
578
+ print(self._menu)
579
+
580
+ def run(self) -> int:
581
+ print(self._menu)
582
+
583
+ if self.name == str():
584
+ print(f"{Fore.RED}[!] Error: Every Menu must be assigned a unique{Fore.YELLOW}name{Fore.RED}.")
585
+ exit(1)
586
+
587
+ while True:
588
+ user_choice = self._prompt()
589
+
590
+ try:
591
+ user_choice = int(user_choice)
592
+
593
+ if user_choice == 0:
594
+ exit(0)
595
+
596
+ if user_choice in self._option_numbers:
597
+ clear_terminal()
598
+
599
+ return user_choice
600
+ else:
601
+ print(f"{Fore.RED}Invalid Option!{Fore.RESET}")
602
+ except ValueError:
603
+ print(f"{Fore.RED}Invalid Option!{Fore.RESET}")
604
+ except Exception as e:
605
+ print(f"{Fore.RED}Invalid Option!{Fore.RESET}")
606
+
607
+ class Menataur:
608
+ def __init__(self):
609
+ # stores the starting position of the Menataur (menu interface)
610
+ self._start_menu = None
611
+
612
+ @property
613
+ def start_menu(self):
614
+ return self._start_menu
615
+
616
+ @start_menu.setter
617
+ def start_menu(self, menu: Menu):
618
+ if menu.main_menu == True:
619
+ self._start_menu = menu
620
+ else:
621
+ print(f"{Fore.RED}[!] Error: The {Fore.YELLOW}start_menu{Fore.RED} must be a {Fore.YELLOW}main_menu{Fore.RED}.{Fore.RESET}")
622
+ print(f"{Fore.BLUE}[?] Help: If you would like to use this {Fore.YELLOW}menu{Fore.BLUE} as your start_menu, set {Fore.YELLOW}main_menu=True{Fore.BLUE}.{Fore.RESET}")
623
+ exit(1)
624
+
625
+ def run(self) -> list[int]:
626
+ # keeps track of the current menu (changed at the end of each loop depending on the user choice)
627
+ _current_menu = self._start_menu
628
+ # keeps track of the previous menu (becomes _current_menu after the _current_menu runs in each loop)
629
+ _previous_menu = None
630
+
631
+ """
632
+ note:
633
+ This data format was chosen because it allows for easy access to the data
634
+ while still maintaing the orignal order the data was added. If just was used it would make
635
+ it hard for the user to sort through the data and if just a dictionary was used the .keys() and
636
+ .values() sets would not retain the original order in which the data was addded.
637
+ """
638
+ _user_choices: list[dict[str, int]] = list()
639
+
640
+ if self._start_menu == None:
641
+ print(f"{Fore.RED}[!] Error: You must set a {Fore.YELLOW}start_menu{Fore.RED}.{Fore.RESET}")
642
+
643
+ # note: think of a Binary Tree when thiking about how the menu navigation works
644
+ while _current_menu != "final_menu":
645
+ # fetch the current Menu's name
646
+ _current_menu_name = _current_menu.name
647
+
648
+ # fetch the current Menu's datamap
649
+ _current_menu_datamap = _current_menu.datamap
650
+
651
+ # run the current menu and store the returned option number
652
+ _menu_output = _current_menu.run()
653
+
654
+ # use the _menu_output to determine what to do next; note: the datamap may provide another Menu or a Menu code
655
+ _next = _current_menu_datamap[_menu_output]
656
+
657
+ match _next:
658
+ case "final_menu":
659
+ clear_terminal()
660
+
661
+ _user_choice_map = {_current_menu_name: _menu_output}
662
+ _user_choices.append(_user_choice_map)
663
+
664
+ return _user_choices
665
+ case "previous_menu":
666
+ clear_terminal()
667
+
668
+ # the _current_menu was already stored as the _previous_menu, so nothing needs to be done
669
+ continue
670
+ case _:
671
+ clear_terminal()
672
+
673
+ # set the previous_menu for the _next Menu's datamap
674
+ _next.datamap[max(_next.datamap.keys())] = _current_menu
675
+
676
+ # add the user's choice to the _user_choices_map
677
+ _user_choice_map = {_current_menu_name: _menu_output}
678
+ _user_choices.append(_user_choice_map)
679
+
680
+ # set the _current_menu to the _next Menu so the _next Menu is called in the next loop
681
+ _current_menu = _next
682
+
683
+ continue
@@ -0,0 +1,9 @@
1
+ [project]
2
+ name = "menataur"
3
+ version = "0.1.0"
4
+ description = "..." # Add a short description
5
+ requires-python = ">=3.10"
6
+ dependencies = ["colorama>=0.4.6"]
7
+
8
+ [tool.setuptools]
9
+ py-modules = ["menataur"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+