visidata 2.11.1__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 (255) 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 +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. {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 +59 -50
  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 +5 -1
  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 +30 -5
  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 +163 -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} +75 -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 +180 -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 +17 -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 +48 -10
  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 +301 -148
  187. visidata/man/vd.txt +290 -153
  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 +50 -201
  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 +229 -257
  206. visidata/shell.py +51 -21
  207. visidata/sidebar.py +162 -0
  208. visidata/sort.py +11 -4
  209. visidata/statusbar.py +113 -104
  210. visidata/stored_list.py +43 -0
  211. visidata/stored_prop.py +38 -0
  212. visidata/tests/conftest.py +3 -3
  213. visidata/tests/test_cliptext.py +39 -0
  214. visidata/tests/test_commands.py +62 -7
  215. visidata/tests/test_edittext.py +2 -2
  216. visidata/tests/test_features.py +17 -0
  217. visidata/tests/test_menu.py +14 -0
  218. visidata/tests/test_path.py +13 -4
  219. visidata/text_source.py +53 -0
  220. visidata/textsheet.py +10 -3
  221. visidata/theme.py +44 -0
  222. visidata/themes/__init__.py +0 -0
  223. visidata/themes/ascii8.py +84 -0
  224. visidata/themes/asciimono.py +84 -0
  225. visidata/themes/light.py +17 -0
  226. visidata/threads.py +87 -39
  227. visidata/tuiwin.py +22 -0
  228. visidata/type_currency.py +22 -3
  229. visidata/type_date.py +31 -9
  230. visidata/type_floatsi.py +5 -1
  231. visidata/undo.py +17 -5
  232. visidata/utils.py +106 -23
  233. visidata/vdobj.py +28 -17
  234. visidata/windows.py +10 -0
  235. visidata/wrappers.py +9 -3
  236. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  237. {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/vd.1 +301 -148
  238. {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +301 -148
  239. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  240. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/METADATA +12 -8
  241. visidata-3.0.dist-info/RECORD +257 -0
  242. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  243. vgit/__init__.py +0 -1
  244. vgit/gitsheet.py +0 -164
  245. visidata/layout.py +0 -44
  246. visidata/misc.py +0 -5
  247. visidata-2.11.1.data/scripts/vgit +0 -9
  248. visidata-2.11.1.dist-info/RECORD +0 -155
  249. {vgit → visidata/apps/vgit}/__main__.py +0 -0
  250. {vgit → visidata/apps/vgit}/abort.py +0 -0
  251. /visidata/{repeat.py → features/repeat.py} +0 -0
  252. {visidata-2.11.1.data → visidata-3.0.data}/scripts/vd +0 -0
  253. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  254. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -0
  255. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,45 @@
1
+ from visidata import Column, vd, ColumnExpr, CompleteExpr, EscapeException, Sheet
2
+
3
+
4
+ @Column.api
5
+ def updateExpr(col, val):
6
+ col.name = val
7
+ try:
8
+ col.expr = val
9
+ except SyntaxError:
10
+ col.expr = None
11
+
12
+ col.sheet.draw(col.sheet._scr)
13
+
14
+
15
+ @Column.api # expr.setter
16
+ def expr(self, expr):
17
+ try:
18
+ self.compiledExpr = compile(expr, '<expr>', 'eval') if expr else None
19
+ self._expr = expr
20
+ except SyntaxError as e:
21
+ self._expr = None
22
+
23
+
24
+ @Sheet.api
25
+ def addcol_expr(sheet):
26
+ try:
27
+ c = sheet.addColumnAtCursor(ColumnExpr("", width=sheet.options.default_width))
28
+ oldidx = sheet.cursorVisibleColIndex
29
+ sheet.cursorVisibleColIndex = sheet.visibleCols.index(c)
30
+
31
+ expr = sheet.editCell(sheet.cursorVisibleColIndex, -1,
32
+ completer=CompleteExpr(sheet),
33
+ updater=lambda val,col=c: col.updateExpr(val))
34
+
35
+ c.expr = expr or vd.fail("no expr")
36
+ c.name = expr
37
+ c.width = None
38
+ except (Exception, EscapeException):
39
+ sheet.columns.remove(c)
40
+ sheet.cursorVisibleColIndex = oldidx
41
+ raise
42
+
43
+
44
+ Sheet.addCommand(None, 'addcol-expr', 'sheet.addcol_expr()', "create new column from Python expression, updating the column's calculated values live")
45
+ Sheet.addCommand(None, 'addcol-new', 'c=addColumnAtIndex(SettableColumn(width=options.default_width)); draw(sheet._scr); cursorVisibleColIndex=visibleCols.index(c); c.name=editCell(cursorVisibleColIndex, -1); c.width=None', 'append new column, updating the column name live')
@@ -0,0 +1,133 @@
1
+ '''
2
+ Marking selected rows with a keystroke, selecting marked rows,
3
+ and viewing lists of marks and their rows.
4
+ '''
5
+
6
+ from copy import copy
7
+ from visidata import vd, asyncthread, vlen, VisiData, TableSheet, ColumnItem, RowColorizer
8
+
9
+ @VisiData.lazy_property
10
+ def marks(vd):
11
+ return MarksSheet('marks')
12
+
13
+
14
+ class MarkSheet(TableSheet):
15
+ pass
16
+
17
+
18
+ class MarksSheet(TableSheet):
19
+ '''
20
+ The Marks Sheet shows all marks in use (on all sheets) and how many rows have each mark.
21
+ '''
22
+ rowtype = "marks" # rowdef: [mark, color, [rows]]
23
+ columns = [
24
+ ColumnItem('mark', 0),
25
+ ColumnItem('color', 1),
26
+ ColumnItem('rows', 2, type=vlen),
27
+ ]
28
+ colorizers = [
29
+ RowColorizer(2, None, lambda s,c,r,v: r and r[1])
30
+ ]
31
+ def __init__(self, *args, **kwargs):
32
+ super().__init__(*args, **kwargs)
33
+ self.marknotes = list('0123456789')
34
+ self.marks = [] #
35
+ self.markedRows = {} # rowid(row): [row, set(marks)]
36
+ self.rows = []
37
+
38
+ def getColor(self, sheet, row):
39
+ mark = self.getMark(sheet, row)
40
+ if not mark:
41
+ return ''
42
+ return self.getMarkRow(sheet, mark)[1]
43
+
44
+ def getMark(self, sheet, row):
45
+ mrow = self.markedRows.get(sheet.rowid(row), None)
46
+ if not mrow:
47
+ return ''
48
+ if mrow[1]:
49
+ return next(iter(mrow[1])) # first item in set
50
+
51
+ def getMarks(self, row):
52
+ 'Return set of all marks for given row'
53
+ return self.markedRows[self.rowid(row)][1]
54
+
55
+ def isMarked(self, row, mark):
56
+ 'Return True if given row has given mark'
57
+ return mark in self.getMarks(row)
58
+
59
+ def getMarkRow(self, sheet, mark):
60
+ for r in self.rows:
61
+ if r[0] == mark:
62
+ return r
63
+ r = [mark, 'color_note_type', MarkSheet('mark_', rows=[], columns=copy(sheet.columns))]
64
+ self.addRow(r)
65
+ return r
66
+
67
+ def setMark(self, sheet, row, mark):
68
+ rowid = self.rowid(row)
69
+ if rowid not in self.markedRows:
70
+ self.markedRows[rowid] = [row, set(mark)]
71
+ else:
72
+ self.markedRows[rowid][1].add(mark)
73
+
74
+ vd.marks.getMarkRow(sheet, mark)[2].addRow(row)
75
+
76
+ def unsetMark(self, sheet, row, mark):
77
+ rowid = self.rowid(row)
78
+ if rowid in self.markedRows:
79
+ self.markedRows[rowid][1].remove(mark)
80
+ vd.marks.getMarkRow(sheet, mark)[2].deleteBy(lambda r,x=row: r is x)
81
+
82
+ def inputmark(self):
83
+ return vd.inputsingle('mark: ') or self.marknotes.pop(0)
84
+
85
+ def openRow(self, row):
86
+ return row[2]
87
+
88
+
89
+ @VisiData.api
90
+ @asyncthread
91
+ def mark(vd, sheet, rows, m):
92
+ for r in rows:
93
+ vd.marks.setMark(sheet, r, m)
94
+
95
+ @VisiData.api
96
+ @asyncthread
97
+ def unmark(vd, sheet, rows, m):
98
+ for r in rows:
99
+ vd.marks.unsetMark(sheet, r, m)
100
+
101
+
102
+ vd.rowNoters.insert(0, lambda sheet, row: vd.marks.getMark(sheet, row))
103
+
104
+ TableSheet.colorizers.append(RowColorizer(2, None, lambda s,c,r,v: not c and r and vd.marks.getColor(s, r)))
105
+
106
+ TableSheet.addCommand('', 'mark-row', 'vd.mark(sheet, [cursorRow], vd.marks.inputmark())', '')
107
+ TableSheet.addCommand('', 'unmark-row', 'vd.unmark(sheet, [cursorRow], vd.marks.inputmark())', '')
108
+ TableSheet.addCommand('', 'mark-selected', 'vd.mark(sheet, selectedRows, vd.marks.inputmark())', '')
109
+ TableSheet.addCommand('', 'unmark-selected', 'vd.unmark(sheet, selectedRows, vd.marks.inputmark())', '')
110
+
111
+ TableSheet.addCommand('', 'select-marks', 'select(gatherBy(lambda r,mark=vd.marks.inputmark(): vd.marks.isMarked(r, mark)), progress=False)', '')
112
+ TableSheet.addCommand('', 'stoggle-marks', 'toggle(gatherBy(lambda r,mark=vd.marks.inputmark(): vd.marks.isMarked(r, mark)), progress=False)', '')
113
+ TableSheet.addCommand('', 'unselect-marks', 'unselect(gatherBy(lambda r,mark=vd.marks.inputmark(): vd.marks.isMarked(r, mark)), progress=False)', '')
114
+
115
+ TableSheet.addCommand('', 'open-marks', 'vd.push(vd.marks)', '')
116
+
117
+ TableSheet.addCommand('', 'go-prev-mark', 'moveToNextRow(lambda row,mark=vd.marks.inputmark(): vd.marks.isMarked(row, mark), reverse=True, msg="no previous marked row")', 'go up current column to previous row with given mark')
118
+ TableSheet.addCommand('', 'go-next-mark', 'moveToNextRow(lambda row,mark=vd.marks.inputmark(): vd.marks.isMarked(row, mark), msg="no next marked row")', 'go down current column to next row with given mark')
119
+
120
+
121
+ vd.addMenuItems('''
122
+ View > Marks > open-marks
123
+ Row > Mark > open Marks Sheet > open-marks
124
+ Row > Mark > current row > mark-row
125
+ Row > Mark > selected rows > mark-selected
126
+ Row > Unmark > current row > unmark-row
127
+ Row > Unmark > selected rows > unmark-selected
128
+ Row > Select > marked rows > select-marks
129
+ Row > Unselect > marked rows > unselect-marks
130
+ Row > Toggle select > marked rows > stoggle-marks
131
+ Row > Goto > next marked row > go-next-mark
132
+ Row > Goto > previous marked row > go-prev-mark
133
+ ''')
@@ -0,0 +1 @@
1
+ from . import tapestry
@@ -0,0 +1,147 @@
1
+ import json
2
+ import time
3
+ import collections
4
+ import random
5
+ from functools import cached_property
6
+ from base64 import b64encode
7
+
8
+ from visidata import vd, VisiData, Canvas, Animation, Path, asyncthread, clipdraw, colors, ItemColumn, Sheet, wraptext
9
+
10
+ vd.theme_option('color_hint', 'black on yellow', '')
11
+
12
+ @VisiData.api
13
+ def getNoahsPath(vd, name):
14
+ return Path(vd.pkg_resources_files('visidata')/f'experimental/noahs_tapestry/{name}')
15
+
16
+ @VisiData.api
17
+ def openNoahsText(vd, name):
18
+ return vd.getNoahsPath(name).open(encoding='utf-8')
19
+
20
+ @VisiData.cached_property
21
+ def noahsDatabase(vd):
22
+ return vd.open_sqlite(vd.getNoahsPath('noahs.sqlite'))
23
+
24
+ class NoahsPuzzle(Sheet):
25
+ guide = '''
26
+ # Puzzle {sheet.puznum}
27
+ - `Shift+A` to input a solution to the puzzle
28
+ - `Shift+Y` to attempt the current cell as the solution
29
+ - `Shift+B` to open Noah's Database Backup
30
+ - `Shift+V` to view Noah's Tapestry
31
+ '''
32
+ rowtype = 'lines' # rowdef: [linenum, text]
33
+ filetype = 'txt'
34
+ columns = [
35
+ ItemColumn('linenum', 0, type=int, width=0),
36
+ ItemColumn('text', 1, width=80, displayer='full'),
37
+ ]
38
+ precious = False
39
+
40
+ def iterload(self):
41
+ clues = json.loads(vd.getNoahsPath(f'clues.json').read_text())
42
+ source = vd.getNoahsPath(f'puzzle{self.puznum}.md')
43
+ winWidth = 78
44
+ formatted_text = source.open(encoding='utf-8').read().format(**clues)
45
+ for startingLine, text in enumerate(formatted_text.splitlines()):
46
+ text = text.strip()
47
+ if text:
48
+ for i, (L, _) in enumerate(wraptext(str(text), width=winWidth)):
49
+ yield [startingLine+i+1, L]
50
+ else:
51
+ yield [startingLine+1, text]
52
+
53
+ @VisiData.cached_property
54
+ def noahsSolutions(vd):
55
+ return json.loads(vd.getNoahsPath(f'solutions.json').read_text())
56
+
57
+ @VisiData.api
58
+ def solve_puzzle(vd, answer):
59
+ puznum = vd.noahsCurrentPuznum
60
+ if b64encode(answer.encode()).decode() != vd.noahsSolutions[f'p{puznum}']:
61
+ vd.fail("Hmmm, that doesn't seem right. Try again?")
62
+
63
+ vd.noahsTapestry.solved.add(puznum)
64
+ vd.status(f'Correct! The candle is now lit.')
65
+ vd.push(vd.noahsTapestry)
66
+
67
+
68
+ class Tapestry(Canvas):
69
+ @property
70
+ def guide(self):
71
+ ret = ''
72
+ if vd.screenWidth < 120 or vd.screenHeight < 50:
73
+ ret = f'''
74
+ # [:black on yellow]WARNING: TERMINAL TOO SMALL[/]
75
+ Please expand your terminal to at least 120x50 (currently {vd.screenWidth}x{vd.screenHeight})
76
+ '''
77
+
78
+ ret += '''
79
+ # Noah's Tapestry
80
+ An interactive data game
81
+
82
+ - `Shift+N` to open the next puzzle
83
+ '''
84
+
85
+ return ret
86
+
87
+ solved = set()
88
+ def reload(self):
89
+ self.noahs_menorah = Animation(vd.openNoahsText('menorah.ddw'))
90
+ self.noahs_tapestry = Animation(vd.openNoahsText('tapestry.ddw'))
91
+ self.noahs_flame = Animation(vd.openNoahsText('flame.ddw'))
92
+ self.solved = set()
93
+
94
+ # self.keep_running()
95
+
96
+ @asyncthread
97
+ def keep_running(self):
98
+ while True:
99
+ time.sleep(1)
100
+
101
+ def draw(self, scr):
102
+ solvedays = ['menorah']+[f'day{i}' for i in self.solved]
103
+ t = time.time()
104
+ self.noahs_menorah.draw(scr, t=0, y=30, x=19)
105
+ if not self.solved:
106
+ clipdraw(scr, 22, 52, "Light the [:italic]shamash[/]", colors['255'])
107
+ return
108
+
109
+ self.noahs_menorah.draw(scr, t=0.1, y=30, x=19, tags=solvedays)
110
+ self.noahs_tapestry.draw(scr, t=t, tags=solvedays)
111
+
112
+ for i in self.solved:
113
+ xs = [58, 22+9*8, 22+9*7, 22+9*6, 22+9*5, 22+27, 22+18, 22+9, 22, 22]
114
+ ys = [28, 32, 32, 32, 32, 32, 32, 32, 32, 32]
115
+ x = xs[i]
116
+ y = ys[i]
117
+
118
+ self.noahs_flame.draw(scr, t=t+(i+random.random())*0.2, y=y, x=x)
119
+
120
+ def open_puzzle(self, puznum=None):
121
+ if puznum is None:
122
+ puznum = 0
123
+ if self.solved:
124
+ puznum = max(self.solved)+1
125
+
126
+ vs = NoahsPuzzle('puzzle', str(puznum), source=self, puznum=puznum)
127
+ vs.ensureLoaded()
128
+ vd.noahsCurrentPuznum = puznum
129
+ return vs
130
+
131
+
132
+ @VisiData.lazy_property
133
+ def noahsTapestry(vd):
134
+ vd.curses_timeout = 50
135
+ return Tapestry('noahs', 'tapestry')
136
+
137
+
138
+ NoahsPuzzle.options.color_default = '178 yellow on 232 black'
139
+
140
+ vd.addCommand('Shift+B', 'open-noahs-database', 'vd.push(noahsDatabase)', "open database for Noah's Tapestry")
141
+ vd.addCommand('Shift+V', 'open-noahs-tapestry', 'vd.push(noahsTapestry)', "open Noah's Tapestry")
142
+ Tapestry.addCommand('Shift+N', 'open-puzzle-next', 'vd.push(open_puzzle())', 'open next unsolved puzzle')
143
+ NoahsPuzzle.addCommand('Shift+A', 'solve-puzzle-input', 'solve_puzzle(input("Answer: "))', 'input an answer to the current puzzle')
144
+ Sheet.addCommand('Shift+Y', 'solve-puzzle-cell', 'solve_puzzle(cursorValue)', 'input an answer to the current puzzle')
145
+ for i in range(9):
146
+ Tapestry.addCommand(f'{i}', f'open-puzzle-{i}', f'vd.push(open_puzzle({i}))')
147
+ Tapestry.addCommand(f'Alt+{i}', f'solve-puzzle-force-{i}', f'sheet.solved.add({i})')
@@ -0,0 +1,73 @@
1
+ from visidata import *
2
+ from functools import wraps, partial
3
+
4
+
5
+ @asyncthread
6
+ @Sheet.api
7
+ def calcRowIndex(sheet, indexes):
8
+ for rownum, r in enumerate(sheet.rows):
9
+ indexes[sheet.rowid(r)] = rownum
10
+
11
+
12
+ @Sheet.lazy_property
13
+ def _rowindex(sheet):
14
+ ret = {}
15
+ sheet.calcRowIndex(ret)
16
+ return ret
17
+
18
+
19
+ @Sheet.api
20
+ def rowindex(sheet, row):
21
+ 'Returns the rowindex given the row. May spawn a thread to compute underlying _rowindex.'
22
+ return sheet._rowindex.get(sheet.rowid(row))
23
+
24
+
25
+ @Sheet.api
26
+ def prev(sheet, row):
27
+ 'Return the row previous to the given row.'
28
+ rownum = max(sheet.rowindex(row)-1, 0)
29
+ return LazyComputeRow(sheet, sheet.rows[rownum])
30
+
31
+
32
+ @Sheet.api
33
+ def addcol_rowindex(sheet, newcol):
34
+ oldAddRow = sheet.addRow
35
+ def rownum_addRow(sheet, col, row, index=None):
36
+ if index is None:
37
+ index = len(sheet.rows)
38
+
39
+ col._rowindex[sheet.rowid(row)] = index
40
+ return oldAddRow(row, index)
41
+
42
+ # wrapper addRow to keep the index up to date
43
+ sheet.addRow = wraps(oldAddRow)(partial(rownum_addRow, sheet, newcol))
44
+ sheet.addColumnAtCursor(newcol)
45
+
46
+ # spawn a little thread to calc the rowindex
47
+ sheet.calcRowIndex(newcol._rowindex)
48
+
49
+
50
+ @Sheet.api
51
+ def addcol_delta(sheet, vcolidx):
52
+ col = sheet.visibleCols[vcolidx]
53
+
54
+ newcol = ColumnExpr("delta_"+col.name,
55
+ type=col.type,
56
+ _rowindex={}, # [rowid(row)] -> rowidx
57
+ expr="{0}-prev(row).{0}".format(col.name))
58
+
59
+ sheet.addcol_rowindex(newcol)
60
+ return newcol
61
+
62
+ @Sheet.api
63
+ def addcol_rownum(sheet):
64
+ newcol = Column("rownum",
65
+ type=int,
66
+ _rowindex={}, # [rowid(row)] -> rowidx
67
+ getter=lambda col,row: col._rowindex.get(col.sheet.rowid(row)))
68
+
69
+ sheet.addcol_rowindex(newcol)
70
+ return newcol
71
+
72
+ Sheet.addCommand(None, 'addcol-rownum', 'addcol_rownum()', helpstr='add column with original row ordering')
73
+ Sheet.addCommand(None, 'addcol-delta', 'addcol_delta(cursorVisibleColIndex)', helpstr='add column with delta of current column')
@@ -0,0 +1,26 @@
1
+ '''
2
+ # TODO:
3
+ - slide-cells-left
4
+ - slide-cells-<dir>-n
5
+ - slide-cells-selected-<dir>-n
6
+ - rename "slide" to "shift"?
7
+ '''
8
+
9
+ from visidata import vd, TableSheet
10
+
11
+
12
+ @TableSheet.api
13
+ def slide_cells_right(sheet, row, vcolidx):
14
+ for oldcol, newcol in reversed(list(zip(sheet.visibleCols[vcolidx:], sheet.visibleCols[vcolidx+1:]))):
15
+ newcol.setValue(row, oldcol.getValue(row))
16
+
17
+ sheet.visibleCols[vcolidx].setValue(row, None)
18
+
19
+
20
+ TableSheet.addCommand('', 'slide-cells-right', 'slide_cells_right(cursorRow, cursorVisibleColIndex)', '''
21
+ Shift individual values in current row one visible column to the right, with leftmost cell set to null.
22
+ ''')
23
+
24
+ vd.addMenuItems('''
25
+ Edit > Slide > cells > right > slide-cells-right
26
+ ''')
visidata/expr.py CHANGED
@@ -38,10 +38,8 @@ def setValuesFromExpr(self, rows, expr):
38
38
  for row in Progress(rows, 'setting'):
39
39
  # Note: expressions that are only calculated once, do not need to pass column identity
40
40
  # they can reference their "previous selves" once without causing a recursive problem
41
- try:
42
- self.setValueSafe(row, self.sheet.evalExpr(compiledExpr, row))
43
- except Exception as e:
44
- vd.exceptionCaught(e)
41
+ v = vd.callNoExceptions(self.sheet.evalExpr, compiledExpr, row)
42
+ vd.callNoExceptions(self.setValue, row, v)
45
43
  self.recalc()
46
44
  vd.status('set %d values = %s' % (len(rows), expr))
47
45
 
@@ -59,3 +57,9 @@ Sheet.addCommand('gz=', 'setcol-iter', 'cursorCol.setValues(someSelectedRows, *l
59
57
  Sheet.addCommand(None, 'show-expr', 'status(evalExpr(inputExpr("show expr="), cursorRow))', 'evaluate Python expression on current row and show result on status line')
60
58
 
61
59
  vd.addGlobals({'CompleteExpr': CompleteExpr})
60
+
61
+ vd.addMenuItems('''
62
+ Edit > Modify > current cell > Python expression > setcell-expr
63
+ Edit > Modify > selected cells > Python sequence > setcol-expr
64
+ Column > Add column > Python expr > addcol-expr
65
+ ''')
visidata/extensible.py CHANGED
@@ -8,7 +8,7 @@ class Extensible:
8
8
 
9
9
  @classmethod
10
10
  def init(cls, membername, initfunc=lambda: None, copy=False):
11
- 'Append equivalent of ``self.<membername> = initfunc()`` to ``<cls>.__init__``. If *copy* is True, <membername> will be copied when object is copied.'
11
+ 'Prepend equivalent of ``self.<membername> = initfunc()`` to ``<cls>.__init__``. If *copy* is True, <membername> will be copied when object is copied.'
12
12
 
13
13
  def thisclass_hasattr(cls, k):
14
14
  return getattr(cls, k, None) is not getattr(cls.__bases__[0], k, None)
@@ -16,12 +16,12 @@ class Extensible:
16
16
  # must check hasattr first or else this might be parent's __init__
17
17
  oldinit = thisclass_hasattr(cls, '__init__') and getattr(cls, '__init__')
18
18
  def newinit(self, *args, **kwargs):
19
+ if not hasattr(self, membername): # can be overridden by a subclass
20
+ setattr(self, membername, initfunc())
19
21
  if oldinit:
20
22
  oldinit(self, *args, **kwargs)
21
23
  else:
22
24
  super(cls, self).__init__(*args, **kwargs)
23
- if not hasattr(self, membername): # can be overridden by a subclass
24
- setattr(self, membername, initfunc())
25
25
  cls.__init__ = wraps(oldinit)(newinit) if oldinit else newinit
26
26
 
27
27
  oldcopy = thisclass_hasattr(cls, '__copy__') and getattr(cls, '__copy__')
@@ -30,15 +30,32 @@ class Extensible:
30
30
  ret = oldcopy(self, *args, **kwargs)
31
31
  else:
32
32
  ret = super(cls, self).__copy__(*args, **kwargs)
33
- setattr(ret, membername, getattr(self, membername) if copy and hasattr(self, membername) else initfunc())
33
+
34
+ if not hasattr(ret, membername):
35
+ if copy and hasattr(self, membername):
36
+ v = getattr(self, membername)
37
+ else:
38
+ v = initfunc()
39
+ setattr(ret, membername, v)
40
+
34
41
  return ret
35
42
  cls.__copy__ = wraps(oldcopy)(newcopy) if oldcopy else newcopy
36
43
 
44
+ @classmethod
45
+ def superclasses(cls):
46
+ yield cls
47
+ yield from cls.__bases__
48
+ for b in cls.__bases__:
49
+ if hasattr(b, 'superclasses'):
50
+ yield from b.superclasses()
51
+
37
52
  @classmethod
38
53
  def api(cls, func):
39
54
  oldfunc = getattr(cls, func.__name__, None)
40
55
  if oldfunc:
41
56
  func = wraps(oldfunc)(func)
57
+ from visidata import vd
58
+ func.importingModule = vd.importingModule
42
59
  setattr(cls, func.__name__, func)
43
60
  return func
44
61
 
@@ -75,6 +92,14 @@ class Extensible:
75
92
 
76
93
  @classmethod
77
94
  def class_api(cls, func):
95
+ '''`@Class.class_api` works much like `@Class.api`, but for class methods. This method is used internally but may not be all that useful for plugin and module authors. Note that `@classmethod` must still be provided, and **the order of multiple decorators is crucial**, in that `@<class>.class_api` must come before `@classmethod`:
96
+
97
+ ::
98
+ @Sheet.class_api
99
+ @classmethod
100
+ def addCommand(cls, ...):
101
+ '''
102
+
78
103
  name = func.__get__(None, dict).__func__.__name__
79
104
  oldfunc = getattr(cls, name, None)
80
105
  if oldfunc:
@@ -99,7 +124,7 @@ class Extensible:
99
124
  @property
100
125
  @wraps(func)
101
126
  def get_if_not(self):
102
- if getattr(self, name) is None:
127
+ if getattr(self, name, None) is None:
103
128
  setattr(self, name, func(self))
104
129
  return getattr(self, name)
105
130
  setattr(cls, func.__name__, get_if_not)
File without changes
@@ -0,0 +1,42 @@
1
+ # requirements: mutagen
2
+
3
+ import functools
4
+
5
+ from visidata import vd, Column, AttrColumn, DirSheet
6
+
7
+
8
+ @functools.lru_cache(None)
9
+ def get_mutagen_info(path):
10
+ mutagen = vd.importExternal('mutagen')
11
+ m = mutagen.File(path)
12
+ return m.info
13
+
14
+
15
+ class MutagenColumn(AttrColumn):
16
+ def calcValue(self, r):
17
+ md = get_mutagen_info(r)
18
+ return getattr(md, self.expr, None)
19
+
20
+
21
+ @DirSheet.api
22
+ def audiometadata_columns(sheet):
23
+ return [
24
+ Column('audio_info', width=0, getter=lambda c,r: get_mutagen_info(r)),
25
+ MutagenColumn('bitrate'),
26
+ MutagenColumn('channels'),
27
+ MutagenColumn('encoder_info'),
28
+ MutagenColumn('encoder_settings'),
29
+ MutagenColumn('frame_offset'),
30
+ MutagenColumn('length'),
31
+ MutagenColumn('mode'),
32
+ MutagenColumn('padding'),
33
+ MutagenColumn('protected'),
34
+ MutagenColumn('sample_rate'),
35
+ MutagenColumn('track_gain'),
36
+ ]
37
+
38
+
39
+ DirSheet.addCommand('', 'addcol-audiometadata', 'addColumn(*audiometadata_columns())', 'add metadata columns for audio files (MP3, FLAC, Ogg, etc)')
40
+
41
+
42
+ vd.addMenuItems('Column > Add column > audio metadata > addcol-audiometadata')
@@ -0,0 +1,34 @@
1
+ from visidata import vd, Column, Sheet, asyncthread, Progress
2
+
3
+
4
+ class HistogramColumn(Column):
5
+ def calcValue(col, row):
6
+ histogram = col.sheet.options.disp_histogram
7
+ histolen = (col.width-2)*col.sourceCol.getTypedValue(row)//(col.sourceCol.largest-col.sourceCol.smallest)
8
+ return histogram*histolen
9
+
10
+
11
+ @Sheet.api
12
+ def addcol_histogram(sheet, col): #2052
13
+ newcol = HistogramColumn(col.name+'_histogram', sourceCol=col)
14
+ col.smallest = None
15
+ col.largest = None
16
+ sheet.calc_histogram_bounds(col)
17
+ return newcol
18
+
19
+
20
+ @Sheet.api
21
+ @asyncthread
22
+ def calc_histogram_bounds(sheet, col):
23
+ for row in Progress(sheet.rows):
24
+ v = col.getTypedValue(row)
25
+ if col.smallest is None or v < col.smallest:
26
+ col.smallest = v
27
+ if col.largest is None or v > col.largest:
28
+ col.largest = v
29
+
30
+
31
+ Sheet.addCommand('', 'addcol-histogram', 'addColumnAtCursor(addcol_histogram(cursorCol))', 'add column with histogram of current column')
32
+
33
+
34
+ vd.addMenuItems('Column > Add column > histogram > addcol-histogram')
@@ -0,0 +1,69 @@
1
+ '''
2
+ Add svg saver to Canvas.
3
+
4
+ Requires matplotlib.pyplot
5
+ '''
6
+
7
+
8
+ import collections
9
+
10
+ from visidata import VisiData, Canvas, vd, Progress
11
+
12
+ vd.option('plt_marker', '.', 'matplotlib.markers')
13
+
14
+
15
+ @Canvas.api
16
+ def plot_sheet(self, ax):
17
+ plt = vd.importExternal('matplotlib.pyplot', 'matplotlib')
18
+ nerrors = 0
19
+ nplotted = 0
20
+
21
+ self.reset()
22
+
23
+ vd.status('loading data points')
24
+ catcols = [c for c in self.xcols if not vd.isNumeric(c)]
25
+ numcols = [c for c in self.xcols if vd.isNumeric(c)]
26
+ for ycol in self.ycols:
27
+ xpts = collections.defaultdict(list)
28
+ ypts = collections.defaultdict(list)
29
+ for rownum, row in enumerate(Progress(self.sourceRows, 'plotting')): # rows being plotted from source
30
+ try:
31
+ k = tuple(c.getValue(row) for c in catcols) if catcols else (ycol.name,)
32
+
33
+ # convert deliberately to float (to e.g. linearize date)
34
+ graph_x = numcols[0].type(numcols[0].getValue(row)) if numcols else rownum
35
+ graph_y = ycol.type(ycol.getValue(row))
36
+
37
+ xpts[k].append(graph_x)
38
+ ypts[k].append(graph_y)
39
+
40
+ nplotted += 1
41
+ except Exception:
42
+ nerrors += 1
43
+ if vd.options.debug:
44
+ raise
45
+ lines = []
46
+ for k in xpts:
47
+ line = ax.scatter(xpts[k], ypts[k], label=' '.join(str(x) for x in k), **vd.options.getall('plt_'))
48
+ lines.append(line)
49
+
50
+ ax.legend(handles=lines)
51
+ ax.set_xlabel(','.join(xcol.name for xcol in self.xcols if vd.isNumeric(xcol)) or 'row#')
52
+ ax.xaxis.set_major_locator(plt.MaxNLocator(4))
53
+ ax.yaxis.set_major_locator(plt.MaxNLocator(4))
54
+
55
+
56
+ @VisiData.api
57
+ def save_svg(vd, p, *sheets):
58
+ plt = vd.importExternal('matplotlib.pyplot', 'matplotlib')
59
+ fig_, ax = plt.subplots()
60
+ for vs in sheets:
61
+ if not isinstance(vs, Canvas):
62
+ vd.warning(f'{vs.name} not a Canvas')
63
+ continue
64
+ vs.plot_sheet(ax)
65
+
66
+ ax.grid()
67
+ ax.set_title('\n'.join(vs.name for vs in sheets))
68
+ plt.xticks()
69
+ plt.savefig(p, format='svg')