listpick 0.1.14.8__tar.gz → 0.1.14.10__tar.gz

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.
Files changed (61) hide show
  1. {listpick-0.1.14.8 → listpick-0.1.14.10}/CHANGELOG.md +3 -0
  2. {listpick-0.1.14.8/src/listpick.egg-info → listpick-0.1.14.10}/PKG-INFO +1 -1
  3. {listpick-0.1.14.8 → listpick-0.1.14.10}/TODO.md +6 -6
  4. {listpick-0.1.14.8 → listpick-0.1.14.10}/setup.py +1 -1
  5. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/listpick_app.py +25 -59
  6. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/ui/input_field.py +101 -105
  7. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/ui/keys.py +4 -2
  8. listpick-0.1.14.10/src/listpick/utils/keycodes.py +88 -0
  9. listpick-0.1.14.10/src/listpick/utils/user_input.py +104 -0
  10. {listpick-0.1.14.8 → listpick-0.1.14.10/src/listpick.egg-info}/PKG-INFO +1 -1
  11. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick.egg-info/SOURCES.txt +2 -1
  12. listpick-0.1.14.8/src/listpick/ui/keycodes.py +0 -70
  13. {listpick-0.1.14.8 → listpick-0.1.14.10}/.gitignore +0 -0
  14. {listpick-0.1.14.8 → listpick-0.1.14.10}/LICENSE.txt +0 -0
  15. {listpick-0.1.14.8 → listpick-0.1.14.10}/README.md +0 -0
  16. {listpick-0.1.14.8 → listpick-0.1.14.10}/assets/aria2tui_screenshot.png +0 -0
  17. {listpick-0.1.14.8 → listpick-0.1.14.10}/assets/file_compare.png +0 -0
  18. {listpick-0.1.14.8 → listpick-0.1.14.10}/assets/lpfman.png +0 -0
  19. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/data_generation/list_files.toml +0 -0
  20. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/data_generation/list_files_empty.toml +0 -0
  21. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/data_generation/video_duplicates.toml +0 -0
  22. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/data_generation/video_mediainfo.toml +0 -0
  23. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/input_files/polynomials.tsv +0 -0
  24. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/input_guides/gnuplot_graph.md +0 -0
  25. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/picker/auxiallary_files/2024-25_Premier_League.pkl +0 -0
  26. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/picker/footer_string_example.py +0 -0
  27. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/picker/picker_example.py +0 -0
  28. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/picker/template.py +0 -0
  29. {listpick-0.1.14.8 → listpick-0.1.14.10}/examples/picker/wikipedia_table.py +0 -0
  30. {listpick-0.1.14.8 → listpick-0.1.14.10}/listpick.py +0 -0
  31. {listpick-0.1.14.8 → listpick-0.1.14.10}/requirements.txt +0 -0
  32. {listpick-0.1.14.8 → listpick-0.1.14.10}/setup.cfg +0 -0
  33. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/__init__.py +0 -0
  34. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/__main__.py +0 -0
  35. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/ui/__init__.py +0 -0
  36. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/ui/build_help.py +0 -0
  37. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/ui/footer.py +0 -0
  38. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/ui/git-bugreport-2025-08-16-1438.txt +0 -0
  39. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/ui/help_screen.py +0 -0
  40. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/ui/pane_stuff.py +0 -0
  41. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/ui/picker_colours.py +0 -0
  42. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/__init__.py +0 -0
  43. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/clipboard_operations.py +0 -0
  44. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/config.py +0 -0
  45. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/dump.py +0 -0
  46. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/filtering.py +0 -0
  47. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/generate_data.py +0 -0
  48. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/options_selectors.py +0 -0
  49. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/paste_operations.py +0 -0
  50. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/picker_log.py +0 -0
  51. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/search_and_filter_utils.py +0 -0
  52. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/searching.py +0 -0
  53. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/sorting.py +0 -0
  54. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/table_to_list_of_lists.py +0 -0
  55. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick/utils/utils.py +0 -0
  56. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick.egg-info/dependency_links.txt +0 -0
  57. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick.egg-info/entry_points.txt +0 -0
  58. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick.egg-info/requires.txt +0 -0
  59. {listpick-0.1.14.8 → listpick-0.1.14.10}/src/listpick.egg-info/top_level.txt +0 -0
  60. {listpick-0.1.14.8 → listpick-0.1.14.10}/tests/kitty_control.sh +0 -0
  61. {listpick-0.1.14.8 → listpick-0.1.14.10}/tests/sorting_dates.csv +0 -0
@@ -24,6 +24,9 @@ Note that the changes between 0.1.11.0 and 1.1.12.0 are listed under 0.1.11
24
24
  - meta+key (input_field)
25
25
  - Added column number to footer
26
26
  - Improved splitting of whitespace separated data passed on stdin.
27
+ - Ensured that main picker elements resize if the terminal is resized when other dialogues are in focus: notifications, options, settings.
28
+ - Added method to check for terminal resize.
29
+ - Was necessary since we are no longer using getch from curses so we can't test whether getch() == curses.KEY_RESIZE.
27
30
 
28
31
  ## [0.1.14] 2025-08-20
29
32
  - Fixed bug when cells are centred vertically.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: listpick
3
- Version: 0.1.14.8
3
+ Version: 0.1.14.10
4
4
  Summary: Listpick is a powerful TUI data tool for creating TUI apps or viewing/comparing tabulated data.
5
5
  Home-page: https://github.com/grimandgreedy/listpick
6
6
  Author: Grim
@@ -14,12 +14,12 @@ ASAP
14
14
  > - [ ] Sheet states remain the same when switching files.
15
15
  > - [x] Deal with nan values in xlsx and ods files.
16
16
  > - [ ] They are set to empyt strings.
17
- > - [ ] Fix special keys not working:
18
- > - [ ] May need to create separate keycodes...
19
- > - [ ] F1-f12
20
- > - [ ] Tab, shift+tab
21
- > - [ ] Delete, shift+delete
22
- > - [ ] Alt+KEY
17
+ > - [x] Fix special keys not working:
18
+ > - [x] May need to create separate keycodes...
19
+ > - [x] F1-f12
20
+ > - [x] Tab, shift+tab
21
+ > - [x] Delete, shift+delete
22
+ > - [x] Alt+KEY
23
23
  > - [x] Arrow keys
24
24
 
25
25
 
@@ -16,7 +16,7 @@ with open("README.md", "r", encoding = "utf-8") as fh:
16
16
 
17
17
  setuptools.setup(
18
18
  name = "listpick",
19
- version = "0.1.14.8",
19
+ version = "0.1.14.10",
20
20
  author = "Grim",
21
21
  author_email = "grimandgreedy@protonmail.com",
22
22
  description = "Listpick is a powerful TUI data tool for creating TUI apps or viewing/comparing tabulated data.",
@@ -40,6 +40,7 @@ from listpick.utils.dump import dump_state, load_state, dump_data
40
40
  from listpick.ui.build_help import build_help_rows
41
41
  from listpick.ui.footer import StandardFooter, CompactFooter, NoFooter
42
42
  from listpick.utils.picker_log import setup_logger
43
+ from listpick.utils.user_input import get_char, open_tty
43
44
 
44
45
 
45
46
  try:
@@ -187,6 +188,8 @@ class Picker:
187
188
  sheet_index = 0,
188
189
  sheet_states = [{}],
189
190
 
191
+ redraw_screen_accessory: Callable = lambda : None,
192
+
190
193
  ):
191
194
  self.stdscr = stdscr
192
195
  self.items = items
@@ -335,6 +338,7 @@ class Picker:
335
338
  self.sheet_states = sheet_states
336
339
  self.sheets = sheets
337
340
 
341
+ self.redraw_screen_accessory = redraw_screen_accessory
338
342
  self.initialise_picker_state(reset_colours=self.reset_colours)
339
343
 
340
344
  # Note: We have to set the footer after initialising the picker state so that the footer can use the get_function_data method
@@ -747,6 +751,8 @@ class Picker:
747
751
 
748
752
  def draw_screen_(self, indexed_items: list[Tuple[int, list[str]]], highlights: list[dict] = [{}], clear: bool = True) -> None:
749
753
  """ Draw Picker screen. """
754
+
755
+ self.redraw_screen_accessory()
750
756
  self.logger.debug("Draw screen.")
751
757
 
752
758
  if clear:
@@ -1236,6 +1242,7 @@ class Picker:
1236
1242
  "sheets": self.sheets,
1237
1243
  "sheet_name": self.sheet_name,
1238
1244
  "sheet_states": self.sheet_states,
1245
+ "redraw_screen_accessory": self.redraw_screen_accessory,
1239
1246
  }
1240
1247
  return function_data
1241
1248
 
@@ -1270,6 +1277,7 @@ class Picker:
1270
1277
  "centre_in_terminal_vertical",
1271
1278
  "centre_in_cols",
1272
1279
  "centre_in_terminal",
1280
+ "redraw_screen_accessory",
1273
1281
  ]
1274
1282
 
1275
1283
  for var in variables:
@@ -1362,7 +1370,6 @@ class Picker:
1362
1370
  "cancel_is_back": True,
1363
1371
  "number_columns": False,
1364
1372
  "reset_colours": False,
1365
- "cell_cursor": False,
1366
1373
  }
1367
1374
  while True:
1368
1375
  h, w = stdscr.getmaxyx()
@@ -1382,7 +1389,7 @@ class Picker:
1382
1389
  if s:
1383
1390
  return {x: options[x] for x in s}, o, f
1384
1391
  return {}, "", f
1385
-
1392
+
1386
1393
 
1387
1394
 
1388
1395
  def notification(self, stdscr: curses.window, message: str="", title:str="Notification", colours_end: int=0, duration:int=4) -> None:
@@ -1393,12 +1400,7 @@ class Picker:
1393
1400
  message_width = notification_width-5
1394
1401
 
1395
1402
  if not message: message = "!!"
1396
- if type(message) == type(""):
1397
- mw = message_width
1398
- submenu_items = [[message[i*mw:(i+1)*mw]] for i in range(len(message)//mw+1)]
1399
- elif type(message) != type([]):
1400
- submenu_items = [[" !!"]]
1401
-
1403
+ submenu_items = [" "+message[i*message_width:(i+1)*message_width] for i in range(len(message)//message_width+1)]
1402
1404
 
1403
1405
  notification_remap_keys = {
1404
1406
  curses.KEY_RESIZE: curses.KEY_F5,
@@ -1408,7 +1410,6 @@ class Picker:
1408
1410
  h, w = stdscr.getmaxyx()
1409
1411
 
1410
1412
  submenu_win = curses.newwin(notification_height, notification_width, 3, w - (notification_width+4))
1411
- # submenu_win = self.stdscr.subwin(notification_height, notification_width, 3, w - (notification_width+4))
1412
1413
  notification_data = {
1413
1414
  "items": submenu_items,
1414
1415
  "title": title,
@@ -1425,11 +1426,6 @@ class Picker:
1425
1426
  "cancel_is_back": True,
1426
1427
  "reset_colours": False,
1427
1428
 
1428
- "loaded_files": [],
1429
- "loaded_file_states": [],
1430
- "loaded_file": "",
1431
- "loaded_file_index": 0,
1432
- "cell_cursor": False,
1433
1429
  }
1434
1430
  OptionPicker = Picker(submenu_win, **notification_data)
1435
1431
  s, o, f = OptionPicker.run()
@@ -2229,6 +2225,13 @@ class Picker:
2229
2225
  # Open tty to accept input
2230
2226
  tty_fd = open_tty()
2231
2227
 
2228
+ h, w = self.stdscr.getmaxyx()
2229
+ def terminal_resized(old_w, old_h) -> bool:
2230
+ w, h = os.get_terminal_size()
2231
+ if old_h != h or old_w != w: return True
2232
+ else: return False
2233
+
2234
+ COLS, LINES = os.get_terminal_size()
2232
2235
  # Main loop
2233
2236
  while True:
2234
2237
  # key = self.stdscr.getch()
@@ -2236,7 +2239,14 @@ class Picker:
2236
2239
  key = get_char(tty_fd, timeout=0.2)
2237
2240
  if key != -1:
2238
2241
  self.logger.info(f"key={key}")
2242
+
2243
+ self.term_resize_event = terminal_resized(COLS, LINES)
2244
+ COLS, LINES = os.get_terminal_size()
2245
+ if self.term_resize_event:
2246
+ key = curses.KEY_RESIZE
2247
+
2239
2248
  h, w = self.stdscr.getmaxyx()
2249
+
2240
2250
  if key in self.disabled_keys: continue
2241
2251
  clear_screen=True
2242
2252
 
@@ -3091,7 +3101,7 @@ class Picker:
3091
3101
  self.logger.info(f"key_function opts_select")
3092
3102
  s, o, f = self.choose_option(self.stdscr, self.options_list)
3093
3103
  if self.user_opts.strip(): self.user_opts += " "
3094
- self.user_opts += " ".join([x for x in s.values()])
3104
+ self.user_opts += " ".join([x[0] for x in s.values()])
3095
3105
  elif self.check_key("notification_toggle", key, self.keys_dict):
3096
3106
  self.logger.info(f"key_function notification_toggle")
3097
3107
  self.notification(self.stdscr, colours_end=self.colours_end)
@@ -3580,50 +3590,6 @@ def unrestrict_curses(stdscr: curses.window) -> None:
3580
3590
  curses.curs_set(False)
3581
3591
 
3582
3592
 
3583
- def open_tty():
3584
- """ Return a file descriptor for the tty that we are opening"""
3585
- tty_fd = os.open('/dev/tty', os.O_RDONLY)
3586
- tty.setraw(tty_fd)
3587
- return tty_fd
3588
-
3589
- def get_char(tty_fd, timeout: float = 0.2, secondary: bool = False) -> int:
3590
- """ Get character from a tty_fd with a timeout. """
3591
- rlist, _, _ = select.select([tty_fd], [], [], timeout)
3592
- if rlist:
3593
- # key = ord(tty_fd.read(1))
3594
- key = ord(os.read(tty_fd, 1))
3595
- if not secondary:
3596
- if key == 27:
3597
- key2 = get_char(tty_fd, timeout=0.01, secondary=True)
3598
- key3 = get_char(tty_fd, timeout=0.01, secondary=True)
3599
- key4 = get_char(tty_fd, timeout=0.01, secondary=True)
3600
- key5 = get_char(tty_fd, timeout=0.01, secondary=True)
3601
- if key2 == ord('O') and key3 == ord('B'):
3602
- key = curses.KEY_DOWN
3603
- elif key2 == ord('O') and key3 == ord('A'):
3604
- key = curses.KEY_UP
3605
- elif key2 == ord('O') and key3 == ord('D'):
3606
- key = curses.KEY_LEFT
3607
- elif key2 == ord('O') and key3 == ord('C'):
3608
- key = curses.KEY_RIGHT
3609
- elif key2 == ord('[') and key3 == ord('Z'):
3610
- key = 353
3611
- elif key2 == ord('O') and key3 == ord('F'):
3612
- key = curses.KEY_END
3613
- elif key2 == ord('O') and key3 == ord('H'):
3614
- key = curses.KEY_HOME
3615
- elif key2 == ord('[') and key3 == ord('3') and key4 == ord('~'):
3616
- key = curses.KEY_DC
3617
- elif key2 == ord('[') and key3 == ord('3') and key4 == ord('~'):
3618
- key = curses.KEY_DC
3619
- elif key2 == ord('O') and key3 == ord('P'):
3620
- key = curses.KEY_F1
3621
- elif key2 == ord('[') and key3 == ord('1') and key4 == ord('5') and key5 == ord('~'):
3622
- key = curses.KEY_F5
3623
-
3624
- else:
3625
- key = -1
3626
- return key
3627
3593
 
3628
3594
  def main() -> None:
3629
3595
  """ Main function when listpick is executed. Deals with command line arguments and starts a Picker. """
@@ -18,22 +18,24 @@ import logging
18
18
  logger = logging.getLogger('picker_log')
19
19
  import select
20
20
  import tty
21
-
22
- def open_tty():
23
- """ Return a file descriptor for the tty that we are opening"""
24
- tty_fd = os.open('/dev/tty', os.O_RDONLY)
25
- tty.setraw(tty_fd)
26
- return tty_fd
27
-
28
- def get_char(tty_fd, timeout: float = 0.2) -> int:
29
- """ Get character from a tty_fd with a timeout. """
30
- rlist, _, _ = select.select([tty_fd], [], [], timeout)
31
- if rlist:
32
- # key = ord(tty_fd.read(1))
33
- key = ord(os.read(tty_fd, 1))
34
- else:
35
- key = -1
36
- return key
21
+ from listpick.utils.user_input import get_char, open_tty
22
+ from listpick.utils import keycodes
23
+
24
+ # def open_tty():
25
+ # """ Return a file descriptor for the tty that we are opening"""
26
+ # tty_fd = os.open('/dev/tty', os.O_RDONLY)
27
+ # tty.setraw(tty_fd)
28
+ # return tty_fd
29
+ #
30
+ # def get_char(tty_fd, timeout: float = 0.2) -> int:
31
+ # """ Get character from a tty_fd with a timeout. """
32
+ # rlist, _, _ = select.select([tty_fd], [], [], timeout)
33
+ # if rlist:
34
+ # # key = ord(tty_fd.read(1))
35
+ # key = ord(os.read(tty_fd, 1))
36
+ # else:
37
+ # key = -1
38
+ # return key
37
39
 
38
40
  def input_field(
39
41
  stdscr: curses.window,
@@ -230,100 +232,94 @@ def input_field(
230
232
  key = get_char(tty_fd, timeout=0.5)
231
233
  # key = stdscr.getch()
232
234
 
233
- if key in [27, 7]: # ESC/ALT key or Ctrl+g
234
- # For Alt-key combinations: set nodelay and get the second key
235
- # stdscr.nodelay(True)
236
- # key2 = stdscr.getch()
237
- key2 = get_char(tty_fd, timeout=0.05)
238
-
239
- if key2 == -1: # ESCAPE key (no key-combination)
240
- stdscr.nodelay(False)
241
- return "", False
242
- elif key2 == curses.KEY_BACKSPACE:
243
- # Delete to backslash or space (word_separator_chars)
244
- search_txt = usrtxt[:-cursor] if cursor > 0 else usrtxt
245
- index = -1
246
- for word_separator_char in word_separator_chars:
247
- tmp_index = search_txt[::-1].find(word_separator_char)
248
- if tmp_index > -1:
249
- if index == -1:
250
- index = tmp_index
251
- else:
252
- index = min(index, tmp_index)
253
-
254
- if index == -1:
255
- if cursor == 0:
256
- kill_ring.append(usrtxt)
257
- usrtxt = ""
258
- else:
259
- kill_ring.append(usrtxt[:-(cursor+1)])
260
- usrtxt = usrtxt[-(cursor+1):]
261
- cursor = len(usrtxt)
262
- else:
263
- if index == 0:
264
- kill_ring.append(search_txt[-1:])
265
- usrtxt = search_txt[:-1] + usrtxt[len(search_txt):]
235
+
236
+ if key in [27, 7]: # ESC/ALT key or Ctrl+g
237
+ return "", False
238
+
239
+ elif key == keycodes.META_BS:
240
+ # Delete to backslash or space (word_separator_chars)
241
+ search_txt = usrtxt[:-cursor] if cursor > 0 else usrtxt
242
+ index = -1
243
+ for word_separator_char in word_separator_chars:
244
+ tmp_index = search_txt[::-1].find(word_separator_char)
245
+ if tmp_index > -1:
246
+ if index == -1:
247
+ index = tmp_index
266
248
  else:
267
- kill_ring.append(search_txt[-index:])
268
- usrtxt = search_txt[:-index] + usrtxt[len(search_txt):]
249
+ index = min(index, tmp_index)
269
250
 
270
- potential_path = usrtxt
271
- kill_ring_active = False
272
-
273
- elif key2 == ord('f'):
274
- # Forward word
275
- search_txt = usrtxt[-cursor:]
276
- index = -1
277
- for word_separator_char in word_separator_chars:
278
- tmp_index = search_txt.find(word_separator_char)
279
-
280
- if tmp_index > -1:
281
- if index == -1:
282
- index = tmp_index
283
- else:
284
- index = min(index, tmp_index)
285
-
286
- if index == -1:
287
- cursor = 0
251
+ if index == -1:
252
+ if cursor == 0:
253
+ kill_ring.append(usrtxt)
254
+ usrtxt = ""
288
255
  else:
289
- cursor -= index + 1
290
- cursor = max(cursor, 0)
291
- kill_ring_active = False
292
-
293
- elif key2 == ord('b'):
294
- # Backwards word
295
- search_txt = usrtxt[:-cursor] if cursor > 0 else usrtxt
296
- index = -1
297
- for word_separator_char in word_separator_chars:
298
- tmp_index = search_txt[::-1].find(word_separator_char)
299
-
300
- if tmp_index == 0:
301
- tmp_index = search_txt[:-1][::-1].find(word_separator_char)
302
-
303
- if tmp_index > -1:
304
- if index == -1:
305
- index = tmp_index
306
- else:
307
- index = min(index, tmp_index)
308
-
309
- if index == -1:
310
- cursor = len(usrtxt)
256
+ kill_ring.append(usrtxt[:-(cursor+1)])
257
+ usrtxt = usrtxt[-(cursor+1):]
258
+ cursor = len(usrtxt)
259
+ else:
260
+ if index == 0:
261
+ kill_ring.append(search_txt[-1:])
262
+ usrtxt = search_txt[:-1] + usrtxt[len(search_txt):]
311
263
  else:
312
- cursor += index + 1
313
- kill_ring_active = False
314
-
315
- elif key2 == ord('y'):
316
- prev_kill_ring_index = kill_ring_index
317
- kill_ring_index = (kill_ring_index + 1)%len(kill_ring)
318
- if kill_ring_active and len(kill_ring):
319
- if cursor == 0:
320
- usrtxt = usrtxt[:-len(kill_ring[prev_kill_ring_index])]
321
- usrtxt += kill_ring[kill_ring_index]
264
+ kill_ring.append(search_txt[-index:])
265
+ usrtxt = search_txt[:-index] + usrtxt[len(search_txt):]
266
+
267
+ potential_path = usrtxt
268
+ kill_ring_active = False
269
+
270
+ elif key == keycodes.META_f:
271
+ # Forward word
272
+ search_txt = usrtxt[-cursor:]
273
+ index = -1
274
+ for word_separator_char in word_separator_chars:
275
+ tmp_index = search_txt.find(word_separator_char)
276
+
277
+ if tmp_index > -1:
278
+ if index == -1:
279
+ index = tmp_index
280
+ else:
281
+ index = min(index, tmp_index)
282
+
283
+ if index == -1:
284
+ cursor = 0
285
+ else:
286
+ cursor -= index + 1
287
+ cursor = max(cursor, 0)
288
+ kill_ring_active = False
289
+
290
+ elif key == keycodes.META_b:
291
+ # Backwards word
292
+ search_txt = usrtxt[:-cursor] if cursor > 0 else usrtxt
293
+ index = -1
294
+ for word_separator_char in word_separator_chars:
295
+ tmp_index = search_txt[::-1].find(word_separator_char)
296
+
297
+ if tmp_index == 0:
298
+ tmp_index = search_txt[:-1][::-1].find(word_separator_char)
299
+
300
+ if tmp_index > -1:
301
+ if index == -1:
302
+ index = tmp_index
322
303
  else:
323
- usrtxt = usrtxt[-cursor:-(cursor+len(kill_ring[prev_kill_ring_index]))]
324
- usrtxt = usrtxt[:-cursor] + kill_ring[kill_ring_index] + usrtxt[-cursor:]
325
- # Return to delayed getch
326
- stdscr.nodelay(False)
304
+ index = min(index, tmp_index)
305
+
306
+ if index == -1:
307
+ cursor = len(usrtxt)
308
+ else:
309
+ cursor += index + 1
310
+ kill_ring_active = False
311
+
312
+ elif key == keycodes.META_y:
313
+ # Kill ring
314
+ prev_kill_ring_index = kill_ring_index
315
+ kill_ring_index = (kill_ring_index + 1)%len(kill_ring)
316
+ if kill_ring_active and len(kill_ring):
317
+ if cursor == 0:
318
+ usrtxt = usrtxt[:-len(kill_ring[prev_kill_ring_index])]
319
+ usrtxt += kill_ring[kill_ring_index]
320
+ else:
321
+ usrtxt = usrtxt[-cursor:-(cursor+len(kill_ring[prev_kill_ring_index]))]
322
+ usrtxt = usrtxt[:-cursor] + kill_ring[kill_ring_index] + usrtxt[-cursor:]
327
323
 
328
324
  elif key == 3: # ctrl+c
329
325
  # Immediate exit
@@ -9,6 +9,7 @@ License: MIT
9
9
  """
10
10
 
11
11
  import curses
12
+ from listpick.utils import keycodes
12
13
 
13
14
  picker_keys = {
14
15
  "refresh": [curses.KEY_F5],
@@ -25,8 +26,8 @@ picker_keys = {
25
26
  "page_down": [curses.KEY_NPAGE, 6], # Ctrl+f
26
27
  "cursor_bottom": [ord('G'), curses.KEY_END],
27
28
  "cursor_top": [ord('g'), curses.KEY_HOME],
28
- "five_up": [ord('K')],
29
- "five_down": [ord('J')],
29
+ "five_up": [ord('K'), keycodes.META_k],
30
+ "five_down": [ord('J'), keycodes.META_j],
30
31
  "toggle_select": [ord(' ')],
31
32
  "select_all": [ord('m'), 1], # Ctrl-a
32
33
  "select_none": [ord('M'), 18], # Ctrl-r
@@ -135,6 +136,7 @@ notification_keys = {
135
136
  "five_up": [ord('K')],
136
137
  "five_down": [ord('J')],
137
138
  "redraw_screen": [12], # Ctrl-l
139
+ "refresh": [curses.KEY_F5, curses.KEY_RESIZE],
138
140
  }
139
141
 
140
142
 
@@ -0,0 +1,88 @@
1
+ # Set a base value to ensure that we have no collisions with any unicode characters
2
+ BASE_VALUE = 2**32
3
+
4
+ # Define constants for alt+a to alt+z
5
+ META_A = BASE_VALUE + 1000
6
+ META_B = BASE_VALUE + 1001
7
+ META_C = BASE_VALUE + 1002
8
+ META_D = BASE_VALUE + 1003
9
+ META_E = BASE_VALUE + 1004
10
+ META_F = BASE_VALUE + 1005
11
+ META_G = BASE_VALUE + 1006
12
+ META_H = BASE_VALUE + 1007
13
+ META_I = BASE_VALUE + 1008
14
+ META_J = BASE_VALUE + 1009
15
+ META_K = BASE_VALUE + 1010
16
+ META_L = BASE_VALUE + 1011
17
+ META_M = BASE_VALUE + 1012
18
+ META_N = BASE_VALUE + 1013
19
+ META_O = BASE_VALUE + 1014
20
+ META_P = BASE_VALUE + 1015
21
+ META_Q = BASE_VALUE + 1016
22
+ META_R = BASE_VALUE + 1017
23
+ META_S = BASE_VALUE + 1018
24
+ META_T = BASE_VALUE + 1019
25
+ META_U = BASE_VALUE + 1020
26
+ META_V = BASE_VALUE + 1021
27
+ META_W = BASE_VALUE + 1022
28
+ META_X = BASE_VALUE + 1023
29
+ META_Y = BASE_VALUE + 1024
30
+ META_Z = BASE_VALUE + 1025
31
+
32
+ # Define constants for alt+A to alt+Z (using uppercase)
33
+ META_a = BASE_VALUE + 1050
34
+ META_b = BASE_VALUE + 1051
35
+ META_c = BASE_VALUE + 1052
36
+ META_d = BASE_VALUE + 1053
37
+ META_e = BASE_VALUE + 1054
38
+ META_f = BASE_VALUE + 1055
39
+ META_g = BASE_VALUE + 1056
40
+ META_h = BASE_VALUE + 1057
41
+ META_i = BASE_VALUE + 1058
42
+ META_j = BASE_VALUE + 1059
43
+ META_k = BASE_VALUE + 1060
44
+ META_l = BASE_VALUE + 1061
45
+ META_m = BASE_VALUE + 1062
46
+ META_n = BASE_VALUE + 1063
47
+ META_o = BASE_VALUE + 1064
48
+ META_p = BASE_VALUE + 1065
49
+ META_q = BASE_VALUE + 1066
50
+ META_r = BASE_VALUE + 1067
51
+ META_s = BASE_VALUE + 1068
52
+ META_t = BASE_VALUE + 1069
53
+ META_u = BASE_VALUE + 1070
54
+ META_v = BASE_VALUE + 1071
55
+ META_w = BASE_VALUE + 1072
56
+ META_x = BASE_VALUE + 1073
57
+ META_y = BASE_VALUE + 1074
58
+ META_z = BASE_VALUE + 1075
59
+
60
+
61
+ META_0 = BASE_VALUE + 1090
62
+ META_1 = BASE_VALUE + 1091
63
+ META_2 = BASE_VALUE + 1092
64
+ META_3 = BASE_VALUE + 1093
65
+ META_4 = BASE_VALUE + 1094
66
+ META_5 = BASE_VALUE + 1095
67
+ META_6 = BASE_VALUE + 1096
68
+ META_7 = BASE_VALUE + 1097
69
+ META_8 = BASE_VALUE + 1098
70
+ META_9 = BASE_VALUE + 1099
71
+
72
+
73
+ META_BS = BASE_VALUE + 1100
74
+
75
+ # Dictionary to map character to its constant value
76
+ META_KEY_MAP = {
77
+ 'a': META_a, 'b': META_b, 'c': META_c, 'd': META_d, 'e': META_e,
78
+ 'f': META_f, 'g': META_g, 'h': META_h, 'i': META_i, 'j': META_j,
79
+ 'k': META_k, 'l': META_l, 'm': META_m, 'n': META_n, 'o': META_o,
80
+ 'p': META_p, 'q': META_q, 'r': META_r, 's': META_s, 't': META_t,
81
+ 'u': META_u, 'v': META_v, 'w': META_w, 'x': META_x, 'y': META_y,
82
+ 'z': META_z, 'A': META_A, 'B': META_B, 'C': META_C, 'D': META_D,
83
+ 'E': META_E, 'F': META_F, 'G': META_G, 'H': META_H, 'I': META_I,
84
+ 'J': META_J, 'K': META_K, 'L': META_L, 'M': META_M, 'N': META_N,
85
+ 'O': META_O, 'P': META_P, 'Q': META_Q, 'R': META_R, 'S': META_S,
86
+ 'T': META_T, 'U': META_U, 'V': META_V, 'W': META_W, 'X': META_X,
87
+ 'Y': META_Y, 'Z': META_Z
88
+ }
@@ -0,0 +1,104 @@
1
+ from listpick.utils import keycodes
2
+ import os, tty, select, curses
3
+
4
+ def open_tty():
5
+ """ Return a file descriptor for the tty that we are opening"""
6
+ tty_fd = os.open('/dev/tty', os.O_RDONLY)
7
+ tty.setraw(tty_fd)
8
+ return tty_fd
9
+
10
+ def get_char(tty_fd, timeout: float = 0.2, secondary: bool = False) -> int:
11
+ """ Get character from a tty_fd with a timeout. """
12
+ rlist, _, _ = select.select([tty_fd], [], [], timeout)
13
+ if rlist:
14
+ # key = ord(tty_fd.read(1))
15
+ key = ord(os.read(tty_fd, 1))
16
+ if not secondary:
17
+ if key == 27:
18
+ key2 = get_char(tty_fd, timeout=0.01, secondary=True)
19
+ key3 = get_char(tty_fd, timeout=0.01, secondary=True)
20
+ key4 = get_char(tty_fd, timeout=0.01, secondary=True)
21
+ key5 = get_char(tty_fd, timeout=0.01, secondary=True)
22
+ key6 = get_char(tty_fd, timeout=0.01, secondary=True)
23
+
24
+ keys = [key2, key3, key4, key5, key6]
25
+
26
+ key_str = "".join([chr(k) for k in keys if k != -1])
27
+
28
+ ## Arrow Keys
29
+ if key2 == ord('O') and key3 == ord('B'):
30
+ key = curses.KEY_DOWN
31
+ elif key2 == ord('O') and key3 == ord('A'):
32
+ key = curses.KEY_UP
33
+ elif key2 == ord('O') and key3 == ord('D'):
34
+ key = curses.KEY_LEFT
35
+ elif key2 == ord('O') and key3 == ord('C'):
36
+ key = curses.KEY_RIGHT
37
+
38
+ ## Shift+ Tab
39
+ elif key2 == ord('[') and key3 == ord('Z'):
40
+ key = 353
41
+
42
+ ## Home, End, Pgup, Pgdn
43
+ elif key2 == ord('O') and key3 == ord('F'):
44
+ key = curses.KEY_END
45
+ elif key2 == ord('O') and key3 == ord('H'):
46
+ key = curses.KEY_HOME
47
+ elif key2 == ord('[') and key3 == ord('6') and key4 == ord("~"):
48
+ key = curses.KEY_NPAGE
49
+ elif key2 == ord('[') and key3 == ord('5') and key4 == ord("~"):
50
+ key = curses.KEY_PPAGE
51
+
52
+
53
+ # Delete key
54
+ elif key_str == "[3~": ## Delete
55
+ key = curses.KEY_DC
56
+ elif key_str == "[3;2~": ## Shift+Delete
57
+ key = 383
58
+
59
+ ## Function Keys
60
+ elif key2 == ord('O') and key3 == ord('P'):
61
+ key = curses.KEY_F1
62
+ elif key_str == "OQ":
63
+ key = curses.KEY_F2
64
+ elif key_str == "OR":
65
+ key = curses.KEY_F3
66
+ elif key_str == "OS":
67
+ key = curses.KEY_F4
68
+ elif key_str == "[15~":
69
+ key = curses.KEY_F5
70
+ elif key_str == "[17~":
71
+ key = curses.KEY_F6
72
+ elif key_str == "[17~":
73
+ key = curses.KEY_F7
74
+ elif key_str == "[19~":
75
+ key = curses.KEY_F8
76
+ elif key_str == "[20~":
77
+ key = curses.KEY_F9
78
+ elif key_str == "[21~":
79
+ key = curses.KEY_F10
80
+ elif key_str == "[23~":
81
+ key = curses.KEY_F11
82
+ elif key_str == "[24~":
83
+ key = curses.KEY_F12
84
+
85
+ ## Alt+KEY
86
+ elif key2 >= ord('a') and key2 <= ord('z') and key3 == -1: ## Alt+[a-zA-Z]
87
+ key = keycodes.META_a + (key2 - ord('a'))
88
+ elif key2 >= ord('A') and key2 <= ord('Z') and key3 == -1: ## Alt+[a-zA-Z]
89
+ key = keycodes.META_A + (key2 - ord('A'))
90
+ elif key2 == ord('0') and key3 == -1: ## Alt+0
91
+ key = keycodes.META_0
92
+ elif key2 >= ord('1') and key2 <= ord('9') and key3 == -1: ## Alt+1-9
93
+ key = keycodes.META_1 + (key2 - ord('1'))
94
+ elif key2 == 127: ## Alt+BS
95
+ key = keycodes.META_BS
96
+
97
+ # If it is an unknown key with an escape sequence then return -1.
98
+ elif key2 != -1:
99
+ key = -1
100
+
101
+
102
+ else:
103
+ key = -1
104
+ return key
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: listpick
3
- Version: 0.1.14.8
3
+ Version: 0.1.14.10
4
4
  Summary: Listpick is a powerful TUI data tool for creating TUI apps or viewing/comparing tabulated data.
5
5
  Home-page: https://github.com/grimandgreedy/listpick
6
6
  Author: Grim
@@ -35,7 +35,6 @@ src/listpick/ui/footer.py
35
35
  src/listpick/ui/git-bugreport-2025-08-16-1438.txt
36
36
  src/listpick/ui/help_screen.py
37
37
  src/listpick/ui/input_field.py
38
- src/listpick/ui/keycodes.py
39
38
  src/listpick/ui/keys.py
40
39
  src/listpick/ui/pane_stuff.py
41
40
  src/listpick/ui/picker_colours.py
@@ -45,6 +44,7 @@ src/listpick/utils/config.py
45
44
  src/listpick/utils/dump.py
46
45
  src/listpick/utils/filtering.py
47
46
  src/listpick/utils/generate_data.py
47
+ src/listpick/utils/keycodes.py
48
48
  src/listpick/utils/options_selectors.py
49
49
  src/listpick/utils/paste_operations.py
50
50
  src/listpick/utils/picker_log.py
@@ -52,6 +52,7 @@ src/listpick/utils/search_and_filter_utils.py
52
52
  src/listpick/utils/searching.py
53
53
  src/listpick/utils/sorting.py
54
54
  src/listpick/utils/table_to_list_of_lists.py
55
+ src/listpick/utils/user_input.py
55
56
  src/listpick/utils/utils.py
56
57
  tests/kitty_control.sh
57
58
  tests/sorting_dates.csv
@@ -1,70 +0,0 @@
1
- # Define constants for alt+a to alt+z
2
- META_A = 1000
3
- META_B = 1001
4
- META_C = 1002
5
- META_D = 1003
6
- META_E = 1004
7
- META_F = 1005
8
- META_G = 1006
9
- META_H = 1007
10
- META_I = 1008
11
- META_J = 1009
12
- META_K = 1010
13
- META_L = 1011
14
- META_M = 1012
15
- META_N = 1013
16
- META_O = 1014
17
- META_P = 1015
18
- META_Q = 1016
19
- META_R = 1017
20
- META_S = 1018
21
- META_T = 1019
22
- META_U = 1020
23
- META_V = 1021
24
- META_W = 1022
25
- META_X = 1023
26
- META_Y = 1024
27
- META_Z = 1025
28
-
29
- # Define constants for alt+A to alt+Z (using uppercase)
30
- META_a = 1050
31
- META_b = 1051
32
- META_c = 1052
33
- META_d = 1053
34
- META_e = 1054
35
- META_f = 1055
36
- META_g = 1056
37
- META_h = 1057
38
- META_i = 1058
39
- META_j = 1059
40
- META_k = 1060
41
- META_l = 1061
42
- META_m = 1062
43
- META_n = 1063
44
- META_o = 1064
45
- META_p = 1065
46
- META_q = 1066
47
- META_r = 1067
48
- META_s = 1068
49
- META_t = 1069
50
- META_u = 1070
51
- META_v = 1071
52
- META_w = 1072
53
- META_x = 1073
54
- META_y = 1074
55
- META_z = 1075
56
-
57
- # Dictionary to map character to its constant value
58
- META_KEY_MAP = {
59
- 'a': META_a, 'b': META_b, 'c': META_c, 'd': META_d, 'e': META_e,
60
- 'f': META_f, 'g': META_g, 'h': META_h, 'i': META_i, 'j': META_j,
61
- 'k': META_k, 'l': META_l, 'm': META_m, 'n': META_n, 'o': META_o,
62
- 'p': META_p, 'q': META_q, 'r': META_r, 's': META_s, 't': META_t,
63
- 'u': META_u, 'v': META_v, 'w': META_w, 'x': META_x, 'y': META_y,
64
- 'z': META_z, 'A': META_A, 'B': META_B, 'C': META_C, 'D': META_D,
65
- 'E': META_E, 'F': META_F, 'G': META_G, 'H': META_H, 'I': META_I,
66
- 'J': META_J, 'K': META_K, 'L': META_L, 'M': META_M, 'N': META_N,
67
- 'O': META_O, 'P': META_P, 'Q': META_Q, 'R': META_R, 'S': META_S,
68
- 'T': META_T, 'U': META_U, 'V': META_V, 'W': META_W, 'X': META_X,
69
- 'Y': META_Y, 'Z': META_Z
70
- }
File without changes
File without changes
File without changes
File without changes
File without changes