Skip to content

Commit

Permalink
twisted#12046 Fix handling of function keys in conch.insults.window.W…
Browse files Browse the repository at this point in the history
…idget object (twisted#12047)

twisted.conch.insults.window.Widget.functionKeyReceived now dispatches functional key events to corresponding `func_KEYNAME` methods, where `KEYNAME` can be `F1`, `F2`, `HOME`, `UP_ARROW` etc. This is a regression introduced with twisted#8214 in Twisted 16.5.0, where events changed from `const` objects to bytestrings in square brackets like `[F1]`.
  • Loading branch information
glyph committed Mar 11, 2024
2 parents 63df84e + 3a56957 commit dabf462
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 20 deletions.
18 changes: 1 addition & 17 deletions src/twisted/conch/insults/insults.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,23 +431,7 @@ def log(s):
"CONTROL",
)


class _const:
"""
@ivar name: A string naming this constant
"""

def __init__(self, name: str) -> None:
self.name = name

def __repr__(self) -> str:
return "[" + self.name + "]"

def __bytes__(self) -> bytes:
return ("[" + self.name + "]").encode("ascii")


FUNCTION_KEYS = [_const(_name).__bytes__() for _name in _KEY_NAMES]
FUNCTION_KEYS = [f"[{_name}]".encode("ascii") for _name in _KEY_NAMES]


@implementer(ITerminalTransport)
Expand Down
12 changes: 10 additions & 2 deletions src/twisted/conch/insults/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
@author: Jp Calderone
"""

from __future__ import annotations

import array

from twisted.conch.insults import helper, insults
Expand Down Expand Up @@ -47,7 +49,8 @@ class Widget:
focused = False
parent = None
dirty = False
width = height = None
width: int | None = None
height: int | None = None

def repaint(self):
if not self.dirty:
Expand Down Expand Up @@ -109,7 +112,12 @@ def functionKeyReceived(self, keyID, modifier):
name = keyID
if not isinstance(keyID, str):
name = name.decode("utf-8")
func = getattr(self, "func_" + name, None)

# Peel off the square brackets added by the computed definition of
# twisted.conch.insults.insults.FUNCTION_KEYS.
methodName = "func_" + name[1:-1]

func = getattr(self, methodName, None)
if func is not None:
func(modifier)

Expand Down
1 change: 1 addition & 0 deletions src/twisted/conch/newsfragments/12046.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
twisted.conch.insults.window.Widget.functionKeyReceived now dispatches functional key events to corresponding `func_KEYNAME` methods, where `KEYNAME` can be `F1`, `F2`, `HOME`, `UP_ARROW` etc. This is a regression introduced with #8214 in Twisted 16.5.0, where events changed from `const` objects to bytestrings in square brackets like `[F1]`.
106 changes: 105 additions & 1 deletion src/twisted/conch/test/test_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@

from typing import Callable

from twisted.conch.insults.window import ScrolledArea, TextOutput, TopWindow
from twisted.conch.insults.insults import ServerProtocol
from twisted.conch.insults.window import (
ScrolledArea,
Selection,
TextOutput,
TopWindow,
Widget,
)
from twisted.trial.unittest import TestCase


Expand Down Expand Up @@ -66,3 +73,100 @@ def test_parent(self) -> None:
scrolled = ScrolledArea(widget)
self.assertIs(widget.parent, scrolled._viewport)
self.assertIs(scrolled._viewport.parent, scrolled)


class SelectionTests(TestCase):
"""
Change focused entry in L{Selection} using function keys.
"""

def setUp(self) -> None:
"""
Create L{ScrolledArea} widget with 10 elements and position selection to 5th element.
"""
seq: list[bytes] = [f"{_num}".encode("ascii") for _num in range(10)]
self.widget = Selection(seq, None)
self.widget.height = 10
self.widget.focusedIndex = 5

def test_selectionDownArrow(self) -> None:
"""
Send DOWN_ARROW to select element just below the current one.
"""
self.widget.keystrokeReceived(ServerProtocol.DOWN_ARROW, None) # type: ignore[attr-defined]
self.assertIs(self.widget.focusedIndex, 6)

def test_selectionUpArrow(self) -> None:
"""
Send UP_ARROW to select element just above the current one.
"""
self.widget.keystrokeReceived(ServerProtocol.UP_ARROW, None) # type: ignore[attr-defined]
self.assertIs(self.widget.focusedIndex, 4)

def test_selectionPGDN(self) -> None:
"""
Send PGDN to select element one page down (here: last element).
"""
self.widget.keystrokeReceived(ServerProtocol.PGDN, None) # type: ignore[attr-defined]
self.assertIs(self.widget.focusedIndex, 9)

def test_selectionPGUP(self) -> None:
"""
Send PGUP to select element one page up (here: first element).
"""
self.widget.keystrokeReceived(ServerProtocol.PGUP, None) # type: ignore[attr-defined]
self.assertIs(self.widget.focusedIndex, 0)


class RecordingWidget(Widget):
"""
A dummy Widget implementation to test handling of function keys by
recording keyReceived events.
"""

def __init__(self) -> None:
Widget.__init__(self)
self.triggered: list[str] = []

def func_F1(self, modifier: str) -> None:
self.triggered.append("F1")

def func_HOME(self, modifier: str) -> None:
self.triggered.append("HOME")

def func_DOWN_ARROW(self, modifier: str) -> None:
self.triggered.append("DOWN_ARROW")

def func_UP_ARROW(self, modifier: str) -> None:
self.triggered.append("UP_ARROW")

def func_PGDN(self, modifier: str) -> None:
self.triggered.append("PGDN")

def func_PGUP(self, modifier: str) -> None:
self.triggered.append("PGUP")


class WidgetFunctionKeyTests(TestCase):
"""
Call functionKeyReceived with key values from insults.ServerProtocol
"""

def test_functionKeyReceivedDispatch(self) -> None:
"""
L{Widget.functionKeyReceived} dispatches its input, a constant on
ServerProtocol, to a matched C{func_KEY} method.
"""
widget = RecordingWidget()

def checkOneKey(key: str) -> None:
widget.functionKeyReceived(getattr(ServerProtocol, key), None)
self.assertEqual([key], widget.triggered)
widget.triggered.clear()

checkOneKey("F1")
checkOneKey("HOME")
checkOneKey("DOWN_ARROW")
checkOneKey("UP_ARROW")
checkOneKey("PGDN")
checkOneKey("PGUP")

0 comments on commit dabf462

Please sign in to comment.