Skip to content

Commit

Permalink
Added window resizing redraws to curses mode (#663)
Browse files Browse the repository at this point in the history
* Added window resizing redraws to curses mode

* Cleanup

* Cleanup

* Notes from PR

* Simplified space calculation for messages
  • Loading branch information
ramo-j authored Oct 22, 2022
1 parent d3d1ea7 commit a065041
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 23 deletions.
48 changes: 30 additions & 18 deletions dftimewolf/cli/curses_display_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from typing import Any, Callable, Dict, List, Optional, Union

import curses
import signal


class Status(Enum):
"""Enum class for module states.
Expand Down Expand Up @@ -157,6 +159,7 @@ def StartCurses(self) -> None:
curses.noecho()
curses.cbreak()
self._stdscr.keypad(True)
signal.signal(signal.SIGWINCH, self.SIGWINCH_Handler)

def EndCurses(self) -> None:
"""Curses finalisation actions."""
Expand Down Expand Up @@ -210,23 +213,33 @@ def EnqueueMessage(self,
if self._messages_longest_source_len < len(source):
self._messages_longest_source_len = len(source)

_, x = self._stdscr.getmaxyx()

lines = []

# textwrap.wrap behaves strangely if there are newlines in the content
for line in content.split('\n'):
lines += textwrap.wrap(line,
width=x - self._messages_longest_source_len - 8,
subsequent_indent=' ',
replace_whitespace=False)

for line in lines:
if line:
self._messages.append(Message(source, line, is_error))

self.Draw()

def PrepareMessagesForDisplay(self, available_lines: int) -> List[str]:
"""Prepares the list of messages to be displayed.
Args:
available_lines: The number of lines available to print messages.
Returns:
A list of strings, formatted for display."""
_, x = self._stdscr.getmaxyx()

lines = []

for m in self._messages:
lines.extend(
textwrap.wrap(m.Stringify(self._messages_longest_source_len),
width=x - self._messages_longest_source_len - 8,
initial_indent=' ', subsequent_indent=' ',
replace_whitespace=False))

return lines[-available_lines:]

def EnqueuePreflight(self,
name: str,
dependencies: List[str],
Expand Down Expand Up @@ -336,13 +349,8 @@ def Draw(self) -> None:
curr_line += 1

message_space = y - 4 - curr_line
start = len(self._messages) - message_space
start = 0 if start < 0 else start

# Slice the aray, we may not be able to fit all messages on the screen
for m in self._messages[start:]:
self._stdscr.addstr(curr_line, 0,
f' {m.Stringify(self._messages_longest_source_len)}'[:x])
for m in self.PrepareMessagesForDisplay(message_space):
self._stdscr.addstr(curr_line, 0, m[:x])
curr_line += 1

# Exceptions
Expand Down Expand Up @@ -383,6 +391,10 @@ def Pause(self) -> None:
self._stdscr.getkey()
self._stdscr.addstr(y - 1, 0, " ")

def SIGWINCH_Handler(self, *unused_argvs: Any) -> None:
"""Redraw the window when SIGWINCH is raised."""
self.Draw()


class CDMStringIOWrapper(io.StringIO):
"""Subclass of io.StringIO, adds a callback to write().
Expand Down
68 changes: 63 additions & 5 deletions tests/cli/curses_display_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,6 @@ def testMessages(self):
mock_getmaxyx.return_value = 30, 60
self.cdm.EnqueueMessage('source 1', 'content 1')
self.cdm.EnqueueMessage('a longer source name', 'error message', True)
self.cdm.EnqueueMessage('source 2', 'this message is longer than the screen width, 60 characters, and will need to be printed on multiple lines.') # pylint: disable=line-too-long
self.cdm.EnqueueMessage('source 3', 'this message\nhas a newline in it')

try:
Expand All @@ -358,10 +357,6 @@ def testMessages(self):
'Messages',
' [ source 1 ] content 1',
' [ a longer source name ] \u001b[31;1merror message\u001b[0m',
' [ source 2 ] this message is longer than the',
' [ source 2 ] screen width, 60 characters,',
' [ source 2 ] and will need to be printed on',
' [ source 2 ] multiple lines.',
' [ source 3 ] this message',
' [ source 3 ] has a newline in it',
'',
Expand Down Expand Up @@ -551,6 +546,69 @@ def testShortWindowDraw(self):
self.assertEqual(mock_addstr.call_count, 12)
# pylint: enable=line-too-long

def testWindowResizeDraw(self):
"""Tests resizing the window."""
with mock.patch('curses.cbreak'), \
mock.patch('curses.noecho'), \
mock.patch('curses.initscr'):
self.cdm.StartCurses()

with mock.patch.object(self.cdm._stdscr, 'getmaxyx') as mock_getmaxyx, \
mock.patch.object(self.cdm, 'Draw'):
mock_getmaxyx.return_value = 30, 60

self.cdm.SetRecipe('Recipe name')
self.cdm.EnqueuePreflight('First Preflight', [], '1st Preflight')
self.cdm.EnqueueModule('First Module', [], '1st Module')
self.cdm.EnqueueMessage('source', 'A standard message')
self.cdm.EnqueueMessage('source', 'this message is longer than the screen width, 60 characters, and will need to be printed on multiple lines.') # pylint: disable=line-too-long
self.cdm.EnqueueMessage('source', 'Another standard message')

with mock.patch.object(self.cdm._stdscr, 'getmaxyx') as mock_getmaxyx, \
mock.patch.object(self.cdm._stdscr, 'clear') as mock_clear, \
mock.patch.object(self.cdm._stdscr, 'addstr') as mock_addstr:
mock_getmaxyx.return_value = 30, 60

self.cdm.Draw()

mock_clear.assert_called_once_with()
# pylint: disable=line-too-long
mock_addstr.assert_has_calls([
mock.call(0, 0, ' Recipe name'),
mock.call(1, 0, ' Preflights:'),
mock.call(2, 0, ' 1st Preflight: Pending'),
mock.call(3, 0, ' Modules:'),
mock.call(4, 0, ' 1st Module: Pending'),
mock.call(6, 0, ' Messages:'),
mock.call(7, 0, ' [ source ] A standard message'),
mock.call(8, 0, ' [ source ] this message is longer than the'),
mock.call(9, 0, ' screen width, 60 characters, and will need'),
mock.call(10, 0, ' to be printed on multiple lines.'),
mock.call(11, 0, ' [ source ] Another standard message')])
self.assertEqual(mock_addstr.call_count, 11)
# pylint: enable=line-too-long

with mock.patch.object(self.cdm._stdscr, 'getmaxyx') as mock_getmaxyx, \
mock.patch.object(self.cdm._stdscr, 'clear') as mock_clear, \
mock.patch.object(self.cdm._stdscr, 'addstr') as mock_addstr:
mock_getmaxyx.return_value = 30, 140

self.cdm.SIGWINCH_Handler(None, None)

mock_clear.assert_called_once_with()
# pylint: disable=line-too-long
mock_addstr.assert_has_calls([
mock.call(0, 0, ' Recipe name'),
mock.call(1, 0, ' Preflights:'),
mock.call(2, 0, ' 1st Preflight: Pending'),
mock.call(3, 0, ' Modules:'),
mock.call(4, 0, ' 1st Module: Pending'),
mock.call(6, 0, ' Messages:'),
mock.call(7, 0, ' [ source ] A standard message'),
mock.call(8, 0, ' [ source ] this message is longer than the screen width, 60 characters, and will need to be printed on multiple lines.'), # pylint: disable=line-too-long
mock.call(9, 0, ' [ source ] Another standard message')])
self.assertEqual(mock_addstr.call_count, 9)


class CDMStringIOWrapperTest(unittest.TestCase):
"""Tests for the CDMStringIOWrapper class."""
Expand Down

0 comments on commit a065041

Please sign in to comment.