listpick 0.1.16.8__py3-none-any.whl → 0.1.16.10__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
@@ -20,8 +20,10 @@ import json
20
20
  import threading
21
21
  import string
22
22
  import logging
23
+ from copy import copy
23
24
 
24
25
  from listpick.pane.pane_utils import get_file_attributes
26
+ from listpick.pane.left_pane_functions import *
25
27
  from listpick.ui.picker_colours import get_colours, get_help_colours, get_notification_colours, get_theme_count, get_fallback_colours
26
28
  from listpick.utils.options_selectors import default_option_input, output_file_option_selector, default_option_selector
27
29
  from listpick.utils.table_to_list_of_lists import *
@@ -94,6 +96,7 @@ class Picker:
94
96
  options_list: list[str] = [],
95
97
  user_settings : str = "",
96
98
  separator : str = " ",
99
+ header_separator : str = " │",
97
100
  search_query : str = "",
98
101
  search_count : int = 0,
99
102
  search_index : int = 0,
@@ -189,9 +192,17 @@ class Picker:
189
192
  right_panes: list = [],
190
193
  right_pane_index: int = 0,
191
194
 
195
+ split_left: bool = False,
196
+ left_panes: list = [],
197
+ left_pane_index: int = 0,
198
+
199
+ screen_size_function = lambda stdscr: os.get_terminal_size()[::-1],
200
+
192
201
  # getting_data: threading.Event = threading.Event(),
193
202
 
194
203
  ):
204
+
205
+ self.screen_size_function = screen_size_function
195
206
  self.stdscr = stdscr
196
207
  self.items = items
197
208
  self.cursor_pos = cursor_pos
@@ -233,6 +244,7 @@ class Picker:
233
244
  self.options_list = options_list
234
245
  self.user_settings = user_settings
235
246
  self.separator = separator
247
+ self.header_separator = header_separator
236
248
  self.search_query = search_query
237
249
  self.search_count = search_count
238
250
  self.search_index = search_index
@@ -342,6 +354,11 @@ class Picker:
342
354
  self.split_right = split_right
343
355
  self.right_panes = right_panes
344
356
  self.right_pane_index = right_pane_index
357
+
358
+ self.split_left = split_left
359
+ self.left_panes = left_panes
360
+ self.left_pane_index = left_pane_index
361
+
345
362
  self.visible_rows_indices = []
346
363
 
347
364
 
@@ -408,7 +425,10 @@ class Picker:
408
425
  return config
409
426
 
410
427
  def update_term_size(self):
411
- self.term_h, self.term_w = self.stdscr.getmaxyx()
428
+ self.term_h, self.term_w = self.screen_size_function(self.stdscr)
429
+ # self.term_h, self.term_w = self.stdscr.getmaxyx()
430
+ # self.term_w, self.term_h = os.get_terminal_size()
431
+
412
432
 
413
433
  def get_term_size(self):
414
434
  return self.stdscr.getmaxyx()
@@ -428,13 +448,35 @@ class Picker:
428
448
  # self.bottom_space
429
449
  self.bottom_space = self.footer.height if self.show_footer else 0
430
450
 
451
+ # self.left_gutter_width
452
+ self.left_gutter_width = 1 if self.highlight_full_row else 2
453
+ if self.show_row_header: self.left_gutter_width += len(str(len(self.items))) + 2
454
+
455
+
431
456
  ## self.top_space
432
457
  self.update_term_size()
458
+ self.rows_w, self.rows_h = self.term_w, self.term_h
459
+ self.rows_box_x_i = 0
460
+ self.rows_box_x_f = self.term_w
461
+ self.left_pane_width = self.right_pane_width = 0
433
462
  if self.split_right and len(self.right_panes):
434
463
  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
464
+ self.right_pane_width = int(self.term_w*proportion)
465
+ self.rows_w -= self.right_pane_width
466
+ self.rows_box_x_f -= self.right_pane_width
467
+ if self.split_left and len(self.left_panes):
468
+ proportion = self.left_panes[self.left_pane_index]["proportion"]
469
+ self.left_pane_width = int(self.term_w*proportion)
470
+ self.rows_w -= self.left_pane_width
471
+ self.rows_box_x_i += self.left_pane_width
472
+ if self.left_pane_width + self.right_pane_width >= self.term_w-3:
473
+ self.rows_w += 10
474
+ self.left_pane_width -= 5
475
+ self.right_pane_width -= 5
476
+ self.rows_box_x_i -= 5
477
+ self.rows_box_x_f += 5
478
+
479
+
438
480
 
439
481
  self.top_space = self.top_gap
440
482
  if self.title: self.top_space+=1
@@ -460,6 +502,14 @@ class Picker:
460
502
  if self.show_row_header: self.startx += len(str(len(self.items))) + 2
461
503
  if visible_columns_total_width < self.rows_w and self.centre_in_terminal:
462
504
  self.startx += (self.rows_w - visible_columns_total_width) // 2
505
+ self.startx += self.left_pane_width
506
+ # if self.split_left and len(self.left_panes):
507
+ # proportion = self.left_panes[self.left_pane_index]["proportion"]
508
+ # self.startx += int(self.term_w*proportion)
509
+
510
+ self.endx = self.startx+self.rows_w
511
+
512
+
463
513
 
464
514
  def get_visible_rows(self) -> list[list[str]]:
465
515
 
@@ -800,7 +850,8 @@ class Picker:
800
850
  self.draw_screen_(clear)
801
851
  except Exception as e:
802
852
  self.logger.warning(f"self.draw_screen_() error. {e}")
803
- pass
853
+ finally:
854
+ self.stdscr.refresh()
804
855
 
805
856
  def draw_screen_(self, clear: bool = True) -> None:
806
857
  """ Draw Picker screen. """
@@ -811,11 +862,11 @@ class Picker:
811
862
  self.stdscr.erase()
812
863
 
813
864
  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
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
819
870
 
820
871
  # The height of the footer may need to be adjusted if the file changes.
821
872
  self.footer.adjust_sizes(self.term_h,self.term_w)
@@ -897,12 +948,14 @@ class Picker:
897
948
 
898
949
 
899
950
  header_str += f"{col_str:^{self.column_widths[i]-len(number)}}"
900
- header_str += self.separator
951
+ header_str += self.header_separator
952
+ header_str_w = min(self.rows_w-self.left_gutter_width, visible_columns_total_width+1, self.term_w-self.startx)
901
953
 
902
954
  header_str = header_str[self.leftmost_char:]
955
+ header_str = header_str[:header_str_w]
903
956
  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)
957
+ self.stdscr.addstr(header_ypos, self.rows_box_x_i, ' '*self.rows_w, curses.color_pair(self.colours_start+28) | curses.A_BOLD)
958
+ self.stdscr.addstr(header_ypos, self.startx, header_str, curses.color_pair(self.colours_start+4) | curses.A_BOLD)
906
959
 
907
960
  # Highlight sort column
908
961
  try:
@@ -918,17 +971,19 @@ class Picker:
918
971
  else:
919
972
  colour = curses.color_pair(self.colours_start+19) | curses.A_BOLD
920
973
  # 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):
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):
922
975
  x_pos = len(up_to_selected_col) - self.leftmost_char + self.startx
923
976
 
924
977
  # 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:
978
+ if len(up_to_selected_col)+col_width - self.leftmost_char < self.rows_w-self.left_gutter_width:
926
979
  disp_str = highlighted_col_str
927
980
 
928
981
  # Start of the cell is on the screen, but the end of the cell is not
929
982
  else:
930
- overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w - self.startx)
983
+ overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w - self.left_gutter_width)
931
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)
932
987
 
933
988
  self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
934
989
  # Start of the cell is to the right of the screen
@@ -939,14 +994,18 @@ class Picker:
939
994
  x_pos = self.startx
940
995
  beg = self.leftmost_char - len(up_to_selected_col)
941
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)
942
999
  self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
943
1000
  # The middle of the cell is on the screen, the start and end of the cell are not
944
1001
  elif self.leftmost_char <= len(up_to_selected_col) + col_width//2 <= self.leftmost_char+self.rows_w:
945
1002
  beg = self.leftmost_char - len(up_to_selected_col)
946
1003
  overflow = (len(up_to_selected_col)+len(highlighted_col_str)) - (self.leftmost_char+self.rows_w)
1004
+ x_pos = self.startx
947
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)
948
1008
 
949
- x_pos = self.startx
950
1009
  self.stdscr.addstr(header_ypos, x_pos , disp_str, colour)
951
1010
  # The cell is to the left of the screen
952
1011
  else:
@@ -982,17 +1041,18 @@ class Picker:
982
1041
  for idx in range(start_index, end_index):
983
1042
  y = idx - start_index + self.top_space
984
1043
  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)
1044
+ 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
1045
  else:
987
- self.stdscr.addstr(y, 0, f" {self.indexed_items[idx][0]} ", curses.color_pair(self.colours_start+4) | curses.A_BOLD)
1046
+ 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
1047
 
989
1048
 
990
1049
  def highlight_cell(row: int, col:int, visible_column_widths, colour_pair_number: int = 5, bold: bool = False, y:int = 0):
991
1050
 
992
1051
  cell_pos = sum(visible_column_widths[:col])+col*len(self.separator)-self.leftmost_char + self.startx
1052
+ cell_pos_relative = sum(visible_column_widths[:col])+col*len(self.separator)-self.leftmost_char + self.left_gutter_width
993
1053
  # cell_width = self.column_widths[self.selected_column]
994
1054
  cell_width = visible_column_widths[col] + len(self.separator)
995
- cell_max_width = self.rows_w-cell_pos
1055
+ cell_max_width = min(self.rows_w-self.left_gutter_width, self.term_w-cell_pos)
996
1056
 
997
1057
  if bold:
998
1058
  colour = curses.color_pair(self.colours_start+colour_pair_number) | curses.A_BOLD
@@ -1000,7 +1060,8 @@ class Picker:
1000
1060
  colour = curses.color_pair(self.colours_start+colour_pair_number)
1001
1061
  try:
1002
1062
  # Start of cell is on screen
1003
- if self.startx <= cell_pos <= self.rows_w:
1063
+ if self.startx <= cell_pos <= self.rows_w+self.startx:
1064
+ s = "max" if cell_max_width <= cell_width else "norm"
1004
1065
  self.stdscr.addstr(y, cell_pos, (' '*cell_width)[:cell_max_width], colour)
1005
1066
  if self.centre_in_cols:
1006
1067
  cell_value = f"{self.indexed_items[row][1][col]:^{cell_width-len(self.separator)}}" + self.separator
@@ -1011,15 +1072,22 @@ class Picker:
1011
1072
  # cell_value = cell_value + self.separator
1012
1073
  # cell_value = cell_value
1013
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
+
1014
1078
  self.stdscr.addstr(y, cell_pos, cell_value, colour)
1015
1079
  # Part of the cell is on screen
1016
- elif self.startx <= cell_pos+cell_width and cell_pos < (self.rows_w):
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"
1017
1082
  cell_start = self.startx - cell_pos
1018
1083
  # self.stdscr.addstr(y, self.startx, ' '*(cell_width-cell_start), curses.color_pair(self.colours_start+colour_pair_number))
1019
1084
  cell_value = self.indexed_items[row][1][col]
1020
1085
  cell_value = f"{cell_value:^{self.column_widths[col]}}"
1021
1086
 
1022
- cell_value = cell_value[cell_start:visible_column_widths[col]][:self.rows_w-self.startx]
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)
1023
1091
  self.stdscr.addstr(y, self.startx, cell_value, colour)
1024
1092
  else:
1025
1093
  pass
@@ -1098,6 +1166,7 @@ class Picker:
1098
1166
  l0_highlights, l1_highlights, l2_highlights = sort_highlights(self.highlights)
1099
1167
 
1100
1168
 
1169
+ row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
1101
1170
  for idx in range(start_index, end_index):
1102
1171
  item = self.indexed_items[idx]
1103
1172
  y = idx - start_index + self.top_space
@@ -1109,18 +1178,21 @@ class Picker:
1109
1178
  # rowstr off screen
1110
1179
  # if self.leftmost_char > len(row_str_orig):
1111
1180
  # 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
1181
+ # if self.leftmost_char + (self.rows_w-self.left_gutter_width) <= len(row_str_orig):
1182
+ # trunc_width = self.rows_w-self.startx
1183
+ # elif self.leftmost_char <= len(row_str_orig):
1184
+ # trunc_width = len(row_str_orig) - self.leftmost_char
1185
+ # else:
1186
+ # trunc_width = 0
1187
+
1188
+
1189
+ trunc_width = min(self.rows_w-self.left_gutter_width, row_width, self.term_w - self.startx)
1118
1190
 
1119
1191
  row_str = truncate_to_display_width(row_str_left_adj, trunc_width, self.unicode_char_width)
1120
1192
  # row_str = truncate_to_display_width(row_str, min(w-self.startx, visible_columns_total_width))[self.leftmost_char:]
1121
1193
 
1122
1194
  ## 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))
1195
+ self.stdscr.addstr(y, self.startx, row_str, curses.color_pair(self.colours_start+2))
1124
1196
 
1125
1197
 
1126
1198
  ## Highlight column
@@ -1157,7 +1229,7 @@ class Picker:
1157
1229
  # Higlight cursor row and selected rows
1158
1230
  elif self.highlight_full_row:
1159
1231
  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)
1232
+ 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
1233
 
1162
1234
  # Visually selected
1163
1235
  if self.is_selecting:
@@ -1191,7 +1263,7 @@ class Picker:
1191
1263
  if self.cell_cursor:
1192
1264
  highlight_cell(idx, self.selected_column, visible_column_widths, colour_pair_number=5, bold=True, y=y)
1193
1265
  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)
1266
+ 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
1267
 
1196
1268
  if not self.highlights_hide:
1197
1269
  draw_highlights(l2_highlights, idx, y, item)
@@ -1211,7 +1283,8 @@ class Picker:
1211
1283
  scroll_bar_length = max(1, scroll_bar_length)
1212
1284
  for i in range(scroll_bar_length):
1213
1285
  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))
1286
+ # self.stdscr.addstr(scroll_bar_start+i, self.startx+self.rows_w-self.left_gutter_width-2, ' ', curses.color_pair(self.colours_start+18))
1287
+ self.stdscr.addstr(scroll_bar_start+i, self.rows_box_x_f-1, ' ', curses.color_pair(self.colours_start+18))
1215
1288
 
1216
1289
  # Display refresh symbol
1217
1290
  if self.auto_refresh:
@@ -1240,20 +1313,46 @@ class Picker:
1240
1313
 
1241
1314
  if self.split_right and len(self.right_panes):
1242
1315
  # 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()
1316
+ pane = self.right_panes[self.right_pane_index]
1317
+ if pane["auto_refresh"] and ((time.time() - self.initial_right_split_time) > pane["refresh_time"]):
1318
+ get_data = pane["get_data"]
1319
+ data = pane["data"]
1320
+ pane["data"] = get_data(data, self.get_function_data())
1321
+ self.initial_right_split_time = time.time()
1248
1322
 
1249
- draw_pane = self.right_panes[self.right_pane_index]["display"]
1250
- data = self.right_panes[self.right_pane_index]["data"]
1323
+ draw_pane = pane["display"]
1324
+ data = pane["data"]
1325
+ # pane_width = int(pane["proportion"]*self.term_w)
1251
1326
 
1252
1327
  draw_pane(
1253
1328
  self.stdscr,
1254
- x = self.rows_w,
1329
+ x = self.rows_w + self.startx - self.left_gutter_width,
1255
1330
  y = self.top_space - int(bool(self.show_header and self.header)),
1256
- w = self.term_w-self.rows_w,
1331
+ w = self.right_pane_width,
1332
+ h = self.items_per_page + int(bool(self.show_header and self.header)),
1333
+ state = self.get_function_data(),
1334
+ row = self.indexed_items[self.cursor_pos] if self.indexed_items else [],
1335
+ cell = self.indexed_items[self.cursor_pos][1][self.selected_column] if self.indexed_items else "",
1336
+ data=data,
1337
+ )
1338
+ if self.split_left and len(self.left_panes):
1339
+ # If we need to refresh the data then do so.
1340
+ pane = self.left_panes[self.left_pane_index]
1341
+ if pane["auto_refresh"] and ((time.time() - self.initial_left_split_time) > pane["refresh_time"]):
1342
+ get_data = pane["get_data"]
1343
+ data = pane["data"]
1344
+ pane["data"] = get_data(data, self.get_function_data())
1345
+ self.initial_left_split_time = time.time()
1346
+
1347
+ draw_pane = pane["display"]
1348
+ data = pane["data"]
1349
+ # pane_width = int(pane["proportion"]*self.term_w)
1350
+
1351
+ draw_pane(
1352
+ self.stdscr,
1353
+ x = 0,
1354
+ y = self.top_space - int(bool(self.show_header and self.header)),
1355
+ w = self.left_pane_width,
1257
1356
  h = self.items_per_page + int(bool(self.show_header and self.header)),
1258
1357
  state = self.get_function_data(),
1259
1358
  row = self.indexed_items[self.cursor_pos] if self.indexed_items else [],
@@ -1261,7 +1360,6 @@ class Picker:
1261
1360
  data=data,
1262
1361
  )
1263
1362
 
1264
- self.stdscr.refresh()
1265
1363
  ## Display infobox
1266
1364
  if self.display_infobox:
1267
1365
  self.infobox(self.stdscr, message=self.infobox_items, title=self.infobox_title)
@@ -1309,6 +1407,7 @@ class Picker:
1309
1407
  "reset_colours": False,
1310
1408
  "cell_cursor": False,
1311
1409
  "split_right": False,
1410
+ "split_left": False,
1312
1411
  "crosshair_cursor": False,
1313
1412
  }
1314
1413
 
@@ -1427,6 +1526,9 @@ class Picker:
1427
1526
  "split_right": self.split_right,
1428
1527
  "right_panes": self.right_panes,
1429
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,
1430
1532
  "crosshair_cursor": self.crosshair_cursor,
1431
1533
 
1432
1534
  }
@@ -1464,6 +1566,9 @@ class Picker:
1464
1566
  "centre_in_cols",
1465
1567
  "centre_in_terminal",
1466
1568
  "split_right",
1569
+ "left_pane_index",
1570
+ "split_left",
1571
+ "left_pane_index",
1467
1572
  ]
1468
1573
 
1469
1574
  for var in variables:
@@ -1557,6 +1662,7 @@ class Picker:
1557
1662
  "number_columns": False,
1558
1663
  "reset_colours": False,
1559
1664
  "split_right": False,
1665
+ "split_left": False,
1560
1666
  "cell_cursor": False,
1561
1667
  "crosshair_cursor": False,
1562
1668
  }
@@ -1569,6 +1675,7 @@ class Picker:
1569
1675
 
1570
1676
  submenu_win = curses.newwin(window_height, window_width, (self.term_h-window_height)//2, (self.term_w-window_width)//2)
1571
1677
  submenu_win.keypad(True)
1678
+ option_picker_data["screen_size_function"] = lambda stdscr: (window_height, window_width)
1572
1679
  OptionPicker = Picker(submenu_win, **option_picker_data)
1573
1680
  s, o, f = OptionPicker.run()
1574
1681
 
@@ -1617,9 +1724,11 @@ class Picker:
1617
1724
  "cancel_is_back": True,
1618
1725
  "reset_colours": False,
1619
1726
  "split_right": False,
1727
+ "split_left": False,
1620
1728
  "cell_cursor": False,
1621
1729
  "crosshair_cursor": False,
1622
1730
  "show_header": False,
1731
+ "screen_size_function": lambda stdscr: (notification_height, notification_width),
1623
1732
 
1624
1733
  }
1625
1734
  OptionPicker = Picker(submenu_win, **notification_data)
@@ -1757,14 +1866,24 @@ class Picker:
1757
1866
  self.footer_style = (self.footer_style+1)%len(self.footer_options)
1758
1867
  self.footer = self.footer_options[self.footer_style]
1759
1868
  self.initialise_variables()
1760
- elif setting == "pane":
1869
+ elif setting == "rpane":
1761
1870
  self.toggle_right_pane()
1762
1871
 
1763
- elif setting == "pane_cycle":
1872
+ elif setting == "rpane_cycle":
1764
1873
  self.cycle_right_pane()
1765
1874
 
1875
+ elif setting == "lpane":
1876
+ self.toggle_left_pane()
1877
+
1878
+ elif setting == "lpane_cycle":
1879
+ self.cycle_left_pane()
1880
+
1766
1881
  elif setting.startswith("cwd="):
1767
1882
  os.chdir(os.path.expandvars(os.path.expanduser(setting[len("cwd="):])))
1883
+ elif setting.startswith("lmc="):
1884
+ rem = setting[4:]
1885
+ if rem.isnumeric():
1886
+ self.leftmost_char = int(rem)
1768
1887
  elif setting.startswith("hl"):
1769
1888
  hl_list = setting.split(",")
1770
1889
  if len(hl_list) > 1:
@@ -1950,6 +2069,7 @@ class Picker:
1950
2069
  if self.indexed_items[new_pos][0] in self.unselectable_indices: new_pos+=1
1951
2070
  else: break
1952
2071
  self.cursor_pos = new_pos
2072
+ self.ensure_no_overscroll()
1953
2073
  return True
1954
2074
 
1955
2075
  def cursor_up(self, count=1) -> bool:
@@ -1963,6 +2083,7 @@ class Picker:
1963
2083
  elif new_pos in self.unselectable_indices: new_pos -= 1
1964
2084
  else: break
1965
2085
  self.cursor_pos = new_pos
2086
+ self.ensure_no_overscroll()
1966
2087
  return True
1967
2088
 
1968
2089
  def remapped_key(self, key: int, val: int, key_remappings: dict) -> bool:
@@ -2388,12 +2509,53 @@ class Picker:
2388
2509
  self.split_right = not self.split_right
2389
2510
  if self.right_panes[self.right_pane_index]["data"] in [[], None, {}]:
2390
2511
  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())
2512
+ self.ensure_no_overscroll()
2513
+
2514
+ def toggle_left_pane(self):
2515
+ if len(self.left_panes):
2516
+ self.split_left = not self.split_left
2517
+ if self.left_panes[self.left_pane_index]["data"] in [[], None, {}]:
2518
+ 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())
2519
+ self.ensure_no_overscroll()
2391
2520
 
2392
2521
 
2393
2522
  def cycle_right_pane(self, increment=1):
2394
2523
  if len(self.right_panes) > 1:
2395
2524
  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"]
2525
+ self.initial_right_split_time -= self.right_panes[self.right_pane_index]["refresh_time"]
2526
+ self.ensure_no_overscroll()
2527
+
2528
+ def cycle_left_pane(self, increment=1):
2529
+ if len(self.left_panes) > 1:
2530
+ self.left_pane_index = (self.left_pane_index+1)%len(self.left_panes)
2531
+ self.initial_left_split_time -= self.left_panes[self.left_pane_index]["refresh_time"]
2532
+ self.ensure_no_overscroll()
2533
+
2534
+ def ensure_no_overscroll(self):
2535
+ """
2536
+ Ensure that we haven't scrolled past the last column.
2537
+
2538
+ This check should be performed after:
2539
+ - Terminal resize event
2540
+ - Scrolling down - i.e., rows with potentially different widths come into view
2541
+ """
2542
+ self.calculate_section_sizes()
2543
+ self.get_visible_rows()
2544
+ self.column_widths = get_column_widths(
2545
+ self.visible_rows,
2546
+ header=self.header,
2547
+ max_column_width=self.max_column_width,
2548
+ number_columns=self.number_columns,
2549
+ max_total_width=self.rows_w,
2550
+ unicode_char_width=self.unicode_char_width
2551
+ )
2552
+
2553
+ row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
2554
+ if row_width - self.leftmost_char < self.rows_w:
2555
+ if row_width <= self.rows_w - self.left_gutter_width:
2556
+ self.leftmost_char = 0
2557
+ else:
2558
+ self.leftmost_char = row_width - (self.rows_w - self.left_gutter_width) + 5
2397
2559
 
2398
2560
  def run(self) -> Tuple[list[int], str, dict]:
2399
2561
  """ Run the picker. """
@@ -2411,7 +2573,8 @@ class Picker:
2411
2573
 
2412
2574
  self.initial_time = time.time()
2413
2575
  self.initial_time_footer = time.time()-self.footer_timer
2414
- self.initial_split_time = time.time()-200
2576
+ self.initial_right_split_time = time.time()-200
2577
+ self.initial_left_split_time = time.time()-200
2415
2578
 
2416
2579
  if self.startup_notification:
2417
2580
  self.notification(self.stdscr, message=self.startup_notification)
@@ -2446,11 +2609,7 @@ class Picker:
2446
2609
  tty_fd, self.saved_terminal_state = open_tty()
2447
2610
 
2448
2611
  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
2612
+ self.calculate_section_sizes()
2454
2613
 
2455
2614
  def terminal_resized(old_w, old_h) -> bool:
2456
2615
  w, h = os.get_terminal_size()
@@ -2486,14 +2645,6 @@ class Picker:
2486
2645
  if self.term_resize_event:
2487
2646
  key = curses.KEY_RESIZE
2488
2647
 
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
2648
  if key in self.disabled_keys: continue
2498
2649
  clear_screen=True
2499
2650
 
@@ -2545,11 +2696,17 @@ class Picker:
2545
2696
  self.initial_time_footer = time.time()
2546
2697
  self.draw_screen()
2547
2698
 
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"]):
2699
+ 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
2700
  get_data = self.right_panes[self.right_pane_index]["get_data"]
2550
2701
  data = self.right_panes[self.right_pane_index]["data"]
2551
2702
  self.right_panes[self.right_pane_index]["data"] = get_data(data, self.get_function_data())
2552
- self.initial_split_time = time.time()
2703
+ self.initial_right_split_time = time.time()
2704
+
2705
+ 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"]):
2706
+ get_data = self.left_panes[self.left_pane_index]["get_data"]
2707
+ data = self.left_panes[self.left_pane_index]["data"]
2708
+ self.left_panes[self.right_pane_index]["data"] = get_data(data, self.get_function_data())
2709
+ self.initial_left_split_time = time.time()
2553
2710
 
2554
2711
  if self.check_key("help", key, self.keys_dict):
2555
2712
  self.logger.info(f"key_function help")
@@ -2868,6 +3025,7 @@ class Picker:
2868
3025
  self.selected_cells_by_row[row] = [col]
2869
3026
 
2870
3027
  self.cursor_down()
3028
+ self.ensure_no_overscroll()
2871
3029
  elif self.check_key("select_all", key, self.keys_dict): # Select all (m or ctrl-a)
2872
3030
  self.select_all()
2873
3031
 
@@ -2882,6 +3040,7 @@ class Picker:
2882
3040
  if new_pos < len(self.indexed_items):
2883
3041
  self.cursor_pos = new_pos
2884
3042
 
3043
+ self.ensure_no_overscroll()
2885
3044
  self.draw_screen()
2886
3045
 
2887
3046
  elif self.check_key("cursor_bottom", key, self.keys_dict):
@@ -2891,6 +3050,7 @@ class Picker:
2891
3050
  else: break
2892
3051
  if new_pos < len(self.items) and new_pos >= 0:
2893
3052
  self.cursor_pos = new_pos
3053
+ self.ensure_no_overscroll()
2894
3054
  self.draw_screen()
2895
3055
  # current_row = items_per_page - 1
2896
3056
  # if current_page + 1 == (len(self.indexed_items) + items_per_page - 1) // items_per_page:
@@ -3006,7 +3166,7 @@ class Picker:
3006
3166
  column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
3007
3167
  start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
3008
3168
  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
3169
+ display_width = self.rows_w-self.left_gutter_width
3010
3170
  # If the full column is within the current display then don't do anything
3011
3171
  if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
3012
3172
  pass
@@ -3015,6 +3175,7 @@ class Picker:
3015
3175
  self.leftmost_char = end_of_cell - display_width
3016
3176
 
3017
3177
  self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
3178
+ self.ensure_no_overscroll()
3018
3179
 
3019
3180
  elif self.check_key("col_select_prev", key, self.keys_dict):
3020
3181
  if len(self.items) > 0 and len(self.items[0]) > 0:
@@ -3033,7 +3194,7 @@ class Picker:
3033
3194
  column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
3034
3195
  start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
3035
3196
  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
3197
+ display_width = self.rows_w-self.left_gutter_width
3037
3198
 
3038
3199
  # If the entire column is within the current display then don't do anything
3039
3200
  if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
@@ -3043,14 +3204,15 @@ class Picker:
3043
3204
  self.leftmost_char = start_of_cell
3044
3205
 
3045
3206
  self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
3207
+ self.ensure_no_overscroll()
3046
3208
 
3047
3209
  elif self.check_key("scroll_right", key, self.keys_dict):
3048
3210
  self.logger.info(f"key_function scroll_right")
3049
3211
  if len(self.indexed_items):
3050
3212
  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:
3213
+ if row_width-self.leftmost_char >= self.rows_w-5:
3052
3214
  self.leftmost_char += 5
3053
- self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w - self.startx) + 5)
3215
+ self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w) + self.left_gutter_width+5)
3054
3216
  if sum(self.column_widths) + len(self.column_widths)*len(self.separator) < self.rows_w:
3055
3217
  self.leftmost_char = 0
3056
3218
 
@@ -3058,9 +3220,9 @@ class Picker:
3058
3220
  self.logger.info(f"key_function scroll_right")
3059
3221
  if len(self.indexed_items):
3060
3222
  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:
3223
+ if row_width-self.leftmost_char+5 >= self.rows_w-25:
3062
3224
  self.leftmost_char += 25
3063
- self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w - self.startx) + 5)
3225
+ self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w) + self.left_gutter_width+5)
3064
3226
  if sum(self.column_widths) + len(self.column_widths)*len(self.separator) < self.rows_w:
3065
3227
  self.leftmost_char = 0
3066
3228
 
@@ -3091,7 +3253,16 @@ class Picker:
3091
3253
  # row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)
3092
3254
  # if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
3093
3255
  # 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)
3256
+ if len(self.indexed_items):
3257
+ row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
3258
+ self.leftmost_char = row_width - (self.rows_w) + self.left_gutter_width+5
3259
+ self.leftmost_char = min(self.leftmost_char, row_width - (self.rows_w) + self.left_gutter_width+5)
3260
+
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)
3263
+
3264
+
3265
+
3095
3266
  if len(self.items):
3096
3267
  self.selected_column = len(self.items[0])-1
3097
3268
 
@@ -3185,7 +3356,8 @@ class Picker:
3185
3356
  elif key == curses.KEY_RESIZE: # Terminal resize signal
3186
3357
 
3187
3358
  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)
3359
+ self.ensure_no_overscroll()
3360
+
3189
3361
  self.stdscr.clear()
3190
3362
  self.stdscr.refresh()
3191
3363
  self.draw_screen()
@@ -3442,6 +3614,12 @@ class Picker:
3442
3614
  elif self.check_key("cycle_right_pane", key, self.keys_dict):
3443
3615
  self.cycle_right_pane()
3444
3616
 
3617
+ elif self.check_key("toggle_left_pane", key, self.keys_dict):
3618
+ self.toggle_left_pane()
3619
+
3620
+ elif self.check_key("cycle_left_pane", key, self.keys_dict):
3621
+ self.cycle_left_pane()
3622
+
3445
3623
  elif self.check_key("pipe_input", key, self.keys_dict):
3446
3624
  self.logger.info(f"key_function pipe_input")
3447
3625
  # usrtxt = "xargs -d '\n' -I{} "
@@ -4003,7 +4181,7 @@ def main() -> None:
4003
4181
  # function_data["cell_cursor"] = True
4004
4182
  # function_data["display_modes"] = True
4005
4183
  # function_data["centre_in_cols"] = True
4006
- # function_data["show_row_header"] = True
4184
+ function_data["show_row_header"] = True
4007
4185
  # function_data["keys_dict"] = picker_keys
4008
4186
  # function_data["id_column"] = -1
4009
4187
  # function_data["track_entries_upon_refresh"] = True
@@ -4019,10 +4197,23 @@ def main() -> None:
4019
4197
  # function_data["debug"] = True
4020
4198
  # function_data["debug_level"] = 1
4021
4199
 
4200
+ function_data["cell_cursor"] = False
4201
+
4022
4202
  function_data["split_right"] = False
4203
+ function_data["split_left"] = False
4023
4204
  function_data["right_pane_index"] = 2
4205
+ function_data["left_pane_index"] = 0
4024
4206
 
4025
4207
  function_data["right_panes"] = [
4208
+ # Nopane
4209
+ {
4210
+ "proportion": 1/3,
4211
+ "auto_refresh": False,
4212
+ "get_data": lambda data, state: [],
4213
+ "display": left_start_pane,
4214
+ "data": ["Files", []],
4215
+ "refresh_time": 1,
4216
+ },
4026
4217
  # Graph or random numbers generated each second
4027
4218
  {
4028
4219
  "proportion": 1/2,
@@ -4034,7 +4225,7 @@ def main() -> None:
4034
4225
  },
4035
4226
  # list of numbers
4036
4227
  {
4037
- "proportion": 2/3,
4228
+ "proportion": 1/3,
4038
4229
  "auto_refresh": False,
4039
4230
  "get_data": data_refresh_randint_title,
4040
4231
  "display": right_split_display_list,
@@ -4043,7 +4234,7 @@ def main() -> None:
4043
4234
  },
4044
4235
  # File attributes
4045
4236
  {
4046
- "proportion": 2/3,
4237
+ "proportion": 1/3,
4047
4238
  "auto_refresh": False,
4048
4239
  "get_data": lambda data, state: [],
4049
4240
  "display": right_split_file_attributes,
@@ -4052,7 +4243,7 @@ def main() -> None:
4052
4243
  },
4053
4244
  # File attributes dynamic
4054
4245
  {
4055
- "proportion": 2/3,
4246
+ "proportion": 1/3,
4056
4247
  "auto_refresh": True,
4057
4248
  "get_data": update_file_attributes,
4058
4249
  "display": right_split_file_attributes_dynamic,
@@ -4078,7 +4269,72 @@ def main() -> None:
4078
4269
  "refresh_time": 1,
4079
4270
  },
4080
4271
  ]
4081
- function_data["require_option"] = [True for _ in function_data["items"]]
4272
+ function_data["left_panes"] = [
4273
+ # Nopane
4274
+ {
4275
+ "proportion": 1/3,
4276
+ "auto_refresh": False,
4277
+ "get_data": lambda data, state: [],
4278
+ "display": left_start_pane,
4279
+ "data": ["Files", []],
4280
+ "refresh_time": 1,
4281
+ },
4282
+ # Graph or random numbers generated each second
4283
+ {
4284
+ "proportion": 1/2,
4285
+ "auto_refresh": True,
4286
+ "get_data": data_refresh_randint,
4287
+ "display": left_split_graph,
4288
+ "data": [],
4289
+ "refresh_time": 1.0,
4290
+ },
4291
+ # list of numbers
4292
+ {
4293
+ "proportion": 1/3,
4294
+ "auto_refresh": False,
4295
+ "get_data": data_refresh_randint_title,
4296
+ "display": left_split_display_list,
4297
+ "data": ["Files", [str(x) for x in range(100)]],
4298
+ "refresh_time": 1.0,
4299
+ },
4300
+ # File attributes
4301
+ {
4302
+ "proportion": 1/3,
4303
+ "auto_refresh": False,
4304
+ "get_data": lambda data, state: [],
4305
+ "display": left_split_file_attributes,
4306
+ "data": [],
4307
+ "refresh_time": 1.0,
4308
+ },
4309
+ # File attributes dynamic
4310
+ {
4311
+ "proportion": 1/3,
4312
+ "auto_refresh": True,
4313
+ "get_data": update_file_attributes,
4314
+ "display": left_split_file_attributes_dynamic,
4315
+ "data": [],
4316
+ "refresh_time": 2.0,
4317
+ },
4318
+ # List of random numbers generated each second
4319
+ {
4320
+ "proportion": 1/2,
4321
+ "auto_refresh": True,
4322
+ "get_data": data_refresh_randint_title,
4323
+ "display": left_split_display_list,
4324
+ "data": ["Files", []],
4325
+ "refresh_time": 2,
4326
+ },
4327
+ # Nopane
4328
+ {
4329
+ "proportion": 1/3,
4330
+ "auto_refresh": False,
4331
+ "get_data": lambda data, state: [],
4332
+ "display": lambda scr, x, y, w, h, state, row, cell, data: [],
4333
+ "data": ["Files", []],
4334
+ "refresh_time": 1,
4335
+ },
4336
+ ]
4337
+ # function_data["require_option"] = [True for _ in function_data["items"]]
4082
4338
 
4083
4339
  stdscr = start_curses()
4084
4340
  try: