diff --git a/cylc/flow/tui/app.py b/cylc/flow/tui/app.py index fd05a613443..eb4e248d49c 100644 --- a/cylc/flow/tui/app.py +++ b/cylc/flow/tui/app.py @@ -481,6 +481,7 @@ def create_overlay(self, widget, kwargs): See `urwid` docs for details. """ + # create the overlay kwargs = {'width': 'pack', 'height': 'pack', **kwargs} overlay = urwid.Overlay( urwid.LineBox( @@ -503,9 +504,15 @@ def create_overlay(self, widget, kwargs): top=self.stack * 5, **kwargs, ) + + # add it into the overlay stack self.loop.widget = overlay self.stack += 1 + # force urwid to render the overlay now rather than waiting until the + # event loop becomes idle + self.loop.draw_screen() + def close_topmost(self): """Remove the topmost frame or uit the app if none present.""" if self.stack <= 0: diff --git a/cylc/flow/tui/data.py b/cylc/flow/tui/data.py index 0a304bec46f..1f78d165638 100644 --- a/cylc/flow/tui/data.py +++ b/cylc/flow/tui/data.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from functools import partial from subprocess import Popen, PIPE import sys @@ -84,7 +85,6 @@ MUTATIONS = { 'workflow': [ 'pause', - 'resume', 'reload', 'stop', ], @@ -130,6 +130,46 @@ } +def cli_cmd(*cmd): + """Issue a CLI command. + + Args: + cmd: + The command without the 'cylc' prefix'. + + Rasies: + ClientError: + In the event of mishap for consistency with the network + client alternative. + + """ + proc = Popen( # nosec (command constructed internally, no untrusted input) + ['cylc', *cmd], + stderr=PIPE, + stdout=PIPE, + text=True, + ) + out, err = proc.communicate() + if proc.returncode != 0: + raise ClientError(err) + + +def _clean(workflow): + # for now we will exit tui when the workflow is cleaned + # this will change when tui supports multiple workflows + cli_cmd('clean', workflow) + sys.exit(0) + + +OFFLINE_MUTATIONS = { + 'workflow': { + 'play': partial(cli_cmd, 'play'), + 'clean': _clean, + 'reinstall-reload': partial(cli_cmd, 'vr', '--yes'), + } +} + + def generate_mutation(mutation, arguments): graphql_args = ', '.join([ f'${argument}: {ARGUMENT_TYPES[argument]}' @@ -149,10 +189,16 @@ def generate_mutation(mutation, arguments): ''' -def list_mutations(selection): +def list_mutations(client, selection): context = extract_context(selection) selection_type = list(context)[-1] - return MUTATIONS.get(selection_type, []) + ret = [] + if client: + # add the online mutations + ret.extend(MUTATIONS.get(selection_type, [])) + # add the offline mutations + ret.extend(OFFLINE_MUTATIONS.get(selection_type, [])) + return sorted(ret) def context_to_variables(context): @@ -181,6 +227,18 @@ def context_to_variables(context): def mutate(client, mutation, selection): + if mutation in OFFLINE_MUTATIONS['workflow']: + offline_mutate(mutation, selection) + elif client: + online_mutate(client, mutation, selection) + else: + raise Exception( + f'Cannot peform command {mutation} on a stopped workflow' + ' or invalid command.' + ) + + +def online_mutate(client, mutation, selection): """Issue a mutation over a network interface.""" context = extract_context(selection) variables = context_to_variables(context) @@ -199,39 +257,5 @@ def offline_mutate(mutation, selection): context = extract_context(selection) variables = context_to_variables(context) for workflow in variables['workflow']: - if mutation == 'play': - cli_cmd('play', workflow) - elif mutation == 'clean': # noqa: SIM106 - cli_cmd('clean', workflow) - # tui only supports single-workflow display ATM so - # clean should shut down the program - sys.exit() - elif mutation in list_mutations(selection): # noqa: SIM106 - # this is an "online" mutation -> ignore - pass - else: - raise Exception(f'Invalid mutation: {mutation}') - - -def cli_cmd(*cmd): - """Issue a CLI command. - - Args: - cmd: - The command without the 'cylc' prefix'. - - Rasies: - ClientError: - In the event of mishap for consistency with the network - client alternative. - - """ - proc = Popen( # nosec (command constructed internally, no untrusted input) - ['cylc', *cmd], - stderr=PIPE, - stdout=PIPE, - text=True, - ) - out, err = proc.communicate() - if proc.returncode != 0: - raise ClientError(err) + # NOTE: this currently only supports workflow mutations + OFFLINE_MUTATIONS['workflow'][mutation](workflow) diff --git a/cylc/flow/tui/overlay.py b/cylc/flow/tui/overlay.py index 539d9826cd6..02e8a479999 100644 --- a/cylc/flow/tui/overlay.py +++ b/cylc/flow/tui/overlay.py @@ -218,16 +218,14 @@ def context(app): def _mutate(mutation, _): nonlocal app + app.open_overlay(partial(progress, text='Running Command')) try: - if app.client: - mutate(app.client, mutation, selection) - else: - offline_mutate(mutation, selection) + mutate(app.client, mutation, selection) except ClientError as exc: - # app.set_header([('workflow_error', str(exc))]) app.open_overlay(partial(error, text=str(exc))) else: app.close_topmost() + app.close_topmost() widget = urwid.ListBox( urwid.SimpleFocusListWalker( @@ -245,18 +243,14 @@ def _mutate(mutation, _): mutation, on_press=partial(_mutate, mutation) ) - for mutation in ( - list_mutations(selection) - if app.client - else ['play', 'clean'] - ) + for mutation in list_mutations(app.client, selection) ] ) ) return ( widget, - {'width': 30, 'height': 15} + {'width': 30, 'height': 20} ) @@ -270,3 +264,13 @@ def error(app, text=''): ]), {'width': 50, 'height': 40} ) + + +def progress(app, text='Working'): + """An overlay for presenting a running action.""" + return ( + urwid.ListBox([ + urwid.Text(text), + ]), + {'width': 30, 'height': 10} + )