listpick 0.1.13.57__py3-none-any.whl → 0.1.13.59__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.py +87 -42
- listpick/ui/footer_1.py +202 -0
- listpick/utils/utils.py +5 -5
- {listpick-0.1.13.57.dist-info → listpick-0.1.13.59.dist-info}/METADATA +1 -1
- {listpick-0.1.13.57.dist-info → listpick-0.1.13.59.dist-info}/RECORD +9 -8
- {listpick-0.1.13.57.dist-info → listpick-0.1.13.59.dist-info}/WHEEL +0 -0
- {listpick-0.1.13.57.dist-info → listpick-0.1.13.59.dist-info}/entry_points.txt +0 -0
- {listpick-0.1.13.57.dist-info → listpick-0.1.13.59.dist-info}/licenses/LICENSE.txt +0 -0
- {listpick-0.1.13.57.dist-info → listpick-0.1.13.59.dist-info}/top_level.txt +0 -0
listpick/listpick_app.py
CHANGED
|
@@ -140,6 +140,7 @@ class Picker:
|
|
|
140
140
|
footer_string_refresh_function: Optional[Callable] = None,
|
|
141
141
|
footer_timer: float=1,
|
|
142
142
|
get_footer_string_startup=False,
|
|
143
|
+
unicode_char_width: bool = True,
|
|
143
144
|
|
|
144
145
|
colours_start: int =0,
|
|
145
146
|
colours_end: int =-1,
|
|
@@ -256,7 +257,7 @@ class Picker:
|
|
|
256
257
|
self.footer_string_refresh_function = footer_string_refresh_function
|
|
257
258
|
self.footer_timer = footer_timer
|
|
258
259
|
self.get_footer_string_startup = get_footer_string_startup,
|
|
259
|
-
|
|
260
|
+
self.unicode_char_width = unicode_char_width
|
|
260
261
|
|
|
261
262
|
|
|
262
263
|
self.colours_start = colours_start
|
|
@@ -312,17 +313,12 @@ class Picker:
|
|
|
312
313
|
self.debug_level = debug_level
|
|
313
314
|
|
|
314
315
|
|
|
315
|
-
|
|
316
|
-
|
|
317
316
|
self.initialise_picker_state(reset_colours=self.reset_colours)
|
|
318
317
|
|
|
319
|
-
|
|
320
318
|
# Note: We have to set the footer after initialising the picker state so that the footer can use the get_function_data method
|
|
321
319
|
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)]
|
|
322
320
|
self.footer = self.footer_options[self.footer_style]
|
|
323
321
|
|
|
324
|
-
# self.footer = CompactFooter(self.stdscr, colours_start, self.get_function_data)
|
|
325
|
-
|
|
326
322
|
|
|
327
323
|
def calculate_section_sizes(self):
|
|
328
324
|
"""
|
|
@@ -384,7 +380,6 @@ class Picker:
|
|
|
384
380
|
def initialise_picker_state(self, reset_colours=False) -> None:
|
|
385
381
|
""" Initialise state variables for the picker. These are: debugging and colours. """
|
|
386
382
|
|
|
387
|
-
|
|
388
383
|
if curses.has_colors() and self.colours != None:
|
|
389
384
|
# raise Exception("Terminal does not support color")
|
|
390
385
|
curses.start_color()
|
|
@@ -694,7 +689,7 @@ class Picker:
|
|
|
694
689
|
# rows = [v[1] for v in self.indexed_items] if len(self.indexed_items) else self.items
|
|
695
690
|
# Determine widths based only on the currently displayed indexed rows
|
|
696
691
|
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)
|
|
692
|
+
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
693
|
visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
|
|
699
694
|
visible_columns_total_width = sum(visible_column_widths) + len(self.separator)*(len(visible_column_widths)-1)
|
|
700
695
|
|
|
@@ -793,10 +788,10 @@ class Picker:
|
|
|
793
788
|
else:
|
|
794
789
|
cell_value = self.indexed_items[row][1][col] + self.separator
|
|
795
790
|
# 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))
|
|
791
|
+
cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width)-len(self.separator), self.unicode_char_width)
|
|
797
792
|
cell_value = cell_value + self.separator
|
|
798
793
|
# cell_value = cell_value
|
|
799
|
-
cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width))
|
|
794
|
+
cell_value = truncate_to_display_width(cell_value, min(cell_width, cell_max_width), self.unicode_char_width)
|
|
800
795
|
# row_str = truncate_to_display_width(row_str_left_adj, min(w-self.startx, visible_columns_total_width))
|
|
801
796
|
self.stdscr.addstr(y, cell_pos, cell_value, curses.color_pair(self.colours_start+colour_pair_number) | curses.A_BOLD)
|
|
802
797
|
# Part of the cell is on screen
|
|
@@ -838,7 +833,7 @@ class Picker:
|
|
|
838
833
|
def draw_highlights(highlights: list[dict], idx: int, y: int, item: tuple[int, list[str]]):
|
|
839
834
|
self.logger.debug(f"function: draw_highlights()")
|
|
840
835
|
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)
|
|
836
|
+
full_row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
|
|
842
837
|
row_str = full_row_str[self.leftmost_char:]
|
|
843
838
|
for highlight in highlights:
|
|
844
839
|
if "row" in highlight:
|
|
@@ -854,13 +849,13 @@ class Picker:
|
|
|
854
849
|
continue
|
|
855
850
|
|
|
856
851
|
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), re.IGNORECASE)
|
|
852
|
+
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
853
|
if not match: continue
|
|
859
854
|
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
855
|
|
|
861
856
|
## We want to search the non-centred values but highlight the centred values.
|
|
862
857
|
if self.centre_in_cols:
|
|
863
|
-
tmp = truncate_to_display_width(item[1][highlight["field"]], self.column_widths[highlight["field"]], self.centre_in_cols)
|
|
858
|
+
tmp = truncate_to_display_width(item[1][highlight["field"]], self.column_widths[highlight["field"]], self.centre_in_cols, self.unicode_char_width)
|
|
864
859
|
field_start += (len(tmp) - len(tmp.lstrip()))
|
|
865
860
|
|
|
866
861
|
highlight_start = field_start + match.start()
|
|
@@ -882,11 +877,11 @@ class Picker:
|
|
|
882
877
|
item = self.indexed_items[idx]
|
|
883
878
|
y = idx - start_index + self.top_space
|
|
884
879
|
|
|
885
|
-
row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)[self.leftmost_char:]
|
|
880
|
+
# row_str = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)[self.leftmost_char:]
|
|
886
881
|
# 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)
|
|
882
|
+
row_str_orig = format_row(item[1], self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
|
|
888
883
|
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))
|
|
884
|
+
row_str = truncate_to_display_width(row_str_left_adj, min(w-self.startx, visible_columns_total_width), self.unicode_char_width)
|
|
890
885
|
# row_str = truncate_to_display_width(row_str, min(w-self.startx, visible_columns_total_width))[self.leftmost_char:]
|
|
891
886
|
|
|
892
887
|
## Display the standard row
|
|
@@ -899,7 +894,7 @@ class Picker:
|
|
|
899
894
|
|
|
900
895
|
# Higlight cursor cell and selected cells
|
|
901
896
|
if self.cell_cursor:
|
|
902
|
-
self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
|
|
897
|
+
# self.selected_cells_by_row = get_selected_cells_by_row(self.cell_selections)
|
|
903
898
|
if item[0] in self.selected_cells_by_row:
|
|
904
899
|
for j in self.selected_cells_by_row[item[0]]:
|
|
905
900
|
highlight_cell(idx, j, visible_column_widths, colour_pair_number=25)
|
|
@@ -993,6 +988,7 @@ class Picker:
|
|
|
993
988
|
self.stdscr.addstr(h - 1, w-footer_string_width-1, " "*footer_string_width, curses.color_pair(self.colours_start+24))
|
|
994
989
|
self.stdscr.addstr(h - 1, w-footer_string_width-1, f"{disp_string}", curses.color_pair(self.colours_start+24))
|
|
995
990
|
|
|
991
|
+
self.stdscr.refresh()
|
|
996
992
|
## Display infobox
|
|
997
993
|
if self.display_infobox:
|
|
998
994
|
self.infobox(self.stdscr, message=self.infobox_items, title=self.infobox_title)
|
|
@@ -1140,6 +1136,7 @@ class Picker:
|
|
|
1140
1136
|
"debug": self.debug,
|
|
1141
1137
|
"debug_level": self.debug_level,
|
|
1142
1138
|
"reset_colours": self.reset_colours,
|
|
1139
|
+
"unicode_char_width": self.unicode_char_width,
|
|
1143
1140
|
}
|
|
1144
1141
|
return function_data
|
|
1145
1142
|
|
|
@@ -1238,7 +1235,7 @@ class Picker:
|
|
|
1238
1235
|
while True:
|
|
1239
1236
|
h, w = stdscr.getmaxyx()
|
|
1240
1237
|
|
|
1241
|
-
choose_opts_widths = get_column_widths(options)
|
|
1238
|
+
choose_opts_widths = get_column_widths(options, unicode_char_width=self.unicode_char_width)
|
|
1242
1239
|
window_width = min(max(sum(choose_opts_widths) + 6, 50) + 6, w)
|
|
1243
1240
|
window_height = min(h//2, max(6, len(options)+3))
|
|
1244
1241
|
|
|
@@ -1393,6 +1390,8 @@ class Picker:
|
|
|
1393
1390
|
self.initialise_variables()
|
|
1394
1391
|
elif setting == "pc":
|
|
1395
1392
|
self.pin_cursor = not self.pin_cursor
|
|
1393
|
+
elif setting == "unicode":
|
|
1394
|
+
self.unicode_char_width = not self.unicode_char_width
|
|
1396
1395
|
|
|
1397
1396
|
elif setting.startswith("ft"):
|
|
1398
1397
|
if len(setting) > 2 and setting[2:].isnumeric():
|
|
@@ -1456,7 +1455,6 @@ class Picker:
|
|
|
1456
1455
|
self.draw_screen(self.indexed_items, self.highlights)
|
|
1457
1456
|
self.notification(self.stdscr, message=f"Theme {self.colour_theme_number} applied.")
|
|
1458
1457
|
|
|
1459
|
-
|
|
1460
1458
|
else:
|
|
1461
1459
|
self.user_settings = ""
|
|
1462
1460
|
return None
|
|
@@ -1489,7 +1487,8 @@ class Picker:
|
|
|
1489
1487
|
self.selections[self.indexed_items[i][0]] = True
|
|
1490
1488
|
for i in self.cell_selections.keys():
|
|
1491
1489
|
self.cell_selections[i] = True
|
|
1492
|
-
|
|
1490
|
+
for row in range(len(self.indexed_items)):
|
|
1491
|
+
self.selected_cells_by_row[row] = list(range(len(self.indexed_items[row][1])))
|
|
1493
1492
|
self.draw_screen(self.indexed_items, self.highlights)
|
|
1494
1493
|
|
|
1495
1494
|
def deselect_all(self) -> None:
|
|
@@ -1499,6 +1498,7 @@ class Picker:
|
|
|
1499
1498
|
self.selections[i] = False
|
|
1500
1499
|
for i in self.cell_selections.keys():
|
|
1501
1500
|
self.cell_selections[i] = False
|
|
1501
|
+
self.selected_cells_by_row = {}
|
|
1502
1502
|
self.draw_screen(self.indexed_items, self.highlights)
|
|
1503
1503
|
|
|
1504
1504
|
def handle_visual_selection(self, selecting:bool = True) -> None:
|
|
@@ -1527,9 +1527,18 @@ class Picker:
|
|
|
1527
1527
|
xend = max(self.start_selection_col, self.selected_column)
|
|
1528
1528
|
for i in range(ystart, yend + 1):
|
|
1529
1529
|
if self.indexed_items[i][0] not in self.unselectable_indices:
|
|
1530
|
-
|
|
1531
|
-
|
|
1530
|
+
row = self.indexed_items[i][0]
|
|
1531
|
+
if row not in self.selected_cells_by_row:
|
|
1532
|
+
self.selected_cells_by_row[row] = []
|
|
1533
|
+
|
|
1534
|
+
for col in range(xstart, xend+1):
|
|
1535
|
+
cell_index = (row, col)
|
|
1532
1536
|
self.cell_selections[cell_index] = True
|
|
1537
|
+
|
|
1538
|
+
self.selected_cells_by_row[row].append(col)
|
|
1539
|
+
# Remove duplicates
|
|
1540
|
+
self.selected_cells_by_row[row] = list(set(self.selected_cells_by_row[row]))
|
|
1541
|
+
|
|
1533
1542
|
self.start_selection = -1
|
|
1534
1543
|
self.end_selection = -1
|
|
1535
1544
|
self.is_selecting = False
|
|
@@ -1551,10 +1560,19 @@ class Picker:
|
|
|
1551
1560
|
xstart = min(self.start_selection_col, self.selected_column)
|
|
1552
1561
|
xend = max(self.start_selection_col, self.selected_column)
|
|
1553
1562
|
for i in range(ystart, yend + 1):
|
|
1563
|
+
row = self.indexed_items[i][0]
|
|
1554
1564
|
if self.indexed_items[i][0] not in self.unselectable_indices:
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1565
|
+
if row in self.selected_cells_by_row:
|
|
1566
|
+
for col in range(xstart, xend+1):
|
|
1567
|
+
try:
|
|
1568
|
+
self.selected_cells_by_row[row].remove(col)
|
|
1569
|
+
except:
|
|
1570
|
+
pass
|
|
1571
|
+
cell_index = (row, col)
|
|
1572
|
+
self.cell_selections[cell_index] = False
|
|
1573
|
+
if self.selected_cells_by_row[row] == []:
|
|
1574
|
+
del self.selected_cells_by_row[row]
|
|
1575
|
+
|
|
1558
1576
|
self.start_selection = -1
|
|
1559
1577
|
self.end_selection = -1
|
|
1560
1578
|
self.is_deselecting = False
|
|
@@ -1833,7 +1851,7 @@ class Picker:
|
|
|
1833
1851
|
|
|
1834
1852
|
def get_word_list(self) -> list[str]:
|
|
1835
1853
|
""" Get a list of all words used in any cell of the picker. Used for completion in search/filter input_field. """
|
|
1836
|
-
self.logger.info(f"function:
|
|
1854
|
+
self.logger.info(f"function: get_word_list()")
|
|
1837
1855
|
translator = str.maketrans('', '', string.punctuation)
|
|
1838
1856
|
|
|
1839
1857
|
words = []
|
|
@@ -2076,6 +2094,7 @@ class Picker:
|
|
|
2076
2094
|
options += [["rh", "Toggle row header"]]
|
|
2077
2095
|
options += [["modes", "Toggle modes"]]
|
|
2078
2096
|
options += [["ft", "Cycle through footer styles (accepts ft#)"]]
|
|
2097
|
+
options += [["unicode", "Toggle b/w using len and wcwidth to calculate char width."]]
|
|
2079
2098
|
options += [[f"s{i}", f"Select col. {i}"] for i in range(len(self.items[0]))]
|
|
2080
2099
|
options += [[f"!{i}", f"Toggle col. {i}"] for i in range(len(self.items[0]))]
|
|
2081
2100
|
options += [["ara", "Add empty row after cursor."]]
|
|
@@ -2140,11 +2159,31 @@ class Picker:
|
|
|
2140
2159
|
if len(self.indexed_items) > 0:
|
|
2141
2160
|
item_index = self.indexed_items[self.cursor_pos][0]
|
|
2142
2161
|
cell_index = (self.indexed_items[self.cursor_pos][0], self.selected_column)
|
|
2162
|
+
row, col = cell_index
|
|
2143
2163
|
selected_count = sum(self.selections.values())
|
|
2144
2164
|
if self.max_selected == -1 or selected_count >= self.max_selected:
|
|
2145
2165
|
self.toggle_item(item_index)
|
|
2146
2166
|
|
|
2147
2167
|
self.cell_selections[cell_index] = not self.cell_selections[cell_index]
|
|
2168
|
+
## Set self.selected_cells_by_row
|
|
2169
|
+
# If any cells in the current row are selected
|
|
2170
|
+
if row in self.selected_cells_by_row:
|
|
2171
|
+
# If the current cell is selected then remove it
|
|
2172
|
+
if col in self.selected_cells_by_row[row]:
|
|
2173
|
+
# If the current cell is the only cell in the row that is selected then remove the row from the dict
|
|
2174
|
+
if len(self.selected_cells_by_row[row]) == 1:
|
|
2175
|
+
|
|
2176
|
+
del self.selected_cells_by_row[row]
|
|
2177
|
+
# else remove only the index of the current cell
|
|
2178
|
+
else:
|
|
2179
|
+
self.selected_cells_by_row[row].remove(col)
|
|
2180
|
+
# If there are cells in the row that are selected then append the current cell to the row
|
|
2181
|
+
else:
|
|
2182
|
+
self.selected_cells_by_row[row].append(col)
|
|
2183
|
+
# Add the a list containing only the current column
|
|
2184
|
+
else:
|
|
2185
|
+
self.selected_cells_by_row[row] = [col]
|
|
2186
|
+
|
|
2148
2187
|
self.cursor_down()
|
|
2149
2188
|
elif self.check_key("select_all", key, self.keys_dict): # Select all (m or ctrl-a)
|
|
2150
2189
|
self.select_all()
|
|
@@ -2278,7 +2317,7 @@ class Picker:
|
|
|
2278
2317
|
|
|
2279
2318
|
## Scroll with column select
|
|
2280
2319
|
rows = self.get_visible_rows()
|
|
2281
|
-
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)
|
|
2320
|
+
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)
|
|
2282
2321
|
visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
|
|
2283
2322
|
column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
|
|
2284
2323
|
start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
|
|
@@ -2305,8 +2344,7 @@ class Picker:
|
|
|
2305
2344
|
|
|
2306
2345
|
## Scroll with column select
|
|
2307
2346
|
rows = self.get_visible_rows()
|
|
2308
|
-
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)
|
|
2309
|
-
|
|
2347
|
+
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)
|
|
2310
2348
|
visible_column_widths = [c for i,c in enumerate(self.column_widths) if i not in self.hidden_columns]
|
|
2311
2349
|
column_set_width = sum(visible_column_widths)+len(self.separator)*len(visible_column_widths)
|
|
2312
2350
|
start_of_cell = sum(visible_column_widths[:self.selected_column])+len(self.separator)*self.selected_column
|
|
@@ -2321,18 +2359,13 @@ class Picker:
|
|
|
2321
2359
|
self.leftmost_char = start_of_cell
|
|
2322
2360
|
|
|
2323
2361
|
self.leftmost_char = max(0, min(column_set_width - display_width + 5, self.leftmost_char))
|
|
2324
|
-
|
|
2362
|
+
|
|
2325
2363
|
elif self.check_key("scroll_right", key, self.keys_dict):
|
|
2326
2364
|
self.logger.info(f"key_function scroll_right")
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
row_str = format_row(item, self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)[self.leftmost_char:]
|
|
2332
|
-
if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
|
|
2333
|
-
|
|
2334
|
-
if longest_row_str_len >= w-self.startx:
|
|
2335
|
-
self.leftmost_char = self.leftmost_char+5
|
|
2365
|
+
if len(self.indexed_items):
|
|
2366
|
+
row_width = sum(self.column_widths) + len(self.separator)*(len(self.column_widths)-1)
|
|
2367
|
+
if row_width-self.leftmost_char >= w-self.startx:
|
|
2368
|
+
self.leftmost_char = self.leftmost_char+5
|
|
2336
2369
|
|
|
2337
2370
|
elif self.check_key("scroll_left", key, self.keys_dict):
|
|
2338
2371
|
self.logger.info(f"key_function scroll_left")
|
|
@@ -2349,7 +2382,7 @@ class Picker:
|
|
|
2349
2382
|
rows = self.get_visible_rows()
|
|
2350
2383
|
for i in range(len(rows)):
|
|
2351
2384
|
item = rows[i]
|
|
2352
|
-
row_str = format_row(item, self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols)
|
|
2385
|
+
row_str = format_row(item, self.hidden_columns, self.column_widths, self.separator, self.centre_in_cols, self.unicode_char_width)
|
|
2353
2386
|
if len(row_str) > longest_row_str_len: longest_row_str_len=len(row_str)
|
|
2354
2387
|
# for i in range(len(self.indexed_items)):
|
|
2355
2388
|
# item = self.indexed_items[i]
|
|
@@ -2450,7 +2483,7 @@ class Picker:
|
|
|
2450
2483
|
elif key == curses.KEY_RESIZE: # Terminal resize signal
|
|
2451
2484
|
|
|
2452
2485
|
self.calculate_section_sizes()
|
|
2453
|
-
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)
|
|
2486
|
+
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)
|
|
2454
2487
|
|
|
2455
2488
|
self.draw_screen(self.indexed_items, self.highlights)
|
|
2456
2489
|
|
|
@@ -2860,7 +2893,6 @@ class Picker:
|
|
|
2860
2893
|
def set_colours(pick: int = 0, start: int = 0) -> Optional[int]:
|
|
2861
2894
|
""" Initialise curses colour pairs from dictionary. """
|
|
2862
2895
|
|
|
2863
|
-
|
|
2864
2896
|
global COLOURS_SET, notification_colours, help_colours
|
|
2865
2897
|
if COLOURS_SET: return None
|
|
2866
2898
|
if start == None: start = 0
|
|
@@ -3108,6 +3140,13 @@ def main() -> None:
|
|
|
3108
3140
|
# 'name': 'mp4',
|
|
3109
3141
|
# },
|
|
3110
3142
|
# ]
|
|
3143
|
+
highlights = [
|
|
3144
|
+
{
|
|
3145
|
+
"field": 1,
|
|
3146
|
+
"match": "a",
|
|
3147
|
+
"color": 8,
|
|
3148
|
+
}
|
|
3149
|
+
]
|
|
3111
3150
|
function_data["cell_cursor"] = True
|
|
3112
3151
|
function_data["display_modes"] = True
|
|
3113
3152
|
function_data["centre_in_cols"] = True
|
|
@@ -3118,6 +3157,12 @@ def main() -> None:
|
|
|
3118
3157
|
function_data["centre_in_terminal_vertical"] = True
|
|
3119
3158
|
function_data["highlight_full_row"] = True
|
|
3120
3159
|
function_data["pin_cursor"] = True
|
|
3160
|
+
function_data["display_infobox"] = True
|
|
3161
|
+
function_data["infobox_items"] = [["1"], ["2"], ["3"]]
|
|
3162
|
+
function_data["infobox_title"] = "Title"
|
|
3163
|
+
# function_data["footer_string"] = "Title"
|
|
3164
|
+
function_data["highlights"] = highlights
|
|
3165
|
+
function_data["show_footer"] = False
|
|
3121
3166
|
# function_data["debug"] = True
|
|
3122
3167
|
# function_data["debug_level"] = 1
|
|
3123
3168
|
stdscr = start_curses()
|
listpick/ui/footer_1.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
footer.py
|
|
3
|
+
Lines to be displayed on the help screen.
|
|
4
|
+
|
|
5
|
+
Author: GrimAndGreedy
|
|
6
|
+
License: MIT
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import curses
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger('picker_log')
|
|
13
|
+
|
|
14
|
+
class Footer:
|
|
15
|
+
def __init__(self, stdscr, colours_start, get_state_function):
|
|
16
|
+
"""
|
|
17
|
+
stdscr: curses screen object
|
|
18
|
+
colours_start: base colour pair index
|
|
19
|
+
get_state_callback: function that returns a dict with all required data for rendering
|
|
20
|
+
"""
|
|
21
|
+
self.stdscr = stdscr
|
|
22
|
+
self.colours_start = colours_start
|
|
23
|
+
self.get_state = get_state_function
|
|
24
|
+
self.height = 0
|
|
25
|
+
|
|
26
|
+
def draw(self, h, w):
|
|
27
|
+
"""
|
|
28
|
+
Draw the footer. Must be implemented by subclasses.
|
|
29
|
+
"""
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
class StandardFooter(Footer):
|
|
33
|
+
def __init__(self, stdscr, colours_start, get_state_function):
|
|
34
|
+
"""
|
|
35
|
+
stdscr: curses screen object
|
|
36
|
+
colours_start: base colour pair index
|
|
37
|
+
get_state_callback: function that returns a dict with all required data for rendering
|
|
38
|
+
"""
|
|
39
|
+
self.stdscr = stdscr
|
|
40
|
+
self.colours_start = colours_start
|
|
41
|
+
self.get_state = get_state_function
|
|
42
|
+
self.height = 3
|
|
43
|
+
def draw(self, h, w):
|
|
44
|
+
state = self.get_state()
|
|
45
|
+
|
|
46
|
+
# Fill background
|
|
47
|
+
for i in range(3):
|
|
48
|
+
self.stdscr.addstr(h-3+i, 0, ' '*(w-1), curses.color_pair(self.colours_start+20))
|
|
49
|
+
|
|
50
|
+
if state["filter_query"]:
|
|
51
|
+
self.stdscr.addstr(h - 2, 2, f" Filter: {state['filter_query']} "[:w-40], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
|
|
52
|
+
if state["search_query"]:
|
|
53
|
+
self.stdscr.addstr(h - 3, 2, f" Search: {state['search_query']} [{state['search_index']}/{state['search_count']}] "[:w-3], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
|
|
54
|
+
if state["user_opts"]:
|
|
55
|
+
self.stdscr.addstr(h - 1, 2, f" Opts: {state['user_opts']} "[:w-3], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
|
|
56
|
+
|
|
57
|
+
# Sort info
|
|
58
|
+
sort_column_info = f"{state['sort_column'] if state['sort_column'] is not None else 'None'}"
|
|
59
|
+
sort_method_info = f"{state['SORT_METHODS'][state['columns_sort_method'][state['sort_column']]]}" if state['sort_column'] is not None else "NA"
|
|
60
|
+
sort_order_info = "Desc." if state["sort_reverse"] else "Asc."
|
|
61
|
+
sort_order_info = "▼" if state["sort_reverse"][state['sort_column']] else "▲"
|
|
62
|
+
sort_disp_str = f" Sort: ({sort_column_info}, {sort_method_info}, {sort_order_info}) "
|
|
63
|
+
self.stdscr.addstr(h - 2, w-35, f"{sort_disp_str:>34}", curses.color_pair(self.colours_start+20))
|
|
64
|
+
|
|
65
|
+
if state["footer_string"]:
|
|
66
|
+
# footer_string_width = min(w-1, max(len(state["footer_string"]), 50))
|
|
67
|
+
# disp_string = f"{state['footer_string'][:footer_string_width]:>{footer_string_width-1}} "
|
|
68
|
+
# self.stdscr.addstr(h - 1, w-footer_string_width-1, " "*footer_string_width, curses.color_pair(self.colours_start+24))
|
|
69
|
+
# self.stdscr.addstr(h - 1, w-footer_string_width-1, f"{disp_string}", curses.color_pair(self.colours_start+24))
|
|
70
|
+
|
|
71
|
+
# disp_string = f"{footer_string:>{footer_string_width-1}} "
|
|
72
|
+
# self.stdscr.addstr(h - 1, w-footer_string_width-1, " "*footer_string_width, curses.color_pair(self.colours_start+24))
|
|
73
|
+
# self.stdscr.addstr(h - 1, w-footer_string_width-1, f"{disp_string}", curses.color_pair(self.colours_start+24))
|
|
74
|
+
|
|
75
|
+
footer_string_width = min(w-1, len(state["footer_string"])+2)
|
|
76
|
+
|
|
77
|
+
disp_string = f"{state["footer_string"][:footer_string_width]}"
|
|
78
|
+
disp_string = f" {disp_string:>{footer_string_width-2}} "
|
|
79
|
+
self.stdscr.addstr(h - 1, w-footer_string_width-1, " "*footer_string_width, curses.color_pair(self.colours_start+24))
|
|
80
|
+
self.stdscr.addstr(h - 1, w-footer_string_width-1, f"{disp_string}", curses.color_pair(self.colours_start+24))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
select_mode = "Cursor"
|
|
85
|
+
if state["is_selecting"]: select_mode = "Visual Selection"
|
|
86
|
+
elif state["is_deselecting"]: select_mode = "Visual deselection"
|
|
87
|
+
self.stdscr.addstr(h - 1, w-35, f"{select_mode:>33} ", curses.color_pair(self.colours_start+20))
|
|
88
|
+
|
|
89
|
+
# Cursor & selection info
|
|
90
|
+
selected_count = sum(state["selections"].values())
|
|
91
|
+
if state["paginate"]:
|
|
92
|
+
cursor_disp_str = f" {state['cursor_pos']+1}/{len(state['indexed_items'])} Page {state['cursor_pos']//state['items_per_page']}/{len(state['indexed_items'])} Selected {selected_count}"
|
|
93
|
+
else:
|
|
94
|
+
cursor_disp_str = f" {state['cursor_pos']+1}/{len(state['indexed_items'])} | Selected {selected_count}"
|
|
95
|
+
self.stdscr.addstr(h - 3, w-35, f"{cursor_disp_str:>33} ", curses.color_pair(self.colours_start+20))
|
|
96
|
+
|
|
97
|
+
self.stdscr.refresh()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class CompactFooter(Footer):
|
|
102
|
+
def __init__(self, stdscr, colours_start, get_state_function):
|
|
103
|
+
"""
|
|
104
|
+
stdscr: curses screen object
|
|
105
|
+
colours_start: base colour pair index
|
|
106
|
+
get_state_callback: function that returns a dict with all required data for rendering
|
|
107
|
+
"""
|
|
108
|
+
self.stdscr = stdscr
|
|
109
|
+
self.colours_start = colours_start
|
|
110
|
+
self.get_state = get_state_function
|
|
111
|
+
self.height = 1
|
|
112
|
+
|
|
113
|
+
def draw(self, h, w):
|
|
114
|
+
state = self.get_state()
|
|
115
|
+
|
|
116
|
+
# Fill background
|
|
117
|
+
if state["search_query"]: self.height = 3
|
|
118
|
+
elif state["filter_query"]: self.height = 2
|
|
119
|
+
elif state["user_opts"]: self.height = 1
|
|
120
|
+
elif state["footer_string"]: self.height = 2
|
|
121
|
+
else: self.height = 1
|
|
122
|
+
for i in range(self.height):
|
|
123
|
+
self.stdscr.addstr(h-(i+1), 0, ' '*(w-1), curses.color_pair(self.colours_start+20))
|
|
124
|
+
|
|
125
|
+
if state["user_opts"]:
|
|
126
|
+
self.stdscr.addstr(h - 1, 2, f" Opts: {state['user_opts']} "[:w-3], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
|
|
127
|
+
if state["filter_query"]:
|
|
128
|
+
self.stdscr.addstr(h - 2, 2, f" Filter: {state['filter_query']} "[:w-40], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
|
|
129
|
+
if state["search_query"]:
|
|
130
|
+
self.stdscr.addstr(h - 3, 2, f" Search: {state['search_query']} [{state['search_index']}/{state['search_count']}] "[:w-3], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
|
|
131
|
+
|
|
132
|
+
right_width = 40
|
|
133
|
+
# Sort info
|
|
134
|
+
sort_column_info = f"{state['sort_column'] if state['sort_column'] is not None else 'None'}"
|
|
135
|
+
sort_method_info = f"{state['SORT_METHODS'][state['columns_sort_method'][state['sort_column']]]}" if state['sort_column'] is not None else "NA"
|
|
136
|
+
sort_order_info = "Desc." if state["sort_reverse"][state['sort_column']] else "Asc."
|
|
137
|
+
sort_order_info = "▼" if state["sort_reverse"][state['sort_column']] else "▲"
|
|
138
|
+
sort_disp_str = f" ({sort_column_info}, {sort_method_info}, {sort_order_info}) "
|
|
139
|
+
# self.stdscr.addstr(h - 2, w-right_width, f"{sort_disp_str:>{right_width-1}}", curses.color_pair(self.colours_start+20))
|
|
140
|
+
|
|
141
|
+
if state["footer_string"]:
|
|
142
|
+
footer_string_width = min(w-1, len(state["footer_string"])+2)
|
|
143
|
+
|
|
144
|
+
disp_string = f"{state["footer_string"][:footer_string_width]}"
|
|
145
|
+
disp_string = f" {disp_string:>{footer_string_width-2}} "
|
|
146
|
+
self.stdscr.addstr(h - 1, w-footer_string_width-1, " "*footer_string_width, curses.color_pair(self.colours_start+24))
|
|
147
|
+
self.stdscr.addstr(h - 1, w-footer_string_width-1, f"{disp_string}", curses.color_pair(self.colours_start+24))
|
|
148
|
+
selected_count = sum(state["selections"].values())
|
|
149
|
+
if state["paginate"]:
|
|
150
|
+
cursor_disp_str = f" {state['cursor_pos']+1}/{len(state['indexed_items'])} Page {state['cursor_pos']//state['items_per_page']}/{len(state['indexed_items'])} Selected {selected_count}"
|
|
151
|
+
else:
|
|
152
|
+
cursor_disp_str = f"{sort_disp_str} [{selected_count}] {state['cursor_pos']+1}/{len(state['indexed_items'])}"
|
|
153
|
+
self.stdscr.addstr(h-2, w-right_width, f"{cursor_disp_str:>{right_width-2}}"[:right_width-1], curses.color_pair(self.colours_start+20))
|
|
154
|
+
else:
|
|
155
|
+
# Cursor & selection info
|
|
156
|
+
selected_count = sum(state["selections"].values())
|
|
157
|
+
if state["paginate"]:
|
|
158
|
+
cursor_disp_str = f" {state['cursor_pos']+1}/{len(state['indexed_items'])} Page {state['cursor_pos']//state['items_per_page']}/{len(state['indexed_items'])} Selected {selected_count}"
|
|
159
|
+
else:
|
|
160
|
+
cursor_disp_str = f"{sort_disp_str} [{selected_count}] {state['cursor_pos']+1}/{len(state['indexed_items'])}"
|
|
161
|
+
self.stdscr.addstr(h - 1, w-right_width, f"{cursor_disp_str:>{right_width-2}}"[:right_width-1], curses.color_pair(self.colours_start+20))
|
|
162
|
+
|
|
163
|
+
self.stdscr.refresh()
|
|
164
|
+
|
|
165
|
+
class NoFooter(Footer):
|
|
166
|
+
def __init__(self, stdscr, colours_start, get_state_function):
|
|
167
|
+
"""
|
|
168
|
+
stdscr: curses screen object
|
|
169
|
+
colours_start: base colour pair index
|
|
170
|
+
get_state_callback: function that returns a dict with all required data for rendering
|
|
171
|
+
"""
|
|
172
|
+
self.stdscr = stdscr
|
|
173
|
+
self.colours_start = colours_start
|
|
174
|
+
self.get_state = get_state_function
|
|
175
|
+
self.height = 0
|
|
176
|
+
def draw(self, h, w):
|
|
177
|
+
state = self.get_state()
|
|
178
|
+
|
|
179
|
+
if state["search_query"]: self.height = 3
|
|
180
|
+
elif state["filter_query"]: self.height = 2
|
|
181
|
+
elif state["user_opts"]: self.height = 1
|
|
182
|
+
elif state["footer_string"]: self.height = 1
|
|
183
|
+
else: self.height = 0
|
|
184
|
+
|
|
185
|
+
for i in range(self.height):
|
|
186
|
+
self.stdscr.addstr(h-(i+1), 0, ' '*(w-1), curses.color_pair(self.colours_start+20))
|
|
187
|
+
|
|
188
|
+
if state["user_opts"]:
|
|
189
|
+
self.stdscr.addstr(h - 1, 2, f" Opts: {state['user_opts']} "[:w-3], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
|
|
190
|
+
if state["filter_query"]:
|
|
191
|
+
self.stdscr.addstr(h - 2, 2, f" Filter: {state['filter_query']} "[:w-40], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
|
|
192
|
+
if state["search_query"]:
|
|
193
|
+
self.stdscr.addstr(h - 3, 2, f" Search: {state['search_query']} [{state['search_index']}/{state['search_count']}] "[:w-3], curses.color_pair(self.colours_start+20) | curses.A_BOLD)
|
|
194
|
+
self.height = 3
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if state["footer_string"]:
|
|
198
|
+
footer_string_width = min(w-1, len(state["footer_string"])+2)
|
|
199
|
+
disp_string = f"{state["footer_string"][:footer_string_width]}"
|
|
200
|
+
disp_string = f" {disp_string:>{footer_string_width-2}} "
|
|
201
|
+
self.stdscr.addstr(h - 1, w-footer_string_width-1, " "*footer_string_width, curses.color_pair(self.colours_start+24))
|
|
202
|
+
self.stdscr.addstr(h - 1, w-footer_string_width-1, f"{disp_string}", curses.color_pair(self.colours_start+24))
|
listpick/utils/utils.py
CHANGED
|
@@ -36,7 +36,7 @@ def clip_left(text, n):
|
|
|
36
36
|
width += char_width
|
|
37
37
|
return text # If the total width is less than n, return the full string
|
|
38
38
|
|
|
39
|
-
def truncate_to_display_width(text: str, max_column_width: int, centre=False) -> str:
|
|
39
|
+
def truncate_to_display_width(text: str, max_column_width: int, centre=False, unicode_char_width: bool = True) -> str:
|
|
40
40
|
"""
|
|
41
41
|
Truncate and/or pad text to max_column_width using wcwidth to ensure visual width is correct
|
|
42
42
|
with foreign character sets.
|
|
@@ -74,16 +74,16 @@ def evaluate_cell(cell:str) -> str:
|
|
|
74
74
|
return str(eval(cell[1:]))
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def format_row_full(row: list[str], hidden_columns:list = []) -> str:
|
|
77
|
+
def format_row_full(row: list[str], hidden_columns:list = [], unicode_char_width: bool = True) -> str:
|
|
78
78
|
""" Format list of strings as a tab-separated single string. No hidden columns. """
|
|
79
79
|
return '\t'.join(str(row[i]) for i in range(len(row)) if i not in hidden_columns)
|
|
80
80
|
|
|
81
|
-
def format_full_row(row:str) -> str:
|
|
81
|
+
def format_full_row(row:str, unicode_char_width: bool = True) -> str:
|
|
82
82
|
""" Format list of strings as a tab-separated single string. Includes hidden columns. """
|
|
83
83
|
return '\t'.join(row)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def format_row(row: list[str], hidden_columns: list, column_widths: list[int], separator: str, centre:bool=False) -> str:
|
|
86
|
+
def format_row(row: list[str], hidden_columns: list, column_widths: list[int], separator: str, centre:bool=False, unicode_char_width: bool = True) -> str:
|
|
87
87
|
""" Format list of strings as a single string. Requires separator string and the maximum width of the columns. """
|
|
88
88
|
row_str = ""
|
|
89
89
|
for i, cell in enumerate(row):
|
|
@@ -96,7 +96,7 @@ def format_row(row: list[str], hidden_columns: list, column_widths: list[int], s
|
|
|
96
96
|
return row_str
|
|
97
97
|
# return row_str.strip()
|
|
98
98
|
|
|
99
|
-
def get_column_widths(items: list[list[str]], header: list[str]=[], max_column_width:int=70, number_columns:bool=True, max_total_width=-1, separator = " ") -> list[int]:
|
|
99
|
+
def get_column_widths(items: list[list[str]], header: list[str]=[], max_column_width:int=70, number_columns:bool=True, max_total_width=-1, separator = " ", unicode_char_width: bool = True) -> list[int]:
|
|
100
100
|
""" Calculate maximum width of each column with clipping. """
|
|
101
101
|
if len(items) == 0: return [0]
|
|
102
102
|
assert len(items) > 0
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
listpick/__init__.py,sha256=ExXc97-bibodH--wlwpQivl0zCNR5D1hvpvrf7OBofU,154
|
|
2
2
|
listpick/__main__.py,sha256=wkCjDdqw093W27yWwnlC3nG_sMRKaIad7hHHWy0RBgY,193
|
|
3
|
-
listpick/listpick_app.py,sha256=
|
|
3
|
+
listpick/listpick_app.py,sha256=cAZXi69fk_1dcmWF36FfTgEptLyzLQRSdBYG8qCY9bA,165151
|
|
4
4
|
listpick/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
listpick/ui/build_help.py,sha256=_rVKKrX3HfFJtw-pyeNb2lQWbml4-AAw8sZIUYGn97Y,8731
|
|
6
6
|
listpick/ui/footer.py,sha256=s1L68MNmhWwbWRy0mn0ChmnE_dMQBAzNlTv917pyHE0,10673
|
|
7
|
+
listpick/ui/footer_1.py,sha256=Mpn0gFAnX_Ely-Jl5KPP6sdZRnk8makt8EVBh9wdr4Y,10710
|
|
7
8
|
listpick/ui/help_screen.py,sha256=zbfGIgb-IXtATpl4_Sx7nPbsnRXZ7eiMYlCKGS9EFmw,5608
|
|
8
9
|
listpick/ui/input_field.py,sha256=eyoWHoApdZybjfXcp7Eth7xwb-C-856ZVnq5j_Q3Ojs,30412
|
|
9
10
|
listpick/ui/keys.py,sha256=TzaadgBP_rC7jbp--RFJZDOkHd0EB4K1wToDTiVs6CI,13029
|
|
@@ -22,10 +23,10 @@ listpick/utils/search_and_filter_utils.py,sha256=XxGfkyDVXO9OAKcftPat8IReMTFIuTH
|
|
|
22
23
|
listpick/utils/searching.py,sha256=Xk5UIqamNHL2L90z3ACB_Giqdpi9iRKoAJ6pKaqaD7Q,3093
|
|
23
24
|
listpick/utils/sorting.py,sha256=WZZiVlVA3Zkcpwji3U5SNFlQ14zVEk3cZJtQirBkecQ,5329
|
|
24
25
|
listpick/utils/table_to_list_of_lists.py,sha256=T-i-nV1p6g8UagdgUPKrhIGpKY_YXZDxf4xZzcPepNA,7635
|
|
25
|
-
listpick/utils/utils.py,sha256=
|
|
26
|
-
listpick-0.1.13.
|
|
27
|
-
listpick-0.1.13.
|
|
28
|
-
listpick-0.1.13.
|
|
29
|
-
listpick-0.1.13.
|
|
30
|
-
listpick-0.1.13.
|
|
31
|
-
listpick-0.1.13.
|
|
26
|
+
listpick/utils/utils.py,sha256=_4HW0p1gnundsTJcKsSsIKCZxy3DtolbxjBpuYQyFBw,12948
|
|
27
|
+
listpick-0.1.13.59.dist-info/licenses/LICENSE.txt,sha256=2mP-MRHJptADDNE9VInMNg1tE-C6Qv93Z4CCQKrpg9w,1061
|
|
28
|
+
listpick-0.1.13.59.dist-info/METADATA,sha256=bD3N7PxFIQnVXf080zULv-TjMr9ia7RL8pLSZO2smYY,7988
|
|
29
|
+
listpick-0.1.13.59.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
30
|
+
listpick-0.1.13.59.dist-info/entry_points.txt,sha256=-QCf_BKIkUz35Y9nkYpjZWs2Qg0KfRna2PAs5DnF6BE,43
|
|
31
|
+
listpick-0.1.13.59.dist-info/top_level.txt,sha256=5mtsGEz86rz3qQDe0D463gGjAfSp6A3EWg4J4AGYr-Q,9
|
|
32
|
+
listpick-0.1.13.59.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|