listpick 0.1.13.54__py3-none-any.whl → 0.1.13.55__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/listpick_app_1.py +3250 -0
- listpick/ui/footer.py +35 -46
- listpick/ui/footer_1.py +213 -0
- {listpick-0.1.13.54.dist-info → listpick-0.1.13.55.dist-info}/METADATA +1 -1
- {listpick-0.1.13.54.dist-info → listpick-0.1.13.55.dist-info}/RECORD +9 -7
- {listpick-0.1.13.54.dist-info → listpick-0.1.13.55.dist-info}/WHEEL +0 -0
- {listpick-0.1.13.54.dist-info → listpick-0.1.13.55.dist-info}/entry_points.txt +0 -0
- {listpick-0.1.13.54.dist-info → listpick-0.1.13.55.dist-info}/licenses/LICENSE.txt +0 -0
- {listpick-0.1.13.54.dist-info → listpick-0.1.13.55.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,3250 @@
|
|
|
1
|
+
#!/bin/python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
listpick_app.py
|
|
5
|
+
Set up environment to parse command-line arguments and run a Picker.
|
|
6
|
+
|
|
7
|
+
Author: GrimAndGreedy
|
|
8
|
+
License: MIT
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import curses
|
|
12
|
+
import re
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
import argparse
|
|
16
|
+
import time
|
|
17
|
+
from wcwidth import wcswidth
|
|
18
|
+
from typing import Callable, Optional, Tuple, Dict
|
|
19
|
+
import json
|
|
20
|
+
import threading
|
|
21
|
+
import string
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
from listpick.ui.picker_colours import get_colours, get_help_colours, get_notification_colours, get_theme_count, get_fallback_colours
|
|
25
|
+
from listpick.utils.options_selectors import default_option_input, output_file_option_selector, default_option_selector
|
|
26
|
+
from listpick.utils.table_to_list_of_lists import *
|
|
27
|
+
from listpick.utils.utils import *
|
|
28
|
+
from listpick.utils.sorting import *
|
|
29
|
+
from listpick.utils.filtering import *
|
|
30
|
+
from listpick.ui.input_field import *
|
|
31
|
+
from listpick.utils.clipboard_operations import *
|
|
32
|
+
from listpick.utils.paste_operations import *
|
|
33
|
+
from listpick.utils.searching import search
|
|
34
|
+
from listpick.ui.help_screen import help_lines
|
|
35
|
+
from listpick.ui.keys import picker_keys, notification_keys, options_keys, help_keys
|
|
36
|
+
from listpick.utils.generate_data import generate_picker_data
|
|
37
|
+
from listpick.utils.dump import dump_state, load_state, dump_data
|
|
38
|
+
from listpick.ui.build_help import build_help_rows
|
|
39
|
+
from listpick.ui.footer import StandardFooter, CompactFooter, NoFooter
|
|
40
|
+
from listpick.utils.picker_log import setup_logger
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from tmp.data_stuff import test_items, test_highlights, test_header
|
|
45
|
+
except:
|
|
46
|
+
test_items, test_highlights, test_header = [], [], []
|
|
47
|
+
|
|
48
|
+
COLOURS_SET = False
|
|
49
|
+
help_colours, notification_colours = {}, {}
|
|
50
|
+
|
|
51
|
+
class Command:
|
|
52
|
+
def __init__(self, command_type, command_value):
|
|
53
|
+
self.command_type = command_type
|
|
54
|
+
self.command_value = command_value
|
|
55
|
+
|
|
56
|
+
class Picker:
|
|
57
|
+
def __init__(self,
|
|
58
|
+
stdscr: curses.window,
|
|
59
|
+
items: list[list[str]] = [],
|
|
60
|
+
cursor_pos: int = 0,
|
|
61
|
+
colours: dict = get_colours(0),
|
|
62
|
+
colour_theme_number: int = 0,
|
|
63
|
+
max_selected: int = -1,
|
|
64
|
+
top_gap: int =0,
|
|
65
|
+
title: str ="Picker",
|
|
66
|
+
header: list =[],
|
|
67
|
+
max_column_width: int =70,
|
|
68
|
+
clear_on_start: bool = False,
|
|
69
|
+
|
|
70
|
+
auto_refresh: bool =False,
|
|
71
|
+
timer: float = 5,
|
|
72
|
+
|
|
73
|
+
get_new_data: bool =False, # Whether we can get new data
|
|
74
|
+
refresh_function: Optional[Callable] = lambda: [], # The function with which we get new data
|
|
75
|
+
get_data_startup: bool =False, # Whether we should get data at statrup
|
|
76
|
+
track_entries_upon_refresh: bool = True,
|
|
77
|
+
pin_cursor: bool = False,
|
|
78
|
+
id_column: int = 0,
|
|
79
|
+
|
|
80
|
+
unselectable_indices: list =[],
|
|
81
|
+
highlights: list =[],
|
|
82
|
+
highlights_hide: bool =False,
|
|
83
|
+
number_columns: bool =True,
|
|
84
|
+
column_widths: list = [],
|
|
85
|
+
column_indices: list = [],
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
current_row : int = 0,
|
|
89
|
+
current_page : int = 0,
|
|
90
|
+
is_selecting : bool = False,
|
|
91
|
+
is_deselecting : int = False,
|
|
92
|
+
start_selection: int = -1,
|
|
93
|
+
start_selection_col: int = -1,
|
|
94
|
+
end_selection: int = -1,
|
|
95
|
+
user_opts : str = "",
|
|
96
|
+
options_list: list[str] = [],
|
|
97
|
+
user_settings : str = "",
|
|
98
|
+
separator : str = " ",
|
|
99
|
+
search_query : str = "",
|
|
100
|
+
search_count : int = 0,
|
|
101
|
+
search_index : int = 0,
|
|
102
|
+
filter_query : str = "",
|
|
103
|
+
hidden_columns: list = [],
|
|
104
|
+
indexed_items: list[Tuple[int, list[str]]] = [],
|
|
105
|
+
scroll_bar : int = True,
|
|
106
|
+
|
|
107
|
+
selections: dict = {},
|
|
108
|
+
cell_selections: dict[tuple[int,int], bool] = {},
|
|
109
|
+
highlight_full_row: bool =False,
|
|
110
|
+
cell_cursor: bool = False,
|
|
111
|
+
|
|
112
|
+
items_per_page : int = -1,
|
|
113
|
+
sort_method : int = 0,
|
|
114
|
+
SORT_METHODS: list[str] = ['Orig', 'lex', 'LEX', 'alnum', 'ALNUM', 'time', 'num', 'size'],
|
|
115
|
+
sort_reverse: list[bool] = [False],
|
|
116
|
+
selected_column: int = 0,
|
|
117
|
+
sort_column : int = 0,
|
|
118
|
+
|
|
119
|
+
columns_sort_method: list[int] = [0],
|
|
120
|
+
key_chain: str = "",
|
|
121
|
+
last_key: Optional[str] = None,
|
|
122
|
+
|
|
123
|
+
paginate: bool =False,
|
|
124
|
+
cancel_is_back: bool = False,
|
|
125
|
+
mode_index: int =0,
|
|
126
|
+
modes: list[dict] = [],
|
|
127
|
+
display_modes: bool =False,
|
|
128
|
+
require_option: list=[],
|
|
129
|
+
require_option_default: list=[],
|
|
130
|
+
option_functions: list[Callable[..., Tuple[bool, str]]] = [],
|
|
131
|
+
default_option_function: Callable[..., Tuple[bool, str]] = default_option_input,
|
|
132
|
+
disabled_keys: list=[],
|
|
133
|
+
|
|
134
|
+
show_header: bool = True,
|
|
135
|
+
show_row_header: bool = False,
|
|
136
|
+
show_footer: bool =True,
|
|
137
|
+
footer_style: int = 0,
|
|
138
|
+
footer_string: str="",
|
|
139
|
+
footer_string_auto_refresh: bool=False,
|
|
140
|
+
footer_string_refresh_function: Optional[Callable] = None,
|
|
141
|
+
footer_timer: float=1,
|
|
142
|
+
get_footer_string_startup=False,
|
|
143
|
+
unicode_char_width: bool = True,
|
|
144
|
+
|
|
145
|
+
colours_start: int =0,
|
|
146
|
+
colours_end: int =-1,
|
|
147
|
+
reset_colours: bool = True,
|
|
148
|
+
key_remappings: dict = {},
|
|
149
|
+
keys_dict:dict = picker_keys,
|
|
150
|
+
display_infobox : bool = False,
|
|
151
|
+
infobox_items: list[list[str]] = [],
|
|
152
|
+
infobox_title: str = "",
|
|
153
|
+
display_only: bool = False,
|
|
154
|
+
|
|
155
|
+
editable_columns: list[int] = [],
|
|
156
|
+
editable_by_default: bool = True,
|
|
157
|
+
|
|
158
|
+
centre_in_terminal: bool = False,
|
|
159
|
+
centre_in_terminal_vertical: bool = False,
|
|
160
|
+
centre_in_cols: bool = False,
|
|
161
|
+
|
|
162
|
+
startup_notification:str = "",
|
|
163
|
+
|
|
164
|
+
leftmost_column: int = 0,
|
|
165
|
+
leftmost_char: int = 0,
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
history_filter_and_search: list[str] = [],
|
|
169
|
+
history_opts: list[str] = [],
|
|
170
|
+
history_settings: list[str] = [],
|
|
171
|
+
history_edits: list[str] = [],
|
|
172
|
+
history_pipes: list[str] = [],
|
|
173
|
+
debug: bool = False,
|
|
174
|
+
debug_level: int = 1,
|
|
175
|
+
|
|
176
|
+
):
|
|
177
|
+
self.stdscr = stdscr
|
|
178
|
+
self.items = items
|
|
179
|
+
self.cursor_pos = cursor_pos
|
|
180
|
+
self.colours = colours
|
|
181
|
+
self.colour_theme_number = colour_theme_number
|
|
182
|
+
self.max_selected = max_selected
|
|
183
|
+
self.top_gap = top_gap
|
|
184
|
+
self.title = title
|
|
185
|
+
self.header = header
|
|
186
|
+
self.max_column_width = max_column_width
|
|
187
|
+
self.clear_on_start = clear_on_start
|
|
188
|
+
|
|
189
|
+
self.auto_refresh = auto_refresh
|
|
190
|
+
self.timer = timer
|
|
191
|
+
|
|
192
|
+
self.get_new_data = get_new_data
|
|
193
|
+
self.refresh_function = refresh_function
|
|
194
|
+
self.get_data_startup = get_data_startup
|
|
195
|
+
self.track_entries_upon_refresh = track_entries_upon_refresh
|
|
196
|
+
self.pin_cursor = pin_cursor
|
|
197
|
+
self.id_column = id_column
|
|
198
|
+
|
|
199
|
+
self.unselectable_indices = unselectable_indices
|
|
200
|
+
self.highlights = highlights
|
|
201
|
+
self.highlights_hide = highlights_hide
|
|
202
|
+
self.number_columns = number_columns
|
|
203
|
+
self.column_widths, = [],
|
|
204
|
+
self.column_indices, = [],
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
self.current_row = current_row
|
|
208
|
+
self.current_page = current_page
|
|
209
|
+
self.is_selecting = is_selecting
|
|
210
|
+
self.is_deselecting = is_deselecting
|
|
211
|
+
self.start_selection = start_selection
|
|
212
|
+
self.start_selection_col = start_selection_col
|
|
213
|
+
self.end_selection = end_selection
|
|
214
|
+
self.user_opts = user_opts
|
|
215
|
+
self.options_list = options_list
|
|
216
|
+
self.user_settings = user_settings
|
|
217
|
+
self.separator = separator
|
|
218
|
+
self.search_query = search_query
|
|
219
|
+
self.search_count = search_count
|
|
220
|
+
self.search_index = search_index
|
|
221
|
+
self.filter_query = filter_query
|
|
222
|
+
self.hidden_columns = hidden_columns
|
|
223
|
+
self.indexed_items = indexed_items
|
|
224
|
+
self.scroll_bar = scroll_bar
|
|
225
|
+
|
|
226
|
+
self.selections = selections
|
|
227
|
+
self.cell_selections = cell_selections
|
|
228
|
+
self.highlight_full_row = highlight_full_row
|
|
229
|
+
self.cell_cursor = cell_cursor
|
|
230
|
+
|
|
231
|
+
self.items_per_page = items_per_page
|
|
232
|
+
self.sort_method = sort_method
|
|
233
|
+
self.sort_reverse = sort_reverse
|
|
234
|
+
self.selected_column = selected_column
|
|
235
|
+
self.sort_column = sort_column
|
|
236
|
+
self.columns_sort_method = columns_sort_method
|
|
237
|
+
self.key_chain = key_chain
|
|
238
|
+
self.last_key = last_key
|
|
239
|
+
|
|
240
|
+
self.paginate = paginate
|
|
241
|
+
self.cancel_is_back = cancel_is_back
|
|
242
|
+
self.mode_index = mode_index
|
|
243
|
+
self.modes = modes
|
|
244
|
+
self.display_modes = display_modes
|
|
245
|
+
self.require_option = require_option
|
|
246
|
+
self.require_option_default = require_option_default
|
|
247
|
+
self.option_functions = option_functions
|
|
248
|
+
self.default_option_function = default_option_function
|
|
249
|
+
self.disabled_keys = disabled_keys
|
|
250
|
+
|
|
251
|
+
self.show_header = show_header
|
|
252
|
+
self.show_row_header = show_row_header
|
|
253
|
+
self.show_footer = show_footer
|
|
254
|
+
self.footer_style = footer_style
|
|
255
|
+
self.footer_string = footer_string
|
|
256
|
+
self.footer_string_auto_refresh = footer_string_auto_refresh
|
|
257
|
+
self.footer_string_refresh_function = footer_string_refresh_function
|
|
258
|
+
self.footer_timer = footer_timer
|
|
259
|
+
self.get_footer_string_startup = get_footer_string_startup,
|
|
260
|
+
self.unicode_char_width = unicode_char_width
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
self.colours_start = colours_start
|
|
264
|
+
self.colours_end = colours_end
|
|
265
|
+
self.reset_colours = reset_colours
|
|
266
|
+
self.key_remappings = key_remappings
|
|
267
|
+
self.keys_dict = keys_dict
|
|
268
|
+
self.display_infobox = display_infobox
|
|
269
|
+
self.infobox_items = infobox_items
|
|
270
|
+
self.infobox_title = infobox_title
|
|
271
|
+
self.display_only = display_only
|
|
272
|
+
|
|
273
|
+
self.editable_columns = editable_columns
|
|
274
|
+
self.editable_by_default = editable_by_default
|
|
275
|
+
|
|
276
|
+
self.centre_in_terminal = centre_in_terminal
|
|
277
|
+
self.centre_in_terminal_vertical = centre_in_terminal_vertical
|
|
278
|
+
self.centre_in_cols = centre_in_cols
|
|
279
|
+
|
|
280
|
+
self.startup_notification = startup_notification
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
self.registers = {}
|
|
284
|
+
|
|
285
|
+
self.SORT_METHODS = SORT_METHODS
|
|
286
|
+
self.command_stack = []
|
|
287
|
+
self.leftmost_column = leftmost_column
|
|
288
|
+
self.leftmost_char = leftmost_char
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# Refresh function variables
|
|
292
|
+
self.data_refreshed = False
|
|
293
|
+
self.refreshing_data = False
|
|
294
|
+
self.data_lock = threading.Lock()
|
|
295
|
+
self.data_ready = False
|
|
296
|
+
self.cursor_pos_id = 0
|
|
297
|
+
self.cursor_pos_prev = 0
|
|
298
|
+
self.ids = []
|
|
299
|
+
self.ids_tuples = []
|
|
300
|
+
self.selected_cells_by_row = {}
|
|
301
|
+
|
|
302
|
+
# History variables
|
|
303
|
+
self.history_filter_and_search = history_filter_and_search
|
|
304
|
+
self.history_pipes = history_pipes
|
|
305
|
+
self.history_opts = history_opts
|
|
306
|
+
self.history_settings = history_settings
|
|
307
|
+
self.history_edits = history_edits
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
self.debug = debug
|
|
313
|
+
self.debug_level = debug_level
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
self.initialise_picker_state(reset_colours=self.reset_colours)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# Note: We have to set the footer after initialising the picker state so that the footer can use the get_function_data method
|
|
320
|
+
# self.footer_options = [StandardFooter(self.stdscr, colours_start, self.get_function_data), CompactFooter(self.stdscr, colours_start, self.get_function_data), NoFooter(self.stdscr, colours_start, self.get_function_data)]
|
|
321
|
+
self.footer_options = []
|
|
322
|
+
# self.footer = self.footer_options[self.footer_style]
|
|
323
|
+
|
|
324
|
+
self.footer = CompactFooter(self.stdscr, colours_start, self.get_function_data)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def calculate_section_sizes(self):
|
|
329
|
+
"""
|
|
330
|
+
Calculte the following for the Picker:
|
|
331
|
+
self.items_per_page: the number of entry rows displayed
|
|
332
|
+
self.bottom_space: the size of the footer + the bottom buffer space
|
|
333
|
+
self.top_space: the size of the space at the top of the picker: title + modes + header + top_gap
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
self.logger.debug(f"function: calculate_section_sizes()")
|
|
337
|
+
|
|
338
|
+
# self.bottom_space
|
|
339
|
+
self.bottom_space = self.footer.height if self.show_footer else 0
|
|
340
|
+
|
|
341
|
+
## self.top_space
|
|
342
|
+
h, w = self.stdscr.getmaxyx()
|
|
343
|
+
self.top_space = self.top_gap
|
|
344
|
+
if self.title: self.top_space+=1
|
|
345
|
+
if self.modes and self.display_modes: self.top_space+=1
|
|
346
|
+
if self.header and self.show_header: self.top_space += 1
|
|
347
|
+
|
|
348
|
+
# self.items_per_page
|
|
349
|
+
self.items_per_page = h - self.top_space - self.bottom_space
|
|
350
|
+
if not self.show_footer and self.footer_string: self.items_per_page-=1
|
|
351
|
+
self.items_per_page = min(h-self.top_space-1, self.items_per_page)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# Adjust top space if centring vertically and we have fewer rows than terminal lines
|
|
355
|
+
if self.centre_in_terminal_vertical and len(self.indexed_items) < self.items_per_page:
|
|
356
|
+
self.top_space += ((h-(self.top_space+self.bottom_space))-len(self.indexed_items))//2
|
|
357
|
+
|
|
358
|
+
# self.column_widths
|
|
359
|
+
visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
|
|
360
|
+
visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
|
|
361
|
+
|
|
362
|
+
# self.startx
|
|
363
|
+
self.startx = 1 if self.highlight_full_row else 2
|
|
364
|
+
if self.show_row_header: self.startx += len(str(len(self.items))) + 2
|
|
365
|
+
if visible_columns_total_width < w and self.centre_in_terminal:
|
|
366
|
+
self.startx += (w - visible_columns_total_width) // 2
|
|
367
|
+
|
|
368
|
+
def get_visible_rows(self) -> list[list[str]]:
|
|
369
|
+
|
|
370
|
+
self.logger.debug(f"function: get_visible_rows()")
|
|
371
|
+
## Scroll with column select
|
|
372
|
+
if self.paginate:
|
|
373
|
+
start_index = (self.cursor_pos//self.items_per_page) * self.items_per_page
|
|
374
|
+
end_index = min(start_index + self.items_per_page, len(self.indexed_items))
|
|
375
|
+
## Scroll
|
|
376
|
+
else:
|
|
377
|
+
scrolloff = self.items_per_page//2
|
|
378
|
+
start_index = max(0, min(self.cursor_pos - (self.items_per_page-scrolloff), len(self.indexed_items)-self.items_per_page))
|
|
379
|
+
end_index = min(start_index + self.items_per_page, len(self.indexed_items))
|
|
380
|
+
if len(self.indexed_items) == 0: start_index, end_index = 0, 0
|
|
381
|
+
|
|
382
|
+
rows = [v[1] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else self.items
|
|
383
|
+
return rows
|
|
384
|
+
|
|
385
|
+
def initialise_picker_state(self, reset_colours=False) -> None:
|
|
386
|
+
""" Initialise state variables for the picker. These are: debugging and colours. """
|
|
387
|
+
|
|
388
|
+
if curses.has_colors() and self.colours != None:
|
|
389
|
+
# raise Exception("Terminal does not support color")
|
|
390
|
+
curses.start_color()
|
|
391
|
+
if reset_colours:
|
|
392
|
+
global COLOURS_SET
|
|
393
|
+
COLOURS_SET = False
|
|
394
|
+
colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
|
|
395
|
+
if curses.COLORS >= 255 and curses.COLOR_PAIRS >= 150:
|
|
396
|
+
self.colours_start = self.colours_start
|
|
397
|
+
self.notification_colours_start = self.colours_start+50
|
|
398
|
+
self.help_colours_start = self.colours_start+100
|
|
399
|
+
else:
|
|
400
|
+
self.colours_start = 0
|
|
401
|
+
self.notification_colours_start = 0
|
|
402
|
+
self.help_colours_start = 0
|
|
403
|
+
else:
|
|
404
|
+
self.colours_start = 0
|
|
405
|
+
self.notification_colours_start = 0
|
|
406
|
+
self.help_colours_start = 0
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
debug_levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]
|
|
410
|
+
dbglvl = debug_levels[self.debug_level]
|
|
411
|
+
self.logger = setup_logger(name="picker_log", log_file="picker.log", log_enabled=self.debug, level =dbglvl)
|
|
412
|
+
self.logger.info(f"Initialiasing Picker.")
|
|
413
|
+
# self.notification(self.stdscr, message=repr(self.logger))
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# 1 2 3 4 5
|
|
417
|
+
# logger = logging.getLogger(__file__)
|
|
418
|
+
# if self.debug_level == 0:
|
|
419
|
+
# logger = logging.getLogger()
|
|
420
|
+
# logger.disabled = True
|
|
421
|
+
# else:
|
|
422
|
+
#
|
|
423
|
+
# file_handler = logging.FileHandler(f"{self.title}.log", mode='w')
|
|
424
|
+
# formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', '%m-%d-%Y %H:%M:%S')
|
|
425
|
+
# file_handler.setFormatter(formatter)
|
|
426
|
+
# logger.addHandler(file_handler)
|
|
427
|
+
# logger.setLevel(debug_levels[self.debug_level-1])
|
|
428
|
+
|
|
429
|
+
# logging.basicConfig(
|
|
430
|
+
# level=debug_levels[self.debug_level-1],
|
|
431
|
+
# format='%(asctime)s - %(levelname)s - %(message)s',
|
|
432
|
+
# datefmt='%m-%d-%Y %H:%M:%S',
|
|
433
|
+
# filename=f"{self.title}.log",
|
|
434
|
+
# filemode="w",
|
|
435
|
+
# )
|
|
436
|
+
#
|
|
437
|
+
# self.logger.info(f"Starging log. Log level {logger.getEffectiveLevel()}")
|
|
438
|
+
# self.logger.info(f"Starging log. Log level {repr(debug_levels)}, {self.debug_level}, {debug_levels[self.debug_level-1]}")
|
|
439
|
+
# self.notification(self.stdscr, f"Starging log. Log level {repr(debug_levels)}, {self.debug_level}, {debug_levels[self.debug_level-1]}")
|
|
440
|
+
# self.notification(self.stdscr, f"{__file__}")
|
|
441
|
+
|
|
442
|
+
## Logging level plan
|
|
443
|
+
# DEBUG: loop functions, draw screen, etc.
|
|
444
|
+
# INFO: main functions
|
|
445
|
+
# WARNING: any try-except fails
|
|
446
|
+
|
|
447
|
+
# No set_escdelay function on windows.
|
|
448
|
+
try:
|
|
449
|
+
curses.set_escdelay(25)
|
|
450
|
+
except:
|
|
451
|
+
logging.warning("Error trying to set curses.set_escdelay")
|
|
452
|
+
|
|
453
|
+
# self.stdscr.clear()
|
|
454
|
+
# self.stdscr.refresh()
|
|
455
|
+
# self.draw_screen(self.indexed_items, self.highlights)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def initialise_variables(self, get_data: bool = False) -> None:
|
|
460
|
+
""" Initialise the variables that keep track of the data. """
|
|
461
|
+
|
|
462
|
+
self.logger.info(f"function: initialise_variables()")
|
|
463
|
+
|
|
464
|
+
tracking = False
|
|
465
|
+
|
|
466
|
+
## Get data synchronously
|
|
467
|
+
if get_data and self.refresh_function != None:
|
|
468
|
+
if self.track_entries_upon_refresh and len(self.items) > 0:
|
|
469
|
+
tracking = True
|
|
470
|
+
selected_indices = get_selected_indices(self.selections)
|
|
471
|
+
self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
|
|
472
|
+
self.ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
|
|
473
|
+
self.ids_tuples = [(i, item[self.id_column]) for i, item in enumerate(self.items) if i in selected_indices]
|
|
474
|
+
|
|
475
|
+
if len(self.indexed_items) > 0 and len(self.indexed_items) >= self.cursor_pos and len(self.indexed_items[0][1]) >= self.id_column:
|
|
476
|
+
self.cursor_pos_id = self.indexed_items[self.cursor_pos][1][self.id_column]
|
|
477
|
+
|
|
478
|
+
self.items, self.header = self.refresh_function()
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
if self.items == []: self.items = [[]]
|
|
482
|
+
## Ensure that items is a List[List[Str]] object
|
|
483
|
+
if len(self.items) > 0 and not isinstance(self.items[0], list):
|
|
484
|
+
self.items = [[item] for item in self.items]
|
|
485
|
+
self.items = [[str(cell) for cell in row] for row in self.items]
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# Ensure that header is of the same length as the rows
|
|
489
|
+
if self.header and len(self.items) > 0 and len(self.header) != len(self.items[0]):
|
|
490
|
+
self.header = [str(self.header[i]) if i < len(self.header) else "" for i in range(len(self.items[0]))]
|
|
491
|
+
|
|
492
|
+
# Constants
|
|
493
|
+
# DEFAULT_ITEMS_PER_PAGE = os.get_terminal_size().lines - top_gap*2-2-int(bool(header))
|
|
494
|
+
|
|
495
|
+
self.calculate_section_sizes()
|
|
496
|
+
|
|
497
|
+
# Initial states
|
|
498
|
+
if len(self.selections) != len(self.items):
|
|
499
|
+
self.selections = {i : False if i not in self.selections else bool(self.selections[i]) for i in range(len(self.items))}
|
|
500
|
+
|
|
501
|
+
if len(self.items) and len(self.cell_selections) != len(self.items)*len(self.items[0]):
|
|
502
|
+
self.cell_selections = {(i, j) : False if (i, j) not in self.cell_selections else self.cell_selections[(i, j)] for i in range(len(self.items)) for j in range(len(self.items[0]))}
|
|
503
|
+
elif len(self.items) == 0:
|
|
504
|
+
self.cell_selections = {}
|
|
505
|
+
|
|
506
|
+
if len(self.require_option) < len(self.items):
|
|
507
|
+
self.require_option += [self.require_option_default for i in range(len(self.items)-len(self.require_option))]
|
|
508
|
+
if len(self.option_functions) < len(self.items):
|
|
509
|
+
self.option_functions += [self.default_option_function for i in range(len(self.items)-len(self.option_functions))]
|
|
510
|
+
if len(self.items)>0 and len(self.columns_sort_method) < len(self.items[0]):
|
|
511
|
+
self.columns_sort_method = self.columns_sort_method + [0 for i in range(len(self.items[0])-len(self.columns_sort_method))]
|
|
512
|
+
if len(self.items)>0 and len(self.sort_reverse) < len(self.items[0]):
|
|
513
|
+
self.sort_reverse = self.sort_reverse + [False for i in range(len(self.items[0])-len(self.sort_reverse))]
|
|
514
|
+
if len(self.items)>0 and len(self.editable_columns) < len(self.items[0]):
|
|
515
|
+
self.editable_columns = self.editable_columns + [self.editable_by_default for i in range(len(self.items[0])-len(self.editable_columns))]
|
|
516
|
+
if len(self.items)>0 and len(self.column_indices) < len(self.items[0]):
|
|
517
|
+
self.column_indices = self.column_indices + [i for i in range(len(self.column_indices), len(self.items[0]))]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# items2 = [[row[self.column_indices[i]] for i in range(len(row))] for row in self.items]
|
|
522
|
+
# self.indexed_items = list(enumerate(items2))
|
|
523
|
+
if self.items == [[]]: self.indexed_items = []
|
|
524
|
+
else: self.indexed_items = list(enumerate(self.items))
|
|
525
|
+
|
|
526
|
+
# If a filter is passed then refilter
|
|
527
|
+
if self.filter_query:
|
|
528
|
+
# prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
|
|
529
|
+
# prev_index = self.indexed_items[cursor_pos][0] if len(self.indexed_items)>0 else 0
|
|
530
|
+
self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
|
|
531
|
+
if self.cursor_pos in [x[0] for x in self.indexed_items]: self.cursor_pos = [x[0] for x in self.indexed_items].index(self.cursor_pos)
|
|
532
|
+
else: self.cursor_pos = 0
|
|
533
|
+
if self.search_query:
|
|
534
|
+
return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
|
|
535
|
+
query=self.search_query,
|
|
536
|
+
indexed_items=self.indexed_items,
|
|
537
|
+
highlights=self.highlights,
|
|
538
|
+
cursor_pos=self.cursor_pos,
|
|
539
|
+
unselectable_indices=self.unselectable_indices,
|
|
540
|
+
continue_search=True,
|
|
541
|
+
)
|
|
542
|
+
if return_val:
|
|
543
|
+
self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
|
|
544
|
+
# If a sort is passed
|
|
545
|
+
if len(self.indexed_items) > 0:
|
|
546
|
+
sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
|
|
547
|
+
# if len(self.items[0]) == 1:
|
|
548
|
+
# self.number_columns = False
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
h, w = self.stdscr.getmaxyx()
|
|
553
|
+
|
|
554
|
+
# Adjust variables to ensure correctness if errors
|
|
555
|
+
## Move to a selectable row (if applicable)
|
|
556
|
+
if len(self.items) <= len(self.unselectable_indices): self.unselectable_indices = []
|
|
557
|
+
new_pos = (self.cursor_pos)%len(self.items)
|
|
558
|
+
while new_pos in self.unselectable_indices and new_pos != self.cursor_pos:
|
|
559
|
+
new_pos = (new_pos + 1) % len(self.items)
|
|
560
|
+
|
|
561
|
+
assert new_pos < len(self.items)
|
|
562
|
+
self.cursor_pos = new_pos
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# if tracking and len(self.items) > 1:
|
|
566
|
+
# Ensure that selected indices are tracked upon data refresh
|
|
567
|
+
if self.track_entries_upon_refresh and (self.data_ready or tracking) and len(self.items) > 1:
|
|
568
|
+
selected_indices = []
|
|
569
|
+
all_ids = [item[self.id_column] for item in self.items]
|
|
570
|
+
self.selections = {i: False for i in range(len(self.items))}
|
|
571
|
+
if len(self.items) > 0:
|
|
572
|
+
self.cell_selections = {(i, j): False for i in range(len(self.items)) for j in range(len(self.items[0]))}
|
|
573
|
+
else:
|
|
574
|
+
self.cell_selections = {}
|
|
575
|
+
|
|
576
|
+
for id in self.ids:
|
|
577
|
+
if id in all_ids:
|
|
578
|
+
selected_indices.append(all_ids.index(id))
|
|
579
|
+
self.selections[all_ids.index(id)] = True
|
|
580
|
+
|
|
581
|
+
for i, id in self.ids_tuples:
|
|
582
|
+
if id in all_ids:
|
|
583
|
+
# rows_with_selected_cells
|
|
584
|
+
for j in self.selected_cells_by_row[i]:
|
|
585
|
+
self.cell_selections[(all_ids.index(id), j)] = True
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
if len(self.indexed_items):
|
|
590
|
+
if self.pin_cursor:
|
|
591
|
+
self.cursor_pos = min(self.cursor_pos_prev, len(self.indexed_items)-1)
|
|
592
|
+
else:
|
|
593
|
+
if self.cursor_pos_id in all_ids:
|
|
594
|
+
cursor_pos_x = all_ids.index(self.cursor_pos_id)
|
|
595
|
+
if cursor_pos_x in [i[0] for i in self.indexed_items]:
|
|
596
|
+
self.cursor_pos = [i[0] for i in self.indexed_items].index(cursor_pos_x)
|
|
597
|
+
else:
|
|
598
|
+
self.cursor_pos = 0
|
|
599
|
+
|
|
600
|
+
# if self.display_infobox:
|
|
601
|
+
# self.infobox_picker = self.infobox(self.stdscr, self.infobox_items, self.infobox_title)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def move_column(self, direction: int) -> None:
|
|
605
|
+
"""
|
|
606
|
+
Cycles the column $direction places.
|
|
607
|
+
E.g., If $direction == -1 and the sort column is 3, then column 3 will swap with column 2
|
|
608
|
+
in each of the rows in $items and 2 will become the new sort column.
|
|
609
|
+
|
|
610
|
+
sort_column = 3, direction = -1
|
|
611
|
+
[[0,1,2,*3*,4],
|
|
612
|
+
[5,6,7,*8*,9]]
|
|
613
|
+
-->
|
|
614
|
+
[[0,1,*3*,2,4],
|
|
615
|
+
[5,6,*8*,7,9]]
|
|
616
|
+
|
|
617
|
+
returns:
|
|
618
|
+
adjusted items, header, sort_column and column_widths
|
|
619
|
+
"""
|
|
620
|
+
self.logger.info(f"function: move_column(direction={direction})")
|
|
621
|
+
if len(self.items) < 1: return None
|
|
622
|
+
if (self.selected_column+direction) < 0 or (self.selected_column+direction) >= len(self.items[0]): return None
|
|
623
|
+
|
|
624
|
+
new_index = self.selected_column + direction
|
|
625
|
+
|
|
626
|
+
# Swap columns in each row
|
|
627
|
+
for row in self.items:
|
|
628
|
+
row[self.selected_column], row[new_index] = row[new_index], row[self.selected_column]
|
|
629
|
+
if self.header:
|
|
630
|
+
self.header[self.selected_column], self.header[new_index] = self.header[new_index], self.header[self.selected_column]
|
|
631
|
+
|
|
632
|
+
# Swap column widths
|
|
633
|
+
self.column_widths[self.selected_column], self.column_widths[new_index] = self.column_widths[new_index], self.column_widths[self.selected_column]
|
|
634
|
+
|
|
635
|
+
# Update current column index
|
|
636
|
+
self.selected_column = new_index
|
|
637
|
+
|
|
638
|
+
def test_screen_size(self):
|
|
639
|
+
self.logger.debug("function: test_screen_size()")
|
|
640
|
+
h, w = self.stdscr.getmaxyx()
|
|
641
|
+
## Terminal too small to display Picker
|
|
642
|
+
if h<3 or w<len("Terminal"): return False
|
|
643
|
+
if (self.show_footer or self.footer_string) and (h<12 or w<35) or (h<12 and w<10):
|
|
644
|
+
self.stdscr.addstr(h//2-1, (w-len("Terminal"))//2, "Terminal")
|
|
645
|
+
self.stdscr.addstr(h//2, (w-len("Too"))//2, "Too")
|
|
646
|
+
self.stdscr.addstr(h//2+1, (w-len("Small"))//2, "Small")
|
|
647
|
+
return False
|
|
648
|
+
return True
|
|
649
|
+
|
|
650
|
+
def splash_screen(self, message=""):
|
|
651
|
+
self.logger.info(f"function: splash_screen({message})")
|
|
652
|
+
""" Display a splash screen with a message. Useful when loading a large data set. """
|
|
653
|
+
h, w =self.stdscr.getmaxyx()
|
|
654
|
+
self.stdscr.bkgd(' ', curses.color_pair(2))
|
|
655
|
+
try:
|
|
656
|
+
self.stdscr.addstr(h//2, (w-len(message))//2, message, curses.color_pair(2))
|
|
657
|
+
except:
|
|
658
|
+
pass
|
|
659
|
+
self.stdscr.refresh()
|
|
660
|
+
|
|
661
|
+
def draw_screen_wr(self, indexed_items: list[Tuple[int, list[str]]], highlights: list[dict] = [{}], clear: bool = True) -> None:
|
|
662
|
+
""" Try-except wrapper for the draw_screen_() method to prevent crashes when rapidly resizing the terminal. """
|
|
663
|
+
try:
|
|
664
|
+
self.draw_screen(self.indexed_items, self.highlights, clear=clear)
|
|
665
|
+
except:
|
|
666
|
+
self.logger.warning(f"draw_screen function error")
|
|
667
|
+
|
|
668
|
+
def draw_screen(self, indexed_items: list[Tuple[int, list[str]]], highlights: list[dict] = [{}], clear: bool = True) -> None:
|
|
669
|
+
""" Draw Picker screen. """
|
|
670
|
+
self.logger.debug("Draw screen.")
|
|
671
|
+
|
|
672
|
+
if clear:
|
|
673
|
+
self.stdscr.erase()
|
|
674
|
+
|
|
675
|
+
h, w = self.stdscr.getmaxyx()
|
|
676
|
+
|
|
677
|
+
# Test if the terminal is of a sufficient size to display the picker
|
|
678
|
+
if not self.test_screen_size(): return None
|
|
679
|
+
|
|
680
|
+
# Determine which rows are to be displayed on the current screen
|
|
681
|
+
## Paginate
|
|
682
|
+
if self.paginate:
|
|
683
|
+
start_index = (self.cursor_pos//self.items_per_page) * self.items_per_page
|
|
684
|
+
end_index = min(start_index + self.items_per_page, len(self.indexed_items))
|
|
685
|
+
## Scroll
|
|
686
|
+
else:
|
|
687
|
+
scrolloff = self.items_per_page//2
|
|
688
|
+
start_index = max(0, min(self.cursor_pos - (self.items_per_page-scrolloff), len(self.indexed_items)-self.items_per_page))
|
|
689
|
+
end_index = min(start_index + self.items_per_page, len(self.indexed_items))
|
|
690
|
+
if len(self.indexed_items) == 0: start_index, end_index = 0, 0
|
|
691
|
+
|
|
692
|
+
# self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w)
|
|
693
|
+
# Determine widths based only on the currently indexed rows
|
|
694
|
+
# rows = [v[1] for v in self.indexed_items] if len(self.indexed_items) else self.items
|
|
695
|
+
# Determine widths based only on the currently displayed indexed rows
|
|
696
|
+
rows = [v[1] for v in self.indexed_items[start_index:end_index]] if len(self.indexed_items) else self.items
|
|
697
|
+
self.column_widths = get_column_widths(rows, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w, unicode_char_width=self.unicode_char_width)
|
|
698
|
+
visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
|
|
699
|
+
visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
|
|
700
|
+
|
|
701
|
+
# Determine the number of items_per_page, top_size and bottom_size
|
|
702
|
+
self.calculate_section_sizes()
|
|
703
|
+
|
|
704
|
+
# top_space = self.top_gap
|
|
705
|
+
|
|
706
|
+
## Display title (if applicable)
|
|
707
|
+
if self.title:
|
|
708
|
+
padded_title = f" {self.title.strip()} "
|
|
709
|
+
self.stdscr.addstr(self.top_gap, 0, f"{' ':^{w}}", curses.color_pair(self.colours_start+16))
|
|
710
|
+
title_x = (w-wcswidth(padded_title))//2
|
|
711
|
+
# title = f"{title:^{w}}"
|
|
712
|
+
self.stdscr.addstr(self.top_gap, title_x, padded_title, curses.color_pair(self.colours_start+16) | curses.A_BOLD)
|
|
713
|
+
# top_space += 1
|
|
714
|
+
|
|
715
|
+
## Display modes
|
|
716
|
+
if self.display_modes and self.modes not in [[{}], []]:
|
|
717
|
+
self.stdscr.addstr(self.top_gap+1, 0, ' '*w, curses.A_REVERSE)
|
|
718
|
+
modes_list = [f"{mode['name']}" if 'name' in mode else f"{i}. " for i, mode in enumerate(self.modes)]
|
|
719
|
+
# mode_colours = [mode["colour"] for mode ]
|
|
720
|
+
mode_widths = get_mode_widths(modes_list)
|
|
721
|
+
split_space = (w-sum(mode_widths))//len(self.modes)
|
|
722
|
+
xmode = 0
|
|
723
|
+
for i, mode in enumerate(modes_list):
|
|
724
|
+
if i == len(modes_list)-1:
|
|
725
|
+
mode_str = f"{mode:^{mode_widths[i]+split_space+(w-sum(mode_widths))%len(self.modes)}}"
|
|
726
|
+
else:
|
|
727
|
+
mode_str = f"{mode:^{mode_widths[i]+split_space}}"
|
|
728
|
+
# current mode
|
|
729
|
+
if i == self.mode_index:
|
|
730
|
+
self.stdscr.addstr(self.top_gap+1, xmode, mode_str, curses.color_pair(self.colours_start+14) | curses.A_BOLD)
|
|
731
|
+
# other modes
|
|
732
|
+
else:
|
|
733
|
+
self.stdscr.addstr(self.top_gap+1, xmode, mode_str, curses.color_pair(self.colours_start+15) | curses.A_UNDERLINE)
|
|
734
|
+
xmode += split_space+mode_widths[i]
|
|
735
|
+
# top_space += 1
|
|
736
|
+
|
|
737
|
+
## Display header
|
|
738
|
+
if self.header and self.show_header:
|
|
739
|
+
header_str = ""
|
|
740
|
+
up_to_selected_col = ""
|
|
741
|
+
selected_col_str = ""
|
|
742
|
+
for i in range(len(self.header)):
|
|
743
|
+
if i == self.selected_column: up_to_selected_col = header_str
|
|
744
|
+
if i in self.hidden_columns: continue
|
|
745
|
+
number = f"{i}. " if self.number_columns else ""
|
|
746
|
+
# number = f"{intStringToExponentString(str(i))}. " if self.number_columns else ""
|
|
747
|
+
header_str += number
|
|
748
|
+
header_str += f"{self.header[i]:^{self.column_widths[i]-len(number)}}"
|
|
749
|
+
header_str += self.separator
|
|
750
|
+
|
|
751
|
+
header_str = header_str[self.leftmost_char:]
|
|
752
|
+
header_ypos = self.top_gap + bool(self.title) + bool(self.display_modes and self.modes)
|
|
753
|
+
self.stdscr.addstr(header_ypos, 0, ' '*w, curses.color_pair(self.colours_start+4) | curses.A_BOLD)
|
|
754
|
+
self.stdscr.addstr(header_ypos, self.startx, header_str[:min(w-self.startx, visible_columns_total_width+1)], curses.color_pair(self.colours_start+4) | curses.A_BOLD)
|
|
755
|
+
|
|
756
|
+
# Highlight sort column
|
|
757
|
+
try:
|
|
758
|
+
if self.selected_column != None and self.selected_column not in self.hidden_columns:
|
|
759
|
+
if len(self.header) > 1 and (len(up_to_selected_col)-self.leftmost_char) < w:
|
|
760
|
+
# if len(up_to_selected_col) + 1 < w or True:
|
|
761
|
+
# if self.startx + len(up_to_selected_col) - self.leftmost_char > 0 or True:
|
|
762
|
+
number = f"{self.selected_column}. " if self.number_columns else ""
|
|
763
|
+
# number = f"{intStringToExponentString(self.selected_column)}. " if self.number_columns else ""
|
|
764
|
+
# self.startx + len(up_to_selected_col) - self.leftmost_char
|
|
765
|
+
highlighed_col_startx = max(self.startx, self.startx + len(up_to_selected_col) - self.leftmost_char)
|
|
766
|
+
highlighted_col_str = (number+f"{self.header[self.selected_column]:^{self.column_widths[self.selected_column]-len(number)}}") + self.separator
|
|
767
|
+
end_of_highlighted_col_str = w-(highlighed_col_startx+len(highlighted_col_str)) if (highlighed_col_startx+len(highlighted_col_str)) > w else len(highlighted_col_str)
|
|
768
|
+
start_of_highlighted_col_str = max(self.leftmost_char - len(up_to_selected_col), 0)
|
|
769
|
+
self.stdscr.addstr(header_ypos, highlighed_col_startx , highlighted_col_str[start_of_highlighted_col_str:end_of_highlighted_col_str], curses.color_pair(self.colours_start+19) | curses.A_BOLD)
|
|
770
|
+
except:
|
|
771
|
+
pass
|
|
772
|
+
|
|
773
|
+
# Display row header
|
|
774
|
+
if self.show_row_header:
|
|
775
|
+
for idx in range(start_index, end_index):
|
|
776
|
+
y = idx - start_index + self.top_space
|
|
777
|
+
if idx == self.cursor_pos:
|
|
778
|
+
self.stdscr.addstr(y, 0, f" {self.indexed_items[idx][0]} ", curses.color_pair(self.colours_start+19) | curses.A_BOLD)
|
|
779
|
+
else:
|
|
780
|
+
self.stdscr.addstr(y, 0, f" {self.indexed_items[idx][0]} ", curses.color_pair(self.colours_start+4) | curses.A_BOLD)
|
|
781
|
+
|
|
782
|
+
def highlight_cell(row: int, col:int, visible_column_widths, colour_pair_number: int = 5):
|
|
783
|
+
cell_pos = sum(visible_column_widths[:col])+col*len(self.separator)-self.leftmost_char + self.startx
|
|
784
|
+
# cell_width = self.column_widths[self.selected_column]
|
|
785
|
+
cell_width = visible_column_widths[col] + len(self.separator)
|
|
786
|
+
cell_max_width = w-cell_pos
|
|
787
|
+
try:
|
|
788
|
+
# Start of cell is on screen
|
|
789
|
+
if self.startx <= cell_pos <= w:
|
|
790
|
+
self.stdscr.addstr(y, cell_pos, (' '*cell_width)[:cell_max_width], curses.color_pair(self.colours_start+colour_pair_number))
|
|
791
|
+
if self.centre_in_cols:
|
|
792
|
+
cell_value = f"{self.indexed_items[row][1][col]:^{cell_width-len(self.separator)}}" + self.separator
|
|
793
|
+
else:
|
|
794
|
+
cell_value = self.indexed_items[row][1][col] + self.separator
|
|
795
|
+
# cell_value = cell_value[:min(cell_width, cell_max_width)-len(self.separator)]
|
|
796
|
+
cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width)-len(self.separator), self.unicode_char_width)
|
|
797
|
+
cell_value = cell_value + self.separator
|
|
798
|
+
# cell_value = cell_value
|
|
799
|
+
cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.unicode_char_width)
|
|
800
|
+
# row_str = truncate_to_display_width(row_str_left_adj, min(w-self.startx, visible_columns_total_width))
|
|
801
|
+
self.stdscr.addstr(y, cell_pos, cell_value, curses.color_pair(self.colours_start+colour_pair_number) | curses.A_BOLD)
|
|
802
|
+
# Part of the cell is on screen
|
|
803
|
+
elif self.startx <= cell_pos+cell_width <= w:
|
|
804
|
+
cell_start = self.startx - cell_pos
|
|
805
|
+
self.stdscr.addstr(y, self.startx, ' '*(cell_width-cell_start), curses.color_pair(self.colours_start+colour_pair_number))
|
|
806
|
+
cell_value = self.indexed_items[row][1][col][cell_start:visible_column_widths[col]]
|
|
807
|
+
self.stdscr.addstr(y, self.startx, cell_value, curses.color_pair(self.colours_start+colour_pair_number) | curses.A_BOLD)
|
|
808
|
+
except:
|
|
809
|
+
pass
|
|
810
|
+
|
|
811
|
+
# Draw:
|
|
812
|
+
# 1. standard row
|
|
813
|
+
# 2. highlights l0
|
|
814
|
+
# 3. selected
|
|
815
|
+
# 4. above-selected highlights l1
|
|
816
|
+
# 5. cursor
|
|
817
|
+
# 6. top-level highlights l2
|
|
818
|
+
## Display rows and highlights
|
|
819
|
+
|
|
820
|
+
def sort_highlights(highlights):
|
|
821
|
+
"""
|
|
822
|
+
Sort highlights into lists based on their display level.
|
|
823
|
+
Highlights with no level defined will be displayed at level 0.
|
|
824
|
+
"""
|
|
825
|
+
l0 = []
|
|
826
|
+
l1 = []
|
|
827
|
+
l2 = []
|
|
828
|
+
for highlight in highlights:
|
|
829
|
+
if "level" in highlight:
|
|
830
|
+
if highlight["level"] == 0: l0.append(highlight)
|
|
831
|
+
elif highlight["level"] == 1: l1.append(highlight)
|
|
832
|
+
elif highlight["level"] == 2: l2.append(highlight)
|
|
833
|
+
else: l0.append(highlight)
|
|
834
|
+
else:
|
|
835
|
+
l0.append(highlight)
|
|
836
|
+
return l0, l1, l2
|
|
837
|
+
|
|
838
|
+
def draw_highlights(highlights: list[dict], idx: int, y: int, item: tuple[int, list[str]]):
|
|
839
|
+
self.logger.debug(f"function: draw_highlights()")
|
|
840
|
+
if len(highlights) == 0: return None
|
|
841
|
+
full_row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
|
|
842
|
+
row_str = full_row_str[self.leftmost_char:]
|
|
843
|
+
for highlight in highlights:
|
|
844
|
+
if "row" in highlight:
|
|
845
|
+
if highlight["row"] != self.indexed_items[idx][0]:
|
|
846
|
+
continue
|
|
847
|
+
try:
|
|
848
|
+
if highlight["field"] == "all":
|
|
849
|
+
match = re.search(highlight["match"], full_row_str, re.IGNORECASE)
|
|
850
|
+
if not match: continue
|
|
851
|
+
highlight_start = match.start()
|
|
852
|
+
highlight_end = match.end()
|
|
853
|
+
if highlight_end - self.leftmost_char < 0:
|
|
854
|
+
continue
|
|
855
|
+
|
|
856
|
+
elif type(highlight["field"]) == type(0) and highlight["field"] not in self.hidden_columns:
|
|
857
|
+
match = re.search(highlight["match"], truncate_to_display_width(item[1][highlight["field"]], self.column_widths[highlight["field"]], centre=False, unicode_char_width=self.unicode_char_width), re.IGNORECASE)
|
|
858
|
+
if not match: continue
|
|
859
|
+
field_start = sum([width for i, width in enumerate(self.column_widths[:highlight["field"]]) if i not in self.hidden_columns]) + sum([1 for i in range(highlight["field"]) if i not in self.hidden_columns])*wcswidth(self.separator)
|
|
860
|
+
|
|
861
|
+
## We want to search the non-centred values but highlight the centred values.
|
|
862
|
+
if self.centre_in_cols:
|
|
863
|
+
tmp = truncate_to_display_width(item[1][highlight["field"]], self.column_widths[highlight["field"]], self.centre_in_cols, self.unicode_char_width)
|
|
864
|
+
field_start += (len(tmp) - len(tmp.lstrip()))
|
|
865
|
+
|
|
866
|
+
highlight_start = field_start + match.start()
|
|
867
|
+
highlight_end = match.end() + field_start
|
|
868
|
+
if highlight_end - self.leftmost_char < 0:
|
|
869
|
+
continue
|
|
870
|
+
else:
|
|
871
|
+
continue
|
|
872
|
+
highlight_start -= self.leftmost_char
|
|
873
|
+
highlight_end -= self.leftmost_char
|
|
874
|
+
self.stdscr.addstr(y, max(self.startx, self.startx+highlight_start), row_str[max(highlight_start,0):min(w-self.startx, highlight_end)], curses.color_pair(self.colours_start+highlight["color"]) | curses.A_BOLD)
|
|
875
|
+
except:
|
|
876
|
+
pass
|
|
877
|
+
|
|
878
|
+
l0_highlights, l1_highlights, l2_highlights = sort_highlights(self.highlights)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
for idx in range(start_index, end_index):
|
|
882
|
+
item = self.indexed_items[idx]
|
|
883
|
+
y = idx - start_index + self.top_space
|
|
884
|
+
|
|
885
|
+
# row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)[self.leftmost_char:]
|
|
886
|
+
# row_str = truncate_to_display_width(row_str, min(w-self.startx, visible_columns_total_width))
|
|
887
|
+
row_str_orig = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
|
|
888
|
+
row_str_left_adj = clip_left(row_str_orig, self.leftmost_char)
|
|
889
|
+
row_str = truncate_to_display_width(row_str_left_adj, min(w-self.startx, visible_columns_total_width), self.unicode_char_width)
|
|
890
|
+
# row_str = truncate_to_display_width(row_str, min(w-self.startx, visible_columns_total_width))[self.leftmost_char:]
|
|
891
|
+
|
|
892
|
+
## Display the standard row
|
|
893
|
+
self.stdscr.addstr(y, self.startx, row_str[:min(w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+2))
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
# Draw the level 0 highlights
|
|
897
|
+
if not self.highlights_hide:
|
|
898
|
+
draw_highlights(l0_highlights, idx, y, item)
|
|
899
|
+
|
|
900
|
+
# Higlight cursor cell and selected cells
|
|
901
|
+
if self.cell_cursor:
|
|
902
|
+
# self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
|
|
903
|
+
if item[0] in self.selected_cells_by_row:
|
|
904
|
+
for j in self.selected_cells_by_row[item[0]]:
|
|
905
|
+
highlight_cell(idx, j, visible_column_widths, colour_pair_number=25)
|
|
906
|
+
|
|
907
|
+
# Visually selected
|
|
908
|
+
if self.is_selecting:
|
|
909
|
+
if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
|
|
910
|
+
x_interval = range(min(self.start_selection_col, self.selected_column), max(self.start_selection_col, self.selected_column)+1)
|
|
911
|
+
for col in x_interval:
|
|
912
|
+
highlight_cell(idx, col, visible_column_widths, colour_pair_number=25)
|
|
913
|
+
|
|
914
|
+
# Visually deslected
|
|
915
|
+
if self.is_deselecting:
|
|
916
|
+
if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
|
|
917
|
+
x_interval = range(min(self.start_selection_col, self.selected_column), max(self.start_selection_col, self.selected_column)+1)
|
|
918
|
+
for col in x_interval:
|
|
919
|
+
highlight_cell(idx, col, visible_column_widths, colour_pair_number=26)
|
|
920
|
+
# Higlight cursor row and selected rows
|
|
921
|
+
elif self.highlight_full_row:
|
|
922
|
+
if self.selections[item[0]]:
|
|
923
|
+
self.stdscr.addstr(y, self.startx, row_str[:min(w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+25) | curses.A_BOLD)
|
|
924
|
+
# Visually selected
|
|
925
|
+
if self.is_selecting:
|
|
926
|
+
if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
|
|
927
|
+
self.stdscr.addstr(y, self.startx, row_str[:min(w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+25))
|
|
928
|
+
# Visually deslected
|
|
929
|
+
elif self.is_deselecting:
|
|
930
|
+
if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
|
|
931
|
+
self.stdscr.addstr(y, self.startx, row_str[:min(w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+26))
|
|
932
|
+
|
|
933
|
+
# Highlight the cursor row and the first char of the selected rows.
|
|
934
|
+
else:
|
|
935
|
+
if self.selections[item[0]]:
|
|
936
|
+
self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
|
|
937
|
+
# Visually selected
|
|
938
|
+
if self.is_selecting:
|
|
939
|
+
if self.start_selection <= idx <= self.cursor_pos or self.start_selection >= idx >= self.cursor_pos:
|
|
940
|
+
self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+1))
|
|
941
|
+
# Visually deslected
|
|
942
|
+
if self.is_deselecting:
|
|
943
|
+
if self.start_selection >= idx >= self.cursor_pos or self.start_selection <= idx <= self.cursor_pos:
|
|
944
|
+
self.stdscr.addstr(y, max(self.startx-2,0), ' ', curses.color_pair(self.colours_start+10))
|
|
945
|
+
|
|
946
|
+
if not self.highlights_hide:
|
|
947
|
+
draw_highlights(l1_highlights, idx, y, item)
|
|
948
|
+
|
|
949
|
+
# Draw cursor
|
|
950
|
+
if idx == self.cursor_pos:
|
|
951
|
+
if self.cell_cursor:
|
|
952
|
+
highlight_cell(idx, self.selected_column, visible_column_widths)
|
|
953
|
+
else:
|
|
954
|
+
self.stdscr.addstr(y, self.startx, row_str[:min(w-self.startx, visible_columns_total_width)], curses.color_pair(self.colours_start+5) | curses.A_BOLD)
|
|
955
|
+
|
|
956
|
+
if not self.highlights_hide:
|
|
957
|
+
draw_highlights(l2_highlights, idx, y, item)
|
|
958
|
+
|
|
959
|
+
## Display scrollbar
|
|
960
|
+
if self.scroll_bar and len(self.indexed_items) and len(self.indexed_items) > (self.items_per_page):
|
|
961
|
+
scroll_bar_length = int(self.items_per_page*self.items_per_page/len(self.indexed_items))
|
|
962
|
+
if self.cursor_pos <= self.items_per_page//2:
|
|
963
|
+
scroll_bar_start=self.top_space
|
|
964
|
+
elif self.cursor_pos + self.items_per_page//2 >= len(self.indexed_items):
|
|
965
|
+
scroll_bar_start = h - int(bool(self.show_footer))*self.footer.height - scroll_bar_length
|
|
966
|
+
else:
|
|
967
|
+
scroll_bar_start = int(((self.cursor_pos)/len(self.indexed_items))*self.items_per_page)+self.top_space - scroll_bar_length//2
|
|
968
|
+
scroll_bar_start = min(scroll_bar_start, h-self.top_space-1)
|
|
969
|
+
scroll_bar_length = min(scroll_bar_length, h - scroll_bar_start-1)
|
|
970
|
+
scroll_bar_length = max(1, scroll_bar_length)
|
|
971
|
+
for i in range(scroll_bar_length):
|
|
972
|
+
v = max(self.top_space+int(bool(self.header)), scroll_bar_start-scroll_bar_length//2)
|
|
973
|
+
self.stdscr.addstr(scroll_bar_start+i, w-1, ' ', curses.color_pair(self.colours_start+18))
|
|
974
|
+
|
|
975
|
+
# Display refresh symbol
|
|
976
|
+
if self.auto_refresh:
|
|
977
|
+
if self.refreshing_data:
|
|
978
|
+
self.stdscr.addstr(0,w-3," ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
|
|
979
|
+
else:
|
|
980
|
+
self.stdscr.addstr(0,w-3," ", curses.color_pair(self.colours_start+23) | curses.A_BOLD)
|
|
981
|
+
|
|
982
|
+
# self.stdscr.refresh()
|
|
983
|
+
|
|
984
|
+
## Display footer
|
|
985
|
+
if self.show_footer:
|
|
986
|
+
# self.footer = NoFooter(self.stdscr, self.colours_start, self.get_function_data)
|
|
987
|
+
h, w = self.stdscr.getmaxyx()
|
|
988
|
+
try:
|
|
989
|
+
self.footer.draw(h, w)
|
|
990
|
+
except:
|
|
991
|
+
pass
|
|
992
|
+
elif self.footer_string:
|
|
993
|
+
footer_string_width = min(w-1, len(self.footer_string)+2)
|
|
994
|
+
disp_string = f" {self.footer_string[:footer_string_width]:>{footer_string_width-2}} "
|
|
995
|
+
self.stdscr.addstr(h - 1, w-footer_string_width-1, " "*footer_string_width, curses.color_pair(self.colours_start+24))
|
|
996
|
+
self.stdscr.addstr(h - 1, w-footer_string_width-1, f"{disp_string}", curses.color_pair(self.colours_start+24))
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
self.stdscr.refresh()
|
|
1000
|
+
|
|
1001
|
+
## Display infobox
|
|
1002
|
+
if self.display_infobox:
|
|
1003
|
+
self.infobox(self.stdscr, message=self.infobox_items, title=self.infobox_title)
|
|
1004
|
+
# self.stdscr.timeout(2000) # timeout is set to 50 in order to get the infobox to be displayed so here we reset it to 2000
|
|
1005
|
+
# self.stdscr.refresh()
|
|
1006
|
+
|
|
1007
|
+
# if self.display_infobox:
|
|
1008
|
+
# # self.stdscr.refresh()
|
|
1009
|
+
# # self.infobox(self.stdscr, message=self.infobox_items, title=self.infobox_title)
|
|
1010
|
+
#
|
|
1011
|
+
# infobox_width, infobox_height = w//2, 3*h//5
|
|
1012
|
+
# infobox_x, infobox_y = w - (infobox_width + 4), 3
|
|
1013
|
+
# self.infobox_picker.stdscr.mvwin(infobox_y, infobox_x)
|
|
1014
|
+
# self.infobox_picker.stdscr.resize(infobox_height, infobox_width)
|
|
1015
|
+
# self.infobox_picker.run()
|
|
1016
|
+
# self.infobox_picker.stdscr.noutrefresh()
|
|
1017
|
+
# else:
|
|
1018
|
+
# # self.stdscr.noutrefresh()
|
|
1019
|
+
# pass
|
|
1020
|
+
#
|
|
1021
|
+
#
|
|
1022
|
+
# if not self.display_only:
|
|
1023
|
+
# curses.doupdate()
|
|
1024
|
+
# self.stdscr.refresh()
|
|
1025
|
+
# pass
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
# def infobox___(self, stdscr: curses.window, message: list =[], title: str ="Infobox", colours_end: int = 0, duration: int = 4):
|
|
1029
|
+
# """ Display non-interactive infobox window. """
|
|
1030
|
+
#
|
|
1031
|
+
# self.logger.info(f"function: infobox()")
|
|
1032
|
+
# h, w = stdscr.getmaxyx()
|
|
1033
|
+
# notification_width, notification_height = w//2, 3*h//5
|
|
1034
|
+
# message_width = notification_width-5
|
|
1035
|
+
#
|
|
1036
|
+
# notification_x, notification_y = w-(notification_width+4), 3
|
|
1037
|
+
# if not message: message = "!!"
|
|
1038
|
+
# if isinstance(message, str):
|
|
1039
|
+
# submenu_items = [" "+message[i*message_width:(i+1)*message_width] for i in range(len(message)//message_width+1)]
|
|
1040
|
+
# else:
|
|
1041
|
+
# submenu_items = message
|
|
1042
|
+
#
|
|
1043
|
+
# notification_remap_keys = {
|
|
1044
|
+
# curses.KEY_RESIZE: curses.KEY_F5,
|
|
1045
|
+
# 27: ord('q')
|
|
1046
|
+
# }
|
|
1047
|
+
# if len(submenu_items) > notification_height - 2:
|
|
1048
|
+
# submenu_items = submenu_items[:notification_height-3] + [f"{'....':^{notification_width}}"]
|
|
1049
|
+
# # while True:
|
|
1050
|
+
# h, w = stdscr.getmaxyx()
|
|
1051
|
+
#
|
|
1052
|
+
# submenu_win = self.stdscr.derwin(notification_height, notification_width, 3, w - (notification_width+4))
|
|
1053
|
+
# infobox_data = {
|
|
1054
|
+
# "items": submenu_items,
|
|
1055
|
+
# "colours": notification_colours,
|
|
1056
|
+
# "colours_start": self.notification_colours_start,
|
|
1057
|
+
# "disabled_keys": [ord('z'), ord('c')],
|
|
1058
|
+
# "show_footer": False,
|
|
1059
|
+
# "top_gap": 0,
|
|
1060
|
+
# "key_remappings": notification_remap_keys,
|
|
1061
|
+
# "display_only": True,
|
|
1062
|
+
# "hidden_columns": [],
|
|
1063
|
+
# "title": title,
|
|
1064
|
+
# "reset_colours": False,
|
|
1065
|
+
# }
|
|
1066
|
+
# submenu_win.noutrefresh()
|
|
1067
|
+
# OptionPicker = Picker(submenu_win, **infobox_data)
|
|
1068
|
+
# return OptionPicker
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def infobox(self, stdscr: curses.window, message: str ="", title: str ="Infobox", colours_end: int = 0, duration: int = 4) -> curses.window:
|
|
1072
|
+
""" Display non-interactive infobox window. """
|
|
1073
|
+
|
|
1074
|
+
self.logger.info(f"function: infobox()")
|
|
1075
|
+
h, w = stdscr.getmaxyx()
|
|
1076
|
+
notification_width, notification_height = w//2, 3*h//5
|
|
1077
|
+
message_width = notification_width-5
|
|
1078
|
+
|
|
1079
|
+
if not message: message = "!!"
|
|
1080
|
+
if isinstance(message, str):
|
|
1081
|
+
submenu_items = [" "+message[i*message_width:(i+1)*message_width] for i in range(len(message)//message_width+1)]
|
|
1082
|
+
else:
|
|
1083
|
+
submenu_items = message
|
|
1084
|
+
|
|
1085
|
+
notification_remap_keys = {
|
|
1086
|
+
curses.KEY_RESIZE: curses.KEY_F5,
|
|
1087
|
+
27: ord('q')
|
|
1088
|
+
}
|
|
1089
|
+
if len(submenu_items) > notification_height - 2:
|
|
1090
|
+
submenu_items = submenu_items[:notification_height-3] + [f"{'....':^{notification_width}}"]
|
|
1091
|
+
while True:
|
|
1092
|
+
h, w = stdscr.getmaxyx()
|
|
1093
|
+
|
|
1094
|
+
submenu_win = curses.newwin(notification_height, notification_width, 3, w - (notification_width+4))
|
|
1095
|
+
infobox_data = {
|
|
1096
|
+
"items": submenu_items,
|
|
1097
|
+
"colours": notification_colours,
|
|
1098
|
+
"colours_start": self.notification_colours_start,
|
|
1099
|
+
"disabled_keys": [ord('z'), ord('c')],
|
|
1100
|
+
"show_footer": False,
|
|
1101
|
+
"top_gap": 0,
|
|
1102
|
+
"key_remappings": notification_remap_keys,
|
|
1103
|
+
"display_only": True,
|
|
1104
|
+
"hidden_columns": [],
|
|
1105
|
+
"title": title,
|
|
1106
|
+
"reset_colours": False,
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
OptionPicker = Picker(submenu_win, **infobox_data)
|
|
1110
|
+
s, o, f = OptionPicker.run()
|
|
1111
|
+
if o != "refresh": break
|
|
1112
|
+
|
|
1113
|
+
return submenu_win
|
|
1114
|
+
|
|
1115
|
+
def get_function_data(self) -> dict:
|
|
1116
|
+
self.logger.debug(f"function: get_function_data()")
|
|
1117
|
+
""" Returns a dict of the main variables needed to restore the state of list_pikcer. """
|
|
1118
|
+
function_data = {
|
|
1119
|
+
"selections": self.selections,
|
|
1120
|
+
"cell_selections": self.cell_selections,
|
|
1121
|
+
"items_per_page": self.items_per_page,
|
|
1122
|
+
"current_row": self.current_row,
|
|
1123
|
+
"current_page": self.current_page,
|
|
1124
|
+
"cursor_pos": self.cursor_pos,
|
|
1125
|
+
"colours": self.colours,
|
|
1126
|
+
"colour_theme_number": self.colour_theme_number,
|
|
1127
|
+
"selected_column": self.selected_column,
|
|
1128
|
+
"sort_column": self.sort_column,
|
|
1129
|
+
"sort_method": self.sort_method,
|
|
1130
|
+
"sort_reverse": self.sort_reverse,
|
|
1131
|
+
"SORT_METHODS": self.SORT_METHODS,
|
|
1132
|
+
"hidden_columns": self.hidden_columns,
|
|
1133
|
+
"is_selecting": self.is_selecting,
|
|
1134
|
+
"is_deselecting": self.is_deselecting,
|
|
1135
|
+
"user_opts": self.user_opts,
|
|
1136
|
+
"options_list": self.options_list,
|
|
1137
|
+
"user_settings": self.user_settings,
|
|
1138
|
+
"separator": self.separator,
|
|
1139
|
+
"search_query": self.search_query,
|
|
1140
|
+
"search_count": self.search_count,
|
|
1141
|
+
"search_index": self.search_index,
|
|
1142
|
+
"filter_query": self.filter_query,
|
|
1143
|
+
"indexed_items": self.indexed_items,
|
|
1144
|
+
"start_selection": self.start_selection,
|
|
1145
|
+
"start_selection_col": self.start_selection_col,
|
|
1146
|
+
"end_selection": self.end_selection,
|
|
1147
|
+
"highlights": self.highlights,
|
|
1148
|
+
"max_column_width": self.max_column_width,
|
|
1149
|
+
"column_indices": self.column_indices,
|
|
1150
|
+
"mode_index": self.mode_index,
|
|
1151
|
+
"modes": self.modes,
|
|
1152
|
+
"title": self.title,
|
|
1153
|
+
"display_modes": self.display_modes,
|
|
1154
|
+
"require_option": self.require_option,
|
|
1155
|
+
"require_option_default": self.require_option_default,
|
|
1156
|
+
"option_functions": self.option_functions,
|
|
1157
|
+
"top_gap": self.top_gap,
|
|
1158
|
+
"number_columns": self.number_columns,
|
|
1159
|
+
"items": self.items,
|
|
1160
|
+
"indexed_items": self.indexed_items,
|
|
1161
|
+
"header": self.header,
|
|
1162
|
+
"scroll_bar": self.scroll_bar,
|
|
1163
|
+
"columns_sort_method": self.columns_sort_method,
|
|
1164
|
+
"disabled_keys": self.disabled_keys,
|
|
1165
|
+
"show_footer": self.show_footer,
|
|
1166
|
+
"footer_string": self.footer_string,
|
|
1167
|
+
"footer_string_auto_refresh": self.footer_string_auto_refresh,
|
|
1168
|
+
"footer_string_refresh_function": self.footer_string_refresh_function,
|
|
1169
|
+
"footer_timer": self.footer_timer,
|
|
1170
|
+
"footer_style": self.footer_style,
|
|
1171
|
+
"colours_start": self.colours_start,
|
|
1172
|
+
"colours_end": self.colours_end,
|
|
1173
|
+
"display_only": self.display_only,
|
|
1174
|
+
"infobox_items": self.infobox_items,
|
|
1175
|
+
"display_infobox": self.display_infobox,
|
|
1176
|
+
"infobox_title": self.infobox_title,
|
|
1177
|
+
"key_remappings": self.key_remappings,
|
|
1178
|
+
"auto_refresh": self.auto_refresh,
|
|
1179
|
+
"get_new_data": self.get_new_data,
|
|
1180
|
+
"refresh_function": self.refresh_function,
|
|
1181
|
+
"timer": self.timer,
|
|
1182
|
+
"get_data_startup": self.get_data_startup,
|
|
1183
|
+
"get_footer_string_startup": self.get_footer_string_startup,
|
|
1184
|
+
"editable_columns": self.editable_columns,
|
|
1185
|
+
"last_key": self.last_key,
|
|
1186
|
+
"centre_in_terminal": self.centre_in_terminal,
|
|
1187
|
+
"centre_in_terminal_vertical": self.centre_in_terminal_vertical,
|
|
1188
|
+
"centre_in_cols": self.centre_in_cols,
|
|
1189
|
+
"highlight_full_row": self.highlight_full_row,
|
|
1190
|
+
"cell_cursor": self.cell_cursor,
|
|
1191
|
+
"column_widths": self.column_widths,
|
|
1192
|
+
"track_entries_upon_refresh": self.track_entries_upon_refresh,
|
|
1193
|
+
"pin_cursor": self.pin_cursor,
|
|
1194
|
+
"id_column": self.id_column,
|
|
1195
|
+
"startup_notification": self.startup_notification,
|
|
1196
|
+
"keys_dict": self.keys_dict,
|
|
1197
|
+
"cancel_is_back": self.cancel_is_back,
|
|
1198
|
+
"paginate": self.paginate,
|
|
1199
|
+
"leftmost_column": self.leftmost_column,
|
|
1200
|
+
"leftmost_char": self.leftmost_char,
|
|
1201
|
+
"history_filter_and_search" : self.history_filter_and_search,
|
|
1202
|
+
"history_pipes" : self.history_pipes,
|
|
1203
|
+
"history_opts" : self.history_opts,
|
|
1204
|
+
"history_edits" : self.history_edits,
|
|
1205
|
+
"history_settings": self.history_settings,
|
|
1206
|
+
"show_header": self.show_header,
|
|
1207
|
+
"show_row_header": self.show_row_header,
|
|
1208
|
+
"debug": self.debug,
|
|
1209
|
+
"debug_level": self.debug_level,
|
|
1210
|
+
"reset_colours": self.reset_colours,
|
|
1211
|
+
"unicode_char_width": self.unicode_char_width,
|
|
1212
|
+
}
|
|
1213
|
+
return function_data
|
|
1214
|
+
|
|
1215
|
+
def set_function_data(self, function_data: dict) -> None:
|
|
1216
|
+
""" Set variables from state dict containing core variables."""
|
|
1217
|
+
self.logger.info(f"function: set_function_data()")
|
|
1218
|
+
variables = self.get_function_data().keys()
|
|
1219
|
+
|
|
1220
|
+
for var in variables:
|
|
1221
|
+
if var in function_data:
|
|
1222
|
+
setattr(self, var, function_data[var])
|
|
1223
|
+
|
|
1224
|
+
reset_colours = bool("colour_theme_number" in function_data)
|
|
1225
|
+
self.initialise_picker_state(reset_colours=reset_colours)
|
|
1226
|
+
|
|
1227
|
+
self.initialise_variables()
|
|
1228
|
+
# if "colour_theme_number" in function_data:
|
|
1229
|
+
# global COLOURS_SET
|
|
1230
|
+
# COLOURS_SET = False
|
|
1231
|
+
# colours_end = set_colours(pick=self.colour_theme_number, start=self.colours_start)
|
|
1232
|
+
|
|
1233
|
+
# if "items" in function_data: self.items = function_data["items"]
|
|
1234
|
+
# if "header" in function_data: self.header = function_data["header"]
|
|
1235
|
+
# self.indexed_items = function_data["indexed_items"] if "indexed_items" in function_data else []
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
def delete_entries(self) -> None:
|
|
1240
|
+
""" Delete entries from view. """
|
|
1241
|
+
|
|
1242
|
+
self.logger.info(f"function: delete_entries()")
|
|
1243
|
+
# Remove selected items from the list
|
|
1244
|
+
selected_indices = [index for index, selected in self.selections.items() if selected]
|
|
1245
|
+
if not selected_indices:
|
|
1246
|
+
# Remove the currently focused item if nothing is selected
|
|
1247
|
+
selected_indices = [self.indexed_items[self.cursor_pos][0]]
|
|
1248
|
+
|
|
1249
|
+
self.items = [item for i, item in enumerate(self.items) if i not in selected_indices]
|
|
1250
|
+
self.indexed_items = [(i, item) for i, item in enumerate(self.items)]
|
|
1251
|
+
self.selections = {i:False for i in range(len(self.indexed_items))}
|
|
1252
|
+
self.cursor_pos = min(self.cursor_pos, len(self.indexed_items)-1)
|
|
1253
|
+
self.initialise_variables()
|
|
1254
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
def choose_option(
|
|
1258
|
+
self,
|
|
1259
|
+
stdscr: curses.window,
|
|
1260
|
+
options: list[list[str]] =[],
|
|
1261
|
+
title: str = "Choose option",
|
|
1262
|
+
x:int=0,
|
|
1263
|
+
y:int=0,
|
|
1264
|
+
literal:bool=False,
|
|
1265
|
+
colours_start:int=0,
|
|
1266
|
+
header: list[str] = [],
|
|
1267
|
+
require_option:list = [],
|
|
1268
|
+
option_functions: list = [],
|
|
1269
|
+
) -> Tuple[dict, str, dict]:
|
|
1270
|
+
"""
|
|
1271
|
+
Display input field at x,y
|
|
1272
|
+
|
|
1273
|
+
---Arguments
|
|
1274
|
+
stdscr: curses screen
|
|
1275
|
+
usrtxt (str): text to be edited by the user
|
|
1276
|
+
title (str): The text to be displayed at the start of the text option picker
|
|
1277
|
+
x (int): prompt begins at (x,y) in the screen given
|
|
1278
|
+
y (int): prompt begins at (x,y) in the screen given
|
|
1279
|
+
colours_start (bool): start index of curses init_pair.
|
|
1280
|
+
|
|
1281
|
+
---Returns
|
|
1282
|
+
usrtxt, return_code
|
|
1283
|
+
usrtxt: the text inputted by the user
|
|
1284
|
+
return_code:
|
|
1285
|
+
0: user hit escape
|
|
1286
|
+
1: user hit return
|
|
1287
|
+
"""
|
|
1288
|
+
self.logger.info(f"function: choose_option()")
|
|
1289
|
+
if options == []: options = [[f"{i}"] for i in range(10)]
|
|
1290
|
+
cursor = 0
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
option_picker_data = {
|
|
1294
|
+
"items": options,
|
|
1295
|
+
"colours": notification_colours,
|
|
1296
|
+
"colours_start": self.notification_colours_start,
|
|
1297
|
+
"title":title,
|
|
1298
|
+
"header":header,
|
|
1299
|
+
"hidden_columns":[],
|
|
1300
|
+
"require_option":require_option,
|
|
1301
|
+
"keys_dict": options_keys,
|
|
1302
|
+
"show_footer": False,
|
|
1303
|
+
"cancel_is_back": True,
|
|
1304
|
+
"number_columns": False,
|
|
1305
|
+
"reset_colours": False,
|
|
1306
|
+
}
|
|
1307
|
+
while True:
|
|
1308
|
+
h, w = stdscr.getmaxyx()
|
|
1309
|
+
|
|
1310
|
+
choose_opts_widths = get_column_widths(options, unicode_char_width=self.unicode_char_width)
|
|
1311
|
+
window_width = min(max(sum(choose_opts_widths) + 6, 50) + 6, w)
|
|
1312
|
+
window_height = min(h//2, max(6, len(options)+3))
|
|
1313
|
+
|
|
1314
|
+
submenu_win = curses.newwin(window_height, window_width, (h-window_height)//2, (w-window_width)//2)
|
|
1315
|
+
submenu_win.keypad(True)
|
|
1316
|
+
OptionPicker = Picker(submenu_win, **option_picker_data)
|
|
1317
|
+
s, o, f = OptionPicker.run()
|
|
1318
|
+
|
|
1319
|
+
if o == "refresh":
|
|
1320
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
1321
|
+
continue
|
|
1322
|
+
if s:
|
|
1323
|
+
return {x: options[x] for x in s}, o, f
|
|
1324
|
+
return {}, "", f
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def notification(self, stdscr: curses.window, message: str="", title:str="Notification", colours_end: int=0, duration:int=4) -> None:
|
|
1329
|
+
|
|
1330
|
+
self.logger.info(f"function: notification()")
|
|
1331
|
+
""" Notification box. """
|
|
1332
|
+
notification_width, notification_height = 50, 7
|
|
1333
|
+
message_width = notification_width-5
|
|
1334
|
+
|
|
1335
|
+
if not message: message = "!!"
|
|
1336
|
+
submenu_items = [" "+message[i*message_width:(i+1)*message_width] for i in range(len(message)//message_width+1)]
|
|
1337
|
+
|
|
1338
|
+
notification_remap_keys = {
|
|
1339
|
+
curses.KEY_RESIZE: curses.KEY_F5,
|
|
1340
|
+
27: ord('q')
|
|
1341
|
+
}
|
|
1342
|
+
while True:
|
|
1343
|
+
h, w = stdscr.getmaxyx()
|
|
1344
|
+
|
|
1345
|
+
submenu_win = curses.newwin(notification_height, notification_width, 3, w - (notification_width+4))
|
|
1346
|
+
notification_data = {
|
|
1347
|
+
"items": submenu_items,
|
|
1348
|
+
"title": title,
|
|
1349
|
+
"colours_start": self.notification_colours_start,
|
|
1350
|
+
"show_footer": False,
|
|
1351
|
+
"centre_in_terminal": True,
|
|
1352
|
+
"centre_in_terminal_vertical": True,
|
|
1353
|
+
"centre_in_cols": True,
|
|
1354
|
+
"hidden_columns": [],
|
|
1355
|
+
"keys_dict": notification_keys,
|
|
1356
|
+
"disabled_keys": [ord('z'), ord('c')],
|
|
1357
|
+
"highlight_full_row": True,
|
|
1358
|
+
"top_gap": 0,
|
|
1359
|
+
"cancel_is_back": True,
|
|
1360
|
+
"reset_colours": False,
|
|
1361
|
+
|
|
1362
|
+
}
|
|
1363
|
+
OptionPicker = Picker(submenu_win, **notification_data)
|
|
1364
|
+
s, o, f = OptionPicker.run()
|
|
1365
|
+
|
|
1366
|
+
if o != "refresh": break
|
|
1367
|
+
submenu_win.clear()
|
|
1368
|
+
submenu_win.refresh()
|
|
1369
|
+
del submenu_win
|
|
1370
|
+
stdscr.clear()
|
|
1371
|
+
stdscr.refresh()
|
|
1372
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
1373
|
+
# set_colours(colours=get_colours(0))
|
|
1374
|
+
|
|
1375
|
+
def toggle_column_visibility(self, col_index:int) -> None:
|
|
1376
|
+
""" Toggle the visibility of the column at col_index. """
|
|
1377
|
+
self.logger.info(f"function: toggle_column_visibility()")
|
|
1378
|
+
if 0 <= col_index < len(self.items[0]):
|
|
1379
|
+
if col_index in self.hidden_columns:
|
|
1380
|
+
self.hidden_columns.remove(col_index)
|
|
1381
|
+
else:
|
|
1382
|
+
self.hidden_columns.append(col_index)
|
|
1383
|
+
|
|
1384
|
+
def apply_settings(self) -> None:
|
|
1385
|
+
"""
|
|
1386
|
+
The users settings will be stored in the user_settings variable. This function applies those settings.
|
|
1387
|
+
|
|
1388
|
+
![0-9]+ show/hide column
|
|
1389
|
+
s[0-9]+ set column focus for sort
|
|
1390
|
+
g[0-9]+ go to index
|
|
1391
|
+
p[0-9]+ go to page
|
|
1392
|
+
nohl hide search highlights
|
|
1393
|
+
"""
|
|
1394
|
+
self.logger.info(f"function: apply_settings()")
|
|
1395
|
+
if self.user_settings:
|
|
1396
|
+
settings = re.split(r'\s+', self.user_settings)
|
|
1397
|
+
for setting in settings:
|
|
1398
|
+
if len(setting) == 0: continue
|
|
1399
|
+
|
|
1400
|
+
if setting[0] == "!" and len(setting) > 1:
|
|
1401
|
+
if setting[1:].isnumeric():
|
|
1402
|
+
cols = setting[1:].split(",")
|
|
1403
|
+
for col in cols:
|
|
1404
|
+
self.toggle_column_visibility(int(col))
|
|
1405
|
+
elif setting[1] == "r":
|
|
1406
|
+
self.auto_refresh = not self.auto_refresh
|
|
1407
|
+
elif setting[1] == "h":
|
|
1408
|
+
self.highlights_hide = not self.highlights_hide
|
|
1409
|
+
|
|
1410
|
+
elif setting in ["nhl", "nohl", "nohighlights"]:
|
|
1411
|
+
# highlights = [highlight for highlight in highlights if "type" not in highlight or highlight["type"] != "search" ]
|
|
1412
|
+
|
|
1413
|
+
self.highlights_hide = not self.highlights_hide
|
|
1414
|
+
elif setting[0] == "s":
|
|
1415
|
+
if 0 <= int(setting[1:]) < len(self.items[0]):
|
|
1416
|
+
self.sort_column = int(setting[1:])
|
|
1417
|
+
if len(self.indexed_items):
|
|
1418
|
+
current_pos = self.indexed_items[self.cursor_pos][0]
|
|
1419
|
+
sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort items based on new column
|
|
1420
|
+
if len(self.indexed_items):
|
|
1421
|
+
new_pos = [row[0] for row in self.indexed_items].index(current_pos)
|
|
1422
|
+
self.cursor_pos = new_pos
|
|
1423
|
+
elif setting == "ct":
|
|
1424
|
+
self.centre_in_terminal = not self.centre_in_terminal
|
|
1425
|
+
elif setting == "cc":
|
|
1426
|
+
self.centre_in_cols = not self.centre_in_cols
|
|
1427
|
+
elif setting == "cv":
|
|
1428
|
+
self.centre_in_terminal_vertical = not self.centre_in_terminal_vertical
|
|
1429
|
+
elif setting == "arb":
|
|
1430
|
+
self.insert_row(self.cursor_pos)
|
|
1431
|
+
elif setting == "ara":
|
|
1432
|
+
self.insert_row(self.cursor_pos+1)
|
|
1433
|
+
elif setting == "aca":
|
|
1434
|
+
self.insert_column(self.selected_column+1)
|
|
1435
|
+
elif setting == "acb":
|
|
1436
|
+
self.insert_column(self.selected_column)
|
|
1437
|
+
elif setting.startswith("ir"):
|
|
1438
|
+
if setting[2:].isnumeric():
|
|
1439
|
+
num = int(setting[2:])
|
|
1440
|
+
else:
|
|
1441
|
+
num = self.cursor_pos
|
|
1442
|
+
self.insert_row(num)
|
|
1443
|
+
elif setting.startswith("ic"):
|
|
1444
|
+
if setting[2:].isnumeric():
|
|
1445
|
+
num = int(setting[2:])
|
|
1446
|
+
else:
|
|
1447
|
+
num = self.selected_column
|
|
1448
|
+
self.insert_column(num)
|
|
1449
|
+
|
|
1450
|
+
elif setting == "modes":
|
|
1451
|
+
self.display_modes = not self.display_modes
|
|
1452
|
+
elif setting == "cell":
|
|
1453
|
+
self.cell_cursor = not self.cell_cursor
|
|
1454
|
+
elif setting == "rh":
|
|
1455
|
+
self.show_row_header = not self.show_row_header
|
|
1456
|
+
elif setting == "header":
|
|
1457
|
+
self.show_header = not self.show_header
|
|
1458
|
+
elif setting[0] == "":
|
|
1459
|
+
cols = setting[1:].split(",")
|
|
1460
|
+
elif setting == "footer":
|
|
1461
|
+
self.show_footer = not self.show_footer
|
|
1462
|
+
self.initialise_variables()
|
|
1463
|
+
elif setting == "pc":
|
|
1464
|
+
self.pin_cursor = not self.pin_cursor
|
|
1465
|
+
elif setting == "unicode":
|
|
1466
|
+
self.unicode_char_width = not self.unicode_char_width
|
|
1467
|
+
|
|
1468
|
+
elif setting.startswith("ft"):
|
|
1469
|
+
if len(setting) > 2 and setting[2:].isnumeric():
|
|
1470
|
+
|
|
1471
|
+
num = int(setting[2:])
|
|
1472
|
+
self.footer_style = max(len(self.footer_options)-1, num)
|
|
1473
|
+
self.footer = self.footer_options[self.footer_style]
|
|
1474
|
+
else:
|
|
1475
|
+
self.footer_style = (self.footer_style+1)%len(self.footer_options)
|
|
1476
|
+
self.footer = self.footer_options[self.footer_style]
|
|
1477
|
+
self.initialise_variables()
|
|
1478
|
+
|
|
1479
|
+
elif setting.startswith("cwd="):
|
|
1480
|
+
os.chdir(os.path.expandvars(os.path.expanduser(setting[len("cwd="):])))
|
|
1481
|
+
elif setting.startswith("hl"):
|
|
1482
|
+
hl_list = setting.split(",")
|
|
1483
|
+
if len(hl_list) > 1:
|
|
1484
|
+
hl_list = hl_list[1:]
|
|
1485
|
+
match = hl_list[0]
|
|
1486
|
+
if len(hl_list) > 1:
|
|
1487
|
+
field = hl_list[1]
|
|
1488
|
+
if field.isnumeric() and field != "-1":
|
|
1489
|
+
field = int(field)
|
|
1490
|
+
else:
|
|
1491
|
+
field = "all"
|
|
1492
|
+
else:
|
|
1493
|
+
field = "all"
|
|
1494
|
+
if len(hl_list) > 2 and hl_list[2].isnumeric():
|
|
1495
|
+
colour_pair = int(hl_list[2])
|
|
1496
|
+
else:
|
|
1497
|
+
colour_pair = 10
|
|
1498
|
+
|
|
1499
|
+
highlight = {
|
|
1500
|
+
"match": match,
|
|
1501
|
+
"field": field,
|
|
1502
|
+
"color": colour_pair
|
|
1503
|
+
}
|
|
1504
|
+
self.highlights.append(highlight)
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
elif setting.startswith("th"):
|
|
1508
|
+
global COLOURS_SET
|
|
1509
|
+
if curses.COLORS < 255:
|
|
1510
|
+
self.notification(self.stdscr, message=f"Theme 4 applied.")
|
|
1511
|
+
|
|
1512
|
+
elif setting[2:].strip().isnumeric():
|
|
1513
|
+
COLOURS_SET = False
|
|
1514
|
+
try:
|
|
1515
|
+
theme_number = int(setting[2:].strip())
|
|
1516
|
+
self.colour_theme_number = min(get_theme_count()-1, theme_number)
|
|
1517
|
+
set_colours(self.colour_theme_number)
|
|
1518
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
1519
|
+
self.notification(self.stdscr, message=f"Theme {self.colour_theme_number} applied.")
|
|
1520
|
+
except:
|
|
1521
|
+
pass
|
|
1522
|
+
else:
|
|
1523
|
+
COLOURS_SET = False
|
|
1524
|
+
self.colour_theme_number = (self.colour_theme_number + 1)%get_theme_count()
|
|
1525
|
+
# self.colour_theme_number = int(not bool(self.colour_theme_number))
|
|
1526
|
+
set_colours(self.colour_theme_number)
|
|
1527
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
1528
|
+
self.notification(self.stdscr, message=f"Theme {self.colour_theme_number} applied.")
|
|
1529
|
+
|
|
1530
|
+
else:
|
|
1531
|
+
self.user_settings = ""
|
|
1532
|
+
return None
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
self.command_stack.append(Command("setting", self.user_settings))
|
|
1536
|
+
self.user_settings = ""
|
|
1537
|
+
|
|
1538
|
+
def apply_command(self, command: Command):
|
|
1539
|
+
self.logger.info(f"function: apply_command()")
|
|
1540
|
+
if command.command_type == "setting":
|
|
1541
|
+
self.user_settings = command.command_value
|
|
1542
|
+
self.apply_settings()
|
|
1543
|
+
|
|
1544
|
+
def redo(self):
|
|
1545
|
+
self.logger.info(f"function: redo()")
|
|
1546
|
+
if len(self.command_stack):
|
|
1547
|
+
self.apply_command(self.command_stack[-1])
|
|
1548
|
+
|
|
1549
|
+
def toggle_item(self, index: int) -> None:
|
|
1550
|
+
""" Toggle selection of item at index. """
|
|
1551
|
+
self.logger.info(f"function: toggle_item()")
|
|
1552
|
+
self.selections[index] = not self.selections[index]
|
|
1553
|
+
|
|
1554
|
+
def select_all(self) -> None:
|
|
1555
|
+
""" Select all in indexed_items. """
|
|
1556
|
+
self.logger.info(f"function: select_all()")
|
|
1557
|
+
for i in range(len(self.indexed_items)):
|
|
1558
|
+
self.selections[self.indexed_items[i][0]] = True
|
|
1559
|
+
for i in self.cell_selections.keys():
|
|
1560
|
+
self.cell_selections[i] = True
|
|
1561
|
+
for row in range(len(self.indexed_items)):
|
|
1562
|
+
self.selected_cells_by_row[row] = list(range(len(self.indexed_items[row][1])))
|
|
1563
|
+
|
|
1564
|
+
def deselect_all(self) -> None:
|
|
1565
|
+
""" Deselect all items in indexed_items. """
|
|
1566
|
+
self.logger.info(f"function: deselect_all()")
|
|
1567
|
+
for i in range(len(self.selections)):
|
|
1568
|
+
self.selections[i] = False
|
|
1569
|
+
for i in self.cell_selections.keys():
|
|
1570
|
+
self.cell_selections[i] = False
|
|
1571
|
+
self.selected_cells_by_row = {}
|
|
1572
|
+
|
|
1573
|
+
def handle_visual_selection(self, selecting:bool = True) -> None:
|
|
1574
|
+
""" Toggle visual selection or deselection. """
|
|
1575
|
+
self.logger.info(f"function: handle_visual_selection()")
|
|
1576
|
+
if not self.is_selecting and not self.is_deselecting and len(self.indexed_items) and len(self.indexed_items[0][1]):
|
|
1577
|
+
self.start_selection = self.cursor_pos
|
|
1578
|
+
self.start_selection_col = self.selected_column
|
|
1579
|
+
if selecting:
|
|
1580
|
+
self.is_selecting = True
|
|
1581
|
+
else:
|
|
1582
|
+
self.is_deselecting = True
|
|
1583
|
+
elif self.is_selecting:
|
|
1584
|
+
# end_selection = indexed_items[current_page * items_per_page + current_row][0]
|
|
1585
|
+
self.end_selection = self.cursor_pos
|
|
1586
|
+
if self.start_selection != -1:
|
|
1587
|
+
start = max(min(self.start_selection, self.end_selection), 0)
|
|
1588
|
+
end = min(max(self.start_selection, self.end_selection), len(self.indexed_items)-1)
|
|
1589
|
+
for i in range(start, end + 1):
|
|
1590
|
+
if self.indexed_items[i][0] not in self.unselectable_indices:
|
|
1591
|
+
self.selections[self.indexed_items[i][0]] = True
|
|
1592
|
+
if self.start_selection != -1:
|
|
1593
|
+
ystart = max(min(self.start_selection, self.end_selection), 0)
|
|
1594
|
+
yend = min(max(self.start_selection, self.end_selection), len(self.indexed_items)-1)
|
|
1595
|
+
xstart = min(self.start_selection_col, self.selected_column)
|
|
1596
|
+
xend = max(self.start_selection_col, self.selected_column)
|
|
1597
|
+
for i in range(ystart, yend + 1):
|
|
1598
|
+
if self.indexed_items[i][0] not in self.unselectable_indices:
|
|
1599
|
+
row = self.indexed_items[i][0]
|
|
1600
|
+
if row not in self.selected_cells_by_row:
|
|
1601
|
+
self.selected_cells_by_row[row] = []
|
|
1602
|
+
|
|
1603
|
+
for col in range(xstart, xend+1):
|
|
1604
|
+
cell_index = (row, col)
|
|
1605
|
+
self.cell_selections[cell_index] = True
|
|
1606
|
+
|
|
1607
|
+
self.selected_cells_by_row[row].append(col)
|
|
1608
|
+
# Remove duplicates
|
|
1609
|
+
self.selected_cells_by_row[row] = list(set(self.selected_cells_by_row[row]))
|
|
1610
|
+
|
|
1611
|
+
self.start_selection = -1
|
|
1612
|
+
self.end_selection = -1
|
|
1613
|
+
self.is_selecting = False
|
|
1614
|
+
|
|
1615
|
+
elif self.is_deselecting:
|
|
1616
|
+
self.end_selection = self.indexed_items[self.cursor_pos][0]
|
|
1617
|
+
self.end_selection = self.cursor_pos
|
|
1618
|
+
if self.start_selection != -1:
|
|
1619
|
+
start = max(min(self.start_selection, self.end_selection), 0)
|
|
1620
|
+
end = min(max(self.start_selection, self.end_selection), len(self.indexed_items)-1)
|
|
1621
|
+
for i in range(start, end + 1):
|
|
1622
|
+
# selections[i] = False
|
|
1623
|
+
self.selections[self.indexed_items[i][0]] = False
|
|
1624
|
+
if self.start_selection != -1:
|
|
1625
|
+
ystart = max(min(self.start_selection, self.end_selection), 0)
|
|
1626
|
+
yend = min(max(self.start_selection, self.end_selection), len(self.indexed_items)-1)
|
|
1627
|
+
xstart = min(self.start_selection_col, self.selected_column)
|
|
1628
|
+
xend = max(self.start_selection_col, self.selected_column)
|
|
1629
|
+
for i in range(ystart, yend + 1):
|
|
1630
|
+
row = self.indexed_items[i][0]
|
|
1631
|
+
if self.indexed_items[i][0] not in self.unselectable_indices:
|
|
1632
|
+
if row in self.selected_cells_by_row:
|
|
1633
|
+
for col in range(xstart, xend+1):
|
|
1634
|
+
try:
|
|
1635
|
+
self.selected_cells_by_row[row].remove(col)
|
|
1636
|
+
except:
|
|
1637
|
+
pass
|
|
1638
|
+
cell_index = (row, col)
|
|
1639
|
+
self.cell_selections[cell_index] = False
|
|
1640
|
+
if self.selected_cells_by_row[row] == []:
|
|
1641
|
+
del self.selected_cells_by_row[row]
|
|
1642
|
+
|
|
1643
|
+
self.start_selection = -1
|
|
1644
|
+
self.end_selection = -1
|
|
1645
|
+
self.is_deselecting = False
|
|
1646
|
+
|
|
1647
|
+
def cursor_down(self, count=1) -> bool:
|
|
1648
|
+
""" Move cursor down. """
|
|
1649
|
+
self.logger.info(f"function: cursor_down()")
|
|
1650
|
+
if len(self.indexed_items) == 0 or self.cursor_pos == len(self.indexed_items) -1:
|
|
1651
|
+
return False
|
|
1652
|
+
# Returns: whether page is turned
|
|
1653
|
+
new_pos = self.cursor_pos + 1
|
|
1654
|
+
new_pos = min(self.cursor_pos+count, len(self.indexed_items)-1)
|
|
1655
|
+
while True:
|
|
1656
|
+
if self.indexed_items[new_pos][0] in self.unselectable_indices: new_pos+=1
|
|
1657
|
+
else: break
|
|
1658
|
+
self.cursor_pos = new_pos
|
|
1659
|
+
return True
|
|
1660
|
+
|
|
1661
|
+
def cursor_up(self, count=1) -> bool:
|
|
1662
|
+
""" Move cursor up. """
|
|
1663
|
+
self.logger.info(f"function: cursor_up()")
|
|
1664
|
+
# Returns: whether page is turned
|
|
1665
|
+
|
|
1666
|
+
new_pos = max(self.cursor_pos - count, 0)
|
|
1667
|
+
while True:
|
|
1668
|
+
if new_pos < 0: return False
|
|
1669
|
+
elif new_pos in self.unselectable_indices: new_pos -= 1
|
|
1670
|
+
else: break
|
|
1671
|
+
self.cursor_pos = new_pos
|
|
1672
|
+
return True
|
|
1673
|
+
|
|
1674
|
+
def remapped_key(self, key: int, val: int, key_remappings: dict) -> bool:
|
|
1675
|
+
""" Check if key has been remapped to val in key_remappings. """
|
|
1676
|
+
# self.logger.info(f"function: remapped_key()")
|
|
1677
|
+
if key in key_remappings:
|
|
1678
|
+
if key_remappings[key] == val or (isinstance(key_remappings[key], list) and val in key_remappings[key]):
|
|
1679
|
+
return True
|
|
1680
|
+
return False
|
|
1681
|
+
|
|
1682
|
+
def check_key(self, function: str, key: int, keys_dict: dict) -> bool:
|
|
1683
|
+
"""
|
|
1684
|
+
Check if $key is assigned to $function in the keys_dict.
|
|
1685
|
+
Allows us to redefine functions to different keys in the keys_dict.
|
|
1686
|
+
|
|
1687
|
+
E.g., keys_dict = { $key, "help": ord('?') },
|
|
1688
|
+
"""
|
|
1689
|
+
if function in keys_dict and key in keys_dict[function]:
|
|
1690
|
+
return True
|
|
1691
|
+
return False
|
|
1692
|
+
|
|
1693
|
+
def copy_dialogue(self) -> None:
|
|
1694
|
+
""" Display dialogue to select how rows/cells should be copied. """
|
|
1695
|
+
self.logger.info(f"function: copy_dialogue()")
|
|
1696
|
+
copy_header = [
|
|
1697
|
+
"Representation",
|
|
1698
|
+
"Columns",
|
|
1699
|
+
]
|
|
1700
|
+
options = [
|
|
1701
|
+
["Python list of lists", "Exclude hidden"],
|
|
1702
|
+
["Python list of lists", "Include hidden"],
|
|
1703
|
+
["Tab-separated values", "Exclude hidden"],
|
|
1704
|
+
["Tab-separated values", "Include hidden"],
|
|
1705
|
+
["Comma-separated values", "Exclude hidden"],
|
|
1706
|
+
["Comma-separated values", "Include hidden"],
|
|
1707
|
+
["Custom separator", "Exclude hidden"],
|
|
1708
|
+
["Custom separator", "Include hidden"],
|
|
1709
|
+
]
|
|
1710
|
+
require_option = [False, False, False, False, False, False, True, True]
|
|
1711
|
+
s, o, f = self.choose_option(self.stdscr, options=options, title="Copy selected", header=copy_header, require_option=require_option)
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
funcs = [
|
|
1715
|
+
lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="python", copy_hidden_cols=False, cellwise=cell_cursor),
|
|
1716
|
+
lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="python", copy_hidden_cols=True, cellwise=cell_cursor),
|
|
1717
|
+
lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="tsv", copy_hidden_cols=False, cellwise=cell_cursor),
|
|
1718
|
+
lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="tsv", copy_hidden_cols=True, cellwise=cell_cursor),
|
|
1719
|
+
lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="csv", copy_hidden_cols=False, cellwise=cell_cursor),
|
|
1720
|
+
lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="csv", copy_hidden_cols=True, cellwise=cell_cursor),
|
|
1721
|
+
lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="custom_sv", copy_hidden_cols=False, separator=o, cellwise=cell_cursor),
|
|
1722
|
+
lambda items, indexed_items, selections, cell_selections, hidden_columns, cell_cursor: copy_to_clipboard(items, indexed_items, selections, cell_selections, hidden_columns, representation="custom_sv", copy_hidden_cols=True, separator=o, cellwise=cell_cursor),
|
|
1723
|
+
]
|
|
1724
|
+
|
|
1725
|
+
# Copy items based on selection
|
|
1726
|
+
if s:
|
|
1727
|
+
for idx in s.keys():
|
|
1728
|
+
funcs[idx](self.items, self.indexed_items, self.selections, self.cell_selections, self.hidden_columns, self.cell_cursor)
|
|
1729
|
+
def paste_dialogue(self) -> None:
|
|
1730
|
+
""" Display dialogue to select how to paste from the clipboard. """
|
|
1731
|
+
self.logger.info(f"function: paste_dialogue()")
|
|
1732
|
+
paste_header = [
|
|
1733
|
+
"Representation",
|
|
1734
|
+
"Columns",
|
|
1735
|
+
]
|
|
1736
|
+
options = [
|
|
1737
|
+
["Paste values", ""],
|
|
1738
|
+
]
|
|
1739
|
+
require_option = [False]
|
|
1740
|
+
s, o, f = self.choose_option(self.stdscr, options=options, title="Paste values", header=paste_header, require_option=require_option)
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
funcs = [
|
|
1744
|
+
lambda items, pasta, paste_row, paste_col: paste_values(items, pasta, paste_row, paste_col)
|
|
1745
|
+
]
|
|
1746
|
+
|
|
1747
|
+
try:
|
|
1748
|
+
pasta = eval(pyperclip.paste())
|
|
1749
|
+
if type(pasta) == type([]):
|
|
1750
|
+
acceptable_data_type = True
|
|
1751
|
+
for row in pasta:
|
|
1752
|
+
if type(row) != type([]):
|
|
1753
|
+
acceptable_data_type = False
|
|
1754
|
+
break
|
|
1755
|
+
|
|
1756
|
+
for cell in row:
|
|
1757
|
+
if cell != None and type(cell) != type(""):
|
|
1758
|
+
acceptable_data_type = False
|
|
1759
|
+
break
|
|
1760
|
+
if not acceptable_data_type:
|
|
1761
|
+
break
|
|
1762
|
+
if not acceptable_data_type:
|
|
1763
|
+
self.notification(self.stdscr, message="Error pasting data.")
|
|
1764
|
+
return None
|
|
1765
|
+
|
|
1766
|
+
except:
|
|
1767
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
1768
|
+
self.notification(self.stdscr, message="Error pasting data.")
|
|
1769
|
+
return None
|
|
1770
|
+
if type(pasta) == type([]) and len(pasta) > 0 and type(pasta[0]) == type([]):
|
|
1771
|
+
if s:
|
|
1772
|
+
for idx in s.keys():
|
|
1773
|
+
return_val, tmp_items = funcs[idx](self.items, pasta, self.cursor_pos, self.selected_column)
|
|
1774
|
+
if return_val:
|
|
1775
|
+
cursor_pos = self.cursor_pos
|
|
1776
|
+
self.items = tmp_items
|
|
1777
|
+
self.initialise_variables()
|
|
1778
|
+
self.cursor_pos = cursor_pos
|
|
1779
|
+
|
|
1780
|
+
def save_dialog(self) -> None:
|
|
1781
|
+
""" Display dialogue to select how to save the picker data. """
|
|
1782
|
+
self.logger.info(f"function: save_dialog()")
|
|
1783
|
+
|
|
1784
|
+
dump_header = []
|
|
1785
|
+
options = [
|
|
1786
|
+
["Save data (pickle)."],
|
|
1787
|
+
["Save data (csv)."],
|
|
1788
|
+
["Save data (tsv)."],
|
|
1789
|
+
["Save data (json)."],
|
|
1790
|
+
["Save data (feather)."],
|
|
1791
|
+
["Save data (parquet)."],
|
|
1792
|
+
["Save data (msgpack)."],
|
|
1793
|
+
["Save state"]
|
|
1794
|
+
]
|
|
1795
|
+
# require_option = [True, True, True, True, True, True, True, True]
|
|
1796
|
+
s, o, f = self.choose_option(self.stdscr, options=options, title="Save...", header=dump_header)
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
funcs = [
|
|
1800
|
+
lambda opts: dump_data(self.get_function_data(), opts),
|
|
1801
|
+
lambda opts: dump_data(self.get_function_data(), opts, format="csv"),
|
|
1802
|
+
lambda opts: dump_data(self.get_function_data(), opts, format="tsv"),
|
|
1803
|
+
lambda opts: dump_data(self.get_function_data(), opts, format="json"),
|
|
1804
|
+
lambda opts: dump_data(self.get_function_data(), opts, format="feather"),
|
|
1805
|
+
lambda opts: dump_data(self.get_function_data(), opts, format="parquet"),
|
|
1806
|
+
lambda opts: dump_data(self.get_function_data(), opts, format="msgpack"),
|
|
1807
|
+
lambda opts: dump_state(self.get_function_data(), opts),
|
|
1808
|
+
]
|
|
1809
|
+
|
|
1810
|
+
if s:
|
|
1811
|
+
for idx in s.keys():
|
|
1812
|
+
save_path_entered, save_path = output_file_option_selector(
|
|
1813
|
+
self.stdscr,
|
|
1814
|
+
refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
|
|
1815
|
+
)
|
|
1816
|
+
if save_path_entered:
|
|
1817
|
+
return_val = funcs[idx](save_path)
|
|
1818
|
+
if return_val:
|
|
1819
|
+
self.notification(self.stdscr, message=return_val, title="Error")
|
|
1820
|
+
|
|
1821
|
+
def load_dialog(self) -> None:
|
|
1822
|
+
""" Display dialogue to select which file to load and in what way it should be loaded. """
|
|
1823
|
+
self.logger.info(f"function: load_dialog()")
|
|
1824
|
+
|
|
1825
|
+
dump_header = []
|
|
1826
|
+
options = [
|
|
1827
|
+
["Load data (pickle)."],
|
|
1828
|
+
]
|
|
1829
|
+
s, o, f = self.choose_option(self.stdscr, options=options, title="Open file...", header=dump_header)
|
|
1830
|
+
|
|
1831
|
+
|
|
1832
|
+
funcs = [
|
|
1833
|
+
lambda opts: load_state(opts)
|
|
1834
|
+
]
|
|
1835
|
+
|
|
1836
|
+
if s:
|
|
1837
|
+
file_to_load = file_picker()
|
|
1838
|
+
if file_to_load:
|
|
1839
|
+
index = list(s.keys())[0]
|
|
1840
|
+
return_val = funcs[index](file_to_load)
|
|
1841
|
+
self.set_function_data(return_val)
|
|
1842
|
+
|
|
1843
|
+
# items = return_val["items"]
|
|
1844
|
+
# header = return_val["header"]
|
|
1845
|
+
self.initialise_variables()
|
|
1846
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
1847
|
+
self.notification(self.stdscr, f"{repr(file_to_load)} has been loaded!")
|
|
1848
|
+
# if return_val:
|
|
1849
|
+
# notification(stdscr, message=return_val, title="Error")
|
|
1850
|
+
|
|
1851
|
+
def set_registers(self):
|
|
1852
|
+
""" Set registers to be sent to the input field. """
|
|
1853
|
+
self.logger.info(f"function: set_registers()")
|
|
1854
|
+
self.registers = {"*": self.indexed_items[self.cursor_pos][1][self.selected_column]} if len(self.indexed_items) and len(self.indexed_items[0][1]) else {}
|
|
1855
|
+
|
|
1856
|
+
|
|
1857
|
+
def fetch_data(self) -> None:
|
|
1858
|
+
""" Refesh data asynchronously. When data has been fetched self.data_ready is set to True. """
|
|
1859
|
+
self.logger.info(f"function: fetch_data()")
|
|
1860
|
+
tmp_items, tmp_header = self.refresh_function()
|
|
1861
|
+
if self.track_entries_upon_refresh:
|
|
1862
|
+
selected_indices = get_selected_indices(self.selections)
|
|
1863
|
+
self.ids = [item[self.id_column] for i, item in enumerate(self.items) if i in selected_indices]
|
|
1864
|
+
self.ids_tuples = [(i, item[self.id_column]) for i, item in enumerate(self.items) if i in selected_indices]
|
|
1865
|
+
self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
|
|
1866
|
+
|
|
1867
|
+
if len(self.indexed_items) > 0 and len(self.indexed_items) >= self.cursor_pos and len(self.indexed_items[0][1]) >= self.id_column:
|
|
1868
|
+
self.cursor_pos_id = self.indexed_items[self.cursor_pos][1][self.id_column]
|
|
1869
|
+
self.cursor_pos_prev = self.cursor_pos
|
|
1870
|
+
with self.data_lock:
|
|
1871
|
+
self.items, self.header = tmp_items, tmp_header
|
|
1872
|
+
self.data_ready = True
|
|
1873
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
1874
|
+
|
|
1875
|
+
def save_input_history(self, file_path: str) -> bool:
|
|
1876
|
+
""" Save input field history. Returns True if successful save. """
|
|
1877
|
+
self.logger.info(f"function: save_input_history()")
|
|
1878
|
+
file_path = os.path.expanduser(file_path)
|
|
1879
|
+
history_dict = {
|
|
1880
|
+
"history_filter_and_search" : self.history_filter_and_search,
|
|
1881
|
+
"history_pipes" : self.history_pipes,
|
|
1882
|
+
"history_opts" : self.history_opts,
|
|
1883
|
+
"history_edits" : self.history_edits,
|
|
1884
|
+
"history_settings": self.history_settings,
|
|
1885
|
+
}
|
|
1886
|
+
with open(file_path, 'w') as f:
|
|
1887
|
+
json.dump(history_dict, f)
|
|
1888
|
+
|
|
1889
|
+
return True
|
|
1890
|
+
|
|
1891
|
+
def load_input_history(self, file_path:str) -> bool:
|
|
1892
|
+
""" Load command history. Returns true if successful load. """
|
|
1893
|
+
self.logger.info(f"function: load_input_history()")
|
|
1894
|
+
file_path = os.path.expanduser(file_path)
|
|
1895
|
+
if not os.path.exists(file_path):
|
|
1896
|
+
return False
|
|
1897
|
+
try:
|
|
1898
|
+
with open(file_path, 'r') as f:
|
|
1899
|
+
history_dict = json.load(f)
|
|
1900
|
+
|
|
1901
|
+
if "history_filter_and_search" in history_dict:
|
|
1902
|
+
self.history_filter_and_search = history_dict["history_filter_and_search"]
|
|
1903
|
+
if "history_pipes" in history_dict:
|
|
1904
|
+
self.history_pipes = history_dict["history_pipes"]
|
|
1905
|
+
if "history_opts" in history_dict:
|
|
1906
|
+
self.history_opts = history_dict["history_opts"]
|
|
1907
|
+
if "history_edits" in history_dict:
|
|
1908
|
+
self.history_edits = history_dict["history_edits"]
|
|
1909
|
+
if "history_settings" in history_dict:
|
|
1910
|
+
self.history_settings = history_dict["history_settings"]
|
|
1911
|
+
|
|
1912
|
+
except:
|
|
1913
|
+
return False
|
|
1914
|
+
|
|
1915
|
+
return True
|
|
1916
|
+
|
|
1917
|
+
|
|
1918
|
+
def get_word_list(self) -> list[str]:
|
|
1919
|
+
""" Get a list of all words used in any cell of the picker. Used for completion in search/filter input_field. """
|
|
1920
|
+
self.logger.info(f"function: get_word_list()")
|
|
1921
|
+
translator = str.maketrans('', '', string.punctuation)
|
|
1922
|
+
|
|
1923
|
+
words = []
|
|
1924
|
+
# Extract words from lists
|
|
1925
|
+
for row in [x[1] for x in self.indexed_items]:
|
|
1926
|
+
for i, cell in enumerate(row):
|
|
1927
|
+
if i != (self.id_column%len(row)):
|
|
1928
|
+
# Split the item into words and strip punctuation from each word
|
|
1929
|
+
words.extend(word.strip(string.punctuation) for word in cell.split())
|
|
1930
|
+
for cell in self.header:
|
|
1931
|
+
# Split the item into words and strip punctuation from each word
|
|
1932
|
+
words.extend(word.strip(string.punctuation) for word in cell.split())
|
|
1933
|
+
def key_f(s):
|
|
1934
|
+
if len(s):
|
|
1935
|
+
starts_with_char = s[0].isalpha()
|
|
1936
|
+
else:
|
|
1937
|
+
starts_with_char = False
|
|
1938
|
+
return (not starts_with_char, s.lower())
|
|
1939
|
+
# key = lambda s: (s != "" or not s[0].isalpha(), s)
|
|
1940
|
+
words = sorted(list(set(words)), key=key_f)
|
|
1941
|
+
return words
|
|
1942
|
+
|
|
1943
|
+
def insert_row(self, pos: int):
|
|
1944
|
+
""" Insert a blank row at position `pos` """
|
|
1945
|
+
self.logger.info(f"function: insert_row(pos={pos})")
|
|
1946
|
+
|
|
1947
|
+
if self.items != [[]]:
|
|
1948
|
+
row_len = 1
|
|
1949
|
+
if self.header: row_len = len(self.header)
|
|
1950
|
+
elif len(self.items): row_len = len(self.items[0])
|
|
1951
|
+
# if len(self.indexed_items) == 0:
|
|
1952
|
+
# insert_at_pos = 0
|
|
1953
|
+
# else:
|
|
1954
|
+
# insert_at_pos = self.indexed_items[self.cursor_pos][0]
|
|
1955
|
+
self.items = self.items[:pos] + [["" for x in range(row_len)]] + self.items[pos:]
|
|
1956
|
+
if pos <= self.cursor_pos:
|
|
1957
|
+
self.cursor_pos += 1
|
|
1958
|
+
# We are adding a row before so we have to move the cursor down
|
|
1959
|
+
# If there is a filter then we know that an empty row doesn't match
|
|
1960
|
+
current_cursor_pos = self.cursor_pos
|
|
1961
|
+
self.initialise_variables()
|
|
1962
|
+
self.cursor_pos = current_cursor_pos
|
|
1963
|
+
else:
|
|
1964
|
+
self.items = [[""]]
|
|
1965
|
+
self.initialise_variables()
|
|
1966
|
+
|
|
1967
|
+
def insert_column(self, pos:int):
|
|
1968
|
+
""" Insert blank column at `pos`"""
|
|
1969
|
+
self.logger.info(f"function: insert_column(pos={pos})")
|
|
1970
|
+
self.items = [row[:pos]+[""]+row[pos:] for row in self.items]
|
|
1971
|
+
self.header = self.header[:pos] + [""] + self.header[pos:]
|
|
1972
|
+
self.editable_columns = self.editable_columns[:pos] + [self.editable_by_default] + self.editable_columns[pos:]
|
|
1973
|
+
if pos <= self.selected_column:
|
|
1974
|
+
self.selected_column += 1
|
|
1975
|
+
current_cursor_pos = self.cursor_pos
|
|
1976
|
+
self.initialise_variables()
|
|
1977
|
+
self.cursor_pos = current_cursor_pos
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
def run(self) -> Tuple[list[int], str, dict]:
|
|
1982
|
+
""" Run the picker. """
|
|
1983
|
+
self.logger.info(f"function: run()")
|
|
1984
|
+
|
|
1985
|
+
if self.get_footer_string_startup and self.footer_string_refresh_function != None:
|
|
1986
|
+
self.footer_string = self.footer_string_refresh_function()
|
|
1987
|
+
|
|
1988
|
+
self.initialise_variables(get_data=self.get_data_startup)
|
|
1989
|
+
|
|
1990
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
1991
|
+
|
|
1992
|
+
initial_time = time.time()
|
|
1993
|
+
initial_time_footer = time.time()-self.footer_timer
|
|
1994
|
+
|
|
1995
|
+
if self.startup_notification:
|
|
1996
|
+
self.notification(self.stdscr, message=self.startup_notification)
|
|
1997
|
+
self.startup_notification = ""
|
|
1998
|
+
|
|
1999
|
+
# curses.curs_set(0)
|
|
2000
|
+
# stdscr.nodelay(1) # Non-blocking input
|
|
2001
|
+
# stdscr.timeout(2000) # Set a timeout for getch() to ensure it does not block indefinitely
|
|
2002
|
+
self.stdscr.timeout(max(min(2000, int(self.timer*1000)//2, int(self.footer_timer*1000))//2, 20)) # Set a timeout for getch() to ensure it does not block indefinitely
|
|
2003
|
+
|
|
2004
|
+
if self.clear_on_start:
|
|
2005
|
+
self.stdscr.clear()
|
|
2006
|
+
self.clear_on_start = False
|
|
2007
|
+
else:
|
|
2008
|
+
self.stdscr.erase()
|
|
2009
|
+
|
|
2010
|
+
self.stdscr.refresh()
|
|
2011
|
+
|
|
2012
|
+
# Initialize colours
|
|
2013
|
+
# Check if terminal supports color
|
|
2014
|
+
|
|
2015
|
+
# Set terminal background color
|
|
2016
|
+
self.stdscr.bkgd(' ', curses.color_pair(self.colours_start+3)) # Apply background color
|
|
2017
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
2018
|
+
|
|
2019
|
+
if self.display_only:
|
|
2020
|
+
self.stdscr.refresh()
|
|
2021
|
+
return [], "", {}
|
|
2022
|
+
|
|
2023
|
+
# Main loop
|
|
2024
|
+
|
|
2025
|
+
while True:
|
|
2026
|
+
key = self.stdscr.getch()
|
|
2027
|
+
h, w = self.stdscr.getmaxyx()
|
|
2028
|
+
if key in self.disabled_keys: continue
|
|
2029
|
+
clear_screen=True
|
|
2030
|
+
|
|
2031
|
+
## Refresh data asyncronously.
|
|
2032
|
+
if self.refreshing_data:
|
|
2033
|
+
self.logger.debug(f"Data refresh check")
|
|
2034
|
+
with self.data_lock:
|
|
2035
|
+
if self.data_ready:
|
|
2036
|
+
self.logger.debug(f"Data ready after refresh")
|
|
2037
|
+
self.initialise_variables()
|
|
2038
|
+
|
|
2039
|
+
initial_time = time.time()
|
|
2040
|
+
|
|
2041
|
+
self.draw_screen(self.indexed_items, self.highlights, clear=False)
|
|
2042
|
+
|
|
2043
|
+
self.refreshing_data = False
|
|
2044
|
+
self.data_ready = False
|
|
2045
|
+
|
|
2046
|
+
elif self.check_key("refresh", key, self.keys_dict) or self.remapped_key(key, curses.KEY_F5, self.key_remappings) or (self.auto_refresh and (time.time() - initial_time) >= self.timer):
|
|
2047
|
+
self.logger.debug(f"Get new data (refresh).")
|
|
2048
|
+
self.stdscr.addstr(0,w-3," ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
|
|
2049
|
+
self.stdscr.refresh()
|
|
2050
|
+
if self.get_new_data and self.refresh_function:
|
|
2051
|
+
self.refreshing_data = True
|
|
2052
|
+
|
|
2053
|
+
t = threading.Thread(target=self.fetch_data)
|
|
2054
|
+
t.start()
|
|
2055
|
+
else:
|
|
2056
|
+
function_data = self.get_function_data()
|
|
2057
|
+
return [], "refresh", function_data
|
|
2058
|
+
|
|
2059
|
+
# Refresh data synchronously
|
|
2060
|
+
# if self.check_key("refresh", key, self.keys_dict) or self.remapped_key(key, curses.KEY_F5, self.key_remappings) or (self.auto_refresh and (time.time() - initial_time) > self.timer):
|
|
2061
|
+
# self.stdscr.addstr(0,w-3," ", curses.color_pair(self.colours_start+21) | curses.A_BOLD)
|
|
2062
|
+
# self.stdscr.refresh()
|
|
2063
|
+
# if self.get_new_data and self.refresh_function:
|
|
2064
|
+
# self.initialise_variables(get_data=True)
|
|
2065
|
+
#
|
|
2066
|
+
# initial_time = time.time()
|
|
2067
|
+
# self.draw_screen(self.indexed_items, self.highlights, clear=False)
|
|
2068
|
+
# else:
|
|
2069
|
+
#
|
|
2070
|
+
# function_data = self.get_function_data()
|
|
2071
|
+
# return [], "refresh", function_data
|
|
2072
|
+
|
|
2073
|
+
if self.footer_string_auto_refresh and ((time.time() - initial_time_footer) > self.footer_timer):
|
|
2074
|
+
self.logger.debug(f"footer_string_auto_refresh")
|
|
2075
|
+
self.footer_string = self.footer_string_refresh_function()
|
|
2076
|
+
initial_time_footer = time.time()
|
|
2077
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
2078
|
+
|
|
2079
|
+
if self.check_key("help", key, self.keys_dict):
|
|
2080
|
+
self.logger.info(f"key_function help")
|
|
2081
|
+
self.stdscr.clear()
|
|
2082
|
+
self.stdscr.refresh()
|
|
2083
|
+
help_data = {
|
|
2084
|
+
# "items": help_lines,
|
|
2085
|
+
"items": build_help_rows(self.keys_dict),
|
|
2086
|
+
"title": f"{self.title} Help",
|
|
2087
|
+
"colours_start": self.help_colours_start,
|
|
2088
|
+
"colours": help_colours,
|
|
2089
|
+
"show_footer": True,
|
|
2090
|
+
"max_selected": 1,
|
|
2091
|
+
"keys_dict": help_keys,
|
|
2092
|
+
"disabled_keys": [ord('?'), ord('v'), ord('V'), ord('m'), ord('M'), ord('l'), curses.KEY_ENTER, ord('\n')],
|
|
2093
|
+
"highlight_full_row": True,
|
|
2094
|
+
"top_gap": 0,
|
|
2095
|
+
"paginate": self.paginate,
|
|
2096
|
+
"centre_in_terminal": True,
|
|
2097
|
+
"centre_in_terminal_vertical": True,
|
|
2098
|
+
"hidden_columns": [],
|
|
2099
|
+
"reset_colours": False,
|
|
2100
|
+
|
|
2101
|
+
}
|
|
2102
|
+
OptionPicker = Picker(self.stdscr, **help_data)
|
|
2103
|
+
s, o, f = OptionPicker.run()
|
|
2104
|
+
|
|
2105
|
+
elif self.check_key("exit", key, self.keys_dict):
|
|
2106
|
+
self.stdscr.clear()
|
|
2107
|
+
function_data = self.get_function_data()
|
|
2108
|
+
function_data["last_key"] = key
|
|
2109
|
+
return [], "", function_data
|
|
2110
|
+
elif self.check_key("full_exit", key, self.keys_dict):
|
|
2111
|
+
close_curses(self.stdscr)
|
|
2112
|
+
exit()
|
|
2113
|
+
|
|
2114
|
+
elif self.check_key("settings_input", key, self.keys_dict):
|
|
2115
|
+
self.logger.info(f"Settings input")
|
|
2116
|
+
usrtxt = f"{self.user_settings.strip()} " if self.user_settings else ""
|
|
2117
|
+
field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
|
|
2118
|
+
if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
|
|
2119
|
+
else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
|
|
2120
|
+
self.set_registers()
|
|
2121
|
+
usrtxt, return_val = input_field(
|
|
2122
|
+
self.stdscr,
|
|
2123
|
+
usrtxt=usrtxt,
|
|
2124
|
+
field_prefix=" Settings: ",
|
|
2125
|
+
x=lambda:2,
|
|
2126
|
+
y=lambda: self.stdscr.getmaxyx()[0]-1,
|
|
2127
|
+
max_length=field_end_f,
|
|
2128
|
+
registers=self.registers,
|
|
2129
|
+
refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
|
|
2130
|
+
history=self.history_settings,
|
|
2131
|
+
path_auto_complete=True,
|
|
2132
|
+
formula_auto_complete=False,
|
|
2133
|
+
function_auto_complete=False,
|
|
2134
|
+
word_auto_complete=True,
|
|
2135
|
+
auto_complete_words=["ft", "ct", "cv"]
|
|
2136
|
+
)
|
|
2137
|
+
if return_val:
|
|
2138
|
+
self.user_settings = usrtxt
|
|
2139
|
+
self.apply_settings()
|
|
2140
|
+
self.history_settings.append(usrtxt)
|
|
2141
|
+
self.user_settings = ""
|
|
2142
|
+
elif self.check_key("toggle_footer", key, self.keys_dict):
|
|
2143
|
+
self.logger.info(f"toggle footer")
|
|
2144
|
+
self.user_settings = "footer"
|
|
2145
|
+
self.apply_settings()
|
|
2146
|
+
|
|
2147
|
+
elif self.check_key("settings_options", key, self.keys_dict):
|
|
2148
|
+
options = []
|
|
2149
|
+
if len(self.items) > 0:
|
|
2150
|
+
options += [["cv", "Centre rows vertically"]]
|
|
2151
|
+
options += [["pc", "Pin cursor to row number when data refreshes"]]
|
|
2152
|
+
options += [["ct", "Centre column-set in terminal"]]
|
|
2153
|
+
options += [["cc", "Centre values in cells"]]
|
|
2154
|
+
options += [["!r", "Toggle auto-refresh"]]
|
|
2155
|
+
options += [["th", "Cycle between themes. (accepts th#)"]]
|
|
2156
|
+
options += [["nohl", "Toggle highlights"]]
|
|
2157
|
+
options += [["footer", "Toggle footer"]]
|
|
2158
|
+
options += [["header", "Toggle header"]]
|
|
2159
|
+
options += [["rh", "Toggle row header"]]
|
|
2160
|
+
options += [["modes", "Toggle modes"]]
|
|
2161
|
+
options += [["ft", "Cycle through footer styles (accepts ft#)"]]
|
|
2162
|
+
options += [["unicode", "Toggle b/w using len and wcwidth to calculate char width."]]
|
|
2163
|
+
options += [[f"s{i}", f"Select col. {i}"] for i in range(len(self.items[0]))]
|
|
2164
|
+
options += [[f"!{i}", f"Toggle col. {i}"] for i in range(len(self.items[0]))]
|
|
2165
|
+
options += [["ara", "Add empty row after cursor."]]
|
|
2166
|
+
options += [["arb", "Add empty row before the cursor."]]
|
|
2167
|
+
options += [["aca", "Add empty column after the selected column."]]
|
|
2168
|
+
options += [["acb", "Add empty column before the selected column."]]
|
|
2169
|
+
|
|
2170
|
+
|
|
2171
|
+
settings_options_header = ["Key", "Setting"]
|
|
2172
|
+
|
|
2173
|
+
s, o, f = self.choose_option(self.stdscr, options=options, title="Settings", header=settings_options_header)
|
|
2174
|
+
if s:
|
|
2175
|
+
self.user_settings = " ".join([x[0] for x in s.values()])
|
|
2176
|
+
self.apply_settings()
|
|
2177
|
+
|
|
2178
|
+
elif self.check_key("redo", key, self.keys_dict):
|
|
2179
|
+
self.redo()
|
|
2180
|
+
# elif self.check_key("move_column_left", key, self.keys_dict):
|
|
2181
|
+
# tmp1 = self.column_indices[self.selected_column]
|
|
2182
|
+
# tmp2 = self.column_indices[(self.selected_column-1)%len(self.column_indices)]
|
|
2183
|
+
# self.column_indices[self.selected_column] = tmp2
|
|
2184
|
+
# self.column_indices[(self.selected_column-1)%(len(self.column_indices))] = tmp1
|
|
2185
|
+
# self.selected_column = (self.selected_column-1)%len(self.column_indices)
|
|
2186
|
+
# # self.notification(self.stdscr, f"{str(self.column_indices)}, {tmp1}, {tmp2}")
|
|
2187
|
+
# self.initialise_variables()
|
|
2188
|
+
# self.column_widths = get_column_widths([v[1] for v in self.indexed_items], header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w)
|
|
2189
|
+
# self.draw_screen(self.indexed_items, self.highlights)
|
|
2190
|
+
# # self.move_column(direction=-1)
|
|
2191
|
+
#
|
|
2192
|
+
# elif self.check_key("move_column_right", key, self.keys_dict):
|
|
2193
|
+
# tmp1 = self.column_indices[self.selected_column]
|
|
2194
|
+
# tmp2 = self.column_indices[(self.selected_column+1)%len(self.column_indices)]
|
|
2195
|
+
# self.column_indices[self.selected_column] = tmp2
|
|
2196
|
+
# self.column_indices[(self.selected_column+1)%(len(self.column_indices))] = tmp1
|
|
2197
|
+
# self.selected_column = (self.selected_column+1)%len(self.column_indices)
|
|
2198
|
+
# self.initialise_variables()
|
|
2199
|
+
# self.draw_screen(self.indexed_items, self.highlights)
|
|
2200
|
+
# # self.move_column(direction=1)
|
|
2201
|
+
|
|
2202
|
+
elif self.check_key("cursor_down", key, self.keys_dict):
|
|
2203
|
+
page_turned = self.cursor_down()
|
|
2204
|
+
if not page_turned: clear_screen = False
|
|
2205
|
+
elif self.check_key("half_page_down", key, self.keys_dict):
|
|
2206
|
+
self.cursor_down(count=self.items_per_page//2)
|
|
2207
|
+
clear_screen = True
|
|
2208
|
+
elif self.check_key("five_down", key, self.keys_dict):
|
|
2209
|
+
clear_screen = False
|
|
2210
|
+
self.cursor_down(count=5)
|
|
2211
|
+
clear_screen = True
|
|
2212
|
+
elif self.check_key("cursor_up", key, self.keys_dict):
|
|
2213
|
+
page_turned = self.cursor_up()
|
|
2214
|
+
if not page_turned: clear_screen = False
|
|
2215
|
+
elif self.check_key("five_up", key, self.keys_dict):
|
|
2216
|
+
# if self.cursor_up(count=5): clear_screen = True
|
|
2217
|
+
self.cursor_up(count=5)
|
|
2218
|
+
clear_screen = True
|
|
2219
|
+
elif self.check_key("half_page_up", key, self.keys_dict):
|
|
2220
|
+
self.cursor_up(count=self.items_per_page//2)
|
|
2221
|
+
clear_screen = True
|
|
2222
|
+
|
|
2223
|
+
elif self.check_key("toggle_select", key, self.keys_dict):
|
|
2224
|
+
if len(self.indexed_items) > 0:
|
|
2225
|
+
item_index = self.indexed_items[self.cursor_pos][0]
|
|
2226
|
+
cell_index = (self.indexed_items[self.cursor_pos][0], self.selected_column)
|
|
2227
|
+
row, col = cell_index
|
|
2228
|
+
selected_count = sum(self.selections.values())
|
|
2229
|
+
if self.max_selected == -1 or selected_count >= self.max_selected:
|
|
2230
|
+
self.toggle_item(item_index)
|
|
2231
|
+
|
|
2232
|
+
self.cell_selections[cell_index] = not self.cell_selections[cell_index]
|
|
2233
|
+
## Set self.selected_cells_by_row
|
|
2234
|
+
# If any cells in the current row are selected
|
|
2235
|
+
if row in self.selected_cells_by_row:
|
|
2236
|
+
# If the current cell is selected then remove it
|
|
2237
|
+
if col in self.selected_cells_by_row[row]:
|
|
2238
|
+
# If the current cell is the only cell in the row that is selected then remove the row from the dict
|
|
2239
|
+
if len(self.selected_cells_by_row[row]) == 1:
|
|
2240
|
+
|
|
2241
|
+
del self.selected_cells_by_row[row]
|
|
2242
|
+
# else remove only the index of the current cell
|
|
2243
|
+
else:
|
|
2244
|
+
self.selected_cells_by_row[row].remove(col)
|
|
2245
|
+
# If there are cells in the row that are selected then append the current cell to the row
|
|
2246
|
+
else:
|
|
2247
|
+
self.selected_cells_by_row[row].append(col)
|
|
2248
|
+
# Add the a list containing only the current column
|
|
2249
|
+
else:
|
|
2250
|
+
self.selected_cells_by_row[row] = [col]
|
|
2251
|
+
|
|
2252
|
+
self.cursor_down()
|
|
2253
|
+
elif self.check_key("select_all", key, self.keys_dict): # Select all (m or ctrl-a)
|
|
2254
|
+
self.select_all()
|
|
2255
|
+
|
|
2256
|
+
elif self.check_key("select_none", key, self.keys_dict): # Deselect all (M or ctrl-r)
|
|
2257
|
+
self.deselect_all()
|
|
2258
|
+
|
|
2259
|
+
elif self.check_key("cursor_top", key, self.keys_dict):
|
|
2260
|
+
new_pos = 0
|
|
2261
|
+
while True:
|
|
2262
|
+
if new_pos in self.unselectable_indices: new_pos+=1
|
|
2263
|
+
else: break
|
|
2264
|
+
if new_pos < len(self.indexed_items):
|
|
2265
|
+
self.cursor_pos = new_pos
|
|
2266
|
+
|
|
2267
|
+
elif self.check_key("cursor_bottom", key, self.keys_dict):
|
|
2268
|
+
new_pos = len(self.indexed_items)-1
|
|
2269
|
+
while True:
|
|
2270
|
+
if new_pos in self.unselectable_indices: new_pos-=1
|
|
2271
|
+
else: break
|
|
2272
|
+
if new_pos < len(self.items) and new_pos >= 0:
|
|
2273
|
+
self.cursor_pos = new_pos
|
|
2274
|
+
|
|
2275
|
+
elif self.check_key("enter", key, self.keys_dict):
|
|
2276
|
+
self.logger.info(f"key_function enter")
|
|
2277
|
+
# Print the selected indices if any, otherwise print the current index
|
|
2278
|
+
if self.is_selecting or self.is_deselecting: self.handle_visual_selection()
|
|
2279
|
+
if len(self.items) == 0:
|
|
2280
|
+
function_data = self.get_function_data()
|
|
2281
|
+
function_data["last_key"] = key
|
|
2282
|
+
return [], "", function_data
|
|
2283
|
+
selected_indices = get_selected_indices(self.selections)
|
|
2284
|
+
if not selected_indices and len(self.indexed_items):
|
|
2285
|
+
selected_indices = [self.indexed_items[self.cursor_pos][0]]
|
|
2286
|
+
|
|
2287
|
+
options_sufficient = True
|
|
2288
|
+
usrtxt = self.user_opts
|
|
2289
|
+
for index in selected_indices:
|
|
2290
|
+
if self.require_option[index]:
|
|
2291
|
+
if self.option_functions[index] != None:
|
|
2292
|
+
options_sufficient, usrtxt = self.option_functions[index](
|
|
2293
|
+
stdscr=self.stdscr,
|
|
2294
|
+
refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights)
|
|
2295
|
+
)
|
|
2296
|
+
else:
|
|
2297
|
+
self.set_registers()
|
|
2298
|
+
options_sufficient, usrtxt = default_option_input(
|
|
2299
|
+
self.stdscr,
|
|
2300
|
+
starting_value=self.user_opts,
|
|
2301
|
+
registers = self.registers
|
|
2302
|
+
)
|
|
2303
|
+
|
|
2304
|
+
if options_sufficient:
|
|
2305
|
+
self.user_opts = usrtxt
|
|
2306
|
+
self.stdscr.clear()
|
|
2307
|
+
self.stdscr.refresh()
|
|
2308
|
+
function_data = self.get_function_data()
|
|
2309
|
+
function_data["last_key"] = key
|
|
2310
|
+
return selected_indices, usrtxt, function_data
|
|
2311
|
+
elif self.check_key("page_down", key, self.keys_dict): # Next page
|
|
2312
|
+
self.cursor_pos = min(len(self.indexed_items) - 1, self.cursor_pos+self.items_per_page)
|
|
2313
|
+
|
|
2314
|
+
elif self.check_key("page_up", key, self.keys_dict):
|
|
2315
|
+
self.cursor_pos = max(0, self.cursor_pos-self.items_per_page)
|
|
2316
|
+
|
|
2317
|
+
elif self.check_key("redraw_screen", key, self.keys_dict):
|
|
2318
|
+
self.logger.info(f"key_function redraw_screen")
|
|
2319
|
+
self.stdscr.clear()
|
|
2320
|
+
self.stdscr.refresh()
|
|
2321
|
+
restrict_curses(self.stdscr)
|
|
2322
|
+
unrestrict_curses(self.stdscr)
|
|
2323
|
+
self.stdscr.clear()
|
|
2324
|
+
self.stdscr.refresh()
|
|
2325
|
+
|
|
2326
|
+
self.draw_screen(self.indexed_items, self.highlights)
|
|
2327
|
+
|
|
2328
|
+
elif self.check_key("cycle_sort_method", key, self.keys_dict):
|
|
2329
|
+
if self.sort_column == self.selected_column:
|
|
2330
|
+
self.columns_sort_method[self.sort_column] = (self.columns_sort_method[self.sort_column]+1) % len(self.SORT_METHODS)
|
|
2331
|
+
else:
|
|
2332
|
+
self.sort_column = self.selected_column
|
|
2333
|
+
if len(self.indexed_items) > 0:
|
|
2334
|
+
current_index = self.indexed_items[self.cursor_pos][0]
|
|
2335
|
+
sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
|
|
2336
|
+
self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
|
|
2337
|
+
|
|
2338
|
+
self.logger.info(f"key_function cycle_sort_method. (sort_column, sort_method) = ({self.sort_column}, {self.columns_sort_method[self.sort_column]})")
|
|
2339
|
+
elif self.check_key("cycle_sort_method_reverse", key, self.keys_dict): # Cycle sort method
|
|
2340
|
+
old_sort_column = self.sort_column
|
|
2341
|
+
self.sort_column = self.selected_column
|
|
2342
|
+
self.columns_sort_method[self.sort_column] = (self.columns_sort_method[self.sort_column]-1) % len(self.SORT_METHODS)
|
|
2343
|
+
if len(self.indexed_items) > 0:
|
|
2344
|
+
current_index = self.indexed_items[self.cursor_pos][0]
|
|
2345
|
+
sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
|
|
2346
|
+
self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
|
|
2347
|
+
self.logger.info(f"key_function cycle_sort_method. (sort_column, sort_method) = ({self.sort_column}, {self.columns_sort_method[self.sort_column]})")
|
|
2348
|
+
|
|
2349
|
+
elif self.check_key("cycle_sort_order", key, self.keys_dict): # Toggle sort order
|
|
2350
|
+
self.sort_reverse[self.sort_column] = not self.sort_reverse[self.sort_column]
|
|
2351
|
+
if len(self.indexed_items) > 0:
|
|
2352
|
+
current_index = self.indexed_items[self.cursor_pos][0]
|
|
2353
|
+
sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
|
|
2354
|
+
self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
|
|
2355
|
+
self.logger.info(f"key_function cycle_sort_order. (sort_column, sort_method, sort_reverse) = ({self.sort_column}, {self.columns_sort_method[self.sort_column]}, {self.sort_reverse[self.sort_column]})")
|
|
2356
|
+
elif self.check_key("col_select", key, self.keys_dict):
|
|
2357
|
+
col_index = key - ord('0')
|
|
2358
|
+
self.logger.info(f"key_function col_select {col_index}")
|
|
2359
|
+
if 0 <= col_index < len(self.items[0]):
|
|
2360
|
+
self.sort_column = col_index
|
|
2361
|
+
if len(self.indexed_items) > 0:
|
|
2362
|
+
current_index = self.indexed_items[self.cursor_pos][0]
|
|
2363
|
+
sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
|
|
2364
|
+
self.cursor_pos = [row[0] for row in self.indexed_items].index(current_index)
|
|
2365
|
+
elif self.check_key("col_select_next", key, self.keys_dict):
|
|
2366
|
+
if len(self.items) > 0 and len(self.items[0]) > 0:
|
|
2367
|
+
col_index = (self.selected_column +1) % (len(self.items[0]))
|
|
2368
|
+
self.selected_column = col_index
|
|
2369
|
+
# Flash when we loop back to the first column
|
|
2370
|
+
# if self.selected_column == 0:
|
|
2371
|
+
# curses.flash()
|
|
2372
|
+
self.logger.info(f"key_function col_select_next {self.selected_column}")
|
|
2373
|
+
|
|
2374
|
+
|
|
2375
|
+
## Scroll with column select
|
|
2376
|
+
rows = self.get_visible_rows()
|
|
2377
|
+
self.column_widths = get_column_widths(rows, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w, unicode_char_width=self.unicode_char_width)
|
|
2378
|
+
visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
|
|
2379
|
+
column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
|
|
2380
|
+
start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
|
|
2381
|
+
end_of_cell = sum(visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
|
|
2382
|
+
display_width = w-self.startx
|
|
2383
|
+
# If the full column is within the current display then don't do anything
|
|
2384
|
+
if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
|
|
2385
|
+
pass
|
|
2386
|
+
# Otherwise right-justify the cell
|
|
2387
|
+
else:
|
|
2388
|
+
self.leftmost_char = end_of_cell - display_width
|
|
2389
|
+
|
|
2390
|
+
self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
|
|
2391
|
+
|
|
2392
|
+
elif self.check_key("col_select_prev", key, self.keys_dict):
|
|
2393
|
+
if len(self.items) > 0 and len(self.items[0]) > 0:
|
|
2394
|
+
col_index = (self.selected_column -1) % (len(self.items[0]))
|
|
2395
|
+
self.selected_column = col_index
|
|
2396
|
+
|
|
2397
|
+
self.logger.info(f"key_function col_select_prev {self.selected_column}")
|
|
2398
|
+
# Flash when we loop back to the last column
|
|
2399
|
+
# if self.selected_column == len(self.column_widths)-1:
|
|
2400
|
+
# curses.flash()
|
|
2401
|
+
|
|
2402
|
+
## Scroll with column select
|
|
2403
|
+
rows = self.get_visible_rows()
|
|
2404
|
+
self.column_widths = get_column_widths(rows, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w, unicode_char_width=self.unicode_char_width)
|
|
2405
|
+
visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
|
|
2406
|
+
column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
|
|
2407
|
+
start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
|
|
2408
|
+
end_of_cell = sum(visible_column_widths[:self.selected_column+1])+len(self.separator)*(self.selected_column+1)
|
|
2409
|
+
display_width = w-self.startx
|
|
2410
|
+
|
|
2411
|
+
# If the entire column is within the current display then don't do anything
|
|
2412
|
+
if start_of_cell >= self.leftmost_char and end_of_cell <= self.leftmost_char + display_width:
|
|
2413
|
+
pass
|
|
2414
|
+
# Otherwise left-justify the cell
|
|
2415
|
+
else:
|
|
2416
|
+
self.leftmost_char = start_of_cell
|
|
2417
|
+
|
|
2418
|
+
self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
|
|
2419
|
+
|
|
2420
|
+
elif self.check_key("scroll_right", key, self.keys_dict):
|
|
2421
|
+
self.logger.info(f"key_function scroll_right")
|
|
2422
|
+
if len(self.indexed_items):
|
|
2423
|
+
row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
|
|
2424
|
+
if row_width-self.leftmost_char >= w-self.startx:
|
|
2425
|
+
self.leftmost_char = self.leftmost_char+5
|
|
2426
|
+
|
|
2427
|
+
elif self.check_key("scroll_left", key, self.keys_dict):
|
|
2428
|
+
self.logger.info(f"key_function scroll_left")
|
|
2429
|
+
self.leftmost_char = max(self.leftmost_char-5, 0)
|
|
2430
|
+
|
|
2431
|
+
elif self.check_key("scroll_far_left", key, self.keys_dict):
|
|
2432
|
+
self.logger.info(f"key_function scroll_far_left")
|
|
2433
|
+
self.leftmost_char = 0
|
|
2434
|
+
self.selected_column = 0
|
|
2435
|
+
|
|
2436
|
+
elif self.check_key("scroll_far_right", key, self.keys_dict):
|
|
2437
|
+
self.logger.info(f"key_function scroll_far_right")
|
|
2438
|
+
longest_row_str_len = 0
|
|
2439
|
+
rows = self.get_visible_rows()
|
|
2440
|
+
for i in range(len(rows)):
|
|
2441
|
+
item = rows[i]
|
|
2442
|
+
row_str = format_row(item, self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
|
|
2443
|
+
if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
|
|
2444
|
+
# for i in range(len(self.indexed_items)):
|
|
2445
|
+
# item = self.indexed_items[i]
|
|
2446
|
+
# row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)
|
|
2447
|
+
# if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
|
|
2448
|
+
# self.notification(self.stdscr, f"{longest_row_str_len}")
|
|
2449
|
+
self.leftmost_char = max(0, longest_row_str_len-w+2+self.startx)
|
|
2450
|
+
if len(self.items):
|
|
2451
|
+
self.selected_column = len(self.items[0])-1
|
|
2452
|
+
|
|
2453
|
+
elif self.check_key("add_column_before", key, self.keys_dict):
|
|
2454
|
+
self.logger.info(f"key_function add_column_before")
|
|
2455
|
+
# self.add_column_before()
|
|
2456
|
+
self.insert_column(self.selected_column)
|
|
2457
|
+
|
|
2458
|
+
elif self.check_key("add_column_after", key, self.keys_dict):
|
|
2459
|
+
self.logger.info(f"key_function add_column_after")
|
|
2460
|
+
# self.add_column_after()
|
|
2461
|
+
self.insert_column(self.selected_column+1)
|
|
2462
|
+
|
|
2463
|
+
elif self.check_key("add_row_before", key, self.keys_dict):
|
|
2464
|
+
self.logger.info(f"key_function add_row_before")
|
|
2465
|
+
# self.add_row_before()
|
|
2466
|
+
self.insert_row(self.cursor_pos)
|
|
2467
|
+
|
|
2468
|
+
elif self.check_key("add_row_after", key, self.keys_dict):
|
|
2469
|
+
self.logger.info(f"key_function add_row_after")
|
|
2470
|
+
# self.add_row_after()
|
|
2471
|
+
self.insert_row(self.cursor_pos+1)
|
|
2472
|
+
|
|
2473
|
+
elif self.check_key("col_hide", key, self.keys_dict):
|
|
2474
|
+
self.logger.info(f"key_function col_hide")
|
|
2475
|
+
d = {'!': 0, '@': 1, '#': 2, '$': 3, '%': 4, '^': 5, '&': 6, '*': 7, '(': 8, ')': 9}
|
|
2476
|
+
d = {s:i for i,s in enumerate(")!@#$%^&*(")}
|
|
2477
|
+
col_index = d[chr(key)]
|
|
2478
|
+
self.toggle_column_visibility(col_index)
|
|
2479
|
+
elif self.check_key("copy", key, self.keys_dict):
|
|
2480
|
+
self.copy_dialogue()
|
|
2481
|
+
elif self.check_key("paste", key, self.keys_dict):
|
|
2482
|
+
self.paste_dialogue()
|
|
2483
|
+
elif self.check_key("save", key, self.keys_dict):
|
|
2484
|
+
self.save_dialog()
|
|
2485
|
+
elif self.check_key("load", key, self.keys_dict):
|
|
2486
|
+
self.load_dialog()
|
|
2487
|
+
|
|
2488
|
+
elif self.check_key("delete", key, self.keys_dict): # Delete key
|
|
2489
|
+
self.delete_entries()
|
|
2490
|
+
|
|
2491
|
+
elif self.check_key("delete_column", key, self.keys_dict): # Delete key
|
|
2492
|
+
self.logger.info(f"key_function delete_column")
|
|
2493
|
+
row_len = 1
|
|
2494
|
+
if self.header: row_len = len(self.header)
|
|
2495
|
+
elif len(self.items): row_len = len(self.items[0])
|
|
2496
|
+
if row_len > 1:
|
|
2497
|
+
self.items = [row[:self.selected_column] + row[self.selected_column+1:] for row in self.items]
|
|
2498
|
+
self.header = self.header[:self.selected_column] + self.header[self.selected_column+1:]
|
|
2499
|
+
self.editable_columns = self.editable_columns[:self.selected_column] + self.editable_columns[self.selected_column+1:]
|
|
2500
|
+
self.selected_column = min(self.selected_column, row_len-2)
|
|
2501
|
+
elif row_len == 1:
|
|
2502
|
+
self.items = [[""] for _ in range(len(self.items))]
|
|
2503
|
+
self.header = [""] if self.header else []
|
|
2504
|
+
self.editable_columns = []
|
|
2505
|
+
self.selected_column = min(self.selected_column, row_len-2)
|
|
2506
|
+
self.initialise_variables()
|
|
2507
|
+
|
|
2508
|
+
|
|
2509
|
+
|
|
2510
|
+
|
|
2511
|
+
# elif self.check_key("increase_lines_per_page", key, self.keys_dict):
|
|
2512
|
+
# self.items_per_page += 1
|
|
2513
|
+
# elif self.check_key("decrease_lines_per_page", key, self.keys_dict):
|
|
2514
|
+
# if self.items_per_page > 1:
|
|
2515
|
+
# self.items_per_page -= 1
|
|
2516
|
+
|
|
2517
|
+
elif self.check_key("decrease_column_width", key, self.keys_dict):
|
|
2518
|
+
self.logger.info(f"key_function decrease_column_width")
|
|
2519
|
+
if self.max_column_width > 10:
|
|
2520
|
+
self.max_column_width -= 10
|
|
2521
|
+
# self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=2)
|
|
2522
|
+
elif self.check_key("increase_column_width", key, self.keys_dict):
|
|
2523
|
+
self.logger.info(f"key_function increase_column_width")
|
|
2524
|
+
if self.max_column_width < 1000:
|
|
2525
|
+
self.max_column_width += 10
|
|
2526
|
+
# self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w)
|
|
2527
|
+
elif self.check_key("visual_selection_toggle", key, self.keys_dict):
|
|
2528
|
+
self.logger.info(f"key_function visual_selection_toggle")
|
|
2529
|
+
self.handle_visual_selection()
|
|
2530
|
+
|
|
2531
|
+
elif self.check_key("visual_deselection_toggle", key, self.keys_dict):
|
|
2532
|
+
self.logger.info(f"key_function visual_deselection_toggle")
|
|
2533
|
+
self.handle_visual_selection(selecting=False)
|
|
2534
|
+
|
|
2535
|
+
elif key == curses.KEY_RESIZE: # Terminal resize signal
|
|
2536
|
+
|
|
2537
|
+
self.calculate_section_sizes()
|
|
2538
|
+
self.column_widths = get_column_widths(self.items, header=self.header, max_column_width=self.max_column_width, number_columns=self.number_columns, max_total_width=w, unicode_char_width=self.unicode_char_width)
|
|
2539
|
+
|
|
2540
|
+
|
|
2541
|
+
|
|
2542
|
+
elif self.check_key("filter_input", key, self.keys_dict):
|
|
2543
|
+
self.logger.info(f"key_function filter_input")
|
|
2544
|
+
usrtxt = f"{self.filter_query} " if self.filter_query else ""
|
|
2545
|
+
field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
|
|
2546
|
+
if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
|
|
2547
|
+
else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
|
|
2548
|
+
self.set_registers()
|
|
2549
|
+
words = self.get_word_list()
|
|
2550
|
+
usrtxt, return_val = input_field(
|
|
2551
|
+
self.stdscr,
|
|
2552
|
+
usrtxt=usrtxt,
|
|
2553
|
+
field_prefix=" Filter: ",
|
|
2554
|
+
x=lambda:2,
|
|
2555
|
+
y=lambda: self.stdscr.getmaxyx()[0]-2,
|
|
2556
|
+
# max_length=field_end,
|
|
2557
|
+
max_length=field_end_f,
|
|
2558
|
+
registers=self.registers,
|
|
2559
|
+
refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
|
|
2560
|
+
history=self.history_filter_and_search,
|
|
2561
|
+
path_auto_complete=True,
|
|
2562
|
+
formula_auto_complete=False,
|
|
2563
|
+
function_auto_complete=False,
|
|
2564
|
+
word_auto_complete=True,
|
|
2565
|
+
auto_complete_words=words,
|
|
2566
|
+
)
|
|
2567
|
+
if return_val:
|
|
2568
|
+
self.filter_query = usrtxt
|
|
2569
|
+
self.history_filter_and_search.append(usrtxt)
|
|
2570
|
+
|
|
2571
|
+
# If the current mode filter has been changed then go back to the first mode
|
|
2572
|
+
if self.modes and "filter" in self.modes[self.mode_index] and self.modes[self.mode_index]["filter"] not in self.filter_query:
|
|
2573
|
+
self.mode_index = 0
|
|
2574
|
+
# elif "filter" in modes[mode_index] and modes[mode_index]["filter"] in filter_query:
|
|
2575
|
+
# filter_query.split(modes[mode_index]["filter"])
|
|
2576
|
+
|
|
2577
|
+
prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
|
|
2578
|
+
self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
|
|
2579
|
+
if prev_index in [x[0] for x in self.indexed_items]: new_index = [x[0] for x in self.indexed_items].index(prev_index)
|
|
2580
|
+
else: new_index = 0
|
|
2581
|
+
self.cursor_pos = new_index
|
|
2582
|
+
# Re-sort self.items after applying filter
|
|
2583
|
+
if self.columns_sort_method[self.selected_column] != 0:
|
|
2584
|
+
sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
|
|
2585
|
+
|
|
2586
|
+
elif self.check_key("search_input", key, self.keys_dict):
|
|
2587
|
+
self.logger.info(f"key_function search_input")
|
|
2588
|
+
usrtxt = f"{self.search_query} " if self.search_query else ""
|
|
2589
|
+
field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
|
|
2590
|
+
if self.show_footer and self.footer.height >= 3: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
|
|
2591
|
+
else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
|
|
2592
|
+
self.set_registers()
|
|
2593
|
+
words = self.get_word_list()
|
|
2594
|
+
usrtxt, return_val = input_field(
|
|
2595
|
+
self.stdscr,
|
|
2596
|
+
usrtxt=usrtxt,
|
|
2597
|
+
field_prefix=" Search: ",
|
|
2598
|
+
x=lambda:2,
|
|
2599
|
+
y=lambda: self.stdscr.getmaxyx()[0]-3,
|
|
2600
|
+
max_length=field_end_f,
|
|
2601
|
+
registers=self.registers,
|
|
2602
|
+
refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
|
|
2603
|
+
history=self.history_filter_and_search,
|
|
2604
|
+
path_auto_complete=True,
|
|
2605
|
+
formula_auto_complete=False,
|
|
2606
|
+
function_auto_complete=False,
|
|
2607
|
+
word_auto_complete=True,
|
|
2608
|
+
auto_complete_words=words,
|
|
2609
|
+
)
|
|
2610
|
+
if return_val:
|
|
2611
|
+
self.search_query = usrtxt
|
|
2612
|
+
self.history_filter_and_search.append(usrtxt)
|
|
2613
|
+
return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
|
|
2614
|
+
query=self.search_query,
|
|
2615
|
+
indexed_items=self.indexed_items,
|
|
2616
|
+
highlights=self.highlights,
|
|
2617
|
+
cursor_pos=self.cursor_pos,
|
|
2618
|
+
unselectable_indices=self.unselectable_indices,
|
|
2619
|
+
)
|
|
2620
|
+
if return_val:
|
|
2621
|
+
self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
|
|
2622
|
+
else:
|
|
2623
|
+
self.search_index, self.search_count = 0, 0
|
|
2624
|
+
|
|
2625
|
+
elif self.check_key("continue_search_forward", key, self.keys_dict):
|
|
2626
|
+
self.logger.info(f"key_function continue_search_forward")
|
|
2627
|
+
return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
|
|
2628
|
+
query=self.search_query,
|
|
2629
|
+
indexed_items=self.indexed_items,
|
|
2630
|
+
highlights=self.highlights,
|
|
2631
|
+
cursor_pos=self.cursor_pos,
|
|
2632
|
+
unselectable_indices=self.unselectable_indices,
|
|
2633
|
+
continue_search=True,
|
|
2634
|
+
)
|
|
2635
|
+
if return_val:
|
|
2636
|
+
self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
|
|
2637
|
+
elif self.check_key("continue_search_backward", key, self.keys_dict):
|
|
2638
|
+
self.logger.info(f"key_function continue_search_backward")
|
|
2639
|
+
return_val, tmp_cursor, tmp_index, tmp_count, tmp_highlights = search(
|
|
2640
|
+
query=self.search_query,
|
|
2641
|
+
indexed_items=self.indexed_items,
|
|
2642
|
+
highlights=self.highlights,
|
|
2643
|
+
cursor_pos=self.cursor_pos,
|
|
2644
|
+
unselectable_indices=self.unselectable_indices,
|
|
2645
|
+
continue_search=True,
|
|
2646
|
+
reverse=True,
|
|
2647
|
+
)
|
|
2648
|
+
if return_val:
|
|
2649
|
+
self.cursor_pos, self.search_index, self.search_count, self.highlights = tmp_cursor, tmp_index, tmp_count, tmp_highlights
|
|
2650
|
+
elif self.check_key("cancel", key, self.keys_dict): # ESC key
|
|
2651
|
+
# order of escapes:
|
|
2652
|
+
# 1. selecting/deslecting
|
|
2653
|
+
# 2. search
|
|
2654
|
+
# 3. filter
|
|
2655
|
+
# 4. if self.cancel_is_back (e.g., notification) then we exit
|
|
2656
|
+
# 4. selecting
|
|
2657
|
+
|
|
2658
|
+
# Cancel visual de/selection
|
|
2659
|
+
if self.is_selecting or self.is_deselecting:
|
|
2660
|
+
self.start_selection = -1
|
|
2661
|
+
self.end_selection = -1
|
|
2662
|
+
self.is_selecting = False
|
|
2663
|
+
self.is_deselecting = False
|
|
2664
|
+
# Cancel search
|
|
2665
|
+
elif self.search_query:
|
|
2666
|
+
self.search_query = ""
|
|
2667
|
+
self.highlights = [highlight for highlight in self.highlights if "type" not in highlight or highlight["type"] != "search" ]
|
|
2668
|
+
# Remove filter
|
|
2669
|
+
elif self.filter_query:
|
|
2670
|
+
if self.modes and "filter" in self.modes[self.mode_index] and self.modes[self.mode_index]["filter"] in self.filter_query and self.filter_query.strip() != self.modes[self.mode_index]["filter"]:
|
|
2671
|
+
self.filter_query = self.modes[self.mode_index]["filter"]
|
|
2672
|
+
# elif "filter" in modes[mode_index]:
|
|
2673
|
+
else:
|
|
2674
|
+
self.filter_query = ""
|
|
2675
|
+
self.mode_index = 0
|
|
2676
|
+
prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
|
|
2677
|
+
self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
|
|
2678
|
+
if prev_index in [x[0] for x in self.indexed_items]: new_index = [x[0] for x in self.indexed_items].index(prev_index)
|
|
2679
|
+
else: new_index = 0
|
|
2680
|
+
self.cursor_pos = new_index
|
|
2681
|
+
# Re-sort self.items after applying filter
|
|
2682
|
+
if self.columns_sort_method[self.selected_column] != 0:
|
|
2683
|
+
sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
|
|
2684
|
+
elif self.cancel_is_back:
|
|
2685
|
+
function_data = self.get_function_data()
|
|
2686
|
+
function_data["last_key"] = key
|
|
2687
|
+
return [], "escape", function_data
|
|
2688
|
+
|
|
2689
|
+
|
|
2690
|
+
# else:
|
|
2691
|
+
# self.search_query = ""
|
|
2692
|
+
# self.mode_index = 0
|
|
2693
|
+
# self.highlights = [highlight for highlight in self.highlights if "type" not in highlight or highlight["type"] != "search" ]
|
|
2694
|
+
# continue
|
|
2695
|
+
|
|
2696
|
+
elif self.check_key("opts_input", key, self.keys_dict):
|
|
2697
|
+
self.logger.info(f"key_function opts_input")
|
|
2698
|
+
usrtxt = f"{self.user_opts} " if self.user_opts else ""
|
|
2699
|
+
field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
|
|
2700
|
+
if self.show_footer and self.footer.height >= 1: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
|
|
2701
|
+
else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
|
|
2702
|
+
self.set_registers()
|
|
2703
|
+
words = self.get_word_list()
|
|
2704
|
+
usrtxt, return_val = input_field(
|
|
2705
|
+
self.stdscr,
|
|
2706
|
+
usrtxt=usrtxt,
|
|
2707
|
+
field_prefix=" Opts: ",
|
|
2708
|
+
x=lambda:2,
|
|
2709
|
+
y=lambda: self.stdscr.getmaxyx()[0]-1,
|
|
2710
|
+
max_length=field_end_f,
|
|
2711
|
+
registers=self.registers,
|
|
2712
|
+
refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
|
|
2713
|
+
history=self.history_opts,
|
|
2714
|
+
path_auto_complete=True,
|
|
2715
|
+
formula_auto_complete=False,
|
|
2716
|
+
function_auto_complete=True,
|
|
2717
|
+
word_auto_complete=True,
|
|
2718
|
+
auto_complete_words=words,
|
|
2719
|
+
)
|
|
2720
|
+
if return_val:
|
|
2721
|
+
self.user_opts = usrtxt
|
|
2722
|
+
self.history_opts.append(usrtxt)
|
|
2723
|
+
elif self.check_key("opts_select", key, self.keys_dict):
|
|
2724
|
+
self.logger.info(f"key_function opts_select")
|
|
2725
|
+
s, o, f = self.choose_option(self.stdscr, self.options_list)
|
|
2726
|
+
if self.user_opts.strip(): self.user_opts += " "
|
|
2727
|
+
self.user_opts += " ".join([x for x in s.values()])
|
|
2728
|
+
elif self.check_key("notification_toggle", key, self.keys_dict):
|
|
2729
|
+
self.logger.info(f"key_function notification_toggle")
|
|
2730
|
+
self.notification(self.stdscr, colours_end=self.colours_end)
|
|
2731
|
+
|
|
2732
|
+
elif self.check_key("mode_next", key, self.keys_dict): # tab key
|
|
2733
|
+
self.logger.info(f"key_function mode_next")
|
|
2734
|
+
# apply setting
|
|
2735
|
+
prev_mode_index = self.mode_index
|
|
2736
|
+
self.mode_index = (self.mode_index+1)%len(self.modes)
|
|
2737
|
+
mode = self.modes[self.mode_index]
|
|
2738
|
+
for key, val in mode.items():
|
|
2739
|
+
if key == 'filter':
|
|
2740
|
+
if 'filter' in self.modes[prev_mode_index]:
|
|
2741
|
+
self.filter_query = self.filter_query.replace(self.modes[prev_mode_index]['filter'], '')
|
|
2742
|
+
self.filter_query = f"{self.filter_query.strip()} {val.strip()}".strip()
|
|
2743
|
+
prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
|
|
2744
|
+
|
|
2745
|
+
self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
|
|
2746
|
+
if prev_index in [x[0] for x in self.indexed_items]: new_index = [x[0] for x in self.indexed_items].index(prev_index)
|
|
2747
|
+
else: new_index = 0
|
|
2748
|
+
self.cursor_pos = new_index
|
|
2749
|
+
# Re-sort self.items after applying filter
|
|
2750
|
+
sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
|
|
2751
|
+
elif self.check_key("mode_prev", key, self.keys_dict): # shift+tab key
|
|
2752
|
+
self.logger.info(f"key_function mode_prev")
|
|
2753
|
+
# apply setting
|
|
2754
|
+
prev_mode_index = self.mode_index
|
|
2755
|
+
self.mode_index = (self.mode_index-1)%len(self.modes)
|
|
2756
|
+
mode = self.modes[self.mode_index]
|
|
2757
|
+
for key, val in mode.items():
|
|
2758
|
+
if key == 'filter':
|
|
2759
|
+
if 'filter' in self.modes[prev_mode_index]:
|
|
2760
|
+
self.filter_query = self.filter_query.replace(self.modes[prev_mode_index]['filter'], '')
|
|
2761
|
+
self.filter_query = f"{self.filter_query.strip()} {val.strip()}".strip()
|
|
2762
|
+
prev_index = self.indexed_items[self.cursor_pos][0] if len(self.indexed_items)>0 else 0
|
|
2763
|
+
self.indexed_items = filter_items(self.items, self.indexed_items, self.filter_query)
|
|
2764
|
+
if prev_index in [x[0] for x in self.indexed_items]: new_index = [x[0] for x in self.indexed_items].index(prev_index)
|
|
2765
|
+
else: new_index = 0
|
|
2766
|
+
self.cursor_pos = new_index
|
|
2767
|
+
# Re-sort self.items after applying filter
|
|
2768
|
+
sort_items(self.indexed_items, sort_method=self.columns_sort_method[self.sort_column], sort_column=self.sort_column, sort_reverse=self.sort_reverse[self.sort_column]) # Re-sort self.items based on new column
|
|
2769
|
+
elif self.check_key("pipe_input", key, self.keys_dict):
|
|
2770
|
+
self.logger.info(f"key_function pipe_input")
|
|
2771
|
+
# usrtxt = "xargs -d '\n' -I{} "
|
|
2772
|
+
usrtxt = "xargs "
|
|
2773
|
+
field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
|
|
2774
|
+
if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
|
|
2775
|
+
else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
|
|
2776
|
+
self.set_registers()
|
|
2777
|
+
|
|
2778
|
+
# Get list of available shell commands
|
|
2779
|
+
try:
|
|
2780
|
+
# result = subprocess.run(['compgen', '-c'], capture_output=True, text=True, check=True)
|
|
2781
|
+
# shell_commands = result.stdout.splitlines()
|
|
2782
|
+
result = subprocess.run(['ls', '/usr/bin'], capture_output=True, text=True, check=True)
|
|
2783
|
+
shell_commands = result.stdout.splitlines()
|
|
2784
|
+
except:
|
|
2785
|
+
shell_commands = []
|
|
2786
|
+
usrtxt, return_val = input_field(
|
|
2787
|
+
self.stdscr,
|
|
2788
|
+
usrtxt=usrtxt,
|
|
2789
|
+
field_prefix=" Command: ",
|
|
2790
|
+
x=lambda:2,
|
|
2791
|
+
y=lambda: self.stdscr.getmaxyx()[0]-2,
|
|
2792
|
+
literal=True,
|
|
2793
|
+
max_length=field_end_f,
|
|
2794
|
+
registers=self.registers,
|
|
2795
|
+
refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
|
|
2796
|
+
history=self.history_pipes,
|
|
2797
|
+
path_auto_complete=True,
|
|
2798
|
+
formula_auto_complete=False,
|
|
2799
|
+
function_auto_complete=False,
|
|
2800
|
+
word_auto_complete=True,
|
|
2801
|
+
auto_complete_words=shell_commands,
|
|
2802
|
+
)
|
|
2803
|
+
|
|
2804
|
+
if return_val:
|
|
2805
|
+
selected_indices = get_selected_indices(self.selections)
|
|
2806
|
+
self.history_pipes.append(usrtxt)
|
|
2807
|
+
if not selected_indices:
|
|
2808
|
+
selected_indices = [self.indexed_items[self.cursor_pos][0]]
|
|
2809
|
+
|
|
2810
|
+
full_values = [format_row_full(self.items[i], self.hidden_columns) for i in selected_indices] # Use format_row_full for full data
|
|
2811
|
+
full_values = [self.items[i][self.selected_column] for i in selected_indices]
|
|
2812
|
+
if full_values:
|
|
2813
|
+
command = usrtxt.split()
|
|
2814
|
+
# command = ['xargs', '-d' , '"\n"' '-I', '{}', 'mpv', '{}']
|
|
2815
|
+
# command = ['xargs', '-d' , '"\n"' '-I', '{}', 'mpv', '{}']
|
|
2816
|
+
# command = "xargs -d '\n' -I{} mpv {}"
|
|
2817
|
+
|
|
2818
|
+
try:
|
|
2819
|
+
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
2820
|
+
|
|
2821
|
+
if process.stdin != None:
|
|
2822
|
+
for value in full_values:
|
|
2823
|
+
process.stdin.write((repr(value) + '\n').encode())
|
|
2824
|
+
|
|
2825
|
+
process.stdin.close()
|
|
2826
|
+
|
|
2827
|
+
self.notification(self.stdscr, message=f"{len(full_values)} strings piped to {repr(usrtxt)}")
|
|
2828
|
+
except Exception as e:
|
|
2829
|
+
self.notification(self.stdscr, message=f"{e}")
|
|
2830
|
+
|
|
2831
|
+
|
|
2832
|
+
elif self.check_key("open", key, self.keys_dict):
|
|
2833
|
+
self.logger.info(f"key_function open")
|
|
2834
|
+
selected_indices = get_selected_indices(self.selections)
|
|
2835
|
+
if not selected_indices:
|
|
2836
|
+
selected_indices = [self.indexed_items[self.cursor_pos][0]]
|
|
2837
|
+
|
|
2838
|
+
file_names = [self.items[i][self.selected_column] for i in selected_indices]
|
|
2839
|
+
response = openFiles(file_names)
|
|
2840
|
+
if response:
|
|
2841
|
+
self.notification(self.stdscr, message=response)
|
|
2842
|
+
|
|
2843
|
+
|
|
2844
|
+
elif self.check_key("reset_opts", key, self.keys_dict):
|
|
2845
|
+
self.logger.info(f"key_function reset_opts")
|
|
2846
|
+
self.user_opts = ""
|
|
2847
|
+
|
|
2848
|
+
elif self.check_key("edit", key, self.keys_dict):
|
|
2849
|
+
self.logger.info(f"key_function edit")
|
|
2850
|
+
if len(self.indexed_items) > 0 and self.selected_column >=0 and self.editable_columns[self.selected_column]:
|
|
2851
|
+
current_val = self.indexed_items[self.cursor_pos][1][self.selected_column]
|
|
2852
|
+
usrtxt = f"{current_val}"
|
|
2853
|
+
field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
|
|
2854
|
+
if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
|
|
2855
|
+
else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
|
|
2856
|
+
self.set_registers()
|
|
2857
|
+
words = self.get_word_list()
|
|
2858
|
+
usrtxt, return_val = input_field(
|
|
2859
|
+
self.stdscr,
|
|
2860
|
+
usrtxt=usrtxt,
|
|
2861
|
+
field_prefix=" Edit value: ",
|
|
2862
|
+
x=lambda:2,
|
|
2863
|
+
y=lambda: self.stdscr.getmaxyx()[0]-2,
|
|
2864
|
+
max_length=field_end_f,
|
|
2865
|
+
registers=self.registers,
|
|
2866
|
+
refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
|
|
2867
|
+
history = self.history_edits,
|
|
2868
|
+
path_auto_complete=True,
|
|
2869
|
+
formula_auto_complete=True,
|
|
2870
|
+
function_auto_complete=True,
|
|
2871
|
+
word_auto_complete=True,
|
|
2872
|
+
auto_complete_words=words,
|
|
2873
|
+
)
|
|
2874
|
+
if return_val:
|
|
2875
|
+
if usrtxt.startswith("```"):
|
|
2876
|
+
usrtxt = str(eval(usrtxt[3:]))
|
|
2877
|
+
self.indexed_items[self.cursor_pos][1][self.selected_column] = usrtxt
|
|
2878
|
+
self.history_edits.append(usrtxt)
|
|
2879
|
+
|
|
2880
|
+
elif self.check_key("edit_picker", key, self.keys_dict):
|
|
2881
|
+
self.logger.info(f"key_function edit_picker")
|
|
2882
|
+
if len(self.indexed_items) > 0 and self.selected_column >=0 and self.editable_columns[self.selected_column]:
|
|
2883
|
+
current_val = self.indexed_items[self.cursor_pos][1][self.selected_column]
|
|
2884
|
+
usrtxt = f"{current_val}"
|
|
2885
|
+
field_end_f = lambda: self.stdscr.getmaxyx()[1]-38 if self.show_footer else lambda: self.stdscr.getmaxyx()[1]-3
|
|
2886
|
+
if self.show_footer and self.footer.height >= 2: field_end_f = lambda: self.stdscr.getmaxyx()[1]-38
|
|
2887
|
+
else: field_end_f = lambda: self.stdscr.getmaxyx()[1]-3
|
|
2888
|
+
self.set_registers()
|
|
2889
|
+
words = self.get_word_list()
|
|
2890
|
+
usrtxt, return_val = input_field(
|
|
2891
|
+
self.stdscr,
|
|
2892
|
+
usrtxt=usrtxt,
|
|
2893
|
+
field_prefix=" Edit value: ",
|
|
2894
|
+
x=lambda:2,
|
|
2895
|
+
y=lambda: self.stdscr.getmaxyx()[0]-2,
|
|
2896
|
+
max_length=field_end_f,
|
|
2897
|
+
registers=self.registers,
|
|
2898
|
+
refresh_screen_function=lambda: self.draw_screen(self.indexed_items, self.highlights),
|
|
2899
|
+
history = self.history_edits,
|
|
2900
|
+
path_auto_complete=True,
|
|
2901
|
+
formula_auto_complete=True,
|
|
2902
|
+
function_auto_complete=True,
|
|
2903
|
+
word_auto_complete=True,
|
|
2904
|
+
auto_complete_words=words,
|
|
2905
|
+
)
|
|
2906
|
+
if return_val:
|
|
2907
|
+
self.indexed_items[self.cursor_pos][1][self.selected_column] = usrtxt
|
|
2908
|
+
self.history_edits.append(usrtxt)
|
|
2909
|
+
elif self.check_key("edit_ipython", key, self.keys_dict):
|
|
2910
|
+
self.logger.info(f"key_function edit_picker")
|
|
2911
|
+
import IPython
|
|
2912
|
+
self.stdscr.clear()
|
|
2913
|
+
restrict_curses(self.stdscr)
|
|
2914
|
+
self.stdscr.clear()
|
|
2915
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
2916
|
+
globals()['self'] = self # make the instance available in IPython namespace
|
|
2917
|
+
|
|
2918
|
+
from traitlets.config import Config
|
|
2919
|
+
c = Config()
|
|
2920
|
+
# Doesn't work; Config only works with start_ipython, not embed... but start_ipython causes errors
|
|
2921
|
+
# c.InteractiveShellApp.exec_lines = [
|
|
2922
|
+
# '%clear'
|
|
2923
|
+
# ]
|
|
2924
|
+
msg = "The active Picker object has variable name self.\n"
|
|
2925
|
+
msg += "\te.g., self.items will display the items in Picker"
|
|
2926
|
+
IPython.embed(header=msg, config=c)
|
|
2927
|
+
|
|
2928
|
+
unrestrict_curses(self.stdscr)
|
|
2929
|
+
|
|
2930
|
+
self.stdscr.clear()
|
|
2931
|
+
self.stdscr.refresh()
|
|
2932
|
+
self.initialise_variables()
|
|
2933
|
+
|
|
2934
|
+
|
|
2935
|
+
# The refresh symbol colour is not updated when the data is retrieved so remains white until a key is pressed.
|
|
2936
|
+
# if key != -1:
|
|
2937
|
+
# self.draw_screen(self.indexed_items, self.highlights, clear=clear_screen)
|
|
2938
|
+
|
|
2939
|
+
|
|
2940
|
+
self.draw_screen(self.indexed_items, self.highlights, clear=clear_screen)
|
|
2941
|
+
|
|
2942
|
+
|
|
2943
|
+
|
|
2944
|
+
def set_colours(pick: int = 0, start: int = 0) -> Optional[int]:
|
|
2945
|
+
""" Initialise curses colour pairs from dictionary. """
|
|
2946
|
+
|
|
2947
|
+
|
|
2948
|
+
global COLOURS_SET, notification_colours, help_colours
|
|
2949
|
+
if COLOURS_SET: return None
|
|
2950
|
+
if start == None: start = 0
|
|
2951
|
+
|
|
2952
|
+
|
|
2953
|
+
if curses.COLORS >= 255:
|
|
2954
|
+
colours = get_colours(pick)
|
|
2955
|
+
notification_colours = get_notification_colours(pick)
|
|
2956
|
+
help_colours = get_help_colours(pick)
|
|
2957
|
+
standard_colours_start, notification_colours_start, help_colours_start = 0, 50, 100
|
|
2958
|
+
else:
|
|
2959
|
+
colours = get_fallback_colours()
|
|
2960
|
+
notification_colours = get_fallback_colours()
|
|
2961
|
+
help_colours = get_fallback_colours()
|
|
2962
|
+
standard_colours_start, help_colours_start, notification_colours_start = 0, 0, 0
|
|
2963
|
+
|
|
2964
|
+
if not colours: return 0
|
|
2965
|
+
|
|
2966
|
+
try:
|
|
2967
|
+
start = standard_colours_start
|
|
2968
|
+
curses.init_pair(start+1, colours['selected_fg'], colours['selected_bg'])
|
|
2969
|
+
curses.init_pair(start+2, colours['unselected_fg'], colours['unselected_bg'])
|
|
2970
|
+
curses.init_pair(start+3, colours['normal_fg'], colours['background'])
|
|
2971
|
+
curses.init_pair(start+4, colours['header_fg'], colours['header_bg'])
|
|
2972
|
+
curses.init_pair(start+5, colours['cursor_fg'], colours['cursor_bg'])
|
|
2973
|
+
curses.init_pair(start+6, colours['normal_fg'], colours['background'])
|
|
2974
|
+
curses.init_pair(start+7, colours['error_fg'], colours['error_bg'])
|
|
2975
|
+
curses.init_pair(start+8, colours['complete_fg'], colours['complete_bg'])
|
|
2976
|
+
curses.init_pair(start+9, colours['active_fg'], colours['active_bg'])
|
|
2977
|
+
curses.init_pair(start+10, colours['search_fg'], colours['search_bg'])
|
|
2978
|
+
curses.init_pair(start+11, colours['waiting_fg'], colours['waiting_bg'])
|
|
2979
|
+
curses.init_pair(start+12, colours['paused_fg'], colours['paused_bg'])
|
|
2980
|
+
curses.init_pair(start+13, colours['active_input_fg'], colours['active_input_bg'])
|
|
2981
|
+
curses.init_pair(start+14, colours['modes_selected_fg'], colours['modes_selected_bg'])
|
|
2982
|
+
curses.init_pair(start+15, colours['modes_unselected_fg'], colours['modes_unselected_bg'])
|
|
2983
|
+
curses.init_pair(start+16, colours['title_fg'], colours['title_bg'])
|
|
2984
|
+
curses.init_pair(start+17, colours['normal_fg'], colours['title_bar'])
|
|
2985
|
+
curses.init_pair(start+18, colours['normal_fg'], colours['scroll_bar_bg'])
|
|
2986
|
+
curses.init_pair(start+19, colours['selected_header_column_fg'], colours['selected_header_column_bg'])
|
|
2987
|
+
curses.init_pair(start+20, colours['footer_fg'], colours['footer_bg'])
|
|
2988
|
+
curses.init_pair(start+21, colours['refreshing_fg'], colours['refreshing_bg'])
|
|
2989
|
+
curses.init_pair(start+22, colours['40pc_fg'], colours['40pc_bg'])
|
|
2990
|
+
curses.init_pair(start+23, colours['refreshing_inactive_fg'], colours['refreshing_inactive_bg'])
|
|
2991
|
+
curses.init_pair(start+24, colours['footer_string_fg'], colours['footer_string_bg'])
|
|
2992
|
+
curses.init_pair(start+25, colours['selected_cell_fg'], colours['selected_cell_bg'])
|
|
2993
|
+
curses.init_pair(start+26, colours['deselecting_cell_fg'], colours['deselecting_cell_bg'])
|
|
2994
|
+
|
|
2995
|
+
|
|
2996
|
+
# notifications 50, infobox 100, help 150
|
|
2997
|
+
# Notification colours
|
|
2998
|
+
colours = notification_colours
|
|
2999
|
+
start = notification_colours_start
|
|
3000
|
+
curses.init_pair(start+1, colours['selected_fg'], colours['selected_bg'])
|
|
3001
|
+
curses.init_pair(start+2, colours['unselected_fg'], colours['unselected_bg'])
|
|
3002
|
+
curses.init_pair(start+3, colours['normal_fg'], colours['background'])
|
|
3003
|
+
curses.init_pair(start+4, colours['header_fg'], colours['header_bg'])
|
|
3004
|
+
curses.init_pair(start+5, colours['cursor_fg'], colours['cursor_bg'])
|
|
3005
|
+
curses.init_pair(start+6, colours['normal_fg'], colours['background'])
|
|
3006
|
+
curses.init_pair(start+7, colours['error_fg'], colours['error_bg'])
|
|
3007
|
+
curses.init_pair(start+8, colours['complete_fg'], colours['complete_bg'])
|
|
3008
|
+
curses.init_pair(start+9, colours['active_fg'], colours['active_bg'])
|
|
3009
|
+
curses.init_pair(start+10, colours['search_fg'], colours['search_bg'])
|
|
3010
|
+
curses.init_pair(start+11, colours['waiting_fg'], colours['waiting_bg'])
|
|
3011
|
+
curses.init_pair(start+12, colours['paused_fg'], colours['paused_bg'])
|
|
3012
|
+
curses.init_pair(start+13, colours['active_input_fg'], colours['active_input_bg'])
|
|
3013
|
+
curses.init_pair(start+14, colours['modes_selected_fg'], colours['modes_selected_bg'])
|
|
3014
|
+
curses.init_pair(start+15, colours['modes_unselected_fg'], colours['modes_unselected_bg'])
|
|
3015
|
+
curses.init_pair(start+16, colours['title_fg'], colours['title_bg'])
|
|
3016
|
+
curses.init_pair(start+17, colours['normal_fg'], colours['title_bar'])
|
|
3017
|
+
curses.init_pair(start+18, colours['normal_fg'], colours['scroll_bar_bg'])
|
|
3018
|
+
curses.init_pair(start+19, colours['selected_header_column_fg'], colours['selected_header_column_bg'])
|
|
3019
|
+
curses.init_pair(start+20, colours['footer_fg'], colours['footer_bg'])
|
|
3020
|
+
curses.init_pair(start+21, colours['refreshing_fg'], colours['refreshing_bg'])
|
|
3021
|
+
curses.init_pair(start+22, colours['40pc_fg'], colours['40pc_bg'])
|
|
3022
|
+
curses.init_pair(start+23, colours['refreshing_inactive_fg'], colours['refreshing_inactive_bg'])
|
|
3023
|
+
curses.init_pair(start+24, colours['footer_string_fg'], colours['footer_string_bg'])
|
|
3024
|
+
curses.init_pair(start+25, colours['selected_cell_fg'], colours['selected_cell_bg'])
|
|
3025
|
+
curses.init_pair(start+26, colours['deselecting_cell_fg'], colours['deselecting_cell_bg'])
|
|
3026
|
+
|
|
3027
|
+
# Help
|
|
3028
|
+
colours = help_colours
|
|
3029
|
+
start = help_colours_start
|
|
3030
|
+
curses.init_pair(start+1, colours['selected_fg'], colours['selected_bg'])
|
|
3031
|
+
curses.init_pair(start+2, colours['unselected_fg'], colours['unselected_bg'])
|
|
3032
|
+
curses.init_pair(start+3, colours['normal_fg'], colours['background'])
|
|
3033
|
+
curses.init_pair(start+4, colours['header_fg'], colours['header_bg'])
|
|
3034
|
+
curses.init_pair(start+5, colours['cursor_fg'], colours['cursor_bg'])
|
|
3035
|
+
curses.init_pair(start+6, colours['normal_fg'], colours['background'])
|
|
3036
|
+
curses.init_pair(start+7, colours['error_fg'], colours['error_bg'])
|
|
3037
|
+
curses.init_pair(start+8, colours['complete_fg'], colours['complete_bg'])
|
|
3038
|
+
curses.init_pair(start+9, colours['active_fg'], colours['active_bg'])
|
|
3039
|
+
curses.init_pair(start+10, colours['search_fg'], colours['search_bg'])
|
|
3040
|
+
curses.init_pair(start+11, colours['waiting_fg'], colours['waiting_bg'])
|
|
3041
|
+
curses.init_pair(start+12, colours['paused_fg'], colours['paused_bg'])
|
|
3042
|
+
curses.init_pair(start+13, colours['active_input_fg'], colours['active_input_bg'])
|
|
3043
|
+
curses.init_pair(start+14, colours['modes_selected_fg'], colours['modes_selected_bg'])
|
|
3044
|
+
curses.init_pair(start+15, colours['modes_unselected_fg'], colours['modes_unselected_bg'])
|
|
3045
|
+
curses.init_pair(start+16, colours['title_fg'], colours['title_bg'])
|
|
3046
|
+
curses.init_pair(start+17, colours['normal_fg'], colours['title_bar'])
|
|
3047
|
+
curses.init_pair(start+18, colours['normal_fg'], colours['scroll_bar_bg'])
|
|
3048
|
+
curses.init_pair(start+19, colours['selected_header_column_fg'], colours['selected_header_column_bg'])
|
|
3049
|
+
curses.init_pair(start+20, colours['footer_fg'], colours['footer_bg'])
|
|
3050
|
+
curses.init_pair(start+21, colours['refreshing_fg'], colours['refreshing_bg'])
|
|
3051
|
+
curses.init_pair(start+22, colours['40pc_fg'], colours['40pc_bg'])
|
|
3052
|
+
curses.init_pair(start+23, colours['refreshing_inactive_fg'], colours['refreshing_inactive_bg'])
|
|
3053
|
+
curses.init_pair(start+24, colours['footer_string_fg'], colours['footer_string_bg'])
|
|
3054
|
+
curses.init_pair(start+25, colours['selected_cell_fg'], colours['selected_cell_bg'])
|
|
3055
|
+
curses.init_pair(start+26, colours['deselecting_cell_fg'], colours['deselecting_cell_bg'])
|
|
3056
|
+
except:
|
|
3057
|
+
pass
|
|
3058
|
+
COLOURS_SET = True
|
|
3059
|
+
return start+21
|
|
3060
|
+
|
|
3061
|
+
def parse_arguments() -> Tuple[argparse.Namespace, dict]:
|
|
3062
|
+
""" Parse command line arguments. """
|
|
3063
|
+
parser = argparse.ArgumentParser(description='Convert table to list of lists.')
|
|
3064
|
+
# parser.add_argument('filename', type=str, help='The file to process')
|
|
3065
|
+
parser.add_argument('-i', dest='file', help='File containing the table to be converted.')
|
|
3066
|
+
parser.add_argument('--load', '-l', dest='load', type=str, help='Load file from Picker dump.')
|
|
3067
|
+
parser.add_argument('--stdin', dest='stdin', action='store_true', help='Table passed on stdin')
|
|
3068
|
+
parser.add_argument('--stdin2', action='store_true', help='Table passed on stdin')
|
|
3069
|
+
parser.add_argument('--generate', '-g', type=str, help='Pass file to generate data for listpick Picker.')
|
|
3070
|
+
parser.add_argument('-d', dest='delimiter', default='\t', help='Delimiter for rows in the table (default: tab)')
|
|
3071
|
+
parser.add_argument('-t', dest='file_type', choices=['tsv', 'csv', 'json', 'xlsx', 'ods', 'pkl'], help='Type of file (tsv, csv, json, xlsx, ods)')
|
|
3072
|
+
parser.add_argument('--debug', action="store_true", help="Enable debug log.")
|
|
3073
|
+
parser.add_argument('--debug-verbose', action="store_true", help="Enable debug verbose log.")
|
|
3074
|
+
args = parser.parse_args()
|
|
3075
|
+
|
|
3076
|
+
|
|
3077
|
+
function_data = {
|
|
3078
|
+
"items" : [],
|
|
3079
|
+
"header": [],
|
|
3080
|
+
"unselectable_indices" : [],
|
|
3081
|
+
"colours": get_colours(0),
|
|
3082
|
+
"top_gap": 0,
|
|
3083
|
+
"max_column_width": 70,
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
if args.file:
|
|
3087
|
+
input_arg = args.file
|
|
3088
|
+
elif args.stdin:
|
|
3089
|
+
input_arg = '--stdin'
|
|
3090
|
+
elif args.stdin2:
|
|
3091
|
+
input_arg = '--stdin2'
|
|
3092
|
+
# elif args.filename:
|
|
3093
|
+
# input_arg = args.filename
|
|
3094
|
+
|
|
3095
|
+
elif args.generate:
|
|
3096
|
+
function_data["refresh_function"] = lambda : generate_picker_data(args.generate)
|
|
3097
|
+
function_data["get_data_startup"] = True
|
|
3098
|
+
function_data["get_new_data"] = True
|
|
3099
|
+
return args, function_data
|
|
3100
|
+
elif args.load:
|
|
3101
|
+
function_data = load_state(args.load)
|
|
3102
|
+
function_data["refresh_function"] = lambda : (load_state(args.load)["items"], load_state(args.load)["header"])
|
|
3103
|
+
function_data["get_new_data"] = True
|
|
3104
|
+
return args, function_data
|
|
3105
|
+
|
|
3106
|
+
else:
|
|
3107
|
+
# print("Error: Please provide input file or use --stdin flag.")
|
|
3108
|
+
print("No data provided. Loading empty Picker.")
|
|
3109
|
+
return args, function_data
|
|
3110
|
+
# sys.exit(1)
|
|
3111
|
+
if args.debug:
|
|
3112
|
+
function_data["debug"] = True
|
|
3113
|
+
function_data["debug_level"] = 1
|
|
3114
|
+
|
|
3115
|
+
if args.debug_verbose:
|
|
3116
|
+
function_data["debug"] = True
|
|
3117
|
+
function_data["debug_level"] = 0
|
|
3118
|
+
|
|
3119
|
+
if not args.file_type:
|
|
3120
|
+
filetype = guess_file_type(input_arg)
|
|
3121
|
+
else:
|
|
3122
|
+
filetype = args.file_type
|
|
3123
|
+
|
|
3124
|
+
|
|
3125
|
+
items, header = table_to_list(input_arg, args.delimiter, filetype)
|
|
3126
|
+
function_data["items"] = items
|
|
3127
|
+
if header: function_data["header"] = header
|
|
3128
|
+
return args, function_data
|
|
3129
|
+
|
|
3130
|
+
def start_curses() -> curses.window:
|
|
3131
|
+
""" Initialise curses and return curses window. """
|
|
3132
|
+
stdscr = curses.initscr()
|
|
3133
|
+
curses.start_color()
|
|
3134
|
+
curses.use_default_colors() # For terminal theme-recolouring
|
|
3135
|
+
curses.noecho() # Turn off automatic echoing of keys to the screen
|
|
3136
|
+
curses.cbreak() # Interpret keystrokes immediately (without requiring Enter)
|
|
3137
|
+
stdscr.keypad(True) # Ensures that arrow and function keys are received as one key by getch
|
|
3138
|
+
curses.raw() # Disable control keys (ctrl-c, ctrl-s, ctrl-q, etc.)
|
|
3139
|
+
curses.curs_set(False)
|
|
3140
|
+
|
|
3141
|
+
return stdscr
|
|
3142
|
+
|
|
3143
|
+
def close_curses(stdscr: curses.window) -> None:
|
|
3144
|
+
""" Close curses. """
|
|
3145
|
+
stdscr.keypad(False)
|
|
3146
|
+
curses.nocbreak()
|
|
3147
|
+
curses.noraw()
|
|
3148
|
+
curses.echo()
|
|
3149
|
+
curses.endwin()
|
|
3150
|
+
|
|
3151
|
+
def restrict_curses(stdscr: curses.window) -> None:
|
|
3152
|
+
""" Restrict curses for normal input. Used when dropping to ipython. """
|
|
3153
|
+
stdscr.keypad(False)
|
|
3154
|
+
curses.nocbreak()
|
|
3155
|
+
curses.noraw()
|
|
3156
|
+
curses.curs_set(True)
|
|
3157
|
+
curses.echo()
|
|
3158
|
+
|
|
3159
|
+
def unrestrict_curses(stdscr: curses.window) -> None:
|
|
3160
|
+
""" Unrestrict curses for terminal input. Used after dropping to ipython. """
|
|
3161
|
+
curses.noecho() # Turn off automatic echoing of keys to the screen
|
|
3162
|
+
curses.cbreak() # Interpret keystrokes immediately (without requiring Enter)
|
|
3163
|
+
stdscr.keypad(True)
|
|
3164
|
+
curses.raw() # Disable control keys (ctrl-c, ctrl-s, ctrl-q, etc.)
|
|
3165
|
+
curses.curs_set(False)
|
|
3166
|
+
|
|
3167
|
+
def main() -> None:
|
|
3168
|
+
""" Main function when listpick is executed. Deals with command line arguments and starts a Picker. """
|
|
3169
|
+
args, function_data = parse_arguments()
|
|
3170
|
+
|
|
3171
|
+
try:
|
|
3172
|
+
if function_data["items"] == []:
|
|
3173
|
+
function_data["items"] = test_items
|
|
3174
|
+
function_data["highlights"] = test_highlights
|
|
3175
|
+
function_data["header"] = test_header
|
|
3176
|
+
except:
|
|
3177
|
+
pass
|
|
3178
|
+
|
|
3179
|
+
function_data["colour_theme_number"] = 3
|
|
3180
|
+
function_data["modes"] = [
|
|
3181
|
+
{
|
|
3182
|
+
'filter': '',
|
|
3183
|
+
'sort': 0,
|
|
3184
|
+
'name': 'All',
|
|
3185
|
+
},
|
|
3186
|
+
{
|
|
3187
|
+
'filter': '--2 miss',
|
|
3188
|
+
'name': 'miss',
|
|
3189
|
+
},
|
|
3190
|
+
{
|
|
3191
|
+
'filter': '--2 mp4',
|
|
3192
|
+
'name': 'mp4',
|
|
3193
|
+
},
|
|
3194
|
+
]
|
|
3195
|
+
highlights = [
|
|
3196
|
+
{
|
|
3197
|
+
"field": 1,
|
|
3198
|
+
"match": "a",
|
|
3199
|
+
"color": 8,
|
|
3200
|
+
}
|
|
3201
|
+
]
|
|
3202
|
+
function_data["cell_cursor"] = True
|
|
3203
|
+
function_data["display_modes"] = True
|
|
3204
|
+
function_data["centre_in_cols"] = True
|
|
3205
|
+
function_data["show_row_header"] = True
|
|
3206
|
+
function_data["keys_dict"] = picker_keys
|
|
3207
|
+
function_data["id_column"] = -1
|
|
3208
|
+
function_data["track_entries_upon_refresh"] = True
|
|
3209
|
+
function_data["centre_in_terminal_vertical"] = True
|
|
3210
|
+
function_data["highlight_full_row"] = True
|
|
3211
|
+
function_data["pin_cursor"] = True
|
|
3212
|
+
function_data["display_infobox"] = True
|
|
3213
|
+
function_data["infobox_items"] = [["1"], ["2"], ["3"]]
|
|
3214
|
+
function_data["infobox_title"] = "Title"
|
|
3215
|
+
function_data["footer_string"] = "Title"
|
|
3216
|
+
function_data["highlights"] = highlights
|
|
3217
|
+
function_data["show_footer"] = False
|
|
3218
|
+
|
|
3219
|
+
|
|
3220
|
+
# function_data["debug"] = True
|
|
3221
|
+
# function_data["debug_level"] = 1
|
|
3222
|
+
stdscr = start_curses()
|
|
3223
|
+
# h, w = stdscr.getmaxyx()
|
|
3224
|
+
# win = stdscr.derwin(h, w//2, 0, 0)
|
|
3225
|
+
try:
|
|
3226
|
+
# Run the Picker
|
|
3227
|
+
# h, w = stdscr.getmaxyx()
|
|
3228
|
+
# if (h>8 and w >20):
|
|
3229
|
+
# curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
|
|
3230
|
+
# stdscr.bkgd(' ', curses.color_pair(1)) # Apply background color
|
|
3231
|
+
# s = "Listpick is loading your data..."
|
|
3232
|
+
# stdscr.addstr(h//2, (w-len(s))//2, s)
|
|
3233
|
+
# stdscr.refresh()
|
|
3234
|
+
|
|
3235
|
+
# app = Picker(stdscr, **function_data)
|
|
3236
|
+
app = Picker(stdscr)
|
|
3237
|
+
app.set_function_data(function_data)
|
|
3238
|
+
app.splash_screen("Listpick is loading your data...")
|
|
3239
|
+
app.load_input_history("~/.config/listpick/cmdhist.json")
|
|
3240
|
+
app.run()
|
|
3241
|
+
|
|
3242
|
+
app.save_input_history("~/.config/listpick/cmdhist.json")
|
|
3243
|
+
except Exception as e:
|
|
3244
|
+
print(e)
|
|
3245
|
+
|
|
3246
|
+
# Clean up
|
|
3247
|
+
close_curses(stdscr)
|
|
3248
|
+
|
|
3249
|
+
if __name__ == '__main__':
|
|
3250
|
+
main()
|