├── .github └── workflows │ ├── publish.yaml │ └── ruff.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo_cogs.py ├── demo_cogs.tcss ├── images ├── cog_demo.gif ├── message_dialog.jpg ├── save_file_dialog.jpg └── text_entry_dialog.jpg ├── pyproject.toml └── src └── textual_cogs ├── __init__.py ├── dialogs ├── __init__.py ├── message_dialog.py ├── quit_dialog.py ├── save_dialog.py ├── single_choice_dialog.py ├── single_color_picker_dialog.py └── text_entry_dialog.py ├── icons.py └── labels.py /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package to PyPI when a Release is Created 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | pypi-publish: 9 | name: Publish release to PyPI 10 | runs-on: ubuntu-latest 11 | environment: 12 | name: pypi 13 | url: https://pypi.org/p/textual-cogs 14 | permissions: 15 | id-token: write 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.x" 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel 26 | - name: Build package 27 | run: | 28 | python setup.py sdist bdist_wheel # Could also be python -m build 29 | - name: Publish package distributions to PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: Ruff 2 | on: [workflow_dispatch, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - name: Install Python 9 | uses: actions/setup-python@v4 10 | with: 11 | python-version: "3.12" 12 | - name: Install dependencies 13 | run: | 14 | python -m pip install --upgrade pip 15 | pip install ruff 16 | # Include `--format=github` to enable automatic inline annotations. 17 | - name: Run Ruff 18 | run: ruff check --output-format=github . 19 | continue-on-error: false 20 | - name: Run Ruff format 21 | run: ruff format --check . 22 | continue-on-error: false 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ipynb 2 | .pytype 3 | .DS_Store 4 | .vscode 5 | .idea 6 | mypy_report 7 | docs/build 8 | docs/source/_build 9 | tools/*.txt 10 | playground/ 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | .spyproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # mkdocs documentation 112 | /site 113 | /docs-offline 114 | /mkdocs-nav-online.yml 115 | /mkdocs-nav-offline.yml 116 | 117 | # mypy 118 | .mypy_cache/ 119 | 120 | # Snapshot testing report output (default location) 121 | snapshot_report.html 122 | 123 | # Sandbox folder - convenient place for us to develop small test apps without leaving the repo 124 | sandbox/ 125 | 126 | # Cache of screenshots used in the docs 127 | .screenshot_cache 128 | 129 | # Used by mkdocs-material social plugin 130 | .cache 131 | 132 | # Wing 133 | .wpr 134 | .wpu -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mike Driscoll 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # textual-cogs 2 | 3 | A collection of Textual dialogs. 4 | 5 | ![screenshot](https://github.com/driscollis/textual-cogs/blob/main/images/cog_demo.gif) 6 | 7 | Dialogs included so far: 8 | 9 | - Generic `MessageDialog` - shows messages to the user 10 | - `SaveFileDialog` - gives the user a way to select a location to save a file 11 | - `SingleChoiceDialog` - gives the user a series of choices to pick from 12 | - `TextEntryDialog` - ask the user a question and get their answer using an `Input` widget 13 | 14 | ## Installation 15 | 16 | You can install `textual-cog` using pip: 17 | 18 | ``` 19 | python -m pip install textual-cogs 20 | ``` 21 | 22 | You also need [Textual](https://github.com/Textualize/textual) to run these dialogs. 23 | 24 | ## Example Usage 25 | 26 | Here is an example of creating a small application that opens the `MessageDialog` immediately. You would normally open the dialog in response to a message or event that has occurred, such as when the application has an error or you need to tell the user something. 27 | 28 | ```python 29 | from textual.app import App 30 | from textual.app import App, ComposeResult 31 | 32 | from textual_cogs.dialogs import MessageDialog 33 | from textual_cogs import icons 34 | 35 | 36 | class DialogApp(App): 37 | def on_mount(self) -> ComposeResult: 38 | def my_callback(value: None | bool) -> None: 39 | self.exit() 40 | 41 | self.push_screen( 42 | MessageDialog( 43 | "What is your favorite language?", 44 | icon=icons.ICON_QUESTION, 45 | title="Warning", 46 | ), 47 | my_callback, 48 | ) 49 | 50 | 51 | if __name__ == "__main__": 52 | app = DialogApp() 53 | app.run() 54 | ``` 55 | 56 | When you run this code, you will get something like the following: 57 | 58 | ![screenshot](https://github.com/driscollis/textual-cogs/blob/main/images/message_dialog.jpg) 59 | 60 | ### Creating a TextEntryDialog 61 | 62 | Here is how you would create a `TextEntryDialog`: 63 | 64 | ```python 65 | from textual.app import App 66 | from textual.app import App, ComposeResult 67 | 68 | from textual_cogs.dialogs import TextEntryDialog 69 | 70 | 71 | class DialogApp(App): 72 | def on_mount(self) -> ComposeResult: 73 | def my_callback(value: str | bool) -> None: 74 | self.exit() 75 | 76 | self.push_screen( 77 | TextEntryDialog("What is your name?", "Information"), my_callback 78 | ) 79 | 80 | 81 | if __name__ == "__main__": 82 | app = DialogApp() 83 | app.run() 84 | ``` 85 | 86 | When you run this code, you will see the following: 87 | 88 | ![screenshot](https://github.com/driscollis/textual-cogs/blob/main/images/text_entry_dialog.jpg) 89 | 90 | ### Creating a SaveFileDialog 91 | 92 | The following code demonstrates how to create a `SaveFileDialog`: 93 | 94 | ```python 95 | from textual.app import App 96 | from textual.app import App, ComposeResult 97 | 98 | from textual_cogs.dialogs import SaveFileDialog 99 | 100 | 101 | class DialogApp(App): 102 | def on_mount(self) -> ComposeResult: 103 | self.push_screen(SaveFileDialog()) 104 | 105 | if __name__ == "__main__": 106 | app = DialogApp() 107 | app.run() 108 | ``` 109 | 110 | When you run this code, you will see the following: 111 | 112 | ![screenshot](https://github.com/driscollis/textual-cogs/blob/main/images/save_file_dialog.jpg) 113 | -------------------------------------------------------------------------------- /demo_cogs.py: -------------------------------------------------------------------------------- 1 | # demo_cogs.py 2 | 3 | import platform 4 | 5 | from textual_cogs import icons, labels 6 | from textual_cogs.dialogs import MessageDialog, SaveFileDialog 7 | from textual_cogs.dialogs import ( 8 | SingleChoiceDialog, 9 | SingleColorPickerDialog, 10 | TextEntryDialog, 11 | ) 12 | 13 | from textual import on 14 | from textual.app import App, ComposeResult 15 | from textual.containers import Center, Vertical 16 | from textual.widgets import Button, TabbedContent, TabPane 17 | 18 | 19 | class DemoCogsApp(App): 20 | DEFAULT_CSS = """ 21 | DemoCogsApp { 22 | Button { 23 | width: 30; 24 | margin: 1; 25 | background: gold; 26 | } 27 | } 28 | 29 | #tabbed { 30 | background: $primary-lighten-1 30%; 31 | } 32 | """ 33 | 34 | def compose(self) -> ComposeResult: 35 | with TabbedContent(initial="msg-dlgs", id="tabbed"): 36 | with TabPane("Message Dialogs", id="msg-dlgs"): 37 | yield Vertical( 38 | Center( 39 | Button("Info MessageDialog", id="info-msg"), 40 | Button("Exclamation MessageDialog", id="exclamation-msg"), 41 | Button("Question MessageDialog", id="question-msg"), 42 | Button("Warning MessageDialog", id="warning-msg"), 43 | Button("Regular MessageDialog", id="regular-msg"), 44 | ) 45 | ) 46 | with TabPane("File Dialogs", id="file-dlgs"): 47 | yield Vertical(Center(Button("SaveFileDialog", id="save-file-dlg"))) 48 | 49 | with TabPane("Choice Dialogs", id="choice-dlgs"): 50 | yield Vertical( 51 | Center( 52 | Button("SingleChoiceDialog", id="single-choice-dlg"), 53 | Button("SingleColorPicker", id="single-color-picker-dlg"), 54 | Button("TextEntryDialog", id="text-entry-dlg"), 55 | ) 56 | ) 57 | 58 | def msg_dialog_callback(self, button_choice: None | bool) -> None: 59 | choices = { 60 | None: "OK", 61 | True: "Yes", 62 | False: "No or Cancel", 63 | } 64 | self.notify(f"You pressed '{choices[button_choice]}'") 65 | 66 | def save_file_dialog_callback(self, file: str) -> None: 67 | if file: 68 | self.notify(f"Saving file to: '{file}'") 69 | else: 70 | self.notify("You cancelled saving the file!") 71 | 72 | def single_choice_callback(self, choice: str) -> None: 73 | severity = "information" if choice == "Python" else "error" 74 | self.notify(f"You picked: '{choice}'", severity=severity) 75 | 76 | def single_color_callback(self, color: str) -> None: 77 | self.notify(f"You chose the color: '{color}'") 78 | 79 | def text_entry_callback(self, entry: str) -> None: 80 | self.notify(f"You entered: '{entry}'") 81 | 82 | @on(Button.Pressed, "#info-msg") 83 | def on_info_msg(self, event: Button.Pressed) -> None: 84 | self.log.info("on_info_msg called!") 85 | self.push_screen( 86 | MessageDialog( 87 | "An informational message", 88 | title="Information", 89 | icon=icons.ICON_INFORMATION, 90 | ), 91 | self.msg_dialog_callback, 92 | ) 93 | 94 | @on(Button.Pressed, "#exclamation-msg") 95 | def on_exclamation_msg(self, event: Button.Pressed) -> None: 96 | self.push_screen( 97 | MessageDialog("DANGER!", title="Exclamation", icon=icons.ICON_EXCLAMATION), 98 | self.msg_dialog_callback, 99 | ) 100 | 101 | @on(Button.Pressed, "#question-msg") 102 | def on_question_msg(self, event: Button.Pressed) -> None: 103 | self.push_screen( 104 | MessageDialog( 105 | "Do you want to save your work?", 106 | title="Question", 107 | icon=icons.ICON_QUESTION, 108 | flags=[labels.YES, labels.NO], 109 | ), 110 | self.msg_dialog_callback, 111 | ) 112 | 113 | @on(Button.Pressed, "#warning-msg") 114 | def on_warning_msg(self, event: Button.Pressed) -> None: 115 | self.push_screen( 116 | MessageDialog( 117 | "This is only a warning!", title="Warning", icon=icons.ICON_WARNING 118 | ), 119 | self.msg_dialog_callback, 120 | ) 121 | 122 | @on(Button.Pressed, "#regular-msg") 123 | def on_regular_msg(self, event: Button.Pressed) -> None: 124 | self.push_screen( 125 | MessageDialog( 126 | "Do you want to continue?", 127 | title="Regular", 128 | flags=[labels.OK, labels.CANCEL], 129 | ), 130 | self.msg_dialog_callback, 131 | ) 132 | 133 | @on(Button.Pressed, "#save-file-dlg") 134 | def on_save_file_dialog(self, event: Button.Pressed) -> None: 135 | if "Windows" in platform.platform(): 136 | self.push_screen(SaveFileDialog(root="C:/"), self.save_file_dialog_callback) 137 | else: 138 | self.push_screen(SaveFileDialog(), self.save_file_dialog_callback) 139 | 140 | @on(Button.Pressed, "#single-choice-dlg") 141 | def on_single_choice_dialog(self, event: Button.Pressed) -> None: 142 | choices = ["Python", "PHP", "C++", "Ruby", "Lua"] 143 | self.push_screen( 144 | SingleChoiceDialog( 145 | "What is your favorite language?", 146 | title="Choose Language", 147 | choices=choices, 148 | ), 149 | self.single_choice_callback, 150 | ) 151 | 152 | @on(Button.Pressed, "#single-color-picker-dlg") 153 | def on_color_picked(self, event: Button.Pressed) -> None: 154 | self.push_screen(SingleColorPickerDialog(), self.single_color_callback) 155 | 156 | @on(Button.Pressed, "#text-entry-dlg") 157 | def on_text_entry_dialog(self, event: Button.Pressed) -> None: 158 | self.push_screen( 159 | TextEntryDialog("Enter your favorite food:", title="Question"), 160 | self.text_entry_callback, 161 | ) 162 | 163 | 164 | if __name__ == "__main__": 165 | app = DemoCogsApp() 166 | app.run() 167 | -------------------------------------------------------------------------------- /demo_cogs.tcss: -------------------------------------------------------------------------------- 1 | DemoCogsApp { 2 | Button { 3 | width: 30; 4 | margin: 1; 5 | background: gold; 6 | } 7 | } 8 | 9 | #tabbed { 10 | background: $primary-lighten-1 30%; 11 | } -------------------------------------------------------------------------------- /images/cog_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/driscollis/textual-cogs/ea68093ebd508514e48cd7ea5644d85ebdcb8bb4/images/cog_demo.gif -------------------------------------------------------------------------------- /images/message_dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/driscollis/textual-cogs/ea68093ebd508514e48cd7ea5644d85ebdcb8bb4/images/message_dialog.jpg -------------------------------------------------------------------------------- /images/save_file_dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/driscollis/textual-cogs/ea68093ebd508514e48cd7ea5644d85ebdcb8bb4/images/save_file_dialog.jpg -------------------------------------------------------------------------------- /images/text_entry_dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/driscollis/textual-cogs/ea68093ebd508514e48cd7ea5644d85ebdcb8bb4/images/text_entry_dialog.jpg -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "textual-cogs" 3 | version = "0.0.3" 4 | authors = [ 5 | { name="Mike Driscoll", email="mike@pythonlibrary.org" }, 6 | ] 7 | description = "Dialogs for use with the Textual package." 8 | readme = "README.md" 9 | classifiers = [ 10 | "Development Status :: 4 - Beta", 11 | "License :: OSI Approved :: MIT License", 12 | "Environment :: Console", 13 | "Intended Audience :: Developers", 14 | "Operating System :: Microsoft :: Windows :: Windows 10", 15 | "Operating System :: Microsoft :: Windows :: Windows 11", 16 | "Operating System :: MacOS", 17 | "Operating System :: POSIX :: Linux", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Typing :: Typed", 24 | ] 25 | 26 | [project.urls] 27 | Homepage = "https://github.com/driscollis/textual-cogs" 28 | Issues = "https://github.com/driscollis/textual-cogs/issues" 29 | 30 | [build-system] 31 | requires = ["setuptools>=61.0"] 32 | build-backend = "setuptools.build_meta" 33 | 34 | [tool.ruff] 35 | exclude = ["__init__.py"] -------------------------------------------------------------------------------- /src/textual_cogs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/driscollis/textual-cogs/ea68093ebd508514e48cd7ea5644d85ebdcb8bb4/src/textual_cogs/__init__.py -------------------------------------------------------------------------------- /src/textual_cogs/dialogs/__init__.py: -------------------------------------------------------------------------------- 1 | from textual_cogs.dialogs.quit_dialog import QuitDialog 2 | from textual_cogs.dialogs.message_dialog import MessageDialog 3 | from textual_cogs.dialogs.save_dialog import SaveFileDialog 4 | from textual_cogs.dialogs.single_choice_dialog import SingleChoiceDialog 5 | from textual_cogs.dialogs.single_color_picker_dialog import SingleColorPickerDialog 6 | from textual_cogs.dialogs.text_entry_dialog import TextEntryDialog -------------------------------------------------------------------------------- /src/textual_cogs/dialogs/message_dialog.py: -------------------------------------------------------------------------------- 1 | # message_dialog.py 2 | 3 | from textual_cogs import labels 4 | 5 | from textual.app import ComposeResult 6 | from textual.containers import Center, Horizontal, Vertical 7 | from textual.screen import ModalScreen 8 | from textual.widgets import Button, Header, Label 9 | 10 | 11 | class MessageDialog(ModalScreen): 12 | DEFAULT_CSS = """ 13 | MessageDialog { 14 | align: center middle; 15 | background: $primary-lighten-1 30%; 16 | } 17 | 18 | #msg-dlg { 19 | width: 80; 20 | height: 12; 21 | border: thick $background 70%; 22 | content-align: center middle; 23 | } 24 | 25 | #message-lbl { 26 | margin-top: 1; 27 | } 28 | 29 | #msg-dlg-buttons{ 30 | align: center middle; 31 | } 32 | 33 | Button { 34 | margin: 1; 35 | margin-top: 0 36 | } 37 | """ 38 | 39 | def __init__( 40 | self, 41 | message: str, 42 | title: str = "", 43 | flags: list | None = None, 44 | icon: str = "", 45 | name: str | None = None, 46 | id: str | None = None, 47 | classes: str | None = None, 48 | ) -> None: 49 | super().__init__(name, id, classes) 50 | self.message = message 51 | self.title = title 52 | if flags is None: 53 | self.flags = [] 54 | else: 55 | self.flags = flags 56 | self.buttons = None 57 | self.icon = icon 58 | 59 | self.verify_flags() 60 | 61 | def compose(self) -> ComposeResult: 62 | """ 63 | Create the widgets for the MessageDialog's user interface 64 | """ 65 | buttons = [] 66 | if self.icon: 67 | message_label = Label(f"{self.icon} {self.message}", id="message-lbl") 68 | else: 69 | message_label = Label(self.message, id="message-lbl") 70 | if "OK" in self.buttons: 71 | buttons.append(Button("OK", id="ok-btn", variant="primary")) 72 | if "Cancel" in self.buttons: 73 | buttons.append(Button("Cancel", id="cancel-btn", variant="error")) 74 | if "Yes" in self.buttons: 75 | buttons.append(Button("Yes", id="yes-btn", variant="primary")) 76 | if "No" in self.buttons: 77 | buttons.append(Button("No", id="no-btn", variant="error")) 78 | 79 | yield Vertical( 80 | Header(), 81 | Center(message_label), 82 | Center(Horizontal(*buttons, id="msg-dlg-buttons")), 83 | id="msg-dlg", 84 | ) 85 | 86 | def on_button_pressed(self, event: Button.Pressed) -> None: 87 | """ 88 | Called when the user presses one of the buttons. 89 | 90 | OK - Returns None (via dismiss callback) 91 | Cancel and No - Returns False (via dismiss callback) 92 | Yes - Returns True (via dismiss callback) 93 | """ 94 | if event.button.id == "ok-btn": 95 | self.dismiss(None) 96 | elif event.button.id in ["cancel-btn", "no-btn"]: 97 | self.dismiss(False) 98 | else: 99 | self.dismiss(True) 100 | 101 | def verify_flags(self) -> None: 102 | """ 103 | Basic verification of the button flags the user sent to create the dialog 104 | """ 105 | self.buttons = [btn for btn in self.flags] 106 | button_count = len(self.buttons) 107 | 108 | # Verify buttons 109 | if button_count > 2: 110 | raise ValueError( 111 | f"You cannot have more than two buttons! Found {button_count}" 112 | ) 113 | elif "OK" in self.buttons and button_count == 2: 114 | if "Cancel" not in self.buttons: 115 | raise ValueError( 116 | f"OK button can only be paired with Cancel button. Found: {self.buttons}" 117 | ) 118 | elif "Yes" in self.buttons and button_count == 2: 119 | if "No" not in self.buttons: 120 | raise ValueError( 121 | f"Yes button can only be paired with No button. Found: {self.buttons}" 122 | ) 123 | elif button_count == 0: 124 | # No buttons found, so default to OK button 125 | self.buttons.append(labels.OK) 126 | -------------------------------------------------------------------------------- /src/textual_cogs/dialogs/quit_dialog.py: -------------------------------------------------------------------------------- 1 | # quit_dialog.py 2 | 3 | from textual.app import ComposeResult 4 | from textual.containers import Grid 5 | from textual.screen import ModalScreen 6 | from textual.widgets import Button, Label 7 | 8 | 9 | class QuitDialog(ModalScreen[bool]): 10 | """Screen with a dialog to quit.""" 11 | 12 | DEFAULT_CSS = """ 13 | QuitDialog { 14 | align: center middle; 15 | } 16 | 17 | #dialog { 18 | grid-size: 2; 19 | grid-gutter: 1 2; 20 | grid-rows: 1fr 3; 21 | padding: 0 1; 22 | width: 60; 23 | height: 11; 24 | border: thick $background 80%; 25 | background: $surface-lighten-1; 26 | } 27 | 28 | #question { 29 | column-span: 2; 30 | height: 1fr; 31 | width: 1fr; 32 | content-align: center middle; 33 | } 34 | 35 | Button { 36 | width: 100%; 37 | } 38 | """ 39 | 40 | def compose(self) -> ComposeResult: 41 | yield Grid( 42 | Label("Are you sure you want to quit?", id="question"), 43 | Button("Quit", variant="error", id="quit"), 44 | Button("Cancel", variant="primary", id="cancel"), 45 | id="dialog", 46 | ) 47 | 48 | def on_button_pressed(self, event: Button.Pressed) -> None: 49 | """ 50 | Called when the user presses a button in the QuitDialog 51 | """ 52 | if event.button.id == "quit": 53 | self.dismiss(True) 54 | else: 55 | self.dismiss(False) 56 | -------------------------------------------------------------------------------- /src/textual_cogs/dialogs/save_dialog.py: -------------------------------------------------------------------------------- 1 | # save_dialog.py 2 | 3 | import os 4 | 5 | from textual import on 6 | from textual.app import ComposeResult 7 | from textual.containers import Grid, Horizontal 8 | from textual.screen import ModalScreen 9 | from textual.widgets import Button, DirectoryTree, Header, Input, Label 10 | 11 | 12 | class SaveFileDialog(ModalScreen): 13 | DEFAULT_CSS = """ 14 | SaveFileDialog { 15 | align: center middle; 16 | background: $primary 30%; 17 | } 18 | 19 | #save_dialog{ 20 | grid-size: 1 5; 21 | grid-gutter: 1 2; 22 | grid-rows: 5% 45% 15% 30%; 23 | padding: 0 1; 24 | width: 100; 25 | height: 25; 26 | border: thick $background 70%; 27 | background: $surface-lighten-1; 28 | } 29 | 30 | #save_file { 31 | background: green; 32 | } 33 | """ 34 | 35 | def __init__( 36 | self, 37 | root="/", 38 | name: str | None = None, 39 | id: str | None = None, 40 | classes: str | None = None, 41 | ) -> None: 42 | super().__init__(name, id, classes) 43 | self.title = "Save File" 44 | self.root = root 45 | self.folder = root 46 | 47 | def compose(self) -> ComposeResult: 48 | """ 49 | Create the widgets for the SaveFileDialog's user interface 50 | """ 51 | yield Grid( 52 | Header(), 53 | Label(f"Folder name: {self.root}", id="folder"), 54 | DirectoryTree(self.root, id="directory"), 55 | Input(placeholder="filename.txt", id="filename"), 56 | Horizontal( 57 | Button("Save File", variant="primary", id="save_file"), 58 | Button("Cancel", variant="error", id="cancel_file"), 59 | ), 60 | id="save_dialog", 61 | ) 62 | 63 | def on_mount(self) -> None: 64 | """ 65 | Focus the input widget so the user can name the file 66 | """ 67 | self.query_one("#filename").focus() 68 | 69 | def on_button_pressed(self, event: Button.Pressed) -> None: 70 | """ 71 | Event handler for when the load file button is pressed 72 | """ 73 | event.stop() 74 | if event.button.id == "save_file": 75 | filename = self.query_one("#filename").value 76 | full_path = os.path.join(self.folder, filename) 77 | self.dismiss(full_path) 78 | else: 79 | self.dismiss(False) 80 | 81 | @on(DirectoryTree.DirectorySelected) 82 | def on_directory_selection(self, event: DirectoryTree.DirectorySelected) -> None: 83 | """ 84 | Called when the DirectorySelected message is emitted from the DirectoryTree 85 | """ 86 | self.folder = event.path 87 | self.query_one("#folder").update(f"Folder name: {self.folder}") 88 | -------------------------------------------------------------------------------- /src/textual_cogs/dialogs/single_choice_dialog.py: -------------------------------------------------------------------------------- 1 | # single_choice_dialog.py 2 | 3 | from textual import on 4 | from textual.app import ComposeResult 5 | from textual.containers import Center, Horizontal, Vertical 6 | from textual.screen import ModalScreen 7 | from textual.widgets import Button, Header, Label, OptionList 8 | 9 | 10 | class SingleChoiceDialog(ModalScreen): 11 | DEFAULT_CSS = """ 12 | SingleChoiceDialog { 13 | align: center middle; 14 | background: $primary 30%; 15 | 16 | #single-choice-dlg { 17 | width: 85; 18 | height: 18; 19 | border: thick $background 70%; 20 | content-align: center middle; 21 | margin: 1; 22 | } 23 | 24 | #single-choice-label { 25 | margin: 1; 26 | } 27 | 28 | Button { 29 | width: 50%; 30 | margin: 1; 31 | } 32 | } 33 | """ 34 | 35 | def __init__( 36 | self, 37 | message: str, 38 | title: str, 39 | choices: list[str], 40 | name: str | None = None, 41 | id: str | None = None, 42 | classes: str | None = None, 43 | ) -> None: 44 | super().__init__(name, id, classes) 45 | self.message = message 46 | self.title = title 47 | self.choices = choices 48 | self.current_option = None 49 | 50 | def compose(self) -> ComposeResult: 51 | """ 52 | Create the widgets for the SingleChoiceDialog's user interface 53 | """ 54 | yield Vertical( 55 | Header(), 56 | Center(Label(self.message, id="single-choice-label")), 57 | OptionList(*self.choices, id="single-choice-answer"), 58 | Center( 59 | Horizontal( 60 | Button("OK", variant="primary", id="single-choice-ok"), 61 | Button("Cancel", variant="error", id="single-choice-cancel"), 62 | ) 63 | ), 64 | id="single-choice-dlg", 65 | ) 66 | 67 | @on(OptionList.OptionHighlighted) 68 | @on(OptionList.OptionSelected) 69 | def on_option_selected( 70 | self, event: OptionList.OptionHighlighted | OptionList.OptionSelected 71 | ) -> None: 72 | """ 73 | Update the currently selected option when the user highlights or selects 74 | an item in the OptionList 75 | """ 76 | self.current_option = event.option.prompt 77 | 78 | @on(Button.Pressed, "#single-choice-ok") 79 | def on_ok(self, event: Button.Pressed) -> None: 80 | """ 81 | Return the user's choice back to the calling application and dismiss the dialog 82 | """ 83 | self.dismiss(self.current_option) 84 | 85 | @on(Button.Pressed, "#single-choice-cancel") 86 | def on_cancel(self, event: Button.Pressed) -> None: 87 | """ 88 | Returns False to the calling application and dismisses the dialog 89 | """ 90 | self.dismiss(False) 91 | -------------------------------------------------------------------------------- /src/textual_cogs/dialogs/single_color_picker_dialog.py: -------------------------------------------------------------------------------- 1 | # single_color_picker_dialog.py 2 | 3 | from textual._color_constants import COLOR_NAME_TO_RGB 4 | from textual import on 5 | from textual.app import ComposeResult 6 | from textual.containers import Center, Horizontal, Vertical 7 | from textual.screen import ModalScreen 8 | from textual.widgets import Button, Header, Static, Select 9 | 10 | 11 | class SingleColorPickerDialog(ModalScreen): 12 | DEFAULT_CSS = """ 13 | SingleColorPickerDialog { 14 | align: center middle; 15 | background: $primary 30%; 16 | 17 | #simple-color-dlg { 18 | width: 85; 19 | height: 18; 20 | border: thick $background 70%; 21 | content-align: center middle; 22 | margin: 1; 23 | } 24 | 25 | Button { 26 | width: 50%; 27 | margin: 1; 28 | } 29 | 30 | Static { 31 | width: 100%; 32 | height:5; 33 | 34 | } 35 | } 36 | """ 37 | 38 | def __init__( 39 | self, name: str | None = None, id: str | None = None, classes: str | None = None 40 | ) -> None: 41 | super().__init__(name, id, classes) 42 | self.title = "Color Picker" 43 | self.current_color = None 44 | 45 | def compose(self) -> ComposeResult: 46 | colors = list(COLOR_NAME_TO_RGB.keys()) 47 | colors.sort() 48 | static = Static(id="chosen-color") 49 | static.styles.background = None 50 | 51 | yield Vertical( 52 | Header(), 53 | Center(Select.from_values(colors, id="simple-color-picker")), 54 | Center(static), 55 | Center( 56 | Horizontal( 57 | Button("OK", variant="primary", id="simple-color-ok"), 58 | Button("Cancel", variant="error", id="simple-color-cancel"), 59 | ) 60 | ), 61 | id="simple-color-dlg", 62 | ) 63 | 64 | @on(Select.Changed, "#simple-color-picker") 65 | def on_selection_changed(self, event: Select.Changed): 66 | self.current_color = event.select.value 67 | self.log.info(f"Selection -> {event.select.value}") 68 | static = self.query_one("#chosen-color") 69 | static.styles.background = self.current_color 70 | 71 | @on(Button.Pressed, "#simple-color-ok") 72 | def on_ok(self, event: Button.Pressed) -> None: 73 | """ 74 | Return the user's choice back to the calling application and dismiss the dialog 75 | """ 76 | self.dismiss(self.current_color) 77 | 78 | @on(Button.Pressed, "#simple-color-cancel") 79 | def on_cancel(self, event: Button.Pressed) -> None: 80 | """ 81 | Returns False to the calling application and dismisses the dialog 82 | """ 83 | self.dismiss(False) 84 | -------------------------------------------------------------------------------- /src/textual_cogs/dialogs/text_entry_dialog.py: -------------------------------------------------------------------------------- 1 | # text_entry_dialog 2 | 3 | from textual import on 4 | from textual.app import ComposeResult 5 | from textual.containers import Center, Horizontal, Vertical 6 | from textual.screen import ModalScreen 7 | from textual.widgets import Button, Header, Input, Label 8 | 9 | 10 | class TextEntryDialog(ModalScreen): 11 | """ 12 | Display a dialog that allows the user to enter some text and return it 13 | """ 14 | 15 | DEFAULT_CSS = """ 16 | TextEntryDialog { 17 | align: center middle; 18 | background: $primary-lighten-1 30%; 19 | } 20 | 21 | #text-entry-dlg { 22 | width: 80; 23 | height: 14; 24 | border: thick $background 70%; 25 | content-align: center middle; 26 | margin: 1; 27 | } 28 | 29 | #text-entry-label { 30 | margin: 1; 31 | } 32 | 33 | Button { 34 | width: 50%; 35 | margin: 1; 36 | } 37 | """ 38 | 39 | def __init__( 40 | self, 41 | message: str, 42 | title: str, 43 | name: str | None = None, 44 | id: str | None = None, 45 | classes: str | None = None, 46 | ) -> None: 47 | super().__init__(name, id, classes) 48 | self.message = message 49 | self.title = title 50 | 51 | def compose(self) -> ComposeResult: 52 | """ 53 | Create the widgets for the TextEntryDialog's user interface 54 | """ 55 | yield Vertical( 56 | Header(), 57 | Center(Label(self.message, id="text-entry-label")), 58 | Input(placeholder="", id="answer"), 59 | Center( 60 | Horizontal( 61 | Button("OK", variant="primary", id="text-entry-ok"), 62 | Button("Cancel", variant="error", id="text-entry-cancel"), 63 | ) 64 | ), 65 | id="text-entry-dlg", 66 | ) 67 | 68 | def on_mount(self) -> None: 69 | """ 70 | Set the focus on the input widget by default when the dialog is loaded 71 | """ 72 | self.query_one("#answer").focus() 73 | 74 | @on(Button.Pressed, "#text-entry-ok") 75 | def on_ok(self, event: Button.Pressed) -> None: 76 | """ 77 | Return the user's entry back to the calling application and dismiss the dialog 78 | """ 79 | answer = self.query_one("#answer").value 80 | self.dismiss(answer) 81 | 82 | @on(Button.Pressed, "#text-entry-cancel") 83 | def on_cancel(self, event: Button.Pressed) -> None: 84 | """ 85 | Returns False to the calling application and dismisses the dialog 86 | """ 87 | self.dismiss(False) 88 | -------------------------------------------------------------------------------- /src/textual_cogs/icons.py: -------------------------------------------------------------------------------- 1 | from rich.text import Text 2 | 3 | ICON_EXCLAMATION = Text.from_markup(":exclamation:") 4 | ICON_INFORMATION = Text.from_markup(":information:") 5 | ICON_QUESTION = Text.from_markup(":question_mark:") 6 | ICON_WARNING = Text.from_markup(":warning:") 7 | -------------------------------------------------------------------------------- /src/textual_cogs/labels.py: -------------------------------------------------------------------------------- 1 | # labels.py 2 | 3 | # Message dialog button labels 4 | OK = "OK" 5 | CANCEL = "Cancel" 6 | YES = "Yes" 7 | NO = "No" 8 | --------------------------------------------------------------------------------