aboutsummaryrefslogtreecommitdiffstats
path: root/lib
Commit message (Collapse)AuthorAgeFilesLines
...
* pgp-provider: set default value to autoRobin Jarry2022-12-062-3/+25
| | | | | | | | | | | Change the default provider to gpg unless the internal keyring is initialized and contains one key. This should be more user friendly. Link: https://lists.sr.ht/~rjarry/aerc-discuss/%3CCO783CI3IU9F.184DBQTPMIPBS%40paul%3E Signed-off-by: Robin Jarry <robin@jarry.cc> Acked-by: Moritz Poldrack <moritz@poldrack.dev>
* store: fix server-side threads togglingKoni Marti2022-12-021-3/+2
| | | | | | | | | | | Fix server-side threads toggling that can sometimes cause the uids to be out-of-sync with the threads. This patch ensures a consistent way of handling the uids and threads in the store. Fixes: https://todo.sr.ht/~rjarry/aerc/102 Signed-off-by: Koni Marti <koni.marti@gmail.com> Tested-by: Inwit <inwit@sindominio.net> Acked-by: Robin Jarry <robin@jarry.cc>
* logging: rename package to logRobin Jarry2022-12-0210-40/+40
| | | | | | | | | | Use the same name than the builtin "log" package. That way, we do not risk logging in the wrong place. Suggested-by: Tim Culverhouse <tim@timculverhouse.com> Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Bence Ferdinandy <bence@ferdinandy.com> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
* logging: homogenize levelsRobin Jarry2022-12-027-12/+16
| | | | | | | | | | | | | | | | | | The main goal is to ensure that by default, the log file (if configured) does not grow out of proportions. Most of the logging messages in aerc are actually for debugging and/or trace purposes. Define clear rules for logging levels. Enforce these rules everywhere. After this patch, here is what the log file looks like after starting up with a single account: INFO 2022/11/24 20:26:16.147164 aerc.go:176: Starting up version 0.13.0-100-g683981479c60 (go1.18.7 amd64 linux) INFO 2022/11/24 20:26:17.546448 account.go:254: [work] connected. Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Bence Ferdinandy <bence@ferdinandy.com> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
* ui: box and frame an interactive widgetKoni Marti2022-11-211-0/+74
| | | | | | | | Draw a framed box with a title containing an interactive-drawable widget. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
* lib: prepare attachments for multiple readsKoni Marti2022-11-091-5/+15
| | | | | | | | | | | Prepare attachments for multiple reads. The data for lib.PartAttachment is stored as an io.Reader which can only be read once. This will cause an issue when we want to call composer.WriteMessage multiple times, i.e. for a message preview. We fix this by keeping a copy of the data and create a new reader everytime the attachment is read. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* lib: implement an eml message viewKoni Marti2022-11-092-3/+97
| | | | | | | | | Implement a MessageView representation for eml data that are not stored in a message store. With this, we can display any rfc822 message data in the message viewer. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* ui: invalidate ui when queuing redrawTim Culverhouse2022-11-062-3/+3
| | | | | | | | | | | | | | | The QueueRedraw function should always be preceeded by a call to ui.Invalidate in order to make a redraw a occur. In one instance, this was not done and it was possible for the UI to not redraw itself (when a terminal closes, a UI redraw request is made but it is possible for the UI to not be invalidated as a result of the close). Move the call to Invalidate into the QueueRedraw function to ensure that every QueueRedraw call will redraw the screen. Fixes: https://todo.sr.ht/~rjarry/aerc/98 Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* msglist: style search resultsTim Culverhouse2022-11-061-0/+10
| | | | | | | | Add option to style search results in the message list. Set default style for results. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* msgstore: use default sort when applying ClearTim Culverhouse2022-11-061-1/+3
| | | | | | | | | | The :clear command clears any sorting, filtering, or marking of messages within the message store. Respect the user's default sort order by storing this in the store and re-applying the default sort when using clear. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* threadbuilder: better scaling for thread insertionKoni Marti2022-11-061-4/+10
| | | | | | | | | | Improve thread builder's performance scaling by inserting a new top-level thread at the beginning of the linked list which is an O(1) operation. The order of the top-level threads does not matter here since they will be sorted later anyways. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
* threadbuilder: sort siblings by sort criteriaKoni Marti2022-11-062-9/+41
| | | | | | | | | | | | | | Sort the client-side thread siblings according to the sort criteria. Activate this option by setting "sort-thread-siblings" to true in the ui section of aerc.conf. "sort-thread-siblings" is false by default and the siblings will be sorted based on their uid number. Note that this options will only work with client-side threading and when the backend supports sorting. Also, it comes with a slight performance penalty. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
* threadbuilder: streamline thread builder internalsKoni Marti2022-11-061-43/+30
| | | | | | | | | | | | Streamline the internals of the client-side thread builder. Handle the jwz dummy threads explicitly and let jwz deal with message-id collisions. This should make the client-side threading more stable overall. Duplicated message-ids will also be properly displayed now. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
* auto-completion: add option to require a min number of charsRobin Jarry2022-11-061-2/+8
| | | | | | | | | | When doing address completion via commands that take a while to run, having the completion trigger even with a single character can be non-optimal. Add an option to allow requiring a minimum number of characters to actually run the completion command. Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Tim Culverhouse <tim@timculverhouse.com>
* threads: reverse thread orderingKoni Marti2022-10-272-12/+30
| | | | | | | | | | | Add reverse-thread-order option to the ui config to enable reverse display of the mesage threads. Default order is the the intial message is on the top with all the replies being displayed below. The reverse options will put the initial message at the bottom with the replies on top. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* store: implement the simplified index handlingKoni Marti2022-10-272-33/+36
| | | | | | | | | | Simplify the index handling for moving to the next or previous message. Same for moving to the next or previous search results. Moving of the index variable relies on the iterator package's StartIndex and EndIndex functions. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* iterator: add functionality to move indicesKoni Marti2022-10-272-0/+184
| | | | | | | | Extract the index acrobatics from the message store and move it to iterator package for re-use and to add unit tests. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* store: reverse message list order with iteratorsKoni Marti2022-10-272-32/+65
| | | | | | | | | | | | | | | | | Reverse the order of the messages in the message list. The complexity of reversing the order is abstracted away by the iterators. To reverse the message list, add the following to your aerc.conf: [ui] reverse-msglist-order=true Thanks to |cos| for sharing his initial implementation of reversing the order in the message list [0]. [0]: https://git.netizen.se/aerc/commit/?h=topic/asc_sort_imap Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* iterator: implement iterators over uid/thread dataKoni Marti2022-10-273-0/+255
| | | | | | | | | | | | Implement an iterator framework for uid and thread data. Iterators ensure that the underlying data is accessed in the right order and provide an abstraction of the index handling. Iterators should be used when the order of the uids/threads is important. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* lib: fallback on raw msg when decoding failsKoni Marti2022-10-191-1/+11
| | | | | | | | | | | | | | Avoid panic when part decoding fails: panic: quotedprintable: invalid unescaped byte 0x0c in body User-friendlier fallback when a (decoding) error occurs while reading a message part. Link: https://lists.sr.ht/~rjarry/aerc-discuss/%3CCNJRVKUG8T68.3TVA2T10DTTBA%40guix-framework%3E Reported-by: "(" <paren@disroot.org> Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* msgview: add separate date formattingBence Ferdinandy2022-10-191-3/+3
| | | | | | | | | | The ThisDayTimeFormat and friends are missing from the message view which just uses the message list's default setting. This might not be desirable since the amount of space available is different. Introduce separate settings for formatting dates in the message view. Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com> Acked-by: Robin Jarry <robin@jarry.cc>
* ui: add :split and :vsplit view optionsTim Culverhouse2022-10-181-0/+18
| | | | | | | | | | | Add :split and :vsplit commands, which split the message list view to include a message viewer. Each command takes an int, or a delta value ("+1", "-1"). The int value is the resulting size of the message list, and a new message viewer will be displayed below / to the right of the message list. This viewer *does not* set seen flags. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* aercmsg: add AercFuncMsg and QueueFuncTim Culverhouse2022-10-181-0/+10
| | | | | | | | | | Introduce AercFuncMsg and QueueFunc. These are used in combination to queue a function to be run in the main goroutine. This can be used to prevent data races in delayed function calls (ie from debounce functions). Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* msgviewer: simplify filter and pager command handlingTim Culverhouse2022-10-181-0/+37
| | | | | | | | | | | | | | | | | | | Refactor the filtering and paging logic to use several fewer goroutines. Fixes data race condition with the timing of filter -> pager -> terminal, can be found when switching message views fast. Check if filter -> pager process is currently running before calling it to start again. Fixes data race between fetching message body and terminal starting. Both can initiate the copying process, and for long running filters and fast message fetching, it is possible to have fetched a message but still be running the filter when the terminal starts. Move StripAnsi to it's own file in lib/parse, similar to the hyperlinks parser. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* msgpart: factorize mime type and filename constructionRobin Jarry2022-10-161-4/+2
| | | | | | | Reduce code duplication. Signed-off-by: Robin Jarry <robin@jarry.cc> Acked-by: Moritz Poldrack <moritz@poldrack.dev>
* invalidatable: cleanup dead codeTim Culverhouse2022-10-1211-159/+19
| | | | | | | | Remove invalidatable type and all associated calls. All items can directly invalidate the UI. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* invalidatable: always mark ui as dirty OnInvalidateTim Culverhouse2022-10-072-15/+20
| | | | | | | | | | | | | | | | | | | | | | | | | | | The Invalidatable struct is designed so that a widget can have a callback function ran when it is Invalidated. This is used to cascade up the widget tree, marking things as Invalid along the way so that only Invalid widgets are drawn. However, this is only implemented at the grid cell level for checks if the cell is invalidated -- and the grid cells are never set back to a "valid" state. The effect of this is that no matter what is invalidated, the entire UI gets drawn again. The calling through the Invalidate callbacks creates *several* race conditions, as Invalidate is called from several different goroutines, and many widgets call invalidate on their parent or children. Tcell has optimizations to only rerender screen cells that have changed their rune and style. The only performance penalty by redrawing the entire screen for aerc is the operations *within the aerc draw methods*. Most of these are not expensive and have relatively no impact on performance. Skip all of the OnInvalidates, and directly invalidate the UI when DoInvalidate is called by a widget. This reduces data races, and simplifies the widget redraw logic signficantly. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* render: clean up render codeTim Culverhouse2022-10-071-11/+1
| | | | | | | | | | | | The render method sets everything as invalid if there was a popover. This is no longer necessary, as everything is redrawn anyways. Remove the check and extra atomic set of dirty and invalidate. Remove unused return value Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* aerc: use single event loopTim Culverhouse2022-10-073-16/+21
| | | | | | | | | Combine tcell events with WorkerMessages to better synchronize state with IO and UI. Remove Tick loop for rendering. Use events to trigger renders. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* events: introduce AercMsg and QueueRedrawTim Culverhouse2022-10-072-0/+11
| | | | | | | | | | Add AercMsg as a main interface for internal communication in aerc in preparation for a main event loop. Add a QueueRedraw function to to trigger a redraw. This will be needed for widgets which should be drawn after some delay (completions, terminal, for example) Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* forward,recall: fix charsets in part attachmentKoni Marti2022-10-071-0/+17
| | | | | | | | | | | Fix charset to UTF-8 in part attachments. The forward and recall commands fetch message parts with the go-message package which decodes to UTF-8. Hence, we should set the charset of the part attachment to utf-8 and not just copying over the one from the original message. Reported-by: Bence Ferdinandy <bence@ferdinandy.com> Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* view: add peek flag and propagateKoni Marti2022-10-041-0/+9
| | | | | | | | | | | | Add a peek flag -p to the view commands to open the message viewer without setting the "seen" flag. If the flag is set, it would ignore the "auto-mark-read" config. The SetSeen flag will be propagated in case the message viewer moves on to other messages, i.e. with the delete or archive commands. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* viewer: option to not mark message as seenKoni Marti2022-10-041-2/+4
| | | | | | | | | | | | | | | | | Add option to open a message in the message viewer without setting the seen flag. Enables the message viewer to be used as a preview pane without changing the message flags unintentionally. Before, the message viewer would set the seen flag by default. The IMAP backend will now always fetch the message body with the peek option enabled (same as we fetch the headers). An "auto-mark-read" option is added to the ui config which is set to true by default. If set the false, the seen flag is not set by the message viewer. Co-authored-by: "James Cook" <falsifian@falsifian.org> Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* msgstore: fix data race on access to store.needsFlagsTim Culverhouse2022-10-021-0/+5
| | | | | | | | | Flag fetching is debounced in the UI, creating a race condition where fields are accessed in the AfterFunc. Protect the needsFlags field with a mutex. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* logging: substitute %w for %vKoni Marti2022-10-021-1/+1
| | | | | | | | | | | | | | | | Subsitute the format specifier %w for %v in the logging facility. The logging functions use a fmt.Sprintf call behind the scene which does not recognize %w. %w should be used in fmt.Errorf when you want to wrap errors. Hence, the log entries that use %w are improperly formatted like this: ERROR 2022/10/02 09:13:57.724529 worker.go:439: could not get message info %!w(*fmt.wrapError=&{could not get structure: [snip] }) ^ Links: https://go.dev/blog/go1.13-errors Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Moritz Poldrack <moritz@poldrack.dev>
* imap,smtp: add XOAUTH2 supportJulian Pidancet2022-10-011-0/+88
| | | | | | | | | | | | | | | | | | | | | Add XOAUTH2 authentication support for IMAP and SMTP. Although XOAUTH2 is now deprecated in favor of OAuthBearer, it is the only way to connect to Office365 since Basic Auth is now completely removed. Since XOAUTH2 is very similar to OAuthBearer and uses the same configuration parameters, this is basically a copy-paste of the existing OAuthBearer code. However, XOAUTH2 support was removed from go-sasl library, so this change reimports the code that was removed from go-sasl and offers it a new home in lib/xoauth2.go. Hopefully it shouldn't be too hard to maintain, being less than 50 SLOC. Link: https://github.com/emersion/go-sasl/commit/7bfe0ed36a21 Implements: https://todo.sr.ht/~rjarry/aerc/78 Signed-off-by: Julian Pidancet <julian.pidancet@oracle.com> Tested-by: Inwit <inwit@sindominio.net> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
* open: allow overriding default programRobin Jarry2022-10-011-4/+36
| | | | | | | | | | | | | | | | | | | | | | | | Instead of xdg-open (or open on MacOS), allow forcing a program to open a message part. The program is determined in that order of priority: 1) If :open has arguments, they will be used as command to open the attachment. If the arguments contain the {} placeholder, the temporary file will be substituted, otherwise the file path is added at the end of the arguments. 2) If a command is specified in the [openers] section of aerc.conf for the part MIME type, then it is used with the same rules of {} substitution. 3) Finally, fallback to xdg-open/open with the file path as argument. Update the docs and default config accordingly with examples. Fixes: https://todo.sr.ht/~rjarry/aerc/64 Co-authored-by: Jason Stewart <support@eggplantsd.com> Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Moritz Poldrack <moritz@poldrack.dev>
* open: simplify codeRobin Jarry2022-10-011-49/+9
| | | | | | | | | | | | There is no need for convoluted channels and other async fanciness. Expose a single XDGOpen static function that runs a command and returns an error if any. Caller is responsible of running this in an async goroutine if needed. Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Moritz Poldrack <moritz@poldrack.dev>
* ui: avoid panic when terminal window is shrunkJason Stewart2022-09-261-2/+4
| | | | | | | | | | | When using a tiling window manager, aerc terminal dimensions may be greatly reduced after a new window has been created by :open. When the ui attempts to render to formerly-valid coordinates, SetCell & Printf may panic. Replace panic() with no-op in both functions to prevent aerc from crashing after a window shrink. Signed-off-by: Jason Stewart <support@eggplantsd.com> Acked-by: Robin Jarry <robin@jarry.cc>
* textinput: prevent data race from debounce functionTim Culverhouse2022-09-261-0/+6
| | | | | | | | Protect access to fields in textinput. Concurrent access can happen in the main event loop and the completion debounce function. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* msgstore: revert 9fdc7acf5b48 "post messageInfo on erroneous fetch"Tim Culverhouse2022-09-251-13/+1
| | | | | | | | | | Commit 9fdc7acf5b48 ("cache: fetch flags from UI") introduced a regression where all messages were marked as erroneous if a single one in the fetch request had an error. Reported-by: Jose Lombera <jose@lombera.dev> Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* charset: handle unknown charsets more user-friendlyKoni Marti2022-09-251-3/+2
| | | | | | | | | Do not throw an error when the charset is unknown; the message entity can still be read, but log the error instead. Reported-by: falsifian Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
* cache: fetch flags from UITim Culverhouse2022-09-201-0/+23
| | | | | | | | | | | | | | | When cached headers are fetched, an action is posted back to the Worker to immediately fetch the flags for the message from the server (we can't know the flags state, therefore it's not cached). When scrolling, a lag occurs when loading cached headers because the n+1 message has to wait for the flag request to return before the cached headers are retrieved. Collect the message UIDs in the UI that need flags, and fetch them based off a debounce timer in a single request. Post the action from the UI to eliminate an (ugly) go routine in the worker. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* grid: protect calls to cell.ContentTim Culverhouse2022-09-201-2/+6
| | | | | | | | | Many panics occur from calling Draw on a nil widget, stemming from the grid ui element. Protect the calls to Draw from within grid to prevent this method of panic. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* grid: remove unused method ChildrenTim Culverhouse2022-09-201-11/+0
| | | | | | | | | | | The grid method Children returns the children of a grid, and is never used. The function is reimplemented in both aerc.go and account.go, also never called. Remove these unused methods. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* msgstore: post MessageInfo on erroneous fetchTim Culverhouse2022-09-202-1/+17
| | | | | | | | | | | | | | When errors occur during a fetch header request, the requested headers are deleted from pending and no information is given to the UI. Spinners keep spinning, and ultimately as the view is refreshed, the headers are fetched again. This can lead to infinite loops, and extremely long logs. Update the store with a MessageInfo message when an error is received. Have the UI display that the header couldn't be fetched in the message list. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* statusline-format: add %p placeholder for current pathBence Ferdinandy2022-09-191-0/+11
| | | | | | | | | | Allow showing the current working directory in the statusline via [statusline] render-format=%p, which is useful if the user changes directories often. Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com> Tested-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* term: add bracketed paste supportTim Culverhouse2022-09-141-0/+1
| | | | | | | | Allow forwarding paste events to embedded applications. When a bracketed paste is in progress, do not process any command bindings. Signed-off-by: Robin Jarry <robin@jarry.cc> Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
* ui: export context.viewport, screen.show, add SetCursorStyleTim Culverhouse2022-09-141-0/+12
| | | | | | | | | | Export context.viewport for use in implementing tcell-term. Bump tcell version to enable SetCursorStyle feature. Add this function to the ui for future use with tcell-term. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
* ui: cleanup internals and apiRobin Jarry2022-09-141-15/+7
| | | | | | | | | | | | | | | | Now that tcell events are handled in a goroutine, no need for a channel to buffer them. Rename ui.Tick() to ui.Render() and ui.Run() to ui.ProcessEvents() to better reflect what these functions do. Move screen.PollEvent() into ui.ProcessEvents(). Register the panic handler in ui.ProcessEvents(). Remove aerc.ui.Tick() from DecryptKeys(). What the hell was that? Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Tim Culverhouse <tim@timculverhouse.com>