• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""LogPane Save As Dialog."""
15
16from __future__ import annotations
17import functools
18from pathlib import Path
19from typing import Optional, TYPE_CHECKING
20
21from prompt_toolkit.buffer import Buffer
22from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
23from prompt_toolkit.completion import PathCompleter
24from prompt_toolkit.filters import Condition
25from prompt_toolkit.history import InMemoryHistory
26from prompt_toolkit.layout import (
27    ConditionalContainer,
28    FormattedTextControl,
29    HSplit,
30    Window,
31    WindowAlign,
32)
33from prompt_toolkit.widgets import TextArea
34from prompt_toolkit.validation import (
35    ValidationError,
36    Validator,
37)
38
39import pw_console.widgets.checkbox
40import pw_console.widgets.border
41import pw_console.widgets.mouse_handlers
42import pw_console.style
43
44if TYPE_CHECKING:
45    from pw_console.log_pane import LogPane
46
47
48class PathValidator(Validator):
49    """Validation of file path input."""
50    def validate(self, document):
51        """Check input path leads to a valid parent directory."""
52        target_path = Path(document.text).expanduser()
53
54        if not target_path.parent.exists():
55            raise ValidationError(
56                # Set cursor position to the end
57                len(document.text),
58                "Directory doesn't exist: %s" % document.text)
59
60        if target_path.is_dir():
61            raise ValidationError(
62                # Set cursor position to the end
63                len(document.text),
64                "File input is an existing directory: %s" % document.text)
65
66
67class LogPaneSaveAsDialog(ConditionalContainer):
68    """Dialog box for saving logs to a file."""
69    # Height of the dialog box contens in lines of text.
70    DIALOG_HEIGHT = 3
71
72    def __init__(self, log_pane: 'LogPane'):
73        self.log_pane = log_pane
74
75        self.path_validator = PathValidator()
76
77        self._export_with_table_formatting: bool = True
78        self._export_with_selected_lines_only: bool = False
79
80        self.starting_file_path: str = str(Path.cwd())
81
82        self.input_field = TextArea(
83            prompt=[
84                ('class:saveas-dialog-setting', 'File: ',
85                 functools.partial(pw_console.widgets.mouse_handlers.on_click,
86                                   self.focus_self))
87            ],
88            # Pre-fill the current working directory.
89            text=self.starting_file_path,
90            focusable=True,
91            focus_on_click=True,
92            scrollbar=False,
93            multiline=False,
94            height=1,
95            dont_extend_height=True,
96            dont_extend_width=False,
97            accept_handler=self._saveas_accept_handler,
98            validator=self.path_validator,
99            history=InMemoryHistory(),
100            completer=PathCompleter(expanduser=True),
101        )
102
103        self.input_field.buffer.cursor_position = len(self.starting_file_path)
104
105        settings_bar_control = FormattedTextControl(
106            self.get_settings_fragments)
107        settings_bar_window = Window(content=settings_bar_control,
108                                     height=1,
109                                     align=WindowAlign.LEFT,
110                                     dont_extend_width=False)
111
112        action_bar_control = FormattedTextControl(self.get_action_fragments)
113        action_bar_window = Window(content=action_bar_control,
114                                   height=1,
115                                   align=WindowAlign.RIGHT,
116                                   dont_extend_width=False)
117
118        # Add additional keybindings for the input_field text area.
119        key_bindings = KeyBindings()
120        register = self.log_pane.application.prefs.register_keybinding
121
122        @register('save-as-dialog.cancel', key_bindings)
123        def _close_saveas_dialog(_event: KeyPressEvent) -> None:
124            """Close save as dialog."""
125            self.close_dialog()
126
127        self.input_field.control.key_bindings = key_bindings
128
129        super().__init__(
130            pw_console.widgets.border.create_border(
131                HSplit(
132                    [
133                        settings_bar_window,
134                        self.input_field,
135                        action_bar_window,
136                    ],
137                    height=LogPaneSaveAsDialog.DIALOG_HEIGHT,
138                    style='class:saveas-dialog',
139                ),
140                LogPaneSaveAsDialog.DIALOG_HEIGHT,
141                border_style='class:saveas-dialog-border',
142                left_margin_columns=1,
143            ),
144            filter=Condition(lambda: self.log_pane.saveas_dialog_active),
145        )
146
147    def focus_self(self):
148        self.log_pane.application.application.layout.focus(self)
149
150    def close_dialog(self):
151        """Close this dialog."""
152        self.log_pane.saveas_dialog_active = False
153        self.log_pane.application.focus_on_container(self.log_pane)
154        self.log_pane.redraw_ui()
155
156    def _toggle_table_formatting(self):
157        self._export_with_table_formatting = (
158            not self._export_with_table_formatting)
159
160    def _toggle_selected_lines(self):
161        self._export_with_selected_lines_only = (
162            not self._export_with_selected_lines_only)
163
164    def set_export_options(self,
165                           table_format: Optional[bool] = None,
166                           selected_lines_only: Optional[bool] = None) -> None:
167        # Allows external callers such as the line selection dialog to set
168        # export format options.
169        if table_format is not None:
170            self._export_with_table_formatting = table_format
171
172        if selected_lines_only is not None:
173            self._export_with_selected_lines_only = selected_lines_only
174
175    def save_action(self):
176        """Trigger save file execution on mouse click.
177
178        This ultimately runs LogPaneSaveAsDialog._saveas_accept_handler()."""
179        self.input_field.buffer.validate_and_handle()
180
181    def _saveas_accept_handler(self, buff: Buffer) -> bool:
182        """Function run when hitting Enter in the input_field."""
183        input_text = buff.text
184        if len(input_text) == 0:
185            self.close_dialog()
186            # Don't save anything if empty input.
187            return False
188
189        if self.log_pane.log_view.export_logs(
190                file_name=input_text,
191                use_table_formatting=self._export_with_table_formatting,
192                selected_lines_only=self._export_with_selected_lines_only):
193            self.close_dialog()
194            # Reset selected_lines_only
195            self.set_export_options(selected_lines_only=False)
196            # Erase existing input text.
197            return False
198
199        # Keep existing text if error
200        return True
201
202    def get_settings_fragments(self):
203        """Return FormattedText with current save settings."""
204        # Mouse handlers
205        focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
206                                  self.focus_self)
207        toggle_table_formatting = functools.partial(
208            pw_console.widgets.mouse_handlers.on_click,
209            self._toggle_table_formatting)
210        toggle_selected_lines = functools.partial(
211            pw_console.widgets.mouse_handlers.on_click,
212            self._toggle_selected_lines)
213
214        # Separator should have the focus mouse handler so clicking on any
215        # whitespace focuses the input field.
216        separator_text = ('', '  ', focus)
217
218        # Default button style
219        button_style = 'class:toolbar-button-inactive'
220
221        fragments = [('class:saveas-dialog-title', 'Save as File', focus)]
222        fragments.append(separator_text)
223
224        # Table checkbox
225        fragments.extend(
226            pw_console.widgets.checkbox.to_checkbox_with_keybind_indicator(
227                checked=self._export_with_table_formatting,
228                key='',  # No key shortcut help text
229                description='Table Formatting',
230                mouse_handler=toggle_table_formatting,
231                base_style=button_style))
232
233        # Two space separator
234        fragments.append(separator_text)
235
236        # Selected lines checkbox
237        fragments.extend(
238            pw_console.widgets.checkbox.to_checkbox_with_keybind_indicator(
239                checked=self._export_with_selected_lines_only,
240                key='',  # No key shortcut help text
241                description='Selected Lines Only',
242                mouse_handler=toggle_selected_lines,
243                base_style=button_style))
244
245        # Two space separator
246        fragments.append(separator_text)
247
248        return fragments
249
250    def get_action_fragments(self):
251        """Return FormattedText with the save action buttons."""
252        # Mouse handlers
253        focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
254                                  self.focus_self)
255        cancel = functools.partial(pw_console.widgets.mouse_handlers.on_click,
256                                   self.close_dialog)
257        save = functools.partial(pw_console.widgets.mouse_handlers.on_click,
258                                 self.save_action)
259
260        # Separator should have the focus mouse handler so clicking on any
261        # whitespace focuses the input field.
262        separator_text = ('', '  ', focus)
263
264        # Default button style
265        button_style = 'class:toolbar-button-inactive'
266
267        fragments = [separator_text]
268        # Cancel button
269        fragments.extend(
270            pw_console.widgets.checkbox.to_keybind_indicator(
271                key='Ctrl-c',
272                description='Cancel',
273                mouse_handler=cancel,
274                base_style=button_style,
275            ))
276
277        # Two space separator
278        fragments.append(separator_text)
279
280        # Save button
281        fragments.extend(
282            pw_console.widgets.checkbox.to_keybind_indicator(
283                key='Enter',
284                description='Save',
285                mouse_handler=save,
286                base_style=button_style,
287            ))
288
289        # One space separator
290        fragments.append(('', ' ', focus))
291
292        return fragments
293