Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tui 1.0 #5731

Merged
merged 6 commits into from
Nov 27, 2023
Merged

Tui 1.0 #5731

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes.d/5731.feat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Major upgrade to `cylc tui` which now supports larger workflows and can browse installed workflows.
2 changes: 1 addition & 1 deletion cylc/flow/data_store_mgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1380,7 +1380,7 @@ def insert_job(self, name, cycle_point, status, job_conf):
name=tproxy.name,
cycle_point=tproxy.cycle_point,
execution_time_limit=job_conf.get('execution_time_limit'),
platform=job_conf.get('platform')['name'],
platform=job_conf['platform']['name'],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, spotted during testing.

The .get here wasn't protecting against anything as the default return value of None would cause the ['name'] bit to error anyway.

job_runner_name=job_conf.get('job_runner_name'),
)
# Not all fields are populated with some submit-failures,
Expand Down
1 change: 1 addition & 0 deletions cylc/flow/option_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
)

WORKFLOW_ID_ARG_DOC = ('WORKFLOW', 'Workflow ID')
OPT_WORKFLOW_ID_ARG_DOC = ('[WORKFLOW]', 'Workflow ID')
WORKFLOW_ID_MULTI_ARG_DOC = ('WORKFLOW ...', 'Workflow ID(s)')
WORKFLOW_ID_OR_PATH_ARG_DOC = ('WORKFLOW | PATH', 'Workflow ID or path')
ID_MULTI_ARG_DOC = ('ID ...', 'Workflow/Cycle/Family/Task ID(s)')
Expand Down
77 changes: 23 additions & 54 deletions cylc/flow/scripts/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,35 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""cylc tui WORKFLOW
"""cylc tui [WORKFLOW]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you give a workflow, tui will startup, pre-filtered to just that workflow. Press "E" to unset this filtering an view all workflows.


View and control running workflows in the terminal.

(Tui = Terminal User Interface)

WARNING: Tui is experimental and may break with large flows.
An upcoming change to the way Tui receives data from the scheduler will make it
much more efficient in the future.
Tui allows you to monitor and interact with workflows in a manner similar
to the GUI.

Press "h" whilst running Tui to bring up the help screen, use the arrow
keys to navigage.

"""
# TODO: remove this warning once Tui is delta-driven
# https://github.com/cylc/cylc-flow/issues/3527

from getpass import getuser
from textwrap import indent
from typing import TYPE_CHECKING
from urwid import html_fragment
from typing import TYPE_CHECKING, Optional

from cylc.flow.id import Tokens
from cylc.flow.id_cli import parse_id
from cylc.flow.option_parsers import (
WORKFLOW_ID_ARG_DOC,
OPT_WORKFLOW_ID_ARG_DOC,
CylcOptionParser as COP,
)
from cylc.flow.terminal import cli_function
from cylc.flow.tui import TUI
from cylc.flow.tui.util import suppress_logging
from cylc.flow.tui.app import (
TuiApp,
TREE_EXPAND_DEPTH
# ^ a nasty solution
)

if TYPE_CHECKING:
Expand All @@ -55,57 +56,25 @@
def get_option_parser() -> COP:
parser = COP(
__doc__,
argdoc=[WORKFLOW_ID_ARG_DOC],
argdoc=[OPT_WORKFLOW_ID_ARG_DOC],
# auto_add=False, NOTE: at present auto_add can not be turned off
color=False
)

parser.add_option(
'--display',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the HTML testing mode from the Tui script as this is now handled by an integration test fixture.

help=(
'Specify the display technology to use.'
' "raw" for interactive in-terminal display.'
' "html" for non-interactive html output.'
),
action='store',
choices=['raw', 'html'],
default='raw',
)
parser.add_option(
'--v-term-size',
help=(
'The virtual terminal size for non-interactive'
'--display options.'
),
action='store',
default='80,24'
)

return parser


@cli_function(get_option_parser)
def main(_, options: 'Values', workflow_id: str) -> None:
workflow_id, *_ = parse_id(
workflow_id,
constraint='workflows',
)
screen = None
if options.display == 'html':
TREE_EXPAND_DEPTH[0] = -1 # expand tree fully
screen = html_fragment.HtmlGenerator()
screen.set_terminal_properties(256)
screen.register_palette(TuiApp.palette)
html_fragment.screenshot_init(
[tuple(map(int, options.v_term_size.split(',')))],
[]
def main(_, options: 'Values', workflow_id: Optional[str] = None) -> None:
# get workflow ID if specified
if workflow_id:
workflow_id, *_ = parse_id(
workflow_id,
constraint='workflows',
)
tokens = Tokens(workflow_id)
workflow_id = tokens.duplicate(user=getuser()).id

try:
TuiApp(workflow_id, screen=screen).main()

if options.display == 'html':
for fragment in html_fragment.screenshot_collect():
print(fragment)
except KeyboardInterrupt:
# start Tui
with suppress_logging(), TuiApp().main(workflow_id):
pass
32 changes: 29 additions & 3 deletions cylc/flow/tui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,26 @@


class Bindings:
"""Represets key bindings for the Tui app."""

def __init__(self):
self.bindings = []
self.groups = {}

def bind(self, keys, group, desc, callback):
"""Register a key binding.

Args:
keys:
The keys to bind.
group:
The group to which this binding should belong.
desc:
Description for this binding, used to generate help.
callback:
The thing to call when this binding is pressed.

"""
if group not in self.groups:
raise ValueError(f'Group {group} not registered.')
binding = {
Expand All @@ -124,6 +138,15 @@ def bind(self, keys, group, desc, callback):
self.groups[group]['bindings'].append(binding)

def add_group(self, group, desc):
"""Add a new binding group.

Args:
group:
The name of the group.
desc:
A description of the group, used to generate help.

"""
self.groups[group] = {
'name': group,
'desc': desc,
Expand All @@ -134,6 +157,12 @@ def __iter__(self):
return iter(self.bindings)

def list_groups(self):
"""List groups and the bindings in them.

Yields:
(group_name, [binding, ...])

"""
for name, group in self.groups.items():
yield (
group,
Expand All @@ -143,6 +172,3 @@ def list_groups(self):
if binding['group'] == name
]
)


BINDINGS = Bindings()
Loading
Loading