visidata 2.11.1__py3-none-any.whl → 3.0.1__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.
Files changed (256) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +259 -42
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +21 -3
  5. visidata/_urlcache.py +17 -4
  6. visidata/aggregators.py +78 -25
  7. visidata/apps/__init__.py +0 -0
  8. visidata/apps/vdsql/__about__.py +8 -0
  9. visidata/apps/vdsql/__init__.py +5 -0
  10. visidata/apps/vdsql/__main__.py +27 -0
  11. visidata/apps/vdsql/_ibis.py +748 -0
  12. visidata/apps/vdsql/bigquery.py +61 -0
  13. visidata/apps/vdsql/clickhouse.py +53 -0
  14. visidata/apps/vdsql/setup.py +40 -0
  15. visidata/apps/vdsql/snowflake.py +67 -0
  16. visidata/apps/vgit/__init__.py +13 -0
  17. {vgit → visidata/apps/vgit}/blame.py +5 -2
  18. {vgit → visidata/apps/vgit}/branch.py +31 -16
  19. {vgit → visidata/apps/vgit}/config.py +3 -3
  20. visidata/apps/vgit/diff.py +169 -0
  21. visidata/apps/vgit/gitsheet.py +161 -0
  22. {vgit → visidata/apps/vgit}/grep.py +6 -5
  23. visidata/apps/vgit/log.py +81 -0
  24. {vgit → visidata/apps/vgit}/main.py +18 -5
  25. {vgit → visidata/apps/vgit}/remote.py +8 -4
  26. visidata/apps/vgit/repos.py +71 -0
  27. {vgit → visidata/apps/vgit}/setup.py +6 -4
  28. visidata/apps/vgit/stash.py +69 -0
  29. visidata/apps/vgit/status.py +204 -0
  30. {vgit → visidata/apps/vgit}/statusbar.py +2 -0
  31. visidata/basesheet.py +63 -51
  32. visidata/canvas.py +208 -93
  33. visidata/choose.py +6 -6
  34. visidata/clean_names.py +29 -0
  35. visidata/clipboard.py +73 -17
  36. visidata/cliptext.py +220 -46
  37. visidata/cmdlog.py +88 -114
  38. visidata/color.py +142 -56
  39. visidata/column.py +121 -129
  40. visidata/ddw/input.ddw +74 -79
  41. visidata/ddw/regex.ddw +57 -0
  42. visidata/ddwplay.py +33 -14
  43. visidata/deprecated.py +77 -3
  44. visidata/desktop/visidata.desktop +7 -0
  45. visidata/editor.py +12 -6
  46. visidata/errors.py +6 -2
  47. visidata/experimental/__init__.py +0 -0
  48. visidata/experimental/diff_sheet.py +29 -0
  49. visidata/experimental/digit_autoedit.py +6 -0
  50. visidata/experimental/gdrive.py +89 -0
  51. visidata/experimental/google.py +37 -0
  52. visidata/experimental/gsheets.py +79 -0
  53. visidata/experimental/live_search.py +37 -0
  54. visidata/experimental/liveupdate.py +45 -0
  55. visidata/experimental/mark.py +133 -0
  56. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  57. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  58. visidata/experimental/rownum.py +73 -0
  59. visidata/experimental/slide_cells.py +26 -0
  60. visidata/expr.py +8 -4
  61. visidata/extensible.py +22 -4
  62. visidata/features/__init__.py +0 -0
  63. visidata/features/addcol_audiometadata.py +42 -0
  64. visidata/features/addcol_histogram.py +34 -0
  65. visidata/features/canvas_save_svg.py +69 -0
  66. visidata/features/change_precision.py +46 -0
  67. visidata/features/cmdpalette.py +197 -0
  68. visidata/features/colorbrewer.py +363 -0
  69. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  70. visidata/features/command_server.py +105 -0
  71. visidata/features/currency_to_usd.py +70 -0
  72. visidata/{customdate.py → features/customdate.py} +2 -0
  73. visidata/features/dedupe.py +132 -0
  74. visidata/{describe.py → features/describe.py} +17 -15
  75. visidata/features/errors_guide.py +26 -0
  76. visidata/features/expand_cols.py +202 -0
  77. visidata/{fill.py → features/fill.py} +3 -1
  78. visidata/{freeze.py → features/freeze.py} +11 -6
  79. visidata/features/graph_seaborn.py +79 -0
  80. visidata/features/helloworld.py +10 -0
  81. visidata/features/hint_types.py +17 -0
  82. visidata/{incr.py → features/incr.py} +5 -0
  83. visidata/{join.py → features/join.py} +107 -53
  84. visidata/features/known_cols.py +21 -0
  85. visidata/features/layout.py +62 -0
  86. visidata/{melt.py → features/melt.py} +32 -21
  87. visidata/features/normcol.py +118 -0
  88. visidata/features/open_config.py +7 -0
  89. visidata/features/open_syspaste.py +18 -0
  90. visidata/features/ping.py +157 -0
  91. visidata/features/procmgr.py +208 -0
  92. visidata/features/random_sample.py +6 -0
  93. visidata/{regex.py → features/regex.py} +47 -31
  94. visidata/features/reload_every.py +55 -0
  95. visidata/features/rename_col_cascade.py +30 -0
  96. visidata/features/scroll_context.py +60 -0
  97. visidata/features/select_equal_selected.py +11 -0
  98. visidata/features/setcol_fake.py +65 -0
  99. visidata/{slide.py → features/slide.py} +77 -21
  100. visidata/features/sparkline.py +48 -0
  101. visidata/features/status_source.py +20 -0
  102. visidata/{sysedit.py → features/sysedit.py} +2 -1
  103. visidata/features/sysopen_mailcap.py +46 -0
  104. visidata/features/term_extras.py +13 -0
  105. visidata/{transpose.py → features/transpose.py} +5 -4
  106. visidata/features/type_ipaddr.py +73 -0
  107. visidata/features/type_url.py +11 -0
  108. visidata/{unfurl.py → features/unfurl.py} +9 -9
  109. visidata/{window.py → features/window.py} +2 -2
  110. visidata/form.py +50 -21
  111. visidata/freqtbl.py +81 -33
  112. visidata/fuzzymatch.py +414 -0
  113. visidata/graph.py +105 -33
  114. visidata/guide.py +200 -0
  115. visidata/help.py +75 -44
  116. visidata/hint.py +39 -0
  117. visidata/indexsheet.py +109 -0
  118. visidata/input_history.py +55 -0
  119. visidata/interface.py +58 -0
  120. visidata/keys.py +20 -16
  121. visidata/loaders/__init__.py +9 -0
  122. visidata/loaders/_pandas.py +61 -21
  123. visidata/loaders/api_airtable.py +70 -0
  124. visidata/loaders/api_bitio.py +102 -0
  125. visidata/loaders/api_matrix.py +148 -0
  126. visidata/loaders/api_reddit.py +306 -0
  127. visidata/loaders/api_zulip.py +249 -0
  128. visidata/loaders/archive.py +41 -7
  129. visidata/loaders/arrow.py +7 -7
  130. visidata/loaders/conll.py +49 -0
  131. visidata/loaders/csv.py +25 -7
  132. visidata/loaders/eml.py +3 -4
  133. visidata/loaders/f5log.py +1204 -0
  134. visidata/loaders/fec.py +325 -0
  135. visidata/loaders/fixed_width.py +2 -4
  136. visidata/loaders/frictionless.py +3 -3
  137. visidata/loaders/geojson.py +8 -5
  138. visidata/loaders/google.py +48 -0
  139. visidata/loaders/graphviz.py +4 -4
  140. visidata/loaders/hdf5.py +4 -4
  141. visidata/loaders/html.py +54 -12
  142. visidata/loaders/http.py +84 -30
  143. visidata/loaders/imap.py +20 -10
  144. visidata/loaders/jrnl.py +52 -0
  145. visidata/loaders/json.py +83 -29
  146. visidata/loaders/jsonla.py +74 -0
  147. visidata/loaders/lsv.py +15 -11
  148. visidata/loaders/mailbox.py +40 -0
  149. visidata/loaders/markdown.py +1 -3
  150. visidata/loaders/mbtiles.py +4 -5
  151. visidata/loaders/mysql.py +11 -13
  152. visidata/loaders/npy.py +7 -7
  153. visidata/loaders/odf.py +4 -1
  154. visidata/loaders/orgmode.py +428 -0
  155. visidata/loaders/pandas_freqtbl.py +14 -20
  156. visidata/loaders/parquet.py +62 -6
  157. visidata/loaders/pcap.py +3 -3
  158. visidata/loaders/pdf.py +4 -3
  159. visidata/loaders/png.py +19 -13
  160. visidata/loaders/postgres.py +9 -8
  161. visidata/loaders/rec.py +7 -3
  162. visidata/loaders/s3.py +342 -0
  163. visidata/loaders/sas.py +5 -5
  164. visidata/loaders/scrape.py +186 -0
  165. visidata/loaders/shp.py +6 -5
  166. visidata/loaders/spss.py +5 -6
  167. visidata/loaders/sqlite.py +68 -28
  168. visidata/loaders/texttables.py +1 -1
  169. visidata/loaders/toml.py +60 -0
  170. visidata/loaders/tsv.py +61 -19
  171. visidata/loaders/ttf.py +19 -7
  172. visidata/loaders/unzip_http.py +6 -5
  173. visidata/loaders/usv.py +1 -1
  174. visidata/loaders/vcf.py +16 -16
  175. visidata/loaders/vds.py +10 -7
  176. visidata/loaders/vdx.py +30 -5
  177. visidata/loaders/xlsb.py +8 -1
  178. visidata/loaders/xlsx.py +145 -25
  179. visidata/loaders/xml.py +6 -3
  180. visidata/loaders/xword.py +4 -4
  181. visidata/loaders/yaml.py +15 -5
  182. visidata/macros.py +129 -42
  183. visidata/main.py +119 -94
  184. visidata/mainloop.py +101 -155
  185. visidata/man/parse_options.py +2 -2
  186. visidata/man/vd.1 +302 -149
  187. visidata/man/vd.txt +291 -154
  188. visidata/memory.py +3 -3
  189. visidata/menu.py +104 -423
  190. visidata/metasheets.py +59 -141
  191. visidata/modify.py +78 -23
  192. visidata/motd.py +3 -3
  193. visidata/mouse.py +137 -0
  194. visidata/movement.py +43 -35
  195. visidata/optionssheet.py +99 -0
  196. visidata/path.py +113 -32
  197. visidata/pivot.py +73 -47
  198. visidata/plugins.py +65 -192
  199. visidata/pyobj.py +55 -205
  200. visidata/rename_col.py +20 -0
  201. visidata/save.py +37 -20
  202. visidata/search.py +54 -10
  203. visidata/selection.py +84 -5
  204. visidata/settings.py +162 -25
  205. visidata/sheets.py +239 -260
  206. visidata/shell.py +51 -21
  207. visidata/sidebar.py +162 -0
  208. visidata/sort.py +11 -4
  209. visidata/statusbar.py +114 -104
  210. visidata/stored_list.py +43 -0
  211. visidata/stored_prop.py +38 -0
  212. visidata/tests/benchmark.csv +52 -0
  213. visidata/tests/conftest.py +3 -3
  214. visidata/tests/test_cliptext.py +39 -0
  215. visidata/tests/test_commands.py +65 -7
  216. visidata/tests/test_edittext.py +2 -2
  217. visidata/tests/test_features.py +28 -0
  218. visidata/tests/test_menu.py +14 -0
  219. visidata/tests/test_path.py +13 -4
  220. visidata/text_source.py +53 -0
  221. visidata/textsheet.py +10 -3
  222. visidata/theme.py +44 -0
  223. visidata/themes/__init__.py +0 -0
  224. visidata/themes/ascii8.py +84 -0
  225. visidata/themes/asciimono.py +84 -0
  226. visidata/themes/light.py +17 -0
  227. visidata/threads.py +89 -40
  228. visidata/tuiwin.py +22 -0
  229. visidata/type_currency.py +22 -3
  230. visidata/type_date.py +31 -9
  231. visidata/type_floatsi.py +5 -1
  232. visidata/undo.py +17 -5
  233. visidata/utils.py +106 -23
  234. visidata/vdobj.py +28 -17
  235. visidata/windows.py +10 -0
  236. visidata/wrappers.py +9 -3
  237. visidata-3.0.1.data/data/share/applications/visidata.desktop +7 -0
  238. {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/vd.1 +302 -149
  239. {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/visidata.1 +302 -149
  240. visidata-3.0.1.data/scripts/vd2to3.vdx +9 -0
  241. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/METADATA +12 -8
  242. visidata-3.0.1.dist-info/RECORD +258 -0
  243. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/WHEEL +1 -1
  244. vgit/__init__.py +0 -1
  245. vgit/gitsheet.py +0 -164
  246. visidata/layout.py +0 -44
  247. visidata/misc.py +0 -5
  248. visidata-2.11.1.data/scripts/vgit +0 -9
  249. visidata-2.11.1.dist-info/RECORD +0 -155
  250. {vgit → visidata/apps/vgit}/__main__.py +0 -0
  251. {vgit → visidata/apps/vgit}/abort.py +0 -0
  252. /visidata/{repeat.py → features/repeat.py} +0 -0
  253. {visidata-2.11.1.data → visidata-3.0.1.data}/scripts/vd +0 -0
  254. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/LICENSE.gpl3 +0 -0
  255. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/entry_points.txt +0 -0
  256. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/top_level.txt +0 -0
visidata/sheets.py CHANGED
@@ -3,87 +3,35 @@ import itertools
3
3
  from copy import copy, deepcopy
4
4
  import textwrap
5
5
 
6
- from visidata import VisiData, Extensible, globalCommand, ColumnAttr, ColumnItem, vd, ENTER, EscapeException, drawcache, drawcache_property, LazyChainMap, asyncthread, ExpectedException, setitem
6
+ from visidata import VisiData, Extensible, globalCommand, ColumnAttr, ColumnItem, vd, ENTER, EscapeException, drawcache, drawcache_property, LazyChainMap, asyncthread, ExpectedException
7
7
  from visidata import (options, Column, namedlist, SettableColumn,
8
8
  TypedExceptionWrapper, BaseSheet, UNLOADED,
9
- clipdraw, ColorAttr, update_attr, colors, undoAttrFunc, vlen)
9
+ clipdraw, clipdraw_chunks, ColorAttr, update_attr, colors, undoAttrFunc, vlen, dispwidth)
10
10
  import visidata
11
11
 
12
12
 
13
13
  vd.activePane = 1 # pane numbering starts at 1; pane 0 means active pane
14
14
 
15
15
 
16
- __all__ = ['RowColorizer', 'CellColorizer', 'ColumnColorizer', 'Sheet', 'TableSheet', 'IndexSheet', 'SheetsSheet', 'LazyComputeRow', 'SequenceSheet']
17
-
18
-
19
- vd.option('default_width', 20, 'default column width', replay=True) # TODO: make not replay and remove from markdown saver
20
- vd.option('default_height', 4, 'default column height')
21
- vd.option('textwrap_cells', True, 'wordwrap text for multiline rows')
22
-
23
- vd.option('quitguard', False, 'confirm before quitting modified sheet')
24
- vd.option('debug', False, 'exit on error and display stacktrace')
25
- vd.option('skip', 0, 'skip N rows before header', replay=True)
26
- vd.option('header', 1, 'parse first N rows as column names', replay=True)
27
- vd.option('load_lazy', False, 'load subsheets always (False) or lazily (True)')
28
-
29
- vd.option('force_256_colors', False, 'use 256 colors even if curses reports fewer')
30
-
31
- vd.option('disp_note_none', '⌀', 'visible contents of a cell whose value is None')
32
- vd.option('disp_truncator', '…', 'indicator that the contents are only partially visible')
33
- vd.option('disp_oddspace', '\u00b7', 'displayable character for odd whitespace')
34
- vd.option('disp_more_left', '<', 'header note indicating more columns to the left')
35
- vd.option('disp_more_right', '>', 'header note indicating more columns to the right')
36
- vd.option('disp_error_val', '', 'displayed contents for computation exception')
37
- vd.option('disp_ambig_width', 1, 'width to use for unicode chars marked ambiguous')
38
-
39
- vd.option('disp_pending', '', 'string to display in pending cells')
40
- vd.option('note_pending', '⌛', 'note to display for pending cells')
41
- vd.option('note_format_exc', '?', 'cell note for an exception during formatting')
42
- vd.option('note_getter_exc', '!', 'cell note for an exception during computation')
43
- vd.option('note_type_exc', '!', 'cell note for an exception during type conversion')
44
-
45
- vd.option('color_note_pending', 'bold magenta', 'color of note in pending cells')
46
- vd.option('color_note_type', '226 yellow', 'color of cell note for non-str types in anytype columns')
47
- vd.option('color_note_row', '220 yellow', 'color of row note on left edge')
48
- vd.option('scroll_incr', -3, 'amount to scroll with scrollwheel')
49
- vd.option('disp_column_sep', '│', 'separator between columns')
50
- vd.option('disp_keycol_sep', '║', 'separator between key columns and rest of columns')
51
- vd.option('disp_rowtop_sep', '│', '') # ╷│┬╽⌜⌐▇
52
- vd.option('disp_rowmid_sep', '⁝', '') # ┃┊│█
53
- vd.option('disp_rowbot_sep', '⁝', '') # ┊┴╿⌞█⍿╵⎢┴⌊ ⋮⁝
54
- vd.option('disp_rowend_sep', '║', '') # ┊┴╿⌞█⍿╵⎢┴⌊
55
- vd.option('disp_keytop_sep', '║', '') # ╽╿┃╖╟
56
- vd.option('disp_keymid_sep', '║', '') # ╽╿┃
57
- vd.option('disp_keybot_sep', '║', '') # ╽╿┃╜‖
58
- vd.option('disp_endtop_sep', '║', '') # ╽╿┃╖╢
59
- vd.option('disp_endmid_sep', '║', '') # ╽╿┃
60
- vd.option('disp_endbot_sep', '║', '') # ╽╿┃╜‖
61
- vd.option('disp_selected_note', '•', '') #
62
- vd.option('disp_sort_asc', '↑↟⇞⇡⇧⇑', 'characters for ascending sort') # ↑▲↟↥↾↿⇞⇡⇧⇈⤉⤒⥔⥘⥜⥠⍏˄ˆ
63
- vd.option('disp_sort_desc', '↓↡⇟⇣⇩⇓', 'characters for descending sort') # ↓▼↡↧⇂⇃⇟⇣⇩⇊⤈⤓⥕⥙⥝⥡⍖˅ˇ
64
- vd.option('color_default', 'white on black', 'the default fg and bg colors')
65
- vd.option('color_default_hdr', 'bold', 'color of the column headers')
66
- vd.option('color_bottom_hdr', 'underline', 'color of the bottom header row')
67
- vd.option('color_current_row', 'reverse', 'color of the cursor row')
68
- vd.option('color_current_col', 'bold', 'color of the cursor column')
69
- vd.option('color_current_hdr', 'bold reverse', 'color of the header for the cursor column')
70
- vd.option('color_column_sep', '246 blue', 'color of column separators')
71
- vd.option('color_key_col', '81 cyan', 'color of key columns')
72
- vd.option('color_hidden_col', '8', 'color of hidden columns on metasheets')
73
- vd.option('color_selected_row', '215 yellow', 'color of selected rows')
74
- vd.option('name_joiner', '_', 'string to join sheet or column names')
75
- vd.option('value_joiner', ' ', 'string to join display values')
16
+ vd.option('name_joiner', '_', 'string to join sheet or column names', max_help=0)
17
+ vd.option('value_joiner', ' ', 'string to join display values', max_help=0)
76
18
 
77
19
 
78
- @VisiData.api
79
20
  @drawcache
80
- def splitcell(sheet, s, width=0):
21
+ def _splitcell(sheet, s, width=0, maxheight=1):
81
22
  if width <= 0 or not sheet.options.textwrap_cells:
82
23
  return [s]
83
24
 
84
25
  ret = []
85
- for L in s.splitlines():
86
- ret.extend(textwrap.wrap(L, width=width, break_long_words=False, replace_whitespace=False))
26
+ for attr, text in s:
27
+ for line in textwrap.wrap(
28
+ text, width=width, break_long_words=False, replace_whitespace=False
29
+ ):
30
+ if len(ret) >= maxheight:
31
+ ret[-1][0][1] += ' ' + line
32
+ break
33
+ else:
34
+ ret.append([[attr, line]])
87
35
  return ret
88
36
 
89
37
  disp_column_fill = ' ' # pad chars after column value
@@ -92,6 +40,7 @@ disp_column_fill = ' ' # pad chars after column value
92
40
  # coloropt is the color option name (like 'color_error')
93
41
  # func(sheet,col,row,value) should return a true value if coloropt should be applied
94
42
  # if coloropt is None, func() should return a coloropt (or None) instead
43
+ Colorizer = collections.namedtuple('Colorizer', 'precedence coloropt func')
95
44
  RowColorizer = collections.namedtuple('RowColorizer', 'precedence coloropt func')
96
45
  CellColorizer = collections.namedtuple('CellColorizer', 'precedence coloropt func')
97
46
  ColumnColorizer = collections.namedtuple('ColumnColorizer', 'precedence coloropt func')
@@ -116,8 +65,15 @@ class LazyComputeRow:
116
65
  lcmobj._lcm = LazyChainMap(self.sheet, self.col, *vd.contexts)
117
66
  return lcmobj._lcm
118
67
 
68
+ def __iter__(self):
69
+ yield from self.sheet._ordered_colnames
70
+ yield from self._lcm.keys()
71
+ yield 'row'
72
+ yield 'sheet'
73
+ yield 'col'
74
+
119
75
  def keys(self):
120
- return self.sheet._ordered_colnames + self._lcm.keys() + ['row', 'sheet', 'col']
76
+ return list(self.__iter__())
121
77
 
122
78
  def __str__(self):
123
79
  return str(self.as_dict())
@@ -169,6 +125,21 @@ class TableSheet(BaseSheet):
169
125
  _coltype = SettableColumn
170
126
 
171
127
  rowtype = 'rows'
128
+ guide = '# {sheet.help_title}\n{sheet.help_columns}\n'
129
+
130
+ @property
131
+ def help_title(self):
132
+ if isinstance(self.source, visidata.Path):
133
+ return 'Source Table'
134
+ else:
135
+ return 'Table Sheet'
136
+
137
+ @property
138
+ def help_columns(self):
139
+ hiddenCols = [c for c in self.columns if c.hidden]
140
+ if hiddenCols:
141
+ return f'- `gv` to unhide {len(hiddenCols)} hidden columns'
142
+ return ''
172
143
 
173
144
  columns = [] # list of Column
174
145
  colorizers = [ # list of Colorizer
@@ -176,14 +147,13 @@ class TableSheet(BaseSheet):
176
147
  ColumnColorizer(2, 'color_current_col', lambda s,c,r,v: c is s.cursorCol),
177
148
  ColumnColorizer(1, 'color_key_col', lambda s,c,r,v: c and c.keycol),
178
149
  CellColorizer(0, 'color_default', lambda s,c,r,v: True),
179
- RowColorizer(2, 'color_selected_row', lambda s,c,r,v: r is not None and s.isSelected(r)),
180
150
  RowColorizer(1, 'color_error', lambda s,c,r,v: isinstance(r, (Exception, TypedExceptionWrapper))),
151
+ CellColorizer(3, 'color_current_cell', lambda s,c,r,v: c is s.cursorCol and r is s.cursorRow)
181
152
  ]
182
153
  nKeys = 0 # columns[:nKeys] are key columns
183
154
 
184
- def __init__(self, *names, **kwargs):
185
- super().__init__(*names, **kwargs)
186
- self.rows = UNLOADED # list of opaque row objects (UNLOADED before first reload)
155
+ def __init__(self, *names, rows=UNLOADED, **kwargs):
156
+ super().__init__(*names, rows=rows, **kwargs)
187
157
  self.cursorRowIndex = 0 # absolute index of cursor into self.rows
188
158
  self.cursorVisibleColIndex = 0 # index of cursor into self.visibleCols
189
159
 
@@ -196,11 +166,13 @@ class TableSheet(BaseSheet):
196
166
  self._visibleColLayout = {} # [vcolidx] -> (x, w)
197
167
 
198
168
  # list of all columns in display order
199
- self.columns = kwargs.get('columns') or [copy(c) for c in self.columns] or [Column('_')]
200
- self._colorizers = []
169
+ self.initialCols = kwargs.pop('columns', None) or type(self).columns
170
+ self.resetCols()
171
+
172
+ self._colorizers = self.classColorizers
201
173
  self.recalc() # set .sheet on columns and start caches
202
174
 
203
- self.setKeys(self.columns[:self.nKeys]) # initial list of key columns
175
+ self._ordering = [] # list of (col:Column, reverse:bool)
204
176
 
205
177
  self.__dict__.update(kwargs) # also done earlier in BaseSheet.__init__
206
178
 
@@ -216,33 +188,30 @@ class TableSheet(BaseSheet):
216
188
  def addColorizer(self, c):
217
189
  'Add Colorizer *c* to the list of colorizers for this sheet.'
218
190
  self._colorizers.append(c)
191
+ self._colorizers = sorted(self._colorizers, key=lambda x: x.precedence, reverse=True)
219
192
 
220
193
  def removeColorizer(self, c):
221
194
  'Remove Colorizer *c* from the list of colorizers for this sheet.'
222
195
  self._colorizers.remove(c)
223
196
 
224
- @drawcache_property
225
- def allColorizers(self):
197
+ @property
198
+ def classColorizers(self) -> list:
199
+ 'List of all colorizers from sheet class hierarchy in precedence order (highest precedence first)'
226
200
  # all colorizers must be in the same bucket
227
201
  # otherwise, precedence does not get applied properly
228
202
  _colorizers = set()
229
- def allParents(cls):
230
- yield from cls.__bases__
231
- for b in cls.__bases__:
232
- yield from allParents(b)
233
203
 
234
- for b in [self] + list(allParents(self.__class__)):
204
+ for b in [self] + list(type(self).superclasses()):
235
205
  for c in getattr(b, 'colorizers', []):
236
206
  _colorizers.add(c)
237
207
 
238
- _colorizers |= set(self._colorizers)
239
208
  return sorted(_colorizers, key=lambda x: x.precedence, reverse=True)
240
209
 
241
210
  def _colorize(self, col, row, value=None) -> ColorAttr:
242
- 'Returns ColorAttr for the given colorizers/col/row/value'
211
+ 'Return ColorAttr for the given colorizers/col/row/value'
243
212
 
244
213
  colorstack = []
245
- for colorizer in self.allColorizers:
214
+ for colorizer in self._colorizers:
246
215
  try:
247
216
  r = colorizer.func(self, col, row, value)
248
217
  if r:
@@ -283,7 +252,33 @@ class TableSheet(BaseSheet):
283
252
 
284
253
  @asyncthread
285
254
  def reload(self):
286
- 'Load rows and/or columns from ``self.source``. Async. Override in subclass.'
255
+ 'Load or reload rows and columns from ``self.source``. Async. Override resetCols() or loader() in subclass.'
256
+ with visidata.ScopedSetattr(self, 'loading', True):
257
+ self.resetCols()
258
+ self.beforeLoad()
259
+ try:
260
+ self.loader()
261
+ vd.debug(f'finished loading {self}')
262
+ finally:
263
+ self.afterLoad()
264
+
265
+ self.recalc()
266
+
267
+ def beforeLoad(self):
268
+ pass
269
+
270
+ def resetCols(self):
271
+ 'Reset columns to class settings'
272
+ self.columns = []
273
+ for c in self.initialCols:
274
+ self.addColumn(deepcopy(c))
275
+ if self.options.disp_help > c.max_help:
276
+ c.hide()
277
+
278
+ self.setKeys(self.columns[:self.nKeys])
279
+
280
+ def loader(self):
281
+ 'Reset rows and sync load ``source`` via iterload. Overrideable.'
287
282
  self.rows = []
288
283
  try:
289
284
  with vd.Progress(gerund='loading', total=0):
@@ -292,15 +287,17 @@ class TableSheet(BaseSheet):
292
287
  except FileNotFoundError:
293
288
  return # let it be a blank sheet without error
294
289
 
295
- # if an ordering has been specified, sort the sheet
296
- if self._ordering:
297
- vd.sync(self.sort())
298
-
299
290
  def iterload(self):
300
291
  'Generate rows from ``self.source``. Override in subclass.'
301
292
  if False:
302
293
  yield vd.fail('no iterload for this loader yet')
303
294
 
295
+ def afterLoad(self):
296
+ 'hook for after loading has finished. Overrideable (be sure to call super).'
297
+ # if an ordering has been specified, sort the sheet
298
+ if self._ordering:
299
+ vd.sync(self.sort())
300
+
304
301
  def iterrows(self):
305
302
  if self.rows is UNLOADED:
306
303
  try:
@@ -324,10 +321,24 @@ class TableSheet(BaseSheet):
324
321
  'Copy sheet design but remain unloaded. Deepcopy columns so their attributes (width, type, name) may be adjusted independently of the original.'
325
322
  ret = super().__copy__()
326
323
  ret.rows = UNLOADED
327
- ret.columns = [copy(c) for c in self.keyCols]
328
- ret.setKeys(ret.columns)
329
- ret.columns.extend(copy(c) for c in self.columns if c not in self.keyCols)
330
- ret.recalc() # set .sheet on columns
324
+
325
+ ret.columns = []
326
+
327
+ col_mapping = {}
328
+ for c in self.columns:
329
+ new_col = copy(c)
330
+ col_mapping[c] = new_col
331
+ ret.addColumn(new_col)
332
+
333
+ ret.setKeys([col_mapping[c] for c in self.columns if c.keycol])
334
+
335
+ ret._ordering = []
336
+ for sortcol,reverse in self._ordering:
337
+ if isinstance(sortcol, str):
338
+ ret._ordering.append((sortcol,reverse))
339
+ else:
340
+ ret._ordering.append((col_mapping[sortcol],reverse))
341
+
331
342
  ret.topRowIndex = ret.cursorRowIndex = 0
332
343
  return ret
333
344
 
@@ -350,15 +361,18 @@ class TableSheet(BaseSheet):
350
361
  memo[id(self)] = ret
351
362
  return ret
352
363
 
353
- def __repr__(self):
364
+ def __str__(self):
354
365
  return self.name
355
366
 
367
+ def __repr__(self):
368
+ return f'<{type(self).__name__}: {self.name}>'
369
+
356
370
  def evalExpr(self, expr, row=None, col=None):
357
371
  if row is not None:
358
372
  # contexts are cached by sheet/rowid for duration of drawcycle
359
373
  contexts = vd._evalcontexts.setdefault((self, self.rowid(row), col), LazyComputeRow(self, row, col=col))
360
374
  else:
361
- contexts = None
375
+ contexts = dict(sheet=self)
362
376
 
363
377
  return eval(expr, vd.getGlobals(), contexts)
364
378
 
@@ -390,7 +404,8 @@ class TableSheet(BaseSheet):
390
404
  @property
391
405
  def cursorRow(self):
392
406
  'The row object at the row cursor.'
393
- return self.rows[self.cursorRowIndex] if self.nRows > 0 else None
407
+ idx = self.cursorRowIndex
408
+ return self.rows[idx] if self.nRows > idx else None
394
409
 
395
410
  @property
396
411
  def visibleRows(self): # onscreen rows
@@ -402,16 +417,6 @@ class TableSheet(BaseSheet):
402
417
  'List of non-hidden columns in display order.'
403
418
  return self.keyCols + [c for c in self.columns if not c.hidden and not c.keycol]
404
419
 
405
- def visibleColAtX(self, x):
406
- for vcolidx, (colx, w) in self._visibleColLayout.items():
407
- if colx <= x <= colx+w:
408
- return vcolidx
409
-
410
- def visibleRowAtY(self, y):
411
- for rowidx, (rowy, h) in self._rowLayout.items():
412
- if rowy <= y <= rowy+h-1:
413
- return rowidx
414
-
415
420
  @drawcache_property
416
421
  def keyCols(self):
417
422
  'List of visible key columns.'
@@ -430,7 +435,10 @@ class TableSheet(BaseSheet):
430
435
  @property
431
436
  def cursorColIndex(self):
432
437
  'Index of current column into `Sheet.columns`. Linear search; prefer `cursorCol` or `cursorVisibleColIndex`.'
433
- return self.columns.index(self.cursorCol)
438
+ try:
439
+ return self.columns.index(self.cursorCol)
440
+ except ValueError:
441
+ return None
434
442
 
435
443
  @property
436
444
  def nonKeyVisibleCols(self):
@@ -442,6 +450,13 @@ class TableSheet(BaseSheet):
442
450
  'String of key column names, for SheetsSheet convenience.'
443
451
  return ' '.join(c.name for c in self.keyCols)
444
452
 
453
+ @keyColNames.setter
454
+ def keyColNames(self, v): #2122
455
+ 'Set key columns on this sheet to the space-separated list of column names.'
456
+ newkeys = [self.column(colname) for colname in v.split()]
457
+ self.unsetKeys(self.keyCols)
458
+ self.setKeys(newkeys)
459
+
445
460
  @property
446
461
  def cursorCell(self):
447
462
  'Displayed value (DisplayWrapper) at current row and column.'
@@ -449,7 +464,7 @@ class TableSheet(BaseSheet):
449
464
 
450
465
  @property
451
466
  def cursorDisplay(self):
452
- 'Displayed value (DisplayWrapper.display) at current row and column.'
467
+ 'Displayed value (DisplayWrapper.text) at current row and column.'
453
468
  return self.cursorCol.getDisplayValue(self.cursorRow)
454
469
 
455
470
  @property
@@ -506,12 +521,10 @@ class TableSheet(BaseSheet):
506
521
 
507
522
  if index is not None:
508
523
  self.setModified()
509
- else:
510
- for col in cols:
511
- col.defer = self.defer
512
524
 
513
525
  for i, col in enumerate(cols):
514
526
  col.name = self.maybeClean(col.name)
527
+ col.defer = self.defer
515
528
 
516
529
  vd.addUndo(self.columns.remove, col)
517
530
  idx = len(self.columns) if index is None else index
@@ -607,7 +620,7 @@ class TableSheet(BaseSheet):
607
620
  break
608
621
  mincolidx, maxcolidx = min(self._visibleColLayout.keys()), max(self._visibleColLayout.keys())
609
622
  if self.cursorVisibleColIndex < mincolidx:
610
- self.leftVisibleColIndex -= max((self.cursorVisibleColIndex - mincolid)//2, 1)
623
+ self.leftVisibleColIndex -= max((self.cursorVisibleColIndex - mincolidx)//2, 1)
611
624
  continue
612
625
  elif self.cursorVisibleColIndex > maxcolidx:
613
626
  self.leftVisibleColIndex += max((maxcolidx - self.cursorVisibleColIndex)//2, 1)
@@ -620,16 +633,25 @@ class TableSheet(BaseSheet):
620
633
 
621
634
  def calcColLayout(self):
622
635
  'Set right-most visible column, based on calculation.'
623
- minColWidth = len(self.options.disp_more_left)+len(self.options.disp_more_right)+2
624
- sepColWidth = len(self.options.disp_column_sep)
636
+ minColWidth = dispwidth(self.options.disp_more_left)+dispwidth(self.options.disp_more_right)+2
637
+ sepColWidth = dispwidth(self.options.disp_column_sep)
625
638
  winWidth = self.windowWidth
626
639
  self._visibleColLayout = {}
627
640
  x = 0
628
641
  vcolidx = 0
629
642
  for vcolidx in range(0, self.nVisibleCols):
643
+ width = self.calcSingleColLayout(vcolidx, x, minColWidth)
644
+ if width:
645
+ x += width+sepColWidth
646
+ if x > winWidth-1:
647
+ break
648
+
649
+ self.rightVisibleColIndex = vcolidx
650
+
651
+ def calcSingleColLayout(self, vcolidx:int, x:int=0, minColWidth:int=4):
630
652
  col = self.visibleCols[vcolidx]
631
653
  if col.width is None and len(self.visibleRows) > 0:
632
- vrows = self.visibleRows if self.nRows > 1000 else self.rows
654
+ vrows = self.visibleRows if self.nRows > 1000 else self.rows[:1000] #1964
633
655
  # handle delayed column width-finding
634
656
  col.width = max(col.getMaxWidth(vrows), minColWidth)
635
657
  if vcolidx != self.nVisibleCols-1: # let last column fill up the max width
@@ -638,12 +660,9 @@ class TableSheet(BaseSheet):
638
660
  if col in self.keyCols:
639
661
  width = max(width, 1) # keycols must all be visible
640
662
  if col in self.keyCols or vcolidx >= self.leftVisibleColIndex: # visible columns
641
- self._visibleColLayout[vcolidx] = [x, min(width, winWidth-x)]
642
- x += width+sepColWidth
643
- if x > winWidth-1:
644
- break
663
+ self._visibleColLayout[vcolidx] = [x, min(width, self.windowWidth-x)]
664
+ return width
645
665
 
646
- self.rightVisibleColIndex = vcolidx
647
666
 
648
667
  def drawColHeader(self, scr, y, h, vcolidx):
649
668
  'Compose and draw column header for given vcolidx.'
@@ -670,24 +689,23 @@ class TableSheet(BaseSheet):
670
689
 
671
690
  hdrs = col.name.split('\n')
672
691
  for i in range(h):
673
- name = ' ' # save room at front for LeftMore or sorted arrow
692
+ name = ''
693
+ if colwidth > 2:
694
+ name = ' ' # save room at front for LeftMore or sorted arrow
674
695
 
675
696
  if h-i-1 < len(hdrs):
676
697
  name += hdrs[::-1][h-i-1]
677
698
 
678
- if len(name) > colwidth-1:
679
- name = name[:colwidth-len(self.options.disp_truncator)] + self.options.disp_truncator
680
-
681
699
  if i == h-1:
682
700
  hdrcattr = update_attr(hdrcattr, colors.color_bottom_hdr, 5)
683
701
 
684
- clipdraw(scr, y+i, x, name, hdrcattr.attr, colwidth)
685
- vd.onMouse(scr, y+i, x, 1, colwidth, BUTTON3_RELEASED='rename-col')
702
+ clipdraw(scr, y+i, x, name, hdrcattr, w=colwidth)
703
+ vd.onMouse(scr, x, y+i, colwidth, 1, BUTTON3_RELEASED='rename-col')
686
704
 
687
705
  if C and x+colwidth+len(C) < self.windowWidth and y+i < self.windowWidth:
688
706
  scr.addstr(y+i, x+colwidth, C, sepcattr.attr)
689
707
 
690
- clipdraw(scr, y+h-1, x+colwidth-len(T), T, hdrcattr.attr)
708
+ clipdraw(scr, y+h-1, x+colwidth-len(T), T, hdrcattr)
691
709
 
692
710
  try:
693
711
  if vcolidx == self.leftVisibleColIndex and col not in self.keyCols and self.nonKeyVisibleCols.index(col) > 0:
@@ -699,6 +717,8 @@ class TableSheet(BaseSheet):
699
717
  try:
700
718
  A = ''
701
719
  for j, (sortcol, sortdir) in enumerate(self._ordering):
720
+ if isinstance(sortcol, str):
721
+ sortcol = self.column(sortcol)
702
722
  if col is sortcol:
703
723
  A = self.options.disp_sort_desc[j] if sortdir else self.options.disp_sort_asc[j]
704
724
  scr.addstr(y+h-1, x, A, hdrcattr.attr)
@@ -713,10 +733,7 @@ class TableSheet(BaseSheet):
713
733
  def draw(self, scr):
714
734
  'Draw entire screen onto the `scr` curses object.'
715
735
  if not self.columns:
716
- if self.options.debug:
717
- self.addColumn(Column())
718
- else:
719
- return
736
+ return
720
737
 
721
738
  drawparams = {
722
739
  'isNull': self.isNullFunc(),
@@ -751,7 +768,7 @@ class TableSheet(BaseSheet):
751
768
  y = headerRow + numHeaderRows
752
769
 
753
770
  rows = self.rows[self.topRowIndex:min(self.topRowIndex+self.nScreenRows+1, self.nRows)]
754
- self.checkCursorNoExceptions()
771
+ vd.callNoExceptions(self.checkCursor)
755
772
 
756
773
  for rowidx, row in enumerate(rows):
757
774
  if y >= self.windowHeight-1:
@@ -762,9 +779,10 @@ class TableSheet(BaseSheet):
762
779
  y += self.drawRow(scr, row, self.topRowIndex+rowidx, y, rowcattr, maxheight=self.windowHeight-y-1, **drawparams)
763
780
 
764
781
  if vcolidx+1 < self.nVisibleCols:
765
- scr.addstr(headerRow, self.windowWidth-2, self.options.disp_more_right, colors.color_column_sep)
782
+ scr.addstr(headerRow, self.windowWidth-2, self.options.disp_more_right, colors.color_column_sep.attr)
766
783
 
767
- def calc_height(self, row, displines=None, isNull=None):
784
+ def calc_height(self, row, displines=None, isNull=None, maxheight=1):
785
+ 'render cell contents ifor row into displines'
768
786
  if displines is None:
769
787
  displines = {} # [vcolidx] -> list of lines in that cell
770
788
 
@@ -775,8 +793,8 @@ class TableSheet(BaseSheet):
775
793
  continue
776
794
  col = vcols[vcolidx]
777
795
  cellval = col.getCell(row)
778
- if colwidth > 1 and vd.isNumeric(col):
779
- cellval.display = cellval.display.rjust(colwidth-2)
796
+
797
+ cellval.display = col.display(cellval, colwidth)
780
798
 
781
799
  try:
782
800
  if isNull and isNull(cellval.value):
@@ -785,13 +803,15 @@ class TableSheet(BaseSheet):
785
803
  except (TypeError, ValueError):
786
804
  pass
787
805
 
788
- if col.voffset or col.height > 1:
789
- lines = splitcell(self, cellval.display, width=colwidth-2)
806
+ if maxheight > 1:
807
+ lines = _splitcell(self, cellval.display, width=colwidth-2, maxheight=maxheight)
790
808
  else:
791
809
  lines = [cellval.display]
792
810
  displines[vcolidx] = (col, cellval, lines)
793
811
 
794
- return self.rowHeight
812
+ if len(displines) == 0:
813
+ return 0
814
+ return max(len(lines) for _, _, lines in displines.values())
795
815
 
796
816
  def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight,
797
817
  isNull='',
@@ -820,8 +840,11 @@ class TableSheet(BaseSheet):
820
840
  else:
821
841
  basecellcattr = rowcattr
822
842
 
843
+ # calc_height renders cell contents into displines
823
844
  displines = {} # [vcolidx] -> list of lines in that cell
824
- height = min(self.calc_height(row, displines), maxheight) or 1 # display even empty rows
845
+ self.calc_height(row, displines, maxheight=self.rowHeight)
846
+
847
+ height = min(self.rowHeight, maxheight) or 1 # display even empty rows
825
848
  self._rowLayout[rowidx] = (ybase, height)
826
849
 
827
850
  for vcolidx, (col, cellval, lines) in displines.items():
@@ -838,26 +861,19 @@ class TableSheet(BaseSheet):
838
861
  notewidth = 1 if note else 0
839
862
  if note:
840
863
  notecattr = update_attr(cattr, colors.get_color(cellval.notecolor), 10)
841
- clipdraw(scr, ybase, x+colwidth-notewidth, note, notecattr.attr)
842
-
843
- if voffset >= 0:
844
- if len(lines)-voffset > height:
845
- # last line should always include as much as possible
846
- firstn = sum(len(i)+1 for i in lines[:voffset+height-1])
847
- lines = lines[:voffset+height]
848
- lines[-1] = cellval.display[firstn:][:col.width]
864
+ scr.addstr(ybase, x+colwidth-notewidth, note, notecattr.attr)
849
865
 
850
866
  lines = lines[voffset:]
851
867
 
852
868
  if len(lines) > height:
853
869
  lines = lines[:height]
854
870
  elif len(lines) < height:
855
- lines.extend(['']*(height-len(lines)))
871
+ lines.extend([[('', '')]]*(height-len(lines)))
856
872
 
857
- for i, line in enumerate(lines):
873
+ for i, chunks in enumerate(lines):
858
874
  y = ybase+i
859
875
 
860
- if vcolidx == self.rightVisibleColIndex: # right edge of sheet
876
+ if vcolidx == self.nVisibleCols-1: # right edge of sheet
861
877
  if len(lines) == 1:
862
878
  sepchars = endsep
863
879
  else:
@@ -889,8 +905,15 @@ class TableSheet(BaseSheet):
889
905
  sepchars = midsep
890
906
 
891
907
  pre = disp_truncator if hoffset != 0 else disp_column_fill
892
- clipdraw(scr, y, x, (pre if colwidth > 2 else '')+line[hoffset:], cattr.attr, w=colwidth-notewidth)
893
- vd.onMouse(scr, y, x, 1, colwidth, BUTTON3_RELEASED='edit-cell')
908
+ prechunks = []
909
+ if colwidth > 2:
910
+ prechunks.append(('', pre))
911
+
912
+ for attr, text in chunks:
913
+ prechunks.append((attr, text[hoffset:]))
914
+
915
+ clipdraw_chunks(scr, y, x, prechunks, cattr, w=colwidth-notewidth)
916
+ vd.onMouse(scr, x, y, colwidth, 1, BUTTON3_RELEASED='edit-cell')
894
917
 
895
918
  if x+colwidth+len(sepchars) <= self.windowWidth:
896
919
  scr.addstr(y, x+colwidth, sepchars, sepcattr.attr)
@@ -904,7 +927,7 @@ class TableSheet(BaseSheet):
904
927
  return height
905
928
 
906
929
  vd.rowNoters = [
907
- lambda sheet, row: sheet.isSelected(row) and sheet.options.disp_selected_note,
930
+ # f(sheet, row) -> character to be displayed on the left side of row
908
931
  ]
909
932
 
910
933
  Sheet = TableSheet # deprecated in 2.0 but still widely used internally
@@ -914,6 +937,7 @@ class SequenceSheet(Sheet):
914
937
  'Sheets with ``ColumnItem`` columns, and rows that are Python sequences (list, namedtuple, etc).'
915
938
  def setCols(self, headerrows):
916
939
  self.columns = []
940
+ vd.clearCaches() #1997
917
941
  for i, colnamelines in enumerate(itertools.zip_longest(*headerrows, fillvalue='')):
918
942
  colnamelines = ['' if c is None else c for c in colnamelines]
919
943
  self.addColumn(ColumnItem(''.join(map(str, colnamelines)), i))
@@ -939,8 +963,7 @@ class SequenceSheet(Sheet):
939
963
  except StopIteration:
940
964
  break
941
965
 
942
- @asyncthread
943
- def reload(self):
966
+ def loader(self):
944
967
  'Skip first options.skip rows; set columns from next options.header rows.'
945
968
 
946
969
  itsource = self.iterload()
@@ -956,72 +979,6 @@ class SequenceSheet(Sheet):
956
979
  for r in vd.Progress(itsource, gerund='loading', total=0):
957
980
  self.addRow(r)
958
981
 
959
- # if an ordering has been specified, sort the sheet
960
- if self._ordering:
961
- vd.sync(self.sort())
962
-
963
-
964
- class IndexSheet(Sheet):
965
- 'Base class for tabular sheets with rows that are Sheets.'
966
- rowtype = 'sheets' # rowdef: Sheet
967
-
968
- columns = [
969
- Column('name', getter=lambda c,r: r.names[-1], setter=lambda c,r,v: setitem(r.names, -1, v)),
970
- ColumnAttr('rows', 'nRows', type=int, width=9),
971
- ColumnAttr('cols', 'nCols', type=int),
972
- ColumnAttr('keys', 'keyColNames'),
973
- ColumnAttr('source'),
974
- ]
975
- nKeys = 1
976
-
977
- def newRow(self):
978
- return Sheet('', columns=[ColumnItem('', 0)], rows=[])
979
-
980
- def openRow(self, row):
981
- return row # rowdef is Sheet
982
-
983
- def getSheet(self, k):
984
- for vs in self.rows:
985
- if vs.name == k:
986
- return vs
987
-
988
- def addRow(self, sheet, **kwargs):
989
- super().addRow(sheet, **kwargs)
990
- if not self.options.load_lazy and not sheet.options.load_lazy:
991
- sheet.ensureLoaded()
992
-
993
- @asyncthread
994
- def reloadSheets(self, sheets):
995
- for vs in vd.Progress(sheets):
996
- vs.reload()
997
-
998
-
999
- class SheetsSheet(IndexSheet):
1000
- columns = [
1001
- ColumnAttr('name'),
1002
- ColumnAttr('type', '__class__.__name__'),
1003
- ColumnAttr('pane', type=int),
1004
- Column('shortcut', getter=lambda c,r: getattr(r, 'shortcut'), setter=lambda c,r,v: setattr(r, '_shortcut', v)),
1005
- ColumnAttr('nRows', type=int),
1006
- ColumnAttr('nCols', type=int),
1007
- ColumnAttr('nVisibleCols', type=int),
1008
- ColumnAttr('cursorDisplay'),
1009
- ColumnAttr('keyColNames'),
1010
- ColumnAttr('source'),
1011
- ColumnAttr('progressPct'),
1012
- # ColumnAttr('threads', 'currentThreads', type=vlen),
1013
- ]
1014
- precious = False
1015
- nKeys = 1
1016
- def reload(self):
1017
- self.rows = self.source
1018
-
1019
- def sort(self):
1020
- self.rows[1:] = sorted(self.rows[1:], key=self.sortkey)
1021
-
1022
- class GlobalSheetsSheet(SheetsSheet): #1620
1023
- def sort(self):
1024
- IndexSheet.sort(self)
1025
982
 
1026
983
  @VisiData.property
1027
984
  @drawcache
@@ -1077,15 +1034,6 @@ def push(vd, vs, pane=0, load=True):
1077
1034
  vs.ensureLoaded()
1078
1035
 
1079
1036
 
1080
- @VisiData.lazy_property
1081
- def allSheetsSheet(vd):
1082
- return GlobalSheetsSheet("sheets_all", source=vd.allSheets)
1083
-
1084
- @VisiData.lazy_property
1085
- def sheetsSheet(vd):
1086
- return SheetsSheet("sheets", source=vd.sheets)
1087
-
1088
-
1089
1037
  @VisiData.api
1090
1038
  def quit(vd, *sheets):
1091
1039
  'Remove *sheets* from sheets stack, asking for confirmation if needed.'
@@ -1098,10 +1046,10 @@ def quit(vd, *sheets):
1098
1046
 
1099
1047
  @BaseSheet.api
1100
1048
  def confirmQuit(vs, verb='quit'):
1101
- if vs.options.quitguard and vs.precious and vs.hasBeenModified:
1049
+ if vs.options.quitguard and vs.precious and vs.hasBeenModified and not vd._nextCommands:
1102
1050
  vd.draw_all()
1103
1051
  vd.confirm(f'{verb} modified sheet "{vs.name}"? ')
1104
- elif vs.options.getonly('quitguard', vs, False): # if this sheet is specifically guarded
1052
+ elif vs.options.getonly('quitguard', vs, False) and not vd._nextCommands: # if this sheet is specifically guarded
1105
1053
  vd.draw_all()
1106
1054
  vd.confirm(f'{verb} guarded sheet "{vs.name}"? ')
1107
1055
 
@@ -1126,7 +1074,9 @@ def quitAndReleaseMemory(vs):
1126
1074
  vs.source.lines.clear() # clear cache of read lines
1127
1075
 
1128
1076
  if vs.precious: # only precious sheets have meaningful data
1077
+ vs.confirmQuit('quit')
1129
1078
  vs.rows.clear()
1079
+ vs.rows = UNLOADED
1130
1080
  vd.remove(vs)
1131
1081
  vd.allSheets.remove(vs)
1132
1082
 
@@ -1161,18 +1111,12 @@ def async_deepcopy(sheet, rowlist):
1161
1111
  return ret
1162
1112
 
1163
1113
 
1164
- IndexSheet.options.header = 0
1165
- IndexSheet.options.skip = 0
1166
1114
 
1167
1115
  BaseSheet.init('pane', lambda: 1)
1168
1116
 
1169
- Sheet.init('_ordering', list, copy=True) # (col:Column, reverse:bool)
1170
1117
 
1171
- globalCommand('S', 'sheets-stack', 'vd.push(vd.sheetsSheet)', 'open Sheets Stack: join or jump between the active sheets on the current stack')
1172
- globalCommand('gS', 'sheets-all', 'vd.push(vd.allSheetsSheet)', 'open Sheets Sheet: join or jump between all sheets from current session')
1173
-
1174
- BaseSheet.addCommand('^R', 'reload-sheet', 'preloadHook(); reload(); recalc(); status("reloaded")', 'Reload current sheet'),
1175
- Sheet.addCommand('^G', 'show-cursor', 'status(statusLine)', 'show cursor position and bounds of current sheet on status line'),
1118
+ BaseSheet.addCommand('^R', 'reload-sheet', 'preloadHook(); reload()', 'Reload current sheet')
1119
+ Sheet.addCommand('^G', 'show-cursor', 'status(statusLine)', 'show cursor position and bounds of current sheet on status line')
1176
1120
 
1177
1121
  Sheet.addCommand('!', 'key-col', 'toggleKeys([cursorCol])', 'toggle current column as a key column')
1178
1122
  Sheet.addCommand('z!', 'key-col-off', 'unsetKeys([cursorCol])', 'unset current column as a key column')
@@ -1180,10 +1124,10 @@ Sheet.addCommand('z!', 'key-col-off', 'unsetKeys([cursorCol])', 'unset current c
1180
1124
  Sheet.addCommand('e', 'edit-cell', 'cursorCol.setValues([cursorRow], editCell(cursorVisibleColIndex)) if not (cursorRow is None) else fail("no rows to edit")', 'edit contents of current cell')
1181
1125
  Sheet.addCommand('ge', 'setcol-input', 'cursorCol.setValuesTyped(selectedRows, input("set selected to: ", value=cursorDisplay))', 'set contents of current column for selected rows to same input')
1182
1126
 
1183
- Sheet.addCommand('"', 'dup-selected', 'vs=copy(sheet); vs.name += "_selectedref"; vs.reload=lambda vs=vs,rows=selectedRows: setattr(vs, "rows", list(rows)); vd.push(vs)', 'open duplicate sheet with only selected rows'),
1184
- Sheet.addCommand('g"', 'dup-rows', 'vs=copy(sheet); vs.name+="_copy"; vs.rows=list(rows); status("copied "+vs.name); vs.select(selectedRows); vd.push(vs)', 'open duplicate sheet with all rows'),
1185
- Sheet.addCommand('z"', 'dup-selected-deep', 'vs = deepcopy(sheet); vs.name += "_selecteddeepcopy"; vs.rows = vs.async_deepcopy(selectedRows); vd.push(vs); status("pushed sheet with async deepcopy of selected rows")', 'open duplicate sheet with deepcopy of selected rows'),
1186
- Sheet.addCommand('gz"', 'dup-rows-deep', 'vs = deepcopy(sheet); vs.name += "_deepcopy"; vs.rows = vs.async_deepcopy(rows); vd.push(vs); status("pushed sheet with async deepcopy of all rows")', 'open duplicate sheet with deepcopy of all rows'),
1127
+ Sheet.addCommand('"', 'dup-selected', 'vs=copy(sheet); vs.name += "_selectedref"; vs.reload=lambda vs=vs,rows=selectedRows: setattr(vs, "rows", list(rows)); vd.push(vs)', 'open a duplicate sheet with only the selected rows')
1128
+ Sheet.addCommand('g"', 'dup-rows', 'vs=copy(sheet); vs.name+="_copy"; vs.rows=list(rows); status("copied "+vs.name); vs.select(selectedRows); vd.push(vs)', 'open a duplicate sheet with all rows')
1129
+ Sheet.addCommand('z"', 'dup-selected-deep', 'vs = deepcopy(sheet); vs.name += "_selecteddeepcopy"; vs.rows = vs.async_deepcopy(selectedRows); vd.push(vs); status("pushed sheet with async deepcopy of selected rows")', 'open duplicate sheet with deepcopy of selected rows')
1130
+ Sheet.addCommand('gz"', 'dup-rows-deep', 'vs = deepcopy(sheet); vs.name += "_deepcopy"; vs.rows = vs.async_deepcopy(rows); vd.push(vs); status("pushed sheet with async deepcopy of all rows")', 'open duplicate sheet with deepcopy of all rows')
1187
1131
 
1188
1132
  Sheet.addCommand('z~', 'type-any', 'cursorCol.type = anytype', 'set type of current column to anytype')
1189
1133
  Sheet.addCommand('~', 'type-string', 'cursorCol.type = str', 'set type of current column to str')
@@ -1192,14 +1136,6 @@ Sheet.addCommand('z#', 'type-len', 'cursorCol.type = vlen', 'set type of current
1192
1136
  Sheet.addCommand('%', 'type-float', 'cursorCol.type = float', 'set type of current column to float')
1193
1137
  Sheet.addCommand('', 'type-floatlocale', 'cursorCol.type = floatlocale', 'set type of current column to float using system locale set in LC_NUMERIC')
1194
1138
 
1195
- # when diving into a sheet, remove the index unless it is precious
1196
- IndexSheet.addCommand('g^R', 'reload-selected', 'reloadSheets(selectedRows or rows)', 'reload all selected sheets')
1197
- SheetsSheet.addCommand('gC', 'columns-selected', 'vd.push(ColumnsSheet("all_columns", source=selectedRows))', 'open Columns Sheet with all visible columns from selected sheets')
1198
- SheetsSheet.addCommand('gI', 'describe-selected', 'vd.push(DescribeSheet("describe_all", source=selectedRows))', 'open Describe Sheet with all visible columns from selected sheets')
1199
- SheetsSheet.addCommand('z^C', 'cancel-row', 'cancelThread(*cursorRow.currentThreads)', 'abort async thread for current sheet')
1200
- SheetsSheet.addCommand('gz^C', 'cancel-rows', 'for vs in selectedRows: cancelThread(*vs.currentThreads)', 'abort async threads for selected sheets')
1201
- SheetsSheet.addCommand(ENTER, 'open-row', 'dest=cursorRow; vd.sheets.remove(sheet) if not sheet.precious else None; vd.push(openRow(dest))', 'open sheet referenced in current row')
1202
-
1203
1139
  BaseSheet.addCommand('q', 'quit-sheet', 'vd.quit(sheet)', 'quit current sheet')
1204
1140
  BaseSheet.addCommand('Q', 'quit-sheet-free', 'quitAndReleaseMemory()', 'discard current sheet and free memory')
1205
1141
  globalCommand('gq', 'quit-all', 'vd.quit(*vd.sheets)', 'quit all sheets (clean exit)')
@@ -1210,20 +1146,19 @@ BaseSheet.addCommand('^I', 'splitwin-swap', 'vd.activePane = 1 if sheet.pane ==
1210
1146
  BaseSheet.addCommand('g^I', 'splitwin-swap-pane', 'vd.options.disp_splitwin_pct=-vd.options.disp_splitwin_pct', 'swap panes onscreen')
1211
1147
  BaseSheet.addCommand('zZ', 'splitwin-input', 'vd.options.disp_splitwin_pct = input("% height for split window: ", value=vd.options.disp_splitwin_pct)', 'set split pane to specific size')
1212
1148
 
1213
- BaseSheet.addCommand('^L', 'redraw', 'vd.redraw(); sheet.refresh()', 'Refresh screen')
1149
+ BaseSheet.addCommand('^L', 'redraw', 'sheet.refresh(); vd.redraw()', 'Refresh screen')
1214
1150
  BaseSheet.addCommand(None, 'guard-sheet', 'options.set("quitguard", True, sheet); status("guarded")', 'Set quitguard on current sheet to confirm before quit')
1215
1151
  BaseSheet.addCommand(None, 'guard-sheet-off', 'options.set("quitguard", False, sheet); status("unguarded")', 'Unset quitguard on current sheet to not confirm before quit')
1216
- BaseSheet.addCommand(None, 'open-source', 'vd.push(source)', 'jump to the source of this sheet')
1152
+ BaseSheet.addCommand(None, 'open-source', 'vd.replace(source)', 'jump to the source of this sheet')
1217
1153
 
1218
1154
  BaseSheet.bindkey('KEY_RESIZE', 'redraw')
1219
1155
 
1220
1156
  BaseSheet.addCommand('A', 'open-new', 'vd.push(vd.newSheet("unnamed", 1))', 'Open new empty sheet')
1221
1157
 
1222
- Sheet.addCommand('^', 'rename-col', 'vd.addUndoColNames([cursorCol]); cursorCol.name = editCell(cursorVisibleColIndex, -1)', 'rename current column')
1223
- Sheet.addCommand('z^', 'rename-col-selected', 'updateColNames(selectedRows or [cursorRow], [sheet.cursorCol], overwrite=True)', 'rename current column to combined contents of current cell in selected rows (or current row)')
1224
- Sheet.addCommand('g^', 'rename-cols-row', 'updateColNames(selectedRows or [cursorRow], sheet.visibleCols)', 'rename all unnamed visible columns to contents of selected rows (or current row)')
1225
- Sheet.addCommand('gz^', 'rename-cols-selected', 'updateColNames(selectedRows or [cursorRow], sheet.visibleCols, overwrite=True)', 'rename all visible columns to combined contents of selected rows (or current row)')
1226
- BaseSheet.addCommand(None, 'rename-sheet', 'sheet.name = input("rename sheet to: ", value=sheet.name)', 'Rename current sheet')
1158
+ BaseSheet.addCommand('`', 'open-source', 'vd.push(source)', 'open source sheet')
1159
+ BaseSheet.addCommand(None, 'rename-sheet', 'sheet.name = input("rename sheet to: ", value=cleanName(sheet.name))', 'Rename current sheet')
1160
+
1161
+ Sheet.addCommand('', 'addcol-source', 'source .addColumn(copy(cursorCol)) if isinstance (source, BaseSheet) else error("source must be sheet")', 'add copy of current column to source sheet') #988 frosencrantz
1227
1162
 
1228
1163
 
1229
1164
  @Column.api
@@ -1232,3 +1167,47 @@ def formatter_enum(col, fmtdict):
1232
1167
 
1233
1168
  Sheet.addCommand('', 'setcol-formatter', 'cursorCol.formatter=input("set formatter to: ", value=cursorCol.formatter or "generic")', 'set formatter for current column (generic, json, python)')
1234
1169
  Sheet.addCommand('', 'setcol-format-enum', 'cursorCol.fmtstr=input("format replacements (k=v): ", value=f"{cursorDisplay}=", i=len(cursorDisplay)+1); cursorCol.formatter="enum"', 'add secondary type translator to current column from input enum (space-separated)')
1170
+
1171
+
1172
+ vd.addGlobals(
1173
+ RowColorizer=RowColorizer,
1174
+ CellColorizer=CellColorizer,
1175
+ ColumnColorizer=ColumnColorizer,
1176
+ RecursiveExprException=RecursiveExprException,
1177
+ LazyComputeRow=LazyComputeRow,
1178
+ Sheet=Sheet,
1179
+ TableSheet=TableSheet,
1180
+ SequenceSheet=SequenceSheet)
1181
+
1182
+ vd.addMenuItems('''
1183
+ File > New > open-new
1184
+ File > Rename > rename-sheet
1185
+ File > Guard > on > guard-sheet
1186
+ File > Guard > off > guard-sheet-off
1187
+ File > Duplicate > selected rows by ref > dup-selected
1188
+ File > Duplicate > all rows by ref > dup-rows
1189
+ File > Duplicate > selected rows deep > dup-selected-deep
1190
+ File > Duplicate > all rows deep > dup-rows-deep
1191
+ File > Reload > rows and columns > reload-sheet
1192
+ File > Quit > top sheet > quit-sheet
1193
+ File > Quit > all sheets > quit-all
1194
+ Edit > Modify > current cell > input > edit-cell
1195
+ Edit > Modify > selected cells > from input > setcol-input
1196
+ View > Sheets > stack > sheets-stack
1197
+ View > Sheets > all > sheets-all
1198
+ View > Other sheet > source sheet > open-source
1199
+ View > Split pane > in half > splitwin-half
1200
+ View > Split pane > in percent > splitwin-input
1201
+ View > Split pane > unsplit > splitwin-close
1202
+ View > Split pane > swap panes > splitwin-swap-pane
1203
+ View > Split pane > goto other pane > splitwin-swap
1204
+ View > Refresh screen > redraw
1205
+ Column > Type as > anytype > type-any
1206
+ Column > Type as > string > type-string
1207
+ Column > Type as > integer > type-int
1208
+ Column > Type as > float > type-float
1209
+ Column > Type as > locale float > type-floatlocale
1210
+ Column > Type as > length > type-len
1211
+ Column > Key > toggle current column > key-col
1212
+ Column > Key > unkey current column > key-col-off
1213
+ ''')