# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import random from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING, Any, Final, Literal, Mapping, Union, cast from typing_extensions import TypeAlias from streamlit.elements.lib.image_utils import AtomicImage, image_to_url from streamlit.errors import ( StreamlitInvalidMenuItemKeyError, StreamlitInvalidPageLayoutError, StreamlitInvalidSidebarStateError, StreamlitInvalidURLError, ) from streamlit.proto.ForwardMsg_pb2 import ForwardMsg as ForwardProto from streamlit.proto.PageConfig_pb2 import PageConfig as PageConfigProto from streamlit.runtime.metrics_util import gather_metrics from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx from streamlit.string_util import is_emoji, validate_material_icon from streamlit.url_util import is_url if TYPE_CHECKING: from typing_extensions import TypeGuard GET_HELP_KEY: Final = "get help" REPORT_A_BUG_KEY: Final = "report a bug" ABOUT_KEY: Final = "about" PageIcon: TypeAlias = Union[AtomicImage, str] Layout: TypeAlias = Literal["centered", "wide"] InitialSideBarState: TypeAlias = Literal["auto", "expanded", "collapsed"] _GetHelp: TypeAlias = Literal["Get help", "Get Help", "get help"] _ReportABug: TypeAlias = Literal["Report a bug", "report a bug"] _About: TypeAlias = Literal["About", "about"] MenuKey: TypeAlias = Literal[_GetHelp, _ReportABug, _About] MenuItems: TypeAlias = Mapping[MenuKey, Union[str, None]] # Emojis recommended by https://share.streamlit.io/rensdimmendaal/emoji-recommender/main/app/streamlit.py # for the term "streamlit". Watch out for zero-width joiners, # as they won't parse correctly in the list() call! RANDOM_EMOJIS: Final = list( "๐Ÿ”ฅโ„ข๐ŸŽ‰๐Ÿš€๐ŸŒŒ๐Ÿ’ฃโœจ๐ŸŒ™๐ŸŽ†๐ŸŽ‡๐Ÿ’ฅ๐Ÿคฉ๐Ÿค™๐ŸŒ›๐Ÿค˜โฌ†๐Ÿ’ก๐Ÿคช๐Ÿฅ‚โšก๐Ÿ’จ๐ŸŒ ๐ŸŽŠ๐Ÿฟ๐Ÿ˜›๐Ÿ”ฎ๐ŸคŸ๐ŸŒƒ๐Ÿƒ๐Ÿพ๐Ÿ’ซโ–ช๐ŸŒด๐ŸŽˆ๐ŸŽฌ๐ŸŒ€๐ŸŽ„๐Ÿ˜โ˜”โ›ฝ๐Ÿ‚๐Ÿ’ƒ๐Ÿ˜Ž๐Ÿธ๐ŸŽจ๐Ÿฅณโ˜€๐Ÿ˜๐Ÿ…ฑ๐ŸŒž๐Ÿ˜ป๐ŸŒŸ๐Ÿ˜œ๐Ÿ’ฆ๐Ÿ’…๐Ÿฆ„๐Ÿ˜‹๐Ÿ˜‰๐Ÿ‘ป๐Ÿ๐Ÿคค๐Ÿ‘ฏ๐ŸŒปโ€ผ๐ŸŒˆ๐Ÿ‘Œ๐ŸŽƒ๐Ÿ’›๐Ÿ˜š๐Ÿ”ซ๐Ÿ™Œ๐Ÿ‘ฝ๐Ÿฌ๐ŸŒ…โ˜๐Ÿท๐Ÿ‘ญโ˜•๐ŸŒš๐Ÿ’๐Ÿ‘…๐Ÿฅฐ๐Ÿœ๐Ÿ˜Œ๐ŸŽฅ๐Ÿ•บโ•๐Ÿงกโ˜„๐Ÿ’•๐Ÿปโœ…๐ŸŒธ๐Ÿšฌ๐Ÿค“๐Ÿนยฎโ˜บ๐Ÿ’ช๐Ÿ˜™โ˜˜๐Ÿค โœŠ๐Ÿค—๐Ÿต๐Ÿคž๐Ÿ˜‚๐Ÿ’ฏ๐Ÿ˜๐Ÿ“ป๐ŸŽ‚๐Ÿ’—๐Ÿ’œ๐ŸŒŠโฃ๐ŸŒ๐Ÿ˜˜๐Ÿ’†๐Ÿค‘๐ŸŒฟ๐Ÿฆ‹๐Ÿ˜ˆโ›„๐Ÿšฟ๐Ÿ˜Š๐ŸŒน๐Ÿฅด๐Ÿ˜ฝ๐Ÿ’‹๐Ÿ˜ญ๐Ÿ–ค๐Ÿ™†๐Ÿ‘โšช๐Ÿ’Ÿโ˜ƒ๐Ÿ™ˆ๐Ÿญ๐Ÿ’ป๐Ÿฅ€๐Ÿš—๐Ÿคง๐Ÿ๐Ÿ’Ž๐Ÿ’“๐Ÿค๐Ÿ’„๐Ÿ’–๐Ÿ”žโ‰โฐ๐Ÿ•Š๐ŸŽงโ˜ โ™ฅ๐ŸŒณ๐Ÿพ๐Ÿ™‰โญ๐Ÿ’Š๐Ÿณ๐ŸŒŽ๐Ÿ™Š๐Ÿ’ธโค๐Ÿ”ช๐Ÿ˜†๐ŸŒพโœˆ๐Ÿ“š๐Ÿ’€๐Ÿ โœŒ๐Ÿƒ๐ŸŒต๐Ÿšจ๐Ÿ’‚๐Ÿคซ๐Ÿคญ๐Ÿ˜—๐Ÿ˜„๐Ÿ’๐Ÿ‘๐Ÿ™ƒ๐Ÿ––๐Ÿ’ž๐Ÿ˜…๐ŸŽ…๐Ÿ„๐Ÿ†“๐Ÿ‘‰๐Ÿ’ฉ๐Ÿ”Š๐ŸคทโŒš๐Ÿ‘ธ๐Ÿ˜‡๐Ÿšฎ๐Ÿ’๐Ÿ‘ณ๐Ÿฝ๐Ÿ’˜๐Ÿ’ฟ๐Ÿ’‰๐Ÿ‘ ๐ŸŽผ๐ŸŽถ๐ŸŽค๐Ÿ‘—โ„๐Ÿ”๐ŸŽต๐Ÿค’๐Ÿฐ๐Ÿ‘“๐Ÿ„๐ŸŒฒ๐ŸŽฎ๐Ÿ™‚๐Ÿ“ˆ๐Ÿš™๐Ÿ“๐Ÿ˜ต๐Ÿ—ฃโ—๐ŸŒบ๐Ÿ™„๐Ÿ‘„๐Ÿš˜๐Ÿฅบ๐ŸŒ๐Ÿกโ™ฆ๐Ÿ’๐ŸŒฑ๐Ÿ‘‘๐Ÿ‘™โ˜‘๐Ÿ‘พ๐Ÿฉ๐Ÿฅถ๐Ÿ“ฃ๐Ÿผ๐Ÿคฃโ˜ฏ๐Ÿ‘ต๐Ÿซโžก๐ŸŽ€๐Ÿ˜ƒโœ‹๐Ÿž๐Ÿ™‡๐Ÿ˜น๐Ÿ™๐Ÿ‘ผ๐Ÿโšซ๐ŸŽ๐Ÿช๐Ÿ”จ๐ŸŒผ๐Ÿ‘†๐Ÿ‘€๐Ÿ˜ณ๐ŸŒ๐Ÿ“–๐Ÿ‘ƒ๐ŸŽธ๐Ÿ‘ง๐Ÿ’‡๐Ÿ”’๐Ÿ’™๐Ÿ˜žโ›…๐Ÿป๐Ÿด๐Ÿ˜ผ๐Ÿ—ฟ๐Ÿ—โ™ ๐Ÿฆโœ”๐Ÿค–โ˜ฎ๐Ÿข๐ŸŽ๐Ÿ’ค๐Ÿ˜€๐Ÿบ๐Ÿ˜๐Ÿ˜ด๐Ÿ“บโ˜น๐Ÿ˜ฒ๐Ÿ‘๐ŸŽญ๐Ÿ’š๐Ÿ†๐Ÿ‹๐Ÿ”ต๐Ÿ๐Ÿ”ด๐Ÿ””๐Ÿง๐Ÿ‘ฐโ˜Ž๐Ÿ†๐Ÿคก๐Ÿ ๐Ÿ“ฒ๐Ÿ™‹๐Ÿ“Œ๐Ÿฌโœ๐Ÿ”‘๐Ÿ“ฑ๐Ÿ’ฐ๐Ÿฑ๐Ÿ’ง๐ŸŽ“๐Ÿ•๐Ÿ‘Ÿ๐Ÿฃ๐Ÿ‘ซ๐Ÿ‘๐Ÿ˜ธ๐Ÿฆ๐Ÿ‘๐Ÿ†—๐ŸŽฏ๐Ÿ“ข๐Ÿšถ๐Ÿฆ…๐Ÿง๐Ÿ’ข๐Ÿ€๐Ÿšซ๐Ÿ’‘๐ŸŸ๐ŸŒฝ๐ŸŠ๐ŸŸ๐Ÿ’๐Ÿ’ฒ๐Ÿ๐Ÿฅ๐Ÿธโ˜โ™ฃ๐Ÿ‘Šโš“โŒ๐Ÿฏ๐Ÿˆ๐Ÿ“ฐ๐ŸŒง๐Ÿ‘ฟ๐Ÿณ๐Ÿ’ท๐Ÿบ๐Ÿ“ž๐Ÿ†’๐Ÿ€๐Ÿค๐Ÿšฒ๐Ÿ”๐Ÿ‘น๐Ÿ™๐ŸŒท๐Ÿ™Ž๐Ÿฅ๐Ÿ’ต๐Ÿ”๐Ÿ“ธโš โ“๐ŸŽฉโœ‚๐Ÿผ๐Ÿ˜‘โฌ‡โšพ๐ŸŽ๐Ÿ’”๐Ÿ”โšฝ๐Ÿ’ญ๐ŸŒ๐Ÿท๐Ÿโœ–๐Ÿ‡๐Ÿ“๐ŸŠ๐Ÿ™๐Ÿ‘‹๐Ÿค”๐ŸฅŠ๐Ÿ—ฝ๐Ÿ‘๐Ÿ˜๐Ÿฐ๐Ÿ’๐Ÿดโ™€๐Ÿฆ๐Ÿ“โœ๐Ÿ‘‚๐Ÿด๐Ÿ‘‡๐Ÿ†˜๐Ÿ˜ก๐Ÿ‰๐Ÿ‘ฉ๐Ÿ’Œ๐Ÿ˜บโœ๐Ÿผ๐Ÿ’๐Ÿถ๐Ÿ‘บ๐Ÿ–•๐Ÿ‘ฌ๐Ÿ‰๐Ÿป๐Ÿพโฌ…โฌโ–ถ๐Ÿ‘ฎ๐ŸŒโ™‚๐Ÿ”ธ๐Ÿ‘ถ๐Ÿฎ๐Ÿ‘ชโ›ณ๐Ÿ๐ŸŽพ๐Ÿ•๐Ÿ‘ด๐Ÿจ๐ŸŠ๐Ÿ”นยฉ๐ŸŽฃ๐Ÿ‘ฆ๐Ÿ‘ฃ๐Ÿ‘จ๐Ÿ‘ˆ๐Ÿ’ฌโญ•๐Ÿ“น๐Ÿ“ท" ) # Also pick out some vanity emojis. ENG_EMOJIS: Final = [ "๐ŸŽˆ", # st.balloons ๐ŸŽˆ๐ŸŽˆ "๐Ÿค“", # Abhi "๐Ÿˆ", # Amey "๐Ÿšฒ", # Thiago "๐Ÿง", # Matteo "๐Ÿฆ’", # Ken "๐Ÿณ", # Karrie "๐Ÿ•น๏ธ", # Jonathan "๐Ÿ‡ฆ๐Ÿ‡ฒ", # Henrikh "๐ŸŽธ", # Guido "๐Ÿฆˆ", # Austin "๐Ÿ’Ž", # Emiliano "๐Ÿ‘ฉโ€๐ŸŽค", # Naomi "๐Ÿง™โ€โ™‚๏ธ", # Jon "๐Ÿป", # Brandon "๐ŸŽŽ", # James # TODO: Solicit emojis from the rest of Streamlit ] def _lower_clean_dict_keys(dict: MenuItems) -> dict[str, Any]: return {str(k).lower().strip(): v for k, v in dict.items()} def _get_favicon_string(page_icon: PageIcon) -> str: """Return the string to pass to the frontend to have it show the given PageIcon. If page_icon is a string that looks like an emoji (or an emoji shortcode), we return it as-is. Otherwise we use `image_to_url` to return a URL. (If `image_to_url` raises an error and page_icon is a string, return the unmodified page_icon string instead of re-raising the error.) """ # Choose a random emoji. if page_icon == "random": return get_random_emoji() # If page_icon is an emoji, return it as is. if isinstance(page_icon, str) and is_emoji(page_icon): return page_icon if isinstance(page_icon, str) and page_icon.startswith(":material"): return validate_material_icon(page_icon) # Convert Path to string if necessary if isinstance(page_icon, Path): page_icon = str(page_icon) # Fall back to image_to_url. try: return image_to_url( page_icon, width=-1, # Always use full width for favicons clamp=False, channels="RGB", output_format="auto", image_id="favicon", ) except Exception: if isinstance(page_icon, str): # This fall-thru handles emoji shortcode strings (e.g. ":shark:"), # which aren't valid filenames and so will cause an Exception from # `image_to_url`. return page_icon raise @gather_metrics("set_page_config") def set_page_config( page_title: str | None = None, page_icon: PageIcon | None = None, layout: Layout = "centered", initial_sidebar_state: InitialSideBarState = "auto", menu_items: MenuItems | None = None, ) -> None: """ Configures the default settings of the page. .. note:: This must be the first Streamlit command used on an app page, and must only be set once per page. Parameters ---------- page_title: str or None The page title, shown in the browser tab. If None, defaults to the filename of the script ("app.py" would show "app โ€ข Streamlit"). page_icon : Anything supported by st.image (except list), str, or None The page favicon. If ``page_icon`` is ``None`` (default), the favicon will be a monochrome Streamlit logo. In addition to the types supported by |st.image|_ (except list), the following strings are valid: - A single-character emoji. For example, you can set ``page_icon="๐Ÿฆˆ"``. - An emoji short code. For example, you can set ``page_icon=":shark:"``. For a list of all supported codes, see https://share.streamlit.io/streamlit/emoji-shortcodes. - The string literal, ``"random"``. You can set ``page_icon="random"`` to set a random emoji from the supported list above. Emoji icons are courtesy of Twemoji and loaded from MaxCDN. - An icon from the Material Symbols library (rounded style) in the format ``":material/icon_name:"`` where "icon_name" is the name of the icon in snake case. For example, ``icon=":material/thumb_up:"`` will display the Thumb Up icon. Find additional icons in the `Material Symbols \ `_ font library. .. note:: Colors are not supported for Material icons. When you use a Material icon for favicon, it will be black, regardless of browser theme. .. |st.image| replace:: ``st.image`` .. _st.image: https://docs.streamlit.io/develop/api-reference/media/st.image layout: "centered" or "wide" How the page content should be laid out. Defaults to "centered", which constrains the elements into a centered column of fixed width; "wide" uses the entire screen. initial_sidebar_state: "auto", "expanded", or "collapsed" How the sidebar should start out. Defaults to "auto", which hides the sidebar on small devices and shows it otherwise. "expanded" shows the sidebar initially; "collapsed" hides it. In most cases, you should just use "auto", otherwise the app will look bad when embedded and viewed on mobile. menu_items: dict Configure the menu that appears on the top-right side of this app. The keys in this dict denote the menu item you'd like to configure: - "Get help": str or None The URL this menu item should point to. If None, hides this menu item. - "Report a Bug": str or None The URL this menu item should point to. If None, hides this menu item. - "About": str or None A markdown string to show in the About dialog. If None, only shows Streamlit's default About text. The URL may also refer to an email address e.g. ``mailto:john@example.com``. Example ------- >>> import streamlit as st >>> >>> st.set_page_config( ... page_title="Ex-stream-ly Cool App", ... page_icon="๐ŸงŠ", ... layout="wide", ... initial_sidebar_state="expanded", ... menu_items={ ... 'Get Help': 'https://www.extremelycoolapp.com/help', ... 'Report a bug': "https://www.extremelycoolapp.com/bug", ... 'About': "# This is a header. This is an *extremely* cool app!" ... } ... ) """ msg = ForwardProto() if page_title is not None: msg.page_config_changed.title = page_title if page_icon is not None: msg.page_config_changed.favicon = _get_favicon_string(page_icon) pb_layout: PageConfigProto.Layout.ValueType if layout == "centered": pb_layout = PageConfigProto.CENTERED elif layout == "wide": pb_layout = PageConfigProto.WIDE else: raise StreamlitInvalidPageLayoutError(layout=layout) msg.page_config_changed.layout = pb_layout pb_sidebar_state: PageConfigProto.SidebarState.ValueType if initial_sidebar_state == "auto": pb_sidebar_state = PageConfigProto.AUTO elif initial_sidebar_state == "expanded": pb_sidebar_state = PageConfigProto.EXPANDED elif initial_sidebar_state == "collapsed": pb_sidebar_state = PageConfigProto.COLLAPSED else: raise StreamlitInvalidSidebarStateError( initial_sidebar_state=initial_sidebar_state ) msg.page_config_changed.initial_sidebar_state = pb_sidebar_state if menu_items is not None: lowercase_menu_items = cast(MenuItems, _lower_clean_dict_keys(menu_items)) validate_menu_items(lowercase_menu_items) menu_items_proto = msg.page_config_changed.menu_items set_menu_items_proto(lowercase_menu_items, menu_items_proto) ctx = get_script_run_ctx() if ctx is None: return ctx.enqueue(msg) def get_random_emoji() -> str: # Weigh our emojis 10x, cuz we're awesome! # TODO: fix the random seed with a hash of the user's app code, for stability? return random.choice(RANDOM_EMOJIS + 10 * ENG_EMOJIS) def set_menu_items_proto(lowercase_menu_items, menu_items_proto) -> None: if GET_HELP_KEY in lowercase_menu_items: if lowercase_menu_items[GET_HELP_KEY] is not None: menu_items_proto.get_help_url = lowercase_menu_items[GET_HELP_KEY] else: menu_items_proto.hide_get_help = True if REPORT_A_BUG_KEY in lowercase_menu_items: if lowercase_menu_items[REPORT_A_BUG_KEY] is not None: menu_items_proto.report_a_bug_url = lowercase_menu_items[REPORT_A_BUG_KEY] else: menu_items_proto.hide_report_a_bug = True if ABOUT_KEY in lowercase_menu_items: if lowercase_menu_items[ABOUT_KEY] is not None: menu_items_proto.about_section_md = dedent(lowercase_menu_items[ABOUT_KEY]) def validate_menu_items(menu_items: MenuItems) -> None: for k, v in menu_items.items(): if not valid_menu_item_key(k): raise StreamlitInvalidMenuItemKeyError(key=k) if v is not None and ( not is_url(v, ("http", "https", "mailto")) and k != ABOUT_KEY ): raise StreamlitInvalidURLError(url=v) def valid_menu_item_key(key: str) -> TypeGuard[MenuKey]: return key in {GET_HELP_KEY, REPORT_A_BUG_KEY, ABOUT_KEY}