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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: make_selection
3
- Version: 1.0.0
3
+ Version: 1.0.2
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 = "1.0.0"
3
+ version = "1.0.2"
4
4
  authors = [
5
5
  { name="Steven Frazee", email="stevefrazee123@gmail.com" },
6
6
  ]
@@ -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
- self.help_string = "Enter: Select, Ctl+C: Cancel"
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 1 < len(self.options_current):
60
- # Update selected index
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.printSelected()
90
- return self.options_current[self.selected_index]
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(clear_label=True)
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 clearMenu(self, clear_label=False):
123
- if clear_label:
124
- print("\x1b[0G\x1b[J", end="", flush=True)
125
- else:
126
- print("\x1b[J")
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
- 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)
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(clear_label=True)
155
- print(f"{self.label}> {self.options_current[self.selected_index]}")
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)}'")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: make_selection
3
- Version: 1.0.0
3
+ Version: 1.0.2
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
File without changes
File without changes
File without changes