visidata 3.0.2__py3-none-any.whl → 3.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.
Files changed (149) hide show
  1. visidata/__init__.py +12 -10
  2. visidata/_input.py +208 -202
  3. visidata/_open.py +4 -1
  4. visidata/_types.py +4 -3
  5. visidata/aggregators.py +88 -39
  6. visidata/apps/vdsql/_ibis.py +7 -11
  7. visidata/apps/vdsql/clickhouse.py +2 -2
  8. visidata/apps/vdsql/snowflake.py +1 -1
  9. visidata/apps/vgit/status.py +1 -1
  10. visidata/basesheet.py +11 -4
  11. visidata/canvas.py +54 -20
  12. visidata/clipboard.py +13 -6
  13. visidata/cliptext.py +7 -6
  14. visidata/cmdlog.py +40 -27
  15. visidata/column.py +14 -49
  16. visidata/ddw/regex.ddw +3 -2
  17. visidata/deprecated.py +14 -2
  18. visidata/desktop/visidata.desktop +2 -2
  19. visidata/editor.py +1 -0
  20. visidata/errors.py +1 -1
  21. visidata/experimental/sort_selected.py +54 -0
  22. visidata/expr.py +69 -18
  23. visidata/features/change_precision.py +1 -3
  24. visidata/features/cmdpalette.py +17 -2
  25. visidata/features/colorsheet.py +1 -1
  26. visidata/features/dedupe.py +3 -3
  27. visidata/features/go_col.py +71 -0
  28. visidata/features/graph_seaborn.py +1 -1
  29. visidata/features/join.py +20 -10
  30. visidata/features/layout.py +16 -3
  31. visidata/features/ping.py +16 -12
  32. visidata/features/regex.py +5 -5
  33. visidata/features/status_source.py +3 -1
  34. visidata/features/sysedit.py +1 -1
  35. visidata/features/transpose.py +2 -1
  36. visidata/features/type_ipaddr.py +2 -4
  37. visidata/features/unfurl.py +1 -0
  38. visidata/form.py +2 -2
  39. visidata/freqtbl.py +16 -11
  40. visidata/fuzzymatch.py +1 -0
  41. visidata/graph.py +163 -12
  42. visidata/guide.py +57 -24
  43. visidata/guides/ClipboardGuide.md +48 -0
  44. visidata/guides/ColumnsGuide.md +52 -0
  45. visidata/guides/CommandsSheet.md +28 -0
  46. visidata/guides/DirSheet.md +34 -0
  47. visidata/guides/ErrorsSheet.md +17 -0
  48. visidata/guides/FrequencyTable.md +42 -0
  49. visidata/guides/GrepSheet.md +28 -0
  50. visidata/guides/JsonSheet.md +38 -0
  51. visidata/guides/MacrosSheet.md +19 -0
  52. visidata/guides/MeltGuide.md +52 -0
  53. visidata/guides/MemorySheet.md +7 -0
  54. visidata/guides/MenuGuide.md +26 -0
  55. visidata/guides/ModifyGuide.md +38 -0
  56. visidata/guides/PivotGuide.md +71 -0
  57. visidata/guides/RegexGuide.md +107 -0
  58. visidata/guides/SelectionGuide.md +44 -0
  59. visidata/guides/SlideGuide.md +26 -0
  60. visidata/guides/SortGuide.md +0 -0
  61. visidata/guides/SplitpaneGuide.md +15 -0
  62. visidata/guides/TypesSheet.md +43 -0
  63. visidata/guides/XsvGuide.md +36 -0
  64. visidata/help.py +6 -6
  65. visidata/hint.py +2 -1
  66. visidata/indexsheet.py +2 -2
  67. visidata/interface.py +13 -14
  68. visidata/keys.py +4 -1
  69. visidata/loaders/api_airtable.py +1 -1
  70. visidata/loaders/archive.py +1 -1
  71. visidata/loaders/csv.py +9 -5
  72. visidata/loaders/eml.py +11 -6
  73. visidata/loaders/f5log.py +1 -0
  74. visidata/loaders/fec.py +18 -42
  75. visidata/loaders/fixed_width.py +19 -3
  76. visidata/loaders/grep.py +121 -0
  77. visidata/loaders/html.py +1 -0
  78. visidata/loaders/http.py +6 -1
  79. visidata/loaders/json.py +22 -4
  80. visidata/loaders/jsonla.py +8 -2
  81. visidata/loaders/mailbox.py +1 -0
  82. visidata/loaders/markdown.py +25 -6
  83. visidata/loaders/msgpack.py +19 -0
  84. visidata/loaders/npy.py +0 -1
  85. visidata/loaders/odf.py +18 -4
  86. visidata/loaders/orgmode.py +1 -1
  87. visidata/loaders/rec.py +6 -4
  88. visidata/loaders/sas.py +11 -4
  89. visidata/loaders/scrape.py +0 -1
  90. visidata/loaders/texttables.py +2 -0
  91. visidata/loaders/tsv.py +24 -7
  92. visidata/loaders/unzip_http.py +127 -3
  93. visidata/loaders/vds.py +4 -0
  94. visidata/loaders/vdx.py +1 -1
  95. visidata/loaders/xlsx.py +5 -0
  96. visidata/loaders/xml.py +2 -1
  97. visidata/macros.py +14 -31
  98. visidata/main.py +14 -13
  99. visidata/mainloop.py +14 -6
  100. visidata/man/vd.1 +72 -39
  101. visidata/man/vd.txt +72 -41
  102. visidata/memory.py +15 -4
  103. visidata/menu.py +14 -3
  104. visidata/metasheets.py +5 -6
  105. visidata/modify.py +4 -4
  106. visidata/mouse.py +2 -0
  107. visidata/movement.py +14 -28
  108. visidata/optionssheet.py +3 -5
  109. visidata/path.py +59 -37
  110. visidata/pivot.py +8 -5
  111. visidata/pyobj.py +63 -9
  112. visidata/save.py +16 -9
  113. visidata/search.py +4 -4
  114. visidata/selection.py +10 -56
  115. visidata/settings.py +37 -35
  116. visidata/sheets.py +186 -108
  117. visidata/shell.py +22 -12
  118. visidata/sidebar.py +71 -16
  119. visidata/sort.py +21 -6
  120. visidata/statusbar.py +42 -5
  121. visidata/stored_list.py +5 -2
  122. visidata/tests/conftest.py +1 -0
  123. visidata/tests/test_commands.py +9 -1
  124. visidata/tests/test_completer.py +18 -0
  125. visidata/tests/test_edittext.py +3 -2
  126. visidata/text_source.py +7 -4
  127. visidata/textsheet.py +20 -6
  128. visidata/themes/ascii8.py +9 -6
  129. visidata/themes/asciimono.py +14 -4
  130. visidata/threads.py +13 -3
  131. visidata/tuiwin.py +5 -1
  132. visidata/type_currency.py +1 -2
  133. visidata/type_date.py +6 -1
  134. visidata/undo.py +10 -5
  135. visidata/utils.py +9 -3
  136. visidata/vdobj.py +21 -1
  137. visidata/wrappers.py +9 -1
  138. {visidata-3.0.2.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
  139. {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/vd.1 +72 -39
  140. {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +72 -39
  141. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/METADATA +24 -6
  142. visidata-3.1.dist-info/RECORD +284 -0
  143. visidata-3.0.2.dist-info/RECORD +0 -258
  144. {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd +0 -0
  145. {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
  146. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
  147. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
  148. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
  149. {visidata-3.0.2.dist-info → visidata-3.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
- return list(vd.aggregators[k] for k in (col.aggstr or '').split())
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, funcRows, funcValues=None, helpstr='foo'):
75
- 'Define aggregator `name` that calls func(col, rows)'
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.func = funcRows # funcRows(col, rows)
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 __call__(self, *args, **kwargs):
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, *args)
89
+ return self.funcValues(vals)
94
90
  except Exception as e:
95
91
  if len(vals) == 0:
96
92
  return None
97
- return e
93
+ raise e
98
94
 
99
- vd.aggregators[name] = _defaggr(name, type, _funcRows, funcValues=funcValues, helpstr=helpstr) # accepts a srccol + list of rows
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 _vsum(vals):
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
- def percentile(pct, helpstr=''):
138
- return _defaggr('p%s'%pct, None, lambda col,rows,pct=pct: _percentile(sorted(col.getValues(rows)), pct/100), helpstr=helpstr)
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 [percentile(round(100*i/q), helpstr) for i in range(1, q)]
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}'] = percentile(pct, f'{pct}th percentile')
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
- # returns keys of the row with the max value
166
- vd.aggregators['keymax'] = _defaggr('keymax', anytype, lambda col, rows: col.sheet.rowkey(max(col.getValueRows(rows))[1]), helpstr='key of the maximum value')
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, agg, rows):
237
+ def memo_aggregate(col, agg_choices, rows):
198
238
  'Show aggregated value in status, and add to memory.'
199
- aggval = agg(col, rows)
200
- typedval = wrapply(agg.type or col.type, aggval)
201
- dispval = col.format(typedval)
202
- k = col.name+'_'+agg.name
203
- vd.status(f'{k}={dispval}')
204
- vd.memory[k] = typedval
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())', 'Add aggregator to current column')
243
- Sheet.addCommand('z+', 'memo-aggregate', 'for agg in chooseAggregators(): cursorCol.memo_aggregate(aggregators[agg], selectedRows or rows)', 'memo result of aggregator over values in selected rows for current column')
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('''
@@ -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 sqlalchemy
91
- try:
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.ibis_to_sql(expr, fragment=True)
211
+ return self.ibis_expr_to_sql(expr, fragment=True)
216
212
 
217
- def ibis_to_sql(self, expr, fragment=False):
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.ibis_to_sql(expr)
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 BaseException, vd, VisiData, Progress
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.con.cancel(qid)
48
+ con._client.cancel(qid)
49
49
 
50
50
 
51
51
  ClickhouseSheet.init('total_rows', lambda: None)
@@ -1,6 +1,6 @@
1
1
  import time
2
2
 
3
- from visidata import vd, BaseException, VisiData
3
+ from visidata import vd, VisiData
4
4
 
5
5
  from ._ibis import IbisTableSheet, IbisConnectionPool, IbisTableIndexSheet
6
6
 
@@ -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.base_stem not in ('.git')] # files in working dir
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.warning('no command for %s' % (longname or keystrokes))
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.name = self.options.name_joiner.join(self.maybeClean(str(x)) for x in self._names)
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, '_names', self._names)
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(self.windowHeight, self.windowWidth)
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, disp_canvas_charset[braille_num], cattr.attr)
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
- # check if the cursor contains the midpoint of the character box
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
- self.plotviewBox = BoundingBox(self.left_margin, self.topMarginPixels,
407
- pvbox_xmax, self.plotheight-self.bottomMarginPixels-1)
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.refresh()
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 round(self.plotviewBox.xmin+(canvasX-self.visibleBox.xmin)*self.xScaler)
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 round(self.plotviewBox.ymin+(canvasY-self.visibleBox.ymin)*self.yScaler)
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(round(x), round(y), attr, row)
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(); refresh()', 'zoom to fit full extent')
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 is empty')
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.memo("clipval", cursorCol, cursorRow)', 'yank (copy) current cell to clipboard')
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', 'for r, v in zip(onlySelectedRows, itertools.cycle(vd.memory.clipcells or [None])): cursorCol.setValuesTyped([r], v)', 'set cells of current column for selected rows to last clipboard value')
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