Skip to content

Add error handling for layout update on macOS#1771

Merged
mkrnr merged 1 commit intoopensteno:mainfrom
skanev:patch-1
Oct 10, 2025
Merged

Add error handling for layout update on macOS#1771
mkrnr merged 1 commit intoopensteno:mainfrom
skanev:patch-1

Conversation

@skanev
Copy link
Contributor

@skanev skanev commented Sep 25, 2025

Apologies for opening the pull request this way, but I'm not quite sure how to solve this issue, and wanted to start a conversation. I'm also proposing a hacky solution that is good enough for me in case it meets the bar.

The issue

The problem happens when I'm using Plover on OS X on a computer that has multiple input sources (English and Bulgarian in my case). Plover works fine when I start it while on the English input source, but crashes when I change to the Bulgarian input source. You can reproduce it by:

  1. Add an input source for Bulgarian (or probably any non-latin language)
  2. Start Plover while in English
  3. Change input source
  4. Plover crashes

This makes it effectively unusable, because every time I need to switch languages and reach for a regular keyboard, I have to restart it.

I've also opened an issue previously, #1708. I've seen a few similar issues in GitHub and Discord, but don't have the time to find them right now.

The "fix"

I've basically just wrapped _update_layout() in a try/except. Changing to Bulgarian no longer crashes the application. If I use steno while in Bulgarian, Plover would output gibberish (in Cyrillic), but that's good enough for me. I also have to be in English when I launch it, but that's also good enough for me.

There is no regression when the user has multiple latin-based layouts (e.g. Qwerty and Colemak), which I assume is the use case for this feature in the first place.

How could it be done differently

KeyboardLayout has an optional constructor argument, watch_layout that's always defaulted to True. If it could be configured through the settings, that will probably solve my problem too, but I couldn't quite figure how to add it there, and also how to start/stop the watcher when the user changes that setting.

The code could also be written with a more precise exception handling, but that would require a bigger rewrite. Right now, the exception I'm getting is:

2025-09-25 13:32:11,976 [MainThread] ERROR: Qt GUI error
Traceback (most recent call last):
  File "/Users/aquarius/code/opensource/plover/plover/scripts/main.py", line 147, in main
    code = gui.main(config, controller)
  File "/Users/aquarius/code/opensource/plover/plover/gui_qt/main.py", line 125, in main
    app = Application(config, controller, use_qt_notifications)
  File "/Users/aquarius/code/opensource/plover/plover/gui_qt/main.py", line 61, in __init__
    config, controller, KeyboardEmulation()
                        ~~~~~~~~~~~~~~~~~^^
  File "/Users/aquarius/code/opensource/plover/plover/oslayer/osx/keyboardcontrol.py", line 412, in __init__
    self._layout = KeyboardLayout()
                   ~~~~~~~~~~~~~~^^
  File "/Users/aquarius/code/opensource/plover/plover/oslayer/osx/keyboardlayout.py", line 130, in __init__
    self._update_layout()
    ~~~~~~~~~~~~~~~~~~~^^
  File "/Users/aquarius/code/opensource/plover/plover/oslayer/osx/keyboardlayout.py", line 153, in _update_layout
    KeyboardLayout._get_layout()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/Users/aquarius/code/opensource/plover/plover/oslayer/osx/keyboardlayout.py", line 265, in _get_layout
    parsed_layout = KeyboardLayout._parse_layout(layout_buffer, keyboard_type)
  File "/Users/aquarius/code/opensource/plover/plover/oslayer/osx/keyboardlayout.py", line 417, in _parse_layout
    mod = modifier_masks[i]
          ~~~~~~~~~~~~~~^^^
KeyError: 9

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/Users/aquarius/code/opensource/plover/plover/scripts/main.py", line 189, in <module>
    main()
    ~~~~^^
  File "/Users/aquarius/code/opensource/plover/plover/scripts/main.py", line 167, in main
    gui.show_error("Unexpected error", traceback.format_exc())
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/aquarius/code/opensource/plover/plover/gui_qt/main.py", line 90, in show_error
    app = QApplication([])
RuntimeError: Please destroy the QApplication singleton before creating a new QApplication instance.

Finally, the watcher could just be removed, but I guess this will be a regression for people actually using multiple latin layouts.

If you're actually happy with this change, I'm also happy to ammend a more detailed commit message that explains the problem so it remains in the git history, just let me know if I should.

Copy link
Contributor

@mkrnr mkrnr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for this PR and the background on the issue!

I was able to reproduce it with Bulgarian. Interestingly, Russian works fine so it's not all non-latin layouts.

Right now I also don't know a better way to fix it but not crashing is certainly an improvement so let's go for this approach.

I have one small remark and it would be great if you could add a news file, see here.

And no need to add more info to the commit message since the commit will link to this PR anyways.

@mkrnr mkrnr changed the title Don't crash if _update_layout fails Add error handling for layout update on macOS Sep 30, 2025
@user202729
Copy link
Member

while programmers are not perfect and continue with a warning is better than outright crash, given that the code that crash is itself the crash handler, the crash handler is not working.

I suggest the following.

diff --git a/plover/gui_qt/main.py b/plover/gui_qt/main.py
index ed63941..e29a6c6 100644
--- a/plover/gui_qt/main.py
+++ b/plover/gui_qt/main.py
@@ -87,7 +87,14 @@ class Application:
 
 def show_error(title, message):
     print("%s: %s" % (title, message))
-    app = QApplication([])
+    try:
+        app = QApplication([])
+    except RuntimeError:
+        # just in case an error *inside* gui.main(...) propagate all the way here,
+        # gui = gui_qt and QApplication object is already constructed
+        import traceback
+        message += "\n\n" + traceback.format_exc()
+        app = None
     QMessageBox.critical(None, title, message)
     del app

or

diff --git a/plover/gui_qt/main.py b/plover/gui_qt/main.py
index ed63941..e17b645 100644
--- a/plover/gui_qt/main.py
+++ b/plover/gui_qt/main.py
@@ -87,7 +87,16 @@ class Application:
 
 def show_error(title, message):
     print("%s: %s" % (title, message))
-    app = QApplication([])
+    app = None
+    if QApplication.instance() is None:
+        # just in case an error *inside* gui.main(...) propagate all the way here,
+        # gui = gui_qt and QApplication object is already constructed, so we cannot construct another
+        try:
+            app = QApplication([])
+        except:
+            # this should not happen
+            import traceback
+            message += "\n\n" + traceback.format_exc()
     QMessageBox.critical(None, title, message)
     del app
 

whichever you prefer. I believe the second version should give less verbose error message.

Also, is log.warning

  • visible to the user (apart from just the console)?
  • show the traceback and the error type+message somewhere?

if yes then there isn't an issue, if no then we should add traceback.format_exc() at appropriate places.

@skanev
Copy link
Contributor Author

skanev commented Oct 2, 2025

@user202729 I'm not sure I follow.

At the very least, I don't want to see this message every time I change languages. If I understand your suggestion correctly, it will be popping up all the time when I'm not doing actively Steno, but I want to keep Plover open in case I need to steno.

@mkrnr Otherwise, Plover not crashing in Russian is an interesting datapoint. I'll see if I can identify something funky in Bulgarian BDS that I could actually fix in keyboard_layout.py, and fix it that way, although I still belive this watcher should either be disable-able or at least not crash the application.

On macOS and with some input sources (Bulgarian BDS in particular),
keyboardlayout.py doesn't properly parse the layout, throws an error,
and makes Plover crash:

    Traceback (most recent call last):
      File "/Users/skanev/code/opensource/plover/plover/scripts/main.py", line 147, in main
        code = gui.main(config, controller)
      File "/Users/skanev/code/opensource/plover/plover/gui_qt/main.py", line 125, in main
        app = Application(config, controller, use_qt_notifications)
      File "/Users/skanev/code/opensource/plover/plover/gui_qt/main.py", line 61, in __init__
        config, controller, KeyboardEmulation()
                            ~~~~~~~~~~~~~~~~~^^
      File "/Users/skanev/code/opensource/plover/plover/oslayer/osx/keyboardcontrol.py", line 412, in __init__
        self._layout = KeyboardLayout()
                       ~~~~~~~~~~~~~~^^
      File "/Users/skanev/code/opensource/plover/plover/oslayer/osx/keyboardlayout.py", line 130, in __init__
        self._update_layout()
        ~~~~~~~~~~~~~~~~~~~^^
      File "/Users/skanev/code/opensource/plover/plover/oslayer/osx/keyboardlayout.py", line 156, in _update_layout
        KeyboardLayout._get_layout()
        ~~~~~~~~~~~~~~~~~~~~~~~~~~^^
      File "/Users/skanev/code/opensource/plover/plover/oslayer/osx/keyboardlayout.py", line 268, in _get_layout
        parsed_layout = KeyboardLayout._parse_layout(layout_buffer, keyboard_type)
      File "/Users/skanev/code/opensource/plover/plover/oslayer/osx/keyboardlayout.py", line 423, in _parse_layout
        mod = modifier_masks[i]
              ~~~~~~~~~~~~~~^^^
    KeyError: 9

This happens both when Plover is started while in Bulgarian, and also
when switching from English to Bulgarian.

This change suppresses the crash when the input source is changed. That
way you can start Plover in English, and if you switch to Bulgarian, it
would not crash. It will output gibberish in Cyrillic, but that's OK, as
there is no Bulgarian steno yet anyway. Plover still crashes when
started in Bulgarian, but that can be worked around by starting it in
English.
@skanev
Copy link
Contributor Author

skanev commented Oct 10, 2025

@mkrnr I've applied your requests. I've also rebased on latest main, and wrote a better commit message. The branch name is unfortunately still patch-1, sorry about that.

I went on a side quest to understand why the parsing of the macOS layout fails, but unfortunately, it's quite dense to figure out and it felt this is a reasonable change anyway.

Copy link
Contributor

@mkrnr mkrnr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again!

@mkrnr mkrnr merged commit 751f94b into opensteno:main Oct 10, 2025
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants