listpick 0.1.16.10__py3-none-any.whl → 0.1.16.12__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.

Potentially problematic release.


This version of listpick might be problematic. Click here for more details.

listpick/listpick_app.py CHANGED
@@ -18,9 +18,13 @@ from wcwidth import wcswidth
18
18
  from typing import Callable, Optional, Tuple, Dict
19
19
  import json
20
20
  import threading
21
+ import multiprocessing
21
22
  import string
22
23
  import logging
23
- from copy import copy
24
+ import copy
25
+ import tempfile
26
+ import queue
27
+ from listpick.utils.generate_data_utils import ProcessSafePriorityQueue
24
28
 
25
29
  from listpick.pane.pane_utils import get_file_attributes
26
30
  from listpick.pane.left_pane_functions import *
@@ -70,9 +74,9 @@ class Picker:
70
74
  auto_refresh: bool =False,
71
75
  timer: float = 5,
72
76
 
73
- get_new_data: bool =False, # Whether we can get new data
74
- refresh_function: Optional[Callable] = lambda items, header, visible_rows_indices, getting_data: None, # The function with which we get new data
75
- get_data_startup: bool =False, # Whether we should get data at statrup
77
+ get_new_data: bool =False,
78
+ refresh_function: Optional[Callable] = lambda items, header, visible_rows_indices, getting_data: None,
79
+ get_data_startup: bool =False,
76
80
  track_entries_upon_refresh: bool = True,
77
81
  pin_cursor: bool = False,
78
82
  id_column: int = 0,
@@ -95,8 +99,12 @@ class Picker:
95
99
  user_opts : str = "",
96
100
  options_list: list[str] = [],
97
101
  user_settings : str = "",
102
+
98
103
  separator : str = " ",
99
104
  header_separator : str = " │",
105
+ header_separator_before_selected_column : str = " ▐",
106
+
107
+
100
108
  search_query : str = "",
101
109
  search_count : int = 0,
102
110
  search_index : int = 0,
@@ -111,6 +119,10 @@ class Picker:
111
119
  highlight_full_row: bool =False,
112
120
  crosshair_cursor: bool = False,
113
121
  cell_cursor: bool = True,
122
+ selected_char: str = "",
123
+ unselected_char: str = "",
124
+ selecting_char: str = "",
125
+ deselecting_char: str = "",
114
126
 
115
127
  items_per_page : int = -1,
116
128
  sort_method : int = 0,
@@ -197,6 +209,8 @@ class Picker:
197
209
  left_pane_index: int = 0,
198
210
 
199
211
  screen_size_function = lambda stdscr: os.get_terminal_size()[::-1],
212
+ generate_data_for_hidden_columns: bool = False,
213
+
200
214
 
201
215
  # getting_data: threading.Event = threading.Event(),
202
216
 
@@ -245,6 +259,7 @@ class Picker:
245
259
  self.user_settings = user_settings
246
260
  self.separator = separator
247
261
  self.header_separator = header_separator
262
+ self.header_separator_before_selected_column = header_separator_before_selected_column
248
263
  self.search_query = search_query
249
264
  self.search_count = search_count
250
265
  self.search_index = search_index
@@ -259,6 +274,10 @@ class Picker:
259
274
  self.highlight_full_row = highlight_full_row
260
275
  self.crosshair_cursor = crosshair_cursor
261
276
  self.cell_cursor = cell_cursor
277
+ self.selected_char = selected_char
278
+ self.unselected_char = unselected_char
279
+ self.selecting_char = selecting_char
280
+ self.deselecting_char = deselecting_char
262
281
 
263
282
  self.items_per_page = items_per_page
264
283
  self.sort_method = sort_method
@@ -361,8 +380,18 @@ class Picker:
361
380
 
362
381
  self.visible_rows_indices = []
363
382
 
383
+ self.generate_data_for_hidden_columns = generate_data_for_hidden_columns
384
+ self.thread_stop_event = threading.Event()
385
+ self.data_generation_queue = queue.PriorityQueue()
386
+ self.threads = []
364
387
 
365
388
 
389
+ self.process_manager = multiprocessing.Manager()
390
+ # self.data_generation_queue = ProcessSafePriorityQueue
391
+ self.processes = []
392
+ self.items_sync_loop_event = threading.Event()
393
+ self.items_sync_thread = None
394
+
366
395
 
367
396
  self.initialise_picker_state(reset_colours=self.reset_colours)
368
397
 
@@ -378,6 +407,11 @@ class Picker:
378
407
  self.getting_data.set()
379
408
 
380
409
  def __sizeof__(self):
410
+ """
411
+ Return the approximate memory footprint of the Picker instance.
412
+
413
+ This includes the size of the instance itself and the sizes of its attributes.
414
+ """
381
415
 
382
416
  size = super().__sizeof__()
383
417
 
@@ -388,7 +422,19 @@ class Picker:
388
422
  return size
389
423
 
390
424
  def set_config(self, path: str ="~/.config/listpick/config.toml") -> bool:
391
- """ Set config from toml file. """
425
+ """ Set config from toml file.
426
+
427
+ This method reads a configuration file in TOML format, applies settings
428
+ to the Picker, and returns a boolean indicating success or failure.
429
+
430
+ Args:
431
+ path (str): The path to the configuration file.
432
+
433
+ Returns:
434
+ bool: True if the configuration was successfully set; False otherwise.
435
+ """
436
+ self.logger.info(f"function: set_config()")
437
+
392
438
  path = os.path.expanduser(os.path.expandvars(path))
393
439
  if not os.path.exists(path):
394
440
  return False
@@ -397,7 +443,9 @@ class Picker:
397
443
  except Exception as e:
398
444
  self.logger.error(f"get_config({path}) load error. {e}")
399
445
  return False
446
+
400
447
 
448
+ # Change the global theme if colour_theme_number is in the loaded config
401
449
  if "general" in config:
402
450
  if "colour_theme_number" in config["general"] and config["general"]["colour_theme_number"] != self.colour_theme_number:
403
451
  global COLOURS_SET
@@ -405,7 +453,7 @@ class Picker:
405
453
  self.colours_end = set_colours(pick=config["general"]["colour_theme_number"], start=1)
406
454
  self.colours = get_colours(config["general"]["colour_theme_number"])
407
455
 
408
- self.logger.info(f"function: set_config()")
456
+ # load the rest of the config options
409
457
  if "general" in config:
410
458
  for key, val in config["general"].items():
411
459
  self.logger.info(f"set_config: key={key}, val={val}.")
@@ -417,30 +465,58 @@ class Picker:
417
465
  return True
418
466
 
419
467
  def get_config(self, path: str ="~/.config/listpick/config.toml") -> dict:
420
- """ Get config from file. """
468
+ """
469
+ Retrieve configuration settings from a specified TOML file.
470
+
471
+ Args:
472
+ path (str): The file path of the configuration file. Default is
473
+ ~/.config/listpick/config.toml.
474
+
475
+ Returns:
476
+ dict: A dictionary containing the configuration settings loaded
477
+ from the TOML file. In case of an error, an empty dictionary is returned.
478
+ """
479
+
421
480
  self.logger.info(f"function: get_config()")
422
481
  import toml
423
482
  with open(os.path.expanduser(path), "r") as f:
424
483
  config = toml.load(f)
425
484
  return config
426
485
 
427
- def update_term_size(self):
486
+ def update_term_size(self) -> None:
487
+ """
488
+ Update self.term_h, self.term_w the function provided to the Picker.
489
+
490
+ Returns:
491
+ None
492
+ """
428
493
  self.term_h, self.term_w = self.screen_size_function(self.stdscr)
429
494
  # self.term_h, self.term_w = self.stdscr.getmaxyx()
430
495
  # self.term_w, self.term_h = os.get_terminal_size()
431
496
 
432
497
 
433
- def get_term_size(self):
434
- return self.stdscr.getmaxyx()
435
- w, h = os.get_terminal_size()
436
- return h, w
498
+ def get_term_size(self) -> Tuple[int, int]:
499
+ """
500
+ Get the current terminal size using the function provided to the Picker.
501
+
502
+ Returns:
503
+ Tuple[int, int]: A tuple containing the (height, width) of the terminal.
504
+ """
505
+ return self.screen_size_function(self.stdscr)
506
+ # return self.stdscr.getmaxyx()
507
+ # w, h = os.get_terminal_size()
508
+ # return h, w
437
509
 
438
- def calculate_section_sizes(self):
510
+ def calculate_section_sizes(self) -> None:
439
511
  """
440
512
  Calculte the following for the Picker:
441
513
  self.items_per_page: the number of entry rows displayed
442
514
  self.bottom_space: the size of the footer + the bottom buffer space
443
515
  self.top_space: the size of the space at the top of the picker: title + modes + header + top_gap
516
+ Calculate and update the sizes of various sections of the Picker.
517
+
518
+ Returns:
519
+ None
444
520
  """
445
521
 
446
522
  self.logger.debug(f"function: calculate_section_sizes()")
@@ -494,8 +570,8 @@ class Picker:
494
570
  self.top_space += ((self.term_h-(self.top_space+self.bottom_space))-len(self.indexed_items))//2
495
571
 
496
572
  # self.column_widths
497
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
498
- visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
573
+ self.visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
574
+ visible_columns_total_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
499
575
 
500
576
  # self.startx
501
577
  self.startx = 1 if self.highlight_full_row else 2
@@ -512,16 +588,27 @@ class Picker:
512
588
 
513
589
 
514
590
  def get_visible_rows(self) -> list[list[str]]:
591
+ """
592
+ Calculate and return the currently visible rows based on the cursor position and pagination settings.
515
593
 
594
+ This method determines which rows from the indexed items are visible on the screen,
595
+ accounting for pagination and scrolling. It sets the starting and ending indices
596
+ based on the current cursor position and the number of items per page.
597
+
598
+ Returns:
599
+ list[list[str]]: The currently visible rows as a list of lists, where each inner
600
+ list represents a row of data. If there are no indexed items, it returns the
601
+ items array.
602
+ """
516
603
  self.logger.debug(f"function: get_visible_rows()")
517
604
  ## Scroll with column select
518
605
  if self.paginate:
519
- start_index = (self.cursor_pos//self.items_per_page) * self.items_per_page
606
+ start_index = (self.cursor_pos // self.items_per_page) * self.items_per_page
520
607
  end_index = min(start_index + self.items_per_page, len(self.indexed_items))
521
608
  ## Scroll
522
609
  else:
523
- scrolloff = self.items_per_page//2
524
- start_index = max(0, min(self.cursor_pos - (self.items_per_page-scrolloff), len(self.indexed_items)-self.items_per_page))
610
+ scrolloff = self.items_per_page // 2
611
+ start_index = max(0, min(self.cursor_pos - (self.items_per_page - scrolloff), len(self.indexed_items) - self.items_per_page))
525
612
  end_index = min(start_index + self.items_per_page, len(self.indexed_items))
526
613
  if len(self.indexed_items) == 0: start_index, end_index = 0, 0
527
614
 
@@ -534,13 +621,15 @@ class Picker:
534
621
  def initialise_picker_state(self, reset_colours=False) -> None:
535
622
  """ Initialise state variables for the picker. These are: debugging and colours. """
536
623
 
624
+ # Define global curses colours
537
625
  if curses.has_colors() and self.colours != None:
538
- # raise Exception("Terminal does not support color")
539
626
  curses.start_color()
627
+
540
628
  if reset_colours:
541
629
  global COLOURS_SET
542
630
  COLOURS_SET = False
543
631
  self.colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
632
+
544
633
  if curses.COLORS >= 255 and curses.COLOR_PAIRS >= 150:
545
634
  self.colours_start = self.colours_start
546
635
  self.notification_colours_start = self.colours_start+50
@@ -557,64 +646,40 @@ class Picker:
557
646
  self.colours = get_colours(self.colour_theme_number)
558
647
 
559
648
 
649
+ # Start logger
560
650
  debug_levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]
561
651
  dbglvl = debug_levels[self.debug_level]
562
652
  self.logger = setup_logger(name="picker_log", log_file="picker.log", log_enabled=self.debug, level =dbglvl)
563
653
  self.logger.info(f"Initialiasing Picker.")
654
+
564
655
  self.update_term_size()
565
- # self.notification(self.stdscr, message=repr(self.logger))
566
-
567
-
568
- # 1 2 3 4 5
569
- # logger = logging.getLogger(__file__)
570
- # if self.debug_level == 0:
571
- # logger = logging.getLogger()
572
- # logger.disabled = True
573
- # else:
574
- #
575
- # file_handler = logging.FileHandler(f"{self.title}.log", mode='w')
576
- # formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', '%m-%d-%Y %H:%M:%S')
577
- # file_handler.setFormatter(formatter)
578
- # logger.addHandler(file_handler)
579
- # logger.setLevel(debug_levels[self.debug_level-1])
580
-
581
- # logging.basicConfig(
582
- # level=debug_levels[self.debug_level-1],
583
- # format='%(asctime)s - %(levelname)s - %(message)s',
584
- # datefmt='%m-%d-%Y %H:%M:%S',
585
- # filename=f"{self.title}.log",
586
- # filemode="w",
587
- # )
588
- #
589
- # self.logger.info(f"Starging log. Log level {logger.getEffectiveLevel()}")
590
- # self.logger.info(f"Starging log. Log level {repr(debug_levels)}, {self.debug_level}, {debug_levels[self.debug_level-1]}")
591
- # self.notification(self.stdscr, f"Starging log. Log level {repr(debug_levels)}, {self.debug_level}, {debug_levels[self.debug_level-1]}")
592
- # self.notification(self.stdscr, f"{__file__}")
593
-
594
- ## Logging level plan
595
- # DEBUG: loop functions, draw screen, etc.
596
- # INFO: main functions
597
- # WARNING: any try-except fails
598
-
599
- # No set_escdelay function on windows.
656
+
657
+ # The curses implementation for some systems (e.g., windows) does not allow set_escdelay
600
658
  try:
601
659
  curses.set_escdelay(25)
602
660
  except:
603
661
  logging.warning("Error trying to set curses.set_escdelay")
604
662
 
605
- # self.stdscr.clear()
606
- # self.stdscr.refresh()
607
- # self.draw_screen()
608
-
609
663
  def initialise_variables(self, get_data: bool = False) -> None:
610
- """ Initialise the variables that keep track of the data. """
664
+ """
665
+ This method sets up the internal state of the Picker by initialising various attributes,
666
+ getting new data (if get_data is True), and ensuring that the lists used for tracking
667
+ selections, options, and items are correctly of the correct type, size, and shape. If
668
+ filter or sort queries are set then they are applied (or re-applied as the case may be).
669
+ The cursor_pos and selections are retained by tracking the id of the rows (where the id
670
+ is row[self.id_column]).
671
+
672
+ Parameters:
673
+ - get_data (bool): If True, pulls data synchronously and updates tracking variables.
674
+ """
611
675
 
612
676
  self.logger.info(f"function: initialise_variables()")
613
677
 
614
678
  tracking = False
615
679
 
616
680
  ## Get data synchronously
617
- if get_data and self.refresh_function != None:
681
+ if get_data:
682
+ # Track cursor_pos and selections by ther id (row[self.id_column][col])
618
683
  if self.track_entries_upon_refresh and len(self.items) > 0:
619
684
  tracking = True
620
685
  selected_indices = get_selected_indices(self.selections)
@@ -622,34 +687,41 @@ class Picker:
622
687
  self.ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
623
688
  self.ids_tuples = [(i, item[self.id_column]) for i, item in enumerate(self.items) if i in selected_indices]
624
689
 
625
- if len(self.indexed_items) > 0 and len(self.indexed_items) >= self.cursor_pos and len(self.indexed_items[0][1]) >= self.id_column:
690
+ if len(self.indexed_items) > 0 and self.cursor_pos < len(self.indexed_items) and len(self.indexed_items[0][1]) >= self.id_column:
626
691
  self.cursor_pos_id = self.indexed_items[self.cursor_pos][1][self.id_column]
627
692
  self.cursor_pos_prev = self.cursor_pos
628
-
629
-
630
693
 
694
+ # Set the state of the threading event
695
+ # Though we are getting data synchronously, we ensure the correct state for self.getting_data
631
696
  self.getting_data.clear()
632
- self.refresh_function(self.items, self.header, self.visible_rows_indices, self.getting_data)
697
+ self.refresh_function(
698
+ self.items,
699
+ self.header,
700
+ self.visible_rows_indices,
701
+ self.getting_data,
702
+ self.get_function_data(),
703
+ )
633
704
 
634
- self.items = pad_lists_to_same_length(self.items)
635
705
 
706
+ # Ensure that an emtpy items object has the form [[]]
636
707
  if self.items == []: self.items = [[]]
637
- ## Ensure that items is a List[List[Str]] object
708
+
709
+ # Ensure that items is a List[List[Str]] object
638
710
  if len(self.items) > 0 and not isinstance(self.items[0], list):
639
711
  self.items = [[item] for item in self.items]
640
712
  # self.items = [[str(cell) for cell in row] for row in self.items]
641
713
 
714
+ # Ensure that the each of the rows of the items are of the same length
715
+ self.items = pad_lists_to_same_length(self.items)
642
716
 
643
717
  # Ensure that header is of the same length as the rows
644
718
  if self.header and len(self.items) > 0 and len(self.header) != len(self.items[0]):
645
719
  self.header = [str(self.header[i]) if i < len(self.header) else "" for i in range(len(self.items[0]))]
646
720
 
647
- # Constants
648
- # DEFAULT_ITEMS_PER_PAGE = os.get_terminal_size().lines - top_gap*2-2-int(bool(header))
649
-
650
721
  self.calculate_section_sizes()
651
722
 
652
- # Initial states
723
+
724
+ # Ensure that the selection-tracking variables are the correct shape
653
725
  if len(self.selections) != len(self.items):
654
726
  self.selections = {i : False if i not in self.selections else bool(self.selections[i]) for i in range(len(self.items))}
655
727
 
@@ -660,31 +732,34 @@ class Picker:
660
732
  self.cell_selections = {}
661
733
  self.selected_cells_by_row = {}
662
734
 
735
+ def extend_list_to_length(lst, length, default_value):
736
+ """Extend a list to the target length using a default value."""
737
+ if len(lst) < length:
738
+ lst.extend([copy.deepcopy(default_value) for _ in range(length - len(lst))])
663
739
 
664
740
 
665
- if len(self.require_option) < len(self.items):
666
- self.require_option += [self.require_option_default for i in range(len(self.items)-len(self.require_option))]
667
- if len(self.option_functions) < len(self.items):
668
- self.option_functions += [self.default_option_function for i in range(len(self.items)-len(self.option_functions))]
669
- if len(self.items)>0 and len(self.columns_sort_method) < len(self.items[0]):
670
- self.columns_sort_method = self.columns_sort_method + [0 for i in range(len(self.items[0])-len(self.columns_sort_method))]
671
- if len(self.items)>0 and len(self.sort_reverse) < len(self.items[0]):
672
- self.sort_reverse = self.sort_reverse + [False for i in range(len(self.items[0])-len(self.sort_reverse))]
673
- if len(self.items)>0 and len(self.editable_columns) < len(self.items[0]):
674
- self.editable_columns = self.editable_columns + [self.editable_by_default for i in range(len(self.items[0])-len(self.editable_columns))]
675
- if len(self.items)>0 and len(self.column_indices) < len(self.items[0]):
741
+ row_count = len(self.items)
742
+ col_count = len(self.items[0]) if row_count else 0
743
+
744
+ # Ensure that the length of the option lists are of the correct length.
745
+ if row_count > 0:
746
+ extend_list_to_length(self.require_option, length=row_count, default_value=self.require_option_default)
747
+ extend_list_to_length(self.option_functions, length=row_count, default_value=self.default_option_function)
748
+ extend_list_to_length(self.columns_sort_method, length=col_count, default_value=0)
749
+ extend_list_to_length(self.sort_reverse, length=col_count, default_value=False)
750
+ extend_list_to_length(self.editable_columns, length=col_count, default_value=self.editable_by_default)
751
+
752
+ if row_count > 0 and len(self.column_indices) < len(self.items[0]):
676
753
  self.column_indices = self.column_indices + [i for i in range(len(self.column_indices), len(self.items[0]))]
677
754
 
678
755
 
679
756
 
680
- # items2 = [[row[self.column_indices[i]] for i in range(len(row))] for row in self.items]
681
- # self.indexed_items = list(enumerate(items2))
757
+ # Create an indexed list of the items which will track the visible rows
682
758
  if self.items == [[]]: self.indexed_items = []
683
759
  else: self.indexed_items = list(enumerate(self.items))
684
760
 
685
- # If a filter is passed then refilter
761
+ # Apply the filter query
686
762
  if self.filter_query:
687
- # prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
688
763
  # prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
689
764
  self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
690
765
  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)
@@ -700,22 +775,30 @@ class Picker:
700
775
  )
701
776
  if return_val:
702
777
  self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
703
- # If a sort is passed
778
+
779
+ # Apply the current sort method
704
780
  if len(self.indexed_items) > 0:
705
781
  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
706
782
 
707
783
 
708
- # Adjust variables to ensure correctness if errors
709
- ## Move to a selectable row (if applicable)
784
+ # If we have more unselectable indices than rows, clear the unselectable_indices
710
785
  if len(self.items) <= len(self.unselectable_indices): self.unselectable_indices = []
711
- new_pos = (self.cursor_pos)%len(self.items)
712
- while new_pos in self.unselectable_indices and new_pos != self.cursor_pos:
713
- new_pos = (new_pos + 1) % len(self.items)
714
786
 
715
- assert new_pos < len(self.items)
716
- self.cursor_pos = new_pos
787
+ # Move cursur to a selectable row if we are currently on an unselectable row)
788
+ if self.cursor_pos * len(self.items) in self.unselectable_indices:
789
+ original_pos = new_pos = (self.cursor_pos)%len(self.items)
790
+ while new_pos in self.unselectable_indices:
791
+ new_pos = (new_pos + 1) % len(self.items)
792
+
793
+ # Break if we loop back to the original position
794
+ if new_pos == original_pos:
795
+ break
796
+
797
+ self.cursor_pos = max(0, min(new_pos, len(self.items)-1))
798
+
799
+ # Initialise sheets
800
+ extend_list_to_length(self.sheet_states, length=len(self.sheets), default_value={})
717
801
 
718
- # Sheets and files
719
802
  if len(self.sheet_states) < len(self.sheets):
720
803
  self.sheet_states += [{} for _ in range(len(self.sheets) - len(self.sheet_states))]
721
804
  if len(self.sheets):
@@ -723,15 +806,16 @@ class Picker:
723
806
  self.sheet_index = 0
724
807
  self.sheet_name = self.sheets[self.sheet_index]
725
808
 
726
- if len(self.loaded_file_states) < len(self.loaded_files):
727
- self.loaded_file_states += [{} for _ in range(len(self.loaded_files) - len(self.loaded_file_states))]
809
+ # Initialise files
810
+ extend_list_to_length(self.loaded_file_states, length=len(self.loaded_files), default_value={})
728
811
  if len(self.loaded_files):
729
812
  if self.loaded_file_index >= len(self.loaded_files):
730
813
  self.loaded_file_index = 0
731
814
  self.loaded_file = self.loaded_files[self.loaded_file_index]
732
815
 
733
- # if tracking and len(self.items) > 1:
734
- # Ensure that selected indices are tracked upon data refresh
816
+
817
+ # Ensure that the correct cursor_pos and selected indices are reselected
818
+ # if we have fetched new data.
735
819
  if self.track_entries_upon_refresh and (self.data_ready or tracking) and len(self.items) > 1:
736
820
  selected_indices = []
737
821
  all_ids = [item[self.id_column] for item in self.items]
@@ -754,6 +838,8 @@ class Picker:
754
838
 
755
839
 
756
840
 
841
+ # Ensure cursor_pos is set to a valid index
842
+ # If we have fetched new data then we attempt to set cursor_pos to the row with the same id as prev
757
843
  if len(self.indexed_items):
758
844
  if self.pin_cursor:
759
845
  self.cursor_pos = min(self.cursor_pos_prev, len(self.indexed_items)-1)
@@ -765,7 +851,12 @@ class Picker:
765
851
  else:
766
852
  self.cursor_pos = 0
767
853
 
768
- self.right_pane_index = max(0, min(self.right_pane_index, len(self.right_panes)-1))
854
+
855
+ # Ensure that the pane indices are within the range of the available panes.
856
+ if len(self.left_panes): self.left_pane_index %= len(self.left_panes)
857
+ else: self.left_pane_index = 0
858
+ if len(self.right_panes): self.right_pane_index %= len(self.right_panes)
859
+ else: self.right_pane_index = 0
769
860
 
770
861
 
771
862
 
@@ -862,14 +953,11 @@ class Picker:
862
953
  self.stdscr.erase()
863
954
 
864
955
  self.update_term_size()
865
- # if self.split_right and len(self.right_panes):
866
- # proportion = self.right_panes[self.right_pane_index]["proportion"]
867
- # self.rows_w, self.rows_h = int(self.term_w*proportion), self.term_h
868
- # else:
869
- # self.rows_w, self.rows_h = self.term_w, self.term_h
870
956
 
871
- # The height of the footer may need to be adjusted if the file changes.
957
+ # Determine footer size
872
958
  self.footer.adjust_sizes(self.term_h,self.term_w)
959
+
960
+ # The height of the footer may need to be adjusted if the file changes.
873
961
  self.calculate_section_sizes()
874
962
 
875
963
  # Test if the terminal is of a sufficient size to display the picker
@@ -887,28 +975,18 @@ class Picker:
887
975
  end_index = min(start_index + self.items_per_page, len(self.indexed_items))
888
976
  if len(self.indexed_items) == 0: start_index, end_index = 0, 0
889
977
 
890
- # 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)
891
- # Determine widths based only on the currently indexed rows
892
- # rows = [v[1] for v in self.indexed_items] if len(self.indexed_items) else self.items
893
- # Determine widths based only on the currently displayed indexed rows
894
- # rows = [v[1] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else self.items
895
978
  self.get_visible_rows()
896
979
  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)
897
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
898
- visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
899
-
900
- # Determine the number of items_per_page, top_size and bottom_size
901
- # self.calculate_section_sizes()
980
+ self.visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
981
+ visible_columns_total_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
902
982
 
903
- # top_space = self.top_gap
904
983
 
905
- ## Display title (if applicable)
984
+ ## Display title
906
985
  if self.title:
907
986
  padded_title = f" {self.title.strip()} "
908
987
  self.stdscr.addstr(self.top_gap, 0, f"{' ':^{self.term_w}}", curses.color_pair(self.colours_start+16))
909
988
  title_x = (self.term_w-wcswidth(padded_title))//2
910
989
  self.stdscr.addstr(self.top_gap, title_x, padded_title, curses.color_pair(self.colours_start+16) | curses.A_BOLD)
911
- # top_space += 1
912
990
 
913
991
  ## Display modes
914
992
  if self.display_modes and self.modes not in [[{}], []]:
@@ -930,7 +1008,6 @@ class Picker:
930
1008
  else:
931
1009
  self.stdscr.addstr(self.top_gap+1, xmode, mode_str, curses.color_pair(self.colours_start+15) | curses.A_UNDERLINE)
932
1010
  xmode += split_space+mode_widths[i]
933
- # top_space += 1
934
1011
 
935
1012
  ## Display header
936
1013
  if self.header and self.show_header:
@@ -948,93 +1025,75 @@ class Picker:
948
1025
 
949
1026
 
950
1027
  header_str += f"{col_str:^{self.column_widths[i]-len(number)}}"
951
- header_str += self.header_separator
1028
+ if i == self.selected_column-1:
1029
+ header_str += self.header_separator_before_selected_column
1030
+ else:
1031
+ header_str += self.header_separator
952
1032
  header_str_w = min(self.rows_w-self.left_gutter_width, visible_columns_total_width+1, self.term_w-self.startx)
953
1033
 
954
1034
  header_str = header_str[self.leftmost_char:]
955
1035
  header_str = header_str[:header_str_w]
956
1036
  header_ypos = self.top_gap + bool(self.title) + bool(self.display_modes and self.modes)
1037
+
1038
+ # Ensure that the full header width is filled--important if the header rows do not fill the terminal width
957
1039
  self.stdscr.addstr(header_ypos, self.rows_box_x_i, ' '*self.rows_w, curses.color_pair(self.colours_start+28) | curses.A_BOLD)
1040
+
1041
+ # Draw header string
958
1042
  self.stdscr.addstr(header_ypos, self.startx, header_str, curses.color_pair(self.colours_start+4) | curses.A_BOLD)
959
1043
 
960
1044
  # Highlight sort column
961
- try:
962
- if self.selected_column != None and self.selected_column not in self.hidden_columns:
963
- # start of string is on screen
964
- col_width = self.column_widths[self.selected_column]
965
- number = f"{self.selected_column}. " if self.number_columns else ""
966
- col_str = self.header[self.selected_column][:self.column_widths[self.selected_column]-len(number)]
967
- highlighted_col_str = (number+f"{col_str:^{self.column_widths[self.selected_column]-len(number)}}") + self.separator
968
-
969
- if len(self.column_widths) == 1:
970
- colour = curses.color_pair(self.colours_start+28) | curses.A_BOLD
971
- else:
972
- colour = curses.color_pair(self.colours_start+19) | curses.A_BOLD
973
- # Start of selected column is on the screen
974
- 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):
975
- x_pos = len(up_to_selected_col) - self.leftmost_char + self.startx
1045
+ if self.selected_column != None and self.selected_column not in self.hidden_columns:
1046
+ # start of string is on screen
1047
+ col_width = self.column_widths[self.selected_column]
1048
+ number = f"{self.selected_column}. " if self.number_columns else ""
1049
+ col_str = self.header[self.selected_column][:self.column_widths[self.selected_column]-len(number)]
1050
+ highlighted_col_str = (number+f"{col_str:^{self.column_widths[self.selected_column]-len(number)}}") + self.separator
1051
+
1052
+ if len(self.column_widths) == 1:
1053
+ colour = curses.color_pair(self.colours_start+28) | curses.A_BOLD
1054
+ else:
1055
+ colour = curses.color_pair(self.colours_start+19) | curses.A_BOLD
1056
+ # Start of selected column is on the screen
1057
+ 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):
1058
+ x_pos = len(up_to_selected_col) - self.leftmost_char + self.startx
976
1059
 
977
- # Whole cell of the selected column is on the screen
978
- if len(up_to_selected_col)+col_width - self.leftmost_char < self.rows_w-self.left_gutter_width:
979
- disp_str = highlighted_col_str
1060
+ # Whole cell of the selected column is on the screen
1061
+ if len(up_to_selected_col)+col_width - self.leftmost_char < self.rows_w-self.left_gutter_width:
1062
+ disp_str = highlighted_col_str
980
1063
 
981
- # Start of the cell is on the screen, but the end of the cell is not
982
- else:
983
- overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w - self.left_gutter_width)
984
- disp_str = highlighted_col_str[:-overflow]
985
- disp_str_w = min(len(disp_str), self.term_w-x_pos)
986
- disp_str = truncate_to_display_width(disp_str, disp_str_w, self.centre_in_cols, self.unicode_char_width)
987
-
988
- self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
989
- # Start of the cell is to the right of the screen
990
- elif self.leftmost_char+self.rows_w <= len(up_to_selected_col):
991
- pass
992
- # The end of the cell is on the screen, the start of the cell is not
993
- elif 0 <= len(up_to_selected_col)+col_width - self.leftmost_char <= self.rows_w :
994
- x_pos = self.startx
995
- beg = self.leftmost_char - len(up_to_selected_col)
996
- disp_str = highlighted_col_str[beg:]
997
- disp_str_w = min(len(disp_str), self.term_w-x_pos)
998
- disp_str = truncate_to_display_width(disp_str, disp_str_w, self.centre_in_cols, self.unicode_char_width)
999
- self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
1000
- # The middle of the cell is on the screen, the start and end of the cell are not
1001
- elif self.leftmost_char <= len(up_to_selected_col) + col_width//2 <= self.leftmost_char+self.rows_w:
1002
- beg = self.leftmost_char - len(up_to_selected_col)
1003
- overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w)
1004
- x_pos = self.startx
1005
- disp_str = highlighted_col_str[beg:-overflow]
1006
- disp_str_w = min(len(disp_str), self.term_w-x_pos)
1007
- disp_str = truncate_to_display_width(disp_str, disp_str_w, self.centre_in_cols, self.unicode_char_width)
1008
-
1009
- self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
1010
- # The cell is to the left of the screen
1064
+ # Start of the cell is on the screen, but the end of the cell is not
1011
1065
  else:
1012
- pass
1013
-
1014
- # elif self.leftmost_char:
1015
- # os.system(f"notify-send 'cell is to the right of the screen'")
1016
-
1017
- #
1018
- #
1019
- # if len(self.header) > 1 and (len(up_to_selected_col)-self.leftmost_char) < self.rows_w:
1020
- # number = f"{self.selected_column}. " if self.number_columns else ""
1021
- # # number = f"{intStringToExponentString(self.selected_column)}. " if self.number_columns else ""
1022
- # # self.startx + len(up_to_selected_col) - self.leftmost_char
1023
- # highlighed_col_startx = max(self.startx, self.startx + len(up_to_selected_col) - self.leftmost_char)
1024
- #
1025
- #
1026
- # col_str = self.header[self.selected_column][:self.column_widths[self.selected_column]-len(number)]
1027
- # highlighted_col_str = (number+f"{col_str:^{self.column_widths[self.selected_column]}}") + self.separator
1028
- # 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)
1029
- # if (highlighed_col_startx+len(highlighted_col_str)) > self.rows_w:
1030
- # end_of_highlighted_col_str = self.rows_w-(highlighed_col_startx+len(highlighted_col_str))
1031
- # else:
1032
- # end_of_highlighted_col_str = len(highlighted_col_str)
1033
- #
1034
- # start_of_highlighted_col_str = max(self.leftmost_char - len(up_to_selected_col), 0)
1035
- # 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)
1036
- except:
1037
- pass
1066
+ overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w - self.left_gutter_width)
1067
+ disp_str = highlighted_col_str[:-overflow]
1068
+ disp_str_w = min(len(disp_str), self.term_w-x_pos)
1069
+ disp_str = truncate_to_display_width(disp_str, disp_str_w, self.centre_in_cols, self.unicode_char_width)
1070
+
1071
+ self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
1072
+ # Start of the cell is to the right of the screen
1073
+ elif self.leftmost_char+self.rows_w <= len(up_to_selected_col):
1074
+ pass
1075
+ # The end of the cell is on the screen, the start of the cell is not
1076
+ elif 0 <= len(up_to_selected_col)+col_width - self.leftmost_char <= self.rows_w :
1077
+ x_pos = self.startx
1078
+ beg = self.leftmost_char - len(up_to_selected_col)
1079
+ disp_str = highlighted_col_str[beg:]
1080
+ disp_str_w = min(len(disp_str), self.term_w-x_pos)
1081
+ disp_str = truncate_to_display_width(disp_str, disp_str_w, self.centre_in_cols, self.unicode_char_width)
1082
+ self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
1083
+ # The middle of the cell is on the screen, the start and end of the cell are not
1084
+ elif self.leftmost_char <= len(up_to_selected_col) + col_width//2 <= self.leftmost_char+self.rows_w:
1085
+ beg = self.leftmost_char - len(up_to_selected_col)
1086
+ overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w)
1087
+ x_pos = self.startx
1088
+ disp_str = highlighted_col_str[beg:-overflow]
1089
+ disp_str_w = min(len(disp_str), self.term_w-x_pos)
1090
+ disp_str = truncate_to_display_width(disp_str, disp_str_w, self.centre_in_cols, self.unicode_char_width)
1091
+
1092
+ self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
1093
+
1094
+ # The cell is to the left of the focused part of the screen
1095
+ else:
1096
+ pass
1038
1097
 
1039
1098
  # Display row header
1040
1099
  if self.show_row_header:
@@ -1058,41 +1117,34 @@ class Picker:
1058
1117
  colour = curses.color_pair(self.colours_start+colour_pair_number) | curses.A_BOLD
1059
1118
  else:
1060
1119
  colour = curses.color_pair(self.colours_start+colour_pair_number)
1061
- try:
1062
- # Start of cell is on screen
1063
- if self.startx <= cell_pos <= self.rows_w+self.startx:
1064
- s = "max" if cell_max_width <= cell_width else "norm"
1065
- self.stdscr.addstr(y, cell_pos, (' '*cell_width)[:cell_max_width], colour)
1066
- if self.centre_in_cols:
1067
- cell_value = f"{self.indexed_items[row][1][col]:^{cell_width-len(self.separator)}}" + self.separator
1068
- else:
1069
- cell_value = self.indexed_items[row][1][col][:self.column_widths[col]] + self.separator
1070
- # cell_value = cell_value[:min(cell_width, cell_max_width)-len(self.separator)]
1071
- cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
1072
- # cell_value = cell_value + self.separator
1073
- # cell_value = cell_value
1074
- cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
1075
- if wcswidth(cell_value) + cell_pos > self.term_w:
1076
- cell_value = truncate_to_display_width(cell_value, self.term_w-cell_pos-10, self.centre_in_cols, self.unicode_char_width)
1077
-
1078
- self.stdscr.addstr(y, cell_pos, cell_value, colour)
1079
- # Part of the cell is on screen
1080
- elif self.startx <= cell_pos+cell_width and cell_pos <= (self.rows_w):
1081
- s = "max" if cell_max_width <= cell_width else "norm"
1082
- cell_start = self.startx - cell_pos
1083
- # self.stdscr.addstr(y, self.startx, ' '*(cell_width-cell_start), curses.color_pair(self.colours_start+colour_pair_number))
1084
- cell_value = self.indexed_items[row][1][col]
1085
- cell_value = f"{cell_value:^{self.column_widths[col]}}"
1086
-
1087
- cell_value = cell_value[cell_start:visible_column_widths[col]][:self.rows_w-self.left_gutter_width]
1088
- 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)
1089
- cell_value += self.separator
1090
- 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)
1091
- self.stdscr.addstr(y, self.startx, cell_value, colour)
1120
+ # Start of cell is on screen
1121
+ if self.startx <= cell_pos <= self.rows_w+self.startx:
1122
+ s = "max" if cell_max_width <= cell_width else "norm"
1123
+ self.stdscr.addstr(y, cell_pos, (' '*cell_width)[:cell_max_width], colour)
1124
+ if self.centre_in_cols:
1125
+ cell_value = f"{self.indexed_items[row][1][col]:^{cell_width-len(self.separator)}}" + self.separator
1092
1126
  else:
1093
- pass
1094
- # if colour_pair_number == 5:
1095
- except:
1127
+ cell_value = self.indexed_items[row][1][col][:self.column_widths[col]] + self.separator
1128
+ cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
1129
+ cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.centre_in_cols, self.unicode_char_width)
1130
+ if wcswidth(cell_value) + cell_pos > self.term_w:
1131
+ cell_value = truncate_to_display_width(cell_value, self.term_w-cell_pos-10, self.centre_in_cols, self.unicode_char_width)
1132
+
1133
+ self.stdscr.addstr(y, cell_pos, cell_value, colour)
1134
+
1135
+ # Part of the cell is on screen
1136
+ elif self.startx <= cell_pos+cell_width and cell_pos <= (self.rows_w):
1137
+ s = "max" if cell_max_width <= cell_width else "norm"
1138
+ cell_start = self.startx - cell_pos
1139
+ cell_value = self.indexed_items[row][1][col]
1140
+ cell_value = f"{cell_value:^{self.column_widths[col]}}"
1141
+
1142
+ cell_value = cell_value[cell_start:visible_column_widths[col]][:self.rows_w-self.left_gutter_width]
1143
+ 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)
1144
+ cell_value += self.separator
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
+ self.stdscr.addstr(y, self.startx, cell_value, colour)
1147
+ else:
1096
1148
  pass
1097
1149
 
1098
1150
 
@@ -1136,10 +1188,11 @@ class Picker:
1136
1188
  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)
1137
1189
  if not match: continue
1138
1190
  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)
1191
+ width = min(self.column_widths[highlight["field"]]-(field_start-self.leftmost_char), self.rows_w-self.left_gutter_width)
1139
1192
 
1140
1193
  ## We want to search the non-centred values but highlight the centred values.
1141
1194
  if self.centre_in_cols:
1142
- tmp = truncate_to_display_width(item[1][highlight["field"]], self.column_widths[highlight["field"]], self.centre_in_cols, self.unicode_char_width)
1195
+ tmp = truncate_to_display_width(item[1][highlight["field"]], width, self.centre_in_cols, self.unicode_char_width)
1143
1196
  field_start += (len(tmp) - len(tmp.lstrip()))
1144
1197
 
1145
1198
  highlight_start = field_start + match.start()
@@ -1150,7 +1203,7 @@ class Picker:
1150
1203
  continue
1151
1204
  highlight_start -= self.leftmost_char
1152
1205
  highlight_end -= self.leftmost_char
1153
- 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)
1206
+ 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)
1154
1207
  except:
1155
1208
  pass
1156
1209
 
@@ -1166,7 +1219,7 @@ class Picker:
1166
1219
  l0_highlights, l1_highlights, l2_highlights = sort_highlights(self.highlights)
1167
1220
 
1168
1221
 
1169
- row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
1222
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
1170
1223
  for idx in range(start_index, end_index):
1171
1224
  item = self.indexed_items[idx]
1172
1225
  y = idx - start_index + self.top_space
@@ -1186,7 +1239,7 @@ class Picker:
1186
1239
  # trunc_width = 0
1187
1240
 
1188
1241
 
1189
- trunc_width = min(self.rows_w-self.left_gutter_width, row_width, self.term_w - self.startx)
1242
+ trunc_width = max(0, min(self.rows_w-self.left_gutter_width, row_width, self.term_w - self.startx))
1190
1243
 
1191
1244
  row_str = truncate_to_display_width(row_str_left_adj, trunc_width, self.unicode_char_width)
1192
1245
  # row_str = truncate_to_display_width(row_str, min(w-self.startx, visible_columns_total_width))[self.leftmost_char:]
@@ -1197,7 +1250,7 @@ class Picker:
1197
1250
 
1198
1251
  ## Highlight column
1199
1252
  if self.crosshair_cursor:
1200
- highlight_cell(idx, self.selected_column, visible_column_widths, colour_pair_number=27, bold=False, y=y)
1253
+ highlight_cell(idx, self.selected_column, self.visible_column_widths, colour_pair_number=27, bold=False, y=y)
1201
1254
  if idx == self.cursor_pos:
1202
1255
  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))
1203
1256
 
@@ -1211,21 +1264,21 @@ class Picker:
1211
1264
  # self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
1212
1265
  if item[0] in self.selected_cells_by_row:
1213
1266
  for j in self.selected_cells_by_row[item[0]]:
1214
- highlight_cell(idx, j, visible_column_widths, colour_pair_number=25, bold=False, y=y)
1267
+ highlight_cell(idx, j, self.visible_column_widths, colour_pair_number=25, bold=False, y=y)
1215
1268
 
1216
1269
  # Visually selected
1217
1270
  if self.is_selecting:
1218
1271
  if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
1219
1272
  x_interval = range(min(self.start_selection_col, self.selected_column), max(self.start_selection_col, self.selected_column)+1)
1220
1273
  for col in x_interval:
1221
- highlight_cell(idx, col, visible_column_widths, colour_pair_number=25, bold=False, y=y)
1274
+ highlight_cell(idx, col, self.visible_column_widths, colour_pair_number=25, bold=False, y=y)
1222
1275
 
1223
1276
  # Visually deslected
1224
1277
  if self.is_deselecting:
1225
1278
  if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
1226
1279
  x_interval = range(min(self.start_selection_col, self.selected_column), max(self.start_selection_col, self.selected_column)+1)
1227
1280
  for col in x_interval:
1228
- highlight_cell(idx, col, visible_column_widths, colour_pair_number=26, bold=False,y=y)
1281
+ highlight_cell(idx, col, self.visible_column_widths, colour_pair_number=26, bold=False,y=y)
1229
1282
  # Higlight cursor row and selected rows
1230
1283
  elif self.highlight_full_row:
1231
1284
  if self.selections[item[0]]:
@@ -1242,16 +1295,30 @@ class Picker:
1242
1295
 
1243
1296
  # Highlight the cursor row and the first char of the selected rows.
1244
1297
  else:
1245
- if self.selections[item[0]]:
1246
- self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
1247
- # Visually selected
1248
- if self.is_selecting:
1249
- if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
1298
+ if self.selected_char:
1299
+ if self.selections[item[0]]:
1300
+ self.stdscr.addstr(y, max(self.startx-2,0), self.selected_char, curses.color_pair(self.colours_start+2))
1301
+ else:
1302
+ self.stdscr.addstr(y, max(self.startx-2,0), self.unselected_char, curses.color_pair(self.colours_start+2))
1303
+ # Visually selected
1304
+ if self.is_selecting:
1305
+ if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
1306
+ self.stdscr.addstr(y, max(self.startx-2,0), self.selecting_char, curses.color_pair(self.colours_start+2))
1307
+ # Visually deslected
1308
+ if self.is_deselecting:
1309
+ if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
1310
+ self.stdscr.addstr(y, max(self.startx-2,0), self.deselecting_char, curses.color_pair(self.colours_start+2))
1311
+ else:
1312
+ if self.selections[item[0]]:
1250
1313
  self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
1251
- # Visually deslected
1252
- if self.is_deselecting:
1253
- if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
1254
- self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+10))
1314
+ # Visually selected
1315
+ if self.is_selecting:
1316
+ if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
1317
+ self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
1318
+ # Visually deslected
1319
+ if self.is_deselecting:
1320
+ if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
1321
+ self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+10))
1255
1322
 
1256
1323
  if not self.highlights_hide:
1257
1324
  draw_highlights(l1_highlights, idx, y, item)
@@ -1261,7 +1328,7 @@ class Picker:
1261
1328
  # Draw cursor
1262
1329
  if idx == self.cursor_pos:
1263
1330
  if self.cell_cursor:
1264
- highlight_cell(idx, self.selected_column, visible_column_widths, colour_pair_number=5, bold=True, y=y)
1331
+ highlight_cell(idx, self.selected_column, self.visible_column_widths, colour_pair_number=5, bold=True, y=y)
1265
1332
  else:
1266
1333
  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)
1267
1334
 
@@ -1366,6 +1433,21 @@ class Picker:
1366
1433
  # 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
1367
1434
 
1368
1435
 
1436
+ def refresh_and_draw_screen(self):
1437
+ """
1438
+ Clears and refreshes the screen, restricts and unrestricts curses,
1439
+ ensures correct terminal settings, and then draws the screen.
1440
+ """
1441
+
1442
+ self.logger.info(f"key_function redraw_screen")
1443
+ self.stdscr.clear()
1444
+ self.stdscr.refresh()
1445
+ restrict_curses(self.stdscr)
1446
+ unrestrict_curses(self.stdscr)
1447
+ self.stdscr.clear()
1448
+ self.stdscr.refresh()
1449
+
1450
+ self.draw_screen()
1369
1451
 
1370
1452
  def infobox(self, stdscr: curses.window, message: str ="", title: str ="Infobox", colours_end: int = 0, duration: int = 4) -> curses.window:
1371
1453
  """ Display non-interactive infobox window. """
@@ -1421,116 +1503,126 @@ class Picker:
1421
1503
  self.logger.debug(f"function: get_function_data()")
1422
1504
  """ Returns a dict of the main variables needed to restore the state of list_pikcer. """
1423
1505
  function_data = {
1424
- "selections": self.selections,
1425
- "cell_selections": self.cell_selections,
1426
- "selected_cells_by_row": self.selected_cells_by_row,
1427
- "items_per_page": self.items_per_page,
1428
- "current_row": self.current_row,
1429
- "current_page": self.current_page,
1430
- "cursor_pos": self.cursor_pos,
1431
- "colours": self.colours,
1432
- "colour_theme_number": self.colour_theme_number,
1433
- "selected_column": self.selected_column,
1434
- "sort_column": self.sort_column,
1435
- "sort_method": self.sort_method,
1436
- "sort_reverse": self.sort_reverse,
1437
- "SORT_METHODS": self.SORT_METHODS,
1438
- "hidden_columns": self.hidden_columns,
1439
- "is_selecting": self.is_selecting,
1440
- "is_deselecting": self.is_deselecting,
1441
- "user_opts": self.user_opts,
1442
- "options_list": self.options_list,
1443
- "user_settings": self.user_settings,
1444
- "separator": self.separator,
1445
- "search_query": self.search_query,
1446
- "search_count": self.search_count,
1447
- "search_index": self.search_index,
1448
- "filter_query": self.filter_query,
1449
- "indexed_items": self.indexed_items,
1450
- "start_selection": self.start_selection,
1451
- "start_selection_col": self.start_selection_col,
1452
- "end_selection": self.end_selection,
1453
- "highlights": self.highlights,
1454
- "max_column_width": self.max_column_width,
1455
- "column_indices": self.column_indices,
1456
- "mode_index": self.mode_index,
1457
- "modes": self.modes,
1458
- "title": self.title,
1459
- "display_modes": self.display_modes,
1460
- "require_option": self.require_option,
1461
- "require_option_default": self.require_option_default,
1462
- "option_functions": self.option_functions,
1463
- "top_gap": self.top_gap,
1464
- "number_columns": self.number_columns,
1465
- "items": self.items,
1466
- "indexed_items": self.indexed_items,
1467
- "header": self.header,
1468
- "scroll_bar": self.scroll_bar,
1469
- "columns_sort_method": self.columns_sort_method,
1470
- "disabled_keys": self.disabled_keys,
1471
- "show_footer": self.show_footer,
1472
- "footer_string": self.footer_string,
1473
- "footer_string_auto_refresh": self.footer_string_auto_refresh,
1474
- "footer_string_refresh_function": self.footer_string_refresh_function,
1475
- "footer_timer": self.footer_timer,
1476
- "footer_style": self.footer_style,
1477
- "colours_start": self.colours_start,
1478
- "colours_end": self.colours_end,
1479
- "display_only": self.display_only,
1480
- "infobox_items": self.infobox_items,
1481
- "display_infobox": self.display_infobox,
1482
- "infobox_title": self.infobox_title,
1483
- "key_remappings": self.key_remappings,
1484
- "auto_refresh": self.auto_refresh,
1485
- "get_new_data": self.get_new_data,
1486
- "refresh_function": self.refresh_function,
1487
- "timer": self.timer,
1488
- "get_data_startup": self.get_data_startup,
1489
- "get_footer_string_startup": self.get_footer_string_startup,
1490
- "editable_columns": self.editable_columns,
1491
- "last_key": self.last_key,
1492
- "centre_in_terminal": self.centre_in_terminal,
1493
- "centre_in_terminal_vertical": self.centre_in_terminal_vertical,
1494
- "centre_in_cols": self.centre_in_cols,
1495
- "highlight_full_row": self.highlight_full_row,
1496
- "cell_cursor": self.cell_cursor,
1497
- "column_widths": self.column_widths,
1498
- "track_entries_upon_refresh": self.track_entries_upon_refresh,
1499
- "pin_cursor": self.pin_cursor,
1500
- "id_column": self.id_column,
1501
- "startup_notification": self.startup_notification,
1502
- "keys_dict": self.keys_dict,
1503
- "cancel_is_back": self.cancel_is_back,
1504
- "paginate": self.paginate,
1505
- "leftmost_char": self.leftmost_char,
1506
- "history_filter_and_search" : self.history_filter_and_search,
1507
- "history_pipes" : self.history_pipes,
1508
- "history_opts" : self.history_opts,
1509
- "history_edits" : self.history_edits,
1510
- "history_settings": self.history_settings,
1511
- "show_header": self.show_header,
1512
- "show_row_header": self.show_row_header,
1513
- "debug": self.debug,
1514
- "debug_level": self.debug_level,
1515
- "reset_colours": self.reset_colours,
1516
- "unicode_char_width": self.unicode_char_width,
1517
- "command_stack": self.command_stack,
1518
- "loaded_file": self.loaded_file,
1519
- "loaded_files": self.loaded_files,
1520
- "loaded_file_index": self.loaded_file_index,
1521
- "loaded_file_states": self.loaded_file_states,
1522
- "sheet_index": self.sheet_index,
1523
- "sheets": self.sheets,
1524
- "sheet_name": self.sheet_name,
1525
- "sheet_states": self.sheet_states,
1526
- "split_right": self.split_right,
1527
- "right_panes": self.right_panes,
1528
- "right_pane_index": self.right_pane_index,
1529
- "split_left": self.split_left,
1530
- "left_panes": self.left_panes,
1531
- "left_pane_index": self.left_pane_index,
1532
- "crosshair_cursor": self.crosshair_cursor,
1533
-
1506
+ "self": self,
1507
+ "selections": self.selections,
1508
+ "cell_selections": self.cell_selections,
1509
+ "selected_cells_by_row": self.selected_cells_by_row,
1510
+ "items_per_page": self.items_per_page,
1511
+ "current_row": self.current_row,
1512
+ "current_page": self.current_page,
1513
+ "cursor_pos": self.cursor_pos,
1514
+ "colours": self.colours,
1515
+ "colour_theme_number": self.colour_theme_number,
1516
+ "selected_column": self.selected_column,
1517
+ "sort_column": self.sort_column,
1518
+ "sort_method": self.sort_method,
1519
+ "sort_reverse": self.sort_reverse,
1520
+ "SORT_METHODS": self.SORT_METHODS,
1521
+ "hidden_columns": self.hidden_columns,
1522
+ "is_selecting": self.is_selecting,
1523
+ "is_deselecting": self.is_deselecting,
1524
+ "user_opts": self.user_opts,
1525
+ "options_list": self.options_list,
1526
+ "user_settings": self.user_settings,
1527
+ "separator": self.separator,
1528
+ "header_separator": self.header_separator,
1529
+ "header_separator_before_selected_column": self.header_separator_before_selected_column,
1530
+ "search_query": self.search_query,
1531
+ "search_count": self.search_count,
1532
+ "search_index": self.search_index,
1533
+ "filter_query": self.filter_query,
1534
+ "indexed_items": self.indexed_items,
1535
+ "start_selection": self.start_selection,
1536
+ "start_selection_col": self.start_selection_col,
1537
+ "end_selection": self.end_selection,
1538
+ "highlights": self.highlights,
1539
+ "max_column_width": self.max_column_width,
1540
+ "column_indices": self.column_indices,
1541
+ "mode_index": self.mode_index,
1542
+ "modes": self.modes,
1543
+ "title": self.title,
1544
+ "display_modes": self.display_modes,
1545
+ "require_option": self.require_option,
1546
+ "require_option_default": self.require_option_default,
1547
+ "option_functions": self.option_functions,
1548
+ "top_gap": self.top_gap,
1549
+ "number_columns": self.number_columns,
1550
+ "items": self.items,
1551
+ "indexed_items": self.indexed_items,
1552
+ "header": self.header,
1553
+ "scroll_bar": self.scroll_bar,
1554
+ "columns_sort_method": self.columns_sort_method,
1555
+ "disabled_keys": self.disabled_keys,
1556
+ "show_footer": self.show_footer,
1557
+ "footer_string": self.footer_string,
1558
+ "footer_string_auto_refresh": self.footer_string_auto_refresh,
1559
+ "footer_string_refresh_function": self.footer_string_refresh_function,
1560
+ "footer_timer": self.footer_timer,
1561
+ "footer_style": self.footer_style,
1562
+ "colours_start": self.colours_start,
1563
+ "colours_end": self.colours_end,
1564
+ "display_only": self.display_only,
1565
+ "infobox_items": self.infobox_items,
1566
+ "display_infobox": self.display_infobox,
1567
+ "infobox_title": self.infobox_title,
1568
+ "key_remappings": self.key_remappings,
1569
+ "auto_refresh": self.auto_refresh,
1570
+ "get_new_data": self.get_new_data,
1571
+ "refresh_function": self.refresh_function,
1572
+ "timer": self.timer,
1573
+ "get_data_startup": self.get_data_startup,
1574
+ "get_footer_string_startup": self.get_footer_string_startup,
1575
+ "editable_columns": self.editable_columns,
1576
+ "last_key": self.last_key,
1577
+ "centre_in_terminal": self.centre_in_terminal,
1578
+ "centre_in_terminal_vertical": self.centre_in_terminal_vertical,
1579
+ "centre_in_cols": self.centre_in_cols,
1580
+ "highlight_full_row": self.highlight_full_row,
1581
+ "cell_cursor": self.cell_cursor,
1582
+ "column_widths": self.column_widths,
1583
+ "track_entries_upon_refresh": self.track_entries_upon_refresh,
1584
+ "pin_cursor": self.pin_cursor,
1585
+ "id_column": self.id_column,
1586
+ "startup_notification": self.startup_notification,
1587
+ "keys_dict": self.keys_dict,
1588
+ "cancel_is_back": self.cancel_is_back,
1589
+ "paginate": self.paginate,
1590
+ "leftmost_char": self.leftmost_char,
1591
+ "history_filter_and_search" : self.history_filter_and_search,
1592
+ "history_pipes" : self.history_pipes,
1593
+ "history_opts" : self.history_opts,
1594
+ "history_edits" : self.history_edits,
1595
+ "history_settings": self.history_settings,
1596
+ "show_header": self.show_header,
1597
+ "show_row_header": self.show_row_header,
1598
+ "debug": self.debug,
1599
+ "debug_level": self.debug_level,
1600
+ "reset_colours": self.reset_colours,
1601
+ "unicode_char_width": self.unicode_char_width,
1602
+ "command_stack": self.command_stack,
1603
+ "loaded_file": self.loaded_file,
1604
+ "loaded_files": self.loaded_files,
1605
+ "loaded_file_index": self.loaded_file_index,
1606
+ "loaded_file_states": self.loaded_file_states,
1607
+ "sheet_index": self.sheet_index,
1608
+ "sheets": self.sheets,
1609
+ "sheet_name": self.sheet_name,
1610
+ "sheet_states": self.sheet_states,
1611
+ "split_right": self.split_right,
1612
+ "right_panes": self.right_panes,
1613
+ "right_pane_index": self.right_pane_index,
1614
+ "split_left": self.split_left,
1615
+ "left_panes": self.left_panes,
1616
+ "left_pane_index": self.left_pane_index,
1617
+ "crosshair_cursor": self.crosshair_cursor,
1618
+ "generate_data_for_hidden_columns": self.generate_data_for_hidden_columns,
1619
+ "thread_stop_event": self.thread_stop_event,
1620
+ "data_generation_queue": self.data_generation_queue,
1621
+ "process_manager": self.process_manager,
1622
+ "threads": self.threads,
1623
+ "processes": self.processes,
1624
+ "items_sync_loop_event": self.items_sync_loop_event,
1625
+ "items_sync_thread": self.items_sync_thread,
1534
1626
  }
1535
1627
  return function_data
1536
1628
 
@@ -1583,15 +1675,6 @@ class Picker:
1583
1675
  self.initialise_picker_state(reset_colours=reset_colours)
1584
1676
 
1585
1677
  self.initialise_variables()
1586
- # if "colour_theme_number" in function_data:
1587
- # global COLOURS_SET
1588
- # COLOURS_SET = False
1589
- # colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
1590
-
1591
- # if "items" in function_data: self.items = function_data["items"]
1592
- # if "header" in function_data: self.header = function_data["header"]
1593
- # self.indexed_items = function_data["indexed_items"] if "indexed_items" in function_data else []
1594
-
1595
1678
 
1596
1679
 
1597
1680
  def delete_entries(self) -> None:
@@ -1665,6 +1748,7 @@ class Picker:
1665
1748
  "split_left": False,
1666
1749
  "cell_cursor": False,
1667
1750
  "crosshair_cursor": False,
1751
+ "header_separator": " │",
1668
1752
  }
1669
1753
  while True:
1670
1754
  self.update_term_size()
@@ -1687,6 +1771,102 @@ class Picker:
1687
1771
  return {}, "", f
1688
1772
 
1689
1773
 
1774
+ def select_columns(
1775
+ self,
1776
+ stdscr: curses.window,
1777
+ # options: list[list[str]] =[],
1778
+ # title: str = "Choose option",
1779
+ # x:int=0,
1780
+ # y:int=0,
1781
+ # literal:bool=False,
1782
+ # colours_start:int=0,
1783
+ # header: list[str] = [],
1784
+ # require_option:list = [],
1785
+ # option_functions: list = [],
1786
+ ) -> Tuple[dict, str, dict]:
1787
+ """
1788
+ Display input field at x,y
1789
+
1790
+ ---Arguments
1791
+ stdscr: curses screen
1792
+ usrtxt (str): text to be edited by the user
1793
+ title (str): The text to be displayed at the start of the text option picker
1794
+ x (int): prompt begins at (x,y) in the screen given
1795
+ y (int): prompt begins at (x,y) in the screen given
1796
+ colours_start (bool): start index of curses init_pair.
1797
+
1798
+ ---Returns
1799
+ usrtxt, return_code
1800
+ usrtxt: the text inputted by the user
1801
+ return_code:
1802
+ 0: user hit escape
1803
+ 1: user hit return
1804
+ """
1805
+ self.logger.info(f"function: select_columns()")
1806
+
1807
+ cursor = 0
1808
+
1809
+ if self.header:
1810
+ columns = [s for i, s in enumerate(self.header)]
1811
+ else:
1812
+ columns = [f"" for i in range(len(self.column_widths))]
1813
+
1814
+ ## Column info variable
1815
+ columns_set = [[f"{i}", columns[i]] for i in range(len(self.column_widths))]
1816
+ header = ["#", "Column Name"]
1817
+
1818
+ selected = [False if i in self.hidden_columns else True for i in range(len(self.column_widths))]
1819
+ selected = {i: False if i in self.hidden_columns else True for i in range(len(self.column_widths))}
1820
+
1821
+ option_picker_data = {
1822
+ "items": columns_set,
1823
+ "colours": notification_colours,
1824
+ "colours_start": self.notification_colours_start,
1825
+ "title":"Select Columns",
1826
+ "header": header,
1827
+ "hidden_columns":[],
1828
+ # "require_option":require_option,
1829
+ # "keys_dict": options_keys,
1830
+ "selections": selected,
1831
+ "show_footer": False,
1832
+ "cancel_is_back": True,
1833
+ "number_columns": False,
1834
+ "reset_colours": False,
1835
+ "split_right": False,
1836
+ "split_left": False,
1837
+ "cell_cursor": False,
1838
+ "crosshair_cursor": False,
1839
+ "separator": " ",
1840
+ "header_separator": " │",
1841
+ "header_separator_before_selected_column": " ▐",
1842
+ "selected_char": "☒",
1843
+ "unselected_char": "☐",
1844
+ "selecting_char": "☒",
1845
+ "deselecting_char": "☐",
1846
+ }
1847
+ while True:
1848
+ self.update_term_size()
1849
+
1850
+ choose_opts_widths = get_column_widths(columns_set, unicode_char_width=self.unicode_char_width)
1851
+ window_width = min(max(sum(choose_opts_widths) + 6, 50) + 6, self.term_w)
1852
+ window_height = min(self.term_h//2, max(6, len(columns_set)+3))
1853
+
1854
+ submenu_win = curses.newwin(window_height, window_width, (self.term_h-window_height)//2, (self.term_w-window_width)//2)
1855
+ submenu_win.keypad(True)
1856
+ option_picker_data["screen_size_function"] = lambda stdscr: (window_height, window_width)
1857
+ OptionPicker = Picker(submenu_win, **option_picker_data)
1858
+ s, o, f = OptionPicker.run()
1859
+
1860
+ if o == "refresh":
1861
+ self.draw_screen()
1862
+ continue
1863
+ if s:
1864
+ selected_columns = s
1865
+ self.hidden_columns = [i for i in range(len(self.column_widths)) if i not in selected_columns]
1866
+
1867
+ # return {x: options[x] for x in s}, o, f
1868
+ break
1869
+ return {}, "", f
1690
1870
 
1691
1871
  def notification(self, stdscr: curses.window, message: str="", title:str="Notification", colours_end: int=0, duration:int=4) -> None:
1692
1872
 
@@ -1729,7 +1909,6 @@ class Picker:
1729
1909
  "crosshair_cursor": False,
1730
1910
  "show_header": False,
1731
1911
  "screen_size_function": lambda stdscr: (notification_height, notification_width),
1732
-
1733
1912
  }
1734
1913
  OptionPicker = Picker(submenu_win, **notification_data)
1735
1914
  s, o, f = OptionPicker.run()
@@ -1933,6 +2112,9 @@ class Picker:
1933
2112
  self.draw_screen()
1934
2113
  self.notification(self.stdscr, message=f"Theme {self.colour_theme_number} applied.")
1935
2114
  self.colours = get_colours(self.colour_theme_number)
2115
+ elif setting == "colsel":
2116
+ self.draw_screen()
2117
+ self.select_columns(self.stdscr)
1936
2118
 
1937
2119
  else:
1938
2120
  self.user_settings = ""
@@ -2298,7 +2480,13 @@ class Picker:
2298
2480
  self.logger.info(f"function: fetch_data()")
2299
2481
  tmp_items, tmp_header = [], []
2300
2482
  self.getting_data.clear()
2301
- self.refresh_function(tmp_items, tmp_header, self.visible_rows_indices, self.getting_data)
2483
+ self.refresh_function(
2484
+ tmp_items,
2485
+ tmp_header,
2486
+ self.visible_rows_indices,
2487
+ self.getting_data,
2488
+ self.get_function_data(),
2489
+ )
2302
2490
  if self.track_entries_upon_refresh:
2303
2491
  selected_indices = get_selected_indices(self.selections)
2304
2492
  self.ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
@@ -2396,10 +2584,6 @@ class Picker:
2396
2584
  row_len = 1
2397
2585
  if self.header: row_len = len(self.header)
2398
2586
  elif len(self.items): row_len = len(self.items[0])
2399
- # if len(self.indexed_items) == 0:
2400
- # insert_at_pos = 0
2401
- # else:
2402
- # insert_at_pos = self.indexed_items[self.cursor_pos][0]
2403
2587
  self.items = self.items[:pos] + [["" for x in range(row_len)]] + self.items[pos:]
2404
2588
  if pos <= self.cursor_pos:
2405
2589
  self.cursor_pos += 1
@@ -2549,18 +2733,45 @@ class Picker:
2549
2733
  max_total_width=self.rows_w,
2550
2734
  unicode_char_width=self.unicode_char_width
2551
2735
  )
2736
+ self.calculate_section_sizes()
2552
2737
 
2553
- row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
2738
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
2554
2739
  if row_width - self.leftmost_char < self.rows_w:
2555
2740
  if row_width <= self.rows_w - self.left_gutter_width:
2556
2741
  self.leftmost_char = 0
2557
2742
  else:
2558
2743
  self.leftmost_char = row_width - (self.rows_w - self.left_gutter_width) + 5
2559
2744
 
2745
+ def cleanup_processes(self):
2746
+ self.thread_stop_event.set()
2747
+ self.data_generation_queue.clear()
2748
+ # with self.data_generation_queue.mutex:
2749
+ # self.data_generation_queue.queue.clear()
2750
+ function_data = self.get_function_data()
2751
+ for proc in self.processes:
2752
+ if proc.is_alive():
2753
+ proc.terminate()
2754
+ proc.join(timeout=0.01)
2755
+ self.processes = []
2756
+ self.items_sync_loop_event.set()
2757
+ if self.items_sync_thread != None:
2758
+ self.items_sync_thread.join(timeout=1)
2759
+
2760
+ def cleanup_threads(self):
2761
+ self.thread_stop_event.set()
2762
+ with self.data_generation_queue.mutex:
2763
+ self.data_generation_queue.queue.clear()
2764
+ function_data = self.get_function_data()
2765
+ for t in self.threads:
2766
+ if t.is_alive():
2767
+ t.join(timeout=0.01)
2768
+
2560
2769
  def run(self) -> Tuple[list[int], str, dict]:
2561
2770
  """ Run the picker. """
2562
2771
  self.logger.info(f"function: run()")
2563
2772
 
2773
+ self.thread_stop_event.clear()
2774
+
2564
2775
  if self.get_footer_string_startup and self.footer_string_refresh_function != None:
2565
2776
  self.footer_string = " "
2566
2777
  self.footer.adjust_sizes(self.term_h, self.term_w)
@@ -2665,9 +2876,12 @@ class Picker:
2665
2876
 
2666
2877
  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):
2667
2878
  self.logger.debug(f"Get new data (refresh).")
2668
- self.stdscr.addstr(0,self.term_w-3,"  ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
2879
+ try:
2880
+ self.stdscr.addstr(0,self.term_w-3,"  ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
2881
+ except:
2882
+ pass
2669
2883
  self.stdscr.refresh()
2670
- if self.get_new_data and self.refresh_function:
2884
+ if self.get_new_data:
2671
2885
  self.refreshing_data = True
2672
2886
 
2673
2887
  t = threading.Thread(target=self.fetch_data)
@@ -2810,6 +3024,14 @@ class Picker:
2810
3024
  data["option_functions"] = f"[...] length = {len(data['option_functions'])}"
2811
3025
  data["loaded_file_states"] = f"[...] length = {len(data['loaded_file_states'])}"
2812
3026
  data["sheet_states"] = f"[...] length = {len(data['sheet_states'])}"
3027
+ data["highlights"] = f"[...] length = {len(data['highlights'])}"
3028
+ data["colours"] = f"[...] length = {len(data['colours'])}"
3029
+ data["keys_dict"] = f"[...] length = {len(data['keys_dict'])}"
3030
+ data["history_filter_and_search"] = f"[...] length = {len(data['history_filter_and_search'])}"
3031
+ data["history_opts"] = f"[...] length = {len(data['history_opts'])}"
3032
+ data["history_edits"] = f"[...] length = {len(data['history_edits'])}"
3033
+ data["history_pipes"] = f"[...] length = {len(data['history_pipes'])}"
3034
+ data["history_settings"] = f"[...] length = {len(data['history_settings'])}"
2813
3035
  info_items += [
2814
3036
  ["",""],
2815
3037
  [" get_function_data()", "-*"*30],
@@ -2855,6 +3077,7 @@ class Picker:
2855
3077
  elif self.check_key("exit", key, self.keys_dict):
2856
3078
  self.stdscr.clear()
2857
3079
  if len(self.loaded_files) <= 1:
3080
+ self.cleanup_threads()
2858
3081
  function_data = self.get_function_data()
2859
3082
  restore_terminal_settings(tty_fd, self.saved_terminal_state)
2860
3083
  return [], "", function_data
@@ -2876,6 +3099,7 @@ class Picker:
2876
3099
  self.draw_screen()
2877
3100
 
2878
3101
  elif self.check_key("full_exit", key, self.keys_dict):
3102
+ self.cleanup_threads()
2879
3103
  close_curses(self.stdscr)
2880
3104
  restore_terminal_settings(tty_fd, self.saved_terminal_state)
2881
3105
  exit()
@@ -2917,11 +3141,12 @@ class Picker:
2917
3141
  options = []
2918
3142
  if len(self.items) > 0:
2919
3143
  options += [["cv", "Centre rows vertically"]]
2920
- options += [["pc", "Pin cursor to row number when data refreshes"]]
3144
+ options += [["pc", "Pin cursor to row index during data refresh."]]
2921
3145
  options += [["ct", "Centre column-set in terminal"]]
2922
3146
  options += [["cc", "Centre values in cells"]]
2923
3147
  options += [["!r", "Toggle auto-refresh"]]
2924
3148
  options += [["th", "Cycle between themes. (accepts th#)"]]
3149
+ options += [["colsel", "Toggle columns."]]
2925
3150
  options += [["nohl", "Toggle highlights"]]
2926
3151
  options += [["footer", "Toggle footer"]]
2927
3152
  options += [["header", "Toggle header"]]
@@ -3052,16 +3277,13 @@ class Picker:
3052
3277
  self.cursor_pos = new_pos
3053
3278
  self.ensure_no_overscroll()
3054
3279
  self.draw_screen()
3055
- # current_row = items_per_page - 1
3056
- # if current_page + 1 == (len(self.indexed_items) + items_per_page - 1) // items_per_page:
3057
- #
3058
- # current_row = (len(self.indexed_items) +items_per_page - 1) % items_per_page
3059
- # self.draw_screen()
3280
+
3060
3281
  elif self.check_key("enter", key, self.keys_dict):
3061
3282
  self.logger.info(f"key_function enter")
3062
3283
  # Print the selected indices if any, otherwise print the current index
3063
3284
  if self.is_selecting or self.is_deselecting: self.handle_visual_selection()
3064
3285
  if len(self.items) == 0:
3286
+ self.cleanup_threads()
3065
3287
  function_data = self.get_function_data()
3066
3288
  restore_terminal_settings(tty_fd, self.saved_terminal_state)
3067
3289
  return [], "", function_data
@@ -3088,6 +3310,7 @@ class Picker:
3088
3310
  )
3089
3311
 
3090
3312
  if options_sufficient:
3313
+ self.cleanup_threads()
3091
3314
  self.user_opts = usrtxt
3092
3315
  self.stdscr.clear()
3093
3316
  self.stdscr.refresh()
@@ -3101,15 +3324,7 @@ class Picker:
3101
3324
  self.cursor_pos = max(0, self.cursor_pos-self.items_per_page)
3102
3325
 
3103
3326
  elif self.check_key("redraw_screen", key, self.keys_dict):
3104
- self.logger.info(f"key_function redraw_screen")
3105
- self.stdscr.clear()
3106
- self.stdscr.refresh()
3107
- restrict_curses(self.stdscr)
3108
- unrestrict_curses(self.stdscr)
3109
- self.stdscr.clear()
3110
- self.stdscr.refresh()
3111
-
3112
- self.draw_screen()
3327
+ self.refresh_and_draw_screen()
3113
3328
 
3114
3329
  elif self.check_key("cycle_sort_method", key, self.keys_dict):
3115
3330
  if self.sort_column == self.selected_column:
@@ -3150,22 +3365,28 @@ class Picker:
3150
3365
  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
3151
3366
  self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
3152
3367
  elif self.check_key("col_select_next", key, self.keys_dict):
3153
- if len(self.items) > 0 and len(self.items[0]) > 0:
3154
- col_index = (self.selected_column +1) % (len(self.items[0]))
3155
- self.selected_column = col_index
3156
- # Flash when we loop back to the first column
3157
- # if self.selected_column == 0:
3158
- # curses.flash()
3159
3368
  self.logger.info(f"key_function col_select_next {self.selected_column}")
3369
+ if len(self.hidden_columns) != len(self.column_widths):
3370
+ if len(self.items) > 0 and len(self.items[0]) > 0:
3371
+ while True:
3372
+ self.hidden_columns
3373
+ col_index = (self.selected_column +1) % (len(self.items[0]))
3374
+ self.selected_column = col_index
3375
+ if self.selected_column not in self.hidden_columns:
3376
+ break
3377
+
3378
+ # Flash when we loop back to the first column
3379
+ # if self.selected_column == 0:
3380
+ # curses.flash()
3160
3381
 
3161
3382
 
3162
3383
  ## Scroll with column select
3163
3384
  self.get_visible_rows()
3164
3385
  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)
3165
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
3166
- column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
3167
- start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
3168
- end_of_cell = sum(visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
3386
+ self.visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
3387
+ column_set_width = sum(self.visible_column_widths)+len(self.separator)*len(self.visible_column_widths)
3388
+ start_of_cell = sum(self.visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
3389
+ end_of_cell = sum(self.visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
3169
3390
  display_width = self.rows_w-self.left_gutter_width
3170
3391
  # If the full column is within the current display then don't do anything
3171
3392
  if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
@@ -3178,11 +3399,17 @@ class Picker:
3178
3399
  self.ensure_no_overscroll()
3179
3400
 
3180
3401
  elif self.check_key("col_select_prev", key, self.keys_dict):
3181
- if len(self.items) > 0 and len(self.items[0]) > 0:
3182
- col_index = (self.selected_column -1) % (len(self.items[0]))
3183
- self.selected_column = col_index
3184
-
3185
3402
  self.logger.info(f"key_function col_select_prev {self.selected_column}")
3403
+
3404
+ if len(self.hidden_columns) != len(self.column_widths):
3405
+ if len(self.items) > 0 and len(self.items[0]) > 0:
3406
+ while True:
3407
+ self.hidden_columns
3408
+ col_index = (self.selected_column -1) % (len(self.items[0]))
3409
+ self.selected_column = col_index
3410
+ if self.selected_column not in self.hidden_columns:
3411
+ break
3412
+
3186
3413
  # Flash when we loop back to the last column
3187
3414
  # if self.selected_column == len(self.column_widths)-1:
3188
3415
  # curses.flash()
@@ -3190,10 +3417,10 @@ class Picker:
3190
3417
  ## Scroll with column select
3191
3418
  self.get_visible_rows()
3192
3419
  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)
3193
- visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
3194
- column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
3195
- start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
3196
- end_of_cell = sum(visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
3420
+ self.visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
3421
+ column_set_width = sum(self.visible_column_widths)+len(self.separator)*len(self.visible_column_widths)
3422
+ start_of_cell = sum(self.visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
3423
+ end_of_cell = sum(self.visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
3197
3424
  display_width = self.rows_w-self.left_gutter_width
3198
3425
 
3199
3426
  # If the entire column is within the current display then don't do anything
@@ -3209,21 +3436,21 @@ class Picker:
3209
3436
  elif self.check_key("scroll_right", key, self.keys_dict):
3210
3437
  self.logger.info(f"key_function scroll_right")
3211
3438
  if len(self.indexed_items):
3212
- row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
3439
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
3213
3440
  if row_width-self.leftmost_char >= self.rows_w-5:
3214
3441
  self.leftmost_char += 5
3215
3442
  self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w) + self.left_gutter_width+5)
3216
- if sum(self.column_widths) + len(self.column_widths)*len(self.separator) < self.rows_w:
3443
+ if sum(self.visible_column_widths) + len(self.visible_column_widths)*len(self.separator) < self.rows_w:
3217
3444
  self.leftmost_char = 0
3218
3445
 
3219
3446
  elif self.check_key("scroll_right_25", key, self.keys_dict):
3220
3447
  self.logger.info(f"key_function scroll_right")
3221
3448
  if len(self.indexed_items):
3222
- row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
3449
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
3223
3450
  if row_width-self.leftmost_char+5 >= self.rows_w-25:
3224
3451
  self.leftmost_char += 25
3225
3452
  self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w) + self.left_gutter_width+5)
3226
- if sum(self.column_widths) + len(self.column_widths)*len(self.separator) < self.rows_w:
3453
+ if sum(self.visible_column_widths) + len(self.visible_column_widths)*len(self.separator) < self.rows_w:
3227
3454
  self.leftmost_char = 0
3228
3455
 
3229
3456
  elif self.check_key("scroll_left", key, self.keys_dict):
@@ -3242,24 +3469,13 @@ class Picker:
3242
3469
  elif self.check_key("scroll_far_right", key, self.keys_dict):
3243
3470
  self.logger.info(f"key_function scroll_far_right")
3244
3471
  longest_row_str_len = 0
3245
- # rows = self.get_visible_rows()
3246
- # for i in range(len(rows)):
3247
- # item = rows[i]
3248
- # row_str = format_row(item, self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
3249
- # if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
3250
- longest_row_str_len = sum(self.column_widths) + (len(self.column_widths)-1)*len(self.separator)
3251
- # for i in range(len(self.indexed_items)):
3252
- # item = self.indexed_items[i]
3253
- # row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)
3254
- # if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
3255
- # self.notification(self.stdscr, f"{longest_row_str_len}")
3472
+ longest_row_str_len = sum(self.visible_column_widths) + (len(self.visible_column_widths)-1)*len(self.separator)
3256
3473
  if len(self.indexed_items):
3257
- row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
3474
+ row_width = sum(self.visible_column_widths) + len(self.separator)*(len(self.visible_column_widths)-1)
3258
3475
  self.leftmost_char = row_width - (self.rows_w) + self.left_gutter_width+5
3259
3476
  self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w) + self.left_gutter_width+5)
3260
3477
 
3261
- longest_row_str_len = sum(self.column_widths) + (len(self.column_widths)-1)*len(self.separator)
3262
- # self.leftmost_char = max(0, longest_row_str_len-self.rows_w+2+5)
3478
+ longest_row_str_len = sum(self.visible_column_widths) + (len(self.visible_column_widths)-1)*len(self.separator)
3263
3479
 
3264
3480
 
3265
3481
 
@@ -3321,16 +3537,6 @@ class Picker:
3321
3537
  self.selected_column = min(self.selected_column, row_len-2)
3322
3538
  self.initialise_variables()
3323
3539
 
3324
-
3325
-
3326
-
3327
- # elif self.check_key("increase_lines_per_page", key, self.keys_dict):
3328
- # self.items_per_page += 1
3329
- # self.draw_screen()
3330
- # elif self.check_key("decrease_lines_per_page", key, self.keys_dict):
3331
- # if self.items_per_page > 1:
3332
- # self.items_per_page -= 1
3333
- # self.draw_screen()
3334
3540
  elif self.check_key("decrease_column_width", key, self.keys_dict):
3335
3541
  self.logger.info(f"key_function decrease_column_width")
3336
3542
  if self.max_column_width > 10:
@@ -3512,12 +3718,6 @@ class Picker:
3512
3718
  function_data = self.get_function_data()
3513
3719
  return [], "escape", function_data
3514
3720
 
3515
-
3516
- # else:
3517
- # self.search_query = ""
3518
- # self.mode_index = 0
3519
- # self.highlights = [highlight for highlight in self.highlights if "type" not in highlight or highlight["type"] != "search" ]
3520
- # continue
3521
3721
  self.draw_screen()
3522
3722
 
3523
3723
  elif self.check_key("opts_input", key, self.keys_dict):
@@ -3733,7 +3933,7 @@ class Picker:
3733
3933
 
3734
3934
  elif self.check_key("edit", key, self.keys_dict):
3735
3935
  self.logger.info(f"key_function edit")
3736
- if len(self.indexed_items) > 0 and self.selected_column >=0 and self.editable_columns[self.selected_column]:
3936
+ if len(self.indexed_items) > 0 and self.editable_columns[self.selected_column]:
3737
3937
  current_val = self.indexed_items[self.cursor_pos][1][self.selected_column]
3738
3938
  usrtxt = f"{current_val}"
3739
3939
  field_end_f = lambda: self.get_term_size()[1]-38 if self.show_footer else lambda: self.get_term_size()[1]-3
@@ -3762,6 +3962,47 @@ class Picker:
3762
3962
  usrtxt = str(eval(usrtxt[3:]))
3763
3963
  self.indexed_items[self.cursor_pos][1][self.selected_column] = usrtxt
3764
3964
  self.history_edits.append(usrtxt)
3965
+ elif self.check_key("edit_nvim", key, self.keys_dict):
3966
+
3967
+ def edit_strings_in_nvim(strings: list[str]) -> list[str]:
3968
+ """
3969
+ Opens a list of strings in nvim for editing and returns the modified strings.
3970
+
3971
+ Args:
3972
+ strings (list[str]): The list of strings to edit.
3973
+
3974
+ Returns:
3975
+ list[str]: The updated list of strings after editing in nvim.
3976
+ """
3977
+
3978
+ # Open the strings in a tmpfile for editing
3979
+ with tempfile.NamedTemporaryFile(mode="w+", suffix=".txt", delete=False) as tmp:
3980
+ tmp.write("\n".join(strings))
3981
+ tmp.flush()
3982
+ tmp_name = tmp.name
3983
+
3984
+ subprocess.run(["nvim", tmp_name])
3985
+
3986
+ # Read the modified strings into a list and return them.
3987
+ with open(tmp_name, "r") as tmp:
3988
+ edited_content = tmp.read().splitlines()
3989
+
3990
+ return edited_content
3991
+
3992
+ if len(self.indexed_items) > 0 and self.editable_columns[self.selected_column]:
3993
+
3994
+ selected_cells = [self.items[index][self.selected_column] for index, selected in self.selections.items() if selected ]
3995
+ selected_cells_indices = [(index, self.selected_column) for index, selected in self.selections.items() if selected ]
3996
+
3997
+ edited_cells = edit_strings_in_nvim(selected_cells)
3998
+ count = 0
3999
+ if len(edited_cells) == len(selected_cells_indices):
4000
+ for i, j in selected_cells_indices:
4001
+ self.items[i][j] = edited_cells[count]
4002
+ count += 1
4003
+
4004
+ self.refresh_and_draw_screen()
4005
+
3765
4006
 
3766
4007
  elif self.check_key("edit_picker", key, self.keys_dict):
3767
4008
  self.logger.info(f"key_function edit_picker")
@@ -3793,7 +4034,7 @@ class Picker:
3793
4034
  self.indexed_items[self.cursor_pos][1][self.selected_column] = usrtxt
3794
4035
  self.history_edits.append(usrtxt)
3795
4036
  elif self.check_key("edit_ipython", key, self.keys_dict):
3796
- self.logger.info(f"key_function edit_picker")
4037
+ self.logger.info(f"key_function edit_ipython")
3797
4038
  import IPython, termios
3798
4039
  self.stdscr.clear()
3799
4040
  restrict_curses(self.stdscr)
@@ -3937,7 +4178,7 @@ def parse_arguments() -> Tuple[argparse.Namespace, dict]:
3937
4178
  # input_arg = args.filename
3938
4179
 
3939
4180
  elif args.generate:
3940
- 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)
4181
+ 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)
3941
4182
  function_data["get_data_startup"] = True
3942
4183
  function_data["get_new_data"] = True
3943
4184
  return args, function_data
@@ -4197,7 +4438,7 @@ def main() -> None:
4197
4438
  # function_data["debug"] = True
4198
4439
  # function_data["debug_level"] = 1
4199
4440
 
4200
- function_data["cell_cursor"] = False
4441
+ # function_data["cell_cursor"] = False
4201
4442
 
4202
4443
  function_data["split_right"] = False
4203
4444
  function_data["split_left"] = False