listpick 0.1.14.4__tar.gz → 0.1.14.6__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 (59) hide show
  1. {listpick-0.1.14.4 → listpick-0.1.14.6}/CHANGELOG.md +15 -0
  2. {listpick-0.1.14.4/src/listpick.egg-info → listpick-0.1.14.6}/PKG-INFO +1 -1
  3. {listpick-0.1.14.4 → listpick-0.1.14.6}/TODO.md +19 -2
  4. {listpick-0.1.14.4 → listpick-0.1.14.6}/setup.py +1 -1
  5. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/listpick_app.py +143 -36
  6. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/ui/footer.py +10 -2
  7. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/ui/input_field.py +29 -6
  8. listpick-0.1.14.6/src/listpick/ui/keycodes.py +70 -0
  9. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/ui/keys.py +5 -5
  10. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/table_to_list_of_lists.py +163 -55
  11. {listpick-0.1.14.4 → listpick-0.1.14.6/src/listpick.egg-info}/PKG-INFO +1 -1
  12. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick.egg-info/SOURCES.txt +1 -0
  13. {listpick-0.1.14.4 → listpick-0.1.14.6}/.gitignore +0 -0
  14. {listpick-0.1.14.4 → listpick-0.1.14.6}/LICENSE.txt +0 -0
  15. {listpick-0.1.14.4 → listpick-0.1.14.6}/README.md +0 -0
  16. {listpick-0.1.14.4 → listpick-0.1.14.6}/assets/aria2tui_screenshot.png +0 -0
  17. {listpick-0.1.14.4 → listpick-0.1.14.6}/assets/file_compare.png +0 -0
  18. {listpick-0.1.14.4 → listpick-0.1.14.6}/assets/lpfman.png +0 -0
  19. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/data_generation/list_files.toml +0 -0
  20. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/data_generation/list_files_empty.toml +0 -0
  21. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/data_generation/video_duplicates.toml +0 -0
  22. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/data_generation/video_mediainfo.toml +0 -0
  23. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/input_files/polynomials.tsv +0 -0
  24. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/input_guides/gnuplot_graph.md +0 -0
  25. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/picker/auxiallary_files/2024-25_Premier_League.pkl +0 -0
  26. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/picker/footer_string_example.py +0 -0
  27. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/picker/picker_example.py +0 -0
  28. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/picker/template.py +0 -0
  29. {listpick-0.1.14.4 → listpick-0.1.14.6}/examples/picker/wikipedia_table.py +0 -0
  30. {listpick-0.1.14.4 → listpick-0.1.14.6}/listpick.py +0 -0
  31. {listpick-0.1.14.4 → listpick-0.1.14.6}/requirements.txt +0 -0
  32. {listpick-0.1.14.4 → listpick-0.1.14.6}/setup.cfg +0 -0
  33. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/__init__.py +0 -0
  34. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/__main__.py +0 -0
  35. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/ui/__init__.py +0 -0
  36. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/ui/build_help.py +0 -0
  37. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/ui/git-bugreport-2025-08-16-1438.txt +0 -0
  38. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/ui/help_screen.py +0 -0
  39. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/ui/pane_stuff.py +0 -0
  40. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/ui/picker_colours.py +0 -0
  41. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/__init__.py +0 -0
  42. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/clipboard_operations.py +0 -0
  43. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/config.py +0 -0
  44. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/dump.py +0 -0
  45. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/filtering.py +0 -0
  46. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/generate_data.py +0 -0
  47. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/options_selectors.py +0 -0
  48. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/paste_operations.py +0 -0
  49. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/picker_log.py +0 -0
  50. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/search_and_filter_utils.py +0 -0
  51. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/searching.py +0 -0
  52. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/sorting.py +0 -0
  53. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick/utils/utils.py +0 -0
  54. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick.egg-info/dependency_links.txt +0 -0
  55. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick.egg-info/entry_points.txt +0 -0
  56. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick.egg-info/requires.txt +0 -0
  57. {listpick-0.1.14.4 → listpick-0.1.14.6}/src/listpick.egg-info/top_level.txt +0 -0
  58. {listpick-0.1.14.4 → listpick-0.1.14.6}/tests/kitty_control.sh +0 -0
  59. {listpick-0.1.14.4 → listpick-0.1.14.6}/tests/sorting_dates.csv +0 -0
@@ -7,6 +7,21 @@ Note that the changes between 0.1.11.0 and 1.1.12.0 are listed under 0.1.11
7
7
  - Added __sizeof__() function for the Picker class.
8
8
  - Fixed rows resizing twice when opening/switching between some files.
9
9
  - Added to settings: goto row, goto column
10
+ - NaN replaced with empty string when loading empty cells from xlsx or ods files.
11
+ - Added Picker.get_config(path) method.
12
+ - Can now take input on stdin -- e.g., `du -h | listpick --stdin`
13
+ - We now get user input via '/dev/tty' rather than stdscr.getch() (which uses stdin).
14
+ - This was necessary to ensure that we can pipe data in via stdin and still receive user input.
15
+ - Bugs fixed:
16
+ - Closing files causes issues switching between files.
17
+ - Create ~/.config/listpick directory for storing input history if it doesn't already exist.
18
+ - Added error checking when opening files.
19
+ - Added --headerless flag to prevent interpreting the first line of the input as the header.
20
+ - Fixed special keys not working:
21
+ - arrow keys (main picker)
22
+ - meta+key (input_field)
23
+ - Added column number to footer
24
+ - Improved splitting of whitespace separated data passed on stdin.
10
25
 
11
26
  ## [0.1.14] 2025-08-20
12
27
  - 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.4
3
+ Version: 0.1.14.6
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
@@ -12,7 +12,16 @@ ASAP
12
12
  > - [ ] Add keys for next page, next sheet, info
13
13
  > - [ ] Sort out what to do when the width of the columns is less than the terminal.
14
14
  > - [ ] Sheet states remain the same when switching files.
15
- > - [ ] nan in xlsx and ods files.
15
+ > - [x] Deal with nan values in xlsx and ods files.
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
23
+ > - [x] Arrow keys
24
+
16
25
 
17
26
 
18
27
 
@@ -109,7 +118,7 @@ ASAP
109
118
  > - [ ] Implement default_option_selector (!!!)
110
119
  > - [ ] Implement macros
111
120
  > - [ ] e.g., ctrl+shift+r turns off auto refresh
112
- > - [ ] Implement config loading
121
+ > - [x] Implement config loading
113
122
  > - [ ] Allow config to be passed as an argument.
114
123
  > - [ ] Make aria2tui compatible with windows.
115
124
  > - [ ] Add different highlight styles:
@@ -129,6 +138,9 @@ ASAP
129
138
  > - [x] Basic log setup complete
130
139
  > - [ ] Need more useful messages to determine where an error is thrown.
131
140
  > - [ ] Pin cursor.
141
+ > - [x] Allow data to be piped into listpick.
142
+ > - [x] When data is piped in it links stdin with the pipe...
143
+ > - [x] Rather than getting user input from stdin we can get it by getting a fd for /dev/tty and reading chars from there.
132
144
 
133
145
 
134
146
 
@@ -352,6 +364,11 @@ ASAP
352
364
  > - [ ] If the longest string in a column is the header string and show_header=False, then get_column_widths still calculates based on the header string length.
353
365
  > - [x] Rows sometimes adjust position a second time after switching files.
354
366
  > - [x] Was due to the height of the footer being set only at the end of the Footer.draw() method. Created a new Footer.adjust_sizes() method which is run at the start of the draw_screen() loop.
367
+ > - [x] Issues switching between open files when some of them have been closed:
368
+ > - [x] Wrong data for some files.
369
+ > - [x] Some can't be switched to...
370
+ > - [x] Fixed.
371
+ >
355
372
 
356
373
 
357
374
 
@@ -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.4",
19
+ version = "0.1.14.6",
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.",
@@ -20,6 +20,8 @@ import json
20
20
  import threading
21
21
  import string
22
22
  import logging
23
+ import tty
24
+ import select
23
25
 
24
26
  from listpick.ui.picker_colours import get_colours, get_help_colours, get_notification_colours, get_theme_count, get_fallback_colours
25
27
  from listpick.utils.options_selectors import default_option_input, output_file_option_selector, default_option_selector
@@ -108,7 +110,7 @@ class Picker:
108
110
  cell_selections: dict[tuple[int,int], bool] = {},
109
111
  selected_cells_by_row: dict = {},
110
112
  highlight_full_row: bool =False,
111
- cell_cursor: bool = False,
113
+ cell_cursor: bool = True,
112
114
 
113
115
  items_per_page : int = -1,
114
116
  sort_method : int = 0,
@@ -349,6 +351,28 @@ class Picker:
349
351
  if not attr_name.startswith('__') and not callable(getattr(self, attr_name)):
350
352
  size += sys.getsizeof(getattr(self, attr_name))
351
353
  return size
354
+
355
+ def set_config(self, path: str ="~/.config/listpick/config.toml"):
356
+ """ Set config from toml file. """
357
+ config = self.get_config(path)
358
+ self.logger.info(f"function: set_config()")
359
+ if "general" in config:
360
+ for key, val in config["general"].items():
361
+ self.logger.info(f"set_config: key={key}, val={val}.")
362
+ try:
363
+ setattr(self, key, val)
364
+ except Exception as e:
365
+ self.logger.error(f"set_config: key={key}, val={val}. {e}")
366
+
367
+
368
+ def get_config(self, path: str ="~/.config/listpick/config.toml") -> dict:
369
+ """ Get config from file. """
370
+ self.logger.info(f"function: get_config()")
371
+ import toml
372
+ if os.path.exists(os.path.expanduser(path)):
373
+ with open(os.path.expanduser(path), "r") as f:
374
+ config = toml.load(f)
375
+ return config
352
376
 
353
377
  def calculate_section_sizes(self):
354
378
  """
@@ -1957,10 +1981,12 @@ class Picker:
1957
1981
  self.items, self.header = tmp_items, tmp_header
1958
1982
  self.data_ready = True
1959
1983
 
1960
- def save_input_history(self, file_path: str) -> bool:
1984
+ def save_input_history(self, file_path: str, force_save: bool=True) -> bool:
1961
1985
  """ Save input field history. Returns True if successful save. """
1962
1986
  self.logger.info(f"function: save_input_history()")
1963
1987
  file_path = os.path.expanduser(file_path)
1988
+ file_path = os.path.expandvars(file_path)
1989
+ directory = os.path.dirname(file_path)
1964
1990
  history_dict = {
1965
1991
  "history_filter_and_search" : self.history_filter_and_search,
1966
1992
  "history_pipes" : self.history_pipes,
@@ -1968,8 +1994,10 @@ class Picker:
1968
1994
  "history_edits" : self.history_edits,
1969
1995
  "history_settings": self.history_settings,
1970
1996
  }
1971
- with open(file_path, 'w') as f:
1972
- json.dump(history_dict, f)
1997
+ if os.path.exists(directory) or force_save:
1998
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
1999
+ with open(file_path, 'w') as f:
2000
+ json.dump(history_dict, f)
1973
2001
 
1974
2002
  return True
1975
2003
 
@@ -2067,27 +2095,32 @@ class Picker:
2067
2095
  self.notification(self.stdscr, message = f"File not found: {filename}")
2068
2096
  return None
2069
2097
 
2070
- filetype = guess_file_type(filename)
2071
- items, header, sheets = table_to_list(filename, file_type=filetype)
2098
+ try:
2099
+ filetype = guess_file_type(filename)
2100
+ items, header, sheets = table_to_list(filename, file_type=filetype)
2072
2101
 
2073
- if items != None:
2074
- self.items = items
2075
- self.header = header if header != None else []
2076
- self.sheets = sheets
2102
+ if items != None:
2103
+ self.items = items
2104
+ self.header = header if header != None else []
2105
+ self.sheets = sheets
2077
2106
 
2078
2107
 
2079
- self.initialise_variables()
2108
+ self.initialise_variables()
2109
+ except Exception as e:
2110
+ self.notification(self.stdscr, message=f"Error loading {filename}: {e}")
2080
2111
 
2081
2112
  def load_sheet(self, filename: str, sheet_number: int = 0):
2082
2113
  filetype = guess_file_type(filename)
2083
- items, header, sheets = table_to_list(filename, file_type=filetype, sheet_number=sheet_number)
2084
-
2085
- if items != None:
2086
- self.items = items
2087
- self.header = header if header != None else []
2088
- self.sheets = sheets
2114
+ try:
2115
+ items, header, sheets = table_to_list(filename, file_type=filetype, sheet_number=sheet_number)
2116
+ if items != None:
2117
+ self.items = items
2118
+ self.header = header if header != None else []
2119
+ self.sheets = sheets
2089
2120
 
2090
- self.initialise_variables()
2121
+ self.initialise_variables()
2122
+ except Exception as e:
2123
+ self.notification(self.stdscr, message=f"Error loading {filename}, sheet {sheet_number}: {e}")
2091
2124
 
2092
2125
  def switch_file(self, increment=1) -> None:
2093
2126
  """ Go to the next file. """
@@ -2100,6 +2133,7 @@ class Picker:
2100
2133
  self.loaded_file_index = (self.loaded_file_index + increment) % len(self.loaded_files)
2101
2134
  self.loaded_file = self.loaded_files[self.loaded_file_index]
2102
2135
 
2136
+ idx, file = self.loaded_file_index, self.loaded_file
2103
2137
  # If we already have a loaded state for this file
2104
2138
  if self.loaded_file_states[self.loaded_file_index]:
2105
2139
  self.set_function_data(self.loaded_file_states[self.loaded_file_index])
@@ -2107,6 +2141,7 @@ class Picker:
2107
2141
  self.set_function_data({}, reset_absent_variables=True)
2108
2142
  self.load_file(self.loaded_file)
2109
2143
 
2144
+ self.loaded_file_index, self.loaded_file = idx, file
2110
2145
 
2111
2146
  def switch_sheet(self, increment=1) -> None:
2112
2147
  if not os.path.exists(self.loaded_file):
@@ -2179,10 +2214,14 @@ class Picker:
2179
2214
  function_data = self.get_function_data()
2180
2215
  return [], "", function_data
2181
2216
 
2182
- # Main loop
2217
+ # Open tty to accept input
2218
+ tty_fd = open_tty()
2183
2219
 
2220
+ # Main loop
2184
2221
  while True:
2185
- key = self.stdscr.getch()
2222
+ # key = self.stdscr.getch()
2223
+
2224
+ key = get_char(tty_fd, timeout=0.2)
2186
2225
  if key != -1:
2187
2226
  self.logger.info(f"key={key}")
2188
2227
  h, w = self.stdscr.getmaxyx()
@@ -2386,6 +2425,7 @@ class Picker:
2386
2425
  del self.loaded_file_states[self.loaded_file_index]
2387
2426
  self.loaded_file_index = min(self.loaded_file_index, len(self.loaded_files)-1)
2388
2427
  self.loaded_file = self.loaded_files[self.loaded_file_index]
2428
+ idx, file = self.loaded_file_index, self.loaded_file
2389
2429
 
2390
2430
 
2391
2431
  # If we already have a loaded state for this file
@@ -2394,6 +2434,7 @@ class Picker:
2394
2434
  else:
2395
2435
  self.set_function_data({}, reset_absent_variables=True)
2396
2436
  self.load_file(self.loaded_file)
2437
+ self.loaded_file_index, self.loaded_file = idx, file
2397
2438
  self.draw_screen(self.indexed_items, self.highlights)
2398
2439
 
2399
2440
  elif self.check_key("full_exit", key, self.keys_dict):
@@ -2421,7 +2462,7 @@ class Picker:
2421
2462
  formula_auto_complete=False,
2422
2463
  function_auto_complete=False,
2423
2464
  word_auto_complete=True,
2424
- auto_complete_words=["ft", "ct", "cv"]
2465
+ auto_complete_words=["ft", "ct", "cv"],
2425
2466
  )
2426
2467
  if return_val:
2427
2468
  self.user_settings = usrtxt
@@ -3402,10 +3443,11 @@ def parse_arguments() -> Tuple[argparse.Namespace, dict]:
3402
3443
  parser.add_argument('--stdin', dest='stdin', action='store_true', help='Table passed on stdin')
3403
3444
  parser.add_argument('--stdin2', action='store_true', help='Table passed on stdin')
3404
3445
  parser.add_argument('--generate', '-g', type=str, help='Pass file to generate data for listpick Picker.')
3405
- parser.add_argument('-d', dest='delimiter', default='\t', help='Delimiter for rows in the table (default: tab)')
3446
+ parser.add_argument('--delimiter', '-d', dest='delimiter', default='\t', help='Delimiter for rows in the table (default: tab)')
3406
3447
  parser.add_argument('-t', dest='file_type', choices=['tsv', 'csv', 'json', 'xlsx', 'ods', 'pkl'], help='Type of file (tsv, csv, json, xlsx, ods)')
3407
3448
  parser.add_argument('--debug', action="store_true", help="Enable debug log.")
3408
3449
  parser.add_argument('--debug-verbose', action="store_true", help="Enable debug verbose log.")
3450
+ parser.add_argument('--headerless', action="store_false", help="By default the first row is interpreted as a header row. If --headerless is passed then there is no header.")
3409
3451
  args = parser.parse_args()
3410
3452
 
3411
3453
 
@@ -3458,13 +3500,31 @@ def parse_arguments() -> Tuple[argparse.Namespace, dict]:
3458
3500
  filetype = args.file_type
3459
3501
 
3460
3502
 
3461
- items, header, sheets = table_to_list(input_arg, args.delimiter, filetype)
3503
+ while True:
3504
+ try:
3505
+ items, header, sheets = table_to_list(
3506
+ input_arg=input_arg,
3507
+ delimiter=args.delimiter,
3508
+ file_type = filetype,
3509
+ first_row_is_header=args.headerless,
3510
+ )
3511
+ if args.file:
3512
+ function_data["loaded_file"] = args.file[0]
3513
+ function_data["loaded_files"] = args.file
3514
+ break
3515
+
3516
+ except Exception as e:
3517
+ items, header, sheets = [], [], []
3518
+ function_data["startup_notification"] = f"Error loading {input_arg}. {e}"
3519
+ if args.file:
3520
+ args.file = args.file[1:]
3521
+ input_arg = args.file[0]
3522
+ else:
3523
+ break
3524
+
3462
3525
  function_data["items"] = items
3463
3526
  if header: function_data["header"] = header
3464
3527
  function_data["sheets"] = sheets
3465
- if args.file:
3466
- function_data["loaded_file"] = args.file[0]
3467
- function_data["loaded_files"] = args.file
3468
3528
 
3469
3529
  return args, function_data
3470
3530
 
@@ -3505,6 +3565,52 @@ def unrestrict_curses(stdscr: curses.window) -> None:
3505
3565
  curses.raw() # Disable control keys (ctrl-c, ctrl-s, ctrl-q, etc.)
3506
3566
  curses.curs_set(False)
3507
3567
 
3568
+
3569
+ def open_tty():
3570
+ """ Return a file descriptor for the tty that we are opening"""
3571
+ tty_fd = os.open('/dev/tty', os.O_RDONLY)
3572
+ tty.setraw(tty_fd)
3573
+ return tty_fd
3574
+
3575
+ def get_char(tty_fd, timeout: float = 0.2, secondary: bool = False) -> int:
3576
+ """ Get character from a tty_fd with a timeout. """
3577
+ rlist, _, _ = select.select([tty_fd], [], [], timeout)
3578
+ if rlist:
3579
+ # key = ord(tty_fd.read(1))
3580
+ key = ord(os.read(tty_fd, 1))
3581
+ if not secondary:
3582
+ if key == 27:
3583
+ key2 = get_char(tty_fd, timeout=0.01, secondary=True)
3584
+ key3 = get_char(tty_fd, timeout=0.01, secondary=True)
3585
+ key4 = get_char(tty_fd, timeout=0.01, secondary=True)
3586
+ key5 = get_char(tty_fd, timeout=0.01, secondary=True)
3587
+ if key2 == ord('O') and key3 == ord('B'):
3588
+ key = curses.KEY_DOWN
3589
+ elif key2 == ord('O') and key3 == ord('A'):
3590
+ key = curses.KEY_UP
3591
+ elif key2 == ord('O') and key3 == ord('D'):
3592
+ key = curses.KEY_LEFT
3593
+ elif key2 == ord('O') and key3 == ord('C'):
3594
+ key = curses.KEY_RIGHT
3595
+ elif key2 == ord('[') and key3 == ord('Z'):
3596
+ key = 353
3597
+ elif key2 == ord('O') and key3 == ord('F'):
3598
+ key = curses.KEY_END
3599
+ elif key2 == ord('O') and key3 == ord('H'):
3600
+ key = curses.KEY_HOME
3601
+ elif key2 == ord('[') and key3 == ord('3') and key4 == ord('~'):
3602
+ key = curses.KEY_DC
3603
+ elif key2 == ord('[') and key3 == ord('3') and key4 == ord('~'):
3604
+ key = curses.KEY_DC
3605
+ elif key2 == ord('O') and key3 == ord('P'):
3606
+ key = curses.KEY_F1
3607
+ elif key2 == ord('[') and key3 == ord('1') and key4 == ord('5') and key5 == ord('~'):
3608
+ key = curses.KEY_F5
3609
+
3610
+ else:
3611
+ key = -1
3612
+ return key
3613
+
3508
3614
  def main() -> None:
3509
3615
  """ Main function when listpick is executed. Deals with command line arguments and starts a Picker. """
3510
3616
  args, function_data = parse_arguments()
@@ -3540,16 +3646,16 @@ def main() -> None:
3540
3646
  "color": 8,
3541
3647
  }
3542
3648
  ]
3543
- function_data["cell_cursor"] = True
3544
- function_data["display_modes"] = True
3545
- function_data["centre_in_cols"] = True
3546
- function_data["show_row_header"] = True
3547
- function_data["keys_dict"] = picker_keys
3548
- function_data["id_column"] = -1
3549
- function_data["track_entries_upon_refresh"] = True
3550
- function_data["centre_in_terminal_vertical"] = True
3551
- function_data["highlight_full_row"] = True
3552
- function_data["pin_cursor"] = True
3649
+ # function_data["cell_cursor"] = True
3650
+ # function_data["display_modes"] = True
3651
+ # function_data["centre_in_cols"] = True
3652
+ # function_data["show_row_header"] = True
3653
+ # function_data["keys_dict"] = picker_keys
3654
+ # function_data["id_column"] = -1
3655
+ # function_data["track_entries_upon_refresh"] = True
3656
+ # function_data["centre_in_terminal_vertical"] = True
3657
+ # function_data["highlight_full_row"] = True
3658
+ # function_data["pin_cursor"] = True
3553
3659
  # function_data["display_infobox"] = True
3554
3660
  # function_data["infobox_items"] = [["1"], ["2"], ["3"]]
3555
3661
  # function_data["infobox_title"] = "Title"
@@ -3572,6 +3678,7 @@ def main() -> None:
3572
3678
 
3573
3679
  # app = Picker(stdscr, **function_data)
3574
3680
  app = Picker(stdscr)
3681
+ app.set_config("~/.config/listpick/config.toml")
3575
3682
  app.set_function_data(function_data)
3576
3683
  app.splash_screen("Listpick is loading your data...")
3577
3684
  app.load_input_history("~/.config/listpick/cmdhist.json")
@@ -94,9 +94,11 @@ class StandardFooter(Footer):
94
94
 
95
95
 
96
96
 
97
+ ## Clear background of footer rows
97
98
  for i in range(self.height):
98
99
  self.stdscr.addstr(h-self.height+i, 0, ' '*(w-1), curses.color_pair(self.colours_start+20))
99
100
 
101
+ # Display loaded files
100
102
  if len(state["loaded_files"]) > 1 and state["loaded_file"] in state["loaded_files"]:
101
103
 
102
104
  sep = "◢ "
@@ -172,17 +174,23 @@ class StandardFooter(Footer):
172
174
 
173
175
 
174
176
 
177
+
178
+ ## Cursor selection mode
175
179
  select_mode = "C"
176
180
  if state["is_selecting"]: select_mode = "VS"
177
181
  elif state["is_deselecting"]: select_mode = "VDS"
178
182
  if state["pin_cursor"]: select_mode = f"{select_mode} "
183
+
179
184
  # Cursor & selection info
180
185
  selected_count = sum(state["selections"].values())
181
186
  if state["paginate"]:
182
- # cursor_disp_str = f" [{selected_count}] {state['cursor_pos']+1}/{len(state['indexed_items'])} | Page {state['cursor_pos']//state['items_per_page']}/{len(state['indexed_items'])//state['items_per_page']} | {select_mode}"
183
187
  cursor_disp_str = f" [{selected_count}] {state['cursor_pos']+1}/{len(state['indexed_items'])} | Page {state['cursor_pos']//state['items_per_page']}/{len(state['indexed_items'])//state['items_per_page']} | {select_mode}"
184
188
  else:
185
- cursor_disp_str = f" [{selected_count}] {state['cursor_pos']+1}/{len(state['indexed_items'])} | {select_mode}"
189
+ # cursor_disp_str = f" [{selected_count}] {state['cursor_pos']+1},{state['selected_column']}/{len(state['indexed_items'])},{len(state['column_widths'])} | {select_mode}"
190
+ if state["cell_cursor"]:
191
+ cursor_disp_str = f" [{selected_count}] {state['cursor_pos']+1}/{len(state['indexed_items'])} | {state['selected_column']}/{len(state['column_widths'])} | {select_mode}"
192
+ else:
193
+ cursor_disp_str = f" [{selected_count}] {state['cursor_pos']+1}/{len(state['indexed_items'])} | {select_mode}"
186
194
 
187
195
  # Maximum chars that should be displayed
188
196
  max_chars = min(len(cursor_disp_str)+2, w)
@@ -16,7 +16,25 @@ from datetime import datetime
16
16
  import logging
17
17
 
18
18
  logger = logging.getLogger('picker_log')
19
-
19
+ import select
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
37
+
20
38
  def input_field(
21
39
  stdscr: curses.window,
22
40
  usrtxt:str="",
@@ -106,6 +124,8 @@ def input_field(
106
124
  offscreen_x, offscreen_y = False, False
107
125
  orig_x, orig_y = x, y
108
126
 
127
+ tty_fd = open_tty()
128
+
109
129
  # Input field loop
110
130
  while True:
111
131
 
@@ -131,7 +151,7 @@ def input_field(
131
151
 
132
152
  # Clear background to end of the input field
133
153
  stdscr.addstr(field_y, x(), " "*(max_field_length), curses.color_pair(colours_start+colour_pair_bg))
134
- stdscr.refresh()
154
+ # stdscr.refresh()
135
155
 
136
156
  if literal:
137
157
  field_string_length = len(repr(usrtxt)) + len(field_prefix)
@@ -206,12 +226,15 @@ def input_field(
206
226
  pass
207
227
 
208
228
 
209
- key = stdscr.getch()
229
+ stdscr.refresh()
230
+ key = get_char(tty_fd, timeout=0.5)
231
+ # key = stdscr.getch()
210
232
 
211
233
  if key in [27, 7]: # ESC/ALT key or Ctrl+g
212
234
  # For Alt-key combinations: set nodelay and get the second key
213
- stdscr.nodelay(True)
214
- key2 = stdscr.getch()
235
+ # stdscr.nodelay(True)
236
+ # key2 = stdscr.getch()
237
+ key2 = get_char(tty_fd, timeout=0.05)
215
238
 
216
239
  if key2 == -1: # ESCAPE key (no key-combination)
217
240
  stdscr.nodelay(False)
@@ -311,7 +334,7 @@ def input_field(
311
334
  curses.endwin()
312
335
  exit()
313
336
 
314
- elif key == 10: # Enter/return key
337
+ elif key in [10, 13]: # Enter/return key
315
338
  # Return
316
339
  return usrtxt, True
317
340
 
@@ -0,0 +1,70 @@
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
+ }
@@ -32,7 +32,7 @@ picker_keys = {
32
32
  "select_none": [ord('M'), 18], # Ctrl-r
33
33
  "visual_selection_toggle": [ord('v')],
34
34
  "visual_deselection_toggle": [ord('V')],
35
- "enter": [ord('\n'), curses.KEY_ENTER],
35
+ "enter": [ord('\n'), curses.KEY_ENTER, 13],
36
36
  "redraw_screen": [12], # Ctrl-l
37
37
  "cycle_sort_method": [ord('s')],
38
38
  "cycle_sort_method_reverse": [ord('S')],
@@ -122,7 +122,7 @@ help_keys = {
122
122
 
123
123
 
124
124
  notification_keys = {
125
- "exit": [ord('q'), ord('h'), curses.KEY_ENTER, ord('\n'), ord(' '), 27],
125
+ "exit": [ord('q'), ord('h'), curses.KEY_ENTER, ord('\n'), ord(' '), 27, 13],
126
126
  "full_exit": [3], # Ctrl+c
127
127
  "cursor_down": [ord('j'), curses.KEY_DOWN],
128
128
  "cursor_up": [ord('k'), curses.KEY_UP],
@@ -152,7 +152,7 @@ menu_keys = {
152
152
  "cursor_top": [ord('g'), curses.KEY_HOME],
153
153
  "five_up": [ord('K')],
154
154
  "five_down": [ord('J')],
155
- "enter": [ord('\n'), curses.KEY_ENTER, ord('l')],
155
+ "enter": [ord('\n'), curses.KEY_ENTER, ord('l'), 13],
156
156
  "redraw_screen": [12], # Ctrl-l
157
157
  "filter_input": [ord('f')],
158
158
  "search_input": [ord('/')],
@@ -183,7 +183,7 @@ options_keys = {
183
183
  "select_none": [ord('M'), 18], # Ctrl-r
184
184
  "visual_selection_toggle": [ord('v')],
185
185
  "visual_deselection_toggle": [ord('V')],
186
- "enter": [ord('\n'), curses.KEY_ENTER, ord('l')],
186
+ "enter": [ord('\n'), curses.KEY_ENTER, ord('l'), 13],
187
187
  "redraw_screen": [12], # Ctrl-l
188
188
  "cycle_sort_method": [ord('s')],
189
189
  "cycle_sort_method_reverse": [ord('S')],
@@ -213,7 +213,7 @@ edit_menu_keys = {
213
213
  "cursor_top": [ord('g'), curses.KEY_HOME],
214
214
  "five_up": [ord('K')],
215
215
  "five_down": [ord('J')],
216
- "enter": [ord('\n'), curses.KEY_ENTER],
216
+ "enter": [ord('\n'), curses.KEY_ENTER, 13],
217
217
  "redraw_screen": [12], # Ctrl-l
218
218
  "cycle_sort_method": [ord('s')],
219
219
  "cycle_sort_method_reverse": [ord('S')],
@@ -36,13 +36,117 @@ def strip_whitespace(item: Iterable) -> Iterable:
36
36
  return item
37
37
 
38
38
 
39
+ def xlsx_to_list(file_name: str, sheet_number:int = 0, extract_formulae: bool = False, first_row_is_header: bool = True):
40
+ import pandas as pd
41
+ from openpyxl import load_workbook
42
+ # wb = load_workbook(filename=input_arg, read_only=True)
43
+ # values or formulae
44
+ if not os.path.exists(file_name):
45
+ return [], [], []
46
+ wb = load_workbook(filename=file_name, read_only=True, data_only=not extract_formulae)
47
+
48
+ if not isinstance(sheet_number, int): sheet_number = 0
49
+ sheet_number = max(0, min(sheet_number, len(wb.sheetnames)-1))
50
+ ws = wb.worksheets[sheet_number]
51
+
52
+ # Read data and formulas from the sheet
53
+ table_data = []
54
+ # table_data = [[cell for cell in row] for row in ws.iter_rows(min_row=1, values_only=False)]
55
+ # table_data = [[cell.value for cell in row] for row in ws.iter_rows(min_row=1, values_only=False)]
56
+ table_data = [[cell if cell != None else "" for cell in row] for row in ws.iter_rows(min_row=1, values_only=True)]
57
+ header = []
58
+ # header = [cell for cell in list(ws.iter_rows(values_only=True))[0]] # Assuming the first row is the header
59
+ if first_row_is_header and len(table_data) > 1:
60
+ header = table_data[0]
61
+ table_data = table_data[1:]
62
+ else:
63
+ header = []
64
+ #
65
+ # for row in ws.iter_rows(min_row=2, values_only=True): # Skip the header row
66
+ # row_data = []
67
+ # for cell in row:
68
+ # if isinstance(cell, str) and '=' in cell: # Check if it's a formula
69
+ # row_data.append(cell)
70
+ # else:
71
+ # row_data.append(str(cell))
72
+ # table_data.append(row_data)
73
+
74
+ return table_data, header, wb.sheetnames
75
+
76
+ def ods_to_list(filename: str, sheet_number: int = 0, extract_formulas: bool = False, first_row_is_header: bool = True):
77
+ from odf.opendocument import load
78
+ from odf import table, text
79
+
80
+ from odf.namespaces import TABLENS
81
+ # Load the ODS file
82
+ doc = load(filename)
83
+
84
+ sheets = doc.spreadsheet.getElementsByType(table.Table)
85
+ sheet_names = [s.attributes.get((TABLENS, 'name')) for s in sheets]
86
+
87
+
88
+ # Get the sheet by index
89
+ sheet = doc.spreadsheet.getElementsByType(table.Table)[sheet_number]
90
+
91
+ data = []
92
+ for row in sheet.getElementsByType(table.TableRow):
93
+ row_data = []
94
+ for cell in row.getElementsByType(table.TableCell):
95
+ if extract_formulas:
96
+ formula = cell.attributes.get((TABLENS, 'formula'))
97
+ if formula is not None:
98
+ row_data.append(formula)
99
+ continue # Skip extracting value if formula found
100
+
101
+ # Extract value (as text) from <text:p> elements
102
+ cell_text = ""
103
+ for p in cell.getElementsByType(text.P):
104
+ cell_text += str(p.firstChild) if p.firstChild is not None else ""
105
+ row_data.append(cell_text)
106
+ data.append(row_data)
107
+ if first_row_is_header and len(data) > 1:
108
+ header = data[0]
109
+ data = data[1:]
110
+ else:
111
+ header = []
112
+
113
+ return data, header, sheet_names
114
+
115
+ def ods_to_list_old(file_name: str, sheet_number:int = 0, extract_formulae: bool = False, first_row_is_header: bool = True):
116
+ try:
117
+ import pandas as pd
118
+ ef = pd.ExcelFile(file_name)
119
+ sheets = ef.sheet_names
120
+ sheet_number = max(0, min(sheet_number, len(sheets)-1))
121
+ df = pd.read_excel(file_name, engine='odf', sheet_name=sheet_number)
122
+ # if sheet_number < len(sheets):
123
+ # df = pd.read_excel(input_arg, engine='odf', sheet_name=sheet_number)
124
+ # else:
125
+ # df = pd.read_excel(input_arg, engine='odf')
126
+ table_data = df.values.tolist()
127
+ table_data = [[x if not pd.isna(x) else "" for x in row] for row in table_data]
128
+
129
+ try:
130
+ header = list(df.columns)
131
+ except:
132
+ header = []
133
+
134
+ if not first_row_is_header and header:
135
+ table_data = [header] + table_data
136
+ header = []
137
+
138
+ return table_data, header, sheets
139
+ except Exception as e:
140
+ print(f"Error loading ODS file: {e}")
141
+ return [], [], []
39
142
 
40
- def table_to_list(
41
143
 
42
- input_arg: str,
43
- delimiter:str='\t',
44
- file_type:Optional[str]=None,
45
- sheet_number:int = 0,
144
+ def table_to_list(
145
+ input_arg: str,
146
+ delimiter:str='\t',
147
+ file_type:Optional[str]=None,
148
+ sheet_number:int = 0,
149
+ first_row_is_header:bool = True,
46
150
 
47
151
  ) -> Tuple[list[list[str]], list[str], list[str]]:
48
152
  """
@@ -62,25 +166,60 @@ def table_to_list(
62
166
  logger.info("function: table_to_list (table_to_list_of_lists.py)")
63
167
  table_data = []
64
168
 
65
- def parse_csv_like(data:str, delimiter:str) -> list[list[str]]:
169
+ def parse_csv_like2(data:str, delimiter:str) -> list[list[str]]:
66
170
  """ Convert value-separated data (e.g., CSV or TSV) to list of lists. """
67
171
  logger.info("function: parse_csv_like (table_to_list_of_lists.py)")
68
172
 
69
173
  try:
70
- reader = csv.reader(StringIO(data), delimiter=delimiter)
174
+ # reader = csv.reader(StringIO(data), delimiter=delimiter)
175
+ reader = csv.reader(StringIO(data), dialect='unix')
71
176
  return [row for row in reader]
72
177
  except Exception as e:
73
178
  print(f"Error reading CSV-like input: {e}")
74
179
  return []
180
+ def parse_csv_like(data:str, delimiter: str=" "):
181
+ import re
182
+ def split_columns(line):
183
+ # Define the regex pattern to match quoted strings and split by whitespace
184
+ # pattern = r"(?:'[^']*'|[^'\s]+)"
185
+ pattern = r"(?:\"[^\"]*\"|'[^']*'|[^'\s]+)"
186
+
187
+ # Find all matches using the defined pattern
188
+ columns = re.findall(pattern, line)
189
+
190
+ return columns
191
+
192
+ lines = data.strip().split('\n')
193
+ result = []
194
+
195
+ for line in lines:
196
+ result.append(split_columns(line))
197
+
198
+ return result
75
199
 
76
- def csv_string_to_list(csv_string:str) -> list[list[str]]:
200
+ def csv_string_to_list(csv_string:str, first_row_is_header: bool = True) -> list[list[str]]:
77
201
  """ Convert csv string to list of lists using csv.reader. """
78
202
  logger.info("function: csv_string_to_list (table_to_list_of_lists.py)")
79
203
  f = StringIO(csv_string)
80
204
  reader = csv.reader(f, skipinitialspace=True)
81
- return [row for row in reader]
205
+ table_data = [row for row in reader]
206
+ if first_row_is_header and len(table_data) > 1:
207
+ header = table_data[0]
208
+ table_data = table_data[1:]
209
+ else:
210
+ header = []
211
+ return table_data, header
212
+
82
213
 
83
- if file_type == 'csv' or delimiter in [',']:
214
+ if input_arg == '--stdin':
215
+ input_data = sys.stdin.read()
216
+ elif input_arg == '--stdin2':
217
+ input_count = int(sys.stdin.readline())
218
+ input_data = "\n".join([sys.stdin.readline() for i in range(input_count)])
219
+ sys.stdin.flush()
220
+ # sys.stdin.close()
221
+ # sys.stdin = open('/dev/tty', 'r')
222
+ elif file_type == 'csv' or delimiter in [',']:
84
223
  try:
85
224
  if input_arg == '--stdin':
86
225
  input_data = sys.stdin.read()
@@ -90,10 +229,11 @@ def table_to_list(
90
229
  else:
91
230
  input_data = read_file_content(input_arg)
92
231
 
93
- table_data = csv_string_to_list(input_data)
232
+ table_data, header = csv_string_to_list(input_data, first_row_is_header)
94
233
  table_data = strip_whitespace(table_data)
234
+ header = strip_whitespace([header])[0]
95
235
  # table_data = parse_csv_like(input_data, ",")
96
- return table_data, [], []
236
+ return table_data, header, []
97
237
  except Exception as e:
98
238
  print(f"Error reading CSV/TSV input: {e}")
99
239
  return [], [], []
@@ -141,45 +281,12 @@ def table_to_list(
141
281
  return [], [], []
142
282
 
143
283
  elif file_type == 'xlsx':
144
- import pandas as pd
145
- ef = pd.ExcelFile(input_arg)
146
- sheets = ef.sheet_names
147
- sheet_number = min(0, max(sheet_number, len(sheets)-1))
148
- df = pd.read_excel(input_arg, engine='odf', sheet_name=sheet_number)
149
- # if sheet_number < len(sheets):
150
- # df = pd.read_excel(input_arg, sheet_name=sheet_number)
151
- # else:
152
- # df = pd.read_excel(input_arg)
153
- table_data = df.values.tolist()
154
- table_data = [[x if not pd.isna(x) else "" for x in row] for row in table_data]
155
- try:
156
- header = list(df.columns)
157
- except:
158
- header = []
159
- return table_data, header, sheets
284
+ extract_formulae = False
285
+ return xlsx_to_list(input_arg, sheet_number, extract_formulae, first_row_is_header)
160
286
 
161
287
  elif file_type == 'ods':
162
- try:
163
- import pandas as pd
164
- ef = pd.ExcelFile(input_arg)
165
- sheets = ef.sheet_names
166
- sheet_number = min(0, max(sheet_number, len(sheets)-1))
167
- df = pd.read_excel(input_arg, engine='odf', sheet_name=sheet_number)
168
- # if sheet_number < len(sheets):
169
- # df = pd.read_excel(input_arg, engine='odf', sheet_name=sheet_number)
170
- # else:
171
- # df = pd.read_excel(input_arg, engine='odf')
172
- table_data = df.values.tolist()
173
- table_data = [[x if not pd.isna(x) else "" for x in row] for row in table_data]
174
-
175
- try:
176
- header = list(df.columns)
177
- except:
178
- header = []
179
- return table_data, header, sheets
180
- except Exception as e:
181
- print(f"Error loading ODS file: {e}")
182
- return [], [], []
288
+ extract_formulae = False
289
+ return ods_to_list(input_arg, sheet_number, extract_formulae, first_row_is_header)
183
290
  elif file_type == 'pkl':
184
291
  with open(os.path.expandvars(os.path.expanduser(input_arg)), 'rb') as f:
185
292
  loaded_data = pickle.load(f)
@@ -187,17 +294,18 @@ def table_to_list(
187
294
  header = loaded_data["header"] if "header" in loaded_data else []
188
295
  return items, header, []
189
296
 
190
- if input_arg == '--stdin':
191
- input_data = sys.stdin.read()
192
- elif input_arg == '--stdin2':
193
- input_count = int(sys.stdin.readline())
194
- input_data = "\n".join([sys.stdin.readline() for i in range(input_count)])
195
297
  else:
196
298
  input_data = read_file_content(input_arg)
197
299
 
198
300
  table_data = parse_csv_like(input_data, delimiter)
301
+ if first_row_is_header and len(table_data) > 1:
302
+ header = table_data[0]
303
+ table_data = table_data[1:]
304
+ else:
305
+ header = []
306
+
199
307
 
200
- return table_data, [], []
308
+ return table_data, header, []
201
309
 
202
310
  if __name__ == '__main__':
203
311
  parser = argparse.ArgumentParser(description='Convert table to list of lists.')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: listpick
3
- Version: 0.1.14.4
3
+ Version: 0.1.14.6
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,6 +35,7 @@ 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
38
39
  src/listpick/ui/keys.py
39
40
  src/listpick/ui/pane_stuff.py
40
41
  src/listpick/ui/picker_colours.py
File without changes
File without changes
File without changes
File without changes
File without changes