listpick 0.1.4.0__py3-none-any.whl

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.
@@ -0,0 +1,2076 @@
1
+ #!/bin/python
2
+
3
+ import curses
4
+ import re
5
+ import os
6
+ import subprocess
7
+ import argparse
8
+ import time
9
+ from wcwidth import wcswidth
10
+ from typing import Callable, Optional, Tuple
11
+
12
+ from list_picker.ui.list_picker_colours import get_colours, get_help_colours, get_notification_colours, get_theme_count
13
+ from list_picker.utils.options_selectors import default_option_input, output_file_option_selector, default_option_selector
14
+ from list_picker.utils.table_to_list_of_lists import *
15
+ from list_picker.utils.utils import *
16
+ from list_picker.utils.sorting import *
17
+ from list_picker.utils.filtering import *
18
+ from list_picker.ui.input_field import *
19
+ from list_picker.utils.clipboard_operations import *
20
+ from list_picker.utils.searching import search
21
+ from list_picker.ui.help_screen import help_lines
22
+ from list_picker.ui.keys import list_picker_keys, notification_keys, options_keys, help_keys
23
+ from list_picker.utils.generate_data import generate_list_picker_data
24
+ from list_picker.utils.dump import dump_state, load_state, dump_data
25
+
26
+ try:
27
+ from tmp.data_stuff import test_items, test_highlights, test_header
28
+ except:
29
+ test_items, test_highlights, test_header = [], [], []
30
+
31
+ COLOURS_SET = False
32
+ help_colours, notification_colours = {}, {}
33
+
34
+
35
+ class Picker:
36
+ def __init__(self,
37
+ stdscr: curses.window,
38
+ items: list[list[str]] = [],
39
+ cursor_pos: int = 0,
40
+ colours: dict = get_colours(0),
41
+ colour_theme_number: int = 0,
42
+ max_selected: int = -1,
43
+ top_gap: int =0,
44
+ title: str ="List Picker",
45
+ header: list =[],
46
+ max_column_width: int =70,
47
+ clear_on_start: bool = False,
48
+
49
+ auto_refresh: bool =False,
50
+ timer: float = 5,
51
+
52
+ get_new_data: bool =False, # Whether we can get new data
53
+ refresh_function: Optional[Callable] = lambda: [], # The function with which we get new data
54
+ get_data_startup: bool =False, # Whether we should get data at statrup
55
+ track_entries_upon_refresh: bool = True,
56
+ id_column: int = 0,
57
+
58
+ unselectable_indices: list =[],
59
+ highlights: list =[],
60
+ highlights_hide: bool =False,
61
+ number_columns: bool =True,
62
+ column_widths: list = [],
63
+ column_indices: list = [],
64
+
65
+
66
+ current_row : int = 0,
67
+ current_page : int = 0,
68
+ is_selecting : bool = False,
69
+ is_deselecting : int = False,
70
+ start_selection: int = -1,
71
+ end_selection: int = -1,
72
+ user_opts : str = "",
73
+ user_settings : str = "",
74
+ separator : str = " ",
75
+ search_query : str = "",
76
+ search_count : int = 0,
77
+ search_index : int = 0,
78
+ filter_query : str = "",
79
+ hidden_columns: list = [],
80
+ indexed_items: list[Tuple[int, list[str]]] = [],
81
+ scroll_bar : int = True,
82
+
83
+ selections: dict = {},
84
+ highlight_full_row: bool =False,
85
+
86
+ items_per_page : int = -1,
87
+ sort_method : int = 0,
88
+ sort_reverse: list[bool] = [False],
89
+ sort_column : int = 0,
90
+ columns_sort_method: list[int] = [0],
91
+ key_chain: str = "",
92
+ last_key: Optional[str] = None,
93
+
94
+ paginate: bool =False,
95
+ cancel_is_back: bool = False,
96
+ mode_index: int =0,
97
+ modes: list[dict] = [{}],
98
+ display_modes: bool =False,
99
+ require_option: list=[],
100
+ option_functions: list[Callable[..., Tuple[bool, str]]] = [],
101
+ disabled_keys: list=[],
102
+
103
+ show_footer: bool =True,
104
+ footer_string: str="",
105
+ footer_string_auto_refresh: bool=False,
106
+ footer_string_refresh_function: Optional[Callable] = None,
107
+ footer_timer=1,
108
+
109
+ colours_start: int =0,
110
+ colours_end: int =-1,
111
+ key_remappings: dict = {},
112
+ keys_dict:dict = list_picker_keys,
113
+ display_infobox : bool = False,
114
+ infobox_items: list[list[str]] = [],
115
+ infobox_title: str = "",
116
+ display_only: bool = False,
117
+
118
+ editable_columns: list[int] = [],
119
+
120
+ centre_in_terminal: bool = False,
121
+ centre_in_terminal_vertical: bool = False,
122
+ centre_in_cols: bool = False,
123
+
124
+ startup_notification:str = "",
125
+ ):
126
+ self.stdscr = stdscr
127
+ self.items = items
128
+ self.cursor_pos = cursor_pos
129
+ self.colours = colours
130
+ self.colour_theme_number = colour_theme_number
131
+ self.max_selected = max_selected
132
+ self.top_gap = top_gap
133
+ self.title = title
134
+ self.header = header
135
+ self.max_column_width = max_column_width
136
+ self.clear_on_start = clear_on_start
137
+
138
+ self.auto_refresh = auto_refresh
139
+ self.timer = timer
140
+
141
+ self.get_new_data = get_new_data
142
+ self.refresh_function = refresh_function
143
+ self.get_data_startup = get_data_startup
144
+ self.track_entries_upon_refresh = track_entries_upon_refresh
145
+ self.id_column = id_column
146
+
147
+ self.unselectable_indices = unselectable_indices
148
+ self.highlights = highlights
149
+ self.highlights_hide = highlights_hide
150
+ self.number_columns = number_columns
151
+ self.column_widths, = [],
152
+ self.column_indices, = [],
153
+
154
+
155
+ self.current_row = current_row
156
+ self.current_page = current_page
157
+ self.is_selecting = is_selecting
158
+ self.is_deselecting = is_deselecting
159
+ self.start_selection = start_selection
160
+ self.end_selection = end_selection
161
+ self.user_opts = user_opts
162
+ self.user_settings = user_settings
163
+ self.separator = separator
164
+ self.search_query = search_query
165
+ self.search_count = search_count
166
+ self.search_index = search_index
167
+ self.filter_query = filter_query
168
+ self.hidden_columns = hidden_columns
169
+ self.indexed_items = indexed_items
170
+ self.scroll_bar = scroll_bar
171
+
172
+ self.selections = selections
173
+ self.highlight_full_row = highlight_full_row
174
+
175
+ self.items_per_page = items_per_page
176
+ self.sort_method = sort_method
177
+ self.sort_reverse = sort_reverse
178
+ self.sort_column = sort_column
179
+ self.columns_sort_method = columns_sort_method
180
+ self.key_chain = key_chain
181
+ self.last_key = last_key
182
+
183
+ self.paginate = paginate
184
+ self.cancel_is_back = cancel_is_back
185
+ self.mode_index = mode_index
186
+ self.modes = modes
187
+ self.display_modes = display_modes
188
+ self.require_option = require_option
189
+ self.option_functions = option_functions
190
+ self.disabled_keys = disabled_keys
191
+
192
+ self.show_footer = show_footer
193
+ self.footer_string = footer_string
194
+ self.footer_string_auto_refresh = footer_string_auto_refresh
195
+ self.footer_string_refresh_function = footer_string_refresh_function
196
+ self.footer_timer = footer_timer
197
+
198
+
199
+ self.colours_start = colours_start
200
+ self.colours_end = colours_end
201
+ self.key_remappings = key_remappings
202
+ self.keys_dict = keys_dict
203
+ self.display_infobox = display_infobox
204
+ self.infobox_items = infobox_items
205
+ self.infobox_title = infobox_title
206
+ self.display_only = display_only
207
+
208
+ self.editable_columns = editable_columns
209
+
210
+ self.centre_in_terminal = centre_in_terminal
211
+ self.centre_in_terminal_vertical = centre_in_terminal_vertical
212
+ self.centre_in_cols = centre_in_cols
213
+
214
+ self.startup_notification = startup_notification
215
+
216
+
217
+ self.registers = {}
218
+ self.SORT_METHODS = ['original', 'lexical', 'LEXICAL', 'alphanum', 'ALPHANUM', 'time', 'numerical', 'size']
219
+
220
+ curses.set_escdelay(25)
221
+
222
+
223
+ def initialise_variables(self, get_data: bool = False) -> None:
224
+ """ Initialise the variables that keep track of the data. """
225
+
226
+ tracking, ids, cursor_pos_id = False, [], 0
227
+
228
+ if get_data and self.refresh_function != None:
229
+ if self.track_entries_upon_refresh and len(self.items) > 0:
230
+ tracking = True
231
+ selected_indices = get_selected_indices(self.selections)
232
+ ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
233
+
234
+ if len(self.indexed_items) > 0 and len(self.indexed_items) >= self.cursor_pos and len(self.indexed_items[0][1]) >= self.id_column:
235
+ cursor_pos_id = self.indexed_items[self.cursor_pos][1][self.id_column]
236
+
237
+ self.items, self.header = self.refresh_function()
238
+
239
+
240
+ if self.items == []: self.items = [[]]
241
+ ## Ensure that items is a List[List[Str]] object
242
+ if len(self.items) > 0 and not isinstance(self.items[0], list):
243
+ self.items = [[item] for item in self.items]
244
+ self.items = [[str(cell) for cell in row] for row in self.items]
245
+
246
+
247
+ # Ensure that header is of the same length as the rows
248
+ if self.header and len(self.items) > 0 and len(self.header) != len(self.items[0]):
249
+ self.header = [str(self.header[i]) if i < len(self.header) else "" for i in range(len(self.items[0]))]
250
+
251
+ # Constants
252
+ # DEFAULT_ITEMS_PER_PAGE = os.get_terminal_size().lines - top_gap*2-2-int(bool(header))
253
+ top_space = self.top_gap
254
+ if self.title: top_space+=1
255
+ if self.display_modes: top_space+=1
256
+
257
+ h, w = self.stdscr.getmaxyx()
258
+
259
+ self.items_per_page = h - top_space-int(bool(self.header)) - 3*int(bool(self.show_footer))
260
+ if not self.show_footer and self.footer_string: self.items_per_page-=1
261
+ self.items_per_page = min(h-top_space-2, self.items_per_page)
262
+
263
+
264
+
265
+ # Initial states
266
+ if len(self.selections) != len(self.items):
267
+ self.selections = {i : False if i not in self.selections else bool(self.selections[i]) for i in range(len(self.items))}
268
+
269
+ if len(self.require_option) < len(self.items):
270
+ self.require_option += [False for i in range(len(self.items)-len(self.require_option))]
271
+ if len(self.option_functions) < len(self.items):
272
+ self.option_functions += [None for i in range(len(self.items)-len(self.option_functions))]
273
+ if len(self.items)>0 and len(self.columns_sort_method) < len(self.items[0]):
274
+ self.columns_sort_method = self.columns_sort_method + [0 for i in range(len(self.items[0])-len(self.columns_sort_method))]
275
+ if len(self.items)>0 and len(self.sort_reverse) < len(self.items[0]):
276
+ self.sort_reverse = self.sort_reverse + [False for i in range(len(self.items[0])-len(self.sort_reverse))]
277
+ if len(self.items)>0 and len(self.editable_columns) < len(self.items[0]):
278
+ self.editable_columns = self.editable_columns + [False for i in range(len(self.items[0])-len(self.editable_columns))]
279
+ if len(self.items)>0 and len(self.column_indices) < len(self.items[0]):
280
+ self.column_indices = self.column_indices + [i for i in range(len(self.column_indices), len(self.items[0]))]
281
+
282
+
283
+
284
+ # items2 = [[row[self.column_indices[i]] for i in range(len(row))] for row in self.items]
285
+ # self.indexed_items = list(enumerate(items2))
286
+ if self.items == [[]]: self.indexed_items = []
287
+ else: self.indexed_items = list(enumerate(self.items))
288
+
289
+ # If a filter is passed then refilter
290
+ if self.filter_query:
291
+ # prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
292
+ # prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
293
+ self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
294
+ if self.cursor_pos in [x[0] for x in self.indexed_items]: self.cursor_pos = [x[0] for x in self.indexed_items].index(self.cursor_pos)
295
+ else: self.cursor_pos = 0
296
+ if self.search_query:
297
+ return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
298
+ query=self.search_query,
299
+ indexed_items=self.indexed_items,
300
+ highlights=self.highlights,
301
+ cursor_pos=self.cursor_pos,
302
+ unselectable_indices=self.unselectable_indices,
303
+ continue_search=True,
304
+ )
305
+ if return_val:
306
+ self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
307
+ # If a sort is passed
308
+ if len(self.indexed_items) > 0:
309
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
310
+ if len(self.items[0]) == 1:
311
+ self.number_columns = False
312
+
313
+
314
+
315
+ h, w = self.stdscr.getmaxyx()
316
+
317
+ # Adjust variables to ensure correctness if errors
318
+ ## Move to a selectable row (if applicable)
319
+ if len(self.items) <= len(self.unselectable_indices): self.unselectable_indices = []
320
+ new_pos = (self.cursor_pos)%len(self.items)
321
+ while new_pos in self.unselectable_indices and new_pos != self.cursor_pos:
322
+ new_pos = (new_pos + 1) % len(self.items)
323
+
324
+ assert new_pos < len(self.items)
325
+ self.cursor_pos = new_pos
326
+
327
+
328
+ if tracking and len(self.items) > 1:
329
+ selected_indices = []
330
+ all_ids = [item[self.id_column] for item in self.items]
331
+ self.selections = {i: False for i in range(len(self.items))}
332
+ for id in ids:
333
+ if id in all_ids:
334
+ selected_indices.append(all_ids.index(id))
335
+ self.selections[all_ids.index(id)] = True
336
+
337
+ if cursor_pos_id in all_ids:
338
+ cursor_pos_x = all_ids.index(cursor_pos_id)
339
+ if cursor_pos_x in [i[0] for i in self.indexed_items]:
340
+ self.cursor_pos = [i[0] for i in self.indexed_items].index(cursor_pos_x)
341
+
342
+
343
+
344
+ def move_column(self, direction: int) -> None:
345
+ """
346
+ Cycles the column $direction places.
347
+ E.g., If $direction == -1 and the sort column is 3, then column 3 will swap with column 2
348
+ in each of the rows in $items and 2 will become the new sort column.
349
+
350
+ sort_column = 3, direction = -1
351
+ [[0,1,2,*3*,4],
352
+ [5,6,7,*8*,9]]
353
+ -->
354
+ [[0,1,*3*,2,4],
355
+ [5,6,*8*,7,9]]
356
+
357
+ returns:
358
+ adjusted items, header, sort_column and column_widths
359
+ """
360
+ if len(self.items) < 1: return None
361
+ if (self.sort_column+direction) < 0 or (self.sort_column+direction) >= len(self.items[0]): return None
362
+
363
+ new_index = self.sort_column + direction
364
+
365
+ # Swap columns in each row
366
+ for row in self.items:
367
+ row[self.sort_column], row[new_index] = row[new_index], row[self.sort_column]
368
+ if self.header:
369
+ self.header[self.sort_column], self.header[new_index] = self.header[new_index], self.header[self.sort_column]
370
+
371
+ # Swap column widths
372
+ self.column_widths[self.sort_column], self.column_widths[new_index] = self.column_widths[new_index], self.column_widths[self.sort_column]
373
+
374
+ # Update current column index
375
+ self.sort_column = new_index
376
+
377
+ def draw_screen(self, indexed_items: list[Tuple[int, list[str]]], highlights: list[dict] = [{}], clear: bool = True) -> None:
378
+ """ Draw the list_picker screen. """
379
+
380
+ if clear:
381
+ self.stdscr.erase()
382
+
383
+ ## Terminal too small to display list_picker
384
+ h, w = self.stdscr.getmaxyx()
385
+ if h<3 or w<len("Terminal"): return None
386
+ if (self.show_footer or self.footer_string) and (h<12 or w<35) or (h<12 and w<10):
387
+ self.stdscr.addstr(h//2-1, (w-len("Terminal"))//2, "Terminal")
388
+ self.stdscr.addstr(h//2, (w-len("Too"))//2, "Too")
389
+ self.stdscr.addstr(h//2+1, (w-len("Small"))//2, "Small")
390
+ return None
391
+
392
+ top_space = self.top_gap
393
+ if self.title: top_space+=1
394
+ if self.display_modes: top_space+=1
395
+
396
+ self.items_per_page = h - top_space-int(bool(self.header)) - 3*int(bool(self.show_footer))
397
+ if not self.show_footer and self.footer_string: self.items_per_page-=1
398
+ self.items_per_page = min(h-top_space-2, self.items_per_page)
399
+
400
+
401
+ # Determine which rows are to be displayed on the current screen
402
+ ## Paginate
403
+ if self.paginate:
404
+ start_index = (self.cursor_pos//self.items_per_page) * self.items_per_page
405
+ end_index = min(start_index + self.items_per_page, len(self.indexed_items))
406
+ ## Scroll
407
+ else:
408
+ scrolloff = self.items_per_page//2
409
+ start_index = max(0, min(self.cursor_pos - (self.items_per_page-scrolloff), len(self.indexed_items)-self.items_per_page))
410
+ end_index = min(start_index + self.items_per_page, len(self.indexed_items))
411
+ if len(self.indexed_items) == 0: start_index, end_index = 0, 0
412
+
413
+ # self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns)
414
+ # Determine widths based only on the currently indexed rows
415
+ # rows = [v[1] for v in self.indexed_items] if len(self.indexed_items) else self.items
416
+ # Determine widths based only on the currently displayed indexed rows
417
+ rows = [v[1] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else self.items
418
+ self.column_widths = get_column_widths(rows, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns)
419
+ visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
420
+ visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
421
+ startx = 0 if self.highlight_full_row else 2
422
+ if visible_columns_total_width < w and self.centre_in_terminal:
423
+ startx += (w - visible_columns_total_width) // 2
424
+
425
+ top_space = self.top_gap
426
+
427
+ ## Display title (if applicable)
428
+ if self.title:
429
+ padded_title = f" {self.title.strip()} "
430
+ # self.stdscr.addstr(self.top_gap, 0, f"{' ':^{w}}", curses.color_pair(self.colours_start+16) | curses.A_UNDERLINE)
431
+ self.stdscr.addstr(self.top_gap, 0, f"{' ':^{w}}", curses.color_pair(self.colours_start+16))
432
+ title_x = (w-wcswidth(padded_title))//2
433
+ # title = f"{title:^{w}}"
434
+ self.stdscr.addstr(self.top_gap, title_x, padded_title, curses.color_pair(self.colours_start+16) | curses.A_BOLD)
435
+ top_space += 1
436
+
437
+ ## Display modes
438
+ if self.display_modes and self.modes not in [[{}], []]:
439
+ self.stdscr.addstr(top_space, 0, ' '*w, curses.A_REVERSE)
440
+ modes_list = [f"{mode['name']}" if 'name' in mode else f"{i}. " for i, mode in enumerate(self.modes)]
441
+ # mode_colours = [mode["colour"] for mode ]
442
+ mode_widths = get_mode_widths(modes_list)
443
+ split_space = (w-sum(mode_widths))//len(self.modes)
444
+ xmode = 0
445
+ for i, mode in enumerate(modes_list):
446
+ if i == len(modes_list)-1:
447
+ mode_str = f"{mode:^{mode_widths[i]+split_space+(w-sum(mode_widths))%len(self.modes)}}"
448
+ else:
449
+ mode_str = f"{mode:^{mode_widths[i]+split_space}}"
450
+ # current mode
451
+ if i == self.mode_index:
452
+ self.stdscr.addstr(top_space, xmode, mode_str, curses.color_pair(self.colours_start+14) | curses.A_BOLD)
453
+ # other modes
454
+ else:
455
+ self.stdscr.addstr(top_space, xmode, mode_str, curses.color_pair(self.colours_start+15) | curses.A_UNDERLINE)
456
+ xmode += split_space+mode_widths[i]
457
+ top_space += 1
458
+
459
+ ## Display header
460
+ if self.header:
461
+ header_str = ""
462
+ up_to_selected_col = ""
463
+ for i in range(len(self.header)):
464
+ if i == self.sort_column: up_to_selected_col = header_str
465
+ if i in self.hidden_columns: continue
466
+ number = f"{i}. " if self.number_columns else ""
467
+ number = f"{intStringToExponentString(str(i))}. " if self.number_columns else ""
468
+ header_str += number
469
+ header_str +=f"{self.header[i]:^{self.column_widths[i]}}"
470
+ header_str += " "
471
+
472
+ self.stdscr.addstr(top_space, 0, ' '*w, curses.color_pair(self.colours_start+4) | curses.A_BOLD)
473
+ self.stdscr.addstr(top_space, startx, header_str[:min(w-startx, visible_columns_total_width+1)], curses.color_pair(self.colours_start+4) | curses.A_BOLD)
474
+
475
+ # Highlight sort column
476
+ if self.sort_column != None and self.sort_column not in self.hidden_columns and len(up_to_selected_col) + 1 < w and len(self.header) > 1:
477
+ number = f"{self.sort_column}. " if self.number_columns else ""
478
+ number = f"{intStringToExponentString(self.sort_column)}. " if self.number_columns else ""
479
+ self.stdscr.addstr(top_space, startx + len(up_to_selected_col), (number+f"{self.header[self.sort_column]:^{self.column_widths[self.sort_column]}}")[:w-len(up_to_selected_col)-startx], curses.color_pair(self.colours_start+19) | curses.A_BOLD)
480
+
481
+ if self.centre_in_terminal_vertical and len(self.indexed_items) < self.items_per_page:
482
+ top_space += (self.items_per_page - len(self.indexed_items)) //2
483
+
484
+ ## Display rows and highlights
485
+ for idx in range(start_index, end_index):
486
+ item = self.indexed_items[idx]
487
+ y = idx - start_index + top_space + int(bool(self.header))
488
+
489
+ row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)
490
+ if idx == self.cursor_pos:
491
+ self.stdscr.addstr(y, startx, row_str[:min(w-startx, visible_columns_total_width)], curses.color_pair(self.colours_start+5) | curses.A_BOLD)
492
+ else:
493
+ self.stdscr.addstr(y, startx, row_str[:min(w-startx, visible_columns_total_width)], curses.color_pair(self.colours_start+2))
494
+ # Highlight the whole string of the selected rows
495
+ if self.highlight_full_row:
496
+ if self.selections[item[0]]:
497
+ self.stdscr.addstr(y, startx, row_str[:min(w-startx, visible_columns_total_width)], curses.color_pair(self.colours_start+1))
498
+ # Visually selected
499
+ if self.is_selecting and self.start_selection <= idx <= self.cursor_pos:
500
+ self.stdscr.addstr(y, startx, row_str[:min(w-startx, visible_columns_total_width)], curses.color_pair(self.colours_start+1))
501
+ elif self.is_selecting and self.start_selection >= idx >= self.cursor_pos:
502
+ self.stdscr.addstr(y, startx, row_str[:min(w-startx, visible_columns_total_width)], curses.color_pair(self.colours_start+1))
503
+ # Visually deslected
504
+ if self.is_deselecting and self.start_selection >= idx >= self.cursor_pos:
505
+ self.stdscr.addstr(y, startx, row_str[:min(w-startx, visible_columns_total_width)], curses.color_pair(self.colours_start+1))
506
+ elif self.is_deselecting and self.start_selection <= idx <= self.cursor_pos:
507
+ self.stdscr.addstr(y, startx, row_str[:min(w-startx, visible_columns_total_width)], curses.color_pair(self.colours_start+1))
508
+
509
+ # Highlight the first char of the selected rows
510
+ else:
511
+ if self.selections[item[0]]:
512
+ self.stdscr.addstr(y, max(startx-2,0), ' ', curses.color_pair(self.colours_start+1))
513
+ # Visually selected
514
+ if self.is_selecting and self.start_selection <= idx <= self.cursor_pos:
515
+ self.stdscr.addstr(y, max(startx-2,0), ' ', curses.color_pair(self.colours_start+1))
516
+ elif self.is_selecting and self.start_selection >= idx >= self.cursor_pos:
517
+ self.stdscr.addstr(y, max(startx-2,0), ' ', curses.color_pair(self.colours_start+1))
518
+ # Visually deslected
519
+ if self.is_deselecting and self.start_selection >= idx >= self.cursor_pos:
520
+ self.stdscr.addstr(y, max(startx-2,0), ' ', curses.color_pair(self.colours_start+10))
521
+ elif self.is_deselecting and self.start_selection <= idx <= self.cursor_pos:
522
+ self.stdscr.addstr(y, max(startx-2,0), ' ', curses.color_pair(self.colours_start+10))
523
+
524
+ # if not highlights_hide:
525
+ if not self.highlights_hide and idx != self.cursor_pos:
526
+ for highlight in self.highlights:
527
+ try:
528
+ if highlight["field"] == "all":
529
+ match = re.search(highlight["match"], row_str, re.IGNORECASE)
530
+ if not match: continue
531
+ highlight_start = match.start()
532
+ highlight_end = match.end()
533
+ elif type(highlight["field"]) == type(4) and highlight["field"] not in self.hidden_columns:
534
+ match = re.search(highlight["match"], truncate_to_display_width(item[1][highlight["field"]], self.column_widths[highlight["field"]], self.centre_in_cols), re.IGNORECASE)
535
+ if not match: continue
536
+ field_start = sum([width for i, width in enumerate(self.column_widths[:highlight["field"]]) if i not in self.hidden_columns]) + sum([1 for i in range(highlight["field"]) if i not in self.hidden_columns])*wcswidth(self.separator)
537
+ highlight_start = field_start + match.start()
538
+ highlight_end = match.end() + field_start
539
+ else:
540
+ continue
541
+ color_pair = curses.color_pair(self.colours_start+highlight["color"]) # Selected item
542
+ if idx == self.cursor_pos:
543
+ color_pair = curses.color_pair(self.colours_start+highlight["color"]) | curses.A_REVERSE
544
+ self.stdscr.addstr(y, startx+highlight_start, row_str[highlight_start:min(w-startx, highlight_end)], curses.color_pair(self.colours_start+highlight["color"]) | curses.A_BOLD)
545
+ except:
546
+ pass
547
+
548
+ ## Display scrollbar
549
+ if self.scroll_bar and len(self.indexed_items) and len(self.indexed_items) > (self.items_per_page):
550
+ scroll_bar_length = int(self.items_per_page*self.items_per_page/len(self.indexed_items))
551
+ if self.cursor_pos <= self.items_per_page//2:
552
+ scroll_bar_start=top_space+int(bool(self.header))
553
+ elif self.cursor_pos + self.items_per_page//2 >= len(self.indexed_items):
554
+ scroll_bar_start = h - int(bool(self.show_footer))*3 - scroll_bar_length
555
+ else:
556
+ scroll_bar_start = int(((self.cursor_pos)/len(self.indexed_items))*self.items_per_page)+top_space+int(bool(self.header)) - scroll_bar_length//2
557
+ scroll_bar_length = min(scroll_bar_length, h - scroll_bar_start-1)
558
+ for i in range(scroll_bar_length):
559
+ v = max(top_space+int(bool(self.header)), scroll_bar_start-scroll_bar_length//2)
560
+ self.stdscr.addstr(scroll_bar_start+i, w-1, ' ', curses.color_pair(self.colours_start+18))
561
+
562
+ # Display refresh symbol
563
+ if self.auto_refresh:
564
+ self.stdscr.addstr(0,w-3,"  ", curses.color_pair(self.colours_start+23) | curses.A_BOLD)
565
+
566
+ ## Display footer
567
+ if self.show_footer:
568
+ # Fill background
569
+ self.stdscr.addstr(h-3, 0, ' '*(w-1), curses.color_pair(self.colours_start+20))
570
+ self.stdscr.addstr(h-2, 0, ' '*(w-1), curses.color_pair(self.colours_start+20))
571
+ self.stdscr.addstr(h-1, 0, ' '*(w-1), curses.color_pair(self.colours_start+20)) # Problem with curses that you can't write to the last char
572
+
573
+ if self.filter_query:
574
+ self.stdscr.addstr(h - 2, 2, f" Filter: {self.filter_query} "[:w-40], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
575
+ if self.search_query:
576
+ self.stdscr.addstr(h - 3, 2, f" Search: {self.search_query} [{self.search_index}/{self.search_count}] "[:w-3], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
577
+ if self.user_opts:
578
+ self.stdscr.addstr(h-1, 2, f" Opts: {self.user_opts} "[:w-3], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
579
+ # Display sort information
580
+ sort_column_info = f"{self.sort_column if self.sort_column is not None else 'None'}"
581
+ sort_method_info = f"{self.SORT_METHODS[self.columns_sort_method[self.sort_column]]}" if self.sort_column != None else "NA"
582
+
583
+
584
+ ## RIGHT
585
+ # Sort status
586
+ sort_order_info = "Desc." if self.sort_reverse[self.sort_column] else "Asc."
587
+ sort_disp_str = f" Sort: ({sort_column_info}, {sort_method_info}, {sort_order_info}) "
588
+ self.stdscr.addstr(h - 2, w-35, f"{sort_disp_str:>34}", curses.color_pair(self.colours_start+20))
589
+
590
+
591
+ if self.footer_string:
592
+ # footer_string_width = min(w, max(len(self.footer_string), w//3, 39))
593
+ footer_string_width = min(w-1, max(len(self.footer_string), 50))
594
+ disp_string = f"{self.footer_string[:footer_string_width]:>{footer_string_width-1}} "
595
+ self.stdscr.addstr(h - 1, w-footer_string_width-1, " "*footer_string_width, curses.color_pair(self.colours_start+24))
596
+ self.stdscr.addstr(h - 1, w-footer_string_width-1, f"{disp_string}", curses.color_pair(self.colours_start+24))
597
+ else:
598
+ # Display cursor mode
599
+ select_mode = "Cursor"
600
+ if self.is_selecting: select_mode = "Visual Selection"
601
+ elif self.is_deselecting: select_mode = "Visual deselection"
602
+ self.stdscr.addstr(h - 1, w-35, f"{select_mode:>33} ", curses.color_pair(self.colours_start+20))
603
+ # Display selection count
604
+ selected_count = sum(self.selections.values())
605
+ if self.paginate:
606
+ cursor_disp_str = f" {self.cursor_pos+1}/{len(self.indexed_items)} Page {self.cursor_pos//self.items_per_page + 1}/{(len(self.indexed_items) + self.items_per_page - 1) // self.items_per_page} Selected {selected_count}"
607
+ self.stdscr.addstr(h - 3, w-35, f"{cursor_disp_str:>33} ", curses.color_pair(self.colours_start+20))
608
+ else:
609
+ cursor_disp_str = f" {self.cursor_pos+1}/{len(self.indexed_items)} | Selected {selected_count}"
610
+ self.stdscr.addstr(h - 3, w-35, f"{cursor_disp_str:>33} ", curses.color_pair(self.colours_start+20))
611
+
612
+ self.stdscr.refresh()
613
+ elif self.footer_string:
614
+ footer_string_width = min(w-1, len(self.footer_string)+2)
615
+ disp_string = f" {self.footer_string[:footer_string_width]:>{footer_string_width-2}} "
616
+ self.stdscr.addstr(h - 1, w-footer_string_width-1, " "*footer_string_width, curses.color_pair(self.colours_start+24))
617
+ self.stdscr.addstr(h - 1, w-footer_string_width-1, f"{disp_string}", curses.color_pair(self.colours_start+24))
618
+
619
+ ## Display infobox
620
+ if self.display_infobox:
621
+ self.infobox(self.stdscr, message=self.infobox_items, title=self.infobox_title)
622
+ # self.stdscr.timeout(2000) # timeout is set to 50 in order to get the infobox to be displayed so here we reset it to 2000
623
+
624
+
625
+
626
+ def infobox(self, stdscr: curses.window, message: str ="", title: str ="Infobox", colours_end: int = 0, duration: int = 4) -> curses.window:
627
+ """ Display non-interactive infobox window. """
628
+ h, w = stdscr.getmaxyx()
629
+ notification_width, notification_height = w//2, 3*h//5
630
+ message_width = notification_width-5
631
+
632
+ if not message: message = "!!"
633
+ if isinstance(message, str):
634
+ submenu_items = [" "+message[i*message_width:(i+1)*message_width] for i in range(len(message)//message_width+1)]
635
+ else:
636
+ submenu_items = message
637
+
638
+ notification_remap_keys = {
639
+ curses.KEY_RESIZE: curses.KEY_F5,
640
+ 27: ord('q')
641
+ }
642
+ if len(submenu_items) > notification_height - 2:
643
+ submenu_items = submenu_items[:notification_height-3] + [f"{'....':^{notification_width}}"]
644
+ while True:
645
+ h, w = stdscr.getmaxyx()
646
+
647
+ submenu_win = curses.newwin(notification_height, notification_width, 3, w - (notification_width+4))
648
+ infobox_data = {
649
+ "items": submenu_items,
650
+ "colours": notification_colours,
651
+ "colours_start": 50,
652
+ "disabled_keys": [ord('z'), ord('c')],
653
+ "show_footer": False,
654
+ "top_gap": 0,
655
+ "key_remappings": notification_remap_keys,
656
+ "display_only": True,
657
+ "hidden_columns": [],
658
+ "title": title,
659
+ }
660
+
661
+ OptionPicker = Picker(submenu_win, **infobox_data)
662
+ s, o, f = OptionPicker.run()
663
+ if o != "refresh": break
664
+
665
+ return submenu_win
666
+
667
+
668
+
669
+ def get_function_data(self) -> dict:
670
+ """ Returns a dict of the main variables needed to restore the state of list_pikcer. """
671
+ function_data = {
672
+ "selections": self.selections,
673
+ "items_per_page": self.items_per_page,
674
+ "current_row": self.current_row,
675
+ "current_page": self.current_page,
676
+ "cursor_pos": self.cursor_pos,
677
+ "colours": self.colours,
678
+ "colour_theme_number": self.colour_theme_number,
679
+ "sort_column": self.sort_column,
680
+ "sort_method": self.sort_method,
681
+ "sort_reverse": self.sort_reverse,
682
+ "hidden_columns": self.hidden_columns,
683
+ "is_selecting": self.is_selecting,
684
+ "is_deselecting": self.is_deselecting,
685
+ "user_opts": self.user_opts,
686
+ "user_settings": self.user_settings,
687
+ "separator": self.separator,
688
+ "search_query": self.search_query,
689
+ "search_count": self.search_count,
690
+ "search_index": self.search_index,
691
+ "filter_query": self.filter_query,
692
+ "indexed_items": self.indexed_items,
693
+ "start_selection": self.start_selection,
694
+ "end_selection": self.end_selection,
695
+ "highlights": self.highlights,
696
+ "max_column_width": self.max_column_width,
697
+ "column_indices": self.column_indices,
698
+ "mode_index": self.mode_index,
699
+ "modes": self.modes,
700
+ "title": self.title,
701
+ "display_modes": self.display_modes,
702
+ "require_option": self.require_option,
703
+ "option_functions": self.option_functions,
704
+ "top_gap": self.top_gap,
705
+ "number_columns": self.number_columns,
706
+ "items": self.items,
707
+ "indexed_items": self.indexed_items,
708
+ "header": self.header,
709
+ "scroll_bar": self.scroll_bar,
710
+ "columns_sort_method": self.columns_sort_method,
711
+ "disabled_keys": self.disabled_keys,
712
+ "show_footer": self.show_footer,
713
+ "footer_string": self.footer_string,
714
+ "footer_string_auto_refresh": self.footer_string_auto_refresh,
715
+ "footer_string_refresh_function": self.footer_string_refresh_function,
716
+ "footer_timer": self.footer_timer,
717
+ "colours_start": self.colours_start,
718
+ "colours_end": self.colours_end,
719
+ "display_only": self.display_only,
720
+ "infobox_items": self.infobox_items,
721
+ "display_infobox": self.display_infobox,
722
+ "infobox_title": self.infobox_title,
723
+ "key_remappings": self.key_remappings,
724
+ "auto_refresh": self.auto_refresh,
725
+ "get_new_data": self.get_new_data,
726
+ "refresh_function": self.refresh_function,
727
+ "get_data_startup": self.get_data_startup,
728
+ "editable_columns": self.editable_columns,
729
+ "last_key": self.last_key,
730
+ "centre_in_terminal": self.centre_in_terminal,
731
+ "centre_in_terminal_vertical": self.centre_in_terminal_vertical,
732
+ "centre_in_cols": self.centre_in_cols,
733
+ "highlight_full_row": self.highlight_full_row,
734
+ "column_widths": self.column_widths,
735
+ "track_entries_upon_refresh": self.track_entries_upon_refresh,
736
+ "id_column": self.id_column,
737
+ "startup_notification": self.startup_notification,
738
+ "keys_dict": self.keys_dict,
739
+ "cancel_is_back": self.cancel_is_back,
740
+ "paginate": self.paginate,
741
+
742
+ }
743
+ return function_data
744
+
745
+ def set_function_data(self, function_data: dict) -> None:
746
+ """ Set variables from state dict containing core variables."""
747
+
748
+ if "items" in function_data: self.items = function_data["items"]
749
+ if "header" in function_data: self.header = function_data["header"]
750
+ self.indexed_items = function_data["indexed_items"] if "indexed_items" in function_data else []
751
+
752
+
753
+
754
+ def delete_entries(self) -> None:
755
+ """ Delete entries from view. """
756
+ # Remove selected items from the list
757
+ selected_indices = [index for index, selected in self.selections.items() if selected]
758
+ if not selected_indices:
759
+ # Remove the currently focused item if nothing is selected
760
+ selected_indices = [self.indexed_items[self.cursor_pos][0]]
761
+
762
+ self.items = [item for i, item in enumerate(self.items) if i not in selected_indices]
763
+ self.indexed_items = [(i, item) for i, item in enumerate(self.items)]
764
+ self.selections = {i:False for i in range(len(self.indexed_items))}
765
+ self.draw_screen(self.indexed_items, self.highlights)
766
+
767
+
768
+ def choose_option(
769
+ self,
770
+ stdscr: curses.window,
771
+ options: list[list[str]] =[],
772
+ field_name: str = "Input",
773
+ x:int=0,
774
+ y:int=0,
775
+ literal:bool=False,
776
+ colours_start:int=0,
777
+ header: list[str] = [],
778
+ require_option:list = [],
779
+ option_functions: list = [],
780
+ ) -> Tuple[dict, str, dict]:
781
+ """
782
+ Display input field at x,y
783
+
784
+ ---Arguments
785
+ stdscr: curses screen
786
+ usrtxt (str): text to be edited by the user
787
+ field_name (str): The text to be displayed at the start of the text input
788
+ x (int): prompt begins at (x,y) in the screen given
789
+ y (int): prompt begins at (x,y) in the screen given
790
+ colours_start (bool): start index of curses init_pair.
791
+
792
+ ---Returns
793
+ usrtxt, return_code
794
+ usrtxt: the text inputted by the user
795
+ return_code:
796
+ 0: user hit escape
797
+ 1: user hit return
798
+ """
799
+ if options == []: options = [[f"{i}"] for i in range(10)]
800
+ cursor = 0
801
+
802
+
803
+ option_picker_data = {
804
+ "items": options,
805
+ "colours": notification_colours,
806
+ "colours_start": 50,
807
+ "title":field_name,
808
+ "header":header,
809
+ "hidden_columns":[],
810
+ "require_option":require_option,
811
+ "keys_dict": options_keys,
812
+ "show_footer": False,
813
+ "cancel_is_back": True,
814
+ }
815
+ while True:
816
+ h, w = stdscr.getmaxyx()
817
+
818
+ choose_opts_widths = get_column_widths(options)
819
+ window_width = min(max(sum(choose_opts_widths) + 6, 50) + 6, w)
820
+ window_height = min(h//2, max(6, len(options)+2))
821
+
822
+ submenu_win = curses.newwin(window_height, window_width, (h-window_height)//2, (w-window_width)//2)
823
+ submenu_win.keypad(True)
824
+ OptionPicker = Picker(submenu_win, **option_picker_data)
825
+ s, o, f = OptionPicker.run()
826
+
827
+ if o == "refresh":
828
+ self.draw_screen(self.indexed_items, self.highlights)
829
+ continue
830
+ if s:
831
+ return {x: options[x] for x in s}, o, f
832
+ return {}, "", f
833
+
834
+
835
+
836
+ def notification(self, stdscr: curses.window, message: str="", title:str="Notification", colours_end: int=0, duration:int=4) -> None:
837
+ """ Notification box. """
838
+ notification_width, notification_height = 50, 7
839
+ message_width = notification_width-5
840
+
841
+ if not message: message = "!!"
842
+ submenu_items = [" "+message[i*message_width:(i+1)*message_width] for i in range(len(message)//message_width+1)]
843
+
844
+ notification_remap_keys = {
845
+ curses.KEY_RESIZE: curses.KEY_F5,
846
+ 27: ord('q')
847
+ }
848
+ while True:
849
+ h, w = stdscr.getmaxyx()
850
+
851
+ submenu_win = curses.newwin(notification_height, notification_width, 3, w - (notification_width+4))
852
+ notification_data = {
853
+ "items": submenu_items,
854
+ "title": title,
855
+ "colours_start": 50,
856
+ "show_footer": False,
857
+ "centre_in_terminal": True,
858
+ "centre_in_terminal_vertical": True,
859
+ "centre_in_cols": True,
860
+ "hidden_columns": [],
861
+ "keys_dict": notification_keys,
862
+ "disabled_keys": [ord('z'), ord('c')],
863
+ "highlight_full_row": True,
864
+ "top_gap": 0,
865
+ "cancel_is_back": True,
866
+
867
+ }
868
+ OptionPicker = Picker(submenu_win, **notification_data)
869
+ s, o, f = OptionPicker.run()
870
+
871
+ if o != "refresh": break
872
+ submenu_win.clear()
873
+ submenu_win.refresh()
874
+ del submenu_win
875
+ stdscr.clear()
876
+ stdscr.refresh()
877
+ self.draw_screen(self.indexed_items, self.highlights)
878
+ # set_colours(colours=get_colours(0))
879
+
880
+ def toggle_column_visibility(self, col_index:int) -> None:
881
+ """ Toggle the visibility of the column at col_index. """
882
+ if 0 <= col_index < len(self.items[0]):
883
+ if col_index in self.hidden_columns:
884
+ self.hidden_columns.remove(col_index)
885
+ else:
886
+ self.hidden_columns.append(col_index)
887
+
888
+ def apply_settings(self) -> None:
889
+ """ The users settings will be stored in the user_settings variable. This function applies those settings. """
890
+
891
+ # settings= usrtxt.split(' ')
892
+ # split settings and appy them
893
+ """
894
+ ![0-9]+ show/hide column
895
+ s[0-9]+ set column focus for sort
896
+ g[0-9]+ go to index
897
+ p[0-9]+ go to page
898
+ nohl hide search highlights
899
+ """
900
+ if self.user_settings:
901
+ settings = re.split(r'\s+', self.user_settings)
902
+ for setting in settings:
903
+ if len(setting) == 0: continue
904
+
905
+ if setting[0] == "!" and len(setting) > 1:
906
+ if setting[1:].isnumeric():
907
+ cols = setting[1:].split(",")
908
+ for col in cols:
909
+ self.toggle_column_visibility(int(col))
910
+ elif setting[1] == "r":
911
+ self.auto_refresh = not self.auto_refresh
912
+ elif setting[1] == "h":
913
+ self.highlights_hide = not self.highlights_hide
914
+
915
+ elif setting in ["nhl", "nohl", "nohighlights"]:
916
+ # highlights = [highlight for highlight in highlights if "type" not in highlight or highlight["type"] != "search" ]
917
+
918
+ self.highlights_hide = not self.highlights_hide
919
+ elif setting[0] == "s":
920
+ if 0 <= int(setting[1:]) < len(self.items[0]):
921
+ self.sort_column = int(setting[1:])
922
+ if len(self.indexed_items):
923
+ current_pos = self.indexed_items[self.cursor_pos][0]
924
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort items based on new column
925
+ if len(self.indexed_items):
926
+ new_pos = [row[0] for row in self.indexed_items].index(self.current_pos)
927
+ self.cursor_pos = new_pos
928
+ elif setting == "ct":
929
+ self.centre_in_terminal = not self.centre_in_terminal
930
+ elif setting == "cc":
931
+ self.centre_in_cols = not self.centre_in_cols
932
+ elif setting == "cv":
933
+ self.centre_in_terminal_vertical = not self.centre_in_terminal_vertical
934
+ elif setting[0] == "":
935
+ cols = setting[1:].split(",")
936
+ elif setting == "footer":
937
+ self.show_footer = not self.show_footer
938
+ self.initialise_variables()
939
+
940
+ elif setting.startswith("cwd="):
941
+ os.chdir(os.path.expandvars(os.path.expanduser(setting[len("cwd="):])))
942
+ elif setting == "th":
943
+ global COLOURS_SET
944
+ COLOURS_SET = False
945
+ self.colour_theme_number = (self.colour_theme_number + 1)%get_theme_count()
946
+ # self.colour_theme_number = int(not bool(self.colour_theme_number))
947
+ set_colours(self.colour_theme_number)
948
+ self.draw_screen(self.indexed_items, self.highlights)
949
+ self.notification(self.stdscr, message=f"Theme {self.colour_theme_number} applied.")
950
+
951
+
952
+ self.user_settings = ""
953
+
954
+ def toggle_item(self, index: int) -> None:
955
+ """ Toggle selection of item at index. """
956
+ self.selections[index] = not self.selections[index]
957
+ self.draw_screen(self.indexed_items, self.highlights)
958
+
959
+ def select_all(self) -> None:
960
+ """ Select all in indexed_items. """
961
+ for i in range(len(self.indexed_items)):
962
+ self.selections[self.indexed_items[i][0]] = True
963
+ self.draw_screen(self.indexed_items, self.highlights)
964
+
965
+ def deselect_all(self) -> None:
966
+ """ Deselect all items in indexed_items. """
967
+ for i in range(len(self.selections)):
968
+ self.selections[i] = False
969
+ self.draw_screen(self.indexed_items, self.highlights)
970
+
971
+ def handle_visual_selection(self, selecting:bool = True) -> None:
972
+ """ Toggle visual selection or deselection. """
973
+ if not self.is_selecting and not self.is_deselecting and len(self.indexed_items) and len(self.indexed_items[0][1]):
974
+ self.start_selection = self.cursor_pos
975
+ if selecting:
976
+ self.is_selecting = True
977
+ else:
978
+ self.is_deselecting = True
979
+ elif self.is_selecting:
980
+ # end_selection = indexed_items[current_page * items_per_page + current_row][0]
981
+ self.end_selection = self.cursor_pos
982
+ if self.start_selection != -1:
983
+ start = max(min(self.start_selection, self.end_selection), 0)
984
+ end = min(max(self.start_selection, self.end_selection), len(self.indexed_items)-1)
985
+ for i in range(start, end + 1):
986
+ if self.indexed_items[i][0] not in self.unselectable_indices:
987
+ self.selections[self.indexed_items[i][0]] = True
988
+ self.start_selection = -1
989
+ self.end_selection = -1
990
+ self.is_selecting = False
991
+
992
+ self.draw_screen(self.indexed_items, self.highlights)
993
+
994
+ elif self.is_deselecting:
995
+ self.end_selection = self.indexed_items[self.cursor_pos][0]
996
+ self.end_selection = self.cursor_pos
997
+ if self.start_selection != -1:
998
+ start = max(min(self.start_selection, self.end_selection), 0)
999
+ end = min(max(self.start_selection, self.end_selection), len(self.indexed_items)-1)
1000
+ for i in range(start, end + 1):
1001
+ # selections[i] = False
1002
+ self.selections[self.indexed_items[i][0]] = False
1003
+ self.start_selection = -1
1004
+ self.end_selection = -1
1005
+ self.is_deselecting = False
1006
+ self.draw_screen(self.indexed_items, self.highlights)
1007
+
1008
+ def cursor_down(self) -> bool:
1009
+ """ Move cursor down. """
1010
+ # Returns: whether page is turned
1011
+ new_pos = self.cursor_pos + 1
1012
+ while True:
1013
+ if new_pos >= len(self.indexed_items): return False
1014
+ if self.indexed_items[new_pos][0] in self.unselectable_indices: new_pos+=1
1015
+ else: break
1016
+ self.cursor_pos = new_pos
1017
+ return True
1018
+
1019
+ def cursor_up(self) -> bool:
1020
+ """ Move cursor up. """
1021
+ # Returns: whether page is turned
1022
+ new_pos = self.cursor_pos - 1
1023
+ while True:
1024
+ if new_pos < 0: return False
1025
+ elif new_pos in self.unselectable_indices: new_pos -= 1
1026
+ else: break
1027
+ self.cursor_pos = new_pos
1028
+ return True
1029
+
1030
+ def remapped_key(self, key: int, val: int, key_remappings: dict) -> bool:
1031
+ """ Check if key has been remapped to val in key_remappings. """
1032
+ if key in key_remappings:
1033
+ if key_remappings[key] == val or (isinstance(key_remappings[key], list) and val in key_remappings[key]):
1034
+ return True
1035
+ return False
1036
+
1037
+ def check_key(self, function: str, key: int, keys_dict: dict) -> bool:
1038
+ """
1039
+ Check if $key is assigned to $function in the keys_dict.
1040
+ Allows us to redefine functions to different keys in the keys_dict.
1041
+
1042
+ E.g., keys_dict = { $key, "help": ord('?') },
1043
+ """
1044
+ if function in keys_dict and key in keys_dict[function]:
1045
+ return True
1046
+ return False
1047
+
1048
+ def copy_dialog(self) -> None:
1049
+ copy_header = [
1050
+ "Representation",
1051
+ "Columns",
1052
+ ]
1053
+ options = [
1054
+ ["Python list of lists", "Exclude hidden"],
1055
+ ["Python list of lists", "Include hidden"],
1056
+ ["Tab-separated values", "Exclude hidden"],
1057
+ ["Tab-separated values", "Include hidden"],
1058
+ ["Comma-separated values", "Exclude hidden"],
1059
+ ["Comma-separated values", "Include hidden"],
1060
+ ["Custom separator", "Exclude hidden"],
1061
+ ["Custom separator", "Include hidden"],
1062
+ ]
1063
+ require_option = [False, False, False, False, False, False, True, True]
1064
+ s, o, f = self.choose_option(self.stdscr, options=options, field_name="Copy selected", header=copy_header, require_option=require_option)
1065
+
1066
+
1067
+ funcs = [
1068
+ lambda items, indexed_items, selections, hidden_columns: copy_to_clipboard(items, indexed_items, selections, hidden_columns, representation="python", copy_hidden_cols=False),
1069
+ lambda items, indexed_items, selections, hidden_columns: copy_to_clipboard(items, indexed_items, selections, hidden_columns, representation="python", copy_hidden_cols=True),
1070
+ lambda items, indexed_items, selections, hidden_columns: copy_to_clipboard(items, indexed_items, selections, hidden_columns, representation="tsv", copy_hidden_cols=False),
1071
+ lambda items, indexed_items, selections, hidden_columns: copy_to_clipboard(items, indexed_items, selections, hidden_columns, representation="tsv", copy_hidden_cols=True),
1072
+ lambda items, indexed_items, selections, hidden_columns: copy_to_clipboard(items, indexed_items, selections, hidden_columns, representation="csv", copy_hidden_cols=False),
1073
+ lambda items, indexed_items, selections, hidden_columns: copy_to_clipboard(items, indexed_items, selections, hidden_columns, representation="csv", copy_hidden_cols=True),
1074
+ lambda items, indexed_items, selections, hidden_columns: copy_to_clipboard(items, indexed_items, selections, hidden_columns, representation="custom_sv", copy_hidden_cols=False, separator=o),
1075
+ lambda items, indexed_items, selections, hidden_columns: copy_to_clipboard(items, indexed_items, selections, hidden_columns, representation="custom_sv", copy_hidden_cols=True, separator=o),
1076
+ ]
1077
+
1078
+ # Copy items based on selection
1079
+ if s:
1080
+ for idx in s.keys():
1081
+ funcs[idx](self.items, self.indexed_items, self.selections, self.hidden_columns)
1082
+
1083
+ def save_dialog(self) -> None:
1084
+
1085
+ dump_header = []
1086
+ options = [
1087
+ ["Save data (pickle)."],
1088
+ ["Save data (csv)."],
1089
+ ["Save data (tsv)."],
1090
+ ["Save data (json)."],
1091
+ ["Save data (feather)."],
1092
+ ["Save data (parquet)."],
1093
+ ["Save data (msgpack)."],
1094
+ ["Save state"]
1095
+ ]
1096
+ # require_option = [True, True, True, True, True, True, True, True]
1097
+ s, o, f = self.choose_option(self.stdscr, options=options, field_name="Save...", header=dump_header)
1098
+
1099
+
1100
+ funcs = [
1101
+ lambda opts: dump_data(self.get_function_data(), opts),
1102
+ lambda opts: dump_data(self.get_function_data(), opts, format="csv"),
1103
+ lambda opts: dump_data(self.get_function_data(), opts, format="tsv"),
1104
+ lambda opts: dump_data(self.get_function_data(), opts, format="json"),
1105
+ lambda opts: dump_data(self.get_function_data(), opts, format="feather"),
1106
+ lambda opts: dump_data(self.get_function_data(), opts, format="parquet"),
1107
+ lambda opts: dump_data(self.get_function_data(), opts, format="msgpack"),
1108
+ lambda opts: dump_state(self.get_function_data(), opts),
1109
+ ]
1110
+
1111
+ if s:
1112
+ for idx in s.keys():
1113
+ save_path_entered, save_path = output_file_option_selector(
1114
+ self.stdscr,
1115
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
1116
+ )
1117
+ if save_path_entered:
1118
+ return_val = funcs[idx](save_path)
1119
+ if return_val:
1120
+ self.notification(self.stdscr, message=return_val, title="Error")
1121
+
1122
+ def load_dialog(self) -> None:
1123
+
1124
+ dump_header = []
1125
+ options = [
1126
+ ["Load data (pickle)."],
1127
+ ]
1128
+ s, o, f = self.choose_option(self.stdscr, options=options, field_name="Open file...", header=dump_header)
1129
+
1130
+
1131
+ funcs = [
1132
+ lambda opts: load_state(opts)
1133
+ ]
1134
+
1135
+ if s:
1136
+ file_to_load = file_picker()
1137
+ if file_to_load:
1138
+ index = list(s.keys())[0]
1139
+ return_val = funcs[index](file_to_load)
1140
+ self.set_function_data(return_val)
1141
+
1142
+ # items = return_val["items"]
1143
+ # header = return_val["header"]
1144
+ self.initialise_variables()
1145
+ self.draw_screen(self.indexed_items, self.highlights)
1146
+ self.notification(self.stdscr, f"{repr(file_to_load)} has been loaded!")
1147
+ # if return_val:
1148
+ # notification(stdscr, message=return_val, title="Error")
1149
+
1150
+ def set_registers(self):
1151
+ self.registers = {"*": self.indexed_items[self.cursor_pos][1][self.sort_column]} if len(self.indexed_items) and len(self.indexed_items[0][1]) else {}
1152
+
1153
+
1154
+ def run(self) -> Tuple[list[int], str, dict]:
1155
+ initial_time = time.time()-self.timer
1156
+ initial_time_footer = time.time()-self.footer_timer
1157
+
1158
+ self.initialise_variables(get_data=self.get_data_startup)
1159
+
1160
+ self.draw_screen(self.indexed_items, self.highlights)
1161
+
1162
+ if self.startup_notification:
1163
+ self.notification(self.stdscr, message=self.startup_notification)
1164
+ self.startup_notification = ""
1165
+
1166
+ curses.curs_set(0)
1167
+ # stdscr.nodelay(1) # Non-blocking input
1168
+ # stdscr.timeout(2000) # Set a timeout for getch() to ensure it does not block indefinitely
1169
+ self.stdscr.timeout(max(min(2000, int(self.timer*1000)), 20)) # Set a timeout for getch() to ensure it does not block indefinitely
1170
+ if self.clear_on_start:
1171
+ self.stdscr.clear()
1172
+ self.clear_on_start = False
1173
+ else:
1174
+ self.stdscr.erase()
1175
+ self.stdscr.refresh()
1176
+
1177
+
1178
+
1179
+ # Initialize colours
1180
+ # Check if terminal supports color
1181
+ if curses.has_colors() and self.colours != None:
1182
+ # raise Exception("Terminal does not support color")
1183
+ curses.start_color()
1184
+ colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
1185
+
1186
+ # Set terminal background color
1187
+ self.stdscr.bkgd(' ', curses.color_pair(self.colours_start+3)) # Apply background color
1188
+ self.draw_screen(self.indexed_items, self.highlights)
1189
+
1190
+ if self.display_only:
1191
+ self.stdscr.refresh()
1192
+ function_data = self.get_function_data()
1193
+ return [], "", function_data
1194
+
1195
+ # Main loop
1196
+ data_refreshed = False
1197
+
1198
+ while True:
1199
+ key = self.stdscr.getch()
1200
+ if key in self.disabled_keys: continue
1201
+ clear_screen=True
1202
+
1203
+ if self.check_key("refresh", key, self.keys_dict) or self.remapped_key(key, curses.KEY_F5, self.key_remappings) or (self.auto_refresh and (time.time() - initial_time) > self.timer):
1204
+ h, w = self.stdscr.getmaxyx()
1205
+ self.stdscr.addstr(0,w-3,"  ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
1206
+ self.stdscr.refresh()
1207
+ if self.get_new_data and self.refresh_function:
1208
+ self.initialise_variables(get_data=True)
1209
+
1210
+ initial_time = time.time()
1211
+ self.draw_screen(self.indexed_items, self.highlights, clear=False)
1212
+ else:
1213
+
1214
+ function_data = self.get_function_data()
1215
+ return [], "refresh", function_data
1216
+
1217
+ if self.footer_string_auto_refresh and ((time.time() - initial_time_footer) > self.footer_timer):
1218
+ self.footer_string = self.footer_string_refresh_function()
1219
+ initial_time_footer = time.time()
1220
+ self.draw_screen(self.indexed_items, self.highlights)
1221
+
1222
+ if self.check_key("help", key, self.keys_dict):
1223
+ self.stdscr.clear()
1224
+ self.stdscr.refresh()
1225
+ help_data = {
1226
+ "items": help_lines,
1227
+ "title": f"{self.title} Help",
1228
+ "colours_start": 150,
1229
+ "colours": help_colours,
1230
+ "show_footer": True,
1231
+ "max_selected": 1,
1232
+ "keys_dict": help_keys,
1233
+ "disabled_keys": [ord('?'), ord('v'), ord('V'), ord('m'), ord('M'), ord('l'), curses.KEY_ENTER, ord('\n')],
1234
+ "highlight_full_row": True,
1235
+ "top_gap": 0,
1236
+ "paginate": self.paginate,
1237
+ "centre_in_terminal": True,
1238
+ "centre_in_terminal_vertical": True,
1239
+ "hidden_columns": [],
1240
+
1241
+ }
1242
+ OptionPicker = Picker(self.stdscr, **help_data)
1243
+ s, o, f = OptionPicker.run()
1244
+
1245
+ elif self.check_key("exit", key, self.keys_dict):
1246
+ self.stdscr.clear()
1247
+ function_data = self.get_function_data()
1248
+ function_data["last_key"] = key
1249
+ return [], "", function_data
1250
+ elif self.check_key("full_exit", key, self.keys_dict):
1251
+ close_curses(self.stdscr)
1252
+ exit()
1253
+
1254
+ elif self.check_key("settings_input", key, self.keys_dict):
1255
+ usrtxt = f"{self.user_settings.strip()} " if self.user_settings else ""
1256
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
1257
+ if self.show_footer: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
1258
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
1259
+ self.set_registers()
1260
+ usrtxt, return_val = input_field(
1261
+ self.stdscr,
1262
+ usrtxt=usrtxt,
1263
+ field_name="Settings",
1264
+ x=lambda:2,
1265
+ y=lambda: self.stdscr.getmaxyx()[0]-1,
1266
+ max_length=field_end_f,
1267
+ registers=self.registers,
1268
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
1269
+ )
1270
+ if return_val:
1271
+ self.user_settings = usrtxt
1272
+ self.apply_settings()
1273
+ self.user_settings = ""
1274
+ elif self.check_key("toggle_footer", key, self.keys_dict):
1275
+ self.user_settings = "footer"
1276
+ self.apply_settings()
1277
+
1278
+ elif self.check_key("settings_options", key, self.keys_dict):
1279
+ options = []
1280
+ if len(self.items) > 0:
1281
+ options += [["cv", "Centre rows vertically"]]
1282
+ options += [["ct", "Centre column-set in terminal"]]
1283
+ options += [["cc", "Centre values in cells"]]
1284
+ options += [["!r", "Toggle auto-refresh"]]
1285
+ options += [["th", "Cycle between themes"]]
1286
+ options += [["nohl", "Toggle highlights"]]
1287
+ options += [["footer", "Toggle footer"]]
1288
+ options += [[f"s{i}", f"Select col. {i}"] for i in range(len(self.items[0]))]
1289
+ options += [[f"!{i}", f"Toggle col. {i}"] for i in range(len(self.items[0]))]
1290
+
1291
+ settings_options_header = ["Key", "Setting"]
1292
+
1293
+ s, o, f = self.choose_option(self.stdscr, options=options, field_name="Settings", header=settings_options_header)
1294
+ if s:
1295
+ self.user_settings = " ".join([x[0] for x in s.values()])
1296
+ self.apply_settings()
1297
+
1298
+ # elif self.check_key("move_column_left", key, self.keys_dict):
1299
+ # tmp1 = self.column_indices[self.sort_column]
1300
+ # tmp2 = self.column_indices[(self.sort_column-1)%len(self.column_indices)]
1301
+ # self.column_indices[self.sort_column] = tmp2
1302
+ # self.column_indices[(self.sort_column-1)%(len(self.column_indices))] = tmp1
1303
+ # self.sort_column = (self.sort_column-1)%len(self.column_indices)
1304
+ # # self.notification(self.stdscr, f"{str(self.column_indices)}, {tmp1}, {tmp2}")
1305
+ # self.initialise_variables()
1306
+ # self.column_widths = get_column_widths([v[1] for v in self.indexed_items], header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns)
1307
+ # self.draw_screen(self.indexed_items, self.highlights)
1308
+ # # self.move_column(direction=-1)
1309
+ #
1310
+ # elif self.check_key("move_column_right", key, self.keys_dict):
1311
+ # tmp1 = self.column_indices[self.sort_column]
1312
+ # tmp2 = self.column_indices[(self.sort_column+1)%len(self.column_indices)]
1313
+ # self.column_indices[self.sort_column] = tmp2
1314
+ # self.column_indices[(self.sort_column+1)%(len(self.column_indices))] = tmp1
1315
+ # self.sort_column = (self.sort_column+1)%len(self.column_indices)
1316
+ # self.initialise_variables()
1317
+ # self.draw_screen(self.indexed_items, self.highlights)
1318
+ # # self.move_column(direction=1)
1319
+
1320
+ elif self.check_key("cursor_down", key, self.keys_dict):
1321
+ page_turned = self.cursor_down()
1322
+ if not page_turned: clear_screen = False
1323
+ elif self.check_key("half_page_down", key, self.keys_dict):
1324
+ clear_screen = False
1325
+ for i in range(self.items_per_page//2):
1326
+ if self.cursor_down(): clear_screen = True
1327
+ elif self.check_key("five_down", key, self.keys_dict):
1328
+ clear_screen = False
1329
+ for i in range(5):
1330
+ if self.cursor_down(): clear_screen = True
1331
+ elif self.check_key("cursor_up", key, self.keys_dict):
1332
+ page_turned = self.cursor_up()
1333
+ if not page_turned: clear_screen = False
1334
+ elif self.check_key("five_up", key, self.keys_dict):
1335
+ clear_screen = False
1336
+ for i in range(5):
1337
+ if self.cursor_up(): clear_screen = True
1338
+ elif self.check_key("half_page_up", key, self.keys_dict):
1339
+ clear_screen = False
1340
+ for i in range(self.items_per_page//2):
1341
+ if self.cursor_up(): clear_screen = True
1342
+
1343
+ elif self.check_key("toggle_select", key, self.keys_dict):
1344
+ if len(self.indexed_items) > 0:
1345
+ item_index = self.indexed_items[self.cursor_pos][0]
1346
+ selected_count = sum(self.selections.values())
1347
+ if self.max_selected == -1 or selected_count >= self.max_selected:
1348
+ self.toggle_item(item_index)
1349
+ self.cursor_down()
1350
+ elif self.check_key("select_all", key, self.keys_dict): # Select all (m or ctrl-a)
1351
+ self.select_all()
1352
+
1353
+ elif self.check_key("select_none", key, self.keys_dict): # Deselect all (M or ctrl-r)
1354
+ self.deselect_all()
1355
+
1356
+ elif self.check_key("cursor_top", key, self.keys_dict):
1357
+ new_pos = 0
1358
+ while True:
1359
+ if new_pos in self.unselectable_indices: new_pos+=1
1360
+ else: break
1361
+ if new_pos < len(self.indexed_items):
1362
+ self.cursor_pos = new_pos
1363
+
1364
+ self.draw_screen(self.indexed_items, self.highlights)
1365
+
1366
+ elif self.check_key("cursor_bottom", key, self.keys_dict):
1367
+ new_pos = len(self.indexed_items)-1
1368
+ while True:
1369
+ if new_pos in self.unselectable_indices: new_pos-=1
1370
+ else: break
1371
+ if new_pos < len(self.items) and new_pos >= 0:
1372
+ self.cursor_pos = new_pos
1373
+ self.draw_screen(self.indexed_items, self.highlights)
1374
+ # current_row = items_per_page - 1
1375
+ # if current_page + 1 == (len(self.indexed_items) + items_per_page - 1) // items_per_page:
1376
+ #
1377
+ # current_row = (len(self.indexed_items) +items_per_page - 1) % items_per_page
1378
+ # self.draw_screen(self.indexed_items, self.highlights)
1379
+ elif self.check_key("enter", key, self.keys_dict):
1380
+ # Print the selected indices if any, otherwise print the current index
1381
+ if self.is_selecting or self.is_deselecting: self.handle_visual_selection()
1382
+ if len(self.items) == 0:
1383
+ function_data = self.get_function_data()
1384
+ function_data["last_key"] = key
1385
+ return [], "", function_data
1386
+ selected_indices = get_selected_indices(self.selections)
1387
+ if not selected_indices:
1388
+ selected_indices = [self.indexed_items[self.cursor_pos][0]]
1389
+
1390
+ options_sufficient = True
1391
+ usrtxt = self.user_opts
1392
+ for index in selected_indices:
1393
+ if self.require_option[index]:
1394
+ if self.option_functions[index] != None:
1395
+ options_sufficient, usrtxt = self.option_functions[index](
1396
+ stdscr=self.stdscr,
1397
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
1398
+ )
1399
+ else:
1400
+ self.set_registers()
1401
+ options_sufficient, usrtxt = default_option_input(
1402
+ self.stdscr,
1403
+ starting_value=self.user_opts,
1404
+ registers = self.registers
1405
+ )
1406
+
1407
+ if options_sufficient:
1408
+ self.user_opts = usrtxt
1409
+ self.stdscr.clear()
1410
+ self.stdscr.refresh()
1411
+ function_data = self.get_function_data()
1412
+ function_data["last_key"] = key
1413
+ return selected_indices, usrtxt, function_data
1414
+ elif self.check_key("page_down", key, self.keys_dict): # Next page
1415
+ self.cursor_pos = min(len(self.indexed_items) - 1, self.cursor_pos+self.items_per_page)
1416
+
1417
+ elif self.check_key("page_up", key, self.keys_dict):
1418
+ self.cursor_pos = max(0, self.cursor_pos-self.items_per_page)
1419
+
1420
+ elif self.check_key("redraw_screen", key, self.keys_dict):
1421
+ self.stdscr.clear()
1422
+ self.stdscr.refresh()
1423
+ self.draw_screen(self.indexed_items, self.highlights)
1424
+
1425
+ elif self.check_key("cycle_sort_method", key, self.keys_dict):
1426
+ self.columns_sort_method[self.sort_column] = (self.columns_sort_method[self.sort_column]+1) % len(self.SORT_METHODS)
1427
+ if len(self.indexed_items) > 0:
1428
+ current_index = self.indexed_items[self.cursor_pos][0]
1429
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
1430
+ self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
1431
+ elif self.check_key("cycle_sort_method_reverse", key, self.keys_dict): # Cycle sort method
1432
+ self.columns_sort_method[self.sort_column] = (self.columns_sort_method[self.sort_column]-1) % len(self.SORT_METHODS)
1433
+ if len(self.indexed_items) > 0:
1434
+ current_index = self.indexed_items[self.cursor_pos][0]
1435
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
1436
+ self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
1437
+ elif self.check_key("cycle_sort_order", key, self.keys_dict): # Toggle sort order
1438
+ self.sort_reverse[self.sort_column] = not self.sort_reverse[self.sort_column]
1439
+ if len(self.indexed_items) > 0:
1440
+ current_index = self.indexed_items[self.cursor_pos][0]
1441
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
1442
+ self.draw_screen(self.indexed_items, self.highlights)
1443
+ self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
1444
+ elif self.check_key("col_select", key, self.keys_dict):
1445
+ col_index = key - ord('0')
1446
+ if 0 <= col_index < len(self.items[0]):
1447
+ self.sort_column = col_index
1448
+ if len(self.indexed_items) > 0:
1449
+ current_index = self.indexed_items[self.cursor_pos][0]
1450
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
1451
+ self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
1452
+ elif self.check_key("col_select_next", key, self.keys_dict):
1453
+ if len(self.items) > 0 and len(self.items[0]) > 0:
1454
+ col_index = (self.sort_column +1) % (len(self.items[0]))
1455
+ self.sort_column = col_index
1456
+ if len(self.indexed_items) > 0:
1457
+ current_index = self.indexed_items[self.cursor_pos][0]
1458
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
1459
+ self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
1460
+ elif self.check_key("col_select_prev", key, self.keys_dict):
1461
+ if len(self.items) > 0 and len(self.items[0]) > 0:
1462
+ col_index = (self.sort_column -1) % (len(self.items[0]))
1463
+ self.sort_column = col_index
1464
+ if len(self.indexed_items) > 0:
1465
+ current_index = self.indexed_items[self.cursor_pos][0]
1466
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
1467
+ self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
1468
+ elif self.check_key("col_hide", key, self.keys_dict):
1469
+ d = {'!': 0, '@': 1, '#': 2, '$': 3, '%': 4, '^': 5, '&': 6, '*': 7, '(': 8, ')': 9}
1470
+ d = {s:i for i,s in enumerate(")!@#$%^&*(")}
1471
+ col_index = d[chr(key)]
1472
+ self.toggle_column_visibility(col_index)
1473
+ elif self.check_key("copy", key, self.keys_dict):
1474
+ self.copy_dialog()
1475
+ elif self.check_key("save", key, self.keys_dict):
1476
+ self.save_dialog()
1477
+ elif self.check_key("load", key, self.keys_dict):
1478
+ self.load_dialog()
1479
+
1480
+ elif self.check_key("delete", key, self.keys_dict): # Delete key
1481
+ self.delete_entries()
1482
+ # elif self.check_key("increase_lines_per_page", key, self.keys_dict):
1483
+ # self.items_per_page += 1
1484
+ # self.draw_screen(self.indexed_items, self.highlights)
1485
+ # elif self.check_key("decrease_lines_per_page", key, self.keys_dict):
1486
+ # if self.items_per_page > 1:
1487
+ # self.items_per_page -= 1
1488
+ # self.draw_screen(self.indexed_items, self.highlights)
1489
+ elif self.check_key("decrease_column_width", key, self.keys_dict):
1490
+ if self.max_column_width > 10:
1491
+ self.max_column_width -= 10
1492
+ self.column_widths[:] = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns)
1493
+ self.draw_screen(self.indexed_items, self.highlights)
1494
+ elif self.check_key("increase_column_width", key, self.keys_dict):
1495
+ if self.max_column_width < 1000:
1496
+ self.max_column_width += 10
1497
+ self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns)
1498
+ self.draw_screen(self.indexed_items, self.highlights)
1499
+ elif self.check_key("visual_selection_toggle", key, self.keys_dict):
1500
+ self.handle_visual_selection()
1501
+ self.draw_screen(self.indexed_items, self.highlights)
1502
+
1503
+ elif self.check_key("visual_deselection_toggle", key, self.keys_dict):
1504
+ self.handle_visual_selection(selecting=False)
1505
+ self.draw_screen(self.indexed_items, self.highlights)
1506
+
1507
+ elif key == curses.KEY_RESIZE: # Terminal resize signal
1508
+ top_space = self.top_gap
1509
+ h, w = self.stdscr.getmaxyx()
1510
+ if self.title: top_space+=1
1511
+ if self.display_modes: top_space+=1
1512
+
1513
+ self.items_per_page = h - top_space-int(bool(self.header)) - 3*int(bool(self.show_footer))
1514
+ if not self.show_footer and self.footer_string: self.items_per_page-=1
1515
+ self.items_per_page = max(min(h-top_space-2, self.items_per_page), 0)
1516
+ self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns)
1517
+
1518
+ self.draw_screen(self.indexed_items, self.highlights)
1519
+
1520
+
1521
+ elif key == ord('r'):
1522
+ # Refresh
1523
+ top_space = self.top_gap + int(bool(self.display_modes)) + int(bool(self.title)) + int(bool(self.header))
1524
+ bottom_space = 3*int(bool(self.show_footer))
1525
+ self.items_per_page = os.get_terminal_size().lines - top_space
1526
+
1527
+ h, w = self.stdscr.getmaxyx()
1528
+ self.items_per_page = h - top_space - bottom_space
1529
+ self.stdscr.refresh()
1530
+
1531
+ elif self.check_key("filter_input", key, self.keys_dict):
1532
+ self.draw_screen(self.indexed_items, self.highlights)
1533
+ usrtxt = f"{self.filter_query} " if self.filter_query else ""
1534
+ h, w = self.stdscr.getmaxyx()
1535
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
1536
+ if self.show_footer: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
1537
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
1538
+ self.set_registers()
1539
+ usrtxt, return_val = input_field(
1540
+ self.stdscr,
1541
+ usrtxt=usrtxt,
1542
+ field_name="Filter",
1543
+ x=lambda:2,
1544
+ y=lambda: self.stdscr.getmaxyx()[0]-2,
1545
+ # max_length=field_end,
1546
+ max_length=field_end_f,
1547
+ registers=self.registers,
1548
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
1549
+ )
1550
+ if return_val:
1551
+ self.filter_query = usrtxt
1552
+
1553
+ # If the current mode filter has been changed then go back to the first mode
1554
+ if "filter" in self.modes[self.mode_index] and self.modes[self.mode_index]["filter"] not in self.filter_query:
1555
+ self.mode_index = 0
1556
+ # elif "filter" in modes[mode_index] and modes[mode_index]["filter"] in filter_query:
1557
+ # filter_query.split(modes[mode_index]["filter"])
1558
+
1559
+ prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
1560
+ self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
1561
+ if prev_index in [x[0] for x in self.indexed_items]: new_index = [x[0] for x in self.indexed_items].index(prev_index)
1562
+ else: new_index = 0
1563
+ self.cursor_pos = new_index
1564
+ # Re-sort self.items after applying filter
1565
+ if self.columns_sort_method[self.sort_column] != 0:
1566
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
1567
+
1568
+ elif self.check_key("search_input", key, self.keys_dict):
1569
+ self.draw_screen(self.indexed_items, self.highlights)
1570
+ usrtxt = f"{self.search_query} " if self.search_query else ""
1571
+ h, w = self.stdscr.getmaxyx()
1572
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
1573
+ if self.show_footer: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
1574
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
1575
+ self.set_registers()
1576
+ usrtxt, return_val = input_field(
1577
+ self.stdscr,
1578
+ usrtxt=usrtxt,
1579
+ field_name="Search",
1580
+ x=lambda:2,
1581
+ y=lambda: self.stdscr.getmaxyx()[0]-3,
1582
+ max_length=field_end_f,
1583
+ registers=self.registers,
1584
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
1585
+ )
1586
+ if return_val:
1587
+ self.search_query = usrtxt
1588
+ return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
1589
+ query=self.search_query,
1590
+ indexed_items=self.indexed_items,
1591
+ highlights=self.highlights,
1592
+ cursor_pos=self.cursor_pos,
1593
+ unselectable_indices=self.unselectable_indices,
1594
+ )
1595
+ if return_val:
1596
+ self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
1597
+ else:
1598
+ self.search_index, self.search_count = 0, 0
1599
+
1600
+ elif self.check_key("continue_search_forward", key, self.keys_dict):
1601
+ return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
1602
+ query=self.search_query,
1603
+ indexed_items=self.indexed_items,
1604
+ highlights=self.highlights,
1605
+ cursor_pos=self.cursor_pos,
1606
+ unselectable_indices=self.unselectable_indices,
1607
+ continue_search=True,
1608
+ )
1609
+ if return_val:
1610
+ self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
1611
+ elif self.check_key("continue_search_backward", key, self.keys_dict):
1612
+ return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
1613
+ query=self.search_query,
1614
+ indexed_items=self.indexed_items,
1615
+ highlights=self.highlights,
1616
+ cursor_pos=self.cursor_pos,
1617
+ unselectable_indices=self.unselectable_indices,
1618
+ continue_search=True,
1619
+ reverse=True,
1620
+ )
1621
+ if return_val:
1622
+ self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
1623
+ elif self.check_key("cancel", key, self.keys_dict): # ESC key
1624
+ # order of escapes:
1625
+ # 1. selecting/deslecting
1626
+ # 2. search
1627
+ # 3. filter
1628
+ # 4. if self.cancel_is_back (e.g., notification) then we exit
1629
+ # 4. selecting
1630
+
1631
+ # Cancel visual de/selection
1632
+ if self.is_selecting or self.is_deselecting:
1633
+ self.start_selection = -1
1634
+ self.end_selection = -1
1635
+ self.is_selecting = False
1636
+ self.is_deselecting = False
1637
+ # Cancel search
1638
+ elif self.search_query:
1639
+ self.search_query = ""
1640
+ self.highlights = [highlight for highlight in self.highlights if "type" not in highlight or highlight["type"] != "search" ]
1641
+ # Remove filter
1642
+ elif self.filter_query:
1643
+ if "filter" in self.modes[self.mode_index] and self.modes[self.mode_index]["filter"] in self.filter_query and self.filter_query.strip() != self.modes[self.mode_index]["filter"]:
1644
+ self.filter_query = self.modes[self.mode_index]["filter"]
1645
+ # elif "filter" in modes[mode_index]:
1646
+ else:
1647
+ self.filter_query = ""
1648
+ self.mode_index = 0
1649
+ prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
1650
+ self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
1651
+ if prev_index in [x[0] for x in self.indexed_items]: new_index = [x[0] for x in self.indexed_items].index(prev_index)
1652
+ else: new_index = 0
1653
+ self.cursor_pos = new_index
1654
+ # Re-sort self.items after applying filter
1655
+ if self.columns_sort_method[self.sort_column] != 0:
1656
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
1657
+ elif self.cancel_is_back:
1658
+ function_data = self.get_function_data()
1659
+ function_data["last_key"] = key
1660
+ return [], "escape", function_data
1661
+
1662
+
1663
+ # else:
1664
+ # self.search_query = ""
1665
+ # self.mode_index = 0
1666
+ # self.highlights = [highlight for highlight in self.highlights if "type" not in highlight or highlight["type"] != "search" ]
1667
+ # continue
1668
+ self.draw_screen(self.indexed_items, self.highlights)
1669
+
1670
+ elif self.check_key("opts_input", key, self.keys_dict):
1671
+ usrtxt = f"{self.user_opts} " if self.user_opts else ""
1672
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
1673
+ if self.show_footer: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
1674
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
1675
+ self.set_registers()
1676
+ usrtxt, return_val = input_field(
1677
+ self.stdscr,
1678
+ usrtxt=usrtxt,
1679
+ field_name="Opts",
1680
+ x=lambda:2,
1681
+ y=lambda: self.stdscr.getmaxyx()[0]-1,
1682
+ max_length=field_end_f,
1683
+ registers=self.registers,
1684
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
1685
+ )
1686
+ if return_val:
1687
+ self.user_opts = usrtxt
1688
+ elif self.check_key("opts_select", key, self.keys_dict):
1689
+ s, o, f = self.choose_option(self.stdscr)
1690
+ if self.user_opts.strip(): self.user_opts += " "
1691
+ self.user_opts += " ".join([x[0] for x in s.values()])
1692
+ elif self.check_key("notification_toggle", key, self.keys_dict):
1693
+ self.notification(self.stdscr, colours_end=self.colours_end)
1694
+
1695
+ elif self.check_key("mode_next", key, self.keys_dict): # tab key
1696
+ # apply setting
1697
+ prev_mode_index = self.mode_index
1698
+ self.mode_index = (self.mode_index+1)%len(self.modes)
1699
+ mode = self.modes[self.mode_index]
1700
+ for key, val in mode.items():
1701
+ if key == 'filter':
1702
+ if 'filter' in self.modes[prev_mode_index]:
1703
+ self.filter_query = self.filter_query.replace(self.modes[prev_mode_index]['filter'], '')
1704
+ self.filter_query = f"{self.filter_query.strip()} {val.strip()}".strip()
1705
+ prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
1706
+
1707
+ self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
1708
+ if prev_index in [x[0] for x in self.indexed_items]: new_index = [x[0] for x in self.indexed_items].index(prev_index)
1709
+ else: new_index = 0
1710
+ self.cursor_pos = new_index
1711
+ # Re-sort self.items after applying filter
1712
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
1713
+ elif self.check_key("mode_prev", key, self.keys_dict): # shift+tab key
1714
+ # apply setting
1715
+ prev_mode_index = self.mode_index
1716
+ self.mode_index = (self.mode_index-1)%len(self.modes)
1717
+ mode = self.modes[self.mode_index]
1718
+ for key, val in mode.items():
1719
+ if key == 'filter':
1720
+ if 'filter' in self.modes[prev_mode_index]:
1721
+ self.filter_query = self.filter_query.replace(self.modes[prev_mode_index]['filter'], '')
1722
+ self.filter_query = f"{self.filter_query.strip()} {val.strip()}".strip()
1723
+ prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
1724
+ self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
1725
+ if prev_index in [x[0] for x in self.indexed_items]: new_index = [x[0] for x in self.indexed_items].index(prev_index)
1726
+ else: new_index = 0
1727
+ self.cursor_pos = new_index
1728
+ # Re-sort self.items after applying filter
1729
+ sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
1730
+ elif self.check_key("pipe_input", key, self.keys_dict):
1731
+ usrtxt = "xargs -d '\n' -I{} "
1732
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
1733
+ if self.show_footer: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
1734
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
1735
+ self.set_registers()
1736
+ usrtxt, return_val = input_field(
1737
+ self.stdscr,
1738
+ usrtxt=usrtxt,
1739
+ field_name="Command",
1740
+ x=lambda:2,
1741
+ y=lambda: self.stdscr.getmaxyx()[0]-2,
1742
+ literal=True,
1743
+ max_length=field_end_f,
1744
+ registers=self.registers,
1745
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
1746
+ )
1747
+ if return_val:
1748
+ selected_indices = get_selected_indices(self.selections)
1749
+ if not selected_indices:
1750
+ selected_indices = [self.indexed_items[self.cursor_pos][0]]
1751
+
1752
+ full_values = [format_row_full(self.items[i], self.hidden_columns) for i in selected_indices] # Use format_row_full for full data
1753
+ full_values = [self.items[i][self.sort_column] for i in selected_indices]
1754
+ if full_values:
1755
+ process = subprocess.Popen(usrtxt, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1756
+ process.communicate(input='\n'.join(full_values).encode('utf-8'))
1757
+
1758
+ elif self.check_key("open", key, self.keys_dict):
1759
+ selected_indices = get_selected_indices(self.selections)
1760
+ if not selected_indices:
1761
+ selected_indices = [self.indexed_items[self.cursor_pos][0]]
1762
+
1763
+ file_names = [self.items[i][self.sort_column] for i in selected_indices]
1764
+ response = openFiles(file_names)
1765
+ if response:
1766
+ self.notification(self.stdscr, message=response)
1767
+
1768
+
1769
+ elif self.check_key("reset_opts", key, self.keys_dict):
1770
+ self.user_opts = ""
1771
+
1772
+ elif self.check_key("edit", key, self.keys_dict):
1773
+ if len(self.indexed_items) > 0 and self.sort_column >=0 and self.editable_columns[self.sort_column]:
1774
+ current_val = self.indexed_items[self.cursor_pos][1][self.sort_column]
1775
+ usrtxt = f"{current_val}"
1776
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
1777
+ if self.show_footer: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
1778
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
1779
+ self.set_registers()
1780
+ usrtxt, return_val = input_field(
1781
+ self.stdscr,
1782
+ usrtxt=usrtxt,
1783
+ field_name="Edit value",
1784
+ x=lambda:2,
1785
+ y=lambda: self.stdscr.getmaxyx()[0]-2,
1786
+ max_length=field_end_f,
1787
+ registers=self.registers,
1788
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
1789
+ )
1790
+ if return_val:
1791
+ self.indexed_items[self.cursor_pos][1][self.sort_column] = usrtxt
1792
+
1793
+ elif self.check_key("edit_picker", key, self.keys_dict):
1794
+ if len(self.indexed_items) > 0 and self.sort_column >=0 and self.editable_columns[self.sort_column]:
1795
+ current_val = self.indexed_items[self.cursor_pos][1][self.sort_column]
1796
+ usrtxt = f"{current_val}"
1797
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
1798
+ if self.show_footer: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
1799
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
1800
+ self.set_registers()
1801
+ usrtxt, return_val = input_field(
1802
+ self.stdscr,
1803
+ usrtxt=usrtxt,
1804
+ field_name="Edit value",
1805
+ x=lambda:2,
1806
+ y=lambda: self.stdscr.getmaxyx()[0]-2,
1807
+ max_length=field_end_f,
1808
+ registers=self.registers,
1809
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
1810
+ )
1811
+ if return_val:
1812
+ self.indexed_items[self.cursor_pos][1][self.sort_column] = usrtxt
1813
+ elif self.check_key("edit_ipython", key, self.keys_dict):
1814
+ import IPython
1815
+ self.stdscr.clear()
1816
+ restrict_curses(self.stdscr)
1817
+ self.stdscr.clear()
1818
+ os.system('cls' if os.name == 'nt' else 'clear')
1819
+ globals()['self'] = self # make the instance available in IPython namespace
1820
+
1821
+ from traitlets.config import Config
1822
+ c = Config()
1823
+ # Doesn't work; Config only works with start_ipython, not embed... but start_ipython causes errors
1824
+ # c.InteractiveShellApp.exec_lines = [
1825
+ # '%clear'
1826
+ # ]
1827
+ msg = "The active Picker object has variable name self.\n"
1828
+ msg += "\te.g., self.items will display the items in Picker"
1829
+ IPython.embed(header=msg, config=c)
1830
+
1831
+ unrestrict_curses(self.stdscr)
1832
+
1833
+ self.stdscr.clear()
1834
+ self.stdscr.refresh()
1835
+ self.initialise_variables()
1836
+ self.draw_screen(self.indexed_items, self.highlights)
1837
+
1838
+
1839
+
1840
+ self.draw_screen(self.indexed_items, self.highlights, clear=clear_screen)
1841
+
1842
+
1843
+ def set_colours(pick: int = 0, start: int = 0) -> Optional[int]:
1844
+ """ Initialise curses colour pairs using dictionary with colour keys. """
1845
+ global COLOURS_SET, notification_colours, help_colours
1846
+ if notification_colours == {}: notification_colours = get_notification_colours(0)
1847
+ if help_colours == {}: help_colours = get_help_colours(0)
1848
+ if COLOURS_SET: return None
1849
+ if start == None: start = 0
1850
+
1851
+ colours = get_colours(pick)
1852
+ notification_colours = get_notification_colours(pick)
1853
+ help_colours = get_help_colours(pick)
1854
+
1855
+ if not colours: return 0
1856
+
1857
+ try:
1858
+ curses.init_pair(start+1, colours['selected_fg'], colours['selected_bg'])
1859
+ curses.init_pair(start+2, colours['unselected_fg'], colours['unselected_bg'])
1860
+ curses.init_pair(start+3, colours['normal_fg'], colours['background'])
1861
+ curses.init_pair(start+4, colours['header_fg'], colours['header_bg'])
1862
+ curses.init_pair(start+5, colours['cursor_fg'], colours['cursor_bg'])
1863
+ curses.init_pair(start+6, colours['normal_fg'], colours['background'])
1864
+ curses.init_pair(start+7, colours['error_fg'], colours['error_bg'])
1865
+ curses.init_pair(start+8, colours['complete_fg'], colours['complete_bg'])
1866
+ curses.init_pair(start+9, colours['active_fg'], colours['active_bg'])
1867
+ curses.init_pair(start+10, colours['search_fg'], colours['search_bg'])
1868
+ curses.init_pair(start+11, colours['waiting_fg'], colours['waiting_bg'])
1869
+ curses.init_pair(start+12, colours['paused_fg'], colours['paused_bg'])
1870
+ curses.init_pair(start+13, colours['active_input_fg'], colours['active_input_bg'])
1871
+ curses.init_pair(start+14, colours['modes_selected_fg'], colours['modes_selected_bg'])
1872
+ curses.init_pair(start+15, colours['modes_unselected_fg'], colours['modes_unselected_bg'])
1873
+ curses.init_pair(start+16, colours['title_fg'], colours['title_bg'])
1874
+ curses.init_pair(start+17, colours['normal_fg'], colours['title_bar'])
1875
+ curses.init_pair(start+18, colours['normal_fg'], colours['scroll_bar_bg'])
1876
+ curses.init_pair(start+19, colours['selected_header_column_fg'], colours['selected_header_column_bg'])
1877
+ curses.init_pair(start+20, colours['footer_fg'], colours['footer_bg'])
1878
+ curses.init_pair(start+21, colours['refreshing_fg'], colours['refreshing_bg'])
1879
+ curses.init_pair(start+22, colours['40pc_fg'], colours['40pc_bg'])
1880
+ curses.init_pair(start+23, colours['refreshing_inactive_fg'], colours['refreshing_inactive_bg'])
1881
+ curses.init_pair(start+24, colours['footer_string_fg'], colours['footer_string_bg'])
1882
+
1883
+
1884
+ # notifications 50, infobox 100, help 150
1885
+ # Notification colours
1886
+ colours = notification_colours
1887
+ start = 50
1888
+ curses.init_pair(start+1, colours['selected_fg'], colours['selected_bg'])
1889
+ curses.init_pair(start+2, colours['unselected_fg'], colours['unselected_bg'])
1890
+ curses.init_pair(start+3, colours['normal_fg'], colours['background'])
1891
+ curses.init_pair(start+4, colours['header_fg'], colours['header_bg'])
1892
+ curses.init_pair(start+5, colours['cursor_fg'], colours['cursor_bg'])
1893
+ curses.init_pair(start+6, colours['normal_fg'], colours['background'])
1894
+ curses.init_pair(start+7, colours['error_fg'], colours['error_bg'])
1895
+ curses.init_pair(start+8, colours['complete_fg'], colours['complete_bg'])
1896
+ curses.init_pair(start+9, colours['active_fg'], colours['active_bg'])
1897
+ curses.init_pair(start+10, colours['search_fg'], colours['search_bg'])
1898
+ curses.init_pair(start+11, colours['waiting_fg'], colours['waiting_bg'])
1899
+ curses.init_pair(start+12, colours['paused_fg'], colours['paused_bg'])
1900
+ curses.init_pair(start+13, colours['active_input_fg'], colours['active_input_bg'])
1901
+ curses.init_pair(start+14, colours['modes_selected_fg'], colours['modes_selected_bg'])
1902
+ curses.init_pair(start+15, colours['modes_unselected_fg'], colours['modes_unselected_bg'])
1903
+ curses.init_pair(start+16, colours['title_fg'], colours['title_bg'])
1904
+ curses.init_pair(start+17, colours['normal_fg'], colours['title_bar'])
1905
+ curses.init_pair(start+18, colours['normal_fg'], colours['scroll_bar_bg'])
1906
+ curses.init_pair(start+19, colours['selected_header_column_fg'], colours['selected_header_column_bg'])
1907
+ curses.init_pair(start+20, colours['footer_fg'], colours['footer_bg'])
1908
+ curses.init_pair(start+21, colours['refreshing_fg'], colours['refreshing_bg'])
1909
+ curses.init_pair(start+22, colours['40pc_fg'], colours['40pc_bg'])
1910
+
1911
+ # Infobox
1912
+ colours = notification_colours
1913
+ start = 100
1914
+ curses.init_pair(start+1, colours['selected_fg'], colours['selected_bg'])
1915
+ curses.init_pair(start+2, colours['unselected_fg'], colours['unselected_bg'])
1916
+ curses.init_pair(start+3, colours['normal_fg'], colours['background'])
1917
+ curses.init_pair(start+4, colours['header_fg'], colours['header_bg'])
1918
+ curses.init_pair(start+5, colours['cursor_fg'], colours['cursor_bg'])
1919
+ curses.init_pair(start+6, colours['normal_fg'], colours['background'])
1920
+ curses.init_pair(start+7, colours['error_fg'], colours['error_bg'])
1921
+ curses.init_pair(start+8, colours['complete_fg'], colours['complete_bg'])
1922
+ curses.init_pair(start+9, colours['active_fg'], colours['active_bg'])
1923
+ curses.init_pair(start+10, colours['search_fg'], colours['search_bg'])
1924
+ curses.init_pair(start+11, colours['waiting_fg'], colours['waiting_bg'])
1925
+ curses.init_pair(start+12, colours['paused_fg'], colours['paused_bg'])
1926
+ curses.init_pair(start+13, colours['active_input_fg'], colours['active_input_bg'])
1927
+ curses.init_pair(start+14, colours['modes_selected_fg'], colours['modes_selected_bg'])
1928
+ curses.init_pair(start+15, colours['modes_unselected_fg'], colours['modes_unselected_bg'])
1929
+ curses.init_pair(start+16, colours['title_fg'], colours['title_bg'])
1930
+ curses.init_pair(start+17, colours['normal_fg'], colours['title_bar'])
1931
+ curses.init_pair(start+18, colours['normal_fg'], colours['scroll_bar_bg'])
1932
+ curses.init_pair(start+19, colours['selected_header_column_fg'], colours['selected_header_column_bg'])
1933
+ curses.init_pair(start+20, colours['footer_fg'], colours['footer_bg'])
1934
+ curses.init_pair(start+21, colours['refreshing_fg'], colours['refreshing_bg'])
1935
+ curses.init_pair(start+22, colours['40pc_fg'], colours['40pc_bg'])
1936
+
1937
+ # Help
1938
+ colours = help_colours
1939
+ start = 150
1940
+ curses.init_pair(start+1, colours['selected_fg'], colours['selected_bg'])
1941
+ curses.init_pair(start+2, colours['unselected_fg'], colours['unselected_bg'])
1942
+ curses.init_pair(start+3, colours['normal_fg'], colours['background'])
1943
+ curses.init_pair(start+4, colours['header_fg'], colours['header_bg'])
1944
+ curses.init_pair(start+5, colours['cursor_fg'], colours['cursor_bg'])
1945
+ curses.init_pair(start+6, colours['normal_fg'], colours['background'])
1946
+ curses.init_pair(start+7, colours['error_fg'], colours['error_bg'])
1947
+ curses.init_pair(start+8, colours['complete_fg'], colours['complete_bg'])
1948
+ curses.init_pair(start+9, colours['active_fg'], colours['active_bg'])
1949
+ curses.init_pair(start+10, colours['search_fg'], colours['search_bg'])
1950
+ curses.init_pair(start+11, colours['waiting_fg'], colours['waiting_bg'])
1951
+ curses.init_pair(start+12, colours['paused_fg'], colours['paused_bg'])
1952
+ curses.init_pair(start+13, colours['active_input_fg'], colours['active_input_bg'])
1953
+ curses.init_pair(start+14, colours['modes_selected_fg'], colours['modes_selected_bg'])
1954
+ curses.init_pair(start+15, colours['modes_unselected_fg'], colours['modes_unselected_bg'])
1955
+ curses.init_pair(start+16, colours['title_fg'], colours['title_bg'])
1956
+ curses.init_pair(start+17, colours['normal_fg'], colours['title_bar'])
1957
+ curses.init_pair(start+18, colours['normal_fg'], colours['scroll_bar_bg'])
1958
+ curses.init_pair(start+19, colours['selected_header_column_fg'], colours['selected_header_column_bg'])
1959
+ curses.init_pair(start+20, colours['footer_fg'], colours['footer_bg'])
1960
+ curses.init_pair(start+21, colours['refreshing_fg'], colours['refreshing_bg'])
1961
+ curses.init_pair(start+22, colours['40pc_fg'], colours['40pc_bg'])
1962
+ except:
1963
+ pass
1964
+ COLOURS_SET = True
1965
+ return start+21
1966
+
1967
+ def parse_arguments() -> Tuple[argparse.Namespace, dict]:
1968
+ """ Parse arguments. """
1969
+ parser = argparse.ArgumentParser(description='Convert table to list of lists.')
1970
+ parser.add_argument('-i', dest='file', help='File containing the table to be converted.')
1971
+ parser.add_argument('--load', '-l', dest='load', type=str, help='File load from list_picker dump.')
1972
+ parser.add_argument('--stdin', dest='stdin', action='store_true', help='Table passed on stdin')
1973
+ parser.add_argument('--stdin2', action='store_true', help='Table passed on stdin')
1974
+ parser.add_argument('--generate', '-g', type=str, help='Pass file to generate data for list picker.')
1975
+ parser.add_argument('-d', dest='delimiter', default='\t', help='Delimiter for rows in the table (default: tab)')
1976
+ parser.add_argument('-t', dest='file_type', choices=['tsv', 'csv', 'json', 'xlsx', 'ods', 'pkl'], help='Type of file (tsv, csv, json, xlsx, ods)')
1977
+ args = parser.parse_args()
1978
+
1979
+ function_data = {
1980
+ "items" : [],
1981
+ "header": [],
1982
+ "unselectable_indices" : [],
1983
+ "colours": get_colours(0),
1984
+ "top_gap": 0,
1985
+ "max_column_width": 70,
1986
+ }
1987
+
1988
+ if args.file:
1989
+ input_arg = args.file
1990
+ elif args.stdin:
1991
+ input_arg = '--stdin'
1992
+ elif args.stdin2:
1993
+ input_arg = '--stdin2'
1994
+
1995
+ elif args.generate:
1996
+ function_data["refresh_function"] = lambda : generate_list_picker_data(args.generate)
1997
+ function_data["get_data_startup"] = True
1998
+ function_data["get_new_data"] = True
1999
+ return args, function_data
2000
+ elif args.load:
2001
+ function_data = load_state(args.load)
2002
+ function_data["refresh_function"] = lambda : (load_state(args.load)["items"], load_state(args.load)["header"])
2003
+ function_data["get_new_data"] = True
2004
+ return args, function_data
2005
+
2006
+ else:
2007
+ print("Error: Please provide input file or use --stdin flag.")
2008
+ return args, function_data
2009
+ # sys.exit(1)
2010
+
2011
+ items, header = table_to_list(input_arg, args.delimiter, args.file_type)
2012
+ function_data["items"] = items
2013
+ if header: function_data["header"] = header
2014
+ return args, function_data
2015
+
2016
+ def start_curses() -> curses.window:
2017
+ stdscr = curses.initscr()
2018
+ curses.start_color()
2019
+ curses.noecho() # Turn off automatic echoing of keys to the screen
2020
+ curses.cbreak() # Interpret keystrokes immediately (without requiring Enter)
2021
+ stdscr.keypad(True)
2022
+ curses.raw()
2023
+
2024
+ return stdscr
2025
+
2026
+ def close_curses(stdscr: curses.window) -> None:
2027
+ stdscr.keypad(False)
2028
+ curses.nocbreak()
2029
+ curses.noraw()
2030
+ curses.echo()
2031
+ curses.endwin()
2032
+
2033
+ def restrict_curses(stdscr: curses.window) -> None:
2034
+ stdscr.keypad(False)
2035
+ curses.nocbreak()
2036
+ curses.noraw()
2037
+ curses.echo()
2038
+
2039
+ def unrestrict_curses(stdscr: curses.window) -> None:
2040
+ curses.noecho() # Turn off automatic echoing of keys to the screen
2041
+ curses.cbreak() # Interpret keystrokes immediately (without requiring Enter)
2042
+ stdscr.keypad(True)
2043
+
2044
+ def main():
2045
+ args, function_data = parse_arguments()
2046
+
2047
+ try:
2048
+ if function_data["items"] == []:
2049
+ function_data["items"] = test_items
2050
+ function_data["highlights"] = test_highlights
2051
+ function_data["header"] = test_header
2052
+ except:
2053
+ pass
2054
+
2055
+ stdscr = start_curses()
2056
+ try:
2057
+ # Run the list picker
2058
+ h, w = stdscr.getmaxyx()
2059
+ if (h>8 and w >20):
2060
+ curses.init_pair(1, 253, 232)
2061
+ stdscr.bkgd(' ', curses.color_pair(1)) # Apply background color
2062
+ stdscr.addstr(h//2, (w-len("List picker is loading your data..."))//2, "List picker is loading your data...")
2063
+ stdscr.refresh()
2064
+
2065
+ app = Picker(stdscr, **function_data)
2066
+ app.run()
2067
+ except Exception as e:
2068
+ print(e)
2069
+
2070
+ # Clean up
2071
+ close_curses(stdscr)
2072
+
2073
+
2074
+
2075
+ if __name__ == '__main__':
2076
+ main()