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.
- visidata/__init__.py +72 -91
- visidata/_input.py +259 -42
- visidata/_open.py +84 -29
- visidata/_types.py +21 -3
- visidata/_urlcache.py +17 -4
- visidata/aggregators.py +65 -25
- visidata/apps/__init__.py +0 -0
- visidata/apps/vdsql/__about__.py +8 -0
- visidata/apps/vdsql/__init__.py +5 -0
- visidata/apps/vdsql/__main__.py +27 -0
- visidata/apps/vdsql/_ibis.py +748 -0
- visidata/apps/vdsql/bigquery.py +61 -0
- visidata/apps/vdsql/clickhouse.py +53 -0
- visidata/apps/vdsql/setup.py +40 -0
- visidata/apps/vdsql/snowflake.py +67 -0
- visidata/apps/vgit/__init__.py +13 -0
- {vgit → visidata/apps/vgit}/blame.py +5 -2
- {vgit → visidata/apps/vgit}/branch.py +31 -16
- {vgit → visidata/apps/vgit}/config.py +3 -3
- visidata/apps/vgit/diff.py +169 -0
- visidata/apps/vgit/gitsheet.py +161 -0
- {vgit → visidata/apps/vgit}/grep.py +6 -5
- visidata/apps/vgit/log.py +81 -0
- {vgit → visidata/apps/vgit}/main.py +18 -5
- {vgit → visidata/apps/vgit}/remote.py +8 -4
- visidata/apps/vgit/repos.py +71 -0
- {vgit → visidata/apps/vgit}/setup.py +6 -4
- visidata/apps/vgit/stash.py +69 -0
- visidata/apps/vgit/status.py +204 -0
- {vgit → visidata/apps/vgit}/statusbar.py +2 -0
- visidata/basesheet.py +59 -50
- visidata/canvas.py +208 -93
- visidata/choose.py +6 -6
- visidata/clean_names.py +29 -0
- visidata/clipboard.py +73 -17
- visidata/cliptext.py +220 -46
- visidata/cmdlog.py +88 -114
- visidata/color.py +142 -56
- visidata/column.py +121 -129
- visidata/ddw/input.ddw +74 -79
- visidata/ddw/regex.ddw +57 -0
- visidata/ddwplay.py +33 -14
- visidata/deprecated.py +77 -3
- visidata/desktop/visidata.desktop +7 -0
- visidata/editor.py +12 -6
- visidata/errors.py +5 -1
- visidata/experimental/__init__.py +0 -0
- visidata/experimental/diff_sheet.py +29 -0
- visidata/experimental/digit_autoedit.py +6 -0
- visidata/experimental/gdrive.py +89 -0
- visidata/experimental/google.py +37 -0
- visidata/experimental/gsheets.py +79 -0
- visidata/experimental/live_search.py +37 -0
- visidata/experimental/liveupdate.py +45 -0
- visidata/experimental/mark.py +133 -0
- visidata/experimental/noahs_tapestry/__init__.py +1 -0
- visidata/experimental/noahs_tapestry/tapestry.py +147 -0
- visidata/experimental/rownum.py +73 -0
- visidata/experimental/slide_cells.py +26 -0
- visidata/expr.py +8 -4
- visidata/extensible.py +30 -5
- visidata/features/__init__.py +0 -0
- visidata/features/addcol_audiometadata.py +42 -0
- visidata/features/addcol_histogram.py +34 -0
- visidata/features/canvas_save_svg.py +69 -0
- visidata/features/change_precision.py +46 -0
- visidata/features/cmdpalette.py +163 -0
- visidata/features/colorbrewer.py +363 -0
- visidata/{colorsheet.py → features/colorsheet.py} +17 -16
- visidata/features/command_server.py +105 -0
- visidata/features/currency_to_usd.py +70 -0
- visidata/{customdate.py → features/customdate.py} +2 -0
- visidata/features/dedupe.py +132 -0
- visidata/{describe.py → features/describe.py} +17 -15
- visidata/features/errors_guide.py +26 -0
- visidata/features/expand_cols.py +202 -0
- visidata/{fill.py → features/fill.py} +3 -1
- visidata/{freeze.py → features/freeze.py} +11 -6
- visidata/features/graph_seaborn.py +79 -0
- visidata/features/helloworld.py +10 -0
- visidata/features/hint_types.py +17 -0
- visidata/{incr.py → features/incr.py} +5 -0
- visidata/{join.py → features/join.py} +107 -53
- visidata/features/known_cols.py +21 -0
- visidata/features/layout.py +62 -0
- visidata/{melt.py → features/melt.py} +32 -21
- visidata/features/normcol.py +118 -0
- visidata/features/open_config.py +7 -0
- visidata/features/open_syspaste.py +18 -0
- visidata/features/ping.py +157 -0
- visidata/features/procmgr.py +208 -0
- visidata/features/random_sample.py +6 -0
- visidata/{regex.py → features/regex.py} +47 -31
- visidata/features/reload_every.py +55 -0
- visidata/features/rename_col_cascade.py +30 -0
- visidata/features/scroll_context.py +60 -0
- visidata/features/select_equal_selected.py +11 -0
- visidata/features/setcol_fake.py +65 -0
- visidata/{slide.py → features/slide.py} +75 -21
- visidata/features/sparkline.py +48 -0
- visidata/features/status_source.py +20 -0
- visidata/{sysedit.py → features/sysedit.py} +2 -1
- visidata/features/sysopen_mailcap.py +46 -0
- visidata/features/term_extras.py +13 -0
- visidata/{transpose.py → features/transpose.py} +5 -4
- visidata/features/type_ipaddr.py +73 -0
- visidata/features/type_url.py +11 -0
- visidata/{unfurl.py → features/unfurl.py} +9 -9
- visidata/{window.py → features/window.py} +2 -2
- visidata/form.py +50 -21
- visidata/freqtbl.py +81 -33
- visidata/fuzzymatch.py +414 -0
- visidata/graph.py +105 -33
- visidata/guide.py +180 -0
- visidata/help.py +75 -44
- visidata/hint.py +39 -0
- visidata/indexsheet.py +109 -0
- visidata/input_history.py +55 -0
- visidata/interface.py +58 -0
- visidata/keys.py +17 -16
- visidata/loaders/__init__.py +9 -0
- visidata/loaders/_pandas.py +61 -21
- visidata/loaders/api_airtable.py +70 -0
- visidata/loaders/api_bitio.py +102 -0
- visidata/loaders/api_matrix.py +148 -0
- visidata/loaders/api_reddit.py +306 -0
- visidata/loaders/api_zulip.py +249 -0
- visidata/loaders/archive.py +41 -7
- visidata/loaders/arrow.py +7 -7
- visidata/loaders/conll.py +49 -0
- visidata/loaders/csv.py +25 -7
- visidata/loaders/eml.py +3 -4
- visidata/loaders/f5log.py +1204 -0
- visidata/loaders/fec.py +325 -0
- visidata/loaders/fixed_width.py +2 -4
- visidata/loaders/frictionless.py +3 -3
- visidata/loaders/geojson.py +8 -5
- visidata/loaders/google.py +48 -0
- visidata/loaders/graphviz.py +4 -4
- visidata/loaders/hdf5.py +4 -4
- visidata/loaders/html.py +48 -10
- visidata/loaders/http.py +84 -30
- visidata/loaders/imap.py +20 -10
- visidata/loaders/jrnl.py +52 -0
- visidata/loaders/json.py +83 -29
- visidata/loaders/jsonla.py +74 -0
- visidata/loaders/lsv.py +15 -11
- visidata/loaders/mailbox.py +40 -0
- visidata/loaders/markdown.py +1 -3
- visidata/loaders/mbtiles.py +4 -5
- visidata/loaders/mysql.py +11 -13
- visidata/loaders/npy.py +7 -7
- visidata/loaders/odf.py +4 -1
- visidata/loaders/orgmode.py +428 -0
- visidata/loaders/pandas_freqtbl.py +14 -20
- visidata/loaders/parquet.py +62 -6
- visidata/loaders/pcap.py +3 -3
- visidata/loaders/pdf.py +4 -3
- visidata/loaders/png.py +19 -13
- visidata/loaders/postgres.py +9 -8
- visidata/loaders/rec.py +7 -3
- visidata/loaders/s3.py +342 -0
- visidata/loaders/sas.py +5 -5
- visidata/loaders/scrape.py +186 -0
- visidata/loaders/shp.py +6 -5
- visidata/loaders/spss.py +5 -6
- visidata/loaders/sqlite.py +68 -28
- visidata/loaders/texttables.py +1 -1
- visidata/loaders/toml.py +60 -0
- visidata/loaders/tsv.py +61 -19
- visidata/loaders/ttf.py +19 -7
- visidata/loaders/unzip_http.py +6 -5
- visidata/loaders/usv.py +1 -1
- visidata/loaders/vcf.py +16 -16
- visidata/loaders/vds.py +10 -7
- visidata/loaders/vdx.py +30 -5
- visidata/loaders/xlsb.py +8 -1
- visidata/loaders/xlsx.py +145 -25
- visidata/loaders/xml.py +6 -3
- visidata/loaders/xword.py +4 -4
- visidata/loaders/yaml.py +15 -5
- visidata/macros.py +129 -42
- visidata/main.py +119 -94
- visidata/mainloop.py +101 -155
- visidata/man/parse_options.py +2 -2
- visidata/man/vd.1 +301 -148
- visidata/man/vd.txt +290 -153
- visidata/memory.py +3 -3
- visidata/menu.py +104 -423
- visidata/metasheets.py +59 -141
- visidata/modify.py +78 -23
- visidata/motd.py +3 -3
- visidata/mouse.py +137 -0
- visidata/movement.py +43 -35
- visidata/optionssheet.py +99 -0
- visidata/path.py +113 -32
- visidata/pivot.py +73 -47
- visidata/plugins.py +65 -192
- visidata/pyobj.py +50 -201
- visidata/rename_col.py +20 -0
- visidata/save.py +37 -20
- visidata/search.py +54 -10
- visidata/selection.py +84 -5
- visidata/settings.py +162 -25
- visidata/sheets.py +229 -257
- visidata/shell.py +51 -21
- visidata/sidebar.py +162 -0
- visidata/sort.py +11 -4
- visidata/statusbar.py +113 -104
- visidata/stored_list.py +43 -0
- visidata/stored_prop.py +38 -0
- visidata/tests/conftest.py +3 -3
- visidata/tests/test_cliptext.py +39 -0
- visidata/tests/test_commands.py +62 -7
- visidata/tests/test_edittext.py +2 -2
- visidata/tests/test_features.py +17 -0
- visidata/tests/test_menu.py +14 -0
- visidata/tests/test_path.py +13 -4
- visidata/text_source.py +53 -0
- visidata/textsheet.py +10 -3
- visidata/theme.py +44 -0
- visidata/themes/__init__.py +0 -0
- visidata/themes/ascii8.py +84 -0
- visidata/themes/asciimono.py +84 -0
- visidata/themes/light.py +17 -0
- visidata/threads.py +87 -39
- visidata/tuiwin.py +22 -0
- visidata/type_currency.py +22 -3
- visidata/type_date.py +31 -9
- visidata/type_floatsi.py +5 -1
- visidata/undo.py +17 -5
- visidata/utils.py +106 -23
- visidata/vdobj.py +28 -17
- visidata/windows.py +10 -0
- visidata/wrappers.py +9 -3
- visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
- {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/vd.1 +301 -148
- {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +301 -148
- visidata-3.0.data/scripts/vd2to3.vdx +9 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/METADATA +12 -8
- visidata-3.0.dist-info/RECORD +257 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
- vgit/__init__.py +0 -1
- vgit/gitsheet.py +0 -164
- visidata/layout.py +0 -44
- visidata/misc.py +0 -5
- visidata-2.11.1.data/scripts/vgit +0 -9
- visidata-2.11.1.dist-info/RECORD +0 -155
- {vgit → visidata/apps/vgit}/__main__.py +0 -0
- {vgit → visidata/apps/vgit}/abort.py +0 -0
- /visidata/{repeat.py → features/repeat.py} +0 -0
- {visidata-2.11.1.data → visidata-3.0.data}/scripts/vd +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
visidata/themes/light.py
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
'Light-mode theme using 256-colors.'
|
2
|
+
|
3
|
+
from visidata import vd
|
4
|
+
|
5
|
+
vd.themes['light'] = dict(
|
6
|
+
color_default = 'black on white', # the default fg and bg colors
|
7
|
+
color_key_col = '20 blue', # color of key columns
|
8
|
+
color_edit_cell = '234 black', # cell color to use when editing cell
|
9
|
+
color_selected_row = '164 magenta', # color of selected rows
|
10
|
+
color_note_row = '164 magenta', # color of row note on left edge
|
11
|
+
color_note_type = '88 red', # color of cell note for non-str types in anytype columns
|
12
|
+
color_warning = '202 11 yellow',
|
13
|
+
color_add_pending = '34 green',
|
14
|
+
color_change_pending = '166 yellow',
|
15
|
+
plot_colors = '20 red magenta black 28 88 94 99 106'
|
16
|
+
)
|
17
|
+
|
visidata/threads.py
CHANGED
@@ -5,16 +5,17 @@ import functools
|
|
5
5
|
import cProfile
|
6
6
|
import threading
|
7
7
|
import collections
|
8
|
+
import subprocess
|
9
|
+
import curses
|
8
10
|
|
9
11
|
from visidata import VisiData, vd, options, globalCommand, Sheet, EscapeException
|
10
|
-
from visidata import ColumnAttr, Column
|
11
|
-
from visidata import *
|
12
|
+
from visidata import ColumnAttr, Column, BaseSheet, ItemColumn
|
12
13
|
|
13
14
|
|
14
|
-
vd.option('profile', False, 'enable profiling on threads')
|
15
|
-
vd.option('min_memory_mb', 0, 'minimum memory to continue loading and async processing')
|
15
|
+
vd.option('profile', False, 'enable profiling on threads', max_help=-1)
|
16
|
+
vd.option('min_memory_mb', 0, 'minimum memory to continue loading and async processing', max_help=-1)
|
16
17
|
|
17
|
-
vd.
|
18
|
+
vd.theme_option('color_working', '118 5', 'color of system running smoothly')
|
18
19
|
|
19
20
|
BaseSheet.init('currentThreads', list)
|
20
21
|
|
@@ -115,29 +116,32 @@ def elapsed_s(t):
|
|
115
116
|
|
116
117
|
@VisiData.api
|
117
118
|
def checkMemoryUsage(vd):
|
118
|
-
min_mem = options.min_memory_mb
|
119
119
|
threads = vd.unfinishedThreads
|
120
120
|
if not threads:
|
121
|
-
return
|
122
|
-
|
123
|
-
|
124
|
-
if min_mem:
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
vd.
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
121
|
+
return ''
|
122
|
+
|
123
|
+
min_mem = vd.options.min_memory_mb
|
124
|
+
if not min_mem:
|
125
|
+
return ''
|
126
|
+
|
127
|
+
try:
|
128
|
+
freestats = subprocess.run('free --total --mega'.split(), check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.strip().splitlines()
|
129
|
+
except FileNotFoundError as e:
|
130
|
+
if vd.options.debug:
|
131
|
+
vd.exceptionCaught(e)
|
132
|
+
vd.options.min_memory_mb = 0
|
133
|
+
vd.warning('disabling min_memory_mb: "free" not installed')
|
134
|
+
return ''
|
135
|
+
tot_m, used_m, free_m = map(int, freestats[-1].split()[1:])
|
136
|
+
ret = f' [{free_m}MB] '
|
137
|
+
if free_m < min_mem:
|
138
|
+
attr = '[:warning]'
|
139
|
+
vd.warning(f'{free_m}MB free < {min_mem}MB minimum, stopping threads')
|
140
|
+
vd.cancelThread(*vd.unfinishedThreads)
|
141
|
+
curses.flash()
|
142
|
+
else:
|
143
|
+
attr = '[:working]'
|
144
|
+
return attr + ret + '[/]'
|
141
145
|
|
142
146
|
|
143
147
|
# for progress bar
|
@@ -170,17 +174,34 @@ def _annotate_thread(t, endTime=None):
|
|
170
174
|
return t
|
171
175
|
|
172
176
|
# all long-running threads, including main and finished
|
173
|
-
|
177
|
+
vd.threads = [_annotate_thread(threading.current_thread(), 0)]
|
174
178
|
|
175
179
|
@VisiData.api
|
176
|
-
def
|
177
|
-
'Execute ``func(*args, **kwargs)`` in
|
180
|
+
def execSync(vd, func, *args, sheet=None, **kwargs):
|
181
|
+
'Execute ``func(*args, **kwargs)`` in this thread (synchronously). A drop-in substitute for vd.execAsync.'
|
182
|
+
vd.callNoExceptions(func, *args, **kwargs)
|
183
|
+
t = threading.current_thread()
|
184
|
+
t.sheet = sheet or vd.activeSheet
|
185
|
+
return t
|
178
186
|
|
179
|
-
|
180
|
-
|
187
|
+
@VisiData.api
|
188
|
+
def execAsync(vd, func, *args, **kwargs):
|
189
|
+
'''Execute ``func(*args, **kwargs)`` in a separate thread. `sheet` is a
|
190
|
+
special kwarg to indicate which sheet the thread should be associated with;
|
191
|
+
by default, uses vd.activeSheet. If `sheet` explicitly given as None, the thread
|
192
|
+
will be ignored by vd.sync and thread status indicators.
|
193
|
+
'''
|
194
|
+
|
195
|
+
if 'sheet' not in kwargs:
|
196
|
+
sheet = vd.activeSheet
|
197
|
+
else:
|
198
|
+
sheet = kwargs.pop('sheet')
|
199
|
+
|
200
|
+
if sheet is not None and (sheet.lastCommandThreads and threading.current_thread() not in sheet.lastCommandThreads):
|
201
|
+
vd.fail(f'still running **{sheet.lastCommandThreads[-1].name}** from previous command')
|
181
202
|
|
182
|
-
|
183
|
-
|
203
|
+
thread = threading.Thread(target=_toplevelTryFunc, daemon=True, args=(func,)+args, kwargs=kwargs)
|
204
|
+
vd.threads.append(_annotate_thread(thread))
|
184
205
|
|
185
206
|
if sheet is not None:
|
186
207
|
sheet.currentThreads.append(thread)
|
@@ -190,7 +211,7 @@ def execAsync(self, func, *args, sheet=None, **kwargs):
|
|
190
211
|
|
191
212
|
return thread
|
192
213
|
|
193
|
-
def _toplevelTryFunc(func, *args,
|
214
|
+
def _toplevelTryFunc(func, *args, **kwargs):
|
194
215
|
with ThreadProfiler(threading.current_thread()) as prof:
|
195
216
|
t = threading.current_thread()
|
196
217
|
t.name = func.__name__
|
@@ -198,8 +219,7 @@ def _toplevelTryFunc(func, *args, status=vd.status, **kwargs):
|
|
198
219
|
t.status = func(*args, **kwargs)
|
199
220
|
except EscapeException as e: # user aborted
|
200
221
|
t.status = 'aborted by user'
|
201
|
-
|
202
|
-
status('%s aborted' % t.name, priority=2)
|
222
|
+
vd.warning(f'{t.name} aborted')
|
203
223
|
except Exception as e:
|
204
224
|
t.exception = e
|
205
225
|
t.status = 'exception'
|
@@ -208,6 +228,18 @@ def _toplevelTryFunc(func, *args, status=vd.status, **kwargs):
|
|
208
228
|
if t.sheet:
|
209
229
|
t.sheet.currentThreads.remove(t)
|
210
230
|
|
231
|
+
def asyncignore(func):
|
232
|
+
'Decorator like `@asyncthread` but without attaching to a sheet, so no sheet.threadStatus will show it.'
|
233
|
+
@functools.wraps(func)
|
234
|
+
def _execAsync(*args, **kwargs):
|
235
|
+
@functools.wraps(func)
|
236
|
+
def _func(*args, **kwargs):
|
237
|
+
func(*args, **kwargs)
|
238
|
+
|
239
|
+
return vd.execAsync(_func, *args, **kwargs, sheet=None)
|
240
|
+
|
241
|
+
return _execAsync
|
242
|
+
|
211
243
|
def asyncsingle(func):
|
212
244
|
'''Function decorator like `@asyncthread` but as a singleton. When called, `func(...)` spawns a new thread, and cancels any previous thread still running *func*.
|
213
245
|
``vd.sync()`` does not wait for unfinished asyncsingle threads.
|
@@ -233,7 +265,7 @@ def asyncsingle(func):
|
|
233
265
|
@VisiData.property
|
234
266
|
def unfinishedThreads(self):
|
235
267
|
'A list of unfinished threads (those without a recorded `endTime`).'
|
236
|
-
return [t for t in self.threads if getattr(t, 'endTime', None) is None]
|
268
|
+
return [t for t in self.threads if getattr(t, 'endTime', None) is None and getattr(t, 'sheet', None) is not None]
|
237
269
|
|
238
270
|
@VisiData.api
|
239
271
|
def checkForFinishedThreads(self):
|
@@ -251,8 +283,9 @@ def sync(self, *joiningThreads):
|
|
251
283
|
while True:
|
252
284
|
deads = set() # dead threads
|
253
285
|
threads = joiningThreads or set(self.unfinishedThreads)
|
254
|
-
threads -= set([threading.current_thread(), getattr(vd, 'drawThread', None)])
|
286
|
+
threads -= set([threading.current_thread(), getattr(vd, 'drawThread', None), getattr(vd, 'outputProgressThread', None)])
|
255
287
|
threads -= deads
|
288
|
+
threads -= set([None])
|
256
289
|
for t in threads:
|
257
290
|
try:
|
258
291
|
if not t.is_alive() or t not in threading.enumerate() or getattr(t, 'noblock', False) is True:
|
@@ -272,7 +305,7 @@ min_thread_time_s = 0.10 # only keep threads that take longer than this number o
|
|
272
305
|
@VisiData.api
|
273
306
|
def open_pyprof(vd, p):
|
274
307
|
import pstats
|
275
|
-
return ProfileStatsSheet(p.
|
308
|
+
return ProfileStatsSheet(p.base_stem, source=pstats.Stats(p.given).stats)
|
276
309
|
|
277
310
|
|
278
311
|
@VisiData.api
|
@@ -308,10 +341,19 @@ class ThreadProfiler:
|
|
308
341
|
# remove very-short-lived async actions
|
309
342
|
if elapsed_s(self.thread) < min_thread_time_s:
|
310
343
|
vd.threads.remove(self.thread)
|
344
|
+
else:
|
345
|
+
if vd.options.profile:
|
346
|
+
self.thread.profile.dump_stats(f'{self.thread.name}.pyprof')
|
311
347
|
|
312
348
|
|
313
349
|
class ProfileSheet(Sheet):
|
314
350
|
rowtype = 'callsites' # rowdef: profiler_entry
|
351
|
+
guide = '''
|
352
|
+
# Profile Sheet
|
353
|
+
- `z Ctrl+S` to save as pyprof file
|
354
|
+
- `Ctrl+O` to open current function in $EDITOR
|
355
|
+
- `Enter` to open list of calls from current function
|
356
|
+
'''
|
315
357
|
columns = [
|
316
358
|
Column('funcname', getter=lambda col,row: codestr(row.code)),
|
317
359
|
Column('filename', getter=lambda col,row: os.path.split(row.code.co_filename)[-1] if not isinstance(row.code, str) else ''),
|
@@ -404,4 +446,10 @@ vd.addGlobals({
|
|
404
446
|
'Progress': Progress,
|
405
447
|
'asynccache': asynccache,
|
406
448
|
'asyncsingle': asyncsingle,
|
449
|
+
'asyncignore': asyncignore,
|
407
450
|
})
|
451
|
+
|
452
|
+
vd.addMenuItems('''
|
453
|
+
System > Threads sheet > threads-all
|
454
|
+
System > Toggle profiling > toggle-profile
|
455
|
+
''')
|
visidata/tuiwin.py
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
from visidata import VisiData, vd
|
2
|
+
|
3
|
+
vd._parentscrs = {} # scr -> parentscr
|
4
|
+
|
5
|
+
|
6
|
+
@VisiData.api
|
7
|
+
def subwindow(vd, scr, x, y, w, h):
|
8
|
+
'Return subwindow with its (0,0) at (x,y) relative to parent scr. Replacement for scr.derwin() to track parent scr.'
|
9
|
+
newscr = scr.derwin(h, w, y, x)
|
10
|
+
vd._parentscrs[newscr] = scr
|
11
|
+
return newscr
|
12
|
+
|
13
|
+
|
14
|
+
@VisiData.api
|
15
|
+
def getrootxy(vd, scr): # like scr.getparyx() but for all ancestor scrs
|
16
|
+
px, py = 0, 0
|
17
|
+
while scr in vd._parentscrs:
|
18
|
+
dy, dx = scr.getparyx()
|
19
|
+
if dy > 0: py += dy
|
20
|
+
if dx > 0: px += dx
|
21
|
+
scr = vd._parentscrs[scr]
|
22
|
+
return px, py
|
visidata/type_currency.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
from visidata import vd, Sheet
|
1
|
+
from visidata import vd, Sheet, Column
|
2
2
|
|
3
|
-
vd.option('disp_currency_fmt', '%.02f', 'default fmtstr to format for currency values', replay=True)
|
3
|
+
vd.option('disp_currency_fmt', '%.02f', 'default fmtstr to format for currency values', replay=True, help=vd.help_float_fmt)
|
4
|
+
vd.theme_option('color_currency_neg', 'red', 'color for negative values in currency displayer', replay=True)
|
4
5
|
|
5
6
|
|
6
7
|
floatchars='+-0123456789.'
|
@@ -13,4 +14,22 @@ def currency(*args):
|
|
13
14
|
return float(*args)
|
14
15
|
|
15
16
|
|
16
|
-
|
17
|
+
@Column.api
|
18
|
+
def displayer_currency(col, dw, width=None):
|
19
|
+
text = dw.text
|
20
|
+
|
21
|
+
if isinstance(dw.typedval, (int, float)):
|
22
|
+
if dw.typedval < 0:
|
23
|
+
text = f'({dw.text[1:]})'.rjust(width-1)
|
24
|
+
yield ('currency_neg', '')
|
25
|
+
else:
|
26
|
+
text = text.rjust(width-2)
|
27
|
+
|
28
|
+
yield ('', text)
|
29
|
+
|
30
|
+
|
31
|
+
Sheet.addCommand('$', 'type-currency', 'cursorCol.type=currency', 'set type of current column to currency')
|
32
|
+
|
33
|
+
vd.addMenuItems('''
|
34
|
+
Column > Type as > dirty float > type-currency
|
35
|
+
''')
|
visidata/type_date.py
CHANGED
@@ -1,16 +1,33 @@
|
|
1
1
|
import datetime
|
2
2
|
|
3
|
-
from visidata import vd, Sheet
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
from visidata import VisiData, vd, Sheet
|
4
|
+
|
5
|
+
@VisiData.lazy_property
|
6
|
+
def date_parse(vd):
|
7
|
+
try:
|
8
|
+
from dateutil.parser import parse
|
9
|
+
return parse
|
10
|
+
except ImportError:
|
9
11
|
vd.warning('install python-dateutil for date type')
|
10
|
-
return
|
12
|
+
return str
|
13
|
+
|
14
|
+
vd.help_date = '''
|
15
|
+
- RFC3339: `%Y-%m-%d %H:%M:%S.%f %z`
|
16
|
+
- `%A` Weekday as locale’s full name.
|
17
|
+
- `%w` Weekday as a decimal number, where 0 is Sunday and 6 is Saturday.
|
18
|
+
- `%d` Day of the month as a zero-padded decimal number.
|
19
|
+
- `%b` Month as locale’s abbreviated name.
|
20
|
+
- `%B` Month as locale’s full name.
|
21
|
+
- `%p` Locale’s equivalent of either AM or PM.
|
22
|
+
- `%c` Locale’s appropriate date and time representation.
|
23
|
+
- `%x` Locale’s appropriate date representation.
|
24
|
+
- `%X` Locale’s appropriate time representation.
|
25
|
+
- `%Z` Time zone name (empty string if the object is naive).
|
11
26
|
|
27
|
+
See [:onclick https://strftime.org]Python strftime()[/] for a full list of format codes.
|
28
|
+
'''
|
12
29
|
|
13
|
-
vd.option('disp_date_fmt','%Y-%m-%d', 'default fmtstr to strftime for date values', replay=True)
|
30
|
+
vd.option('disp_date_fmt','%Y-%m-%d', 'default fmtstr passed to strftime for date values', replay=True, help=vd.help_date)
|
14
31
|
|
15
32
|
|
16
33
|
@vd.numericType('@', '', formatter=lambda fmtstr,val: val.strftime(fmtstr or vd.options.disp_date_fmt))
|
@@ -28,7 +45,7 @@ class date(datetime.datetime):
|
|
28
45
|
if isinstance(s, int) or isinstance(s, float):
|
29
46
|
r = datetime.datetime.fromtimestamp(s)
|
30
47
|
elif isinstance(s, str):
|
31
|
-
r = date_parse(s)
|
48
|
+
r = vd.date_parse(s)
|
32
49
|
elif isinstance(s, (datetime.datetime, datetime.date)):
|
33
50
|
r = s
|
34
51
|
else:
|
@@ -109,3 +126,8 @@ vd.addGlobals(
|
|
109
126
|
|
110
127
|
|
111
128
|
Sheet.addCommand('@', 'type-date', 'cursorCol.type = date', 'set type of current column to date')
|
129
|
+
Sheet.addCommand('', 'type-datetime', 'cursorCol.type=date; cursorCol.fmtstr="%Y-%m-%d %H:%M:%S"', 'set type of current column to datetime')
|
130
|
+
|
131
|
+
vd.addMenuItems('''
|
132
|
+
Column > Type as > date > type-date
|
133
|
+
''')
|
visidata/type_floatsi.py
CHANGED
@@ -22,7 +22,7 @@ def floatsi(*args):
|
|
22
22
|
if not args:
|
23
23
|
return 0.0
|
24
24
|
if not isinstance(args[0], str):
|
25
|
-
return args[0]
|
25
|
+
return float(args[0])
|
26
26
|
|
27
27
|
s=args[0].strip()
|
28
28
|
for i, p in enumerate(vd.si_prefixes):
|
@@ -33,3 +33,7 @@ def floatsi(*args):
|
|
33
33
|
|
34
34
|
|
35
35
|
Sheet.addCommand('z%', 'type-floatsi', 'cursorCol.type = floatsi', 'set type of current column to SI float')
|
36
|
+
|
37
|
+
vd.addMenuItems('''
|
38
|
+
Column > Type as > SI float > type-floatsi
|
39
|
+
''')
|
visidata/undo.py
CHANGED
@@ -18,13 +18,14 @@ def isUndoableCommand(longname):
|
|
18
18
|
@VisiData.api
|
19
19
|
def addUndo(vd, undofunc, *args, **kwargs):
|
20
20
|
'On undo of latest command, call ``undofunc(*args, **kwargs)``.'
|
21
|
-
if options.undo:
|
21
|
+
if vd.options.undo:
|
22
22
|
# occurs when VisiData is just starting up
|
23
23
|
if getattr(vd, 'activeCommand', UNLOADED) is UNLOADED:
|
24
24
|
return
|
25
25
|
r = vd.modifyCommand
|
26
26
|
# some special commands, like open-file, do not have an undofuncs set
|
27
|
-
|
27
|
+
# do not set undofuncs for non-logged commands
|
28
|
+
if not r or not isUndoableCommand(r.longname) or not vd.activeCommand or not vd.isLoggableCommand(vd.activeCommand.longname):
|
28
29
|
return
|
29
30
|
if not r.undofuncs:
|
30
31
|
r.undofuncs = []
|
@@ -33,16 +34,17 @@ def addUndo(vd, undofunc, *args, **kwargs):
|
|
33
34
|
|
34
35
|
@VisiData.api
|
35
36
|
def undo(vd, sheet):
|
36
|
-
if not options.undo:
|
37
|
+
if not vd.options.undo:
|
37
38
|
vd.fail("options.undo not enabled")
|
38
39
|
|
39
40
|
# don't allow undo of first command on a sheet, which is always the command that created the sheet.
|
40
|
-
for cmdlogrow in sheet.cmdlog_sheet.rows[:0:-1]:
|
41
|
+
for i, cmdlogrow in enumerate(sheet.cmdlog_sheet.rows[:0:-1]):
|
41
42
|
if cmdlogrow.undofuncs:
|
42
43
|
for undofunc, args, kwargs, in cmdlogrow.undofuncs[::-1]:
|
43
44
|
undofunc(*args, **kwargs)
|
44
45
|
sheet.undone.append(cmdlogrow)
|
45
|
-
sheet.cmdlog_sheet.rows
|
46
|
+
row_idx = len(sheet.cmdlog_sheet.rows)-1 - i
|
47
|
+
del sheet.cmdlog_sheet.rows[row_idx]
|
46
48
|
|
47
49
|
vd.clearCaches() # undofunc can invalidate the drawcache
|
48
50
|
|
@@ -113,3 +115,13 @@ def addUndoColNames(vd, cols):
|
|
113
115
|
|
114
116
|
BaseSheet.addCommand('U', 'undo-last', 'vd.undo(sheet)', 'Undo the most recent change (options.undo must be enabled)')
|
115
117
|
BaseSheet.addCommand('R', 'redo-last', 'vd.redo(sheet)', 'Redo the most recent undo (options.undo must be enabled)')
|
118
|
+
|
119
|
+
vd.addGlobals(
|
120
|
+
undoAttrFunc=undoAttrFunc,
|
121
|
+
Fanout=Fanout,
|
122
|
+
undoAttrCopyFunc=undoAttrCopyFunc)
|
123
|
+
|
124
|
+
vd.addMenuItems('''
|
125
|
+
Edit > Undo > undo-last
|
126
|
+
Edit > Redo > redo-last
|
127
|
+
''')
|
visidata/utils.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
+
from contextlib import contextmanager
|
1
2
|
import operator
|
2
3
|
import string
|
3
4
|
import re
|
4
5
|
|
5
6
|
'Various helper classes and functions.'
|
6
7
|
|
7
|
-
__all__ = ['AlwaysDict', 'AttrDict', 'moveListItem', 'namedlist', 'classproperty', '
|
8
|
+
__all__ = ['AlwaysDict', 'AttrDict', 'DefaultAttrDict', 'moveListItem', 'namedlist', 'classproperty', 'MissingAttrFormatter', 'getitem', 'setitem', 'getitemdef', 'getitemdeep', 'setitemdeep', 'getattrdeep', 'setattrdeep', 'ExplodingMock', 'ScopedSetattr']
|
8
9
|
|
9
10
|
|
10
11
|
class AlwaysDict(dict):
|
@@ -36,6 +37,24 @@ class AttrDict(dict):
|
|
36
37
|
return self.keys()
|
37
38
|
|
38
39
|
|
40
|
+
class DefaultAttrDict(dict):
|
41
|
+
'Augment a dict with more convenient .attr syntax. not-present keys store new DefaultAttrDict. like a recursive defaultdict.'
|
42
|
+
def __getattr__(self, k):
|
43
|
+
if k not in self:
|
44
|
+
if k.startswith("__"):
|
45
|
+
raise AttributeError from e
|
46
|
+
self[k] = DefaultAttrDict()
|
47
|
+
return self[k]
|
48
|
+
|
49
|
+
def __setattr__(self, k, v):
|
50
|
+
self[k] = v
|
51
|
+
|
52
|
+
def __dir__(self):
|
53
|
+
return self.keys()
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
|
39
58
|
class classproperty(property):
|
40
59
|
def __get__(self, cls, obj):
|
41
60
|
return classmethod(self.fget).__get__(None, obj or cls)()
|
@@ -50,34 +69,75 @@ def moveListItem(L, fromidx, toidx):
|
|
50
69
|
return toidx
|
51
70
|
|
52
71
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
72
|
+
def setitem(r, i, v): # function needed for use in lambda
|
73
|
+
r[i] = v
|
74
|
+
return True
|
75
|
+
|
76
|
+
def getitem(o, k, default=None):
|
77
|
+
return default if o is None else o[k]
|
78
|
+
|
79
|
+
def getitemdef(o, k, default=None):
|
80
|
+
try:
|
81
|
+
return default if o is None else o[k]
|
82
|
+
except Exception:
|
83
|
+
return default
|
84
|
+
|
58
85
|
|
86
|
+
def getattrdeep(obj, attr, *default, getter=getattr):
|
87
|
+
try:
|
88
|
+
'Return dotted attr (like "a.b.c") from obj, or default if any of the components are missing.'
|
89
|
+
if not isinstance(attr, str):
|
90
|
+
return getter(obj, attr, *default)
|
91
|
+
|
92
|
+
try: # if attribute exists, return toplevel value, even if dotted
|
93
|
+
if attr in obj:
|
94
|
+
return getter(obj, attr)
|
95
|
+
except RecursionError: #1696
|
96
|
+
raise
|
97
|
+
except Exception as e:
|
98
|
+
pass
|
99
|
+
|
100
|
+
attrs = attr.split('.')
|
101
|
+
for a in attrs[:-1]:
|
102
|
+
obj = getter(obj, a)
|
103
|
+
|
104
|
+
return getter(obj, attrs[-1])
|
105
|
+
except Exception as e:
|
106
|
+
if not default: raise
|
107
|
+
return default[0]
|
59
108
|
|
60
|
-
class OnExit:
|
61
|
-
'"with OnExit(func, ...):" calls func(...) when the context is exited'
|
62
|
-
def __init__(self, func, *args, **kwargs):
|
63
|
-
self.func = func
|
64
|
-
self.args = args
|
65
|
-
self.kwargs = kwargs
|
66
109
|
|
67
|
-
|
68
|
-
|
110
|
+
def setattrdeep(obj, attr, val, getter=getattr, setter=setattr):
|
111
|
+
'Set dotted attr (like "a.b.c") on obj to val.'
|
112
|
+
if not isinstance(attr, str):
|
113
|
+
return setter(obj, attr, val)
|
69
114
|
|
70
|
-
|
115
|
+
try: # if attribute exists, overwrite toplevel value, even if dotted
|
116
|
+
getter(obj, attr)
|
117
|
+
return setter(obj, attr, val)
|
118
|
+
except Exception as e:
|
119
|
+
pass
|
120
|
+
|
121
|
+
attrs = attr.split('.')
|
122
|
+
for a in attrs[:-1]:
|
71
123
|
try:
|
72
|
-
|
124
|
+
obj = getter(obj, a)
|
73
125
|
except Exception as e:
|
74
|
-
|
126
|
+
obj = obj[a] = type(obj)() # assume homogeneous nesting
|
127
|
+
|
128
|
+
setter(obj, attrs[-1], val)
|
75
129
|
|
76
130
|
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
|
131
|
+
def getitemdeep(obj, k, *default):
|
132
|
+
if not isinstance(k, str):
|
133
|
+
try:
|
134
|
+
return obj[k]
|
135
|
+
except IndexError:
|
136
|
+
pass
|
137
|
+
return getattrdeep(obj, k, *default, getter=getitem)
|
138
|
+
|
139
|
+
def setitemdeep(obj, k, val):
|
140
|
+
return setattrdeep(obj, k, val, getter=getitemdef, setter=setitem)
|
81
141
|
|
82
142
|
|
83
143
|
def namedlist(objname, fieldnames):
|
@@ -111,11 +171,24 @@ def namedlist(objname, fieldnames):
|
|
111
171
|
|
112
172
|
return NamedListTemplate
|
113
173
|
|
174
|
+
|
175
|
+
class ExplodingMock:
|
176
|
+
'A mock object that raises an exception for everything except conversion to True/False.'
|
177
|
+
def __init__(self, msg):
|
178
|
+
self.__msg = msg
|
179
|
+
|
180
|
+
def __getattr__(self, k):
|
181
|
+
raise Exception(self.__msg)
|
182
|
+
|
183
|
+
def __bool__(self):
|
184
|
+
return False
|
185
|
+
|
186
|
+
|
114
187
|
class MissingAttrFormatter(string.Formatter):
|
115
188
|
"formats {} fields with `''`, that would normally result in a raised KeyError or AttributeError; intended for user customisable format strings."
|
116
|
-
def get_field(self, field_name,
|
189
|
+
def get_field(self, field_name, args, kwargs):
|
117
190
|
try:
|
118
|
-
return super().get_field(field_name,
|
191
|
+
return super().get_field(field_name, args, kwargs)
|
119
192
|
except (KeyError, AttributeError):
|
120
193
|
return (None, field_name)
|
121
194
|
|
@@ -126,3 +199,13 @@ class MissingAttrFormatter(string.Formatter):
|
|
126
199
|
elif not value:
|
127
200
|
return str(value)
|
128
201
|
return super().format_field(value, format_spec)
|
202
|
+
|
203
|
+
|
204
|
+
@contextmanager
|
205
|
+
def ScopedSetattr(obj, attrname, val):
|
206
|
+
oldval = getattr(obj, attrname)
|
207
|
+
try:
|
208
|
+
setattr(obj, attrname, val)
|
209
|
+
yield
|
210
|
+
finally:
|
211
|
+
setattr(obj, attrname, oldval)
|