listpick 0.1.15.19__py3-none-any.whl → 0.1.16.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
listpick/ui/keys.py CHANGED
@@ -51,8 +51,8 @@ picker_keys = {
51
51
  "continue_search_forward": [ord('n')],
52
52
  "continue_search_backward": [ord('N')],
53
53
  "cancel": [27], # Escape key
54
- "opts_input": [ord(':')],
55
- "opts_select": [ord('o')],
54
+ "opts_input": [keycodes.META_o],
55
+ # "opts_select": [ord('+')],
56
56
  "mode_next": [9], # Tab key
57
57
  "mode_prev": [353], # Shift+Tab key
58
58
  "pipe_input": [ord('|')],
@@ -62,13 +62,14 @@ picker_keys = {
62
62
  "col_select_prev": [ord('<')],
63
63
  "col_hide": [ord('!'), ord('@'), ord('#'), ord('$'), ord('%'), ord('^'), ord('&'), ord('*'), ord('('), ord(')')],
64
64
  "edit": [ord('e')],
65
- "edit_picker": [ord('E')],
65
+ # "edit_picker": [ord('E')],
66
+ "edit_nvim": [ord('E')],
66
67
  "edit_ipython": [5], # Ctrl+e
67
68
  "copy": [ord('y')],
68
69
  "paste": [ord('p')],
69
70
  "save": [19, ord('D')], # Ctrl+s
70
71
  "load": [15], # Ctrl+o
71
- "open": [ord('O')],
72
+ # "open": [ord('O')],
72
73
  "toggle_footer": [ord('_')],
73
74
  "notification_toggle": [ord('z')],
74
75
  "redo": [ord('.')],
@@ -90,6 +91,8 @@ picker_keys = {
90
91
  # "sheet_prev": [],
91
92
  "toggle_right_pane": [ord("'")],
92
93
  "cycle_right_pane": [ord('"')],
94
+ "toggle_left_pane": [ord(";")],
95
+ "cycle_left_pane": [ord(':')],
93
96
  }
94
97
 
95
98
 
@@ -104,8 +107,8 @@ help_keys = {
104
107
  "page_down": [curses.KEY_NPAGE, 6],
105
108
  "cursor_bottom": [ord('G'), curses.KEY_END],
106
109
  "cursor_top": [ord('g'), curses.KEY_HOME],
107
- "five_up": [ord('K')],
108
- "five_down": [ord('J')],
110
+ "five_up": [ord('K'), keycodes.META_k],
111
+ "five_down": [ord('J'), keycodes.META_j],
109
112
  "redraw_screen": [12], # Ctrl+l
110
113
  "cycle_sort_method": [ord('s')],
111
114
  "cycle_sort_method_reverse": [ord('S')],
@@ -235,8 +238,8 @@ edit_menu_keys = {
235
238
  "continue_search_forward": [ord('n'), ord('i')],
236
239
  "continue_search_backward": [ord('N'), ord('I')],
237
240
  "cancel": [27], # Escape key
238
- "opts_input": [ord(':')],
239
- "opts_select": [ord('o')],
241
+ "opts_input": [keycodes.META_o],
242
+ # "opts_select": [ord('+')],
240
243
  "mode_next": [9], # Tab key
241
244
  "mode_prev": [353], # Shift+Tab key
242
245
  "pipe_input": [ord('|')],
@@ -246,11 +249,14 @@ edit_menu_keys = {
246
249
  "col_select_prev": [ord('<'), ord('h')],
247
250
  "col_hide": [ord('!'), ord('@'), ord('#'), ord('$'), ord('%'), ord('^'), ord('&'), ord('*'), ord('('), ord(')')],
248
251
  "edit": [ord('e')],
249
- "edit_picker": [ord('E')],
252
+ "edit_nvim": [ord('E')],
250
253
  "edit_ipython": [5], # Ctrl+e
251
254
  "copy": [ord('y')],
252
255
  "save": [19, ord('D')], # Ctrl+s
253
256
  "load": [ord('L'), 15], # Ctrl+o
254
- "open": [ord('O')],
257
+ # "open": [ord('O')],
255
258
  "toggle_footer": [ord('_')],
259
+ "visual_selection_toggle": [ord('v')],
260
+ "visual_deselection_toggle": [ord('V')],
261
+ "toggle_select": [ord(' ')],
256
262
  }
@@ -294,6 +294,61 @@ def get_colours(pick:int=0) -> Dict[str, int]:
294
294
  'active_column_bg': curses.COLOR_BLACK,
295
295
  'active_column_fg': curses.COLOR_WHITE,
296
296
  },
297
+ ### (6) Use default colors for bg
298
+ # {
299
+ # 'background': -1,
300
+ # 'normal_fg': -1,
301
+ # 'unselected_bg': -1,
302
+ # 'unselected_fg': -1,
303
+ # 'cursor_bg': 21,
304
+ # 'cursor_fg': -1,
305
+ # 'selected_bg': 54,
306
+ # 'selected_fg': -1,
307
+ # 'header_bg': 255,
308
+ # 'header_fg': 232,
309
+ # 'error_bg': 232,
310
+ # 'error_fg': curses.COLOR_RED,
311
+ # 'complete_bg': 232,
312
+ # 'complete_fg': 82,
313
+ # 'waiting_bg': 232,
314
+ # 'waiting_fg': curses.COLOR_YELLOW,
315
+ # 'active_bg': 232,
316
+ # 'active_fg': 33,
317
+ # 'paused_bg': -1,
318
+ # 'paused_fg': 244,
319
+ # 'search_bg': 162,
320
+ # 'search_fg': -1,
321
+ # 'active_input_bg': -1,
322
+ # 'active_input_fg': 232,
323
+ # 'modes_selected_bg': 232,
324
+ # 'modes_selected_fg': -1,
325
+ # 'modes_unselected_bg': 255,
326
+ # 'modes_unselected_fg': 232,
327
+ # 'title_bar': 232,
328
+ # 'title_bg': 232,
329
+ # 'title_fg': -1,
330
+ # 'scroll_bar_bg': 247,
331
+ # 'selected_header_column_bg': 232,
332
+ # 'selected_header_column_fg': -1,
333
+ # 'unselected_header_column_bg': -1,
334
+ # 'unselected_header_column_fg': -1,
335
+ # 'footer_bg': 232,
336
+ # 'footer_fg': -1,
337
+ # 'refreshing_bg': 232,
338
+ # 'refreshing_fg': -1,
339
+ # 'refreshing_inactive_bg': 232,
340
+ # 'refreshing_inactive_fg': 232,
341
+ # '40pc_bg': 232,
342
+ # '40pc_fg': 166,
343
+ # 'footer_string_bg': 232,
344
+ # 'footer_string_fg': -1,
345
+ # 'selected_cell_bg': 54,
346
+ # 'selected_cell_fg': -1,
347
+ # 'deselecting_cell_bg': 162,
348
+ # 'deselecting_cell_fg': -1,
349
+ # 'active_column_bg': 234,
350
+ # 'active_column_fg': -1,
351
+ # },
297
352
  ]
298
353
  for colour in colours:
299
354
  colour["20pc_bg"] = colour["background"]
@@ -15,8 +15,45 @@ import toml
15
15
  import logging
16
16
 
17
17
  logger = logging.getLogger('picker_log')
18
+ import concurrent.futures
19
+ import re
18
20
 
19
21
  def generate_columns(funcs: list, files: list) -> list[list[str]]:
22
+ """
23
+ Takes a list of functions and a list of files.
24
+ Tasks are run in parallel using concurrent.futures.
25
+ """
26
+ logger.info("function: generate_columns (generate_data.py)")
27
+
28
+ results = []
29
+ # Create a future object for each combination of func and file
30
+ with concurrent.futures.ThreadPoolExecutor() as executor:
31
+ futures = [[executor.submit(func, file) for func in funcs] for file in files]
32
+
33
+ for file_futures in futures:
34
+ result = [future.result() for future in file_futures]
35
+ results.append(result)
36
+ return results
37
+
38
+
39
+ def generate_columns_multithread(funcs: list, files: list) -> list[list[str]]:
40
+ """
41
+ Takes a list of functions and a list of files.
42
+ Tasks are run in parallel using concurrent.futures.
43
+ """
44
+ logger.info("function: generate_columns (generate_data.py)")
45
+
46
+ results = []
47
+ # Create a future object for each combination of func and file
48
+ with concurrent.futures.ThreadPoolExecutor() as executor:
49
+ futures = [[executor.submit(func, file) for func in funcs] for file in files]
50
+
51
+ for file_futures in futures:
52
+ result = [future.result() for future in file_futures]
53
+ results.append(result)
54
+ return results
55
+
56
+ def generate_columns_single_thread(funcs: list, files: list) -> list[list[str]]:
20
57
  """
21
58
  Takes a list of functions and a list of files. Each function is run for each file and a list of lists is returned.
22
59
  """
@@ -30,7 +67,7 @@ def generate_columns(funcs: list, files: list) -> list[list[str]]:
30
67
  except:
31
68
  item.append("")
32
69
  items.append(item)
33
-
70
+
34
71
  return items
35
72
 
36
73
  def command_to_func(command: str) -> Callable:
@@ -43,7 +80,7 @@ def command_to_func(command: str) -> Callable:
43
80
  """
44
81
  logger.info("function: command_to_func (generate_data.py)")
45
82
 
46
- func = lambda arg: subprocess.run(command.format(repr(arg)), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.decode("utf-8")
83
+ func = lambda arg: subprocess.run(replace_braces(command, repr(arg)), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.decode("utf-8").strip()
47
84
  return func
48
85
 
49
86
  def load_environment(envs:dict):
@@ -55,6 +92,12 @@ def load_environment(envs:dict):
55
92
  if "cwd" in envs:
56
93
  os.chdir(os.path.expandvars(os.path.expanduser(envs["cwd"])))
57
94
 
95
+ def replace_braces(text, s):
96
+ text = re.sub(r'\{\{(.*?)\}\}', r'@@\1@@', text)
97
+ text = re.sub(r'\{\}', s, text)
98
+ text = re.sub(r'@@(.*?)@@', r'{{\1}}', text)
99
+ return text
100
+
58
101
 
59
102
  def read_toml(file_path) -> Tuple[dict, list, list]:
60
103
  """
@@ -0,0 +1,10 @@
1
+ #!/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ generate_data_utils.py
5
+
6
+ Author: GrimAndGreedy
7
+ License: MIT
8
+ """
9
+
10
+ from listpick.utils.generate_data_utils import ProcessSafePriorityQueue
@@ -0,0 +1,234 @@
1
+ #!/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ generate_data_multithreaded.py
5
+ Generate data for listpick Picker.
6
+
7
+
8
+ 1. Read toml file.
9
+ 2. Set environment variables.
10
+ 3. Get files from first command.
11
+ 4. Create items with "..." for cells to be filled.
12
+ 5. Create a priority queue which determines which cells are to be filled first.
13
+ 6. Create a queue updater which increases the priorty of cells which are on screen which does so each second.
14
+ 7. Create threads to start generating data for cells.
15
+
16
+ Author: GrimAndGreedy
17
+ License: MIT
18
+ """
19
+
20
+ import subprocess
21
+ import os
22
+ from typing import Tuple, Callable
23
+ import toml
24
+ import logging
25
+ import threading
26
+ from queue import PriorityQueue
27
+ import time
28
+ import re
29
+ import shlex
30
+
31
+ logger = logging.getLogger('picker_log')
32
+
33
+ def generate_columns_worker(
34
+ funcs: list,
35
+ files: list,
36
+ items: list[list[str]],
37
+ getting_data: threading.Event,
38
+ task_queue: PriorityQueue,
39
+ completed_cells: set,
40
+ state: dict,
41
+ ) -> None:
42
+ """ Get a task from the priorty queue and fill the data for that cell."""
43
+ while task_queue.qsize() > 0 and not state["thread_stop_event"].is_set():
44
+ _, (i, j) = task_queue.get()
45
+
46
+ if (i, j) in completed_cells:
47
+ task_queue.task_done()
48
+ continue
49
+
50
+ if state["thread_stop_event"].is_set():
51
+ with task_queue.mutex:
52
+ task_queue.queue.clear()
53
+
54
+ generate_cell(
55
+ func=funcs[j],
56
+ file=files[i],
57
+ items=items,
58
+ row=i,
59
+ col=j+1,
60
+ state=state,
61
+ )
62
+ completed_cells.add((i, j))
63
+ task_queue.task_done()
64
+ getting_data.set()
65
+
66
+ def generate_cell(func: Callable, file: str, items: list[list[str]], row: int, col: int, state: dict) -> None:
67
+ """
68
+ Takes a function, file and a file and then sets items[row][col] to the result.
69
+ """
70
+ if not state["thread_stop_event"].is_set():
71
+ try:
72
+ result = func(file).strip()
73
+ if not state["thread_stop_event"].is_set():
74
+ items[row][col] = result
75
+ except Exception as e:
76
+ pass
77
+ # import pyperclip
78
+ # pyperclip.copy(f"({row}, {col}): len(items)={len(items)}, len(items[0])={len(items[0])} {e}")
79
+
80
+ def update_queue(task_queue: PriorityQueue, visible_rows_indices: list[int], rows: int, cols: int, state: dict):
81
+ """ Increase the priority of getting the data for the cells that are currently visible. """
82
+ while task_queue.qsize() > 0:
83
+ time.sleep(0.1)
84
+ if state["thread_stop_event"].is_set():
85
+ with task_queue.mutex:
86
+ task_queue.queue.clear()
87
+ break
88
+ for row in visible_rows_indices:
89
+ for col in range(cols):
90
+ if state["generate_data_for_hidden_columns"] == False and col+1 in state["hidden_columns"]: continue
91
+ if 0 <= row < rows:
92
+ task_queue.put((1, (row, col)))
93
+
94
+ # Delay
95
+ time.sleep(0.9)
96
+
97
+ def command_to_func(command: str) -> Callable:
98
+ """
99
+ Convert a command string to a function that will run the command.
100
+
101
+ E.g.,
102
+ mediainfo {} | grep -i format
103
+ mediainfo {} | grep -i format | head -n 1 | awk '{{print $3}}'
104
+ """
105
+ logger.info("function: command_to_func (generate_data.py)")
106
+
107
+ func = lambda arg: subprocess.run(replace_braces(command, arg), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.decode("utf-8").strip()
108
+ return func
109
+
110
+ def load_environment(envs:dict):
111
+ """
112
+ Load environment variables from an envs dict.
113
+ """
114
+ logger.info("function: load_environment (generate_data.py)")
115
+
116
+ if "cwd" in envs:
117
+ os.chdir(os.path.expandvars(os.path.expanduser(envs["cwd"])))
118
+
119
+ def replace_braces(text, s):
120
+ text = re.sub(r'\{\{(.*?)\}\}', r'@@\1@@', text)
121
+ text = re.sub(r'\{\}', shlex.quote(s), text)
122
+ text = re.sub(r'@@(.*?)@@', r'{{\1}}', text)
123
+ return text
124
+
125
+ def read_toml(file_path) -> Tuple[dict, list, list]:
126
+ """
127
+ Read toml file and return the environment, commands and header sections.
128
+ """
129
+ logger.info("function: read_toml (generate_data.py)")
130
+ with open(file_path, 'r') as file:
131
+ config = toml.load(file)
132
+
133
+ environment = config['environment'] if 'environment' in config else {}
134
+ data = config['data'] if 'data' in config else {}
135
+ commands = [command.strip() for command in data['commands']] if 'commands' in data else []
136
+ header = [header for header in data['header']] if 'header' in data else []
137
+ return environment, commands, header
138
+
139
+
140
+ def generate_picker_data_from_file(
141
+ file_path: str,
142
+ items,
143
+ header,
144
+ visible_rows_indices,
145
+ getting_data,
146
+ state,
147
+
148
+ ) -> None:
149
+ """
150
+ Generate data for Picker based upon the toml file commands.
151
+ """
152
+ logger.info("function: generate_picker_data (generate_data.py)")
153
+
154
+
155
+ environment, lines, hdr = read_toml(file_path)
156
+
157
+ # Load any environment variables from the toml file
158
+ if environment:
159
+ load_environment(environment)
160
+
161
+
162
+ # Get list of files to be displayed in the first column.
163
+ get_files_command = lines[0]
164
+ files = subprocess.run(get_files_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.decode("utf-8").strip().split("\n")
165
+ files = [file.strip() for file in files if files]
166
+
167
+ commands_list = [line.strip() for line in lines[1:]]
168
+ command_funcs = [command_to_func(command) for command in commands_list]
169
+
170
+ generate_picker_data(
171
+ files = files,
172
+ column_functions = command_funcs,
173
+ data_header = hdr,
174
+ items = items,
175
+ picker_header = header,
176
+ visible_rows_indices = visible_rows_indices,
177
+ getting_data = getting_data,
178
+ state=state,
179
+ )
180
+
181
+ def generate_picker_data(
182
+ files: list[str],
183
+ column_functions: list[Callable],
184
+ data_header,
185
+ items,
186
+ picker_header,
187
+ visible_rows_indices,
188
+ getting_data,
189
+ state,
190
+ ) -> None:
191
+ """
192
+ Generate data from a list of files and a list of column functions which will be used to
193
+ generate subsequent columns.
194
+
195
+ This function is performed asynchronously with os.cpu_count() threads.
196
+
197
+ data_header: header list to be set for the picker
198
+ picker_header: the picker header will be passed in so that it can be set for the class
199
+
200
+ """
201
+ logger.info("function: generate_picker_data (generate_data.py)")
202
+
203
+ items.clear()
204
+ items.extend([[file] + ["..." for _ in column_functions] for file in files])
205
+ picker_header[:] = data_header
206
+
207
+
208
+ task_queue = state["data_generation_queue"]
209
+ for i in range(len(files)):
210
+ for j in range(len(column_functions)):
211
+ if state["generate_data_for_hidden_columns"] == False and j+1 in state["hidden_columns"]: continue
212
+ task_queue.put((10, (i, j)))
213
+
214
+ num_workers = os.cpu_count()
215
+ if num_workers in [None, -1]: num_workers = 4
216
+ if num_workers == None or num_workers < 1: num_workers = 1
217
+ completed_cells = set()
218
+
219
+ for _ in range(num_workers):
220
+ gen_items_thread = threading.Thread(
221
+ target=generate_columns_worker,
222
+ args=(column_functions, files, items, getting_data, task_queue, completed_cells, state),
223
+ )
224
+ state["threads"].append(gen_items_thread)
225
+ gen_items_thread.daemon = True
226
+ gen_items_thread.start()
227
+
228
+ update_queue_thread = threading.Thread(
229
+ target=update_queue,
230
+ args=(task_queue, visible_rows_indices, len(files), len(column_functions), state),
231
+ )
232
+ state["threads"].append(update_queue_thread)
233
+ update_queue_thread.daemon = True
234
+ update_queue_thread.start()
@@ -0,0 +1,43 @@
1
+ #!/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ generate_data_utils.py
5
+
6
+ Author: GrimAndGreedy
7
+ License: MIT
8
+ """
9
+
10
+ def sort_priority_first(element):
11
+ return element[0]
12
+
13
+ class ProcessSafePriorityQueue:
14
+ def __init__(self, manager):
15
+ self.data = manager.list()
16
+ self.lock = manager.Lock()
17
+
18
+ def put(self, item):
19
+ with self.lock:
20
+ self.data.append(item)
21
+ self.data.sort(key=sort_priority_first)
22
+
23
+ def get(self, timeout=None):
24
+ start = time.time()
25
+ while True:
26
+ with self.lock:
27
+ if self.data:
28
+ return self.data.pop(0)
29
+ if timeout is not None and (time.time() - start) > timeout:
30
+ raise IndexError("get timeout")
31
+ time.sleep(0.01)
32
+
33
+ def qsize(self):
34
+ with self.lock:
35
+ return len(self.data)
36
+
37
+ def empty(self):
38
+ with self.lock:
39
+ return len(self.data) == 0
40
+
41
+ def clear(self):
42
+ with self.lock:
43
+ self.data[:] = []
@@ -1,11 +1,17 @@
1
1
  from listpick.utils import keycodes
2
2
  import os, tty, select, curses
3
+ import termios
3
4
 
4
5
  def open_tty():
5
6
  """ Return a file descriptor for the tty that we are opening"""
6
7
  tty_fd = os.open('/dev/tty', os.O_RDONLY)
8
+ old_terminal_settings = termios.tcgetattr(tty_fd)
7
9
  tty.setraw(tty_fd)
8
- return tty_fd
10
+ return tty_fd, old_terminal_settings
11
+
12
+ def restore_terminal_settings(tty_fd, old_settings):
13
+ """ Restore the terminal to its previous state """
14
+ termios.tcsetattr(tty_fd, termios.TCSADRAIN, old_settings)
9
15
 
10
16
  def get_char(tty_fd, timeout: float = 0.2, secondary: bool = False) -> int:
11
17
  """ Get character from a tty_fd with a timeout. """
listpick/utils/utils.py CHANGED
@@ -14,9 +14,14 @@ import tempfile
14
14
  import os
15
15
  from typing import Tuple, Dict
16
16
  import logging
17
+ import shlex
18
+ from collections import defaultdict
19
+ import time
17
20
 
18
21
  logger = logging.getLogger('picker_log')
19
22
 
23
+
24
+
20
25
  def clip_left(text, n):
21
26
  """
22
27
  Clips the first `n` display-width characters from the left of the input string.
@@ -98,7 +103,12 @@ def format_row(row: list[str], hidden_columns: list, column_widths: list[int], s
98
103
 
99
104
  def get_column_widths(items: list[list[str]], header: list[str]=[], max_column_width:int=70, number_columns:bool=True, max_total_width=-1, separator = " ", unicode_char_width: bool = True) -> list[int]:
100
105
  """ Calculate maximum width of each column with clipping. """
101
- if len(items) == 0: return [0]
106
+ if len(items) == 0 and len(header) == 0: return [0]
107
+ elif len(items) == 0:
108
+ header_widths = [wcswidth(f"{i}. {str(h)}") if number_columns else wcswidth(str(h)) for i, h in enumerate(header)]
109
+ col_widths = [min(max_column_width, header_widths[i]) for i in range(len(header))]
110
+ return col_widths
111
+
102
112
  assert len(items) > 0
103
113
  widths = [max(wcswidth(str(row[i])) for row in items) for i in range(len(items[0]))]
104
114
  # widths = [max(len(str(row[i])) for row in items) for i in range(len(items[0]))]
@@ -204,17 +214,11 @@ def get_selected_cells(cell_selections: Dict[Tuple[int, int], bool]) -> list[Tup
204
214
  def get_selected_cells_by_row(cell_selections: dict[tuple[int, int], bool]) -> dict[int, list[int]]:
205
215
  """ {0: [1,2], 9: [1] }"""
206
216
 
207
- d = {}
208
- try:
209
- for tup in cell_selections.keys():
210
- if cell_selections[tup]:
211
- if tup[0] in d:
212
- d[tup[0]].append(tup[1])
213
- else:
214
- d[tup[0]] = [tup[1]]
215
- except:
216
- pass
217
- return d
217
+ d = defaultdict(list)
218
+ for (row, col), selected in cell_selections.items():
219
+ if selected:
220
+ d[row].append(col)
221
+ return dict(d)
218
222
 
219
223
  def get_selected_values(items: list[list[str]], selections: dict[int, bool]) -> list[list[str]]:
220
224
  """ Return a list of rows based on wich are True in the selections dictionary. """
@@ -290,9 +294,7 @@ def openFiles(files: list[str]) -> str:
290
294
  apps[app] = [t]
291
295
 
292
296
  return apps
293
- for i in range(len(files)):
294
- if ' ' in files[i] and files[i][0] not in ["'", '"']:
295
- files[i] = repr(files[i])
297
+ files = [shlex.quote(file) for file in files]
296
298
 
297
299
  types = get_mime_types(files)
298
300
  apps = get_applications(types.keys())