diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd613af4..2b7ac2ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # VisiData version history +# v2.11.1 (2023-07-XX) + +- [tests] fix tests for Python >=3.11 +- [path] update for Python 3.12 API (reported by @QuLogic #1934) + +## Improvements + +- [chooser] choose only exactly matching strings in chooser (PR by @daviewales #1902) +- [columns] speed up `getMaxWidth()` for wide columns, and correct some edge cases (PR by @midichef #1747) +- [freqtbl] Default `disp_histogram` to U+25A0 BLACK SQUARE (■)) (PR by @daviewales #1949) +- [loaders fixed] do not truncate wide columns with fixed-width saver (PR by @daviewales #1890) + +## Bugfixes + +- add missing import `copy` +- [graph] fix graph ranges for xmax, ymax < 1 (PR by @midichef #1752) +- [graph] fix data on edges being drawn offscreen (PR by @midichef #1850) +- [input] fix `Ctrl+T` swap on empty input (reported by @gfrmin #1684) +- [inputsingle] loop until keystroke (do not timeout) +- [fill] allow filling with values that are logically false (PR by @midichef #1794) +- [macos] do not bind empty string to any keybinding +- [paste] add new rows to sheet if insufficient rows +- [path Dirsheet] set name to '.' for givenpath of '.' (reported by @geekscrapy #1768) +- [path] fix progress for compressed files (reported by @bitwisecook #1255 #1175) +- [replay] clearCaches before moving cursor (reported by @mokalan #1773) +- [save] handle saving 0 sheets (reported by @reagle #1266 #1720) +- [settings] clear cache correctly before set +- [undo] fix so that undo is Sheet-specific on copied sheets (reported by @geekscrapy #1780) +- [undo] undoing `zd` now removes `[M]` (modification mark) (reported by @Freed-Wu #1800) + # v2.11 (2023-01-15) - [ci] drop support for Python 3.6 (related to https://github.com/actions/setup-python/issues/543) diff --git a/README.md b/README.md index 3a6e558e2..8029085b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# VisiData v2.11 +# VisiData v2.11.1 [![twitter @VisiData][1.1]][1] [![Tests](https://github.com/saulpw/visidata/workflows/visidata-ci-build/badge.svg)](https://github.com/saulpw/visidata/actions/workflows/main.yml) diff --git a/dev/test.sh b/dev/test.sh index 4b9cb68c8..1670695cb 100755 --- a/dev/test.sh +++ b/dev/test.sh @@ -3,6 +3,7 @@ # Usage: test.sh [testname] set -e +set -x shopt -s failglob trap "echo aborted; exit;" SIGINT SIGTERM @@ -20,8 +21,22 @@ for i in $TESTS ; do outbase=${i##tests/} if [ "${i%-nosave.vd*}-nosave" == "${i%.vd*}" ]; then - echo "$1" + TEST=false + elif [ "${i%-311.vd*}-311" == "${i%.vd*}" ]; + then + if [ `python -c 'import sys; print(sys.version_info[:2] >= (3,11))'` == "True" ]; + then + TEST=true + else + TEST=false + fi + else + TEST=true + fi + if $TEST == true; + then + echo $TEST for goldfn in tests/golden/${outbase%.vd*}.*; do PYTHONPATH=. bin/vd --confirm-overwrite=False --play "$i" --batch --output "$goldfn" --config tests/.visidatarc --visidata-dir tests/.visidata echo "save: $goldfn" diff --git a/docs/man.md b/docs/man.md index a1f7cfc14..c0d83a8aa 100644 --- a/docs/man.md +++ b/docs/man.md @@ -610,7 +610,7 @@ vd(1) disp_menu_input … indicator if input required for command disp_menu_fmt Ctrl+H for help menu right-side menu format string - disp_histogram * histogram element character + disp_histogram ■ histogram element character disp_histolen 50 width of histogram column disp_canvas_charset ⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾⣿ charset to render 2x4 blocks on canvas diff --git a/setup.py b/setup.py index 68a710140..23eed1d40 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup # tox can't actually run python3 setup.py: https://github.com/tox-dev/tox/issues/96 #from visidata import __version__ -__version__ = '2.11' +__version__ = '2.11.1' setup(name='visidata', version=__version__, diff --git a/tests/error-passthru.vd b/tests/error-passthru.vd index 19e93ef11..b05a69606 100644 --- a/tests/error-passthru.vd +++ b/tests/error-passthru.vd @@ -4,3 +4,4 @@ benchmark Quantity 1 select-equal-cell , benchmark Quantity delete-cells gzd benchmark Quantity addcol-expr Quantity.foo = benchmark Quantity.foo freq-col F +benchmark_Quantity.foo_freq histogram hide-col - Hide current column diff --git a/tests/errors.vd b/tests/errors-311.vd similarity index 100% rename from tests/errors.vd rename to tests/errors-311.vd diff --git a/tests/extend.vd b/tests/extend.vd index d1f5a046a..c7709ecb8 100644 --- a/tests/extend.vd +++ b/tests/extend.vd @@ -1,20 +1,18 @@ sheet col row longname input keystrokes comment open-file tests/data2.tsv o open-file tests/data1.tsv o -data1 jump-sheet-2 ^[2 data2 Key key-col ! -data2 jump-sheet-1 ^[1 data1 Key key-col ! - sheets-stack S +data1 sheets-stack S sheets キdata2 select-row s sheets キdata1 select-row s -sheets join-sheets extend & +sheets join-selected extend & data1+data2 A freq-col F -data1+data2_A_freq jump-sheet-1 ^[1 data1 dup-rows g" data1_copy A key-col ! data1_copy Key key-col ! data1_copy sheets-all S sheets_all キdata1_copy select-row s sheets_all キdata1+data2_A_freq select-row s -sheets_all join-sheets extend & +sheets_all join-selected extend & +data1+data2_A_freq+data1_copy histogram hide-col - Hide current column diff --git a/tests/freq-same-int.vd b/tests/freq-same-int.vd index 462f64c4b..5ecdce85c 100644 --- a/tests/freq-same-int.vd +++ b/tests/freq-same-int.vd @@ -7,6 +7,6 @@ benchmark Quantity キ2018-07-03 setcol-input 3 ge benchmark unselect-rows gu benchmark Quantity type-int # benchmark Quantity freq-col F -benchmark_Quantity_freq quit-sheet q benchmark Quantity キ2018-07-03 edit-cell 2 e benchmark Quantity freq-col F +benchmark_Quantity_freq histogram hide-col - Hide current column diff --git a/tests/golden/error-passthru.tsv b/tests/golden/error-passthru.tsv index d066c34ab..01025a119 100644 --- a/tests/golden/error-passthru.tsv +++ b/tests/golden/error-passthru.tsv @@ -1,2 +1,2 @@ -Quantity.foo count percent histogram -#ERR 51 100.00 ************************************************** +Quantity.foo count percent +#ERR 51 100.00 diff --git a/tests/golden/errors-311.tsv b/tests/golden/errors-311.tsv new file mode 100644 index 000000000..906d8e740 Binary files /dev/null and b/tests/golden/errors-311.tsv differ diff --git a/tests/golden/errors.csv b/tests/golden/errors.csv deleted file mode 100644 index 00eea709f..000000000 --- a/tests/golden/errors.csv +++ /dev/null @@ -1,31 +0,0 @@ -1.00,2021-06-04,unquoted value,X, -1.01,2021-06-04,quoted value,X, -1.02,2021-06-04,unquoted blank,, -1.03,2021-06-04,quoted blank,, -1.04,2021-06-04,multiline value," -X -", -1.05,2021-06-04,"internal ""unquoted"" quotes",X, -1.06,2021-06-04,"comma, in quotes",",", -1.07,2021-06-04,quoted quotes at beginning and end,"""X""", -1.08,2021-06-04,quoted quotes internal,"X""Y""X", -1.09,2021-06-04,multiline value with LF," -X -", -1.10,2021-06-04,utf8 bytes,☃, -2.01,2021-06-04,missing field,, -2.02,2021-06-04,extra field,,X -2.03,2021-06-04,extra newline,, -,,,, -2.04,2021-06-04,TAB after ending quote,X , -2.05,2021-06-04,LF after ending quote,X, -2.06,2021-06-04,bad character,X�, -2.07,2021-06-04,no leading quote,"X""", -#ERR,,,, -#ERR,,,, -2.10,2021-06-04,"missing comma ""X""",, -3.0,2021-06-04,NA is NULL?,NA, -3.1,2021-06-04,N/A is NULL?,N/A, -3.2,2021-06-04,NONE is NULL?,NONE, -3.3,2021-06-04,NULL is NULL?,NULL, -3.4,2021-06-04,. is NULL?,., diff --git a/tests/golden/extend.tsv b/tests/golden/extend.tsv index 6f56f8a2f..0de7d298d 100644 --- a/tests/golden/extend.tsv +++ b/tests/golden/extend.tsv @@ -1,4 +1,4 @@ -A count percent histogram data1_copy_Key data1_copy_B -a1 1 33.33 ************************************************** 1 b1 -c1 1 33.33 ************************************************** 2 d1 -e1 1 33.33 ************************************************** 2 f1 +A count percent data1_copy_Key data1_copy_B +a1 1 33.33 1 b1 +c1 1 33.33 2 d1 +e1 1 33.33 2 f1 diff --git a/tests/golden/freq-same-int.tsv b/tests/golden/freq-same-int.tsv index 1d9969841..6a14fe57e 100644 --- a/tests/golden/freq-same-int.tsv +++ b/tests/golden/freq-same-int.tsv @@ -1,3 +1,3 @@ -Quantity count percent histogram -2 1 1.96 * -3 50 98.04 ************************************************** +Quantity count percent +2 1 1.96 +3 50 98.04 diff --git a/tests/golden/histogram.tsv b/tests/golden/histogram.tsv index 159893432..fb0874f31 100644 --- a/tests/golden/histogram.tsv +++ b/tests/golden/histogram.tsv @@ -1,6 +1,6 @@ -Quantity count percent histogram -2.00 - 30.40 26 92.86 ************************************************** -30.40 - 58.80 1 3.57 * -58.80 - 87.20 0 0.00 -87.20 - 115.60 0 0.00 -115.60 - 144.00 1 3.57 * +Quantity count percent +2.00 - 30.40 26 92.86 +30.40 - 58.80 1 3.57 +58.80 - 87.20 0 0.00 +87.20 - 115.60 0 0.00 +115.60 - 144.00 1 3.57 diff --git a/tests/golden/numeric_binning.tsv b/tests/golden/numeric_binning.tsv index b1a921a82..6b7556bdc 100644 --- a/tests/golden/numeric_binning.tsv +++ b/tests/golden/numeric_binning.tsv @@ -1,8 +1,8 @@ -Quantity count percent histogram -1.00 - 21.43 46 90.20 ************************************************** -21.43 - 41.86 3 5.88 *** -41.86 - 62.29 1 1.96 * -123.57 - 144.00 1 1.96 * -62.29 - 82.71 0 0.00 -82.71 - 103.14 0 0.00 -103.14 - 123.57 0 0.00 +Quantity count percent +1.00 - 21.43 46 90.20 +21.43 - 41.86 3 5.88 +41.86 - 62.29 1 1.96 +123.57 - 144.00 1 1.96 +62.29 - 82.71 0 0.00 +82.71 - 103.14 0 0.00 +103.14 - 123.57 0 0.00 diff --git a/tests/histogram.vd b/tests/histogram.vd index 340767bc4..376015245 100644 --- a/tests/histogram.vd +++ b/tests/histogram.vd @@ -1,8 +1,9 @@ sheet col row longname input keystrokes comment open-file sample_data/benchmark.csv o - numeric_binning set-option True +global numeric_binning set-option True benchmark Quantity type-int # benchmark Quantity addcol-expr Quantity > 1 = benchmark Quantity > 1 select-col-regex True | benchmark dup-selected " benchmark_selectedref Quantity freq-col F +benchmark_selectedref_Quantity_freq histogram hide-col - Hide current column diff --git a/tests/numeric_binning.vd b/tests/numeric_binning.vd index 07b4671ab..231a2dff0 100644 --- a/tests/numeric_binning.vd +++ b/tests/numeric_binning.vd @@ -1,6 +1,7 @@ sheet col row longname input keystrokes comment open-file sample_data/benchmark.csv o - override numeric_binning set-option True +global numeric_binning set-option True benchmark Quantity type-int # benchmark Quantity freq-col F benchmark_Quantity_freq count sort-desc ] +benchmark_Quantity_freq histogram hide-col - Hide current column diff --git a/visidata/__init__.py b/visidata/__init__.py index 8cb0389d7..8328569f5 100644 --- a/visidata/__init__.py +++ b/visidata/__init__.py @@ -1,6 +1,6 @@ 'VisiData: a curses interface for exploring and arranging tabular data' -__version__ = '2.11' +__version__ = '2.11,1' __version_info__ = 'VisiData v' + __version__ __author__ = 'Saul Pwanson ' __status__ = 'Production/Stable' diff --git a/visidata/_input.py b/visidata/_input.py index 9181c086f..877a9b444 100644 --- a/visidata/_input.py +++ b/visidata/_input.py @@ -222,7 +222,7 @@ def find_nonword(s, a, b, incr): elif ch == '^K': v = v[:i] # ^Kill to end-of-line elif ch == '^O': v = vd.launchExternalEditor(v) elif ch == '^R': v = str(value) # ^Reload initial value - elif ch == '^T': v = delchar(splice(v, i-2, v[i-1]), i) # swap chars + elif ch == '^T': v = delchar(splice(v, i-2, v[i-1:i]), i) # swap chars elif ch == '^U': v = v[i:]; i = 0 # clear to beginning elif ch == '^V': v = splice(v, i, until_get_wch(scr)); i += 1 # literal character elif ch == '^W': j = find_nonword(v, 0, i-1, -1); v = v[:j+1] + v[i:]; i = j+1 # erase word @@ -300,7 +300,9 @@ def inputsingle(vd, prompt, record=True): rstatuslen = vd.drawRightStatus(sheet._scr, sheet) promptlen = clipdraw(sheet._scr, y, 0, prompt, 0, w=w-rstatuslen-1) sheet._scr.move(y, w-promptlen-rstatuslen-2) - v = vd.getkeystroke(sheet._scr) + + while not v: + v = vd.getkeystroke(sheet._scr) if record and vd.cmdlog: vd.setLastArgs(v) diff --git a/visidata/canvas.py b/visidata/canvas.py index 48beb67e0..673792e64 100644 --- a/visidata/canvas.py +++ b/visidata/canvas.py @@ -482,16 +482,26 @@ def resetBounds(self): if ymin is None or y < ymin: ymin = y if xmax is None or x > xmax: xmax = x if ymax is None or y > ymax: ymax = y - self.canvasBox = BoundingBox(float(xmin or 0), float(ymin or 0), float(xmax or 0)+1, float(ymax or 0)+1) - + xmin = xmin or 0 + xmax = xmax or 0 + ymin = ymin or 0 + ymax = ymax or 0 + if xmin == xmax: + xmax += 1 + if ymin == ymax: + ymax += 1 + self.canvasBox = BoundingBox(float(xmin), float(ymin), float(xmax), float(ymax)) + + w = self.calcVisibleBoxWidth() + h = self.calcVisibleBoxHeight() if not self.visibleBox: # initialize minx/miny, but w/h must be set first to center properly - self.visibleBox = Box(0, 0, self.plotviewBox.w/self.xScaler, self.plotviewBox.h/self.yScaler) - self.visibleBox.xmin = self.canvasBox.xcenter - self.visibleBox.w/2 - self.visibleBox.ymin = self.canvasBox.ycenter - self.visibleBox.h/2 + self.visibleBox = Box(0, 0, w, h) + self.visibleBox.xmin = self.canvasBox.xmin + (self.canvasBox.w / 2) * (1 - self.xzoomlevel) + self.visibleBox.ymin = self.canvasBox.ymin + (self.canvasBox.h / 2) * (1 - self.yzoomlevel) else: - self.visibleBox.w = self.plotviewBox.w/self.xScaler - self.visibleBox.h = self.plotviewBox.h/self.yScaler + self.visibleBox.w = w + self.visibleBox.h = h if not self.cursorBox: self.cursorBox = Box(self.visibleBox.xmin, self.visibleBox.ymin, self.canvasCharWidth, self.canvasCharHeight) @@ -534,6 +544,33 @@ def yScaler(self): else: return yratio + def calcVisibleBoxWidth(self): + w = self.canvasBox.w * self.xzoomlevel + if self.aspectRatio: + h = self.canvasBox.h * self.yzoomlevel + xratio = self.plotviewBox.w / w + yratio = self.plotviewBox.h / h + if xratio <= yratio: + return w / self.aspectRatio + else: + return self.plotviewBox.w / (self.aspectRatio * yratio) + else: + return w + + def calcVisibleBoxHeight(self): + h = self.canvasBox.h * self.yzoomlevel + if self.aspectRatio: + w = self.canvasBox.w * self.yzoomlevel + xratio = self.plotviewBox.w / w + yratio = self.plotviewBox.h / h + if xratio < yratio: + return self.plotviewBox.h / xratio + else: + return h + else: + return h + + #could be called canvas_to_plotterX() def scaleX(self, x): 'returns plotter x coordinate' return round(self.plotviewBox.xmin+(x-self.visibleBox.xmin)*self.xScaler) diff --git a/visidata/choose.py b/visidata/choose.py index e7e5c11be..efb9ce567 100644 --- a/visidata/choose.py +++ b/visidata/choose.py @@ -40,7 +40,12 @@ def chooseFancy(vd, choices): @VisiData.api def chooseMany(vd, choices): - 'Return a list of 1 or more keys from *choices*, which is a list of dicts. Each element dict must have a unique "key", which must be typed directly by the user in non-fancy mode (therefore no spaces). All other items in the dicts are also shown in fancy chooser mode. Use previous choices from the replay input if available. Add chosen keys (space-separated) to the cmdlog as input for the current command.''' + '''Return a list of 1 or more keys from *choices*, which is a list of + dicts. Each element dict must have a unique "key", which must be typed + directly by the user in non-fancy mode (therefore no spaces). All other + items in the dicts are also shown in fancy chooser mode. Use previous + choices from the replay input if available. Add chosen keys + (space-separated) to the cmdlog as input for the current command.''' if vd.cmdlog: v = vd.getLastArgs() if v is not None: @@ -62,11 +67,10 @@ def throw_fancy(v, i): return v, i chosenstr = vd.input(prompt+': ', completer=CompleteKey(choice_keys), bindings={'^X': throw_fancy}) for c in chosenstr.split(): - poss = [p for p in choice_keys if str(p).startswith(c)] - if not poss: - vd.warning('invalid choice "%s"' % c) + if c in choice_keys: + chosen.append(c) else: - chosen.extend(poss) + vd.warning('invalid choice "%s"' % c) except ReturnValue as e: chosen = e.args[0] diff --git a/visidata/clipboard.py b/visidata/clipboard.py index 41fb1090b..d617e5347 100644 --- a/visidata/clipboard.py +++ b/visidata/clipboard.py @@ -90,8 +90,18 @@ def pasteFromClipboard(vd, cols, rows): text = vd.getLastArgs() or vd.sysclip_value().strip() or vd.fail('system clipboard is empty') vd.addUndoSetValues(cols, rows) + lines = text.split('\n') + if not lines: + vd.warning('nothing to paste') + return + + vs = cols[0].sheet + newrows = [vs.newRow() for i in range(len(lines)-len(rows))] + if newrows: + rows.extend(newrows) + vs.addRows(newrows) - for line, r in zip(text.split('\n'), rows): + for line, r in zip(lines, rows): for v, c in zip(line.split('\t'), cols): c.setValue(r, v) diff --git a/visidata/cmdlog.py b/visidata/cmdlog.py index e1e363178..66c3ab369 100644 --- a/visidata/cmdlog.py +++ b/visidata/cmdlog.py @@ -287,11 +287,12 @@ def replay_cancel(vd): @VisiData.api def moveToReplayContext(vd, r, vs): 'set the sheet/row/col to the values in the replay row' + vd.clearCaches() if r.row not in [None, '']: - vs.moveToRow(r.row) or vd.error('no "%s" row' % r.row) + vs.moveToRow(r.row) or vd.error(f'no {r.row} row on {vs}') if r.col not in [None, '']: - vs.moveToCol(r.col) or vd.error('no "%s" column' % r.col) + vs.moveToCol(r.col) or vd.error(f'no {r.col} column on {vs}') @VisiData.api diff --git a/visidata/column.py b/visidata/column.py index c6682e92b..792ea717a 100644 --- a/visidata/column.py +++ b/visidata/column.py @@ -391,13 +391,14 @@ def putValue(self, row, val): 'Change value for *row* in this column to *val* immediately. Does not check the type. Overridable; by default calls ``.setter(row, val)``.' return self.setter(self, row, val) - def setValue(self, row, val): + def setValue(self, row, val, setModified=True): 'Change value for *row* in this column to *val*. Call ``putValue`` immediately if not a deferred column (added to deferred parent at load-time); otherwise cache until later ``putChanges``. Caller must add undo function.' if self.defer: self.cellChanged(row, val) else: self.putValue(row, val) - self.sheet.setModified() + if setModified: #1800 + self.sheet.setModified() def setValueSafe(self, row, value): 'setValue and ignore exceptions.' @@ -430,8 +431,18 @@ def getMaxWidth(self, rows): w = 0 nlen = dispwidth(self.name) if len(rows) > 0: - w = max(max(dispwidth(self.getDisplayValue(r), maxwidth=self.sheet.windowWidth) for r in rows), nlen)+2 - return max(w, nlen) + w_max = 0 + for r in rows: + row_w = dispwidth(self.getDisplayValue(r), maxwidth=self.sheet.windowWidth) + if w_max < row_w: + w_max = row_w + if w_max >= self.sheet.windowWidth: + break + w = w_max + w = max(w, nlen)+2 + w = min(w, self.sheet.windowWidth) + return w + # ---- Column makers diff --git a/visidata/extensible.py b/visidata/extensible.py index 249c76939..fb8c939ef 100644 --- a/visidata/extensible.py +++ b/visidata/extensible.py @@ -94,11 +94,12 @@ def dofunc(self): @classmethod def lazy_property(cls, func): 'Return ``func()`` on first access and cache result; return cached result thereafter.' + name = '_' + func.__name__ + cls.init(name, lambda: None, copy=False) @property @wraps(func) def get_if_not(self): - name = '_' + func.__name__ - if not hasattr(self, name): + if getattr(self, name) is None: setattr(self, name, func(self)) return getattr(self, name) setattr(cls, func.__name__, get_if_not) diff --git a/visidata/fill.py b/visidata/fill.py index ac390abc0..bd6cdef41 100644 --- a/visidata/fill.py +++ b/visidata/fill.py @@ -17,7 +17,7 @@ def fillNullValues(vd, col, rows): val = e if isNull(val): - if lastval and (id(r) in rowsToFill): + if not isNull(lastval) and (id(r) in rowsToFill): oldvals.append((col,r,val)) col.setValue(r, lastval) n += 1 diff --git a/visidata/freqtbl.py b/visidata/freqtbl.py index 49cad4756..9a38b28cc 100644 --- a/visidata/freqtbl.py +++ b/visidata/freqtbl.py @@ -5,7 +5,7 @@ from visidata.pivot import PivotSheet, PivotGroupRow -vd.option('disp_histogram', '*', 'histogram element character') +vd.option('disp_histogram', '■', 'histogram element character') vd.option('disp_histolen', 50, 'width of histogram column') vd.option('histogram_bins', 0, 'number of bins for histogram of numeric columns') vd.option('numeric_binning', False, 'bin numeric columns into ranges', replay=True) diff --git a/visidata/loaders/fixed_width.py b/visidata/loaders/fixed_width.py index 359a12938..8cd4d7248 100644 --- a/visidata/loaders/fixed_width.py +++ b/visidata/loaders/fixed_width.py @@ -84,7 +84,7 @@ def save_fixed(vd, p, *vsheets): widths = {} # Column -> width:int # headers for col in Progress(sheet.visibleCols, gerund='sizing'): - widths[col] = col.width or sheet.options.default_width or col.getMaxWidth(sheet.rows) + widths[col] = col.getMaxWidth(sheet.rows) #1849 fp.write(('{0:%s} ' % widths[col]).format(col.name)) fp.write('\n') diff --git a/visidata/macos.py b/visidata/macos.py index 930064921..c3e739345 100644 --- a/visidata/macos.py +++ b/visidata/macos.py @@ -69,7 +69,7 @@ BaseSheet.bindkey('•', 'Alt+8') BaseSheet.bindkey('ª', 'Alt+9') BaseSheet.bindkey('º', 'Alt+0') -BaseSheet.bindkey('', 'Alt+`') +#BaseSheet.bindkey('', 'Alt+`') BaseSheet.bindkey('–', 'Alt+-') BaseSheet.bindkey('≠', 'Alt+=') BaseSheet.bindkey('“', 'Alt+[') diff --git a/visidata/macros.py b/visidata/macros.py index c31b4bbba..85e3e404a 100644 --- a/visidata/macros.py +++ b/visidata/macros.py @@ -1,6 +1,8 @@ -from visidata import * +from copy import copy from functools import wraps +from visidata import * + from visidata.cmdlog import CommandLog, CommandLogJsonl vd.macroMode = None diff --git a/visidata/main.py b/visidata/main.py index e0dd789be..829c7719e 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -2,7 +2,7 @@ # Usage: $0 [] [ ...] # $0 [] --play [--batch] [-w ] [-o ] [field=value ...] -__version__ = '2.11' +__version__ = '2.11.1' __version_info__ = 'saul.pw/VisiData v' + __version__ from copy import copy diff --git a/visidata/man/vd.1 b/visidata/man/vd.1 index 1225cddd2..6522e8285 100644 --- a/visidata/man/vd.1 +++ b/visidata/man/vd.1 @@ -1225,7 +1225,7 @@ indicator if command pushes sheet onto sheet stack indicator if input required for command .It Sy "disp_menu_fmt " No "Ctrl+H for help menu" right-side menu format string -.It Sy "disp_histogram " No "*" +.It Sy "disp_histogram " No "\[u25A0]" histogram element character .It Sy "disp_histolen " No "50" width of histogram column diff --git a/visidata/man/vd.txt b/visidata/man/vd.txt index c41a2ce06..4dae3cc04 100644 --- a/visidata/man/vd.txt +++ b/visidata/man/vd.txt @@ -897,7 +897,7 @@ COMMANDLINE OPTIONS command disp_menu_fmt Ctrl+H for help menu right-side menu format string - disp_histogram * histogram element character + disp_histogram ■ histogram element character disp_histolen 50 width of histogram column disp_canvas_charset ⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾⣿ diff --git a/visidata/man/visidata.1 b/visidata/man/visidata.1 index 1225cddd2..6522e8285 100644 --- a/visidata/man/visidata.1 +++ b/visidata/man/visidata.1 @@ -1225,7 +1225,7 @@ indicator if command pushes sheet onto sheet stack indicator if input required for command .It Sy "disp_menu_fmt " No "Ctrl+H for help menu" right-side menu format string -.It Sy "disp_histogram " No "*" +.It Sy "disp_histogram " No "\[u25A0]" histogram element character .It Sy "disp_histolen " No "50" width of histogram column diff --git a/visidata/melt.py b/visidata/melt.py index bfc9390e4..e6faafc83 100644 --- a/visidata/melt.py +++ b/visidata/melt.py @@ -1,5 +1,6 @@ import collections import re +from copy import copy from visidata import * diff --git a/visidata/modify.py b/visidata/modify.py index d36e9ffa8..50799e612 100644 --- a/visidata/modify.py +++ b/visidata/modify.py @@ -1,3 +1,4 @@ +from copy import copy from visidata import * vd.option('color_add_pending', 'green', 'color for rows pending add') diff --git a/visidata/path.py b/visidata/path.py index 47a044a82..e030e7b30 100644 --- a/visidata/path.py +++ b/visidata/path.py @@ -74,11 +74,9 @@ def __init__(self, path, fp, mode='r', **kwargs): # track Progress on original fp self.fp_orig_read = self.fp.read self.fp_orig_close = self.fp.close - # These two lines result in bug #1159, a corrupted save of corruption formats - # for now we are reverting by commenting out, and opened #1175 to investigate - # Progress bars for compression formats might not work in the meanwhile - #self.fp.read = self.read - #self.fp.close = self.close + + self.fp.read = self.read + self.fp.close = self.close if self.prog: self.prog.__enter__() @@ -151,6 +149,8 @@ def given(self, given): self.ext = self.suffix[1:] if self.suffix: #1450 don't make this a oneliner; [:-0] doesn't work self.name = self._path.name[:-len(self.suffix)] + elif self._given == '.': #1768 + self.name = '.' else: self.name = self._path.name @@ -245,22 +245,29 @@ def open(self, *args, **kwargs): return FileProgress(self, fp=BytesIOWrapper(self.fptext), **kwargs) path = self - binmode = 'wb' if 'w' in kwargs.get('mode', '') else 'rb' + if self.compression == 'gz': import gzip - return gzip.open(FileProgress(path, fp=open(path, mode=binmode), **kwargs), *args, **kwargs) + zopen = gzip.open elif self.compression == 'bz2': import bz2 - return bz2.open(FileProgress(path, fp=open(path, mode=binmode), **kwargs), *args, **kwargs) + zopen = bz2.open elif self.compression in ['xz', 'lzma']: import lzma - return lzma.open(FileProgress(path, fp=open(path, mode=binmode), **kwargs), *args, **kwargs) + zopen = lzma.open elif self.compression == 'zst': import zstandard - return zstandard.open(FileProgress(path, fp=open(path, mode=binmode), **kwargs), *args, **kwargs) + zopen = zstandard.open else: return FileProgress(path, fp=self._path.open(*args, **kwargs), **kwargs) + if 'w' in kwargs.get('mode', ''): + #1159 FileProgress on the outside to close properly when writing + return FileProgress(path, fp=zopen(path, **kwargs), **kwargs) + + #1255 FileProgress on the inside to track uncompressed bytes when reading + return zopen(FileProgress(path, fp=open(path, mode='rb'), **kwargs), **kwargs) + def __iter__(self): with Progress(total=filesize(self)) as prog: with self.open_text(encoding=vd.options.encoding) as fd: @@ -315,10 +322,10 @@ def with_name(self, name): 'Return a sibling Path with *name* as a filename in the same directory.' if self.is_url(): urlparts = list(urlparse(self.given)) - urlparts[2] = '/'.join(Path(urlparts[2])._parts[1:-1] + [name]) + urlparts[2] = '/'.join(list(Path(urlparts[2]).parts[1:-1]) + [name]) return Path(urlunparse(urlparts)) else: - return Path(self._from_parsed_parts(self._drv, self._root, self._parts[:-1] + [name])) + return Path(self._from_parsed_parts(self._drv, self._root, list(self.parts[:-1]) + [name])) class RepeatFile: diff --git a/visidata/pivot.py b/visidata/pivot.py index dbda705fd..1ae5b9670 100644 --- a/visidata/pivot.py +++ b/visidata/pivot.py @@ -1,4 +1,5 @@ import collections +from copy import copy from visidata import * diff --git a/visidata/save.py b/visidata/save.py index 2ee0a0ad6..75752e1f6 100644 --- a/visidata/save.py +++ b/visidata/save.py @@ -1,4 +1,5 @@ import collections +from copy import copy from visidata import * @@ -105,6 +106,10 @@ def save_cols(vd, cols): def saveSheets(vd, givenpath, *vsheets, confirm_overwrite=False): 'Save all *vsheets* to *givenpath*.' + if not vsheets: # blank tuple + vd.warning('no sheets to save') + return + filetype = givenpath.ext or options.save_filetype vd.clearCaches() diff --git a/visidata/settings.py b/visidata/settings.py index 6db415718..1fc62df4c 100644 --- a/visidata/settings.py +++ b/visidata/settings.py @@ -141,6 +141,7 @@ def _get(self, k, obj=None): return opt def _set(self, k, v, obj=None, helpstr=''): + opt = self._get(k) or Option(k, v, '') self._cache.clear() # invalidate entire cache on any change return self._opts.set(k, Option(k, v, helpstr), obj) diff --git a/visidata/undo.py b/visidata/undo.py index 8a4b20dd5..746028f4c 100644 --- a/visidata/undo.py +++ b/visidata/undo.py @@ -99,7 +99,7 @@ def addUndoSetValues(vd, cols, rows): oldvals = [(c, r, c.getValue(r)) for c,r in itertools.product(cols, vd.Progress(rows, gerund='doing'))] def _undo(): for c, r, v in oldvals: - c.setValue(r, v) + c.setValue(r, v, setModified=False) vd.addUndo(_undo) @VisiData.api