2026-03-21 01:58:22 -04:00
|
|
|
/***************************************************************************
|
|
|
|
|
* Copyright (C) 2026 by Mudlet Developers *
|
|
|
|
|
* *
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
|
|
|
* it under the terms of the GNU General Public License as published by *
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
|
|
|
* (at your option) any later version. *
|
|
|
|
|
* *
|
|
|
|
|
* This program is distributed in the hope that it will be useful, *
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
|
|
|
* GNU General Public License for more details. *
|
|
|
|
|
* *
|
|
|
|
|
* You should have received a copy of the GNU General Public License *
|
|
|
|
|
* along with this program; if not, write to the *
|
|
|
|
|
* Free Software Foundation, Inc., *
|
|
|
|
|
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
|
|
|
|
***************************************************************************/
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Tests for OSC (Operating System Command) escape sequence handling.
|
|
|
|
|
*
|
2026-03-26 15:01:24 +01:00
|
|
|
* Uses loopbackTest() to inject data directly into the telnet processing
|
|
|
|
|
* pipeline, avoiding per-test TCP connections and profile creation.
|
|
|
|
|
* Mudlet is started once in initTestCase and reused across all tests.
|
|
|
|
|
*
|
2026-03-21 01:58:22 -04:00
|
|
|
* Run with: ctest -R TOscTest -V
|
|
|
|
|
*/
|
|
|
|
|
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
#include <QKeyEvent>
|
2026-03-21 01:58:22 -04:00
|
|
|
#include <QtTest/QtTest>
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
#include <chrono>
|
2026-03-21 01:58:22 -04:00
|
|
|
|
Infrastructure: Swap out QtConcurrent module header for sub-module ones (#9246)
#### Brief overview of PR changes/additions
The Qt documentation for `QtConcurrent` points out:
> If you include the `<QtConcurrent>` header, the entire Qt Concurrent
module with the entire Qt Core module will be included, which may
increase compilation times and binary sizes. To use individual functions
from the QtConcurrent namespace, you can include more specific headers.
>
> The table below lists the functions in the QtConcurrent namespace and
their corresponding headers:
|Function|Header|
|--------|------|
|`QtConcurrent::run()`|`<QtConcurrentRun>`|
|`QtConcurrent::task()`| `<QtConcurrentTask>`|
|`QtConcurrent::filter()`,<br>`QtConcurrent::filtered()`,<br>`QtConcurrent::filteredReduced()`|`<QtConcurrentFilter>`|
|`QtConcurrent::map()`,<br>`QtConcurrent::mapped()`,<br>`QtConcurrent::mappedReduced()`|`<QtConcurrentMap>`|
#### Motivation for adding to Mudlet
To speed up the build a little by removing stuff that isn't needed.
#### Other info (issues closed, discussion etc)
In doing this I happened to start cleaning up a couple of header files
`T2DMap.h` and then `mudlet.h`, I then got into converting some
`#include`s into forward declarations in a "include-what-you-use" move.
This then rippled through into a (more than 10!) number of files but
should "improve" things.
Note that the ordering of `#include` in many files seems to be rather
haphazard and is due for some serious overhaul - I suggest that we
should actually declare an "official" style for this project so that
everyone knows what it is.
**During the CI/CB process I discovered that Linux and then MacOS builds
were failing because the file referred to by the `#include
<QtConcurrentTask>` header file was missing, yet was present on my local
PC when I was using the Qt framework from the On-line installer.
Initially I suspected a Debian (and then Devuan - as the packaged
version on my own machine also had this defect AND Ubuntu) package
problem; however it now seems to be an upstream Qt issue as the various
Qt versions & OS combinations suggest that Qt themselves fixed it for Qt
6.10:**
| OS | QtVersion | Missing header |
|--------|-----------------------|----------------|
| Windows| 6.11.0 package | No |
| Devuan | 6.8.2 package | Yes |
| Devuan | 6.10.0 online install | No |
| Ubuntu | 6.9.0 package | Yes |
| Debian | 6.8.2 package | Yes |
| MacOS | 6.9.0 package | Yes |
**To fix this I reverted to an `#include <qtconcurrenttask.h>` for Linux
and MacOS builds - although it would probably have been better to make
it conditional on the Qt Version instead...**
*I have reported this upstream to Debian - see:
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1135197*
---------
Signed-off-by: Stephen Lyons <slysven@virginmedia.com>
2026-04-29 13:35:29 +01:00
|
|
|
#include "MudletInstanceCoordinator.h"
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
#include "TAccessibleTextEdit.h"
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
#include "THyperlinkStyling.h"
|
2026-03-21 01:58:22 -04:00
|
|
|
#include "TLinkStore.h"
|
|
|
|
|
#include "TMainConsole.h"
|
|
|
|
|
#include "TelnetServerStub.h"
|
|
|
|
|
#include "ctelnet.h"
|
|
|
|
|
#include "dlgConnectionProfiles.h"
|
|
|
|
|
#include "mudlet.h"
|
|
|
|
|
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
using namespace std::chrono_literals;
|
|
|
|
|
|
2026-03-21 01:58:22 -04:00
|
|
|
extern void qInitResources_mudlet();
|
|
|
|
|
extern void qInitResources_qm();
|
|
|
|
|
extern void qInitResources_additional_splash_screens();
|
|
|
|
|
extern void qInitResources_mudlet_fonts_common();
|
|
|
|
|
extern void qInitResources_mudlet_fonts_posix();
|
|
|
|
|
void initializeQRCResourcesForOscTest();
|
|
|
|
|
|
|
|
|
|
class TOscTest : public QObject {
|
|
|
|
|
Q_OBJECT
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
TelnetServerStub *mpServer = nullptr;
|
2026-03-26 15:01:24 +01:00
|
|
|
Host *mpHost = nullptr;
|
2026-03-21 01:58:22 -04:00
|
|
|
const QString mHostname = "OSC-Test-Host";
|
2026-07-20 09:01:51 +02:00
|
|
|
QString mPort; // assigned the stub's actual ephemeral port in initTestCase()
|
2026-03-21 01:58:22 -04:00
|
|
|
const QString mLocalhost = "localhost";
|
|
|
|
|
|
2026-03-26 15:01:24 +01:00
|
|
|
// Injects raw telnet data into the processing pipeline via loopback and
|
|
|
|
|
// waits for the buffer to process it.
|
|
|
|
|
void injectData(const QString &message) {
|
|
|
|
|
QByteArray data = (message + qsl("\r\n")).toUtf8();
|
|
|
|
|
mpHost->mTelnet.loopbackTest(data);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(50ms);
|
2026-03-26 15:01:24 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-21 01:58:22 -04:00
|
|
|
// Scans backward through the buffer to find the first hyperlink and returns
|
|
|
|
|
// its command list from the link store.
|
|
|
|
|
QStringList findFirstLinkCommands() {
|
2026-03-26 15:01:24 +01:00
|
|
|
if (!mpHost || !mpHost->mpConsole) {
|
2026-03-21 01:58:22 -04:00
|
|
|
return {};
|
|
|
|
|
}
|
2026-03-26 15:01:24 +01:00
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
2026-03-21 01:58:22 -04:00
|
|
|
int linkId = 0;
|
|
|
|
|
for (int line = console->buffer.getLastLineNumber();
|
|
|
|
|
line >= 0 && linkId == 0; --line) {
|
|
|
|
|
linkId = console->buffer.getLinkIndexAt(line, 0);
|
|
|
|
|
}
|
|
|
|
|
if (linkId <= 0) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
return console->getLinkStore().getLinksConst(linkId);
|
|
|
|
|
}
|
|
|
|
|
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
// Scans backward through the buffer to find the first hyperlink and returns
|
|
|
|
|
// its HyperlinkStyling from the link store.
|
|
|
|
|
Mudlet::HyperlinkStyling findFirstLinkStyling() {
|
2026-03-26 15:01:24 +01:00
|
|
|
if (!mpHost || !mpHost->mpConsole) {
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
return {};
|
|
|
|
|
}
|
2026-03-26 15:01:24 +01:00
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
int linkId = 0;
|
|
|
|
|
for (int line = console->buffer.getLastLineNumber();
|
|
|
|
|
line >= 0 && linkId == 0; --line) {
|
|
|
|
|
linkId = console->buffer.getLinkIndexAt(line, 0);
|
|
|
|
|
}
|
|
|
|
|
if (linkId <= 0) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
return console->getLinkStore().getStyling(linkId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Builds an OSC 8 hyperlink message with optional JSON config in the URL.
|
|
|
|
|
// The config is appended as a ?config= query parameter.
|
|
|
|
|
QString buildOsc8WithConfig(const QString &baseUrl, const QString &linkText,
|
|
|
|
|
const QString &jsonConfig) {
|
|
|
|
|
QString url = baseUrl;
|
|
|
|
|
if (!jsonConfig.isEmpty()) {
|
|
|
|
|
url += qsl("?config=") + jsonConfig;
|
|
|
|
|
}
|
|
|
|
|
return qsl("\x1b]8;;") + url + qsl("\x1b\\") + linkText +
|
|
|
|
|
qsl("\x1b]8;;\x1b\\");
|
|
|
|
|
}
|
|
|
|
|
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
// Scans forward through the buffer to find the first hyperlink and returns
|
|
|
|
|
// its (line, column) position, or std::nullopt if none found.
|
|
|
|
|
std::optional<std::pair<int, int>> findFirstLinkPosition() {
|
|
|
|
|
if (!mpHost || !mpHost->mpConsole) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
for (int line = 0; line <= console->buffer.getLastLineNumber(); ++line) {
|
|
|
|
|
for (int col = 0; col < console->buffer.line(line).length(); ++col) {
|
|
|
|
|
if (console->buffer.getLinkIndexAt(line, col) > 0) {
|
|
|
|
|
return std::pair{line, col};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scans forward through the buffer to find the first hyperlink and returns
|
|
|
|
|
// its (line, column, linkId), or std::nullopt if none found.
|
|
|
|
|
std::optional<std::tuple<int, int, int>> findFirstLinkPositionWithId() {
|
|
|
|
|
if (!mpHost || !mpHost->mpConsole) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
for (int line = 0; line <= console->buffer.getLastLineNumber(); ++line) {
|
|
|
|
|
for (int col = 0; col < console->buffer.line(line).length(); ++col) {
|
|
|
|
|
int lid = console->buffer.getLinkIndexAt(line, col);
|
|
|
|
|
if (lid > 0) {
|
|
|
|
|
return std::tuple{line, col, lid};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scans forward through the buffer to find the nth distinct hyperlink
|
|
|
|
|
// (1-based) and returns its (line, column, linkId), or std::nullopt.
|
|
|
|
|
std::optional<std::tuple<int, int, int>> findNthDistinctLinkPosition(int n) {
|
|
|
|
|
if (!mpHost || !mpHost->mpConsole) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
QSet<int> seenIds;
|
|
|
|
|
for (int line = 0; line <= console->buffer.getLastLineNumber(); ++line) {
|
|
|
|
|
for (int col = 0; col < console->buffer.line(line).length(); ++col) {
|
|
|
|
|
int lid = console->buffer.getLinkIndexAt(line, col);
|
|
|
|
|
if (lid > 0 && !seenIds.contains(lid)) {
|
|
|
|
|
seenIds.insert(lid);
|
|
|
|
|
if (seenIds.size() == n) {
|
|
|
|
|
return std::tuple{line, col, lid};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 10:16:39 +02:00
|
|
|
// Joins all buffer lines into one string.
|
|
|
|
|
QString allBufferText() {
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
QString allText;
|
|
|
|
|
for (int i = 0; i <= console->buffer.getLastLineNumber(); ++i) {
|
|
|
|
|
allText += console->buffer.line(i);
|
|
|
|
|
}
|
|
|
|
|
return allText;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 01:58:22 -04:00
|
|
|
private slots:
|
2026-03-26 15:01:24 +01:00
|
|
|
// Start mudlet and create a profile once for all tests.
|
|
|
|
|
void initTestCase() {
|
|
|
|
|
initializeQRCResourcesForOscTest();
|
2026-03-21 01:58:22 -04:00
|
|
|
|
|
|
|
|
mpServer = new TelnetServerStub(qApp);
|
2026-07-20 09:01:51 +02:00
|
|
|
mpServer->start(mLocalhost, 0); // ephemeral OS-assigned port avoids collisions across concurrent test runs
|
2026-07-27 19:17:18 +02:00
|
|
|
QVERIFY2(mpServer->isListening(),
|
|
|
|
|
"TelnetServerStub failed to bind a loopback port");
|
2026-07-20 09:01:51 +02:00
|
|
|
mPort = QString::number(mpServer->serverPort());
|
2026-03-21 01:58:22 -04:00
|
|
|
mudlet::start();
|
|
|
|
|
mudlet::self()->setupConfig();
|
|
|
|
|
mudlet::self()->takeOwnershipOfInstanceCoordinator(
|
|
|
|
|
std::make_unique<MudletInstanceCoordinator>(
|
|
|
|
|
"MudletInstanceCoordinator"));
|
|
|
|
|
mudlet::self()->init();
|
|
|
|
|
mudlet::self()->setStorePasswordsSecurely(false);
|
|
|
|
|
|
2026-03-26 15:01:24 +01:00
|
|
|
const QString path =
|
|
|
|
|
mudlet::getMudletPath(enums::profileHomePath, mHostname);
|
|
|
|
|
QDir(path).removeRecursively();
|
2026-03-21 01:58:22 -04:00
|
|
|
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTimer::singleShot(0ms, qApp, [this]() {
|
2026-03-26 15:01:24 +01:00
|
|
|
mudlet::self()->startAutoLogin({});
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::mouseClick(mudlet::self()->mpConnectionDialog->new_profile_button,
|
|
|
|
|
Qt::LeftButton);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::keyClicks(QApplication::focusWidget(), mHostname);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::keyClicks(QApplication::focusWidget(), mLocalhost);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::keyClicks(QApplication::focusWidget(), mPort);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return);
|
|
|
|
|
});
|
2026-03-21 01:58:22 -04:00
|
|
|
|
2026-03-26 15:01:24 +01:00
|
|
|
QSignalSpy spy(mudlet::self(), &mudlet::signal_profileLoaded);
|
|
|
|
|
if (!spy.wait(1000)) {
|
|
|
|
|
QFAIL("Profile took too long to load.");
|
|
|
|
|
}
|
|
|
|
|
mpHost = mudlet::self()->getActiveHost();
|
|
|
|
|
if (!mpHost) {
|
|
|
|
|
QFAIL("No active host available for the test.");
|
|
|
|
|
}
|
2026-03-21 01:58:22 -04:00
|
|
|
|
2026-03-26 15:01:24 +01:00
|
|
|
QSignalSpy spy2(&(mpHost->mTelnet), &cTelnet::signal_connected);
|
|
|
|
|
if (!spy2.wait(500)) {
|
|
|
|
|
QFAIL("Could not connect with the host.");
|
|
|
|
|
}
|
2026-03-21 01:58:22 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 15:01:24 +01:00
|
|
|
// Clear buffer and link state before each test for isolation.
|
|
|
|
|
void init() {
|
|
|
|
|
QVERIFY(mpHost);
|
|
|
|
|
QVERIFY(mpHost->mpConsole);
|
|
|
|
|
mpHost->mpConsole->buffer.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Data-driven test: verifies text after various OSC sequences is displayed
|
|
|
|
|
// correctly (not swallowed by the parser).
|
|
|
|
|
void test_OscTextDisplay_data() {
|
|
|
|
|
QTest::addColumn<QString>("message");
|
|
|
|
|
QTest::addColumn<QString>("expectedText");
|
|
|
|
|
// BEL-terminated OSC 8 splits link text across buffer lines in loopback
|
|
|
|
|
// mode, so exact match on the joined text isn't possible - use contains.
|
|
|
|
|
QTest::addColumn<bool>("exactMatch");
|
|
|
|
|
|
|
|
|
|
QTest::newRow("BEL-terminated OSC 2 (window title)")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< QString("\x1b]2;Window Title\x07Hello World") << qsl("Hello World")
|
|
|
|
|
<< true;
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("ST-terminated OSC P (color redefine)")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< QString("\x1b]P0FF0000\x1b\\Hello") << qsl("Hello") << true;
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("BEL-terminated OSC 8 (hyperlink)")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< QString(
|
|
|
|
|
"\x1b]8;;http://example.com\x07Link Text\x1b]8;;\x07 After Link")
|
2026-03-26 15:01:24 +01:00
|
|
|
<< qsl("Link Text After Link") << false;
|
|
|
|
|
QTest::newRow("BEL-terminated OSC P")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< QString("\x1b]P0FF0000\x07Hello") << qsl("Hello") << true;
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("empty OSC sequence")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< QString("\x1b]\x07Normal text") << qsl("Normal text") << true;
|
2026-07-27 19:17:18 +02:00
|
|
|
QTest::newRow("stray ESC c then literal bracket text")
|
|
|
|
|
<< QString("\x1b"
|
|
|
|
|
"c[literal] text")
|
|
|
|
|
<< qsl("[literal] text") << true;
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("OSC exceeds length limit")
|
|
|
|
|
<< QString("\x1b]2;") + QString(5000, 'A') + QString("\x07Normal text")
|
|
|
|
|
<< qsl("Normal text") << true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_OscTextDisplay() {
|
|
|
|
|
QFETCH(QString, message);
|
|
|
|
|
QFETCH(QString, expectedText);
|
|
|
|
|
QFETCH(bool, exactMatch);
|
|
|
|
|
|
|
|
|
|
injectData(message);
|
|
|
|
|
|
|
|
|
|
// Join all buffer lines since cursor position varies with loopback.
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
QString allText;
|
|
|
|
|
for (int i = 0; i <= console->buffer.getLastLineNumber(); ++i) {
|
|
|
|
|
allText += console->buffer.line(i);
|
|
|
|
|
}
|
|
|
|
|
if (exactMatch) {
|
|
|
|
|
QCOMPARE(allText.trimmed(), expectedText);
|
|
|
|
|
} else {
|
|
|
|
|
QVERIFY2(allText.contains(expectedText),
|
|
|
|
|
qPrintable(qsl("Expected buffer to contain '%1' but got '%2'")
|
|
|
|
|
.arg(expectedText, allText)));
|
|
|
|
|
}
|
2026-03-21 01:58:22 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 15:01:24 +01:00
|
|
|
// =====================================================================
|
2026-07-19 10:16:39 +02:00
|
|
|
// Split sequence / interleaved local feed tests
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
// A CSI/OSC sequence split across two server packets must still parse when
|
|
|
|
|
// locally generated text (a feedTriggers() call, an MMCP chat message)
|
|
|
|
|
// arrives in between: the local feed runs through the same parser and must
|
|
|
|
|
// not consume or clear the pending ESC latch of the server stream.
|
|
|
|
|
void test_SplitSequenceSurvivesInterleavedLocalFeed_data() {
|
|
|
|
|
QTest::addColumn<QString>("secondPacket");
|
|
|
|
|
QTest::addColumn<QString>("mustNotContain");
|
|
|
|
|
|
|
|
|
|
QTest::newRow("split CSI") << qsl("[32mAfter\n") << qsl("[32mAfter");
|
|
|
|
|
QTest::newRow("split OSC")
|
|
|
|
|
<< qsl("]2;Window Title\x07"
|
|
|
|
|
"After\n")
|
|
|
|
|
<< qsl("]2;Window Title");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_SplitSequenceSurvivesInterleavedLocalFeed() {
|
|
|
|
|
QFETCH(QString, secondPacket);
|
|
|
|
|
QFETCH(QString, mustNotContain);
|
|
|
|
|
|
|
|
|
|
// First server packet ends in a bare ESC...
|
|
|
|
|
std::string part1{"Before\n\x1b"};
|
|
|
|
|
mpHost->mpConsole->printOnDisplay(part1, true);
|
|
|
|
|
// ...then locally generated text arrives in between...
|
|
|
|
|
std::string localText{"local tick\n"};
|
|
|
|
|
mpHost->mpConsole->printOnDisplay(localText, false);
|
|
|
|
|
// ...then the server packet with the rest of the sequence:
|
|
|
|
|
std::string part2{secondPacket.toStdString()};
|
|
|
|
|
mpHost->mpConsole->printOnDisplay(part2, true);
|
|
|
|
|
|
|
|
|
|
const QString allText = allBufferText();
|
|
|
|
|
QVERIFY2(!allText.contains(mustNotContain),
|
|
|
|
|
qPrintable(qsl("Sequence fragment '%1' leaked into display: '%2'")
|
|
|
|
|
.arg(mustNotContain, allText)));
|
|
|
|
|
QVERIFY2(allText.contains(qsl("After")),
|
|
|
|
|
qPrintable(
|
|
|
|
|
qsl("Text after the split sequence went missing from: '%1'")
|
|
|
|
|
.arg(allText)));
|
|
|
|
|
QVERIFY2(allText.contains(qsl("local tick")),
|
|
|
|
|
qPrintable(qsl("Locally fed text went missing from: '%1'")
|
|
|
|
|
.arg(allText)));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 19:17:18 +02:00
|
|
|
// The don't-decode-this-payload flag set by a DCS/SOS/PM/APC introducer
|
|
|
|
|
// (mGotString) is carry-over parser state just like mGotOSC and must swap
|
|
|
|
|
// with the rest of it around a local feed. If it leaks: a complete OSC in
|
|
|
|
|
// the local feed is consumed but silently not decoded, and the server's DCS
|
|
|
|
|
// payload is wrongly decoded as an OSC when its terminator arrives.
|
|
|
|
|
void test_SplitDcsStringStateDoesNotLeakAcrossChannels() {
|
|
|
|
|
const bool savedMayRedefine = mpHost->getMayRedefineColors();
|
|
|
|
|
mpHost->setMayRedefineColors(true);
|
|
|
|
|
const QColor savedRed = mpHost->mRed;
|
|
|
|
|
const QColor savedGreen = mpHost->mGreen;
|
|
|
|
|
|
|
|
|
|
// First server packet ends inside a DCS (ESC P) with no terminator; its
|
|
|
|
|
// payload is shaped like an OSC colour redefinition for ANSI colour 2 so
|
|
|
|
|
// that wrongly feeding it to the OSC decoder is observable:
|
|
|
|
|
std::string part1{"Before\n\x1bPP2665544"};
|
|
|
|
|
mpHost->mpConsole->printOnDisplay(part1, true);
|
|
|
|
|
|
|
|
|
|
// Interleaved local feed carrying a complete OSC colour redefinition for
|
|
|
|
|
// ANSI colour 1 that must be decoded:
|
|
|
|
|
std::string localText{"\x1b]P1223344\x07local tick\n"};
|
|
|
|
|
mpHost->mpConsole->printOnDisplay(localText, false);
|
|
|
|
|
|
|
|
|
|
// The server DCS terminator arrives; the payload must be consumed
|
|
|
|
|
// without being decoded:
|
|
|
|
|
std::string part2{"\x07"
|
|
|
|
|
"After\n"};
|
|
|
|
|
mpHost->mpConsole->printOnDisplay(part2, true);
|
|
|
|
|
|
|
|
|
|
const QColor redAfter = mpHost->mRed;
|
|
|
|
|
const QColor greenAfter = mpHost->mGreen;
|
|
|
|
|
const QString allText = allBufferText();
|
|
|
|
|
|
|
|
|
|
// Restore profile colour state before asserting so a failure does not
|
|
|
|
|
// poison later tests:
|
|
|
|
|
mpHost->mRed = savedRed;
|
|
|
|
|
mpHost->mGreen = savedGreen;
|
|
|
|
|
mpHost->setMayRedefineColors(savedMayRedefine);
|
|
|
|
|
|
|
|
|
|
QVERIFY2(allText.contains(qsl("After")),
|
|
|
|
|
qPrintable(
|
|
|
|
|
qsl("Text after the split sequence went missing from: '%1'")
|
|
|
|
|
.arg(allText)));
|
|
|
|
|
QVERIFY2(allText.contains(qsl("local tick")),
|
|
|
|
|
qPrintable(qsl("Locally fed text went missing from: '%1'")
|
|
|
|
|
.arg(allText)));
|
|
|
|
|
QVERIFY2(!allText.contains(qsl("P2665544")),
|
|
|
|
|
qPrintable(qsl("DCS payload leaked into display: '%1'")
|
|
|
|
|
.arg(allText)));
|
|
|
|
|
QVERIFY2(redAfter == QColor(0x22, 0x33, 0x44),
|
|
|
|
|
qPrintable(qsl("Complete OSC inside the local feed was not "
|
|
|
|
|
"decoded: expected colour 1 to become #223344 "
|
|
|
|
|
"but it is %1")
|
|
|
|
|
.arg(redAfter.name())));
|
|
|
|
|
QVERIFY2(greenAfter == savedGreen,
|
|
|
|
|
qPrintable(qsl("Server DCS payload was wrongly fed to the OSC "
|
|
|
|
|
"decoder: colour 2 changed to %1")
|
|
|
|
|
.arg(greenAfter.name())));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 10:16:39 +02:00
|
|
|
// =====================================================================
|
2026-03-21 01:58:22 -04:00
|
|
|
// OSC 8 Hyperlink URL Parameter Tests
|
2026-03-26 15:01:24 +01:00
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
// Data-driven test: verifies OSC 8 URL query parameter handling - reserved
|
|
|
|
|
// params (config, preset) are stripped while user params are preserved.
|
|
|
|
|
void test_Osc8UrlParams_data() {
|
|
|
|
|
QTest::addColumn<QString>("message");
|
|
|
|
|
QTest::addColumn<QStringList>("mustContain");
|
|
|
|
|
QTest::addColumn<QStringList>("mustNotContain");
|
|
|
|
|
|
|
|
|
|
QTest::newRow("preserves web URL query params")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("\x1b]8;;https://example.com/"
|
|
|
|
|
"?id=42&lang=en\x1b\\Link\x1b]8;;\x1b\\")
|
|
|
|
|
<< QStringList{"id=42&lang=en"} << QStringList{};
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("strips config param")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("\x1b]8;;https://example.com/"
|
|
|
|
|
"?config=%7B%22style%22%3A%7B%22color%22%3A%22red%22%7D%"
|
|
|
|
|
"7D\x1b\\Styled\x1b]8;;\x1b\\")
|
|
|
|
|
<< QStringList{} << QStringList{"config="};
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("strips preset, preserves other params")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("\x1b]8;;https://example.com/"
|
|
|
|
|
"?page=1&preset=danger\x1b\\Link\x1b]8;;\x1b\\")
|
|
|
|
|
<< QStringList{"page=1"} << QStringList{"preset="};
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("strips preset with encoded equals")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("\x1b]8;;https://example.com/"
|
|
|
|
|
"?preset%3Ddefault&page=1\x1b\\Link\x1b]8;;\x1b\\")
|
|
|
|
|
<< QStringList{"page=1"} << QStringList{"preset%3D"};
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("strips config with encoded equals (lowercase)")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("\x1b]8;;https://example.com/"
|
|
|
|
|
"?config%3dvalue&foo=bar\x1b\\Link\x1b]8;;\x1b\\")
|
|
|
|
|
<< QStringList{"foo=bar"} << QStringList{"config%3d"};
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("preserves percent-encoded reserved names")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("\x1b]8;;https://example.com/"
|
|
|
|
|
"?%63%6F%6E%66%69%67=value\x1b\\Link\x1b]8;;\x1b\\")
|
|
|
|
|
<< QStringList{"%63%6F%6E%66%69%67=value"} << QStringList{};
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("send URL strips all query params")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("\x1b]8;;send:attack?config=%7B%22style%22%3A%7B%22color%22%3A%"
|
|
|
|
|
"22red%22%7D%7D\x1b\\Attack\x1b]8;;\x1b\\")
|
|
|
|
|
<< QStringList{"attack"} << QStringList{"config="};
|
2026-03-26 15:01:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_Osc8UrlParams() {
|
|
|
|
|
QFETCH(QString, message);
|
|
|
|
|
QFETCH(QStringList, mustContain);
|
|
|
|
|
QFETCH(QStringList, mustNotContain);
|
|
|
|
|
|
|
|
|
|
injectData(message);
|
2026-03-24 07:52:33 -04:00
|
|
|
|
|
|
|
|
QStringList commands = findFirstLinkCommands();
|
|
|
|
|
QVERIFY2(!commands.isEmpty(), "No hyperlink found in buffer");
|
2026-03-26 15:01:24 +01:00
|
|
|
for (const auto &expected : mustContain) {
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
QVERIFY2(
|
|
|
|
|
commands.first().contains(expected),
|
|
|
|
|
qPrintable(
|
|
|
|
|
qsl("Expected '%1' in: %2").arg(expected, commands.first())));
|
2026-03-26 15:01:24 +01:00
|
|
|
}
|
|
|
|
|
for (const auto &forbidden : mustNotContain) {
|
|
|
|
|
QVERIFY2(!commands.first().contains(forbidden),
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
qPrintable(qsl("Did not expect '%1' in: %2")
|
|
|
|
|
.arg(forbidden, commands.first())));
|
2026-03-26 15:01:24 +01:00
|
|
|
}
|
2026-03-21 01:58:22 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 15:01:24 +01:00
|
|
|
// =====================================================================
|
|
|
|
|
// OSC 8 Hyperlink Context Menu Title Tests
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
// Data-driven test: verifies menuTitle text is parsed correctly from various
|
|
|
|
|
// config JSON shapes (simple string, object, compact syntax, invalid types).
|
|
|
|
|
void test_Osc8Title_TextParsing_data() {
|
|
|
|
|
QTest::addColumn<QString>("config");
|
|
|
|
|
QTest::addColumn<QString>("expectedTitle");
|
|
|
|
|
QTest::addColumn<bool>("hasCustomStyling");
|
|
|
|
|
|
|
|
|
|
QTest::newRow("simple string")
|
|
|
|
|
<< qsl(R"({"title":"Lamb and Barley Stew","menu":[{"View Details":"send:look stew"}]})")
|
|
|
|
|
<< qsl("Lamb and Barley Stew") << false;
|
|
|
|
|
QTest::newRow("compact ti shorthand")
|
|
|
|
|
<< qsl(R"({"ti":"Rusty Sword","m":[{"Equip":"send:wield sword"}]})")
|
|
|
|
|
<< qsl("Rusty Sword") << false;
|
|
|
|
|
QTest::newRow("title without menu")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl(R"({"title":"Lonely Title"})") << qsl("Lonely Title") << false;
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("menu without title")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl(R"({"menu":[{"North":"send:north"}]})") << QString() << false;
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("empty string")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl(R"({"title":"","menu":[{"Action":"send:action"}]})") << QString()
|
|
|
|
|
<< false;
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("object with empty text")
|
|
|
|
|
<< qsl(R"({"title":{"text":"","style":{"color":"#ff0000"}},"menu":[{"Action":"send:action"}]})")
|
|
|
|
|
<< QString() << true;
|
|
|
|
|
QTest::newRow("object without text key")
|
|
|
|
|
<< qsl(R"({"title":{"style":{"color":"#ff0000"}},"menu":[{"Action":"send:action"}]})")
|
|
|
|
|
<< QString() << true;
|
|
|
|
|
QTest::newRow("object without style key")
|
|
|
|
|
<< qsl(R"({"title":{"text":"Style-less Title"},"menu":[{"Action":"send:action"}]})")
|
|
|
|
|
<< qsl("Style-less Title") << false;
|
|
|
|
|
QTest::newRow("unicode characters")
|
|
|
|
|
<< qsl(R"({"title":"Potion du Guerrier","menu":[{"Drink":"send:drink potion"}]})")
|
|
|
|
|
<< qsl("Potion du Guerrier") << false;
|
|
|
|
|
QTest::newRow("special characters")
|
|
|
|
|
<< qsl(R"({"title":"Item <Rare> [+5] & More!","menu":[{"Use":"send:use item"}]})")
|
|
|
|
|
<< qsl("Item <Rare> [+5] & More!") << false;
|
|
|
|
|
QTest::newRow("alongside other config")
|
|
|
|
|
<< qsl(R"({"title":"Full Config","tooltip":"A helpful tooltip","style":{"color":"#00ffff"},"menu":[{"Action 1":"send:action1"}]})")
|
|
|
|
|
<< qsl("Full Config") << true;
|
|
|
|
|
QTest::newRow("numeric value ignored")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl(R"({"title":42,"menu":[{"Action":"send:action"}]})") << QString()
|
|
|
|
|
<< false;
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("boolean value ignored")
|
|
|
|
|
<< qsl(R"({"title":true,"menu":[{"Action":"send:action"}]})")
|
|
|
|
|
<< QString() << false;
|
|
|
|
|
QTest::newRow("array value ignored")
|
|
|
|
|
<< qsl(R"({"title":["a","b"],"menu":[{"Action":"send:action"}]})")
|
|
|
|
|
<< QString() << false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_Osc8Title_TextParsing() {
|
|
|
|
|
QFETCH(QString, config);
|
|
|
|
|
QFETCH(QString, expectedTitle);
|
|
|
|
|
QFETCH(bool, hasCustomStyling);
|
|
|
|
|
|
|
|
|
|
injectData(buildOsc8WithConfig(qsl("send:action"), qsl("[Link]"), config));
|
|
|
|
|
|
|
|
|
|
// Verify a link was actually created in the buffer - prevents silent
|
|
|
|
|
// passes when the injection pipeline is broken entirely.
|
2026-03-21 01:58:22 -04:00
|
|
|
QStringList commands = findFirstLinkCommands();
|
|
|
|
|
QVERIFY2(!commands.isEmpty(), "No hyperlink found in buffer");
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
|
|
|
|
|
auto styling = findFirstLinkStyling();
|
2026-03-26 15:01:24 +01:00
|
|
|
QCOMPARE(styling.menuTitle, expectedTitle);
|
|
|
|
|
if (!hasCustomStyling) {
|
|
|
|
|
QVERIFY2(!styling.menuTitleStyle.hasCustomStyling,
|
|
|
|
|
"Expected no custom styling on menu title");
|
|
|
|
|
}
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 15:01:24 +01:00
|
|
|
// Data-driven test: verifies menuTitleStyle properties are parsed correctly.
|
|
|
|
|
void test_Osc8Title_StyleParsing_data() {
|
|
|
|
|
QTest::addColumn<QString>("config");
|
|
|
|
|
QTest::addColumn<QString>("expectedTitle");
|
|
|
|
|
QTest::addColumn<bool>("bold");
|
|
|
|
|
QTest::addColumn<bool>("italic");
|
|
|
|
|
QTest::addColumn<bool>("underlined");
|
|
|
|
|
QTest::addColumn<bool>("strikeOut");
|
|
|
|
|
QTest::addColumn<QString>("fgColor");
|
|
|
|
|
QTest::addColumn<QString>("bgColor");
|
|
|
|
|
QTest::addColumn<int>("underlineStyle");
|
|
|
|
|
QTest::addColumn<QString>("underlineColor");
|
|
|
|
|
|
|
|
|
|
QTest::newRow("bold and color")
|
|
|
|
|
<< qsl(R"({"title":{"text":"Magic Shop - Potions","style":{"color":"#ffd700","bold":true}},"menu":[{"Buy":"send:buy potion"}]})")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("Magic Shop - Potions") << true << false << false << false
|
2026-03-26 15:01:24 +01:00
|
|
|
<< qsl("#ffd700") << QString() << -1 << QString();
|
|
|
|
|
QTest::newRow("italic and background")
|
|
|
|
|
<< qsl(R"({"title":{"text":"Sir Galahad the Brave","style":{"color":"#ffffff","bg":"#333333","italic":true}},"menu":[{"Talk":"send:talk galahad"}]})")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("Sir Galahad the Brave") << false << true << false << false
|
2026-03-26 15:01:24 +01:00
|
|
|
<< qsl("#ffffff") << qsl("#333333") << -1 << QString();
|
|
|
|
|
QTest::newRow("all text decorations")
|
|
|
|
|
<< qsl(R"({"title":{"text":"Decorated Title","style":{"color":"#ff0000","bold":true,"italic":true,"underline":true,"strikethrough":true}},"menu":[{"Action":"send:action"}]})")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("Decorated Title") << true << true << true << true
|
2026-03-26 15:01:24 +01:00
|
|
|
<< qsl("#ff0000") << QString() << -1 << QString();
|
|
|
|
|
QTest::newRow("wavy underline")
|
|
|
|
|
<< qsl(R"({"title":{"text":"Wavy Title","style":{"color":"#00ff00","underline":"wavy"}},"menu":[{"Test":"send:test"}]})")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("Wavy Title") << false << false << true << false
|
2026-03-26 15:01:24 +01:00
|
|
|
<< qsl("#00ff00") << QString()
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< static_cast<int>(Mudlet::HyperlinkStyling::UnderlineWavy)
|
|
|
|
|
<< QString();
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("dotted underline")
|
|
|
|
|
<< qsl(R"({"title":{"text":"Dotted Title","style":{"color":"#00ff00","underline":"dotted"}},"menu":[{"Test":"send:test"}]})")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("Dotted Title") << false << false << true << false
|
2026-03-26 15:01:24 +01:00
|
|
|
<< qsl("#00ff00") << QString()
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< static_cast<int>(Mudlet::HyperlinkStyling::UnderlineDotted)
|
|
|
|
|
<< QString();
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("dashed underline")
|
|
|
|
|
<< qsl(R"({"title":{"text":"Dashed Title","style":{"color":"#00ff00","underline":"dashed"}},"menu":[{"Test":"send:test"}]})")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("Dashed Title") << false << false << true << false
|
2026-03-26 15:01:24 +01:00
|
|
|
<< qsl("#00ff00") << QString()
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< static_cast<int>(Mudlet::HyperlinkStyling::UnderlineDashed)
|
|
|
|
|
<< QString();
|
2026-03-26 15:01:24 +01:00
|
|
|
QTest::newRow("background-color CSS property")
|
|
|
|
|
<< qsl(R"({"title":{"text":"CSS BG Title","style":{"color":"#ffffff","background-color":"#660000"}},"menu":[{"Action":"send:action"}]})")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("CSS BG Title") << false << false << false << false
|
2026-03-26 15:01:24 +01:00
|
|
|
<< qsl("#ffffff") << qsl("#660000") << -1 << QString();
|
|
|
|
|
QTest::newRow("text-decoration-color")
|
|
|
|
|
<< qsl(R"({"title":{"text":"Color Decoration","style":{"color":"#ffffff","underline":true,"text-decoration-color":"#ff00ff"}},"menu":[{"Action":"send:action"}]})")
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
<< qsl("Color Decoration") << false << false << true << false
|
2026-03-26 15:01:24 +01:00
|
|
|
<< qsl("#ffffff") << QString() << -1 << qsl("#ff00ff");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_Osc8Title_StyleParsing() {
|
|
|
|
|
QFETCH(QString, config);
|
|
|
|
|
QFETCH(QString, expectedTitle);
|
|
|
|
|
QFETCH(bool, bold);
|
|
|
|
|
QFETCH(bool, italic);
|
|
|
|
|
QFETCH(bool, underlined);
|
|
|
|
|
QFETCH(bool, strikeOut);
|
|
|
|
|
QFETCH(QString, fgColor);
|
|
|
|
|
QFETCH(QString, bgColor);
|
|
|
|
|
QFETCH(int, underlineStyle);
|
|
|
|
|
QFETCH(QString, underlineColor);
|
|
|
|
|
|
|
|
|
|
injectData(buildOsc8WithConfig(qsl("send:action"), qsl("[Link]"), config));
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
|
|
|
|
|
auto styling = findFirstLinkStyling();
|
2026-03-26 15:01:24 +01:00
|
|
|
QCOMPARE(styling.menuTitle, expectedTitle);
|
|
|
|
|
QCOMPARE(styling.menuTitleStyle.isBold, bold);
|
|
|
|
|
QCOMPARE(styling.menuTitleStyle.isItalic, italic);
|
|
|
|
|
QCOMPARE(styling.menuTitleStyle.isUnderlined, underlined);
|
|
|
|
|
QCOMPARE(styling.menuTitleStyle.isStrikeOut, strikeOut);
|
|
|
|
|
if (!fgColor.isEmpty()) {
|
|
|
|
|
QVERIFY(styling.menuTitleStyle.hasForegroundColor);
|
|
|
|
|
QCOMPARE(styling.menuTitleStyle.foregroundColor, QColor(fgColor));
|
|
|
|
|
}
|
|
|
|
|
if (!bgColor.isEmpty()) {
|
|
|
|
|
QVERIFY(styling.menuTitleStyle.hasBackgroundColor);
|
|
|
|
|
QCOMPARE(styling.menuTitleStyle.backgroundColor, QColor(bgColor));
|
|
|
|
|
}
|
|
|
|
|
if (underlineStyle >= 0) {
|
|
|
|
|
QCOMPARE(styling.menuTitleStyle.underlineStyle,
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
static_cast<Mudlet::HyperlinkStyling::UnderlineStyle>(
|
|
|
|
|
underlineStyle));
|
2026-03-26 15:01:24 +01:00
|
|
|
}
|
|
|
|
|
if (!underlineColor.isEmpty()) {
|
|
|
|
|
QVERIFY(styling.menuTitleStyle.hasUnderlineColor);
|
|
|
|
|
QCOMPARE(styling.menuTitleStyle.underlineColor, QColor(underlineColor));
|
|
|
|
|
}
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_Osc8Title_LinkTextDisplayedInBuffer() {
|
|
|
|
|
QString config = qsl(
|
|
|
|
|
R"({"title":"Lamb and Barley Stew","menu":[{"View Details":"send:look stew"}]})");
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
injectData(buildOsc8WithConfig(qsl("send:look stew"),
|
|
|
|
|
qsl("[Lamb and Barley Stew]"), config));
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
|
2026-03-26 15:01:24 +01:00
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
QString allText;
|
|
|
|
|
for (int i = 0; i <= console->buffer.getLastLineNumber(); ++i) {
|
|
|
|
|
allText += console->buffer.line(i);
|
|
|
|
|
}
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
QVERIFY2(
|
2026-03-26 15:01:24 +01:00
|
|
|
allText.contains(qsl("[Lamb and Barley Stew]")),
|
Improve: ARIA accessibility for OSC 8 hyperlinks in caret mode (#9206)
#### Brief overview of PR changes/additions
Adds screen reader accessibility support for OSC 8 hyperlinks when using
caret mode. Users can now navigate between links with Tab/Shift+Tab,
activate links with Enter/Space, and open link context menus with the
Menu key or Shift+F10. Links announce their tooltip,
visited/disabled/selected state, and menu availability to assistive
technology. IAccessible2 text attributes now expose link URLs, visited
state, disabled state, selection state, and group membership. Hidden
links are announced when the visibility manager conceals them. The
Accessibility settings combobox that picks the caret-mode pane-switching
key now lists these link shortcuts in its tooltip and accessible
description, announces them when the key is changed, and warns if Tab is
selected (since Tab is then shared with link navigation). Ctrl+End /
Ctrl+Home jump to the latest content / start of the buffer with audible
announcements so screen-reader users can opt in to following new output
without losing their current link focus.
**Scope note:** While this PR was motivated by OSC 8 hyperlinks, the
keyboard navigation and screen reader features work with **all link
types** (MXP, `echoLink`, `cechoLink`, etc.). The link system uses a
unified `linkIndex` with no type field, so Tab/Shift+Tab, Enter/Space
activation, `announceLinkFocus`, and IAccessible2 text attributes apply
to every link in the buffer. The only OSC 8-specific behaviors are
additive: spoiler reveal, selection group toggling, disabled state, and
custom styling—these gracefully no-op for links that lack styling data.
#### Motivation for adding to Mudlet
MUD games rely heavily on clickable hyperlinks for interaction, and
blind or low-vision players using screen readers had no way to discover,
navigate, or activate these links from the keyboard. This brings
Mudlet's hyperlink system in line with web accessibility standards.
#### Other info (issues closed, discussion etc)
[Wiki
Update](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Accessibility)
- Tests are included.
- `say !osc8-docs` will work from anywhere, or type `osc8` on StickMUD
and `look` to try out the test room.
- For accessibility, you can try out caret mode (go to Accessibility tab
on Mudlet settings and pick a shortcut like F6).
- Navigate links with Tab/Shift+Tab, activate with Enter or Space, open
the link context menu with Menu or Shift+F10, and use Ctrl+End /
Ctrl+Home to jump to the latest content or the start of the buffer.
- Use arrow keys on a menu, space or enter to select a menu item or
follow the hyperlinks.
- Output-window navigation in caret mode has been tuned for snappier
response.
2026-04-25 10:19:05 -04:00
|
|
|
qPrintable(
|
|
|
|
|
qsl("Expected link text in buffer but got '%1'").arg(allText)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// OSC 8 Hyperlink Navigation Tests (Tab/Shift+Tab support)
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
void test_FindNextLink_MultipleLinks() {
|
|
|
|
|
// Inject two distinct links on the same line
|
|
|
|
|
injectData(qsl("\x1b]8;;send:north\x1b\\North\x1b]8;;\x1b\\ - "
|
|
|
|
|
"\x1b]8;;send:south\x1b\\South\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
// Starting from position 0 (on the first link), findNextLink should find
|
|
|
|
|
// the second
|
|
|
|
|
int outLine = -1, outCol = -1;
|
|
|
|
|
bool found = console->buffer.findNextLink(0, 0, outLine, outCol);
|
|
|
|
|
QVERIFY2(found, "Expected to find a second link after the first");
|
|
|
|
|
|
|
|
|
|
// The second link should be at a column past "North - "
|
|
|
|
|
int secondLinkIndex = console->buffer.getLinkIndexAt(outLine, outCol);
|
|
|
|
|
QVERIFY2(secondLinkIndex > 0,
|
|
|
|
|
"Expected a valid link index at the found position");
|
|
|
|
|
|
|
|
|
|
// Verify it's actually a different link from the first
|
|
|
|
|
int firstLinkIndex = console->buffer.getLinkIndexAt(0, 0);
|
|
|
|
|
QVERIFY2(
|
|
|
|
|
firstLinkIndex != secondLinkIndex,
|
|
|
|
|
"findNextLink should return a different link than the starting one");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_FindNextLink_NoLinks() {
|
|
|
|
|
// Inject plain text with no links
|
|
|
|
|
injectData(qsl("Just some plain text with no links"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
int outLine = -1, outCol = -1;
|
|
|
|
|
bool found = console->buffer.findNextLink(0, 0, outLine, outCol);
|
|
|
|
|
QVERIFY2(!found, "findNextLink should return false when no links exist");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_FindNextLink_SkipsCurrentLink() {
|
|
|
|
|
// Inject a single link
|
|
|
|
|
injectData(qsl("\x1b]8;;send:look\x1b\\Look around\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
// Find where the link starts
|
|
|
|
|
auto pos = findFirstLinkPosition();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol] = *pos;
|
|
|
|
|
|
|
|
|
|
// Starting from the link, there should be no "next" link
|
|
|
|
|
int outLine = -1, outCol = -1;
|
|
|
|
|
bool found2 =
|
|
|
|
|
console->buffer.findNextLink(linkLine, linkCol, outLine, outCol);
|
|
|
|
|
QVERIFY2(!found2,
|
|
|
|
|
"findNextLink should skip the current link and find nothing else");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_FindPreviousLink_MultipleLinks() {
|
|
|
|
|
// Inject two distinct links
|
|
|
|
|
injectData(qsl("\x1b]8;;send:north\x1b\\North\x1b]8;;\x1b\\ - "
|
|
|
|
|
"\x1b]8;;send:south\x1b\\South\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
// Find the first and second distinct links
|
|
|
|
|
auto first = findNthDistinctLinkPosition(1);
|
|
|
|
|
QVERIFY2(first.has_value(), "Expected to find a first link in the buffer");
|
|
|
|
|
auto [firstLine, firstCol, firstLinkId] = *first;
|
|
|
|
|
|
|
|
|
|
auto second = findNthDistinctLinkPosition(2);
|
|
|
|
|
QVERIFY2(second.has_value(),
|
|
|
|
|
"Expected to find a second link in the buffer");
|
|
|
|
|
auto [secondLine, secondCol, secondLinkId] = *second;
|
|
|
|
|
|
|
|
|
|
// From the second link, findPreviousLink should find the first
|
|
|
|
|
int outLine = -1, outCol = -1;
|
|
|
|
|
bool found = console->buffer.findPreviousLink(secondLine, secondCol,
|
|
|
|
|
outLine, outCol);
|
|
|
|
|
QVERIFY2(found, "Expected to find the first link when scanning backward "
|
|
|
|
|
"from the second");
|
|
|
|
|
|
|
|
|
|
int foundLinkId = console->buffer.getLinkIndexAt(outLine, outCol);
|
|
|
|
|
QCOMPARE(foundLinkId, firstLinkId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_FindPreviousLink_AtStartOfBuffer() {
|
|
|
|
|
// Inject a single link
|
|
|
|
|
injectData(qsl("\x1b]8;;send:look\x1b\\Look\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
// Starting from 0,0 there should be no previous link
|
|
|
|
|
int outLine = -1, outCol = -1;
|
|
|
|
|
bool found = console->buffer.findPreviousLink(0, 0, outLine, outCol);
|
|
|
|
|
QVERIFY2(!found,
|
|
|
|
|
"findPreviousLink should return false at the start of the buffer");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// OSC 8 Hyperlink Tooltip Tests
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
void test_GetLinkTooltip_data() {
|
|
|
|
|
QTest::addColumn<QString>("config");
|
|
|
|
|
QTest::addColumn<QString>("expectedTooltip");
|
|
|
|
|
|
|
|
|
|
QTest::newRow("tooltip from config")
|
|
|
|
|
<< qsl(R"({"tooltip":"A magical sword","style":{"color":"#ff0000"}})")
|
|
|
|
|
<< qsl("A magical sword");
|
|
|
|
|
QTest::newRow("no tooltip in config - uses default send hint")
|
|
|
|
|
<< qsl(R"({"style":{"color":"#00ff00"}})") << qsl("Send: look sword");
|
|
|
|
|
QTest::newRow("empty config - uses default send hint")
|
|
|
|
|
<< QString() << qsl("Send: look sword");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_GetLinkTooltip() {
|
|
|
|
|
QFETCH(QString, config);
|
|
|
|
|
QFETCH(QString, expectedTooltip);
|
|
|
|
|
|
|
|
|
|
injectData(
|
|
|
|
|
buildOsc8WithConfig(qsl("send:look sword"), qsl("[Sword]"), config));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
int linkId = 0;
|
|
|
|
|
for (int line = console->buffer.getLastLineNumber();
|
|
|
|
|
line >= 0 && linkId == 0; --line) {
|
|
|
|
|
linkId = console->buffer.getLinkIndexAt(line, 0);
|
|
|
|
|
}
|
|
|
|
|
QVERIFY2(linkId > 0, "Expected to find a link in the buffer");
|
|
|
|
|
|
|
|
|
|
QString tooltip = console->buffer.getLinkTooltip(linkId);
|
|
|
|
|
QCOMPARE(tooltip, expectedTooltip);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_GetLinkTooltip_InvalidIndex() {
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
// Index 0 and negative should return empty
|
|
|
|
|
QCOMPARE(console->buffer.getLinkTooltip(0), QString());
|
|
|
|
|
QCOMPARE(console->buffer.getLinkTooltip(-1), QString());
|
|
|
|
|
// Non-existent positive index should return empty
|
|
|
|
|
QCOMPARE(console->buffer.getLinkTooltip(99999), QString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// OSC 8 Hyperlink Visited State Tests
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
void test_LinkVisitedState() {
|
|
|
|
|
injectData(qsl("\x1b]8;;send:look\x1b\\Look\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
int linkId = 0;
|
|
|
|
|
for (int line = console->buffer.getLastLineNumber();
|
|
|
|
|
line >= 0 && linkId == 0; --line) {
|
|
|
|
|
linkId = console->buffer.getLinkIndexAt(line, 0);
|
|
|
|
|
}
|
|
|
|
|
QVERIFY2(linkId > 0, "Expected to find a link in the buffer");
|
|
|
|
|
|
|
|
|
|
// Should not be visited initially
|
|
|
|
|
QVERIFY2(!console->buffer.isLinkVisited(linkId),
|
|
|
|
|
"Link should not be visited initially");
|
|
|
|
|
|
|
|
|
|
// Mark as visited
|
|
|
|
|
console->buffer.markLinkAsVisited(linkId);
|
|
|
|
|
QVERIFY2(console->buffer.isLinkVisited(linkId),
|
|
|
|
|
"Link should be visited after markLinkAsVisited");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// OSC 8 Hyperlink Accessible Attributes Tests
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
void test_LinkAttributes_ExposedInAccessible() {
|
|
|
|
|
injectData(qsl("\x1b]8;;https://example.com\x1b\\Click me\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
// Find the link position in the buffer
|
|
|
|
|
auto pos = findFirstLinkPosition();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol] = *pos;
|
|
|
|
|
|
|
|
|
|
// Create the accessible interface for the upper pane
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY2(pane, "Expected upper pane to exist");
|
|
|
|
|
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
// Compute the offset for the link position
|
|
|
|
|
int offset = 0;
|
|
|
|
|
for (int i = 0; i < linkLine; ++i) {
|
|
|
|
|
offset += console->buffer.line(i).length() + 1; // +1 for \n
|
|
|
|
|
}
|
|
|
|
|
offset += linkCol;
|
|
|
|
|
|
|
|
|
|
// Query attributes at the link offset
|
|
|
|
|
int startOffset = 0, endOffset = 0;
|
|
|
|
|
QString attrs = accessible.attributes(offset, &startOffset, &endOffset);
|
|
|
|
|
|
|
|
|
|
// Verify link-specific attributes are present
|
|
|
|
|
QVERIFY2(
|
|
|
|
|
attrs.contains(qsl("text-link:")),
|
|
|
|
|
qPrintable(
|
|
|
|
|
qsl("Expected 'text-link:' in attributes but got: %1").arg(attrs)));
|
|
|
|
|
QVERIFY2(
|
|
|
|
|
attrs.contains(qsl("example.com")),
|
|
|
|
|
qPrintable(qsl("Expected URL in attributes but got: %1").arg(attrs)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_LinkAttributes_NotPresentOnPlainText() {
|
|
|
|
|
injectData(qsl("Just plain text here"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY2(pane, "Expected upper pane to exist");
|
|
|
|
|
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
// Query attributes at position 0 (plain text)
|
|
|
|
|
int startOffset = 0, endOffset = 0;
|
|
|
|
|
QString attrs = accessible.attributes(0, &startOffset, &endOffset);
|
|
|
|
|
|
|
|
|
|
// Verify no link attributes are present
|
|
|
|
|
QVERIFY2(!attrs.contains(qsl("text-link:")),
|
|
|
|
|
qPrintable(
|
|
|
|
|
qsl("Did not expect 'text-link:' in plain text attributes: %1")
|
|
|
|
|
.arg(attrs)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_LinkAttributes_VisitedState() {
|
|
|
|
|
injectData(qsl("\x1b]8;;send:look\x1b\\Look\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
// Find the link
|
|
|
|
|
auto pos = findFirstLinkPositionWithId();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol, linkId] = *pos;
|
|
|
|
|
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
for (int i = 0; i < linkLine; ++i) {
|
|
|
|
|
offset += console->buffer.line(i).length() + 1;
|
|
|
|
|
}
|
|
|
|
|
offset += linkCol;
|
|
|
|
|
|
|
|
|
|
// Before visiting: should not have visited attribute
|
|
|
|
|
int s = 0, e = 0;
|
|
|
|
|
QString attrsBefore = accessible.attributes(offset, &s, &e);
|
|
|
|
|
QVERIFY2(!attrsBefore.contains(qsl("text-link-visited:true")),
|
|
|
|
|
"Link should not show visited before being visited");
|
|
|
|
|
|
|
|
|
|
// Mark as visited
|
|
|
|
|
console->buffer.markLinkAsVisited(linkId);
|
|
|
|
|
|
|
|
|
|
// After visiting: should have visited attribute
|
|
|
|
|
QString attrsAfter = accessible.attributes(offset, &s, &e);
|
|
|
|
|
QVERIFY2(
|
|
|
|
|
attrsAfter.contains(qsl("text-link-visited:true")),
|
|
|
|
|
qPrintable(
|
|
|
|
|
qsl("Expected 'text-link-visited:true' after visiting but got: %1")
|
|
|
|
|
.arg(attrsAfter)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_LinkAttributes_DisabledState() {
|
|
|
|
|
injectData(qsl("\x1b]8;;send:look\x1b\\Disabled Link\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
auto pos = findFirstLinkPositionWithId();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol, linkId] = *pos;
|
|
|
|
|
|
|
|
|
|
// Set link state to disabled
|
|
|
|
|
console->buffer.setLinkState(linkId,
|
|
|
|
|
Mudlet::HyperlinkStyling::StateDisabled);
|
|
|
|
|
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
for (int i = 0; i < linkLine; ++i) {
|
|
|
|
|
offset += console->buffer.line(i).length() + 1;
|
|
|
|
|
}
|
|
|
|
|
offset += linkCol;
|
|
|
|
|
|
|
|
|
|
int s = 0, e = 0;
|
|
|
|
|
QString attrs = accessible.attributes(offset, &s, &e);
|
|
|
|
|
QVERIFY2(
|
|
|
|
|
attrs.contains(qsl("text-link-disabled:true;")),
|
|
|
|
|
qPrintable(
|
|
|
|
|
qsl("Expected 'text-link-disabled:true;' in attributes but got: %1")
|
|
|
|
|
.arg(attrs)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_LinkAttributes_SelectedState() {
|
|
|
|
|
injectData(qsl("\x1b]8;;send:look\x1b\\Select Me\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
auto pos = findFirstLinkPositionWithId();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol, linkId] = *pos;
|
|
|
|
|
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
for (int i = 0; i < linkLine; ++i) {
|
|
|
|
|
offset += console->buffer.line(i).length() + 1;
|
|
|
|
|
}
|
|
|
|
|
offset += linkCol;
|
|
|
|
|
|
|
|
|
|
// Before selecting: should not have selected attribute
|
|
|
|
|
int s = 0, e = 0;
|
|
|
|
|
QString attrsBefore = accessible.attributes(offset, &s, &e);
|
|
|
|
|
QVERIFY2(!attrsBefore.contains(qsl("text-link-selected:true")),
|
|
|
|
|
"Link should not show selected before being selected");
|
|
|
|
|
|
|
|
|
|
// Mark as selected
|
|
|
|
|
console->buffer.setLinkSelected(linkId, true);
|
|
|
|
|
|
|
|
|
|
// After selecting: should have selected attribute
|
|
|
|
|
QString attrsAfter = accessible.attributes(offset, &s, &e);
|
|
|
|
|
QVERIFY2(attrsAfter.contains(qsl("text-link-selected:true;")),
|
|
|
|
|
qPrintable(qsl("Expected 'text-link-selected:true;' after "
|
|
|
|
|
"selecting but got: %1")
|
|
|
|
|
.arg(attrsAfter)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_LinkAttributes_UrlSanitization() {
|
|
|
|
|
injectData(
|
|
|
|
|
qsl("\x1b]8;;https://example.com:8080/path\x1b\\Look\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
auto pos = findFirstLinkPosition();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol] = *pos;
|
|
|
|
|
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
for (int i = 0; i < linkLine; ++i) {
|
|
|
|
|
offset += console->buffer.line(i).length() + 1;
|
|
|
|
|
}
|
|
|
|
|
offset += linkCol;
|
|
|
|
|
|
|
|
|
|
int s = 0, e = 0;
|
|
|
|
|
QString attrs = accessible.attributes(offset, &s, &e);
|
|
|
|
|
// Colons in the URL should be escaped as \:
|
|
|
|
|
QVERIFY2(attrs.contains(qsl("\\:")),
|
|
|
|
|
qPrintable(qsl("Expected escaped colons (\\:) in text-link "
|
|
|
|
|
"attribute but got: %1")
|
|
|
|
|
.arg(attrs)));
|
|
|
|
|
// Verify the attribute does not contain an unescaped colon after
|
|
|
|
|
// "text-link:" The "text-link:" prefix itself has an unescaped colon, but
|
|
|
|
|
// colons within the URL value must be escaped
|
|
|
|
|
int linkAttrStart = attrs.indexOf(qsl("text-link:"));
|
|
|
|
|
QVERIFY(linkAttrStart >= 0);
|
|
|
|
|
QString linkValue = attrs.mid(linkAttrStart + 10); // after "text-link:"
|
|
|
|
|
int semiPos = linkValue.indexOf(QLatin1Char(';'));
|
|
|
|
|
QVERIFY(semiPos > 0);
|
|
|
|
|
linkValue = linkValue.left(semiPos);
|
|
|
|
|
// Every colon in the value should be preceded by a backslash
|
|
|
|
|
for (int i = 0; i < linkValue.size(); ++i) {
|
|
|
|
|
if (linkValue.at(i) == QLatin1Char(':')) {
|
|
|
|
|
QVERIFY2(i > 0 && linkValue.at(i - 1) == QLatin1Char('\\'),
|
|
|
|
|
qPrintable(qsl("Found unescaped colon in text-link value: %1")
|
|
|
|
|
.arg(linkValue)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_LinkAttributes_SemicolonAndBackslashEscaping() {
|
|
|
|
|
// URL contains semicolons and backslashes that must be escaped in
|
|
|
|
|
// IAccessible2 text attributes
|
|
|
|
|
injectData(qsl("\x1b]8;;https://example.com/"
|
|
|
|
|
"path;param\\extra\x1b\\Look\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
auto pos = findFirstLinkPosition();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol] = *pos;
|
|
|
|
|
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
for (int i = 0; i < linkLine; ++i) {
|
|
|
|
|
offset += console->buffer.line(i).length() + 1;
|
|
|
|
|
}
|
|
|
|
|
offset += linkCol;
|
|
|
|
|
|
|
|
|
|
int s = 0, e = 0;
|
|
|
|
|
QString attrs = accessible.attributes(offset, &s, &e);
|
|
|
|
|
|
|
|
|
|
// Extract the text-link value, skipping escaped semicolons (\;)
|
|
|
|
|
int linkAttrStart = attrs.indexOf(qsl("text-link:"));
|
|
|
|
|
QVERIFY(linkAttrStart >= 0);
|
|
|
|
|
QString linkValue = attrs.mid(linkAttrStart + 10);
|
|
|
|
|
int semiEnd = -1;
|
|
|
|
|
for (int i = 0; i < linkValue.length(); ++i) {
|
|
|
|
|
if (linkValue.at(i) == QLatin1Char(';') &&
|
|
|
|
|
(i == 0 || linkValue.at(i - 1) != QLatin1Char('\\'))) {
|
|
|
|
|
semiEnd = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
QVERIFY(semiEnd > 0);
|
|
|
|
|
linkValue = linkValue.left(semiEnd);
|
|
|
|
|
|
|
|
|
|
// Semicolons within the URL value should be escaped as \;
|
|
|
|
|
QVERIFY2(linkValue.contains(qsl("\\;")),
|
|
|
|
|
qPrintable(qsl("Expected escaped semicolons in text-link "
|
|
|
|
|
"value but got: %1")
|
|
|
|
|
.arg(linkValue)));
|
|
|
|
|
// Backslashes within the URL value should be escaped as double backslash
|
|
|
|
|
const QString escapedBackslash = qsl("\\\\");
|
|
|
|
|
QVERIFY2(linkValue.contains(escapedBackslash),
|
|
|
|
|
qPrintable(qsl("Expected escaped backslashes in text-link "
|
|
|
|
|
"value but got: %1")
|
|
|
|
|
.arg(linkValue)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_LinkAttributes_GroupAttribute() {
|
|
|
|
|
// Link with a selection group should expose text-link-group attribute
|
|
|
|
|
QString config = qsl(
|
|
|
|
|
R"({"selection":{"group":"combat","value":"sword","toggle":true}})");
|
|
|
|
|
injectData(
|
|
|
|
|
buildOsc8WithConfig(qsl("send:equip sword"), qsl("[Sword]"), config));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
auto pos = findFirstLinkPosition();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol] = *pos;
|
|
|
|
|
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
for (int i = 0; i < linkLine; ++i) {
|
|
|
|
|
offset += console->buffer.line(i).length() + 1;
|
|
|
|
|
}
|
|
|
|
|
offset += linkCol;
|
|
|
|
|
|
|
|
|
|
int s = 0, e = 0;
|
|
|
|
|
QString attrs = accessible.attributes(offset, &s, &e);
|
|
|
|
|
|
|
|
|
|
QVERIFY2(attrs.contains(qsl("text-link-group:")),
|
|
|
|
|
qPrintable(qsl("Expected 'text-link-group:' in attributes "
|
|
|
|
|
"but got: %1")
|
|
|
|
|
.arg(attrs)));
|
|
|
|
|
// The group value should contain the group name (may be escaped)
|
|
|
|
|
QVERIFY2(attrs.contains(qsl("combat")),
|
|
|
|
|
qPrintable(qsl("Expected group name 'combat' in attributes "
|
|
|
|
|
"but got: %1")
|
|
|
|
|
.arg(attrs)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// OSC 8 Hyperlink QAccessible::Description Tests
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
void test_Description_CaretOnLink() {
|
|
|
|
|
QString config = qsl(
|
|
|
|
|
R"({"tooltip":"A magical sword","menu":[{"View Details":"send:look sword"}]})");
|
|
|
|
|
injectData(
|
|
|
|
|
buildOsc8WithConfig(qsl("send:look sword"), qsl("[Sword]"), config));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
// Find the link position
|
|
|
|
|
auto pos = findFirstLinkPosition();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol] = *pos;
|
|
|
|
|
|
|
|
|
|
// Enable caret mode and move caret to the link
|
|
|
|
|
mpHost->setCaretEnabled(true);
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
pane->setCaretPosition(linkLine, linkCol);
|
|
|
|
|
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
QString desc = accessible.text(QAccessible::Description);
|
|
|
|
|
|
|
|
|
|
QVERIFY2(
|
|
|
|
|
desc.contains(qsl("A magical sword")),
|
|
|
|
|
qPrintable(
|
|
|
|
|
qsl("Expected tooltip in Description but got: '%1'").arg(desc)));
|
|
|
|
|
|
|
|
|
|
// Cleanup: disable caret mode
|
|
|
|
|
mpHost->setCaretEnabled(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_Description_CaretNotOnLink() {
|
|
|
|
|
injectData(qsl("Plain text with no links"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
|
|
|
|
|
// Enable caret mode, caret at line 0 col 0 (plain text)
|
|
|
|
|
mpHost->setCaretEnabled(true);
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
pane->setCaretPosition(0, 0);
|
|
|
|
|
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
QString desc = accessible.text(QAccessible::Description);
|
|
|
|
|
|
|
|
|
|
QVERIFY(desc.isEmpty());
|
|
|
|
|
|
|
|
|
|
// Cleanup
|
|
|
|
|
mpHost->setCaretEnabled(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// OSC 8 attributes() run-range / caret-cell isolation Tests
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
// Helper: compute the offset of a (line, column) position the same way the
|
|
|
|
|
// accessible interface does it (one '\n' per line break).
|
|
|
|
|
int offsetForPosition(TMainConsole *console, int line, int column) {
|
|
|
|
|
int offset = 0;
|
|
|
|
|
for (int i = 0; i < line; ++i) {
|
|
|
|
|
offset += console->buffer.line(i).length() + 1;
|
|
|
|
|
}
|
|
|
|
|
return offset + column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_Attributes_RunRangeCoversFullLink() {
|
|
|
|
|
// A whole link run should be returned in a single attributes() call
|
|
|
|
|
// (start..end describe the contiguous range with these attributes).
|
|
|
|
|
injectData(qsl("\x1b]8;;https://example.com\x1b\\Click me\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
auto pos = findFirstLinkPositionWithId();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol, linkId] = *pos;
|
|
|
|
|
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
const int offset = offsetForPosition(console, linkLine, linkCol);
|
|
|
|
|
int startOffset = -1, endOffset = -1;
|
|
|
|
|
QString attrs = accessible.attributes(offset, &startOffset, &endOffset);
|
|
|
|
|
|
|
|
|
|
QVERIFY2(attrs.contains(qsl("text-link:")),
|
|
|
|
|
qPrintable(qsl("Expected link attributes; got: %1").arg(attrs)));
|
|
|
|
|
|
|
|
|
|
// The reported range must include the queried offset and stay within the
|
|
|
|
|
// link span on this line.
|
|
|
|
|
QVERIFY2(startOffset <= offset && offset < endOffset,
|
|
|
|
|
qPrintable(qsl("Range [%1,%2) must include offset %3")
|
|
|
|
|
.arg(startOffset)
|
|
|
|
|
.arg(endOffset)
|
|
|
|
|
.arg(offset)));
|
|
|
|
|
|
|
|
|
|
// Every column inside [startCol, endCol) on this line must belong to the
|
|
|
|
|
// same linkId, otherwise the run-expansion crossed a boundary.
|
|
|
|
|
const int lineStart = offsetForPosition(console, linkLine, 0);
|
|
|
|
|
for (int o = startOffset; o < endOffset; ++o) {
|
|
|
|
|
const int col = o - lineStart;
|
|
|
|
|
QVERIFY2(col >= 0 && col < console->buffer.line(linkLine).length(),
|
|
|
|
|
"run range should not cross a line boundary");
|
|
|
|
|
QCOMPARE(console->buffer.getLinkIndexAt(linkLine, col), linkId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_Attributes_RunRangeStopsAtLinkBoundary() {
|
|
|
|
|
// " " between "X" and the link must not be merged with the link's run.
|
|
|
|
|
injectData(qsl("X \x1b]8;;https://example.com\x1b\\L\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
auto pos = findFirstLinkPositionWithId();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol, linkId] = *pos;
|
|
|
|
|
QVERIFY2(linkCol >= 1, "Link should be preceded by plain text");
|
|
|
|
|
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
// Query attributes at the plain-text character immediately before the link.
|
|
|
|
|
const int plainOffset = offsetForPosition(console, linkLine, linkCol - 1);
|
|
|
|
|
int s = -1, e = -1;
|
|
|
|
|
QString plainAttrs = accessible.attributes(plainOffset, &s, &e);
|
|
|
|
|
QVERIFY2(!plainAttrs.contains(qsl("text-link:")),
|
|
|
|
|
"Plain text before link should not have link attributes");
|
|
|
|
|
// The run from the plain-text query must end at or before the link starts.
|
|
|
|
|
const int linkOffset = offsetForPosition(console, linkLine, linkCol);
|
|
|
|
|
QVERIFY2(e <= linkOffset,
|
|
|
|
|
qPrintable(qsl("Plain run [%1,%2) leaked into link starting at %3")
|
|
|
|
|
.arg(s)
|
|
|
|
|
.arg(e)
|
|
|
|
|
.arg(linkOffset)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_Attributes_CaretCellIsolatedFromRun() {
|
|
|
|
|
// The caret cell renders inverted; it must not be merged into a
|
|
|
|
|
// neighbouring run even when they otherwise share styling.
|
|
|
|
|
injectData(qsl("\x1b]8;;https://example.com\x1b\\Hello\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
auto pos = findFirstLinkPositionWithId();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol, linkId] = *pos;
|
|
|
|
|
|
|
|
|
|
mpHost->setCaretEnabled(true);
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
// Park the caret on the second character of the link.
|
|
|
|
|
pane->setCaretPosition(linkLine, linkCol + 1);
|
|
|
|
|
|
|
|
|
|
TAccessibleTextEdit accessible(pane);
|
|
|
|
|
|
|
|
|
|
const int caretOffset = offsetForPosition(console, linkLine, linkCol + 1);
|
|
|
|
|
int sCaret = -1, eCaret = -1;
|
|
|
|
|
QString attrsCaret = accessible.attributes(caretOffset, &sCaret, &eCaret);
|
|
|
|
|
QVERIFY2(attrsCaret.contains(qsl("text-link:")),
|
|
|
|
|
"Caret-on-link should still report link attributes");
|
|
|
|
|
// The caret cell's range must be just the caret cell, not the whole link.
|
|
|
|
|
QCOMPARE(eCaret - sCaret, 1);
|
|
|
|
|
|
|
|
|
|
// The character before the caret must form its own run that ends at the
|
|
|
|
|
// caret cell (does not engulf it).
|
|
|
|
|
const int beforeOffset = offsetForPosition(console, linkLine, linkCol);
|
|
|
|
|
int sBefore = -1, eBefore = -1;
|
|
|
|
|
accessible.attributes(beforeOffset, &sBefore, &eBefore);
|
|
|
|
|
QVERIFY2(eBefore == caretOffset,
|
|
|
|
|
qPrintable(qsl("Run before caret should end at caret offset %1, "
|
|
|
|
|
"got [%2,%3)")
|
|
|
|
|
.arg(caretOffset)
|
|
|
|
|
.arg(sBefore)
|
|
|
|
|
.arg(eBefore)));
|
|
|
|
|
|
|
|
|
|
mpHost->setCaretEnabled(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// OSC 8 link navigation key Tests
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
void test_FindLink_WrapForward() {
|
|
|
|
|
// With only one link in the buffer, starting from after it we should wrap
|
|
|
|
|
// to the start and rediscover the same link (and report wrapped == true).
|
|
|
|
|
injectData(qsl("\x1b]8;;send:look\x1b\\Look\x1b]8;;\x1b\\ trailing text"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
auto pos = findFirstLinkPositionWithId();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol, linkId] = *pos;
|
|
|
|
|
Q_UNUSED(linkLine);
|
|
|
|
|
Q_UNUSED(linkCol);
|
|
|
|
|
|
|
|
|
|
// Scan forward starting at a position past the only link: the search must
|
|
|
|
|
// fall through to the wrap-around branch and rediscover the same link.
|
|
|
|
|
const int lastLine = console->buffer.getLastLineNumber();
|
|
|
|
|
const int lastCol = console->buffer.line(lastLine).length() - 1;
|
|
|
|
|
int outLine2 = -1, outCol2 = -1;
|
|
|
|
|
bool wrapped2 = false;
|
|
|
|
|
bool found2 = console->buffer.findNextLink(lastLine, lastCol, outLine2,
|
|
|
|
|
outCol2, &wrapped2);
|
|
|
|
|
QVERIFY2(found2, "findNextLink with wrap should rediscover the only link");
|
|
|
|
|
QVERIFY2(wrapped2, "wrapped should be set to true when the search wrapped");
|
|
|
|
|
QCOMPARE(console->buffer.getLinkIndexAt(outLine2, outCol2), linkId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_FindLink_WrapBackward() {
|
|
|
|
|
// Place the link on a later line so that a backward search from line 0
|
|
|
|
|
// exhausts the pre-start range and falls through to the wrap path.
|
|
|
|
|
injectData(qsl("plain leading line"));
|
|
|
|
|
injectData(qsl("\x1b]8;;send:look\x1b\\Look\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
auto pos = findFirstLinkPositionWithId();
|
|
|
|
|
QVERIFY2(pos.has_value(), "Expected to find a link in the buffer");
|
|
|
|
|
auto [linkLine, linkCol, linkId] = *pos;
|
|
|
|
|
QVERIFY2(linkLine > 0,
|
|
|
|
|
"Test setup expects the link on a line after the leading text");
|
|
|
|
|
Q_UNUSED(linkCol);
|
|
|
|
|
|
|
|
|
|
// Searching backward from the very start of the buffer must wrap to find
|
|
|
|
|
// the only link further down the buffer.
|
|
|
|
|
int outLine = -1, outCol = -1;
|
|
|
|
|
bool wrapped = false;
|
|
|
|
|
bool found =
|
|
|
|
|
console->buffer.findPreviousLink(0, 0, outLine, outCol, &wrapped);
|
|
|
|
|
QVERIFY2(found,
|
|
|
|
|
"findPreviousLink with wrap should rediscover the only link");
|
|
|
|
|
QVERIFY2(wrapped,
|
|
|
|
|
"wrapped should be set to true when backward search wrapped");
|
|
|
|
|
QCOMPARE(outLine, linkLine);
|
|
|
|
|
QCOMPARE(console->buffer.getLinkIndexAt(outLine, outCol), linkId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_KeyNav_CtrlBracketRight_MovesToNextLink() {
|
|
|
|
|
injectData(qsl("\x1b]8;;send:north\x1b\\North\x1b]8;;\x1b\\ - "
|
|
|
|
|
"\x1b]8;;send:south\x1b\\South\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
auto first = findNthDistinctLinkPosition(1);
|
|
|
|
|
auto second = findNthDistinctLinkPosition(2);
|
|
|
|
|
QVERIFY(first.has_value() && second.has_value());
|
|
|
|
|
auto [firstLine, firstCol, firstLinkId] = *first;
|
|
|
|
|
auto [secondLine, secondCol, secondLinkId] = *second;
|
|
|
|
|
Q_UNUSED(firstLinkId);
|
|
|
|
|
|
|
|
|
|
mpHost->setCaretEnabled(true);
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
pane->setCaretPosition(firstLine, firstCol);
|
|
|
|
|
|
|
|
|
|
QKeyEvent press(QEvent::KeyPress, Qt::Key_BracketRight,
|
|
|
|
|
Qt::ControlModifier);
|
|
|
|
|
QApplication::sendEvent(pane, &press);
|
|
|
|
|
|
|
|
|
|
QCOMPARE(pane->mCaretLine, secondLine);
|
|
|
|
|
QCOMPARE(pane->mCaretColumn, secondCol);
|
|
|
|
|
QCOMPARE(console->buffer.getFocusedLink(), secondLinkId);
|
|
|
|
|
|
|
|
|
|
mpHost->setCaretEnabled(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void test_KeyNav_CtrlBracketLeft_MovesToPreviousLink() {
|
|
|
|
|
injectData(qsl("\x1b]8;;send:north\x1b\\North\x1b]8;;\x1b\\ - "
|
|
|
|
|
"\x1b]8;;send:south\x1b\\South\x1b]8;;\x1b\\"));
|
|
|
|
|
|
|
|
|
|
TMainConsole *console = mpHost->mpConsole;
|
|
|
|
|
auto first = findNthDistinctLinkPosition(1);
|
|
|
|
|
auto second = findNthDistinctLinkPosition(2);
|
|
|
|
|
QVERIFY(first.has_value() && second.has_value());
|
|
|
|
|
auto [firstLine, firstCol, firstLinkId] = *first;
|
|
|
|
|
auto [secondLine, secondCol, secondLinkId] = *second;
|
|
|
|
|
Q_UNUSED(secondLinkId);
|
|
|
|
|
|
|
|
|
|
mpHost->setCaretEnabled(true);
|
|
|
|
|
TTextEdit *pane = console->mUpperPane;
|
|
|
|
|
QVERIFY(pane);
|
|
|
|
|
pane->setCaretPosition(secondLine, secondCol);
|
|
|
|
|
|
|
|
|
|
QKeyEvent press(QEvent::KeyPress, Qt::Key_BracketLeft, Qt::ControlModifier);
|
|
|
|
|
QApplication::sendEvent(pane, &press);
|
|
|
|
|
|
|
|
|
|
QCOMPARE(pane->mCaretLine, firstLine);
|
|
|
|
|
QCOMPARE(pane->mCaretColumn, firstCol);
|
|
|
|
|
QCOMPARE(console->buffer.getFocusedLink(), firstLinkId);
|
|
|
|
|
|
|
|
|
|
mpHost->setCaretEnabled(false);
|
Add: OSC 8 context menu title support (#8910)
## Summary
Adds a `"title"` property to OSC 8 hyperlink configuration that renders
a non-clickable section header at the top of right-click context menus.
This gives players immediate context about what a menu relates to (e.g.,
an item name, NPC name, or action category).
Context menus now also use the main console display font set in
Settings, matching the rest of the game output.
## Configuration
**Simple string** (renders with default teal color):
```json
{"title": "Lamb and Barley Stew", "menu": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
**With Tier 2 style properties:**
```json
{
"title": {
"text": "Lamb and Barley Stew",
"style": {"color": "#5fbdaf", "bold": true}
},
"menu": [
{"View Details": "send:look stew"},
{"Buy": "send:buy stew"}
]
}
```
**Compact syntax** (shorthand `ti` for `title`):
```json
{"ti": "Lamb and Barley Stew", "m": [{"View Details": "send:look stew"}, {"Buy": "send:buy stew"}]}
```
## Title style
The title's `style` object uses the same [Tier 2 style
properties](https://wiki.mudlet.org/w/Manual:Supported_Protocols#Style_Properties)
as OSC 8 hyperlink styling, including all supported color formats (named
colors, hex, rgb). Currently supported properties for the title:
| Property | Type | Description |
|----------|------|-------------|
| `color` | String | Title text color (default: `#5fbdaf`) |
| `bg` | String | Title background color |
| `bold` | Boolean | Bold text |
| `italic` | Boolean | Italic text |
## Behaviour notes
- Title renders as a non-clickable label with a separator line below,
appearing above all menu items
- If `title` is set but no `menu` is configured, the title has no effect
(no popup is shown for single-action links)
- Left-click behaviour is unaffected
- When no style is specified, the title defaults to teal (`#5fbdaf`) to
visually distinguish it from clickable menu items
- Title without a `style` object is backward-compatible - older clients
that don't recognize the `title` key will silently ignore it
## Files changed
- **`src/TBuffer.h`** - Added `menuTitle` and `menuTitleStyle` fields to
`HyperlinkStyling`
- **`src/TBuffer.cpp`** - JSON config parsing for `title` (string and
object formats), query param extraction and URL stripping
- **`src/TTextEdit.cpp`** - Renders the title label in the popup menu
with style properties; sets console font on context menus
- **`src/TConsole.h/cpp`** - Registers `ti` shorthand and `title` preset
property
## Screenshots
<img width="500" height="329" alt="Screenshot 2026-02-06 at 14 21 56"
src="https://github.com/user-attachments/assets/6d7d047d-2654-4aec-8241-e1ea1614a6f0"
/>
## Test plan
- [x] Send an OSC 8 link with `"title"` as a simple string - verify teal
header appears above menu items
- [x] Send an OSC 8 link with `"title"` as an object with custom style -
verify color/bold/italic apply
- [x] Send an OSC 8 link with menu but no title - verify no header
appears
- [x] Send an OSC 8 link with title but no menu - verify no popup on
right-click
- [x] Verify left-click still executes the primary action
- [x] Change the console font in Settings - verify context menu font
updates to match
- [x] Verify compact syntax `"ti"` works as shorthand for `"title"`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zooka <136661366+ZookaOnGit@users.noreply.github.com>
2026-03-26 08:33:44 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 15:01:24 +01:00
|
|
|
void cleanupTestCase() {
|
2026-03-21 01:58:22 -04:00
|
|
|
delete mpServer;
|
|
|
|
|
mpServer = nullptr;
|
2026-03-26 15:01:24 +01:00
|
|
|
mpHost = nullptr;
|
2026-03-21 01:58:22 -04:00
|
|
|
const QString path =
|
2026-03-26 15:01:24 +01:00
|
|
|
mudlet::getMudletPath(enums::profileHomePath, mHostname);
|
|
|
|
|
QDir(path).removeRecursively();
|
|
|
|
|
delete mudlet::self();
|
2026-03-21 01:58:22 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void initializeQRCResourcesForOscTest() {
|
|
|
|
|
#ifdef INCLUDE_VARIABLE_SPLASH_SCREEN
|
|
|
|
|
qInitResources_additional_splash_screens();
|
|
|
|
|
#endif
|
|
|
|
|
#ifdef INCLUDE_FONTS
|
|
|
|
|
qInitResources_mudlet_fonts_common();
|
|
|
|
|
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
|
|
|
|
|
qInitResources_mudlet_fonts_posix();
|
|
|
|
|
#endif
|
|
|
|
|
#endif
|
|
|
|
|
qInitResources_mudlet();
|
|
|
|
|
qInitResources_qm();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#include "TOscTest.moc"
|
|
|
|
|
QTEST_MAIN(TOscTest)
|