make-selection 0.0.1__tar.gz → 1.0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: make_selection
3
- Version: 0.0.1
3
+ Version: 1.0.0
4
4
  Summary: Package for interactive command line menu
5
5
  Author-email: Steven Frazee <stevefrazee123@gmail.com>
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "make_selection"
3
- version = "0.0.1"
3
+ version = "1.0.0"
4
4
  authors = [
5
5
  { name="Steven Frazee", email="stevefrazee123@gmail.com" },
6
6
  ]
@@ -0,0 +1,180 @@
1
+ """
2
+ Module for interactive command line menu. Simply accepts a list of str-able objects
3
+ and allows user to select using arrow keys.
4
+ """
5
+ import sys
6
+ if sys.platform != "win32":
7
+ raise NotImplementedError("This module is only available on Windows.")
8
+ from typing import Any
9
+ import msvcrt
10
+ import ctypes
11
+
12
+ stdout = -11
13
+ enable_ansi_codes = 7
14
+ kernel32 = ctypes.windll.kernel32
15
+ kernel32.SetConsoleMode(kernel32.GetStdHandle(stdout), enable_ansi_codes)
16
+
17
+ ANSI_MOVE_CURSOR = "\x1b[{up}F\r\x1b[{right}C"
18
+ ANSI_HIGHLIGHT = "\x1b[30;47m"
19
+ ANSI_HIGHLIGHT_SEARCH_STRING = "\x1b[95;47m"
20
+ ANSI_YELLOW = "\x1b[93m"
21
+ ANSI_BLUE = "\x1b[94m"
22
+ ANSI_MAGENTA = "\x1b[95m"
23
+ ANSI_RESET = "\x1b[0m"
24
+
25
+ SPECIAL_KEY = 224
26
+ UP_ARROW = 72
27
+ DOWN_ARROW = 80
28
+ ENTER_KEY = 13
29
+ CTL_C = 3
30
+ BACKSPACE = 8
31
+ SPACEBAR = 32
32
+
33
+ class Menu:
34
+ def __init__(self, options: list, label: str, window_size: int=10) -> None:
35
+ assert options
36
+ assert label
37
+ assert 1 <= window_size
38
+ window_size = min((len(options)), window_size)
39
+
40
+ self.options_original = options
41
+ self.options_current = options
42
+ self.search_indices = []
43
+ self.search_string = ""
44
+ self.label = label
45
+ self.selected_index = 0
46
+ self.window_top = 0
47
+ self.window_size_original = window_size
48
+ self.window_size_current = window_size
49
+ self.help_string = "Enter: Select, Ctl+C: Cancel"
50
+
51
+ def show(self):
52
+ print(f"{ANSI_BLUE}{self.label}>{ANSI_RESET}")
53
+ self.printMenu()
54
+ while True:
55
+ something_changed = False
56
+ char = self.getChar()
57
+ if char == SPECIAL_KEY:
58
+ char = self.getChar()
59
+ if 1 < len(self.options_current):
60
+ # Update selected index
61
+ if char == UP_ARROW:
62
+ self.selected_index = (self.selected_index - 1) % len(self.options_current)
63
+ something_changed = True
64
+ elif char == DOWN_ARROW:
65
+ self.selected_index = (self.selected_index + 1) % len(self.options_current)
66
+ something_changed = True
67
+
68
+ # Update window
69
+ if something_changed:
70
+ bottom = self.window_top + self.window_size_current
71
+ if self.selected_index < self.window_top:
72
+ self.window_top = self.selected_index
73
+ elif bottom <= self.selected_index:
74
+ self.window_top = self.selected_index - self.window_size_current + 1
75
+ elif self.isSearchableChar(char):
76
+ char = chr(char)
77
+ self.search_string = f"{self.search_string}{char}".lstrip()
78
+ if self.search_string:
79
+ print(char, end="", flush=True)
80
+ self.search(self.options_current)
81
+ something_changed = True
82
+ elif char == BACKSPACE and 0 < len(self.search_string):
83
+ self.search_string = self.search_string[:-1]
84
+ # Move left, print space, move left again
85
+ print("\x1b[1D \x1b[1D", end="", flush=True)
86
+ self.search(self.options_original)
87
+ something_changed = True
88
+ elif char == ENTER_KEY and self.options_current:
89
+ self.printSelected()
90
+ return self.options_current[self.selected_index]
91
+ elif char == CTL_C:
92
+ self.clearMenu(clear_label=True)
93
+ print("cancelled")
94
+ return None
95
+
96
+ if something_changed:
97
+ self.clearMenu()
98
+ self.printMenu()
99
+
100
+ def search(self, search_list: list) -> None:
101
+ found_indices = []
102
+ found_options = []
103
+ for o in search_list:
104
+ found_i = str(o).lower().find(self.search_string.lower())
105
+ if found_i != -1:
106
+ found_indices.append(found_i)
107
+ found_options.append(o)
108
+ self.options_current = found_options
109
+ self.search_indices = found_indices
110
+
111
+ # Reset window after modifying options
112
+ self.window_top = 0
113
+ self.selected_index = 0
114
+ self.window_size_current = min((len(self.options_current), self.window_size_original))
115
+
116
+ def getChar(self) -> int:
117
+ return ord(msvcrt.getch())
118
+
119
+ def isSearchableChar(self, char):
120
+ return (32 <= char and char <= 126)
121
+
122
+ def clearMenu(self, clear_label=False):
123
+ if clear_label:
124
+ print("\x1b[0G\x1b[J", end="", flush=True)
125
+ else:
126
+ print("\x1b[J")
127
+
128
+ def printMenu(self):
129
+ bottom = self.window_top + self.window_size_current
130
+ if not self.options_current:
131
+ print(f"{ANSI_BLUE}no matches found{ANSI_RESET}")
132
+ # NOTE: window size is 0 here after empty search, but we are printing 1 line
133
+ # so we need to set it so the footer prints correctly
134
+ self.window_size_current = 1
135
+ else:
136
+ for i in range(self.window_top, bottom):
137
+ option_to_print = str(self.options_current[i])
138
+ if self.search_indices:
139
+ highlight_beg = self.search_indices[i]
140
+ highlight_end = highlight_beg + len(self.search_string)
141
+ opt_beg = option_to_print[0 : highlight_beg]
142
+ opt_mid = option_to_print[highlight_beg : highlight_end]
143
+ opt_end = option_to_print[highlight_end :]
144
+ if i == self.selected_index:
145
+ option_to_print = f"{ANSI_HIGHLIGHT}{opt_beg}{ANSI_HIGHLIGHT_SEARCH_STRING}{opt_mid}{ANSI_HIGHLIGHT}{opt_end}{ANSI_RESET}"
146
+ else:
147
+ option_to_print = f"{opt_beg}{ANSI_MAGENTA}{opt_mid}{ANSI_RESET}{opt_end}"
148
+ elif i == self.selected_index:
149
+ option_to_print = f"{ANSI_HIGHLIGHT}{option_to_print}{ANSI_RESET}"
150
+ print(option_to_print)
151
+ print(f"{ANSI_YELLOW}{self.help_string}{ANSI_RESET}\n{ANSI_MOVE_CURSOR.format(up=self.window_size_current + 2, right=len(self.label) + len(self.search_string) + 1)}", end="", flush=True)
152
+
153
+ def printSelected(self):
154
+ self.clearMenu(clear_label=True)
155
+ print(f"{self.label}> {self.options_current[self.selected_index]}")
156
+
157
+ def makeSelection(options: list[Any], label: str, window_size: int=None) -> Any:
158
+ """
159
+ Entry point for menu selection.
160
+
161
+ Parameters
162
+ ----------
163
+ options
164
+ List of str-able objects.
165
+ label
166
+ Label to describe the items being selected.
167
+ window_size
168
+ Max number of items to show at once.
169
+
170
+ Returns
171
+ -------
172
+ Selected value.
173
+ """
174
+ if window_size:
175
+ return Menu(options, label, window_size).show()
176
+ else:
177
+ return Menu(options, label).show()
178
+
179
+ if __name__ == "__main__":
180
+ print(f"Returns: '{makeSelection(['interactive', 'cli', 'menu'], 'make_selection')}'")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: make_selection
3
- Version: 0.0.1
3
+ Version: 1.0.0
4
4
  Summary: Package for interactive command line menu
5
5
  Author-email: Steven Frazee <stevefrazee123@gmail.com>
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,140 +0,0 @@
1
- """
2
- Module for interactive command line menu. Simply accepts a list of str-able objects
3
- and allows user to select using arrow keys.
4
- """
5
- import sys
6
- if sys.platform != "win32":
7
- raise NotImplementedError("This module is only available on Windows.")
8
- from typing import Any
9
- import msvcrt
10
- import ctypes
11
-
12
- stdout = -11
13
- enable_ansi_codes = 7
14
- kernel32 = ctypes.windll.kernel32
15
- kernel32.SetConsoleMode(kernel32.GetStdHandle(stdout), enable_ansi_codes)
16
-
17
- ANSI_MOVE_CURSOR = "\x1b[{up}F\r\x1b[{right}C"
18
- ANSI_HIGHLIGHT = "\x1b[30;47m"
19
- ANSI_YELLOW = "\x1b[93m"
20
- ANSI_BLUE = "\x1b[94m"
21
- ANSI_RESET = "\x1b[0m"
22
- SPECIAL_KEY = 224
23
- UP_ARROW = 72
24
- DOWN_ARROW = 80
25
- ENTER_KEY = 13
26
- CTL_C = 3
27
- BACKSPACE = 8
28
- SPACEBAR = 32
29
-
30
- class Menu:
31
- def __init__(self, options: list, label: str, window_size: int=10) -> None:
32
- assert options
33
- assert label
34
- assert 1 < window_size
35
- if len(options) < window_size:
36
- window_size = len(options)
37
-
38
- self.options_original = options
39
- self.options_current = options
40
- self.indices = []
41
- self.search_string = ""
42
- self.label = label
43
- self.selected_index = 0
44
- self.window_top = 0
45
- self.window_original_size = window_size
46
- self.window_current_size = window_size
47
- self.help_string = "Enter: Select, Ctl+C: Cancel"
48
-
49
- def show(self):
50
- print(f"{ANSI_BLUE}{self.label}>{ANSI_RESET}")
51
- self.printMenu()
52
- while True:
53
- something_changed = False
54
- char = self.getChar()
55
- if char == SPECIAL_KEY:
56
- char = self.getChar()
57
- if 1 < len(self.options_current):
58
- # Update selected index
59
- if char == UP_ARROW:
60
- self.selected_index = (self.selected_index - 1) % len(self.options_current)
61
- something_changed = True
62
- elif char == DOWN_ARROW:
63
- self.selected_index = (self.selected_index + 1) % len(self.options_current)
64
- something_changed = True
65
-
66
- # Update window
67
- if something_changed:
68
- bottom = self.window_top + self.window_current_size
69
- if self.selected_index < self.window_top:
70
- self.window_top = self.selected_index
71
- elif bottom <= self.selected_index:
72
- self.window_top = self.selected_index - self.window_current_size + 1
73
- elif self.isAscii(char) or (char == SPACEBAR and 0 < len(self.search_string)):
74
- # TODO: search string
75
- pass
76
- elif char == BACKSPACE and 0 < len(self.search_string):
77
- # TODO: search string
78
- pass
79
- elif char == ENTER_KEY:
80
- if self.options_current:
81
- self.printSelected()
82
- return self.options_original[self.selected_index]
83
- elif char == CTL_C:
84
- self.clearMenu(clear_label=True)
85
- print("cancelled")
86
- return None
87
-
88
- if something_changed:
89
- self.clearMenu()
90
- self.printMenu()
91
-
92
- def getChar(self):
93
- return ord(msvcrt.getch())
94
-
95
- def isAscii(self, char):
96
- return (33 <= char and char <= 126)
97
-
98
- def clearMenu(self, clear_label=False):
99
- if clear_label:
100
- print("\x1b[0G\x1b[J", end="", flush=True)
101
- else:
102
- print("\x1b[J")
103
-
104
- def printMenu(self):
105
- bottom = self.window_top + self.window_current_size
106
- for i in range(self.window_top, bottom):
107
- if i == self.selected_index:
108
- print(f"{ANSI_HIGHLIGHT}{self.options_current[i]}{ANSI_RESET}")
109
- else:
110
- print(self.options_current[i])
111
- print(f"{ANSI_YELLOW}{self.help_string}{ANSI_RESET}\n{ANSI_MOVE_CURSOR.format(up=self.window_current_size + 2, right=len(self.label) + len(self.search_string) + 1)}", end="", flush=True)
112
-
113
- def printSelected(self):
114
- self.clearMenu(clear_label=True)
115
- print(f"{self.label}> {self.options_current[self.selected_index]}")
116
-
117
- def makeSelection(options: list[Any], label: str, window_size: int=None) -> Any:
118
- """
119
- Entry point for menu selection.
120
-
121
- Parameters
122
- ----------
123
- options
124
- List of str-able objects.
125
- label
126
- Label to describe the items being selected.
127
- window_size
128
- Max number of items to show at once.
129
-
130
- Returns
131
- -------
132
- Selected value.
133
- """
134
- if window_size:
135
- return Menu(options, label, window_size).show()
136
- else:
137
- return Menu(options, label).show()
138
-
139
- if __name__ == "__main__":
140
- makeSelection(["interactive", "cli", "menu"], "make_selection")
File without changes
File without changes
File without changes