How to Write a Site Specific Formatter#

This guide explains how you can create a pvi formatter to generate screens for your own use cases.

Overview#

The formatters role is to take a device.yaml file and turn this into a screen file that can be used by the display software. Inside of the device.yaml file is a list of components that specify its name, a widget type and any additional properties that can be assigned to that widget (such as a pv name). During formatting, the device.yaml file is deserialised into component objects, which are later translated into widgets:

class Component(Named):
    """These make up a Device"""

    label: Annotated[str | None, Field(description="Label for component")] = None
    description: Annotated[
        str | None, Field(description="Description for label tooltip")
    ] = None

    def get_label(self):
        return self.label or to_title_case(self.name)
class SignalR(Signal):
    """Read-only `Signal` backed by a single PV."""

    _access_mode = "r"

    read_pv: str = Field(description="PV to use for readback")
    read_widget: ReadWidgetUnion = Field(
        None,
        description="Widget to use for display. `TextRead` will be used if unset.",
        validate_default=True,
    )

    @field_validator("read_widget", mode="before")
    @classmethod
    def _validate_read_widget(cls, read_widget: Any):
        if read_widget is None:
            return TextRead()

        return read_widget

To make a screen from this, we need a template file. This contains a blank representation of each supported widget for each of the supported file formats (bob, edl etc…). Below is an example of a ‘text entry’ widget for a .bob file:

    <pv_name>SignalX</pv_name>
    <actions>
      <action type="write_pv">
        <pv_name>$(pv_name)</pv_name>
        <value>value</value>
        <description>$(name)</description>
      </action>
    </actions>
    <text>SignalX</text>
    <x>18</x>
    <y>118</y>
    <width>120</width>
    <height>40</height>
    <tooltip>$(tooltip)</tooltip>
  </widget>
  <widget type="textupdate" version="2.0.0">
    <name>TextUpdate</name>

By extracting and altering the template widgets with the information provided by the components, we can create a screen file.

Create a formatter subclass#

To start, we will need to create our own formatter class. These inherit from an abstract ‘Formatter’ class that is defined in base.py. Inside, we need to define one mandatory ‘format’ function, which will be used to create our screen file:

class Formatter(TypedModel, YamlValidatorMixin):
    """Base UI formatter."""

    @classmethod
    def type_adapter(cls) -> TypeAdapter:
        """Create TypeAdapter of all child classes"""
        return TypeAdapter(
            as_tagged_union(Union[tuple(cls.__subclasses__())])  # type: ignore
        )

    @classmethod
    def from_dict(cls, serialized: dict) -> "Formatter":
        """Instantiate a Formatter child class from a dictionary.

        Args:
            serialized: Dictionary of class instance

        """
        return cls.type_adapter().validate_python(serialized)

    @classmethod
    def deserialize(cls, yaml: Path) -> "Formatter":
        """Instantiate a Formatter child class from YAML.

        Args:
            yaml: Path of YAML file

        """
        serialized = cls.validate_yaml(yaml)
        return cls.from_dict(serialized)

    @classmethod
    def create_schema(cls) -> dict[str, Any]:
        """Create a schema of Formatter child classes.

        Formatter itself is not included, as it should not be instanstiated directly.

        """
        cls._rebuild_child_models()
        return cls.type_adapter().json_schema()

    def format(self, device: Device, path: Path):
        """To be implemented by child classes to define how to format specific UIs.

        Args:
            device: Device to populate UI from
            path: Output file path to write UI to

        """
        raise NotImplementedError(self)

    def format_index(self, label: str, index_entries: list[IndexEntry], path: Path):
        """Format an index of buttons to open the given UIs.

        Args:
            label: Title of generated index UI
            index_entries: Buttons to format on the index UI
            path: Output path of generated UI

        """
        self.format(
            Device(
                label=label,
                children=[
                    DeviceRef(
                        name=enforce_pascal_case(index.label),
                        label=index.label,
                        pv=index.label.upper(),
                        ui=index.ui,
                        macros=index.macros,
                    )
                    for index in index_entries
                ],
            ),
            path=path,
        )

The format function takes in a device: a list of components obtained from our deserialised device.yaml file, A prefix: the pv prefix of the device, and a path: the output destination for the generated screen file.

With a formatter defined, we now can start to populate this by defining the screen dependencies.

Define the Screen Layout Properties#

Each screen requires a number of layout properties that allow you to customise the size and placement of widgets. These are stored within a ‘ScrenLayout’ dataclass that can be imported from utils.py. Within the dataclass are the following configurable parameters:

class ScreenLayout(BaseModel):
    spacing: int = Field(description="Spacing between widgets")
    title_height: int = Field(description="Height of screen title bar")
    max_height: int = Field(description="Max height of the screen")
    group_label_height: int = Field(description="Height of the group title label")
    label_width: int = Field(description="Width of the labels describing widgets")
    widget_width: int = Field(description="Width of the widgets")
    widget_height: int = Field(
        description="Height of the widgets (Labels use this too)"
    )
    group_widget_indent: int = Field(
        0, description="Indentation of widgets within groups. Defaults to 0"
    )
    group_width_offset: int = Field(
        0, description="Additional border width when using group objects. Defaults to 0"
    )

When defining these in our formatter, we have the option of deciding which properties should be configurable inside of the formatter.yaml. Properties defined as member variables of the formatter class (and then referenced by the layout properties in the screen format function) will be available to adjust inside of the formatter.yaml. Anything else, should be considered as defaults for the formatter:

        screen_layout = ScreenLayout(
            spacing=self.spacing,
            title_height=self.title_height,
            max_height=self.max_height,
            group_label_height=26,
            label_width=self.label_width,
            widget_width=self.widget_width,
            widget_height=self.widget_height,
            group_widget_indent=18,
            group_width_offset=26,
        )

In the example above, everything has been made adjustable from the formatter.yaml except the properties relating to groups. This is becuase they are more dependant on the file format used rather than the users personal preference.

For clarity, the example below shows how the formatter.yaml can be used to set the layout properties. Note that these are optional as each property is defined with a default value:

# yaml-language-server: $schema=../schemas/pvi.formatter.schema.json
type: DLSFormatter
# Remove screen property to use default value
spacing: 4
title_height: 26
max_height: 900
label_width: 120
widget_width: 120
widget_height: 20

Assign a Template File#

As previously stated, a template file provides the formatter with a base model of all of the supported widgets that it can then overwrite with component data. Currently, pvi supports templates for edl, adl and bob files, which can be referenced from the _format directory with the filename ‘dls’ + the file formats suffix (eg. dls.bob).

Inside of the format function, we need to provide a reference to the template file that can then be used to identify what each widget should look like:

template = BobTemplate(str(Path(__file__).parent / "dls.bob"))

Divide the Template into Widgets#

With a template defined, we now need to assign each part of it to a supported widget. This is achieved using the ScreenWidgets dataclass (from utils.py). With this, we can assign each of the widget classes to a snippet of the template using the WidgetFactory.from_template method:

        widget_formatter_factory = WidgetFormatterFactory(
            header_formatter_cls=LabelWidgetFormatter.from_template(
                template,
                search="Heading",
                property_map={"text": "text"},
            ),
            label_formatter_cls=LabelWidgetFormatter.from_template(
                template,
                search="Label",
                property_map={"text": "text", "tooltip": "description"},
            ),
            led_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="LED",
                sized=Bounds.square,
                property_map={"pv_name": "pv"},
            ),
            progress_bar_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ProgressBar",
                property_map={"pv_name": "pv"},
            ),
            bitfield_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="BitField",
                property_map={"pv_name": "pv"},
            ),
            button_panel_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ButtonPanel",
                property_map={"pv_name": "pv"},
            ),
            array_trace_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ArrayTrace",
                property_map={"y_pv": "pv"},
            ),
            image_read_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ImageRead",
                property_map={"pv_name": "pv"},
            ),
            text_read_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="TextUpdate",
                property_map={"pv_name": "pv"},
            ),
            check_box_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ChoiceButton",
                property_map={"pv_name": "pv"},
            ),
            toggle_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ToggleButton",
                property_map={"pv_name": "pv"},
            ),
            combo_box_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ComboBox",
                property_map={"pv_name": "pv"},
            ),
            text_write_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="TextEntry",
                property_map={"pv_name": "pv"},
            ),
            table_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="Table",
                property_map={"pv_name": "pv"},
            ),
            action_formatter_cls=ActionWidgetFormatter.from_template(
                template,
                search="WritePV",
                property_map={
                    "text": "label",
                    "pv_name": "pv",
                    "value": "value",
                    "tooltip": "tooltip",
                },
            ),
            sub_screen_formatter_cls=SubScreenWidgetFormatter.from_template(
                template,
                search="OpenDisplay",
                property_map={"file": "file_name", "text": "label", "macros": "macros"},
            ),
        )

This function uses a unique search term to locate and extract a widget from the template. As such, the search term MUST be unique to avoid extracing multiple or irrelevant widgets from the template.

Define screen and group widget functions#

Two widgets that are not handled by ScreenWidgets are the screen title and group object. This is because the style of these widgets differ greatly for each file type. For instance, with edl and adl files, groups are represented by a rectangle and title placed behind a collection of widgets. Conversely, bob files handle groups using its dedicated group object, which places widgets as children under the group object. Becuase of this, we need to define two functions: one for the additional screen widgets (such as the title), and one to represent the group widgets.

We then need to define two functions that can be used to create multiple instances of these widgets. In this example, we provide two arguments: The ‘bounds’, to set the widgets size and position, and the ‘title’ to populate the label with.

        screen_title_cls = LabelWidgetFormatter.from_template(
            template,
            search="Title",
            property_map={"text": "text"},
        )
        group_title_cls = LabelWidgetFormatter.from_template(
            template,
            search="Group",
            property_map={"name": "text"},
        )

        def create_group_object_formatter(
            bounds: Bounds, title: str
        ) -> List[WidgetFormatter[str]]:
            return [
                group_title_cls(
                    bounds=Bounds(x=bounds.x, y=bounds.y, w=bounds.w, h=bounds.h),
                    text=f"{title}",
                )
            ]

        def create_screen_title_formatter(
            bounds: Bounds, title: str
        ) -> List[WidgetFormatter[str]]:
            return [
                screen_title_cls(
                    bounds=Bounds(x=0, y=0, w=bounds.w, h=screen_layout.title_height),
                    text=title,
                )
            ]

Construct a Screen Object#

Provided that you have defined the LayoutProperties, template, ScreenWidgets and the screen title and group object functions, we are now ready to define a screen object.

        formatter_factory: ScreenFormatterFactory = ScreenFormatterFactory(
            screen_formatter_cls=GroupFormatter.from_template(
                template,
                search=GroupType.SCREEN,
                sized=with_title(screen_layout.spacing, screen_layout.title_height),
                widget_formatter_hook=create_screen_title_formatter,
            ),
            group_formatter_cls=GroupFormatter.from_template(
                template,
                search=GroupType.GROUP,
                sized=with_title(
                    screen_layout.spacing, screen_layout.group_label_height
                ),
                widget_formatter_hook=create_group_object_formatter,
            ),
            widget_formatter_factory=widget_formatter_factory,
            layout=screen_layout,
            base_file_name=path.stem,
        )

Note that screen_cls and group_cls are defined separately here as GroupFactories. This is because they take in the make_widgets function, which has the possibility of returning multiple widgets. (In edl files for example, we return a rectangle and label widget to represent a group.)

The screen object itself contains two key functions: The ‘screen’ function takes a deserialised device.yaml file and converts each of its components into widgets. It then calculates the size and position of these widgets to generate a uniform screen layout. On the output of this, we can call a (screen.)format function that populates these widgets with the extracted properties from the device.yaml, and converts them into the chosen file format:

        title = f"{device.label}"

        screen_formatter, sub_screens = formatter_factory.create_screen_formatter(
            device.children, title
        )

        write_bob(screen_formatter, path)
        for sub_screen_name, sub_screen_formatter in sub_screens:
            sub_screen_path = Path(path.parent / f"{sub_screen_name}{path.suffix}")
            write_bob(sub_screen_formatter, sub_screen_path)


def write_bob(screen_formatter: GroupFormatter, path: Path):

Generate the Screen file#

After calling format on the screen object, you will be left with a list of strings that represent each widget in your chosen file format. The final step is to create a screen file by unpacking the list and writing each widget to the file:

    # The root:'Display' is always the first element in texts
    texts = screen_formatter.format()
    ET = etree.fromstring(etree.tostring(texts[0]))
    for element in texts[:0:-1]:
        ET.insert(ET.index(ET.find("grid_step_y")) + 1, element)
    ET = ET.getroottree()
    find_element(ET, "name").text = screen_formatter.title
    ET.write(str(path), pretty_print=True)

And thats it. With this you can now create your own custom formatters. Below you can find a complete example formatter, supporting both edl and bob file formats for DLS:

class DLSFormatter(Formatter):
    spacing: int = Field(5, description="Spacing between widgets")
    title_height: int = Field(25, description="Height of screen title bar")
    max_height: int = Field(900, description="Max height of the screen")
    label_width: int = Field(150, description="Width of the widget description labels")
    widget_width: int = Field(200, description="Width of the widgets")
    widget_height: int = Field(20, description="Height of the widgets")

    def format(self, device: Device, path: Path):
        if path.suffix == ".edl":
            f = self.format_edl
        elif path.suffix == ".bob":
            f = self.format_bob
        else:
            raise ValueError("Can only write .edl or .bob files")
        f(device, path)

    def format_edl(self, device: Device, path: Path):
        template = EdlTemplate((Path(__file__).parent / "dls.edl").read_text())
        screen_layout = ScreenLayout(
            spacing=self.spacing,
            title_height=self.title_height,
            max_height=self.max_height,
            group_label_height=10,
            label_width=self.label_width,
            widget_width=self.widget_width,
            widget_height=self.widget_height,
            group_widget_indent=5,
            group_width_offset=0,
        )
        widget_formatter_factory = WidgetFormatterFactory(
            header_formatter_cls=LabelWidgetFormatter.from_template(
                template,
                search='"Heading"',
                property_map={"value": "text"},
            ),
            label_formatter_cls=LabelWidgetFormatter.from_template(
                template,
                search='"Label"',
                property_map={"value": "text"},
            ),
            led_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"LED"',
                sized=Bounds.square,
                property_map={"controlPv": "pv"},
            ),
            progress_bar_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"ProgressBar"',
                property_map={"indicatorPv": "pv"},
            ),
            text_read_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"TextRead"',
                property_map={"controlPv": "pv"},
            ),
            check_box_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"ComboBox"',
                property_map={"controlPv": "pv"},
            ),
            toggle_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"ToggleButton"',
                property_map={"controlPv": "pv"},
            ),
            combo_box_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"ComboBox"',
                property_map={"controlPv": "pv"},
            ),
            text_write_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"TextWrite"',
                property_map={"controlPv": "pv"},
            ),
            # Cannot handle dynamic tables so insert a label with the PV name
            table_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"Label"',
                property_map={"value": "pv"},
            ),
            action_formatter_cls=ActionWidgetFormatter.from_template(
                template,
                search='"SignalX"',
                property_map={
                    "onLabel": "label",
                    "offLabel": "label",
                    "controlPv": "pv",
                },
            ),
            sub_screen_formatter_cls=SubScreenWidgetFormatter.from_template(
                template,
                search='"SubScreenFile"',
                property_map={"displayFileName": "file_name"},
            ),
            bitfield_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"LED"',
                property_map={"controlPv": "pv"},
            ),
            button_panel_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"Label"',
                property_map={"value": "pv"},
            ),
            array_trace_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"Label"',
                property_map={"value": "pv"},
            ),
            image_read_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search='"Label"',
                property_map={"value": "pv"},
            ),
        )
        screen_title_cls = LabelWidgetFormatter.from_template(
            template,
            search='"Title"',
            property_map={"value": "text"},
        )
        group_title_cls = LabelWidgetFormatter.from_template(
            template,
            search='"  Group  "',
            property_map={"value": "text"},
        )
        group_box_cls = WidgetFormatter.from_template(
            template, search="fillColor index 5"
        )

        def create_group_box_formatter(
            bounds: Bounds, title: str
        ) -> List[WidgetFormatter[str]]:
            x, y, w, h = bounds.x, bounds.y, bounds.w, bounds.h
            return [
                group_box_cls(
                    bounds=Bounds(
                        x=x,
                        y=y + screen_layout.spacing,
                        w=w,
                        h=h - screen_layout.spacing,
                    )
                ),
                group_title_cls(
                    bounds=Bounds(x=x, y=y, w=w, h=screen_layout.group_label_height),
                    text=f"  {title}  ",
                ),
            ]

        def create_screen_title_formatter(
            bounds: Bounds, title: str
        ) -> List[WidgetFormatter[str]]:
            return [
                screen_title_cls(
                    bounds=Bounds(x=0, y=0, w=bounds.w, h=screen_layout.title_height),
                    text=title,
                )
            ]

        formatter_factory: ScreenFormatterFactory = ScreenFormatterFactory(
            screen_formatter_cls=GroupFormatter.from_template(
                template,
                search=GroupType.SCREEN,
                sized=with_title(screen_layout.spacing, screen_layout.title_height),
                widget_formatter_hook=create_screen_title_formatter,
            ),
            group_formatter_cls=GroupFormatter.from_template(
                template,
                search=GroupType.GROUP,
                sized=with_title(
                    screen_layout.spacing, screen_layout.group_label_height
                ),
                widget_formatter_hook=create_group_box_formatter,
            ),
            widget_formatter_factory=widget_formatter_factory,
            layout=screen_layout,
            base_file_name=path.stem,
        )
        title = f"{device.label}"

        screen_formatter, sub_screens = formatter_factory.create_screen_formatter(
            device.children, title
        )

        path.write_text("".join(screen_formatter.format()))
        for sub_screen_name, sub_screen_formatter in sub_screens:
            sub_screen_path = Path(path.parent / f"{sub_screen_name}{path.suffix}")
            sub_screen_path.write_text("".join(sub_screen_formatter.format()))

    def format_bob(self, device: Device, path: Path):
        template = BobTemplate(str(Path(__file__).parent / "dls.bob"))
        # LP DOCS REF: Define the layout properties
        screen_layout = ScreenLayout(
            spacing=self.spacing,
            title_height=self.title_height,
            max_height=self.max_height,
            group_label_height=26,
            label_width=self.label_width,
            widget_width=self.widget_width,
            widget_height=self.widget_height,
            group_widget_indent=18,
            group_width_offset=26,
        )
        # SW DOCS REF: Extract widget types from template file
        widget_formatter_factory = WidgetFormatterFactory(
            header_formatter_cls=LabelWidgetFormatter.from_template(
                template,
                search="Heading",
                property_map={"text": "text"},
            ),
            label_formatter_cls=LabelWidgetFormatter.from_template(
                template,
                search="Label",
                property_map={"text": "text", "tooltip": "description"},
            ),
            led_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="LED",
                sized=Bounds.square,
                property_map={"pv_name": "pv"},
            ),
            progress_bar_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ProgressBar",
                property_map={"pv_name": "pv"},
            ),
            bitfield_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="BitField",
                property_map={"pv_name": "pv"},
            ),
            button_panel_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ButtonPanel",
                property_map={"pv_name": "pv"},
            ),
            array_trace_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ArrayTrace",
                property_map={"y_pv": "pv"},
            ),
            image_read_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ImageRead",
                property_map={"pv_name": "pv"},
            ),
            text_read_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="TextUpdate",
                property_map={"pv_name": "pv"},
            ),
            check_box_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ChoiceButton",
                property_map={"pv_name": "pv"},
            ),
            toggle_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ToggleButton",
                property_map={"pv_name": "pv"},
            ),
            combo_box_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="ComboBox",
                property_map={"pv_name": "pv"},
            ),
            text_write_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="TextEntry",
                property_map={"pv_name": "pv"},
            ),
            table_formatter_cls=PVWidgetFormatter.from_template(
                template,
                search="Table",
                property_map={"pv_name": "pv"},
            ),
            action_formatter_cls=ActionWidgetFormatter.from_template(
                template,
                search="WritePV",
                property_map={
                    "text": "label",
                    "pv_name": "pv",
                    "value": "value",
                    "tooltip": "tooltip",
                },
            ),
            sub_screen_formatter_cls=SubScreenWidgetFormatter.from_template(
                template,
                search="OpenDisplay",
                property_map={"file": "file_name", "text": "label", "macros": "macros"},
            ),
        )
        # MAKE_WIDGETS DOCS REF: Define screen and group widgets
        screen_title_cls = LabelWidgetFormatter.from_template(
            template,
            search="Title",
            property_map={"text": "text"},
        )
        group_title_cls = LabelWidgetFormatter.from_template(
            template,
            search="Group",
            property_map={"name": "text"},
        )

        def create_group_object_formatter(
            bounds: Bounds, title: str
        ) -> List[WidgetFormatter[str]]:
            return [
                group_title_cls(
                    bounds=Bounds(x=bounds.x, y=bounds.y, w=bounds.w, h=bounds.h),
                    text=f"{title}",
                )
            ]

        def create_screen_title_formatter(
            bounds: Bounds, title: str
        ) -> List[WidgetFormatter[str]]:
            return [
                screen_title_cls(
                    bounds=Bounds(x=0, y=0, w=bounds.w, h=screen_layout.title_height),
                    text=title,
                )
            ]

        # SCREEN_INI DOCS REF: Construct a screen object
        formatter_factory: ScreenFormatterFactory = ScreenFormatterFactory(
            screen_formatter_cls=GroupFormatter.from_template(
                template,
                search=GroupType.SCREEN,
                sized=with_title(screen_layout.spacing, screen_layout.title_height),
                widget_formatter_hook=create_screen_title_formatter,
            ),
            group_formatter_cls=GroupFormatter.from_template(
                template,
                search=GroupType.GROUP,
                sized=with_title(
                    screen_layout.spacing, screen_layout.group_label_height
                ),
                widget_formatter_hook=create_group_object_formatter,
            ),
            widget_formatter_factory=widget_formatter_factory,
            layout=screen_layout,
            base_file_name=path.stem,
        )
        # SCREEN_FORMAT DOCS REF: Format the screen
        title = f"{device.label}"

        screen_formatter, sub_screens = formatter_factory.create_screen_formatter(
            device.children, title
        )

        write_bob(screen_formatter, path)
        for sub_screen_name, sub_screen_formatter in sub_screens:
            sub_screen_path = Path(path.parent / f"{sub_screen_name}{path.suffix}")
            write_bob(sub_screen_formatter, sub_screen_path)