patches: review fixes — defvar, method extraction, GC safety, window_end_valid

Review-based improvements:
- ns-accessibility-enabled DEFVAR_BOOL (disable AX overhead)
- window_end_valid guard in ns_ax_window_end_charpos
- GC safety comments on Lisp_Object ObjC ivars
- postAccessibilityNotificationsForFrame split into 4 methods
- block_input in ns_ax_completion_text_for_span
- Fplist_get predicate comment
- macos.texi VoiceOver section with defvar docs
- README updated with USER OPTION + REVIEW CHANGES sections
This commit is contained in:
2026-02-27 17:51:39 +01:00
parent b83a061322
commit d408a542e5
3 changed files with 524 additions and 439 deletions

View File

@@ -1,7 +1,7 @@
From 17a100d99a31e0fae9b641c7ce163efd9bf5945b Mon Sep 17 00:00:00 2001 From c4c5ae47fd944cc04f7e229c2a66fb44fa9d006e Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Fri, 27 Feb 2026 15:09:15 +0100 Date: Fri, 27 Feb 2026 17:48:49 +0100
Subject: [PATCH] ns: implement VoiceOver accessibility for macOS Subject: [PATCH 1/2] ns: implement VoiceOver accessibility for macOS
Add comprehensive macOS VoiceOver accessibility support to the NS Add comprehensive macOS VoiceOver accessibility support to the NS
(Cocoa) port. Before this patch, Emacs exposed only a minimal, (Cocoa) port. Before this patch, Emacs exposed only a minimal,
@@ -29,8 +29,6 @@ New types and classes:
for Tab-navigable interactive spans (buttons, links, checkboxes, for Tab-navigable interactive spans (buttons, links, checkboxes,
completion candidates, Org-mode links, keymap overlays). completion candidates, Org-mode links, keymap overlays).
EmacsAXSpanType: enum for span classification.
New functions: New functions:
ns_ax_buffer_text: build accessibility string with visible-run ns_ax_buffer_text: build accessibility string with visible-run
@@ -57,7 +55,7 @@ New functions:
completions-highlight overlay for completion announcements. completions-highlight overlay for completion announcements.
ns_ax_completion_text_for_span: extract announcement text for a ns_ax_completion_text_for_span: extract announcement text for a
completion overlay span. completion overlay span (with block_input/unblock_input protection).
EmacsView extensions: EmacsView extensions:
@@ -72,10 +70,16 @@ EmacsView extensions:
ns_draw_phys_cursor: stores cursor rect for Zoom, calls ns_draw_phys_cursor: stores cursor rect for Zoom, calls
UAZoomChangeFocus with correct CG coordinate-space transform. UAZoomChangeFocus with correct CG coordinate-space transform.
DEFSYM additions in syms_of_nsterm (ns_ax_ prefix to avoid DEFSYM additions in syms_of_nsterm: line navigation command symbols
collisions): Qns_ax_widget, Qns_ax_button, Qns_ax_follow_link, (Qns_ax_next_line, Qns_ax_previous_line, evil/dired variants) and
Qns_ax_org_link, Qns_ax_completion_list_mode, Qns_ax_completion__string, Qns_ax_completion, span scanning symbols (Qns_ax_widget, Qns_ax_button,
Qns_ax_completions_highlight, Qns_ax_backtab. Qns_ax_follow_link, Qns_ax_org_link, Qns_ax_completion_list_mode,
Qns_ax_completion__string, Qns_ax_completion,
Qns_ax_completions_highlight, Qns_ax_backtab).
New user option: ns-accessibility-enabled (default t). When nil,
the accessibility virtual element tree is not built and no
notifications are posted, eliminating overhead.
Threading model: all Lisp calls on main thread; AX getters use Threading model: all Lisp calls on main thread; AX getters use
dispatch_sync to main; index mapping methods are thread-safe (no dispatch_sync to main; index mapping methods are thread-safe (no
@@ -85,16 +89,16 @@ Lisp calls, read only immutable NSString and scalar cache).
* src/nsterm.h: New class declarations and EmacsView ivar extensions. * src/nsterm.h: New class declarations and EmacsView ivar extensions.
* etc/NEWS: Document VoiceOver accessibility support. * etc/NEWS: Document VoiceOver accessibility support.
--- ---
etc/NEWS | 11 + etc/NEWS | 13 +
src/nsterm.h | 108 ++ src/nsterm.h | 119 ++
src/nsterm.m | 2870 +++++++++++++++++++++++++++++++++++++++++++++++--- src/nsterm.m | 3024 +++++++++++++++++++++++++++++++++++++++++++++++---
3 files changed, 2987 insertions(+), 149 deletions(-) 3 files changed, 3009 insertions(+), 147 deletions(-)
diff --git a/etc/NEWS b/etc/NEWS diff --git a/etc/NEWS b/etc/NEWS
index 7367e3cc..0e4480ad 100644 index 7367e3c..608650e 100644
--- a/etc/NEWS --- a/etc/NEWS
+++ b/etc/NEWS +++ b/etc/NEWS
@@ -4374,6 +4374,17 @@ allowing Emacs users access to speech recognition utilities. @@ -4374,6 +4374,19 @@ allowing Emacs users access to speech recognition utilities.
Note: Accepting this permission allows the use of system APIs, which may Note: Accepting this permission allows the use of system APIs, which may
send user data to Apple's speech recognition servers. send user data to Apple's speech recognition servers.
@@ -108,15 +112,17 @@ index 7367e3cc..0e4480ad 100644
+for the *Completions* buffer. The implementation uses a virtual +for the *Completions* buffer. The implementation uses a virtual
+accessibility tree with per-window elements, hybrid SelectedTextChanged +accessibility tree with per-window elements, hybrid SelectedTextChanged
+and AnnouncementRequested notifications, and thread-safe text caching. +and AnnouncementRequested notifications, and thread-safe text caching.
+Set 'ns-accessibility-enabled' to nil to disable the accessibility
+interface and eliminate the associated overhead.
+ +
--- ---
** Re-introduced dictation, lost in Emacs v30 (macOS). ** Re-introduced dictation, lost in Emacs v30 (macOS).
We lost macOS dictation in v30 when migrating to NSTextInputClient. We lost macOS dictation in v30 when migrating to NSTextInputClient.
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4cf..542e7d59 100644 index 7c1ee4c..393fc4c 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -453,6 +453,97 @@ enum ns_return_frame_mode @@ -453,6 +453,110 @@ enum ns_return_frame_mode
@end @end
@@ -132,7 +138,18 @@ index 7c1ee4cf..542e7d59 100644
+/* Base class for virtual accessibility elements attached to EmacsView. */ +/* Base class for virtual accessibility elements attached to EmacsView. */
+@interface EmacsAccessibilityElement : NSAccessibilityElement +@interface EmacsAccessibilityElement : NSAccessibilityElement
+@property (nonatomic, unsafe_unretained) EmacsView *emacsView; +@property (nonatomic, unsafe_unretained) EmacsView *emacsView;
+/* Lisp window object — safe across GC cycles. NULL_LISP when unset. */ +/* Lisp window object — safe across GC cycles.
+ GC safety: these Lisp_Objects are NOT visible to GC via staticpro
+ or the specpdl stack. This is safe because:
+ (1) Emacs GC runs only on the main thread, at well-defined safe
+ points during Lisp evaluation — never during redisplay.
+ (2) Accessibility elements are owned by EmacsView which belongs to
+ an active frame; windows referenced here are always reachable
+ from the frame's window tree until rebuildAccessibilityTree
+ updates them during the next redisplay cycle.
+ (3) AX getters dispatch_sync to main before accessing Lisp state,
+ so GC cannot run concurrently with any access to lispWindow.
+ (4) validWindow checks WINDOW_LIVE_P before dereferencing. */
+@property (nonatomic, assign) Lisp_Object lispWindow; +@property (nonatomic, assign) Lisp_Object lispWindow;
+- (struct window *)validWindow; /* Returns live window or NULL. */ +- (struct window *)validWindow; /* Returns live window or NULL. */
+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h; +- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h;
@@ -216,11 +233,12 @@ index 7c1ee4cf..542e7d59 100644
/* ========================================================================== /* ==========================================================================
The main Emacs view The main Emacs view
@@ -471,6 +562,13 @@ enum ns_return_frame_mode @@ -471,6 +575,14 @@ enum ns_return_frame_mode
#ifdef NS_IMPL_COCOA #ifdef NS_IMPL_COCOA
char *old_title; char *old_title;
BOOL maximizing_resize; BOOL maximizing_resize;
+ NSMutableArray *accessibilityElements; + NSMutableArray *accessibilityElements;
+ /* See GC safety comment on EmacsAccessibilityElement.lispWindow. */
+ Lisp_Object lastSelectedWindow; + Lisp_Object lastSelectedWindow;
+ Lisp_Object lastRootWindow; + Lisp_Object lastRootWindow;
+ BOOL accessibilityTreeValid; + BOOL accessibilityTreeValid;
@@ -230,7 +248,7 @@ index 7c1ee4cf..542e7d59 100644
#endif #endif
BOOL font_panel_active; BOOL font_panel_active;
NSFont *font_panel_result; NSFont *font_panel_result;
@@ -528,6 +626,13 @@ enum ns_return_frame_mode @@ -528,6 +640,13 @@ enum ns_return_frame_mode
- (void)windowWillExitFullScreen; - (void)windowWillExitFullScreen;
- (void)windowDidExitFullScreen; - (void)windowDidExitFullScreen;
- (void)windowDidBecomeKey; - (void)windowDidBecomeKey;
@@ -245,7 +263,7 @@ index 7c1ee4cf..542e7d59 100644
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209f..ea2de6f2 100644 index 932d209..6b27c6c 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -268,18 +286,19 @@ index 932d209f..ea2de6f2 100644
} }
static void static void
@@ -3232,6 +3238,42 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. @@ -3232,6 +3238,43 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
/* Prevent the cursor from being drawn outside the text area. */ /* Prevent the cursor from being drawn outside the text area. */
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
+#ifdef NS_IMPL_COCOA +#ifdef NS_IMPL_COCOA
+ /* Accessibility: store cursor rect for Zoom and bounds queries. + /* Accessibility: store cursor rect for Zoom and bounds queries.
+ Skipped when ns-accessibility-enabled is nil to avoid overhead.
+ VoiceOver notifications are handled solely by + VoiceOver notifications are handled solely by
+ postAccessibilityUpdates (called from ns_update_end) + postAccessibilityUpdates (called from ns_update_end)
+ to avoid duplicate notifications and mid-redisplay fragility. */ + to avoid duplicate notifications and mid-redisplay fragility. */
+ { + {
+ EmacsView *view = FRAME_NS_VIEW (f); + EmacsView *view = FRAME_NS_VIEW (f);
+ if (view && on_p && active_p) + if (view && on_p && active_p && ns_accessibility_enabled)
+ { + {
+ view->lastAccessibilityCursorRect = r; + view->lastAccessibilityCursorRect = r;
+ +
@@ -311,7 +330,7 @@ index 932d209f..ea2de6f2 100644
ns_focus (f, NULL, 0); ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -6849,218 +6891,2498 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg @@ -6849,218 +6892,2522 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
/* ========================================================================== /* ==========================================================================
@@ -786,14 +805,6 @@ index 932d209f..ea2de6f2 100644
+static bool +static bool
+ns_ax_event_is_line_nav_key (int *which) +ns_ax_event_is_line_nav_key (int *which)
+{ +{
-
-#ifdef NS_IMPL_COCOA
- if (!canceled)
- font_panel_result = nil;
-#endif
-
- result = font_panel_result;
- font_panel_result = nil;
+ /* 1. Check Vthis_command for known navigation command symbols. + /* 1. Check Vthis_command for known navigation command symbols.
+ All symbols are registered via DEFSYM in syms_of_nsterm to avoid + All symbols are registered via DEFSYM in syms_of_nsterm to avoid
+ per-call obarray lookups in this hot path (runs every cursor move). */ + per-call obarray lookups in this hot path (runs every cursor move). */
@@ -819,12 +830,18 @@ index 932d209f..ea2de6f2 100644
+ return true; + return true;
+ } + }
+ } + }
+
-#ifdef NS_IMPL_COCOA
- if (!canceled)
- font_panel_result = nil;
-#endif
+ /* 2. Fallback: check raw key events for Tab/backtab. */ + /* 2. Fallback: check raw key events for Tab/backtab. */
+ Lisp_Object ev = last_command_event; + Lisp_Object ev = last_command_event;
+ if (CONSP (ev)) + if (CONSP (ev))
+ ev = EVENT_HEAD (ev); + ev = EVENT_HEAD (ev);
+
- result = font_panel_result;
- font_panel_result = nil;
+ if (SYMBOLP (ev) && EQ (ev, Qns_ax_backtab)) + if (SYMBOLP (ev) && EQ (ev, Qns_ax_backtab))
+ { + {
+ if (which) *which = -1; + if (which) *which = -1;
@@ -875,12 +892,16 @@ index 932d209f..ea2de6f2 100644
-- (BOOL)acceptsFirstResponder -- (BOOL)acceptsFirstResponder
+/* Compute visible-end charpos for window W. +/* Compute visible-end charpos for window W.
+ Emacs stores it as BUF_Z - window_end_pos. */ + Emacs stores it as BUF_Z - window_end_pos.
+ Falls back to BUF_ZV when window_end_valid is false (e.g., when
+ called from an AX getter before the next redisplay cycle). */
+static ptrdiff_t +static ptrdiff_t
+ns_ax_window_end_charpos (struct window *w, struct buffer *b) +ns_ax_window_end_charpos (struct window *w, struct buffer *b)
{ {
- NSTRACE ("[EmacsView acceptsFirstResponder]"); - NSTRACE ("[EmacsView acceptsFirstResponder]");
- return YES; - return YES;
+ if (!w->window_end_valid)
+ return BUF_ZV (b);
+ return BUF_Z (b) - w->window_end_pos; + return BUF_Z (b) - w->window_end_pos;
} }
@@ -894,6 +915,8 @@ index 932d209f..ea2de6f2 100644
- [theEvent type], [theEvent clickCount]); - [theEvent type], [theEvent clickCount]);
- return ns_click_through; - return ns_click_through;
+ Lisp_Object plist = Ftext_properties_at (make_fixnum (pos), buf_obj); + Lisp_Object plist = Ftext_properties_at (make_fixnum (pos), buf_obj);
+ /* Third argument to Fplist_get is PREDICATE (Emacs 29+), not a
+ default value. Qnil selects the default `eq' comparison. */
+ return Fplist_get (plist, prop, Qnil); + return Fplist_get (plist, prop, Qnil);
} }
-- (void)resetCursorRects -- (void)resetCursorRects
@@ -970,7 +993,7 @@ index 932d209f..ea2de6f2 100644
+ NSAccessibilityPostNotification (element, name); + NSAccessibilityPostNotification (element, name);
+ }); + });
+} +}
+
+static inline void +static inline void
+ns_ax_post_notification_with_info (id element, +ns_ax_post_notification_with_info (id element,
+ NSAccessibilityNotificationName name, + NSAccessibilityNotificationName name,
@@ -980,30 +1003,29 @@ index 932d209f..ea2de6f2 100644
+ NSAccessibilityPostNotificationWithUserInfo (element, name, info); + NSAccessibilityPostNotificationWithUserInfo (element, name, info);
+ }); + });
+} +}
+
-/*****************************************************************************/
-/* Keyboard handling. */
-#define NS_KEYLOG 0
+/* Scan visible range of window W for interactive spans. +/* Scan visible range of window W for interactive spans.
+ Returns NSArray<EmacsAccessibilityInteractiveSpan *>. + Returns NSArray<EmacsAccessibilityInteractiveSpan *>.
-- (void)keyDown: (NSEvent *)theEvent
+ Priority when properties overlap: + Priority when properties overlap:
+ widget > button > follow-link > org-link > + widget > button > follow-link > org-link >
+ completion-candidate > keymap-overlay. */ + completion-candidate > keymap-overlay. */
+static NSArray * +static NSArray *
+ns_ax_scan_interactive_spans (struct window *w, +ns_ax_scan_interactive_spans (struct window *w,
+ EmacsAccessibilityBuffer *parent_buf) + EmacsAccessibilityBuffer *parent_buf)
+{ {
- Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe);
+ if (!w) + if (!w)
+ return @[]; + return @[];
+
-/*****************************************************************************/
-/* Keyboard handling. */
-#define NS_KEYLOG 0
+ Lisp_Object buf_obj = ns_ax_window_buffer_object (w); + Lisp_Object buf_obj = ns_ax_window_buffer_object (w);
+ if (NILP (buf_obj)) + if (NILP (buf_obj))
+ return @[]; + return @[];
+
-- (void)keyDown: (NSEvent *)theEvent
-{
- Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe);
+ struct buffer *b = XBUFFER (buf_obj); + struct buffer *b = XBUFFER (buf_obj);
+ ptrdiff_t vis_start = marker_position (w->start); + ptrdiff_t vis_start = marker_position (w->start);
+ ptrdiff_t vis_end = ns_ax_window_end_charpos (w, b); + ptrdiff_t vis_end = ns_ax_window_end_charpos (w, b);
@@ -1278,6 +1300,11 @@ index 932d209f..ea2de6f2 100644
+ NSString *text = nil; + NSString *text = nil;
+ specpdl_ref count = SPECPDL_INDEX (); + specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer (); + record_unwind_current_buffer ();
+ /* Block input to prevent concurrent redisplay from modifying buffer
+ state while we read text properties. Unwind-protected so
+ block_input is always matched by unblock_input on signal. */
+ record_unwind_protect_void (unblock_input);
+ block_input ();
+ if (b != current_buffer) + if (b != current_buffer)
+ set_buffer_internal_1 (b); + set_buffer_internal_1 (b);
+ +
@@ -2214,27 +2241,12 @@ index 932d209f..ea2de6f2 100644
+ height:text_h]; + height:text_h];
+} +}
+ +
+/* ---- Notification dispatch ---- */ +/* ---- Notification dispatch (helper methods) ---- */
+ +
+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f +/* Post NSAccessibilityValueChangedNotification for a text edit.
+ Called when BUF_MODIFF changes between redisplay cycles. */
+- (void)postTextChangedNotification:(ptrdiff_t)point
+{ +{
+ NSTRACE ("[EmacsView postAccessibilityNotificationsForFrame:]");
+ struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w))
+ return;
+
+ struct buffer *b = XBUFFER (w->contents);
+ if (!b)
+ return;
+
+ ptrdiff_t modiff = BUF_MODIFF (b);
+ ptrdiff_t point = BUF_PT (b);
+ BOOL markActive = !NILP (BVAR (b, mark_active));
+
+ /* --- Text changed → typing echo ---
+ WebKit AXObjectCacheMac fallback enum: Edit = 1, Typing = 3. */
+ if (modiff != self.cachedModiff)
+ {
+ /* Capture changed char before invalidating cache. */ + /* Capture changed char before invalidating cache. */
+ NSString *changedChar = @""; + NSString *changedChar = @"";
+ if (point > self.cachedPoint + if (point > self.cachedPoint
@@ -2256,11 +2268,10 @@ index 932d209f..ea2de6f2 100644
+ [self invalidateTextCache]; + [self invalidateTextCache];
+ } + }
+ +
+ self.cachedModiff = modiff; + /* Update cachedPoint here so the selection-move branch does NOT
+ /* Update cachedPoint here so the selection-move branch below + fire for point changes caused by edits. WebKit and Chromium
+ does NOT fire for point changes caused by edits. WebKit and + never send both ValueChanged and SelectedTextChanged for the
+ Chromium never send both ValueChanged and SelectedTextChanged + same user action — they are mutually exclusive. */
+ for the same user action — they are mutually exclusive. */
+ self.cachedPoint = point; + self.cachedPoint = point;
+ +
+ NSDictionary *change = @{ + NSDictionary *change = @{
@@ -2275,94 +2286,16 @@ index 932d209f..ea2de6f2 100644
+ }; + };
+ ns_ax_post_notification_with_info ( + ns_ax_post_notification_with_info (
+ self, NSAccessibilityValueChangedNotification, userInfo); + self, NSAccessibilityValueChangedNotification, userInfo);
+ } +}
+ +
+ /* --- Cursor moved or selection changed → line reading --- +/* Post SelectedTextChanged and AnnouncementRequested for the
+ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. + focused buffer element when point or mark changes. */
+ Use 'else if' — edits and selection moves are mutually exclusive +- (void)postFocusedCursorNotification:(ptrdiff_t)point
+ per the WebKit/Chromium pattern. VoiceOver gets confused if + direction:(NSInteger)direction
+ both notifications arrive in the same runloop iteration. */ + granularity:(NSInteger)granularity
+ else if (point != self.cachedPoint || markActive != self.cachedMarkActive) + markActive:(BOOL)markActive
+ { + oldMarkActive:(BOOL)oldMarkActive
+ ptrdiff_t oldPoint = self.cachedPoint; +{
+ BOOL oldMarkActive = self.cachedMarkActive;
+ self.cachedPoint = point;
+ self.cachedMarkActive = markActive;
+
+ /* Compute direction. */
+ NSInteger direction = ns_ax_text_selection_direction_discontiguous;
+ if (point > oldPoint)
+ direction = ns_ax_text_selection_direction_next;
+ else if (point < oldPoint)
+ direction = ns_ax_text_selection_direction_previous;
+
+ int ctrlNP = 0;
+ bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP);
+
+ /* --- Granularity detection ---
+ Compare old and new cursor positions in cachedText to determine
+ what kind of move happened. Three levels:
+ - line: different line (lineRangeForRange)
+ - word: same line, distance > 1 UTF-16 unit
+ - character: same line, distance == 1 UTF-16 unit
+ C-n/C-p force line regardless of detected granularity. */
+ NSInteger granularity = ns_ax_text_selection_granularity_unknown;
+ [self ensureTextCache];
+ NSUInteger oldIdx = 0, newIdx = 0;
+ if (cachedText && oldPoint > 0)
+ {
+ NSUInteger tlen = [cachedText length];
+ oldIdx = [self accessibilityIndexForCharpos:oldPoint];
+ newIdx = [self accessibilityIndexForCharpos:point];
+ if (oldIdx > tlen) oldIdx = tlen;
+ if (newIdx > tlen) newIdx = tlen;
+
+ NSRange oldLine = [cachedText lineRangeForRange:
+ NSMakeRange (oldIdx, 0)];
+ NSRange newLine = [cachedText lineRangeForRange:
+ NSMakeRange (newIdx, 0)];
+ if (oldLine.location != newLine.location)
+ granularity = ns_ax_text_selection_granularity_line;
+ else
+ {
+ NSUInteger dist = (newIdx > oldIdx
+ ? newIdx - oldIdx
+ : oldIdx - newIdx);
+ if (dist > 1)
+ granularity = ns_ax_text_selection_granularity_word;
+ else if (dist == 1)
+ granularity = ns_ax_text_selection_granularity_character;
+ }
+ }
+
+ /* Force line semantics for explicit C-n/C-p / Tab / backtab. */
+ if (isCtrlNP)
+ {
+ direction = (ctrlNP > 0
+ ? ns_ax_text_selection_direction_next
+ : ns_ax_text_selection_direction_previous);
+ granularity = ns_ax_text_selection_granularity_line;
+ }
+
+ /* --- NOTIFICATION STRATEGY ---
+ SelectedTextChanged ALWAYS posted for focused element:
+ - Interrupts VoiceOver auto-read (buffer switch reading)
+ - Provides word/line/selection reading via VoiceOver defaults
+
+ For CHARACTER moves only: omit granularity from userInfo so
+ VoiceOver cannot derive speech from SelectedTextChanged, then
+ post AnnouncementRequested with char AT point. This avoids
+ double-speech while keeping the interrupt behaviour.
+
+ For WORD and LINE moves: include granularity in userInfo —
+ VoiceOver reads the word/line correctly on its own.
+
+ For SELECTION changes: include granularity — VoiceOver reads
+ selected/deselected text.
+
+ Non-focused buffers: AnnouncementRequested only (see below). */
+ if ([self isAccessibilityFocused])
+ {
+ BOOL isCharMove + BOOL isCharMove
+ = (!markActive && !oldMarkActive + = (!markActive && !oldMarkActive
+ && granularity + && granularity
@@ -2423,11 +2356,11 @@ index 932d209f..ea2de6f2 100644
+ } + }
+ +
+ /* For focused line moves: always announce line text explicitly. + /* For focused line moves: always announce line text explicitly.
+ SelectedTextChanged with granularity=line works for arrow + SelectedTextChanged with granularity=line works for arrow keys,
+ keys, but C-n/C-p need the explicit announcement (VoiceOver + but C-n/C-p need the explicit announcement (VoiceOver processes
+ processes these keystrokes differently from arrows). + these keystrokes differently from arrows).
+ In completion-list-mode, read the completion candidate + In completion-list-mode, read the completion candidate instead
+ instead of the whole line. */ + of the whole line. */
+ if (cachedText + if (cachedText
+ && granularity == ns_ax_text_selection_granularity_line) + && granularity == ns_ax_text_selection_granularity_line)
+ { + {
@@ -2477,19 +2410,15 @@ index 932d209f..ea2de6f2 100644
+ } + }
+ } + }
+ } + }
+ } +}
+ +
+ /* --- Completions announcement --- +/* Post AnnouncementRequested for non-focused buffers (typically
+ When point changes in a non-focused buffer (e.g. *Completions* + *Completions* while minibuffer has keyboard focus).
+ while the minibuffer has keyboard focus), VoiceOver won't read + VoiceOver does not automatically read changes in non-focused
+ the change because it's tracking the focused element. Post an + elements, so we announce the selected completion explicitly. */
+ announcement so the user hears the selected completion. +- (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
+ + point:(ptrdiff_t)point
+ If there is a `completions-highlight` overlay at point (Emacs +{
+ highlights the selected completion candidate), read its full
+ text instead of just the current line. */
+ if (![self isAccessibilityFocused] && cachedText)
+ {
+ NSString *announceText = nil; + NSString *announceText = nil;
+ ptrdiff_t currentOverlayStart = 0; + ptrdiff_t currentOverlayStart = 0;
+ ptrdiff_t currentOverlayEnd = 0; + ptrdiff_t currentOverlayEnd = 0;
@@ -2499,17 +2428,13 @@ index 932d209f..ea2de6f2 100644
+ if (b != current_buffer) + if (b != current_buffer)
+ set_buffer_internal_1 (b); + set_buffer_internal_1 (b);
+ +
+ /* 1) Prefer explicit completion candidate property when present. + /* 1) Prefer explicit completion candidate property. */
+ completion--string can be a plain string (simple completion)
+ or a list ("candidate" "annotation") for annotated completions.
+ In the list case, use car (the completion itself). */
+ Lisp_Object cstr = Fget_char_property (make_fixnum (point), + Lisp_Object cstr = Fget_char_property (make_fixnum (point),
+ Qns_ax_completion__string, + Qns_ax_completion__string,
+ Qnil); + Qnil);
+ announceText = ns_ax_completion_string_from_prop (cstr); + announceText = ns_ax_completion_string_from_prop (cstr);
+ +
+ /* 2) Fallback: announce the mouse-face span at point. + /* 2) Fallback: mouse-face span at point. */
+ completion-list-mode often marks the active candidate this way. */
+ if (!announceText) + if (!announceText)
+ { + {
+ Lisp_Object mf = Fget_char_property (make_fixnum (point), + Lisp_Object mf = Fget_char_property (make_fixnum (point),
@@ -2519,8 +2444,6 @@ index 932d209f..ea2de6f2 100644
+ ptrdiff_t begv2 = BUF_BEGV (b); + ptrdiff_t begv2 = BUF_BEGV (b);
+ ptrdiff_t zv2 = BUF_ZV (b); + ptrdiff_t zv2 = BUF_ZV (b);
+ +
+ /* Find mouse-face span boundaries using property
+ change functions — O(log n) instead of O(n). */
+ Lisp_Object prev_change + Lisp_Object prev_change
+ = Fprevious_single_char_property_change ( + = Fprevious_single_char_property_change (
+ make_fixnum (point + 1), Qmouse_face, + make_fixnum (point + 1), Qmouse_face,
@@ -2548,7 +2471,7 @@ index 932d209f..ea2de6f2 100644
+ } + }
+ } + }
+ +
+ /* 3) Fallback: check completions-highlight overlay span at point. */ + /* 3) Fallback: completions-highlight overlay at point. */
+ if (!announceText) + if (!announceText)
+ { + {
+ Lisp_Object faceSym = Qns_ax_completions_highlight; + Lisp_Object faceSym = Qns_ax_completions_highlight;
@@ -2578,17 +2501,16 @@ index 932d209f..ea2de6f2 100644
+ } + }
+ } + }
+ +
+ /* 4) Fallback: select the best completions-highlight overlay. + /* 4) Fallback: nearest completions-highlight overlay. */
+ Prefer overlay nearest to point over first-found in buffer. */
+ if (!announceText) + if (!announceText)
+ { + {
+ ptrdiff_t ov_start = 0; + ptrdiff_t ov_start = 0;
+ ptrdiff_t ov_end = 0; + ptrdiff_t ov_end = 0;
+ if (ns_ax_find_completion_overlay_range (b, point, &ov_start, &ov_end)) + if (ns_ax_find_completion_overlay_range (b, point,
+ &ov_start, &ov_end))
+ { + {
+ announceText = ns_ax_completion_text_for_span (self, b, + announceText = ns_ax_completion_text_for_span (self, b,
+ ov_start, + ov_start, ov_end,
+ ov_end,
+ cachedText); + cachedText);
+ currentOverlayStart = ov_start; + currentOverlayStart = ov_start;
+ currentOverlayEnd = ov_end; + currentOverlayEnd = ov_end;
@@ -2597,7 +2519,7 @@ index 932d209f..ea2de6f2 100644
+ +
+ unbind_to (count2, Qnil); + unbind_to (count2, Qnil);
+ +
+ /* Final fallback: read the current line at point. */ + /* Final fallback: read current line at point. */
+ if (!announceText) + if (!announceText)
+ { + {
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:point]; + NSUInteger point_idx = [self accessibilityIndexForCharpos:point];
@@ -2614,6 +2536,7 @@ index 932d209f..ea2de6f2 100644
+ } + }
+ } + }
+ +
+ /* Deduplicate: post only when text, overlay, or point changed. */
+ if (announceText) + if (announceText)
+ { + {
+ announceText = [announceText stringByTrimmingCharactersInSet: + announceText = [announceText stringByTrimmingCharactersInSet:
@@ -2658,15 +2581,108 @@ index 932d209f..ea2de6f2 100644
+ self.cachedCompletionOverlayEnd = 0; + self.cachedCompletionOverlayEnd = 0;
+ self.cachedCompletionPoint = 0; + self.cachedCompletionPoint = 0;
+ } + }
+}
+
+/* ---- Notification dispatch (main entry point) ---- */
+
+/* Dispatch accessibility notifications after a redisplay cycle.
+ Detects three mutually exclusive events: text edit, cursor/mark
+ change, or no change. Delegates to helper methods above. */
+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f
+{
+ NSTRACE ("[EmacsView postAccessibilityNotificationsForFrame:]");
+ struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w))
+ return;
+
+ struct buffer *b = XBUFFER (w->contents);
+ if (!b)
+ return;
+
+ ptrdiff_t modiff = BUF_MODIFF (b);
+ ptrdiff_t point = BUF_PT (b);
+ BOOL markActive = !NILP (BVAR (b, mark_active));
+
+ /* --- Text changed (edit) --- */
+ if (modiff != self.cachedModiff)
+ {
+ self.cachedModiff = modiff;
+ [self postTextChangedNotification:point];
+ } + }
+ +
+ /* --- Cursor moved or selection changed ---
+ Use 'else if' — edits and selection moves are mutually exclusive
+ per the WebKit/Chromium pattern. */
+ else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
+ {
+ ptrdiff_t oldPoint = self.cachedPoint;
+ BOOL oldMarkActive = self.cachedMarkActive;
+ self.cachedPoint = point;
+ self.cachedMarkActive = markActive;
+
+ /* Compute direction. */
+ NSInteger direction = ns_ax_text_selection_direction_discontiguous;
+ if (point > oldPoint)
+ direction = ns_ax_text_selection_direction_next;
+ else if (point < oldPoint)
+ direction = ns_ax_text_selection_direction_previous;
+
+ int ctrlNP = 0;
+ bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP);
+
+ /* --- Granularity detection --- */
+ NSInteger granularity = ns_ax_text_selection_granularity_unknown;
+ [self ensureTextCache];
+ if (cachedText && oldPoint > 0)
+ {
+ NSUInteger tlen = [cachedText length];
+ NSUInteger oldIdx = [self accessibilityIndexForCharpos:oldPoint];
+ NSUInteger newIdx = [self accessibilityIndexForCharpos:point];
+ if (oldIdx > tlen) oldIdx = tlen;
+ if (newIdx > tlen) newIdx = tlen;
+
+ NSRange oldLine = [cachedText lineRangeForRange:
+ NSMakeRange (oldIdx, 0)];
+ NSRange newLine = [cachedText lineRangeForRange:
+ NSMakeRange (newIdx, 0)];
+ if (oldLine.location != newLine.location)
+ granularity = ns_ax_text_selection_granularity_line;
+ else
+ {
+ NSUInteger dist = (newIdx > oldIdx
+ ? newIdx - oldIdx
+ : oldIdx - newIdx);
+ if (dist > 1)
+ granularity = ns_ax_text_selection_granularity_word;
+ else if (dist == 1)
+ granularity = ns_ax_text_selection_granularity_character;
+ }
+ }
+
+ /* Force line semantics for explicit C-n/C-p / Tab / backtab. */
+ if (isCtrlNP)
+ {
+ direction = (ctrlNP > 0
+ ? ns_ax_text_selection_direction_next
+ : ns_ax_text_selection_direction_previous);
+ granularity = ns_ax_text_selection_granularity_line;
+ }
+
+ /* Post notifications for focused and non-focused elements. */
+ if ([self isAccessibilityFocused])
+ [self postFocusedCursorNotification:point
+ direction:direction
+ granularity:granularity
+ markActive:markActive
+ oldMarkActive:oldMarkActive];
+
+ if (![self isAccessibilityFocused] && cachedText)
+ [self postCompletionAnnouncementForBuffer:b point:point];
+ } + }
+ else + else
+ { + {
+ /* Nothing changed (no text edit, no cursor move, no mark change). + /* Nothing changed. Reset completion cache for focused buffer
+ Overlay state cannot change without a modiff bump, so no scan + to avoid stale announcements. */
+ needed for non-focused buffers. Just reset completion cache
+ for focused buffer to avoid stale announcements. */
+ if ([self isAccessibilityFocused]) + if ([self isAccessibilityFocused])
+ { + {
+ self.cachedCompletionAnnouncement = nil; + self.cachedCompletionAnnouncement = nil;
@@ -2984,7 +3000,7 @@ index 932d209f..ea2de6f2 100644
int code; int code;
unsigned fnKeysym = 0; unsigned fnKeysym = 0;
static NSMutableArray *nsEvArray; static NSMutableArray *nsEvArray;
@@ -8237,6 +10559,31 @@ - (void)windowDidBecomeKey /* for direct calls */ @@ -8237,6 +10584,32 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe); XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event); kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop ns_send_appdefined (-1); // Kick main loop
@@ -2993,6 +3009,7 @@ index 932d209f..ea2de6f2 100644
+ /* Notify VoiceOver that the focused accessibility element changed. + /* Notify VoiceOver that the focused accessibility element changed.
+ Post on the focused virtual element so VoiceOver starts tracking it. + Post on the focused virtual element so VoiceOver starts tracking it.
+ This is critical for initial focus and app-switch scenarios. */ + This is critical for initial focus and app-switch scenarios. */
+ if (ns_accessibility_enabled)
+ { + {
+ id focused = [self accessibilityFocusedUIElement]; + id focused = [self accessibilityFocusedUIElement];
+ if (focused + if (focused
@@ -3016,7 +3033,7 @@ index 932d209f..ea2de6f2 100644
} }
@@ -9474,6 +11821,332 @@ - (int) fullscreenState @@ -9474,6 +11847,332 @@ - (int) fullscreenState
return fs_state; return fs_state;
} }
@@ -3171,7 +3188,7 @@ index 932d209f..ea2de6f2 100644
+ NSTRACE ("[EmacsView postAccessibilityUpdates]"); + NSTRACE ("[EmacsView postAccessibilityUpdates]");
+ eassert ([NSThread isMainThread]); + eassert ([NSThread isMainThread]);
+ +
+ if (!emacsframe) + if (!emacsframe || !ns_accessibility_enabled)
+ return; + return;
+ +
+ /* Re-entrance guard: VoiceOver callbacks during notification posting + /* Re-entrance guard: VoiceOver callbacks during notification posting
@@ -3349,7 +3366,7 @@ index 932d209f..ea2de6f2 100644
@end /* EmacsView */ @end /* EmacsView */
@@ -11303,7 +13976,29 @@ Convert an X font name (XLFD) to an NS font name. @@ -11303,6 +14002,28 @@ Convert an X font name (XLFD) to an NS font name.
DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic"); DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic");
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");
@@ -3375,10 +3392,25 @@ index 932d209f..ea2de6f2 100644
+ DEFSYM (Qns_ax_completions_highlight, "completions-highlight"); + DEFSYM (Qns_ax_completions_highlight, "completions-highlight");
+ DEFSYM (Qns_ax_backtab, "backtab"); + DEFSYM (Qns_ax_backtab, "backtab");
+ /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */ + /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */
+
Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier));
Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier)); Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier));
Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier)); Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier));
@@ -11451,6 +14172,15 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
This variable is ignored on Mac OS X < 10.7 and GNUstep. */);
ns_use_srgb_colorspace = YES;
+ DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled,
+ doc: /* Non-nil means expose buffer content to the macOS accessibility
+subsystem (VoiceOver, Zoom, and other assistive technology).
+When nil, the accessibility virtual element tree is not built and no
+notifications are posted, eliminating the associated overhead.
+Requires the Cocoa (NS) build on macOS; ignored on GNUstep.
+Default is t. */);
+ ns_accessibility_enabled = YES;
+
DEFVAR_BOOL ("ns-use-mwheel-acceleration",
ns_use_mwheel_acceleration,
doc: /* Non-nil means use macOS's standard mouse wheel acceleration.
-- --
2.43.0 2.43.0

View File

@@ -1,24 +1,24 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From ce3b2a8091c99f738ec59acd6f6ebf0d84826e34 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Fri, 27 Feb 2026 17:30:00 +0100 Date: Fri, 27 Feb 2026 17:49:51 +0100
Subject: [PATCH 2/2] doc: add VoiceOver accessibility section to macOS Subject: [PATCH 2/2] doc: add VoiceOver accessibility section to macOS
appendix appendix
Document the new VoiceOver accessibility support in the Emacs manual. Document the new VoiceOver accessibility support in the Emacs manual.
Add a new section to the macOS appendix covering screen reader usage, Add a new section to the macOS appendix covering screen reader usage,
keyboard navigation feedback, completion announcements, and Zoom keyboard navigation feedback, completion announcements, Zoom cursor
cursor tracking. tracking, and the ns-accessibility-enabled user option.
* doc/emacs/macos.texi (VoiceOver Accessibility): New section. * doc/emacs/macos.texi (VoiceOver Accessibility): New section.
--- ---
doc/emacs/macos.texi | 46 +++++++++++++++++++++++++++++++++++++++++++ doc/emacs/macos.texi | 53 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 46 insertions(+) 1 file changed, 53 insertions(+)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 1234567..abcdefg 100644 index 6bd334f..1d969f9 100644
--- a/doc/emacs/macos.texi --- a/doc/emacs/macos.texi
+++ b/doc/emacs/macos.texi +++ b/doc/emacs/macos.texi
@@ -31,6 +31,7 @@ Support}), but we hope to improve it in the future. @@ -36,6 +36,7 @@ Support}), but we hope to improve it in the future.
* Mac / GNUstep Basics:: Basic Emacs usage under GNUstep or macOS. * Mac / GNUstep Basics:: Basic Emacs usage under GNUstep or macOS.
* Mac / GNUstep Customization:: Customizations under GNUstep or macOS. * Mac / GNUstep Customization:: Customizations under GNUstep or macOS.
* Mac / GNUstep Events:: How window system events are handled. * Mac / GNUstep Events:: How window system events are handled.
@@ -26,10 +26,10 @@ index 1234567..abcdefg 100644
* GNUstep Support:: Details on status of GNUstep support. * GNUstep Support:: Details on status of GNUstep support.
@end menu @end menu
@@ -272,6 +273,51 @@ services and receive the results back. Note that you may need to @@ -272,6 +273,58 @@ and return the result as a string. You can also use the Lisp function
services and receive the results back. Note that you may need to
restart Emacs to access newly-available services. restart Emacs to access newly-available services.
+@node VoiceOver Accessibility +@node VoiceOver Accessibility
+@section VoiceOver Accessibility (macOS) +@section VoiceOver Accessibility (macOS)
+@cindex VoiceOver +@cindex VoiceOver
@@ -70,6 +70,12 @@ index 1234567..abcdefg 100644
+position is communicated via @code{UAZoomChangeFocus} and the +position is communicated via @code{UAZoomChangeFocus} and the
+@code{AXBoundsForRange} accessibility attribute. +@code{AXBoundsForRange} accessibility attribute.
+ +
+@vindex ns-accessibility-enabled
+ To disable the accessibility interface entirely (for instance, to
+eliminate overhead on systems where assistive technology is not in
+use), set @code{ns-accessibility-enabled} to @code{nil}. The default
+is @code{t}.
+
+ This support is available only on the Cocoa build; GNUstep has a + This support is available only on the Cocoa build; GNUstep has a
+different accessibility model and is not yet supported +different accessibility model and is not yet supported
+(@pxref{GNUstep Support}). Evil-mode block cursors are handled +(@pxref{GNUstep Support}). Evil-mode block cursors are handled
@@ -81,3 +87,4 @@ index 1234567..abcdefg 100644
-- --
2.43.0 2.43.0

View File

@@ -3,10 +3,10 @@ EMACS NS VOICEOVER ACCESSIBILITY PATCH
patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch
0002-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch 0002-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch
author: Martin Sukany <martin@sukany.cz> author: Martin Sukany <martin@sukany.cz>
files: src/nsterm.h (+105 lines) files: src/nsterm.h (+119 lines)
src/nsterm.m (+2846 ins, -151 del, +2695 net) src/nsterm.m (+3024 ins, -147 del, +2877 net)
doc/emacs/macos.texi (+46 lines) doc/emacs/macos.texi (+53 lines)
etc/NEWS (+11 lines) etc/NEWS (+13 lines)
OVERVIEW OVERVIEW
@@ -114,6 +114,18 @@ ARCHITECTURE
accessibilityAttributeValue:forParameter: API. accessibilityAttributeValue:forParameter: API.
USER OPTION
-----------
ns-accessibility-enabled (DEFVAR_BOOL, default t):
When nil, the accessibility virtual element tree is not built, no
notifications are posted, and ns_draw_phys_cursor skips the Zoom
update. This eliminates accessibility overhead entirely on systems
where assistive technology is not in use. Guarded at three entry
points: postAccessibilityUpdates, ns_draw_phys_cursor, and
windowDidBecomeKey.
THREADING MODEL THREADING MODEL
--------------- ---------------
@@ -615,4 +627,38 @@ TESTING CHECKLIST
26. Open an org-mode file with many folded sections. Verify that 26. Open an org-mode file with many folded sections. Verify that
folded (invisible) text is not announced during navigation. folded (invisible) text is not announced during navigation.
REVIEW CHANGES (post initial implementation)
---------------------------------------------
The following changes were made based on maintainer-style code review:
1. ns_ax_window_end_charpos: added window_end_valid guard. Falls
back to BUF_ZV when the window has not been fully redisplayed,
preventing stale data in AX getters called before next redisplay.
2. GC safety documentation: detailed comment on lispWindow ivar
explaining why staticpro is not needed (windows reachable from
frame tree, GC only on main thread, AX getters dispatch to main).
3. ns-accessibility-enabled (DEFVAR_BOOL): new user option to
disable accessibility entirely. Guards three entry points.
4. postAccessibilityNotificationsForFrame: extracted from one ~200
line method into four focused helpers:
- postTextChangedNotification: (typing echo)
- postFocusedCursorNotification:direction:granularity:markActive:
oldMarkActive: (focused cursor/selection)
- postCompletionAnnouncementForBuffer:point: (completions)
- postAccessibilityNotificationsForFrame: (orchestrator, ~60 lines)
5. ns_ax_completion_text_for_span: added block_input/unblock_input
with specpdl unwind protection for signal safety.
6. Fplist_get third-argument comment (PREDICATE, not default value).
7. Documentation: macos.texi section updated with
ns-accessibility-enabled variable reference. etc/NEWS updated.
-- end of README -- -- end of README --