Commit Graph

364 Commits

Author SHA1 Message Date
RumovZ
11bdeb9ca4 Add column logic on backend 2021-04-06 19:46:12 +02:00
Damien Elmes
6e954e82a5 current deck change is now undoable
- make sure we set flag in changes when config var changed
- move current deck get/set into backend
- set_config() now returns a bool indicating whether a change was
made, so other operations can be gated off it
- active decks generation is deferred until sched.reset()
2021-04-06 21:52:06 +10:00
Damien Elmes
84fe309583 update scheduling ops
- migrate to CollectionOp()
- return actual change count when suspending/burying
- add helper to convert vec to vec of newtype
2021-04-06 16:38:42 +10:00
Damien Elmes
2de8cc1a94 update note ops
remove_note() now returns the count of removed cards, allowing us
to unify the tooltip between browser and review screen

I've left the old translation in - we'll need to write a script at
one point that gathers all references to translations in the code,
and shows ones that are unused.
2021-04-06 14:56:36 +10:00
Damien Elmes
3f62f54f14 more perform_op() tweaks
- pass the handler directly
- reviewer special-cases for flags and notes are now applied at
call site
- drop the kind attribute on OpChanges which is not needed
2021-04-06 10:14:11 +10:00
Damien Elmes
ebf7cc61d4 switch next_day_at to a newtype 2021-04-05 16:17:26 +10:00
Damien Elmes
a18bb2af12 add booleans for various screens to OpChanges
The backend knows exactly which op has executed, and it saves us having
to re-implement this logic on each client.

Fixes the browser table refreshing when toggling decks.
2021-04-05 14:28:56 +10:00
Damien Elmes
996d9f9bbc undo support for tag collapse; expand->collapse for consistency w/ decks 2021-04-05 11:47:12 +10:00
Damien Elmes
2168dfe63d add routine to set deck collapse state
Updating a deck via protobuf is now exposed on the backend, but not
currently on the frontend - I suspect we'll be better off writing
separate routines for the actions we need instead, and we get a better
undo description for free.

This is currently causing an ugly redraw in the browse screen, which
will need fixing.
2021-04-05 11:19:04 +10:00
Damien Elmes
42a4d11416 embed deck config and expose to frontend 2021-04-04 22:52:53 +10:00
Damien Elmes
1a4c4373d2 expose read-only access to new notetype objects 2021-04-04 20:45:37 +10:00
Damien Elmes
c60b88cd2f expose read-only access to new deck objects 2021-04-04 20:39:56 +10:00
Damien Elmes
8e16c94b96 recognize select statements with a leading newline from old stats screen 2021-04-03 23:23:33 +10:00
Damien Elmes
41c5a25dc8 simplify errors
- use a flat enum instead of oneof messages, most of which were empty
- tidy up the Python side
2021-04-03 16:06:46 +10:00
Damien Elmes
fe6888f9a4 rename backend/err.rs -> error.rs 2021-04-03 14:47:52 +10:00
Damien Elmes
f666f15b63 use perform_op() for undo()
Instead of manually updating the UI after undoing, we just rely
on the same change notification infrastructure regular operations
use.
2021-04-03 14:38:49 +10:00
Damien Elmes
e73359510d move filtered deck labels to backend
- use strum to generate an iterator for the protobuf enum so we don't
forget to add new labels if extending in the future
- no add-ons appear to be using dynOrderLabels(), so it has been removed

@RumovZ perhaps a similar approach might work for listing the available
browser columns as well?
2021-04-01 23:53:38 +10:00
Damien Elmes
ac1b9fadde merge the filtered deck errors into an enum
Fixes the wrong message being shown when trying to move cards to a
filtered deck
2021-04-01 22:30:00 +10:00
Damien Elmes
7a29d987c4 convert Json and Proto errors to tuple 2021-04-01 17:45:12 +10:00
Damien Elmes
af37164fba move sync/network errors into separate file 2021-04-01 17:02:54 +10:00
Damien Elmes
8363fcf2a8 move DB error into separate file; add InvalidRegex error 2021-04-01 16:28:23 +10:00
Damien Elmes
094e4ad461 crate::err -> crate::error 2021-04-01 16:07:13 +10:00
RumovZ
8779fb5ede Add note interval column 2021-03-30 23:44:35 +02:00
RumovZ
1ad91a5312 Add note due column 2021-03-30 21:39:15 +02:00
RumovZ
da156cd759 Rename browser_rows to browser_table
Reflects the addition of column handling.
2021-03-30 12:08:35 +02:00
RumovZ
ffe77b1291 Add browser column enum for backend 2021-03-30 11:59:52 +02:00
RumovZ
4933b922f7 Add note lapses column 2021-03-29 16:06:15 +02:00
RumovZ
32e538d0db Add note reps column 2021-03-29 15:52:02 +02:00
Damien Elmes
3383f1742a rename BrowserCardState
Use a more verbose name, and use 'note' rather than 'card', so we
can rely on the default of False
2021-03-29 17:12:45 +10:00
RumovZ
0d8b1c9d0b squash merge browser refactor
Closes #1100
2021-03-29 16:14:54 +10:00
Damien Elmes
cfac40febc switch NoteType to Notetype
When used as a variable, we were typically calling it a 'notetype', not
a 'note type'.
2021-03-27 22:03:19 +10:00
Damien Elmes
9f4a06abee ID -> Id in protobuf and Python
follow-up to dc81a7fed0
2021-03-27 21:38:20 +10:00
Damien Elmes
fcfa6bab4e update to latest Rust 2021-03-27 20:49:10 +10:00
Damien Elmes
1055acb9f2 fix more issues uncovered by the latest clippy 2021-03-27 20:25:34 +10:00
Damien Elmes
dc81a7fed0 use mixed case for abbreviations in Rust code
So, this is fun. Apparently "DeckId" is considered preferable to the
"DeckID" were were using until now, and the latest clippy will start
warning about it. We could of course disable the warning, but probably
better to bite the bullet and switch to the naming that's generally
considered best.
2021-03-27 19:53:33 +10:00
Damien Elmes
e4002d7a5e update to latest Fluent 2021-03-27 13:24:11 +10:00
Damien Elmes
3433c02242 i18n->tr in rslib/ to match Python/TS code 2021-03-27 12:09:51 +10:00
Damien Elmes
d6b9cc4d9b drop the legacy enum from rslib, and pass separate module/message idx 2021-03-27 11:56:31 +10:00
Damien Elmes
f485efce16 update remaining TR references in rslib 2021-03-27 11:18:34 +10:00
Damien Elmes
c45ab78b73 update multi-arg TR references, where some needed reordering 2021-03-27 10:49:40 +10:00
Damien Elmes
698ae855d3 update 1 arg tr strings in rslib 2021-03-27 10:39:53 +10:00
Damien Elmes
dc5fdd30d4 update no-arg TR references in rslib/ 2021-03-26 23:16:08 +10:00
Damien Elmes
cca1d29cc8 fix warning in latest Rust 2021-03-26 23:06:15 +10:00
Damien Elmes
b57e9be46f allow js to request specific i18n modules
Brings the payload on the congrats page with a non-English language
down from about 150k to 15k
2021-03-26 21:43:36 +10:00
Damien Elmes
9aece2a7b8 rework translation handling
Instead of generating a fluent.proto file with a giant enum, create
a .json file representing the translations that downstream consumers
can use for code generation.

This enables the generation of a separate method for each translation,
with a docstring that shows the actual text, and any required arguments
listed in the function signature.

The codebase is still using the old enum for now; updating it will need
to come in future commits, and the old enum will need to be kept
around, as add-ons are referencing it.

Other changes:

- move translation code into a separate crate
- store the translations on a per-file/module basis, which will allow
us to avoid sending 1000+ strings on each JS page load in the future
- drop the undocumented support for external .ftl files, that we weren't
using
- duplicate strings in translation files are now checked for at build
time
- fix i18n test failing when run outside Bazel
- drop slog dependency in i18n module
2021-03-26 09:41:32 +10:00
Damien Elmes
d382b33585 rework filtered deck screen & search errors
- Filtered deck creation now happens as an atomic operation, and is
undoable.
- The logic for initial search text, normalizing searches and so on
has been pushed into the backend.
- Use protobuf to pass the filtered deck to the updated dialog, so
we don't need to deal with untyped JSON.
- Change the "revise your search?" prompt to be a simple info box -
user has access to cancel and build buttons, and doesn't need a separate
prompt. Tweak the wording so the 'show excluded' button should be more
obvious.
- Filtered decks have a time appended to them instead of a number,
primarily because it's easier to implement. No objections going back to
the old behaviour if someone wants to contribute a clean patch.
The standard de-duplication will happen if two decks are created in the
same minute with the same name.
- Tweak the default sort order, and start with two searches. The UI
will still hide the second search by default, but by starting with two,
the frontend doesn't need logic for creating the starting text.
- Search errors now have their own error type, instead of using
InvalidInput, as that was intended mainly for bad API calls. The markdown
conversion is done when the error is converted from the backend, allowing
errors to printed as a string without any special handling by the calling
code.

TODO: when building a new filtered deck, update_active() is clobbering
the undo log when the overview is refreshed
2021-03-24 22:04:35 +10:00
Damien Elmes
12597e1094 support undo of filtered deck build/empty 2021-03-24 12:56:06 +10:00
Damien Elmes
ff0d346927 move browser_row method into search service
For want of a better place to put it. Also split the node conversion
into a separate file.
2021-03-23 18:55:05 +10:00
Damien Elmes
5fd79d9246
Merge pull request #1082 from RumovZ/backend-rows
Backend rows
2021-03-23 18:31:42 +10:00
Damien Elmes
01161c8ed2 use perform_op() for deck creation 2021-03-22 23:17:07 +10:00
Damien Elmes
6a11c0398c use perform_op() for deck drag&drop 2021-03-22 18:23:56 +10:00
Damien Elmes
7273996041 fix note importing detecting changes due to unicode differences
https://forums.ankiweb.net/t/python-checksum-rust-checksum/8195/16
2021-03-22 10:56:24 +10:00
RumovZ
04ae6f727b Rename browser_rows/RowColor to Color 2021-03-20 17:31:16 +01:00
RumovZ
7425aa6b58 Refactor search/browser.rs to browser_rows.rs 2021-03-20 17:20:49 +01:00
RumovZ
c68a6131e0 Add backend routine for browser rows 2021-03-20 12:02:51 +01:00
Damien Elmes
4c61c92806 speed up tag drag&drop and finish tag tidyup
approx 4x speedup when reparenting 10-15 tags and their children at once
2021-03-19 19:45:21 +10:00
Damien Elmes
9c2bff5b6d change bulk_update() into find_and_replace_tag()
Now behaves the same way as standard find&replace:
- Will match substrings
- Regexs can be used to match multiple items; we no longer split
input on spaces.
- The find&replace dialog has been updated to add tags to the field
list.
2021-03-19 19:45:21 +10:00
Damien Elmes
08895c58d9 introduce separate routine to remove tags from specific notes
We were (ab)using the bulk update routine to do deletions, but that
code was really intended to be used for finding&replacing, where an
exact match is not a requirement.
2021-03-19 19:45:21 +10:00
Damien Elmes
09076da937 make tag deletion undoable, and speed it up
- ~4x faster than before on tag tree with 30k notes
- remove the separate clear_tag() backend method
2021-03-19 19:45:21 +10:00
Damien Elmes
157b74b671 make tag renaming undoable, and speed it up
~3x speedup when renaming a tag that's on 25k notes
2021-03-19 19:45:21 +10:00
Damien Elmes
0331d8b588 make reposition undoable 2021-03-19 19:45:21 +10:00
Damien Elmes
de668441b5 clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-19 19:45:21 +10:00
Damien Elmes
6b0fe4b381 undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.

Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-19 19:45:21 +10:00
Damien Elmes
30c7cf1fdd fade out webview when pending updates; do some reviewer updates immediately
Issues that need fixing:
- when the editor saves the note with perform_op(), if it isn't modified,
no new undo entry is created, and perform_op then returns the changes
made by the previous operation instead
- the approach of fetching the last action in a subsequent backend
method is unsound, as another queued operation may sneak in first before
we have a chance to query the result - it would be better if it were
returned in a single atomic action
- redrawing the current card while editing is likely to make sound
autoplay annoyingly, and it has an unpleasant redraw. We may be better off
fading it out instead

Side note: the editor cursor moves to the start of the field when the
note is updated in another window - it might be nicer to have it move
the cursor to the end instead.
2021-03-19 19:45:21 +10:00
Damien Elmes
1e849316be more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.

I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.

Other changes:

- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-19 19:45:21 +10:00
Damien Elmes
7fab319dad derive reset scope from last undoable operation 2021-03-19 19:45:21 +10:00
Damien Elmes
90526c61cd move ops.rs out of undo/ 2021-03-19 19:45:21 +10:00
Damien Elmes
8fc43956c2 move collection mtime bump into backend
Fixes the following issue:
- some code directly modifies the database, causing modified_in_python
to be set to true
- an undoable operation is run, which calls autosave() at the end
- autosave() notices there's an undoable operation, and commits immediately
- because modified_in_python was true, col.mtime was bumped in Python
- that invalidated the undo queue, preventing the operation from being
undone
2021-03-19 19:45:21 +10:00
Damien Elmes
c1316bb65f 'set due date' now undoable 2021-03-12 14:50:31 +10:00
RumovZ
f4c2fe6485 Merge branch 'master' into sidebar-tools 2021-03-11 12:08:32 +01:00
Damien Elmes
984e2c2666 add a separate 'rename deck' method 2021-03-11 19:24:54 +10:00
RumovZ
f1dd010489 Remove deck remove prompt but show card count 2021-03-11 09:52:11 +01:00
RumovZ
186a0202ea Show tooltip instead of prompt for removing tags 2021-03-11 09:14:50 +01:00
Damien Elmes
4bd120cc4b split out remaining rpc methods
@david-allison-1 note this also changes the method index to start at
0 instead of 1
2021-03-11 17:04:32 +10:00
Damien Elmes
378b116977 split out stats, media and search 2021-03-11 16:16:40 +10:00
Damien Elmes
cd14987812 split out tags, deck config and card rendering 2021-03-11 16:05:06 +10:00
Damien Elmes
1b8d6c6e85 split out sync, notetypes and config code 2021-03-11 15:47:31 +10:00
Damien Elmes
5df684fa6b rework backend codegen to support multiple services; split out sched
Rust requires all methods of impl Trait to be in a single file, which
means we had a giant backend/mod.rs covering all exposed methods. By
using separate service definitions for the separate areas, and updating
the code generation, we can split it into more manageable chunks -
this commit starts with the scheduling code.

In the long run, we'll probably want to split up the protobuf file into
multiple files as well.

Also dropped want_release_gil() from rsbridge, and the associated method
enum. While it allows us to skip the thread save/restore and mutex unlock/
lock, it looks to only be buying about 2.5% extra performance in the
best case (tested with timeit+format_timespan), and the majority of
the backend methods deal with I/O, and thus were already releasing the
GIL.
2021-03-11 14:51:29 +10:00
RumovZ
e1db8e1da1 Borrow dids in remove_decks_and_child_decks 2021-03-10 15:56:54 +01:00
RumovZ
0c2ac4ba04 Merge branch 'master' into sidebar-tools 2021-03-10 10:34:36 +01:00
Damien Elmes
6b1dd9ee19 expand backend Preferences and make undoable
- moved 'default to current deck when adding' into prefs
- move some profile options into the collection config, so they're
undoable and will sync. There is (currently) no automatic migration
from the old profile settings, meaning users will need to set the
options again if they've customized them.
- tidy up preferences.py
- drop the deleteMedia option that was not exposed in the UI
2021-03-10 18:51:03 +10:00
Damien Elmes
d2f48e1663 move sync/err backend code into separate files 2021-03-10 11:53:27 +10:00
Damien Elmes
30865eae51 move progress and search backend code into separate files 2021-03-10 11:53:27 +10:00
Damien Elmes
ce243c2cae Simplify note adding and the deck/notetype choosers
The existing code was really difficult to reason about:

- The default notetype depended on the selected deck, and vice versa,
and this logic was buried in the deck and notetype choosing screens,
and models.py.
- Changes to the notetype were not passed back directly, but were fired
via a hook, which changed any screen in the app that had a notetype
selector.

It also wasn't great for performance, as the most recent deck and tags
were embedded in the notetype, which can be expensive to save and sync
for large notetypes.

To address these points:

- The current deck for a notetype, and notetype for a deck, are now
stored in separate config variables, instead of directly in the deck
or notetype. These are cheap to read and write, and we'll be able to
sync them individually in the future once config syncing is updated in
the future. I seem to recall some users not wanting the tag saving
behaviour, so I've dropped that for now, but if people end up missing
it, it would be simple to add as an extra auxiliary config variable.
- The logic for getting the starting deck and notetype has been moved
into the backend. It should be the same as the older Python code, with
one exception: when "change deck depending on notetype" is enabled in
the preferences, it will start with the current notetype ("curModel"),
instead of first trying to get a deck-specific notetype.
- ModelChooser has been duplicated into notetypechooser.py, and it
has been updated to solely be concerned with keeping track of a selected
notetype - it no longer alters global state.
2021-03-10 11:53:27 +10:00
Damien Elmes
545cb76018 native stringkey 2021-03-10 11:53:27 +10:00
Damien Elmes
1dea8babee use native boolkey instead of separate getters/setters
Makes it easier to add new config settings in the future, especially
if we don't need to export them via protobuf.
2021-03-10 11:53:27 +10:00
Damien Elmes
3d0ddc8539 make flag changes in the reviewer undoable
This splits update_card() into separate undoable/non-undoable ops
like the change to notes in b4396b94abdeba3347d30025c5c0240d991006c9

It means that actions get a blanket 'Update Card' description - in the
future we'll probably want to either add specific actions to the backend,
or allow an enum or string to be passed in to describe the op.

Other changes:
- card.flush() can no longer be used to add new cards. Card creation
is only supposed to be done in response to changes in a note's fields,
and this functionality was only exposed because the card generation
hadn't been migrated to the backend at that point. As far as I'm aware,
only Arthur's "copy notes" add-on used this functionality, and that should
be an easy fix - when the new note is added, the associated cards will
be generated, and they can then be retrieved with note.cards()
- tidy ups/PEP8
2021-03-10 11:53:27 +10:00
Damien Elmes
ecea2161e3 dispatch undo operations via enum instead of trait
To coalesce successive note edits into a single undo op we'll need to
be able to get the original Undoable type, which is awkward to do with
a trait object.
2021-03-10 11:53:27 +10:00
Damien Elmes
f1a1b0891e make mark toggling undoable
- note.flush() behaves like before, as otherwise actions or add-ons
that perform bulk flushing would end up creating an undo entry for
each note
- added col.update_note() to opt in to the new behaviour
- tidy up the names of some related routines
2021-03-10 11:53:27 +10:00
Damien Elmes
bec77fd420 undo support for bulk tag add/remove 2021-03-10 11:47:53 +10:00
Damien Elmes
49a1970399 note deletion undo; refactoring
- transact() now automatically clears card queues unless an op
opts-out (and currently only AnswerCard does). This means there's no
risk of forgetting to clear the queues in an operation, or when undoing/
redoing
- CollectionOp->UndoableOp
- clear queues when redoing "answer card", instead of clearing redo
when clearing queues
2021-03-10 11:47:53 +10:00
Damien Elmes
41779c1aad implement bury/suspend undo 2021-03-10 11:47:53 +10:00
Damien Elmes
b466f0ce90 rework undo
- use dataclasses for the review/checkpoint undo cases, instead of the
nasty ad-hoc list structure
- expose backend review undo to Python, and hook it into GUI
- redo is not currently exposed on the GUI, and the backend can only
cope with reviews done by the new scheduler at the moment
- the initial undo prototype code was bumping mtime/usn on undo, but
that was not ideal, as it was breaking the queue handling which expected
the mtime to match. The original rationale for bumping mtime/usn was
to avoid problems with syncing, but various operations like removing
a revlog can't be synced anyway - so we just need to ensure we clear the
undo queue prior to syncing
2021-03-10 11:47:53 +10:00
Damien Elmes
67c490a8dc support undoing deck mutations
This required refactoring the deck code a bit to split up the 'update'
and 'add' cases better.
2021-03-10 11:47:53 +10:00
Damien Elmes
c9eeb91e0a initial work on undoing reviews+burying siblings
- fetch sfld and csum when fetching notes, to make it cheaper
to write them back out unmodified
- make `fields` private, and access it via accessors, so we can
still catch when fields have been mutated without calling
prepare_for_update()
- fix python importing code passing a string in as the checksum
2021-03-10 11:47:53 +10:00
Damien Elmes
359d0bc331 use native struct for QueuedCard 2021-03-10 11:47:53 +10:00
Damien Elmes
7ce75479b2 fix sync download failing when temp dir on different mount
https://forums.ankiweb.net/t/problems-with-2-1-41-on-arch-linux/8103
2021-03-09 11:44:49 +10:00
Damien Elmes
b81e2c0265 Ensure we purge caches when rolling back
Fixes #1056
2021-03-08 10:39:18 +10:00
RumovZ
25d57574c9 Enable removal of multiple tags from the sidebar 2021-03-02 11:05:16 +01:00
Damien Elmes
de7baa80bd switch to 4 buttons when previewing in test scheduler
- Currently we just use 1.5x and 2x the normal preview delay; we could
change this in the future.
- Don't try to capture the current state; just use a flag to denote
exit status.
- Show (end) when exiting
2021-03-01 23:47:00 +10:00