<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>steinbro.github.io</title>
    <subtitle>Content by Daniel W. Steinbrook</subtitle>
    <link rel="self" type="application/atom+xml" href="https://steinbro.github.io/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://steinbro.github.io"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-06T00:00:00+00:00</updated>
    <id>https://steinbro.github.io/atom.xml</id>
    <entry xml:lang="en">
        <title>An unobtrusive text-to-speech workflow in GNOME</title>
        <published>2026-06-06T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              steinbro
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://steinbro.github.io/blog/speak-selected-text-on-linux/"/>
        <id>https://steinbro.github.io/blog/speak-selected-text-on-linux/</id>
        
        <content type="html" xml:base="https://steinbro.github.io/blog/speak-selected-text-on-linux/">&lt;p&gt;When text-to-speech is not a convenience but a necessity, it needs to be available instantly, for any text on the screen. The operating system, or more specifically the desktop environment, can step in to provide it system-wide, across all applications. For Linux, this functionality (as with much else on Linux) is possible, with some tinkering.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;on-demand-speech-on-other-operating-systems&quot;&gt;On-demand speech on other operating systems&lt;&#x2F;h3&gt;
&lt;p&gt;macOS has had &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.apple.com&#x2F;en-ca&#x2F;guide&#x2F;mac-help&#x2F;mh27448&#x2F;mac&quot;&gt;“Speak Selection” functionality&lt;&#x2F;a&gt; for ages. When actively speaking, the system shows a floating controller for playback and speech rate adjustment.
&lt;img src=&quot;https:&#x2F;&#x2F;photos5.appleinsider.com&#x2F;gallery&#x2F;43696-85254-macos-speech-tip-3-xl.jpg&quot; alt=&quot;Speech controller on macOS&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Microsoft similarly introduced &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blogs.windows.com&#x2F;windowsexperience&#x2F;2020&#x2F;05&#x2F;21&#x2F;whats-coming-in-windows-10-accessibility&#x2F;&quot;&gt;speech features into Magnifier&lt;&#x2F;a&gt; in Windows 10. Clicking the “Read from here” button starts reading text aloud from wherever the user specifies.
&lt;img src=&quot;https:&#x2F;&#x2F;winblogs.thesourcemediaassets.com&#x2F;sites&#x2F;2&#x2F;2020&#x2F;05&#x2F;343dbcf643139fc55528c962b84b8da0.png&quot; alt=&quot;Windows Magnifier controls&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.mozilla.org&#x2F;en-US&#x2F;kb&#x2F;firefox-reader-view-clutter-free-web-pages&quot;&gt;Reader View in Firefox&lt;&#x2F;a&gt; has text-to-speech built in as well.
&lt;img src=&quot;https:&#x2F;&#x2F;assets-prod.sumo.prod.webservices.mozgcp.net&#x2F;media&#x2F;uploads&#x2F;gallery&#x2F;images&#x2F;2024-08-06-05-16-50-40e344.png&quot; alt=&quot;Speech controls in Firefox Reader View&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Screen readers also have some features that are geared towards partially-sighted users. Both NVDA on Windows and VoiceOver on macOS support a mode that reads text under the mouse pointer. But on Linux, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;discourse.gnome.org&#x2F;t&#x2F;mouse-review-is-hit-and-miss&#x2F;34949&quot;&gt;Orca’s Mouse Review feature works inconsistently under Wayland&lt;&#x2F;a&gt; the modern display system for Linux.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;prior-art-on-linux&quot;&gt;Prior art on Linux&lt;&#x2F;h3&gt;
&lt;p&gt;A &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dev.to&#x2F;tylerlwsmith&#x2F;read-selected-text-out-loud-on-ubuntu-linux-45lj&quot;&gt;2021 blog post&lt;&#x2F;a&gt; demonstrates how to set up a keyboard shortcut to trigger text-to-speech on most any Linux desktop environment. The impressively short shell script simply grabs the highlighted text from the clipboard’s primary selection buffer, and feeds that directly into the espeak synthesizer.&lt;&#x2F;p&gt;
&lt;p&gt;This works great much of the time, but you’ll run into many unhandled edge cases if you use it extensively. For example. text containing Unicode mathematical characters, a common social media trick for adding styling, can turn into a stream of arcane code point names. “𝐉𝐚𝐯𝐚𝐒𝐜𝐫𝐢𝐩𝐭” becomes “letter 1D409, letter 1D41A, …” Emoji fare even worse, often disappearing entirely from the spoken output.&lt;&#x2F;p&gt;
&lt;p&gt;My favorite failure mode is when the script encounters text surrounded by double square brackets. The espeak synthesizer interprets what’s inside as raw phonetic data. The result is text that is a very exotic flavor of garbled. [[ If you happen to be using it now, this example will demonstrate exactly what I  mean,. ]]&lt;&#x2F;p&gt;
&lt;h3 id=&quot;a-native-extension-for-gnome&quot;&gt;A native extension for GNOME&lt;&#x2F;h3&gt;
&lt;p&gt;I’ve published a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;extensions.gnome.org&#x2F;extension&#x2F;9659&#x2F;speak-selection&#x2F;&quot;&gt;GNOME shell extension&lt;&#x2F;a&gt; that builds on the same basic approach to capturing and speaking selected text, but makes it a bit more robust to text encountered in the wild.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;steinbro.github.io&#x2F;blog&#x2F;speak-selected-text-on-linux&#x2F;speak_selection_extension_tray_icon_menu.png&quot; alt=&quot;Speak Selection extension tray icon menu&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The most visible improvement is its integration with the GNOME desktop, but the bigger change comes from inserting Speech Dispatcher between your text and the speech engine. This long-standing Linux service handles many of the failure modes we encountered earlier: it translates Unicode characters into something meaningful, announces emoji names instead of silently dropping them, and filters out control sequences that can trip up speech synthesizers.&lt;&#x2F;p&gt;
&lt;p&gt;Because Speech Dispatcher operates as a system-wide service, it also improves the overall listening experience. Other applications won’t try to speak over the extension, and you can change your preferred voice or speaking rate without needing to edit the values hard-coded into the original script.&lt;&#x2F;p&gt;
&lt;p&gt;There are still some limitations. Most significantly, the extension still relies on the primary selection buffer, a legacy feature of the Linux desktop that isn’t supported universally. For example, applications that are based on the Flutter toolkit, including the Ubuntu App Center and FluffyChat, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;flutter&#x2F;flutter&#x2F;issues&#x2F;180231&quot;&gt;don’t populate this buffer&lt;&#x2F;a&gt; when the user highlights text. That means the extension will ignore what you’ve highlighted in your Flutter-based app, and instead speak the last text highlighted in some other non-Flutter-based application.&lt;&#x2F;p&gt;
&lt;p&gt;Nevertheless, give it a try and let me know if you find it helpful!&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Adapting magnifier controls for mouse-centric websites</title>
        <published>2026-05-25T00:00:00+00:00</published>
        <updated>2026-05-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              steinbro
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://steinbro.github.io/blog/hover-magnifier-interactions/"/>
        <id>https://steinbro.github.io/blog/hover-magnifier-interactions/</id>
        
        <content type="html" xml:base="https://steinbro.github.io/blog/hover-magnifier-interactions/">&lt;h3 id=&quot;the-problem&quot;&gt;The problem&lt;&#x2F;h3&gt;
&lt;p&gt;Modern websites love using hover effects. Pausing the mouse pointer over various parts of the page can trigger preview cards, floating menus, or something else entirely.&lt;&#x2F;p&gt;
&lt;p&gt;For most users, this is fine and sometimes even helpful. If it’s unwanted, the user can just move the mouse elsewhere. But it poses an accessibility challenge for full-screen magnification users. Their visible area of the page, known as a viewport, will be much smaller than what most other users have.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;steinbro.github.io&#x2F;blog&#x2F;hover-magnifier-interactions&#x2F;laptop-full-screen-zoom.png&quot; alt=&quot;Ubuntu laptop with full-screen magnification&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Many hover-heavy interfaces are built around a pair of assumptions:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Moving the mouse over something means the user wants to interact with it more deeply.&lt;&#x2F;li&gt;
&lt;li&gt;Users can comfortably look at one part of the screen while manipulating another.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For full-screen magnification users, neither assumption really holds. Moving the mouse is often just a way to pan the viewport or keep content visible, not an intentional request for more UI. And because only a small portion of the screen is visible at once, users frequently cannot see the area they are interacting with and the area where the resulting information appears at the same time.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;obscured-content&quot;&gt;Obscured content&lt;&#x2F;h4&gt;
&lt;p&gt;At high zoom levels, the visible portion of the screen is tiny, and the magnified area usually follows the mouse pointer. So when a website decides to reveal extra information on hover, that information can end up covering the very thing the user was trying to look at.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;lh4.googleusercontent.com&#x2F;bPcgol-gx6iVl5VbH3X3GOznPHMEludSLFxyRw0sk1eyKv7rlheRbF1I_fDKjZMG7VrnfDxud7VfOTtmfLBVdKgeSwY-SGyNfoICjbH5n7X9Rcob1mK8Kt7K9lRjBWULRkBDCm-d=s0&quot; alt=&quot;A link preview in Google Sheets covering most of the screen&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The obvious solution, moving the mouse elsewhere, doesn’t really work. Moving the pointer also moves the zoomed viewport, which can push the content completely off-screen.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;important-information-outside-the-viewport&quot;&gt;Important information outside the viewport&lt;&#x2F;h4&gt;
&lt;p&gt;In some cases, the problem is spatial rather than transient. An interactive graph, for example, might show numerical values in a fixed corner of the screen while the user moves the mouse across the graph. Under full-screen magnification, the user may be able to see the graph or the readout, but not both at the same time. Moving the mouse to explore the graph also moves the zoomed viewport, making it difficult or impossible to keep the changing values visible.&lt;&#x2F;p&gt;
&lt;video width=&quot;100%&quot; controls&gt;
  &lt;source src=&quot;graph-values-offscreen.mp4&quot; type=&quot;video&#x2F;mp4&quot;&gt;
  Your browser does not support the video tag.
&lt;&#x2F;video&gt;
&lt;p&gt;The root problem is that, for magnification users, mouse movement is doing two completely different jobs at the same time. It’s being used both to indicate what they want to interact with, and to control which part of the screen they can see.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-solution&quot;&gt;The solution&lt;&#x2F;h3&gt;
&lt;p&gt;Fortunately, these problems don’t require changes to every offending site. We can introduce some small tweaks on the user side to decouple pointer interaction from panning the viewport.&lt;&#x2F;p&gt;
&lt;p&gt;In macOS, this kind of functionality is built-in. One of the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.apple.com&#x2F;guide&#x2F;mac-help&#x2F;change-zoom-advanced-options-accessibility-mh35715&#x2F;26&#x2F;mac&#x2F;26&quot;&gt;“advanced options” of the full-screen magnifier&lt;&#x2F;a&gt; is “Detach zoom view from pointer.” When the user holds down the Control and Command keys, the mouse pointer is effectively frozen in place. Moving the mouse moves the zoomed area, but the focused application doesn’t register any mouse movement.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;freezing-the-pointer&quot;&gt;Freezing the pointer&lt;&#x2F;h4&gt;
&lt;p&gt;With some AI assistance, I’ve replicated this as a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;steinbro&#x2F;gnome-low-vis-config&#x2F;blob&#x2F;main&#x2F;userscripts&#x2F;A11yFreezePointerOnCtrl.js&quot;&gt;“freeze pointer” user script&lt;&#x2F;a&gt;, which can be installed in a Web browser through an extension like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.tampermonkey.net&#x2F;&quot;&gt;Tampermonkey&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Below, as illustrated earlier, the user is moving the mouse across a graph while the exact values appear in a fixed location outside the magnified area. With the frozen-pointer user script, however, the user can hold down the Control key to temporarily freeze the hover position on a single data point. This enables them to pan the magnified viewport until the readout is visible again, without changing which data point is selected under the mouse pointer.&lt;&#x2F;p&gt;
&lt;video width=&quot;100%&quot; controls&gt;
  &lt;source src=&quot;freeze-pointer-demo.mp4&quot; type=&quot;video&#x2F;mp4&quot;&gt;
  Your browser does not support the video tag.
&lt;&#x2F;video&gt;
&lt;h4 id=&quot;shielding-the-page-from-interaction&quot;&gt;Shielding the page from interaction&lt;&#x2F;h4&gt;
&lt;p&gt;I also created a second user script that &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;steinbro&#x2F;gnome-low-vis-config&#x2F;blob&#x2F;main&#x2F;userscripts&#x2F;A11yDisableHoverOnCtrl.js&quot;&gt;temporarily adds an “invisible wall” between the pointer and the web page&lt;&#x2F;a&gt;. This takes a more aggressive, but also more reliable, approach: instead of freezing the current hover state in place, it prevents the page from receiving pointer interactions altogether while the modifier key is held. The downside is that it disables hover behavior entirely rather than preserving the currently displayed item or tooltip, so it’s less helpful in cases like the interactive graph.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
