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
@@ -0,0 +1,29 @@
1
+ import re
2
+ from visidata import vd, VisiData, Sheet
3
+
4
+
5
+ vd.option('clean_names', False, 'clean column/sheet names to be valid Python identifiers', replay=True)
6
+
7
+
8
+ @VisiData.global_api
9
+ def cleanName(vd, s):
10
+ #[Nas Banov] https://stackoverflow.com/a/3305731
11
+ # return re.sub(r'\W|^(?=\d)', '_', str(s)).strip('_')
12
+ s = re.sub(r'[^\w\d_]', '_', s) # replace non-alphanum chars with _
13
+ s = re.sub(r'_+', '_', s) # replace runs of _ with a single _
14
+ s = s.strip('_')
15
+ return s
16
+
17
+
18
+ @Sheet.api
19
+ def maybeClean(sheet, s):
20
+ if sheet.options.clean_names:
21
+ s = vd.cleanName(s)
22
+ return s
23
+
24
+
25
+ Sheet.addCommand('', 'clean-names', '''
26
+ options.clean_names = True;
27
+ for c in visibleCols:
28
+ c.name = cleanName(c.name)
29
+ ''', 'set options.clean_names on sheet and clean visible column names')
visidata/clipboard.py CHANGED
@@ -5,19 +5,24 @@ import io
5
5
  import sys
6
6
  import tempfile
7
7
  import functools
8
+ import os
8
9
 
9
- from visidata import VisiData, vd, asyncthread
10
- from visidata import Sheet, Path
10
+ from visidata import VisiData, vd, asyncthread, SettableColumn
11
+ from visidata import Sheet, Path, Column
11
12
 
12
13
  if sys.platform == 'win32':
13
- syscopy_cmd_default = 'clip'
14
- syspaste_cmd_default = 'clip'
14
+ syscopy_cmd_default = 'clip.exe'
15
+ syspaste_cmd_default = 'powershell -command Get-Clipboard'
15
16
  elif sys.platform == 'darwin':
16
17
  syscopy_cmd_default = 'pbcopy w'
17
18
  syspaste_cmd_default = 'pbpaste'
18
19
  else:
19
- syscopy_cmd_default = 'xclip -selection clipboard -filter' # xsel --clipboard --input
20
- syspaste_cmd_default = 'xclip -selection clipboard -o' # xsel --clipboard
20
+ if 'WAYLAND_DISPLAY' in os.environ:
21
+ syscopy_cmd_default = 'wl-copy'
22
+ syspaste_cmd_default = 'wl-paste'
23
+ else:
24
+ syscopy_cmd_default = 'xclip -selection clipboard -filter' # xsel --clipboard --input
25
+ syspaste_cmd_default = 'xclip -selection clipboard -o' # xsel --clipboard
21
26
 
22
27
  vd.option('clipboard_copy_cmd', syscopy_cmd_default, 'command to copy stdin to system clipboard', sheettype=None)
23
28
  vd.option('clipboard_paste_cmd', syspaste_cmd_default, 'command to send contents of system clipboard to stdout', sheettype=None)
@@ -48,7 +53,7 @@ def syscopyValue(sheet, val):
48
53
  p = subprocess.run(
49
54
  sheet.options.clipboard_copy_cmd.split(),
50
55
  input=val,
51
- encoding=sheet.options.encoding,
56
+ encoding='utf-8',
52
57
  stdout=subprocess.DEVNULL)
53
58
 
54
59
  vd.status('copied value to system clipboard')
@@ -70,16 +75,19 @@ def syscopyCells_async(sheet, cols, rows, filetype):
70
75
  vd.status(f'copying {vs.nRows} {vs.rowtype} to system clipboard as {filetype}')
71
76
 
72
77
  with io.StringIO() as buf:
73
- vd.sync(vd.saveSheets(Path(sheet.name+'.'+filetype, fptext=buf), vs))
74
- subprocess.run(
75
- sheet.options.clipboard_copy_cmd.split(),
76
- input=buf.getvalue(),
77
- encoding=sheet.options.encoding,
78
- stdout=subprocess.DEVNULL)
78
+ with tempfile.NamedTemporaryFile() as temp:
79
+ temp.close() #2118
80
+
81
+ vd.sync(vd.saveSheets(Path(f'{temp.name}.{filetype}', fptext=buf), vs, confirm_overwrite=False))
82
+ subprocess.run(
83
+ sheet.options.clipboard_copy_cmd.split(),
84
+ input=buf.getvalue(),
85
+ encoding='utf-8',
86
+ stdout=subprocess.DEVNULL)
79
87
 
80
88
 
81
89
  @VisiData.api
82
- def sysclip_value(vd):
90
+ def sysclipValue(vd):
83
91
  cmd = vd.options.clipboard_paste_cmd
84
92
  return subprocess.check_output(vd.options.clipboard_paste_cmd.split()).decode('utf-8')
85
93
 
@@ -87,17 +95,29 @@ def sysclip_value(vd):
87
95
  @VisiData.api
88
96
  @asyncthread
89
97
  def pasteFromClipboard(vd, cols, rows):
90
- text = vd.getLastArgs() or vd.sysclip_value().strip() or vd.fail('system clipboard is empty')
98
+ text = vd.getLastArgs() or vd.sysclipValue().strip() or vd.fail('system clipboard is empty')
91
99
 
92
100
  vd.addUndoSetValues(cols, rows)
101
+ lines = text.split('\n')
102
+ if not lines:
103
+ vd.warning('nothing to paste')
104
+ return
93
105
 
94
- for line, r in zip(text.split('\n'), rows):
106
+ vs = cols[0].sheet
107
+ newrows = [vs.newRow() for i in range(len(lines)-len(rows))]
108
+ if newrows:
109
+ rows.extend(newrows)
110
+ vs.addRows(newrows)
111
+
112
+ for line, r in zip(lines, rows):
95
113
  for v, c in zip(line.split('\t'), cols):
96
114
  c.setValue(r, v)
97
115
 
98
116
 
99
117
  @Sheet.api
100
118
  def delete_row(sheet, rowidx):
119
+ if not sheet.rows:
120
+ vd.fail("no row to delete")
101
121
  if not sheet.defer:
102
122
  oldrow = sheet.rows.pop(rowidx)
103
123
  vd.addUndo(sheet.rows.insert, rowidx, oldrow)
@@ -112,11 +132,33 @@ def delete_row(sheet, rowidx):
112
132
  sheet.setModified()
113
133
  return oldrow
114
134
 
135
+
115
136
  @Sheet.api
137
+ @asyncthread
116
138
  def paste_after(sheet, rowidx):
117
- to_paste = list(deepcopy(r) for r in reversed(vd.memory.cliprows))
118
- sheet.addRows(to_paste, index=rowidx)
139
+ 'Paste rows from *vd.cliprows* at *rowidx*.'
140
+ if not vd.memory.cliprows: #1793
141
+ vd.warning('nothing to paste')
142
+ return
143
+
144
+ for col in vd.memory.clipcols[sheet.nVisibleCols:]:
145
+ newcol = SettableColumn()
146
+ newcol.__setstate__(col.__getstate__())
147
+ sheet.addColumn(newcol)
148
+
149
+ addedRows = []
119
150
 
151
+ for extrow in vd.memory.cliprows:
152
+ if isinstance(extrow, Column):
153
+ newrow = copy(extrow)
154
+ else:
155
+ newrow = sheet.newRow()
156
+ for col, extcol in zip(sheet.visibleCols, vd.memory.clipcols):
157
+ col.setValue(newrow, extcol.getTypedValue(extrow))
158
+
159
+ addedRows.append(newrow)
160
+
161
+ sheet.addRows(addedRows, index=rowidx)
120
162
 
121
163
 
122
164
  Sheet.addCommand('y', 'copy-row', 'copyRows([cursorRow])', 'yank (copy) current row to clipboard')
@@ -157,3 +199,27 @@ Sheet.addCommand('gzx', 'cut-cells', 'copyCells(cursorCol, onlySelectedRows); cu
157
199
 
158
200
  Sheet.bindkey('KEY_DC', 'delete-cell'),
159
201
  Sheet.bindkey('gKEY_DC', 'delete-cells'),
202
+
203
+ vd.addMenuItems('''
204
+ Edit > Delete > current row > delete-row
205
+ Edit > Delete > current cell > delete-cell
206
+ Edit > Delete > selected rows > delete-selected
207
+ Edit > Delete > selected cells > delete-cells
208
+ Edit > Copy > current cell > copy-cell
209
+ Edit > Copy > current row > copy-row
210
+ Edit > Copy > selected cells > copy-cells
211
+ Edit > Copy > selected rows > copy-selected
212
+ Edit > Copy > to system clipboard > current cell > syscopy-cell
213
+ Edit > Copy > to system clipboard > current row > syscopy-row
214
+ Edit > Copy > to system clipboard > selected cells > syscopy-cells
215
+ Edit > Copy > to system clipboard > selected rows > syscopy-selected
216
+ Edit > Cut > current row > cut-row
217
+ Edit > Cut > selected cells > cut-selected
218
+ Edit > Cut > current cell > cut-cell
219
+ Edit > Paste > row after > paste-after
220
+ Edit > Paste > row before > paste-before
221
+ Edit > Paste > into selected cells > setcol-clipboard
222
+ Edit > Paste > into current cell > paste-cell
223
+ Edit > Paste > from system clipboard > cells at cursor > syspaste-cells
224
+ Edit > Paste > from system clipboard > selected cells > syspaste-cells-selected
225
+ ''')
visidata/cliptext.py CHANGED
@@ -1,12 +1,13 @@
1
1
  import unicodedata
2
2
  import sys
3
+ import re
3
4
  import functools
5
+ import textwrap
4
6
 
5
- from visidata import options, drawcache
6
-
7
- __all__ = ['clipstr', 'clipdraw', 'clipbox', 'dispwidth', 'iterchars']
7
+ from visidata import options, drawcache, vd, update_attr, colors, ColorAttr
8
8
 
9
9
  disp_column_fill = ' '
10
+ internal_markup_re = r'(\[[:/][^\]]*?\])' # [:whatever until the closing bracket] or [/whatever] or [:]
10
11
 
11
12
  ### Curses helpers
12
13
 
@@ -52,16 +53,56 @@ def wcwidth(cc, ambig=1):
52
53
  return 0
53
54
 
54
55
 
56
+ def is_vdcode(s:str) -> bool:
57
+ return (s.startswith('[:') and s.endswith(']')) or \
58
+ (s.startswith('[/') and s.endswith(']'))
59
+
60
+
61
+ def iterchunks(s, literal=False):
62
+ attrstack = [dict(link='', cattr=ColorAttr())]
63
+ legitopens = 0
64
+ chunks = re.split(internal_markup_re, s)
65
+ for chunk in chunks:
66
+ if not chunk:
67
+ continue
68
+
69
+ if not literal and is_vdcode(chunk):
70
+ cattr = attrstack[-1]['cattr']
71
+ link = attrstack[-1]['link']
72
+
73
+ if chunk.startswith('[:onclick '):
74
+ attrstack.append(dict(link=chunk[10:-1], cattr=cattr.update(colors.clickable)))
75
+ continue
76
+ elif chunk == '[:]': # clear stack, keep origattr
77
+ if len(attrstack) > 1:
78
+ del attrstack[1:]
79
+ continue
80
+ elif chunk.startswith('[/'): # pop last attr off stack
81
+ if len(attrstack) > 1:
82
+ attrstack.pop()
83
+ continue # don't display trailing [/foo] ever
84
+ else: # push updated color on stack
85
+ newcolor = colors.get_color(chunk[2:-1])
86
+ if newcolor:
87
+ cattr = update_attr(cattr, newcolor, len(attrstack))
88
+ attrstack.append(dict(link=link, cattr=cattr))
89
+ continue
90
+
91
+ yield attrstack[-1], chunk
92
+
93
+
55
94
  @functools.lru_cache(maxsize=100000)
56
- def dispwidth(ss, maxwidth=None):
95
+ def dispwidth(ss, maxwidth=None, literal=False):
57
96
  'Return display width of string, according to unicodedata width and options.disp_ambig_width.'
58
97
  disp_ambig_width = options.disp_ambig_width
59
98
  w = 0
60
99
 
61
- for cc in ss:
62
- w += wcwidth(cc, disp_ambig_width)
63
- if maxwidth and w > maxwidth:
64
- break
100
+ for _, s in iterchunks(ss, literal=literal):
101
+ for cc in s:
102
+ if cc:
103
+ w += wcwidth(cc, disp_ambig_width)
104
+ if maxwidth and w > maxwidth:
105
+ return maxwidth
65
106
  return w
66
107
 
67
108
 
@@ -76,7 +117,7 @@ def _dispch(c, oddspacech=None, combch=None, modch=None):
76
117
  elif c in ZERO_WIDTH_CF:
77
118
  return combch, 1
78
119
 
79
- return c, dispwidth(c)
120
+ return c, dispwidth(c, literal=True)
80
121
 
81
122
 
82
123
  def iterchars(x):
@@ -103,24 +144,35 @@ def iterchars(x):
103
144
  def _clipstr(s, dispw, trunch='', oddspacech='', combch='', modch=''):
104
145
  '''Return clipped string and width in terminal display characters.
105
146
  Note: width may differ from len(s) if East Asian chars are 'fullwidth'.'''
147
+ if not s:
148
+ return '', 0
149
+
150
+ if dispw == 1:
151
+ return s[0], 1
152
+
106
153
  w = 0
107
154
  ret = ''
108
155
 
109
156
  trunchlen = dispwidth(trunch)
110
157
  for c in s:
111
158
  newc, chlen = _dispch(c, oddspacech=oddspacech, combch=combch, modch=modch)
112
- if newc:
113
- ret += newc
114
- w += chlen
115
- else:
116
- ret += c
117
- w += dispwidth(c)
118
-
119
- if dispw and w > dispw-trunchlen+1:
120
- ret = ret[:-2] + trunch # replace final char with ellipsis
121
- w += trunchlen
159
+ if not newc:
160
+ newc = c
161
+ chlen = dispwidth(c)
162
+
163
+ if dispw and w+chlen > dispw:
164
+ if trunchlen and dispw > trunchlen:
165
+ lastchlen = _dispch(ret[-1])[1]
166
+ if w+trunchlen > dispw:
167
+ ret = ret[:-1]
168
+ w -= lastchlen
169
+ ret += trunch # replace final char with ellipsis
170
+ w += trunchlen
122
171
  break
123
172
 
173
+ w += chlen
174
+ ret += newc
175
+
124
176
  return ret, w
125
177
 
126
178
 
@@ -139,38 +191,149 @@ def clipstr(s, dispw, truncator=None, oddspace=None):
139
191
  modch='',
140
192
  combch='')
141
193
 
142
- def clipdraw(scr, y, x, s, attr, w=None, clear=True, rtl=False, **kwargs):
143
- 'Draw string `s` at (y,x)-(y,x+w) with curses attr, clipping with ellipsis char. if rtl, draw inside (x-w, x). If *clear*, clear whole editing area before displaying. Returns width drawn (max of w).'
194
+
195
+ def clipdraw(scr, y, x, s, attr, w=None, clear=True, literal=False, **kwargs):
196
+ '''Draw `s` at (y,x)-(y,x+w) with curses `attr`, clipping with ellipsis char.
197
+ If `clear`, clear whole editing area before displaying.
198
+ If `literal`, do not interpret internal color code markup.
199
+ Return width drawn (max of w).
200
+ '''
201
+ if not literal:
202
+ chunks = iterchunks(s, literal=literal)
203
+ else:
204
+ chunks = [(dict(link='', cattr=ColorAttr()), s)]
205
+
206
+ x = max(0, x)
207
+ y = max(0, y)
208
+ assert x >= 0, x
209
+ assert y >= 0, y
210
+
211
+ return clipdraw_chunks(scr, y, x, chunks, attr, w=w, clear=clear, **kwargs)
212
+
213
+
214
+ def clipdraw_chunks(scr, y, x, chunks, cattr:ColorAttr=ColorAttr(), w=None, clear=True, literal=False, **kwargs):
215
+ '''Draw `chunks` (sequence of (color:str, text:str) as from iterchunks) at (y,x)-(y,x+w) with curses `attr`, clipping with ellipsis char.
216
+ If `clear`, clear whole editing area before displaying.
217
+ Return width drawn (max of w).
218
+ '''
144
219
  if scr:
145
- _, windowWidth = scr.getmaxyx()
220
+ windowHeight, windowWidth = scr.getmaxyx()
146
221
  else:
147
- windowWidth = 80
148
- dispw = 0
222
+ windowHeight, windowWidth = 25, 80
223
+ totaldispw = 0
224
+
225
+ assert isinstance(cattr, ColorAttr), cattr
226
+ origattr = cattr
227
+ origw = w
228
+ clipped = ''
229
+ link = ''
230
+
231
+ if w and clear:
232
+ actualw = min(w, windowWidth-x-1)
233
+ if scr:
234
+ scr.addstr(y, x, disp_column_fill*actualw, cattr.attr) # clear whole area before displaying
235
+
149
236
  try:
150
- if w is None:
151
- w = dispwidth(s, maxwidth=windowWidth)
152
- w = min(w, (x-1) if rtl else (windowWidth-x-1))
153
- if w <= 0: # no room anyway
154
- return 0
155
- if not scr:
156
- return w
157
-
158
- # convert to string just before drawing
159
- clipped, dispw = clipstr(s, w, **kwargs)
160
- if rtl:
161
- # clearing whole area (w) has negative display effects; clearing just dispw area is useless
162
- # scr.addstr(y, x-dispw-1, disp_column_fill*dispw, attr)
163
- scr.addstr(y, x-dispw-1, clipped, attr)
164
- else:
165
- if clear:
166
- scr.addstr(y, x, disp_column_fill*w, attr) # clear whole area before displaying
167
- scr.addstr(y, x, clipped, attr)
237
+ for colorstate, chunk in chunks:
238
+ if isinstance(colorstate, str):
239
+ cattr = cattr.update(colors.get_color(colorstate))
240
+ else:
241
+ cattr = origattr.update(colorstate['cattr'])
242
+ link = colorstate['link']
243
+
244
+ if not chunk:
245
+ continue
246
+
247
+ if origw is None:
248
+ chunkw = dispwidth(chunk, maxwidth=windowWidth-totaldispw)
249
+ else:
250
+ chunkw = origw-totaldispw
251
+
252
+ chunkw = min(chunkw, windowWidth-x-1)
253
+ if chunkw <= 0: # no room anyway
254
+ return totaldispw
255
+ if not scr:
256
+ return totaldispw
257
+
258
+ # convert to string just before drawing
259
+ clipped, dispw = clipstr(chunk, chunkw, **kwargs)
260
+
261
+ if y >= 0 and y < windowHeight:
262
+ scr.addstr(y, x, clipped, cattr.attr)
263
+ else:
264
+ if vd.options.debug:
265
+ raise Exception(f'addstr(y={y} x={x}) out of bounds')
266
+
267
+ if link:
268
+ vd.onMouse(scr, x, y, dispw, 1, BUTTON1_RELEASED=link)
269
+
270
+ x += dispw
271
+ totaldispw += dispw
272
+
273
+ if chunkw < dispw:
274
+ break
168
275
  except Exception as e:
169
- pass
170
- # raise type(e)('%s [clip_draw y=%s x=%s dispw=%s w=%s clippedlen=%s]' % (e, y, x, dispw, w, len(clipped))
276
+ if vd.options.debug:
277
+ raise
278
+ # raise type(e)('%s [clip_draw y=%s x=%s dispw=%s w=%s clippedlen=%s]' % (e, y, x, totaldispw, w, len(clipped))
171
279
  # ).with_traceback(sys.exc_info()[2])
172
280
 
173
- return dispw
281
+ return totaldispw
282
+
283
+
284
+ def _markdown_to_internal(text):
285
+ 'Return markdown-formatted `text` converted to internal formatting (like `[:color]text[/]`).'
286
+ text = re.sub(r'`(.*?)`', r'[:code]\1[/]', text)
287
+ text = re.sub(r'(^#.*?)$', r'[:heading]\1[/]', text)
288
+ text = re.sub(r'\*\*(.*?)\*\*', r'[:bold]\1[/]', text)
289
+ text = re.sub(r'\*(.*?)\*', r'[:italic]\1[/]', text)
290
+ text = re.sub(r'\b_(.*?)_\b', r'[:underline]\1[/]', text)
291
+ return text
292
+
293
+
294
+ def wraptext(text, width=80, indent=''):
295
+ '''
296
+ Word-wrap `text` and yield (formatted_line, textonly_line) for each line of at most `width` characters.
297
+ Formatting like `[:color]text[/]` is ignored for purposes of computing width, and not included in `textonly_line`.
298
+ '''
299
+ import re
300
+
301
+ if width <= 0:
302
+ return
303
+
304
+ for line in text.splitlines():
305
+ if not line:
306
+ yield '', ''
307
+ continue
308
+
309
+ line = _markdown_to_internal(line)
310
+ chunks = re.split(internal_markup_re, line)
311
+ textchunks = [x for x in chunks if not is_vdcode(x)]
312
+ for linenum, textline in enumerate(textwrap.wrap(''.join(textchunks), width=width, drop_whitespace=False)):
313
+ txt = textline
314
+ r = ''
315
+ while chunks:
316
+ c = chunks[0]
317
+ if len(c) > len(txt):
318
+ r += txt
319
+ chunks[0] = c[len(txt):]
320
+ break
321
+
322
+ if len(chunks) == 1:
323
+ r += chunks.pop(0)
324
+ else:
325
+ chunks.pop(0)
326
+ r += txt[:len(c)] + chunks.pop(0)
327
+
328
+ txt = txt[len(c):]
329
+
330
+ r = r.strip()
331
+ if linenum > 0:
332
+ r = indent + r
333
+ yield r, textline
334
+
335
+ for c in chunks:
336
+ yield c, ''
174
337
 
175
338
 
176
339
  def clipbox(scr, lines, attr, title=''):
@@ -178,6 +341,17 @@ def clipbox(scr, lines, attr, title=''):
178
341
  scr.bkgd(attr)
179
342
  scr.box()
180
343
  h, w = scr.getmaxyx()
181
- clipdraw(scr, 0, w-len(title)-6, f"| {title} |", attr)
182
344
  for i, line in enumerate(lines):
183
345
  clipdraw(scr, i+1, 2, line, attr)
346
+
347
+ clipdraw(scr, 0, w-len(title)-6, f"| {title} |", attr)
348
+
349
+
350
+ vd.addGlobals(clipstr=clipstr,
351
+ clipdraw=clipdraw,
352
+ clipdraw_chunks=clipdraw_chunks,
353
+ clipbox=clipbox,
354
+ dispwidth=dispwidth,
355
+ iterchars=iterchars,
356
+ iterchunks=iterchunks,
357
+ wraptext=wraptext)