make-selection 1.0.0__tar.gz → 1.0.2__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.
- {make_selection-1.0.0 → make_selection-1.0.2}/PKG-INFO +1 -1
- {make_selection-1.0.0 → make_selection-1.0.2}/pyproject.toml +1 -1
- {make_selection-1.0.0 → make_selection-1.0.2}/src/make_selection/make_selection.py +80 -30
- {make_selection-1.0.0 → make_selection-1.0.2}/src/make_selection.egg-info/PKG-INFO +1 -1
- {make_selection-1.0.0 → make_selection-1.0.2}/LICENSE +0 -0
- {make_selection-1.0.0 → make_selection-1.0.2}/README.md +0 -0
- {make_selection-1.0.0 → make_selection-1.0.2}/setup.cfg +0 -0
- {make_selection-1.0.0 → make_selection-1.0.2}/src/make_selection/__init__.py +0 -0
- {make_selection-1.0.0 → make_selection-1.0.2}/src/make_selection.egg-info/SOURCES.txt +0 -0
- {make_selection-1.0.0 → make_selection-1.0.2}/src/make_selection.egg-info/dependency_links.txt +0 -0
- {make_selection-1.0.0 → make_selection-1.0.2}/src/make_selection.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Module for interactive command line menu. Simply accepts a list of str-able objects
|
|
3
3
|
and allows user to select using arrow keys.
|
|
4
|
+
|
|
5
|
+
Ansi escape codes are used as described here: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
|
|
4
6
|
"""
|
|
7
|
+
# TODO: work on all platforms
|
|
8
|
+
# TODO: multi_select: Maintain original index for re-insertion.
|
|
9
|
+
# TODO: multi_select: TAB switches to delete mode.
|
|
5
10
|
import sys
|
|
6
11
|
if sys.platform != "win32":
|
|
7
12
|
raise NotImplementedError("This module is only available on Windows.")
|
|
8
13
|
from typing import Any
|
|
14
|
+
from copy import copy
|
|
9
15
|
import msvcrt
|
|
10
16
|
import ctypes
|
|
11
17
|
|
|
@@ -20,6 +26,7 @@ ANSI_HIGHLIGHT_SEARCH_STRING = "\x1b[95;47m"
|
|
|
20
26
|
ANSI_YELLOW = "\x1b[93m"
|
|
21
27
|
ANSI_BLUE = "\x1b[94m"
|
|
22
28
|
ANSI_MAGENTA = "\x1b[95m"
|
|
29
|
+
ANSI_GREEN = "\x1b[92m"
|
|
23
30
|
ANSI_RESET = "\x1b[0m"
|
|
24
31
|
|
|
25
32
|
SPECIAL_KEY = 224
|
|
@@ -29,35 +36,43 @@ ENTER_KEY = 13
|
|
|
29
36
|
CTL_C = 3
|
|
30
37
|
BACKSPACE = 8
|
|
31
38
|
SPACEBAR = 32
|
|
39
|
+
CTL_RIGHT = 116
|
|
32
40
|
|
|
33
41
|
class Menu:
|
|
34
|
-
def __init__(self, options: list, label: str, window_size: int=10) -> None:
|
|
42
|
+
def __init__(self, options: list, label: str, window_size: int=10, multi_select: bool=False) -> None:
|
|
35
43
|
assert options
|
|
36
44
|
assert label
|
|
37
|
-
assert 1 <= window_size
|
|
45
|
+
assert 1 <= window_size and window_size <= 25
|
|
38
46
|
window_size = min((len(options)), window_size)
|
|
39
47
|
|
|
40
48
|
self.options_original = options
|
|
41
|
-
self.options_current = options
|
|
49
|
+
self.options_current = copy(options)
|
|
50
|
+
self.options_selected = []
|
|
42
51
|
self.search_indices = []
|
|
43
52
|
self.search_string = ""
|
|
44
53
|
self.label = label
|
|
45
54
|
self.selected_index = 0
|
|
55
|
+
self.multi_select = multi_select
|
|
46
56
|
self.window_top = 0
|
|
47
57
|
self.window_size_original = window_size
|
|
48
58
|
self.window_size_current = window_size
|
|
49
|
-
|
|
59
|
+
if multi_select:
|
|
60
|
+
self.help_string = "Enter: Select, Ctl+C: Cancel, Ctl\u2192: Done"
|
|
61
|
+
else:
|
|
62
|
+
self.help_string = "Enter: Select, Ctl+C: Cancel"
|
|
50
63
|
|
|
51
64
|
def show(self):
|
|
52
|
-
print(f"{ANSI_BLUE}{self.label}>{ANSI_RESET}")
|
|
53
65
|
self.printMenu()
|
|
54
66
|
while True:
|
|
55
67
|
something_changed = False
|
|
56
68
|
char = self.getChar()
|
|
57
69
|
if char == SPECIAL_KEY:
|
|
58
70
|
char = self.getChar()
|
|
59
|
-
if
|
|
60
|
-
|
|
71
|
+
if char == CTL_RIGHT and self.multi_select:
|
|
72
|
+
self.printSelected()
|
|
73
|
+
return self.options_selected
|
|
74
|
+
elif 1 < len(self.options_current):
|
|
75
|
+
# NOTE: Update selected index
|
|
61
76
|
if char == UP_ARROW:
|
|
62
77
|
self.selected_index = (self.selected_index - 1) % len(self.options_current)
|
|
63
78
|
something_changed = True
|
|
@@ -65,7 +80,7 @@ class Menu:
|
|
|
65
80
|
self.selected_index = (self.selected_index + 1) % len(self.options_current)
|
|
66
81
|
something_changed = True
|
|
67
82
|
|
|
68
|
-
# Update window
|
|
83
|
+
# NOTE: Update window
|
|
69
84
|
if something_changed:
|
|
70
85
|
bottom = self.window_top + self.window_size_current
|
|
71
86
|
if self.selected_index < self.window_top:
|
|
@@ -81,15 +96,19 @@ class Menu:
|
|
|
81
96
|
something_changed = True
|
|
82
97
|
elif char == BACKSPACE and 0 < len(self.search_string):
|
|
83
98
|
self.search_string = self.search_string[:-1]
|
|
84
|
-
# Move left, print space, move left again
|
|
99
|
+
# NOTE: Move left once ([1D), print space, move left again
|
|
85
100
|
print("\x1b[1D \x1b[1D", end="", flush=True)
|
|
86
101
|
self.search(self.options_original)
|
|
87
102
|
something_changed = True
|
|
88
103
|
elif char == ENTER_KEY and self.options_current:
|
|
89
|
-
self.
|
|
90
|
-
|
|
104
|
+
if self.multi_select:
|
|
105
|
+
self.multiSelectAdd()
|
|
106
|
+
something_changed = True
|
|
107
|
+
else:
|
|
108
|
+
self.printSelected()
|
|
109
|
+
return self.options_current[self.selected_index]
|
|
91
110
|
elif char == CTL_C:
|
|
92
|
-
self.clearMenu(
|
|
111
|
+
self.clearMenu()
|
|
93
112
|
print("cancelled")
|
|
94
113
|
return None
|
|
95
114
|
|
|
@@ -107,25 +126,44 @@ class Menu:
|
|
|
107
126
|
found_options.append(o)
|
|
108
127
|
self.options_current = found_options
|
|
109
128
|
self.search_indices = found_indices
|
|
129
|
+
self.resetWindow()
|
|
130
|
+
|
|
131
|
+
def multiSelectAdd(self) -> None:
|
|
132
|
+
selected_option = self.options_current[self.selected_index]
|
|
133
|
+
self.options_current.remove(selected_option)
|
|
134
|
+
self.options_original.remove(selected_option)
|
|
135
|
+
self.options_selected.append(selected_option)
|
|
136
|
+
self.resetWindow()
|
|
110
137
|
|
|
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
138
|
def getChar(self) -> int:
|
|
117
139
|
return ord(msvcrt.getch())
|
|
118
140
|
|
|
119
141
|
def isSearchableChar(self, char):
|
|
120
142
|
return (32 <= char and char <= 126)
|
|
121
143
|
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
def resetWindow(self):
|
|
145
|
+
self.window_top = 0
|
|
146
|
+
self.selected_index = 0
|
|
147
|
+
self.window_size_current = min((len(self.options_current), self.window_size_original))
|
|
148
|
+
|
|
149
|
+
def clearMenu(self):
|
|
150
|
+
"""
|
|
151
|
+
Clears from beginning of current line to end of screen (not end of line).
|
|
152
|
+
This assumes the cursor is on the top line (label), which is currently always true.
|
|
153
|
+
|
|
154
|
+
[0G move cursor to column 0.
|
|
155
|
+
[J clears to end of screen (not end of line).
|
|
156
|
+
"""
|
|
157
|
+
print("\x1b[0G\x1b[J", end="", flush=True)
|
|
127
158
|
|
|
128
159
|
def printMenu(self):
|
|
160
|
+
header = f"{ANSI_BLUE}{self.label}>{ANSI_RESET}{self.search_string}"
|
|
161
|
+
header_num_lines = 1
|
|
162
|
+
if self.multi_select:
|
|
163
|
+
header += f"\n{ANSI_GREEN}{len(self.options_selected)} items added!{ANSI_RESET}"
|
|
164
|
+
header_num_lines = 2
|
|
165
|
+
print(header)
|
|
166
|
+
|
|
129
167
|
bottom = self.window_top + self.window_size_current
|
|
130
168
|
if not self.options_current:
|
|
131
169
|
print(f"{ANSI_BLUE}no matches found{ANSI_RESET}")
|
|
@@ -148,13 +186,22 @@ class Menu:
|
|
|
148
186
|
elif i == self.selected_index:
|
|
149
187
|
option_to_print = f"{ANSI_HIGHLIGHT}{option_to_print}{ANSI_RESET}"
|
|
150
188
|
print(option_to_print)
|
|
151
|
-
|
|
189
|
+
|
|
190
|
+
footer = f"{ANSI_YELLOW}{self.help_string}{ANSI_RESET}\n"
|
|
191
|
+
footer_num_lines = 1
|
|
192
|
+
print(f"{footer}{ANSI_MOVE_CURSOR.format(up=self.window_size_current + header_num_lines + footer_num_lines, right=len(self.label) + len(self.search_string) + 1)}", end="", flush=True)
|
|
152
193
|
|
|
153
194
|
def printSelected(self):
|
|
154
|
-
self.clearMenu(
|
|
155
|
-
|
|
195
|
+
self.clearMenu()
|
|
196
|
+
if self.multi_select:
|
|
197
|
+
if len(self.options_selected) <= 3:
|
|
198
|
+
print(f"{self.label}> {self.options_selected} ({len(self.options_selected)} items)")
|
|
199
|
+
else:
|
|
200
|
+
print(f"{self.label}> [{self.options_selected[0]}, ..., {self.options_selected[-1]}] ({len(self.options_selected)} items)")
|
|
201
|
+
else:
|
|
202
|
+
print(f"{self.label}> {self.options_current[self.selected_index]}")
|
|
156
203
|
|
|
157
|
-
def makeSelection(options: list[Any], label: str, window_size: int=None) -> Any:
|
|
204
|
+
def makeSelection(options: list[Any], label: str, window_size: int=None, multi_select: bool=False) -> Any:
|
|
158
205
|
"""
|
|
159
206
|
Entry point for menu selection.
|
|
160
207
|
|
|
@@ -166,15 +213,18 @@ def makeSelection(options: list[Any], label: str, window_size: int=None) -> Any:
|
|
|
166
213
|
Label to describe the items being selected.
|
|
167
214
|
window_size
|
|
168
215
|
Max number of items to show at once.
|
|
216
|
+
multi_select
|
|
217
|
+
Select list of items.
|
|
169
218
|
|
|
170
219
|
Returns
|
|
171
220
|
-------
|
|
172
|
-
Selected value.
|
|
221
|
+
Selected value or list of selected values.
|
|
173
222
|
"""
|
|
174
223
|
if window_size:
|
|
175
|
-
return Menu(options, label, window_size).show()
|
|
224
|
+
return Menu(options, label, window_size=window_size, multi_select=multi_select).show()
|
|
176
225
|
else:
|
|
177
|
-
return Menu(options, label).show()
|
|
226
|
+
return Menu(options, label, multi_select=multi_select).show()
|
|
178
227
|
|
|
179
228
|
if __name__ == "__main__":
|
|
180
|
-
print(f"Returns: '{makeSelection(['interactive', 'cli', 'menu'], 'make_selection')}'")
|
|
229
|
+
print(f"Returns: '{makeSelection(['interactive', 'cli', 'menu'], 'make_selection')}'")
|
|
230
|
+
print(f"Returns: '{makeSelection(['interactive', 'cli', 'menu'], 'make_selection', multi_select=True)}'")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{make_selection-1.0.0 → make_selection-1.0.2}/src/make_selection.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|