listpick 0.1.15.19__py3-none-any.whl → 0.1.16.17__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.
listpick/listpick_app.py CHANGED
@@ -18,12 +18,15 @@ from wcwidth import wcswidth
18
18
  from typing import Callable, Optional, Tuple, Dict
19
19
  import json
20
20
  import threading
21
+ import multiprocessing
21
22
  import string
22
23
  import logging
23
- import tty
24
- import select
24
+ import copy
25
+ import tempfile
26
+ import queue
25
27
 
26
28
  from listpick.pane.pane_utils import get_file_attributes
29
+ from listpick.pane.left_pane_functions import *
27
30
  from listpick.ui.picker_colours import get_colours, get_help_colours, get_notification_colours, get_theme_count, get_fallback_colours
28
31
  from listpick.utils.options_selectors import default_option_input, output_file_option_selector, default_option_selector
29
32
  from listpick.utils.table_to_list_of_lists import *
@@ -36,24 +39,20 @@ from listpick.utils.paste_operations import *
36
39
  from listpick.utils.searching import search
37
40
  from listpick.ui.help_screen import help_lines
38
41
  from listpick.ui.keys import picker_keys, notification_keys, options_keys, help_keys
39
- from listpick.utils.generate_data import generate_picker_data
42
+ from listpick.utils.generate_data_multithreaded import generate_picker_data_from_file
40
43
  from listpick.utils.dump import dump_state, load_state, dump_data
41
44
  from listpick.ui.build_help import build_help_rows
42
45
  from listpick.ui.footer import StandardFooter, CompactFooter, NoFooter
43
46
  from listpick.utils.picker_log import setup_logger
44
- from listpick.utils.user_input import get_char, open_tty
45
- from listpick.pane.pane_functions import right_split_file_attributes, right_split_graph, right_split_display_list
47
+ from listpick.utils.user_input import get_char, open_tty, restore_terminal_settings
48
+ from listpick.pane.pane_functions import right_split_file_attributes, right_split_file_attributes_dynamic, right_split_graph, right_split_display_list
46
49
  from listpick.pane.get_data import *
47
50
 
48
-
49
- try:
50
- from tmp.data_stuff import test_items, test_highlights, test_header
51
- except:
52
- test_items, test_highlights, test_header = [], [], []
53
-
54
51
  COLOURS_SET = False
55
52
  help_colours, notification_colours = {}, {}
56
53
 
54
+ os.environ["TMPDIR"] = "/tmp"
55
+
57
56
  class Command:
58
57
  def __init__(self, command_type, command_value):
59
58
  self.command_type = command_type
@@ -64,7 +63,7 @@ class Picker:
64
63
  stdscr: curses.window,
65
64
  items: list[list[str]] = [],
66
65
  cursor_pos: int = 0,
67
- colours: dict = get_colours(0),
66
+ colours: dict = {},
68
67
  colour_theme_number: int = 3,
69
68
  max_selected: int = -1,
70
69
  top_gap: int =0,
@@ -76,9 +75,9 @@ class Picker:
76
75
  auto_refresh: bool =False,
77
76
  timer: float = 5,
78
77
 
79
- get_new_data: bool =False, # Whether we can get new data
80
- refresh_function: Optional[Callable] = lambda: [], # The function with which we get new data
81
- get_data_startup: bool =False, # Whether we should get data at statrup
78
+ get_new_data: bool =False,
79
+ refresh_function: Optional[Callable] = lambda items, header, visible_rows_indices, getting_data: None,
80
+ get_data_startup: bool =False,
82
81
  track_entries_upon_refresh: bool = True,
83
82
  pin_cursor: bool = False,
84
83
  id_column: int = 0,
@@ -101,7 +100,12 @@ class Picker:
101
100
  user_opts : str = "",
102
101
  options_list: list[str] = [],
103
102
  user_settings : str = "",
103
+
104
104
  separator : str = " ",
105
+ header_separator : str = " │",
106
+ header_separator_before_selected_column : str = " ▐",
107
+
108
+
105
109
  search_query : str = "",
106
110
  search_count : int = 0,
107
111
  search_index : int = 0,
@@ -116,6 +120,10 @@ class Picker:
116
120
  highlight_full_row: bool =False,
117
121
  crosshair_cursor: bool = False,
118
122
  cell_cursor: bool = True,
123
+ selected_char: str = "",
124
+ unselected_char: str = "",
125
+ selecting_char: str = "",
126
+ deselecting_char: str = "",
119
127
 
120
128
  items_per_page : int = -1,
121
129
  sort_method : int = 0,
@@ -127,17 +135,18 @@ class Picker:
127
135
  columns_sort_method: list[int] = [0],
128
136
  key_chain: str = "",
129
137
  last_key: Optional[str] = None,
138
+ disabled_keys: list=[],
130
139
 
131
140
  paginate: bool =False,
132
141
  cancel_is_back: bool = False,
133
142
  mode_index: int = 0,
134
143
  modes: list[dict] = [],
135
- display_modes: bool =False,
136
- require_option: list=[],
137
- require_option_default: list=[],
144
+ display_modes: bool = False,
145
+ require_option: list[bool] = [],
146
+ require_option_default: bool = False,
138
147
  option_functions: list[Callable[..., Tuple[bool, str]]] = [],
139
148
  default_option_function: Callable[..., Tuple[bool, str]] = default_option_input,
140
- disabled_keys: list=[],
149
+
141
150
 
142
151
  show_header: bool = True,
143
152
  show_row_header: bool = False,
@@ -145,7 +154,7 @@ class Picker:
145
154
  footer_style: int = 0,
146
155
  footer_string: str="",
147
156
  footer_string_auto_refresh: bool=False,
148
- footer_string_refresh_function: Optional[Callable] = None,
157
+ footer_string_refresh_function: Optional[Callable] = lambda : None,
149
158
  footer_timer: float=1,
150
159
  get_footer_string_startup=False,
151
160
  unicode_char_width: bool = True,
@@ -155,6 +164,7 @@ class Picker:
155
164
  reset_colours: bool = True,
156
165
  key_remappings: dict = {},
157
166
  keys_dict:dict = picker_keys,
167
+ macros: list = [],
158
168
  display_infobox : bool = False,
159
169
  infobox_items: list[list[str]] = [],
160
170
  infobox_title: str = "",
@@ -195,11 +205,24 @@ class Picker:
195
205
  split_right: bool = False,
196
206
  right_panes: list = [],
197
207
  right_pane_index: int = 0,
208
+
209
+ split_left: bool = False,
210
+ left_panes: list = [],
211
+ left_pane_index: int = 0,
212
+
213
+ screen_size_function = lambda stdscr: os.get_terminal_size()[::-1],
214
+ generate_data_for_hidden_columns: bool = False,
215
+
216
+
217
+ # getting_data: threading.Event = threading.Event(),
218
+
198
219
  ):
220
+
221
+ self.screen_size_function = screen_size_function
199
222
  self.stdscr = stdscr
200
223
  self.items = items
201
224
  self.cursor_pos = cursor_pos
202
- self.colours = colours
225
+ self.colours = get_colours(colour_theme_number)
203
226
  self.colour_theme_number = colour_theme_number
204
227
  self.max_selected = max_selected
205
228
  self.top_gap = top_gap
@@ -237,6 +260,8 @@ class Picker:
237
260
  self.options_list = options_list
238
261
  self.user_settings = user_settings
239
262
  self.separator = separator
263
+ self.header_separator = header_separator
264
+ self.header_separator_before_selected_column = header_separator_before_selected_column
240
265
  self.search_query = search_query
241
266
  self.search_count = search_count
242
267
  self.search_index = search_index
@@ -251,6 +276,10 @@ class Picker:
251
276
  self.highlight_full_row = highlight_full_row
252
277
  self.crosshair_cursor = crosshair_cursor
253
278
  self.cell_cursor = cell_cursor
279
+ self.selected_char = selected_char
280
+ self.unselected_char = unselected_char
281
+ self.selecting_char = selecting_char
282
+ self.deselecting_char = deselecting_char
254
283
 
255
284
  self.items_per_page = items_per_page
256
285
  self.sort_method = sort_method
@@ -280,7 +309,7 @@ class Picker:
280
309
  self.footer_string_auto_refresh = footer_string_auto_refresh
281
310
  self.footer_string_refresh_function = footer_string_refresh_function
282
311
  self.footer_timer = footer_timer
283
- self.get_footer_string_startup = get_footer_string_startup,
312
+ self.get_footer_string_startup = get_footer_string_startup
284
313
  self.unicode_char_width = unicode_char_width
285
314
 
286
315
 
@@ -289,6 +318,7 @@ class Picker:
289
318
  self.reset_colours = reset_colours
290
319
  self.key_remappings = key_remappings
291
320
  self.keys_dict = keys_dict
321
+ self.macros = macros
292
322
  self.display_infobox = display_infobox
293
323
  self.infobox_items = infobox_items
294
324
  self.infobox_title = infobox_title
@@ -312,7 +342,6 @@ class Picker:
312
342
 
313
343
 
314
344
  # Refresh function variables
315
- self.data_refreshed = False
316
345
  self.refreshing_data = False
317
346
  self.data_lock = threading.Lock()
318
347
  self.data_ready = False
@@ -347,14 +376,45 @@ class Picker:
347
376
  self.split_right = split_right
348
377
  self.right_panes = right_panes
349
378
  self.right_pane_index = right_pane_index
379
+
380
+ self.split_left = split_left
381
+ self.left_panes = left_panes
382
+ self.left_pane_index = left_pane_index
383
+
384
+ self.visible_rows_indices = []
385
+
386
+ self.generate_data_for_hidden_columns = generate_data_for_hidden_columns
387
+ self.thread_stop_event = threading.Event()
388
+ self.data_generation_queue = queue.PriorityQueue()
389
+ self.threads = []
390
+
391
+
392
+ self.process_manager = multiprocessing.Manager()
393
+ # self.data_generation_queue = ProcessSafePriorityQueue
394
+ self.processes = []
395
+ self.items_sync_loop_event = threading.Event()
396
+ self.items_sync_thread = None
397
+
398
+
350
399
  self.initialise_picker_state(reset_colours=self.reset_colours)
351
400
 
352
401
  # Note: We have to set the footer after initialising the picker state so that the footer can use the get_function_data method
353
402
  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)]
354
403
  self.footer = self.footer_options[self.footer_style]
355
- self.__version__ = "1.0"
404
+ self.footer.adjust_sizes(self.term_h, self.term_w)
405
+
406
+
407
+
408
+ # getting_data.is_set() is True when we are getting data
409
+ self.getting_data = threading.Event()
410
+ self.getting_data.set()
356
411
 
357
412
  def __sizeof__(self):
413
+ """
414
+ Return the approximate memory footprint of the Picker instance.
415
+
416
+ This includes the size of the instance itself and the sizes of its attributes.
417
+ """
358
418
 
359
419
  size = super().__sizeof__()
360
420
 
@@ -365,7 +425,19 @@ class Picker:
365
425
  return size
366
426
 
367
427
  def set_config(self, path: str ="~/.config/listpick/config.toml") -> bool:
368
- """ Set config from toml file. """
428
+ """ Set config from toml file.
429
+
430
+ This method reads a configuration file in TOML format, applies settings
431
+ to the Picker, and returns a boolean indicating success or failure.
432
+
433
+ Args:
434
+ path (str): The path to the configuration file.
435
+
436
+ Returns:
437
+ bool: True if the configuration was successfully set; False otherwise.
438
+ """
439
+ self.logger.info(f"function: set_config()")
440
+
369
441
  path = os.path.expanduser(os.path.expandvars(path))
370
442
  if not os.path.exists(path):
371
443
  return False
@@ -374,14 +446,17 @@ class Picker:
374
446
  except Exception as e:
375
447
  self.logger.error(f"get_config({path}) load error. {e}")
376
448
  return False
449
+
377
450
 
451
+ # Change the global theme if colour_theme_number is in the loaded config
378
452
  if "general" in config:
379
453
  if "colour_theme_number" in config["general"] and config["general"]["colour_theme_number"] != self.colour_theme_number:
380
454
  global COLOURS_SET
381
455
  COLOURS_SET = False
382
456
  self.colours_end = set_colours(pick=config["general"]["colour_theme_number"], start=1)
457
+ self.colours = get_colours(config["general"]["colour_theme_number"])
383
458
 
384
- self.logger.info(f"function: set_config()")
459
+ # load the rest of the config options
385
460
  if "general" in config:
386
461
  for key, val in config["general"].items():
387
462
  self.logger.info(f"set_config: key={key}, val={val}.")
@@ -393,19 +468,58 @@ class Picker:
393
468
  return True
394
469
 
395
470
  def get_config(self, path: str ="~/.config/listpick/config.toml") -> dict:
396
- """ Get config from file. """
471
+ """
472
+ Retrieve configuration settings from a specified TOML file.
473
+
474
+ Args:
475
+ path (str): The file path of the configuration file. Default is
476
+ ~/.config/listpick/config.toml.
477
+
478
+ Returns:
479
+ dict: A dictionary containing the configuration settings loaded
480
+ from the TOML file. In case of an error, an empty dictionary is returned.
481
+ """
482
+
397
483
  self.logger.info(f"function: get_config()")
398
484
  import toml
399
485
  with open(os.path.expanduser(path), "r") as f:
400
486
  config = toml.load(f)
401
487
  return config
402
488
 
403
- def calculate_section_sizes(self):
489
+ def update_term_size(self) -> None:
490
+ """
491
+ Update self.term_h, self.term_w the function provided to the Picker.
492
+
493
+ Returns:
494
+ None
495
+ """
496
+ self.term_h, self.term_w = self.screen_size_function(self.stdscr)
497
+ # self.term_h, self.term_w = self.stdscr.getmaxyx()
498
+ # self.term_w, self.term_h = os.get_terminal_size()
499
+
500
+
501
+ def get_term_size(self) -> Tuple[int, int]:
502
+ """
503
+ Get the current terminal size using the function provided to the Picker.
504
+
505
+ Returns:
506
+ Tuple[int, int]: A tuple containing the (height, width) of the terminal.
507
+ """
508
+ return self.screen_size_function(self.stdscr)
509
+ # return self.stdscr.getmaxyx()
510
+ # w, h = os.get_terminal_size()
511
+ # return h, w
512
+
513
+ def calculate_section_sizes(self) -> None:
404
514
  """
405
515
  Calculte the following for the Picker:
406
516
  self.items_per_page: the number of entry rows displayed
407
517
  self.bottom_space: the size of the footer + the bottom buffer space
408
518
  self.top_space: the size of the space at the top of the picker: title + modes + header + top_gap
519
+ Calculate and update the sizes of various sections of the Picker.
520
+
521
+ Returns:
522
+ None
409
523
  """
410
524
 
411
525
  self.logger.debug(f"function: calculate_section_sizes()")
@@ -413,14 +527,35 @@ class Picker:
413
527
  # self.bottom_space
414
528
  self.bottom_space = self.footer.height if self.show_footer else 0
415
529
 
530
+ # self.left_gutter_width
531
+ self.left_gutter_width = 1 if self.highlight_full_row else 2
532
+ if self.show_row_header: self.left_gutter_width += len(str(len(self.items))) + 2
533
+
534
+
416
535
  ## self.top_space
417
- h, w = self.stdscr.getmaxyx()
418
- self.term_h, self.term_w = self.stdscr.getmaxyx()
536
+ self.update_term_size()
537
+ self.rows_w, self.rows_h = self.term_w, self.term_h
538
+ self.rows_box_x_i = 0
539
+ self.rows_box_x_f = self.term_w
540
+ self.left_pane_width = self.right_pane_width = 0
419
541
  if self.split_right and len(self.right_panes):
420
542
  proportion = self.right_panes[self.right_pane_index]["proportion"]
421
- self.rows_w, self.rows_h = int(self.term_w*proportion), self.term_h
422
- else:
423
- self.rows_w, self.rows_h = self.term_w, self.term_h
543
+ self.right_pane_width = int(self.term_w*proportion)
544
+ self.rows_w -= self.right_pane_width
545
+ self.rows_box_x_f -= self.right_pane_width
546
+ if self.split_left and len(self.left_panes):
547
+ proportion = self.left_panes[self.left_pane_index]["proportion"]
548
+ self.left_pane_width = int(self.term_w*proportion)
549
+ self.rows_w -= self.left_pane_width
550
+ self.rows_box_x_i += self.left_pane_width
551
+ if self.left_pane_width + self.right_pane_width >= self.term_w-3:
552
+ self.rows_w += 10
553
+ self.left_pane_width -= 5
554
+ self.right_pane_width -= 5
555
+ self.rows_box_x_i -= 5
556
+ self.rows_box_x_f += 5
557
+
558
+
424
559
 
425
560
  self.top_space = self.top_gap
426
561
  if self.title: self.top_space+=1
@@ -438,42 +573,66 @@ class Picker:
438
573
  self.top_space += ((self.term_h-(self.top_space+self.bottom_space))-len(self.indexed_items))//2
439
574
 
440
575
  # self.column_widths
441
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
442
- visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
576
+ self.visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
577
+ visible_columns_total_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
443
578
 
444
579
  # self.startx
445
580
  self.startx = 1 if self.highlight_full_row else 2
446
581
  if self.show_row_header: self.startx += len(str(len(self.items))) + 2
447
582
  if visible_columns_total_width < self.rows_w and self.centre_in_terminal:
448
583
  self.startx += (self.rows_w - visible_columns_total_width) // 2
584
+ self.startx += self.left_pane_width
585
+ # if self.split_left and len(self.left_panes):
586
+ # proportion = self.left_panes[self.left_pane_index]["proportion"]
587
+ # self.startx += int(self.term_w*proportion)
588
+
589
+ self.endx = self.startx+self.rows_w
590
+
591
+
449
592
 
450
593
  def get_visible_rows(self) -> list[list[str]]:
594
+ """
595
+ Calculate and return the currently visible rows based on the cursor position and pagination settings.
596
+
597
+ This method determines which rows from the indexed items are visible on the screen,
598
+ accounting for pagination and scrolling. It sets the starting and ending indices
599
+ based on the current cursor position and the number of items per page.
451
600
 
601
+ Returns:
602
+ list[list[str]]: The currently visible rows as a list of lists, where each inner
603
+ list represents a row of data. If there are no indexed items, it returns the
604
+ items array.
605
+ """
452
606
  self.logger.debug(f"function: get_visible_rows()")
453
607
  ## Scroll with column select
454
608
  if self.paginate:
455
- start_index = (self.cursor_pos//self.items_per_page) * self.items_per_page
609
+ start_index = (self.cursor_pos // self.items_per_page) * self.items_per_page
456
610
  end_index = min(start_index + self.items_per_page, len(self.indexed_items))
457
611
  ## Scroll
458
612
  else:
459
- scrolloff = self.items_per_page//2
460
- start_index = max(0, min(self.cursor_pos - (self.items_per_page-scrolloff), len(self.indexed_items)-self.items_per_page))
613
+ scrolloff = self.items_per_page // 2
614
+ start_index = max(0, min(self.cursor_pos - (self.items_per_page - scrolloff), len(self.indexed_items) - self.items_per_page))
461
615
  end_index = min(start_index + self.items_per_page, len(self.indexed_items))
462
616
  if len(self.indexed_items) == 0: start_index, end_index = 0, 0
463
617
 
464
- rows = [v[1] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else self.items
465
- return rows
618
+ self.visible_rows = [v[1] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else self.items
619
+ # self.visible_rows_indices = [v[0] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else []
620
+ self.visible_rows_indices.clear()
621
+ self.visible_rows_indices.extend([v[0] for v in self.indexed_items[start_index:end_index]])
622
+ return self.visible_rows
466
623
 
467
624
  def initialise_picker_state(self, reset_colours=False) -> None:
468
625
  """ Initialise state variables for the picker. These are: debugging and colours. """
469
626
 
627
+ # Define global curses colours
470
628
  if curses.has_colors() and self.colours != None:
471
- # raise Exception("Terminal does not support color")
472
629
  curses.start_color()
630
+
473
631
  if reset_colours:
474
632
  global COLOURS_SET
475
633
  COLOURS_SET = False
476
634
  self.colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
635
+
477
636
  if curses.COLORS >= 255 and curses.COLOR_PAIRS >= 150:
478
637
  self.colours_start = self.colours_start
479
638
  self.notification_colours_start = self.colours_start+50
@@ -487,64 +646,43 @@ class Picker:
487
646
  self.notification_colours_start = 0
488
647
  self.help_colours_start = 0
489
648
 
649
+ self.colours = get_colours(self.colour_theme_number)
650
+
490
651
 
652
+ # Start logger
491
653
  debug_levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]
492
654
  dbglvl = debug_levels[self.debug_level]
493
655
  self.logger = setup_logger(name="picker_log", log_file="picker.log", log_enabled=self.debug, level =dbglvl)
494
656
  self.logger.info(f"Initialiasing Picker.")
495
- # self.notification(self.stdscr, message=repr(self.logger))
496
-
497
-
498
- # 1 2 3 4 5
499
- # logger = logging.getLogger(__file__)
500
- # if self.debug_level == 0:
501
- # logger = logging.getLogger()
502
- # logger.disabled = True
503
- # else:
504
- #
505
- # file_handler = logging.FileHandler(f"{self.title}.log", mode='w')
506
- # formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', '%m-%d-%Y %H:%M:%S')
507
- # file_handler.setFormatter(formatter)
508
- # logger.addHandler(file_handler)
509
- # logger.setLevel(debug_levels[self.debug_level-1])
510
-
511
- # logging.basicConfig(
512
- # level=debug_levels[self.debug_level-1],
513
- # format='%(asctime)s - %(levelname)s - %(message)s',
514
- # datefmt='%m-%d-%Y %H:%M:%S',
515
- # filename=f"{self.title}.log",
516
- # filemode="w",
517
- # )
518
- #
519
- # self.logger.info(f"Starging log. Log level {logger.getEffectiveLevel()}")
520
- # self.logger.info(f"Starging log. Log level {repr(debug_levels)}, {self.debug_level}, {debug_levels[self.debug_level-1]}")
521
- # self.notification(self.stdscr, f"Starging log. Log level {repr(debug_levels)}, {self.debug_level}, {debug_levels[self.debug_level-1]}")
522
- # self.notification(self.stdscr, f"{__file__}")
523
-
524
- ## Logging level plan
525
- # DEBUG: loop functions, draw screen, etc.
526
- # INFO: main functions
527
- # WARNING: any try-except fails
528
-
529
- # No set_escdelay function on windows.
657
+
658
+ self.update_term_size()
659
+
660
+ # The curses implementation for some systems (e.g., windows) does not allow set_escdelay
530
661
  try:
531
662
  curses.set_escdelay(25)
532
663
  except:
533
664
  logging.warning("Error trying to set curses.set_escdelay")
534
665
 
535
- # self.stdscr.clear()
536
- # self.stdscr.refresh()
537
- # self.draw_screen(self.indexed_items, self.highlights)
538
-
539
666
  def initialise_variables(self, get_data: bool = False) -> None:
540
- """ Initialise the variables that keep track of the data. """
667
+ """
668
+ This method sets up the internal state of the Picker by initialising various attributes,
669
+ getting new data (if get_data is True), and ensuring that the lists used for tracking
670
+ selections, options, and items are correctly of the correct type, size, and shape. If
671
+ filter or sort queries are set then they are applied (or re-applied as the case may be).
672
+ The cursor_pos and selections are retained by tracking the id of the rows (where the id
673
+ is row[self.id_column]).
674
+
675
+ Parameters:
676
+ - get_data (bool): If True, pulls data synchronously and updates tracking variables.
677
+ """
541
678
 
542
679
  self.logger.info(f"function: initialise_variables()")
543
680
 
544
681
  tracking = False
545
682
 
546
683
  ## Get data synchronously
547
- if get_data and self.refresh_function != None:
684
+ if get_data:
685
+ # Track cursor_pos and selections by ther id (row[self.id_column][col])
548
686
  if self.track_entries_upon_refresh and len(self.items) > 0:
549
687
  tracking = True
550
688
  selected_indices = get_selected_indices(self.selections)
@@ -552,31 +690,41 @@ class Picker:
552
690
  self.ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
553
691
  self.ids_tuples = [(i, item[self.id_column]) for i, item in enumerate(self.items) if i in selected_indices]
554
692
 
555
- if len(self.indexed_items) > 0 and len(self.indexed_items) >= self.cursor_pos and len(self.indexed_items[0][1]) >= self.id_column:
693
+ if len(self.indexed_items) > 0 and self.cursor_pos < len(self.indexed_items) and len(self.indexed_items[0][1]) >= self.id_column:
556
694
  self.cursor_pos_id = self.indexed_items[self.cursor_pos][1][self.id_column]
557
695
  self.cursor_pos_prev = self.cursor_pos
558
-
559
- self.items, self.header = self.refresh_function()
560
696
 
561
- self.items = pad_lists_to_same_length(self.items)
697
+ # Set the state of the threading event
698
+ # Though we are getting data synchronously, we ensure the correct state for self.getting_data
699
+ self.getting_data.clear()
700
+ self.refresh_function(
701
+ self.items,
702
+ self.header,
703
+ self.visible_rows_indices,
704
+ self.getting_data,
705
+ self.get_function_data(),
706
+ )
707
+
562
708
 
709
+ # Ensure that an emtpy items object has the form [[]]
563
710
  if self.items == []: self.items = [[]]
564
- ## Ensure that items is a List[List[Str]] object
711
+
712
+ # Ensure that items is a List[List[Str]] object
565
713
  if len(self.items) > 0 and not isinstance(self.items[0], list):
566
714
  self.items = [[item] for item in self.items]
567
- self.items = [[str(cell) for cell in row] for row in self.items]
715
+ # self.items = [[str(cell) for cell in row] for row in self.items]
568
716
 
717
+ # Ensure that the each of the rows of the items are of the same length
718
+ self.items = pad_lists_to_same_length(self.items)
569
719
 
570
720
  # Ensure that header is of the same length as the rows
571
721
  if self.header and len(self.items) > 0 and len(self.header) != len(self.items[0]):
572
722
  self.header = [str(self.header[i]) if i < len(self.header) else "" for i in range(len(self.items[0]))]
573
723
 
574
- # Constants
575
- # DEFAULT_ITEMS_PER_PAGE = os.get_terminal_size().lines - top_gap*2-2-int(bool(header))
576
-
577
724
  self.calculate_section_sizes()
578
725
 
579
- # Initial states
726
+
727
+ # Ensure that the selection-tracking variables are the correct shape
580
728
  if len(self.selections) != len(self.items):
581
729
  self.selections = {i : False if i not in self.selections else bool(self.selections[i]) for i in range(len(self.items))}
582
730
 
@@ -587,31 +735,34 @@ class Picker:
587
735
  self.cell_selections = {}
588
736
  self.selected_cells_by_row = {}
589
737
 
738
+ def extend_list_to_length(lst, length, default_value):
739
+ """Extend a list to the target length using a default value."""
740
+ if len(lst) < length:
741
+ lst.extend([copy.deepcopy(default_value) for _ in range(length - len(lst))])
590
742
 
591
743
 
592
- if len(self.require_option) < len(self.items):
593
- self.require_option += [self.require_option_default for i in range(len(self.items)-len(self.require_option))]
594
- if len(self.option_functions) < len(self.items):
595
- self.option_functions += [self.default_option_function for i in range(len(self.items)-len(self.option_functions))]
596
- if len(self.items)>0 and len(self.columns_sort_method) < len(self.items[0]):
597
- self.columns_sort_method = self.columns_sort_method + [0 for i in range(len(self.items[0])-len(self.columns_sort_method))]
598
- if len(self.items)>0 and len(self.sort_reverse) < len(self.items[0]):
599
- self.sort_reverse = self.sort_reverse + [False for i in range(len(self.items[0])-len(self.sort_reverse))]
600
- if len(self.items)>0 and len(self.editable_columns) < len(self.items[0]):
601
- self.editable_columns = self.editable_columns + [self.editable_by_default for i in range(len(self.items[0])-len(self.editable_columns))]
602
- if len(self.items)>0 and len(self.column_indices) < len(self.items[0]):
744
+ row_count = len(self.items)
745
+ col_count = len(self.items[0]) if row_count else 0
746
+
747
+ # Ensure that the length of the option lists are of the correct length.
748
+ if row_count > 0:
749
+ extend_list_to_length(self.require_option, length=row_count, default_value=self.require_option_default)
750
+ extend_list_to_length(self.option_functions, length=row_count, default_value=self.default_option_function)
751
+ extend_list_to_length(self.columns_sort_method, length=col_count, default_value=0)
752
+ extend_list_to_length(self.sort_reverse, length=col_count, default_value=False)
753
+ extend_list_to_length(self.editable_columns, length=col_count, default_value=self.editable_by_default)
754
+
755
+ if row_count > 0 and len(self.column_indices) < len(self.items[0]):
603
756
  self.column_indices = self.column_indices + [i for i in range(len(self.column_indices), len(self.items[0]))]
604
757
 
605
758
 
606
759
 
607
- # items2 = [[row[self.column_indices[i]] for i in range(len(row))] for row in self.items]
608
- # self.indexed_items = list(enumerate(items2))
760
+ # Create an indexed list of the items which will track the visible rows
609
761
  if self.items == [[]]: self.indexed_items = []
610
762
  else: self.indexed_items = list(enumerate(self.items))
611
763
 
612
- # If a filter is passed then refilter
764
+ # Apply the filter query
613
765
  if self.filter_query:
614
- # prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
615
766
  # prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
616
767
  self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
617
768
  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)
@@ -627,22 +778,30 @@ class Picker:
627
778
  )
628
779
  if return_val:
629
780
  self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
630
- # If a sort is passed
781
+
782
+ # Apply the current sort method
631
783
  if len(self.indexed_items) > 0:
632
784
  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
633
785
 
634
786
 
635
- # Adjust variables to ensure correctness if errors
636
- ## Move to a selectable row (if applicable)
787
+ # If we have more unselectable indices than rows, clear the unselectable_indices
637
788
  if len(self.items) <= len(self.unselectable_indices): self.unselectable_indices = []
638
- new_pos = (self.cursor_pos)%len(self.items)
639
- while new_pos in self.unselectable_indices and new_pos != self.cursor_pos:
640
- new_pos = (new_pos + 1) % len(self.items)
641
789
 
642
- assert new_pos < len(self.items)
643
- self.cursor_pos = new_pos
790
+ # Move cursur to a selectable row if we are currently on an unselectable row)
791
+ if self.cursor_pos * len(self.items) in self.unselectable_indices:
792
+ original_pos = new_pos = (self.cursor_pos)%len(self.items)
793
+ while new_pos in self.unselectable_indices:
794
+ new_pos = (new_pos + 1) % len(self.items)
795
+
796
+ # Break if we loop back to the original position
797
+ if new_pos == original_pos:
798
+ break
799
+
800
+ self.cursor_pos = max(0, min(new_pos, len(self.items)-1))
801
+
802
+ # Initialise sheets
803
+ extend_list_to_length(self.sheet_states, length=len(self.sheets), default_value={})
644
804
 
645
- # Sheets and files
646
805
  if len(self.sheet_states) < len(self.sheets):
647
806
  self.sheet_states += [{} for _ in range(len(self.sheets) - len(self.sheet_states))]
648
807
  if len(self.sheets):
@@ -650,15 +809,16 @@ class Picker:
650
809
  self.sheet_index = 0
651
810
  self.sheet_name = self.sheets[self.sheet_index]
652
811
 
653
- if len(self.loaded_file_states) < len(self.loaded_files):
654
- self.loaded_file_states += [{} for _ in range(len(self.loaded_files) - len(self.loaded_file_states))]
812
+ # Initialise files
813
+ extend_list_to_length(self.loaded_file_states, length=len(self.loaded_files), default_value={})
655
814
  if len(self.loaded_files):
656
815
  if self.loaded_file_index >= len(self.loaded_files):
657
816
  self.loaded_file_index = 0
658
817
  self.loaded_file = self.loaded_files[self.loaded_file_index]
659
818
 
660
- # if tracking and len(self.items) > 1:
661
- # Ensure that selected indices are tracked upon data refresh
819
+
820
+ # Ensure that the correct cursor_pos and selected indices are reselected
821
+ # if we have fetched new data.
662
822
  if self.track_entries_upon_refresh and (self.data_ready or tracking) and len(self.items) > 1:
663
823
  selected_indices = []
664
824
  all_ids = [item[self.id_column] for item in self.items]
@@ -681,6 +841,8 @@ class Picker:
681
841
 
682
842
 
683
843
 
844
+ # Ensure cursor_pos is set to a valid index
845
+ # If we have fetched new data then we attempt to set cursor_pos to the row with the same id as prev
684
846
  if len(self.indexed_items):
685
847
  if self.pin_cursor:
686
848
  self.cursor_pos = min(self.cursor_pos_prev, len(self.indexed_items)-1)
@@ -692,7 +854,12 @@ class Picker:
692
854
  else:
693
855
  self.cursor_pos = 0
694
856
 
695
- self.right_pane_index = max(0, min(self.right_pane_index, len(self.right_panes)-1))
857
+
858
+ # Ensure that the pane indices are within the range of the available panes.
859
+ if len(self.left_panes): self.left_pane_index %= len(self.left_panes)
860
+ else: self.left_pane_index = 0
861
+ if len(self.right_panes): self.right_pane_index %= len(self.right_panes)
862
+ else: self.right_pane_index = 0
696
863
 
697
864
 
698
865
 
@@ -739,13 +906,13 @@ class Picker:
739
906
 
740
907
  """
741
908
  self.logger.debug("function: test_screen_size()")
742
- h, w = self.stdscr.getmaxyx()
909
+ self.update_term_size()
743
910
  ## Terminal too small to display Picker
744
- if h<3 or w<len("Terminal"): return False
745
- if (self.show_footer or self.footer_string) and (h<12 or w<35) or (h<12 and w<10):
746
- self.stdscr.addstr(h//2-1, (w-len("Terminal"))//2, "Terminal")
747
- self.stdscr.addstr(h//2, (w-len("Too"))//2, "Too")
748
- self.stdscr.addstr(h//2+1, (w-len("Small"))//2, "Small")
911
+ if self.term_h<3 or self.term_w<len("Terminal"): return False
912
+ if (self.show_footer or self.footer_string) and (self.term_h<12 or self.term_w<35) or (self.term_h<12 and self.term_w<10):
913
+ self.stdscr.addstr(self.term_h//2-1, (self.term_w-len("Terminal"))//2, "Terminal")
914
+ self.stdscr.addstr(self.term_h//2, (self.term_w-len("Too"))//2, "Too")
915
+ self.stdscr.addstr(self.term_h//2+1, (self.term_w-len("Small"))//2, "Small")
749
916
  return False
750
917
  return True
751
918
 
@@ -758,28 +925,29 @@ class Picker:
758
925
 
759
926
  if type(message) == type(""): message = [message]
760
927
 
761
- h, w =self.stdscr.getmaxyx()
762
- if len(message) > h: start_y = 0
763
- else: start_y = (h-len(message))//2
928
+ self.update_term_size()
929
+ if len(message) > self.term_h: start_y = 0
930
+ else: start_y = (self.term_h-len(message))//2
764
931
 
765
932
  for i in range(len(message)):
766
933
  try:
767
934
  s = message[i]
768
- if len(s) > w: s = s[:w-2]
769
- self.stdscr.addstr(start_y+i, (w-len(s))//2, s, curses.color_pair(2))
935
+ if len(s) > self.term_w: s = s[:self.term_w-2]
936
+ self.stdscr.addstr(start_y+i, (self.term_w-len(s))//2, s, curses.color_pair(2))
770
937
  except:
771
938
  pass
772
939
  self.stdscr.refresh()
773
940
 
774
- def draw_screen(self, indexed_items: list[Tuple[int, list[str]]], highlights: list[dict] = [{}], clear: bool = True) -> None:
941
+ def draw_screen(self, clear: bool = True) -> None:
775
942
  """ Try-except wrapper for the draw_screen_ function. """
776
943
  try:
777
- self.draw_screen_(self.indexed_items, self.highlights)
944
+ self.draw_screen_(clear)
778
945
  except Exception as e:
779
946
  self.logger.warning(f"self.draw_screen_() error. {e}")
780
- pass
947
+ finally:
948
+ self.stdscr.refresh()
781
949
 
782
- def draw_screen_(self, indexed_items: list[Tuple[int, list[str]]], highlights: list[dict] = [{}], clear: bool = True) -> None:
950
+ def draw_screen_(self, clear: bool = True) -> None:
783
951
  """ Draw Picker screen. """
784
952
 
785
953
  self.logger.debug("Draw screen.")
@@ -787,16 +955,12 @@ class Picker:
787
955
  if clear:
788
956
  self.stdscr.erase()
789
957
 
790
- h, w = self.stdscr.getmaxyx()
791
- self.term_h, self.term_w = self.stdscr.getmaxyx()
792
- if self.split_right and len(self.right_panes):
793
- proportion = self.right_panes[self.right_pane_index]["proportion"]
794
- self.rows_w, self.rows_h = int(self.term_w*proportion), self.term_h
795
- else:
796
- self.rows_w, self.rows_h = self.term_w, self.term_h
958
+ self.update_term_size()
797
959
 
798
- # The height of the footer may need to be adjusted if the file changes.
960
+ # Determine footer size
799
961
  self.footer.adjust_sizes(self.term_h,self.term_w)
962
+
963
+ # The height of the footer may need to be adjusted if the file changes.
800
964
  self.calculate_section_sizes()
801
965
 
802
966
  # Test if the terminal is of a sufficient size to display the picker
@@ -814,27 +978,18 @@ class Picker:
814
978
  end_index = min(start_index + self.items_per_page, len(self.indexed_items))
815
979
  if len(self.indexed_items) == 0: start_index, end_index = 0, 0
816
980
 
817
- # 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)
818
- # Determine widths based only on the currently indexed rows
819
- # rows = [v[1] for v in self.indexed_items] if len(self.indexed_items) else self.items
820
- # Determine widths based only on the currently displayed indexed rows
821
- rows = [v[1] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else self.items
822
- 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=self.rows_w, unicode_char_width=self.unicode_char_width)
823
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
824
- visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
825
-
826
- # Determine the number of items_per_page, top_size and bottom_size
827
- # self.calculate_section_sizes()
828
-
829
- # top_space = self.top_gap
981
+ self.get_visible_rows()
982
+ self.column_widths = get_column_widths(self.visible_rows, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=self.rows_w, unicode_char_width=self.unicode_char_width)
983
+ self.visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
984
+ visible_columns_total_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
985
+
830
986
 
831
- ## Display title (if applicable)
987
+ ## Display title
832
988
  if self.title:
833
989
  padded_title = f" {self.title.strip()} "
834
990
  self.stdscr.addstr(self.top_gap, 0, f"{' ':^{self.term_w}}", curses.color_pair(self.colours_start+16))
835
991
  title_x = (self.term_w-wcswidth(padded_title))//2
836
992
  self.stdscr.addstr(self.top_gap, title_x, padded_title, curses.color_pair(self.colours_start+16) | curses.A_BOLD)
837
- # top_space += 1
838
993
 
839
994
  ## Display modes
840
995
  if self.display_modes and self.modes not in [[{}], []]:
@@ -856,7 +1011,6 @@ class Picker:
856
1011
  else:
857
1012
  self.stdscr.addstr(self.top_gap+1, xmode, mode_str, curses.color_pair(self.colours_start+15) | curses.A_UNDERLINE)
858
1013
  xmode += split_space+mode_widths[i]
859
- # top_space += 1
860
1014
 
861
1015
  ## Display header
862
1016
  if self.header and self.show_header:
@@ -874,137 +1028,129 @@ class Picker:
874
1028
 
875
1029
 
876
1030
  header_str += f"{col_str:^{self.column_widths[i]-len(number)}}"
877
- header_str += self.separator
1031
+ if i == self.selected_column-1:
1032
+ header_str += self.header_separator_before_selected_column
1033
+ else:
1034
+ header_str += self.header_separator
1035
+ header_str_w = min(self.rows_w-self.left_gutter_width, visible_columns_total_width+1, self.term_w-self.startx)
878
1036
 
879
1037
  header_str = header_str[self.leftmost_char:]
1038
+ header_str = header_str[:header_str_w]
880
1039
  header_ypos = self.top_gap + bool(self.title) + bool(self.display_modes and self.modes)
881
- self.stdscr.addstr(header_ypos, 0, ' '*self.rows_w, curses.color_pair(self.colours_start+28) | curses.A_BOLD)
882
- self.stdscr.addstr(header_ypos, self.startx, header_str[:min(self.rows_w-self.startx, visible_columns_total_width+1)], curses.color_pair(self.colours_start+4) | curses.A_BOLD)
1040
+
1041
+ # Ensure that the full header width is filled--important if the header rows do not fill the terminal width
1042
+ self.stdscr.addstr(header_ypos, self.rows_box_x_i, ' '*self.rows_w, curses.color_pair(self.colours_start+28) | curses.A_BOLD)
1043
+
1044
+ # Draw header string
1045
+ self.stdscr.addstr(header_ypos, self.startx, header_str, curses.color_pair(self.colours_start+4) | curses.A_BOLD)
883
1046
 
884
1047
  # Highlight sort column
885
- try:
886
- if self.selected_column != None and self.selected_column not in self.hidden_columns:
887
- # start of string is on screen
888
- col_width = self.column_widths[self.selected_column]
889
- number = f"{self.selected_column}. " if self.number_columns else ""
890
- col_str = self.header[self.selected_column][:self.column_widths[self.selected_column]-len(number)]
891
- highlighted_col_str = (number+f"{col_str:^{self.column_widths[self.selected_column]-len(number)}}") + self.separator
892
-
893
- if len(self.column_widths) == 1:
894
- colour = curses.color_pair(self.colours_start+28) | curses.A_BOLD
895
- else:
896
- colour = curses.color_pair(self.colours_start+19) | curses.A_BOLD
897
- # Start of selected column is on the screen
898
- if self.leftmost_char <= len(up_to_selected_col) and self.leftmost_char+self.rows_w-self.startx > len(up_to_selected_col):
899
- x_pos = len(up_to_selected_col) - self.leftmost_char + self.startx
900
-
901
- # Whole cell of the selected column is on the screen
902
- if len(up_to_selected_col)+col_width - self.leftmost_char < self.rows_w-self.startx:
903
- disp_str = highlighted_col_str
904
-
905
- # Start of the cell is on the screen, but the end of the cell is not
906
- else:
907
- overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w - self.startx)
908
- disp_str = highlighted_col_str[:-overflow]
909
-
910
- self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
911
- # Start of the cell is to the right of the screen
912
- elif self.leftmost_char+self.rows_w <= len(up_to_selected_col):
913
- pass
914
- # The end of the cell is on the screen, the start of the cell is not
915
- elif 0 <= len(up_to_selected_col)+col_width - self.leftmost_char <= self.rows_w :
916
- x_pos = self.startx
917
- beg = self.leftmost_char - len(up_to_selected_col)
918
- disp_str = highlighted_col_str[beg:]
919
- self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
920
- # The middle of the cell is on the screen, the start and end of the cell are not
921
- elif self.leftmost_char <= len(up_to_selected_col) + col_width//2 <= self.leftmost_char+self.rows_w:
922
- beg = self.leftmost_char - len(up_to_selected_col)
923
- overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w)
924
- disp_str = highlighted_col_str[beg:-overflow]
925
-
926
- x_pos = self.startx
927
- self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
928
- # The cell is to the left of the screen
1048
+ if self.selected_column != None and self.selected_column not in self.hidden_columns:
1049
+ # start of string is on screen
1050
+ col_width = self.column_widths[self.selected_column]
1051
+ number = f"{self.selected_column}. " if self.number_columns else ""
1052
+ col_str = self.header[self.selected_column][:self.column_widths[self.selected_column]-len(number)]
1053
+ highlighted_col_str = (number+f"{col_str:^{self.column_widths[self.selected_column]-len(number)}}") + self.separator
1054
+
1055
+ if len(self.column_widths) == 1:
1056
+ colour = curses.color_pair(self.colours_start+28) | curses.A_BOLD
1057
+ else:
1058
+ colour = curses.color_pair(self.colours_start+19) | curses.A_BOLD
1059
+ # Start of selected column is on the screen
1060
+ if self.leftmost_char <= len(up_to_selected_col) and self.leftmost_char+self.rows_w-self.left_gutter_width > len(up_to_selected_col):
1061
+ x_pos = len(up_to_selected_col) - self.leftmost_char + self.startx
1062
+
1063
+ # Whole cell of the selected column is on the screen
1064
+ if len(up_to_selected_col)+col_width - self.leftmost_char < self.rows_w-self.left_gutter_width:
1065
+ disp_str = highlighted_col_str
1066
+
1067
+ # Start of the cell is on the screen, but the end of the cell is not
929
1068
  else:
930
- pass
931
-
932
- # elif self.leftmost_char:
933
- # os.system(f"notify-send 'cell is to the right of the screen'")
934
-
935
- #
936
- #
937
- # if len(self.header) > 1 and (len(up_to_selected_col)-self.leftmost_char) < self.rows_w:
938
- # number = f"{self.selected_column}. " if self.number_columns else ""
939
- # # number = f"{intStringToExponentString(self.selected_column)}. " if self.number_columns else ""
940
- # # self.startx + len(up_to_selected_col) - self.leftmost_char
941
- # highlighed_col_startx = max(self.startx, self.startx + len(up_to_selected_col) - self.leftmost_char)
942
- #
943
- #
944
- # col_str = self.header[self.selected_column][:self.column_widths[self.selected_column]-len(number)]
945
- # highlighted_col_str = (number+f"{col_str:^{self.column_widths[self.selected_column]}}") + self.separator
946
- # end_of_highlighted_col_str = self.rows_w-(highlighed_col_startx+len(highlighted_col_str)) if (highlighed_col_startx+len(highlighted_col_str)) > self.rows_w else len(highlighted_col_str)
947
- # if (highlighed_col_startx+len(highlighted_col_str)) > self.rows_w:
948
- # end_of_highlighted_col_str = self.rows_w-(highlighed_col_startx+len(highlighted_col_str))
949
- # else:
950
- # end_of_highlighted_col_str = len(highlighted_col_str)
951
- #
952
- # start_of_highlighted_col_str = max(self.leftmost_char - len(up_to_selected_col), 0)
953
- # self.stdscr.addstr(header_ypos, highlighed_col_startx , highlighted_col_str[start_of_highlighted_col_str:end_of_highlighted_col_str][:self.column_widths[self.selected_column]+len(self.separator)], curses.color_pair(self.colours_start+19) | curses.A_BOLD)
954
- except:
955
- pass
956
-
1069
+ overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w - self.left_gutter_width)
1070
+ disp_str = highlighted_col_str[:-overflow]
1071
+ disp_str_w = min(len(disp_str), self.term_w-x_pos)
1072
+ disp_str = truncate_to_display_width(disp_str, disp_str_w, self.centre_in_cols, self.unicode_char_width)
1073
+
1074
+ self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
1075
+ # Start of the cell is to the right of the screen
1076
+ elif self.leftmost_char+self.rows_w <= len(up_to_selected_col):
1077
+ pass
1078
+ # The end of the cell is on the screen, the start of the cell is not
1079
+ elif 0 <= len(up_to_selected_col)+col_width - self.leftmost_char <= self.rows_w :
1080
+ x_pos = self.startx
1081
+ beg = self.leftmost_char - len(up_to_selected_col)
1082
+ disp_str = highlighted_col_str[beg:]
1083
+ disp_str_w = min(len(disp_str), self.term_w-x_pos)
1084
+ disp_str = truncate_to_display_width(disp_str, disp_str_w, self.centre_in_cols, self.unicode_char_width)
1085
+ self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
1086
+ # The middle of the cell is on the screen, the start and end of the cell are not
1087
+ elif self.leftmost_char <= len(up_to_selected_col) + col_width//2 <= self.leftmost_char+self.rows_w:
1088
+ beg = self.leftmost_char - len(up_to_selected_col)
1089
+ overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w)
1090
+ x_pos = self.startx
1091
+ disp_str = highlighted_col_str[beg:-overflow]
1092
+ disp_str_w = min(len(disp_str), self.term_w-x_pos)
1093
+ disp_str = truncate_to_display_width(disp_str, disp_str_w, self.centre_in_cols, self.unicode_char_width)
1094
+
1095
+ self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
1096
+
1097
+ # The cell is to the left of the focused part of the screen
1098
+ else:
1099
+ pass
1100
+
957
1101
  # Display row header
958
1102
  if self.show_row_header:
959
1103
  for idx in range(start_index, end_index):
960
1104
  y = idx - start_index + self.top_space
961
1105
  if idx == self.cursor_pos:
962
- self.stdscr.addstr(y, 0, f" {self.indexed_items[idx][0]} ", curses.color_pair(self.colours_start+19) | curses.A_BOLD)
1106
+ self.stdscr.addstr(y, self.startx-self.left_gutter_width, f" {self.indexed_items[idx][0]} ", curses.color_pair(self.colours_start+19) | curses.A_BOLD)
963
1107
  else:
964
- self.stdscr.addstr(y, 0, f" {self.indexed_items[idx][0]} ", curses.color_pair(self.colours_start+4) | curses.A_BOLD)
1108
+ self.stdscr.addstr(y, self.startx-self.left_gutter_width, f" {self.indexed_items[idx][0]} ", curses.color_pair(self.colours_start+4) | curses.A_BOLD)
965
1109
 
966
1110
 
967
1111
  def highlight_cell(row: int, col:int, visible_column_widths, colour_pair_number: int = 5, bold: bool = False, y:int = 0):
968
-
1112
+
969
1113
  cell_pos = sum(visible_column_widths[:col])+col*len(self.separator)-self.leftmost_char + self.startx
1114
+ cell_pos_relative = sum(visible_column_widths[:col])+col*len(self.separator)-self.leftmost_char + self.left_gutter_width
970
1115
  # cell_width = self.column_widths[self.selected_column]
971
1116
  cell_width = visible_column_widths[col] + len(self.separator)
972
- cell_max_width = self.rows_w-cell_pos
1117
+ cell_max_width = min(self.rows_w-self.left_gutter_width, self.term_w-cell_pos)
973
1118
 
974
1119
  if bold:
975
1120
  colour = curses.color_pair(self.colours_start+colour_pair_number) | curses.A_BOLD
976
1121
  else:
977
1122
  colour = curses.color_pair(self.colours_start+colour_pair_number)
978
- try:
979
- # Start of cell is on screen
980
- if self.startx <= cell_pos <= self.rows_w:
981
- self.stdscr.addstr(y, cell_pos, (' '*cell_width)[:cell_max_width], colour)
982
- if self.centre_in_cols:
983
- cell_value = f"{self.indexed_items[row][1][col]:^{cell_width-len(self.separator)}}" + self.separator
984
- else:
985
- cell_value = self.indexed_items[row][1][col][:self.column_widths[col]] + self.separator
986
- # cell_value = cell_value[:min(cell_width, cell_max_width)-len(self.separator)]
987
- cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
988
- # cell_value = cell_value + self.separator
989
- # cell_value = cell_value
990
- cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
991
- self.stdscr.addstr(y, cell_pos, cell_value, colour)
992
- # Part of the cell is on screen
993
- elif self.startx <= cell_pos+cell_width and cell_pos < (self.rows_w):
994
- cell_start = self.startx - cell_pos
995
- # self.stdscr.addstr(y, self.startx, ' '*(cell_width-cell_start), curses.color_pair(self.colours_start+colour_pair_number))
996
- cell_value = self.indexed_items[row][1][col]
997
- cell_value = f"{cell_value:^{self.column_widths[col]}}"
998
-
999
- cell_value = cell_value[cell_start:visible_column_widths[col]][:self.rows_w-self.startx]
1000
- self.stdscr.addstr(y, self.startx, cell_value, colour)
1123
+ # Start of cell is on screen
1124
+ if self.startx <= cell_pos <= self.rows_w+self.startx:
1125
+ s = "max" if cell_max_width <= cell_width else "norm"
1126
+ self.stdscr.addstr(y, cell_pos, (' '*cell_width)[:cell_max_width], colour)
1127
+ if self.centre_in_cols:
1128
+ cell_value = f"{self.indexed_items[row][1][col]:^{cell_width-len(self.separator)}}" + self.separator
1001
1129
  else:
1002
- pass
1003
- # if colour_pair_number == 5:
1004
- except:
1130
+ cell_value = self.indexed_items[row][1][col][:self.column_widths[col]] + self.separator
1131
+ cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
1132
+ cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
1133
+ if wcswidth(cell_value) + cell_pos > self.term_w:
1134
+ cell_value = truncate_to_display_width(cell_value, self.term_w-cell_pos-10, self.centre_in_cols, self.unicode_char_width)
1135
+ self.stdscr.addstr(y, cell_pos, cell_value, colour)
1136
+
1137
+ # Part of the cell is on screen
1138
+ elif self.startx <= cell_pos+cell_width and cell_pos <= (self.rows_w):
1139
+ s = "max" if cell_max_width <= cell_width else "norm"
1140
+ cell_start = self.startx - cell_pos
1141
+ cell_value = self.indexed_items[row][1][col]
1142
+ cell_value = f"{cell_value:^{self.column_widths[col]}}"
1143
+
1144
+ cell_value = cell_value[cell_start:visible_column_widths[col]][:self.rows_w-self.left_gutter_width]
1145
+ cell_value = truncate_to_display_width(cell_value, min(wcswidth(cell_value), cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
1146
+ cell_value += self.separator
1147
+ cell_value = truncate_to_display_width(cell_value, min(wcswidth(cell_value), cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
1148
+ self.stdscr.addstr(y, self.startx, cell_value, colour)
1149
+ else:
1005
1150
  pass
1006
1151
 
1007
1152
 
1153
+
1008
1154
  def sort_highlights(highlights):
1009
1155
  """
1010
1156
  Sort highlights into lists based on their display level.
@@ -1045,10 +1191,11 @@ class Picker:
1045
1191
  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)
1046
1192
  if not match: continue
1047
1193
  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)
1194
+ width = min(self.column_widths[highlight["field"]]-(field_start-self.leftmost_char), self.rows_w-self.left_gutter_width)
1048
1195
 
1049
1196
  ## We want to search the non-centred values but highlight the centred values.
1050
1197
  if self.centre_in_cols:
1051
- tmp = truncate_to_display_width(item[1][highlight["field"]], self.column_widths[highlight["field"]], self.centre_in_cols, self.unicode_char_width)
1198
+ tmp = truncate_to_display_width(item[1][highlight["field"]], width, self.centre_in_cols, self.unicode_char_width)
1052
1199
  field_start += (len(tmp) - len(tmp.lstrip()))
1053
1200
 
1054
1201
  highlight_start = field_start + match.start()
@@ -1059,7 +1206,7 @@ class Picker:
1059
1206
  continue
1060
1207
  highlight_start -= self.leftmost_char
1061
1208
  highlight_end -= self.leftmost_char
1062
- self.stdscr.addstr(y, max(self.startx, self.startx+highlight_start), row_str[max(highlight_start,0):min(self.rows_w-self.startx, highlight_end)], curses.color_pair(self.colours_start+highlight["color"]) | curses.A_BOLD)
1209
+ self.stdscr.addstr(y, max(self.startx, self.startx+highlight_start), row_str[max(highlight_start,0):min(self.rows_w-self.left_gutter_width, highlight_end)], curses.color_pair(self.colours_start+highlight["color"]) | curses.A_BOLD)
1063
1210
  except:
1064
1211
  pass
1065
1212
 
@@ -1075,6 +1222,7 @@ class Picker:
1075
1222
  l0_highlights, l1_highlights, l2_highlights = sort_highlights(self.highlights)
1076
1223
 
1077
1224
 
1225
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
1078
1226
  for idx in range(start_index, end_index):
1079
1227
  item = self.indexed_items[idx]
1080
1228
  y = idx - start_index + self.top_space
@@ -1086,23 +1234,26 @@ class Picker:
1086
1234
  # rowstr off screen
1087
1235
  # if self.leftmost_char > len(row_str_orig):
1088
1236
  # trunc_width = 0
1089
- if self.leftmost_char + (self.rows_w-self.startx) <= len(row_str_orig):
1090
- trunc_width = self.rows_w-self.startx
1091
- elif self.leftmost_char <= len(row_str_orig):
1092
- trunc_width = len(row_str_orig) - self.leftmost_char
1093
- else:
1094
- trunc_width = 0
1237
+ # if self.leftmost_char + (self.rows_w-self.left_gutter_width) <= len(row_str_orig):
1238
+ # trunc_width = self.rows_w-self.startx
1239
+ # elif self.leftmost_char <= len(row_str_orig):
1240
+ # trunc_width = len(row_str_orig) - self.leftmost_char
1241
+ # else:
1242
+ # trunc_width = 0
1243
+
1244
+
1245
+ trunc_width = max(0, min(self.rows_w-self.left_gutter_width, row_width, self.term_w - self.startx))
1095
1246
 
1096
1247
  row_str = truncate_to_display_width(row_str_left_adj, trunc_width, self.unicode_char_width)
1097
1248
  # row_str = truncate_to_display_width(row_str, min(w-self.startx, visible_columns_total_width))[self.leftmost_char:]
1098
1249
 
1099
1250
  ## Display the standard row
1100
- self.stdscr.addstr(y, self.startx, row_str[:min(self.rows_w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+2))
1101
-
1251
+ self.stdscr.addstr(y, self.startx, row_str, curses.color_pair(self.colours_start+2))
1252
+
1102
1253
 
1103
1254
  ## Highlight column
1104
1255
  if self.crosshair_cursor:
1105
- highlight_cell(idx, self.selected_column, visible_column_widths, colour_pair_number=27, bold=False, y=y)
1256
+ highlight_cell(idx, self.selected_column, self.visible_column_widths, colour_pair_number=27, bold=False, y=y)
1106
1257
  if idx == self.cursor_pos:
1107
1258
  self.stdscr.addstr(y, self.startx, row_str[:min(self.rows_w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+27))
1108
1259
 
@@ -1116,25 +1267,25 @@ class Picker:
1116
1267
  # self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
1117
1268
  if item[0] in self.selected_cells_by_row:
1118
1269
  for j in self.selected_cells_by_row[item[0]]:
1119
- highlight_cell(idx, j, visible_column_widths, colour_pair_number=25, bold=False, y=y)
1270
+ highlight_cell(idx, j, self.visible_column_widths, colour_pair_number=25, bold=False, y=y)
1120
1271
 
1121
1272
  # Visually selected
1122
1273
  if self.is_selecting:
1123
1274
  if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
1124
1275
  x_interval = range(min(self.start_selection_col, self.selected_column), max(self.start_selection_col, self.selected_column)+1)
1125
1276
  for col in x_interval:
1126
- highlight_cell(idx, col, visible_column_widths, colour_pair_number=25, bold=False, y=y)
1277
+ highlight_cell(idx, col, self.visible_column_widths, colour_pair_number=25, bold=False, y=y)
1127
1278
 
1128
1279
  # Visually deslected
1129
1280
  if self.is_deselecting:
1130
1281
  if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
1131
1282
  x_interval = range(min(self.start_selection_col, self.selected_column), max(self.start_selection_col, self.selected_column)+1)
1132
1283
  for col in x_interval:
1133
- highlight_cell(idx, col, visible_column_widths, colour_pair_number=26, bold=False,y=y)
1284
+ highlight_cell(idx, col, self.visible_column_widths, colour_pair_number=26, bold=False,y=y)
1134
1285
  # Higlight cursor row and selected rows
1135
1286
  elif self.highlight_full_row:
1136
1287
  if self.selections[item[0]]:
1137
- self.stdscr.addstr(y, self.startx, row_str[:min(self.rows_w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+25) | curses.A_BOLD)
1288
+ self.stdscr.addstr(y, self.startx, row_str[:min(self.rows_w-self.left_gutter_width, visible_columns_total_width)], curses.color_pair(self.colours_start+25) | curses.A_BOLD)
1138
1289
 
1139
1290
  # Visually selected
1140
1291
  if self.is_selecting:
@@ -1147,16 +1298,30 @@ class Picker:
1147
1298
 
1148
1299
  # Highlight the cursor row and the first char of the selected rows.
1149
1300
  else:
1150
- if self.selections[item[0]]:
1151
- self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
1152
- # Visually selected
1153
- if self.is_selecting:
1154
- if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
1301
+ if self.selected_char:
1302
+ if self.selections[item[0]]:
1303
+ self.stdscr.addstr(y, max(self.startx-2,0), self.selected_char, curses.color_pair(self.colours_start+2))
1304
+ else:
1305
+ self.stdscr.addstr(y, max(self.startx-2,0), self.unselected_char, curses.color_pair(self.colours_start+2))
1306
+ # Visually selected
1307
+ if self.is_selecting:
1308
+ if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
1309
+ self.stdscr.addstr(y, max(self.startx-2,0), self.selecting_char, curses.color_pair(self.colours_start+2))
1310
+ # Visually deslected
1311
+ if self.is_deselecting:
1312
+ if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
1313
+ self.stdscr.addstr(y, max(self.startx-2,0), self.deselecting_char, curses.color_pair(self.colours_start+2))
1314
+ else:
1315
+ if self.selections[item[0]]:
1155
1316
  self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
1156
- # Visually deslected
1157
- if self.is_deselecting:
1158
- if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
1159
- self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+10))
1317
+ # Visually selected
1318
+ if self.is_selecting:
1319
+ if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
1320
+ self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
1321
+ # Visually deslected
1322
+ if self.is_deselecting:
1323
+ if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
1324
+ self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+10))
1160
1325
 
1161
1326
  if not self.highlights_hide:
1162
1327
  draw_highlights(l1_highlights, idx, y, item)
@@ -1166,10 +1331,10 @@ class Picker:
1166
1331
  # Draw cursor
1167
1332
  if idx == self.cursor_pos:
1168
1333
  if self.cell_cursor:
1169
- highlight_cell(idx, self.selected_column, visible_column_widths, colour_pair_number=5, bold=True, y=y)
1334
+ highlight_cell(idx, self.selected_column, self.visible_column_widths, colour_pair_number=5, bold=True, y=y)
1170
1335
  else:
1171
- self.stdscr.addstr(y, self.startx, row_str[:min(self.rows_w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+5) | curses.A_BOLD)
1172
-
1336
+ self.stdscr.addstr(y, self.startx, row_str[:self.rows_w-self.left_gutter_width], curses.color_pair(self.colours_start+5) | curses.A_BOLD)
1337
+
1173
1338
  if not self.highlights_hide:
1174
1339
  draw_highlights(l2_highlights, idx, y, item)
1175
1340
 
@@ -1188,7 +1353,8 @@ class Picker:
1188
1353
  scroll_bar_length = max(1, scroll_bar_length)
1189
1354
  for i in range(scroll_bar_length):
1190
1355
  v = max(self.top_space+int(bool(self.header)), scroll_bar_start-scroll_bar_length//2)
1191
- self.stdscr.addstr(scroll_bar_start+i, self.rows_w-1, ' ', curses.color_pair(self.colours_start+18))
1356
+ # self.stdscr.addstr(scroll_bar_start+i, self.startx+self.rows_w-self.left_gutter_width-2, ' ', curses.color_pair(self.colours_start+18))
1357
+ self.stdscr.addstr(scroll_bar_start+i, self.rows_box_x_f-1, ' ', curses.color_pair(self.colours_start+18))
1192
1358
 
1193
1359
  # Display refresh symbol
1194
1360
  if self.auto_refresh:
@@ -1197,10 +1363,14 @@ class Picker:
1197
1363
  else:
1198
1364
  self.stdscr.addstr(0,self.term_w-3,"  ", curses.color_pair(self.colours_start+23) | curses.A_BOLD)
1199
1365
 
1366
+ # Display data fetch symbol
1367
+ if not self.getting_data.is_set():
1368
+ self.stdscr.addstr(0,self.term_w-3,"  ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
1369
+ # self.stdscr.addstr(0,self.term_w-6,"⏳", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
1370
+
1200
1371
  ## Display footer
1201
1372
  if self.show_footer:
1202
1373
  # self.footer = NoFooter(self.stdscr, self.colours_start, self.get_function_data)
1203
- h, w = self.stdscr.getmaxyx()
1204
1374
  try:
1205
1375
  self.footer.draw(self.term_h, self.term_w)
1206
1376
  except:
@@ -1213,41 +1383,83 @@ class Picker:
1213
1383
 
1214
1384
  if self.split_right and len(self.right_panes):
1215
1385
  # If we need to refresh the data then do so.
1216
- if self.right_panes[self.right_pane_index]["auto_refresh"] and ((time.time() - self.initial_split_time) > self.right_panes[self.right_pane_index]["refresh_time"]):
1217
- get_data = self.right_panes[self.right_pane_index]["get_data"]
1218
- data = self.right_panes[self.right_pane_index]["data"]
1219
- self.right_panes[self.right_pane_index]["data"] = get_data(data, self.get_function_data())
1220
- self.initial_split_time = time.time()
1386
+ pane = self.right_panes[self.right_pane_index]
1387
+ if pane["auto_refresh"] and ((time.time() - self.initial_right_split_time) > pane["refresh_time"]):
1388
+ get_data = pane["get_data"]
1389
+ data = pane["data"]
1390
+ pane["data"] = get_data(data, self.get_function_data())
1391
+ self.initial_right_split_time = time.time()
1221
1392
 
1222
- draw_pane = self.right_panes[self.right_pane_index]["display"]
1223
- data = self.right_panes[self.right_pane_index]["data"]
1393
+ draw_pane = pane["display"]
1394
+ data = pane["data"]
1395
+ # pane_width = int(pane["proportion"]*self.term_w)
1224
1396
 
1225
1397
  draw_pane(
1226
1398
  self.stdscr,
1227
- x = self.rows_w,
1399
+ x = self.rows_w + self.startx - self.left_gutter_width,
1228
1400
  y = self.top_space - int(bool(self.show_header and self.header)),
1229
- w = self.term_w-self.rows_w,
1401
+ w = self.right_pane_width,
1230
1402
  h = self.items_per_page + int(bool(self.show_header and self.header)),
1231
1403
  state = self.get_function_data(),
1232
1404
  row = self.indexed_items[self.cursor_pos] if self.indexed_items else [],
1233
1405
  cell = self.indexed_items[self.cursor_pos][1][self.selected_column] if self.indexed_items else "",
1234
1406
  data=data,
1235
1407
  )
1236
-
1237
- self.stdscr.refresh()
1408
+ if self.split_left and len(self.left_panes):
1409
+ # If we need to refresh the data then do so.
1410
+ pane = self.left_panes[self.left_pane_index]
1411
+ if pane["auto_refresh"] and ((time.time() - self.initial_left_split_time) > pane["refresh_time"]):
1412
+ get_data = pane["get_data"]
1413
+ data = pane["data"]
1414
+ pane["data"] = get_data(data, self.get_function_data())
1415
+ self.initial_left_split_time = time.time()
1416
+
1417
+ draw_pane = pane["display"]
1418
+ data = pane["data"]
1419
+ # pane_width = int(pane["proportion"]*self.term_w)
1420
+
1421
+ draw_pane(
1422
+ self.stdscr,
1423
+ x = 0,
1424
+ y = self.top_space - int(bool(self.show_header and self.header)),
1425
+ w = self.left_pane_width,
1426
+ h = self.items_per_page + int(bool(self.show_header and self.header)),
1427
+ state = self.get_function_data(),
1428
+ row = self.indexed_items[self.cursor_pos] if self.indexed_items else [],
1429
+ cell = self.indexed_items[self.cursor_pos][1][self.selected_column] if self.indexed_items else "",
1430
+ data=data,
1431
+ )
1432
+
1238
1433
  ## Display infobox
1239
1434
  if self.display_infobox:
1240
1435
  self.infobox(self.stdscr, message=self.infobox_items, title=self.infobox_title)
1241
1436
  # 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
1242
1437
 
1243
1438
 
1439
+ def refresh_and_draw_screen(self):
1440
+ """
1441
+ Clears and refreshes the screen, restricts and unrestricts curses,
1442
+ ensures correct terminal settings, and then draws the screen.
1443
+ """
1444
+
1445
+ self.logger.info(f"key_function redraw_screen")
1446
+ self.stdscr.clear()
1447
+ self.stdscr.refresh()
1448
+ restrict_curses(self.stdscr)
1449
+ unrestrict_curses(self.stdscr)
1450
+ self.stdscr.clear()
1451
+ self.stdscr.refresh()
1452
+
1453
+ self.draw_screen()
1244
1454
 
1245
1455
  def infobox(self, stdscr: curses.window, message: str ="", title: str ="Infobox", colours_end: int = 0, duration: int = 4) -> curses.window:
1246
1456
  """ Display non-interactive infobox window. """
1247
1457
 
1248
1458
  self.logger.info(f"function: infobox()")
1249
- h, w = stdscr.getmaxyx()
1250
- notification_width, notification_height = w//2, 3*h//5
1459
+ self.update_term_size()
1460
+
1461
+
1462
+ notification_width, notification_height = self.term_w//2, 3*self.term_h//5
1251
1463
  message_width = notification_width-5
1252
1464
 
1253
1465
  if not message: message = "!!"
@@ -1263,9 +1475,9 @@ class Picker:
1263
1475
  if len(submenu_items) > notification_height - 2:
1264
1476
  submenu_items = submenu_items[:notification_height-3] + [f"{'....':^{notification_width}}"]
1265
1477
  while True:
1266
- h, w = stdscr.getmaxyx()
1267
1478
 
1268
- submenu_win = curses.newwin(notification_height, notification_width, 3, w - (notification_width+4))
1479
+ self.update_term_size()
1480
+ submenu_win = curses.newwin(notification_height, notification_width, 3, self.term_w - (notification_width+4))
1269
1481
  infobox_data = {
1270
1482
  "items": submenu_items,
1271
1483
  "colours": notification_colours,
@@ -1280,6 +1492,7 @@ class Picker:
1280
1492
  "reset_colours": False,
1281
1493
  "cell_cursor": False,
1282
1494
  "split_right": False,
1495
+ "split_left": False,
1283
1496
  "crosshair_cursor": False,
1284
1497
  }
1285
1498
 
@@ -1293,113 +1506,127 @@ class Picker:
1293
1506
  self.logger.debug(f"function: get_function_data()")
1294
1507
  """ Returns a dict of the main variables needed to restore the state of list_pikcer. """
1295
1508
  function_data = {
1296
- "selections": self.selections,
1297
- "cell_selections": self.cell_selections,
1298
- "selected_cells_by_row": self.selected_cells_by_row,
1299
- "items_per_page": self.items_per_page,
1300
- "current_row": self.current_row,
1301
- "current_page": self.current_page,
1302
- "cursor_pos": self.cursor_pos,
1303
- "colours": self.colours,
1304
- "colour_theme_number": self.colour_theme_number,
1305
- "selected_column": self.selected_column,
1306
- "sort_column": self.sort_column,
1307
- "sort_method": self.sort_method,
1308
- "sort_reverse": self.sort_reverse,
1309
- "SORT_METHODS": self.SORT_METHODS,
1310
- "hidden_columns": self.hidden_columns,
1311
- "is_selecting": self.is_selecting,
1312
- "is_deselecting": self.is_deselecting,
1313
- "user_opts": self.user_opts,
1314
- "options_list": self.options_list,
1315
- "user_settings": self.user_settings,
1316
- "separator": self.separator,
1317
- "search_query": self.search_query,
1318
- "search_count": self.search_count,
1319
- "search_index": self.search_index,
1320
- "filter_query": self.filter_query,
1321
- "indexed_items": self.indexed_items,
1322
- "start_selection": self.start_selection,
1323
- "start_selection_col": self.start_selection_col,
1324
- "end_selection": self.end_selection,
1325
- "highlights": self.highlights,
1326
- "max_column_width": self.max_column_width,
1327
- "column_indices": self.column_indices,
1328
- "mode_index": self.mode_index,
1329
- "modes": self.modes,
1330
- "title": self.title,
1331
- "display_modes": self.display_modes,
1332
- "require_option": self.require_option,
1333
- "require_option_default": self.require_option_default,
1334
- "option_functions": self.option_functions,
1335
- "top_gap": self.top_gap,
1336
- "number_columns": self.number_columns,
1337
- "items": self.items,
1338
- "indexed_items": self.indexed_items,
1339
- "header": self.header,
1340
- "scroll_bar": self.scroll_bar,
1341
- "columns_sort_method": self.columns_sort_method,
1342
- "disabled_keys": self.disabled_keys,
1343
- "show_footer": self.show_footer,
1344
- "footer_string": self.footer_string,
1345
- "footer_string_auto_refresh": self.footer_string_auto_refresh,
1346
- "footer_string_refresh_function": self.footer_string_refresh_function,
1347
- "footer_timer": self.footer_timer,
1348
- "footer_style": self.footer_style,
1349
- "colours_start": self.colours_start,
1350
- "colours_end": self.colours_end,
1351
- "display_only": self.display_only,
1352
- "infobox_items": self.infobox_items,
1353
- "display_infobox": self.display_infobox,
1354
- "infobox_title": self.infobox_title,
1355
- "key_remappings": self.key_remappings,
1356
- "auto_refresh": self.auto_refresh,
1357
- "get_new_data": self.get_new_data,
1358
- "refresh_function": self.refresh_function,
1359
- "timer": self.timer,
1360
- "get_data_startup": self.get_data_startup,
1361
- "get_footer_string_startup": self.get_footer_string_startup,
1362
- "editable_columns": self.editable_columns,
1363
- "last_key": self.last_key,
1364
- "centre_in_terminal": self.centre_in_terminal,
1365
- "centre_in_terminal_vertical": self.centre_in_terminal_vertical,
1366
- "centre_in_cols": self.centre_in_cols,
1367
- "highlight_full_row": self.highlight_full_row,
1368
- "cell_cursor": self.cell_cursor,
1369
- "column_widths": self.column_widths,
1370
- "track_entries_upon_refresh": self.track_entries_upon_refresh,
1371
- "pin_cursor": self.pin_cursor,
1372
- "id_column": self.id_column,
1373
- "startup_notification": self.startup_notification,
1374
- "keys_dict": self.keys_dict,
1375
- "cancel_is_back": self.cancel_is_back,
1376
- "paginate": self.paginate,
1377
- "leftmost_char": self.leftmost_char,
1378
- "history_filter_and_search" : self.history_filter_and_search,
1379
- "history_pipes" : self.history_pipes,
1380
- "history_opts" : self.history_opts,
1381
- "history_edits" : self.history_edits,
1382
- "history_settings": self.history_settings,
1383
- "show_header": self.show_header,
1384
- "show_row_header": self.show_row_header,
1385
- "debug": self.debug,
1386
- "debug_level": self.debug_level,
1387
- "reset_colours": self.reset_colours,
1388
- "unicode_char_width": self.unicode_char_width,
1389
- "command_stack": self.command_stack,
1390
- "loaded_file": self.loaded_file,
1391
- "loaded_files": self.loaded_files,
1392
- "loaded_file_index": self.loaded_file_index,
1393
- "loaded_file_states": self.loaded_file_states,
1394
- "sheet_index": self.sheet_index,
1395
- "sheets": self.sheets,
1396
- "sheet_name": self.sheet_name,
1397
- "sheet_states": self.sheet_states,
1398
- "split_right": self.split_right,
1399
- "right_panes": self.right_panes,
1400
- "right_pane_index": self.right_pane_index,
1401
- "crosshair_cursor": self.crosshair_cursor,
1402
-
1509
+ "self": self,
1510
+ "selections": self.selections,
1511
+ "cell_selections": self.cell_selections,
1512
+ "selected_cells_by_row": self.selected_cells_by_row,
1513
+ "items_per_page": self.items_per_page,
1514
+ "current_row": self.current_row,
1515
+ "current_page": self.current_page,
1516
+ "cursor_pos": self.cursor_pos,
1517
+ "colours": self.colours,
1518
+ "colour_theme_number": self.colour_theme_number,
1519
+ "selected_column": self.selected_column,
1520
+ "sort_column": self.sort_column,
1521
+ "sort_method": self.sort_method,
1522
+ "sort_reverse": self.sort_reverse,
1523
+ "SORT_METHODS": self.SORT_METHODS,
1524
+ "hidden_columns": self.hidden_columns,
1525
+ "is_selecting": self.is_selecting,
1526
+ "is_deselecting": self.is_deselecting,
1527
+ "user_opts": self.user_opts,
1528
+ "options_list": self.options_list,
1529
+ "user_settings": self.user_settings,
1530
+ "separator": self.separator,
1531
+ "header_separator": self.header_separator,
1532
+ "header_separator_before_selected_column": self.header_separator_before_selected_column,
1533
+ "search_query": self.search_query,
1534
+ "search_count": self.search_count,
1535
+ "search_index": self.search_index,
1536
+ "filter_query": self.filter_query,
1537
+ "indexed_items": self.indexed_items,
1538
+ "start_selection": self.start_selection,
1539
+ "start_selection_col": self.start_selection_col,
1540
+ "end_selection": self.end_selection,
1541
+ "highlights": self.highlights,
1542
+ "max_column_width": self.max_column_width,
1543
+ "column_indices": self.column_indices,
1544
+ "mode_index": self.mode_index,
1545
+ "modes": self.modes,
1546
+ "title": self.title,
1547
+ "display_modes": self.display_modes,
1548
+ "require_option": self.require_option,
1549
+ "require_option_default": self.require_option_default,
1550
+ "option_functions": self.option_functions,
1551
+ "top_gap": self.top_gap,
1552
+ "number_columns": self.number_columns,
1553
+ "items": self.items,
1554
+ "indexed_items": self.indexed_items,
1555
+ "header": self.header,
1556
+ "scroll_bar": self.scroll_bar,
1557
+ "columns_sort_method": self.columns_sort_method,
1558
+ "disabled_keys": self.disabled_keys,
1559
+ "show_footer": self.show_footer,
1560
+ "footer_string": self.footer_string,
1561
+ "footer_string_auto_refresh": self.footer_string_auto_refresh,
1562
+ "footer_string_refresh_function": self.footer_string_refresh_function,
1563
+ "footer_timer": self.footer_timer,
1564
+ "footer_style": self.footer_style,
1565
+ "colours_start": self.colours_start,
1566
+ "colours_end": self.colours_end,
1567
+ "display_only": self.display_only,
1568
+ "infobox_items": self.infobox_items,
1569
+ "display_infobox": self.display_infobox,
1570
+ "infobox_title": self.infobox_title,
1571
+ "key_remappings": self.key_remappings,
1572
+ "auto_refresh": self.auto_refresh,
1573
+ "get_new_data": self.get_new_data,
1574
+ "refresh_function": self.refresh_function,
1575
+ "timer": self.timer,
1576
+ "get_data_startup": self.get_data_startup,
1577
+ "get_footer_string_startup": self.get_footer_string_startup,
1578
+ "editable_columns": self.editable_columns,
1579
+ "last_key": self.last_key,
1580
+ "centre_in_terminal": self.centre_in_terminal,
1581
+ "centre_in_terminal_vertical": self.centre_in_terminal_vertical,
1582
+ "centre_in_cols": self.centre_in_cols,
1583
+ "highlight_full_row": self.highlight_full_row,
1584
+ "cell_cursor": self.cell_cursor,
1585
+ "column_widths": self.column_widths,
1586
+ "track_entries_upon_refresh": self.track_entries_upon_refresh,
1587
+ "pin_cursor": self.pin_cursor,
1588
+ "id_column": self.id_column,
1589
+ "startup_notification": self.startup_notification,
1590
+ "keys_dict": self.keys_dict,
1591
+ "macros": self.macros,
1592
+ "cancel_is_back": self.cancel_is_back,
1593
+ "paginate": self.paginate,
1594
+ "leftmost_char": self.leftmost_char,
1595
+ "history_filter_and_search" : self.history_filter_and_search,
1596
+ "history_pipes" : self.history_pipes,
1597
+ "history_opts" : self.history_opts,
1598
+ "history_edits" : self.history_edits,
1599
+ "history_settings": self.history_settings,
1600
+ "show_header": self.show_header,
1601
+ "show_row_header": self.show_row_header,
1602
+ "debug": self.debug,
1603
+ "debug_level": self.debug_level,
1604
+ "reset_colours": self.reset_colours,
1605
+ "unicode_char_width": self.unicode_char_width,
1606
+ "command_stack": self.command_stack,
1607
+ "loaded_file": self.loaded_file,
1608
+ "loaded_files": self.loaded_files,
1609
+ "loaded_file_index": self.loaded_file_index,
1610
+ "loaded_file_states": self.loaded_file_states,
1611
+ "sheet_index": self.sheet_index,
1612
+ "sheets": self.sheets,
1613
+ "sheet_name": self.sheet_name,
1614
+ "sheet_states": self.sheet_states,
1615
+ "split_right": self.split_right,
1616
+ "right_panes": self.right_panes,
1617
+ "right_pane_index": self.right_pane_index,
1618
+ "split_left": self.split_left,
1619
+ "left_panes": self.left_panes,
1620
+ "left_pane_index": self.left_pane_index,
1621
+ "crosshair_cursor": self.crosshair_cursor,
1622
+ "generate_data_for_hidden_columns": self.generate_data_for_hidden_columns,
1623
+ "thread_stop_event": self.thread_stop_event,
1624
+ "data_generation_queue": self.data_generation_queue,
1625
+ "process_manager": self.process_manager,
1626
+ "threads": self.threads,
1627
+ "processes": self.processes,
1628
+ "items_sync_loop_event": self.items_sync_loop_event,
1629
+ "items_sync_thread": self.items_sync_thread,
1403
1630
  }
1404
1631
  return function_data
1405
1632
 
@@ -1435,6 +1662,9 @@ class Picker:
1435
1662
  "centre_in_cols",
1436
1663
  "centre_in_terminal",
1437
1664
  "split_right",
1665
+ "left_pane_index",
1666
+ "split_left",
1667
+ "left_pane_index",
1438
1668
  ]
1439
1669
 
1440
1670
  for var in variables:
@@ -1449,15 +1679,6 @@ class Picker:
1449
1679
  self.initialise_picker_state(reset_colours=reset_colours)
1450
1680
 
1451
1681
  self.initialise_variables()
1452
- # if "colour_theme_number" in function_data:
1453
- # global COLOURS_SET
1454
- # COLOURS_SET = False
1455
- # colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
1456
-
1457
- # if "items" in function_data: self.items = function_data["items"]
1458
- # if "header" in function_data: self.header = function_data["header"]
1459
- # self.indexed_items = function_data["indexed_items"] if "indexed_items" in function_data else []
1460
-
1461
1682
 
1462
1683
 
1463
1684
  def delete_entries(self) -> None:
@@ -1475,7 +1696,7 @@ class Picker:
1475
1696
  self.selections = {i:False for i in range(len(self.indexed_items))}
1476
1697
  self.cursor_pos = min(self.cursor_pos, len(self.indexed_items)-1)
1477
1698
  self.initialise_variables()
1478
- self.draw_screen(self.indexed_items, self.highlights)
1699
+ self.draw_screen()
1479
1700
 
1480
1701
 
1481
1702
  def choose_option(
@@ -1528,35 +1749,134 @@ class Picker:
1528
1749
  "number_columns": False,
1529
1750
  "reset_colours": False,
1530
1751
  "split_right": False,
1752
+ "split_left": False,
1531
1753
  "cell_cursor": False,
1532
1754
  "crosshair_cursor": False,
1755
+ "header_separator": " │",
1533
1756
  }
1534
1757
  while True:
1535
- h, w = stdscr.getmaxyx()
1758
+ self.update_term_size()
1536
1759
 
1537
1760
  choose_opts_widths = get_column_widths(options, unicode_char_width=self.unicode_char_width)
1538
- window_width = min(max(sum(choose_opts_widths) + 6, 50) + 6, w)
1539
- window_height = min(h//2, max(6, len(options)+3))
1761
+ window_width = min(max(sum(choose_opts_widths) + 6, 50) + 6, self.term_w)
1762
+ window_height = min(self.term_h//2, max(6, len(options)+3))
1540
1763
 
1541
- submenu_win = curses.newwin(window_height, window_width, (h-window_height)//2, (w-window_width)//2)
1764
+ submenu_win = curses.newwin(window_height, window_width, (self.term_h-window_height)//2, (self.term_w-window_width)//2)
1542
1765
  submenu_win.keypad(True)
1766
+ option_picker_data["screen_size_function"] = lambda stdscr: (window_height, window_width)
1543
1767
  OptionPicker = Picker(submenu_win, **option_picker_data)
1544
1768
  s, o, f = OptionPicker.run()
1545
1769
 
1546
1770
  if o == "refresh":
1547
- self.draw_screen(self.indexed_items, self.highlights)
1771
+ self.draw_screen()
1548
1772
  continue
1549
1773
  if s:
1550
1774
  return {x: options[x] for x in s}, o, f
1551
1775
  return {}, "", f
1552
1776
 
1553
1777
 
1778
+ def select_columns(
1779
+ self,
1780
+ stdscr: curses.window,
1781
+ # options: list[list[str]] =[],
1782
+ # title: str = "Choose option",
1783
+ # x:int=0,
1784
+ # y:int=0,
1785
+ # literal:bool=False,
1786
+ # colours_start:int=0,
1787
+ # header: list[str] = [],
1788
+ # require_option:list = [],
1789
+ # option_functions: list = [],
1790
+ ) -> Tuple[dict, str, dict]:
1791
+ """
1792
+ Display input field at x,y
1793
+
1794
+ ---Arguments
1795
+ stdscr: curses screen
1796
+ usrtxt (str): text to be edited by the user
1797
+ title (str): The text to be displayed at the start of the text option picker
1798
+ x (int): prompt begins at (x,y) in the screen given
1799
+ y (int): prompt begins at (x,y) in the screen given
1800
+ colours_start (bool): start index of curses init_pair.
1801
+
1802
+ ---Returns
1803
+ usrtxt, return_code
1804
+ usrtxt: the text inputted by the user
1805
+ return_code:
1806
+ 0: user hit escape
1807
+ 1: user hit return
1808
+ """
1809
+ self.logger.info(f"function: select_columns()")
1810
+
1811
+ cursor = 0
1812
+
1813
+ if self.header:
1814
+ columns = [s for i, s in enumerate(self.header)]
1815
+ else:
1816
+ columns = [f"" for i in range(len(self.column_widths))]
1817
+
1818
+ ## Column info variable
1819
+ columns_set = [[f"{i}", columns[i]] for i in range(len(self.column_widths))]
1820
+ header = ["#", "Column Name"]
1821
+
1822
+ selected = [False if i in self.hidden_columns else True for i in range(len(self.column_widths))]
1823
+ selected = {i: False if i in self.hidden_columns else True for i in range(len(self.column_widths))}
1824
+
1825
+ option_picker_data = {
1826
+ "items": columns_set,
1827
+ "colours": notification_colours,
1828
+ "colours_start": self.notification_colours_start,
1829
+ "title":"Select Columns",
1830
+ "header": header,
1831
+ "hidden_columns":[],
1832
+ # "require_option":require_option,
1833
+ # "keys_dict": options_keys,
1834
+ "selections": selected,
1835
+ "show_footer": False,
1836
+ "cancel_is_back": True,
1837
+ "number_columns": False,
1838
+ "reset_colours": False,
1839
+ "split_right": False,
1840
+ "split_left": False,
1841
+ "cell_cursor": False,
1842
+ "crosshair_cursor": False,
1843
+ "separator": " ",
1844
+ "header_separator": " │",
1845
+ "header_separator_before_selected_column": " ▐",
1846
+ "selected_char": "☒",
1847
+ "unselected_char": "☐",
1848
+ "selecting_char": "☒",
1849
+ "deselecting_char": "☐",
1850
+ }
1851
+ while True:
1852
+ self.update_term_size()
1853
+
1854
+ choose_opts_widths = get_column_widths(columns_set, unicode_char_width=self.unicode_char_width)
1855
+ window_width = min(max(sum(choose_opts_widths) + 6, 50) + 6, self.term_w)
1856
+ window_height = min(self.term_h//2, max(6, len(columns_set)+3))
1857
+
1858
+ submenu_win = curses.newwin(window_height, window_width, (self.term_h-window_height)//2, (self.term_w-window_width)//2)
1859
+ submenu_win.keypad(True)
1860
+ option_picker_data["screen_size_function"] = lambda stdscr: (window_height, window_width)
1861
+ OptionPicker = Picker(submenu_win, **option_picker_data)
1862
+ s, o, f = OptionPicker.run()
1863
+
1864
+ if o == "refresh":
1865
+ self.draw_screen()
1866
+ continue
1867
+ if s:
1868
+ selected_columns = s
1869
+ self.hidden_columns = [i for i in range(len(self.column_widths)) if i not in selected_columns]
1870
+
1871
+ # return {x: options[x] for x in s}, o, f
1872
+ break
1873
+ return {}, "", f
1554
1874
 
1555
1875
  def notification(self, stdscr: curses.window, message: str="", title:str="Notification", colours_end: int=0, duration:int=4) -> None:
1556
1876
 
1557
1877
  self.logger.info(f"function: notification()")
1558
1878
  """ Notification box. """
1559
- notification_width, notification_height = 50, 7
1879
+ notification_width, notification_height = min(self.term_w-4, 50), 7
1560
1880
  message_width = notification_width-5
1561
1881
 
1562
1882
  if not message: message = "!!"
@@ -1569,9 +1889,9 @@ class Picker:
1569
1889
  27: ord('q')
1570
1890
  }
1571
1891
  while True:
1572
- h, w = stdscr.getmaxyx()
1892
+ self.update_term_size()
1573
1893
 
1574
- submenu_win = curses.newwin(notification_height, notification_width, 3, w - (notification_width+4))
1894
+ submenu_win = curses.newwin(notification_height, notification_width, 3, self.term_w - (notification_width+2))
1575
1895
  notification_data = {
1576
1896
  "items": submenu_items,
1577
1897
  "title": title,
@@ -1588,9 +1908,11 @@ class Picker:
1588
1908
  "cancel_is_back": True,
1589
1909
  "reset_colours": False,
1590
1910
  "split_right": False,
1911
+ "split_left": False,
1591
1912
  "cell_cursor": False,
1592
1913
  "crosshair_cursor": False,
1593
-
1914
+ "show_header": False,
1915
+ "screen_size_function": lambda stdscr: (notification_height, notification_width),
1594
1916
  }
1595
1917
  OptionPicker = Picker(submenu_win, **notification_data)
1596
1918
  s, o, f = OptionPicker.run()
@@ -1601,7 +1923,7 @@ class Picker:
1601
1923
  del submenu_win
1602
1924
  stdscr.clear()
1603
1925
  stdscr.refresh()
1604
- self.draw_screen(self.indexed_items, self.highlights)
1926
+ self.draw_screen()
1605
1927
  # set_colours(colours=get_colours(0))
1606
1928
 
1607
1929
  def toggle_column_visibility(self, col_index:int) -> None:
@@ -1708,7 +2030,7 @@ class Picker:
1708
2030
  elif setting == "file_prev":
1709
2031
  self.command_stack.append(Command("setting", self.user_settings))
1710
2032
  self.switch_file(increment=-1)
1711
- # self.draw_screen(self.indexed_items, self.highlights)
2033
+ # self.draw_screen()
1712
2034
  # self.stdscr.refresh()
1713
2035
 
1714
2036
  elif setting == "sheet_next":
@@ -1727,14 +2049,24 @@ class Picker:
1727
2049
  self.footer_style = (self.footer_style+1)%len(self.footer_options)
1728
2050
  self.footer = self.footer_options[self.footer_style]
1729
2051
  self.initialise_variables()
1730
- elif setting == "pane":
2052
+ elif setting == "rpane":
1731
2053
  self.toggle_right_pane()
1732
2054
 
1733
- elif setting == "pane_cycle":
2055
+ elif setting == "rpane_cycle":
1734
2056
  self.cycle_right_pane()
1735
2057
 
2058
+ elif setting == "lpane":
2059
+ self.toggle_left_pane()
2060
+
2061
+ elif setting == "lpane_cycle":
2062
+ self.cycle_left_pane()
2063
+
1736
2064
  elif setting.startswith("cwd="):
1737
2065
  os.chdir(os.path.expandvars(os.path.expanduser(setting[len("cwd="):])))
2066
+ elif setting.startswith("lmc="):
2067
+ rem = setting[4:]
2068
+ if rem.isnumeric():
2069
+ self.leftmost_char = int(rem)
1738
2070
  elif setting.startswith("hl"):
1739
2071
  hl_list = setting.split(",")
1740
2072
  if len(hl_list) > 1:
@@ -1772,7 +2104,7 @@ class Picker:
1772
2104
  theme_number = int(setting[2:].strip())
1773
2105
  self.colour_theme_number = min(get_theme_count()-1, theme_number)
1774
2106
  set_colours(self.colour_theme_number)
1775
- self.draw_screen(self.indexed_items, self.highlights)
2107
+ self.draw_screen()
1776
2108
  self.notification(self.stdscr, message=f"Theme {self.colour_theme_number} applied.")
1777
2109
  except:
1778
2110
  pass
@@ -1781,8 +2113,12 @@ class Picker:
1781
2113
  self.colour_theme_number = (self.colour_theme_number + 1)%get_theme_count()
1782
2114
  # self.colour_theme_number = int(not bool(self.colour_theme_number))
1783
2115
  set_colours(self.colour_theme_number)
1784
- self.draw_screen(self.indexed_items, self.highlights)
2116
+ self.draw_screen()
1785
2117
  self.notification(self.stdscr, message=f"Theme {self.colour_theme_number} applied.")
2118
+ self.colours = get_colours(self.colour_theme_number)
2119
+ elif setting == "colsel":
2120
+ self.draw_screen()
2121
+ self.select_columns(self.stdscr)
1786
2122
 
1787
2123
  else:
1788
2124
  self.user_settings = ""
@@ -1807,7 +2143,7 @@ class Picker:
1807
2143
  """ Toggle selection of item at index. """
1808
2144
  self.logger.info(f"function: toggle_item()")
1809
2145
  self.selections[index] = not self.selections[index]
1810
- self.draw_screen(self.indexed_items, self.highlights)
2146
+ self.draw_screen()
1811
2147
 
1812
2148
  def select_all(self) -> None:
1813
2149
  """ Select all in indexed_items. """
@@ -1818,7 +2154,7 @@ class Picker:
1818
2154
  self.cell_selections[i] = True
1819
2155
  for row in range(len(self.indexed_items)):
1820
2156
  self.selected_cells_by_row[row] = list(range(len(self.indexed_items[row][1])))
1821
- self.draw_screen(self.indexed_items, self.highlights)
2157
+ self.draw_screen()
1822
2158
 
1823
2159
  def deselect_all(self) -> None:
1824
2160
  """ Deselect all items in indexed_items. """
@@ -1828,7 +2164,7 @@ class Picker:
1828
2164
  for i in self.cell_selections.keys():
1829
2165
  self.cell_selections[i] = False
1830
2166
  self.selected_cells_by_row = {}
1831
- self.draw_screen(self.indexed_items, self.highlights)
2167
+ self.draw_screen()
1832
2168
 
1833
2169
  def handle_visual_selection(self, selecting:bool = True) -> None:
1834
2170
  """ Toggle visual selection or deselection. """
@@ -1872,7 +2208,7 @@ class Picker:
1872
2208
  self.end_selection = -1
1873
2209
  self.is_selecting = False
1874
2210
 
1875
- self.draw_screen(self.indexed_items, self.highlights)
2211
+ self.draw_screen()
1876
2212
 
1877
2213
  elif self.is_deselecting:
1878
2214
  self.end_selection = self.indexed_items[self.cursor_pos][0]
@@ -1905,7 +2241,7 @@ class Picker:
1905
2241
  self.start_selection = -1
1906
2242
  self.end_selection = -1
1907
2243
  self.is_deselecting = False
1908
- self.draw_screen(self.indexed_items, self.highlights)
2244
+ self.draw_screen()
1909
2245
 
1910
2246
  def cursor_down(self, count=1) -> bool:
1911
2247
  """ Move cursor down. """
@@ -1919,6 +2255,7 @@ class Picker:
1919
2255
  if self.indexed_items[new_pos][0] in self.unselectable_indices: new_pos+=1
1920
2256
  else: break
1921
2257
  self.cursor_pos = new_pos
2258
+ self.ensure_no_overscroll()
1922
2259
  return True
1923
2260
 
1924
2261
  def cursor_up(self, count=1) -> bool:
@@ -1932,6 +2269,7 @@ class Picker:
1932
2269
  elif new_pos in self.unselectable_indices: new_pos -= 1
1933
2270
  else: break
1934
2271
  self.cursor_pos = new_pos
2272
+ self.ensure_no_overscroll()
1935
2273
  return True
1936
2274
 
1937
2275
  def remapped_key(self, key: int, val: int, key_remappings: dict) -> bool:
@@ -1953,6 +2291,19 @@ class Picker:
1953
2291
  return True
1954
2292
  return False
1955
2293
 
2294
+ def check_and_run_macro(self, key: int) -> bool:
2295
+ macro_match = False
2296
+ for macro in self.macros:
2297
+ try:
2298
+ if key in macro["keys"]:
2299
+ macro_match = True
2300
+ macro["function"](self)
2301
+ break
2302
+ except:
2303
+ pass
2304
+ return macro_match
2305
+
2306
+
1956
2307
  def copy_dialogue(self) -> None:
1957
2308
  """ Display dialogue to select how rows/cells should be copied. """
1958
2309
  self.logger.info(f"function: copy_dialogue()")
@@ -2023,12 +2374,12 @@ class Picker:
2023
2374
  if not acceptable_data_type:
2024
2375
  break
2025
2376
  if not acceptable_data_type:
2026
- self.draw_screen(self.indexed_items, self.highlights)
2377
+ self.draw_screen()
2027
2378
  self.notification(self.stdscr, message="Error pasting data.")
2028
2379
  return None
2029
2380
 
2030
2381
  except:
2031
- self.draw_screen(self.indexed_items, self.highlights)
2382
+ self.draw_screen()
2032
2383
  self.notification(self.stdscr, message="Error pasting data.")
2033
2384
  return None
2034
2385
  if type(pasta) == type([]) and len(pasta) > 0 and type(pasta[0]) == type([]):
@@ -2075,7 +2426,7 @@ class Picker:
2075
2426
  for idx in s.keys():
2076
2427
  save_path_entered, save_path = output_file_option_selector(
2077
2428
  self.stdscr,
2078
- refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
2429
+ refresh_screen_function=lambda: self.draw_screen()
2079
2430
  )
2080
2431
  if save_path_entered:
2081
2432
  return_val = funcs[idx](save_path)
@@ -2109,7 +2460,7 @@ class Picker:
2109
2460
  self.loaded_file_states[self.loaded_file_index] = self.get_function_data()
2110
2461
 
2111
2462
  self.stdscr.clear()
2112
- self.draw_screen(self.indexed_items, self.highlights)
2463
+ self.draw_screen()
2113
2464
 
2114
2465
  tmp = self.stdscr
2115
2466
 
@@ -2121,7 +2472,7 @@ class Picker:
2121
2472
 
2122
2473
  self.stdscr = tmp
2123
2474
 
2124
- self.notification(self.stdscr, f"{repr(file_to_load)} has been loaded!")
2475
+ # self.notification(self.stdscr, f"{repr(file_to_load)} has been loaded!")
2125
2476
 
2126
2477
  self.set_function_data({}, reset_absent_variables=True)
2127
2478
  self.load_file(self.loaded_file)
@@ -2129,7 +2480,7 @@ class Picker:
2129
2480
  # header = return_val["header"]
2130
2481
  self.stdscr.clear()
2131
2482
  # self.initialise_variables()
2132
- self.draw_screen(self.indexed_items, self.highlights)
2483
+ self.draw_screen()
2133
2484
  # self.stdscr.refresh()
2134
2485
 
2135
2486
  # if return_val:
@@ -2144,7 +2495,15 @@ class Picker:
2144
2495
  def fetch_data(self) -> None:
2145
2496
  """ Refesh data asynchronously. When data has been fetched self.data_ready is set to True. """
2146
2497
  self.logger.info(f"function: fetch_data()")
2147
- tmp_items, tmp_header = self.refresh_function()
2498
+ tmp_items, tmp_header = [], []
2499
+ self.getting_data.clear()
2500
+ self.refresh_function(
2501
+ tmp_items,
2502
+ tmp_header,
2503
+ self.visible_rows_indices,
2504
+ self.getting_data,
2505
+ self.get_function_data(),
2506
+ )
2148
2507
  if self.track_entries_upon_refresh:
2149
2508
  selected_indices = get_selected_indices(self.selections)
2150
2509
  self.ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
@@ -2242,10 +2601,6 @@ class Picker:
2242
2601
  row_len = 1
2243
2602
  if self.header: row_len = len(self.header)
2244
2603
  elif len(self.items): row_len = len(self.items[0])
2245
- # if len(self.indexed_items) == 0:
2246
- # insert_at_pos = 0
2247
- # else:
2248
- # insert_at_pos = self.indexed_items[self.cursor_pos][0]
2249
2604
  self.items = self.items[:pos] + [["" for x in range(row_len)]] + self.items[pos:]
2250
2605
  if pos <= self.cursor_pos:
2251
2606
  self.cursor_pos += 1
@@ -2355,27 +2710,99 @@ class Picker:
2355
2710
  self.split_right = not self.split_right
2356
2711
  if self.right_panes[self.right_pane_index]["data"] in [[], None, {}]:
2357
2712
  self.right_panes[self.right_pane_index]["data"] = self.right_panes[self.right_pane_index]["get_data"](self.right_panes[self.right_pane_index]["data"], self.get_function_data())
2713
+ self.ensure_no_overscroll()
2714
+
2715
+ def toggle_left_pane(self):
2716
+ if len(self.left_panes):
2717
+ self.split_left = not self.split_left
2718
+ if self.left_panes[self.left_pane_index]["data"] in [[], None, {}]:
2719
+ self.left_panes[self.left_pane_index]["data"] = self.left_panes[self.left_pane_index]["get_data"](self.left_panes[self.left_pane_index]["data"], self.get_function_data())
2720
+ self.ensure_no_overscroll()
2358
2721
 
2359
2722
 
2360
2723
  def cycle_right_pane(self, increment=1):
2361
2724
  if len(self.right_panes) > 1:
2362
2725
  self.right_pane_index = (self.right_pane_index+1)%len(self.right_panes)
2363
- self.initial_split_time = self.initial_split_time - self.right_panes[self.right_pane_index]["refresh_time"]
2726
+ self.initial_right_split_time -= self.right_panes[self.right_pane_index]["refresh_time"]
2727
+ self.ensure_no_overscroll()
2728
+
2729
+ def cycle_left_pane(self, increment=1):
2730
+ if len(self.left_panes) > 1:
2731
+ self.left_pane_index = (self.left_pane_index+1)%len(self.left_panes)
2732
+ self.initial_left_split_time -= self.left_panes[self.left_pane_index]["refresh_time"]
2733
+ self.ensure_no_overscroll()
2734
+
2735
+ def ensure_no_overscroll(self):
2736
+ """
2737
+ Ensure that we haven't scrolled past the last column.
2738
+
2739
+ This check should be performed after:
2740
+ - Terminal resize event
2741
+ - Scrolling down - i.e., rows with potentially different widths come into view
2742
+ """
2743
+ self.calculate_section_sizes()
2744
+ self.get_visible_rows()
2745
+ self.column_widths = get_column_widths(
2746
+ self.visible_rows,
2747
+ header=self.header,
2748
+ max_column_width=self.max_column_width,
2749
+ number_columns=self.number_columns,
2750
+ max_total_width=self.rows_w,
2751
+ unicode_char_width=self.unicode_char_width
2752
+ )
2753
+ self.calculate_section_sizes()
2754
+
2755
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
2756
+ if row_width - self.leftmost_char < self.rows_w:
2757
+ if row_width <= self.rows_w - self.left_gutter_width:
2758
+ self.leftmost_char = 0
2759
+ else:
2760
+ self.leftmost_char = row_width - (self.rows_w - self.left_gutter_width) + 5
2761
+
2762
+ def cleanup_processes(self):
2763
+ self.thread_stop_event.set()
2764
+ self.data_generation_queue.clear()
2765
+ # with self.data_generation_queue.mutex:
2766
+ # self.data_generation_queue.queue.clear()
2767
+ function_data = self.get_function_data()
2768
+ for proc in self.processes:
2769
+ if proc.is_alive():
2770
+ proc.terminate()
2771
+ proc.join(timeout=0.01)
2772
+ self.processes = []
2773
+ self.items_sync_loop_event.set()
2774
+ if self.items_sync_thread != None:
2775
+ self.items_sync_thread.join(timeout=1)
2776
+
2777
+ def cleanup_threads(self):
2778
+ self.thread_stop_event.set()
2779
+ with self.data_generation_queue.mutex:
2780
+ self.data_generation_queue.queue.clear()
2781
+ function_data = self.get_function_data()
2782
+ for t in self.threads:
2783
+ if t.is_alive():
2784
+ t.join(timeout=0.01)
2364
2785
 
2365
2786
  def run(self) -> Tuple[list[int], str, dict]:
2366
2787
  """ Run the picker. """
2367
2788
  self.logger.info(f"function: run()")
2368
2789
 
2790
+ self.thread_stop_event.clear()
2791
+
2369
2792
  if self.get_footer_string_startup and self.footer_string_refresh_function != None:
2793
+ self.footer_string = " "
2794
+ self.footer.adjust_sizes(self.term_h, self.term_w)
2795
+ self.draw_screen()
2370
2796
  self.footer_string = self.footer_string_refresh_function()
2371
2797
 
2372
2798
  self.initialise_variables(get_data=self.get_data_startup)
2373
2799
 
2374
- self.draw_screen(self.indexed_items, self.highlights)
2800
+ self.draw_screen()
2375
2801
 
2376
2802
  self.initial_time = time.time()
2377
2803
  self.initial_time_footer = time.time()-self.footer_timer
2378
- self.initial_split_time = time.time()-200
2804
+ self.initial_right_split_time = time.time()-200
2805
+ self.initial_left_split_time = time.time()-200
2379
2806
 
2380
2807
  if self.startup_notification:
2381
2808
  self.notification(self.stdscr, message=self.startup_notification)
@@ -2399,7 +2826,7 @@ class Picker:
2399
2826
 
2400
2827
  # Set terminal background color
2401
2828
  self.stdscr.bkgd(' ', curses.color_pair(self.colours_start+3)) # Apply background color
2402
- self.draw_screen(self.indexed_items, self.highlights)
2829
+ self.draw_screen()
2403
2830
 
2404
2831
  if self.display_only:
2405
2832
  self.stdscr.refresh()
@@ -2407,21 +2834,21 @@ class Picker:
2407
2834
  return [], "", function_data
2408
2835
 
2409
2836
  # Open tty to accept input
2410
- tty_fd = open_tty()
2837
+ tty_fd, self.saved_terminal_state = open_tty()
2838
+
2839
+ self.update_term_size()
2840
+ self.calculate_section_sizes()
2411
2841
 
2412
- h, w = self.stdscr.getmaxyx()
2413
- self.term_h, self.term_w = self.stdscr.getmaxyx()
2414
- if self.split_right and len(self.right_panes):
2415
- proportion = self.right_panes[self.right_pane_index]["proportion"]
2416
- self.rows_w, self.rows_h = int(self.term_w*proportion), self.term_h
2417
- else:
2418
- self.rows_w, self.rows_h = self.term_w, self.term_h
2419
2842
  def terminal_resized(old_w, old_h) -> bool:
2420
2843
  w, h = os.get_terminal_size()
2421
2844
  if old_h != h or old_w != w: return True
2422
2845
  else: return False
2423
2846
 
2424
2847
  COLS, LINES = os.get_terminal_size()
2848
+
2849
+
2850
+ getting_data_prev = False
2851
+
2425
2852
  # Main loop
2426
2853
  while True:
2427
2854
  # key = self.stdscr.getch()
@@ -2429,21 +2856,23 @@ class Picker:
2429
2856
  key = get_char(tty_fd, timeout=0.2)
2430
2857
  if key != -1:
2431
2858
  self.logger.info(f"key={key}")
2859
+ self.last_key = key
2860
+
2861
+ # Ensure that
2862
+
2863
+ if not self.getting_data.is_set():
2864
+ self.initialise_variables()
2865
+ getting_data_prev = True
2866
+ elif getting_data_prev:
2867
+ ## Ensure that we reinitialise one final time after all data is retrieved.
2868
+ self.initialise_variables()
2869
+ getting_data_prev = False
2432
2870
 
2433
2871
  self.term_resize_event = terminal_resized(COLS, LINES)
2434
2872
  COLS, LINES = os.get_terminal_size()
2435
2873
  if self.term_resize_event:
2436
2874
  key = curses.KEY_RESIZE
2437
2875
 
2438
- h, w = self.stdscr.getmaxyx()
2439
- self.term_h, self.term_w = self.stdscr.getmaxyx()
2440
-
2441
- if self.split_right and len(self.right_panes):
2442
- proportion = self.right_panes[self.right_pane_index]["proportion"]
2443
- self.rows_w, self.rows_h = int(self.term_w*proportion), self.term_h
2444
- else:
2445
- self.rows_w, self.rows_h = self.term_w, self.term_h
2446
-
2447
2876
  if key in self.disabled_keys: continue
2448
2877
  clear_screen=True
2449
2878
 
@@ -2457,16 +2886,20 @@ class Picker:
2457
2886
 
2458
2887
  self.initial_time = time.time()
2459
2888
 
2460
- self.draw_screen(self.indexed_items, self.highlights, clear=False)
2889
+ self.draw_screen(clear=False)
2461
2890
 
2462
2891
  self.refreshing_data = False
2463
2892
  self.data_ready = False
2464
2893
 
2894
+
2465
2895
  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() - self.initial_time) >= self.timer):
2466
2896
  self.logger.debug(f"Get new data (refresh).")
2467
- self.stdscr.addstr(0,self.term_w-3,"  ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
2897
+ try:
2898
+ self.stdscr.addstr(0,self.term_w-3,"  ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
2899
+ except:
2900
+ pass
2468
2901
  self.stdscr.refresh()
2469
- if self.get_new_data and self.refresh_function:
2902
+ if self.get_new_data:
2470
2903
  self.refreshing_data = True
2471
2904
 
2472
2905
  t = threading.Thread(target=self.fetch_data)
@@ -2493,13 +2926,19 @@ class Picker:
2493
2926
  self.logger.debug(f"footer_string_auto_refresh")
2494
2927
  self.footer_string = self.footer_string_refresh_function()
2495
2928
  self.initial_time_footer = time.time()
2496
- self.draw_screen(self.indexed_items, self.highlights)
2929
+ self.draw_screen()
2497
2930
 
2498
- if self.split_right and len(self.right_panes) and self.right_panes[self.right_pane_index]["auto_refresh"] and ((time.time() - self.initial_split_time) > self.right_panes[self.right_pane_index]["refresh_time"]):
2931
+ if self.split_right and len(self.right_panes) and self.right_panes[self.right_pane_index]["auto_refresh"] and ((time.time() - self.initial_right_split_time) > self.right_panes[self.right_pane_index]["refresh_time"]):
2499
2932
  get_data = self.right_panes[self.right_pane_index]["get_data"]
2500
2933
  data = self.right_panes[self.right_pane_index]["data"]
2501
2934
  self.right_panes[self.right_pane_index]["data"] = get_data(data, self.get_function_data())
2502
- self.initial_split_time = time.time()
2935
+ self.initial_right_split_time = time.time()
2936
+
2937
+ if self.split_left and len(self.left_panes) and self.left_panes[self.left_pane_index]["auto_refresh"] and ((time.time() - self.initial_left_split_time) > self.left_panes[self.left_pane_index]["refresh_time"]):
2938
+ get_data = self.left_panes[self.left_pane_index]["get_data"]
2939
+ data = self.left_panes[self.left_pane_index]["data"]
2940
+ self.left_panes[self.right_pane_index]["data"] = get_data(data, self.get_function_data())
2941
+ self.initial_left_split_time = time.time()
2503
2942
 
2504
2943
  if self.check_key("help", key, self.keys_dict):
2505
2944
  self.logger.info(f"key_function help")
@@ -2507,7 +2946,7 @@ class Picker:
2507
2946
  self.stdscr.refresh()
2508
2947
  help_data = {
2509
2948
  # "items": help_lines,
2510
- "items": build_help_rows(self.keys_dict),
2949
+ "items": build_help_rows(self.keys_dict, self.macros),
2511
2950
  "title": f"{self.title} Help",
2512
2951
  "colours_start": self.help_colours_start,
2513
2952
  "colours": help_colours,
@@ -2529,7 +2968,11 @@ class Picker:
2529
2968
  }
2530
2969
  OptionPicker = Picker(self.stdscr, **help_data)
2531
2970
  s, o, f = OptionPicker.run()
2532
- self.draw_screen(self.indexed_items, self.highlights)
2971
+ self.draw_screen()
2972
+
2973
+ if self.check_and_run_macro(key):
2974
+ self.draw_screen()
2975
+ continue
2533
2976
 
2534
2977
  if self.check_key("info", key, self.keys_dict):
2535
2978
  self.logger.info(f"key_function help")
@@ -2603,6 +3046,14 @@ class Picker:
2603
3046
  data["option_functions"] = f"[...] length = {len(data['option_functions'])}"
2604
3047
  data["loaded_file_states"] = f"[...] length = {len(data['loaded_file_states'])}"
2605
3048
  data["sheet_states"] = f"[...] length = {len(data['sheet_states'])}"
3049
+ data["highlights"] = f"[...] length = {len(data['highlights'])}"
3050
+ data["colours"] = f"[...] length = {len(data['colours'])}"
3051
+ data["keys_dict"] = f"[...] length = {len(data['keys_dict'])}"
3052
+ data["history_filter_and_search"] = f"[...] length = {len(data['history_filter_and_search'])}"
3053
+ data["history_opts"] = f"[...] length = {len(data['history_opts'])}"
3054
+ data["history_edits"] = f"[...] length = {len(data['history_edits'])}"
3055
+ data["history_pipes"] = f"[...] length = {len(data['history_pipes'])}"
3056
+ data["history_settings"] = f"[...] length = {len(data['history_settings'])}"
2606
3057
  info_items += [
2607
3058
  ["",""],
2608
3059
  [" get_function_data()", "-*"*30],
@@ -2643,13 +3094,14 @@ class Picker:
2643
3094
  OptionPicker = Picker(self.stdscr, **info_data)
2644
3095
  s, o, f = OptionPicker.run()
2645
3096
 
2646
- self.draw_screen(self.indexed_items, self.highlights)
3097
+ self.draw_screen()
2647
3098
 
2648
3099
  elif self.check_key("exit", key, self.keys_dict):
2649
3100
  self.stdscr.clear()
2650
3101
  if len(self.loaded_files) <= 1:
3102
+ self.cleanup_threads()
2651
3103
  function_data = self.get_function_data()
2652
- function_data["last_key"] = key
3104
+ restore_terminal_settings(tty_fd, self.saved_terminal_state)
2653
3105
  return [], "", function_data
2654
3106
  else:
2655
3107
  del self.loaded_files[self.loaded_file_index]
@@ -2666,28 +3118,30 @@ class Picker:
2666
3118
  self.set_function_data({}, reset_absent_variables=True)
2667
3119
  self.load_file(self.loaded_file)
2668
3120
  self.loaded_file_index, self.loaded_file = idx, file
2669
- self.draw_screen(self.indexed_items, self.highlights)
3121
+ self.draw_screen()
2670
3122
 
2671
3123
  elif self.check_key("full_exit", key, self.keys_dict):
3124
+ self.cleanup_threads()
2672
3125
  close_curses(self.stdscr)
3126
+ restore_terminal_settings(tty_fd, self.saved_terminal_state)
2673
3127
  exit()
2674
3128
 
2675
3129
  elif self.check_key("settings_input", key, self.keys_dict):
2676
3130
  self.logger.info(f"Settings input")
2677
3131
  usrtxt = f"{self.user_settings.strip()} " if self.user_settings else ""
2678
- field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
2679
- if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
2680
- else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
3132
+ field_end_f = lambda: self.get_term_size()[1]-38 if self.show_footer else lambda: self.get_term_size()[1]-3
3133
+ if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.get_term_size()[1]-38
3134
+ else: field_end_f = lambda: self.get_term_size()[1]-3
2681
3135
  self.set_registers()
2682
3136
  usrtxt, return_val = input_field(
2683
3137
  self.stdscr,
2684
3138
  usrtxt=usrtxt,
2685
3139
  field_prefix=" Settings: ",
2686
3140
  x=lambda:2,
2687
- y=lambda: self.stdscr.getmaxyx()[0]-1,
3141
+ y=lambda: self.get_term_size()[0]-1,
2688
3142
  max_length=field_end_f,
2689
3143
  registers=self.registers,
2690
- refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
3144
+ refresh_screen_function=lambda: self.draw_screen(),
2691
3145
  history=self.history_settings,
2692
3146
  path_auto_complete=True,
2693
3147
  formula_auto_complete=False,
@@ -2707,28 +3161,29 @@ class Picker:
2707
3161
 
2708
3162
  elif self.check_key("settings_options", key, self.keys_dict):
2709
3163
  options = []
3164
+ options += [["cv", "Centre rows vertically"]]
3165
+ options += [["pc", "Pin cursor to row index during data refresh."]]
3166
+ options += [["ct", "Centre column-set in terminal"]]
3167
+ options += [["cc", "Centre values in cells"]]
3168
+ options += [["!r", "Toggle auto-refresh"]]
3169
+ options += [["th", "Cycle between themes. (accepts th#)"]]
3170
+ options += [["colsel", "Toggle columns."]]
3171
+ options += [["nohl", "Toggle highlights"]]
3172
+ options += [["footer", "Toggle footer"]]
3173
+ options += [["header", "Toggle header"]]
3174
+ options += [["rh", "Toggle row header"]]
3175
+ options += [["modes", "Toggle modes"]]
3176
+ options += [["ft", "Cycle through footer styles (accepts ft#)"]]
3177
+ options += [["file_next", "Go to the next open file."]]
3178
+ options += [["file_prev", "Go to the previous open file."]]
3179
+ options += [["sheet_next", "Go to the next sheet."]]
3180
+ options += [["sheet_prev", "Go to the previous sheet."]]
3181
+ options += [["unicode", "Toggle b/w using len and wcwidth to calculate char width."]]
3182
+ options += [["ara", "Add empty row after cursor."]]
3183
+ options += [["arb", "Add empty row before the cursor."]]
3184
+ options += [["aca", "Add empty column after the selected column."]]
3185
+ options += [["acb", "Add empty column before the selected column."]]
2710
3186
  if len(self.items) > 0:
2711
- options += [["cv", "Centre rows vertically"]]
2712
- options += [["pc", "Pin cursor to row number when data refreshes"]]
2713
- options += [["ct", "Centre column-set in terminal"]]
2714
- options += [["cc", "Centre values in cells"]]
2715
- options += [["!r", "Toggle auto-refresh"]]
2716
- options += [["th", "Cycle between themes. (accepts th#)"]]
2717
- options += [["nohl", "Toggle highlights"]]
2718
- options += [["footer", "Toggle footer"]]
2719
- options += [["header", "Toggle header"]]
2720
- options += [["rh", "Toggle row header"]]
2721
- options += [["modes", "Toggle modes"]]
2722
- options += [["ft", "Cycle through footer styles (accepts ft#)"]]
2723
- options += [["file_next", "Go to the next open file."]]
2724
- options += [["file_prev", "Go to the previous open file."]]
2725
- options += [["sheet_next", "Go to the next sheet."]]
2726
- options += [["sheet_prev", "Go to the previous sheet."]]
2727
- options += [["unicode", "Toggle b/w using len and wcwidth to calculate char width."]]
2728
- options += [["ara", "Add empty row after cursor."]]
2729
- options += [["arb", "Add empty row before the cursor."]]
2730
- options += [["aca", "Add empty column after the selected column."]]
2731
- options += [["acb", "Add empty column before the selected column."]]
2732
3187
  options += [[f"col{i}", f"Select column {i}"] for i in range(len(self.items[0]))]
2733
3188
  options += [[f"s{i}", f"Sort by column {i}"] for i in range(len(self.items[0]))]
2734
3189
  options += [[f"!{i}", f"Toggle visibility of column {i}"] for i in range(len(self.items[0]))]
@@ -2743,6 +3198,7 @@ class Picker:
2743
3198
 
2744
3199
  elif self.check_key("redo", key, self.keys_dict):
2745
3200
  self.redo()
3201
+
2746
3202
  # elif self.check_key("move_column_left", key, self.keys_dict):
2747
3203
  # tmp1 = self.column_indices[self.selected_column]
2748
3204
  # tmp2 = self.column_indices[(self.selected_column-1)%len(self.column_indices)]
@@ -2752,7 +3208,7 @@ class Picker:
2752
3208
  # # self.notification(self.stdscr, f"{str(self.column_indices)}, {tmp1}, {tmp2}")
2753
3209
  # self.initialise_variables()
2754
3210
  # 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)
2755
- # self.draw_screen(self.indexed_items, self.highlights)
3211
+ # self.draw_screen()
2756
3212
  # # self.move_column(direction=-1)
2757
3213
  #
2758
3214
  # elif self.check_key("move_column_right", key, self.keys_dict):
@@ -2762,7 +3218,7 @@ class Picker:
2762
3218
  # self.column_indices[(self.selected_column+1)%(len(self.column_indices))] = tmp1
2763
3219
  # self.selected_column = (self.selected_column+1)%len(self.column_indices)
2764
3220
  # self.initialise_variables()
2765
- # self.draw_screen(self.indexed_items, self.highlights)
3221
+ # self.draw_screen()
2766
3222
  # # self.move_column(direction=1)
2767
3223
 
2768
3224
  elif self.check_key("cursor_down", key, self.keys_dict):
@@ -2816,6 +3272,7 @@ class Picker:
2816
3272
  self.selected_cells_by_row[row] = [col]
2817
3273
 
2818
3274
  self.cursor_down()
3275
+ self.ensure_no_overscroll()
2819
3276
  elif self.check_key("select_all", key, self.keys_dict): # Select all (m or ctrl-a)
2820
3277
  self.select_all()
2821
3278
 
@@ -2830,7 +3287,8 @@ class Picker:
2830
3287
  if new_pos < len(self.indexed_items):
2831
3288
  self.cursor_pos = new_pos
2832
3289
 
2833
- self.draw_screen(self.indexed_items, self.highlights)
3290
+ self.ensure_no_overscroll()
3291
+ self.draw_screen()
2834
3292
 
2835
3293
  elif self.check_key("cursor_bottom", key, self.keys_dict):
2836
3294
  new_pos = len(self.indexed_items)-1
@@ -2839,19 +3297,17 @@ class Picker:
2839
3297
  else: break
2840
3298
  if new_pos < len(self.items) and new_pos >= 0:
2841
3299
  self.cursor_pos = new_pos
2842
- self.draw_screen(self.indexed_items, self.highlights)
2843
- # current_row = items_per_page - 1
2844
- # if current_page + 1 == (len(self.indexed_items) + items_per_page - 1) // items_per_page:
2845
- #
2846
- # current_row = (len(self.indexed_items) +items_per_page - 1) % items_per_page
2847
- # self.draw_screen(self.indexed_items, self.highlights)
3300
+ self.ensure_no_overscroll()
3301
+ self.draw_screen()
3302
+
2848
3303
  elif self.check_key("enter", key, self.keys_dict):
2849
3304
  self.logger.info(f"key_function enter")
2850
3305
  # Print the selected indices if any, otherwise print the current index
2851
3306
  if self.is_selecting or self.is_deselecting: self.handle_visual_selection()
2852
3307
  if len(self.items) == 0:
3308
+ self.cleanup_threads()
2853
3309
  function_data = self.get_function_data()
2854
- function_data["last_key"] = key
3310
+ restore_terminal_settings(tty_fd, self.saved_terminal_state)
2855
3311
  return [], "", function_data
2856
3312
  selected_indices = get_selected_indices(self.selections)
2857
3313
  if not selected_indices and len(self.indexed_items):
@@ -2864,8 +3320,7 @@ class Picker:
2864
3320
  if self.option_functions[index] != None:
2865
3321
  options_sufficient, usrtxt = self.option_functions[index](
2866
3322
  stdscr=self.stdscr,
2867
- refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
2868
- field_prefix=f" Opts ({index}): ",
3323
+ refresh_screen_function=lambda: self.draw_screen(),
2869
3324
  )
2870
3325
  else:
2871
3326
  self.set_registers()
@@ -2877,11 +3332,12 @@ class Picker:
2877
3332
  )
2878
3333
 
2879
3334
  if options_sufficient:
3335
+ self.cleanup_threads()
2880
3336
  self.user_opts = usrtxt
2881
3337
  self.stdscr.clear()
2882
3338
  self.stdscr.refresh()
2883
3339
  function_data = self.get_function_data()
2884
- function_data["last_key"] = key
3340
+ restore_terminal_settings(tty_fd, self.saved_terminal_state)
2885
3341
  return selected_indices, usrtxt, function_data
2886
3342
  elif self.check_key("page_down", key, self.keys_dict): # Next page
2887
3343
  self.cursor_pos = min(len(self.indexed_items) - 1, self.cursor_pos+self.items_per_page)
@@ -2890,15 +3346,7 @@ class Picker:
2890
3346
  self.cursor_pos = max(0, self.cursor_pos-self.items_per_page)
2891
3347
 
2892
3348
  elif self.check_key("redraw_screen", key, self.keys_dict):
2893
- self.logger.info(f"key_function redraw_screen")
2894
- self.stdscr.clear()
2895
- self.stdscr.refresh()
2896
- restrict_curses(self.stdscr)
2897
- unrestrict_curses(self.stdscr)
2898
- self.stdscr.clear()
2899
- self.stdscr.refresh()
2900
-
2901
- self.draw_screen(self.indexed_items, self.highlights)
3349
+ self.refresh_and_draw_screen()
2902
3350
 
2903
3351
  elif self.check_key("cycle_sort_method", key, self.keys_dict):
2904
3352
  if self.sort_column == self.selected_column:
@@ -2926,7 +3374,7 @@ class Picker:
2926
3374
  if len(self.indexed_items) > 0:
2927
3375
  current_index = self.indexed_items[self.cursor_pos][0]
2928
3376
  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
2929
- self.draw_screen(self.indexed_items, self.highlights)
3377
+ self.draw_screen()
2930
3378
  self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
2931
3379
  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]})")
2932
3380
  elif self.check_key("col_select", key, self.keys_dict):
@@ -2939,23 +3387,29 @@ class Picker:
2939
3387
  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
2940
3388
  self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
2941
3389
  elif self.check_key("col_select_next", key, self.keys_dict):
2942
- if len(self.items) > 0 and len(self.items[0]) > 0:
2943
- col_index = (self.selected_column +1) % (len(self.items[0]))
2944
- self.selected_column = col_index
2945
- # Flash when we loop back to the first column
2946
- # if self.selected_column == 0:
2947
- # curses.flash()
2948
3390
  self.logger.info(f"key_function col_select_next {self.selected_column}")
3391
+ if len(self.hidden_columns) != len(self.column_widths):
3392
+ if len(self.column_widths):
3393
+ while True:
3394
+ self.hidden_columns
3395
+ col_index = (self.selected_column +1) % (len(self.column_widths))
3396
+ self.selected_column = col_index
3397
+ if self.selected_column not in self.hidden_columns:
3398
+ break
3399
+
3400
+ # Flash when we loop back to the first column
3401
+ # if self.selected_column == 0:
3402
+ # curses.flash()
2949
3403
 
2950
3404
 
2951
3405
  ## Scroll with column select
2952
- rows = self.get_visible_rows()
2953
- 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=self.rows_w, unicode_char_width=self.unicode_char_width)
2954
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
2955
- column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
2956
- start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
2957
- end_of_cell = sum(visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
2958
- display_width = self.rows_w-self.startx
3406
+ self.get_visible_rows()
3407
+ self.column_widths = get_column_widths(self.visible_rows, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=self.rows_w, unicode_char_width=self.unicode_char_width)
3408
+ self.visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
3409
+ column_set_width = sum(self.visible_column_widths)+len(self.separator)*len(self.visible_column_widths)
3410
+ start_of_cell = sum(self.visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
3411
+ end_of_cell = sum(self.visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
3412
+ display_width = self.rows_w-self.left_gutter_width
2959
3413
  # If the full column is within the current display then don't do anything
2960
3414
  if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
2961
3415
  pass
@@ -2964,25 +3418,32 @@ class Picker:
2964
3418
  self.leftmost_char = end_of_cell - display_width
2965
3419
 
2966
3420
  self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
3421
+ self.ensure_no_overscroll()
2967
3422
 
2968
3423
  elif self.check_key("col_select_prev", key, self.keys_dict):
2969
- if len(self.items) > 0 and len(self.items[0]) > 0:
2970
- col_index = (self.selected_column -1) % (len(self.items[0]))
2971
- self.selected_column = col_index
2972
-
2973
3424
  self.logger.info(f"key_function col_select_prev {self.selected_column}")
3425
+
3426
+ if len(self.hidden_columns) != len(self.column_widths):
3427
+ if len(self.column_widths):
3428
+ while True:
3429
+ self.hidden_columns
3430
+ col_index = (self.selected_column -1) % (len(self.column_widths))
3431
+ self.selected_column = col_index
3432
+ if self.selected_column not in self.hidden_columns:
3433
+ break
3434
+
2974
3435
  # Flash when we loop back to the last column
2975
3436
  # if self.selected_column == len(self.column_widths)-1:
2976
3437
  # curses.flash()
2977
3438
 
2978
3439
  ## Scroll with column select
2979
- rows = self.get_visible_rows()
2980
- 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=self.rows_w, unicode_char_width=self.unicode_char_width)
2981
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
2982
- column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
2983
- start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
2984
- end_of_cell = sum(visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
2985
- display_width = self.rows_w-self.startx
3440
+ self.get_visible_rows()
3441
+ self.column_widths = get_column_widths(self.visible_rows, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=self.rows_w, unicode_char_width=self.unicode_char_width)
3442
+ self.visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
3443
+ column_set_width = sum(self.visible_column_widths)+len(self.separator)*len(self.visible_column_widths)
3444
+ start_of_cell = sum(self.visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
3445
+ end_of_cell = sum(self.visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
3446
+ display_width = self.rows_w-self.left_gutter_width
2986
3447
 
2987
3448
  # If the entire column is within the current display then don't do anything
2988
3449
  if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
@@ -2992,25 +3453,26 @@ class Picker:
2992
3453
  self.leftmost_char = start_of_cell
2993
3454
 
2994
3455
  self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
3456
+ self.ensure_no_overscroll()
2995
3457
 
2996
3458
  elif self.check_key("scroll_right", key, self.keys_dict):
2997
3459
  self.logger.info(f"key_function scroll_right")
2998
3460
  if len(self.indexed_items):
2999
- row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
3000
- if row_width-self.leftmost_char >= self.rows_w-self.startx-5:
3461
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
3462
+ if row_width-self.leftmost_char >= self.rows_w-5:
3001
3463
  self.leftmost_char += 5
3002
- self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w - self.startx) + 5)
3003
- if sum(self.column_widths) + len(self.column_widths)*len(self.separator) < self.rows_w:
3464
+ self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w) + self.left_gutter_width+5)
3465
+ if sum(self.visible_column_widths) + len(self.visible_column_widths)*len(self.separator) < self.rows_w:
3004
3466
  self.leftmost_char = 0
3005
3467
 
3006
3468
  elif self.check_key("scroll_right_25", key, self.keys_dict):
3007
3469
  self.logger.info(f"key_function scroll_right")
3008
3470
  if len(self.indexed_items):
3009
- row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
3010
- if row_width-self.leftmost_char >= self.rows_w-self.startx-25:
3471
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
3472
+ if row_width-self.leftmost_char+5 >= self.rows_w-25:
3011
3473
  self.leftmost_char += 25
3012
- self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w - self.startx) + 5)
3013
- if sum(self.column_widths) + len(self.column_widths)*len(self.separator) < self.rows_w:
3474
+ self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w) + self.left_gutter_width+5)
3475
+ if sum(self.visible_column_widths) + len(self.visible_column_widths)*len(self.separator) < self.rows_w:
3014
3476
  self.leftmost_char = 0
3015
3477
 
3016
3478
  elif self.check_key("scroll_left", key, self.keys_dict):
@@ -3029,20 +3491,14 @@ class Picker:
3029
3491
  elif self.check_key("scroll_far_right", key, self.keys_dict):
3030
3492
  self.logger.info(f"key_function scroll_far_right")
3031
3493
  longest_row_str_len = 0
3032
- # rows = self.get_visible_rows()
3033
- # for i in range(len(rows)):
3034
- # item = rows[i]
3035
- # row_str = format_row(item, self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
3036
- # if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
3037
- longest_row_str_len = sum(self.column_widths) + (len(self.column_widths)-1)*len(self.separator)
3038
- # for i in range(len(self.indexed_items)):
3039
- # item = self.indexed_items[i]
3040
- # row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)
3041
- # if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
3042
- # self.notification(self.stdscr, f"{longest_row_str_len}")
3043
- self.leftmost_char = max(0, longest_row_str_len-self.rows_w+2+self.startx+5)
3044
- if len(self.items):
3045
- self.selected_column = len(self.items[0])-1
3494
+ longest_row_str_len = sum(self.visible_column_widths) + (len(self.visible_column_widths)-1)*len(self.separator)
3495
+ if len(self.column_widths):
3496
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
3497
+ self.leftmost_char = row_width - (self.rows_w) + self.left_gutter_width+5
3498
+ self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w) + self.left_gutter_width+5)
3499
+
3500
+ longest_row_str_len = sum(self.visible_column_widths) + (len(self.visible_column_widths)-1)*len(self.separator)
3501
+ self.selected_column = len(self.column_widths)-1
3046
3502
 
3047
3503
  elif self.check_key("add_column_before", key, self.keys_dict):
3048
3504
  self.logger.info(f"key_function add_column_before")
@@ -3099,52 +3555,45 @@ class Picker:
3099
3555
  self.selected_column = min(self.selected_column, row_len-2)
3100
3556
  self.initialise_variables()
3101
3557
 
3102
-
3103
-
3104
-
3105
- # elif self.check_key("increase_lines_per_page", key, self.keys_dict):
3106
- # self.items_per_page += 1
3107
- # self.draw_screen(self.indexed_items, self.highlights)
3108
- # elif self.check_key("decrease_lines_per_page", key, self.keys_dict):
3109
- # if self.items_per_page > 1:
3110
- # self.items_per_page -= 1
3111
- # self.draw_screen(self.indexed_items, self.highlights)
3112
3558
  elif self.check_key("decrease_column_width", key, self.keys_dict):
3113
3559
  self.logger.info(f"key_function decrease_column_width")
3114
3560
  if self.max_column_width > 10:
3115
3561
  self.max_column_width -= 10
3116
3562
  # 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)
3117
- self.draw_screen(self.indexed_items, self.highlights)
3563
+ self.draw_screen()
3118
3564
  elif self.check_key("increase_column_width", key, self.keys_dict):
3119
3565
  self.logger.info(f"key_function increase_column_width")
3120
3566
  if self.max_column_width < 1000:
3121
3567
  self.max_column_width += 10
3122
3568
  # 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)
3123
- self.draw_screen(self.indexed_items, self.highlights)
3569
+ self.draw_screen()
3124
3570
  elif self.check_key("visual_selection_toggle", key, self.keys_dict):
3125
3571
  self.logger.info(f"key_function visual_selection_toggle")
3126
3572
  self.handle_visual_selection()
3127
- self.draw_screen(self.indexed_items, self.highlights)
3573
+ self.draw_screen()
3128
3574
 
3129
3575
  elif self.check_key("visual_deselection_toggle", key, self.keys_dict):
3130
3576
  self.logger.info(f"key_function visual_deselection_toggle")
3131
3577
  self.handle_visual_selection(selecting=False)
3132
- self.draw_screen(self.indexed_items, self.highlights)
3578
+ self.draw_screen()
3133
3579
 
3134
3580
  elif key == curses.KEY_RESIZE: # Terminal resize signal
3135
3581
 
3136
3582
  self.calculate_section_sizes()
3137
- 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=self.rows_w, unicode_char_width=self.unicode_char_width)
3138
- self.draw_screen(self.indexed_items, self.highlights)
3583
+ self.ensure_no_overscroll()
3584
+
3585
+ self.stdscr.clear()
3586
+ self.stdscr.refresh()
3587
+ self.draw_screen()
3139
3588
 
3140
3589
 
3141
3590
  elif self.check_key("filter_input", key, self.keys_dict):
3142
3591
  self.logger.info(f"key_function filter_input")
3143
- self.draw_screen(self.indexed_items, self.highlights)
3592
+ self.draw_screen()
3144
3593
  usrtxt = f"{self.filter_query} " if self.filter_query else ""
3145
- field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
3146
- if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
3147
- else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
3594
+ field_end_f = lambda: self.get_term_size()[1]-38 if self.show_footer else lambda: self.get_term_size()[1]-3
3595
+ if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.get_term_size()[1]-38
3596
+ else: field_end_f = lambda: self.get_term_size()[1]-3
3148
3597
  self.set_registers()
3149
3598
  words = self.get_word_list()
3150
3599
  usrtxt, return_val = input_field(
@@ -3152,11 +3601,11 @@ class Picker:
3152
3601
  usrtxt=usrtxt,
3153
3602
  field_prefix=" Filter: ",
3154
3603
  x=lambda:2,
3155
- y=lambda: self.stdscr.getmaxyx()[0]-2,
3604
+ y=lambda: self.get_term_size()[0]-2,
3156
3605
  # max_length=field_end,
3157
3606
  max_length=field_end_f,
3158
3607
  registers=self.registers,
3159
- refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
3608
+ refresh_screen_function=lambda: self.draw_screen(),
3160
3609
  history=self.history_filter_and_search,
3161
3610
  path_auto_complete=True,
3162
3611
  formula_auto_complete=False,
@@ -3185,11 +3634,11 @@ class Picker:
3185
3634
 
3186
3635
  elif self.check_key("search_input", key, self.keys_dict):
3187
3636
  self.logger.info(f"key_function search_input")
3188
- self.draw_screen(self.indexed_items, self.highlights)
3637
+ self.draw_screen()
3189
3638
  usrtxt = f"{self.search_query} " if self.search_query else ""
3190
- field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
3191
- if self.show_footer and self.footer.height >= 3: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
3192
- else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
3639
+ field_end_f = lambda: self.get_term_size()[1]-38 if self.show_footer else lambda: self.get_term_size()[1]-3
3640
+ if self.show_footer and self.footer.height >= 3: field_end_f = lambda: self.get_term_size()[1]-38
3641
+ else: field_end_f = lambda: self.get_term_size()[1]-3
3193
3642
  self.set_registers()
3194
3643
  words = self.get_word_list()
3195
3644
  usrtxt, return_val = input_field(
@@ -3197,10 +3646,10 @@ class Picker:
3197
3646
  usrtxt=usrtxt,
3198
3647
  field_prefix=" Search: ",
3199
3648
  x=lambda:2,
3200
- y=lambda: self.stdscr.getmaxyx()[0]-3,
3649
+ y=lambda: self.get_term_size()[0]-3,
3201
3650
  max_length=field_end_f,
3202
3651
  registers=self.registers,
3203
- refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
3652
+ refresh_screen_function=lambda: self.draw_screen(),
3204
3653
  history=self.history_filter_and_search,
3205
3654
  path_auto_complete=True,
3206
3655
  formula_auto_complete=False,
@@ -3285,23 +3734,16 @@ class Picker:
3285
3734
  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
3286
3735
  elif self.cancel_is_back:
3287
3736
  function_data = self.get_function_data()
3288
- function_data["last_key"] = key
3289
3737
  return [], "escape", function_data
3290
3738
 
3291
-
3292
- # else:
3293
- # self.search_query = ""
3294
- # self.mode_index = 0
3295
- # self.highlights = [highlight for highlight in self.highlights if "type" not in highlight or highlight["type"] != "search" ]
3296
- # continue
3297
- self.draw_screen(self.indexed_items, self.highlights)
3739
+ self.draw_screen()
3298
3740
 
3299
3741
  elif self.check_key("opts_input", key, self.keys_dict):
3300
3742
  self.logger.info(f"key_function opts_input")
3301
3743
  usrtxt = f"{self.user_opts} " if self.user_opts else ""
3302
- field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
3303
- if self.show_footer and self.footer.height >= 1: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
3304
- else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
3744
+ field_end_f = lambda: self.get_term_size()[1]-38 if self.show_footer else lambda: self.get_term_size()[1]-3
3745
+ if self.show_footer and self.footer.height >= 1: field_end_f = lambda: self.get_term_size()[1]-38
3746
+ else: field_end_f = lambda: self.get_term_size()[1]-3
3305
3747
  self.set_registers()
3306
3748
  words = self.get_word_list()
3307
3749
  usrtxt, return_val = input_field(
@@ -3309,10 +3751,10 @@ class Picker:
3309
3751
  usrtxt=usrtxt,
3310
3752
  field_prefix=" Opts: ",
3311
3753
  x=lambda:2,
3312
- y=lambda: self.stdscr.getmaxyx()[0]-1,
3754
+ y=lambda: self.get_term_size()[0]-1,
3313
3755
  max_length=field_end_f,
3314
3756
  registers=self.registers,
3315
- refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
3757
+ refresh_screen_function=lambda: self.draw_screen(),
3316
3758
  history=self.history_opts,
3317
3759
  path_auto_complete=True,
3318
3760
  formula_auto_complete=False,
@@ -3390,13 +3832,19 @@ class Picker:
3390
3832
  elif self.check_key("cycle_right_pane", key, self.keys_dict):
3391
3833
  self.cycle_right_pane()
3392
3834
 
3835
+ elif self.check_key("toggle_left_pane", key, self.keys_dict):
3836
+ self.toggle_left_pane()
3837
+
3838
+ elif self.check_key("cycle_left_pane", key, self.keys_dict):
3839
+ self.cycle_left_pane()
3840
+
3393
3841
  elif self.check_key("pipe_input", key, self.keys_dict):
3394
3842
  self.logger.info(f"key_function pipe_input")
3395
3843
  # usrtxt = "xargs -d '\n' -I{} "
3396
3844
  usrtxt = "xargs "
3397
- field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
3398
- if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
3399
- else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
3845
+ field_end_f = lambda: self.get_term_size()[1]-38 if self.show_footer else lambda: self.get_term_size()[1]-3
3846
+ if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.get_term_size()[1]-38
3847
+ else: field_end_f = lambda: self.get_term_size()[1]-3
3400
3848
  self.set_registers()
3401
3849
 
3402
3850
  # Get list of available shell commands
@@ -3412,11 +3860,11 @@ class Picker:
3412
3860
  usrtxt=usrtxt,
3413
3861
  field_prefix=" Command: ",
3414
3862
  x=lambda:2,
3415
- y=lambda: self.stdscr.getmaxyx()[0]-2,
3416
- literal=True,
3863
+ y=lambda: self.get_term_size()[0]-2,
3864
+ literal=False,
3417
3865
  max_length=field_end_f,
3418
3866
  registers=self.registers,
3419
- refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
3867
+ refresh_screen_function=lambda: self.draw_screen(),
3420
3868
  history=self.history_pipes,
3421
3869
  path_auto_complete=True,
3422
3870
  formula_auto_complete=False,
@@ -3503,12 +3951,12 @@ class Picker:
3503
3951
 
3504
3952
  elif self.check_key("edit", key, self.keys_dict):
3505
3953
  self.logger.info(f"key_function edit")
3506
- if len(self.indexed_items) > 0 and self.selected_column >=0 and self.editable_columns[self.selected_column]:
3954
+ if len(self.indexed_items) > 0 and self.editable_columns[self.selected_column]:
3507
3955
  current_val = self.indexed_items[self.cursor_pos][1][self.selected_column]
3508
3956
  usrtxt = f"{current_val}"
3509
- field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
3510
- if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
3511
- else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
3957
+ field_end_f = lambda: self.get_term_size()[1]-38 if self.show_footer else lambda: self.get_term_size()[1]-3
3958
+ if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.get_term_size()[1]-38
3959
+ else: field_end_f = lambda: self.get_term_size()[1]-3
3512
3960
  self.set_registers()
3513
3961
  words = self.get_word_list()
3514
3962
  usrtxt, return_val = input_field(
@@ -3516,10 +3964,10 @@ class Picker:
3516
3964
  usrtxt=usrtxt,
3517
3965
  field_prefix=" Edit value: ",
3518
3966
  x=lambda:2,
3519
- y=lambda: self.stdscr.getmaxyx()[0]-2,
3967
+ y=lambda: self.get_term_size()[0]-2,
3520
3968
  max_length=field_end_f,
3521
3969
  registers=self.registers,
3522
- refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
3970
+ refresh_screen_function=lambda: self.draw_screen(),
3523
3971
  history = self.history_edits,
3524
3972
  path_auto_complete=True,
3525
3973
  formula_auto_complete=True,
@@ -3532,15 +3980,56 @@ class Picker:
3532
3980
  usrtxt = str(eval(usrtxt[3:]))
3533
3981
  self.indexed_items[self.cursor_pos][1][self.selected_column] = usrtxt
3534
3982
  self.history_edits.append(usrtxt)
3983
+ elif self.check_key("edit_nvim", key, self.keys_dict):
3984
+
3985
+ def edit_strings_in_nvim(strings: list[str]) -> list[str]:
3986
+ """
3987
+ Opens a list of strings in nvim for editing and returns the modified strings.
3988
+
3989
+ Args:
3990
+ strings (list[str]): The list of strings to edit.
3991
+
3992
+ Returns:
3993
+ list[str]: The updated list of strings after editing in nvim.
3994
+ """
3995
+
3996
+ # Open the strings in a tmpfile for editing
3997
+ with tempfile.NamedTemporaryFile(mode="w+", suffix=".txt", delete=False) as tmp:
3998
+ tmp.write("\n".join(strings))
3999
+ tmp.flush()
4000
+ tmp_name = tmp.name
4001
+
4002
+ subprocess.run(["nvim", tmp_name])
4003
+
4004
+ # Read the modified strings into a list and return them.
4005
+ with open(tmp_name, "r") as tmp:
4006
+ edited_content = tmp.read().splitlines()
4007
+
4008
+ return edited_content
4009
+
4010
+ if len(self.indexed_items) > 0 and self.editable_columns[self.selected_column]:
4011
+
4012
+ selected_cells = [self.items[index][self.selected_column] for index, selected in self.selections.items() if selected ]
4013
+ selected_cells_indices = [(index, self.selected_column) for index, selected in self.selections.items() if selected ]
4014
+
4015
+ edited_cells = edit_strings_in_nvim(selected_cells)
4016
+ count = 0
4017
+ if len(edited_cells) == len(selected_cells_indices):
4018
+ for i, j in selected_cells_indices:
4019
+ self.items[i][j] = edited_cells[count]
4020
+ count += 1
4021
+
4022
+ self.refresh_and_draw_screen()
4023
+
3535
4024
 
3536
4025
  elif self.check_key("edit_picker", key, self.keys_dict):
3537
4026
  self.logger.info(f"key_function edit_picker")
3538
4027
  if len(self.indexed_items) > 0 and self.selected_column >=0 and self.editable_columns[self.selected_column]:
3539
4028
  current_val = self.indexed_items[self.cursor_pos][1][self.selected_column]
3540
4029
  usrtxt = f"{current_val}"
3541
- field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
3542
- if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
3543
- else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
4030
+ field_end_f = lambda: self.get_term_size()[1]-38 if self.show_footer else lambda: self.get_term_size()[1]-3
4031
+ if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.get_term_size()[1]-38
4032
+ else: field_end_f = lambda: self.get_term_size()[1]-3
3544
4033
  self.set_registers()
3545
4034
  words = self.get_word_list()
3546
4035
  usrtxt, return_val = input_field(
@@ -3548,10 +4037,10 @@ class Picker:
3548
4037
  usrtxt=usrtxt,
3549
4038
  field_prefix=" Edit value: ",
3550
4039
  x=lambda:2,
3551
- y=lambda: self.stdscr.getmaxyx()[0]-2,
4040
+ y=lambda: self.get_term_size()[0]-2,
3552
4041
  max_length=field_end_f,
3553
4042
  registers=self.registers,
3554
- refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
4043
+ refresh_screen_function=lambda: self.draw_screen(),
3555
4044
  history = self.history_edits,
3556
4045
  path_auto_complete=True,
3557
4046
  formula_auto_complete=True,
@@ -3563,7 +4052,7 @@ class Picker:
3563
4052
  self.indexed_items[self.cursor_pos][1][self.selected_column] = usrtxt
3564
4053
  self.history_edits.append(usrtxt)
3565
4054
  elif self.check_key("edit_ipython", key, self.keys_dict):
3566
- self.logger.info(f"key_function edit_picker")
4055
+ self.logger.info(f"key_function edit_ipython")
3567
4056
  import IPython, termios
3568
4057
  self.stdscr.clear()
3569
4058
  restrict_curses(self.stdscr)
@@ -3600,11 +4089,11 @@ class Picker:
3600
4089
  self.stdscr.clear()
3601
4090
  self.stdscr.refresh()
3602
4091
  self.initialise_variables()
3603
- self.draw_screen(self.indexed_items, self.highlights)
4092
+ self.draw_screen()
3604
4093
 
3605
4094
 
3606
4095
 
3607
- self.draw_screen(self.indexed_items, self.highlights, clear=clear_screen)
4096
+ self.draw_screen(clear=clear_screen)
3608
4097
 
3609
4098
 
3610
4099
 
@@ -3707,7 +4196,7 @@ def parse_arguments() -> Tuple[argparse.Namespace, dict]:
3707
4196
  # input_arg = args.filename
3708
4197
 
3709
4198
  elif args.generate:
3710
- function_data["refresh_function"] = lambda : generate_picker_data(args.generate)
4199
+ function_data["refresh_function"] = lambda items, header, visible_rows_indices, getting_data, state: generate_picker_data_from_file(args.generate, items, header, visible_rows_indices, getting_data, state)
3711
4200
  function_data["get_data_startup"] = True
3712
4201
  function_data["get_new_data"] = True
3713
4202
  return args, function_data
@@ -3806,14 +4295,6 @@ def unrestrict_curses(stdscr: curses.window) -> None:
3806
4295
  def main() -> None:
3807
4296
  """ Main function when listpick is executed. Deals with command line arguments and starts a Picker. """
3808
4297
  args, function_data = parse_arguments()
3809
-
3810
- try:
3811
- if function_data["items"] == []:
3812
- function_data["items"] = test_items
3813
- function_data["highlights"] = test_highlights
3814
- function_data["header"] = test_header
3815
- except:
3816
- pass
3817
4298
 
3818
4299
  # function_data["colour_theme_number"] = 3
3819
4300
  function_data["highlights"] = [
@@ -3959,7 +4440,7 @@ def main() -> None:
3959
4440
  # function_data["cell_cursor"] = True
3960
4441
  # function_data["display_modes"] = True
3961
4442
  # function_data["centre_in_cols"] = True
3962
- # function_data["show_row_header"] = True
4443
+ function_data["show_row_header"] = True
3963
4444
  # function_data["keys_dict"] = picker_keys
3964
4445
  # function_data["id_column"] = -1
3965
4446
  # function_data["track_entries_upon_refresh"] = True
@@ -3975,10 +4456,23 @@ def main() -> None:
3975
4456
  # function_data["debug"] = True
3976
4457
  # function_data["debug_level"] = 1
3977
4458
 
4459
+ # function_data["cell_cursor"] = False
4460
+
3978
4461
  function_data["split_right"] = False
3979
- function_data["right_pane_index"] = 3
4462
+ function_data["split_left"] = False
4463
+ function_data["right_pane_index"] = 2
4464
+ function_data["left_pane_index"] = 0
3980
4465
 
3981
4466
  function_data["right_panes"] = [
4467
+ # Nopane
4468
+ {
4469
+ "proportion": 1/3,
4470
+ "auto_refresh": False,
4471
+ "get_data": lambda data, state: [],
4472
+ "display": left_start_pane,
4473
+ "data": ["Files", []],
4474
+ "refresh_time": 1,
4475
+ },
3982
4476
  # Graph or random numbers generated each second
3983
4477
  {
3984
4478
  "proportion": 1/2,
@@ -3990,22 +4484,31 @@ def main() -> None:
3990
4484
  },
3991
4485
  # list of numbers
3992
4486
  {
3993
- "proportion": 2/3,
4487
+ "proportion": 1/3,
3994
4488
  "auto_refresh": False,
3995
4489
  "get_data": data_refresh_randint_title,
3996
4490
  "display": right_split_display_list,
3997
4491
  "data": ["Files", [str(x) for x in range(100)]],
3998
4492
  "refresh_time": 1.0,
3999
4493
  },
4000
- # File attribures
4494
+ # File attributes
4001
4495
  {
4002
- "proportion": 2/3,
4496
+ "proportion": 1/3,
4003
4497
  "auto_refresh": False,
4004
4498
  "get_data": lambda data, state: [],
4005
4499
  "display": right_split_file_attributes,
4006
- "data": ["Files", [str(x) for x in range(100)]],
4500
+ "data": [],
4007
4501
  "refresh_time": 1.0,
4008
4502
  },
4503
+ # File attributes dynamic
4504
+ {
4505
+ "proportion": 1/3,
4506
+ "auto_refresh": True,
4507
+ "get_data": update_file_attributes,
4508
+ "display": right_split_file_attributes_dynamic,
4509
+ "data": [],
4510
+ "refresh_time": 2.0,
4511
+ },
4009
4512
  # List of random numbers generated each second
4010
4513
  {
4011
4514
  "proportion": 1/2,
@@ -4025,7 +4528,79 @@ def main() -> None:
4025
4528
  "refresh_time": 1,
4026
4529
  },
4027
4530
  ]
4028
- function_data["require_option"] = [True for _ in function_data["items"]]
4531
+ function_data["left_panes"] = [
4532
+ # Nopane
4533
+ {
4534
+ "proportion": 1/3,
4535
+ "auto_refresh": False,
4536
+ "get_data": lambda data, state: [],
4537
+ "display": left_start_pane,
4538
+ "data": ["Files", []],
4539
+ "refresh_time": 1,
4540
+ },
4541
+ # Graph or random numbers generated each second
4542
+ {
4543
+ "proportion": 1/2,
4544
+ "auto_refresh": True,
4545
+ "get_data": data_refresh_randint,
4546
+ "display": left_split_graph,
4547
+ "data": [],
4548
+ "refresh_time": 1.0,
4549
+ },
4550
+ # list of numbers
4551
+ {
4552
+ "proportion": 1/3,
4553
+ "auto_refresh": False,
4554
+ "get_data": data_refresh_randint_title,
4555
+ "display": left_split_display_list,
4556
+ "data": ["Files", [str(x) for x in range(100)]],
4557
+ "refresh_time": 1.0,
4558
+ },
4559
+ # File attributes
4560
+ {
4561
+ "proportion": 1/3,
4562
+ "auto_refresh": False,
4563
+ "get_data": lambda data, state: [],
4564
+ "display": left_split_file_attributes,
4565
+ "data": [],
4566
+ "refresh_time": 1.0,
4567
+ },
4568
+ # File attributes dynamic
4569
+ {
4570
+ "proportion": 1/3,
4571
+ "auto_refresh": True,
4572
+ "get_data": update_file_attributes,
4573
+ "display": left_split_file_attributes_dynamic,
4574
+ "data": [],
4575
+ "refresh_time": 2.0,
4576
+ },
4577
+ # List of random numbers generated each second
4578
+ {
4579
+ "proportion": 1/2,
4580
+ "auto_refresh": True,
4581
+ "get_data": data_refresh_randint_title,
4582
+ "display": left_split_display_list,
4583
+ "data": ["Files", []],
4584
+ "refresh_time": 2,
4585
+ },
4586
+ # Nopane
4587
+ {
4588
+ "proportion": 1/3,
4589
+ "auto_refresh": False,
4590
+ "get_data": lambda data, state: [],
4591
+ "display": lambda scr, x, y, w, h, state, row, cell, data: [],
4592
+ "data": ["Files", []],
4593
+ "refresh_time": 1,
4594
+ },
4595
+ ]
4596
+ function_data["macros"] = [
4597
+ # {
4598
+ # "keys": [ord('z')],
4599
+ # "description": "Display message via dbus.",
4600
+ # "function": lambda picker_obj: os.system("notify-send 'zkey pressed'")
4601
+ # },
4602
+ ]
4603
+ # function_data["require_option"] = [True for _ in function_data["items"]]
4029
4604
 
4030
4605
  stdscr = start_curses()
4031
4606
  try: