More trouble with pty vs console behavior.
This commit is contained in:
@@ -249,11 +249,7 @@ class ScreenManager:
|
||||
cursor_line_end = (
|
||||
cursor_line_start + 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 (
|
||||
cursor_moved_horizontally = (
|
||||
abs(
|
||||
self.env["screen"]["old_cursor"]["x"]
|
||||
- self.env["screen"]["new_cursor"]["x"]
|
||||
@@ -261,6 +257,32 @@ class ScreenManager:
|
||||
>= 1
|
||||
and self.env["screen"]["old_cursor"]["y"]
|
||||
== self.env["screen"]["new_cursor"]["y"]
|
||||
)
|
||||
cursor_line_typing_delta = (
|
||||
self._get_cursor_line_typing_delta()
|
||||
)
|
||||
cursor_line_is_typing = (
|
||||
cursor_line_typing_delta is not None
|
||||
and (
|
||||
cursor_moved_horizontally
|
||||
or self._is_recent_input()
|
||||
)
|
||||
)
|
||||
|
||||
# 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 (
|
||||
cursor_line_is_typing
|
||||
and cursor_line_typing_delta["changed_lines"]
|
||||
== [cursor_line_typing_delta["cursor_y"]]
|
||||
):
|
||||
diff_list = [
|
||||
"+ " + cursor_line_typing_delta["text"]
|
||||
]
|
||||
typing = True
|
||||
elif (
|
||||
cursor_moved_horizontally
|
||||
and self.env["screen"]["new_content_text"][
|
||||
:cursor_line_start
|
||||
]
|
||||
@@ -356,11 +378,33 @@ class ScreenManager:
|
||||
# Not typing - handle as line-by-line content change
|
||||
# This catches: incoming messages, screen updates,
|
||||
# application output, etc.
|
||||
appended_text = self._get_recent_cursor_line_append()
|
||||
if appended_text is not None:
|
||||
diff_list = ["+ " + appended_text]
|
||||
typing_delta = (
|
||||
cursor_line_typing_delta
|
||||
if cursor_line_is_typing
|
||||
else None
|
||||
)
|
||||
if (
|
||||
typing_delta is not None
|
||||
and typing_delta["changed_lines"]
|
||||
== [typing_delta["cursor_y"]]
|
||||
):
|
||||
diff_list = ["+ " + typing_delta["text"]]
|
||||
typing = True
|
||||
else:
|
||||
old_content_text = self.env["screen"][
|
||||
"old_content_text"
|
||||
]
|
||||
new_content_text = self.env["screen"][
|
||||
"new_content_text"
|
||||
]
|
||||
if typing_delta is not None:
|
||||
old_lines = old_content_text.split("\n")
|
||||
new_lines = new_content_text.split("\n")
|
||||
new_lines[typing_delta["cursor_y"]] = old_lines[
|
||||
typing_delta["cursor_y"]
|
||||
]
|
||||
old_content_text = "\n".join(old_lines)
|
||||
new_content_text = "\n".join(new_lines)
|
||||
|
||||
# Pre-process screen text for comparison - collapse multiple spaces to single space
|
||||
# This normalization prevents spurious diffs from spacing
|
||||
@@ -369,17 +413,13 @@ class ScreenManager:
|
||||
" ",
|
||||
self.env["runtime"][
|
||||
"ScreenManager"
|
||||
].get_window_area_in_text(
|
||||
self.env["screen"]["old_content_text"]
|
||||
),
|
||||
].get_window_area_in_text(old_content_text),
|
||||
)
|
||||
new_screen_text = self._space_normalize_regex.sub(
|
||||
" ",
|
||||
self.env["runtime"][
|
||||
"ScreenManager"
|
||||
].get_window_area_in_text(
|
||||
self.env["screen"]["new_content_text"]
|
||||
),
|
||||
].get_window_area_in_text(new_content_text),
|
||||
)
|
||||
|
||||
diff = self.differ.compare(
|
||||
@@ -458,13 +498,20 @@ class ScreenManager:
|
||||
)
|
||||
return screen in ignore_screens
|
||||
|
||||
def _get_recent_cursor_line_append(self):
|
||||
def _is_recent_input(self):
|
||||
try:
|
||||
if time.time() - self.env["runtime"][
|
||||
"InputManager"
|
||||
].get_last_input_time() > 0.3:
|
||||
return None
|
||||
return (
|
||||
time.time()
|
||||
- self.env["runtime"][
|
||||
"InputManager"
|
||||
].get_last_input_time()
|
||||
<= 0.3
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _get_cursor_line_typing_delta(self):
|
||||
try:
|
||||
old_lines = self.env["screen"]["old_content_text"].split("\n")
|
||||
new_lines = self.env["screen"]["new_content_text"].split("\n")
|
||||
cursor_y = self.env["screen"]["new_cursor"]["y"]
|
||||
@@ -480,19 +527,40 @@ class ScreenManager:
|
||||
for index, old_line in enumerate(old_lines)
|
||||
if index >= len(new_lines) or old_line != new_lines[index]
|
||||
]
|
||||
if changed_lines != [cursor_y]:
|
||||
if cursor_y not in changed_lines:
|
||||
return None
|
||||
|
||||
old_line = old_lines[cursor_y]
|
||||
new_line = new_lines[cursor_y]
|
||||
old_line = old_line.rstrip()
|
||||
if not new_line.startswith(old_line):
|
||||
old_line = old_lines[cursor_y].rstrip()
|
||||
new_line = new_lines[cursor_y].rstrip()
|
||||
if old_line == new_line:
|
||||
return None
|
||||
|
||||
appended_text = new_line[len(old_line):].strip()
|
||||
if appended_text == "" or len(appended_text) > 4:
|
||||
matcher = difflib.SequenceMatcher(
|
||||
None,
|
||||
old_line,
|
||||
new_line,
|
||||
autojunk=False,
|
||||
)
|
||||
inserted_parts = []
|
||||
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
||||
if tag == "equal":
|
||||
continue
|
||||
if tag == "insert":
|
||||
inserted_parts.append(new_line[j1:j2])
|
||||
continue
|
||||
if tag == "replace" and old_line[i1:i2].strip() == "":
|
||||
inserted_parts.append(new_line[j1:j2])
|
||||
continue
|
||||
return None
|
||||
return appended_text
|
||||
|
||||
typed_text = "".join(inserted_parts).strip()
|
||||
if typed_text == "" or len(typed_text) > 4:
|
||||
return None
|
||||
return {
|
||||
"text": typed_text,
|
||||
"cursor_y": cursor_y,
|
||||
"changed_lines": changed_lines,
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@@ -70,11 +70,11 @@ def test_tui_input_line_append_is_typing_delta():
|
||||
manager, env = _build_screen_manager(
|
||||
"\n".join(
|
||||
[
|
||||
"<UserA> hi".ljust(20),
|
||||
"[Username] ".ljust(20),
|
||||
]
|
||||
),
|
||||
{"x": 11, "y": 1},
|
||||
"<UserA> hi".ljust(20),
|
||||
"[Username] ".ljust(20),
|
||||
]
|
||||
),
|
||||
{"x": 11, "y": 1},
|
||||
)
|
||||
env["runtime"]["InputManager"].get_last_input_time.return_value = time.time()
|
||||
|
||||
@@ -83,11 +83,11 @@ def test_tui_input_line_append_is_typing_delta():
|
||||
"bytes": b"",
|
||||
"lines": 2,
|
||||
"columns": 20,
|
||||
"textCursor": {"x": 12, "y": 1},
|
||||
"textCursor": {"x": 12, "y": 1},
|
||||
"screen": "pty",
|
||||
"text": "\n".join(
|
||||
[
|
||||
"<UserA> hi".ljust(20),
|
||||
"<UserA> hi".ljust(20),
|
||||
"[Username] l".ljust(20),
|
||||
]
|
||||
),
|
||||
@@ -98,3 +98,100 @@ def test_tui_input_line_append_is_typing_delta():
|
||||
|
||||
assert env["screen"]["new_delta"] == "l"
|
||||
assert env["screen"]["new_delta_is_typing"] is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_tui_input_line_cursor_jump_keeps_only_typed_delta():
|
||||
manager, env = _build_screen_manager(
|
||||
"[Username] ".ljust(30),
|
||||
{"x": 0, "y": 0},
|
||||
)
|
||||
|
||||
manager.update(
|
||||
{
|
||||
"bytes": b"",
|
||||
"lines": 1,
|
||||
"columns": 30,
|
||||
"textCursor": {"x": 12, "y": 0},
|
||||
"screen": "pty",
|
||||
"text": "[Username] l".ljust(30),
|
||||
"attributes": [],
|
||||
},
|
||||
"onScreenUpdate",
|
||||
)
|
||||
|
||||
assert env["screen"]["new_delta"] == "l"
|
||||
assert env["screen"]["new_delta_is_typing"] is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_tui_input_line_insert_with_channel_prefix_is_typing_delta():
|
||||
manager, env = _build_screen_manager(
|
||||
"\n".join(
|
||||
[
|
||||
"<UserA> hi".ljust(40),
|
||||
"[#channel] [Username] | 12:00".ljust(40),
|
||||
]
|
||||
),
|
||||
{"x": 22, "y": 1},
|
||||
)
|
||||
env["runtime"]["InputManager"].get_last_input_time.return_value = time.time()
|
||||
|
||||
manager.update(
|
||||
{
|
||||
"bytes": b"",
|
||||
"lines": 2,
|
||||
"columns": 40,
|
||||
"textCursor": {"x": 23, "y": 1},
|
||||
"screen": "pty",
|
||||
"text": "\n".join(
|
||||
[
|
||||
"<UserA> hi".ljust(40),
|
||||
"[#channel] [Username] l | 12:00".ljust(40),
|
||||
]
|
||||
),
|
||||
"attributes": [],
|
||||
},
|
||||
"onScreenUpdate",
|
||||
)
|
||||
|
||||
assert env["screen"]["new_delta"] == "l"
|
||||
assert env["screen"]["new_delta_is_typing"] is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_tui_input_line_typing_is_filtered_from_mixed_repaint_delta():
|
||||
manager, env = _build_screen_manager(
|
||||
"\n".join(
|
||||
[
|
||||
"<UserA> hi".ljust(40),
|
||||
"[#channel] [Username] | 12:00".ljust(40),
|
||||
"status 12:00".ljust(40),
|
||||
]
|
||||
),
|
||||
{"x": 22, "y": 1},
|
||||
)
|
||||
env["runtime"]["InputManager"].get_last_input_time.return_value = time.time()
|
||||
|
||||
manager.update(
|
||||
{
|
||||
"bytes": b"",
|
||||
"lines": 3,
|
||||
"columns": 40,
|
||||
"textCursor": {"x": 23, "y": 1},
|
||||
"screen": "pty",
|
||||
"text": "\n".join(
|
||||
[
|
||||
"<UserA> hi".ljust(40),
|
||||
"[#channel] [Username] l | 12:00".ljust(40),
|
||||
"status 12:01".ljust(40),
|
||||
]
|
||||
),
|
||||
"attributes": [],
|
||||
},
|
||||
"onScreenUpdate",
|
||||
)
|
||||
|
||||
assert "Username" not in env["screen"]["new_delta"]
|
||||
assert "#channel" not in env["screen"]["new_delta"]
|
||||
assert env["screen"]["new_delta_is_typing"] is False
|
||||
|
||||
Reference in New Issue
Block a user