2026.04.04 merged
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("speak diff review key help")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].present_key_help()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to next added diff line")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].next_added()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to next character in diff line")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].next_char()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to next diff file section")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].next_file_section()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to next diff hunk")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].next_hunk()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to next diff line")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].next_line()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to next removed diff line")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].next_removed()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to next word in diff line")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].next_word()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to previous added diff line")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].prev_added()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to previous character in diff line")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].prev_char()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to previous diff file section")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].prev_file_section()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to previous diff hunk")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].prev_hunk()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to previous diff line")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].prev_line()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to previous removed diff line")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].prev_removed()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("move to previous word in diff line")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].prev_word()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("speak diff summary")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].present_summary()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("exit diff review mode")
|
||||
|
||||
def run(self):
|
||||
if not self.env["runtime"]["DiffReviewManager"].is_active():
|
||||
return
|
||||
self.env["runtime"]["DiffReviewManager"].disable_mode()
|
||||
self.env["runtime"]["DiffReviewManager"].present_mode_message(
|
||||
_("Diff review mode disabled."),
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("toggle diff review mode")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["DiffReviewManager"].toggle_mode()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -4,6 +4,7 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
import difflib
|
||||
import time
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
@@ -111,6 +112,71 @@ class command:
|
||||
|
||||
return False
|
||||
|
||||
def _normalize_line(self, line):
|
||||
return " ".join(line.split())
|
||||
|
||||
def _is_subsequence(self, subset_lines, source_lines):
|
||||
subset_index = 0
|
||||
for source_line in source_lines:
|
||||
if source_line == subset_lines[subset_index]:
|
||||
subset_index += 1
|
||||
if subset_index == len(subset_lines):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _prefer_inserted_lower_screen_text(self, delta_text):
|
||||
delta_lines = [
|
||||
line for line in delta_text.splitlines() if line.strip() != ""
|
||||
]
|
||||
if len(delta_lines) < 2:
|
||||
return delta_text
|
||||
|
||||
old_lines = self.env["screen"]["old_content_text"].split("\n")
|
||||
new_lines = self.env["screen"]["new_content_text"].split("\n")
|
||||
if len(old_lines) != len(new_lines) or len(new_lines) < 4:
|
||||
return delta_text
|
||||
|
||||
normalized_old_lines = [
|
||||
self._normalize_line(line) for line in old_lines
|
||||
]
|
||||
normalized_new_lines = [
|
||||
self._normalize_line(line) for line in new_lines
|
||||
]
|
||||
matcher = difflib.SequenceMatcher(
|
||||
None, normalized_old_lines, normalized_new_lines, autojunk=False
|
||||
)
|
||||
|
||||
top_lines_changed = False
|
||||
inserted_lines = []
|
||||
lower_screen_start = max(2, len(new_lines) // 2)
|
||||
|
||||
for tag, old_start, old_end, new_start, new_end in matcher.get_opcodes():
|
||||
if tag in {"replace", "delete"} and old_start < 2:
|
||||
top_lines_changed = True
|
||||
if tag != "insert" or new_start < lower_screen_start:
|
||||
continue
|
||||
inserted_lines.extend(
|
||||
line for line in new_lines[new_start:new_end] if line.strip() != ""
|
||||
)
|
||||
|
||||
if not top_lines_changed or not inserted_lines:
|
||||
return delta_text
|
||||
|
||||
normalized_delta_lines = [
|
||||
self._normalize_line(line) for line in delta_lines
|
||||
]
|
||||
normalized_inserted_lines = [
|
||||
self._normalize_line(line) for line in inserted_lines
|
||||
]
|
||||
if not self._is_subsequence(
|
||||
normalized_inserted_lines, normalized_delta_lines
|
||||
):
|
||||
return delta_text
|
||||
if normalized_delta_lines == normalized_inserted_lines:
|
||||
return delta_text
|
||||
|
||||
return "\n".join(inserted_lines)
|
||||
|
||||
def run(self):
|
||||
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||
"speech", "auto_read_incoming"
|
||||
@@ -169,6 +235,7 @@ class command:
|
||||
return
|
||||
|
||||
# print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta']))
|
||||
delta_text = self._prefer_inserted_lower_screen_text(delta_text)
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
delta_text, interrupt=False, flush=False
|
||||
)
|
||||
|
||||
@@ -0,0 +1,917 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from fenrirscreenreader.core import debug
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class DiffReviewManager:
|
||||
def __init__(self):
|
||||
self.env = None
|
||||
self.active = False
|
||||
self.loaded_path = ""
|
||||
self.lines = []
|
||||
self.line_meta = []
|
||||
self.file_sections = []
|
||||
self.hunk_sections = []
|
||||
self.file_starts = []
|
||||
self.hunk_starts = []
|
||||
self.added_lines = []
|
||||
self.removed_lines = []
|
||||
self.current_line = -1
|
||||
self.bindings_backup = None
|
||||
self.raw_bindings_backup = None
|
||||
self.total_added = 0
|
||||
self.total_removed = 0
|
||||
self.current_char = 0
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
if self.active:
|
||||
self._restore_bindings()
|
||||
self.active = False
|
||||
|
||||
def is_active(self):
|
||||
return self.active
|
||||
|
||||
def has_loaded_diff(self):
|
||||
return len(self.lines) > 0
|
||||
|
||||
def get_loaded_path(self):
|
||||
return self.loaded_path
|
||||
|
||||
def toggle_mode(self):
|
||||
if self.active:
|
||||
self.disable_mode()
|
||||
self.present_mode_message(
|
||||
_("Diff review mode disabled."),
|
||||
)
|
||||
return False
|
||||
|
||||
clipboard_path = self._get_clipboard_path_or_none()
|
||||
if (
|
||||
clipboard_path
|
||||
and (
|
||||
(not self.has_loaded_diff())
|
||||
or (self.loaded_path != clipboard_path)
|
||||
)
|
||||
):
|
||||
if not self.load_file(clipboard_path):
|
||||
return False
|
||||
elif not self.has_loaded_diff():
|
||||
self.present_error(
|
||||
_("Clipboard does not contain full path to diff file.")
|
||||
)
|
||||
return False
|
||||
|
||||
enabled = self.enable_mode()
|
||||
if enabled:
|
||||
self.present_mode_message(
|
||||
_("Diff review mode enabled."),
|
||||
)
|
||||
return enabled
|
||||
|
||||
def enable_mode(self):
|
||||
if self.active:
|
||||
return True
|
||||
if not self.has_loaded_diff():
|
||||
return False
|
||||
self._install_diff_bindings()
|
||||
self.active = True
|
||||
return True
|
||||
|
||||
def disable_mode(self):
|
||||
if not self.active:
|
||||
return
|
||||
self._restore_bindings()
|
||||
self.active = False
|
||||
|
||||
def load_from_clipboard(self):
|
||||
clipboard_path = self._get_clipboard_path_or_none()
|
||||
if not clipboard_path:
|
||||
self.present_error(
|
||||
_("Clipboard does not contain full path to diff file.")
|
||||
)
|
||||
return False
|
||||
|
||||
return self.load_file(clipboard_path)
|
||||
|
||||
def reload_file(self):
|
||||
if not self.loaded_path:
|
||||
self.present_error(_("No diff file loaded."))
|
||||
return False
|
||||
return self.load_file(self.loaded_path)
|
||||
|
||||
def load_file(self, file_path):
|
||||
normalized_path = os.path.abspath(os.path.expanduser(file_path))
|
||||
if not os.path.exists(normalized_path):
|
||||
self.present_error(
|
||||
_("Diff file not found: ") + normalized_path
|
||||
)
|
||||
return False
|
||||
if os.path.isdir(normalized_path):
|
||||
self.present_error(
|
||||
_("Diff path is a directory: ") + normalized_path
|
||||
)
|
||||
return False
|
||||
if not os.access(normalized_path, os.R_OK):
|
||||
self.present_error(
|
||||
_("Cannot read diff file: ") + normalized_path
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(normalized_path, "r", encoding="utf-8", errors="replace") as diff_file:
|
||||
file_lines = diff_file.read().splitlines()
|
||||
except Exception as load_error:
|
||||
self._write_debug(
|
||||
"DiffReviewManager load_file: " + str(load_error),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.present_error(
|
||||
_("Failed to load diff file: ") + normalized_path
|
||||
)
|
||||
return False
|
||||
|
||||
self.present_mode_message(
|
||||
_("Loading ") + normalized_path + ".",
|
||||
)
|
||||
self._parse_lines(file_lines, normalized_path)
|
||||
self.present_mode_message(
|
||||
_("Diff loaded. ")
|
||||
+ str(len(self.file_sections))
|
||||
+ _(" files changed. ")
|
||||
+ str(self.total_added)
|
||||
+ _(" additions. ")
|
||||
+ str(self.total_removed)
|
||||
+ _(" removals."),
|
||||
)
|
||||
return True
|
||||
|
||||
def _parse_lines(self, file_lines, loaded_path):
|
||||
self.lines = file_lines
|
||||
self.loaded_path = loaded_path
|
||||
self.line_meta = []
|
||||
self.file_sections = []
|
||||
self.hunk_sections = []
|
||||
self.file_starts = []
|
||||
self.hunk_starts = []
|
||||
self.added_lines = []
|
||||
self.removed_lines = []
|
||||
self.current_line = -1
|
||||
self.total_added = 0
|
||||
self.total_removed = 0
|
||||
self.current_char = 0
|
||||
|
||||
has_git_header = any(
|
||||
line.startswith("diff --git ") for line in self.lines
|
||||
)
|
||||
|
||||
current_file_index = -1
|
||||
current_hunk_index = -1
|
||||
for line_index, line in enumerate(self.lines):
|
||||
role = self._classify_line(line)
|
||||
|
||||
if self._starts_new_file_section(
|
||||
line=line,
|
||||
has_git_header=has_git_header,
|
||||
):
|
||||
current_file_index = self._add_file_section(line_index)
|
||||
current_hunk_index = -1
|
||||
|
||||
if (
|
||||
current_file_index == -1
|
||||
and role
|
||||
in {
|
||||
"hunk_header",
|
||||
"added_line",
|
||||
"removed_line",
|
||||
"context_line",
|
||||
"note_line",
|
||||
}
|
||||
):
|
||||
current_file_index = self._add_file_section(line_index)
|
||||
|
||||
if role == "hunk_header":
|
||||
if current_file_index == -1:
|
||||
current_file_index = self._add_file_section(line_index)
|
||||
current_hunk_index = self._add_hunk_section(
|
||||
line_index=line_index,
|
||||
file_index=current_file_index,
|
||||
)
|
||||
|
||||
if role in {"added_line", "removed_line"} and current_hunk_index == -1:
|
||||
if current_file_index == -1:
|
||||
current_file_index = self._add_file_section(line_index)
|
||||
current_hunk_index = self._add_hunk_section(
|
||||
line_index=line_index,
|
||||
file_index=current_file_index,
|
||||
)
|
||||
|
||||
if role == "added_line":
|
||||
self.added_lines.append(line_index)
|
||||
self.total_added += 1
|
||||
if current_file_index != -1:
|
||||
self.file_sections[current_file_index]["added_count"] += 1
|
||||
if current_hunk_index != -1:
|
||||
self.hunk_sections[current_hunk_index]["added_count"] += 1
|
||||
elif role == "removed_line":
|
||||
self.removed_lines.append(line_index)
|
||||
self.total_removed += 1
|
||||
if current_file_index != -1:
|
||||
self.file_sections[current_file_index]["removed_count"] += 1
|
||||
if current_hunk_index != -1:
|
||||
self.hunk_sections[current_hunk_index]["removed_count"] += 1
|
||||
|
||||
self.line_meta.append(
|
||||
{
|
||||
"role": role,
|
||||
"file_index": current_file_index,
|
||||
"hunk_index": current_hunk_index,
|
||||
}
|
||||
)
|
||||
|
||||
for file_index, file_section in enumerate(self.file_sections):
|
||||
file_hunks = file_section["hunk_indices"]
|
||||
for local_position, hunk_index in enumerate(file_hunks, start=1):
|
||||
self.hunk_sections[hunk_index]["position_in_file"] = local_position
|
||||
self.hunk_sections[hunk_index]["hunks_in_file"] = len(file_hunks)
|
||||
|
||||
def _classify_line(self, line):
|
||||
if line.startswith("\\ No newline at end of file"):
|
||||
return "note_line"
|
||||
if self._is_file_header(line):
|
||||
return "file_header"
|
||||
if line.startswith("@@"):
|
||||
return "hunk_header"
|
||||
if line.startswith("+") and not line.startswith("+++ "):
|
||||
return "added_line"
|
||||
if line.startswith("-") and not line.startswith("--- "):
|
||||
return "removed_line"
|
||||
return "context_line"
|
||||
|
||||
def _get_clipboard_path_or_none(self):
|
||||
if self.env["runtime"]["MemoryManager"].is_index_list_empty(
|
||||
"clipboardHistory"
|
||||
):
|
||||
return None
|
||||
|
||||
clipboard_value = self.env["runtime"]["MemoryManager"].get_index_list_element(
|
||||
"clipboardHistory"
|
||||
)
|
||||
if not isinstance(clipboard_value, str):
|
||||
return None
|
||||
|
||||
clipboard_path = clipboard_value.strip().strip("\"'")
|
||||
clipboard_path = os.path.expanduser(clipboard_path)
|
||||
if not os.path.isabs(clipboard_path):
|
||||
return None
|
||||
return os.path.abspath(clipboard_path)
|
||||
|
||||
def _is_file_header(self, line):
|
||||
file_header_prefixes = (
|
||||
"diff --git ",
|
||||
"--- ",
|
||||
"+++ ",
|
||||
"index ",
|
||||
"old mode ",
|
||||
"new mode ",
|
||||
"deleted file mode ",
|
||||
"new file mode ",
|
||||
"rename from ",
|
||||
"rename to ",
|
||||
"similarity index ",
|
||||
"dissimilarity index ",
|
||||
"Binary files ",
|
||||
)
|
||||
return line.startswith(file_header_prefixes)
|
||||
|
||||
def _starts_new_file_section(self, line, has_git_header):
|
||||
if has_git_header:
|
||||
return line.startswith("diff --git ")
|
||||
return line.startswith("--- ")
|
||||
|
||||
def _add_file_section(self, line_index):
|
||||
file_section = {
|
||||
"start_line": line_index,
|
||||
"added_count": 0,
|
||||
"removed_count": 0,
|
||||
"hunk_indices": [],
|
||||
}
|
||||
self.file_sections.append(file_section)
|
||||
self.file_starts.append(line_index)
|
||||
return len(self.file_sections) - 1
|
||||
|
||||
def _add_hunk_section(self, line_index, file_index):
|
||||
hunk_section = {
|
||||
"start_line": line_index,
|
||||
"file_index": file_index,
|
||||
"added_count": 0,
|
||||
"removed_count": 0,
|
||||
"position_in_file": 0,
|
||||
"hunks_in_file": 0,
|
||||
}
|
||||
self.hunk_sections.append(hunk_section)
|
||||
self.hunk_starts.append(line_index)
|
||||
hunk_index = len(self.hunk_sections) - 1
|
||||
if file_index != -1:
|
||||
self.file_sections[file_index]["hunk_indices"].append(hunk_index)
|
||||
return hunk_index
|
||||
|
||||
def next_file_section(self):
|
||||
self._jump_to_anchor(
|
||||
anchors=self.file_starts,
|
||||
previous=False,
|
||||
empty_message=_("No file sections in loaded diff."),
|
||||
edge_message=_("No further file sections."),
|
||||
announce_role="file",
|
||||
)
|
||||
|
||||
def prev_file_section(self):
|
||||
self._jump_to_anchor(
|
||||
anchors=self.file_starts,
|
||||
previous=True,
|
||||
empty_message=_("No file sections in loaded diff."),
|
||||
edge_message=_("No previous file sections."),
|
||||
announce_role="file",
|
||||
)
|
||||
|
||||
def next_hunk(self):
|
||||
self._jump_to_anchor(
|
||||
anchors=self.hunk_starts,
|
||||
previous=False,
|
||||
empty_message=_("No hunks in loaded diff."),
|
||||
edge_message=_("No further hunks."),
|
||||
announce_role="hunk",
|
||||
)
|
||||
|
||||
def prev_hunk(self):
|
||||
self._jump_to_anchor(
|
||||
anchors=self.hunk_starts,
|
||||
previous=True,
|
||||
empty_message=_("No hunks in loaded diff."),
|
||||
edge_message=_("No previous hunks."),
|
||||
announce_role="hunk",
|
||||
)
|
||||
|
||||
def next_added(self):
|
||||
self._jump_to_anchor(
|
||||
anchors=self.added_lines,
|
||||
previous=False,
|
||||
empty_message=_("No added lines in loaded diff."),
|
||||
edge_message=_("No further added lines."),
|
||||
announce_role="added",
|
||||
)
|
||||
|
||||
def prev_added(self):
|
||||
self._jump_to_anchor(
|
||||
anchors=self.added_lines,
|
||||
previous=True,
|
||||
empty_message=_("No added lines in loaded diff."),
|
||||
edge_message=_("No previous added lines."),
|
||||
announce_role="added",
|
||||
)
|
||||
|
||||
def next_removed(self):
|
||||
self._jump_to_anchor(
|
||||
anchors=self.removed_lines,
|
||||
previous=False,
|
||||
empty_message=_("No removed lines in loaded diff."),
|
||||
edge_message=_("No further removed lines."),
|
||||
announce_role="removed",
|
||||
)
|
||||
|
||||
def prev_removed(self):
|
||||
self._jump_to_anchor(
|
||||
anchors=self.removed_lines,
|
||||
previous=True,
|
||||
empty_message=_("No removed lines in loaded diff."),
|
||||
edge_message=_("No previous removed lines."),
|
||||
announce_role="removed",
|
||||
)
|
||||
|
||||
def present_summary(self):
|
||||
if not self.has_loaded_diff():
|
||||
self.present_mode_message(_("No diff file loaded."))
|
||||
return
|
||||
|
||||
summary = (
|
||||
_("Diff summary. Files changed: ")
|
||||
+ str(len(self.file_sections))
|
||||
+ _(". Additions: ")
|
||||
+ str(self.total_added)
|
||||
+ _(". Removals: ")
|
||||
+ str(self.total_removed)
|
||||
+ "."
|
||||
)
|
||||
|
||||
if self.current_line >= 0 and self.current_line < len(self.line_meta):
|
||||
location = self._build_location_text(self.current_line)
|
||||
if location:
|
||||
summary += " " + location
|
||||
|
||||
self.present_mode_message(summary)
|
||||
|
||||
def present_key_help(self):
|
||||
help_text = _(
|
||||
"Diff keys: h next hunk, shift h previous hunk, a next addition, "
|
||||
"shift a previous addition, d next removal, shift d previous removal, "
|
||||
"f next file section, shift f previous file section, up previous line, "
|
||||
"down next line, left previous character, right next character, "
|
||||
"control left previous word, control right next word, s summary, "
|
||||
"escape exit diff mode."
|
||||
)
|
||||
self.present_mode_message(help_text)
|
||||
|
||||
def next_line(self):
|
||||
if not self.has_loaded_diff():
|
||||
self.present_mode_message(_("No diff file loaded."))
|
||||
return
|
||||
if self.current_line < 0:
|
||||
self.current_line = 0
|
||||
self.current_char = 0
|
||||
self._announce_current_line()
|
||||
return
|
||||
if self.current_line >= len(self.lines) - 1:
|
||||
self.present_mode_message(_("End of diff."))
|
||||
return
|
||||
self.current_line += 1
|
||||
self.current_char = 0
|
||||
self._announce_current_line()
|
||||
|
||||
def prev_line(self):
|
||||
if not self.has_loaded_diff():
|
||||
self.present_mode_message(_("No diff file loaded."))
|
||||
return
|
||||
if self.current_line < 0:
|
||||
self.current_line = 0
|
||||
self.current_char = 0
|
||||
self._announce_current_line()
|
||||
return
|
||||
if self.current_line <= 0:
|
||||
self.present_mode_message(_("Start of diff."))
|
||||
return
|
||||
self.current_line -= 1
|
||||
self.current_char = 0
|
||||
self._announce_current_line()
|
||||
|
||||
def next_char(self):
|
||||
if not self._ensure_current_line():
|
||||
return
|
||||
line_text = self.lines[self.current_line]
|
||||
if len(line_text) == 0:
|
||||
self.present_mode_message(_("blank"))
|
||||
return
|
||||
if self.current_char >= len(line_text) - 1:
|
||||
self.present_mode_message(_("End of line."))
|
||||
return
|
||||
self.current_char += 1
|
||||
self._announce_current_char()
|
||||
|
||||
def prev_char(self):
|
||||
if not self._ensure_current_line():
|
||||
return
|
||||
line_text = self.lines[self.current_line]
|
||||
if len(line_text) == 0:
|
||||
self.present_mode_message(_("blank"))
|
||||
return
|
||||
if self.current_char <= 0:
|
||||
self.present_mode_message(_("Start of line."))
|
||||
return
|
||||
self.current_char -= 1
|
||||
self._announce_current_char()
|
||||
|
||||
def next_word(self):
|
||||
if not self._ensure_current_line():
|
||||
return
|
||||
line_text = self.lines[self.current_line]
|
||||
word_spans = self._get_word_spans(line_text)
|
||||
if len(word_spans) == 0:
|
||||
self.present_mode_message(_("No words on this line."))
|
||||
return
|
||||
|
||||
next_word_start = None
|
||||
for start, end in word_spans:
|
||||
if start > self.current_char:
|
||||
next_word_start = start
|
||||
break
|
||||
if start <= self.current_char < end:
|
||||
continue
|
||||
|
||||
if next_word_start is None:
|
||||
self.present_mode_message(_("No next word on this line."))
|
||||
return
|
||||
|
||||
self.current_char = next_word_start
|
||||
self._announce_word_at_current_char()
|
||||
|
||||
def prev_word(self):
|
||||
if not self._ensure_current_line():
|
||||
return
|
||||
line_text = self.lines[self.current_line]
|
||||
word_spans = self._get_word_spans(line_text)
|
||||
if len(word_spans) == 0:
|
||||
self.present_mode_message(_("No words on this line."))
|
||||
return
|
||||
|
||||
prev_word_start = None
|
||||
for start, end in word_spans:
|
||||
if end - 1 < self.current_char:
|
||||
prev_word_start = start
|
||||
elif start <= self.current_char < end:
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
if prev_word_start is None:
|
||||
self.present_mode_message(_("No previous word on this line."))
|
||||
return
|
||||
|
||||
self.current_char = prev_word_start
|
||||
self._announce_word_at_current_char()
|
||||
|
||||
def _jump_to_anchor(
|
||||
self,
|
||||
anchors,
|
||||
previous,
|
||||
empty_message,
|
||||
edge_message,
|
||||
announce_role,
|
||||
):
|
||||
if not self.has_loaded_diff():
|
||||
self.present_mode_message(_("No diff file loaded."))
|
||||
return
|
||||
|
||||
if len(anchors) == 0:
|
||||
self.present_mode_message(empty_message)
|
||||
return
|
||||
|
||||
new_line = self._find_next_anchor(anchors, previous)
|
||||
if new_line is None:
|
||||
self.present_mode_message(edge_message)
|
||||
return
|
||||
|
||||
self.current_line = new_line
|
||||
self.current_char = 0
|
||||
self._announce_line(announce_role, new_line)
|
||||
|
||||
def _find_next_anchor(self, anchors, previous):
|
||||
if self.current_line < 0:
|
||||
if previous:
|
||||
return anchors[-1]
|
||||
return anchors[0]
|
||||
|
||||
if previous:
|
||||
for anchor_line in reversed(anchors):
|
||||
if anchor_line < self.current_line:
|
||||
return anchor_line
|
||||
return None
|
||||
|
||||
for anchor_line in anchors:
|
||||
if anchor_line > self.current_line:
|
||||
return anchor_line
|
||||
return None
|
||||
|
||||
def _announce_line(self, announce_role, line_index):
|
||||
line_text = self.lines[line_index] if line_index < len(self.lines) else ""
|
||||
line_meta = self.line_meta[line_index]
|
||||
role = line_meta["role"]
|
||||
verbosity = self._get_diff_verbosity()
|
||||
|
||||
if announce_role == "file":
|
||||
self._announce_file_section(line_index, line_meta)
|
||||
return
|
||||
if announce_role == "hunk":
|
||||
self._announce_hunk_section(line_index, line_meta, line_text)
|
||||
return
|
||||
|
||||
if role == "added_line":
|
||||
base_message = _("Added line.")
|
||||
elif role == "removed_line":
|
||||
base_message = _("Removed line.")
|
||||
else:
|
||||
base_message = _("Diff line.")
|
||||
|
||||
message = base_message + " " + self._build_location_text(line_index)
|
||||
if verbosity == "verbose":
|
||||
content = self._line_content_for_speech(line_text, role)
|
||||
message = message + " " + content
|
||||
self.present_mode_message(message)
|
||||
|
||||
def _announce_current_line(self):
|
||||
if self.current_line < 0 or self.current_line >= len(self.lines):
|
||||
self.present_mode_message(_("No diff file loaded."))
|
||||
return
|
||||
line_text = self.lines[self.current_line]
|
||||
role = self.line_meta[self.current_line]["role"]
|
||||
|
||||
if role == "context_line":
|
||||
formatted_classic_diff = self._format_classic_diff_command(line_text)
|
||||
if formatted_classic_diff:
|
||||
self.present_mode_message(formatted_classic_diff)
|
||||
return
|
||||
|
||||
role_prefix = {
|
||||
"added_line": _("Added"),
|
||||
"removed_line": _("Removed"),
|
||||
"hunk_header": _("Hunk header"),
|
||||
"file_header": _("File header"),
|
||||
"note_line": _("Note"),
|
||||
"context_line": "",
|
||||
}.get(role, _("Line"))
|
||||
if line_text.strip() == "":
|
||||
if role_prefix:
|
||||
self.present_mode_message(role_prefix + ": " + _("blank"))
|
||||
else:
|
||||
self.present_mode_message(_("blank"))
|
||||
return
|
||||
if role_prefix:
|
||||
line_text = self._normalize_diff_line_content(line_text, role)
|
||||
if line_text.strip() == "":
|
||||
self.present_mode_message(role_prefix + ": " + _("blank"))
|
||||
return
|
||||
if role in {"added_line", "removed_line"} and line_text in {
|
||||
_("added"),
|
||||
_("removed"),
|
||||
}:
|
||||
self.present_mode_message(line_text)
|
||||
return
|
||||
self.present_mode_message(role_prefix + ": " + line_text)
|
||||
else:
|
||||
self.present_mode_message(line_text)
|
||||
|
||||
def _format_classic_diff_command(self, line_text):
|
||||
match = re.match(r"^(\d+)(?:,(\d+))?([acd])(\d+)(?:,(\d+))?$", line_text.strip())
|
||||
if not match:
|
||||
return None
|
||||
|
||||
old_start = int(match.group(1))
|
||||
old_end = int(match.group(2) or match.group(1))
|
||||
op = match.group(3)
|
||||
new_start = int(match.group(4))
|
||||
new_end = int(match.group(5) or match.group(4))
|
||||
|
||||
if op == "c":
|
||||
return self._format_line_span(old_start, old_end) + " " + _("changed")
|
||||
if op == "d":
|
||||
return self._format_line_span(old_start, old_end) + " " + _("deleted")
|
||||
if op == "a":
|
||||
return self._format_line_span(new_start, new_end) + " " + _("added")
|
||||
return None
|
||||
|
||||
def _format_line_span(self, start_line, end_line):
|
||||
if start_line == end_line:
|
||||
return _("line ") + str(start_line)
|
||||
return (
|
||||
_("lines ")
|
||||
+ str(start_line)
|
||||
+ _(" through ")
|
||||
+ str(end_line)
|
||||
)
|
||||
|
||||
def _announce_current_char(self):
|
||||
if self.current_line < 0 or self.current_line >= len(self.lines):
|
||||
self.present_mode_message(_("No diff file loaded."))
|
||||
return
|
||||
line_text = self.lines[self.current_line]
|
||||
if len(line_text) == 0:
|
||||
self.present_mode_message(_("blank"))
|
||||
return
|
||||
if self.current_char < 0:
|
||||
self.current_char = 0
|
||||
if self.current_char >= len(line_text):
|
||||
self.current_char = len(line_text) - 1
|
||||
char_value = line_text[self.current_char]
|
||||
if char_value == " ":
|
||||
self.present_mode_message(_("space"))
|
||||
return
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
char_value,
|
||||
interrupt=True,
|
||||
ignore_punctuation=True,
|
||||
announce_capital=True,
|
||||
)
|
||||
|
||||
def _announce_word_at_current_char(self):
|
||||
if self.current_line < 0 or self.current_line >= len(self.lines):
|
||||
self.present_mode_message(_("No diff file loaded."))
|
||||
return
|
||||
line_text = self.lines[self.current_line]
|
||||
for start, end in self._get_word_spans(line_text):
|
||||
if start <= self.current_char < end:
|
||||
self.present_mode_message(line_text[start:end])
|
||||
return
|
||||
self.present_mode_message(_("No word at current position."))
|
||||
|
||||
def _ensure_current_line(self):
|
||||
if not self.has_loaded_diff():
|
||||
self.present_mode_message(_("No diff file loaded."))
|
||||
return False
|
||||
if self.current_line < 0:
|
||||
self.current_line = 0
|
||||
self.current_char = 0
|
||||
if self.current_line >= len(self.lines):
|
||||
self.current_line = len(self.lines) - 1
|
||||
self.current_char = 0
|
||||
return True
|
||||
|
||||
def _get_word_spans(self, line_text):
|
||||
return [match.span() for match in re.finditer(r"\S+", line_text)]
|
||||
|
||||
def _announce_file_section(self, line_index, line_meta):
|
||||
file_index = line_meta["file_index"]
|
||||
if file_index == -1 or file_index >= len(self.file_sections):
|
||||
self.present_mode_message(_("File section."))
|
||||
return
|
||||
file_section = self.file_sections[file_index]
|
||||
message = (
|
||||
_("File section ")
|
||||
+ str(file_index + 1)
|
||||
+ _(" of ")
|
||||
+ str(len(self.file_sections))
|
||||
+ _(". Plus ")
|
||||
+ str(file_section["added_count"])
|
||||
+ _(", minus ")
|
||||
+ str(file_section["removed_count"])
|
||||
+ "."
|
||||
)
|
||||
self.present_mode_message(message)
|
||||
|
||||
def _announce_hunk_section(self, line_index, line_meta, line_text):
|
||||
hunk_index = line_meta["hunk_index"]
|
||||
if hunk_index == -1 or hunk_index >= len(self.hunk_sections):
|
||||
self.present_mode_message(_("Hunk section."))
|
||||
return
|
||||
|
||||
hunk_section = self.hunk_sections[hunk_index]
|
||||
file_index = hunk_section["file_index"]
|
||||
file_position = file_index + 1 if file_index >= 0 else 0
|
||||
hunk_position = hunk_section["position_in_file"]
|
||||
hunk_total = hunk_section["hunks_in_file"]
|
||||
|
||||
message = (
|
||||
_("Hunk ")
|
||||
+ str(hunk_position)
|
||||
+ _(" of ")
|
||||
+ str(hunk_total)
|
||||
+ _(" in file ")
|
||||
+ str(file_position)
|
||||
+ _(" of ")
|
||||
+ str(len(self.file_sections))
|
||||
+ _(". Plus ")
|
||||
+ str(hunk_section["added_count"])
|
||||
+ _(", minus ")
|
||||
+ str(hunk_section["removed_count"])
|
||||
+ "."
|
||||
)
|
||||
|
||||
if self._get_diff_verbosity() == "verbose":
|
||||
message += " " + self._line_content_for_speech(line_text, "hunk_header")
|
||||
|
||||
self.present_mode_message(message)
|
||||
|
||||
def _line_content_for_speech(self, line_text, role):
|
||||
line_text = self._normalize_diff_line_content(line_text, role)
|
||||
if line_text.strip() == "":
|
||||
return _("blank")
|
||||
return line_text
|
||||
|
||||
def _normalize_diff_line_content(self, line_text, role):
|
||||
if role == "added_line":
|
||||
stripped_line = line_text.lstrip("+")
|
||||
if stripped_line.strip() == "" and line_text.startswith("+"):
|
||||
return _("added")
|
||||
return stripped_line
|
||||
if role == "removed_line":
|
||||
stripped_line = line_text.lstrip("-")
|
||||
if stripped_line.strip() == "" and line_text.startswith("-"):
|
||||
return _("removed")
|
||||
return stripped_line
|
||||
return line_text
|
||||
|
||||
def _build_location_text(self, line_index):
|
||||
if line_index < 0 or line_index >= len(self.line_meta):
|
||||
return ""
|
||||
line_meta = self.line_meta[line_index]
|
||||
file_index = line_meta["file_index"]
|
||||
hunk_index = line_meta["hunk_index"]
|
||||
location_parts = []
|
||||
|
||||
if file_index != -1 and file_index < len(self.file_sections):
|
||||
location_parts.append(
|
||||
_("File ")
|
||||
+ str(file_index + 1)
|
||||
+ _(" of ")
|
||||
+ str(len(self.file_sections))
|
||||
+ ".")
|
||||
|
||||
if hunk_index != -1 and hunk_index < len(self.hunk_sections):
|
||||
hunk_info = self.hunk_sections[hunk_index]
|
||||
location_parts.append(
|
||||
_("Hunk ")
|
||||
+ str(hunk_info["position_in_file"])
|
||||
+ _(" of ")
|
||||
+ str(hunk_info["hunks_in_file"])
|
||||
+ "."
|
||||
)
|
||||
|
||||
return " ".join(location_parts)
|
||||
|
||||
def _get_diff_verbosity(self):
|
||||
verbosity = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"general", "diff_verbosity"
|
||||
).lower()
|
||||
if verbosity not in {"compact", "verbose"}:
|
||||
return "compact"
|
||||
return verbosity
|
||||
|
||||
def present_error(self, message):
|
||||
self.present_mode_message(message)
|
||||
|
||||
def present_mode_message(self, message):
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
message,
|
||||
interrupt=True,
|
||||
)
|
||||
|
||||
def _install_diff_bindings(self):
|
||||
self.bindings_backup = self.env["bindings"].copy()
|
||||
self.raw_bindings_backup = self.env["rawBindings"].copy()
|
||||
|
||||
diff_bindings = {
|
||||
str([1, ["KEY_H"]]): "DIFF_NEXT_HUNK",
|
||||
str([1, sorted(["KEY_SHIFT", "KEY_H"])]): "DIFF_PREV_HUNK",
|
||||
str([1, ["KEY_A"]]): "DIFF_NEXT_ADDED",
|
||||
str([1, sorted(["KEY_SHIFT", "KEY_A"])]): "DIFF_PREV_ADDED",
|
||||
str([1, ["KEY_D"]]): "DIFF_NEXT_REMOVED",
|
||||
str([1, sorted(["KEY_SHIFT", "KEY_D"])]): "DIFF_PREV_REMOVED",
|
||||
str([1, ["KEY_F"]]): "DIFF_NEXT_FILE_SECTION",
|
||||
str([1, sorted(["KEY_SHIFT", "KEY_F"])]): "DIFF_PREV_FILE_SECTION",
|
||||
str([1, ["KEY_S"]]): "DIFF_SUMMARY",
|
||||
str([1, ["KEY_F1"]]): "DIFF_KEY_HELP",
|
||||
str([1, ["KEY_UP"]]): "DIFF_PREV_LINE",
|
||||
str([1, ["KEY_DOWN"]]): "DIFF_NEXT_LINE",
|
||||
str([1, ["KEY_LEFT"]]): "DIFF_PREV_CHAR",
|
||||
str([1, ["KEY_RIGHT"]]): "DIFF_NEXT_CHAR",
|
||||
str([1, sorted(["KEY_CTRL", "KEY_LEFT"])]): "DIFF_PREV_WORD",
|
||||
str([1, sorted(["KEY_CTRL", "KEY_RIGHT"])]): "DIFF_NEXT_WORD",
|
||||
str([1, ["KEY_ESC"]]): "EXIT_DIFF_MODE",
|
||||
str([1, sorted(["KEY_FENRIR", "KEY_CTRL", "KEY_D"])]): "TOGGLE_DIFF_MODE",
|
||||
}
|
||||
|
||||
diff_raw_bindings = {
|
||||
shortcut: [1, keys]
|
||||
for shortcut, keys in [
|
||||
(str([1, ["KEY_H"]]), ["KEY_H"]),
|
||||
(str([1, sorted(["KEY_SHIFT", "KEY_H"])]), sorted(["KEY_SHIFT", "KEY_H"])),
|
||||
(str([1, ["KEY_A"]]), ["KEY_A"]),
|
||||
(str([1, sorted(["KEY_SHIFT", "KEY_A"])]), sorted(["KEY_SHIFT", "KEY_A"])),
|
||||
(str([1, ["KEY_D"]]), ["KEY_D"]),
|
||||
(str([1, sorted(["KEY_SHIFT", "KEY_D"])]), sorted(["KEY_SHIFT", "KEY_D"])),
|
||||
(str([1, ["KEY_F"]]), ["KEY_F"]),
|
||||
(str([1, sorted(["KEY_SHIFT", "KEY_F"])]), sorted(["KEY_SHIFT", "KEY_F"])),
|
||||
(str([1, ["KEY_S"]]), ["KEY_S"]),
|
||||
(str([1, ["KEY_F1"]]), ["KEY_F1"]),
|
||||
(str([1, ["KEY_UP"]]), ["KEY_UP"]),
|
||||
(str([1, ["KEY_DOWN"]]), ["KEY_DOWN"]),
|
||||
(str([1, ["KEY_LEFT"]]), ["KEY_LEFT"]),
|
||||
(str([1, ["KEY_RIGHT"]]), ["KEY_RIGHT"]),
|
||||
(
|
||||
str([1, sorted(["KEY_CTRL", "KEY_LEFT"])]),
|
||||
sorted(["KEY_CTRL", "KEY_LEFT"]),
|
||||
),
|
||||
(
|
||||
str([1, sorted(["KEY_CTRL", "KEY_RIGHT"])]),
|
||||
sorted(["KEY_CTRL", "KEY_RIGHT"]),
|
||||
),
|
||||
(str([1, ["KEY_ESC"]]), ["KEY_ESC"]),
|
||||
(
|
||||
str([1, sorted(["KEY_FENRIR", "KEY_CTRL", "KEY_D"])]),
|
||||
sorted(["KEY_FENRIR", "KEY_CTRL", "KEY_D"]),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
self.env["bindings"] = diff_bindings
|
||||
self.env["rawBindings"] = diff_raw_bindings
|
||||
|
||||
def _restore_bindings(self):
|
||||
if self.bindings_backup is not None:
|
||||
self.env["bindings"] = self.bindings_backup
|
||||
if self.raw_bindings_backup is not None:
|
||||
self.env["rawBindings"] = self.raw_bindings_backup
|
||||
self.bindings_backup = None
|
||||
self.raw_bindings_backup = None
|
||||
|
||||
def _write_debug(self, message, level):
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
message, level
|
||||
)
|
||||
@@ -96,6 +96,10 @@ class FenrirManager:
|
||||
self.environment["runtime"][
|
||||
"InputManager"
|
||||
].clear_event_buffer()
|
||||
if self.environment["runtime"]["DiffReviewManager"].is_active():
|
||||
self.environment["runtime"][
|
||||
"InputManager"
|
||||
].clear_event_buffer()
|
||||
|
||||
self.detect_shortcut_command()
|
||||
|
||||
@@ -301,6 +305,14 @@ class FenrirManager:
|
||||
if self.environment["runtime"]["InputManager"].is_key_press():
|
||||
if self.command != "":
|
||||
self.singleKeyCommand = True
|
||||
elif (
|
||||
self.environment["runtime"]["DiffReviewManager"].is_active()
|
||||
and self.command != ""
|
||||
):
|
||||
# Diff mode uses non-Fenrir modified bindings (Shift/Ctrl).
|
||||
# Promote resolved shortcuts to executable commands so
|
||||
# combinations like Shift+H and Ctrl+Right are dispatched.
|
||||
self.singleKeyCommand = True
|
||||
|
||||
if not (self.singleKeyCommand or self.modifierInput):
|
||||
return
|
||||
|
||||
@@ -28,6 +28,7 @@ general_data = {
|
||||
"VmenuManager",
|
||||
"QuickMenuManager",
|
||||
"ReadAllManager",
|
||||
"DiffReviewManager",
|
||||
"RemoteManager",
|
||||
"SettingsManager",
|
||||
"SayAllManager",
|
||||
|
||||
@@ -21,4 +21,5 @@ runtime_data = {
|
||||
"FenrirManager": None,
|
||||
"EventManager": None,
|
||||
"ProcessManager": None,
|
||||
"DiffReviewManager": None,
|
||||
}
|
||||
|
||||
@@ -75,10 +75,13 @@ settings_data = {
|
||||
"auto_present_indent_mode": 1,
|
||||
"has_attributes": True,
|
||||
"shell": "",
|
||||
"diff_presentation": "both",
|
||||
"diff_verbosity": "compact",
|
||||
},
|
||||
"focus": {
|
||||
"cursor": True,
|
||||
"highlight": False,
|
||||
"tui": False,
|
||||
},
|
||||
"remote": {
|
||||
"enabled": True,
|
||||
|
||||
@@ -16,6 +16,7 @@ from fenrirscreenreader.core import commandManager
|
||||
from fenrirscreenreader.core import cursorManager
|
||||
from fenrirscreenreader.core import debug
|
||||
from fenrirscreenreader.core import debugManager
|
||||
from fenrirscreenreader.core import diffReviewManager
|
||||
from fenrirscreenreader.core import environment
|
||||
from fenrirscreenreader.core import eventManager
|
||||
from fenrirscreenreader.core import helpManager
|
||||
@@ -642,6 +643,11 @@ class SettingsManager:
|
||||
environment["runtime"]["RemoteManager"] = remoteManager.RemoteManager()
|
||||
environment["runtime"]["RemoteManager"].initialize(environment)
|
||||
|
||||
environment["runtime"][
|
||||
"DiffReviewManager"
|
||||
] = diffReviewManager.DiffReviewManager()
|
||||
environment["runtime"]["DiffReviewManager"].initialize(environment)
|
||||
|
||||
if environment["runtime"]["InputManager"].get_shortcut_type() == "KEY":
|
||||
if not os.path.exists(
|
||||
self.get_setting("keyboard", "keyboard_layout")
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
version = "2026.01.28"
|
||||
version = "2026.04.04"
|
||||
code_name = "master"
|
||||
|
||||
@@ -68,7 +68,9 @@ class driver(inputDriver):
|
||||
self.gDevices = {}
|
||||
self.iDeviceNo = 0
|
||||
self.watch_dog = Value(c_bool, True)
|
||||
self.UInputinject = UInput()
|
||||
self.UInputinject = (
|
||||
UInput() if _evdevAvailable and "UInput" in globals() else None
|
||||
)
|
||||
self._deviceLock = threading.Lock()
|
||||
|
||||
def initialize(self, environment):
|
||||
@@ -634,6 +636,8 @@ class driver(inputDriver):
|
||||
self.uDevices[fd] = None
|
||||
if self.uDevices[fd] is not None:
|
||||
return
|
||||
if "UInput" not in globals():
|
||||
return
|
||||
try:
|
||||
self.uDevices[fd] = UInput.from_device(
|
||||
self.iDevices[fd], name="fenrir-uinput", phys="fenrir-uinput"
|
||||
@@ -936,6 +940,8 @@ class driver(inputDriver):
|
||||
"""
|
||||
if not self._initialized:
|
||||
return
|
||||
if self.UInputinject is None:
|
||||
return
|
||||
try:
|
||||
self.UInputinject.write(e.EV_KEY, e.ecodes[key], state)
|
||||
self.UInputinject.syn()
|
||||
|
||||
Reference in New Issue
Block a user