visidata 3.0.2__py3-none-any.whl → 3.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- visidata/__init__.py +12 -10
- visidata/_input.py +208 -202
- visidata/_open.py +4 -1
- visidata/_types.py +4 -3
- visidata/aggregators.py +88 -39
- visidata/apps/vdsql/_ibis.py +7 -11
- visidata/apps/vdsql/clickhouse.py +2 -2
- visidata/apps/vdsql/snowflake.py +1 -1
- visidata/apps/vgit/status.py +1 -1
- visidata/basesheet.py +11 -4
- visidata/canvas.py +54 -20
- visidata/clipboard.py +13 -6
- visidata/cliptext.py +7 -6
- visidata/cmdlog.py +40 -27
- visidata/column.py +14 -49
- visidata/ddw/regex.ddw +3 -2
- visidata/deprecated.py +14 -2
- visidata/desktop/visidata.desktop +2 -2
- visidata/editor.py +1 -0
- visidata/errors.py +1 -1
- visidata/experimental/sort_selected.py +54 -0
- visidata/expr.py +69 -18
- visidata/features/change_precision.py +1 -3
- visidata/features/cmdpalette.py +17 -2
- visidata/features/colorsheet.py +1 -1
- visidata/features/dedupe.py +3 -3
- visidata/features/go_col.py +71 -0
- visidata/features/graph_seaborn.py +1 -1
- visidata/features/join.py +20 -10
- visidata/features/layout.py +16 -3
- visidata/features/ping.py +16 -12
- visidata/features/regex.py +5 -5
- visidata/features/status_source.py +3 -1
- visidata/features/sysedit.py +1 -1
- visidata/features/transpose.py +2 -1
- visidata/features/type_ipaddr.py +2 -4
- visidata/features/unfurl.py +1 -0
- visidata/form.py +2 -2
- visidata/freqtbl.py +16 -11
- visidata/fuzzymatch.py +1 -0
- visidata/graph.py +163 -12
- visidata/guide.py +57 -24
- visidata/guides/ClipboardGuide.md +48 -0
- visidata/guides/ColumnsGuide.md +52 -0
- visidata/guides/CommandsSheet.md +28 -0
- visidata/guides/DirSheet.md +34 -0
- visidata/{features/errors_guide.py → guides/ErrorsSheet.md} +2 -11
- visidata/guides/FrequencyTable.md +42 -0
- visidata/guides/GrepSheet.md +28 -0
- visidata/guides/JsonSheet.md +38 -0
- visidata/guides/MacrosSheet.md +19 -0
- visidata/guides/MeltGuide.md +52 -0
- visidata/guides/MemorySheet.md +7 -0
- visidata/guides/MenuGuide.md +26 -0
- visidata/guides/ModifyGuide.md +38 -0
- visidata/guides/PivotGuide.md +71 -0
- visidata/guides/RegexGuide.md +107 -0
- visidata/guides/SelectionGuide.md +44 -0
- visidata/guides/SlideGuide.md +26 -0
- visidata/guides/SplitpaneGuide.md +15 -0
- visidata/guides/TypesSheet.md +43 -0
- visidata/guides/XsvGuide.md +36 -0
- visidata/help.py +6 -6
- visidata/hint.py +2 -1
- visidata/indexsheet.py +2 -2
- visidata/interface.py +13 -14
- visidata/keys.py +4 -1
- visidata/loaders/api_airtable.py +1 -1
- visidata/loaders/archive.py +1 -1
- visidata/loaders/csv.py +9 -5
- visidata/loaders/eml.py +11 -6
- visidata/loaders/f5log.py +1 -0
- visidata/loaders/fec.py +18 -42
- visidata/loaders/fixed_width.py +19 -3
- visidata/loaders/grep.py +121 -0
- visidata/loaders/html.py +1 -0
- visidata/loaders/http.py +6 -1
- visidata/loaders/json.py +22 -4
- visidata/loaders/jsonla.py +8 -2
- visidata/loaders/mailbox.py +1 -0
- visidata/loaders/markdown.py +25 -6
- visidata/loaders/msgpack.py +19 -0
- visidata/loaders/npy.py +0 -1
- visidata/loaders/odf.py +18 -4
- visidata/loaders/orgmode.py +1 -1
- visidata/loaders/rec.py +6 -4
- visidata/loaders/sas.py +11 -4
- visidata/loaders/scrape.py +0 -1
- visidata/loaders/texttables.py +2 -0
- visidata/loaders/tsv.py +24 -7
- visidata/loaders/unzip_http.py +127 -3
- visidata/loaders/vds.py +4 -0
- visidata/loaders/vdx.py +1 -1
- visidata/loaders/xlsx.py +5 -0
- visidata/loaders/xml.py +2 -1
- visidata/macros.py +14 -31
- visidata/main.py +14 -13
- visidata/mainloop.py +14 -6
- visidata/man/vd.1 +72 -39
- visidata/man/vd.txt +72 -41
- visidata/memory.py +15 -4
- visidata/menu.py +14 -3
- visidata/metasheets.py +5 -6
- visidata/modify.py +4 -4
- visidata/mouse.py +2 -0
- visidata/movement.py +14 -28
- visidata/optionssheet.py +3 -5
- visidata/path.py +59 -37
- visidata/pivot.py +8 -5
- visidata/pyobj.py +63 -9
- visidata/save.py +16 -9
- visidata/search.py +4 -4
- visidata/selection.py +10 -56
- visidata/settings.py +37 -35
- visidata/sheets.py +186 -108
- visidata/shell.py +22 -12
- visidata/sidebar.py +71 -16
- visidata/sort.py +21 -6
- visidata/statusbar.py +42 -5
- visidata/stored_list.py +5 -2
- visidata/tests/conftest.py +1 -0
- visidata/tests/test_commands.py +9 -1
- visidata/tests/test_completer.py +18 -0
- visidata/tests/test_edittext.py +3 -2
- visidata/text_source.py +7 -4
- visidata/textsheet.py +20 -6
- visidata/themes/ascii8.py +9 -6
- visidata/themes/asciimono.py +14 -4
- visidata/threads.py +13 -3
- visidata/tuiwin.py +5 -1
- visidata/type_currency.py +1 -2
- visidata/type_date.py +6 -1
- visidata/undo.py +10 -5
- visidata/utils.py +9 -3
- visidata/vdobj.py +21 -1
- visidata/wrappers.py +9 -1
- {visidata-3.0.2.data → visidata-3.1.1.data}/data/share/applications/visidata.desktop +2 -2
- {visidata-3.0.2.data → visidata-3.1.1.data}/data/share/man/man1/vd.1 +72 -39
- {visidata-3.0.2.data → visidata-3.1.1.data}/data/share/man/man1/visidata.1 +72 -39
- {visidata-3.0.2.dist-info → visidata-3.1.1.dist-info}/METADATA +24 -6
- visidata-3.1.1.dist-info/RECORD +280 -0
- visidata/loaders/api_bitio.py +0 -102
- visidata/stored_prop.py +0 -38
- visidata-3.0.2.dist-info/RECORD +0 -258
- {visidata-3.0.2.data → visidata-3.1.1.data}/scripts/vd +0 -0
- {visidata-3.0.2.data → visidata-3.1.1.data}/scripts/vd2to3.vdx +0 -0
- {visidata-3.0.2.dist-info → visidata-3.1.1.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-3.0.2.dist-info → visidata-3.1.1.dist-info}/WHEEL +0 -0
- {visidata-3.0.2.dist-info → visidata-3.1.1.dist-info}/entry_points.txt +0 -0
- {visidata-3.0.2.dist-info → visidata-3.1.1.dist-info}/top_level.txt +0 -0
visidata/_types.py
CHANGED
@@ -53,9 +53,8 @@ def numericFormatter(vd, fmtstr, typedval):
|
|
53
53
|
def numericType(vd, icon='', fmtstr='', formatter=vd.numericFormatter):
|
54
54
|
'''Decorator for numeric types.'''
|
55
55
|
def _decorator(f):
|
56
|
-
vd.addType(f, icon=icon, fmtstr=fmtstr, formatter=formatter)
|
56
|
+
vd.addType(f, icon=icon, fmtstr=fmtstr, formatter=formatter, name=f.__name__)
|
57
57
|
vd.numericTypes.append(f)
|
58
|
-
vd.addGlobals({f.__name__: f})
|
59
58
|
return f
|
60
59
|
return _decorator
|
61
60
|
|
@@ -82,6 +81,8 @@ def addType(vd, typetype=None, icon=None, fmtstr='', formatter=vd.numericFormatt
|
|
82
81
|
t = VisiDataType(typetype=typetype, icon=icon, fmtstr=fmtstr, formatter=formatter, key=key, name=name)
|
83
82
|
if typetype:
|
84
83
|
vd.typemap[typetype] = t
|
84
|
+
if name:
|
85
|
+
vd.addGlobals({name: typetype})
|
85
86
|
return t
|
86
87
|
|
87
88
|
vdtype = vd.addType
|
@@ -94,7 +95,7 @@ vd.typemap = {}
|
|
94
95
|
def getType(vd, typetype):
|
95
96
|
return vd.typemap.get(typetype) or VisiDataType()
|
96
97
|
|
97
|
-
vdtype(None, '∅')
|
98
|
+
vdtype(None, '∅', name='none')
|
98
99
|
vdtype(anytype, '', formatter=lambda _,v: str(v))
|
99
100
|
vdtype(str, '~', formatter=lambda _,v: v)
|
100
101
|
vdtype(int, '#')
|
visidata/aggregators.py
CHANGED
@@ -5,7 +5,7 @@ import collections
|
|
5
5
|
import statistics
|
6
6
|
|
7
7
|
from visidata import Progress, Sheet, Column, ColumnsSheet, VisiData
|
8
|
-
from visidata import vd, anytype, vlen, asyncthread, wrapply, AttrDict
|
8
|
+
from visidata import vd, anytype, vlen, asyncthread, wrapply, AttrDict, date, INPROGRESS
|
9
9
|
|
10
10
|
vd.help_aggregators = '''# Choose Aggregators
|
11
11
|
Start typing an aggregator name or description.
|
@@ -48,10 +48,15 @@ def getValues(self, rows):
|
|
48
48
|
vd.aggregators = collections.OrderedDict() # [aggname] -> annotated func, or list of same
|
49
49
|
|
50
50
|
Column.init('aggstr', str, copy=True)
|
51
|
+
Column.init('_aggregatedTotals', dict) # [aggname] -> agg total over all rows
|
51
52
|
|
52
53
|
def aggregators_get(col):
|
53
54
|
'A space-separated names of aggregators on this column.'
|
54
|
-
|
55
|
+
aggs = []
|
56
|
+
for k in (col.aggstr or '').split():
|
57
|
+
agg = vd.aggregators[k]
|
58
|
+
aggs += agg if isinstance(agg, list) else [agg]
|
59
|
+
return aggs
|
55
60
|
|
56
61
|
def aggregators_set(col, aggs):
|
57
62
|
if isinstance(aggs, str):
|
@@ -71,32 +76,28 @@ Column.aggregators = property(aggregators_get, aggregators_set)
|
|
71
76
|
|
72
77
|
|
73
78
|
class Aggregator:
|
74
|
-
def __init__(self, name, type,
|
75
|
-
'Define aggregator `name` that calls
|
79
|
+
def __init__(self, name, type, funcValues=None, helpstr='foo'):
|
80
|
+
'Define aggregator `name` that calls funcValues(values)'
|
76
81
|
self.type = type
|
77
|
-
self.
|
78
|
-
self.funcValues = funcValues # funcValues(values, *args)
|
82
|
+
self.funcValues = funcValues # funcValues(values)
|
79
83
|
self.helpstr = helpstr
|
80
84
|
self.name = name
|
81
85
|
|
82
|
-
def
|
83
|
-
return self.func(*args, **kwargs)
|
84
|
-
|
85
|
-
_defaggr = Aggregator
|
86
|
-
|
87
|
-
@VisiData.api
|
88
|
-
def aggregator(vd, name, funcValues, helpstr='', *args, type=None):
|
89
|
-
'Define simple aggregator *name* that calls ``funcValues(values, *args)`` to aggregate *values*. Use *type* to force the default type of the aggregated column.'
|
90
|
-
def _funcRows(col, rows): # wrap builtins so they can have a .type
|
86
|
+
def aggregate(self, col, rows): # wrap builtins so they can have a .type
|
91
87
|
vals = list(col.getValues(rows))
|
92
88
|
try:
|
93
|
-
return funcValues(vals
|
89
|
+
return self.funcValues(vals)
|
94
90
|
except Exception as e:
|
95
91
|
if len(vals) == 0:
|
96
92
|
return None
|
97
|
-
|
93
|
+
raise e
|
98
94
|
|
99
|
-
|
95
|
+
|
96
|
+
@VisiData.api
|
97
|
+
def aggregator(vd, name, funcValues, helpstr='', *, type=None):
|
98
|
+
'''Define simple aggregator *name* that calls ``funcValues(values)`` to aggregate *values*.
|
99
|
+
Use *type* to force type of aggregated column (default to use type of source column).'''
|
100
|
+
vd.aggregators[name] = Aggregator(name, type, funcValues=funcValues, helpstr=helpstr)
|
100
101
|
|
101
102
|
## specific aggregator implementations
|
102
103
|
|
@@ -105,12 +106,9 @@ def mean(vals):
|
|
105
106
|
if vals:
|
106
107
|
return float(sum(vals))/len(vals)
|
107
108
|
|
108
|
-
def
|
109
|
+
def vsum(vals):
|
109
110
|
return sum(vals, start=type(vals[0] if len(vals) else 0)()) #1996
|
110
111
|
|
111
|
-
# start parameter in sum() added in Python 3.8
|
112
|
-
vsum = _vsum if sys.version_info[:2] >= (3, 8) else sum
|
113
|
-
|
114
112
|
# http://code.activestate.com/recipes/511478-finding-the-percentile-of-the-values/
|
115
113
|
def _percentile(N, percent, key=lambda x:x):
|
116
114
|
"""
|
@@ -134,11 +132,18 @@ def _percentile(N, percent, key=lambda x:x):
|
|
134
132
|
return d0+d1
|
135
133
|
|
136
134
|
@functools.lru_cache(100)
|
137
|
-
|
138
|
-
|
135
|
+
class PercentileAggregator(Aggregator):
|
136
|
+
def __init__(self, pct, helpstr=''):
|
137
|
+
super().__init__('p%s'%pct, None, helpstr=helpstr)
|
138
|
+
self.pct = pct
|
139
|
+
|
140
|
+
def aggregate(self, col, rows):
|
141
|
+
return _percentile(sorted(col.getValues(rows)), self.pct/100, key=float)
|
142
|
+
|
139
143
|
|
140
144
|
def quantiles(q, helpstr):
|
141
|
-
return [
|
145
|
+
return [PercentileAggregator(round(100*i/q), helpstr) for i in range(1, q)]
|
146
|
+
|
142
147
|
|
143
148
|
vd.aggregator('min', min, 'minimum value')
|
144
149
|
vd.aggregator('max', max, 'maximum value')
|
@@ -149,7 +154,7 @@ vd.aggregator('mode', statistics.mode, 'mode of values')
|
|
149
154
|
vd.aggregator('sum', vsum, 'sum of values')
|
150
155
|
vd.aggregator('distinct', set, 'distinct values', type=vlen)
|
151
156
|
vd.aggregator('count', lambda values: sum(1 for v in values), 'number of values', type=int)
|
152
|
-
vd.aggregator('list', list, 'list of values')
|
157
|
+
vd.aggregator('list', list, 'list of values', type=anytype)
|
153
158
|
vd.aggregator('stdev', statistics.stdev, 'standard deviation of values', type=float)
|
154
159
|
|
155
160
|
vd.aggregators['q3'] = quantiles(3, 'tertiles (33/66th pctile)')
|
@@ -160,10 +165,31 @@ vd.aggregators['q10'] = quantiles(10, 'deciles (10/20/30/40/50/60/70/80/90th pct
|
|
160
165
|
# since bb29b6e, a record of every aggregator
|
161
166
|
# is needed in vd.aggregators
|
162
167
|
for pct in (10, 20, 25, 30, 33, 40, 50, 60, 67, 70, 75, 80, 90, 95, 99):
|
163
|
-
vd.aggregators[f'p{pct}'] =
|
168
|
+
vd.aggregators[f'p{pct}'] = PercentileAggregator(pct, f'{pct}th percentile')
|
169
|
+
|
170
|
+
class KeyFindingAggregator(Aggregator):
|
171
|
+
'''Return the key of the row that results from applying *aggr_func* to *rows*.
|
172
|
+
Return None if *rows* is an empty list.
|
173
|
+
*aggr_func* takes a list of (value, row) tuples, one for each row in the column,
|
174
|
+
excluding rows where the column holds null and error values.
|
175
|
+
*aggr_func* must also take the parameters *default* and *key*, as max() does:
|
176
|
+
https://docs.python.org/3/library/functions.html#max'''
|
164
177
|
|
165
|
-
|
166
|
-
|
178
|
+
def __init__(self, aggr_func, *args, **kwargs):
|
179
|
+
self.aggr_func = aggr_func
|
180
|
+
super().__init__(*args, **kwargs)
|
181
|
+
|
182
|
+
def aggregate(self, col, rows):
|
183
|
+
if not col.sheet.keyCols:
|
184
|
+
vd.error('key aggregator function requires one or more key columns')
|
185
|
+
return None
|
186
|
+
# convert dicts to lists because functions like max() can't compare dicts
|
187
|
+
sortkey = lambda t: (t[0], sorted(t[1].items())) if isinstance(t[1], dict) else t
|
188
|
+
row = self.aggr_func(col.getValueRows(rows), default=(None, None), key=sortkey)[1]
|
189
|
+
return col.sheet.rowkey(row) if row else None
|
190
|
+
|
191
|
+
vd.aggregators['keymin'] = KeyFindingAggregator(min, 'keymin', anytype, helpstr='key of the minimum value')
|
192
|
+
vd.aggregators['keymax'] = KeyFindingAggregator(max, 'keymax', anytype, helpstr='key of the maximum value')
|
167
193
|
|
168
194
|
|
169
195
|
ColumnsSheet.columns += [
|
@@ -175,7 +201,7 @@ ColumnsSheet.columns += [
|
|
175
201
|
|
176
202
|
@Sheet.api
|
177
203
|
def addAggregators(sheet, cols, aggrnames):
|
178
|
-
'Add each aggregator in list of *aggrnames* to each of *cols*.'
|
204
|
+
'Add each aggregator in list of *aggrnames* to each of *cols*. Ignores names that are not valid.'
|
179
205
|
for aggrname in aggrnames:
|
180
206
|
aggrs = vd.aggregators.get(aggrname)
|
181
207
|
aggrs = aggrs if isinstance(aggrs, list) else [aggrs]
|
@@ -192,16 +218,35 @@ def aggname(col, agg):
|
|
192
218
|
'Consistent formatting of the name of given aggregator for this column. e.g. "col1_sum"'
|
193
219
|
return '%s_%s' % (col.name, agg.name)
|
194
220
|
|
221
|
+
@Column.api
|
222
|
+
def aggregateTotal(col, agg):
|
223
|
+
if agg not in col._aggregatedTotals:
|
224
|
+
col._aggregatedTotals[agg] = INPROGRESS
|
225
|
+
col._aggregateTotalAsync(agg)
|
226
|
+
return col._aggregatedTotals[agg]
|
227
|
+
|
228
|
+
|
229
|
+
@Column.api
|
230
|
+
@asyncthread
|
231
|
+
def _aggregateTotalAsync(col, agg):
|
232
|
+
col._aggregatedTotals[agg] = agg.aggregate(col, col.sheet.rows)
|
233
|
+
|
234
|
+
|
195
235
|
@Column.api
|
196
236
|
@asyncthread
|
197
|
-
def memo_aggregate(col,
|
237
|
+
def memo_aggregate(col, agg_choices, rows):
|
198
238
|
'Show aggregated value in status, and add to memory.'
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
239
|
+
for agg_choice in agg_choices:
|
240
|
+
agg = vd.aggregators.get(agg_choice)
|
241
|
+
if not agg: continue
|
242
|
+
aggs = agg if isinstance(agg, list) else [agg]
|
243
|
+
for agg in aggs:
|
244
|
+
aggval = agg.aggregate(col, rows)
|
245
|
+
typedval = wrapply(agg.type or col.type, aggval)
|
246
|
+
dispval = col.format(typedval)
|
247
|
+
k = col.name+'_'+agg.name
|
248
|
+
vd.status(f'{k}={dispval}')
|
249
|
+
vd.memory[k] = typedval
|
205
250
|
|
206
251
|
|
207
252
|
@VisiData.property
|
@@ -215,6 +260,7 @@ def aggregator_choices(vd):
|
|
215
260
|
|
216
261
|
@VisiData.api
|
217
262
|
def chooseAggregators(vd):
|
263
|
+
'''Return a list of aggregator name strings chosen or entered by the user. User-entered names may be invalid.'''
|
218
264
|
prompt = 'choose aggregators: '
|
219
265
|
def _fmt_aggr_summary(match, row, trigger_key):
|
220
266
|
formatted_aggrname = match.formatted.get('key', row.key) if match else row.key
|
@@ -235,12 +281,15 @@ def chooseAggregators(vd):
|
|
235
281
|
multiple=True)
|
236
282
|
|
237
283
|
aggrs = r.split()
|
284
|
+
valid_choices = vd.aggregators.keys()
|
238
285
|
for aggr in aggrs:
|
239
286
|
vd.usedInputs[aggr] += 1
|
287
|
+
if aggr not in valid_choices:
|
288
|
+
vd.warning(f'aggregator does not exist: {aggr}')
|
240
289
|
return aggrs
|
241
290
|
|
242
|
-
Sheet.addCommand('+', 'aggregate-col', 'addAggregators([cursorCol], chooseAggregators())', '
|
243
|
-
Sheet.addCommand('z+', 'memo-aggregate', '
|
291
|
+
Sheet.addCommand('+', 'aggregate-col', 'addAggregators([cursorCol], chooseAggregators())', 'add aggregator to current column')
|
292
|
+
Sheet.addCommand('z+', 'memo-aggregate', 'cursorCol.memo_aggregate(chooseAggregators(), selectedRows or rows)', 'memo result of aggregator over values in selected rows for current column')
|
244
293
|
ColumnsSheet.addCommand('g+', 'aggregate-cols', 'addAggregators(selectedRows or source[0].nonKeyVisibleCols, chooseAggregators())', 'add aggregators to selected source columns')
|
245
294
|
|
246
295
|
vd.addMenuItems('''
|
visidata/apps/vdsql/_ibis.py
CHANGED
@@ -72,7 +72,9 @@ def open_vdsql(vd, p, filetype=None):
|
|
72
72
|
return IbisTableIndexSheet(p.base_stem, source=p, filetype=None, database_name=None,
|
73
73
|
ibis_conpool=IbisConnectionPool(p), sheet_type=IbisTableSheet)
|
74
74
|
|
75
|
+
|
75
76
|
vd.open_ibis = vd.open_vdsql
|
77
|
+
vd.openurl_sqlite = vd.open_vdsql
|
76
78
|
|
77
79
|
|
78
80
|
class IbisConnectionPool:
|
@@ -87,14 +89,8 @@ class IbisConnectionPool:
|
|
87
89
|
@contextmanager
|
88
90
|
def get_conn(self):
|
89
91
|
if not self.pool:
|
90
|
-
import
|
91
|
-
|
92
|
-
import ibis
|
93
|
-
r = ibis.connect(str(self.source))
|
94
|
-
except sqlalchemy.exc.NoSuchModuleError as e:
|
95
|
-
dialect = str(e).split(':')[-1]
|
96
|
-
vd.warning(f'{dialect} not installed')
|
97
|
-
vd.fail(f'pip install ibis-framework[{dialect}]')
|
92
|
+
import ibis
|
93
|
+
r = ibis.connect(str(self.source))
|
98
94
|
else:
|
99
95
|
r = self.pool.pop(0)
|
100
96
|
|
@@ -212,9 +208,9 @@ class IbisTableSheet(Sheet):
|
|
212
208
|
def curcol_sql(self):
|
213
209
|
expr = self.cursorCol.get_ibis_col(self.ibis_current_expr)
|
214
210
|
if expr is not None:
|
215
|
-
return self.
|
211
|
+
return self.ibis_expr_to_sql(expr, fragment=True)
|
216
212
|
|
217
|
-
def
|
213
|
+
def ibis_expr_to_sql(self, expr, fragment=False):
|
218
214
|
import sqlparse
|
219
215
|
with self.con as con:
|
220
216
|
context = con.compiler.make_context()
|
@@ -325,7 +321,7 @@ class IbisTableSheet(Sheet):
|
|
325
321
|
def sqlize(self, expr):
|
326
322
|
if vd.options.debug:
|
327
323
|
expr = self.withRowcount(expr)
|
328
|
-
return self.
|
324
|
+
return self.ibis_expr_to_sql(expr)
|
329
325
|
|
330
326
|
@property
|
331
327
|
def substrait(self):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import time
|
2
2
|
|
3
|
-
from visidata import
|
3
|
+
from visidata import vd, VisiData, Progress
|
4
4
|
|
5
5
|
from ._ibis import IbisTableSheet, IbisTableIndexSheet, IbisConnectionPool
|
6
6
|
|
@@ -45,7 +45,7 @@ class ClickhouseSheet(IbisTableSheet):
|
|
45
45
|
raise
|
46
46
|
except BaseException:
|
47
47
|
if qid:
|
48
|
-
con.
|
48
|
+
con._client.cancel(qid)
|
49
49
|
|
50
50
|
|
51
51
|
ClickhouseSheet.init('total_rows', lambda: None)
|
visidata/apps/vdsql/snowflake.py
CHANGED
visidata/apps/vgit/status.py
CHANGED
@@ -112,7 +112,7 @@ class GitStatus(GitSheet):
|
|
112
112
|
return self.gitBranchStatuses.get(self.branch, 'no branch')
|
113
113
|
|
114
114
|
def iterload(self):
|
115
|
-
files = [GitFile(p, self.source) for p in self.source.iterdir() if p.
|
115
|
+
files = [GitFile(p, self.source) for p in self.source.iterdir() if p.stem not in ('.git')] # files in working dir
|
116
116
|
|
117
117
|
filenames = dict((gf.filename, gf) for gf in files)
|
118
118
|
|
visidata/basesheet.py
CHANGED
@@ -67,6 +67,10 @@ class DrawablePane(Extensible):
|
|
67
67
|
'Width of the current sheet window, in single-width characters.'
|
68
68
|
return self._scr.getmaxyx()[1] if self._scr else 80
|
69
69
|
|
70
|
+
@property
|
71
|
+
def currow(self):
|
72
|
+
return None
|
73
|
+
|
70
74
|
def execCommand2(self, cmd, vdglobals=None):
|
71
75
|
"Execute `cmd` with `vdglobals` as globals and this sheet's attributes as locals. Return True if user cancelled."
|
72
76
|
|
@@ -112,9 +116,9 @@ class BaseSheet(DrawablePane):
|
|
112
116
|
|
113
117
|
def __init__(self, *names, rows=UNLOADED, **kwargs):
|
114
118
|
self._name = None # initial cache value necessary for self.options
|
119
|
+
self._names = []
|
115
120
|
self.loading = False
|
116
121
|
self.names = list(names)
|
117
|
-
self.name = self.options.name_joiner.join(str(x) for x in self.names if x)
|
118
122
|
self.source = None
|
119
123
|
self.rows = rows # list of opaque objects
|
120
124
|
self._scr = None
|
@@ -194,7 +198,7 @@ class BaseSheet(DrawablePane):
|
|
194
198
|
|
195
199
|
cmd = self.getCommand(longname or keystrokes)
|
196
200
|
if not cmd:
|
197
|
-
vd.
|
201
|
+
vd.fail('no command for %s' % (longname or keystrokes))
|
198
202
|
return False
|
199
203
|
|
200
204
|
escaped = False
|
@@ -238,8 +242,10 @@ class BaseSheet(DrawablePane):
|
|
238
242
|
|
239
243
|
@names.setter
|
240
244
|
def names(self, names):
|
245
|
+
if self._names:
|
246
|
+
vd.addUndo(setattr, self, 'names', self._names)
|
241
247
|
self._names = names
|
242
|
-
self.
|
248
|
+
self._name = self.options.name_joiner.join(self.maybeClean(str(x)) for x in self._names)
|
243
249
|
|
244
250
|
@property
|
245
251
|
def name(self):
|
@@ -250,8 +256,9 @@ class BaseSheet(DrawablePane):
|
|
250
256
|
def name(self, name):
|
251
257
|
'Set name without spaces.'
|
252
258
|
if self._names:
|
253
|
-
vd.addUndo(setattr, self, '
|
259
|
+
vd.addUndo(setattr, self, 'names', self._names)
|
254
260
|
self._name = self.maybeClean(str(name))
|
261
|
+
self._names = [self._name]
|
255
262
|
|
256
263
|
def maybeClean(self, s):
|
257
264
|
'stub'
|
visidata/canvas.py
CHANGED
@@ -145,7 +145,7 @@ class Plotter(BaseSheet):
|
|
145
145
|
self.labels = [] # (x, y, text, attr, row)
|
146
146
|
self.hiddenAttrs = set()
|
147
147
|
self.needsRefresh = False
|
148
|
-
self.resetCanvasDimensions(
|
148
|
+
self.resetCanvasDimensions(1, 1) #2171
|
149
149
|
|
150
150
|
@property
|
151
151
|
def nRows(self):
|
@@ -234,12 +234,28 @@ class Plotter(BaseSheet):
|
|
234
234
|
|
235
235
|
def draw(self, scr):
|
236
236
|
windowHeight, windowWidth = scr.getmaxyx()
|
237
|
-
disp_canvas_charset = self.options.disp_canvas_charset or ' o'
|
238
|
-
disp_canvas_charset += (256 - len(disp_canvas_charset)) * disp_canvas_charset[-1]
|
239
|
-
|
240
237
|
if self.needsRefresh:
|
241
238
|
self.render(windowHeight, windowWidth)
|
242
239
|
|
240
|
+
self.draw_pixels(scr)
|
241
|
+
self.draw_labels(scr)
|
242
|
+
|
243
|
+
def draw_empty(self, scr):
|
244
|
+
# use draw_empty() when calling draw_pixels() with clear_empty_squares=False
|
245
|
+
cursorBBox = self.plotterCursorBox
|
246
|
+
for char_y in range(0, self.plotheight//4):
|
247
|
+
for char_x in range(0, self.plotwidth//2):
|
248
|
+
cattr = ColorAttr()
|
249
|
+
ch = ' '
|
250
|
+
# draw cursor
|
251
|
+
if cursorBBox.contains(char_x*2, char_y*4) or \
|
252
|
+
cursorBBox.contains(char_x*2+1, char_y*4+3):
|
253
|
+
cattr = update_attr(cattr, colors.color_current_row)
|
254
|
+
scr.addstr(char_y, char_x, ch, cattr.attr)
|
255
|
+
|
256
|
+
def draw_pixels(self, scr, clear_empty_squares=True):
|
257
|
+
disp_canvas_charset = self.options.disp_canvas_charset or ' o'
|
258
|
+
disp_canvas_charset += (256 - len(disp_canvas_charset)) * disp_canvas_charset[-1]
|
243
259
|
if self.pixels:
|
244
260
|
cursorBBox = self.plotterCursorBox
|
245
261
|
getPixelAttr = self.getPixelAttrRandom if self.options.disp_pixel_random else self.getPixelAttrMost
|
@@ -264,19 +280,26 @@ class Plotter(BaseSheet):
|
|
264
280
|
braille_num += pow2
|
265
281
|
pow2 *= 2
|
266
282
|
|
283
|
+
ch = disp_canvas_charset[braille_num]
|
267
284
|
if braille_num != 0:
|
268
285
|
color = Counter(c for c in block_attrs if c).most_common(1)[0][0]
|
269
286
|
cattr = colors.get_color(color)
|
270
287
|
else:
|
271
288
|
cattr = ColorAttr()
|
289
|
+
# don't erase empty squares, useful for subclasses that draw elements like reflines
|
290
|
+
# before pixels are drawn
|
291
|
+
if not clear_empty_squares:
|
292
|
+
continue
|
272
293
|
|
294
|
+
# draw cursor
|
273
295
|
if cursorBBox.contains(char_x*2, char_y*4) or \
|
274
296
|
cursorBBox.contains(char_x*2+1, char_y*4+3):
|
275
297
|
cattr = update_attr(cattr, colors.color_current_row)
|
276
298
|
|
277
299
|
if cattr.attr:
|
278
|
-
scr.addstr(char_y, char_x,
|
300
|
+
scr.addstr(char_y, char_x, ch, cattr.attr)
|
279
301
|
|
302
|
+
def draw_labels(self, scr):
|
280
303
|
def _mark_overlap_text(labels, textobj):
|
281
304
|
def _overlaps(a, b):
|
282
305
|
a_x1, _, a_txt, _, _ = a
|
@@ -318,7 +341,7 @@ class Plotter(BaseSheet):
|
|
318
341
|
cursorBBox = self.plotterCursorBox
|
319
342
|
for c in txt:
|
320
343
|
w = dispwidth(c)
|
321
|
-
#
|
344
|
+
# draw cursor if the cursor contains the midpoint of the character cell
|
322
345
|
if cursorBBox.contains(char_x*2+1, char_y*4+2):
|
323
346
|
char_attr = update_attr(cattr, colors.color_current_row)
|
324
347
|
clipdraw(scr, char_y, char_x, c, char_attr, w)
|
@@ -393,6 +416,12 @@ class Canvas(Plotter):
|
|
393
416
|
if self.cursorBox.xmin == self.visibleBox.xmin and self.cursorBox.ymin == self.calcBottomCursorY():
|
394
417
|
realign_cursor = True
|
395
418
|
super().resetCanvasDimensions(windowHeight, windowWidth)
|
419
|
+
# if window is not big enough to contain a particular margin, pretend that margin is 0
|
420
|
+
pvbox_x = pvbox_y = 0
|
421
|
+
if self.plotwidth > self.left_margin:
|
422
|
+
pvbox_x = self.left_margin
|
423
|
+
if self.plotheight > self.topMarginPixels:
|
424
|
+
pvbox_y = self.topMarginPixels
|
396
425
|
if hasattr(self, 'legendwidth'):
|
397
426
|
# +4 = 1 empty space after the graph + 2 characters for the legend prefixes of "1:", "2:", etc +
|
398
427
|
# 1 character for the empty rightmost column
|
@@ -403,8 +432,10 @@ class Canvas(Plotter):
|
|
403
432
|
else:
|
404
433
|
pvbox_xmax = self.plotwidth-self.rightMarginPixels-1
|
405
434
|
self.left_margin = min(self.left_margin, math.ceil(self.plotwidth * 1/3)//2*2)
|
406
|
-
|
407
|
-
|
435
|
+
# enforce a minimum plotview box size of 1x1
|
436
|
+
pvbox_xmax = max(pvbox_xmax, 1)
|
437
|
+
pvbox_ymax = max(self.plotheight-self.bottomMarginPixels-1, 1)
|
438
|
+
self.plotviewBox = BoundingBox(pvbox_x, pvbox_y, pvbox_xmax, pvbox_ymax)
|
408
439
|
if [self.plotheight, self.plotwidth] != old_plotsize:
|
409
440
|
if hasattr(self, 'cursorBox') and self.cursorBox:
|
410
441
|
self.setCursorSizeInPlotterPixels(2, 4)
|
@@ -521,13 +552,14 @@ class Canvas(Plotter):
|
|
521
552
|
'adjust visibleBox.xymin so that canvasPoint is plotted at plotterPoint'
|
522
553
|
self.visibleBox.xmin = canvasPoint.x - self.canvasW(plotterPoint.x-self.plotviewBox.xmin)
|
523
554
|
self.visibleBox.ymin = canvasPoint.y - self.canvasH(plotterPoint.y-self.plotviewBox.ymin)
|
524
|
-
self.
|
555
|
+
self.resetBounds()
|
525
556
|
|
526
557
|
def zoomTo(self, bbox):
|
527
558
|
'set visible area to bbox, maintaining aspectRatio if applicable'
|
528
559
|
self.fixPoint(self.plotviewBox.xymin, bbox.xymin)
|
529
560
|
self.xzoomlevel=bbox.w/self.canvasBox.w
|
530
561
|
self.yzoomlevel=bbox.h/self.canvasBox.h
|
562
|
+
self.resetBounds()
|
531
563
|
|
532
564
|
def incrZoom(self, incr):
|
533
565
|
self.xzoomlevel *= incr
|
@@ -535,7 +567,7 @@ class Canvas(Plotter):
|
|
535
567
|
|
536
568
|
self.resetBounds()
|
537
569
|
|
538
|
-
def resetBounds(self):
|
570
|
+
def resetBounds(self, refresh=True):
|
539
571
|
'create canvasBox and cursorBox if necessary, and set visibleBox w/h according to zoomlevels. then redisplay legends.'
|
540
572
|
if not self.canvasBox:
|
541
573
|
xmin, ymin, xmax, ymax = None, None, None, None
|
@@ -572,6 +604,8 @@ class Canvas(Plotter):
|
|
572
604
|
self.cursorBox = Box(cb_xmin, cb_ymin, self.canvasCharWidth, self.canvasCharHeight)
|
573
605
|
|
574
606
|
self.plotlegends()
|
607
|
+
if refresh:
|
608
|
+
self.refresh()
|
575
609
|
|
576
610
|
def calcTopCursorY(self):
|
577
611
|
'ymin for the cursor that will align its top with the top edge of the graph'
|
@@ -649,13 +683,13 @@ class Canvas(Plotter):
|
|
649
683
|
else:
|
650
684
|
return h
|
651
685
|
|
652
|
-
def scaleX(self, canvasX):
|
686
|
+
def scaleX(self, canvasX) -> int:
|
653
687
|
'returns a plotter x coordinate'
|
654
|
-
return
|
688
|
+
return self.plotviewBox.xmin+round((canvasX-self.visibleBox.xmin)*self.xScaler)
|
655
689
|
|
656
|
-
def scaleY(self, canvasY):
|
690
|
+
def scaleY(self, canvasY) -> int:
|
657
691
|
'returns a plotter y coordinate'
|
658
|
-
return
|
692
|
+
return self.plotviewBox.ymin+round((canvasY-self.visibleBox.ymin)*self.yScaler)
|
659
693
|
|
660
694
|
def unscaleX(self, plotterX):
|
661
695
|
'performs the inverse of scaleX, returns a canvas x coordinate'
|
@@ -683,6 +717,7 @@ class Canvas(Plotter):
|
|
683
717
|
vd.cancelThread(*(t for t in self.currentThreads if t.name == 'plotAll_async'))
|
684
718
|
self.labels.clear()
|
685
719
|
self.resetCanvasDimensions(h, w)
|
720
|
+
self.resetBounds(refresh=False)
|
686
721
|
self.render_async()
|
687
722
|
|
688
723
|
@asyncthread
|
@@ -709,12 +744,12 @@ class Canvas(Plotter):
|
|
709
744
|
x1, y1 = float(x1), float(y1)
|
710
745
|
if xmin <= x1 <= xmax and ymin <= y1 <= ymax:
|
711
746
|
# equivalent to self.scaleX(x1) and self.scaleY(y1), inlined for speed
|
712
|
-
x = plotxmin+(x1-xmin)*xfactor
|
747
|
+
x = plotxmin+round((x1-xmin)*xfactor)
|
713
748
|
if invert_y:
|
714
|
-
y = plotymax-(y1-ymin)*yfactor
|
749
|
+
y = plotymax-round((y1-ymin)*yfactor)
|
715
750
|
else:
|
716
|
-
y = plotymin+(y1-ymin)*yfactor
|
717
|
-
self.plotpixel(
|
751
|
+
y = plotymin+round((y1-ymin)*yfactor)
|
752
|
+
self.plotpixel(x, y, attr, row)
|
718
753
|
continue
|
719
754
|
|
720
755
|
prev_x, prev_y = vertexes[0]
|
@@ -743,7 +778,6 @@ class Canvas(Plotter):
|
|
743
778
|
self.source.deleteBy(lambda r,rows=rows: r in rows)
|
744
779
|
self.reload()
|
745
780
|
|
746
|
-
|
747
781
|
Plotter.addCommand('v', 'visibility', 'options.disp_graph_labels = not options.disp_graph_labels', 'toggle disp_graph_labels option')
|
748
782
|
|
749
783
|
Canvas.addCommand(None, 'go-left', 'if cursorBox: sheet.cursorBox.xmin -= cursorBox.w', 'move cursor left by its width')
|
@@ -776,7 +810,7 @@ Canvas.addCommand('zz', 'zoom-cursor', 'zoomTo(cursorBox)', 'set visible bounds
|
|
776
810
|
|
777
811
|
Canvas.addCommand('-', 'zoomout-cursor', 'tmp=cursorBox.center; incrZoom(options.disp_zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom out from cursor center')
|
778
812
|
Canvas.addCommand('+', 'zoomin-cursor', 'tmp=cursorBox.center; incrZoom(1.0/options.disp_zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom into cursor center')
|
779
|
-
Canvas.addCommand('_', 'zoom-all', 'sheet.canvasBox = None; sheet.visibleBox = None; sheet.xzoomlevel=sheet.yzoomlevel=1.0; resetBounds()
|
813
|
+
Canvas.addCommand('_', 'zoom-all', 'sheet.canvasBox = None; sheet.visibleBox = None; sheet.xzoomlevel=sheet.yzoomlevel=1.0; resetBounds()', 'zoom to fit full extent')
|
780
814
|
Canvas.addCommand('z_', 'set-aspect', 'sheet.aspectRatio = float(input("aspect ratio=", value=aspectRatio)); refresh()', 'set aspect ratio')
|
781
815
|
|
782
816
|
# set cursor box with left click
|
visidata/clipboard.py
CHANGED
@@ -6,6 +6,7 @@ import sys
|
|
6
6
|
import tempfile
|
7
7
|
import functools
|
8
8
|
import os
|
9
|
+
import itertools
|
9
10
|
|
10
11
|
from visidata import VisiData, vd, asyncthread, SettableColumn
|
11
12
|
from visidata import Sheet, Path, Column
|
@@ -58,6 +59,12 @@ def syscopyValue(sheet, val):
|
|
58
59
|
|
59
60
|
vd.status('copied value to system clipboard')
|
60
61
|
|
62
|
+
@Sheet.api
|
63
|
+
def setColClipboard(sheet):
|
64
|
+
if not vd.memory.clipcells:
|
65
|
+
vd.warning("nothing to paste from clipcells")
|
66
|
+
return
|
67
|
+
sheet.cursorCol.setValuesTyped(sheet.onlySelectedRows, *vd.memory.clipcells)
|
61
68
|
|
62
69
|
@Sheet.api
|
63
70
|
def syscopyCells(sheet, cols, rows, filetype=None):
|
@@ -95,12 +102,12 @@ def sysclipValue(vd):
|
|
95
102
|
@VisiData.api
|
96
103
|
@asyncthread
|
97
104
|
def pasteFromClipboard(vd, cols, rows):
|
98
|
-
text = vd.getLastArgs() or vd.sysclipValue().strip() or vd.fail('system clipboard
|
105
|
+
text = vd.getLastArgs() or vd.sysclipValue().strip() or vd.fail('nothing to paste from system clipboard')
|
99
106
|
|
100
107
|
vd.addUndoSetValues(cols, rows)
|
101
108
|
lines = text.split('\n')
|
102
109
|
if not lines:
|
103
|
-
vd.warning('nothing to paste')
|
110
|
+
vd.warning('nothing to paste from system clipboard')
|
104
111
|
return
|
105
112
|
|
106
113
|
vs = cols[0].sheet
|
@@ -138,7 +145,7 @@ def delete_row(sheet, rowidx):
|
|
138
145
|
def paste_after(sheet, rowidx):
|
139
146
|
'Paste rows from *vd.cliprows* at *rowidx*.'
|
140
147
|
if not vd.memory.cliprows: #1793
|
141
|
-
vd.warning('nothing to paste')
|
148
|
+
vd.warning('nothing to paste from cliprows')
|
142
149
|
return
|
143
150
|
|
144
151
|
for col in vd.memory.clipcols[sheet.nVisibleCols:]:
|
@@ -168,8 +175,8 @@ Sheet.addCommand('P', 'paste-before', 'paste_after(cursorRowIndex-1)', 'paste cl
|
|
168
175
|
|
169
176
|
Sheet.addCommand('gy', 'copy-selected', 'copyRows(onlySelectedRows)', 'yank (copy) selected rows to clipboard')
|
170
177
|
|
171
|
-
Sheet.addCommand('zy', 'copy-cell', 'copyCells(cursorCol, [cursorRow]); vd.
|
172
|
-
Sheet.addCommand('zp', 'paste-cell', 'cursorCol.setValuesTyped([cursorRow], vd.memory.clipval)', 'set contents of current cell to last clipboard value')
|
178
|
+
Sheet.addCommand('zy', 'copy-cell', 'copyCells(cursorCol, [cursorRow]); vd.memoValue("clipval", cursorTypedValue, cursorDisplay)', 'yank (copy) current cell to clipboard')
|
179
|
+
Sheet.addCommand('zp', 'paste-cell', 'cursorCol.setValuesTyped([cursorRow], vd.memory.clipval) if vd.memory.clipval else vd.warning("nothing to paste from clipval")', 'set contents of current cell to last clipboard value')
|
173
180
|
|
174
181
|
Sheet.addCommand('d', 'delete-row', 'delete_row(cursorRowIndex); defer and cursorDown(1)', 'delete current row')
|
175
182
|
Sheet.addCommand('gd', 'delete-selected', 'deleteSelected()', 'delete selected rows')
|
@@ -183,7 +190,7 @@ Sheet.bindkey('zP', 'syspaste-cells')
|
|
183
190
|
Sheet.addCommand('gzP', 'syspaste-cells-selected', 'pasteFromClipboard(visibleCols[cursorVisibleColIndex:], someSelectedRows)', 'paste from system clipboard to selected cells')
|
184
191
|
|
185
192
|
Sheet.addCommand('gzy', 'copy-cells', 'copyCells(cursorCol, onlySelectedRows)', 'yank (copy) contents of current column for selected rows to clipboard')
|
186
|
-
Sheet.addCommand('gzp', 'setcol-clipboard', '
|
193
|
+
Sheet.addCommand('gzp', 'setcol-clipboard', 'setColClipboard()', 'set cells of current column for selected rows to last clipboard value')
|
187
194
|
|
188
195
|
Sheet.addCommand('Y', 'syscopy-row', 'syscopyCells(visibleCols, [cursorRow])', 'yank (copy) current row to system clipboard (using options.clipboard_copy_cmd)')
|
189
196
|
|