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/shell.py CHANGED
@@ -10,19 +10,25 @@ except ImportError:
10
10
  pass # pwd,grp modules not available on Windows
11
11
 
12
12
  from visidata import Column, Sheet, LazyComputeRow, asynccache, BaseSheet, vd
13
- from visidata import Path, ENTER, asyncthread, FileExistsError, VisiData
13
+ from visidata import Path, ENTER, asyncthread, VisiData
14
14
  from visidata import modtime, filesize, vstat, Progress, TextSheet
15
15
  from visidata.type_date import date
16
16
 
17
17
 
18
- vd.option('dir_recurse', False, 'walk source path recursively on DirSheet')
18
+ vd.option('dir_depth', 0, 'folder recursion depth on DirSheet')
19
19
  vd.option('dir_hidden', False, 'load hidden files on DirSheet')
20
20
 
21
21
 
22
+ @VisiData.api
23
+ def guess_dir(vd, p):
24
+ if p.is_dir():
25
+ return dict(filetype='dir')
26
+
27
+
22
28
  @VisiData.lazy_property
23
29
  def currentDirSheet(p):
24
30
  'Support opening the current DirSheet from the vdmenu'
25
- return DirSheet('.', source=Path('.'))
31
+ return DirSheet(Path('.').absolute().name, source=Path('.'))
26
32
 
27
33
  @asyncthread
28
34
  def exec_shell(*args):
@@ -35,11 +41,11 @@ def exec_shell(*args):
35
41
 
36
42
  @VisiData.api
37
43
  def open_dir(vd, p):
38
- return DirSheet(p.name, source=p)
44
+ return DirSheet(p.base_stem, source=p)
39
45
 
40
46
  @VisiData.api
41
47
  def open_fdir(vd, p):
42
- return FileListSheet(p.name, source=p)
48
+ return FileListSheet(p.base_stem, source=p)
43
49
 
44
50
  @VisiData.api
45
51
  def addShellColumns(vd, cmd, sheet):
@@ -76,6 +82,21 @@ class ColumnShell(Column):
76
82
 
77
83
  class DirSheet(Sheet):
78
84
  'Sheet displaying directory, using ENTER to open a particular file. Edited fields are applied to the filesystem.'
85
+ guide = '''
86
+ # Directory Sheet
87
+ This is a list of files in the {sheet.displaySource} folder.
88
+
89
+ - {help.commands.open_row_file}
90
+ - {help.commands.open_rows}
91
+ - (`open-dir-parent`) to open parent directory
92
+ - {help.commands.sysopen_row}
93
+
94
+ ## Options (must reload to take effect)
95
+
96
+ - {help.options.dir_depth}
97
+ - [CLI] `-r` to include all files in all subfolders
98
+ - {help.options.dir_hidden}
99
+ '''
79
100
  rowtype = 'files' # rowdef: Path
80
101
  defer = True
81
102
  columns = [
@@ -145,11 +166,14 @@ class DirSheet(Sheet):
145
166
 
146
167
  def removeFile(self, path):
147
168
  if path.is_dir():
148
- os.rmdir(path)
169
+ if self.options.safety_first:
170
+ os.rmdir(path)
171
+ else:
172
+ shutil.rmtree(path) #1965
149
173
  else:
150
174
  path.unlink()
151
175
 
152
- def deleteSourceRow(self, r):
176
+ def commitDeleteRow(self, r):
153
177
  self.removeFile(r)
154
178
 
155
179
  def newRow(self):
@@ -158,7 +182,7 @@ class DirSheet(Sheet):
158
182
  def iterload(self):
159
183
  hidden_files = self.options.dir_hidden
160
184
 
161
- def _walkfiles(p):
185
+ def _walkfiles(p, dir_depth:int=0):
162
186
  basepath = str(p)
163
187
  for folder, subdirs, files in os.walk(basepath):
164
188
  subfolder = folder[len(basepath)+1:]
@@ -166,24 +190,23 @@ class DirSheet(Sheet):
166
190
  if subfolder in ['.', '..']: continue
167
191
 
168
192
  fpath = Path(folder)
169
- yield fpath
193
+ if str(fpath) != str(p):
194
+ yield fpath
170
195
 
171
196
  for fn in files:
172
197
  yield fpath/fn
173
198
 
174
- def _listfiles(p):
175
- basepath = str(p)
176
- for fn in os.listdir(basepath):
177
- yield p/fn
178
-
199
+ if dir_depth < len(fpath.parents)-len(p.parents)+1:
200
+ for d in subdirs:
201
+ yield fpath/d
202
+ subdirs.clear()
179
203
 
180
204
  basepath = str(self.source)
181
205
 
182
206
  folders = set()
183
- f = _walkfiles if self.options.dir_recurse else _listfiles
184
207
 
185
- for p in f(self.source):
186
- if not hidden_files and p.name.startswith('.'):
208
+ for p in _walkfiles(self.source, self.options.dir_depth):
209
+ if not hidden_files and str(p).startswith('.') and not str(p).startswith('..'):
187
210
  continue
188
211
 
189
212
  yield p
@@ -204,11 +227,14 @@ class DirSheet(Sheet):
204
227
  self._deferredDels.clear()
205
228
  self.reload()
206
229
 
230
+ def getDefaultSaveName(sheet):
231
+ return sheet.name + '.' + sheet.options.save_filetype
232
+
207
233
 
208
234
  class FileListSheet(DirSheet):
209
235
  _ordering = []
210
236
  def iterload(self):
211
- for fn in self.source.open_text():
237
+ for fn in self.source.open():
212
238
  yield Path(fn.rstrip())
213
239
 
214
240
 
@@ -219,10 +245,10 @@ def inputShell(vd):
219
245
  vd.warning('no $column in command')
220
246
  return cmd
221
247
 
222
- DirSheet.addCommand('`', 'open-dir-parent', 'vd.push(openSource(source/".."))', 'open parent directory')
248
+ DirSheet.addCommand('`', 'open-dir-parent', 'vd.push(openSource(source.parent if source.resolve()!=Path(".").resolve() else os.path.dirname(source.resolve())))', 'open parent directory') #1801
223
249
  BaseSheet.addCommand('', 'open-dir-current', 'vd.push(vd.currentDirSheet)', 'open Directory Sheet: browse properties of files in current directory')
224
250
 
225
- Sheet.addCommand('z;', 'addcol-sh', 'cmd=inputShell(); addShellColumns(cmd, sheet)', 'create new column from bash expression, with $columnNames as variables')
251
+ Sheet.addCommand('z;', 'addcol-shell', 'cmd=inputShell(); addShellColumns(cmd, sheet)', 'create new column from bash expression, with $columnNames as variables')
226
252
 
227
253
  DirSheet.addCommand(ENTER, 'open-row-file', 'vd.push(openSource(cursorRow or fail("no row"), filetype="dir" if cursorRow.is_dir() else LazyComputeRow(sheet, cursorRow).ext))', 'open current file as a new sheet')
228
254
  DirSheet.addCommand('g'+ENTER, 'open-rows', 'for r in selectedRows: vd.push(openSource(r))', 'open selected files as new sheets')
@@ -243,7 +269,7 @@ def copy_files(sheet, paths, dest):
243
269
  try:
244
270
  destpath = destdir/str(srcpath._path.name)
245
271
  if srcpath.is_dir():
246
- shutil.copy_tree(srcpath, destpath)
272
+ shutil.copytree(srcpath, destpath)
247
273
  else:
248
274
  shutil.copyfile(srcpath, destpath)
249
275
  except Exception as e:
@@ -253,3 +279,7 @@ def copy_files(sheet, paths, dest):
253
279
  vd.addGlobals({
254
280
  'DirSheet': DirSheet
255
281
  })
282
+
283
+ vd.addMenuItems('''
284
+ Column > Add column > shell > addcol-shell
285
+ ''')
visidata/sidebar.py ADDED
@@ -0,0 +1,162 @@
1
+ from typing import Optional, Union
2
+ import textwrap
3
+
4
+ from visidata import vd, VisiData, BaseSheet, colors, TextSheet, clipdraw, wraptext, dispwidth, AttrDict, wrmap
5
+ from visidata import CommandHelpGetter, OptionHelpGetter
6
+
7
+
8
+ vd.option('disp_sidebar', True, 'whether to display sidebar')
9
+ vd.option('disp_sidebar_fmt', '{guide}', 'format string for default sidebar')
10
+ vd.theme_option('disp_sidebar_width', 0, 'max width for sidebar')
11
+ vd.theme_option('disp_sidebar_height', 0, 'max height for sidebar')
12
+ vd.theme_option('color_sidebar', 'black on 114 blue', 'base color of sidebar')
13
+ vd.theme_option('color_sidebar_title', 'black on yellow', 'color of sidebar title')
14
+
15
+ @BaseSheet.property
16
+ def formatter_helpstr(sheet):
17
+ return AttrDict(commands=CommandHelpGetter(type(sheet)),
18
+ options=OptionHelpGetter())
19
+
20
+
21
+ @BaseSheet.property
22
+ def default_sidebar(sheet):
23
+ 'Default to format options.disp_sidebar_fmt. Overridable.'
24
+ fmt = sheet.options.disp_sidebar_fmt
25
+ return sheet.formatString(fmt, help=sheet.formatter_helpstr)
26
+
27
+
28
+ @VisiData.property
29
+ def recentStatusMessages(vd) -> str:
30
+ r = ''
31
+ for (pri, msgparts), n in vd.statuses.items():
32
+ msg = '; '.join(wrmap(str, msgparts))
33
+ msg = f'[{n}x] {msg}' if n > 1 else msg
34
+
35
+ if pri == 3: msgattr = '[:error]'
36
+ elif pri == 2: msgattr = '[:warning]'
37
+ elif pri == 1: msgattr = '[:warning]'
38
+ else: msgattr = ''
39
+
40
+ if msgattr:
41
+ r += '\n' + f'{msgattr}{msg}[/]'
42
+ else:
43
+ r += '\n' + msg
44
+
45
+ if r:
46
+ return '# statuses' + r
47
+
48
+ return ''
49
+
50
+
51
+ @VisiData.api
52
+ def drawSidebar(vd, scr, sheet):
53
+ sidebar = vd.recentStatusMessages
54
+ bottommsg = ''
55
+ overflowmsg = '[:reverse] Ctrl+P to view all status messages [/]'
56
+ try:
57
+ if not sidebar and sheet.options.disp_sidebar:
58
+ sidebar = sheet.default_sidebar
59
+ if not sidebar and sheet.options.disp_help > 0:
60
+ sidebar = sheet.formatString(sheet.guide, help=sheet.formatter_helpstr)
61
+
62
+ if sheet.options.disp_help < 0:
63
+ bottommsg = '[:onclick sidebar-toggle][:reverse][x][:]'
64
+ overflowmsg = '[:onclick open-sidebar]…↓…[/]'
65
+ else:
66
+ bottommsg = '[:onclick sidebar-toggle][:reverse] b to toggle sidebar [:]'
67
+ overflowmsg = '[:reverse] see full sidebar with [:code]gb[/] [:]'
68
+ except Exception as e:
69
+ vd.exceptionCaught(e)
70
+ sidebar = f'# error\n{e}'
71
+
72
+ sheet.current_sidebar = sidebar
73
+
74
+ return sheet.drawSidebarText(scr, text=sheet.current_sidebar, overflowmsg=overflowmsg, bottommsg=bottommsg)
75
+
76
+ @BaseSheet.api
77
+ def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', overflowmsg:str='', bottommsg:str=''):
78
+ scrh, scrw = scr.getmaxyx()
79
+ maxw = sheet.options.disp_sidebar_width or scrw//2
80
+ maxh = sheet.options.disp_sidebar_height or scrh-2
81
+
82
+ cattr = colors.get_color('color_sidebar')
83
+
84
+ text = text or ''
85
+
86
+ if hasattr(text, 'draw'): # like a HelpPane
87
+ maxlinew = text.width
88
+ winh = min(maxh, text.height+2)+1
89
+ else:
90
+ text = textwrap.dedent(text.strip('\n'))
91
+
92
+ if not text:
93
+ return
94
+
95
+ lines = text.splitlines()
96
+ if not title and lines and lines[0].strip().startswith('# '):
97
+ title = lines[0].strip()[2:]
98
+ text = '\n'.join(lines[1:])
99
+
100
+ if not text:
101
+ return
102
+
103
+ lines = list(wraptext(text, width=maxw-4))
104
+ maxlinew = 0
105
+ if lines:
106
+ maxlinew = max(maxlinew, max(dispwidth(textonly, maxwidth=maxw) for line, textonly in lines))
107
+ winh = min(maxh, len(lines)+2)
108
+
109
+ titlew = dispwidth(title)
110
+
111
+ maxlinew = max(maxlinew, dispwidth(overflowmsg)+4)
112
+ maxlinew = max(maxlinew, dispwidth(bottommsg)+4)
113
+ maxlinew = max(maxlinew, titlew)
114
+ winw = min(maxw, maxlinew+4)
115
+ x, y, w, h = scrw-winw-1, scrh-winh-1, winw, winh
116
+
117
+ sidebarscr = vd.subwindow(scr, x, y, w, h)
118
+
119
+ sidebarscr.erase()
120
+ sidebarscr.bkgd(' ', cattr.attr)
121
+ sidebarscr.border()
122
+ vd.onMouse(sidebarscr, 0, 0, w, h, BUTTON1_RELEASED='no-op', BUTTON1_PRESSED='no-op')
123
+
124
+ if hasattr(text, 'draw'): # like a HelpPane
125
+ text.draw(sidebarscr, attr=cattr)
126
+ else:
127
+ i = 0
128
+ for line, _ in lines:
129
+ if i >= h-2:
130
+ bottommsg = overflowmsg
131
+ break
132
+
133
+ x += clipdraw(sidebarscr, i+1, 2, line, cattr, w=w-3)
134
+ i += 1
135
+
136
+ x = max(0, w-titlew-6)
137
+ clipdraw(sidebarscr, 0, x, f"|[:sidebar_title] {title} [:]|", cattr, w=titlew+4)
138
+ if bottommsg:
139
+ clipdraw(sidebarscr, h-1, winw-dispwidth(bottommsg)-4, '|'+bottommsg+'|[:]', cattr)
140
+
141
+ sidebarscr.refresh()
142
+
143
+
144
+ @VisiData.api
145
+ class SidebarSheet(TextSheet):
146
+ guide = '''
147
+ # Sidebar Guide
148
+ The sidebar provides additional information about the current sheet, defaulting to a basic guide to the current sheet type.
149
+ It can be configured to show many useful attributes via `options.disp_sidebar_fmt`.
150
+
151
+ - `gb` to open the sidebar in a new sheet
152
+ - `b` to toggle the sidebar on/off for the current sheet
153
+ '''
154
+
155
+ BaseSheet.addCommand('b', 'sidebar-toggle', 'sheet.options.disp_sidebar = not sheet.options.disp_sidebar', 'toggle sidebar on/off')
156
+ BaseSheet.addCommand('gb', 'open-sidebar', 'sheet.current_sidebar = "" if not hasattr(sheet, "current_sidebar") else sheet.current_sidebar; vd.push(SidebarSheet(name, options.disp_sidebar_fmt, source=sheet.current_sidebar.splitlines()))', 'open sidebar in new sheet')
157
+
158
+
159
+ vd.addMenuItems('''
160
+ View > Sidebar > toggle > sidebar-toggle
161
+ View > Sidebar > open in new sheet > open-sidebar
162
+ ''')
visidata/sort.py CHANGED
@@ -36,7 +36,7 @@ class Reversor:
36
36
 
37
37
 
38
38
  @Sheet.api
39
- def sortkey(self, r, prog=None):
39
+ def sortkey(self, r):
40
40
  ret = []
41
41
  for col, reverse in self._ordering:
42
42
  if isinstance(col, str):
@@ -44,8 +44,6 @@ def sortkey(self, r, prog=None):
44
44
  val = col.getTypedValue(r)
45
45
  ret.append(Reversor(val) if reverse else val)
46
46
 
47
- if prog:
48
- prog.addProgress(1)
49
47
 
50
48
  return ret
51
49
 
@@ -58,7 +56,7 @@ def sort(self):
58
56
  try:
59
57
  with Progress(gerund='sorting', total=self.nRows) as prog:
60
58
  # must not reassign self.rows: use .sort() instead of sorted()
61
- self.rows.sort(key=lambda r,self=self,prog=prog: self.sortkey(r, prog=prog))
59
+ self.rows.sort(key=lambda r,self=self,prog=prog: (prog.addProgress(1), self.sortkey(r))[1])
62
60
  except TypeError as e:
63
61
  vd.warning('sort incomplete due to TypeError; change column type')
64
62
  vd.exceptionCaught(e, status=False)
@@ -75,3 +73,12 @@ Sheet.addCommand('z[', 'sort-asc-add', 'orderBy(cursorCol)', 'sort ascending by
75
73
  Sheet.addCommand('z]', 'sort-desc-add', 'orderBy(cursorCol, reverse=True)', 'sort descending by current column; add to existing sort criteria')
76
74
  Sheet.addCommand('gz[', 'sort-keys-asc-add', 'orderBy(*keyCols)', 'sort ascending by all key columns; add to existing sort criteria')
77
75
  Sheet.addCommand('gz]', 'sort-keys-desc-add', 'orderBy(*keyCols, reverse=True)', 'sort descending by all key columns; add to existing sort criteria')
76
+
77
+ vd.addMenuItems('''
78
+ Column > Sort by > current column only > ascending > sort-asc
79
+ Column > Sort by > current column only > descending > sort-desc
80
+ Column > Sort by > current column also > ascending > sort-asc-add
81
+ Column > Sort by > current column also > descending > sort-desc-add
82
+ Column > Sort by > key columns > ascending > sort-keys-asc
83
+ Column > Sort by > key columns > descending > sort-keys-desc
84
+ ''')
visidata/statusbar.py CHANGED
@@ -1,30 +1,43 @@
1
+ '''
2
+ Status messages get added with vd.{debug/aside/status/warning/fail/error}(), and cleared in mainloop
3
+ '''
4
+
5
+ import builtins
1
6
  import collections
2
7
  import curses
8
+ import sys
3
9
 
4
- from visidata import vd, VisiData, BaseSheet, Sheet, ColumnItem, Column, RowColorizer, options, colors, wrmap, clipdraw, ExpectedException, update_attr, MissingAttrFormatter
10
+ import visidata
11
+ from visidata import vd, VisiData, BaseSheet, Sheet, ColumnItem, Column, RowColorizer, options, colors, wrmap, clipdraw, ExpectedException, update_attr, dispwidth, ColorAttr
5
12
 
6
13
 
7
- vd.option('disp_rstatus_fmt', ' {sheet.longname} {sheet.nRows:9d} {sheet.rowtype} {sheet.modifiedStatus} {sheet.options.disp_selected_note}{sheet.nSelectedRows}', 'right-side status format string')
8
- vd.option('disp_status_fmt', '{sheet.shortcut}› {sheet.name}| ', 'status line prefix')
9
- vd.option('disp_lstatus_max', 0, 'maximum length of left status line')
10
- vd.option('disp_status_sep', ' ', 'separator between statuses')
14
+ vd.option('disp_rstatus_fmt', '{sheet.threadStatus} {sheet.keystrokeStatus} [:longname]{sheet.longname}[/] {sheet.nRows:9d} {sheet.rowtype} {sheet.modifiedStatus}{sheet.selectedStatus}{vd.replayStatus}', 'right-side status format string')
15
+ vd.option('disp_status_fmt', '[:onclick sheets-stack]{sheet.shortcut}› {sheet.name}[/]| ', 'status line prefix')
16
+ vd.theme_option('disp_lstatus_max', 0, 'maximum length of left status line')
17
+ vd.theme_option('disp_status_sep', '│', 'separator between statuses')
11
18
 
12
- vd.option('color_keystrokes', 'bold 233 black on 110 cyan', 'color of input keystrokes on status line')
13
- vd.option('color_status', 'bold black on 110 cyan', 'status line color')
14
- vd.option('color_error', 'red', 'error message color')
15
- vd.option('color_warning', 'yellow', 'warning message color')
16
- vd.option('color_top_status', 'underline', 'top window status bar color')
17
- vd.option('color_active_status', 'black on 110 cyan', ' active window status bar color')
18
- vd.option('color_inactive_status', '8 on black', 'inactive window status bar color')
19
+ vd.theme_option('color_keystrokes', 'bold white on 237', 'color of input keystrokes')
20
+ vd.theme_option('color_longname', '6', 'color of command longnames')
21
+ vd.theme_option('color_keys', 'bold', 'color of keystrokes in help')
22
+ vd.theme_option('color_status', 'bold on 238', 'status line color')
23
+ vd.theme_option('color_error', '202 1', 'error message color')
24
+ vd.theme_option('color_warning', '166 15', 'warning message color')
25
+ vd.theme_option('color_top_status', 'underline', 'top window status bar color')
26
+ vd.theme_option('color_active_status', 'black on 68 blue', ' active window status bar color')
27
+ vd.theme_option('color_inactive_status', '8 on black', 'inactive window status bar color')
28
+ vd.theme_option('color_highlight_status', 'black on green', 'color of highlighted elements in statusbar')
19
29
 
20
30
  BaseSheet.init('longname', lambda: '')
21
31
 
22
- vd.beforeExecHooks.append(lambda sheet, cmd, args, ks: setattr(sheet, 'longname', cmd.longname))
32
+ @BaseSheet.api
33
+ def _updateStatusBeforeExec(sheet, cmd, args, ks):
34
+ sheet.longname = cmd.longname
35
+ if sheet._scr:
36
+ vd.drawRightStatus(sheet._scr, sheet) #996 show longname during commands
37
+ sheet._scr.refresh()
23
38
 
24
39
 
25
- @BaseSheet.property
26
- def modifiedStatus(sheet):
27
- return ' [M]' if sheet.hasBeenModified else ''
40
+ vd.beforeExecHooks.append(BaseSheet._updateStatusBeforeExec)
28
41
 
29
42
 
30
43
  @VisiData.lazy_property
@@ -36,6 +49,11 @@ def statuses(vd):
36
49
  def statusHistory(vd):
37
50
  return list() # list of [priority, statusmsg, repeats] for all status messages ever
38
51
 
52
+ @VisiData.api
53
+ def getStatusSource(vd):
54
+ return None
55
+
56
+
39
57
  @VisiData.api
40
58
  def status(vd, *args, priority=0):
41
59
  'Display *args* on status until next action.'
@@ -45,17 +63,25 @@ def status(vd, *args, priority=0):
45
63
  k = (priority, tuple(map(str, args)))
46
64
  vd.statuses[k] = vd.statuses.get(k, 0) + 1
47
65
 
48
- return vd.addToStatusHistory(*args, priority=priority)
66
+ source = vd.getStatusSource()
67
+
68
+ if not vd.cursesEnabled:
69
+ msg = '\r' + composeStatus(args)
70
+ if vd.options.debug:
71
+ msg += f' [{source}]'
72
+ builtins.print(msg, file=sys.stderr)
73
+
74
+ return vd.addToStatusHistory(*args, priority=priority, source=source)
49
75
 
50
76
  @VisiData.api
51
- def addToStatusHistory(vd, *args, priority=0):
77
+ def addToStatusHistory(vd, *args, priority=0, source=None):
52
78
  if vd.statusHistory:
53
- prevpri, prevargs, prevn = vd.statusHistory[-1]
79
+ prevpri, prevargs, _, _ = vd.statusHistory[-1]
54
80
  if prevpri == priority and prevargs == args:
55
81
  vd.statusHistory[-1][2] += 1
56
82
  return True
57
83
 
58
- vd.statusHistory.append([priority, args, 1])
84
+ vd.statusHistory.append([priority, args, 1, source])
59
85
  return True
60
86
 
61
87
  @VisiData.api
@@ -75,6 +101,11 @@ def warning(vd, *args):
75
101
  'Display *args* on status as a warning.'
76
102
  vd.status(*args, priority=1)
77
103
 
104
+ @VisiData.api
105
+ def aside(vd, *args, priority=0):
106
+ 'Add a message to statuses without showing the message proactively.'
107
+ return vd.addToStatusHistory(*args, priority=priority, source=vd.getStatusSource())
108
+
78
109
  @VisiData.api
79
110
  def debug(vd, *args, **kwargs):
80
111
  'Display *args* on status if options.debug is set.'
@@ -87,7 +118,7 @@ def middleTruncate(s, w):
87
118
  return s[:w] + options.disp_truncator + s[-w:]
88
119
 
89
120
 
90
- def composeStatus(msgparts, n):
121
+ def composeStatus(msgparts, n=1):
91
122
  msg = '; '.join(wrmap(str, msgparts))
92
123
  if n > 1:
93
124
  msg = '[%sx] %s' % (n, msg)
@@ -97,13 +128,13 @@ def composeStatus(msgparts, n):
97
128
  @BaseSheet.api
98
129
  def leftStatus(sheet):
99
130
  'Return left side of status bar for this sheet. Overridable.'
100
- return MissingAttrFormatter().format(sheet.options.disp_status_fmt, sheet=sheet, vd=vd)
131
+ return sheet.formatString(sheet.options.disp_status_fmt)
101
132
 
102
133
 
103
134
  @VisiData.api
104
135
  def drawLeftStatus(vd, scr, vs):
105
136
  'Draw left side of status bar.'
106
- cattr = colors.get_color('color_status')
137
+ cattr = colors.get_color('color_active_status')
107
138
  active = (vs is vd.activeSheet)
108
139
  if active:
109
140
  cattr = update_attr(cattr, colors.color_active_status, 1)
@@ -113,103 +144,77 @@ def drawLeftStatus(vd, scr, vs):
113
144
  if scr is vd.winTop:
114
145
  cattr = update_attr(cattr, colors.color_top_status, 1)
115
146
 
116
- attr = cattr.attr
117
- error_attr = update_attr(cattr, colors.color_error, 1).attr
118
- warn_attr = update_attr(cattr, colors.color_warning, 2).attr
119
- sep = options.disp_status_sep
120
-
121
147
  x = 0
122
148
  y = vs.windowHeight-1 # status for each window
123
- try:
124
- lstatus = vs.leftStatus()
125
- maxwidth = options.disp_lstatus_max
126
- if maxwidth > 0:
127
- lstatus = middleTruncate(lstatus, maxwidth//2)
128
-
129
- x = clipdraw(scr, y, 0, lstatus, attr, w=vs.windowWidth-1)
130
-
131
- vd.onMouse(scr, y, 0, 1, x,
132
- BUTTON1_PRESSED='sheets-stack',
133
- BUTTON1_RELEASED='sheets-stack',
134
- BUTTON3_PRESSED='rename-sheet',
135
- BUTTON3_CLICKED='rename-sheet')
136
- except Exception as e:
137
- vd.exceptionCaught(e)
138
-
139
- if not active:
140
- return
141
-
142
- one = False
143
- for (pri, msgparts), n in sorted(vd.statuses.items(), key=lambda k: -k[0][0]):
144
- try:
145
- if x > vs.windowWidth:
146
- break
147
- if one: # any messages already:
148
- x += clipdraw(scr, y, x, sep, attr, w=vs.windowWidth-x)
149
- one = True
150
- msg = composeStatus(msgparts, n)
151
-
152
- if pri == 3: msgattr = error_attr
153
- elif pri == 2: msgattr = warn_attr
154
- elif pri == 1: msgattr = warn_attr
155
- else: msgattr = attr
156
- x += clipdraw(scr, y, x, msg, msgattr, w=vs.windowWidth-x)
157
- except Exception as e:
158
- vd.exceptionCaught(e)
149
+ lstatus = vs.leftStatus()
150
+ maxwidth = options.disp_lstatus_max
151
+ if maxwidth > 0:
152
+ lstatus = middleTruncate(lstatus, maxwidth//2)
153
+
154
+ x = clipdraw(scr, y, 0, lstatus, cattr, w=vs.windowWidth-1)
155
+
156
+ vd.onMouse(scr, 0, y, x, 1,
157
+ BUTTON3_PRESSED='rename-sheet',
158
+ BUTTON3_CLICKED='rename-sheet')
159
159
 
160
160
 
161
161
  @VisiData.api
162
162
  def rightStatus(vd, sheet):
163
163
  'Return right side of status bar. Overridable.'
164
- return MissingAttrFormatter().format(sheet.options.disp_rstatus_fmt, sheet=sheet, vd=vd)
164
+ return sheet.formatString(sheet.options.disp_rstatus_fmt)
165
165
 
166
166
 
167
- @VisiData.api
168
- def drawRightStatus(vd, scr, vs):
169
- 'Draw right side of status bar. Return length displayed.'
170
- rightx = vs.windowWidth
171
-
172
- ret = 0
173
- statcolors = [
174
- (vd.rightStatus(vs), 'color_status'),
175
- ]
167
+ @BaseSheet.property
168
+ def keystrokeStatus(vs):
169
+ if vs is vd.activeSheet:
170
+ return f'[:keystrokes]{vd.keystrokes}[/]'
176
171
 
177
- active = vs is vd.activeSheet
172
+ return ''
178
173
 
179
- if active:
180
- statcolors.append((f'{vd.prettykeys(vd.keystrokes)} ' or '', 'color_keystrokes'))
181
174
 
175
+ @BaseSheet.property
176
+ def threadStatus(vs) -> str:
182
177
  if vs.currentThreads:
183
- statcolors.insert(0, vd.checkMemoryUsage())
178
+ ret = str(vd.checkMemoryUsage())
184
179
  gerunds = [p.gerund for p in vs.progresses if p.gerund] or ['processing']
185
- statcolors.insert(1, (' %s %s…' % (vs.progressPct, gerunds[0]), 'color_working'))
186
-
187
- if active and vd.currentReplay:
188
- statcolors.insert(0, (vd.replayStatus, 'color_status_replay'))
189
-
190
- for rstatcolor in statcolors:
191
- if rstatcolor:
192
- try:
193
- rstatus, coloropt = rstatcolor
194
- rstatus = ' '+rstatus
195
- cattr = colors.get_color(coloropt)
196
- if scr is vd.winTop:
197
- cattr = update_attr(cattr, colors.color_top_status, 0)
198
- if active:
199
- cattr = update_attr(cattr, colors.color_active_status, 0)
200
- else:
201
- cattr = update_attr(cattr, colors.color_inactive_status, 0)
202
- statuslen = clipdraw(scr, vs.windowHeight-1, rightx, rstatus, cattr.attr, w=vs.windowWidth-1, rtl=True)
203
- rightx -= statuslen
204
- ret += statuslen
205
- except Exception as e:
206
- vd.exceptionCaught(e)
207
-
208
- if scr:
209
- curses.doupdate()
180
+ ret += f' [:working]{vs.progressPct} {gerunds[0]}…[/]'
181
+ return ret
182
+ return ''
183
+
184
+ @BaseSheet.property
185
+ def modifiedStatus(sheet):
186
+ ret = ' [M]' if sheet.hasBeenModified else ''
187
+ if not vd.couldOverwrite():
188
+ ret += ' [:highlight_status][RO][/] '
210
189
  return ret
211
190
 
212
191
 
192
+ @Sheet.property
193
+ def selectedStatus(sheet):
194
+ if sheet.nSelectedRows:
195
+ return f' [:selected_row][:onclick dup-selected]{sheet.options.disp_selected_note}{sheet.nSelectedRows}[/][/] '
196
+
197
+
198
+ @VisiData.api
199
+ def drawRightStatus(vd, scr, vs):
200
+ 'Draw right side of status bar. Return length displayed.'
201
+ rightx = vs.windowWidth
202
+
203
+ statuslen = 0
204
+ try:
205
+ cattr = ColorAttr()
206
+ if scr is vd.winTop:
207
+ cattr = update_attr(cattr, colors.color_top_status, 0)
208
+ cattr = update_attr(cattr, colors.color_active_status if vs is vd.activeSheet else colors.color_inactive_status, 0)
209
+ rstat = vd.rightStatus(vs)
210
+ x = max(2, rightx-dispwidth(rstat)-1)
211
+ statuslen = clipdraw(scr, vs.windowHeight-1, x, rstat, cattr, w=vs.windowWidth-1)
212
+ finally:
213
+ if scr:
214
+ curses.doupdate()
215
+ return statuslen
216
+
217
+
213
218
  class StatusSheet(Sheet):
214
219
  precious = False
215
220
  rowtype = 'statuses' # rowdef: (priority, args, nrepeats)
@@ -217,7 +222,8 @@ class StatusSheet(Sheet):
217
222
  ColumnItem('priority', 0, type=int, width=0),
218
223
  ColumnItem('nrepeats', 2, type=int, width=0),
219
224
  ColumnItem('args', 1, width=0),
220
- Column('message', getter=lambda col,row: composeStatus(row[1], row[2])),
225
+ Column('message', width=50, getter=lambda col,row: composeStatus(row[1], row[2])),
226
+ ColumnItem('source', 3, max_help=1),
221
227
  ]
222
228
  colorizers = [
223
229
  RowColorizer(1, 'color_error', lambda s,c,r,v: r and r[0] == 3),
@@ -234,3 +240,7 @@ def statusHistorySheet(vd):
234
240
 
235
241
 
236
242
  BaseSheet.addCommand('^P', 'open-statuses', 'vd.push(vd.statusHistorySheet)', 'open Status History')
243
+
244
+ vd.addMenuItems('''
245
+ View > Statuses > open-statuses
246
+ ''')