listpick 0.1.13.54__py3-none-any.whl → 0.1.13.55__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,3250 @@
1
+ #!/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ listpick_app.py
5
+ Set up environment to parse command-line arguments and run a Picker.
6
+
7
+ Author: GrimAndGreedy
8
+ License: MIT
9
+ """
10
+
11
+ import curses
12
+ import re
13
+ import os
14
+ import subprocess
15
+ import argparse
16
+ import time
17
+ from wcwidth import wcswidth
18
+ from typing import Callable, Optional, Tuple, Dict
19
+ import json
20
+ import threading
21
+ import string
22
+ import logging
23
+
24
+ from listpick.ui.picker_colours import get_colours, get_help_colours, get_notification_colours, get_theme_count, get_fallback_colours
25
+ from listpick.utils.options_selectors import default_option_input, output_file_option_selector, default_option_selector
26
+ from listpick.utils.table_to_list_of_lists import *
27
+ from listpick.utils.utils import *
28
+ from listpick.utils.sorting import *
29
+ from listpick.utils.filtering import *
30
+ from listpick.ui.input_field import *
31
+ from listpick.utils.clipboard_operations import *
32
+ from listpick.utils.paste_operations import *
33
+ from listpick.utils.searching import search
34
+ from listpick.ui.help_screen import help_lines
35
+ from listpick.ui.keys import picker_keys, notification_keys, options_keys, help_keys
36
+ from listpick.utils.generate_data import generate_picker_data
37
+ from listpick.utils.dump import dump_state, load_state, dump_data
38
+ from listpick.ui.build_help import build_help_rows
39
+ from listpick.ui.footer import StandardFooter, CompactFooter, NoFooter
40
+ from listpick.utils.picker_log import setup_logger
41
+
42
+
43
+ try:
44
+ from tmp.data_stuff import test_items, test_highlights, test_header
45
+ except:
46
+ test_items, test_highlights, test_header = [], [], []
47
+
48
+ COLOURS_SET = False
49
+ help_colours, notification_colours = {}, {}
50
+
51
+ class Command:
52
+ def __init__(self, command_type, command_value):
53
+ self.command_type = command_type
54
+ self.command_value = command_value
55
+
56
+ class Picker:
57
+ def __init__(self,
58
+ stdscr: curses.window,
59
+ items: list[list[str]] = [],
60
+ cursor_pos: int = 0,
61
+ colours: dict = get_colours(0),
62
+ colour_theme_number: int = 0,
63
+ max_selected: int = -1,
64
+ top_gap: int =0,
65
+ title: str ="Picker",
66
+ header: list =[],
67
+ max_column_width: int =70,
68
+ clear_on_start: bool = False,
69
+
70
+ auto_refresh: bool =False,
71
+ timer: float = 5,
72
+
73
+ get_new_data: bool =False, # Whether we can get new data
74
+ refresh_function: Optional[Callable] = lambda: [], # The function with which we get new data
75
+ get_data_startup: bool =False, # Whether we should get data at statrup
76
+ track_entries_upon_refresh: bool = True,
77
+ pin_cursor: bool = False,
78
+ id_column: int = 0,
79
+
80
+ unselectable_indices: list =[],
81
+ highlights: list =[],
82
+ highlights_hide: bool =False,
83
+ number_columns: bool =True,
84
+ column_widths: list = [],
85
+ column_indices: list = [],
86
+
87
+
88
+ current_row : int = 0,
89
+ current_page : int = 0,
90
+ is_selecting : bool = False,
91
+ is_deselecting : int = False,
92
+ start_selection: int = -1,
93
+ start_selection_col: int = -1,
94
+ end_selection: int = -1,
95
+ user_opts : str = "",
96
+ options_list: list[str] = [],
97
+ user_settings : str = "",
98
+ separator : str = " ",
99
+ search_query : str = "",
100
+ search_count : int = 0,
101
+ search_index : int = 0,
102
+ filter_query : str = "",
103
+ hidden_columns: list = [],
104
+ indexed_items: list[Tuple[int, list[str]]] = [],
105
+ scroll_bar : int = True,
106
+
107
+ selections: dict = {},
108
+ cell_selections: dict[tuple[int,int], bool] = {},
109
+ highlight_full_row: bool =False,
110
+ cell_cursor: bool = False,
111
+
112
+ items_per_page : int = -1,
113
+ sort_method : int = 0,
114
+ SORT_METHODS: list[str] = ['Orig', 'lex', 'LEX', 'alnum', 'ALNUM', 'time', 'num', 'size'],
115
+ sort_reverse: list[bool] = [False],
116
+ selected_column: int = 0,
117
+ sort_column : int = 0,
118
+
119
+ columns_sort_method: list[int] = [0],
120
+ key_chain: str = "",
121
+ last_key: Optional[str] = None,
122
+
123
+ paginate: bool =False,
124
+ cancel_is_back: bool = False,
125
+ mode_index: int =0,
126
+ modes: list[dict] = [],
127
+ display_modes: bool =False,
128
+ require_option: list=[],
129
+ require_option_default: list=[],
130
+ option_functions: list[Callable[..., Tuple[bool, str]]] = [],
131
+ default_option_function: Callable[..., Tuple[bool, str]] = default_option_input,
132
+ disabled_keys: list=[],
133
+
134
+ show_header: bool = True,
135
+ show_row_header: bool = False,
136
+ show_footer: bool =True,
137
+ footer_style: int = 0,
138
+ footer_string: str="",
139
+ footer_string_auto_refresh: bool=False,
140
+ footer_string_refresh_function: Optional[Callable] = None,
141
+ footer_timer: float=1,
142
+ get_footer_string_startup=False,
143
+ unicode_char_width: bool = True,
144
+
145
+ colours_start: int =0,
146
+ colours_end: int =-1,
147
+ reset_colours: bool = True,
148
+ key_remappings: dict = {},
149
+ keys_dict:dict = picker_keys,
150
+ display_infobox : bool = False,
151
+ infobox_items: list[list[str]] = [],
152
+ infobox_title: str = "",
153
+ display_only: bool = False,
154
+
155
+ editable_columns: list[int] = [],
156
+ editable_by_default: bool = True,
157
+
158
+ centre_in_terminal: bool = False,
159
+ centre_in_terminal_vertical: bool = False,
160
+ centre_in_cols: bool = False,
161
+
162
+ startup_notification:str = "",
163
+
164
+ leftmost_column: int = 0,
165
+ leftmost_char: int = 0,
166
+
167
+
168
+ history_filter_and_search: list[str] = [],
169
+ history_opts: list[str] = [],
170
+ history_settings: list[str] = [],
171
+ history_edits: list[str] = [],
172
+ history_pipes: list[str] = [],
173
+ debug: bool = False,
174
+ debug_level: int = 1,
175
+
176
+ ):
177
+ self.stdscr = stdscr
178
+ self.items = items
179
+ self.cursor_pos = cursor_pos
180
+ self.colours = colours
181
+ self.colour_theme_number = colour_theme_number
182
+ self.max_selected = max_selected
183
+ self.top_gap = top_gap
184
+ self.title = title
185
+ self.header = header
186
+ self.max_column_width = max_column_width
187
+ self.clear_on_start = clear_on_start
188
+
189
+ self.auto_refresh = auto_refresh
190
+ self.timer = timer
191
+
192
+ self.get_new_data = get_new_data
193
+ self.refresh_function = refresh_function
194
+ self.get_data_startup = get_data_startup
195
+ self.track_entries_upon_refresh = track_entries_upon_refresh
196
+ self.pin_cursor = pin_cursor
197
+ self.id_column = id_column
198
+
199
+ self.unselectable_indices = unselectable_indices
200
+ self.highlights = highlights
201
+ self.highlights_hide = highlights_hide
202
+ self.number_columns = number_columns
203
+ self.column_widths, = [],
204
+ self.column_indices, = [],
205
+
206
+
207
+ self.current_row = current_row
208
+ self.current_page = current_page
209
+ self.is_selecting = is_selecting
210
+ self.is_deselecting = is_deselecting
211
+ self.start_selection = start_selection
212
+ self.start_selection_col = start_selection_col
213
+ self.end_selection = end_selection
214
+ self.user_opts = user_opts
215
+ self.options_list = options_list
216
+ self.user_settings = user_settings
217
+ self.separator = separator
218
+ self.search_query = search_query
219
+ self.search_count = search_count
220
+ self.search_index = search_index
221
+ self.filter_query = filter_query
222
+ self.hidden_columns = hidden_columns
223
+ self.indexed_items = indexed_items
224
+ self.scroll_bar = scroll_bar
225
+
226
+ self.selections = selections
227
+ self.cell_selections = cell_selections
228
+ self.highlight_full_row = highlight_full_row
229
+ self.cell_cursor = cell_cursor
230
+
231
+ self.items_per_page = items_per_page
232
+ self.sort_method = sort_method
233
+ self.sort_reverse = sort_reverse
234
+ self.selected_column = selected_column
235
+ self.sort_column = sort_column
236
+ self.columns_sort_method = columns_sort_method
237
+ self.key_chain = key_chain
238
+ self.last_key = last_key
239
+
240
+ self.paginate = paginate
241
+ self.cancel_is_back = cancel_is_back
242
+ self.mode_index = mode_index
243
+ self.modes = modes
244
+ self.display_modes = display_modes
245
+ self.require_option = require_option
246
+ self.require_option_default = require_option_default
247
+ self.option_functions = option_functions
248
+ self.default_option_function = default_option_function
249
+ self.disabled_keys = disabled_keys
250
+
251
+ self.show_header = show_header
252
+ self.show_row_header = show_row_header
253
+ self.show_footer = show_footer
254
+ self.footer_style = footer_style
255
+ self.footer_string = footer_string
256
+ self.footer_string_auto_refresh = footer_string_auto_refresh
257
+ self.footer_string_refresh_function = footer_string_refresh_function
258
+ self.footer_timer = footer_timer
259
+ self.get_footer_string_startup = get_footer_string_startup,
260
+ self.unicode_char_width = unicode_char_width
261
+
262
+
263
+ self.colours_start = colours_start
264
+ self.colours_end = colours_end
265
+ self.reset_colours = reset_colours
266
+ self.key_remappings = key_remappings
267
+ self.keys_dict = keys_dict
268
+ self.display_infobox = display_infobox
269
+ self.infobox_items = infobox_items
270
+ self.infobox_title = infobox_title
271
+ self.display_only = display_only
272
+
273
+ self.editable_columns = editable_columns
274
+ self.editable_by_default = editable_by_default
275
+
276
+ self.centre_in_terminal = centre_in_terminal
277
+ self.centre_in_terminal_vertical = centre_in_terminal_vertical
278
+ self.centre_in_cols = centre_in_cols
279
+
280
+ self.startup_notification = startup_notification
281
+
282
+
283
+ self.registers = {}
284
+
285
+ self.SORT_METHODS = SORT_METHODS
286
+ self.command_stack = []
287
+ self.leftmost_column = leftmost_column
288
+ self.leftmost_char = leftmost_char
289
+
290
+
291
+ # Refresh function variables
292
+ self.data_refreshed = False
293
+ self.refreshing_data = False
294
+ self.data_lock = threading.Lock()
295
+ self.data_ready = False
296
+ self.cursor_pos_id = 0
297
+ self.cursor_pos_prev = 0
298
+ self.ids = []
299
+ self.ids_tuples = []
300
+ self.selected_cells_by_row = {}
301
+
302
+ # History variables
303
+ self.history_filter_and_search = history_filter_and_search
304
+ self.history_pipes = history_pipes
305
+ self.history_opts = history_opts
306
+ self.history_settings = history_settings
307
+ self.history_edits = history_edits
308
+
309
+
310
+
311
+
312
+ self.debug = debug
313
+ self.debug_level = debug_level
314
+
315
+
316
+ self.initialise_picker_state(reset_colours=self.reset_colours)
317
+
318
+
319
+ # Note: We have to set the footer after initialising the picker state so that the footer can use the get_function_data method
320
+ # self.footer_options = [StandardFooter(self.stdscr, colours_start, self.get_function_data), CompactFooter(self.stdscr, colours_start, self.get_function_data), NoFooter(self.stdscr, colours_start, self.get_function_data)]
321
+ self.footer_options = []
322
+ # self.footer = self.footer_options[self.footer_style]
323
+
324
+ self.footer = CompactFooter(self.stdscr, colours_start, self.get_function_data)
325
+
326
+
327
+
328
+ def calculate_section_sizes(self):
329
+ """
330
+ Calculte the following for the Picker:
331
+ self.items_per_page: the number of entry rows displayed
332
+ self.bottom_space: the size of the footer + the bottom buffer space
333
+ self.top_space: the size of the space at the top of the picker: title + modes + header + top_gap
334
+ """
335
+
336
+ self.logger.debug(f"function: calculate_section_sizes()")
337
+
338
+ # self.bottom_space
339
+ self.bottom_space = self.footer.height if self.show_footer else 0
340
+
341
+ ## self.top_space
342
+ h, w = self.stdscr.getmaxyx()
343
+ self.top_space = self.top_gap
344
+ if self.title: self.top_space+=1
345
+ if self.modes and self.display_modes: self.top_space+=1
346
+ if self.header and self.show_header: self.top_space += 1
347
+
348
+ # self.items_per_page
349
+ self.items_per_page = h - self.top_space - self.bottom_space
350
+ if not self.show_footer and self.footer_string: self.items_per_page-=1
351
+ self.items_per_page = min(h-self.top_space-1, self.items_per_page)
352
+
353
+
354
+ # Adjust top space if centring vertically and we have fewer rows than terminal lines
355
+ if self.centre_in_terminal_vertical and len(self.indexed_items) < self.items_per_page:
356
+ self.top_space += ((h-(self.top_space+self.bottom_space))-len(self.indexed_items))//2
357
+
358
+ # self.column_widths
359
+ visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
360
+ visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
361
+
362
+ # self.startx
363
+ self.startx = 1 if self.highlight_full_row else 2
364
+ if self.show_row_header: self.startx += len(str(len(self.items))) + 2
365
+ if visible_columns_total_width < w and self.centre_in_terminal:
366
+ self.startx += (w - visible_columns_total_width) // 2
367
+
368
+ def get_visible_rows(self) -> list[list[str]]:
369
+
370
+ self.logger.debug(f"function: get_visible_rows()")
371
+ ## Scroll with column select
372
+ if self.paginate:
373
+ start_index = (self.cursor_pos//self.items_per_page) * self.items_per_page
374
+ end_index = min(start_index + self.items_per_page, len(self.indexed_items))
375
+ ## Scroll
376
+ else:
377
+ scrolloff = self.items_per_page//2
378
+ start_index = max(0, min(self.cursor_pos - (self.items_per_page-scrolloff), len(self.indexed_items)-self.items_per_page))
379
+ end_index = min(start_index + self.items_per_page, len(self.indexed_items))
380
+ if len(self.indexed_items) == 0: start_index, end_index = 0, 0
381
+
382
+ rows = [v[1] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else self.items
383
+ return rows
384
+
385
+ def initialise_picker_state(self, reset_colours=False) -> None:
386
+ """ Initialise state variables for the picker. These are: debugging and colours. """
387
+
388
+ if curses.has_colors() and self.colours != None:
389
+ # raise Exception("Terminal does not support color")
390
+ curses.start_color()
391
+ if reset_colours:
392
+ global COLOURS_SET
393
+ COLOURS_SET = False
394
+ colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
395
+ if curses.COLORS >= 255 and curses.COLOR_PAIRS >= 150:
396
+ self.colours_start = self.colours_start
397
+ self.notification_colours_start = self.colours_start+50
398
+ self.help_colours_start = self.colours_start+100
399
+ else:
400
+ self.colours_start = 0
401
+ self.notification_colours_start = 0
402
+ self.help_colours_start = 0
403
+ else:
404
+ self.colours_start = 0
405
+ self.notification_colours_start = 0
406
+ self.help_colours_start = 0
407
+
408
+
409
+ debug_levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]
410
+ dbglvl = debug_levels[self.debug_level]
411
+ self.logger = setup_logger(name="picker_log", log_file="picker.log", log_enabled=self.debug, level =dbglvl)
412
+ self.logger.info(f"Initialiasing Picker.")
413
+ # self.notification(self.stdscr, message=repr(self.logger))
414
+
415
+
416
+ # 1 2 3 4 5
417
+ # logger = logging.getLogger(__file__)
418
+ # if self.debug_level == 0:
419
+ # logger = logging.getLogger()
420
+ # logger.disabled = True
421
+ # else:
422
+ #
423
+ # file_handler = logging.FileHandler(f"{self.title}.log", mode='w')
424
+ # formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', '%m-%d-%Y %H:%M:%S')
425
+ # file_handler.setFormatter(formatter)
426
+ # logger.addHandler(file_handler)
427
+ # logger.setLevel(debug_levels[self.debug_level-1])
428
+
429
+ # logging.basicConfig(
430
+ # level=debug_levels[self.debug_level-1],
431
+ # format='%(asctime)s - %(levelname)s - %(message)s',
432
+ # datefmt='%m-%d-%Y %H:%M:%S',
433
+ # filename=f"{self.title}.log",
434
+ # filemode="w",
435
+ # )
436
+ #
437
+ # self.logger.info(f"Starging log. Log level {logger.getEffectiveLevel()}")
438
+ # self.logger.info(f"Starging log. Log level {repr(debug_levels)}, {self.debug_level}, {debug_levels[self.debug_level-1]}")
439
+ # self.notification(self.stdscr, f"Starging log. Log level {repr(debug_levels)}, {self.debug_level}, {debug_levels[self.debug_level-1]}")
440
+ # self.notification(self.stdscr, f"{__file__}")
441
+
442
+ ## Logging level plan
443
+ # DEBUG: loop functions, draw screen, etc.
444
+ # INFO: main functions
445
+ # WARNING: any try-except fails
446
+
447
+ # No set_escdelay function on windows.
448
+ try:
449
+ curses.set_escdelay(25)
450
+ except:
451
+ logging.warning("Error trying to set curses.set_escdelay")
452
+
453
+ # self.stdscr.clear()
454
+ # self.stdscr.refresh()
455
+ # self.draw_screen(self.indexed_items, self.highlights)
456
+
457
+
458
+
459
+ def initialise_variables(self, get_data: bool = False) -> None:
460
+ """ Initialise the variables that keep track of the data. """
461
+
462
+ self.logger.info(f"function: initialise_variables()")
463
+
464
+ tracking = False
465
+
466
+ ## Get data synchronously
467
+ if get_data and self.refresh_function != None:
468
+ if self.track_entries_upon_refresh and len(self.items) > 0:
469
+ tracking = True
470
+ selected_indices = get_selected_indices(self.selections)
471
+ self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
472
+ self.ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
473
+ self.ids_tuples = [(i, item[self.id_column]) for i, item in enumerate(self.items) if i in selected_indices]
474
+
475
+ if len(self.indexed_items) > 0 and len(self.indexed_items) >= self.cursor_pos and len(self.indexed_items[0][1]) >= self.id_column:
476
+ self.cursor_pos_id = self.indexed_items[self.cursor_pos][1][self.id_column]
477
+
478
+ self.items, self.header = self.refresh_function()
479
+
480
+
481
+ if self.items == []: self.items = [[]]
482
+ ## Ensure that items is a List[List[Str]] object
483
+ if len(self.items) > 0 and not isinstance(self.items[0], list):
484
+ self.items = [[item] for item in self.items]
485
+ self.items = [[str(cell) for cell in row] for row in self.items]
486
+
487
+
488
+ # Ensure that header is of the same length as the rows
489
+ if self.header and len(self.items) > 0 and len(self.header) != len(self.items[0]):
490
+ self.header = [str(self.header[i]) if i < len(self.header) else "" for i in range(len(self.items[0]))]
491
+
492
+ # Constants
493
+ # DEFAULT_ITEMS_PER_PAGE = os.get_terminal_size().lines - top_gap*2-2-int(bool(header))
494
+
495
+ self.calculate_section_sizes()
496
+
497
+ # Initial states
498
+ if len(self.selections) != len(self.items):
499
+ self.selections = {i : False if i not in self.selections else bool(self.selections[i]) for i in range(len(self.items))}
500
+
501
+ if len(self.items) and len(self.cell_selections) != len(self.items)*len(self.items[0]):
502
+ self.cell_selections = {(i, j) : False if (i, j) not in self.cell_selections else self.cell_selections[(i, j)] for i in range(len(self.items)) for j in range(len(self.items[0]))}
503
+ elif len(self.items) == 0:
504
+ self.cell_selections = {}
505
+
506
+ if len(self.require_option) < len(self.items):
507
+ self.require_option += [self.require_option_default for i in range(len(self.items)-len(self.require_option))]
508
+ if len(self.option_functions) < len(self.items):
509
+ self.option_functions += [self.default_option_function for i in range(len(self.items)-len(self.option_functions))]
510
+ if len(self.items)>0 and len(self.columns_sort_method) < len(self.items[0]):
511
+ self.columns_sort_method = self.columns_sort_method + [0 for i in range(len(self.items[0])-len(self.columns_sort_method))]
512
+ if len(self.items)>0 and len(self.sort_reverse) < len(self.items[0]):
513
+ self.sort_reverse = self.sort_reverse + [False for i in range(len(self.items[0])-len(self.sort_reverse))]
514
+ if len(self.items)>0 and len(self.editable_columns) < len(self.items[0]):
515
+ self.editable_columns = self.editable_columns + [self.editable_by_default for i in range(len(self.items[0])-len(self.editable_columns))]
516
+ if len(self.items)>0 and len(self.column_indices) < len(self.items[0]):
517
+ self.column_indices = self.column_indices + [i for i in range(len(self.column_indices), len(self.items[0]))]
518
+
519
+
520
+
521
+ # items2 = [[row[self.column_indices[i]] for i in range(len(row))] for row in self.items]
522
+ # self.indexed_items = list(enumerate(items2))
523
+ if self.items == [[]]: self.indexed_items = []
524
+ else: self.indexed_items = list(enumerate(self.items))
525
+
526
+ # If a filter is passed then refilter
527
+ if self.filter_query:
528
+ # prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
529
+ # prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
530
+ self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
531
+ 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)
532
+ else: self.cursor_pos = 0
533
+ if self.search_query:
534
+ return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
535
+ query=self.search_query,
536
+ indexed_items=self.indexed_items,
537
+ highlights=self.highlights,
538
+ cursor_pos=self.cursor_pos,
539
+ unselectable_indices=self.unselectable_indices,
540
+ continue_search=True,
541
+ )
542
+ if return_val:
543
+ self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
544
+ # If a sort is passed
545
+ if len(self.indexed_items) > 0:
546
+ 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
547
+ # if len(self.items[0]) == 1:
548
+ # self.number_columns = False
549
+
550
+
551
+
552
+ h, w = self.stdscr.getmaxyx()
553
+
554
+ # Adjust variables to ensure correctness if errors
555
+ ## Move to a selectable row (if applicable)
556
+ if len(self.items) <= len(self.unselectable_indices): self.unselectable_indices = []
557
+ new_pos = (self.cursor_pos)%len(self.items)
558
+ while new_pos in self.unselectable_indices and new_pos != self.cursor_pos:
559
+ new_pos = (new_pos + 1) % len(self.items)
560
+
561
+ assert new_pos < len(self.items)
562
+ self.cursor_pos = new_pos
563
+
564
+
565
+ # if tracking and len(self.items) > 1:
566
+ # Ensure that selected indices are tracked upon data refresh
567
+ if self.track_entries_upon_refresh and (self.data_ready or tracking) and len(self.items) > 1:
568
+ selected_indices = []
569
+ all_ids = [item[self.id_column] for item in self.items]
570
+ self.selections = {i: False for i in range(len(self.items))}
571
+ if len(self.items) > 0:
572
+ self.cell_selections = {(i, j): False for i in range(len(self.items)) for j in range(len(self.items[0]))}
573
+ else:
574
+ self.cell_selections = {}
575
+
576
+ for id in self.ids:
577
+ if id in all_ids:
578
+ selected_indices.append(all_ids.index(id))
579
+ self.selections[all_ids.index(id)] = True
580
+
581
+ for i, id in self.ids_tuples:
582
+ if id in all_ids:
583
+ # rows_with_selected_cells
584
+ for j in self.selected_cells_by_row[i]:
585
+ self.cell_selections[(all_ids.index(id), j)] = True
586
+
587
+
588
+
589
+ if len(self.indexed_items):
590
+ if self.pin_cursor:
591
+ self.cursor_pos = min(self.cursor_pos_prev, len(self.indexed_items)-1)
592
+ else:
593
+ if self.cursor_pos_id in all_ids:
594
+ cursor_pos_x = all_ids.index(self.cursor_pos_id)
595
+ if cursor_pos_x in [i[0] for i in self.indexed_items]:
596
+ self.cursor_pos = [i[0] for i in self.indexed_items].index(cursor_pos_x)
597
+ else:
598
+ self.cursor_pos = 0
599
+
600
+ # if self.display_infobox:
601
+ # self.infobox_picker = self.infobox(self.stdscr, self.infobox_items, self.infobox_title)
602
+
603
+
604
+ def move_column(self, direction: int) -> None:
605
+ """
606
+ Cycles the column $direction places.
607
+ E.g., If $direction == -1 and the sort column is 3, then column 3 will swap with column 2
608
+ in each of the rows in $items and 2 will become the new sort column.
609
+
610
+ sort_column = 3, direction = -1
611
+ [[0,1,2,*3*,4],
612
+ [5,6,7,*8*,9]]
613
+ -->
614
+ [[0,1,*3*,2,4],
615
+ [5,6,*8*,7,9]]
616
+
617
+ returns:
618
+ adjusted items, header, sort_column and column_widths
619
+ """
620
+ self.logger.info(f"function: move_column(direction={direction})")
621
+ if len(self.items) < 1: return None
622
+ if (self.selected_column+direction) < 0 or (self.selected_column+direction) >= len(self.items[0]): return None
623
+
624
+ new_index = self.selected_column + direction
625
+
626
+ # Swap columns in each row
627
+ for row in self.items:
628
+ row[self.selected_column], row[new_index] = row[new_index], row[self.selected_column]
629
+ if self.header:
630
+ self.header[self.selected_column], self.header[new_index] = self.header[new_index], self.header[self.selected_column]
631
+
632
+ # Swap column widths
633
+ self.column_widths[self.selected_column], self.column_widths[new_index] = self.column_widths[new_index], self.column_widths[self.selected_column]
634
+
635
+ # Update current column index
636
+ self.selected_column = new_index
637
+
638
+ def test_screen_size(self):
639
+ self.logger.debug("function: test_screen_size()")
640
+ h, w = self.stdscr.getmaxyx()
641
+ ## Terminal too small to display Picker
642
+ if h<3 or w<len("Terminal"): return False
643
+ if (self.show_footer or self.footer_string) and (h<12 or w<35) or (h<12 and w<10):
644
+ self.stdscr.addstr(h//2-1, (w-len("Terminal"))//2, "Terminal")
645
+ self.stdscr.addstr(h//2, (w-len("Too"))//2, "Too")
646
+ self.stdscr.addstr(h//2+1, (w-len("Small"))//2, "Small")
647
+ return False
648
+ return True
649
+
650
+ def splash_screen(self, message=""):
651
+ self.logger.info(f"function: splash_screen({message})")
652
+ """ Display a splash screen with a message. Useful when loading a large data set. """
653
+ h, w =self.stdscr.getmaxyx()
654
+ self.stdscr.bkgd(' ', curses.color_pair(2))
655
+ try:
656
+ self.stdscr.addstr(h//2, (w-len(message))//2, message, curses.color_pair(2))
657
+ except:
658
+ pass
659
+ self.stdscr.refresh()
660
+
661
+ def draw_screen_wr(self, indexed_items: list[Tuple[int, list[str]]], highlights: list[dict] = [{}], clear: bool = True) -> None:
662
+ """ Try-except wrapper for the draw_screen_() method to prevent crashes when rapidly resizing the terminal. """
663
+ try:
664
+ self.draw_screen(self.indexed_items, self.highlights, clear=clear)
665
+ except:
666
+ self.logger.warning(f"draw_screen function error")
667
+
668
+ def draw_screen(self, indexed_items: list[Tuple[int, list[str]]], highlights: list[dict] = [{}], clear: bool = True) -> None:
669
+ """ Draw Picker screen. """
670
+ self.logger.debug("Draw screen.")
671
+
672
+ if clear:
673
+ self.stdscr.erase()
674
+
675
+ h, w = self.stdscr.getmaxyx()
676
+
677
+ # Test if the terminal is of a sufficient size to display the picker
678
+ if not self.test_screen_size(): return None
679
+
680
+ # Determine which rows are to be displayed on the current screen
681
+ ## Paginate
682
+ if self.paginate:
683
+ start_index = (self.cursor_pos//self.items_per_page) * self.items_per_page
684
+ end_index = min(start_index + self.items_per_page, len(self.indexed_items))
685
+ ## Scroll
686
+ else:
687
+ scrolloff = self.items_per_page//2
688
+ start_index = max(0, min(self.cursor_pos - (self.items_per_page-scrolloff), len(self.indexed_items)-self.items_per_page))
689
+ end_index = min(start_index + self.items_per_page, len(self.indexed_items))
690
+ if len(self.indexed_items) == 0: start_index, end_index = 0, 0
691
+
692
+ # self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w)
693
+ # Determine widths based only on the currently indexed rows
694
+ # rows = [v[1] for v in self.indexed_items] if len(self.indexed_items) else self.items
695
+ # Determine widths based only on the currently displayed indexed rows
696
+ rows = [v[1] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else self.items
697
+ self.column_widths = get_column_widths(rows, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w, unicode_char_width=self.unicode_char_width)
698
+ visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
699
+ visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
700
+
701
+ # Determine the number of items_per_page, top_size and bottom_size
702
+ self.calculate_section_sizes()
703
+
704
+ # top_space = self.top_gap
705
+
706
+ ## Display title (if applicable)
707
+ if self.title:
708
+ padded_title = f" {self.title.strip()} "
709
+ self.stdscr.addstr(self.top_gap, 0, f"{' ':^{w}}", curses.color_pair(self.colours_start+16))
710
+ title_x = (w-wcswidth(padded_title))//2
711
+ # title = f"{title:^{w}}"
712
+ self.stdscr.addstr(self.top_gap, title_x, padded_title, curses.color_pair(self.colours_start+16) | curses.A_BOLD)
713
+ # top_space += 1
714
+
715
+ ## Display modes
716
+ if self.display_modes and self.modes not in [[{}], []]:
717
+ self.stdscr.addstr(self.top_gap+1, 0, ' '*w, curses.A_REVERSE)
718
+ modes_list = [f"{mode['name']}" if 'name' in mode else f"{i}. " for i, mode in enumerate(self.modes)]
719
+ # mode_colours = [mode["colour"] for mode ]
720
+ mode_widths = get_mode_widths(modes_list)
721
+ split_space = (w-sum(mode_widths))//len(self.modes)
722
+ xmode = 0
723
+ for i, mode in enumerate(modes_list):
724
+ if i == len(modes_list)-1:
725
+ mode_str = f"{mode:^{mode_widths[i]+split_space+(w-sum(mode_widths))%len(self.modes)}}"
726
+ else:
727
+ mode_str = f"{mode:^{mode_widths[i]+split_space}}"
728
+ # current mode
729
+ if i == self.mode_index:
730
+ self.stdscr.addstr(self.top_gap+1, xmode, mode_str, curses.color_pair(self.colours_start+14) | curses.A_BOLD)
731
+ # other modes
732
+ else:
733
+ self.stdscr.addstr(self.top_gap+1, xmode, mode_str, curses.color_pair(self.colours_start+15) | curses.A_UNDERLINE)
734
+ xmode += split_space+mode_widths[i]
735
+ # top_space += 1
736
+
737
+ ## Display header
738
+ if self.header and self.show_header:
739
+ header_str = ""
740
+ up_to_selected_col = ""
741
+ selected_col_str = ""
742
+ for i in range(len(self.header)):
743
+ if i == self.selected_column: up_to_selected_col = header_str
744
+ if i in self.hidden_columns: continue
745
+ number = f"{i}. " if self.number_columns else ""
746
+ # number = f"{intStringToExponentString(str(i))}. " if self.number_columns else ""
747
+ header_str += number
748
+ header_str += f"{self.header[i]:^{self.column_widths[i]-len(number)}}"
749
+ header_str += self.separator
750
+
751
+ header_str = header_str[self.leftmost_char:]
752
+ header_ypos = self.top_gap + bool(self.title) + bool(self.display_modes and self.modes)
753
+ self.stdscr.addstr(header_ypos, 0, ' '*w, curses.color_pair(self.colours_start+4) | curses.A_BOLD)
754
+ self.stdscr.addstr(header_ypos, self.startx, header_str[:min(w-self.startx, visible_columns_total_width+1)], curses.color_pair(self.colours_start+4) | curses.A_BOLD)
755
+
756
+ # Highlight sort column
757
+ try:
758
+ if self.selected_column != None and self.selected_column not in self.hidden_columns:
759
+ if len(self.header) > 1 and (len(up_to_selected_col)-self.leftmost_char) < w:
760
+ # if len(up_to_selected_col) + 1 < w or True:
761
+ # if self.startx + len(up_to_selected_col) - self.leftmost_char > 0 or True:
762
+ number = f"{self.selected_column}. " if self.number_columns else ""
763
+ # number = f"{intStringToExponentString(self.selected_column)}. " if self.number_columns else ""
764
+ # self.startx + len(up_to_selected_col) - self.leftmost_char
765
+ highlighed_col_startx = max(self.startx, self.startx + len(up_to_selected_col) - self.leftmost_char)
766
+ highlighted_col_str = (number+f"{self.header[self.selected_column]:^{self.column_widths[self.selected_column]-len(number)}}") + self.separator
767
+ end_of_highlighted_col_str = w-(highlighed_col_startx+len(highlighted_col_str)) if (highlighed_col_startx+len(highlighted_col_str)) > w else len(highlighted_col_str)
768
+ start_of_highlighted_col_str = max(self.leftmost_char - len(up_to_selected_col), 0)
769
+ self.stdscr.addstr(header_ypos, highlighed_col_startx , highlighted_col_str[start_of_highlighted_col_str:end_of_highlighted_col_str], curses.color_pair(self.colours_start+19) | curses.A_BOLD)
770
+ except:
771
+ pass
772
+
773
+ # Display row header
774
+ if self.show_row_header:
775
+ for idx in range(start_index, end_index):
776
+ y = idx - start_index + self.top_space
777
+ if idx == self.cursor_pos:
778
+ self.stdscr.addstr(y, 0, f" {self.indexed_items[idx][0]} ", curses.color_pair(self.colours_start+19) | curses.A_BOLD)
779
+ else:
780
+ self.stdscr.addstr(y, 0, f" {self.indexed_items[idx][0]} ", curses.color_pair(self.colours_start+4) | curses.A_BOLD)
781
+
782
+ def highlight_cell(row: int, col:int, visible_column_widths, colour_pair_number: int = 5):
783
+ cell_pos = sum(visible_column_widths[:col])+col*len(self.separator)-self.leftmost_char + self.startx
784
+ # cell_width = self.column_widths[self.selected_column]
785
+ cell_width = visible_column_widths[col] + len(self.separator)
786
+ cell_max_width = w-cell_pos
787
+ try:
788
+ # Start of cell is on screen
789
+ if self.startx <= cell_pos <= w:
790
+ self.stdscr.addstr(y, cell_pos, (' '*cell_width)[:cell_max_width], curses.color_pair(self.colours_start+colour_pair_number))
791
+ if self.centre_in_cols:
792
+ cell_value = f"{self.indexed_items[row][1][col]:^{cell_width-len(self.separator)}}" + self.separator
793
+ else:
794
+ cell_value = self.indexed_items[row][1][col] + self.separator
795
+ # cell_value = cell_value[:min(cell_width, cell_max_width)-len(self.separator)]
796
+ cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width)-len(self.separator), self.unicode_char_width)
797
+ cell_value = cell_value + self.separator
798
+ # cell_value = cell_value
799
+ cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.unicode_char_width)
800
+ # row_str = truncate_to_display_width(row_str_left_adj, min(w-self.startx, visible_columns_total_width))
801
+ self.stdscr.addstr(y, cell_pos, cell_value, curses.color_pair(self.colours_start+colour_pair_number) | curses.A_BOLD)
802
+ # Part of the cell is on screen
803
+ elif self.startx <= cell_pos+cell_width <= w:
804
+ cell_start = self.startx - cell_pos
805
+ self.stdscr.addstr(y, self.startx, ' '*(cell_width-cell_start), curses.color_pair(self.colours_start+colour_pair_number))
806
+ cell_value = self.indexed_items[row][1][col][cell_start:visible_column_widths[col]]
807
+ self.stdscr.addstr(y, self.startx, cell_value, curses.color_pair(self.colours_start+colour_pair_number) | curses.A_BOLD)
808
+ except:
809
+ pass
810
+
811
+ # Draw:
812
+ # 1. standard row
813
+ # 2. highlights l0
814
+ # 3. selected
815
+ # 4. above-selected highlights l1
816
+ # 5. cursor
817
+ # 6. top-level highlights l2
818
+ ## Display rows and highlights
819
+
820
+ def sort_highlights(highlights):
821
+ """
822
+ Sort highlights into lists based on their display level.
823
+ Highlights with no level defined will be displayed at level 0.
824
+ """
825
+ l0 = []
826
+ l1 = []
827
+ l2 = []
828
+ for highlight in highlights:
829
+ if "level" in highlight:
830
+ if highlight["level"] == 0: l0.append(highlight)
831
+ elif highlight["level"] == 1: l1.append(highlight)
832
+ elif highlight["level"] == 2: l2.append(highlight)
833
+ else: l0.append(highlight)
834
+ else:
835
+ l0.append(highlight)
836
+ return l0, l1, l2
837
+
838
+ def draw_highlights(highlights: list[dict], idx: int, y: int, item: tuple[int, list[str]]):
839
+ self.logger.debug(f"function: draw_highlights()")
840
+ if len(highlights) == 0: return None
841
+ full_row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
842
+ row_str = full_row_str[self.leftmost_char:]
843
+ for highlight in highlights:
844
+ if "row" in highlight:
845
+ if highlight["row"] != self.indexed_items[idx][0]:
846
+ continue
847
+ try:
848
+ if highlight["field"] == "all":
849
+ match = re.search(highlight["match"], full_row_str, re.IGNORECASE)
850
+ if not match: continue
851
+ highlight_start = match.start()
852
+ highlight_end = match.end()
853
+ if highlight_end - self.leftmost_char < 0:
854
+ continue
855
+
856
+ elif type(highlight["field"]) == type(0) and highlight["field"] not in self.hidden_columns:
857
+ match = re.search(highlight["match"], truncate_to_display_width(item[1][highlight["field"]], self.column_widths[highlight["field"]], centre=False, unicode_char_width=self.unicode_char_width), re.IGNORECASE)
858
+ if not match: continue
859
+ 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)
860
+
861
+ ## We want to search the non-centred values but highlight the centred values.
862
+ if self.centre_in_cols:
863
+ tmp = truncate_to_display_width(item[1][highlight["field"]], self.column_widths[highlight["field"]], self.centre_in_cols, self.unicode_char_width)
864
+ field_start += (len(tmp) - len(tmp.lstrip()))
865
+
866
+ highlight_start = field_start + match.start()
867
+ highlight_end = match.end() + field_start
868
+ if highlight_end - self.leftmost_char < 0:
869
+ continue
870
+ else:
871
+ continue
872
+ highlight_start -= self.leftmost_char
873
+ highlight_end -= self.leftmost_char
874
+ self.stdscr.addstr(y, max(self.startx, self.startx+highlight_start), row_str[max(highlight_start,0):min(w-self.startx, highlight_end)], curses.color_pair(self.colours_start+highlight["color"]) | curses.A_BOLD)
875
+ except:
876
+ pass
877
+
878
+ l0_highlights, l1_highlights, l2_highlights = sort_highlights(self.highlights)
879
+
880
+
881
+ for idx in range(start_index, end_index):
882
+ item = self.indexed_items[idx]
883
+ y = idx - start_index + self.top_space
884
+
885
+ # row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)[self.leftmost_char:]
886
+ # row_str = truncate_to_display_width(row_str, min(w-self.startx, visible_columns_total_width))
887
+ row_str_orig = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
888
+ row_str_left_adj = clip_left(row_str_orig, self.leftmost_char)
889
+ row_str = truncate_to_display_width(row_str_left_adj, min(w-self.startx, visible_columns_total_width), self.unicode_char_width)
890
+ # row_str = truncate_to_display_width(row_str, min(w-self.startx, visible_columns_total_width))[self.leftmost_char:]
891
+
892
+ ## Display the standard row
893
+ self.stdscr.addstr(y, self.startx, row_str[:min(w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+2))
894
+
895
+
896
+ # Draw the level 0 highlights
897
+ if not self.highlights_hide:
898
+ draw_highlights(l0_highlights, idx, y, item)
899
+
900
+ # Higlight cursor cell and selected cells
901
+ if self.cell_cursor:
902
+ # self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
903
+ if item[0] in self.selected_cells_by_row:
904
+ for j in self.selected_cells_by_row[item[0]]:
905
+ highlight_cell(idx, j, visible_column_widths, colour_pair_number=25)
906
+
907
+ # Visually selected
908
+ if self.is_selecting:
909
+ if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
910
+ x_interval = range(min(self.start_selection_col, self.selected_column), max(self.start_selection_col, self.selected_column)+1)
911
+ for col in x_interval:
912
+ highlight_cell(idx, col, visible_column_widths, colour_pair_number=25)
913
+
914
+ # Visually deslected
915
+ if self.is_deselecting:
916
+ if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
917
+ x_interval = range(min(self.start_selection_col, self.selected_column), max(self.start_selection_col, self.selected_column)+1)
918
+ for col in x_interval:
919
+ highlight_cell(idx, col, visible_column_widths, colour_pair_number=26)
920
+ # Higlight cursor row and selected rows
921
+ elif self.highlight_full_row:
922
+ if self.selections[item[0]]:
923
+ self.stdscr.addstr(y, self.startx, row_str[:min(w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+25) | curses.A_BOLD)
924
+ # Visually selected
925
+ if self.is_selecting:
926
+ if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
927
+ self.stdscr.addstr(y, self.startx, row_str[:min(w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+25))
928
+ # Visually deslected
929
+ elif self.is_deselecting:
930
+ if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
931
+ self.stdscr.addstr(y, self.startx, row_str[:min(w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+26))
932
+
933
+ # Highlight the cursor row and the first char of the selected rows.
934
+ else:
935
+ if self.selections[item[0]]:
936
+ self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
937
+ # Visually selected
938
+ if self.is_selecting:
939
+ if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
940
+ self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
941
+ # Visually deslected
942
+ if self.is_deselecting:
943
+ if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
944
+ self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+10))
945
+
946
+ if not self.highlights_hide:
947
+ draw_highlights(l1_highlights, idx, y, item)
948
+
949
+ # Draw cursor
950
+ if idx == self.cursor_pos:
951
+ if self.cell_cursor:
952
+ highlight_cell(idx, self.selected_column, visible_column_widths)
953
+ else:
954
+ self.stdscr.addstr(y, self.startx, row_str[:min(w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+5) | curses.A_BOLD)
955
+
956
+ if not self.highlights_hide:
957
+ draw_highlights(l2_highlights, idx, y, item)
958
+
959
+ ## Display scrollbar
960
+ if self.scroll_bar and len(self.indexed_items) and len(self.indexed_items) > (self.items_per_page):
961
+ scroll_bar_length = int(self.items_per_page*self.items_per_page/len(self.indexed_items))
962
+ if self.cursor_pos <= self.items_per_page//2:
963
+ scroll_bar_start=self.top_space
964
+ elif self.cursor_pos + self.items_per_page//2 >= len(self.indexed_items):
965
+ scroll_bar_start = h - int(bool(self.show_footer))*self.footer.height - scroll_bar_length
966
+ else:
967
+ scroll_bar_start = int(((self.cursor_pos)/len(self.indexed_items))*self.items_per_page)+self.top_space - scroll_bar_length//2
968
+ scroll_bar_start = min(scroll_bar_start, h-self.top_space-1)
969
+ scroll_bar_length = min(scroll_bar_length, h - scroll_bar_start-1)
970
+ scroll_bar_length = max(1, scroll_bar_length)
971
+ for i in range(scroll_bar_length):
972
+ v = max(self.top_space+int(bool(self.header)), scroll_bar_start-scroll_bar_length//2)
973
+ self.stdscr.addstr(scroll_bar_start+i, w-1, ' ', curses.color_pair(self.colours_start+18))
974
+
975
+ # Display refresh symbol
976
+ if self.auto_refresh:
977
+ if self.refreshing_data:
978
+ self.stdscr.addstr(0,w-3,"  ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
979
+ else:
980
+ self.stdscr.addstr(0,w-3,"  ", curses.color_pair(self.colours_start+23) | curses.A_BOLD)
981
+
982
+ # self.stdscr.refresh()
983
+
984
+ ## Display footer
985
+ if self.show_footer:
986
+ # self.footer = NoFooter(self.stdscr, self.colours_start, self.get_function_data)
987
+ h, w = self.stdscr.getmaxyx()
988
+ try:
989
+ self.footer.draw(h, w)
990
+ except:
991
+ pass
992
+ elif self.footer_string:
993
+ footer_string_width = min(w-1, len(self.footer_string)+2)
994
+ disp_string = f" {self.footer_string[:footer_string_width]:>{footer_string_width-2}} "
995
+ self.stdscr.addstr(h - 1, w-footer_string_width-1, " "*footer_string_width, curses.color_pair(self.colours_start+24))
996
+ self.stdscr.addstr(h - 1, w-footer_string_width-1, f"{disp_string}", curses.color_pair(self.colours_start+24))
997
+
998
+
999
+ self.stdscr.refresh()
1000
+
1001
+ ## Display infobox
1002
+ if self.display_infobox:
1003
+ self.infobox(self.stdscr, message=self.infobox_items, title=self.infobox_title)
1004
+ # 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
1005
+ # self.stdscr.refresh()
1006
+
1007
+ # if self.display_infobox:
1008
+ # # self.stdscr.refresh()
1009
+ # # self.infobox(self.stdscr, message=self.infobox_items, title=self.infobox_title)
1010
+ #
1011
+ # infobox_width, infobox_height = w//2, 3*h//5
1012
+ # infobox_x, infobox_y = w - (infobox_width + 4), 3
1013
+ # self.infobox_picker.stdscr.mvwin(infobox_y, infobox_x)
1014
+ # self.infobox_picker.stdscr.resize(infobox_height, infobox_width)
1015
+ # self.infobox_picker.run()
1016
+ # self.infobox_picker.stdscr.noutrefresh()
1017
+ # else:
1018
+ # # self.stdscr.noutrefresh()
1019
+ # pass
1020
+ #
1021
+ #
1022
+ # if not self.display_only:
1023
+ # curses.doupdate()
1024
+ # self.stdscr.refresh()
1025
+ # pass
1026
+
1027
+
1028
+ # def infobox___(self, stdscr: curses.window, message: list =[], title: str ="Infobox", colours_end: int = 0, duration: int = 4):
1029
+ # """ Display non-interactive infobox window. """
1030
+ #
1031
+ # self.logger.info(f"function: infobox()")
1032
+ # h, w = stdscr.getmaxyx()
1033
+ # notification_width, notification_height = w//2, 3*h//5
1034
+ # message_width = notification_width-5
1035
+ #
1036
+ # notification_x, notification_y = w-(notification_width+4), 3
1037
+ # if not message: message = "!!"
1038
+ # if isinstance(message, str):
1039
+ # submenu_items = [" "+message[i*message_width:(i+1)*message_width] for i in range(len(message)//message_width+1)]
1040
+ # else:
1041
+ # submenu_items = message
1042
+ #
1043
+ # notification_remap_keys = {
1044
+ # curses.KEY_RESIZE: curses.KEY_F5,
1045
+ # 27: ord('q')
1046
+ # }
1047
+ # if len(submenu_items) > notification_height - 2:
1048
+ # submenu_items = submenu_items[:notification_height-3] + [f"{'....':^{notification_width}}"]
1049
+ # # while True:
1050
+ # h, w = stdscr.getmaxyx()
1051
+ #
1052
+ # submenu_win = self.stdscr.derwin(notification_height, notification_width, 3, w - (notification_width+4))
1053
+ # infobox_data = {
1054
+ # "items": submenu_items,
1055
+ # "colours": notification_colours,
1056
+ # "colours_start": self.notification_colours_start,
1057
+ # "disabled_keys": [ord('z'), ord('c')],
1058
+ # "show_footer": False,
1059
+ # "top_gap": 0,
1060
+ # "key_remappings": notification_remap_keys,
1061
+ # "display_only": True,
1062
+ # "hidden_columns": [],
1063
+ # "title": title,
1064
+ # "reset_colours": False,
1065
+ # }
1066
+ # submenu_win.noutrefresh()
1067
+ # OptionPicker = Picker(submenu_win, **infobox_data)
1068
+ # return OptionPicker
1069
+
1070
+
1071
+ def infobox(self, stdscr: curses.window, message: str ="", title: str ="Infobox", colours_end: int = 0, duration: int = 4) -> curses.window:
1072
+ """ Display non-interactive infobox window. """
1073
+
1074
+ self.logger.info(f"function: infobox()")
1075
+ h, w = stdscr.getmaxyx()
1076
+ notification_width, notification_height = w//2, 3*h//5
1077
+ message_width = notification_width-5
1078
+
1079
+ if not message: message = "!!"
1080
+ if isinstance(message, str):
1081
+ submenu_items = [" "+message[i*message_width:(i+1)*message_width] for i in range(len(message)//message_width+1)]
1082
+ else:
1083
+ submenu_items = message
1084
+
1085
+ notification_remap_keys = {
1086
+ curses.KEY_RESIZE: curses.KEY_F5,
1087
+ 27: ord('q')
1088
+ }
1089
+ if len(submenu_items) > notification_height - 2:
1090
+ submenu_items = submenu_items[:notification_height-3] + [f"{'....':^{notification_width}}"]
1091
+ while True:
1092
+ h, w = stdscr.getmaxyx()
1093
+
1094
+ submenu_win = curses.newwin(notification_height, notification_width, 3, w - (notification_width+4))
1095
+ infobox_data = {
1096
+ "items": submenu_items,
1097
+ "colours": notification_colours,
1098
+ "colours_start": self.notification_colours_start,
1099
+ "disabled_keys": [ord('z'), ord('c')],
1100
+ "show_footer": False,
1101
+ "top_gap": 0,
1102
+ "key_remappings": notification_remap_keys,
1103
+ "display_only": True,
1104
+ "hidden_columns": [],
1105
+ "title": title,
1106
+ "reset_colours": False,
1107
+ }
1108
+
1109
+ OptionPicker = Picker(submenu_win, **infobox_data)
1110
+ s, o, f = OptionPicker.run()
1111
+ if o != "refresh": break
1112
+
1113
+ return submenu_win
1114
+
1115
+ def get_function_data(self) -> dict:
1116
+ self.logger.debug(f"function: get_function_data()")
1117
+ """ Returns a dict of the main variables needed to restore the state of list_pikcer. """
1118
+ function_data = {
1119
+ "selections": self.selections,
1120
+ "cell_selections": self.cell_selections,
1121
+ "items_per_page": self.items_per_page,
1122
+ "current_row": self.current_row,
1123
+ "current_page": self.current_page,
1124
+ "cursor_pos": self.cursor_pos,
1125
+ "colours": self.colours,
1126
+ "colour_theme_number": self.colour_theme_number,
1127
+ "selected_column": self.selected_column,
1128
+ "sort_column": self.sort_column,
1129
+ "sort_method": self.sort_method,
1130
+ "sort_reverse": self.sort_reverse,
1131
+ "SORT_METHODS": self.SORT_METHODS,
1132
+ "hidden_columns": self.hidden_columns,
1133
+ "is_selecting": self.is_selecting,
1134
+ "is_deselecting": self.is_deselecting,
1135
+ "user_opts": self.user_opts,
1136
+ "options_list": self.options_list,
1137
+ "user_settings": self.user_settings,
1138
+ "separator": self.separator,
1139
+ "search_query": self.search_query,
1140
+ "search_count": self.search_count,
1141
+ "search_index": self.search_index,
1142
+ "filter_query": self.filter_query,
1143
+ "indexed_items": self.indexed_items,
1144
+ "start_selection": self.start_selection,
1145
+ "start_selection_col": self.start_selection_col,
1146
+ "end_selection": self.end_selection,
1147
+ "highlights": self.highlights,
1148
+ "max_column_width": self.max_column_width,
1149
+ "column_indices": self.column_indices,
1150
+ "mode_index": self.mode_index,
1151
+ "modes": self.modes,
1152
+ "title": self.title,
1153
+ "display_modes": self.display_modes,
1154
+ "require_option": self.require_option,
1155
+ "require_option_default": self.require_option_default,
1156
+ "option_functions": self.option_functions,
1157
+ "top_gap": self.top_gap,
1158
+ "number_columns": self.number_columns,
1159
+ "items": self.items,
1160
+ "indexed_items": self.indexed_items,
1161
+ "header": self.header,
1162
+ "scroll_bar": self.scroll_bar,
1163
+ "columns_sort_method": self.columns_sort_method,
1164
+ "disabled_keys": self.disabled_keys,
1165
+ "show_footer": self.show_footer,
1166
+ "footer_string": self.footer_string,
1167
+ "footer_string_auto_refresh": self.footer_string_auto_refresh,
1168
+ "footer_string_refresh_function": self.footer_string_refresh_function,
1169
+ "footer_timer": self.footer_timer,
1170
+ "footer_style": self.footer_style,
1171
+ "colours_start": self.colours_start,
1172
+ "colours_end": self.colours_end,
1173
+ "display_only": self.display_only,
1174
+ "infobox_items": self.infobox_items,
1175
+ "display_infobox": self.display_infobox,
1176
+ "infobox_title": self.infobox_title,
1177
+ "key_remappings": self.key_remappings,
1178
+ "auto_refresh": self.auto_refresh,
1179
+ "get_new_data": self.get_new_data,
1180
+ "refresh_function": self.refresh_function,
1181
+ "timer": self.timer,
1182
+ "get_data_startup": self.get_data_startup,
1183
+ "get_footer_string_startup": self.get_footer_string_startup,
1184
+ "editable_columns": self.editable_columns,
1185
+ "last_key": self.last_key,
1186
+ "centre_in_terminal": self.centre_in_terminal,
1187
+ "centre_in_terminal_vertical": self.centre_in_terminal_vertical,
1188
+ "centre_in_cols": self.centre_in_cols,
1189
+ "highlight_full_row": self.highlight_full_row,
1190
+ "cell_cursor": self.cell_cursor,
1191
+ "column_widths": self.column_widths,
1192
+ "track_entries_upon_refresh": self.track_entries_upon_refresh,
1193
+ "pin_cursor": self.pin_cursor,
1194
+ "id_column": self.id_column,
1195
+ "startup_notification": self.startup_notification,
1196
+ "keys_dict": self.keys_dict,
1197
+ "cancel_is_back": self.cancel_is_back,
1198
+ "paginate": self.paginate,
1199
+ "leftmost_column": self.leftmost_column,
1200
+ "leftmost_char": self.leftmost_char,
1201
+ "history_filter_and_search" : self.history_filter_and_search,
1202
+ "history_pipes" : self.history_pipes,
1203
+ "history_opts" : self.history_opts,
1204
+ "history_edits" : self.history_edits,
1205
+ "history_settings": self.history_settings,
1206
+ "show_header": self.show_header,
1207
+ "show_row_header": self.show_row_header,
1208
+ "debug": self.debug,
1209
+ "debug_level": self.debug_level,
1210
+ "reset_colours": self.reset_colours,
1211
+ "unicode_char_width": self.unicode_char_width,
1212
+ }
1213
+ return function_data
1214
+
1215
+ def set_function_data(self, function_data: dict) -> None:
1216
+ """ Set variables from state dict containing core variables."""
1217
+ self.logger.info(f"function: set_function_data()")
1218
+ variables = self.get_function_data().keys()
1219
+
1220
+ for var in variables:
1221
+ if var in function_data:
1222
+ setattr(self, var, function_data[var])
1223
+
1224
+ reset_colours = bool("colour_theme_number" in function_data)
1225
+ self.initialise_picker_state(reset_colours=reset_colours)
1226
+
1227
+ self.initialise_variables()
1228
+ # if "colour_theme_number" in function_data:
1229
+ # global COLOURS_SET
1230
+ # COLOURS_SET = False
1231
+ # colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
1232
+
1233
+ # if "items" in function_data: self.items = function_data["items"]
1234
+ # if "header" in function_data: self.header = function_data["header"]
1235
+ # self.indexed_items = function_data["indexed_items"] if "indexed_items" in function_data else []
1236
+
1237
+
1238
+
1239
+ def delete_entries(self) -> None:
1240
+ """ Delete entries from view. """
1241
+
1242
+ self.logger.info(f"function: delete_entries()")
1243
+ # Remove selected items from the list
1244
+ selected_indices = [index for index, selected in self.selections.items() if selected]
1245
+ if not selected_indices:
1246
+ # Remove the currently focused item if nothing is selected
1247
+ selected_indices = [self.indexed_items[self.cursor_pos][0]]
1248
+
1249
+ self.items = [item for i, item in enumerate(self.items) if i not in selected_indices]
1250
+ self.indexed_items = [(i, item) for i, item in enumerate(self.items)]
1251
+ self.selections = {i:False for i in range(len(self.indexed_items))}
1252
+ self.cursor_pos = min(self.cursor_pos, len(self.indexed_items)-1)
1253
+ self.initialise_variables()
1254
+ self.draw_screen(self.indexed_items, self.highlights)
1255
+
1256
+
1257
+ def choose_option(
1258
+ self,
1259
+ stdscr: curses.window,
1260
+ options: list[list[str]] =[],
1261
+ title: str = "Choose option",
1262
+ x:int=0,
1263
+ y:int=0,
1264
+ literal:bool=False,
1265
+ colours_start:int=0,
1266
+ header: list[str] = [],
1267
+ require_option:list = [],
1268
+ option_functions: list = [],
1269
+ ) -> Tuple[dict, str, dict]:
1270
+ """
1271
+ Display input field at x,y
1272
+
1273
+ ---Arguments
1274
+ stdscr: curses screen
1275
+ usrtxt (str): text to be edited by the user
1276
+ title (str): The text to be displayed at the start of the text option picker
1277
+ x (int): prompt begins at (x,y) in the screen given
1278
+ y (int): prompt begins at (x,y) in the screen given
1279
+ colours_start (bool): start index of curses init_pair.
1280
+
1281
+ ---Returns
1282
+ usrtxt, return_code
1283
+ usrtxt: the text inputted by the user
1284
+ return_code:
1285
+ 0: user hit escape
1286
+ 1: user hit return
1287
+ """
1288
+ self.logger.info(f"function: choose_option()")
1289
+ if options == []: options = [[f"{i}"] for i in range(10)]
1290
+ cursor = 0
1291
+
1292
+
1293
+ option_picker_data = {
1294
+ "items": options,
1295
+ "colours": notification_colours,
1296
+ "colours_start": self.notification_colours_start,
1297
+ "title":title,
1298
+ "header":header,
1299
+ "hidden_columns":[],
1300
+ "require_option":require_option,
1301
+ "keys_dict": options_keys,
1302
+ "show_footer": False,
1303
+ "cancel_is_back": True,
1304
+ "number_columns": False,
1305
+ "reset_colours": False,
1306
+ }
1307
+ while True:
1308
+ h, w = stdscr.getmaxyx()
1309
+
1310
+ choose_opts_widths = get_column_widths(options, unicode_char_width=self.unicode_char_width)
1311
+ window_width = min(max(sum(choose_opts_widths) + 6, 50) + 6, w)
1312
+ window_height = min(h//2, max(6, len(options)+3))
1313
+
1314
+ submenu_win = curses.newwin(window_height, window_width, (h-window_height)//2, (w-window_width)//2)
1315
+ submenu_win.keypad(True)
1316
+ OptionPicker = Picker(submenu_win, **option_picker_data)
1317
+ s, o, f = OptionPicker.run()
1318
+
1319
+ if o == "refresh":
1320
+ self.draw_screen(self.indexed_items, self.highlights)
1321
+ continue
1322
+ if s:
1323
+ return {x: options[x] for x in s}, o, f
1324
+ return {}, "", f
1325
+
1326
+
1327
+
1328
+ def notification(self, stdscr: curses.window, message: str="", title:str="Notification", colours_end: int=0, duration:int=4) -> None:
1329
+
1330
+ self.logger.info(f"function: notification()")
1331
+ """ Notification box. """
1332
+ notification_width, notification_height = 50, 7
1333
+ message_width = notification_width-5
1334
+
1335
+ if not message: message = "!!"
1336
+ submenu_items = [" "+message[i*message_width:(i+1)*message_width] for i in range(len(message)//message_width+1)]
1337
+
1338
+ notification_remap_keys = {
1339
+ curses.KEY_RESIZE: curses.KEY_F5,
1340
+ 27: ord('q')
1341
+ }
1342
+ while True:
1343
+ h, w = stdscr.getmaxyx()
1344
+
1345
+ submenu_win = curses.newwin(notification_height, notification_width, 3, w - (notification_width+4))
1346
+ notification_data = {
1347
+ "items": submenu_items,
1348
+ "title": title,
1349
+ "colours_start": self.notification_colours_start,
1350
+ "show_footer": False,
1351
+ "centre_in_terminal": True,
1352
+ "centre_in_terminal_vertical": True,
1353
+ "centre_in_cols": True,
1354
+ "hidden_columns": [],
1355
+ "keys_dict": notification_keys,
1356
+ "disabled_keys": [ord('z'), ord('c')],
1357
+ "highlight_full_row": True,
1358
+ "top_gap": 0,
1359
+ "cancel_is_back": True,
1360
+ "reset_colours": False,
1361
+
1362
+ }
1363
+ OptionPicker = Picker(submenu_win, **notification_data)
1364
+ s, o, f = OptionPicker.run()
1365
+
1366
+ if o != "refresh": break
1367
+ submenu_win.clear()
1368
+ submenu_win.refresh()
1369
+ del submenu_win
1370
+ stdscr.clear()
1371
+ stdscr.refresh()
1372
+ self.draw_screen(self.indexed_items, self.highlights)
1373
+ # set_colours(colours=get_colours(0))
1374
+
1375
+ def toggle_column_visibility(self, col_index:int) -> None:
1376
+ """ Toggle the visibility of the column at col_index. """
1377
+ self.logger.info(f"function: toggle_column_visibility()")
1378
+ if 0 <= col_index < len(self.items[0]):
1379
+ if col_index in self.hidden_columns:
1380
+ self.hidden_columns.remove(col_index)
1381
+ else:
1382
+ self.hidden_columns.append(col_index)
1383
+
1384
+ def apply_settings(self) -> None:
1385
+ """
1386
+ The users settings will be stored in the user_settings variable. This function applies those settings.
1387
+
1388
+ ![0-9]+ show/hide column
1389
+ s[0-9]+ set column focus for sort
1390
+ g[0-9]+ go to index
1391
+ p[0-9]+ go to page
1392
+ nohl hide search highlights
1393
+ """
1394
+ self.logger.info(f"function: apply_settings()")
1395
+ if self.user_settings:
1396
+ settings = re.split(r'\s+', self.user_settings)
1397
+ for setting in settings:
1398
+ if len(setting) == 0: continue
1399
+
1400
+ if setting[0] == "!" and len(setting) > 1:
1401
+ if setting[1:].isnumeric():
1402
+ cols = setting[1:].split(",")
1403
+ for col in cols:
1404
+ self.toggle_column_visibility(int(col))
1405
+ elif setting[1] == "r":
1406
+ self.auto_refresh = not self.auto_refresh
1407
+ elif setting[1] == "h":
1408
+ self.highlights_hide = not self.highlights_hide
1409
+
1410
+ elif setting in ["nhl", "nohl", "nohighlights"]:
1411
+ # highlights = [highlight for highlight in highlights if "type" not in highlight or highlight["type"] != "search" ]
1412
+
1413
+ self.highlights_hide = not self.highlights_hide
1414
+ elif setting[0] == "s":
1415
+ if 0 <= int(setting[1:]) < len(self.items[0]):
1416
+ self.sort_column = int(setting[1:])
1417
+ if len(self.indexed_items):
1418
+ current_pos = self.indexed_items[self.cursor_pos][0]
1419
+ 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
1420
+ if len(self.indexed_items):
1421
+ new_pos = [row[0] for row in self.indexed_items].index(current_pos)
1422
+ self.cursor_pos = new_pos
1423
+ elif setting == "ct":
1424
+ self.centre_in_terminal = not self.centre_in_terminal
1425
+ elif setting == "cc":
1426
+ self.centre_in_cols = not self.centre_in_cols
1427
+ elif setting == "cv":
1428
+ self.centre_in_terminal_vertical = not self.centre_in_terminal_vertical
1429
+ elif setting == "arb":
1430
+ self.insert_row(self.cursor_pos)
1431
+ elif setting == "ara":
1432
+ self.insert_row(self.cursor_pos+1)
1433
+ elif setting == "aca":
1434
+ self.insert_column(self.selected_column+1)
1435
+ elif setting == "acb":
1436
+ self.insert_column(self.selected_column)
1437
+ elif setting.startswith("ir"):
1438
+ if setting[2:].isnumeric():
1439
+ num = int(setting[2:])
1440
+ else:
1441
+ num = self.cursor_pos
1442
+ self.insert_row(num)
1443
+ elif setting.startswith("ic"):
1444
+ if setting[2:].isnumeric():
1445
+ num = int(setting[2:])
1446
+ else:
1447
+ num = self.selected_column
1448
+ self.insert_column(num)
1449
+
1450
+ elif setting == "modes":
1451
+ self.display_modes = not self.display_modes
1452
+ elif setting == "cell":
1453
+ self.cell_cursor = not self.cell_cursor
1454
+ elif setting == "rh":
1455
+ self.show_row_header = not self.show_row_header
1456
+ elif setting == "header":
1457
+ self.show_header = not self.show_header
1458
+ elif setting[0] == "":
1459
+ cols = setting[1:].split(",")
1460
+ elif setting == "footer":
1461
+ self.show_footer = not self.show_footer
1462
+ self.initialise_variables()
1463
+ elif setting == "pc":
1464
+ self.pin_cursor = not self.pin_cursor
1465
+ elif setting == "unicode":
1466
+ self.unicode_char_width = not self.unicode_char_width
1467
+
1468
+ elif setting.startswith("ft"):
1469
+ if len(setting) > 2 and setting[2:].isnumeric():
1470
+
1471
+ num = int(setting[2:])
1472
+ self.footer_style = max(len(self.footer_options)-1, num)
1473
+ self.footer = self.footer_options[self.footer_style]
1474
+ else:
1475
+ self.footer_style = (self.footer_style+1)%len(self.footer_options)
1476
+ self.footer = self.footer_options[self.footer_style]
1477
+ self.initialise_variables()
1478
+
1479
+ elif setting.startswith("cwd="):
1480
+ os.chdir(os.path.expandvars(os.path.expanduser(setting[len("cwd="):])))
1481
+ elif setting.startswith("hl"):
1482
+ hl_list = setting.split(",")
1483
+ if len(hl_list) > 1:
1484
+ hl_list = hl_list[1:]
1485
+ match = hl_list[0]
1486
+ if len(hl_list) > 1:
1487
+ field = hl_list[1]
1488
+ if field.isnumeric() and field != "-1":
1489
+ field = int(field)
1490
+ else:
1491
+ field = "all"
1492
+ else:
1493
+ field = "all"
1494
+ if len(hl_list) > 2 and hl_list[2].isnumeric():
1495
+ colour_pair = int(hl_list[2])
1496
+ else:
1497
+ colour_pair = 10
1498
+
1499
+ highlight = {
1500
+ "match": match,
1501
+ "field": field,
1502
+ "color": colour_pair
1503
+ }
1504
+ self.highlights.append(highlight)
1505
+
1506
+
1507
+ elif setting.startswith("th"):
1508
+ global COLOURS_SET
1509
+ if curses.COLORS < 255:
1510
+ self.notification(self.stdscr, message=f"Theme 4 applied.")
1511
+
1512
+ elif setting[2:].strip().isnumeric():
1513
+ COLOURS_SET = False
1514
+ try:
1515
+ theme_number = int(setting[2:].strip())
1516
+ self.colour_theme_number = min(get_theme_count()-1, theme_number)
1517
+ set_colours(self.colour_theme_number)
1518
+ self.draw_screen(self.indexed_items, self.highlights)
1519
+ self.notification(self.stdscr, message=f"Theme {self.colour_theme_number} applied.")
1520
+ except:
1521
+ pass
1522
+ else:
1523
+ COLOURS_SET = False
1524
+ self.colour_theme_number = (self.colour_theme_number + 1)%get_theme_count()
1525
+ # self.colour_theme_number = int(not bool(self.colour_theme_number))
1526
+ set_colours(self.colour_theme_number)
1527
+ self.draw_screen(self.indexed_items, self.highlights)
1528
+ self.notification(self.stdscr, message=f"Theme {self.colour_theme_number} applied.")
1529
+
1530
+ else:
1531
+ self.user_settings = ""
1532
+ return None
1533
+
1534
+
1535
+ self.command_stack.append(Command("setting", self.user_settings))
1536
+ self.user_settings = ""
1537
+
1538
+ def apply_command(self, command: Command):
1539
+ self.logger.info(f"function: apply_command()")
1540
+ if command.command_type == "setting":
1541
+ self.user_settings = command.command_value
1542
+ self.apply_settings()
1543
+
1544
+ def redo(self):
1545
+ self.logger.info(f"function: redo()")
1546
+ if len(self.command_stack):
1547
+ self.apply_command(self.command_stack[-1])
1548
+
1549
+ def toggle_item(self, index: int) -> None:
1550
+ """ Toggle selection of item at index. """
1551
+ self.logger.info(f"function: toggle_item()")
1552
+ self.selections[index] = not self.selections[index]
1553
+
1554
+ def select_all(self) -> None:
1555
+ """ Select all in indexed_items. """
1556
+ self.logger.info(f"function: select_all()")
1557
+ for i in range(len(self.indexed_items)):
1558
+ self.selections[self.indexed_items[i][0]] = True
1559
+ for i in self.cell_selections.keys():
1560
+ self.cell_selections[i] = True
1561
+ for row in range(len(self.indexed_items)):
1562
+ self.selected_cells_by_row[row] = list(range(len(self.indexed_items[row][1])))
1563
+
1564
+ def deselect_all(self) -> None:
1565
+ """ Deselect all items in indexed_items. """
1566
+ self.logger.info(f"function: deselect_all()")
1567
+ for i in range(len(self.selections)):
1568
+ self.selections[i] = False
1569
+ for i in self.cell_selections.keys():
1570
+ self.cell_selections[i] = False
1571
+ self.selected_cells_by_row = {}
1572
+
1573
+ def handle_visual_selection(self, selecting:bool = True) -> None:
1574
+ """ Toggle visual selection or deselection. """
1575
+ self.logger.info(f"function: handle_visual_selection()")
1576
+ if not self.is_selecting and not self.is_deselecting and len(self.indexed_items) and len(self.indexed_items[0][1]):
1577
+ self.start_selection = self.cursor_pos
1578
+ self.start_selection_col = self.selected_column
1579
+ if selecting:
1580
+ self.is_selecting = True
1581
+ else:
1582
+ self.is_deselecting = True
1583
+ elif self.is_selecting:
1584
+ # end_selection = indexed_items[current_page * items_per_page + current_row][0]
1585
+ self.end_selection = self.cursor_pos
1586
+ if self.start_selection != -1:
1587
+ start = max(min(self.start_selection, self.end_selection), 0)
1588
+ end = min(max(self.start_selection, self.end_selection), len(self.indexed_items)-1)
1589
+ for i in range(start, end + 1):
1590
+ if self.indexed_items[i][0] not in self.unselectable_indices:
1591
+ self.selections[self.indexed_items[i][0]] = True
1592
+ if self.start_selection != -1:
1593
+ ystart = max(min(self.start_selection, self.end_selection), 0)
1594
+ yend = min(max(self.start_selection, self.end_selection), len(self.indexed_items)-1)
1595
+ xstart = min(self.start_selection_col, self.selected_column)
1596
+ xend = max(self.start_selection_col, self.selected_column)
1597
+ for i in range(ystart, yend + 1):
1598
+ if self.indexed_items[i][0] not in self.unselectable_indices:
1599
+ row = self.indexed_items[i][0]
1600
+ if row not in self.selected_cells_by_row:
1601
+ self.selected_cells_by_row[row] = []
1602
+
1603
+ for col in range(xstart, xend+1):
1604
+ cell_index = (row, col)
1605
+ self.cell_selections[cell_index] = True
1606
+
1607
+ self.selected_cells_by_row[row].append(col)
1608
+ # Remove duplicates
1609
+ self.selected_cells_by_row[row] = list(set(self.selected_cells_by_row[row]))
1610
+
1611
+ self.start_selection = -1
1612
+ self.end_selection = -1
1613
+ self.is_selecting = False
1614
+
1615
+ elif self.is_deselecting:
1616
+ self.end_selection = self.indexed_items[self.cursor_pos][0]
1617
+ self.end_selection = self.cursor_pos
1618
+ if self.start_selection != -1:
1619
+ start = max(min(self.start_selection, self.end_selection), 0)
1620
+ end = min(max(self.start_selection, self.end_selection), len(self.indexed_items)-1)
1621
+ for i in range(start, end + 1):
1622
+ # selections[i] = False
1623
+ self.selections[self.indexed_items[i][0]] = False
1624
+ if self.start_selection != -1:
1625
+ ystart = max(min(self.start_selection, self.end_selection), 0)
1626
+ yend = min(max(self.start_selection, self.end_selection), len(self.indexed_items)-1)
1627
+ xstart = min(self.start_selection_col, self.selected_column)
1628
+ xend = max(self.start_selection_col, self.selected_column)
1629
+ for i in range(ystart, yend + 1):
1630
+ row = self.indexed_items[i][0]
1631
+ if self.indexed_items[i][0] not in self.unselectable_indices:
1632
+ if row in self.selected_cells_by_row:
1633
+ for col in range(xstart, xend+1):
1634
+ try:
1635
+ self.selected_cells_by_row[row].remove(col)
1636
+ except:
1637
+ pass
1638
+ cell_index = (row, col)
1639
+ self.cell_selections[cell_index] = False
1640
+ if self.selected_cells_by_row[row] == []:
1641
+ del self.selected_cells_by_row[row]
1642
+
1643
+ self.start_selection = -1
1644
+ self.end_selection = -1
1645
+ self.is_deselecting = False
1646
+
1647
+ def cursor_down(self, count=1) -> bool:
1648
+ """ Move cursor down. """
1649
+ self.logger.info(f"function: cursor_down()")
1650
+ if len(self.indexed_items) == 0 or self.cursor_pos == len(self.indexed_items) -1:
1651
+ return False
1652
+ # Returns: whether page is turned
1653
+ new_pos = self.cursor_pos + 1
1654
+ new_pos = min(self.cursor_pos+count, len(self.indexed_items)-1)
1655
+ while True:
1656
+ if self.indexed_items[new_pos][0] in self.unselectable_indices: new_pos+=1
1657
+ else: break
1658
+ self.cursor_pos = new_pos
1659
+ return True
1660
+
1661
+ def cursor_up(self, count=1) -> bool:
1662
+ """ Move cursor up. """
1663
+ self.logger.info(f"function: cursor_up()")
1664
+ # Returns: whether page is turned
1665
+
1666
+ new_pos = max(self.cursor_pos - count, 0)
1667
+ while True:
1668
+ if new_pos < 0: return False
1669
+ elif new_pos in self.unselectable_indices: new_pos -= 1
1670
+ else: break
1671
+ self.cursor_pos = new_pos
1672
+ return True
1673
+
1674
+ def remapped_key(self, key: int, val: int, key_remappings: dict) -> bool:
1675
+ """ Check if key has been remapped to val in key_remappings. """
1676
+ # self.logger.info(f"function: remapped_key()")
1677
+ if key in key_remappings:
1678
+ if key_remappings[key] == val or (isinstance(key_remappings[key], list) and val in key_remappings[key]):
1679
+ return True
1680
+ return False
1681
+
1682
+ def check_key(self, function: str, key: int, keys_dict: dict) -> bool:
1683
+ """
1684
+ Check if $key is assigned to $function in the keys_dict.
1685
+ Allows us to redefine functions to different keys in the keys_dict.
1686
+
1687
+ E.g., keys_dict = { $key, "help": ord('?') },
1688
+ """
1689
+ if function in keys_dict and key in keys_dict[function]:
1690
+ return True
1691
+ return False
1692
+
1693
+ def copy_dialogue(self) -> None:
1694
+ """ Display dialogue to select how rows/cells should be copied. """
1695
+ self.logger.info(f"function: copy_dialogue()")
1696
+ copy_header = [
1697
+ "Representation",
1698
+ "Columns",
1699
+ ]
1700
+ options = [
1701
+ ["Python list of lists", "Exclude hidden"],
1702
+ ["Python list of lists", "Include hidden"],
1703
+ ["Tab-separated values", "Exclude hidden"],
1704
+ ["Tab-separated values", "Include hidden"],
1705
+ ["Comma-separated values", "Exclude hidden"],
1706
+ ["Comma-separated values", "Include hidden"],
1707
+ ["Custom separator", "Exclude hidden"],
1708
+ ["Custom separator", "Include hidden"],
1709
+ ]
1710
+ require_option = [False, False, False, False, False, False, True, True]
1711
+ s, o, f = self.choose_option(self.stdscr, options=options, title="Copy selected", header=copy_header, require_option=require_option)
1712
+
1713
+
1714
+ funcs = [
1715
+ lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="python", copy_hidden_cols=False, cellwise=cell_cursor),
1716
+ lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="python", copy_hidden_cols=True, cellwise=cell_cursor),
1717
+ lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="tsv", copy_hidden_cols=False, cellwise=cell_cursor),
1718
+ lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="tsv", copy_hidden_cols=True, cellwise=cell_cursor),
1719
+ lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="csv", copy_hidden_cols=False, cellwise=cell_cursor),
1720
+ lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="csv", copy_hidden_cols=True, cellwise=cell_cursor),
1721
+ lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="custom_sv", copy_hidden_cols=False, separator=o, cellwise=cell_cursor),
1722
+ lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="custom_sv", copy_hidden_cols=True, separator=o, cellwise=cell_cursor),
1723
+ ]
1724
+
1725
+ # Copy items based on selection
1726
+ if s:
1727
+ for idx in s.keys():
1728
+ funcs[idx](self.items, self.indexed_items, self.selections, self.cell_selections, self.hidden_columns, self.cell_cursor)
1729
+ def paste_dialogue(self) -> None:
1730
+ """ Display dialogue to select how to paste from the clipboard. """
1731
+ self.logger.info(f"function: paste_dialogue()")
1732
+ paste_header = [
1733
+ "Representation",
1734
+ "Columns",
1735
+ ]
1736
+ options = [
1737
+ ["Paste values", ""],
1738
+ ]
1739
+ require_option = [False]
1740
+ s, o, f = self.choose_option(self.stdscr, options=options, title="Paste values", header=paste_header, require_option=require_option)
1741
+
1742
+
1743
+ funcs = [
1744
+ lambda items, pasta, paste_row, paste_col: paste_values(items, pasta, paste_row, paste_col)
1745
+ ]
1746
+
1747
+ try:
1748
+ pasta = eval(pyperclip.paste())
1749
+ if type(pasta) == type([]):
1750
+ acceptable_data_type = True
1751
+ for row in pasta:
1752
+ if type(row) != type([]):
1753
+ acceptable_data_type = False
1754
+ break
1755
+
1756
+ for cell in row:
1757
+ if cell != None and type(cell) != type(""):
1758
+ acceptable_data_type = False
1759
+ break
1760
+ if not acceptable_data_type:
1761
+ break
1762
+ if not acceptable_data_type:
1763
+ self.notification(self.stdscr, message="Error pasting data.")
1764
+ return None
1765
+
1766
+ except:
1767
+ self.draw_screen(self.indexed_items, self.highlights)
1768
+ self.notification(self.stdscr, message="Error pasting data.")
1769
+ return None
1770
+ if type(pasta) == type([]) and len(pasta) > 0 and type(pasta[0]) == type([]):
1771
+ if s:
1772
+ for idx in s.keys():
1773
+ return_val, tmp_items = funcs[idx](self.items, pasta, self.cursor_pos, self.selected_column)
1774
+ if return_val:
1775
+ cursor_pos = self.cursor_pos
1776
+ self.items = tmp_items
1777
+ self.initialise_variables()
1778
+ self.cursor_pos = cursor_pos
1779
+
1780
+ def save_dialog(self) -> None:
1781
+ """ Display dialogue to select how to save the picker data. """
1782
+ self.logger.info(f"function: save_dialog()")
1783
+
1784
+ dump_header = []
1785
+ options = [
1786
+ ["Save data (pickle)."],
1787
+ ["Save data (csv)."],
1788
+ ["Save data (tsv)."],
1789
+ ["Save data (json)."],
1790
+ ["Save data (feather)."],
1791
+ ["Save data (parquet)."],
1792
+ ["Save data (msgpack)."],
1793
+ ["Save state"]
1794
+ ]
1795
+ # require_option = [True, True, True, True, True, True, True, True]
1796
+ s, o, f = self.choose_option(self.stdscr, options=options, title="Save...", header=dump_header)
1797
+
1798
+
1799
+ funcs = [
1800
+ lambda opts: dump_data(self.get_function_data(), opts),
1801
+ lambda opts: dump_data(self.get_function_data(), opts, format="csv"),
1802
+ lambda opts: dump_data(self.get_function_data(), opts, format="tsv"),
1803
+ lambda opts: dump_data(self.get_function_data(), opts, format="json"),
1804
+ lambda opts: dump_data(self.get_function_data(), opts, format="feather"),
1805
+ lambda opts: dump_data(self.get_function_data(), opts, format="parquet"),
1806
+ lambda opts: dump_data(self.get_function_data(), opts, format="msgpack"),
1807
+ lambda opts: dump_state(self.get_function_data(), opts),
1808
+ ]
1809
+
1810
+ if s:
1811
+ for idx in s.keys():
1812
+ save_path_entered, save_path = output_file_option_selector(
1813
+ self.stdscr,
1814
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
1815
+ )
1816
+ if save_path_entered:
1817
+ return_val = funcs[idx](save_path)
1818
+ if return_val:
1819
+ self.notification(self.stdscr, message=return_val, title="Error")
1820
+
1821
+ def load_dialog(self) -> None:
1822
+ """ Display dialogue to select which file to load and in what way it should be loaded. """
1823
+ self.logger.info(f"function: load_dialog()")
1824
+
1825
+ dump_header = []
1826
+ options = [
1827
+ ["Load data (pickle)."],
1828
+ ]
1829
+ s, o, f = self.choose_option(self.stdscr, options=options, title="Open file...", header=dump_header)
1830
+
1831
+
1832
+ funcs = [
1833
+ lambda opts: load_state(opts)
1834
+ ]
1835
+
1836
+ if s:
1837
+ file_to_load = file_picker()
1838
+ if file_to_load:
1839
+ index = list(s.keys())[0]
1840
+ return_val = funcs[index](file_to_load)
1841
+ self.set_function_data(return_val)
1842
+
1843
+ # items = return_val["items"]
1844
+ # header = return_val["header"]
1845
+ self.initialise_variables()
1846
+ self.draw_screen(self.indexed_items, self.highlights)
1847
+ self.notification(self.stdscr, f"{repr(file_to_load)} has been loaded!")
1848
+ # if return_val:
1849
+ # notification(stdscr, message=return_val, title="Error")
1850
+
1851
+ def set_registers(self):
1852
+ """ Set registers to be sent to the input field. """
1853
+ self.logger.info(f"function: set_registers()")
1854
+ self.registers = {"*": self.indexed_items[self.cursor_pos][1][self.selected_column]} if len(self.indexed_items) and len(self.indexed_items[0][1]) else {}
1855
+
1856
+
1857
+ def fetch_data(self) -> None:
1858
+ """ Refesh data asynchronously. When data has been fetched self.data_ready is set to True. """
1859
+ self.logger.info(f"function: fetch_data()")
1860
+ tmp_items, tmp_header = self.refresh_function()
1861
+ if self.track_entries_upon_refresh:
1862
+ selected_indices = get_selected_indices(self.selections)
1863
+ self.ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
1864
+ self.ids_tuples = [(i, item[self.id_column]) for i, item in enumerate(self.items) if i in selected_indices]
1865
+ self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
1866
+
1867
+ if len(self.indexed_items) > 0 and len(self.indexed_items) >= self.cursor_pos and len(self.indexed_items[0][1]) >= self.id_column:
1868
+ self.cursor_pos_id = self.indexed_items[self.cursor_pos][1][self.id_column]
1869
+ self.cursor_pos_prev = self.cursor_pos
1870
+ with self.data_lock:
1871
+ self.items, self.header = tmp_items, tmp_header
1872
+ self.data_ready = True
1873
+ self.draw_screen(self.indexed_items, self.highlights)
1874
+
1875
+ def save_input_history(self, file_path: str) -> bool:
1876
+ """ Save input field history. Returns True if successful save. """
1877
+ self.logger.info(f"function: save_input_history()")
1878
+ file_path = os.path.expanduser(file_path)
1879
+ history_dict = {
1880
+ "history_filter_and_search" : self.history_filter_and_search,
1881
+ "history_pipes" : self.history_pipes,
1882
+ "history_opts" : self.history_opts,
1883
+ "history_edits" : self.history_edits,
1884
+ "history_settings": self.history_settings,
1885
+ }
1886
+ with open(file_path, 'w') as f:
1887
+ json.dump(history_dict, f)
1888
+
1889
+ return True
1890
+
1891
+ def load_input_history(self, file_path:str) -> bool:
1892
+ """ Load command history. Returns true if successful load. """
1893
+ self.logger.info(f"function: load_input_history()")
1894
+ file_path = os.path.expanduser(file_path)
1895
+ if not os.path.exists(file_path):
1896
+ return False
1897
+ try:
1898
+ with open(file_path, 'r') as f:
1899
+ history_dict = json.load(f)
1900
+
1901
+ if "history_filter_and_search" in history_dict:
1902
+ self.history_filter_and_search = history_dict["history_filter_and_search"]
1903
+ if "history_pipes" in history_dict:
1904
+ self.history_pipes = history_dict["history_pipes"]
1905
+ if "history_opts" in history_dict:
1906
+ self.history_opts = history_dict["history_opts"]
1907
+ if "history_edits" in history_dict:
1908
+ self.history_edits = history_dict["history_edits"]
1909
+ if "history_settings" in history_dict:
1910
+ self.history_settings = history_dict["history_settings"]
1911
+
1912
+ except:
1913
+ return False
1914
+
1915
+ return True
1916
+
1917
+
1918
+ def get_word_list(self) -> list[str]:
1919
+ """ Get a list of all words used in any cell of the picker. Used for completion in search/filter input_field. """
1920
+ self.logger.info(f"function: get_word_list()")
1921
+ translator = str.maketrans('', '', string.punctuation)
1922
+
1923
+ words = []
1924
+ # Extract words from lists
1925
+ for row in [x[1] for x in self.indexed_items]:
1926
+ for i, cell in enumerate(row):
1927
+ if i != (self.id_column%len(row)):
1928
+ # Split the item into words and strip punctuation from each word
1929
+ words.extend(word.strip(string.punctuation) for word in cell.split())
1930
+ for cell in self.header:
1931
+ # Split the item into words and strip punctuation from each word
1932
+ words.extend(word.strip(string.punctuation) for word in cell.split())
1933
+ def key_f(s):
1934
+ if len(s):
1935
+ starts_with_char = s[0].isalpha()
1936
+ else:
1937
+ starts_with_char = False
1938
+ return (not starts_with_char, s.lower())
1939
+ # key = lambda s: (s != "" or not s[0].isalpha(), s)
1940
+ words = sorted(list(set(words)), key=key_f)
1941
+ return words
1942
+
1943
+ def insert_row(self, pos: int):
1944
+ """ Insert a blank row at position `pos` """
1945
+ self.logger.info(f"function: insert_row(pos={pos})")
1946
+
1947
+ if self.items != [[]]:
1948
+ row_len = 1
1949
+ if self.header: row_len = len(self.header)
1950
+ elif len(self.items): row_len = len(self.items[0])
1951
+ # if len(self.indexed_items) == 0:
1952
+ # insert_at_pos = 0
1953
+ # else:
1954
+ # insert_at_pos = self.indexed_items[self.cursor_pos][0]
1955
+ self.items = self.items[:pos] + [["" for x in range(row_len)]] + self.items[pos:]
1956
+ if pos <= self.cursor_pos:
1957
+ self.cursor_pos += 1
1958
+ # We are adding a row before so we have to move the cursor down
1959
+ # If there is a filter then we know that an empty row doesn't match
1960
+ current_cursor_pos = self.cursor_pos
1961
+ self.initialise_variables()
1962
+ self.cursor_pos = current_cursor_pos
1963
+ else:
1964
+ self.items = [[""]]
1965
+ self.initialise_variables()
1966
+
1967
+ def insert_column(self, pos:int):
1968
+ """ Insert blank column at `pos`"""
1969
+ self.logger.info(f"function: insert_column(pos={pos})")
1970
+ self.items = [row[:pos]+[""]+row[pos:] for row in self.items]
1971
+ self.header = self.header[:pos] + [""] + self.header[pos:]
1972
+ self.editable_columns = self.editable_columns[:pos] + [self.editable_by_default] + self.editable_columns[pos:]
1973
+ if pos <= self.selected_column:
1974
+ self.selected_column += 1
1975
+ current_cursor_pos = self.cursor_pos
1976
+ self.initialise_variables()
1977
+ self.cursor_pos = current_cursor_pos
1978
+
1979
+
1980
+
1981
+ def run(self) -> Tuple[list[int], str, dict]:
1982
+ """ Run the picker. """
1983
+ self.logger.info(f"function: run()")
1984
+
1985
+ if self.get_footer_string_startup and self.footer_string_refresh_function != None:
1986
+ self.footer_string = self.footer_string_refresh_function()
1987
+
1988
+ self.initialise_variables(get_data=self.get_data_startup)
1989
+
1990
+ self.draw_screen(self.indexed_items, self.highlights)
1991
+
1992
+ initial_time = time.time()
1993
+ initial_time_footer = time.time()-self.footer_timer
1994
+
1995
+ if self.startup_notification:
1996
+ self.notification(self.stdscr, message=self.startup_notification)
1997
+ self.startup_notification = ""
1998
+
1999
+ # curses.curs_set(0)
2000
+ # stdscr.nodelay(1) # Non-blocking input
2001
+ # stdscr.timeout(2000) # Set a timeout for getch() to ensure it does not block indefinitely
2002
+ self.stdscr.timeout(max(min(2000, int(self.timer*1000)//2, int(self.footer_timer*1000))//2, 20)) # Set a timeout for getch() to ensure it does not block indefinitely
2003
+
2004
+ if self.clear_on_start:
2005
+ self.stdscr.clear()
2006
+ self.clear_on_start = False
2007
+ else:
2008
+ self.stdscr.erase()
2009
+
2010
+ self.stdscr.refresh()
2011
+
2012
+ # Initialize colours
2013
+ # Check if terminal supports color
2014
+
2015
+ # Set terminal background color
2016
+ self.stdscr.bkgd(' ', curses.color_pair(self.colours_start+3)) # Apply background color
2017
+ self.draw_screen(self.indexed_items, self.highlights)
2018
+
2019
+ if self.display_only:
2020
+ self.stdscr.refresh()
2021
+ return [], "", {}
2022
+
2023
+ # Main loop
2024
+
2025
+ while True:
2026
+ key = self.stdscr.getch()
2027
+ h, w = self.stdscr.getmaxyx()
2028
+ if key in self.disabled_keys: continue
2029
+ clear_screen=True
2030
+
2031
+ ## Refresh data asyncronously.
2032
+ if self.refreshing_data:
2033
+ self.logger.debug(f"Data refresh check")
2034
+ with self.data_lock:
2035
+ if self.data_ready:
2036
+ self.logger.debug(f"Data ready after refresh")
2037
+ self.initialise_variables()
2038
+
2039
+ initial_time = time.time()
2040
+
2041
+ self.draw_screen(self.indexed_items, self.highlights, clear=False)
2042
+
2043
+ self.refreshing_data = False
2044
+ self.data_ready = False
2045
+
2046
+ elif 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):
2047
+ self.logger.debug(f"Get new data (refresh).")
2048
+ self.stdscr.addstr(0,w-3,"  ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
2049
+ self.stdscr.refresh()
2050
+ if self.get_new_data and self.refresh_function:
2051
+ self.refreshing_data = True
2052
+
2053
+ t = threading.Thread(target=self.fetch_data)
2054
+ t.start()
2055
+ else:
2056
+ function_data = self.get_function_data()
2057
+ return [], "refresh", function_data
2058
+
2059
+ # Refresh data synchronously
2060
+ # 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):
2061
+ # self.stdscr.addstr(0,w-3,"  ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
2062
+ # self.stdscr.refresh()
2063
+ # if self.get_new_data and self.refresh_function:
2064
+ # self.initialise_variables(get_data=True)
2065
+ #
2066
+ # initial_time = time.time()
2067
+ # self.draw_screen(self.indexed_items, self.highlights, clear=False)
2068
+ # else:
2069
+ #
2070
+ # function_data = self.get_function_data()
2071
+ # return [], "refresh", function_data
2072
+
2073
+ if self.footer_string_auto_refresh and ((time.time() - initial_time_footer) > self.footer_timer):
2074
+ self.logger.debug(f"footer_string_auto_refresh")
2075
+ self.footer_string = self.footer_string_refresh_function()
2076
+ initial_time_footer = time.time()
2077
+ self.draw_screen(self.indexed_items, self.highlights)
2078
+
2079
+ if self.check_key("help", key, self.keys_dict):
2080
+ self.logger.info(f"key_function help")
2081
+ self.stdscr.clear()
2082
+ self.stdscr.refresh()
2083
+ help_data = {
2084
+ # "items": help_lines,
2085
+ "items": build_help_rows(self.keys_dict),
2086
+ "title": f"{self.title} Help",
2087
+ "colours_start": self.help_colours_start,
2088
+ "colours": help_colours,
2089
+ "show_footer": True,
2090
+ "max_selected": 1,
2091
+ "keys_dict": help_keys,
2092
+ "disabled_keys": [ord('?'), ord('v'), ord('V'), ord('m'), ord('M'), ord('l'), curses.KEY_ENTER, ord('\n')],
2093
+ "highlight_full_row": True,
2094
+ "top_gap": 0,
2095
+ "paginate": self.paginate,
2096
+ "centre_in_terminal": True,
2097
+ "centre_in_terminal_vertical": True,
2098
+ "hidden_columns": [],
2099
+ "reset_colours": False,
2100
+
2101
+ }
2102
+ OptionPicker = Picker(self.stdscr, **help_data)
2103
+ s, o, f = OptionPicker.run()
2104
+
2105
+ elif self.check_key("exit", key, self.keys_dict):
2106
+ self.stdscr.clear()
2107
+ function_data = self.get_function_data()
2108
+ function_data["last_key"] = key
2109
+ return [], "", function_data
2110
+ elif self.check_key("full_exit", key, self.keys_dict):
2111
+ close_curses(self.stdscr)
2112
+ exit()
2113
+
2114
+ elif self.check_key("settings_input", key, self.keys_dict):
2115
+ self.logger.info(f"Settings input")
2116
+ usrtxt = f"{self.user_settings.strip()} " if self.user_settings else ""
2117
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
2118
+ if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
2119
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
2120
+ self.set_registers()
2121
+ usrtxt, return_val = input_field(
2122
+ self.stdscr,
2123
+ usrtxt=usrtxt,
2124
+ field_prefix=" Settings: ",
2125
+ x=lambda:2,
2126
+ y=lambda: self.stdscr.getmaxyx()[0]-1,
2127
+ max_length=field_end_f,
2128
+ registers=self.registers,
2129
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
2130
+ history=self.history_settings,
2131
+ path_auto_complete=True,
2132
+ formula_auto_complete=False,
2133
+ function_auto_complete=False,
2134
+ word_auto_complete=True,
2135
+ auto_complete_words=["ft", "ct", "cv"]
2136
+ )
2137
+ if return_val:
2138
+ self.user_settings = usrtxt
2139
+ self.apply_settings()
2140
+ self.history_settings.append(usrtxt)
2141
+ self.user_settings = ""
2142
+ elif self.check_key("toggle_footer", key, self.keys_dict):
2143
+ self.logger.info(f"toggle footer")
2144
+ self.user_settings = "footer"
2145
+ self.apply_settings()
2146
+
2147
+ elif self.check_key("settings_options", key, self.keys_dict):
2148
+ options = []
2149
+ if len(self.items) > 0:
2150
+ options += [["cv", "Centre rows vertically"]]
2151
+ options += [["pc", "Pin cursor to row number when data refreshes"]]
2152
+ options += [["ct", "Centre column-set in terminal"]]
2153
+ options += [["cc", "Centre values in cells"]]
2154
+ options += [["!r", "Toggle auto-refresh"]]
2155
+ options += [["th", "Cycle between themes. (accepts th#)"]]
2156
+ options += [["nohl", "Toggle highlights"]]
2157
+ options += [["footer", "Toggle footer"]]
2158
+ options += [["header", "Toggle header"]]
2159
+ options += [["rh", "Toggle row header"]]
2160
+ options += [["modes", "Toggle modes"]]
2161
+ options += [["ft", "Cycle through footer styles (accepts ft#)"]]
2162
+ options += [["unicode", "Toggle b/w using len and wcwidth to calculate char width."]]
2163
+ options += [[f"s{i}", f"Select col. {i}"] for i in range(len(self.items[0]))]
2164
+ options += [[f"!{i}", f"Toggle col. {i}"] for i in range(len(self.items[0]))]
2165
+ options += [["ara", "Add empty row after cursor."]]
2166
+ options += [["arb", "Add empty row before the cursor."]]
2167
+ options += [["aca", "Add empty column after the selected column."]]
2168
+ options += [["acb", "Add empty column before the selected column."]]
2169
+
2170
+
2171
+ settings_options_header = ["Key", "Setting"]
2172
+
2173
+ s, o, f = self.choose_option(self.stdscr, options=options, title="Settings", header=settings_options_header)
2174
+ if s:
2175
+ self.user_settings = " ".join([x[0] for x in s.values()])
2176
+ self.apply_settings()
2177
+
2178
+ elif self.check_key("redo", key, self.keys_dict):
2179
+ self.redo()
2180
+ # elif self.check_key("move_column_left", key, self.keys_dict):
2181
+ # tmp1 = self.column_indices[self.selected_column]
2182
+ # tmp2 = self.column_indices[(self.selected_column-1)%len(self.column_indices)]
2183
+ # self.column_indices[self.selected_column] = tmp2
2184
+ # self.column_indices[(self.selected_column-1)%(len(self.column_indices))] = tmp1
2185
+ # self.selected_column = (self.selected_column-1)%len(self.column_indices)
2186
+ # # self.notification(self.stdscr, f"{str(self.column_indices)}, {tmp1}, {tmp2}")
2187
+ # self.initialise_variables()
2188
+ # 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, max_total_width=w)
2189
+ # self.draw_screen(self.indexed_items, self.highlights)
2190
+ # # self.move_column(direction=-1)
2191
+ #
2192
+ # elif self.check_key("move_column_right", key, self.keys_dict):
2193
+ # tmp1 = self.column_indices[self.selected_column]
2194
+ # tmp2 = self.column_indices[(self.selected_column+1)%len(self.column_indices)]
2195
+ # self.column_indices[self.selected_column] = tmp2
2196
+ # self.column_indices[(self.selected_column+1)%(len(self.column_indices))] = tmp1
2197
+ # self.selected_column = (self.selected_column+1)%len(self.column_indices)
2198
+ # self.initialise_variables()
2199
+ # self.draw_screen(self.indexed_items, self.highlights)
2200
+ # # self.move_column(direction=1)
2201
+
2202
+ elif self.check_key("cursor_down", key, self.keys_dict):
2203
+ page_turned = self.cursor_down()
2204
+ if not page_turned: clear_screen = False
2205
+ elif self.check_key("half_page_down", key, self.keys_dict):
2206
+ self.cursor_down(count=self.items_per_page//2)
2207
+ clear_screen = True
2208
+ elif self.check_key("five_down", key, self.keys_dict):
2209
+ clear_screen = False
2210
+ self.cursor_down(count=5)
2211
+ clear_screen = True
2212
+ elif self.check_key("cursor_up", key, self.keys_dict):
2213
+ page_turned = self.cursor_up()
2214
+ if not page_turned: clear_screen = False
2215
+ elif self.check_key("five_up", key, self.keys_dict):
2216
+ # if self.cursor_up(count=5): clear_screen = True
2217
+ self.cursor_up(count=5)
2218
+ clear_screen = True
2219
+ elif self.check_key("half_page_up", key, self.keys_dict):
2220
+ self.cursor_up(count=self.items_per_page//2)
2221
+ clear_screen = True
2222
+
2223
+ elif self.check_key("toggle_select", key, self.keys_dict):
2224
+ if len(self.indexed_items) > 0:
2225
+ item_index = self.indexed_items[self.cursor_pos][0]
2226
+ cell_index = (self.indexed_items[self.cursor_pos][0], self.selected_column)
2227
+ row, col = cell_index
2228
+ selected_count = sum(self.selections.values())
2229
+ if self.max_selected == -1 or selected_count >= self.max_selected:
2230
+ self.toggle_item(item_index)
2231
+
2232
+ self.cell_selections[cell_index] = not self.cell_selections[cell_index]
2233
+ ## Set self.selected_cells_by_row
2234
+ # If any cells in the current row are selected
2235
+ if row in self.selected_cells_by_row:
2236
+ # If the current cell is selected then remove it
2237
+ if col in self.selected_cells_by_row[row]:
2238
+ # If the current cell is the only cell in the row that is selected then remove the row from the dict
2239
+ if len(self.selected_cells_by_row[row]) == 1:
2240
+
2241
+ del self.selected_cells_by_row[row]
2242
+ # else remove only the index of the current cell
2243
+ else:
2244
+ self.selected_cells_by_row[row].remove(col)
2245
+ # If there are cells in the row that are selected then append the current cell to the row
2246
+ else:
2247
+ self.selected_cells_by_row[row].append(col)
2248
+ # Add the a list containing only the current column
2249
+ else:
2250
+ self.selected_cells_by_row[row] = [col]
2251
+
2252
+ self.cursor_down()
2253
+ elif self.check_key("select_all", key, self.keys_dict): # Select all (m or ctrl-a)
2254
+ self.select_all()
2255
+
2256
+ elif self.check_key("select_none", key, self.keys_dict): # Deselect all (M or ctrl-r)
2257
+ self.deselect_all()
2258
+
2259
+ elif self.check_key("cursor_top", key, self.keys_dict):
2260
+ new_pos = 0
2261
+ while True:
2262
+ if new_pos in self.unselectable_indices: new_pos+=1
2263
+ else: break
2264
+ if new_pos < len(self.indexed_items):
2265
+ self.cursor_pos = new_pos
2266
+
2267
+ elif self.check_key("cursor_bottom", key, self.keys_dict):
2268
+ new_pos = len(self.indexed_items)-1
2269
+ while True:
2270
+ if new_pos in self.unselectable_indices: new_pos-=1
2271
+ else: break
2272
+ if new_pos < len(self.items) and new_pos >= 0:
2273
+ self.cursor_pos = new_pos
2274
+
2275
+ elif self.check_key("enter", key, self.keys_dict):
2276
+ self.logger.info(f"key_function enter")
2277
+ # Print the selected indices if any, otherwise print the current index
2278
+ if self.is_selecting or self.is_deselecting: self.handle_visual_selection()
2279
+ if len(self.items) == 0:
2280
+ function_data = self.get_function_data()
2281
+ function_data["last_key"] = key
2282
+ return [], "", function_data
2283
+ selected_indices = get_selected_indices(self.selections)
2284
+ if not selected_indices and len(self.indexed_items):
2285
+ selected_indices = [self.indexed_items[self.cursor_pos][0]]
2286
+
2287
+ options_sufficient = True
2288
+ usrtxt = self.user_opts
2289
+ for index in selected_indices:
2290
+ if self.require_option[index]:
2291
+ if self.option_functions[index] != None:
2292
+ options_sufficient, usrtxt = self.option_functions[index](
2293
+ stdscr=self.stdscr,
2294
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
2295
+ )
2296
+ else:
2297
+ self.set_registers()
2298
+ options_sufficient, usrtxt = default_option_input(
2299
+ self.stdscr,
2300
+ starting_value=self.user_opts,
2301
+ registers = self.registers
2302
+ )
2303
+
2304
+ if options_sufficient:
2305
+ self.user_opts = usrtxt
2306
+ self.stdscr.clear()
2307
+ self.stdscr.refresh()
2308
+ function_data = self.get_function_data()
2309
+ function_data["last_key"] = key
2310
+ return selected_indices, usrtxt, function_data
2311
+ elif self.check_key("page_down", key, self.keys_dict): # Next page
2312
+ self.cursor_pos = min(len(self.indexed_items) - 1, self.cursor_pos+self.items_per_page)
2313
+
2314
+ elif self.check_key("page_up", key, self.keys_dict):
2315
+ self.cursor_pos = max(0, self.cursor_pos-self.items_per_page)
2316
+
2317
+ elif self.check_key("redraw_screen", key, self.keys_dict):
2318
+ self.logger.info(f"key_function redraw_screen")
2319
+ self.stdscr.clear()
2320
+ self.stdscr.refresh()
2321
+ restrict_curses(self.stdscr)
2322
+ unrestrict_curses(self.stdscr)
2323
+ self.stdscr.clear()
2324
+ self.stdscr.refresh()
2325
+
2326
+ self.draw_screen(self.indexed_items, self.highlights)
2327
+
2328
+ elif self.check_key("cycle_sort_method", key, self.keys_dict):
2329
+ if self.sort_column == self.selected_column:
2330
+ self.columns_sort_method[self.sort_column] = (self.columns_sort_method[self.sort_column]+1) % len(self.SORT_METHODS)
2331
+ else:
2332
+ self.sort_column = self.selected_column
2333
+ if len(self.indexed_items) > 0:
2334
+ current_index = self.indexed_items[self.cursor_pos][0]
2335
+ 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
2336
+ self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
2337
+
2338
+ self.logger.info(f"key_function cycle_sort_method. (sort_column, sort_method) = ({self.sort_column}, {self.columns_sort_method[self.sort_column]})")
2339
+ elif self.check_key("cycle_sort_method_reverse", key, self.keys_dict): # Cycle sort method
2340
+ old_sort_column = self.sort_column
2341
+ self.sort_column = self.selected_column
2342
+ self.columns_sort_method[self.sort_column] = (self.columns_sort_method[self.sort_column]-1) % len(self.SORT_METHODS)
2343
+ if len(self.indexed_items) > 0:
2344
+ current_index = self.indexed_items[self.cursor_pos][0]
2345
+ 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
2346
+ self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
2347
+ self.logger.info(f"key_function cycle_sort_method. (sort_column, sort_method) = ({self.sort_column}, {self.columns_sort_method[self.sort_column]})")
2348
+
2349
+ elif self.check_key("cycle_sort_order", key, self.keys_dict): # Toggle sort order
2350
+ self.sort_reverse[self.sort_column] = not self.sort_reverse[self.sort_column]
2351
+ if len(self.indexed_items) > 0:
2352
+ current_index = self.indexed_items[self.cursor_pos][0]
2353
+ 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
2354
+ self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
2355
+ self.logger.info(f"key_function cycle_sort_order. (sort_column, sort_method, sort_reverse) = ({self.sort_column}, {self.columns_sort_method[self.sort_column]}, {self.sort_reverse[self.sort_column]})")
2356
+ elif self.check_key("col_select", key, self.keys_dict):
2357
+ col_index = key - ord('0')
2358
+ self.logger.info(f"key_function col_select {col_index}")
2359
+ if 0 <= col_index < len(self.items[0]):
2360
+ self.sort_column = col_index
2361
+ if len(self.indexed_items) > 0:
2362
+ current_index = self.indexed_items[self.cursor_pos][0]
2363
+ 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
2364
+ self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
2365
+ elif self.check_key("col_select_next", key, self.keys_dict):
2366
+ if len(self.items) > 0 and len(self.items[0]) > 0:
2367
+ col_index = (self.selected_column +1) % (len(self.items[0]))
2368
+ self.selected_column = col_index
2369
+ # Flash when we loop back to the first column
2370
+ # if self.selected_column == 0:
2371
+ # curses.flash()
2372
+ self.logger.info(f"key_function col_select_next {self.selected_column}")
2373
+
2374
+
2375
+ ## Scroll with column select
2376
+ rows = self.get_visible_rows()
2377
+ self.column_widths = get_column_widths(rows, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w, unicode_char_width=self.unicode_char_width)
2378
+ visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
2379
+ column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
2380
+ start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
2381
+ end_of_cell = sum(visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
2382
+ display_width = w-self.startx
2383
+ # If the full column is within the current display then don't do anything
2384
+ if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
2385
+ pass
2386
+ # Otherwise right-justify the cell
2387
+ else:
2388
+ self.leftmost_char = end_of_cell - display_width
2389
+
2390
+ self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
2391
+
2392
+ elif self.check_key("col_select_prev", key, self.keys_dict):
2393
+ if len(self.items) > 0 and len(self.items[0]) > 0:
2394
+ col_index = (self.selected_column -1) % (len(self.items[0]))
2395
+ self.selected_column = col_index
2396
+
2397
+ self.logger.info(f"key_function col_select_prev {self.selected_column}")
2398
+ # Flash when we loop back to the last column
2399
+ # if self.selected_column == len(self.column_widths)-1:
2400
+ # curses.flash()
2401
+
2402
+ ## Scroll with column select
2403
+ rows = self.get_visible_rows()
2404
+ self.column_widths = get_column_widths(rows, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w, unicode_char_width=self.unicode_char_width)
2405
+ visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
2406
+ column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
2407
+ start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
2408
+ end_of_cell = sum(visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
2409
+ display_width = w-self.startx
2410
+
2411
+ # If the entire column is within the current display then don't do anything
2412
+ if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
2413
+ pass
2414
+ # Otherwise left-justify the cell
2415
+ else:
2416
+ self.leftmost_char = start_of_cell
2417
+
2418
+ self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
2419
+
2420
+ elif self.check_key("scroll_right", key, self.keys_dict):
2421
+ self.logger.info(f"key_function scroll_right")
2422
+ if len(self.indexed_items):
2423
+ row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
2424
+ if row_width-self.leftmost_char >= w-self.startx:
2425
+ self.leftmost_char = self.leftmost_char+5
2426
+
2427
+ elif self.check_key("scroll_left", key, self.keys_dict):
2428
+ self.logger.info(f"key_function scroll_left")
2429
+ self.leftmost_char = max(self.leftmost_char-5, 0)
2430
+
2431
+ elif self.check_key("scroll_far_left", key, self.keys_dict):
2432
+ self.logger.info(f"key_function scroll_far_left")
2433
+ self.leftmost_char = 0
2434
+ self.selected_column = 0
2435
+
2436
+ elif self.check_key("scroll_far_right", key, self.keys_dict):
2437
+ self.logger.info(f"key_function scroll_far_right")
2438
+ longest_row_str_len = 0
2439
+ rows = self.get_visible_rows()
2440
+ for i in range(len(rows)):
2441
+ item = rows[i]
2442
+ row_str = format_row(item, self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
2443
+ if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
2444
+ # for i in range(len(self.indexed_items)):
2445
+ # item = self.indexed_items[i]
2446
+ # row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)
2447
+ # if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
2448
+ # self.notification(self.stdscr, f"{longest_row_str_len}")
2449
+ self.leftmost_char = max(0, longest_row_str_len-w+2+self.startx)
2450
+ if len(self.items):
2451
+ self.selected_column = len(self.items[0])-1
2452
+
2453
+ elif self.check_key("add_column_before", key, self.keys_dict):
2454
+ self.logger.info(f"key_function add_column_before")
2455
+ # self.add_column_before()
2456
+ self.insert_column(self.selected_column)
2457
+
2458
+ elif self.check_key("add_column_after", key, self.keys_dict):
2459
+ self.logger.info(f"key_function add_column_after")
2460
+ # self.add_column_after()
2461
+ self.insert_column(self.selected_column+1)
2462
+
2463
+ elif self.check_key("add_row_before", key, self.keys_dict):
2464
+ self.logger.info(f"key_function add_row_before")
2465
+ # self.add_row_before()
2466
+ self.insert_row(self.cursor_pos)
2467
+
2468
+ elif self.check_key("add_row_after", key, self.keys_dict):
2469
+ self.logger.info(f"key_function add_row_after")
2470
+ # self.add_row_after()
2471
+ self.insert_row(self.cursor_pos+1)
2472
+
2473
+ elif self.check_key("col_hide", key, self.keys_dict):
2474
+ self.logger.info(f"key_function col_hide")
2475
+ d = {'!': 0, '@': 1, '#': 2, '$': 3, '%': 4, '^': 5, '&': 6, '*': 7, '(': 8, ')': 9}
2476
+ d = {s:i for i,s in enumerate(")!@#$%^&*(")}
2477
+ col_index = d[chr(key)]
2478
+ self.toggle_column_visibility(col_index)
2479
+ elif self.check_key("copy", key, self.keys_dict):
2480
+ self.copy_dialogue()
2481
+ elif self.check_key("paste", key, self.keys_dict):
2482
+ self.paste_dialogue()
2483
+ elif self.check_key("save", key, self.keys_dict):
2484
+ self.save_dialog()
2485
+ elif self.check_key("load", key, self.keys_dict):
2486
+ self.load_dialog()
2487
+
2488
+ elif self.check_key("delete", key, self.keys_dict): # Delete key
2489
+ self.delete_entries()
2490
+
2491
+ elif self.check_key("delete_column", key, self.keys_dict): # Delete key
2492
+ self.logger.info(f"key_function delete_column")
2493
+ row_len = 1
2494
+ if self.header: row_len = len(self.header)
2495
+ elif len(self.items): row_len = len(self.items[0])
2496
+ if row_len > 1:
2497
+ self.items = [row[:self.selected_column] + row[self.selected_column+1:] for row in self.items]
2498
+ self.header = self.header[:self.selected_column] + self.header[self.selected_column+1:]
2499
+ self.editable_columns = self.editable_columns[:self.selected_column] + self.editable_columns[self.selected_column+1:]
2500
+ self.selected_column = min(self.selected_column, row_len-2)
2501
+ elif row_len == 1:
2502
+ self.items = [[""] for _ in range(len(self.items))]
2503
+ self.header = [""] if self.header else []
2504
+ self.editable_columns = []
2505
+ self.selected_column = min(self.selected_column, row_len-2)
2506
+ self.initialise_variables()
2507
+
2508
+
2509
+
2510
+
2511
+ # elif self.check_key("increase_lines_per_page", key, self.keys_dict):
2512
+ # self.items_per_page += 1
2513
+ # elif self.check_key("decrease_lines_per_page", key, self.keys_dict):
2514
+ # if self.items_per_page > 1:
2515
+ # self.items_per_page -= 1
2516
+
2517
+ elif self.check_key("decrease_column_width", key, self.keys_dict):
2518
+ self.logger.info(f"key_function decrease_column_width")
2519
+ if self.max_column_width > 10:
2520
+ self.max_column_width -= 10
2521
+ # self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=2)
2522
+ elif self.check_key("increase_column_width", key, self.keys_dict):
2523
+ self.logger.info(f"key_function increase_column_width")
2524
+ if self.max_column_width < 1000:
2525
+ self.max_column_width += 10
2526
+ # self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w)
2527
+ elif self.check_key("visual_selection_toggle", key, self.keys_dict):
2528
+ self.logger.info(f"key_function visual_selection_toggle")
2529
+ self.handle_visual_selection()
2530
+
2531
+ elif self.check_key("visual_deselection_toggle", key, self.keys_dict):
2532
+ self.logger.info(f"key_function visual_deselection_toggle")
2533
+ self.handle_visual_selection(selecting=False)
2534
+
2535
+ elif key == curses.KEY_RESIZE: # Terminal resize signal
2536
+
2537
+ self.calculate_section_sizes()
2538
+ self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w, unicode_char_width=self.unicode_char_width)
2539
+
2540
+
2541
+
2542
+ elif self.check_key("filter_input", key, self.keys_dict):
2543
+ self.logger.info(f"key_function filter_input")
2544
+ usrtxt = f"{self.filter_query} " if self.filter_query else ""
2545
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
2546
+ if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
2547
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
2548
+ self.set_registers()
2549
+ words = self.get_word_list()
2550
+ usrtxt, return_val = input_field(
2551
+ self.stdscr,
2552
+ usrtxt=usrtxt,
2553
+ field_prefix=" Filter: ",
2554
+ x=lambda:2,
2555
+ y=lambda: self.stdscr.getmaxyx()[0]-2,
2556
+ # max_length=field_end,
2557
+ max_length=field_end_f,
2558
+ registers=self.registers,
2559
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
2560
+ history=self.history_filter_and_search,
2561
+ path_auto_complete=True,
2562
+ formula_auto_complete=False,
2563
+ function_auto_complete=False,
2564
+ word_auto_complete=True,
2565
+ auto_complete_words=words,
2566
+ )
2567
+ if return_val:
2568
+ self.filter_query = usrtxt
2569
+ self.history_filter_and_search.append(usrtxt)
2570
+
2571
+ # If the current mode filter has been changed then go back to the first mode
2572
+ if self.modes and "filter" in self.modes[self.mode_index] and self.modes[self.mode_index]["filter"] not in self.filter_query:
2573
+ self.mode_index = 0
2574
+ # elif "filter" in modes[mode_index] and modes[mode_index]["filter"] in filter_query:
2575
+ # filter_query.split(modes[mode_index]["filter"])
2576
+
2577
+ prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
2578
+ self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
2579
+ 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)
2580
+ else: new_index = 0
2581
+ self.cursor_pos = new_index
2582
+ # Re-sort self.items after applying filter
2583
+ if self.columns_sort_method[self.selected_column] != 0:
2584
+ 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
2585
+
2586
+ elif self.check_key("search_input", key, self.keys_dict):
2587
+ self.logger.info(f"key_function search_input")
2588
+ usrtxt = f"{self.search_query} " if self.search_query else ""
2589
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
2590
+ if self.show_footer and self.footer.height >= 3: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
2591
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
2592
+ self.set_registers()
2593
+ words = self.get_word_list()
2594
+ usrtxt, return_val = input_field(
2595
+ self.stdscr,
2596
+ usrtxt=usrtxt,
2597
+ field_prefix=" Search: ",
2598
+ x=lambda:2,
2599
+ y=lambda: self.stdscr.getmaxyx()[0]-3,
2600
+ max_length=field_end_f,
2601
+ registers=self.registers,
2602
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
2603
+ history=self.history_filter_and_search,
2604
+ path_auto_complete=True,
2605
+ formula_auto_complete=False,
2606
+ function_auto_complete=False,
2607
+ word_auto_complete=True,
2608
+ auto_complete_words=words,
2609
+ )
2610
+ if return_val:
2611
+ self.search_query = usrtxt
2612
+ self.history_filter_and_search.append(usrtxt)
2613
+ return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
2614
+ query=self.search_query,
2615
+ indexed_items=self.indexed_items,
2616
+ highlights=self.highlights,
2617
+ cursor_pos=self.cursor_pos,
2618
+ unselectable_indices=self.unselectable_indices,
2619
+ )
2620
+ if return_val:
2621
+ self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
2622
+ else:
2623
+ self.search_index, self.search_count = 0, 0
2624
+
2625
+ elif self.check_key("continue_search_forward", key, self.keys_dict):
2626
+ self.logger.info(f"key_function continue_search_forward")
2627
+ return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
2628
+ query=self.search_query,
2629
+ indexed_items=self.indexed_items,
2630
+ highlights=self.highlights,
2631
+ cursor_pos=self.cursor_pos,
2632
+ unselectable_indices=self.unselectable_indices,
2633
+ continue_search=True,
2634
+ )
2635
+ if return_val:
2636
+ self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
2637
+ elif self.check_key("continue_search_backward", key, self.keys_dict):
2638
+ self.logger.info(f"key_function continue_search_backward")
2639
+ return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
2640
+ query=self.search_query,
2641
+ indexed_items=self.indexed_items,
2642
+ highlights=self.highlights,
2643
+ cursor_pos=self.cursor_pos,
2644
+ unselectable_indices=self.unselectable_indices,
2645
+ continue_search=True,
2646
+ reverse=True,
2647
+ )
2648
+ if return_val:
2649
+ self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
2650
+ elif self.check_key("cancel", key, self.keys_dict): # ESC key
2651
+ # order of escapes:
2652
+ # 1. selecting/deslecting
2653
+ # 2. search
2654
+ # 3. filter
2655
+ # 4. if self.cancel_is_back (e.g., notification) then we exit
2656
+ # 4. selecting
2657
+
2658
+ # Cancel visual de/selection
2659
+ if self.is_selecting or self.is_deselecting:
2660
+ self.start_selection = -1
2661
+ self.end_selection = -1
2662
+ self.is_selecting = False
2663
+ self.is_deselecting = False
2664
+ # Cancel search
2665
+ elif self.search_query:
2666
+ self.search_query = ""
2667
+ self.highlights = [highlight for highlight in self.highlights if "type" not in highlight or highlight["type"] != "search" ]
2668
+ # Remove filter
2669
+ elif self.filter_query:
2670
+ if self.modes and "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"]:
2671
+ self.filter_query = self.modes[self.mode_index]["filter"]
2672
+ # elif "filter" in modes[mode_index]:
2673
+ else:
2674
+ self.filter_query = ""
2675
+ self.mode_index = 0
2676
+ prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
2677
+ self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
2678
+ 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)
2679
+ else: new_index = 0
2680
+ self.cursor_pos = new_index
2681
+ # Re-sort self.items after applying filter
2682
+ if self.columns_sort_method[self.selected_column] != 0:
2683
+ 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
2684
+ elif self.cancel_is_back:
2685
+ function_data = self.get_function_data()
2686
+ function_data["last_key"] = key
2687
+ return [], "escape", function_data
2688
+
2689
+
2690
+ # else:
2691
+ # self.search_query = ""
2692
+ # self.mode_index = 0
2693
+ # self.highlights = [highlight for highlight in self.highlights if "type" not in highlight or highlight["type"] != "search" ]
2694
+ # continue
2695
+
2696
+ elif self.check_key("opts_input", key, self.keys_dict):
2697
+ self.logger.info(f"key_function opts_input")
2698
+ usrtxt = f"{self.user_opts} " if self.user_opts else ""
2699
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
2700
+ if self.show_footer and self.footer.height >= 1: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
2701
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
2702
+ self.set_registers()
2703
+ words = self.get_word_list()
2704
+ usrtxt, return_val = input_field(
2705
+ self.stdscr,
2706
+ usrtxt=usrtxt,
2707
+ field_prefix=" Opts: ",
2708
+ x=lambda:2,
2709
+ y=lambda: self.stdscr.getmaxyx()[0]-1,
2710
+ max_length=field_end_f,
2711
+ registers=self.registers,
2712
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
2713
+ history=self.history_opts,
2714
+ path_auto_complete=True,
2715
+ formula_auto_complete=False,
2716
+ function_auto_complete=True,
2717
+ word_auto_complete=True,
2718
+ auto_complete_words=words,
2719
+ )
2720
+ if return_val:
2721
+ self.user_opts = usrtxt
2722
+ self.history_opts.append(usrtxt)
2723
+ elif self.check_key("opts_select", key, self.keys_dict):
2724
+ self.logger.info(f"key_function opts_select")
2725
+ s, o, f = self.choose_option(self.stdscr, self.options_list)
2726
+ if self.user_opts.strip(): self.user_opts += " "
2727
+ self.user_opts += " ".join([x for x in s.values()])
2728
+ elif self.check_key("notification_toggle", key, self.keys_dict):
2729
+ self.logger.info(f"key_function notification_toggle")
2730
+ self.notification(self.stdscr, colours_end=self.colours_end)
2731
+
2732
+ elif self.check_key("mode_next", key, self.keys_dict): # tab key
2733
+ self.logger.info(f"key_function mode_next")
2734
+ # apply setting
2735
+ prev_mode_index = self.mode_index
2736
+ self.mode_index = (self.mode_index+1)%len(self.modes)
2737
+ mode = self.modes[self.mode_index]
2738
+ for key, val in mode.items():
2739
+ if key == 'filter':
2740
+ if 'filter' in self.modes[prev_mode_index]:
2741
+ self.filter_query = self.filter_query.replace(self.modes[prev_mode_index]['filter'], '')
2742
+ self.filter_query = f"{self.filter_query.strip()} {val.strip()}".strip()
2743
+ prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
2744
+
2745
+ self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
2746
+ 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)
2747
+ else: new_index = 0
2748
+ self.cursor_pos = new_index
2749
+ # Re-sort self.items after applying filter
2750
+ 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
2751
+ elif self.check_key("mode_prev", key, self.keys_dict): # shift+tab key
2752
+ self.logger.info(f"key_function mode_prev")
2753
+ # apply setting
2754
+ prev_mode_index = self.mode_index
2755
+ self.mode_index = (self.mode_index-1)%len(self.modes)
2756
+ mode = self.modes[self.mode_index]
2757
+ for key, val in mode.items():
2758
+ if key == 'filter':
2759
+ if 'filter' in self.modes[prev_mode_index]:
2760
+ self.filter_query = self.filter_query.replace(self.modes[prev_mode_index]['filter'], '')
2761
+ self.filter_query = f"{self.filter_query.strip()} {val.strip()}".strip()
2762
+ prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
2763
+ self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
2764
+ 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)
2765
+ else: new_index = 0
2766
+ self.cursor_pos = new_index
2767
+ # Re-sort self.items after applying filter
2768
+ 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
2769
+ elif self.check_key("pipe_input", key, self.keys_dict):
2770
+ self.logger.info(f"key_function pipe_input")
2771
+ # usrtxt = "xargs -d '\n' -I{} "
2772
+ usrtxt = "xargs "
2773
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
2774
+ if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
2775
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
2776
+ self.set_registers()
2777
+
2778
+ # Get list of available shell commands
2779
+ try:
2780
+ # result = subprocess.run(['compgen', '-c'], capture_output=True, text=True, check=True)
2781
+ # shell_commands = result.stdout.splitlines()
2782
+ result = subprocess.run(['ls', '/usr/bin'], capture_output=True, text=True, check=True)
2783
+ shell_commands = result.stdout.splitlines()
2784
+ except:
2785
+ shell_commands = []
2786
+ usrtxt, return_val = input_field(
2787
+ self.stdscr,
2788
+ usrtxt=usrtxt,
2789
+ field_prefix=" Command: ",
2790
+ x=lambda:2,
2791
+ y=lambda: self.stdscr.getmaxyx()[0]-2,
2792
+ literal=True,
2793
+ max_length=field_end_f,
2794
+ registers=self.registers,
2795
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
2796
+ history=self.history_pipes,
2797
+ path_auto_complete=True,
2798
+ formula_auto_complete=False,
2799
+ function_auto_complete=False,
2800
+ word_auto_complete=True,
2801
+ auto_complete_words=shell_commands,
2802
+ )
2803
+
2804
+ if return_val:
2805
+ selected_indices = get_selected_indices(self.selections)
2806
+ self.history_pipes.append(usrtxt)
2807
+ if not selected_indices:
2808
+ selected_indices = [self.indexed_items[self.cursor_pos][0]]
2809
+
2810
+ full_values = [format_row_full(self.items[i], self.hidden_columns) for i in selected_indices] # Use format_row_full for full data
2811
+ full_values = [self.items[i][self.selected_column] for i in selected_indices]
2812
+ if full_values:
2813
+ command = usrtxt.split()
2814
+ # command = ['xargs', '-d' , '"\n"' '-I', '{}', 'mpv', '{}']
2815
+ # command = ['xargs', '-d' , '"\n"' '-I', '{}', 'mpv', '{}']
2816
+ # command = "xargs -d '\n' -I{} mpv {}"
2817
+
2818
+ try:
2819
+ process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
2820
+
2821
+ if process.stdin != None:
2822
+ for value in full_values:
2823
+ process.stdin.write((repr(value) + '\n').encode())
2824
+
2825
+ process.stdin.close()
2826
+
2827
+ self.notification(self.stdscr, message=f"{len(full_values)} strings piped to {repr(usrtxt)}")
2828
+ except Exception as e:
2829
+ self.notification(self.stdscr, message=f"{e}")
2830
+
2831
+
2832
+ elif self.check_key("open", key, self.keys_dict):
2833
+ self.logger.info(f"key_function open")
2834
+ selected_indices = get_selected_indices(self.selections)
2835
+ if not selected_indices:
2836
+ selected_indices = [self.indexed_items[self.cursor_pos][0]]
2837
+
2838
+ file_names = [self.items[i][self.selected_column] for i in selected_indices]
2839
+ response = openFiles(file_names)
2840
+ if response:
2841
+ self.notification(self.stdscr, message=response)
2842
+
2843
+
2844
+ elif self.check_key("reset_opts", key, self.keys_dict):
2845
+ self.logger.info(f"key_function reset_opts")
2846
+ self.user_opts = ""
2847
+
2848
+ elif self.check_key("edit", key, self.keys_dict):
2849
+ self.logger.info(f"key_function edit")
2850
+ if len(self.indexed_items) > 0 and self.selected_column >=0 and self.editable_columns[self.selected_column]:
2851
+ current_val = self.indexed_items[self.cursor_pos][1][self.selected_column]
2852
+ usrtxt = f"{current_val}"
2853
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
2854
+ if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
2855
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
2856
+ self.set_registers()
2857
+ words = self.get_word_list()
2858
+ usrtxt, return_val = input_field(
2859
+ self.stdscr,
2860
+ usrtxt=usrtxt,
2861
+ field_prefix=" Edit value: ",
2862
+ x=lambda:2,
2863
+ y=lambda: self.stdscr.getmaxyx()[0]-2,
2864
+ max_length=field_end_f,
2865
+ registers=self.registers,
2866
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
2867
+ history = self.history_edits,
2868
+ path_auto_complete=True,
2869
+ formula_auto_complete=True,
2870
+ function_auto_complete=True,
2871
+ word_auto_complete=True,
2872
+ auto_complete_words=words,
2873
+ )
2874
+ if return_val:
2875
+ if usrtxt.startswith("```"):
2876
+ usrtxt = str(eval(usrtxt[3:]))
2877
+ self.indexed_items[self.cursor_pos][1][self.selected_column] = usrtxt
2878
+ self.history_edits.append(usrtxt)
2879
+
2880
+ elif self.check_key("edit_picker", key, self.keys_dict):
2881
+ self.logger.info(f"key_function edit_picker")
2882
+ if len(self.indexed_items) > 0 and self.selected_column >=0 and self.editable_columns[self.selected_column]:
2883
+ current_val = self.indexed_items[self.cursor_pos][1][self.selected_column]
2884
+ usrtxt = f"{current_val}"
2885
+ field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
2886
+ if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
2887
+ else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
2888
+ self.set_registers()
2889
+ words = self.get_word_list()
2890
+ usrtxt, return_val = input_field(
2891
+ self.stdscr,
2892
+ usrtxt=usrtxt,
2893
+ field_prefix=" Edit value: ",
2894
+ x=lambda:2,
2895
+ y=lambda: self.stdscr.getmaxyx()[0]-2,
2896
+ max_length=field_end_f,
2897
+ registers=self.registers,
2898
+ refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
2899
+ history = self.history_edits,
2900
+ path_auto_complete=True,
2901
+ formula_auto_complete=True,
2902
+ function_auto_complete=True,
2903
+ word_auto_complete=True,
2904
+ auto_complete_words=words,
2905
+ )
2906
+ if return_val:
2907
+ self.indexed_items[self.cursor_pos][1][self.selected_column] = usrtxt
2908
+ self.history_edits.append(usrtxt)
2909
+ elif self.check_key("edit_ipython", key, self.keys_dict):
2910
+ self.logger.info(f"key_function edit_picker")
2911
+ import IPython
2912
+ self.stdscr.clear()
2913
+ restrict_curses(self.stdscr)
2914
+ self.stdscr.clear()
2915
+ os.system('cls' if os.name == 'nt' else 'clear')
2916
+ globals()['self'] = self # make the instance available in IPython namespace
2917
+
2918
+ from traitlets.config import Config
2919
+ c = Config()
2920
+ # Doesn't work; Config only works with start_ipython, not embed... but start_ipython causes errors
2921
+ # c.InteractiveShellApp.exec_lines = [
2922
+ # '%clear'
2923
+ # ]
2924
+ msg = "The active Picker object has variable name self.\n"
2925
+ msg += "\te.g., self.items will display the items in Picker"
2926
+ IPython.embed(header=msg, config=c)
2927
+
2928
+ unrestrict_curses(self.stdscr)
2929
+
2930
+ self.stdscr.clear()
2931
+ self.stdscr.refresh()
2932
+ self.initialise_variables()
2933
+
2934
+
2935
+ # The refresh symbol colour is not updated when the data is retrieved so remains white until a key is pressed.
2936
+ # if key != -1:
2937
+ # self.draw_screen(self.indexed_items, self.highlights, clear=clear_screen)
2938
+
2939
+
2940
+ self.draw_screen(self.indexed_items, self.highlights, clear=clear_screen)
2941
+
2942
+
2943
+
2944
+ def set_colours(pick: int = 0, start: int = 0) -> Optional[int]:
2945
+ """ Initialise curses colour pairs from dictionary. """
2946
+
2947
+
2948
+ global COLOURS_SET, notification_colours, help_colours
2949
+ if COLOURS_SET: return None
2950
+ if start == None: start = 0
2951
+
2952
+
2953
+ if curses.COLORS >= 255:
2954
+ colours = get_colours(pick)
2955
+ notification_colours = get_notification_colours(pick)
2956
+ help_colours = get_help_colours(pick)
2957
+ standard_colours_start, notification_colours_start, help_colours_start = 0, 50, 100
2958
+ else:
2959
+ colours = get_fallback_colours()
2960
+ notification_colours = get_fallback_colours()
2961
+ help_colours = get_fallback_colours()
2962
+ standard_colours_start, help_colours_start, notification_colours_start = 0, 0, 0
2963
+
2964
+ if not colours: return 0
2965
+
2966
+ try:
2967
+ start = standard_colours_start
2968
+ curses.init_pair(start+1, colours['selected_fg'], colours['selected_bg'])
2969
+ curses.init_pair(start+2, colours['unselected_fg'], colours['unselected_bg'])
2970
+ curses.init_pair(start+3, colours['normal_fg'], colours['background'])
2971
+ curses.init_pair(start+4, colours['header_fg'], colours['header_bg'])
2972
+ curses.init_pair(start+5, colours['cursor_fg'], colours['cursor_bg'])
2973
+ curses.init_pair(start+6, colours['normal_fg'], colours['background'])
2974
+ curses.init_pair(start+7, colours['error_fg'], colours['error_bg'])
2975
+ curses.init_pair(start+8, colours['complete_fg'], colours['complete_bg'])
2976
+ curses.init_pair(start+9, colours['active_fg'], colours['active_bg'])
2977
+ curses.init_pair(start+10, colours['search_fg'], colours['search_bg'])
2978
+ curses.init_pair(start+11, colours['waiting_fg'], colours['waiting_bg'])
2979
+ curses.init_pair(start+12, colours['paused_fg'], colours['paused_bg'])
2980
+ curses.init_pair(start+13, colours['active_input_fg'], colours['active_input_bg'])
2981
+ curses.init_pair(start+14, colours['modes_selected_fg'], colours['modes_selected_bg'])
2982
+ curses.init_pair(start+15, colours['modes_unselected_fg'], colours['modes_unselected_bg'])
2983
+ curses.init_pair(start+16, colours['title_fg'], colours['title_bg'])
2984
+ curses.init_pair(start+17, colours['normal_fg'], colours['title_bar'])
2985
+ curses.init_pair(start+18, colours['normal_fg'], colours['scroll_bar_bg'])
2986
+ curses.init_pair(start+19, colours['selected_header_column_fg'], colours['selected_header_column_bg'])
2987
+ curses.init_pair(start+20, colours['footer_fg'], colours['footer_bg'])
2988
+ curses.init_pair(start+21, colours['refreshing_fg'], colours['refreshing_bg'])
2989
+ curses.init_pair(start+22, colours['40pc_fg'], colours['40pc_bg'])
2990
+ curses.init_pair(start+23, colours['refreshing_inactive_fg'], colours['refreshing_inactive_bg'])
2991
+ curses.init_pair(start+24, colours['footer_string_fg'], colours['footer_string_bg'])
2992
+ curses.init_pair(start+25, colours['selected_cell_fg'], colours['selected_cell_bg'])
2993
+ curses.init_pair(start+26, colours['deselecting_cell_fg'], colours['deselecting_cell_bg'])
2994
+
2995
+
2996
+ # notifications 50, infobox 100, help 150
2997
+ # Notification colours
2998
+ colours = notification_colours
2999
+ start = notification_colours_start
3000
+ curses.init_pair(start+1, colours['selected_fg'], colours['selected_bg'])
3001
+ curses.init_pair(start+2, colours['unselected_fg'], colours['unselected_bg'])
3002
+ curses.init_pair(start+3, colours['normal_fg'], colours['background'])
3003
+ curses.init_pair(start+4, colours['header_fg'], colours['header_bg'])
3004
+ curses.init_pair(start+5, colours['cursor_fg'], colours['cursor_bg'])
3005
+ curses.init_pair(start+6, colours['normal_fg'], colours['background'])
3006
+ curses.init_pair(start+7, colours['error_fg'], colours['error_bg'])
3007
+ curses.init_pair(start+8, colours['complete_fg'], colours['complete_bg'])
3008
+ curses.init_pair(start+9, colours['active_fg'], colours['active_bg'])
3009
+ curses.init_pair(start+10, colours['search_fg'], colours['search_bg'])
3010
+ curses.init_pair(start+11, colours['waiting_fg'], colours['waiting_bg'])
3011
+ curses.init_pair(start+12, colours['paused_fg'], colours['paused_bg'])
3012
+ curses.init_pair(start+13, colours['active_input_fg'], colours['active_input_bg'])
3013
+ curses.init_pair(start+14, colours['modes_selected_fg'], colours['modes_selected_bg'])
3014
+ curses.init_pair(start+15, colours['modes_unselected_fg'], colours['modes_unselected_bg'])
3015
+ curses.init_pair(start+16, colours['title_fg'], colours['title_bg'])
3016
+ curses.init_pair(start+17, colours['normal_fg'], colours['title_bar'])
3017
+ curses.init_pair(start+18, colours['normal_fg'], colours['scroll_bar_bg'])
3018
+ curses.init_pair(start+19, colours['selected_header_column_fg'], colours['selected_header_column_bg'])
3019
+ curses.init_pair(start+20, colours['footer_fg'], colours['footer_bg'])
3020
+ curses.init_pair(start+21, colours['refreshing_fg'], colours['refreshing_bg'])
3021
+ curses.init_pair(start+22, colours['40pc_fg'], colours['40pc_bg'])
3022
+ curses.init_pair(start+23, colours['refreshing_inactive_fg'], colours['refreshing_inactive_bg'])
3023
+ curses.init_pair(start+24, colours['footer_string_fg'], colours['footer_string_bg'])
3024
+ curses.init_pair(start+25, colours['selected_cell_fg'], colours['selected_cell_bg'])
3025
+ curses.init_pair(start+26, colours['deselecting_cell_fg'], colours['deselecting_cell_bg'])
3026
+
3027
+ # Help
3028
+ colours = help_colours
3029
+ start = help_colours_start
3030
+ curses.init_pair(start+1, colours['selected_fg'], colours['selected_bg'])
3031
+ curses.init_pair(start+2, colours['unselected_fg'], colours['unselected_bg'])
3032
+ curses.init_pair(start+3, colours['normal_fg'], colours['background'])
3033
+ curses.init_pair(start+4, colours['header_fg'], colours['header_bg'])
3034
+ curses.init_pair(start+5, colours['cursor_fg'], colours['cursor_bg'])
3035
+ curses.init_pair(start+6, colours['normal_fg'], colours['background'])
3036
+ curses.init_pair(start+7, colours['error_fg'], colours['error_bg'])
3037
+ curses.init_pair(start+8, colours['complete_fg'], colours['complete_bg'])
3038
+ curses.init_pair(start+9, colours['active_fg'], colours['active_bg'])
3039
+ curses.init_pair(start+10, colours['search_fg'], colours['search_bg'])
3040
+ curses.init_pair(start+11, colours['waiting_fg'], colours['waiting_bg'])
3041
+ curses.init_pair(start+12, colours['paused_fg'], colours['paused_bg'])
3042
+ curses.init_pair(start+13, colours['active_input_fg'], colours['active_input_bg'])
3043
+ curses.init_pair(start+14, colours['modes_selected_fg'], colours['modes_selected_bg'])
3044
+ curses.init_pair(start+15, colours['modes_unselected_fg'], colours['modes_unselected_bg'])
3045
+ curses.init_pair(start+16, colours['title_fg'], colours['title_bg'])
3046
+ curses.init_pair(start+17, colours['normal_fg'], colours['title_bar'])
3047
+ curses.init_pair(start+18, colours['normal_fg'], colours['scroll_bar_bg'])
3048
+ curses.init_pair(start+19, colours['selected_header_column_fg'], colours['selected_header_column_bg'])
3049
+ curses.init_pair(start+20, colours['footer_fg'], colours['footer_bg'])
3050
+ curses.init_pair(start+21, colours['refreshing_fg'], colours['refreshing_bg'])
3051
+ curses.init_pair(start+22, colours['40pc_fg'], colours['40pc_bg'])
3052
+ curses.init_pair(start+23, colours['refreshing_inactive_fg'], colours['refreshing_inactive_bg'])
3053
+ curses.init_pair(start+24, colours['footer_string_fg'], colours['footer_string_bg'])
3054
+ curses.init_pair(start+25, colours['selected_cell_fg'], colours['selected_cell_bg'])
3055
+ curses.init_pair(start+26, colours['deselecting_cell_fg'], colours['deselecting_cell_bg'])
3056
+ except:
3057
+ pass
3058
+ COLOURS_SET = True
3059
+ return start+21
3060
+
3061
+ def parse_arguments() -> Tuple[argparse.Namespace, dict]:
3062
+ """ Parse command line arguments. """
3063
+ parser = argparse.ArgumentParser(description='Convert table to list of lists.')
3064
+ # parser.add_argument('filename', type=str, help='The file to process')
3065
+ parser.add_argument('-i', dest='file', help='File containing the table to be converted.')
3066
+ parser.add_argument('--load', '-l', dest='load', type=str, help='Load file from Picker dump.')
3067
+ parser.add_argument('--stdin', dest='stdin', action='store_true', help='Table passed on stdin')
3068
+ parser.add_argument('--stdin2', action='store_true', help='Table passed on stdin')
3069
+ parser.add_argument('--generate', '-g', type=str, help='Pass file to generate data for listpick Picker.')
3070
+ parser.add_argument('-d', dest='delimiter', default='\t', help='Delimiter for rows in the table (default: tab)')
3071
+ parser.add_argument('-t', dest='file_type', choices=['tsv', 'csv', 'json', 'xlsx', 'ods', 'pkl'], help='Type of file (tsv, csv, json, xlsx, ods)')
3072
+ parser.add_argument('--debug', action="store_true", help="Enable debug log.")
3073
+ parser.add_argument('--debug-verbose', action="store_true", help="Enable debug verbose log.")
3074
+ args = parser.parse_args()
3075
+
3076
+
3077
+ function_data = {
3078
+ "items" : [],
3079
+ "header": [],
3080
+ "unselectable_indices" : [],
3081
+ "colours": get_colours(0),
3082
+ "top_gap": 0,
3083
+ "max_column_width": 70,
3084
+ }
3085
+
3086
+ if args.file:
3087
+ input_arg = args.file
3088
+ elif args.stdin:
3089
+ input_arg = '--stdin'
3090
+ elif args.stdin2:
3091
+ input_arg = '--stdin2'
3092
+ # elif args.filename:
3093
+ # input_arg = args.filename
3094
+
3095
+ elif args.generate:
3096
+ function_data["refresh_function"] = lambda : generate_picker_data(args.generate)
3097
+ function_data["get_data_startup"] = True
3098
+ function_data["get_new_data"] = True
3099
+ return args, function_data
3100
+ elif args.load:
3101
+ function_data = load_state(args.load)
3102
+ function_data["refresh_function"] = lambda : (load_state(args.load)["items"], load_state(args.load)["header"])
3103
+ function_data["get_new_data"] = True
3104
+ return args, function_data
3105
+
3106
+ else:
3107
+ # print("Error: Please provide input file or use --stdin flag.")
3108
+ print("No data provided. Loading empty Picker.")
3109
+ return args, function_data
3110
+ # sys.exit(1)
3111
+ if args.debug:
3112
+ function_data["debug"] = True
3113
+ function_data["debug_level"] = 1
3114
+
3115
+ if args.debug_verbose:
3116
+ function_data["debug"] = True
3117
+ function_data["debug_level"] = 0
3118
+
3119
+ if not args.file_type:
3120
+ filetype = guess_file_type(input_arg)
3121
+ else:
3122
+ filetype = args.file_type
3123
+
3124
+
3125
+ items, header = table_to_list(input_arg, args.delimiter, filetype)
3126
+ function_data["items"] = items
3127
+ if header: function_data["header"] = header
3128
+ return args, function_data
3129
+
3130
+ def start_curses() -> curses.window:
3131
+ """ Initialise curses and return curses window. """
3132
+ stdscr = curses.initscr()
3133
+ curses.start_color()
3134
+ curses.use_default_colors() # For terminal theme-recolouring
3135
+ curses.noecho() # Turn off automatic echoing of keys to the screen
3136
+ curses.cbreak() # Interpret keystrokes immediately (without requiring Enter)
3137
+ stdscr.keypad(True) # Ensures that arrow and function keys are received as one key by getch
3138
+ curses.raw() # Disable control keys (ctrl-c, ctrl-s, ctrl-q, etc.)
3139
+ curses.curs_set(False)
3140
+
3141
+ return stdscr
3142
+
3143
+ def close_curses(stdscr: curses.window) -> None:
3144
+ """ Close curses. """
3145
+ stdscr.keypad(False)
3146
+ curses.nocbreak()
3147
+ curses.noraw()
3148
+ curses.echo()
3149
+ curses.endwin()
3150
+
3151
+ def restrict_curses(stdscr: curses.window) -> None:
3152
+ """ Restrict curses for normal input. Used when dropping to ipython. """
3153
+ stdscr.keypad(False)
3154
+ curses.nocbreak()
3155
+ curses.noraw()
3156
+ curses.curs_set(True)
3157
+ curses.echo()
3158
+
3159
+ def unrestrict_curses(stdscr: curses.window) -> None:
3160
+ """ Unrestrict curses for terminal input. Used after dropping to ipython. """
3161
+ curses.noecho() # Turn off automatic echoing of keys to the screen
3162
+ curses.cbreak() # Interpret keystrokes immediately (without requiring Enter)
3163
+ stdscr.keypad(True)
3164
+ curses.raw() # Disable control keys (ctrl-c, ctrl-s, ctrl-q, etc.)
3165
+ curses.curs_set(False)
3166
+
3167
+ def main() -> None:
3168
+ """ Main function when listpick is executed. Deals with command line arguments and starts a Picker. """
3169
+ args, function_data = parse_arguments()
3170
+
3171
+ try:
3172
+ if function_data["items"] == []:
3173
+ function_data["items"] = test_items
3174
+ function_data["highlights"] = test_highlights
3175
+ function_data["header"] = test_header
3176
+ except:
3177
+ pass
3178
+
3179
+ function_data["colour_theme_number"] = 3
3180
+ function_data["modes"] = [
3181
+ {
3182
+ 'filter': '',
3183
+ 'sort': 0,
3184
+ 'name': 'All',
3185
+ },
3186
+ {
3187
+ 'filter': '--2 miss',
3188
+ 'name': 'miss',
3189
+ },
3190
+ {
3191
+ 'filter': '--2 mp4',
3192
+ 'name': 'mp4',
3193
+ },
3194
+ ]
3195
+ highlights = [
3196
+ {
3197
+ "field": 1,
3198
+ "match": "a",
3199
+ "color": 8,
3200
+ }
3201
+ ]
3202
+ function_data["cell_cursor"] = True
3203
+ function_data["display_modes"] = True
3204
+ function_data["centre_in_cols"] = True
3205
+ function_data["show_row_header"] = True
3206
+ function_data["keys_dict"] = picker_keys
3207
+ function_data["id_column"] = -1
3208
+ function_data["track_entries_upon_refresh"] = True
3209
+ function_data["centre_in_terminal_vertical"] = True
3210
+ function_data["highlight_full_row"] = True
3211
+ function_data["pin_cursor"] = True
3212
+ function_data["display_infobox"] = True
3213
+ function_data["infobox_items"] = [["1"], ["2"], ["3"]]
3214
+ function_data["infobox_title"] = "Title"
3215
+ function_data["footer_string"] = "Title"
3216
+ function_data["highlights"] = highlights
3217
+ function_data["show_footer"] = False
3218
+
3219
+
3220
+ # function_data["debug"] = True
3221
+ # function_data["debug_level"] = 1
3222
+ stdscr = start_curses()
3223
+ # h, w = stdscr.getmaxyx()
3224
+ # win = stdscr.derwin(h, w//2, 0, 0)
3225
+ try:
3226
+ # Run the Picker
3227
+ # h, w = stdscr.getmaxyx()
3228
+ # if (h>8 and w >20):
3229
+ # curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
3230
+ # stdscr.bkgd(' ', curses.color_pair(1)) # Apply background color
3231
+ # s = "Listpick is loading your data..."
3232
+ # stdscr.addstr(h//2, (w-len(s))//2, s)
3233
+ # stdscr.refresh()
3234
+
3235
+ # app = Picker(stdscr, **function_data)
3236
+ app = Picker(stdscr)
3237
+ app.set_function_data(function_data)
3238
+ app.splash_screen("Listpick is loading your data...")
3239
+ app.load_input_history("~/.config/listpick/cmdhist.json")
3240
+ app.run()
3241
+
3242
+ app.save_input_history("~/.config/listpick/cmdhist.json")
3243
+ except Exception as e:
3244
+ print(e)
3245
+
3246
+ # Clean up
3247
+ close_curses(stdscr)
3248
+
3249
+ if __name__ == '__main__':
3250
+ main()