improved documentation for the diff generation.
This commit is contained in:
parent
6998706934
commit
b0ac6e1409
@ -233,57 +233,110 @@ class attributeManager():
|
|||||||
|
|
||||||
return attributeFormatString
|
return attributeFormatString
|
||||||
def trackHighlights(self):
|
def trackHighlights(self):
|
||||||
|
"""
|
||||||
|
Detects text with changed attributes (highlighting) between screen updates.
|
||||||
|
|
||||||
|
This is crucial for screen readers to announce when text becomes highlighted,
|
||||||
|
selected, or changes visual emphasis (bold, reverse video, color changes, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (highlighted_text, cursor_position)
|
||||||
|
- highlighted_text: string of characters that gained highlighting
|
||||||
|
- cursor_position: dict {'x': col, 'y': row} of first highlighted char
|
||||||
|
"""
|
||||||
result = ''
|
result = ''
|
||||||
currCursor = None
|
currCursor = None
|
||||||
# screen change
|
|
||||||
|
# Early exit conditions - no attribute comparison possible
|
||||||
if self.prevAttributes == None:
|
if self.prevAttributes == None:
|
||||||
|
# First screen load - no previous attributes to compare against
|
||||||
return result, currCursor
|
return result, currCursor
|
||||||
# no change
|
|
||||||
if self.prevAttributes == self.currAttributes:
|
if self.prevAttributes == self.currAttributes:
|
||||||
|
# No attribute changes detected
|
||||||
return result, currCursor
|
return result, currCursor
|
||||||
# error case
|
|
||||||
if self.currAttributes == None:
|
if self.currAttributes == None:
|
||||||
|
# Error condition - current attributes missing
|
||||||
return result, currCursor
|
return result, currCursor
|
||||||
# special case for pty if not text exists.
|
|
||||||
if len(self.currAttributes) == 0:
|
if len(self.currAttributes) == 0:
|
||||||
|
# Special case for PTY environments with no text content
|
||||||
return result, currCursor
|
return result, currCursor
|
||||||
|
|
||||||
|
# Get current screen text to correlate with attribute changes
|
||||||
text = self.env['runtime']['screenManager'].getScreenText()
|
text = self.env['runtime']['screenManager'].getScreenText()
|
||||||
textLines = text.split('\n')
|
textLines = text.split('\n')
|
||||||
|
|
||||||
|
# Sanity check: text lines must match attribute array dimensions
|
||||||
if len(textLines) != len(self.currAttributes):
|
if len(textLines) != len(self.currAttributes):
|
||||||
return result, currCursor
|
return result, currCursor
|
||||||
|
|
||||||
|
# Compare attributes line by line, character by character
|
||||||
for line in range(len(self.prevAttributes)):
|
for line in range(len(self.prevAttributes)):
|
||||||
if self.prevAttributes[line] != self.currAttributes[line]:
|
if self.prevAttributes[line] != self.currAttributes[line]:
|
||||||
|
# This line has attribute changes - examine each character position
|
||||||
for column in range(len(self.prevAttributes[line])):
|
for column in range(len(self.prevAttributes[line])):
|
||||||
if self.prevAttributes[line][column] == self.currAttributes[line][column]:
|
if self.prevAttributes[line][column] == self.currAttributes[line][column]:
|
||||||
|
# No change at this position
|
||||||
continue
|
continue
|
||||||
|
# Attribute changed at this position - check if it's worth announcing
|
||||||
if self.isUsefulForTracking(line, column, self.currAttributes, self.prevAttributes):
|
if self.isUsefulForTracking(line, column, self.currAttributes, self.prevAttributes):
|
||||||
|
# First highlighted character becomes cursor position for navigation
|
||||||
if not currCursor:
|
if not currCursor:
|
||||||
currCursor = {'x': column, 'y': line}
|
currCursor = {'x': column, 'y': line}
|
||||||
|
# Accumulate highlighted characters
|
||||||
result += textLines[line][column]
|
result += textLines[line][column]
|
||||||
|
# Add space between lines of highlighted text for speech clarity
|
||||||
result += ' '
|
result += ' '
|
||||||
return result, currCursor
|
return result, currCursor
|
||||||
def isUsefulForTracking(self, line, column, currAttributes, prevAttributes, attribute=1 , mode = 'zaxe'):
|
def isUsefulForTracking(self, line, column, currAttributes, prevAttributes, attribute=1 , mode = 'zaxe'):
|
||||||
|
"""
|
||||||
|
Determines if an attribute change at a specific position is worth announcing.
|
||||||
|
|
||||||
|
This prevents announcing every minor attribute change and focuses on meaningful
|
||||||
|
highlighting that indicates user selections, focus changes, or important emphasis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line, column: Position of the attribute change
|
||||||
|
currAttributes, prevAttributes: Current and previous attribute arrays
|
||||||
|
attribute: Which attribute to examine (1=background color by default)
|
||||||
|
mode: Detection algorithm ('zaxe', 'default', 'barrier')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if this attribute change should be announced to user
|
||||||
|
"""
|
||||||
|
# Sanity checks for valid position and sufficient screen content
|
||||||
if len(currAttributes) <= 3:
|
if len(currAttributes) <= 3:
|
||||||
return False
|
return False
|
||||||
if line < 0:
|
if line < 0:
|
||||||
return False
|
return False
|
||||||
if line > len(currAttributes):
|
if line > len(currAttributes):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
useful = False
|
useful = False
|
||||||
|
|
||||||
if mode == 'default':
|
if mode == 'default':
|
||||||
# non default tracking
|
# Simple mode: announce any non-default attribute
|
||||||
useful = not self.isDefaultAttribute(currAttributes[line][column])
|
useful = not self.isDefaultAttribute(currAttributes[line][column])
|
||||||
|
|
||||||
elif (mode == 'zaxe') or (mode == ''):
|
elif (mode == 'zaxe') or (mode == ''):
|
||||||
# arround me tracking for bg
|
# Context-aware mode: only announce attributes that stand out from surroundings
|
||||||
|
# This prevents announcing entire blocks of highlighted text character by character
|
||||||
|
# by checking if the attribute differs from adjacent lines
|
||||||
|
|
||||||
if line == 0:
|
if line == 0:
|
||||||
useful = (currAttributes[line][column][attribute] != currAttributes[line + 1][column][attribute]) and (currAttributes[line][column][attribute] != currAttributes[line + 2][column][attribute])
|
# Top line: compare against lines below
|
||||||
|
useful = (currAttributes[line][column][attribute] != currAttributes[line + 1][column][attribute]) and \
|
||||||
|
(currAttributes[line][column][attribute] != currAttributes[line + 2][column][attribute])
|
||||||
elif line >= len(prevAttributes):
|
elif line >= len(prevAttributes):
|
||||||
useful = (currAttributes[line][column][attribute] != currAttributes[line - 1][column][attribute]) and (currAttributes[line][column][attribute] != currAttributes[line - 2][column][attribute])
|
# Bottom line: compare against lines above
|
||||||
|
useful = (currAttributes[line][column][attribute] != currAttributes[line - 1][column][attribute]) and \
|
||||||
|
(currAttributes[line][column][attribute] != currAttributes[line - 2][column][attribute])
|
||||||
else:
|
else:
|
||||||
useful = (currAttributes[line][column][attribute] != currAttributes[line + 1][column][attribute]) and (currAttributes[line][column][attribute] != currAttributes[line - 1][column][attribute])
|
# Middle lines: compare against both directions
|
||||||
|
useful = (currAttributes[line][column][attribute] != currAttributes[line + 1][column][attribute]) and \
|
||||||
|
(currAttributes[line][column][attribute] != currAttributes[line - 1][column][attribute])
|
||||||
|
|
||||||
elif mode == 'barrier':
|
elif mode == 'barrier':
|
||||||
# to be implement
|
# Barrier mode: future enhancement for detecting screen boundaries/separators
|
||||||
useful = True
|
useful = True
|
||||||
|
|
||||||
return useful
|
return useful
|
||||||
|
@ -120,47 +120,94 @@ class screenManager():
|
|||||||
self.env['screen']['newDelta'] = ''
|
self.env['screen']['newDelta'] = ''
|
||||||
self.env['runtime']['attributeManager'].resetAttributeDelta()
|
self.env['runtime']['attributeManager'].resetAttributeDelta()
|
||||||
|
|
||||||
# changes on the screen
|
# Diff generation - critical for screen reader functionality
|
||||||
|
# This code detects and categorizes screen content changes to provide appropriate
|
||||||
|
# speech feedback (typing echo vs incoming text vs screen updates)
|
||||||
|
|
||||||
|
# Pre-process screen text for comparison - collapse multiple spaces to single space
|
||||||
|
# This normalization prevents spurious diffs from spacing inconsistencies
|
||||||
oldScreenText = re.sub(' +',' ',self.env['runtime']['screenManager'].getWindowAreaInText(self.env['screen']['oldContentText']))
|
oldScreenText = re.sub(' +',' ',self.env['runtime']['screenManager'].getWindowAreaInText(self.env['screen']['oldContentText']))
|
||||||
newScreenText = re.sub(' +',' ',self.env['runtime']['screenManager'].getWindowAreaInText(self.env['screen']['newContentText']))
|
newScreenText = re.sub(' +',' ',self.env['runtime']['screenManager'].getWindowAreaInText(self.env['screen']['newContentText']))
|
||||||
|
|
||||||
|
# Track whether this appears to be typing (user input) vs other screen changes
|
||||||
typing = False
|
typing = False
|
||||||
diffList = []
|
diffList = []
|
||||||
|
|
||||||
if (self.env['screen']['oldContentText'] != self.env['screen']['newContentText']):
|
if (self.env['screen']['oldContentText'] != self.env['screen']['newContentText']):
|
||||||
|
# Special case: Initial screen content (going from empty to populated)
|
||||||
|
# This handles first screen load or TTY switch scenarios
|
||||||
if self.env['screen']['newContentText'] != '' and self.env['screen']['oldContentText'] == '':
|
if self.env['screen']['newContentText'] != '' and self.env['screen']['oldContentText'] == '':
|
||||||
if oldScreenText == '' and\
|
if oldScreenText == '' and\
|
||||||
newScreenText != '':
|
newScreenText != '':
|
||||||
self.env['screen']['newDelta'] = newScreenText
|
self.env['screen']['newDelta'] = newScreenText
|
||||||
else:
|
else:
|
||||||
|
# Calculate byte positions for the current cursor's line in the flat text buffer
|
||||||
|
# Formula: (line_number * columns) + line_number accounts for newlines
|
||||||
|
# Each line contributes 'columns' chars + 1 newline char
|
||||||
cursorLineStart = self.env['screen']['newCursor']['y'] * self.env['screen']['columns'] + self.env['screen']['newCursor']['y']
|
cursorLineStart = self.env['screen']['newCursor']['y'] * self.env['screen']['columns'] + self.env['screen']['newCursor']['y']
|
||||||
cursorLineEnd = cursorLineStart + self.env['screen']['columns']
|
cursorLineEnd = cursorLineStart + self.env['screen']['columns']
|
||||||
|
|
||||||
|
# TYPING DETECTION ALGORITHM
|
||||||
|
# Determines if this screen change is likely user typing vs other content changes
|
||||||
|
# All conditions must be met for typing detection:
|
||||||
if abs(self.env['screen']['oldCursor']['x'] - self.env['screen']['newCursor']['x']) >= 1 and \
|
if abs(self.env['screen']['oldCursor']['x'] - self.env['screen']['newCursor']['x']) >= 1 and \
|
||||||
self.env['screen']['oldCursor']['y'] == self.env['screen']['newCursor']['y'] and \
|
self.env['screen']['oldCursor']['y'] == self.env['screen']['newCursor']['y'] and \
|
||||||
self.env['screen']['newContentText'][:cursorLineStart] == self.env['screen']['oldContentText'][:cursorLineStart] and \
|
self.env['screen']['newContentText'][:cursorLineStart] == self.env['screen']['oldContentText'][:cursorLineStart] and \
|
||||||
self.env['screen']['newContentText'][cursorLineEnd:] == self.env['screen']['oldContentText'][cursorLineEnd:]:
|
self.env['screen']['newContentText'][cursorLineEnd:] == self.env['screen']['oldContentText'][cursorLineEnd:]:
|
||||||
|
# Condition 1: Cursor moved horizontally by at least 1 position (typical of typing)
|
||||||
|
# Condition 2: Cursor stayed on same line (typing doesn't usually change lines)
|
||||||
|
# Condition 3: Content BEFORE cursor line is unchanged (text above typing area)
|
||||||
|
# Condition 4: Content AFTER cursor line is unchanged (text below typing area)
|
||||||
|
# Together: only the current line changed, cursor moved horizontally = likely typing
|
||||||
|
|
||||||
|
# Optimize diff calculation for typing by focusing on a small window around cursor
|
||||||
cursorLineStartOffset = cursorLineStart
|
cursorLineStartOffset = cursorLineStart
|
||||||
cursorLineEndOffset = cursorLineEnd
|
cursorLineEndOffset = cursorLineEnd
|
||||||
|
|
||||||
|
# Limit analysis window to avoid processing entire long lines
|
||||||
|
# +3 provides safety buffer beyond cursor position to catch edge cases
|
||||||
if cursorLineEnd > cursorLineStart + self.env['screen']['newCursor']['x'] + 3:
|
if cursorLineEnd > cursorLineStart + self.env['screen']['newCursor']['x'] + 3:
|
||||||
cursorLineEndOffset = cursorLineStart + self.env['screen']['newCursor']['x'] + 3
|
cursorLineEndOffset = cursorLineStart + self.env['screen']['newCursor']['x'] + 3
|
||||||
|
|
||||||
|
# Extract just the relevant text sections for character-level diff
|
||||||
oldScreenText = self.env['screen']['oldContentText'][cursorLineStartOffset:cursorLineEndOffset]
|
oldScreenText = self.env['screen']['oldContentText'][cursorLineStartOffset:cursorLineEndOffset]
|
||||||
newScreenText = self.env['screen']['newContentText'][cursorLineStartOffset:cursorLineEndOffset]
|
newScreenText = self.env['screen']['newContentText'][cursorLineStartOffset:cursorLineEndOffset]
|
||||||
|
|
||||||
|
# Character-level diff for typing detection (not line-level)
|
||||||
diff = self.differ.compare(oldScreenText, newScreenText)
|
diff = self.differ.compare(oldScreenText, newScreenText)
|
||||||
diffList = list(diff)
|
diffList = list(diff)
|
||||||
typing = True
|
typing = True
|
||||||
|
|
||||||
|
# Validate typing assumption by checking if detected changes match cursor movement
|
||||||
tempNewDelta = ''.join(x[2:] for x in diffList if x[0] == '+')
|
tempNewDelta = ''.join(x[2:] for x in diffList if x[0] == '+')
|
||||||
if tempNewDelta.strip() != '':
|
if tempNewDelta.strip() != '':
|
||||||
if tempNewDelta != ''.join(newScreenText[self.env['screen']['oldCursor']['x']:self.env['screen']['newCursor']['x']].rstrip()):
|
# Compare diff result against expected typing pattern
|
||||||
|
# Expected: characters between old and new cursor positions
|
||||||
|
expectedTyping = ''.join(newScreenText[self.env['screen']['oldCursor']['x']:self.env['screen']['newCursor']['x']].rstrip())
|
||||||
|
|
||||||
|
# If diff doesn't match expected typing pattern, treat as general screen change
|
||||||
|
if tempNewDelta != expectedTyping:
|
||||||
|
# Fallback: treat entire current line as new content
|
||||||
diffList = ['+ ' + self.env['screen']['newContentText'].split('\n')[self.env['screen']['newCursor']['y']] +'\n']
|
diffList = ['+ ' + self.env['screen']['newContentText'].split('\n')[self.env['screen']['newCursor']['y']] +'\n']
|
||||||
typing = False
|
typing = False
|
||||||
else:
|
else:
|
||||||
|
# GENERAL SCREEN CHANGE DETECTION
|
||||||
|
# Not typing - handle as line-by-line content change
|
||||||
|
# This catches: incoming messages, screen updates, application output, etc.
|
||||||
diff = self.differ.compare(oldScreenText.split('\n'),\
|
diff = self.differ.compare(oldScreenText.split('\n'),\
|
||||||
newScreenText.split('\n'))
|
newScreenText.split('\n'))
|
||||||
diffList = list(diff)
|
diffList = list(diff)
|
||||||
|
|
||||||
|
# Extract added and removed content from diff results
|
||||||
|
# Output format depends on whether this was detected as typing or general change
|
||||||
if not typing:
|
if not typing:
|
||||||
|
# Line-based changes: join with newlines for proper speech cadence
|
||||||
self.env['screen']['newDelta'] = '\n'.join(x[2:] for x in diffList if x[0] == '+')
|
self.env['screen']['newDelta'] = '\n'.join(x[2:] for x in diffList if x[0] == '+')
|
||||||
else:
|
else:
|
||||||
|
# Character-based changes: no newlines for smooth typing echo
|
||||||
self.env['screen']['newDelta'] = ''.join(x[2:] for x in diffList if x[0] == '+')
|
self.env['screen']['newDelta'] = ''.join(x[2:] for x in diffList if x[0] == '+')
|
||||||
|
|
||||||
|
# Negative delta (removed content) - used for backspace/delete detection
|
||||||
self.env['screen']['newNegativeDelta'] = ''.join(x[2:] for x in diffList if x[0] == '-')
|
self.env['screen']['newNegativeDelta'] = ''.join(x[2:] for x in diffList if x[0] == '-')
|
||||||
|
|
||||||
# track highlighted
|
# track highlighted
|
||||||
|
Loading…
x
Reference in New Issue
Block a user