visidata 2.11.dev0__py3-none-any.whl → 3.0__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 (253) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +263 -44
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +22 -4
  5. visidata/_urlcache.py +17 -4
  6. visidata/aggregators.py +65 -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. visidata/apps/vgit/__main__.py +3 -0
  18. visidata/apps/vgit/abort.py +23 -0
  19. visidata/apps/vgit/blame.py +76 -0
  20. visidata/apps/vgit/branch.py +153 -0
  21. visidata/apps/vgit/config.py +95 -0
  22. visidata/apps/vgit/diff.py +169 -0
  23. visidata/apps/vgit/gitsheet.py +161 -0
  24. visidata/apps/vgit/grep.py +37 -0
  25. visidata/apps/vgit/log.py +81 -0
  26. visidata/apps/vgit/main.py +55 -0
  27. visidata/apps/vgit/remote.py +57 -0
  28. visidata/apps/vgit/repos.py +71 -0
  29. visidata/apps/vgit/setup.py +37 -0
  30. visidata/apps/vgit/stash.py +69 -0
  31. visidata/apps/vgit/status.py +204 -0
  32. visidata/apps/vgit/statusbar.py +34 -0
  33. visidata/basesheet.py +59 -50
  34. visidata/canvas.py +251 -99
  35. visidata/choose.py +15 -11
  36. visidata/clean_names.py +29 -0
  37. visidata/clipboard.py +84 -18
  38. visidata/cliptext.py +220 -46
  39. visidata/cmdlog.py +89 -114
  40. visidata/color.py +142 -56
  41. visidata/column.py +134 -131
  42. visidata/ddw/input.ddw +74 -79
  43. visidata/ddw/regex.ddw +57 -0
  44. visidata/ddwplay.py +33 -14
  45. visidata/deprecated.py +77 -3
  46. visidata/desktop/visidata.desktop +7 -0
  47. visidata/editor.py +12 -6
  48. visidata/errors.py +5 -1
  49. visidata/experimental/__init__.py +0 -0
  50. visidata/experimental/diff_sheet.py +29 -0
  51. visidata/experimental/digit_autoedit.py +6 -0
  52. visidata/experimental/gdrive.py +89 -0
  53. visidata/experimental/google.py +37 -0
  54. visidata/experimental/gsheets.py +79 -0
  55. visidata/experimental/live_search.py +37 -0
  56. visidata/experimental/liveupdate.py +45 -0
  57. visidata/experimental/mark.py +133 -0
  58. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  59. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  60. visidata/experimental/rownum.py +73 -0
  61. visidata/experimental/slide_cells.py +26 -0
  62. visidata/expr.py +8 -4
  63. visidata/extensible.py +32 -6
  64. visidata/features/__init__.py +0 -0
  65. visidata/features/addcol_audiometadata.py +42 -0
  66. visidata/features/addcol_histogram.py +34 -0
  67. visidata/features/canvas_save_svg.py +69 -0
  68. visidata/features/change_precision.py +46 -0
  69. visidata/features/cmdpalette.py +163 -0
  70. visidata/features/colorbrewer.py +363 -0
  71. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  72. visidata/features/command_server.py +105 -0
  73. visidata/features/currency_to_usd.py +70 -0
  74. visidata/{customdate.py → features/customdate.py} +2 -0
  75. visidata/features/dedupe.py +132 -0
  76. visidata/{describe.py → features/describe.py} +17 -15
  77. visidata/features/errors_guide.py +26 -0
  78. visidata/features/expand_cols.py +202 -0
  79. visidata/{fill.py → features/fill.py} +4 -2
  80. visidata/{freeze.py → features/freeze.py} +11 -6
  81. visidata/features/graph_seaborn.py +79 -0
  82. visidata/features/helloworld.py +10 -0
  83. visidata/features/hint_types.py +17 -0
  84. visidata/{incr.py → features/incr.py} +5 -0
  85. visidata/{join.py → features/join.py} +107 -53
  86. visidata/features/known_cols.py +21 -0
  87. visidata/features/layout.py +62 -0
  88. visidata/{melt.py → features/melt.py} +33 -21
  89. visidata/features/normcol.py +118 -0
  90. visidata/features/open_config.py +7 -0
  91. visidata/features/open_syspaste.py +18 -0
  92. visidata/features/ping.py +157 -0
  93. visidata/features/procmgr.py +208 -0
  94. visidata/features/random_sample.py +6 -0
  95. visidata/{regex.py → features/regex.py} +47 -31
  96. visidata/features/reload_every.py +55 -0
  97. visidata/features/rename_col_cascade.py +30 -0
  98. visidata/features/scroll_context.py +60 -0
  99. visidata/features/select_equal_selected.py +11 -0
  100. visidata/features/setcol_fake.py +65 -0
  101. visidata/{slide.py → features/slide.py} +75 -21
  102. visidata/features/sparkline.py +48 -0
  103. visidata/features/status_source.py +20 -0
  104. visidata/{sysedit.py → features/sysedit.py} +2 -1
  105. visidata/features/sysopen_mailcap.py +46 -0
  106. visidata/features/term_extras.py +13 -0
  107. visidata/{transpose.py → features/transpose.py} +5 -4
  108. visidata/features/type_ipaddr.py +73 -0
  109. visidata/features/type_url.py +11 -0
  110. visidata/{unfurl.py → features/unfurl.py} +9 -9
  111. visidata/{window.py → features/window.py} +2 -2
  112. visidata/form.py +50 -21
  113. visidata/freqtbl.py +81 -33
  114. visidata/fuzzymatch.py +414 -0
  115. visidata/graph.py +105 -33
  116. visidata/guide.py +180 -0
  117. visidata/help.py +75 -44
  118. visidata/hint.py +39 -0
  119. visidata/indexsheet.py +109 -0
  120. visidata/input_history.py +55 -0
  121. visidata/interface.py +58 -0
  122. visidata/keys.py +17 -16
  123. visidata/loaders/__init__.py +9 -0
  124. visidata/loaders/_pandas.py +61 -21
  125. visidata/loaders/api_airtable.py +70 -0
  126. visidata/loaders/api_bitio.py +102 -0
  127. visidata/loaders/api_matrix.py +148 -0
  128. visidata/loaders/api_reddit.py +306 -0
  129. visidata/loaders/api_zulip.py +249 -0
  130. visidata/loaders/archive.py +41 -7
  131. visidata/loaders/arrow.py +7 -7
  132. visidata/loaders/conll.py +49 -0
  133. visidata/loaders/csv.py +25 -7
  134. visidata/loaders/eml.py +3 -4
  135. visidata/loaders/f5log.py +1204 -0
  136. visidata/loaders/fec.py +325 -0
  137. visidata/loaders/fixed_width.py +3 -5
  138. visidata/loaders/frictionless.py +3 -3
  139. visidata/loaders/geojson.py +8 -5
  140. visidata/loaders/google.py +48 -0
  141. visidata/loaders/graphviz.py +4 -4
  142. visidata/loaders/hdf5.py +4 -4
  143. visidata/loaders/html.py +48 -10
  144. visidata/loaders/http.py +84 -30
  145. visidata/loaders/imap.py +20 -10
  146. visidata/loaders/jrnl.py +52 -0
  147. visidata/loaders/json.py +83 -29
  148. visidata/loaders/jsonla.py +74 -0
  149. visidata/loaders/lsv.py +15 -11
  150. visidata/loaders/mailbox.py +40 -0
  151. visidata/loaders/markdown.py +1 -3
  152. visidata/loaders/mbtiles.py +4 -5
  153. visidata/loaders/mysql.py +11 -13
  154. visidata/loaders/npy.py +7 -7
  155. visidata/loaders/odf.py +4 -1
  156. visidata/loaders/orgmode.py +428 -0
  157. visidata/loaders/pandas_freqtbl.py +14 -20
  158. visidata/loaders/parquet.py +62 -6
  159. visidata/loaders/pcap.py +3 -3
  160. visidata/loaders/pdf.py +4 -3
  161. visidata/loaders/png.py +19 -13
  162. visidata/loaders/postgres.py +9 -8
  163. visidata/loaders/rec.py +7 -3
  164. visidata/loaders/s3.py +342 -0
  165. visidata/loaders/sas.py +5 -5
  166. visidata/loaders/scrape.py +186 -0
  167. visidata/loaders/shp.py +6 -5
  168. visidata/loaders/spss.py +5 -6
  169. visidata/loaders/sqlite.py +68 -28
  170. visidata/loaders/texttables.py +1 -1
  171. visidata/loaders/toml.py +60 -0
  172. visidata/loaders/tsv.py +61 -19
  173. visidata/loaders/ttf.py +19 -7
  174. visidata/loaders/unzip_http.py +6 -5
  175. visidata/loaders/usv.py +1 -1
  176. visidata/loaders/vcf.py +16 -16
  177. visidata/loaders/vds.py +10 -7
  178. visidata/loaders/vdx.py +30 -5
  179. visidata/loaders/xlsb.py +8 -1
  180. visidata/loaders/xlsx.py +145 -25
  181. visidata/loaders/xml.py +6 -3
  182. visidata/loaders/xword.py +4 -4
  183. visidata/loaders/yaml.py +15 -5
  184. visidata/macos.py +1 -1
  185. visidata/macros.py +130 -41
  186. visidata/main.py +119 -94
  187. visidata/mainloop.py +101 -154
  188. visidata/man/parse_options.py +2 -2
  189. visidata/man/vd.1 +302 -147
  190. visidata/man/vd.txt +291 -151
  191. visidata/memory.py +3 -3
  192. visidata/menu.py +104 -423
  193. visidata/metasheets.py +59 -141
  194. visidata/modify.py +79 -23
  195. visidata/motd.py +3 -3
  196. visidata/mouse.py +137 -0
  197. visidata/movement.py +43 -35
  198. visidata/optionssheet.py +99 -0
  199. visidata/path.py +131 -43
  200. visidata/pivot.py +74 -47
  201. visidata/plugins.py +65 -192
  202. visidata/pyobj.py +50 -201
  203. visidata/rename_col.py +20 -0
  204. visidata/save.py +42 -20
  205. visidata/search.py +54 -10
  206. visidata/selection.py +84 -5
  207. visidata/settings.py +162 -24
  208. visidata/sheets.py +229 -257
  209. visidata/shell.py +51 -21
  210. visidata/sidebar.py +162 -0
  211. visidata/sort.py +11 -4
  212. visidata/statusbar.py +113 -104
  213. visidata/stored_list.py +43 -0
  214. visidata/stored_prop.py +38 -0
  215. visidata/tests/conftest.py +3 -3
  216. visidata/tests/test_cliptext.py +39 -0
  217. visidata/tests/test_commands.py +62 -7
  218. visidata/tests/test_edittext.py +2 -2
  219. visidata/tests/test_features.py +17 -0
  220. visidata/tests/test_menu.py +14 -0
  221. visidata/tests/test_path.py +13 -4
  222. visidata/text_source.py +53 -0
  223. visidata/textsheet.py +10 -3
  224. visidata/theme.py +44 -0
  225. visidata/themes/__init__.py +0 -0
  226. visidata/themes/ascii8.py +84 -0
  227. visidata/themes/asciimono.py +84 -0
  228. visidata/themes/light.py +17 -0
  229. visidata/threads.py +87 -39
  230. visidata/tuiwin.py +22 -0
  231. visidata/type_currency.py +22 -3
  232. visidata/type_date.py +31 -9
  233. visidata/type_floatsi.py +5 -1
  234. visidata/undo.py +18 -6
  235. visidata/utils.py +106 -23
  236. visidata/vdobj.py +28 -17
  237. visidata/windows.py +10 -0
  238. visidata/wrappers.py +9 -3
  239. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  240. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
  241. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
  242. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  243. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
  244. visidata-3.0.dist-info/RECORD +257 -0
  245. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  246. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
  247. visidata/layout.py +0 -44
  248. visidata/misc.py +0 -5
  249. visidata-2.11.dev0.dist-info/RECORD +0 -142
  250. /visidata/{repeat.py → features/repeat.py} +0 -0
  251. {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
  252. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  253. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
visidata/cmdlog.py CHANGED
@@ -1,40 +1,47 @@
1
1
  import threading
2
2
 
3
- from visidata import *
3
+ from visidata import vd, UNLOADED, namedlist, vlen, asyncthread, globalCommand, date
4
+ from visidata import VisiData, BaseSheet, Sheet, ColumnAttr, VisiDataMetaSheet, JsonLinesSheet, TypedWrapper, AttrDict, Progress, ErrorSheet, CompleteKey, Path
4
5
  import visidata
5
6
 
6
7
  vd.option('replay_wait', 0.0, 'time to wait between replayed commands, in seconds', sheettype=None)
7
- vd.option('disp_replay_play', '▶', 'status indicator for active replay')
8
- vd.option('disp_replay_pause', '', 'status indicator for paused replay')
9
- vd.option('color_status_replay', 'green', 'color of replay status indicator')
10
- vd.option('replay_movement', False, 'insert movements during replay', sheettype=None)
8
+ vd.theme_option('disp_replay_play', '▶', 'status indicator for active replay')
9
+ vd.theme_option('color_status_replay', 'green', 'color of replay status indicator')
11
10
 
12
11
  # prefixes which should not be logged
13
12
  nonLogged = '''forget exec-longname undo redo quit
14
13
  show error errors statuses options threads jump
15
- replay cancel save-cmdlog macro cmdlog-sheet menu repeat
14
+ replay cancel save-cmdlog macro cmdlog-sheet menu repeat reload-every
16
15
  go- search scroll prev next page start end zoom resize visibility sidebar
17
16
  mouse suspend redraw no-op help syscopy sysopen profile toggle'''.split()
18
17
 
19
18
  vd.option('rowkey_prefix', 'キ', 'string prefix for rowkey in the cmdlog', sheettype=None)
20
- vd.option('cmdlog_histfile', '', 'file to autorecord each cmdlog action to', sheettype=None)
21
19
 
22
20
  vd.activeCommand = UNLOADED
21
+ vd._nextCommands = [] # list[str|CommandLogRow] for vd.queueCommand
22
+
23
+ CommandLogRow = namedlist('CommandLogRow', 'sheet col row longname input keystrokes comment undofuncs'.split())
24
+
25
+ @VisiData.api
26
+ def queueCommand(vd, longname, input=None, sheet=None, col=None, row=None):
27
+ 'Add command to queue of next commands to execute.'
28
+ vd._nextCommands.append(CommandLogRow(longname=longname, input=input, sheet=sheet, col=col, row=row))
29
+
23
30
 
24
31
  @VisiData.api
25
32
  def open_vd(vd, p):
26
- return CommandLog(p.name, source=p, precious=True)
33
+ return CommandLog(p.base_stem, source=p, precious=True)
27
34
 
28
35
  @VisiData.api
29
36
  def open_vdj(vd, p):
30
- return CommandLogJsonl(p.name, source=p, precious=True)
37
+ return CommandLogJsonl(p.base_stem, source=p, precious=True)
31
38
 
32
39
  VisiData.save_vd = VisiData.save_tsv
33
40
 
34
41
 
35
42
  @VisiData.api
36
43
  def save_vdj(vd, p, *vsheets):
37
- with p.open_text(mode='w', encoding=vsheets[0].options.encoding) as fp:
44
+ with p.open(mode='w', encoding=vsheets[0].options.save_encoding) as fp:
38
45
  fp.write("#!vd -p\n")
39
46
  for vs in vsheets:
40
47
  vs.write_jsonl(fp)
@@ -46,7 +53,7 @@ def checkVersion(vd, desired_version):
46
53
  vd.fail("version %s required" % desired_version)
47
54
 
48
55
  @VisiData.api
49
- def fnSuffix(vd, prefix):
56
+ def fnSuffix(vd, prefix:str):
50
57
  i = 0
51
58
  fn = prefix + '.vdj'
52
59
  while Path(fn).exists():
@@ -55,18 +62,6 @@ def fnSuffix(vd, prefix):
55
62
 
56
63
  return fn
57
64
 
58
- @BaseSheet.api
59
- def inputLongname(sheet):
60
- longnames = set(k for (k, obj), v in vd.commands.iter(sheet))
61
- return vd.input("command name: ", completer=CompleteKey(sorted(longnames)), type='longname')
62
-
63
- @BaseSheet.api
64
- def exec_longname(sheet, longname):
65
- if not sheet.getCommand(longname):
66
- vd.warning(f'no command {longname}')
67
- return
68
- sheet.execCommand(longname)
69
-
70
65
  def indexMatch(L, func):
71
66
  'returns the smallest i for which func(L[i]) is true'
72
67
  for i, x in enumerate(L):
@@ -74,7 +69,7 @@ def indexMatch(L, func):
74
69
  return i
75
70
 
76
71
  def keystr(k):
77
- return options.rowkey_prefix+','.join(map(str, k))
72
+ return vd.options.rowkey_prefix+','.join(map(str, k))
78
73
 
79
74
  @VisiData.api
80
75
  def isLoggableCommand(vd, longname):
@@ -84,7 +79,7 @@ def isLoggableCommand(vd, longname):
84
79
  return True
85
80
 
86
81
  def isLoggableSheet(sheet):
87
- return sheet is not vd.cmdlog and not isinstance(sheet, (OptionsSheet, ErrorSheet))
82
+ return sheet is not vd.cmdlog and not isinstance(sheet, (vd.OptionsSheet, ErrorSheet))
88
83
 
89
84
 
90
85
  @Sheet.api
@@ -94,13 +89,7 @@ def moveToRow(vs, rowstr):
94
89
  if rowidx is None:
95
90
  return False
96
91
 
97
- if vs.options.replay_movement:
98
- while vs.cursorRowIndex != rowidx:
99
- vs.cursorRowIndex += 1 if (rowidx - vs.cursorRowIndex) > 0 else -1
100
- while not vd.delay(0.5):
101
- pass
102
- else:
103
- vs.cursorRowIndex = rowidx
92
+ vs.cursorRowIndex = rowidx
104
93
 
105
94
  return True
106
95
 
@@ -126,13 +115,7 @@ def moveToCol(vs, col):
126
115
  if vcolidx is None or vcolidx >= vs.nVisibleCols:
127
116
  return False
128
117
 
129
- if vs.options.replay_movement:
130
- while vs.cursorVisibleColIndex != vcolidx:
131
- vs.cursorVisibleColIndex += 1 if (vcolidx - vs.cursorVisibleColIndex) > 0 else -1
132
- while not vd.delay(0.5):
133
- pass
134
- else:
135
- vs.cursorVisibleColIndex = vcolidx
118
+ vs.cursorVisibleColIndex = vcolidx
136
119
 
137
120
  return True
138
121
 
@@ -147,7 +130,10 @@ def commandCursor(sheet, execstr):
147
130
  rowname = keystr(k) if k else sheet.cursorRowIndex
148
131
 
149
132
  if contains(execstr, 'cursorTypedValue', 'cursorDisplay', 'cursorValue', 'cursorCell', 'cursorCol', 'cursorVisibleCol', 'ColumnAtCursor'):
150
- colname = sheet.cursorCol.name or sheet.visibleCols.index(sheet.cursorCol)
133
+ if sheet.cursorCol:
134
+ colname = sheet.cursorCol.name or sheet.visibleCols.index(sheet.cursorCol)
135
+ else:
136
+ colname = None
151
137
  return colname, rowname
152
138
 
153
139
 
@@ -156,7 +142,7 @@ class CommandLogBase:
156
142
  'Log of commands for current session.'
157
143
  rowtype = 'logged commands'
158
144
  precious = False
159
- _rowtype = namedlist('CommandLogRow', 'sheet col row longname input keystrokes comment undofuncs'.split())
145
+ _rowtype = CommandLogRow
160
146
  columns = [
161
147
  ColumnAttr('sheet'),
162
148
  ColumnAttr('col'),
@@ -185,7 +171,7 @@ class CommandLogBase:
185
171
 
186
172
  contains = lambda s, *substrs: any((a in s) for a in substrs)
187
173
  if contains(cmd.execstr, 'pasteFromClipboard'):
188
- args = vd.sysclip_value().strip()
174
+ args = vd.sysclipValue().strip()
189
175
 
190
176
 
191
177
  comment = vd.currentReplayRow.comment if vd.currentReplayRow else cmd.helpstr
@@ -211,18 +197,10 @@ class CommandLogBase:
211
197
  return
212
198
 
213
199
  # remove user-aborted commands and simple movements (unless first command on the sheet, which created the sheet)
214
- if not sheet.cmdlog.rows or vd.isLoggableCommand(vd.activeCommand.longname):
200
+ if not sheet.cmdlog_sheet.rows or vd.isLoggableCommand(vd.activeCommand.longname):
215
201
  if isLoggableSheet(sheet): # don't record actions from cmdlog or other internal sheets on global cmdlog
216
202
  self.addRow(vd.activeCommand) # add to global cmdlog
217
203
  sheet.cmdlog_sheet.addRow(vd.activeCommand) # add to sheet-specific cmdlog
218
- if options.cmdlog_histfile:
219
- name = date().strftime(options.cmdlog_histfile)
220
- p = Path(name)
221
- if not p.is_absolute():
222
- p = Path(options.visidata_dir)/f'{name}.jsonl'
223
- if not getattr(vd, 'sessionlog', None):
224
- vd.sessionlog = vd.loadInternalSheet(CommandLog, p)
225
- vd.sessionlog.append_tsv_row(vd.activeCommand)
226
204
 
227
205
  vd.activeCommand = None
228
206
 
@@ -256,49 +234,27 @@ class CommandLogJsonl(CommandLogBase, JsonLinesSheet):
256
234
  vd.paused = False
257
235
  vd.currentReplay = None # CommandLog replaying currently
258
236
  vd.currentReplayRow = None # must be global, to allow replay
259
- vd.semaphore = threading.Semaphore(0)
260
-
261
-
262
- @VisiData.api
263
- def replay_pause(vd):
264
- if not vd.currentReplay:
265
- vd.fail('no replay to pause')
266
- else:
267
- if vd.paused:
268
- vd.replay_advance()
269
- vd.paused = not vd.paused
270
- vd.status('paused' if vd.paused else 'resumed')
271
-
272
-
273
- @VisiData.api
274
- def replay_advance(vd):
275
- vd.currentReplay or vd.fail("no replay to advance")
276
- vd.semaphore.release()
277
237
 
278
238
 
279
239
  @VisiData.api
280
240
  def replay_cancel(vd):
281
- vd.currentReplay or vd.fail("no replay to cancel")
282
- vd.currentReplayRow = None
283
- vd.currentReplay = None
284
- vd.semaphore.release()
241
+ vd.currentReplayRow = None
242
+ vd.currentReplay = None
243
+ vd._nextCommands.clear()
285
244
 
286
245
 
287
246
  @VisiData.api
288
247
  def moveToReplayContext(vd, r, vs):
289
248
  'set the sheet/row/col to the values in the replay row'
249
+ vs.ensureLoaded()
250
+ vd.sync()
251
+ vd.clearCaches()
252
+
290
253
  if r.row not in [None, '']:
291
- vs.moveToRow(r.row) or vd.error('no "%s" row' % r.row)
254
+ vs.moveToRow(r.row) or vd.error(f'no "{r.row}" row on {vs}')
292
255
 
293
256
  if r.col not in [None, '']:
294
- vs.moveToCol(r.col) or vd.error('no "%s" column' % r.col)
295
-
296
-
297
- @VisiData.api
298
- def delay(vd, factor=1):
299
- 'returns True if delay satisfied'
300
- acquired = vd.semaphore.acquire(timeout=options.replay_wait*factor if not vd.paused else None)
301
- return acquired or not vd.paused
257
+ vs.moveToCol(r.col) or vd.error(f'no "{r.col}" column on {vs}')
302
258
 
303
259
 
304
260
  @VisiData.api
@@ -329,18 +285,21 @@ def replayOne(vd, r):
329
285
  else:
330
286
  vs = vs or vd.activeSheet
331
287
  if vs:
332
- vd.push(vs)
288
+ if vs in vd.sheets: # if already on sheet stack, push to top
289
+ vd.push(vs)
333
290
  else:
334
291
  vs = vd.cmdlog
335
292
 
336
- vd.moveToReplayContext(r, vs)
337
-
338
- if r.comment:
339
- vd.status(r.comment)
293
+ try:
294
+ vd.moveToReplayContext(r, vs)
295
+ if r.comment:
296
+ vd.status(r.comment)
340
297
 
341
- vd.keystrokes = r.keystrokes
342
- # <=v1.2 used keystrokes in longname column; getCommand fetches both
343
- escaped = vs.execCommand(longname if longname else r.keystrokes, keystrokes=r.keystrokes)
298
+ # <=v1.2 used keystrokes in longname column; getCommand fetches both
299
+ escaped = vs.execCommand(longname if longname else r.keystrokes, keystrokes=r.keystrokes)
300
+ except Exception as e:
301
+ vd.exceptionCaught(e)
302
+ escaped = True
344
303
 
345
304
  vd.currentReplayRow = None
346
305
 
@@ -350,10 +309,21 @@ def replayOne(vd, r):
350
309
 
351
310
 
352
311
  @VisiData.api
353
- def replay_sync(vd, cmdlog, live=False):
354
- 'Replay all commands in log.'
312
+ class DisableAsync:
313
+ def __enter__(self):
314
+ vd.execAsync = vd.execSync
315
+
316
+ def __exit__(self, exc_type, exc_val, tb):
317
+ vd.execAsync = lambda *args, vd=vd, **kwargs: visidata.VisiData.execAsync(vd, *args, **kwargs)
318
+
319
+
320
+ @VisiData.api
321
+ def replay_sync(vd, cmdlog):
322
+ 'Replay all commands in *cmdlog*.'
323
+ with vd.DisableAsync():
355
324
  cmdlog.cursorRowIndex = 0
356
325
  vd.currentReplay = cmdlog
326
+
357
327
  with Progress(total=len(cmdlog.rows)) as prog:
358
328
  while cmdlog.cursorRowIndex < len(cmdlog.rows):
359
329
  if vd.currentReplay is None:
@@ -376,22 +346,16 @@ def replay_sync(vd, cmdlog, live=False):
376
346
 
377
347
  if vd.activeSheet:
378
348
  vd.activeSheet.ensureLoaded()
379
- vd.sync()
380
- while not vd.delay():
381
- pass
382
349
 
383
350
  vd.status('replay complete')
384
351
  vd.currentReplay = None
385
352
 
386
353
 
387
354
  @VisiData.api
388
- @asyncthread
389
355
  def replay(vd, cmdlog):
390
- 'Inject commands into live execution with interface.'
391
- for thread in vd.threads:
392
- if thread.name == 'replay':
393
- thread.noblock = True
394
- vd.replay_sync(cmdlog, live=True)
356
+ 'Inject commands into live execution with interface.'
357
+ vd.push(cmdlog)
358
+ vd._nextCommands.extend(cmdlog.rows)
395
359
 
396
360
 
397
361
  @VisiData.api
@@ -413,8 +377,9 @@ def setLastArgs(vd, args):
413
377
 
414
378
  @VisiData.property
415
379
  def replayStatus(vd):
416
- x = options.disp_replay_pause if vd.paused else options.disp_replay_play
417
- return ' %s %s/%s' % (x, vd.currentReplay.cursorRowIndex, len(vd.currentReplay.rows))
380
+ if vd._nextCommands:
381
+ return f' | [:status_replay] {len(vd._nextCommands)} {vd.options.disp_replay_play}[:]'
382
+ return ''
418
383
 
419
384
 
420
385
  @BaseSheet.property
@@ -427,7 +392,14 @@ def cmdlog(sheet):
427
392
 
428
393
  @BaseSheet.lazy_property
429
394
  def cmdlog_sheet(sheet):
430
- return CommandLogJsonl(sheet.name+'_cmdlog', source=sheet, rows=[])
395
+ c = CommandLogJsonl(sheet.name+'_cmdlog', source=sheet, rows=[])
396
+ # copy over all existing globally set options
397
+ # you only need to do this for the first BaseSheet in a tree
398
+ if not isinstance(sheet.source, BaseSheet):
399
+ for r in vd.cmdlog.rows:
400
+ if r.sheet == 'global' and (r.longname == 'set-option') or (r.longname == 'unset-option'):
401
+ c.addRow(r)
402
+ return c
431
403
 
432
404
 
433
405
  @BaseSheet.property
@@ -451,7 +423,7 @@ def shortcut(self):
451
423
  def cmdlog(vd):
452
424
  if not vd._cmdlog:
453
425
  vd._cmdlog = CommandLogJsonl('cmdlog', rows=[]) # no reload
454
- vd._cmdlog.reloadCols()
426
+ vd._cmdlog.resetCols()
455
427
  vd.beforeExecHooks.append(vd._cmdlog.beforeExecHook)
456
428
  return vd._cmdlog
457
429
 
@@ -488,27 +460,30 @@ BaseSheet.init('_shortcut')
488
460
  globalCommand('gD', 'cmdlog-all', 'vd.push(vd.cmdlog)', 'open global CommandLog for all commands executed in current session')
489
461
  globalCommand('D', 'cmdlog-sheet', 'vd.push(sheet.cmdlog)', "open current sheet's CommandLog with all other loose ends removed; includes commands from parent sheets")
490
462
  globalCommand('zD', 'cmdlog-sheet-only', 'vd.push(sheet.cmdlog_sheet)', 'open CommandLog for current sheet with commands from parent sheets removed')
491
- globalCommand('^D', 'save-cmdlog', 'saveSheets(inputPath("save cmdlog to: ", value=fnSuffix(name)), vd.cmdlog, confirm_overwrite=options.confirm_overwrite)', 'save CommandLog to filename.vd file')
492
- globalCommand('^U', 'replay-pause', 'vd.replay_pause()', 'pause/resume replay')
493
- globalCommand('^N', 'replay-advance', 'vd.replay_advance()', 'execute next row in replaying sheet')
494
- globalCommand('^K', 'replay-stop', 'vd.replay_cancel()', 'cancel current replay')
463
+ BaseSheet.addCommand('^D', 'save-cmdlog', 'saveSheets(inputPath("save cmdlog to: ", value=fnSuffix(name)), vd.cmdlog)', 'save CommandLog to filename.vdj file')
464
+ BaseSheet.bindkey('^N', 'no-op')
465
+ BaseSheet.addCommand('^K', 'replay-stop', 'vd.replay_cancel(); vd.warning("replay canceled")', 'cancel current replay')
495
466
 
496
467
  globalCommand(None, 'show-status', 'status(input("status: "))', 'show given message on status line')
497
468
  globalCommand('^V', 'show-version', 'status(__version_info__);', 'Show version and copyright information on status line')
498
469
  globalCommand('z^V', 'check-version', 'checkVersion(input("require version: ", value=__version_info__))', 'check VisiData version against given version')
499
470
 
500
- globalCommand(' ', 'exec-longname', 'exec_longname(inputLongname())', 'execute command by its longname')
501
-
502
471
  CommandLog.addCommand('x', 'replay-row', 'vd.replayOne(cursorRow); status("replayed one row")', 'replay command in current row')
503
472
  CommandLog.addCommand('gx', 'replay-all', 'vd.replay(sheet)', 'replay contents of entire CommandLog')
504
- CommandLog.addCommand('^C', 'replay-stop', 'sheet.cursorRowIndex = sheet.nRows', 'abort replay')
505
473
 
506
474
  CommandLogJsonl.addCommand('x', 'replay-row', 'vd.replayOne(cursorRow); status("replayed one row")', 'replay command in current row')
507
475
  CommandLogJsonl.addCommand('gx', 'replay-all', 'vd.replay(sheet)', 'replay contents of entire CommandLog')
508
- CommandLogJsonl.addCommand('^C', 'replay-stop', 'sheet.cursorRowIndex = sheet.nRows', 'abort replay')
509
476
 
510
477
  CommandLog.options.json_sort_keys = False
511
478
  CommandLog.options.encoding = 'utf-8'
512
479
  CommandLogJsonl.options.json_sort_keys = False
513
480
 
514
- vd.addGlobals({"CommandLogBase": CommandLogBase})
481
+ vd.addGlobals(CommandLogBase=CommandLogBase, CommandLogRow=CommandLogRow)
482
+
483
+ vd.addMenuItems('''
484
+ View > Command log > this sheet > cmdlog-sheet
485
+ View > Command log > this sheet only > cmdlog-sheet-only
486
+ View > Command log > all commands > cmdlog-all
487
+ System > Execute longname > exec-longname
488
+ Help > Version > show-version
489
+ ''')
visidata/color.py CHANGED
@@ -1,45 +1,83 @@
1
1
  import curses
2
2
  import functools
3
3
  from copy import copy
4
+ from collections import namedtuple
5
+ from dataclasses import dataclass
4
6
 
5
- from visidata import options, Extensible, drawcache, drawcache_property, VisiData
7
+ from visidata import vd, options, Extensible, drawcache, drawcache_property, VisiData
6
8
  import visidata
7
- from collections import namedtuple
8
9
 
9
- __all__ = ['ColorAttr', 'colors', 'update_attr', 'ColorMaker']
10
+ __all__ = ['ColorAttr', 'colors', 'update_attr', 'ColorMaker', 'rgb_to_attr']
10
11
 
12
+ vd.help_color = '''Color syntax: `<attribute> <fg-color> on <bg-color>`
11
13
 
12
- ColorAttr = namedtuple('ColorAttr', ('color', 'attributes', 'precedence', 'attr'))
14
+ - attributes: [:bold]bold[/] [:underline]underline[/] [:italic]italic[/] [:reverse]reverse[/]
15
+ - colors: 0-255 or [:black on 238]black[/] [:red on 238]red[/] [:green on 238]green[/] [:yellow on 238]yellow[/] [:blue on 238]blue[/] [:magenta on 238]magenta[/] [:cyan on 238]cyan[/] [:white on 238]white[/]
16
+ - the second color is used as a fallback if the first color is not available
13
17
 
18
+ See [:onclick https://visidata.org/docs/colors]https://visidata.org/docs/colors[/] for more detailed info.
19
+ '''
14
20
 
15
- def update_attr(oldattr, updattr, updprec=None):
16
- if isinstance(updattr, ColorAttr):
17
- if updprec is None:
18
- updprec = updattr.precedence
19
- updcolor = updattr.color
20
- updattr = updattr.attributes
21
- else:
22
- updcolor = updattr & curses.A_COLOR
23
- updattr = updattr & ~curses.A_COLOR
24
- if updprec is None:
25
- updprec = 0
21
+
22
+ @dataclass
23
+ class ColorAttr:
24
+ fg:int = -1 # default is no foreground specified
25
+ bg:int = -1 # default is no background specified
26
+ attributes:int = 0 # default is no attributes
27
+ precedence:int = 0 # default is lowest priority
28
+ colorname:str = ''
29
+
30
+ def __init__(self, fg:int=-1, bg:int=-1, attributes:int=0, precedence:int=0, colorname:str=''):
31
+ assert fg < 256, fg
32
+ assert bg < 256, bg
33
+ self.fg = fg
34
+ self.bg = bg
35
+ self.attributes = attributes
36
+ self.precedence = precedence
37
+ self.colorname = colorname
38
+
39
+ def update(self, b:'ColorAttr', prec:int=10) -> 'ColorAttr':
40
+ return update_attr(self, b, prec)
41
+
42
+ @property
43
+ def attr(self) -> int:
44
+ a = colors._get_colorpair(self.fg, self.bg, self.colorname) | self.attributes
45
+ assert a >= 0, a
46
+ return a
47
+
48
+ def as_str(self) -> str:
49
+ attrnames = [attrname for attrname, attr in colors._attrs.items() if self.attributes & attr]
50
+ attrnames.append(f'{self.fg} on {self.bg}')
51
+ return ' '.join(attrnames)
52
+
53
+
54
+ def update_attr(oldattr:ColorAttr, updattr:ColorAttr, updprec:int=None) -> ColorAttr:
55
+ assert isinstance(updattr, ColorAttr), updattr
56
+ if updprec is None:
57
+ updprec = updattr.precedence
58
+ updfg = updattr.fg
59
+ updbg = updattr.bg
60
+ updattr = updattr.attributes
26
61
 
27
62
  # starting values, work backwards
28
- newcolor = oldattr.color
63
+ newfg = oldattr.fg
64
+ newbg = oldattr.bg
29
65
  newattr = oldattr.attributes | updattr
30
66
  newprec = oldattr.precedence
31
67
 
32
- if not newcolor or updprec > newprec:
33
- if updcolor:
34
- newcolor = updcolor
35
- newprec = updprec
68
+ if newfg < 0 or (updfg >= 0 and updprec > newprec):
69
+ newfg = updfg
70
+ if newbg < 0 or (updbg >= 0 and updprec > newprec):
71
+ newbg = updbg
36
72
 
37
- return ColorAttr(newcolor, newattr, newprec, newcolor | newattr)
73
+ newprec = max(updprec, newprec)
74
+
75
+ return ColorAttr(newfg, newbg, newattr, newprec)
38
76
 
39
77
 
40
78
  class ColorMaker:
41
79
  def __init__(self):
42
- self.color_pairs = {} # (fg,bg) -> (color_attr, colornamestr) (can be or'ed with other attrs)
80
+ self.color_pairs = {} # (fg,bg) -> (pairnum, colornamestr) (pairnum can be or'ed with other attrs)
43
81
  self.color_cache = {} # colorname -> colorpair
44
82
 
45
83
  @drawcache_property
@@ -47,30 +85,44 @@ class ColorMaker:
47
85
  return {}
48
86
 
49
87
  def setup(self):
50
- curses.use_default_colors()
88
+ try:
89
+ curses.use_default_colors()
90
+ except Exception as e:
91
+ pass
51
92
 
52
93
  @drawcache_property
53
94
  def colors(self):
54
95
  'not computed until curses color has been initialized'
55
96
  return {x[6:]:getattr(curses, x) for x in dir(curses) if x.startswith('COLOR_') and x != 'COLOR_PAIRS'}
56
97
 
57
- def __getitem__(self, colornamestr):
58
- return self._colornames_to_cattr(colornamestr).attr
98
+ def __getitem__(self, colorname:str) -> ColorAttr:
99
+ 'colors["green"] or colors["foo"] returns parsed ColorAttr.'
100
+ return self.get_color(colorname)
59
101
 
60
- def __getattr__(self, optname):
61
- 'colors.color_foo returns colors[options.color_foo]'
62
- return self.get_color(optname).attr
102
+ def __getattr__(self, optname) -> ColorAttr:
103
+ 'colors.color_foo or colors.foo returns parsed ColorAttr.'
104
+ return self.get_color(optname)
63
105
 
64
106
  @drawcache
65
107
  def resolve_colors(self, colorstack):
66
108
  'Returns the ColorAttr for the colorstack, a list of (prec, color_option_name) sorted highest-precedence color first.'
67
- cattr = ColorAttr(0,0,0,0)
109
+ cattr = ColorAttr()
68
110
  for prec, coloropt in colorstack:
69
111
  c = self.get_color(coloropt)
70
112
  cattr = update_attr(cattr, c, prec)
71
113
  return cattr
72
114
 
73
- def split_colorstr(self, colorstr):
115
+ def get_color(self, optname:str, precedence:int=0) -> ColorAttr:
116
+ '''Return ColorAttr for options.color_foo if *optname* of either "foo" or "color_foo",
117
+ Otherwise parse *optname* for colorstring like "bold 34 red on 135 blue".'''
118
+ r = self.colorcache.get(optname, None)
119
+ if r is None:
120
+ coloropt = vd.options._get(optname) or vd.options._get(f'color_{optname}')
121
+ colornamestr = coloropt.value if coloropt else optname
122
+ r = self.colorcache[optname] = self._colornames_to_cattr(colornamestr, precedence)
123
+ return r
124
+
125
+ def _split_colorstr(self, colorstr):
74
126
  'Return (fgstr, bgstr, attrlist) parsed from colorstr.'
75
127
  fgbgattrs = ['', '', []] # fgstr, bgstr, attrlist
76
128
  if not colorstr:
@@ -92,12 +144,19 @@ class ColorMaker:
92
144
  if not fgbgattrs[i]: # keep first known color
93
145
  if self._get_colornum(x) is not None: # only set known colors
94
146
  fgbgattrs[i] = x
147
+ else:
148
+ fgbgattrs[i] = None
95
149
 
96
150
  return fgbgattrs
97
151
 
98
- def _get_colornum(self, colorname, default=-1):
152
+ def _get_colornum(self, colorname:'str|int', default:int=-1) -> int:
99
153
  'Return terminal color number for colorname.'
100
- if not colorname: return default
154
+ if isinstance(colorname, int):
155
+ return colorname
156
+
157
+ if not colorname:
158
+ return default
159
+
101
160
  r = self.color_cache.get(colorname, None)
102
161
  if r is not None:
103
162
  return r
@@ -119,41 +178,68 @@ class ColorMaker:
119
178
  except ValueError: # Python 3.10+ issue #1227
120
179
  return None
121
180
 
122
- @drawcache
123
- def _colornames_to_cattr(self, colornamestr, precedence=0):
124
- fg, bg, attrlist = self.split_colorstr(colornamestr)
181
+ def _attrnames_to_num(self, attrnames:'list[str]') -> int:
125
182
  attrs = 0
126
- for attr in attrlist:
183
+ for attr in attrnames:
127
184
  attrs |= getattr(curses, 'A_'+attr.upper())
185
+ return attrs
128
186
 
129
- if not fg and not bg:
130
- color = 0
131
- else:
132
- deffg, defbg, _ = self.split_colorstr(options.color_default)
133
- fgbg = (self._get_colornum(fg, self._get_colornum(deffg)),
134
- self._get_colornum(bg, self._get_colornum(defbg)))
135
- pairnum, _ = self.color_pairs.get(fgbg, (None, ''))
187
+ @drawcache_property
188
+ def _attrs(self):
189
+ return {k[2:].lower():getattr(curses, k) for k in dir(curses) if k.startswith('A_') and k != 'A_ATTRIBUTES'}
190
+
191
+ @drawcache
192
+ def _colornames_to_cattr(self, colorname:str, precedence=0) -> ColorAttr:
193
+ fg, bg, attrlist = self._split_colorstr(colorname)
194
+
195
+ fg = self._get_colornum(fg)
196
+ bg = self._get_colornum(bg)
197
+ return ColorAttr(fg, bg,
198
+ self._attrnames_to_num(attrlist),
199
+ precedence, colorname)
200
+
201
+ def _get_colorpair(self, fg:'int|None', bg:'int|None', colorname:str) -> int:
202
+ pairnum, _ = self.color_pairs.get((fg, bg), (None, ''))
136
203
  if pairnum is None:
137
204
  if len(self.color_pairs) > 254:
138
205
  self.color_pairs.clear() # start over
139
206
  self.color_cache.clear()
140
207
  pairnum = len(self.color_pairs)+1
208
+ if fg is None: fg = -1
209
+ if bg is None: bg = -1
141
210
  try:
142
- curses.init_pair(pairnum, *fgbg)
211
+ curses.init_pair(pairnum, fg, bg)
143
212
  except curses.error as e:
144
- return ColorAttr(0, attrs, precedence, attrs)
145
- self.color_pairs[fgbg] = (pairnum, colornamestr)
213
+ return 0 # do not cache
214
+ self.color_pairs[(fg, bg)] = (pairnum, colorname)
146
215
 
147
- color = curses.color_pair(pairnum)
148
- return ColorAttr(color, attrs, precedence, color | attrs)
216
+ return curses.color_pair(pairnum)
149
217
 
150
- def get_color(self, optname, precedence=0):
151
- 'colors.color_foo returns colors[options.color_foo]'
152
- r = self.colorcache.get(optname, None)
153
- if r is None:
154
- coloropt = options._get(optname)
155
- colornamestr = coloropt.value if coloropt else optname
156
- r = self.colorcache[optname] = self._colornames_to_cattr(colornamestr, precedence)
157
- return r
158
218
 
159
219
  colors = ColorMaker()
220
+
221
+ def rgb_to_xterm256(r:int,g:int,b:int,a:int=255) -> int:
222
+ if a == 0:
223
+ return -1
224
+
225
+ if max(r,g,b) - min(r,g,b) < 8:
226
+ if r <= 4: return 16
227
+ elif r <= 8: return 232
228
+ elif r >= 247: return 231
229
+ elif r >= 238: return 255
230
+ else:
231
+ return int(232 + (r-8)//10)
232
+ else:
233
+ r = max(0, r-(95-40)) // 40
234
+ g = max(0, g-(95-40)) // 40
235
+ b = max(0, b-(95-40)) // 40
236
+ return int(16 + r*36 + g*6 + b)
237
+
238
+
239
+ @functools.lru_cache(256)
240
+ def rgb_to_attr(r:int,g:int,b:int,a:int=255) -> str:
241
+ return str(rgb_to_xterm256(r,g,b,a))
242
+
243
+
244
+ import sys
245
+ vd.addGlobals({k:getattr(sys.modules[__name__], k) for k in __all__})