-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathutils.py
356 lines (300 loc) · 11.9 KB
/
utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
"""
A collection of utility functions for the editor.
"""
import curses
import os
from functools import partial
from math import ceil
import typing_extensions
from typing import Tuple
def _return_list_with_substrings(lst: tuple, substring: str, enabled: bool) -> tuple:
"""
Function returning only the elements of the list that contain the given substring.
:param lst: A tuple of commands for the display_menu function.
:param substring: A string to look for, that has to be in the menu item's name.
:param enabled: Whether to enable the function. If false, will return lst.
:return: A tuple of 2-tuples of commands for the display_menu function in the form (index, command).
"""
if not enabled:
return tuple((i, e) for i, e in enumerate(lst))
else:
new_lst = []
for i, element in enumerate(lst):
if substring.lower() in element[0].lower():
new_lst.append((i, element))
return tuple(new_lst)
def display_menu(
stdscr, commands: tuple, default_selected_element: int = 0, label: str = None, clear: bool = True,
space_out_last_option: bool = False, allow_key_input: bool = False, highlight_indexes: Tuple[int, ...] = tuple(),
highlight_pair: int = None, align_left: bool = False
):
"""
Displays a menu at the center of the screen, with every option chosen by the user.
:param stdscr: The standard screen.
:param commands: A tuple of commands.
:param default_selected_element: The menu element selected by default. 0 by default. It is composed of
tuples of 2 elements : the command name, and the function to call upon selection.
:param label: Displays a title above the menu. None by default.
:param clear: Whether to clear the screen before creating the menu. True by default.
:param space_out_last_option: Adds a newline before the last option of the menu.
:param allow_key_input: If true, allows the user to type in a string. The menu will only show the elements
containing the string.
:param highlight_indexes: A tuple of indexes from the commands tuple that should be highlighted in instruction color
:param highlight_pair: The index of the color pair to use for highlighting.
:param align_left: Whether to align the commands on the left side. False by default. Last option is still centered
if space_out_last_option is True.
"""
# Gets the middle of the screen coordinates
screen_middle_y, screen_middle_x = get_screen_middle_coords(stdscr)
# Selects an element
selected_element = default_selected_element
# Gets the amount of given commands, and stores it into a variable, for optimization purposes.
cmd_len = len(commands)
# Clears the contents of the screen
if clear:
stdscr.clear()
# Gets the rows and columns
rows, cols = stdscr.getmaxyx()
# Keeps in mind the amount of pages and the current page
max_items_per_page = rows - 5 - allow_key_input
current_page = 0
total_pages = ceil(cmd_len / max_items_per_page)
# Initializing the key
key = ""
# Initializing the string to search for
string_to_search_for = ""
# Looping until the user selects an item
while key not in ('\n', '\t', "PADENTER"):
# Displays the menu title
if label is not None:
# Checking for the horizontal size
if len(label) > cols - 5:
label = label[:cols - 5] + "..."
# Displaying label
stdscr.addstr(
screen_middle_y - min(max_items_per_page, cmd_len) // 2 - 2,
screen_middle_x - len(label) // 2,
label
)
# Displays the current search string
if allow_key_input:
# Checking for the horizontal size
if len(repr(string_to_search_for)) > cols - 5:
string_to_search_for = string_to_search_for[:cols - 5] + "..."
# Displaying label
stdscr.addstr(
screen_middle_y - min(max_items_per_page, cmd_len) // 2 - 1,
screen_middle_x - len(repr(string_to_search_for)) // 2,
repr(string_to_search_for)
)
# Remembering the length of the selected slice
current_command_len = lambda: len(
_return_list_with_substrings(commands, string_to_search_for, allow_key_input)[max_items_per_page * current_page: max_items_per_page * (current_page + 1)]
)
# Remembering the size of the full commands list
size_of_temp_list = len(_return_list_with_substrings(commands, string_to_search_for, allow_key_input))
# Displays the menu
for i, (command_index, command) in enumerate(
# Only displays the menu elements from the current page
_return_list_with_substrings(commands, string_to_search_for, allow_key_input)[max_items_per_page * current_page : max_items_per_page * (current_page + 1)]
):
# Checking for the horizontal size
if len(command[0]) > cols - 5:
command[0] = command[0][:cols - 5] + "..."
# Gets the styling of the menu
styling = curses.A_NORMAL
if highlight_pair is not None and command_index in highlight_indexes:
styling |= curses.color_pair(highlight_pair)
if i == selected_element: # Reverses the color if the item is selected
styling |= curses.A_REVERSE
# -- Gets the y position of the element --
element_y_position = screen_middle_y + i
# Moves the element up or down based on where it is in the list
element_y_position -= min(max_items_per_page, cmd_len) // 2
# If we want to space out the last option of the dropdown and that nothing is being searched
if space_out_last_option and ((not allow_key_input) or (allow_key_input and string_to_search_for == '')):
# If this is the last element of the list, we move it downward one line
if command_index == (size_of_temp_list - 1):
element_y_position += 1
is_last_element = True
else:
is_last_element = False
else:
is_last_element = False
# Pushes the element down further if we allow the key input
if allow_key_input:
element_y_position += 1
# Displays the menu item
if align_left and (is_last_element is False or space_out_last_option is False):
element_x_position = int(cols * 0.3)
else:
element_x_position = screen_middle_x - len(command[0]) // 2
stdscr.addstr(
element_y_position,
element_x_position,
command[0],
styling
)
# Displays at the bottom right how many pages are available
if total_pages > 1:
page_left_str = f"Page {current_page + 1}/{total_pages}"
stdscr.addstr(rows - 3, cols - len(page_left_str) - 3, page_left_str, curses.A_REVERSE)
# Fetches a key
key = stdscr.getkey()
# Selects another item
if key == "KEY_UP":
selected_element -= 1
elif key == "KEY_DOWN":
selected_element += 1
elif key == "KEY_LEFT":
# If this is the first page, we get to the last one
if current_page == 0:
current_page = total_pages - 1
# Putting the cursor at the end if there are fewer elements on this page
if selected_element >= current_command_len():
selected_element = current_command_len() - 1
# Otherwise, we get to the previous
else:
current_page -= 1
stdscr.clear()
elif key == "KEY_RIGHT":
# If this is the last page, we get to the first one
if current_page == total_pages - 1:
current_page = 0
# Otherwise, we get to the next
else:
current_page += 1
# Putting the cursor at the end if there are fewer elements on this page
if selected_element >= current_command_len():
selected_element = current_command_len() - 1
stdscr.clear()
elif key in ('\n', '\t', "PADENTER"): pass
elif allow_key_input:
if key == "\b":
if string_to_search_for != "":
string_to_search_for = string_to_search_for[:-1]
elif key == "\x1b": # Escape key deletes the search
string_to_search_for = ""
else:
string_to_search_for += key
selected_element = 0
stdscr.clear()
# Wrap-around
if selected_element < 0:
selected_element = current_command_len() - 1
elif selected_element >= current_command_len():
selected_element = 0
# Clears the screen
stdscr.clear()
# Calls the function from the appropriate item
try:
# Returns the value given by the appropriate command's execution
return _return_list_with_substrings(commands, string_to_search_for, allow_key_input)\
[selected_element + current_page * max_items_per_page][1][1]()
except IndexError:
return 0
def get_screen_middle_coords(stdscr) -> tuple[int, int]:
"""
Returns the middle coordinates of the screen.
:param stdscr: The standard screen.
:return: A tuple of 2 integers : the middle coordinates of the screen, as (rows, cols).
"""
screen_y_size, screen_x_size = stdscr.getmaxyx()
return screen_y_size // 2, screen_x_size // 2
def input_text(stdscr, position_x: int = 0, position_y: int = None) -> str:
"""
Asks the user for input and then returns the given text.
:param stdscr: The standard screen.
:param position_x: The x coordinates of the input. Default is to the left of the screen.
:param position_y: The y coordinates of the input. Default is to the bottom of the screen.
:return: Returns the string inputted by the user.
"""
# Initializing vars
key = ""
final_text = ""
if position_y is None: position_y = stdscr.getmaxyx()[0] - 1
# Loops until the user presses Enter
while key not in ('\n', "PADENTER"):
# Awaits for a keypress
key = stdscr.getkey()
# Sanitizes the input
if key in ("KEY_BACKSPACE", "\b", "\0"):
# If the character is a backspace, we remove the last character from the final text
final_text = final_text[:-1]
# Removes the character from the screen
stdscr.addstr(position_y, position_x + len(final_text), " ")
elif key == "SHF_PADSLASH": # Fix for '!' character
final_text += "!"
elif key == "KEY_SEND": # Fix for '<' character
final_text += "<"
elif key == "CTL_END": # Fix for '<' character
final_text += ">"
elif key.startswith("KEY_") or (key.startswith("^") and key != "^") or key in ('\n', "PADENTER"):
# Does nothing if it is a special key
pass
else:
# Adds the key to the input
final_text += key
# Shows the final text at the bottom
stdscr.addstr(position_y, position_x, final_text)
# Writes the full length of the final text as spaces where it was written
stdscr.addstr(position_y, position_x, " " * len(final_text))
# Returns the final text
return final_text
class browse_files:
last_browsed_path = ""
def __init__(self, stdscr, given_path:str=None, can_create_files:bool=True):
"""
Browse files to find one, returns a path to this file.
:param stdscr: The standard screen.
:return: A path to the selected file.
"""
self.path = browse_files.last_browsed_path if given_path is None else os.path.normpath(given_path)
self.stdscr = stdscr
self.can_create_files = can_create_files
def __call__(self, stdscr=None, given_path:str=None) -> str:
self.path = browse_files.last_browsed_path if given_path is None else os.path.normpath(given_path)
if self.path == "":
self.path = os.path.normpath(os.path.join(os.path.dirname(__file__), "../"))
if stdscr is not None:
self.stdscr = stdscr
folders_list = []
files_list = []
for element in os.listdir(self.path):
if os.path.isdir(os.path.join(self.path, element)):
folders_list.append(element)
else:
files_list.append(element)
def set_new_path(new_path:str):
self.path = new_path
menu_items = [("📁 ../", partial(self, self.stdscr, os.path.join(self.path, "../")))]
menu_items.extend([
(f"📁 {name}", partial(self, self.stdscr, os.path.join(self.path, name))) \
for name in folders_list
])
menu_items.extend([
(f"📄 {name}", partial(set_new_path, os.path.normpath(os.path.join(self.path, name)))) \
for name in files_list
])
menu_items.extend([
("Cancel", partial(set_new_path, ""))
])
if self.can_create_files:
menu_items.extend([
("New file :", partial(self.create_new_file, len(menu_items) + 1))
])
menu_items = tuple(menu_items)
display_menu(
self.stdscr,
menu_items,
label=self.path,
allow_key_input=True
)
browse_files.last_browsed_path = os.path.dirname(self.path)
return self.path
def create_new_file(self, position_y:int):
"""
Asks the user to input a name for a file and creates it, then sets the path to this file.
"""
filename = input_text(self.stdscr, 30, position_y)
self.path = os.path.normpath(os.path.join(self.path, filename))