vibefpmgui.window.output
The Output panel: a command's output (limited in size), or one item's.
1"""The Output panel: a command's output (limited in size), or one item's.""" 2 3from __future__ import annotations 4 5import html 6import re 7 8from qtpy.QtCore import Qt 9from qtpy.QtGui import QFontDatabase, QTextCursor 10from qtpy.QtWidgets import ( 11 QHBoxLayout, 12 QLabel, 13 QPushButton, 14 QSizePolicy, 15 QStackedWidget, 16 QVBoxLayout, 17 QWidget, 18) 19 20from .. import ansi 21from ..compat import reveal_label 22from ..console_view import ConsoleView 23from ..editor import EditorTab 24from ..output_links import OutputLinks 25from ..project_tree import reveal_in_file_manager 26 27# A command's output beyond this many characters isn't added to the Output 28# panel (it would freeze the app: an lfortran crash, for one, prints the whole 29# compiled program, 100+ MB); the last lines are shown when it finishes and 30# everything is saved to a file. 31OUTPUT_SHOW_LIMIT = 2_000_000 32 33 34OUTPUT_TAIL_LINES = 200 35 36 37MAX_LINE_SHOWN = 20_000 # longer lines are shortened 38 39 40def _breakable(text: str) -> str: 41 """Let long names wrap: add zero-width spaces after _ . / - (word wrap 42 otherwise only breaks at spaces). For display only: text copied from the 43 label may include them.""" 44 return re.sub(r"([_./\\-])(?=\w)", "\\1\u200b", text) 45 46 47class OutputMixin: 48 """The Output panel (a part of `vibefpmgui.main_window.MainWindow`).""" 49 50 def _build_output_panel(self): 51 """The Output panel: all of a command's output, or one item's.""" 52 mono = QFontDatabase.systemFont(QFontDatabase.FixedFont) 53 self.console = ConsoleView() # (fpm's "[ 25%]" progress drawn as badges) 54 self.console.setReadOnly(True) 55 self.console.setFont(mono) 56 self.console.setMaximumBlockCount(20000) 57 # Output of a single test, shown when a test row is clicked. 58 self.test_output = ConsoleView() 59 self.test_output.setReadOnly(True) 60 self.test_output.setFont(mono) 61 # Compiler messages may be colored (lfortran always colors them). 62 self.console_writer = ansi.AnsiWriter(self.console) 63 self.single_writer = ansi.AnsiWriter(self.test_output) 64 # File locations in the output ("At line 12 of file src/x.f90", "src/x.f90:12:5:") open on a click. 65 self.output_links = [] 66 for view in (self.console, self.test_output): 67 links = OutputLinks(view, lambda: self.manifest_path.parent if self.manifest_path else None) 68 links.open_requested.connect(lambda path, line, column: self.navigate_to(path, line, column, 69 preview=True)) 70 self.output_links.append(links) 71 self.output_stack = QStackedWidget() 72 self.output_stack.addWidget(self.console) 73 self.output_stack.addWidget(self.test_output) 74 self._set_output_wrap(EditorTab.word_wrap) 75 console_box = QWidget() 76 cl = QVBoxLayout(console_box) 77 cl.setContentsMargins(0, 0, 0, 0) 78 # (No spacing: macOS buttons already keep some room around themselves, 79 # for the focus ring, which the layout counts as part of the button.) 80 cl.setSpacing(0) 81 # What's shown, when it's one item's output (a test, a file's compile, 82 # an AI review); hidden for all the output ("Output" is the panel's title). 83 self.output_title = QLabel() 84 self.output_title.hide() 85 # Titles like "AI review of a_very_long_file_name.f90 (model)" wrap 86 # instead of forcing the panel wider. 87 self.output_title.setWordWrap(True) 88 self.output_title.setMinimumWidth(40) 89 self.output_title.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) 90 self.output_title.setTextInteractionFlags(Qt.TextSelectableByMouse) 91 # Title alone at the top; the buttons sit below the output. 92 footer = QHBoxLayout() 93 footer.setContentsMargins(0, 0, 0, 0) 94 footer.setSpacing(6) 95 footer.addStretch(1) 96 self.show_all_button = QPushButton("Show all output") 97 self.show_all_button.clicked.connect(self.show_all_output) 98 self.show_all_button.hide() 99 footer.addWidget(self.show_all_button) 100 self.full_output_button = QPushButton("Full Output…") 101 self.full_output_button.setToolTip("The last command's output was too long to show in full; " 102 f"show the file it was saved to in {reveal_label()}") 103 self.full_output_button.clicked.connect(lambda: reveal_in_file_manager(str(self._full_output_path))) 104 self.full_output_button.hide() 105 footer.addWidget(self.full_output_button) 106 clear = QPushButton("Clear") 107 clear.setToolTip("Clear the output") 108 clear.clicked.connect(self.console.clear) 109 footer.addWidget(clear) 110 cl.addWidget(self.output_title) 111 cl.addWidget(self.output_stack, 1) 112 cl.addLayout(footer) 113 return console_box 114 115 def _append(self, text: str, bold: bool = False, colored: bool = False): 116 """Add text to the Output panel; ``colored``: program output, whose 117 terminal color codes are shown as colors (and which is limited to 118 OUTPUT_SHOW_LIMIT characters per command).""" 119 if not colored: 120 self.console_writer.write(text, bold, ansi=False) 121 return 122 if self._full_output is not None: 123 try: 124 self._full_output.write(ansi.strip(text)) 125 except (OSError, ValueError): 126 self._full_output = None 127 if not self._hidden_chars and self._shown_chars + len(text) <= OUTPUT_SHOW_LIMIT: 128 self._shown_chars += len(text) 129 if len(text) > MAX_LINE_SHOWN and max(map(len, text.split("\n"))) > MAX_LINE_SHOWN: 130 text = "\n".join(line if len(line) <= MAX_LINE_SHOWN 131 else line[:MAX_LINE_SHOWN] + f" … [{len(line) - MAX_LINE_SHOWN} more characters]" 132 for line in text.split("\n")) 133 self.console_writer.write(text, bold, ansi=True) 134 return 135 if not self._hidden_chars: 136 self.console_writer.write( 137 f"\n[The output is very long, so the rest isn't shown here (that would freeze the app). " 138 f"The last {OUTPUT_TAIL_LINES} lines are shown when the command finishes, and all of " 139 f"it is saved to {self._full_output_path} (Full Output… below).]\n", True, ansi=False) 140 self.full_output_button.show() 141 self._hidden_chars += len(text) 142 lines = (self._tail_partial + text).split("\n") 143 self._tail_partial = lines.pop() 144 self._hidden_lines += len(lines) 145 self._tail.extend(line[:MAX_LINE_SHOWN] for line in lines) 146 147 def _begin_output(self): 148 """A command is starting: reset the output limit and the saved copy.""" 149 self._end_output(show_tail=False) 150 self._decoder.reset() 151 for links in self.output_links: 152 links.forget() # (files may have come or gone) 153 self._shown_chars = self._hidden_chars = self._hidden_lines = 0 154 self._tail.clear() 155 self._tail_partial = "" 156 self.full_output_button.hide() 157 try: 158 self._full_output = open(self._full_output_path, "w", encoding="utf-8", errors="replace") # noqa: SIM115 159 except OSError: 160 self._full_output = None 161 162 def _end_output(self, show_tail: bool = True): 163 """The command has finished: show the end of any output that wasn't shown.""" 164 if self._full_output is not None: 165 try: 166 self._full_output.close() 167 except OSError: 168 pass 169 self._full_output = None 170 if show_tail and self._hidden_chars: 171 tail = list(self._tail) + ([self._tail_partial] if self._tail_partial else []) 172 skipped = self._hidden_lines + bool(self._tail_partial) - len(tail) 173 self.console_writer.write( 174 f"\n[… {skipped:,} lines ({self._hidden_chars / 1e6:.1f} MB) not shown — " 175 f"full output: {self._full_output_path} …]\n", True, ansi=False) 176 self.console_writer.write("\n".join(tail) + "\n", ansi=True) 177 self._hidden_chars = 0 178 179 def _show_marked_output(self, monitor, name: str): 180 """Show one test's or program's output (or everything when empty).""" 181 if not name: 182 self.show_all_output() 183 return 184 self.build_monitor.clear_selection() 185 other = self.test_monitor if monitor is self.run_monitor else self.run_monitor 186 if other.selected: 187 other.selected = "" 188 other.table.viewport().update() 189 self._show_single_output(name, monitor.output_for(name) or "(no output)") 190 191 def show_test_output(self, name: str): 192 """Show one test's output (or everything when name is empty).""" 193 self._show_marked_output(self.test_monitor, name) 194 195 def show_build_details(self, row: int): 196 """Show one file's compiler messages (or everything when row is -1).""" 197 if row < 0: 198 self.show_all_output() 199 return 200 for monitor in (self.test_monitor, self.run_monitor): 201 if monitor.selected: 202 monitor.selected = "" 203 monitor.table.viewport().update() 204 self._show_single_output(self.build_monitor.details_title(row), 205 self.build_monitor.details_text(row)) 206 207 def _show_single_output(self, title: str, text: str): 208 self.test_output.clear() 209 self.single_writer.reset() 210 self.single_writer.write(text) 211 self.test_output.moveCursor(QTextCursor.Start) 212 self.output_title.setText(f"<b>{_breakable(html.escape(title))}</b>") 213 self.output_title.show() 214 self.output_stack.setCurrentWidget(self.test_output) 215 self.show_all_button.show() 216 217 def show_all_output(self): 218 self.output_title.hide() 219 self.output_stack.setCurrentWidget(self.console) 220 self.show_all_button.hide() 221 self.build_monitor.clear_selection() 222 for monitor in (self.test_monitor, self.run_monitor): 223 if monitor.selected: 224 monitor.selected = "" 225 monitor.table.viewport().update()
OUTPUT_SHOW_LIMIT =
2000000
OUTPUT_TAIL_LINES =
200
MAX_LINE_SHOWN =
20000
class
OutputMixin:
48class OutputMixin: 49 """The Output panel (a part of `vibefpmgui.main_window.MainWindow`).""" 50 51 def _build_output_panel(self): 52 """The Output panel: all of a command's output, or one item's.""" 53 mono = QFontDatabase.systemFont(QFontDatabase.FixedFont) 54 self.console = ConsoleView() # (fpm's "[ 25%]" progress drawn as badges) 55 self.console.setReadOnly(True) 56 self.console.setFont(mono) 57 self.console.setMaximumBlockCount(20000) 58 # Output of a single test, shown when a test row is clicked. 59 self.test_output = ConsoleView() 60 self.test_output.setReadOnly(True) 61 self.test_output.setFont(mono) 62 # Compiler messages may be colored (lfortran always colors them). 63 self.console_writer = ansi.AnsiWriter(self.console) 64 self.single_writer = ansi.AnsiWriter(self.test_output) 65 # File locations in the output ("At line 12 of file src/x.f90", "src/x.f90:12:5:") open on a click. 66 self.output_links = [] 67 for view in (self.console, self.test_output): 68 links = OutputLinks(view, lambda: self.manifest_path.parent if self.manifest_path else None) 69 links.open_requested.connect(lambda path, line, column: self.navigate_to(path, line, column, 70 preview=True)) 71 self.output_links.append(links) 72 self.output_stack = QStackedWidget() 73 self.output_stack.addWidget(self.console) 74 self.output_stack.addWidget(self.test_output) 75 self._set_output_wrap(EditorTab.word_wrap) 76 console_box = QWidget() 77 cl = QVBoxLayout(console_box) 78 cl.setContentsMargins(0, 0, 0, 0) 79 # (No spacing: macOS buttons already keep some room around themselves, 80 # for the focus ring, which the layout counts as part of the button.) 81 cl.setSpacing(0) 82 # What's shown, when it's one item's output (a test, a file's compile, 83 # an AI review); hidden for all the output ("Output" is the panel's title). 84 self.output_title = QLabel() 85 self.output_title.hide() 86 # Titles like "AI review of a_very_long_file_name.f90 (model)" wrap 87 # instead of forcing the panel wider. 88 self.output_title.setWordWrap(True) 89 self.output_title.setMinimumWidth(40) 90 self.output_title.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) 91 self.output_title.setTextInteractionFlags(Qt.TextSelectableByMouse) 92 # Title alone at the top; the buttons sit below the output. 93 footer = QHBoxLayout() 94 footer.setContentsMargins(0, 0, 0, 0) 95 footer.setSpacing(6) 96 footer.addStretch(1) 97 self.show_all_button = QPushButton("Show all output") 98 self.show_all_button.clicked.connect(self.show_all_output) 99 self.show_all_button.hide() 100 footer.addWidget(self.show_all_button) 101 self.full_output_button = QPushButton("Full Output…") 102 self.full_output_button.setToolTip("The last command's output was too long to show in full; " 103 f"show the file it was saved to in {reveal_label()}") 104 self.full_output_button.clicked.connect(lambda: reveal_in_file_manager(str(self._full_output_path))) 105 self.full_output_button.hide() 106 footer.addWidget(self.full_output_button) 107 clear = QPushButton("Clear") 108 clear.setToolTip("Clear the output") 109 clear.clicked.connect(self.console.clear) 110 footer.addWidget(clear) 111 cl.addWidget(self.output_title) 112 cl.addWidget(self.output_stack, 1) 113 cl.addLayout(footer) 114 return console_box 115 116 def _append(self, text: str, bold: bool = False, colored: bool = False): 117 """Add text to the Output panel; ``colored``: program output, whose 118 terminal color codes are shown as colors (and which is limited to 119 OUTPUT_SHOW_LIMIT characters per command).""" 120 if not colored: 121 self.console_writer.write(text, bold, ansi=False) 122 return 123 if self._full_output is not None: 124 try: 125 self._full_output.write(ansi.strip(text)) 126 except (OSError, ValueError): 127 self._full_output = None 128 if not self._hidden_chars and self._shown_chars + len(text) <= OUTPUT_SHOW_LIMIT: 129 self._shown_chars += len(text) 130 if len(text) > MAX_LINE_SHOWN and max(map(len, text.split("\n"))) > MAX_LINE_SHOWN: 131 text = "\n".join(line if len(line) <= MAX_LINE_SHOWN 132 else line[:MAX_LINE_SHOWN] + f" … [{len(line) - MAX_LINE_SHOWN} more characters]" 133 for line in text.split("\n")) 134 self.console_writer.write(text, bold, ansi=True) 135 return 136 if not self._hidden_chars: 137 self.console_writer.write( 138 f"\n[The output is very long, so the rest isn't shown here (that would freeze the app). " 139 f"The last {OUTPUT_TAIL_LINES} lines are shown when the command finishes, and all of " 140 f"it is saved to {self._full_output_path} (Full Output… below).]\n", True, ansi=False) 141 self.full_output_button.show() 142 self._hidden_chars += len(text) 143 lines = (self._tail_partial + text).split("\n") 144 self._tail_partial = lines.pop() 145 self._hidden_lines += len(lines) 146 self._tail.extend(line[:MAX_LINE_SHOWN] for line in lines) 147 148 def _begin_output(self): 149 """A command is starting: reset the output limit and the saved copy.""" 150 self._end_output(show_tail=False) 151 self._decoder.reset() 152 for links in self.output_links: 153 links.forget() # (files may have come or gone) 154 self._shown_chars = self._hidden_chars = self._hidden_lines = 0 155 self._tail.clear() 156 self._tail_partial = "" 157 self.full_output_button.hide() 158 try: 159 self._full_output = open(self._full_output_path, "w", encoding="utf-8", errors="replace") # noqa: SIM115 160 except OSError: 161 self._full_output = None 162 163 def _end_output(self, show_tail: bool = True): 164 """The command has finished: show the end of any output that wasn't shown.""" 165 if self._full_output is not None: 166 try: 167 self._full_output.close() 168 except OSError: 169 pass 170 self._full_output = None 171 if show_tail and self._hidden_chars: 172 tail = list(self._tail) + ([self._tail_partial] if self._tail_partial else []) 173 skipped = self._hidden_lines + bool(self._tail_partial) - len(tail) 174 self.console_writer.write( 175 f"\n[… {skipped:,} lines ({self._hidden_chars / 1e6:.1f} MB) not shown — " 176 f"full output: {self._full_output_path} …]\n", True, ansi=False) 177 self.console_writer.write("\n".join(tail) + "\n", ansi=True) 178 self._hidden_chars = 0 179 180 def _show_marked_output(self, monitor, name: str): 181 """Show one test's or program's output (or everything when empty).""" 182 if not name: 183 self.show_all_output() 184 return 185 self.build_monitor.clear_selection() 186 other = self.test_monitor if monitor is self.run_monitor else self.run_monitor 187 if other.selected: 188 other.selected = "" 189 other.table.viewport().update() 190 self._show_single_output(name, monitor.output_for(name) or "(no output)") 191 192 def show_test_output(self, name: str): 193 """Show one test's output (or everything when name is empty).""" 194 self._show_marked_output(self.test_monitor, name) 195 196 def show_build_details(self, row: int): 197 """Show one file's compiler messages (or everything when row is -1).""" 198 if row < 0: 199 self.show_all_output() 200 return 201 for monitor in (self.test_monitor, self.run_monitor): 202 if monitor.selected: 203 monitor.selected = "" 204 monitor.table.viewport().update() 205 self._show_single_output(self.build_monitor.details_title(row), 206 self.build_monitor.details_text(row)) 207 208 def _show_single_output(self, title: str, text: str): 209 self.test_output.clear() 210 self.single_writer.reset() 211 self.single_writer.write(text) 212 self.test_output.moveCursor(QTextCursor.Start) 213 self.output_title.setText(f"<b>{_breakable(html.escape(title))}</b>") 214 self.output_title.show() 215 self.output_stack.setCurrentWidget(self.test_output) 216 self.show_all_button.show() 217 218 def show_all_output(self): 219 self.output_title.hide() 220 self.output_stack.setCurrentWidget(self.console) 221 self.show_all_button.hide() 222 self.build_monitor.clear_selection() 223 for monitor in (self.test_monitor, self.run_monitor): 224 if monitor.selected: 225 monitor.selected = "" 226 monitor.table.viewport().update()
The Output panel (a part of vibefpmgui.main_window.MainWindow).
def
show_test_output(self, name: str):
192 def show_test_output(self, name: str): 193 """Show one test's output (or everything when name is empty).""" 194 self._show_marked_output(self.test_monitor, name)
Show one test's output (or everything when name is empty).
def
show_build_details(self, row: int):
196 def show_build_details(self, row: int): 197 """Show one file's compiler messages (or everything when row is -1).""" 198 if row < 0: 199 self.show_all_output() 200 return 201 for monitor in (self.test_monitor, self.run_monitor): 202 if monitor.selected: 203 monitor.selected = "" 204 monitor.table.viewport().update() 205 self._show_single_output(self.build_monitor.details_title(row), 206 self.build_monitor.details_text(row))
Show one file's compiler messages (or everything when row is -1).
def
show_all_output(self):
218 def show_all_output(self): 219 self.output_title.hide() 220 self.output_stack.setCurrentWidget(self.console) 221 self.show_all_button.hide() 222 self.build_monitor.clear_selection() 223 for monitor in (self.test_monitor, self.run_monitor): 224 if monitor.selected: 225 monitor.selected = "" 226 monitor.table.viewport().update()