• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4#
5# Copyright (c) 2020 Huawei Device Co., Ltd.
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19from __future__ import print_function
20from __future__ import unicode_literals
21from collections import defaultdict
22
23from prompt_toolkit.application import Application
24from prompt_toolkit.key_binding.manager import KeyBindingManager
25from prompt_toolkit.keys import Keys
26from prompt_toolkit.layout.containers import Window
27from prompt_toolkit.filters import IsDone
28from prompt_toolkit.layout.controls import TokenListControl
29from prompt_toolkit.layout.containers import ConditionalContainer
30from prompt_toolkit.layout.containers import ScrollOffsets
31from prompt_toolkit.layout.containers import HSplit
32from prompt_toolkit.layout.dimension import LayoutDimension as D
33from prompt_toolkit.token import Token
34
35from hb_internal.cts.common import get_style
36from hb_internal.cts.common import if_mousedown
37from hb_internal.cts.common import select_node
38from hb_internal.cts.common import deselect_node
39from hb_internal.cts.common import Separator
40
41
42class InquirerControl(TokenListControl):
43    def __init__(self, choices, **kwargs):
44        self.pointer_index = 0
45        self.selected_options = []  # list of names
46        self.answered = False
47        self._init_choices(choices)
48        self.deps = kwargs.pop('deps')
49        self.nodes_from = defaultdict(list)
50        super(InquirerControl, self).__init__(self._get_choice_tokens,
51                                              **kwargs)
52
53    def _init_choices(self, choices):
54        # helper to convert from question format to internal format
55        self.choices = []  # list (name, value)
56        searching_first_choice = True
57        for index, choice in enumerate(choices):
58            if isinstance(choice, Separator):
59                self.choices.append(choice)
60            else:
61                name = choice['name']
62                value = choice.get('value', name)
63                disabled = choice.get('disabled', None)
64                if 'checked' in choice and choice['checked'] and not disabled:
65                    self.selected_options.append(choice['name'])
66                self.choices.append((name, value, disabled))
67                if searching_first_choice and not disabled:
68                    self.pointer_index = index
69                    searching_first_choice = False
70
71    @property
72    def choice_count(self):
73        return len(self.choices)
74
75    def _get_choice_tokens(self, cli):
76        tokens = []
77        token = Token
78
79        def append(index, line):
80            if isinstance(line, Separator):
81                tokens.append((token.Separator, '  %s\n' % line))
82            else:
83                line_name = line[0]
84                line_value = line[1]
85                selected = (line_value in self.selected_options)
86                pointed_at = (index == self.pointer_index)
87
88                @if_mousedown
89                def select_item(cli, mouse_event):
90                    # bind option with this index to mouse event
91                    if line_value in self.selected_options:
92                        deselect_node(line_value, self.selected_options,
93                                      self.nodes_from, self.deps)
94                    else:
95                        select_node(line_value, self.selected_options,
96                                    self.nodes_from, self.deps)
97
98                if pointed_at:
99                    tokens.append((token.Pointer, '    \u276f', select_item))
100                else:
101                    tokens.append((token, '     ', select_item))
102                # 'o ' - FISHEYE
103                if choice[2]:  # disabled
104                    tokens.append((token, '- %s (%s)' %
105                                  (choice[0], choice[2])))
106                else:
107                    if selected:
108                        tokens.append((token.Selected, '\u25cf ', select_item))
109                    else:
110                        tokens.append((token, '\u25cb ', select_item))
111
112                    if pointed_at:
113                        tokens.append((Token.SetCursorPosition, ''))
114
115                    tokens.append((token, line_name, select_item))
116                tokens.append((token, '\n'))
117
118        # prepare the select choices
119        for i, choice in enumerate(self.choices):
120            append(i, choice)
121        tokens.pop()  # Remove last newline.
122        return tokens
123
124    def get_selected_values(self):
125        # get values not labels
126        return [c[1] for c in self.choices if not isinstance(c, Separator) and
127                c[1] in self.selected_options]
128
129    @property
130    def line_count(self):
131        return len(self.choices)
132
133
134def question(message, **kwargs):
135    if 'default' in kwargs:
136        raise ValueError('Checkbox does not implement \'default\' '
137                         'use \'checked\':True\' in choice!')
138
139    deps = kwargs.pop('deps')
140    choices = kwargs.pop('choices', None)
141    style = kwargs.pop('style', get_style('terminal'))
142
143    inquirer_control = kwargs.pop('inquirer_control', None)
144    if inquirer_control is None:
145        inquirer_control = InquirerControl(choices, deps=deps)
146
147    qmark = kwargs.pop('qmark', '?')
148
149    def get_prompt_tokens(cli):
150        tokens = []
151
152        tokens.append((Token.QuestionMark, qmark))
153        tokens.append((Token.Question, ' %s ' % message))
154        if inquirer_control.answered:
155            nbr_selected = len(inquirer_control.selected_options)
156            if nbr_selected == 0:
157                tokens.append((Token.Answer, ' done'))
158            elif nbr_selected == 1:
159                tokens.append((Token.Answer, ' [%s]' %
160                               inquirer_control.selected_options[0]))
161            else:
162                tokens.append((Token.Answer,
163                               ' done (%d selections)' % nbr_selected))
164        else:
165            tokens.append((Token.Instruction,
166                           ' (<up>, <down> to move, <space> to select, <a> '
167                           'to toggle, <i> to invert)'))
168        return tokens
169
170    # assemble layout
171    layout = HSplit([
172        Window(height=D.exact(1),
173               content=TokenListControl(get_prompt_tokens,
174                                        align_center=False)),
175        ConditionalContainer(
176            Window(
177                inquirer_control,
178                width=D.exact(43),
179                height=D(min=3),
180                scroll_offsets=ScrollOffsets(top=1, bottom=1)
181            ),
182            filter=~IsDone()
183        )
184    ])
185
186    # key bindings
187    manager = KeyBindingManager.for_prompt()
188
189    @manager.registry.add_binding(Keys.ControlQ, eager=True)
190    @manager.registry.add_binding(Keys.ControlC, eager=True)
191    def _(event):
192        raise KeyboardInterrupt()
193
194    @manager.registry.add_binding(' ', eager=True)
195    def toggle(event):
196        pointer_index = inquirer_control.pointer_index
197        pointed_choice = inquirer_control.choices[pointer_index][1]  # value
198        if pointed_choice in inquirer_control.selected_options:
199            deselect_node(pointed_choice, inquirer_control.selected_options,
200                          inquirer_control.nodes_from, deps)
201        else:
202            select_node(pointed_choice, inquirer_control.selected_options,
203                        inquirer_control.nodes_from, deps)
204
205    @manager.registry.add_binding('i', eager=True)
206    def invert(event):
207        inverted_selection = [c[1] for c in inquirer_control.choices if
208                              not isinstance(c, Separator) and
209                              c[1] not in inquirer_control.selected_options and
210                              not c[2]]
211        inquirer_control.selected_options = inverted_selection
212
213    @manager.registry.add_binding('a', eager=True)
214    def select_all(event):
215        all_selected = True  # all choices have been selected
216        for choice in inquirer_control.choices:
217            if not isinstance(choice, Separator) and \
218                    choice[1] not in inquirer_control.selected_options and \
219                    not choice[2]:
220                # add missing ones
221                inquirer_control.selected_options.append(choice[1])
222                all_selected = False
223        if all_selected:
224            inquirer_control.selected_options = []
225
226    @manager.registry.add_binding(Keys.Down, eager=True)
227    def move_cursor_down(event):
228        def _next():
229            inquirer_control.pointer_index = \
230                ((inquirer_control.pointer_index + 1) %
231                 inquirer_control.line_count)
232        _next()
233        while isinstance(inquirer_control.choices[
234            inquirer_control.pointer_index], Separator) or \
235                inquirer_control.choices[inquirer_control.pointer_index][2]:
236            _next()
237
238    @manager.registry.add_binding(Keys.Up, eager=True)
239    def move_cursor_up(event):
240        def _prev():
241            inquirer_control.pointer_index = \
242                ((inquirer_control.pointer_index - 1) %
243                 inquirer_control.line_count)
244        _prev()
245        while isinstance(inquirer_control.choices[
246            inquirer_control.pointer_index], Separator) or \
247                inquirer_control.choices[inquirer_control.pointer_index][2]:
248            _prev()
249
250    @manager.registry.add_binding(Keys.Enter, eager=True)
251    def set_answer(event):
252        inquirer_control.answered = True
253        event.cli.set_return_value(inquirer_control)
254
255    return Application(
256        layout=layout,
257        key_bindings_registry=manager.registry,
258        mouse_support=True,
259        style=style
260    )
261