We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[freqtbl] toggling rows selects/unselects an inconsistent number of rows
**Small description**
In a FreqTableSheet where its source sheet has many rows, when rows are toggled, the number of rows that is selected/unselected on the source sheet is inconsistent.

**Expected result**
Every time the toggle is performed, the number of rows selected/unselected on the source sheet should be the same.

**Actual result with screenshot**
The number of rows selected on the source sheet is different every time. Sometimes, the number of unselected rows is negative.
https://asciinema.org/a/ksxBTGy0E1tGh5unX2Gsu0eeR

**Steps to reproduce with sample data and a .vd**
`python -c "n = 200_000; s = 'line\n'; print(f'{s*n}',end='')" |vd`
hit `F` and then repeatedly hit `t` (or `gt` or `gzt` or, in a sheet with more than one kind of cell, `zt`)

**Additional context**
VisiData v2.10.2

Workaround:
Use `s` and `u` to select/unselect rows instead of toggling.

There is a simple fix for the inconsistent behavior, by adding vd.sync() to selectRow() and unselectRow().
https://github.com/midichef/visidata/commit/52d8596ed01fed17bca8b5215130a77801f1e3b3
However, I'm unsure if this is the correct solution for 2 reasons.
First, I don't have a good understanding of how threading works in the visidata codebase.
Second, it's not clear to me how FreqTableSheet toggling should behave. Suppose that some rows on the source sheet are already selected, but not all of them. The way it works now is that after a toggle, all matching rows in the source sheet will end up in the same state: either all are selected, or all are unselected. But arguably the effect should be to toggle the selected status of all the matching rows in the source sheet, leaving some selected and some unselected.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `visidata/selection.py`
Content:
```
1 from copy import copy
2 from visidata import vd, Sheet, Progress, asyncthread, options, rotateRange, Fanout, undoAttrCopyFunc, RowColorizer
3 
4 vd.option('bulk_select_clear', False, 'clear selected rows before new bulk selections', replay=True)
5 vd.option('some_selected_rows', False, 'if no rows selected, if True, someSelectedRows returns all rows; if False, fails')
6 
7 Sheet.init('_selectedRows', dict)  # rowid(row) -> row
8 
9 vd.rowNoters.append(
10         lambda sheet, row: sheet.isSelected(row) and sheet.options.disp_selected_note
11 )
12 Sheet.colorizers.append( RowColorizer(2, 'color_selected_row', lambda s,c,r,v:
13     r is not None and s.isSelected(r))
14 )
15 
16 @Sheet.api
17 def isSelected(self, row):
18     'Return True if *row* is selected.'
19     return self.rowid(row) in self._selectedRows
20 
21 @Sheet.api
22 @asyncthread
23 def toggle(self, rows):
24     'Toggle selection of given *rows*.  Async.'
25     self.addUndoSelection()
26     for r in Progress(rows, 'toggling', total=len(self.rows)):
27         if not self.unselectRow(r):
28             self.selectRow(r)
29 
30 
31 @Sheet.api
32 def select_row(self, row):
33     'Add single *row* to set of selected rows.'
34     self.addUndoSelection()
35     self.selectRow(row)
36 
37 
38 @Sheet.api
39 def toggle_row(self, row):
40     'Toggle selection of given *row*.'
41     self.addUndoSelection()
42     if not self.unselectRow(row):
43         self.selectRow(row)
44 
45 
46 @Sheet.api
47 def unselect_row(self, row):
48     'Remove single *row* from set of selected rows.'
49     self.addUndoSelection()
50     self.unselectRow(row) or vd.warning('row not selected')
51 
52 
53 @Sheet.api
54 def selectRow(self, row):
55     'Add *row* to set of selected rows.  May be called multiple times in one command.  Overridable.'
56     self._selectedRows[self.rowid(row)] = row
57 
58 
59 @Sheet.api
60 def unselectRow(self, row):
61     'Remove *row* from set of selected rows.  Return True if row was previously selected.  Overridable.'
62     if self.rowid(row) in self._selectedRows:
63         del self._selectedRows[self.rowid(row)]
64         return True
65     else:
66         return False
67 
68 @Sheet.api
69 def clearSelected(self):
70     'Clear set of selected rows, without calling ``unselectRow`` for each one.'
71     self.addUndoSelection()
72     self._selectedRows.clear()
73 
74 @Sheet.api
75 @asyncthread
76 def select(self, rows, status=True, progress=True):
77     "Add *rows* to set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False."
78     self.addUndoSelection()
79     before = self.nSelectedRows
80     if self.options.bulk_select_clear:
81         self.clearSelected()
82     for r in (Progress(rows, 'selecting') if progress else rows):
83         self.selectRow(r)
84     if status:
85         if options.bulk_select_clear:
86             msg = 'selected %s %s%s' % (self.nSelectedRows, self.rowtype, ' instead' if before > 0 else '')
87         else:
88             msg = 'selected %s%s %s' % (self.nSelectedRows-before, ' more' if before > 0 else '', self.rowtype)
89         vd.status(msg)
90 
91 @Sheet.api
92 @asyncthread
93 def unselect(self, rows, status=True, progress=True):
94     "Remove *rows* from set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False."
95     self.addUndoSelection()
96     before = self.nSelectedRows
97     for r in (Progress(rows, 'unselecting') if progress else rows):
98         self.unselectRow(r)
99     if status:
100         vd.status('unselected %s/%s %s' % (before-self.nSelectedRows, before, self.rowtype))
101 
102 @Sheet.api
103 def selectByIdx(self, rowIdxs):
104     'Add rows indicated by row indexes in *rowIdxs* to set of selected rows.  Async.'
105     self.select((self.rows[i] for i in rowIdxs), progress=False)
106 
107 @Sheet.api
108 def unselectByIdx(self, rowIdxs):
109     'Remove rows indicated by row indexes in *rowIdxs* from set of selected rows.  Async.'
110     self.unselect((self.rows[i] for i in rowIdxs), progress=False)
111 
112 @Sheet.api
113 def gatherBy(self, func, gerund='gathering'):
114     'Generate rows for which ``func(row)`` returns True, starting from the cursor.'
115     for i in Progress(rotateRange(self.nRows, self.cursorRowIndex-1), total=self.nRows, gerund=gerund):
116         try:
117             r = self.rows[i]
118             if func(r):
119                 yield r
120         except Exception as e:
121             vd.exceptionCaught(e, status=False)
122 
123 @Sheet.property
124 def selectedRows(self):
125     'List of selected rows in sheet order.'
126     if self.nSelectedRows <= 1:
127         return Fanout(self._selectedRows.values())
128     return Fanout((r for r in self.rows if self.rowid(r) in self._selectedRows))
129 
130 @Sheet.property
131 def onlySelectedRows(self):
132     'List of selected rows in sheet order.  Fail if no rows are selected.'
133     if self.nSelectedRows == 0:
134         vd.fail('no rows selected')
135     return self.selectedRows
136 
137 @Sheet.property
138 def someSelectedRows(self):
139     '''Return a list of rows:
140         (a) in batch mode, always return selectedRows
141         (b) in interactive mode, if options.some_selected_rows is True, return selectedRows or all rows if none selected
142         (c) in interactive mode, if options.some_selected_rows is False, return selectedRows or fail if none selected'''
143     if options.batch:
144         return self.selectedRows
145     if options.some_selected_rows:
146         return self.selectedRows or self.rows
147     return self.onlySelectedRows
148 
149 @Sheet.property
150 def nSelectedRows(self):
151     'Number of selected rows.'
152     return len(self._selectedRows)
153 
154 @Sheet.api
155 @asyncthread
156 def deleteSelected(self):
157     'Delete all selected rows.  Async.'
158     ndeleted = self.deleteBy(self.isSelected)
159     nselected = self.nSelectedRows
160     self.clearSelected()
161     if ndeleted != nselected:
162         vd.warning(f'deleted {ndeleted}, expected {nselected}')
163 
164 
165 @Sheet.api
166 def addUndoSelection(sheet):
167     vd.addUndo(undoAttrCopyFunc([sheet], '_selectedRows'))
168 
169 
170 Sheet.addCommand('t', 'stoggle-row', 'toggle_row(cursorRow); cursorDown(1)', 'toggle selection of current row')
171 Sheet.addCommand('s', 'select-row', 'select_row(cursorRow); cursorDown(1)', 'select current row')
172 Sheet.addCommand('u', 'unselect-row', 'unselect_row(cursorRow); cursorDown(1)', 'unselect current row')
173 
174 Sheet.addCommand('gt', 'stoggle-rows', 'toggle(rows)', 'toggle selection of all rows')
175 Sheet.addCommand('gs', 'select-rows', 'select(rows)', 'select all rows')
176 Sheet.addCommand('gu', 'unselect-rows', 'clearSelected()', 'unselect all rows')
177 
178 Sheet.addCommand('zt', 'stoggle-before', 'toggle(rows[:cursorRowIndex])', 'toggle selection of rows from top to cursor')
179 Sheet.addCommand('zs', 'select-before', 'select(rows[:cursorRowIndex])', 'select all rows from top to cursor')
180 Sheet.addCommand('zu', 'unselect-before', 'unselect(rows[:cursorRowIndex])', 'unselect all rows from top to cursor')
181 Sheet.addCommand('gzt', 'stoggle-after', 'toggle(rows[cursorRowIndex:])', 'toggle selection of all rows from cursor to bottom')
182 Sheet.addCommand('gzs', 'select-after', 'select(rows[cursorRowIndex:])', 'select all rows from cursor to bottom')
183 Sheet.addCommand('gzu', 'unselect-after', 'unselect(rows[cursorRowIndex:])', 'unselect all rows from cursor to bottom')
184 
185 Sheet.addCommand('|', 'select-col-regex', 'selectByIdx(searchInputRegex("select", columns="cursorCol"))', 'select rows matching regex in current column')
186 Sheet.addCommand('\\', 'unselect-col-regex', 'unselectByIdx(searchInputRegex("unselect", columns="cursorCol"))', 'unselect rows matching regex in current column')
187 Sheet.addCommand('g|', 'select-cols-regex', 'selectByIdx(searchInputRegex("select", columns="visibleCols"))', 'select rows matching regex in any visible column')
188 Sheet.addCommand('g\\', 'unselect-cols-regex', 'unselectByIdx(searchInputRegex("unselect", columns="visibleCols"))', 'unselect rows matching regex in any visible column')
189 
190 Sheet.addCommand(',', 'select-equal-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorDisplay: c.getDisplayValue(r) == v), progress=False)', 'select rows matching current cell in current column')
191 Sheet.addCommand('g,', 'select-equal-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getDisplayValue(r) == c.getDisplayValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')
192 Sheet.addCommand('z,', 'select-exact-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorTypedValue: c.getTypedValue(r) == v), progress=False)', 'select rows matching current cell in current column')
193 Sheet.addCommand('gz,', 'select-exact-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getTypedValue(r) == c.getTypedValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')
194 
195 Sheet.addCommand('z|', 'select-expr', 'expr=inputExpr("select by expr: "); select(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'select rows matching Python expression in any visible column')
196 Sheet.addCommand('z\\', 'unselect-expr', 'expr=inputExpr("unselect by expr: "); unselect(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'unselect rows matching Python expression in any visible column')
197 
198 Sheet.addCommand(None, 'select-error-col', 'select(gatherBy(lambda r,c=cursorCol: c.isError(r)), progress=False)', 'select rows with errors in current column')
199 Sheet.addCommand(None, 'select-error', 'select(gatherBy(lambda r,vcols=visibleCols: isinstance(r, TypedExceptionWrapper) or any([c.isError(r) for c in vcols])), progress=False)', 'select rows with errors in any column')
200 
201 vd.addMenuItems('''
202     Row > Select > current row > select-row
203     Row > Select > all rows > select-rows
204     Row > Select > from top > select-before
205     Row > Select > to bottom > select-after
206     Row > Select > by Python expr > select-expr
207     Row > Select > equal to current cell > select-equal-cell
208     Row > Select > equal to current row > select-equal-row
209     Row > Select > errors > current column > select-error-col
210     Row > Select > errors > any column > select-error
211     Row > Unselect > current row > unselect-row
212     Row > Unselect > all rows > unselect-rows
213     Row > Unselect > from top > unselect-before
214     Row > Unselect > to bottom > unselect-after
215     Row > Unselect > by Python expr > unselect-expr
216     Row > Toggle select > current row > stoggle-row
217     Row > Toggle select > all rows > stoggle-rows
218     Row > Toggle select > from top > stoggle-before
219     Row > Toggle select > to bottom > stoggle-after
220 ''')
221
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/visidata/selection.py b/visidata/selection.py
--- a/visidata/selection.py
+++ b/visidata/selection.py
@@ -23,8 +23,10 @@
 def toggle(self, rows):
     'Toggle selection of given *rows*.  Async.'
     self.addUndoSelection()
-    for r in Progress(rows, 'toggling', total=len(self.rows)):
-        if not self.unselectRow(r):
+    for r in Progress(rows, 'toggling', total=len(rows)):
+        if self.isSelected(r):  #1671
+            self.unselectRow(r)
+        else:
             self.selectRow(r)
 
 
@@ -39,7 +41,9 @@
 def toggle_row(self, row):
     'Toggle selection of given *row*.'
     self.addUndoSelection()
-    if not self.unselectRow(row):
+    if self.isSelected(row):
+        self.unselectRow(row)
+    else:
         self.selectRow(row)

{"golden_diff": "diff --git a/visidata/selection.py b/visidata/selection.py\n--- a/visidata/selection.py\n+++ b/visidata/selection.py\n@@ -23,8 +23,10 @@\n def toggle(self, rows):\n     'Toggle selection of given *rows*.  Async.'\n     self.addUndoSelection()\n-    for r in Progress(rows, 'toggling', total=len(self.rows)):\n-        if not self.unselectRow(r):\n+    for r in Progress(rows, 'toggling', total=len(rows)):\n+        if self.isSelected(r):  #1671\n+            self.unselectRow(r)\n+        else:\n             self.selectRow(r)\n \n \n@@ -39,7 +41,9 @@\n def toggle_row(self, row):\n     'Toggle selection of given *row*.'\n     self.addUndoSelection()\n-    if not self.unselectRow(row):\n+    if self.isSelected(row):\n+        self.unselectRow(row)\n+    else:\n         self.selectRow(row)\n", "issue": "[freqtbl] toggling rows selects/unselects an inconsistent number of rows\n**Small description**\r\nIn a FreqTableSheet where its source sheet has many rows, when rows are toggled, the number of rows that is selected/unselected on the source sheet is inconsistent.\r\n\r\n**Expected result**\r\nEvery time the toggle is performed, the number of rows selected/unselected on the source sheet should be the same.\r\n\r\n**Actual result with screenshot**\r\nThe number of rows selected on the source sheet is different every time. Sometimes, the number of unselected rows is negative.\r\nhttps://asciinema.org/a/ksxBTGy0E1tGh5unX2Gsu0eeR\r\n\r\n**Steps to reproduce with sample data and a .vd**\r\n`python -c \"n = 200_000; s = 'line\\n'; print(f'{s*n}',end='')\" |vd`\r\nhit `F` and then repeatedly hit `t` (or `gt` or `gzt` or, in a sheet with more than one kind of cell, `zt`)\r\n\r\n**Additional context**\r\nVisiData v2.10.2\r\n\r\nWorkaround:\r\nUse `s` and `u` to select/unselect rows instead of toggling.\r\n\r\nThere is a simple fix for the inconsistent behavior, by adding vd.sync() to selectRow() and unselectRow().\r\nhttps://github.com/midichef/visidata/commit/52d8596ed01fed17bca8b5215130a77801f1e3b3\r\nHowever, I'm unsure if this is the correct solution for 2 reasons.\r\nFirst, I don't have a good understanding of how threading works in the visidata codebase.\r\nSecond, it's not clear to me how FreqTableSheet toggling should behave. Suppose that some rows on the source sheet are already selected, but not all of them. The way it works now is that after a toggle, all matching rows in the source sheet will end up in the same state: either all are selected, or all are unselected. But arguably the effect should be to toggle the selected status of all the matching rows in the source sheet, leaving some selected and some unselected.\n", "before_files": [{"content": "from copy import copy\nfrom visidata import vd, Sheet, Progress, asyncthread, options, rotateRange, Fanout, undoAttrCopyFunc, RowColorizer\n\nvd.option('bulk_select_clear', False, 'clear selected rows before new bulk selections', replay=True)\nvd.option('some_selected_rows', False, 'if no rows selected, if True, someSelectedRows returns all rows; if False, fails')\n\nSheet.init('_selectedRows', dict)  # rowid(row) -> row\n\nvd.rowNoters.append(\n        lambda sheet, row: sheet.isSelected(row) and sheet.options.disp_selected_note\n)\nSheet.colorizers.append( RowColorizer(2, 'color_selected_row', lambda s,c,r,v:\n    r is not None and s.isSelected(r))\n)\n\n@Sheet.api\ndef isSelected(self, row):\n    'Return True if *row* is selected.'\n    return self.rowid(row) in self._selectedRows\n\n@Sheet.api\n@asyncthread\ndef toggle(self, rows):\n    'Toggle selection of given *rows*.  Async.'\n    self.addUndoSelection()\n    for r in Progress(rows, 'toggling', total=len(self.rows)):\n        if not self.unselectRow(r):\n            self.selectRow(r)\n\n\n@Sheet.api\ndef select_row(self, row):\n    'Add single *row* to set of selected rows.'\n    self.addUndoSelection()\n    self.selectRow(row)\n\n\n@Sheet.api\ndef toggle_row(self, row):\n    'Toggle selection of given *row*.'\n    self.addUndoSelection()\n    if not self.unselectRow(row):\n        self.selectRow(row)\n\n\n@Sheet.api\ndef unselect_row(self, row):\n    'Remove single *row* from set of selected rows.'\n    self.addUndoSelection()\n    self.unselectRow(row) or vd.warning('row not selected')\n\n\n@Sheet.api\ndef selectRow(self, row):\n    'Add *row* to set of selected rows.  May be called multiple times in one command.  Overridable.'\n    self._selectedRows[self.rowid(row)] = row\n\n\n@Sheet.api\ndef unselectRow(self, row):\n    'Remove *row* from set of selected rows.  Return True if row was previously selected.  Overridable.'\n    if self.rowid(row) in self._selectedRows:\n        del self._selectedRows[self.rowid(row)]\n        return True\n    else:\n        return False\n\n@Sheet.api\ndef clearSelected(self):\n    'Clear set of selected rows, without calling ``unselectRow`` for each one.'\n    self.addUndoSelection()\n    self._selectedRows.clear()\n\n@Sheet.api\n@asyncthread\ndef select(self, rows, status=True, progress=True):\n    \"Add *rows* to set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False.\"\n    self.addUndoSelection()\n    before = self.nSelectedRows\n    if self.options.bulk_select_clear:\n        self.clearSelected()\n    for r in (Progress(rows, 'selecting') if progress else rows):\n        self.selectRow(r)\n    if status:\n        if options.bulk_select_clear:\n            msg = 'selected %s %s%s' % (self.nSelectedRows, self.rowtype, ' instead' if before > 0 else '')\n        else:\n            msg = 'selected %s%s %s' % (self.nSelectedRows-before, ' more' if before > 0 else '', self.rowtype)\n        vd.status(msg)\n\n@Sheet.api\n@asyncthread\ndef unselect(self, rows, status=True, progress=True):\n    \"Remove *rows* from set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False.\"\n    self.addUndoSelection()\n    before = self.nSelectedRows\n    for r in (Progress(rows, 'unselecting') if progress else rows):\n        self.unselectRow(r)\n    if status:\n        vd.status('unselected %s/%s %s' % (before-self.nSelectedRows, before, self.rowtype))\n\n@Sheet.api\ndef selectByIdx(self, rowIdxs):\n    'Add rows indicated by row indexes in *rowIdxs* to set of selected rows.  Async.'\n    self.select((self.rows[i] for i in rowIdxs), progress=False)\n\n@Sheet.api\ndef unselectByIdx(self, rowIdxs):\n    'Remove rows indicated by row indexes in *rowIdxs* from set of selected rows.  Async.'\n    self.unselect((self.rows[i] for i in rowIdxs), progress=False)\n\n@Sheet.api\ndef gatherBy(self, func, gerund='gathering'):\n    'Generate rows for which ``func(row)`` returns True, starting from the cursor.'\n    for i in Progress(rotateRange(self.nRows, self.cursorRowIndex-1), total=self.nRows, gerund=gerund):\n        try:\n            r = self.rows[i]\n            if func(r):\n                yield r\n        except Exception as e:\n            vd.exceptionCaught(e, status=False)\n\n@Sheet.property\ndef selectedRows(self):\n    'List of selected rows in sheet order.'\n    if self.nSelectedRows <= 1:\n        return Fanout(self._selectedRows.values())\n    return Fanout((r for r in self.rows if self.rowid(r) in self._selectedRows))\n\n@Sheet.property\ndef onlySelectedRows(self):\n    'List of selected rows in sheet order.  Fail if no rows are selected.'\n    if self.nSelectedRows == 0:\n        vd.fail('no rows selected')\n    return self.selectedRows\n\n@Sheet.property\ndef someSelectedRows(self):\n    '''Return a list of rows:\n        (a) in batch mode, always return selectedRows\n        (b) in interactive mode, if options.some_selected_rows is True, return selectedRows or all rows if none selected\n        (c) in interactive mode, if options.some_selected_rows is False, return selectedRows or fail if none selected'''\n    if options.batch:\n        return self.selectedRows\n    if options.some_selected_rows:\n        return self.selectedRows or self.rows\n    return self.onlySelectedRows\n\n@Sheet.property\ndef nSelectedRows(self):\n    'Number of selected rows.'\n    return len(self._selectedRows)\n\n@Sheet.api\n@asyncthread\ndef deleteSelected(self):\n    'Delete all selected rows.  Async.'\n    ndeleted = self.deleteBy(self.isSelected)\n    nselected = self.nSelectedRows\n    self.clearSelected()\n    if ndeleted != nselected:\n        vd.warning(f'deleted {ndeleted}, expected {nselected}')\n\n\n@Sheet.api\ndef addUndoSelection(sheet):\n    vd.addUndo(undoAttrCopyFunc([sheet], '_selectedRows'))\n\n\nSheet.addCommand('t', 'stoggle-row', 'toggle_row(cursorRow); cursorDown(1)', 'toggle selection of current row')\nSheet.addCommand('s', 'select-row', 'select_row(cursorRow); cursorDown(1)', 'select current row')\nSheet.addCommand('u', 'unselect-row', 'unselect_row(cursorRow); cursorDown(1)', 'unselect current row')\n\nSheet.addCommand('gt', 'stoggle-rows', 'toggle(rows)', 'toggle selection of all rows')\nSheet.addCommand('gs', 'select-rows', 'select(rows)', 'select all rows')\nSheet.addCommand('gu', 'unselect-rows', 'clearSelected()', 'unselect all rows')\n\nSheet.addCommand('zt', 'stoggle-before', 'toggle(rows[:cursorRowIndex])', 'toggle selection of rows from top to cursor')\nSheet.addCommand('zs', 'select-before', 'select(rows[:cursorRowIndex])', 'select all rows from top to cursor')\nSheet.addCommand('zu', 'unselect-before', 'unselect(rows[:cursorRowIndex])', 'unselect all rows from top to cursor')\nSheet.addCommand('gzt', 'stoggle-after', 'toggle(rows[cursorRowIndex:])', 'toggle selection of all rows from cursor to bottom')\nSheet.addCommand('gzs', 'select-after', 'select(rows[cursorRowIndex:])', 'select all rows from cursor to bottom')\nSheet.addCommand('gzu', 'unselect-after', 'unselect(rows[cursorRowIndex:])', 'unselect all rows from cursor to bottom')\n\nSheet.addCommand('|', 'select-col-regex', 'selectByIdx(searchInputRegex(\"select\", columns=\"cursorCol\"))', 'select rows matching regex in current column')\nSheet.addCommand('\\\\', 'unselect-col-regex', 'unselectByIdx(searchInputRegex(\"unselect\", columns=\"cursorCol\"))', 'unselect rows matching regex in current column')\nSheet.addCommand('g|', 'select-cols-regex', 'selectByIdx(searchInputRegex(\"select\", columns=\"visibleCols\"))', 'select rows matching regex in any visible column')\nSheet.addCommand('g\\\\', 'unselect-cols-regex', 'unselectByIdx(searchInputRegex(\"unselect\", columns=\"visibleCols\"))', 'unselect rows matching regex in any visible column')\n\nSheet.addCommand(',', 'select-equal-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorDisplay: c.getDisplayValue(r) == v), progress=False)', 'select rows matching current cell in current column')\nSheet.addCommand('g,', 'select-equal-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getDisplayValue(r) == c.getDisplayValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')\nSheet.addCommand('z,', 'select-exact-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorTypedValue: c.getTypedValue(r) == v), progress=False)', 'select rows matching current cell in current column')\nSheet.addCommand('gz,', 'select-exact-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getTypedValue(r) == c.getTypedValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')\n\nSheet.addCommand('z|', 'select-expr', 'expr=inputExpr(\"select by expr: \"); select(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'select rows matching Python expression in any visible column')\nSheet.addCommand('z\\\\', 'unselect-expr', 'expr=inputExpr(\"unselect by expr: \"); unselect(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'unselect rows matching Python expression in any visible column')\n\nSheet.addCommand(None, 'select-error-col', 'select(gatherBy(lambda r,c=cursorCol: c.isError(r)), progress=False)', 'select rows with errors in current column')\nSheet.addCommand(None, 'select-error', 'select(gatherBy(lambda r,vcols=visibleCols: isinstance(r, TypedExceptionWrapper) or any([c.isError(r) for c in vcols])), progress=False)', 'select rows with errors in any column')\n\nvd.addMenuItems('''\n    Row > Select > current row > select-row\n    Row > Select > all rows > select-rows\n    Row > Select > from top > select-before\n    Row > Select > to bottom > select-after\n    Row > Select > by Python expr > select-expr\n    Row > Select > equal to current cell > select-equal-cell\n    Row > Select > equal to current row > select-equal-row\n    Row > Select > errors > current column > select-error-col\n    Row > Select > errors > any column > select-error\n    Row > Unselect > current row > unselect-row\n    Row > Unselect > all rows > unselect-rows\n    Row > Unselect > from top > unselect-before\n    Row > Unselect > to bottom > unselect-after\n    Row > Unselect > by Python expr > unselect-expr\n    Row > Toggle select > current row > stoggle-row\n    Row > Toggle select > all rows > stoggle-rows\n    Row > Toggle select > from top > stoggle-before\n    Row > Toggle select > to bottom > stoggle-after\n''')\n", "path": "visidata/selection.py"}], "after_files": [{"content": "from copy import copy\nfrom visidata import vd, Sheet, Progress, asyncthread, options, rotateRange, Fanout, undoAttrCopyFunc, RowColorizer\n\nvd.option('bulk_select_clear', False, 'clear selected rows before new bulk selections', replay=True)\nvd.option('some_selected_rows', False, 'if no rows selected, if True, someSelectedRows returns all rows; if False, fails')\n\nSheet.init('_selectedRows', dict)  # rowid(row) -> row\n\nvd.rowNoters.append(\n        lambda sheet, row: sheet.isSelected(row) and sheet.options.disp_selected_note\n)\nSheet.colorizers.append( RowColorizer(2, 'color_selected_row', lambda s,c,r,v:\n    r is not None and s.isSelected(r))\n)\n\n@Sheet.api\ndef isSelected(self, row):\n    'Return True if *row* is selected.'\n    return self.rowid(row) in self._selectedRows\n\n@Sheet.api\n@asyncthread\ndef toggle(self, rows):\n    'Toggle selection of given *rows*.  Async.'\n    self.addUndoSelection()\n    for r in Progress(rows, 'toggling', total=len(rows)):\n        if self.isSelected(r):  #1671\n            self.unselectRow(r)\n        else:\n            self.selectRow(r)\n\n\n@Sheet.api\ndef select_row(self, row):\n    'Add single *row* to set of selected rows.'\n    self.addUndoSelection()\n    self.selectRow(row)\n\n\n@Sheet.api\ndef toggle_row(self, row):\n    'Toggle selection of given *row*.'\n    self.addUndoSelection()\n    if self.isSelected(row):\n        self.unselectRow(row)\n    else:\n        self.selectRow(row)\n\n\n@Sheet.api\ndef unselect_row(self, row):\n    'Remove single *row* from set of selected rows.'\n    self.addUndoSelection()\n    self.unselectRow(row) or vd.warning('row not selected')\n\n\n@Sheet.api\ndef selectRow(self, row):\n    'Add *row* to set of selected rows.  May be called multiple times in one command.  Overridable.'\n    self._selectedRows[self.rowid(row)] = row\n\n\n@Sheet.api\ndef unselectRow(self, row):\n    'Remove *row* from set of selected rows.  Return True if row was previously selected.  Overridable.'\n    if self.rowid(row) in self._selectedRows:\n        del self._selectedRows[self.rowid(row)]\n        return True\n    else:\n        return False\n\n@Sheet.api\ndef clearSelected(self):\n    'Clear set of selected rows, without calling ``unselectRow`` for each one.'\n    self.addUndoSelection()\n    self._selectedRows.clear()\n\n@Sheet.api\n@asyncthread\ndef select(self, rows, status=True, progress=True):\n    \"Add *rows* to set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False.\"\n    self.addUndoSelection()\n    before = self.nSelectedRows\n    if self.options.bulk_select_clear:\n        self.clearSelected()\n    for r in (Progress(rows, 'selecting') if progress else rows):\n        self.selectRow(r)\n    if status:\n        if options.bulk_select_clear:\n            msg = 'selected %s %s%s' % (self.nSelectedRows, self.rowtype, ' instead' if before > 0 else '')\n        else:\n            msg = 'selected %s%s %s' % (self.nSelectedRows-before, ' more' if before > 0 else '', self.rowtype)\n        vd.status(msg)\n\n@Sheet.api\n@asyncthread\ndef unselect(self, rows, status=True, progress=True):\n    \"Remove *rows* from set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False.\"\n    self.addUndoSelection()\n    before = self.nSelectedRows\n    for r in (Progress(rows, 'unselecting') if progress else rows):\n        self.unselectRow(r)\n    if status:\n        vd.status('unselected %s/%s %s' % (before-self.nSelectedRows, before, self.rowtype))\n\n@Sheet.api\ndef selectByIdx(self, rowIdxs):\n    'Add rows indicated by row indexes in *rowIdxs* to set of selected rows.  Async.'\n    self.select((self.rows[i] for i in rowIdxs), progress=False)\n\n@Sheet.api\ndef unselectByIdx(self, rowIdxs):\n    'Remove rows indicated by row indexes in *rowIdxs* from set of selected rows.  Async.'\n    self.unselect((self.rows[i] for i in rowIdxs), progress=False)\n\n@Sheet.api\ndef gatherBy(self, func, gerund='gathering'):\n    'Generate rows for which ``func(row)`` returns True, starting from the cursor.'\n    for i in Progress(rotateRange(self.nRows, self.cursorRowIndex-1), total=self.nRows, gerund=gerund):\n        try:\n            r = self.rows[i]\n            if func(r):\n                yield r\n        except Exception as e:\n            vd.exceptionCaught(e, status=False)\n\n@Sheet.property\ndef selectedRows(self):\n    'List of selected rows in sheet order.'\n    if self.nSelectedRows <= 1:\n        return Fanout(self._selectedRows.values())\n    return Fanout((r for r in self.rows if self.rowid(r) in self._selectedRows))\n\n@Sheet.property\ndef onlySelectedRows(self):\n    'List of selected rows in sheet order.  Fail if no rows are selected.'\n    if self.nSelectedRows == 0:\n        vd.fail('no rows selected')\n    return self.selectedRows\n\n@Sheet.property\ndef someSelectedRows(self):\n    '''Return a list of rows:\n        (a) in batch mode, always return selectedRows\n        (b) in interactive mode, if options.some_selected_rows is True, return selectedRows or all rows if none selected\n        (c) in interactive mode, if options.some_selected_rows is False, return selectedRows or fail if none selected'''\n    if options.batch:\n        return self.selectedRows\n    if options.some_selected_rows:\n        return self.selectedRows or self.rows\n    return self.onlySelectedRows\n\n@Sheet.property\ndef nSelectedRows(self):\n    'Number of selected rows.'\n    return len(self._selectedRows)\n\n@Sheet.api\n@asyncthread\ndef deleteSelected(self):\n    'Delete all selected rows.  Async.'\n    ndeleted = self.deleteBy(self.isSelected)\n    nselected = self.nSelectedRows\n    self.clearSelected()\n    if ndeleted != nselected:\n        vd.warning(f'deleted {ndeleted}, expected {nselected}')\n\n\n@Sheet.api\ndef addUndoSelection(sheet):\n    vd.addUndo(undoAttrCopyFunc([sheet], '_selectedRows'))\n\n\nSheet.addCommand('t', 'stoggle-row', 'toggle_row(cursorRow); cursorDown(1)', 'toggle selection of current row')\nSheet.addCommand('s', 'select-row', 'select_row(cursorRow); cursorDown(1)', 'select current row')\nSheet.addCommand('u', 'unselect-row', 'unselect_row(cursorRow); cursorDown(1)', 'unselect current row')\n\nSheet.addCommand('gt', 'stoggle-rows', 'toggle(rows)', 'toggle selection of all rows')\nSheet.addCommand('gs', 'select-rows', 'select(rows)', 'select all rows')\nSheet.addCommand('gu', 'unselect-rows', 'clearSelected()', 'unselect all rows')\n\nSheet.addCommand('zt', 'stoggle-before', 'toggle(rows[:cursorRowIndex])', 'toggle selection of rows from top to cursor')\nSheet.addCommand('zs', 'select-before', 'select(rows[:cursorRowIndex])', 'select all rows from top to cursor')\nSheet.addCommand('zu', 'unselect-before', 'unselect(rows[:cursorRowIndex])', 'unselect all rows from top to cursor')\nSheet.addCommand('gzt', 'stoggle-after', 'toggle(rows[cursorRowIndex:])', 'toggle selection of all rows from cursor to bottom')\nSheet.addCommand('gzs', 'select-after', 'select(rows[cursorRowIndex:])', 'select all rows from cursor to bottom')\nSheet.addCommand('gzu', 'unselect-after', 'unselect(rows[cursorRowIndex:])', 'unselect all rows from cursor to bottom')\n\nSheet.addCommand('|', 'select-col-regex', 'selectByIdx(searchInputRegex(\"select\", columns=\"cursorCol\"))', 'select rows matching regex in current column')\nSheet.addCommand('\\\\', 'unselect-col-regex', 'unselectByIdx(searchInputRegex(\"unselect\", columns=\"cursorCol\"))', 'unselect rows matching regex in current column')\nSheet.addCommand('g|', 'select-cols-regex', 'selectByIdx(searchInputRegex(\"select\", columns=\"visibleCols\"))', 'select rows matching regex in any visible column')\nSheet.addCommand('g\\\\', 'unselect-cols-regex', 'unselectByIdx(searchInputRegex(\"unselect\", columns=\"visibleCols\"))', 'unselect rows matching regex in any visible column')\n\nSheet.addCommand(',', 'select-equal-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorDisplay: c.getDisplayValue(r) == v), progress=False)', 'select rows matching current cell in current column')\nSheet.addCommand('g,', 'select-equal-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getDisplayValue(r) == c.getDisplayValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')\nSheet.addCommand('z,', 'select-exact-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorTypedValue: c.getTypedValue(r) == v), progress=False)', 'select rows matching current cell in current column')\nSheet.addCommand('gz,', 'select-exact-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getTypedValue(r) == c.getTypedValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')\n\nSheet.addCommand('z|', 'select-expr', 'expr=inputExpr(\"select by expr: \"); select(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'select rows matching Python expression in any visible column')\nSheet.addCommand('z\\\\', 'unselect-expr', 'expr=inputExpr(\"unselect by expr: \"); unselect(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'unselect rows matching Python expression in any visible column')\n\nSheet.addCommand(None, 'select-error-col', 'select(gatherBy(lambda r,c=cursorCol: c.isError(r)), progress=False)', 'select rows with errors in current column')\nSheet.addCommand(None, 'select-error', 'select(gatherBy(lambda r,vcols=visibleCols: isinstance(r, TypedExceptionWrapper) or any([c.isError(r) for c in vcols])), progress=False)', 'select rows with errors in any column')\n\nvd.addMenuItems('''\n    Row > Select > current row > select-row\n    Row > Select > all rows > select-rows\n    Row > Select > from top > select-before\n    Row > Select > to bottom > select-after\n    Row > Select > by Python expr > select-expr\n    Row > Select > equal to current cell > select-equal-cell\n    Row > Select > equal to current row > select-equal-row\n    Row > Select > errors > current column > select-error-col\n    Row > Select > errors > any column > select-error\n    Row > Unselect > current row > unselect-row\n    Row > Unselect > all rows > unselect-rows\n    Row > Unselect > from top > unselect-before\n    Row > Unselect > to bottom > unselect-after\n    Row > Unselect > by Python expr > unselect-expr\n    Row > Toggle select > current row > stoggle-row\n    Row > Toggle select > all rows > stoggle-rows\n    Row > Toggle select > from top > stoggle-before\n    Row > Toggle select > to bottom > stoggle-after\n''')\n", "path": "visidata/selection.py"}]}