Skip to content

Commit 5898cbb

Browse files
authored
fix(cli): fix renku workflow visualize crashing for large graphs due to a curses error (#3273)
1 parent 0e59063 commit 5898cbb

File tree

2 files changed

+39
-11
lines changed

2 files changed

+39
-11
lines changed

renku/ui/cli/utils/curses.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from renku.command.view_model.text_canvas import Point
2424
from renku.core import errors
25+
from renku.core.util import communication
2526
from renku.domain_model.provenance.activity import Activity
2627

2728

@@ -59,13 +60,14 @@ def __init__(
5960
self.current_layer = 0
6061
self.layer_position = 0
6162
self.max_layer = len(navigation_data) - 1
62-
self.y_pos = 0
63-
self.x_pos = 0
63+
self.y_pos: int = 0
64+
self.x_pos: int = 0
6465
self.color_cache: Dict[str, int] = {}
6566
self.activity_overlay: Optional[curses._CursesWindow] = None
6667
self.help_overlay: Optional[curses._CursesWindow] = None
6768
self._select_activity()
6869
self.use_color = use_color
70+
self.free_move: bool = False
6971

7072
def _init_curses(self, screen):
7173
"""Initialize curses screen for interactive mode."""
@@ -88,7 +90,17 @@ def _init_curses(self, screen):
8890
text_data_lines = self.text_data.splitlines()
8991

9092
self.content_max_x = max(len(line) for line in text_data_lines)
91-
self.content_max_y = len(self.text_data)
93+
self.content_max_y = len(text_data_lines)
94+
95+
int16_max = 32767
96+
if self.content_max_y > int16_max or self.content_max_x > int16_max:
97+
communication.warn(
98+
f"Graph is too large for interactive visualization, cropping to {int16_max} lines/columns."
99+
)
100+
self.content_max_x = min(self.content_max_x, int16_max)
101+
self.content_max_y = min(self.content_max_y, int16_max)
102+
text_data_lines = [line[: self.content_max_x] for line in text_data_lines[self.content_max_y]]
103+
92104
self.content_pad = curses.newpad(self.content_max_y, self.content_max_x)
93105
for i, l in enumerate(text_data_lines):
94106
self._addstr_with_color_codes(self.content_pad, i, 0, l)
@@ -281,6 +293,7 @@ def _update_help_overlay(self, screen):
281293
content = (
282294
"Navigate using arrow keys\n"
283295
"Press <enter> to show activity details\n"
296+
"Press <f> to toggle free arrow movement\n"
284297
"Press <h> to show/hide this help\n"
285298
"Press <q> to exit\n"
286299
)
@@ -290,7 +303,7 @@ def _update_help_overlay(self, screen):
290303
del self.help_overlay
291304
self.help_overlay = None
292305

293-
def _move_viewscreen(self):
306+
def _move_viewscreen_to_activity(self):
294307
"""Move viewscreen to include selected activity."""
295308
if self.activity_start.x - 1 < self.x_pos:
296309
self.x_pos = max(self.activity_start.x - 1, 0)
@@ -314,25 +327,40 @@ def _loop(self, screen):
314327

315328
# handle keypress
316329
if input_char == curses.KEY_DOWN or chr(input_char) == "k":
317-
self._change_layer(1)
330+
if self.free_move:
331+
self.y_pos = min(self.y_pos + 1, self.content_max_y - self.rows - 1)
332+
else:
333+
self._change_layer(1)
318334
elif input_char == curses.KEY_UP or chr(input_char) == "i":
319-
self._change_layer(-1)
335+
if self.free_move:
336+
self.y_pos = max(self.y_pos - 1, 0)
337+
else:
338+
self._change_layer(-1)
320339
elif input_char == curses.KEY_RIGHT or chr(input_char) == "l":
321-
self._change_layer_position(1)
340+
if self.free_move:
341+
self.x_pos = min(self.x_pos + 1, self.content_max_x - self.cols - 1)
342+
else:
343+
self._change_layer_position(1)
322344
elif input_char == curses.KEY_LEFT or chr(input_char) == "j":
323-
self._change_layer_position(-1)
345+
if self.free_move:
346+
self.x_pos = max(self.x_pos - 1, 0)
347+
else:
348+
self._change_layer_position(-1)
324349
elif input_char == curses.KEY_ENTER or input_char == 10 or input_char == 13:
325350
self._update_activity_overlay(screen)
326351
elif chr(input_char) == "h":
327352
self._update_help_overlay(screen)
353+
elif chr(input_char) == "f":
354+
self.free_move = not self.free_move
328355
elif input_char < 256 and chr(input_char) == "q":
329356
running = False
330357

331358
self._unblink_text(self.content_pad, self.activity_start, self.activity_end, bold=True)
332359
self._select_activity()
333360
self._blink_text(self.content_pad, self.activity_start, self.activity_end, bold=True)
334361

335-
self._move_viewscreen()
362+
if not self.free_move:
363+
self._move_viewscreen_to_activity()
336364

337365
self._refresh(screen)
338366

renku/ui/cli/workflow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,7 +1193,7 @@ def execute(
11931193
)
11941194

11951195

1196-
@workflow.command(no_args_is_help=True)
1196+
@workflow.command()
11971197
@click.option(
11981198
"--from",
11991199
"sources",
@@ -1267,7 +1267,7 @@ def visualize(sources, columns, exclude_files, ascii, revision, format, interact
12671267
max_width = max(node[1].x for layer in navigation_data for node in layer)
12681268
tty_size = shutil.get_terminal_size(fallback=(120, 120))
12691269

1270-
if no_pager or not sys.stdout.isatty() or os.system(f"less 2>{os.devnull}") != 0:
1270+
if no_pager or not sys.stdout.isatty() or os.system(f"less 2>{os.devnull}") != 0: # nosec
12711271
use_pager = False
12721272
elif pager:
12731273
use_pager = True

0 commit comments

Comments
 (0)