From e546c9ea85686f95ab653d0a73c85e7f374add82 Mon Sep 17 00:00:00 2001 From: Xiaojian Xu Date: Thu, 3 Dec 2020 12:28:36 +0800 Subject: [PATCH] {TerminalUI} Initial --- src/__init__.py | 0 src/azure-cli-core/__init__.py | 0 src/azure-cli-core/azure/cli/core/__init__.py | 74 +++++++++++++++++++ .../azure/cli/core/terminal/__init__.py | 4 + .../azure/cli/core/terminal/app.py | 44 +++++++++++ .../azure/cli/core/terminal/panel/__init__.py | 15 ++++ .../azure/cli/core/terminal/panel/command.py | 22 ++++++ .../azure/cli/core/terminal/panel/context.py | 16 ++++ .../azure/cli/core/terminal/panel/header.py | 58 +++++++++++++++ .../cli/core/terminal/panel/navigator.py | 17 +++++ .../azure/cli/core/terminal/panel/output.py | 17 +++++ 11 files changed, 267 insertions(+) create mode 100644 src/__init__.py create mode 100644 src/azure-cli-core/__init__.py create mode 100644 src/azure-cli-core/azure/cli/core/terminal/__init__.py create mode 100644 src/azure-cli-core/azure/cli/core/terminal/app.py create mode 100644 src/azure-cli-core/azure/cli/core/terminal/panel/__init__.py create mode 100644 src/azure-cli-core/azure/cli/core/terminal/panel/command.py create mode 100644 src/azure-cli-core/azure/cli/core/terminal/panel/context.py create mode 100644 src/azure-cli-core/azure/cli/core/terminal/panel/header.py create mode 100644 src/azure-cli-core/azure/cli/core/terminal/panel/navigator.py create mode 100644 src/azure-cli-core/azure/cli/core/terminal/panel/output.py diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/azure-cli-core/__init__.py b/src/azure-cli-core/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/azure-cli-core/azure/cli/core/__init__.py b/src/azure-cli-core/azure/cli/core/__init__.py index 11e406eaf30..3c0c5133850 100644 --- a/src/azure-cli-core/azure/cli/core/__init__.py +++ b/src/azure-cli-core/azure/cli/core/__init__.py @@ -159,6 +159,80 @@ def save_local_context(self, parsed_args, argument_definitions, specified_argume logger.warning('Your preference of %s now saved to local context. To learn more, type in `az ' 'local-context --help`', ', '.join(args_str) + ' is' if len(args_str) == 1 else ' are') + def invoke(self, args, initial_invocation_data=None, out_file=None): + """ Invoke a command. + + :param args: The arguments that represent the command + :type args: list, tuple + :param initial_invocation_data: Prime the in memory collection of key-value data for this invocation. + :type initial_invocation_data: dict + :param out_file: The file to send output to. If not used, we use out_file for knack.cli.CLI instance + :type out_file: file-like object + :return: The exit code of the invocation + :rtype: int + """ + from knack.util import CommandResultItem + from knack.events import EVENT_CLI_PRE_EXECUTE, EVENT_CLI_POST_EXECUTE + + if not isinstance(args, (list, tuple)): + raise TypeError('args should be a list or tuple.') + exit_code = 0 + try: + if self.enable_color: + import colorama + colorama.init() + if self.out_file == sys.__stdout__: + # point out_file to the new sys.stdout which is overwritten by colorama + self.out_file = sys.stdout + + args = self.completion.get_completion_args() or args + out_file = out_file or self.out_file + + self.logging.configure(args) + logger.debug('Command arguments: %s', args) + + self.raise_event(EVENT_CLI_PRE_EXECUTE) + if CLI._should_show_version(args): + self.show_version() + self.result = CommandResultItem(None) + elif args and (args[0] == '--terminal' or args[0] == '-t'): + self.start_terminal() + else: + self.invocation = self.invocation_cls(cli_ctx=self, + parser_cls=self.parser_cls, + commands_loader_cls=self.commands_loader_cls, + help_cls=self.help_cls, + initial_data=initial_invocation_data) + cmd_result = self.invocation.execute(args) + self.result = cmd_result + exit_code = self.result.exit_code + output_type = self.invocation.data['output'] + if cmd_result and cmd_result.result is not None: + formatter = self.output.get_formatter(output_type) + self.output.out(cmd_result, formatter=formatter, out_file=out_file) + except KeyboardInterrupt as ex: + exit_code = 1 + self.result = CommandResultItem(None, error=ex, exit_code=exit_code) + except Exception as ex: # pylint: disable=broad-except + exit_code = self.exception_handler(ex) + self.result = CommandResultItem(None, error=ex, exit_code=exit_code) + except SystemExit as ex: + exit_code = ex.code + self.result = CommandResultItem(None, error=ex, exit_code=exit_code) + raise ex + finally: + self.raise_event(EVENT_CLI_POST_EXECUTE) + + if self.enable_color: + colorama.deinit() + + return exit_code + + def start_terminal(self): + from azure.cli.core.terminal.app import TerminalApplication + app = TerminalApplication() + app.run() + class MainCommandsLoader(CLICommandsLoader): diff --git a/src/azure-cli-core/azure/cli/core/terminal/__init__.py b/src/azure-cli-core/azure/cli/core/terminal/__init__.py new file mode 100644 index 00000000000..34913fb394d --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/terminal/__init__.py @@ -0,0 +1,4 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/src/azure-cli-core/azure/cli/core/terminal/app.py b/src/azure-cli-core/azure/cli/core/terminal/app.py new file mode 100644 index 00000000000..a0af5657535 --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/terminal/app.py @@ -0,0 +1,44 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from prompt_toolkit import Application +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout.containers import HSplit, VSplit +from prompt_toolkit.layout.layout import Layout +from azure.cli.core.terminal.panel import HeaderPanel, CommandPanel, NavigatorPanel, OutputPanel, ContextPanel +from prompt_toolkit.layout.dimension import D + + +kb = KeyBindings() + + +@kb.add('c-q') +def _(event): + event.app.exit() + + +@kb.add('c-r') +def _(event): + event.app.toggle_recording() + + +class TerminalApplication(Application): + + def __init__(self): + self.recording = False + super(TerminalApplication, self).__init__( + layout=Layout(HSplit([ + HeaderPanel('2.15.0', 'xiaojxu@microsoft.com', 'AzureSDKTeam'), + CommandPanel(), + VSplit([NavigatorPanel(), OutputPanel()], height=D()), + ContextPanel() + ], padding=0)), + full_screen=True, + mouse_support=True, + key_bindings=kb + ) + + def toggle_recording(self): + self.recording = not self.recording diff --git a/src/azure-cli-core/azure/cli/core/terminal/panel/__init__.py b/src/azure-cli-core/azure/cli/core/terminal/panel/__init__.py new file mode 100644 index 00000000000..017135d83a6 --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/terminal/panel/__init__.py @@ -0,0 +1,15 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from .command import CommandPanel +from .context import ContextPanel +from .header import HeaderPanel +from .navigator import NavigatorPanel +from .output import OutputPanel + +__all__ = [ + 'CommandPanel', 'ContextPanel', 'HeaderPanel', 'NavigatorPanel', 'OutputPanel' +] diff --git a/src/azure-cli-core/azure/cli/core/terminal/panel/command.py b/src/azure-cli-core/azure/cli/core/terminal/panel/command.py new file mode 100644 index 00000000000..9f81d8a5935 --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/terminal/panel/command.py @@ -0,0 +1,22 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from prompt_toolkit.widgets.base import Frame, TextArea +from prompt_toolkit.formatted_text import HTML + + +def get_line_prefix(lineno, wrap_count): + return HTML(' ') + + +class CommandPanel(object): + def __init__(self): + self.container = Frame( + body=TextArea(text='az ', get_line_prefix=get_line_prefix), + height=3 + ) + + def __pt_container__(self): + return self.container diff --git a/src/azure-cli-core/azure/cli/core/terminal/panel/context.py b/src/azure-cli-core/azure/cli/core/terminal/panel/context.py new file mode 100644 index 00000000000..f30db81fcdb --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/terminal/panel/context.py @@ -0,0 +1,16 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from prompt_toolkit.widgets.base import Frame, TextArea + + +class ContextPanel(object): + def __init__(self): + self.container = Frame( + body=TextArea(text='context'), + height=6 + ) + + def __pt_container__(self): + return self.container diff --git a/src/azure-cli-core/azure/cli/core/terminal/panel/header.py b/src/azure-cli-core/azure/cli/core/terminal/panel/header.py new file mode 100644 index 00000000000..88d3741184b --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/terminal/panel/header.py @@ -0,0 +1,58 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from prompt_toolkit.widgets.base import Label, Box +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.layout.containers import HSplit, VSplit + + +class MetaItem(object): + def __init__(self, label, value): + self.container = VSplit([ + Box(body=Label(text=label), width=15), + Box(body=Label(text=value), width=35) + ], height=2) + + def __pt_container__(self): + return self.container + + +class MetaInfo(object): + def __init__(self, cli_version, current_user, subscription): + self.container = HSplit([ + MetaItem('CLI Version:', cli_version), + MetaItem('User:', current_user), + MetaItem('Subscription:', subscription) + ], width=50) + + def __pt_container__(self): + return self.container + + +class ShortcutAction(object): + def __init__(self, shortcuts): + self.container = Label(text='', width=D()) + + def __pt_container__(self): + return self.container + + +class RecordingMode(object): + def __init__(self, recording): + self.container = Label(text='R', width=1) + + def __pt_container__(self): + return self.container + + +class HeaderPanel(object): + def __init__(self, cli_version, current_user, subscription, shortcuts=None, recording=False): + self.container = VSplit([ + MetaInfo(cli_version, current_user, subscription), + ShortcutAction(shortcuts), + RecordingMode(recording) + ], height=6) + + def __pt_container__(self): + return self.container diff --git a/src/azure-cli-core/azure/cli/core/terminal/panel/navigator.py b/src/azure-cli-core/azure/cli/core/terminal/panel/navigator.py new file mode 100644 index 00000000000..db722194943 --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/terminal/panel/navigator.py @@ -0,0 +1,17 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from prompt_toolkit.widgets.base import Frame, TextArea + + +class NavigatorPanel(object): + def __init__(self): + self.container = Frame( + body=TextArea(text='navigator'), + width=50 + ) + + def __pt_container__(self): + return self.container diff --git a/src/azure-cli-core/azure/cli/core/terminal/panel/output.py b/src/azure-cli-core/azure/cli/core/terminal/panel/output.py new file mode 100644 index 00000000000..ad8db8e4ecf --- /dev/null +++ b/src/azure-cli-core/azure/cli/core/terminal/panel/output.py @@ -0,0 +1,17 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from prompt_toolkit.widgets.base import Frame, TextArea +from prompt_toolkit.layout.dimension import D + + +class OutputPanel(object): + def __init__(self): + self.container = Frame( + body=TextArea(text='output'), + width=D() + ) + + def __pt_container__(self): + return self.container