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