listpick 0.1.16.8__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,10 +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
24
+ import copy
25
+ import tempfile
26
+ import queue
23
27
 
24
28
  from listpick.pane.pane_utils import get_file_attributes
29
+ from listpick.pane.left_pane_functions import *
25
30
  from listpick.ui.picker_colours import get_colours, get_help_colours, get_notification_colours, get_theme_count, get_fallback_colours
26
31
  from listpick.utils.options_selectors import default_option_input, output_file_option_selector, default_option_selector
27
32
  from listpick.utils.table_to_list_of_lists import *
@@ -46,6 +51,8 @@ from listpick.pane.get_data import *
46
51
  COLOURS_SET = False
47
52
  help_colours, notification_colours = {}, {}
48
53
 
54
+ os.environ["TMPDIR"] = "/tmp"
55
+
49
56
  class Command:
50
57
  def __init__(self, command_type, command_value):
51
58
  self.command_type = command_type
@@ -68,9 +75,9 @@ class Picker:
68
75
  auto_refresh: bool =False,
69
76
  timer: float = 5,
70
77
 
71
- get_new_data: bool =False, # Whether we can get new data
72
- refresh_function: Optional[Callable] = lambda items, header, visible_rows_indices, getting_data: None, # The function with which we get new data
73
- 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,
74
81
  track_entries_upon_refresh: bool = True,
75
82
  pin_cursor: bool = False,
76
83
  id_column: int = 0,
@@ -93,7 +100,12 @@ class Picker:
93
100
  user_opts : str = "",
94
101
  options_list: list[str] = [],
95
102
  user_settings : str = "",
103
+
96
104
  separator : str = " ",
105
+ header_separator : str = " │",
106
+ header_separator_before_selected_column : str = " ▐",
107
+
108
+
97
109
  search_query : str = "",
98
110
  search_count : int = 0,
99
111
  search_index : int = 0,
@@ -108,6 +120,10 @@ class Picker:
108
120
  highlight_full_row: bool =False,
109
121
  crosshair_cursor: bool = False,
110
122
  cell_cursor: bool = True,
123
+ selected_char: str = "",
124
+ unselected_char: str = "",
125
+ selecting_char: str = "",
126
+ deselecting_char: str = "",
111
127
 
112
128
  items_per_page : int = -1,
113
129
  sort_method : int = 0,
@@ -148,6 +164,7 @@ class Picker:
148
164
  reset_colours: bool = True,
149
165
  key_remappings: dict = {},
150
166
  keys_dict:dict = picker_keys,
167
+ macros: list = [],
151
168
  display_infobox : bool = False,
152
169
  infobox_items: list[list[str]] = [],
153
170
  infobox_title: str = "",
@@ -189,9 +206,19 @@ class Picker:
189
206
  right_panes: list = [],
190
207
  right_pane_index: int = 0,
191
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
+
192
217
  # getting_data: threading.Event = threading.Event(),
193
218
 
194
219
  ):
220
+
221
+ self.screen_size_function = screen_size_function
195
222
  self.stdscr = stdscr
196
223
  self.items = items
197
224
  self.cursor_pos = cursor_pos
@@ -233,6 +260,8 @@ class Picker:
233
260
  self.options_list = options_list
234
261
  self.user_settings = user_settings
235
262
  self.separator = separator
263
+ self.header_separator = header_separator
264
+ self.header_separator_before_selected_column = header_separator_before_selected_column
236
265
  self.search_query = search_query
237
266
  self.search_count = search_count
238
267
  self.search_index = search_index
@@ -247,6 +276,10 @@ class Picker:
247
276
  self.highlight_full_row = highlight_full_row
248
277
  self.crosshair_cursor = crosshair_cursor
249
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
250
283
 
251
284
  self.items_per_page = items_per_page
252
285
  self.sort_method = sort_method
@@ -285,6 +318,7 @@ class Picker:
285
318
  self.reset_colours = reset_colours
286
319
  self.key_remappings = key_remappings
287
320
  self.keys_dict = keys_dict
321
+ self.macros = macros
288
322
  self.display_infobox = display_infobox
289
323
  self.infobox_items = infobox_items
290
324
  self.infobox_title = infobox_title
@@ -342,9 +376,24 @@ class Picker:
342
376
  self.split_right = split_right
343
377
  self.right_panes = right_panes
344
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
+
345
384
  self.visible_rows_indices = []
346
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
+
347
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
348
397
 
349
398
 
350
399
  self.initialise_picker_state(reset_colours=self.reset_colours)
@@ -361,6 +410,11 @@ class Picker:
361
410
  self.getting_data.set()
362
411
 
363
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
+ """
364
418
 
365
419
  size = super().__sizeof__()
366
420
 
@@ -371,7 +425,19 @@ class Picker:
371
425
  return size
372
426
 
373
427
  def set_config(self, path: str ="~/.config/listpick/config.toml") -> bool:
374
- """ 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
+
375
441
  path = os.path.expanduser(os.path.expandvars(path))
376
442
  if not os.path.exists(path):
377
443
  return False
@@ -380,7 +446,9 @@ class Picker:
380
446
  except Exception as e:
381
447
  self.logger.error(f"get_config({path}) load error. {e}")
382
448
  return False
449
+
383
450
 
451
+ # Change the global theme if colour_theme_number is in the loaded config
384
452
  if "general" in config:
385
453
  if "colour_theme_number" in config["general"] and config["general"]["colour_theme_number"] != self.colour_theme_number:
386
454
  global COLOURS_SET
@@ -388,7 +456,7 @@ class Picker:
388
456
  self.colours_end = set_colours(pick=config["general"]["colour_theme_number"], start=1)
389
457
  self.colours = get_colours(config["general"]["colour_theme_number"])
390
458
 
391
- self.logger.info(f"function: set_config()")
459
+ # load the rest of the config options
392
460
  if "general" in config:
393
461
  for key, val in config["general"].items():
394
462
  self.logger.info(f"set_config: key={key}, val={val}.")
@@ -400,27 +468,58 @@ class Picker:
400
468
  return True
401
469
 
402
470
  def get_config(self, path: str ="~/.config/listpick/config.toml") -> dict:
403
- """ 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
+
404
483
  self.logger.info(f"function: get_config()")
405
484
  import toml
406
485
  with open(os.path.expanduser(path), "r") as f:
407
486
  config = toml.load(f)
408
487
  return config
409
488
 
410
- def update_term_size(self):
411
- self.term_h, self.term_w = self.stdscr.getmaxyx()
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.
412
504
 
413
- def get_term_size(self):
414
- return self.stdscr.getmaxyx()
415
- w, h = os.get_terminal_size()
416
- return h, w
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
417
512
 
418
- def calculate_section_sizes(self):
513
+ def calculate_section_sizes(self) -> None:
419
514
  """
420
515
  Calculte the following for the Picker:
421
516
  self.items_per_page: the number of entry rows displayed
422
517
  self.bottom_space: the size of the footer + the bottom buffer space
423
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
424
523
  """
425
524
 
426
525
  self.logger.debug(f"function: calculate_section_sizes()")
@@ -428,13 +527,35 @@ class Picker:
428
527
  # self.bottom_space
429
528
  self.bottom_space = self.footer.height if self.show_footer else 0
430
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
+
431
535
  ## self.top_space
432
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
433
541
  if self.split_right and len(self.right_panes):
434
542
  proportion = self.right_panes[self.right_pane_index]["proportion"]
435
- self.rows_w, self.rows_h = int(self.term_w*proportion), self.term_h
436
- else:
437
- 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
+
438
559
 
439
560
  self.top_space = self.top_gap
440
561
  if self.title: self.top_space+=1
@@ -452,26 +573,45 @@ class Picker:
452
573
  self.top_space += ((self.term_h-(self.top_space+self.bottom_space))-len(self.indexed_items))//2
453
574
 
454
575
  # self.column_widths
455
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
456
- 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)
457
578
 
458
579
  # self.startx
459
580
  self.startx = 1 if self.highlight_full_row else 2
460
581
  if self.show_row_header: self.startx += len(str(len(self.items))) + 2
461
582
  if visible_columns_total_width < self.rows_w and self.centre_in_terminal:
462
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
+
463
592
 
464
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.
465
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.
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
+ """
466
606
  self.logger.debug(f"function: get_visible_rows()")
467
607
  ## Scroll with column select
468
608
  if self.paginate:
469
- 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
470
610
  end_index = min(start_index + self.items_per_page, len(self.indexed_items))
471
611
  ## Scroll
472
612
  else:
473
- scrolloff = self.items_per_page//2
474
- 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))
475
615
  end_index = min(start_index + self.items_per_page, len(self.indexed_items))
476
616
  if len(self.indexed_items) == 0: start_index, end_index = 0, 0
477
617
 
@@ -484,13 +624,15 @@ class Picker:
484
624
  def initialise_picker_state(self, reset_colours=False) -> None:
485
625
  """ Initialise state variables for the picker. These are: debugging and colours. """
486
626
 
627
+ # Define global curses colours
487
628
  if curses.has_colors() and self.colours != None:
488
- # raise Exception("Terminal does not support color")
489
629
  curses.start_color()
630
+
490
631
  if reset_colours:
491
632
  global COLOURS_SET
492
633
  COLOURS_SET = False
493
634
  self.colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
635
+
494
636
  if curses.COLORS >= 255 and curses.COLOR_PAIRS >= 150:
495
637
  self.colours_start = self.colours_start
496
638
  self.notification_colours_start = self.colours_start+50
@@ -507,64 +649,40 @@ class Picker:
507
649
  self.colours = get_colours(self.colour_theme_number)
508
650
 
509
651
 
652
+ # Start logger
510
653
  debug_levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]
511
654
  dbglvl = debug_levels[self.debug_level]
512
655
  self.logger = setup_logger(name="picker_log", log_file="picker.log", log_enabled=self.debug, level =dbglvl)
513
656
  self.logger.info(f"Initialiasing Picker.")
657
+
514
658
  self.update_term_size()
515
- # self.notification(self.stdscr, message=repr(self.logger))
516
-
517
-
518
- # 1 2 3 4 5
519
- # logger = logging.getLogger(__file__)
520
- # if self.debug_level == 0:
521
- # logger = logging.getLogger()
522
- # logger.disabled = True
523
- # else:
524
- #
525
- # file_handler = logging.FileHandler(f"{self.title}.log", mode='w')
526
- # formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', '%m-%d-%Y %H:%M:%S')
527
- # file_handler.setFormatter(formatter)
528
- # logger.addHandler(file_handler)
529
- # logger.setLevel(debug_levels[self.debug_level-1])
530
-
531
- # logging.basicConfig(
532
- # level=debug_levels[self.debug_level-1],
533
- # format='%(asctime)s - %(levelname)s - %(message)s',
534
- # datefmt='%m-%d-%Y %H:%M:%S',
535
- # filename=f"{self.title}.log",
536
- # filemode="w",
537
- # )
538
- #
539
- # self.logger.info(f"Starging log. Log level {logger.getEffectiveLevel()}")
540
- # self.logger.info(f"Starging log. Log level {repr(debug_levels)}, {self.debug_level}, {debug_levels[self.debug_level-1]}")
541
- # self.notification(self.stdscr, f"Starging log. Log level {repr(debug_levels)}, {self.debug_level}, {debug_levels[self.debug_level-1]}")
542
- # self.notification(self.stdscr, f"{__file__}")
543
-
544
- ## Logging level plan
545
- # DEBUG: loop functions, draw screen, etc.
546
- # INFO: main functions
547
- # WARNING: any try-except fails
548
-
549
- # No set_escdelay function on windows.
659
+
660
+ # The curses implementation for some systems (e.g., windows) does not allow set_escdelay
550
661
  try:
551
662
  curses.set_escdelay(25)
552
663
  except:
553
664
  logging.warning("Error trying to set curses.set_escdelay")
554
665
 
555
- # self.stdscr.clear()
556
- # self.stdscr.refresh()
557
- # self.draw_screen()
558
-
559
666
  def initialise_variables(self, get_data: bool = False) -> None:
560
- """ 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
+ """
561
678
 
562
679
  self.logger.info(f"function: initialise_variables()")
563
680
 
564
681
  tracking = False
565
682
 
566
683
  ## Get data synchronously
567
- 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])
568
686
  if self.track_entries_upon_refresh and len(self.items) > 0:
569
687
  tracking = True
570
688
  selected_indices = get_selected_indices(self.selections)
@@ -572,34 +690,41 @@ class Picker:
572
690
  self.ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
573
691
  self.ids_tuples = [(i, item[self.id_column]) for i, item in enumerate(self.items) if i in selected_indices]
574
692
 
575
- 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:
576
694
  self.cursor_pos_id = self.indexed_items[self.cursor_pos][1][self.id_column]
577
695
  self.cursor_pos_prev = self.cursor_pos
578
-
579
-
580
696
 
697
+ # Set the state of the threading event
698
+ # Though we are getting data synchronously, we ensure the correct state for self.getting_data
581
699
  self.getting_data.clear()
582
- self.refresh_function(self.items, self.header, self.visible_rows_indices, self.getting_data)
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
+ )
583
707
 
584
- self.items = pad_lists_to_same_length(self.items)
585
708
 
709
+ # Ensure that an emtpy items object has the form [[]]
586
710
  if self.items == []: self.items = [[]]
587
- ## Ensure that items is a List[List[Str]] object
711
+
712
+ # Ensure that items is a List[List[Str]] object
588
713
  if len(self.items) > 0 and not isinstance(self.items[0], list):
589
714
  self.items = [[item] for item in self.items]
590
715
  # self.items = [[str(cell) for cell in row] for row in self.items]
591
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)
592
719
 
593
720
  # Ensure that header is of the same length as the rows
594
721
  if self.header and len(self.items) > 0 and len(self.header) != len(self.items[0]):
595
722
  self.header = [str(self.header[i]) if i < len(self.header) else "" for i in range(len(self.items[0]))]
596
723
 
597
- # Constants
598
- # DEFAULT_ITEMS_PER_PAGE = os.get_terminal_size().lines - top_gap*2-2-int(bool(header))
599
-
600
724
  self.calculate_section_sizes()
601
725
 
602
- # Initial states
726
+
727
+ # Ensure that the selection-tracking variables are the correct shape
603
728
  if len(self.selections) != len(self.items):
604
729
  self.selections = {i : False if i not in self.selections else bool(self.selections[i]) for i in range(len(self.items))}
605
730
 
@@ -610,31 +735,34 @@ class Picker:
610
735
  self.cell_selections = {}
611
736
  self.selected_cells_by_row = {}
612
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))])
613
742
 
614
743
 
615
- if len(self.require_option) < len(self.items):
616
- self.require_option += [self.require_option_default for i in range(len(self.items)-len(self.require_option))]
617
- if len(self.option_functions) < len(self.items):
618
- self.option_functions += [self.default_option_function for i in range(len(self.items)-len(self.option_functions))]
619
- if len(self.items)>0 and len(self.columns_sort_method) < len(self.items[0]):
620
- self.columns_sort_method = self.columns_sort_method + [0 for i in range(len(self.items[0])-len(self.columns_sort_method))]
621
- if len(self.items)>0 and len(self.sort_reverse) < len(self.items[0]):
622
- self.sort_reverse = self.sort_reverse + [False for i in range(len(self.items[0])-len(self.sort_reverse))]
623
- if len(self.items)>0 and len(self.editable_columns) < len(self.items[0]):
624
- self.editable_columns = self.editable_columns + [self.editable_by_default for i in range(len(self.items[0])-len(self.editable_columns))]
625
- 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]):
626
756
  self.column_indices = self.column_indices + [i for i in range(len(self.column_indices), len(self.items[0]))]
627
757
 
628
758
 
629
759
 
630
- # items2 = [[row[self.column_indices[i]] for i in range(len(row))] for row in self.items]
631
- # self.indexed_items = list(enumerate(items2))
760
+ # Create an indexed list of the items which will track the visible rows
632
761
  if self.items == [[]]: self.indexed_items = []
633
762
  else: self.indexed_items = list(enumerate(self.items))
634
763
 
635
- # If a filter is passed then refilter
764
+ # Apply the filter query
636
765
  if self.filter_query:
637
- # prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
638
766
  # prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
639
767
  self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
640
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)
@@ -650,22 +778,30 @@ class Picker:
650
778
  )
651
779
  if return_val:
652
780
  self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
653
- # If a sort is passed
781
+
782
+ # Apply the current sort method
654
783
  if len(self.indexed_items) > 0:
655
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
656
785
 
657
786
 
658
- # Adjust variables to ensure correctness if errors
659
- ## Move to a selectable row (if applicable)
787
+ # If we have more unselectable indices than rows, clear the unselectable_indices
660
788
  if len(self.items) <= len(self.unselectable_indices): self.unselectable_indices = []
661
- new_pos = (self.cursor_pos)%len(self.items)
662
- while new_pos in self.unselectable_indices and new_pos != self.cursor_pos:
663
- new_pos = (new_pos + 1) % len(self.items)
664
789
 
665
- assert new_pos < len(self.items)
666
- 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={})
667
804
 
668
- # Sheets and files
669
805
  if len(self.sheet_states) < len(self.sheets):
670
806
  self.sheet_states += [{} for _ in range(len(self.sheets) - len(self.sheet_states))]
671
807
  if len(self.sheets):
@@ -673,15 +809,16 @@ class Picker:
673
809
  self.sheet_index = 0
674
810
  self.sheet_name = self.sheets[self.sheet_index]
675
811
 
676
- if len(self.loaded_file_states) < len(self.loaded_files):
677
- 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={})
678
814
  if len(self.loaded_files):
679
815
  if self.loaded_file_index >= len(self.loaded_files):
680
816
  self.loaded_file_index = 0
681
817
  self.loaded_file = self.loaded_files[self.loaded_file_index]
682
818
 
683
- # if tracking and len(self.items) > 1:
684
- # 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.
685
822
  if self.track_entries_upon_refresh and (self.data_ready or tracking) and len(self.items) > 1:
686
823
  selected_indices = []
687
824
  all_ids = [item[self.id_column] for item in self.items]
@@ -704,6 +841,8 @@ class Picker:
704
841
 
705
842
 
706
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
707
846
  if len(self.indexed_items):
708
847
  if self.pin_cursor:
709
848
  self.cursor_pos = min(self.cursor_pos_prev, len(self.indexed_items)-1)
@@ -715,7 +854,12 @@ class Picker:
715
854
  else:
716
855
  self.cursor_pos = 0
717
856
 
718
- 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
719
863
 
720
864
 
721
865
 
@@ -800,7 +944,8 @@ class Picker:
800
944
  self.draw_screen_(clear)
801
945
  except Exception as e:
802
946
  self.logger.warning(f"self.draw_screen_() error. {e}")
803
- pass
947
+ finally:
948
+ self.stdscr.refresh()
804
949
 
805
950
  def draw_screen_(self, clear: bool = True) -> None:
806
951
  """ Draw Picker screen. """
@@ -811,14 +956,11 @@ class Picker:
811
956
  self.stdscr.erase()
812
957
 
813
958
  self.update_term_size()
814
- if self.split_right and len(self.right_panes):
815
- proportion = self.right_panes[self.right_pane_index]["proportion"]
816
- self.rows_w, self.rows_h = int(self.term_w*proportion), self.term_h
817
- else:
818
- self.rows_w, self.rows_h = self.term_w, self.term_h
819
959
 
820
- # The height of the footer may need to be adjusted if the file changes.
960
+ # Determine footer size
821
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.
822
964
  self.calculate_section_sizes()
823
965
 
824
966
  # Test if the terminal is of a sufficient size to display the picker
@@ -836,28 +978,18 @@ class Picker:
836
978
  end_index = min(start_index + self.items_per_page, len(self.indexed_items))
837
979
  if len(self.indexed_items) == 0: start_index, end_index = 0, 0
838
980
 
839
- # 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)
840
- # Determine widths based only on the currently indexed rows
841
- # rows = [v[1] for v in self.indexed_items] if len(self.indexed_items) else self.items
842
- # Determine widths based only on the currently displayed indexed rows
843
- # rows = [v[1] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else self.items
844
981
  self.get_visible_rows()
845
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)
846
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
847
- visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
848
-
849
- # Determine the number of items_per_page, top_size and bottom_size
850
- # self.calculate_section_sizes()
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)
851
985
 
852
- # top_space = self.top_gap
853
986
 
854
- ## Display title (if applicable)
987
+ ## Display title
855
988
  if self.title:
856
989
  padded_title = f" {self.title.strip()} "
857
990
  self.stdscr.addstr(self.top_gap, 0, f"{' ':^{self.term_w}}", curses.color_pair(self.colours_start+16))
858
991
  title_x = (self.term_w-wcswidth(padded_title))//2
859
992
  self.stdscr.addstr(self.top_gap, title_x, padded_title, curses.color_pair(self.colours_start+16) | curses.A_BOLD)
860
- # top_space += 1
861
993
 
862
994
  ## Display modes
863
995
  if self.display_modes and self.modes not in [[{}], []]:
@@ -879,7 +1011,6 @@ class Picker:
879
1011
  else:
880
1012
  self.stdscr.addstr(self.top_gap+1, xmode, mode_str, curses.color_pair(self.colours_start+15) | curses.A_UNDERLINE)
881
1013
  xmode += split_space+mode_widths[i]
882
- # top_space += 1
883
1014
 
884
1015
  ## Display header
885
1016
  if self.header and self.show_header:
@@ -897,137 +1028,129 @@ class Picker:
897
1028
 
898
1029
 
899
1030
  header_str += f"{col_str:^{self.column_widths[i]-len(number)}}"
900
- 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)
901
1036
 
902
1037
  header_str = header_str[self.leftmost_char:]
1038
+ header_str = header_str[:header_str_w]
903
1039
  header_ypos = self.top_gap + bool(self.title) + bool(self.display_modes and self.modes)
904
- self.stdscr.addstr(header_ypos, 0, ' '*self.rows_w, curses.color_pair(self.colours_start+28) | curses.A_BOLD)
905
- 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)
906
1046
 
907
1047
  # Highlight sort column
908
- try:
909
- if self.selected_column != None and self.selected_column not in self.hidden_columns:
910
- # start of string is on screen
911
- col_width = self.column_widths[self.selected_column]
912
- number = f"{self.selected_column}. " if self.number_columns else ""
913
- col_str = self.header[self.selected_column][:self.column_widths[self.selected_column]-len(number)]
914
- highlighted_col_str = (number+f"{col_str:^{self.column_widths[self.selected_column]-len(number)}}") + self.separator
915
-
916
- if len(self.column_widths) == 1:
917
- colour = curses.color_pair(self.colours_start+28) | curses.A_BOLD
918
- else:
919
- colour = curses.color_pair(self.colours_start+19) | curses.A_BOLD
920
- # Start of selected column is on the screen
921
- if self.leftmost_char <= len(up_to_selected_col) and self.leftmost_char+self.rows_w-self.startx > len(up_to_selected_col):
922
- x_pos = len(up_to_selected_col) - self.leftmost_char + self.startx
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
923
1062
 
924
- # Whole cell of the selected column is on the screen
925
- if len(up_to_selected_col)+col_width - self.leftmost_char < self.rows_w-self.startx:
926
- disp_str = highlighted_col_str
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
927
1066
 
928
- # Start of the cell is on the screen, but the end of the cell is not
929
- else:
930
- overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w - self.startx)
931
- disp_str = highlighted_col_str[:-overflow]
932
-
933
- self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
934
- # Start of the cell is to the right of the screen
935
- elif self.leftmost_char+self.rows_w <= len(up_to_selected_col):
936
- pass
937
- # The end of the cell is on the screen, the start of the cell is not
938
- elif 0 <= len(up_to_selected_col)+col_width - self.leftmost_char <= self.rows_w :
939
- x_pos = self.startx
940
- beg = self.leftmost_char - len(up_to_selected_col)
941
- disp_str = highlighted_col_str[beg:]
942
- self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
943
- # The middle of the cell is on the screen, the start and end of the cell are not
944
- elif self.leftmost_char <= len(up_to_selected_col) + col_width//2 <= self.leftmost_char+self.rows_w:
945
- beg = self.leftmost_char - len(up_to_selected_col)
946
- overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w)
947
- disp_str = highlighted_col_str[beg:-overflow]
948
-
949
- x_pos = self.startx
950
- self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
951
- # The cell is to the left of the screen
1067
+ # Start of the cell is on the screen, but the end of the cell is not
952
1068
  else:
953
- pass
954
-
955
- # elif self.leftmost_char:
956
- # os.system(f"notify-send 'cell is to the right of the screen'")
957
-
958
- #
959
- #
960
- # if len(self.header) > 1 and (len(up_to_selected_col)-self.leftmost_char) < self.rows_w:
961
- # number = f"{self.selected_column}. " if self.number_columns else ""
962
- # # number = f"{intStringToExponentString(self.selected_column)}. " if self.number_columns else ""
963
- # # self.startx + len(up_to_selected_col) - self.leftmost_char
964
- # highlighed_col_startx = max(self.startx, self.startx + len(up_to_selected_col) - self.leftmost_char)
965
- #
966
- #
967
- # col_str = self.header[self.selected_column][:self.column_widths[self.selected_column]-len(number)]
968
- # highlighted_col_str = (number+f"{col_str:^{self.column_widths[self.selected_column]}}") + self.separator
969
- # 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)
970
- # if (highlighed_col_startx+len(highlighted_col_str)) > self.rows_w:
971
- # end_of_highlighted_col_str = self.rows_w-(highlighed_col_startx+len(highlighted_col_str))
972
- # else:
973
- # end_of_highlighted_col_str = len(highlighted_col_str)
974
- #
975
- # start_of_highlighted_col_str = max(self.leftmost_char - len(up_to_selected_col), 0)
976
- # 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)
977
- except:
978
- pass
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
979
1100
 
980
1101
  # Display row header
981
1102
  if self.show_row_header:
982
1103
  for idx in range(start_index, end_index):
983
1104
  y = idx - start_index + self.top_space
984
1105
  if idx == self.cursor_pos:
985
- 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)
986
1107
  else:
987
- 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)
988
1109
 
989
1110
 
990
1111
  def highlight_cell(row: int, col:int, visible_column_widths, colour_pair_number: int = 5, bold: bool = False, y:int = 0):
991
1112
 
992
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
993
1115
  # cell_width = self.column_widths[self.selected_column]
994
1116
  cell_width = visible_column_widths[col] + len(self.separator)
995
- 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)
996
1118
 
997
1119
  if bold:
998
1120
  colour = curses.color_pair(self.colours_start+colour_pair_number) | curses.A_BOLD
999
1121
  else:
1000
1122
  colour = curses.color_pair(self.colours_start+colour_pair_number)
1001
- try:
1002
- # Start of cell is on screen
1003
- if self.startx <= cell_pos <= self.rows_w:
1004
- self.stdscr.addstr(y, cell_pos, (' '*cell_width)[:cell_max_width], colour)
1005
- if self.centre_in_cols:
1006
- cell_value = f"{self.indexed_items[row][1][col]:^{cell_width-len(self.separator)}}" + self.separator
1007
- else:
1008
- cell_value = self.indexed_items[row][1][col][:self.column_widths[col]] + self.separator
1009
- # cell_value = cell_value[:min(cell_width, cell_max_width)-len(self.separator)]
1010
- cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
1011
- # cell_value = cell_value + self.separator
1012
- # cell_value = cell_value
1013
- cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
1014
- self.stdscr.addstr(y, cell_pos, cell_value, colour)
1015
- # Part of the cell is on screen
1016
- elif self.startx <= cell_pos+cell_width and cell_pos < (self.rows_w):
1017
- cell_start = self.startx - cell_pos
1018
- # self.stdscr.addstr(y, self.startx, ' '*(cell_width-cell_start), curses.color_pair(self.colours_start+colour_pair_number))
1019
- cell_value = self.indexed_items[row][1][col]
1020
- cell_value = f"{cell_value:^{self.column_widths[col]}}"
1021
-
1022
- cell_value = cell_value[cell_start:visible_column_widths[col]][:self.rows_w-self.startx]
1023
- 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
1024
1129
  else:
1025
- pass
1026
- # if colour_pair_number == 5:
1027
- 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:
1028
1150
  pass
1029
1151
 
1030
1152
 
1153
+
1031
1154
  def sort_highlights(highlights):
1032
1155
  """
1033
1156
  Sort highlights into lists based on their display level.
@@ -1068,10 +1191,11 @@ class Picker:
1068
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)
1069
1192
  if not match: continue
1070
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)
1071
1195
 
1072
1196
  ## We want to search the non-centred values but highlight the centred values.
1073
1197
  if self.centre_in_cols:
1074
- 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)
1075
1199
  field_start += (len(tmp) - len(tmp.lstrip()))
1076
1200
 
1077
1201
  highlight_start = field_start + match.start()
@@ -1082,7 +1206,7 @@ class Picker:
1082
1206
  continue
1083
1207
  highlight_start -= self.leftmost_char
1084
1208
  highlight_end -= self.leftmost_char
1085
- 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)
1086
1210
  except:
1087
1211
  pass
1088
1212
 
@@ -1098,6 +1222,7 @@ class Picker:
1098
1222
  l0_highlights, l1_highlights, l2_highlights = sort_highlights(self.highlights)
1099
1223
 
1100
1224
 
1225
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
1101
1226
  for idx in range(start_index, end_index):
1102
1227
  item = self.indexed_items[idx]
1103
1228
  y = idx - start_index + self.top_space
@@ -1109,23 +1234,26 @@ class Picker:
1109
1234
  # rowstr off screen
1110
1235
  # if self.leftmost_char > len(row_str_orig):
1111
1236
  # trunc_width = 0
1112
- if self.leftmost_char + (self.rows_w-self.startx) <= len(row_str_orig):
1113
- trunc_width = self.rows_w-self.startx
1114
- elif self.leftmost_char <= len(row_str_orig):
1115
- trunc_width = len(row_str_orig) - self.leftmost_char
1116
- else:
1117
- 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))
1118
1246
 
1119
1247
  row_str = truncate_to_display_width(row_str_left_adj, trunc_width, self.unicode_char_width)
1120
1248
  # row_str = truncate_to_display_width(row_str, min(w-self.startx, visible_columns_total_width))[self.leftmost_char:]
1121
1249
 
1122
1250
  ## Display the standard row
1123
- 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))
1251
+ self.stdscr.addstr(y, self.startx, row_str, curses.color_pair(self.colours_start+2))
1124
1252
 
1125
1253
 
1126
1254
  ## Highlight column
1127
1255
  if self.crosshair_cursor:
1128
- 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)
1129
1257
  if idx == self.cursor_pos:
1130
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))
1131
1259
 
@@ -1139,25 +1267,25 @@ class Picker:
1139
1267
  # self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
1140
1268
  if item[0] in self.selected_cells_by_row:
1141
1269
  for j in self.selected_cells_by_row[item[0]]:
1142
- 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)
1143
1271
 
1144
1272
  # Visually selected
1145
1273
  if self.is_selecting:
1146
1274
  if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
1147
1275
  x_interval = range(min(self.start_selection_col, self.selected_column), max(self.start_selection_col, self.selected_column)+1)
1148
1276
  for col in x_interval:
1149
- 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)
1150
1278
 
1151
1279
  # Visually deslected
1152
1280
  if self.is_deselecting:
1153
1281
  if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
1154
1282
  x_interval = range(min(self.start_selection_col, self.selected_column), max(self.start_selection_col, self.selected_column)+1)
1155
1283
  for col in x_interval:
1156
- 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)
1157
1285
  # Higlight cursor row and selected rows
1158
1286
  elif self.highlight_full_row:
1159
1287
  if self.selections[item[0]]:
1160
- 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)
1161
1289
 
1162
1290
  # Visually selected
1163
1291
  if self.is_selecting:
@@ -1170,16 +1298,30 @@ class Picker:
1170
1298
 
1171
1299
  # Highlight the cursor row and the first char of the selected rows.
1172
1300
  else:
1173
- if self.selections[item[0]]:
1174
- self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
1175
- # Visually selected
1176
- if self.is_selecting:
1177
- 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]]:
1178
1316
  self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
1179
- # Visually deslected
1180
- if self.is_deselecting:
1181
- if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
1182
- 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))
1183
1325
 
1184
1326
  if not self.highlights_hide:
1185
1327
  draw_highlights(l1_highlights, idx, y, item)
@@ -1189,9 +1331,9 @@ class Picker:
1189
1331
  # Draw cursor
1190
1332
  if idx == self.cursor_pos:
1191
1333
  if self.cell_cursor:
1192
- 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)
1193
1335
  else:
1194
- 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)
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)
1195
1337
 
1196
1338
  if not self.highlights_hide:
1197
1339
  draw_highlights(l2_highlights, idx, y, item)
@@ -1211,7 +1353,8 @@ class Picker:
1211
1353
  scroll_bar_length = max(1, scroll_bar_length)
1212
1354
  for i in range(scroll_bar_length):
1213
1355
  v = max(self.top_space+int(bool(self.header)), scroll_bar_start-scroll_bar_length//2)
1214
- 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))
1215
1358
 
1216
1359
  # Display refresh symbol
1217
1360
  if self.auto_refresh:
@@ -1240,20 +1383,46 @@ class Picker:
1240
1383
 
1241
1384
  if self.split_right and len(self.right_panes):
1242
1385
  # If we need to refresh the data then do so.
1243
- 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"]):
1244
- get_data = self.right_panes[self.right_pane_index]["get_data"]
1245
- data = self.right_panes[self.right_pane_index]["data"]
1246
- self.right_panes[self.right_pane_index]["data"] = get_data(data, self.get_function_data())
1247
- 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()
1248
1392
 
1249
- draw_pane = self.right_panes[self.right_pane_index]["display"]
1250
- 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)
1251
1396
 
1252
1397
  draw_pane(
1253
1398
  self.stdscr,
1254
- x = self.rows_w,
1399
+ x = self.rows_w + self.startx - self.left_gutter_width,
1255
1400
  y = self.top_space - int(bool(self.show_header and self.header)),
1256
- w = self.term_w-self.rows_w,
1401
+ w = self.right_pane_width,
1402
+ h = self.items_per_page + int(bool(self.show_header and self.header)),
1403
+ state = self.get_function_data(),
1404
+ row = self.indexed_items[self.cursor_pos] if self.indexed_items else [],
1405
+ cell = self.indexed_items[self.cursor_pos][1][self.selected_column] if self.indexed_items else "",
1406
+ data=data,
1407
+ )
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,
1257
1426
  h = self.items_per_page + int(bool(self.show_header and self.header)),
1258
1427
  state = self.get_function_data(),
1259
1428
  row = self.indexed_items[self.cursor_pos] if self.indexed_items else [],
@@ -1261,13 +1430,27 @@ class Picker:
1261
1430
  data=data,
1262
1431
  )
1263
1432
 
1264
- self.stdscr.refresh()
1265
1433
  ## Display infobox
1266
1434
  if self.display_infobox:
1267
1435
  self.infobox(self.stdscr, message=self.infobox_items, title=self.infobox_title)
1268
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
1269
1437
 
1270
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()
1271
1454
 
1272
1455
  def infobox(self, stdscr: curses.window, message: str ="", title: str ="Infobox", colours_end: int = 0, duration: int = 4) -> curses.window:
1273
1456
  """ Display non-interactive infobox window. """
@@ -1309,6 +1492,7 @@ class Picker:
1309
1492
  "reset_colours": False,
1310
1493
  "cell_cursor": False,
1311
1494
  "split_right": False,
1495
+ "split_left": False,
1312
1496
  "crosshair_cursor": False,
1313
1497
  }
1314
1498
 
@@ -1322,113 +1506,127 @@ class Picker:
1322
1506
  self.logger.debug(f"function: get_function_data()")
1323
1507
  """ Returns a dict of the main variables needed to restore the state of list_pikcer. """
1324
1508
  function_data = {
1325
- "selections": self.selections,
1326
- "cell_selections": self.cell_selections,
1327
- "selected_cells_by_row": self.selected_cells_by_row,
1328
- "items_per_page": self.items_per_page,
1329
- "current_row": self.current_row,
1330
- "current_page": self.current_page,
1331
- "cursor_pos": self.cursor_pos,
1332
- "colours": self.colours,
1333
- "colour_theme_number": self.colour_theme_number,
1334
- "selected_column": self.selected_column,
1335
- "sort_column": self.sort_column,
1336
- "sort_method": self.sort_method,
1337
- "sort_reverse": self.sort_reverse,
1338
- "SORT_METHODS": self.SORT_METHODS,
1339
- "hidden_columns": self.hidden_columns,
1340
- "is_selecting": self.is_selecting,
1341
- "is_deselecting": self.is_deselecting,
1342
- "user_opts": self.user_opts,
1343
- "options_list": self.options_list,
1344
- "user_settings": self.user_settings,
1345
- "separator": self.separator,
1346
- "search_query": self.search_query,
1347
- "search_count": self.search_count,
1348
- "search_index": self.search_index,
1349
- "filter_query": self.filter_query,
1350
- "indexed_items": self.indexed_items,
1351
- "start_selection": self.start_selection,
1352
- "start_selection_col": self.start_selection_col,
1353
- "end_selection": self.end_selection,
1354
- "highlights": self.highlights,
1355
- "max_column_width": self.max_column_width,
1356
- "column_indices": self.column_indices,
1357
- "mode_index": self.mode_index,
1358
- "modes": self.modes,
1359
- "title": self.title,
1360
- "display_modes": self.display_modes,
1361
- "require_option": self.require_option,
1362
- "require_option_default": self.require_option_default,
1363
- "option_functions": self.option_functions,
1364
- "top_gap": self.top_gap,
1365
- "number_columns": self.number_columns,
1366
- "items": self.items,
1367
- "indexed_items": self.indexed_items,
1368
- "header": self.header,
1369
- "scroll_bar": self.scroll_bar,
1370
- "columns_sort_method": self.columns_sort_method,
1371
- "disabled_keys": self.disabled_keys,
1372
- "show_footer": self.show_footer,
1373
- "footer_string": self.footer_string,
1374
- "footer_string_auto_refresh": self.footer_string_auto_refresh,
1375
- "footer_string_refresh_function": self.footer_string_refresh_function,
1376
- "footer_timer": self.footer_timer,
1377
- "footer_style": self.footer_style,
1378
- "colours_start": self.colours_start,
1379
- "colours_end": self.colours_end,
1380
- "display_only": self.display_only,
1381
- "infobox_items": self.infobox_items,
1382
- "display_infobox": self.display_infobox,
1383
- "infobox_title": self.infobox_title,
1384
- "key_remappings": self.key_remappings,
1385
- "auto_refresh": self.auto_refresh,
1386
- "get_new_data": self.get_new_data,
1387
- "refresh_function": self.refresh_function,
1388
- "timer": self.timer,
1389
- "get_data_startup": self.get_data_startup,
1390
- "get_footer_string_startup": self.get_footer_string_startup,
1391
- "editable_columns": self.editable_columns,
1392
- "last_key": self.last_key,
1393
- "centre_in_terminal": self.centre_in_terminal,
1394
- "centre_in_terminal_vertical": self.centre_in_terminal_vertical,
1395
- "centre_in_cols": self.centre_in_cols,
1396
- "highlight_full_row": self.highlight_full_row,
1397
- "cell_cursor": self.cell_cursor,
1398
- "column_widths": self.column_widths,
1399
- "track_entries_upon_refresh": self.track_entries_upon_refresh,
1400
- "pin_cursor": self.pin_cursor,
1401
- "id_column": self.id_column,
1402
- "startup_notification": self.startup_notification,
1403
- "keys_dict": self.keys_dict,
1404
- "cancel_is_back": self.cancel_is_back,
1405
- "paginate": self.paginate,
1406
- "leftmost_char": self.leftmost_char,
1407
- "history_filter_and_search" : self.history_filter_and_search,
1408
- "history_pipes" : self.history_pipes,
1409
- "history_opts" : self.history_opts,
1410
- "history_edits" : self.history_edits,
1411
- "history_settings": self.history_settings,
1412
- "show_header": self.show_header,
1413
- "show_row_header": self.show_row_header,
1414
- "debug": self.debug,
1415
- "debug_level": self.debug_level,
1416
- "reset_colours": self.reset_colours,
1417
- "unicode_char_width": self.unicode_char_width,
1418
- "command_stack": self.command_stack,
1419
- "loaded_file": self.loaded_file,
1420
- "loaded_files": self.loaded_files,
1421
- "loaded_file_index": self.loaded_file_index,
1422
- "loaded_file_states": self.loaded_file_states,
1423
- "sheet_index": self.sheet_index,
1424
- "sheets": self.sheets,
1425
- "sheet_name": self.sheet_name,
1426
- "sheet_states": self.sheet_states,
1427
- "split_right": self.split_right,
1428
- "right_panes": self.right_panes,
1429
- "right_pane_index": self.right_pane_index,
1430
- "crosshair_cursor": self.crosshair_cursor,
1431
-
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,
1432
1630
  }
1433
1631
  return function_data
1434
1632
 
@@ -1464,6 +1662,9 @@ class Picker:
1464
1662
  "centre_in_cols",
1465
1663
  "centre_in_terminal",
1466
1664
  "split_right",
1665
+ "left_pane_index",
1666
+ "split_left",
1667
+ "left_pane_index",
1467
1668
  ]
1468
1669
 
1469
1670
  for var in variables:
@@ -1478,15 +1679,6 @@ class Picker:
1478
1679
  self.initialise_picker_state(reset_colours=reset_colours)
1479
1680
 
1480
1681
  self.initialise_variables()
1481
- # if "colour_theme_number" in function_data:
1482
- # global COLOURS_SET
1483
- # COLOURS_SET = False
1484
- # colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
1485
-
1486
- # if "items" in function_data: self.items = function_data["items"]
1487
- # if "header" in function_data: self.header = function_data["header"]
1488
- # self.indexed_items = function_data["indexed_items"] if "indexed_items" in function_data else []
1489
-
1490
1682
 
1491
1683
 
1492
1684
  def delete_entries(self) -> None:
@@ -1557,8 +1749,10 @@ class Picker:
1557
1749
  "number_columns": False,
1558
1750
  "reset_colours": False,
1559
1751
  "split_right": False,
1752
+ "split_left": False,
1560
1753
  "cell_cursor": False,
1561
1754
  "crosshair_cursor": False,
1755
+ "header_separator": " │",
1562
1756
  }
1563
1757
  while True:
1564
1758
  self.update_term_size()
@@ -1569,6 +1763,7 @@ class Picker:
1569
1763
 
1570
1764
  submenu_win = curses.newwin(window_height, window_width, (self.term_h-window_height)//2, (self.term_w-window_width)//2)
1571
1765
  submenu_win.keypad(True)
1766
+ option_picker_data["screen_size_function"] = lambda stdscr: (window_height, window_width)
1572
1767
  OptionPicker = Picker(submenu_win, **option_picker_data)
1573
1768
  s, o, f = OptionPicker.run()
1574
1769
 
@@ -1580,6 +1775,102 @@ class Picker:
1580
1775
  return {}, "", f
1581
1776
 
1582
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
1583
1874
 
1584
1875
  def notification(self, stdscr: curses.window, message: str="", title:str="Notification", colours_end: int=0, duration:int=4) -> None:
1585
1876
 
@@ -1617,10 +1908,11 @@ class Picker:
1617
1908
  "cancel_is_back": True,
1618
1909
  "reset_colours": False,
1619
1910
  "split_right": False,
1911
+ "split_left": False,
1620
1912
  "cell_cursor": False,
1621
1913
  "crosshair_cursor": False,
1622
1914
  "show_header": False,
1623
-
1915
+ "screen_size_function": lambda stdscr: (notification_height, notification_width),
1624
1916
  }
1625
1917
  OptionPicker = Picker(submenu_win, **notification_data)
1626
1918
  s, o, f = OptionPicker.run()
@@ -1757,14 +2049,24 @@ class Picker:
1757
2049
  self.footer_style = (self.footer_style+1)%len(self.footer_options)
1758
2050
  self.footer = self.footer_options[self.footer_style]
1759
2051
  self.initialise_variables()
1760
- elif setting == "pane":
2052
+ elif setting == "rpane":
1761
2053
  self.toggle_right_pane()
1762
2054
 
1763
- elif setting == "pane_cycle":
2055
+ elif setting == "rpane_cycle":
1764
2056
  self.cycle_right_pane()
1765
2057
 
2058
+ elif setting == "lpane":
2059
+ self.toggle_left_pane()
2060
+
2061
+ elif setting == "lpane_cycle":
2062
+ self.cycle_left_pane()
2063
+
1766
2064
  elif setting.startswith("cwd="):
1767
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)
1768
2070
  elif setting.startswith("hl"):
1769
2071
  hl_list = setting.split(",")
1770
2072
  if len(hl_list) > 1:
@@ -1814,6 +2116,9 @@ class Picker:
1814
2116
  self.draw_screen()
1815
2117
  self.notification(self.stdscr, message=f"Theme {self.colour_theme_number} applied.")
1816
2118
  self.colours = get_colours(self.colour_theme_number)
2119
+ elif setting == "colsel":
2120
+ self.draw_screen()
2121
+ self.select_columns(self.stdscr)
1817
2122
 
1818
2123
  else:
1819
2124
  self.user_settings = ""
@@ -1950,6 +2255,7 @@ class Picker:
1950
2255
  if self.indexed_items[new_pos][0] in self.unselectable_indices: new_pos+=1
1951
2256
  else: break
1952
2257
  self.cursor_pos = new_pos
2258
+ self.ensure_no_overscroll()
1953
2259
  return True
1954
2260
 
1955
2261
  def cursor_up(self, count=1) -> bool:
@@ -1963,6 +2269,7 @@ class Picker:
1963
2269
  elif new_pos in self.unselectable_indices: new_pos -= 1
1964
2270
  else: break
1965
2271
  self.cursor_pos = new_pos
2272
+ self.ensure_no_overscroll()
1966
2273
  return True
1967
2274
 
1968
2275
  def remapped_key(self, key: int, val: int, key_remappings: dict) -> bool:
@@ -1984,6 +2291,19 @@ class Picker:
1984
2291
  return True
1985
2292
  return False
1986
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
+
1987
2307
  def copy_dialogue(self) -> None:
1988
2308
  """ Display dialogue to select how rows/cells should be copied. """
1989
2309
  self.logger.info(f"function: copy_dialogue()")
@@ -2177,7 +2497,13 @@ class Picker:
2177
2497
  self.logger.info(f"function: fetch_data()")
2178
2498
  tmp_items, tmp_header = [], []
2179
2499
  self.getting_data.clear()
2180
- self.refresh_function(tmp_items, tmp_header, self.visible_rows_indices, self.getting_data)
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
+ )
2181
2507
  if self.track_entries_upon_refresh:
2182
2508
  selected_indices = get_selected_indices(self.selections)
2183
2509
  self.ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
@@ -2275,10 +2601,6 @@ class Picker:
2275
2601
  row_len = 1
2276
2602
  if self.header: row_len = len(self.header)
2277
2603
  elif len(self.items): row_len = len(self.items[0])
2278
- # if len(self.indexed_items) == 0:
2279
- # insert_at_pos = 0
2280
- # else:
2281
- # insert_at_pos = self.indexed_items[self.cursor_pos][0]
2282
2604
  self.items = self.items[:pos] + [["" for x in range(row_len)]] + self.items[pos:]
2283
2605
  if pos <= self.cursor_pos:
2284
2606
  self.cursor_pos += 1
@@ -2388,17 +2710,85 @@ class Picker:
2388
2710
  self.split_right = not self.split_right
2389
2711
  if self.right_panes[self.right_pane_index]["data"] in [[], None, {}]:
2390
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()
2391
2721
 
2392
2722
 
2393
2723
  def cycle_right_pane(self, increment=1):
2394
2724
  if len(self.right_panes) > 1:
2395
2725
  self.right_pane_index = (self.right_pane_index+1)%len(self.right_panes)
2396
- 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)
2397
2785
 
2398
2786
  def run(self) -> Tuple[list[int], str, dict]:
2399
2787
  """ Run the picker. """
2400
2788
  self.logger.info(f"function: run()")
2401
2789
 
2790
+ self.thread_stop_event.clear()
2791
+
2402
2792
  if self.get_footer_string_startup and self.footer_string_refresh_function != None:
2403
2793
  self.footer_string = " "
2404
2794
  self.footer.adjust_sizes(self.term_h, self.term_w)
@@ -2411,7 +2801,8 @@ class Picker:
2411
2801
 
2412
2802
  self.initial_time = time.time()
2413
2803
  self.initial_time_footer = time.time()-self.footer_timer
2414
- 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
2415
2806
 
2416
2807
  if self.startup_notification:
2417
2808
  self.notification(self.stdscr, message=self.startup_notification)
@@ -2446,11 +2837,7 @@ class Picker:
2446
2837
  tty_fd, self.saved_terminal_state = open_tty()
2447
2838
 
2448
2839
  self.update_term_size()
2449
- if self.split_right and len(self.right_panes):
2450
- proportion = self.right_panes[self.right_pane_index]["proportion"]
2451
- self.rows_w, self.rows_h = int(self.term_w*proportion), self.term_h
2452
- else:
2453
- self.rows_w, self.rows_h = self.term_w, self.term_h
2840
+ self.calculate_section_sizes()
2454
2841
 
2455
2842
  def terminal_resized(old_w, old_h) -> bool:
2456
2843
  w, h = os.get_terminal_size()
@@ -2486,14 +2873,6 @@ class Picker:
2486
2873
  if self.term_resize_event:
2487
2874
  key = curses.KEY_RESIZE
2488
2875
 
2489
- self.update_term_size()
2490
-
2491
- if self.split_right and len(self.right_panes):
2492
- proportion = self.right_panes[self.right_pane_index]["proportion"]
2493
- self.rows_w, self.rows_h = int(self.term_w*proportion), self.term_h
2494
- else:
2495
- self.rows_w, self.rows_h = self.term_w, self.term_h
2496
-
2497
2876
  if key in self.disabled_keys: continue
2498
2877
  clear_screen=True
2499
2878
 
@@ -2512,11 +2891,15 @@ class Picker:
2512
2891
  self.refreshing_data = False
2513
2892
  self.data_ready = False
2514
2893
 
2894
+
2515
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):
2516
2896
  self.logger.debug(f"Get new data (refresh).")
2517
- 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
2518
2901
  self.stdscr.refresh()
2519
- if self.get_new_data and self.refresh_function:
2902
+ if self.get_new_data:
2520
2903
  self.refreshing_data = True
2521
2904
 
2522
2905
  t = threading.Thread(target=self.fetch_data)
@@ -2545,11 +2928,17 @@ class Picker:
2545
2928
  self.initial_time_footer = time.time()
2546
2929
  self.draw_screen()
2547
2930
 
2548
- 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"]):
2549
2932
  get_data = self.right_panes[self.right_pane_index]["get_data"]
2550
2933
  data = self.right_panes[self.right_pane_index]["data"]
2551
2934
  self.right_panes[self.right_pane_index]["data"] = get_data(data, self.get_function_data())
2552
- 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()
2553
2942
 
2554
2943
  if self.check_key("help", key, self.keys_dict):
2555
2944
  self.logger.info(f"key_function help")
@@ -2557,7 +2946,7 @@ class Picker:
2557
2946
  self.stdscr.refresh()
2558
2947
  help_data = {
2559
2948
  # "items": help_lines,
2560
- "items": build_help_rows(self.keys_dict),
2949
+ "items": build_help_rows(self.keys_dict, self.macros),
2561
2950
  "title": f"{self.title} Help",
2562
2951
  "colours_start": self.help_colours_start,
2563
2952
  "colours": help_colours,
@@ -2581,6 +2970,10 @@ class Picker:
2581
2970
  s, o, f = OptionPicker.run()
2582
2971
  self.draw_screen()
2583
2972
 
2973
+ if self.check_and_run_macro(key):
2974
+ self.draw_screen()
2975
+ continue
2976
+
2584
2977
  if self.check_key("info", key, self.keys_dict):
2585
2978
  self.logger.info(f"key_function help")
2586
2979
  self.stdscr.clear()
@@ -2653,6 +3046,14 @@ class Picker:
2653
3046
  data["option_functions"] = f"[...] length = {len(data['option_functions'])}"
2654
3047
  data["loaded_file_states"] = f"[...] length = {len(data['loaded_file_states'])}"
2655
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'])}"
2656
3057
  info_items += [
2657
3058
  ["",""],
2658
3059
  [" get_function_data()", "-*"*30],
@@ -2698,6 +3099,7 @@ class Picker:
2698
3099
  elif self.check_key("exit", key, self.keys_dict):
2699
3100
  self.stdscr.clear()
2700
3101
  if len(self.loaded_files) <= 1:
3102
+ self.cleanup_threads()
2701
3103
  function_data = self.get_function_data()
2702
3104
  restore_terminal_settings(tty_fd, self.saved_terminal_state)
2703
3105
  return [], "", function_data
@@ -2719,6 +3121,7 @@ class Picker:
2719
3121
  self.draw_screen()
2720
3122
 
2721
3123
  elif self.check_key("full_exit", key, self.keys_dict):
3124
+ self.cleanup_threads()
2722
3125
  close_curses(self.stdscr)
2723
3126
  restore_terminal_settings(tty_fd, self.saved_terminal_state)
2724
3127
  exit()
@@ -2758,28 +3161,29 @@ class Picker:
2758
3161
 
2759
3162
  elif self.check_key("settings_options", key, self.keys_dict):
2760
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."]]
2761
3186
  if len(self.items) > 0:
2762
- options += [["cv", "Centre rows vertically"]]
2763
- options += [["pc", "Pin cursor to row number when data refreshes"]]
2764
- options += [["ct", "Centre column-set in terminal"]]
2765
- options += [["cc", "Centre values in cells"]]
2766
- options += [["!r", "Toggle auto-refresh"]]
2767
- options += [["th", "Cycle between themes. (accepts th#)"]]
2768
- options += [["nohl", "Toggle highlights"]]
2769
- options += [["footer", "Toggle footer"]]
2770
- options += [["header", "Toggle header"]]
2771
- options += [["rh", "Toggle row header"]]
2772
- options += [["modes", "Toggle modes"]]
2773
- options += [["ft", "Cycle through footer styles (accepts ft#)"]]
2774
- options += [["file_next", "Go to the next open file."]]
2775
- options += [["file_prev", "Go to the previous open file."]]
2776
- options += [["sheet_next", "Go to the next sheet."]]
2777
- options += [["sheet_prev", "Go to the previous sheet."]]
2778
- options += [["unicode", "Toggle b/w using len and wcwidth to calculate char width."]]
2779
- options += [["ara", "Add empty row after cursor."]]
2780
- options += [["arb", "Add empty row before the cursor."]]
2781
- options += [["aca", "Add empty column after the selected column."]]
2782
- options += [["acb", "Add empty column before the selected column."]]
2783
3187
  options += [[f"col{i}", f"Select column {i}"] for i in range(len(self.items[0]))]
2784
3188
  options += [[f"s{i}", f"Sort by column {i}"] for i in range(len(self.items[0]))]
2785
3189
  options += [[f"!{i}", f"Toggle visibility of column {i}"] for i in range(len(self.items[0]))]
@@ -2868,6 +3272,7 @@ class Picker:
2868
3272
  self.selected_cells_by_row[row] = [col]
2869
3273
 
2870
3274
  self.cursor_down()
3275
+ self.ensure_no_overscroll()
2871
3276
  elif self.check_key("select_all", key, self.keys_dict): # Select all (m or ctrl-a)
2872
3277
  self.select_all()
2873
3278
 
@@ -2882,6 +3287,7 @@ class Picker:
2882
3287
  if new_pos < len(self.indexed_items):
2883
3288
  self.cursor_pos = new_pos
2884
3289
 
3290
+ self.ensure_no_overscroll()
2885
3291
  self.draw_screen()
2886
3292
 
2887
3293
  elif self.check_key("cursor_bottom", key, self.keys_dict):
@@ -2891,17 +3297,15 @@ class Picker:
2891
3297
  else: break
2892
3298
  if new_pos < len(self.items) and new_pos >= 0:
2893
3299
  self.cursor_pos = new_pos
3300
+ self.ensure_no_overscroll()
2894
3301
  self.draw_screen()
2895
- # current_row = items_per_page - 1
2896
- # if current_page + 1 == (len(self.indexed_items) + items_per_page - 1) // items_per_page:
2897
- #
2898
- # current_row = (len(self.indexed_items) +items_per_page - 1) % items_per_page
2899
- # self.draw_screen()
3302
+
2900
3303
  elif self.check_key("enter", key, self.keys_dict):
2901
3304
  self.logger.info(f"key_function enter")
2902
3305
  # Print the selected indices if any, otherwise print the current index
2903
3306
  if self.is_selecting or self.is_deselecting: self.handle_visual_selection()
2904
3307
  if len(self.items) == 0:
3308
+ self.cleanup_threads()
2905
3309
  function_data = self.get_function_data()
2906
3310
  restore_terminal_settings(tty_fd, self.saved_terminal_state)
2907
3311
  return [], "", function_data
@@ -2928,6 +3332,7 @@ class Picker:
2928
3332
  )
2929
3333
 
2930
3334
  if options_sufficient:
3335
+ self.cleanup_threads()
2931
3336
  self.user_opts = usrtxt
2932
3337
  self.stdscr.clear()
2933
3338
  self.stdscr.refresh()
@@ -2941,15 +3346,7 @@ class Picker:
2941
3346
  self.cursor_pos = max(0, self.cursor_pos-self.items_per_page)
2942
3347
 
2943
3348
  elif self.check_key("redraw_screen", key, self.keys_dict):
2944
- self.logger.info(f"key_function redraw_screen")
2945
- self.stdscr.clear()
2946
- self.stdscr.refresh()
2947
- restrict_curses(self.stdscr)
2948
- unrestrict_curses(self.stdscr)
2949
- self.stdscr.clear()
2950
- self.stdscr.refresh()
2951
-
2952
- self.draw_screen()
3349
+ self.refresh_and_draw_screen()
2953
3350
 
2954
3351
  elif self.check_key("cycle_sort_method", key, self.keys_dict):
2955
3352
  if self.sort_column == self.selected_column:
@@ -2990,23 +3387,29 @@ class Picker:
2990
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
2991
3388
  self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
2992
3389
  elif self.check_key("col_select_next", key, self.keys_dict):
2993
- if len(self.items) > 0 and len(self.items[0]) > 0:
2994
- col_index = (self.selected_column +1) % (len(self.items[0]))
2995
- self.selected_column = col_index
2996
- # Flash when we loop back to the first column
2997
- # if self.selected_column == 0:
2998
- # curses.flash()
2999
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()
3000
3403
 
3001
3404
 
3002
3405
  ## Scroll with column select
3003
3406
  self.get_visible_rows()
3004
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)
3005
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
3006
- column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
3007
- start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
3008
- end_of_cell = sum(visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
3009
- display_width = self.rows_w-self.startx
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
3010
3413
  # If the full column is within the current display then don't do anything
3011
3414
  if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
3012
3415
  pass
@@ -3015,13 +3418,20 @@ class Picker:
3015
3418
  self.leftmost_char = end_of_cell - display_width
3016
3419
 
3017
3420
  self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
3421
+ self.ensure_no_overscroll()
3018
3422
 
3019
3423
  elif self.check_key("col_select_prev", key, self.keys_dict):
3020
- if len(self.items) > 0 and len(self.items[0]) > 0:
3021
- col_index = (self.selected_column -1) % (len(self.items[0]))
3022
- self.selected_column = col_index
3023
-
3024
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
+
3025
3435
  # Flash when we loop back to the last column
3026
3436
  # if self.selected_column == len(self.column_widths)-1:
3027
3437
  # curses.flash()
@@ -3029,11 +3439,11 @@ class Picker:
3029
3439
  ## Scroll with column select
3030
3440
  self.get_visible_rows()
3031
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)
3032
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
3033
- column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
3034
- start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
3035
- end_of_cell = sum(visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
3036
- display_width = self.rows_w-self.startx
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
3037
3447
 
3038
3448
  # If the entire column is within the current display then don't do anything
3039
3449
  if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
@@ -3043,25 +3453,26 @@ class Picker:
3043
3453
  self.leftmost_char = start_of_cell
3044
3454
 
3045
3455
  self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
3456
+ self.ensure_no_overscroll()
3046
3457
 
3047
3458
  elif self.check_key("scroll_right", key, self.keys_dict):
3048
3459
  self.logger.info(f"key_function scroll_right")
3049
3460
  if len(self.indexed_items):
3050
- row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
3051
- 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:
3052
3463
  self.leftmost_char += 5
3053
- self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w - self.startx) + 5)
3054
- 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:
3055
3466
  self.leftmost_char = 0
3056
3467
 
3057
3468
  elif self.check_key("scroll_right_25", key, self.keys_dict):
3058
3469
  self.logger.info(f"key_function scroll_right")
3059
3470
  if len(self.indexed_items):
3060
- row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
3061
- 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:
3062
3473
  self.leftmost_char += 25
3063
- self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w - self.startx) + 5)
3064
- 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:
3065
3476
  self.leftmost_char = 0
3066
3477
 
3067
3478
  elif self.check_key("scroll_left", key, self.keys_dict):
@@ -3080,20 +3491,14 @@ class Picker:
3080
3491
  elif self.check_key("scroll_far_right", key, self.keys_dict):
3081
3492
  self.logger.info(f"key_function scroll_far_right")
3082
3493
  longest_row_str_len = 0
3083
- # rows = self.get_visible_rows()
3084
- # for i in range(len(rows)):
3085
- # item = rows[i]
3086
- # row_str = format_row(item, self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
3087
- # if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
3088
- longest_row_str_len = sum(self.column_widths) + (len(self.column_widths)-1)*len(self.separator)
3089
- # for i in range(len(self.indexed_items)):
3090
- # item = self.indexed_items[i]
3091
- # row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)
3092
- # if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
3093
- # self.notification(self.stdscr, f"{longest_row_str_len}")
3094
- self.leftmost_char = max(0, longest_row_str_len-self.rows_w+2+self.startx+5)
3095
- if len(self.items):
3096
- 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
3097
3502
 
3098
3503
  elif self.check_key("add_column_before", key, self.keys_dict):
3099
3504
  self.logger.info(f"key_function add_column_before")
@@ -3150,16 +3555,6 @@ class Picker:
3150
3555
  self.selected_column = min(self.selected_column, row_len-2)
3151
3556
  self.initialise_variables()
3152
3557
 
3153
-
3154
-
3155
-
3156
- # elif self.check_key("increase_lines_per_page", key, self.keys_dict):
3157
- # self.items_per_page += 1
3158
- # self.draw_screen()
3159
- # elif self.check_key("decrease_lines_per_page", key, self.keys_dict):
3160
- # if self.items_per_page > 1:
3161
- # self.items_per_page -= 1
3162
- # self.draw_screen()
3163
3558
  elif self.check_key("decrease_column_width", key, self.keys_dict):
3164
3559
  self.logger.info(f"key_function decrease_column_width")
3165
3560
  if self.max_column_width > 10:
@@ -3185,7 +3580,8 @@ class Picker:
3185
3580
  elif key == curses.KEY_RESIZE: # Terminal resize signal
3186
3581
 
3187
3582
  self.calculate_section_sizes()
3188
- 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)
3583
+ self.ensure_no_overscroll()
3584
+
3189
3585
  self.stdscr.clear()
3190
3586
  self.stdscr.refresh()
3191
3587
  self.draw_screen()
@@ -3340,12 +3736,6 @@ class Picker:
3340
3736
  function_data = self.get_function_data()
3341
3737
  return [], "escape", function_data
3342
3738
 
3343
-
3344
- # else:
3345
- # self.search_query = ""
3346
- # self.mode_index = 0
3347
- # self.highlights = [highlight for highlight in self.highlights if "type" not in highlight or highlight["type"] != "search" ]
3348
- # continue
3349
3739
  self.draw_screen()
3350
3740
 
3351
3741
  elif self.check_key("opts_input", key, self.keys_dict):
@@ -3442,6 +3832,12 @@ class Picker:
3442
3832
  elif self.check_key("cycle_right_pane", key, self.keys_dict):
3443
3833
  self.cycle_right_pane()
3444
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
+
3445
3841
  elif self.check_key("pipe_input", key, self.keys_dict):
3446
3842
  self.logger.info(f"key_function pipe_input")
3447
3843
  # usrtxt = "xargs -d '\n' -I{} "
@@ -3465,7 +3861,7 @@ class Picker:
3465
3861
  field_prefix=" Command: ",
3466
3862
  x=lambda:2,
3467
3863
  y=lambda: self.get_term_size()[0]-2,
3468
- literal=True,
3864
+ literal=False,
3469
3865
  max_length=field_end_f,
3470
3866
  registers=self.registers,
3471
3867
  refresh_screen_function=lambda: self.draw_screen(),
@@ -3555,7 +3951,7 @@ class Picker:
3555
3951
 
3556
3952
  elif self.check_key("edit", key, self.keys_dict):
3557
3953
  self.logger.info(f"key_function edit")
3558
- 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]:
3559
3955
  current_val = self.indexed_items[self.cursor_pos][1][self.selected_column]
3560
3956
  usrtxt = f"{current_val}"
3561
3957
  field_end_f = lambda: self.get_term_size()[1]-38 if self.show_footer else lambda: self.get_term_size()[1]-3
@@ -3584,6 +3980,47 @@ class Picker:
3584
3980
  usrtxt = str(eval(usrtxt[3:]))
3585
3981
  self.indexed_items[self.cursor_pos][1][self.selected_column] = usrtxt
3586
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
+
3587
4024
 
3588
4025
  elif self.check_key("edit_picker", key, self.keys_dict):
3589
4026
  self.logger.info(f"key_function edit_picker")
@@ -3615,7 +4052,7 @@ class Picker:
3615
4052
  self.indexed_items[self.cursor_pos][1][self.selected_column] = usrtxt
3616
4053
  self.history_edits.append(usrtxt)
3617
4054
  elif self.check_key("edit_ipython", key, self.keys_dict):
3618
- self.logger.info(f"key_function edit_picker")
4055
+ self.logger.info(f"key_function edit_ipython")
3619
4056
  import IPython, termios
3620
4057
  self.stdscr.clear()
3621
4058
  restrict_curses(self.stdscr)
@@ -3759,7 +4196,7 @@ def parse_arguments() -> Tuple[argparse.Namespace, dict]:
3759
4196
  # input_arg = args.filename
3760
4197
 
3761
4198
  elif args.generate:
3762
- function_data["refresh_function"] = lambda items, header, visible_rows_indices, getting_data: generate_picker_data_from_file(args.generate, items, header, visible_rows_indices, getting_data)
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)
3763
4200
  function_data["get_data_startup"] = True
3764
4201
  function_data["get_new_data"] = True
3765
4202
  return args, function_data
@@ -4003,7 +4440,7 @@ def main() -> None:
4003
4440
  # function_data["cell_cursor"] = True
4004
4441
  # function_data["display_modes"] = True
4005
4442
  # function_data["centre_in_cols"] = True
4006
- # function_data["show_row_header"] = True
4443
+ function_data["show_row_header"] = True
4007
4444
  # function_data["keys_dict"] = picker_keys
4008
4445
  # function_data["id_column"] = -1
4009
4446
  # function_data["track_entries_upon_refresh"] = True
@@ -4019,10 +4456,23 @@ def main() -> None:
4019
4456
  # function_data["debug"] = True
4020
4457
  # function_data["debug_level"] = 1
4021
4458
 
4459
+ # function_data["cell_cursor"] = False
4460
+
4022
4461
  function_data["split_right"] = False
4462
+ function_data["split_left"] = False
4023
4463
  function_data["right_pane_index"] = 2
4464
+ function_data["left_pane_index"] = 0
4024
4465
 
4025
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
+ },
4026
4476
  # Graph or random numbers generated each second
4027
4477
  {
4028
4478
  "proportion": 1/2,
@@ -4034,7 +4484,7 @@ def main() -> None:
4034
4484
  },
4035
4485
  # list of numbers
4036
4486
  {
4037
- "proportion": 2/3,
4487
+ "proportion": 1/3,
4038
4488
  "auto_refresh": False,
4039
4489
  "get_data": data_refresh_randint_title,
4040
4490
  "display": right_split_display_list,
@@ -4043,7 +4493,7 @@ def main() -> None:
4043
4493
  },
4044
4494
  # File attributes
4045
4495
  {
4046
- "proportion": 2/3,
4496
+ "proportion": 1/3,
4047
4497
  "auto_refresh": False,
4048
4498
  "get_data": lambda data, state: [],
4049
4499
  "display": right_split_file_attributes,
@@ -4052,7 +4502,7 @@ def main() -> None:
4052
4502
  },
4053
4503
  # File attributes dynamic
4054
4504
  {
4055
- "proportion": 2/3,
4505
+ "proportion": 1/3,
4056
4506
  "auto_refresh": True,
4057
4507
  "get_data": update_file_attributes,
4058
4508
  "display": right_split_file_attributes_dynamic,
@@ -4078,7 +4528,79 @@ def main() -> None:
4078
4528
  "refresh_time": 1,
4079
4529
  },
4080
4530
  ]
4081
- 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"]]
4082
4604
 
4083
4605
  stdscr = start_curses()
4084
4606
  try: