{"id":24221410,"url":"https://github.com/basnijholt/tuitorial","last_synced_at":"2025-10-03T17:38:31.167Z","repository":{"id":269606847,"uuid":"907972384","full_name":"basnijholt/tuitorial","owner":"basnijholt","description":"Create beautiful terminal-based code tutorials with syntax highlighting and interactive navigation.","archived":false,"fork":false,"pushed_at":"2025-08-07T08:56:51.000Z","size":480,"stargazers_count":504,"open_issues_count":9,"forks_count":8,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-08-07T10:31:40.218Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://tuitorial.readthedocs.io/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/basnijholt.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":"AUTHORS.md","dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-12-24T18:38:21.000Z","updated_at":"2025-08-07T08:56:44.000Z","dependencies_parsed_at":"2025-01-08T03:23:28.300Z","dependency_job_id":"2e7247f6-17a3-4303-ad1b-67a4c0b0e7f2","html_url":"https://github.com/basnijholt/tuitorial","commit_stats":null,"previous_names":["basnijholt/tuitorial"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/basnijholt/tuitorial","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basnijholt%2Ftuitorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basnijholt%2Ftuitorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basnijholt%2Ftuitorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basnijholt%2Ftuitorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/basnijholt","download_url":"https://codeload.github.com/basnijholt/tuitorial/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basnijholt%2Ftuitorial/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":269683790,"owners_count":24458756,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-10T02:00:08.965Z","response_time":71,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-14T06:01:51.453Z","updated_at":"2025-10-03T17:38:31.160Z","avatar_url":"https://github.com/basnijholt.png","language":"Python","funding_links":[],"categories":["Python","\u003ca name=\"office\"\u003e\u003c/a\u003eOffice tools"],"sub_categories":[],"readme":"# Tuitorial 📚\n\n\u003e Create beautiful terminal-based code tutorials with syntax highlighting and interactive navigation.\n\n[![Documentation](https://readthedocs.org/projects/tuitorial/badge/?version=latest)](https://tuitorial.readthedocs.io/en/latest/?badge=latest)\n[![PyPI version](https://badge.fury.io/py/tuitorial.svg)](https://badge.fury.io/py/tuitorial)\n[![Python](https://img.shields.io/pypi/pyversions/tuitorial.svg)](https://pypi.org/project/tuitorial/)\n[![Tests](https://github.com/basnijholt/tuitorial/actions/workflows/pytest.yml/badge.svg)](https://github.com/basnijholt/tuitorial/actions/workflows/pytest.yml)\n[![Coverage](https://codecov.io/gh/basnijholt/tuitorial/branch/main/graph/badge.svg)](https://codecov.io/gh/basnijholt/tuitorial)\n[![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![GitHub Repo stars](https://img.shields.io/github/stars/basnijholt/tuitorial)](https://github.com/basnijholt/tuitorial)\n\n\u003e [!NOTE]\n\u003e **tuitorial**? Typo? No, a combination of \"TUI\" (Terminal User Interface) and \"tutorial\".\n\n\u003c!-- toc-start --\u003e\n\u003cdetails\u003e\u003csummary\u003e\u003cb\u003e\u003cu\u003e[ToC]\u003c/u\u003e\u003c/b\u003e 📚\u003c/summary\u003e\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n- [🎯 Features](#-features)\n- [🤨 Why?](#-why)\n- [📸 Recording](#-recording)\n- [🚀 Installation](#-installation)\n- [🎮 Quick Start](#-quick-start)\n- [📖 Usage](#-usage)\n  - [📚 Multiple Chapters](#-multiple-chapters)\n  - [🚶 Steps](#-steps)\n  - [🎯 Focus Types](#-focus-types)\n    - [Literal Match](#literal-match)\n    - [Regular Expression](#regular-expression)\n    - [Line Number](#line-number)\n    - [Range](#range)\n    - [Starts With](#starts-with)\n    - [Between](#between)\n    - [Line Containing](#line-containing)\n    - [Markdown](#markdown)\n    - [Syntax Highlighting](#syntax-highlighting)\n  - [🎨 Styling](#-styling)\n  - [🔄 Live Reloading (Development Mode)](#-live-reloading-development-mode)\n  - [🎨 Custom Highlighting Patterns](#-custom-highlighting-patterns)\n  - [✨ Multiple Highlights per Step](#-multiple-highlights-per-step)\n  - [🖼️ Displaying Images](#-displaying-images)\n    - [ImageStep](#imagestep)\n    - [Image Positioning and Sizing](#image-positioning-and-sizing)\n    - [Alignment](#alignment)\n  - [🎬 Title Slide](#-title-slide)\n  - [📖 Helper Functions](#-helper-functions)\n    - [`create_bullet_point_chapter`](#create_bullet_point_chapter)\n- [⌨️ Controls](#-controls)\n- [🧪 Development](#-development)\n- [🤝 Contributing](#-contributing)\n- [📝 License](#-license)\n- [🙏 Acknowledgments](#-acknowledgments)\n- [📚 Similar Projects](#-similar-projects)\n- [🐛 Troubleshooting](#-troubleshooting)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n\u003c/details\u003e\n\u003c!-- toc-end --\u003e\n\n## 🎯 Features\n\n- 🎨 **Rich Syntax Highlighting:** Customizable styles, wide language support.\n- 🔍 **Multiple Focus Types:** Literal, regex, line, range, startswith, between, line containing, and syntax highlighting.\n- 📝 **Step-by-Step Tutorials:** Interactive, sequential steps with clear descriptions.\n- 🖼️ **Multimedia:** Markdown rendering and image embedding.\n- ⌨️ **Interactive Navigation:** Intuitive keyboard and scroll controls.\n- 🖥️ **Beautiful Terminal UI:** Powered by [Textual](https://textual.textualize.io/).\n- 🚀 **Customizable:** Python or YAML configuration, custom highlighting.\n- 🎓 **Beginner Friendly:** Simple API, no Textual knowledge required.\n- ⚡ **Title Slide:** Eye-catching ASCII art title slides.\n- 🔄 **Live Reloading:** Automatically refreshes app on YAML update.\n\n## 🤨 Why?\n\n99.9% shouldn't use `tuitorial`. But those 0.1% that should, will love it.\n\n- **Escape the Tyranny of Slide Decks:** Ditch PowerPoint, Google Slides, and Beamer. Embrace the terminal.\n- **Code is King:** Keep the focus on your code, where it belongs.\n- **Interactive and Engaging:** Step-by-step walkthroughs with interactive navigation.\n- **Reproducible and Versionable:** Define tutorials in code (Python or YAML) for easy tracking and updates.\n- **Lightweight and Fast:** No more bloated presentation software.\n- **Perfect for Nerds:** Ideal for explaining code, technical workshops, interactive documentation, and anyone who loves the terminal.\n- **Parameterize:** Create dynamic tutorials driven by code snippets and focus points.\n\n## 📸 Recording\n\nhttps://github.com/user-attachments/assets/53a0cdff-ed1b-479f-b94c-6a7b113bd8b3\n\nThis is a recording of the [`examples/pipefunc.yaml`](./examples/pipefunc.yaml) tutorial of the [`pipefunc` package](https://github.com/pipefunc/pipefunc).\n\n## 🚀 Installation\n\n```bash\npip install tuitorial\n```\n\n## 🎮 Quick Start\n\n\u003e [!TIP]\n\u003e Have `uv` installed? Run the following command to see a quick example:\n\u003e `uvx tuitorial https://raw.githubusercontent.com/basnijholt/tuitorial/refs/heads/main/examples/pipefunc.yaml`\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePython\u003c/b\u003e\u003c/summary\u003e\n\n```python markdown-code-runner\nfrom tuitorial import Chapter, Step, TuitorialApp, Focus\nfrom rich.style import Style\n\n# Your code to present\ncode = '''\ndef hello(name: str) -\u003e str:\n    return f\"Hello, {name}!\"\n\ndef main():\n    print(hello(\"World\"))\n'''\n\n# Define tutorial steps\nsteps = [\n    Step(\n        \"Function Definition\",\n        [Focus.regex(r\"def hello.*:$\", style=\"bold yellow\")]\n    ),\n    Step(\n        \"Return Statement\",\n        [Focus.literal('return f\"Hello, {name}!\"', style=\"bold green\")]\n    ),\n    Step(\n        \"Main Function\",\n        [Focus.range(code.find(\"def main\"), len(code), style=\"bold blue\")]\n    ),\n]\n\n# Create a chapter\nchapter = Chapter(\"Basic Example\", code, steps)\n\n# Run the tutorial\napp = TuitorialApp([chapter])\napp.run()\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eYAML\u003c/b\u003e\u003c/summary\u003e\n\n```yaml\nchapters:\n  - title: \"Basic Example\"\n    code: |\n      def hello(name: str) -\u003e str:\n          return f\"Hello, {name}!\"\n\n      def main():\n          print(hello(\"World\"))\n    steps:\n      - description: \"Function Definition\"\n        focus:\n          - type: regex\n            pattern: \"def hello.*:$\"\n            style: \"bold yellow\"\n      - description: \"Return Statement\"\n        focus:\n          - type: literal\n            text: 'return f\"Hello, {name}!\"'\n            style: \"bold green\"\n      - description: \"Main Function\"\n        focus:\n          - type: range\n            start: 26 #  Calculated index for \"def main\"\n            end: 53 #  Calculated length of the code\n            style: \"bold blue\"\n```\n\nTo run the YAML example:\n\n1. Save the YAML content as a `.yaml` file (e.g., `tuitorial.yaml`).\n2. Either:\n   - Use the provided `tuitorial.run_from_yaml(...)` function:\n   - Run `tuitorial --watch tuitorial.yaml` from the command line.\n\n```bash\n# From the command line\ntuitorial --watch tuitorial.yaml\n```\n\nor\n\n```python\n# In a separate Python file (e.g., run_yaml.py)\nfrom tuitorial.parse_yaml import run_from_yaml\n\nrun_from_yaml(\"tuitorial.yaml\")\n```\n\n\u003c/details\u003e\n\n## 📖 Usage\n\n### 📚 Multiple Chapters\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePython\u003c/b\u003e\u003c/summary\u003e\n\n```python markdown-code-runner\n# First chapter\nchapter1_code = '''\ndef greet(name: str) -\u003e str:\n    return f\"Hello, {name}!\"\n'''\nchapter1_steps = [\n    Step(\"Greeting Function\", [Focus.regex(r\"def greet.*:$\")]),\n    Step(\"Return Statement\", [Focus.literal('return f\"Hello, {name}!\"')]),\n]\nchapter1 = Chapter(\"Greetings\", chapter1_code, chapter1_steps)\n\n# Second chapter\nchapter2_code = '''\ndef farewell(name: str) -\u003e str:\n    return f\"Goodbye, {name}!\"\n'''\nchapter2_steps = [\n    Step(\"Farewell Function\", [Focus.regex(r\"def farewell.*:$\")]),\n    Step(\"Return Statement\", [Focus.literal('return f\"Goodbye, {name}!\"')]),\n]\nchapter2 = Chapter(\"Farewells\", chapter2_code, chapter2_steps)\n\n# Run tutorial with multiple chapters\napp = TuitorialApp([chapter1, chapter2])\napp.run()\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eYAML\u003c/b\u003e\u003c/summary\u003e\n\n```yaml\nchapters:\n  - title: \"Greetings\"\n    code: |\n      def greet(name: str) -\u003e str:\n          return f\"Hello, {name}!\"\n    steps:\n      - description: \"Greeting Function\"\n        focus:\n          - type: regex\n            pattern: \"def greet.*:$\"\n      - description: \"Return Statement\"\n        focus:\n          - type: literal\n            text: 'return f\"Hello, {name}!\"'\n\n  - title: \"Farewells\"\n    code: |\n      def farewell(name: str) -\u003e str:\n          return f\"Goodbye, {name}!\"\n    steps:\n      - description: \"Farewell Function\"\n        focus:\n          - type: regex\n            pattern: \"def farewell.*:$\"\n      - description: \"Return Statement\"\n        focus:\n          - type: literal\n            text: 'return f\"Goodbye, {name}!\"'\n```\n\n\u003c/details\u003e\n\n### 🚶 Steps\n\nEach step in a tutorial consists of a description and a list of focuses.\n\n**Python:**\n\n```python markdown-code-runner\nStep(\n    \"Step Description\",  # Shown in the UI\n    [\n        Focus.literal(\"some text\"),  # One or more Focus objects\n        Focus.regex(r\"pattern.*\"),   # Can combine different focus types\n    ]\n)\n```\n\n**YAML:**\n\n```yaml\nsteps:\n  - description: \"Step Description\"\n    focus:\n      - type: literal\n        text: \"some text\"\n      - type: regex\n        pattern: \"pattern.*\"\n```\n\n### 🎯 Focus Types\n\n#### Literal Match\n\n**Python:**\n\n```python markdown-code-runner\nFocus.literal(\"def\", style=\"bold yellow\")\nFocus.literal(\"def\", style=\"bold yellow\", match_index=[0, 2]) # Highlight the first and third \"def\"\n```\n\n**YAML:**\n\n```yaml\nfocus:\n  - type: literal\n    text: \"def\"\n    style: \"bold yellow\"\n  - type: literal\n    text: \"def\"\n    style: \"bold yellow\"\n    match_index: [0, 2] # Highlight the first and third \"def\"\n```\n\n**`match_index` note:**\n\n- If provided as an integer, only highlight the nth match (0-based).\n- If provided as a list of integers, highlight the matches corresponding to those indices.\n- If None, highlight all matches.\n\n#### Regular Expression\n\n**Python:**\n\n```python markdown-code-runner\nFocus.regex(r\"def \\w+\\(.*\\):\", style=\"bold green\")\n```\n\n**YAML:**\n\n```yaml\nfocus:\n  - type: regex\n    pattern: \"def \\\\w+\\\\(.*\\\\):\"\n    style: \"bold green\"\n```\n\n#### Line Number\n\n**Python:**\n\n```python markdown-code-runner\nFocus.line(1, style=\"bold blue\")  # Highlight first line\n```\n\n**YAML:**\n\n```yaml\nfocus:\n  - type: line\n    line_number: 1\n    style: \"bold blue\"\n```\n\n#### Range\n\nHighlights a specific range of characters within the code based on their indices (0-based).\n\n**Python:**\n\n```python markdown-code-runner\nFocus.range(0, 10, style=\"bold magenta\")  # Highlight first 10 characters\n```\n\n**YAML:**\n\n```yaml\nfocus:\n  - type: range\n    start: 0\n    end: 10\n    style: \"bold magenta\"\n```\n\n#### Starts With\n\nHighlights lines starting with the specified text. Can be configured to match from the start of any line or only at the start of the line.\n\n**Python:**\n\n```python markdown-code-runner\nFocus.startswith(\"import\", style=\"bold blue\", from_start_of_line=True)\nFocus.startswith(\"from\", style=\"bold blue\", from_start_of_line=False)\n```\n\n**YAML:**\n\n```yaml\nfocus:\n  - type: startswith\n    text: \"import\"\n    style: \"bold blue\"\n    from_start_of_line: true\n  - type: startswith\n    text: \"from\"\n    style: \"bold blue\"\n    from_start_of_line: false\n```\n\n#### Between\n\nHighlights text between two specified patterns. Supports inclusive or exclusive bounds, multiline matching, and greedy or non-greedy matching.\n\n**Python:**\n\n```python markdown-code-runner\nFocus.between(\"start_function\", \"end_function\", style=\"bold blue\", inclusive=True, multiline=True)\n```\n\n**YAML:**\n\n```yaml\nfocus:\n  - type: between\n    start_pattern: \"start_function\"\n    end_pattern: \"end_function\"\n    style: \"bold blue\"\n    inclusive: true\n    multiline: true\n    match_index: 0 # Only highlight the first match (0-based)\n    greedy: true # Use greedy matching (longest possible match)\n```\n\n#### Line Containing\n\nHighlights entire lines that contain a specified pattern, with optional inclusion of surrounding lines.\nCan match either literal text or regular expressions, and can select specific matches.\n\n**Python:**\n\n```python markdown-code-runner\n# Highlight all lines containing \"def\"\nFocus.line_containing(\"def\", style=\"bold yellow\")\n\n# Include surrounding lines\nFocus.line_containing(\n    \"def\",\n    style=\"bold yellow\",\n    lines_before=1,\n    lines_after=1,\n)\n\n# Use regex and only highlight second match\nFocus.line_containing(\n    r\"def \\w+\",\n    style=\"bold blue\",\n    regex=True,\n    match_index=1,\n)\n```\n\n**YAML:**\n\n```yaml\nfocus:\n  - type: line_containing\n    pattern: \"def\"\n    style: \"bold yellow\"\n    lines_before: 1 # optional: include line before match\n    lines_after: 1 # optional: include line after match\n    regex: false # optional: treat pattern as regex\n    match_index: 0 # optional: only highlight first match (0-based)\n```\n\nThe `line_containing` focus type is particularly useful for:\n\n- Highlighting function definitions and their body\n- Showing imports and their surrounding context\n- Focusing on specific sections of code while maintaining readability\n- Matching patterns across multiple lines with surrounding context\n\n#### Markdown\n\nDisplays the content as Markdown instead of code, using Textual's built-in `Markdown` widget. Only one `markdown` focus is allowed per step, and it will take precedence over any other focus types.\n\n**Python:**\n\n```python markdown-code-runner\nFocus.markdown()\n```\n\n**YAML:**\n\n```yaml\nfocus:\n  - type: markdown\n```\n\n#### Syntax Highlighting\n\nUses Rich's built-in syntax highlighting for the entire code or specific lines. Only one `syntax` focus is allowed per step, and it will take precedence over any other focus types besides `markdown`.\n\n**Python:**\n\n```python markdown-code-runner\n# Highlight all code\nFocus.syntax(theme=\"monokai\", line_numbers=True)\n\n# Highlight specific lines\nFocus.syntax(\n    theme=\"monokai\",\n    start_line=0,\n    end_line=3,\n)\n```\n\n**YAML:**\n\n```yaml\nfocus:\n  - type: syntax\n    lexer: \"python\" # optional: language to highlight (default: python)\n    theme: \"monokai\" # optional: color theme\n    line_numbers: true # optional: show line numbers\n    start_line: 0 # optional: first line to highlight\n    end_line: 3 # optional: last line to highlight\n```\n\n### 🎨 Styling\n\nStyles can be customized using Rich's style syntax:\n\n**Python:**\n\n```python markdown-code-runner\nfrom rich.style import Style\n\n# Using string syntax\nFocus.literal(\"def\", style=\"bold yellow\")\n\n# Using Style object\nFocus.literal(\"def\", style=Style(bold=True, color=\"yellow\"))\n```\n\n**YAML:**\n\n```yaml\nfocus:\n  - type: literal\n    text: \"def\"\n    style: \"bold yellow\" # Using string syntax\n\n  - type: literal\n    text: \"def\"\n    style: \"bold color(yellow)\" # Using Style object\n```\n\n\u003c/details\u003e\n\n### 🔄 Live Reloading (Development Mode)\n\n`tuitorial` offers a convenient development mode that automatically reloads your tutorial whenever you make changes to the YAML configuration file. This allows you to iterate quickly on your tutorial's content and see your changes reflected in real-time without manually restarting the application.\n\n**Usage:**\n\nTo enable live reloading, use the `--watch` (or `-w`) flag when running `tuitorial` from the command line:\n\n```bash\ntuitorial tutorial.yaml --watch\n```\n\nor\n\n```bash\ntuitorial tutorial.yaml -w\n```\n\n**How it Works:**\n\nWhen you run `tuitorial` with the `--watch` flag, it will monitor the specified YAML file for any modifications. If a change is detected, `tuitorial` will automatically:\n\n1. Parse the updated YAML configuration.\n2. Reload the tutorial content within the running application.\n3. Preserve the current chapter and step, so you can continue where you left off.\n\n### 🎨 Custom Highlighting Patterns\n\n**Python:**\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePython\u003c/b\u003e\u003c/summary\u003e\n\n```python markdown-code-runner\nfrom tuitorial import TuitorialApp, Focus\nfrom rich.style import Style\n\n# Define custom styles\nFUNCTION_STYLE = Style(color=\"bright_yellow\", bold=True)\nARGUMENT_STYLE = Style(color=\"bright_green\", italic=True)\n\n# Your code to present\ncode = '''\ndef hello(name: str) -\u003e str:\n    return f\"Hello, {name}!\"\n'''\n\n# Create focused patterns\npatterns = [\n    Focus.regex(r\"def \\w+\", style=FUNCTION_STYLE),\n    Focus.regex(r\"\\([^)]*\\)\", style=ARGUMENT_STYLE),\n]\n\n# Create tutorial step\ntutorial_steps = [\n    Step(\"Function Definition\", patterns),\n]\n\n# Create a chapter\nchapter = Chapter(\"Custom Patterns\", code, tutorial_steps)\n\n# Run the tutorial\napp = TuitorialApp([chapter])\napp.run()\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eYAML\u003c/b\u003e\u003c/summary\u003e\n\n```yaml\nchapters:\n  - title: \"Custom Patterns\"\n    code: |\n      def hello(name: str) -\u003e str:\n          return f\"Hello, {name}!\"\n    steps:\n      - description: \"Function Definition\"\n        focus:\n          - type: regex\n            pattern: \"def \\\\w+\"\n            style: \"bright_yellow bold\"\n          - type: regex\n            pattern: \"\\\\([^)]*\\\\)\"\n            style: \"bright_green italic\"\n```\n\n\u003c/details\u003e\n\n### ✨ Multiple Highlights per Step\n\n**Python:**\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePython\u003c/b\u003e\u003c/summary\u003e\n\n```python markdown-code-runner\nfrom tuitorial import Chapter, Step, TuitorialApp, Focus\nfrom rich.style import Style\n\n# Your code to present\ncode = '''\ndef hello(name: str) -\u003e str:\n    return f\"Hello, {name}!\"\n'''\n\ntutorial_steps = [\n    Step(\n        \"Input/Output\",\n        [\n            Focus.literal(\"name\", style=\"bold cyan\"),\n            Focus.regex(r\"-\u003e.*$\", style=\"bold yellow\"),\n        ]\n    ),\n    Step(\n        \"Complex Example\",\n        [\n            Focus.literal(\"def\", style=\"bold yellow\"),\n            Focus.regex(r\"\\(.*\\)\", style=\"italic green\"),  # Highlight function arguments\n            Focus.line(2, style=\"underline blue\"),  # Highlight the second line\n        ],\n    ),\n]\n\n# Create a chapter\nchapter = Chapter(\"Multiple Highlights\", code, tutorial_steps)\n\n# Run the tutorial\napp = TuitorialApp([chapter])\napp.run()\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eYAML\u003c/b\u003e\u003c/summary\u003e\n\n```yaml\nchapters:\n  - title: \"Multiple Highlights\"\n    code: |\n      def hello(name: str) -\u003e str:\n          return f\"Hello, {name}!\"\n    steps:\n      - description: \"Input/Output\"\n        focus:\n          - type: literal\n            text: \"name\"\n            style: \"bold cyan\"\n          - type: regex\n            pattern: \"-\u003e.*$\"\n            style: \"bold yellow\"\n```\n\n\u003c/details\u003e\n\n### 🖼️ Displaying Images\n\n`tuitorial` supports displaying images within your tutorials using the `ImageStep` class.\nThis allows you to incorporate visual aids, diagrams, or any other images to enhance your presentations.\n\n#### ImageStep\n\nThe `ImageStep` class takes the path or URL to an image file (or a PIL Image object) and a description as input.\n\n**Python:**\n\n```python markdown-code-runner\nfrom pathlib import Path\nfrom tuitorial import Chapter, ImageStep, TuitorialApp\n\n# Path to your image\nimage_path = Path(\"path/to/your/image.png\")\n\n# Define an ImageStep\nimage_step = ImageStep(\"Displaying an example image\", image_path)\n\n# Create a chapter with the image step\nchapter = Chapter(\"Image Example\", \"\", [image_step])\n\n# Run the tutorial\napp = TuitorialApp([chapter])\napp.run()\n```\n\n**YAML:**\n\n```yaml\nchapters:\n  - title: \"Image Example\"\n    steps:\n      - description: \"Displaying an example image\"\n        image: \"path/to/your/image.png\" # or URL\n```\n\n#### Image Positioning and Sizing\n\nYou can control the size of the image using the `width` and `height` properties when creating the `Image` widget within the `ImageStep`. These properties accept either integer values (for pixel dimensions) or strings representing percentages (relative to the container's size).\n\n**Python:**\n\n```python markdown-code-runner\nfrom pathlib import Path\nfrom textual_image.widget import Image\nfrom tuitorial import Chapter, ImageStep, TuitorialApp\n\nimage_path = Path(\"path/to/your/image.png\")\n\n# Set fixed width in cells and auto height\nimage_step_fixed = ImageStep(\"Fixed Size Image\", image_path, width=300, height=\"auto\")\n\n# Set width as a percentage of the container and height in cells\nimage_step_percentage = ImageStep(\"Percentage Width Image\", image_path, width=\"50%\", height=200)\n\nchapter = Chapter(\"Image Examples\", \"\", [image_step_fixed, image_step_percentage])\napp = TuitorialApp([chapter])\napp.run()\n```\n\n**YAML:**\n\n```yaml\nchapters:\n  - title: \"Image Examples\"\n    steps:\n      - description: \"Fixed Size Image\"\n        image: \"path/to/your/image.png\"\n        width: 300 # Fixed width in cells\n        height: \"auto\" # Auto height\n      - description: \"Percentage Width Image\"\n        image: \"path/to/your/image.png\"\n        width: \"50%\" # Width as a percentage\n        height: 200 # Fixed height in cells\n```\n\n#### Alignment\n\nBy default, images are aligned to the center.\nYou can align images to the left or right by setting `halign` to `\"left\"` or `\"right\"` respectively when creating the `ImageStep`.\n\n\n```yaml\nchapters:\n  - title: \"Image Alignment\"\n    steps:\n      - description: \"Left Aligned Image\"\n        image: \"path/to/your/image.png\"\n        halign: \"left\"\n      - description: \"Right Aligned Image\"\n        image: \"path/to/your/image.png\"\n        halign: \"right\"\n```\n\n### 🎬 Title Slide\n\n`tuitorial` allows you to create a visually appealing title slide for your tutorial using ASCII art generated by [PyFiglet](https://github.com/pwaller/pyfiglet).\n\n**Python:**\n\n```python markdown-code-runner\nfrom tuitorial import TuitorialApp, TitleSlide\n\ntitle_slide = TitleSlide(\n    \"My Tutorial\",     # Title text (required)\n    subtitle=\"An Awesome Tutorial\",  # Optional subtitle\n    font=\"slant\",   # Optional: PyFiglet font (see available fonts below)\n    gradient=\"lava\",  # Optional: Gradient color (see available gradients below)\n)\n\napp = TuitorialApp([], title_slide=title_slide)\napp.run()\n```\n\n**YAML:**\n\n```yaml\ntitle_slide:\n  title: \"My Tutorial\"\n  subtitle: \"An Awesome Tutorial\"\n  font: \"slant\"\n  gradient: \"lava\"\n\nchapters:\n  # ... your chapters ...\n```\n\n**Available Fonts:**\n\nYou can choose from a variety of fonts provided by PyFiglet. Some popular options include:\n\n- `slant`\n- `3-d`\n- `3x5`\n- `5lineoblique`\n- `acrobatic`\n- `avatar`\n- `banner`\n- `big`\n- `block`\n- `bubble`\n- `digital`\n- `doom`\n- `isometric1`\n- `letters`\n- `rectangles`\n- `standard`\n\nYou can find a full list of available fonts in the [PyFiglet documentation](https://github.com/pwaller/pyfiglet/tree/master/pyfiglet/fonts) or by running:\n\n```python\nimport pyfiglet\nprint(pyfiglet.FigletFont.getFonts())\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eClick to see all\u003c/b\u003e\u003c/summary\u003e\n\n\u003c!-- CODE:START --\u003e\n\u003c!-- import pyfiglet --\u003e\n\u003c!-- for name in sorted(pyfiglet.FigletFont.getFonts()): --\u003e\n\u003c!--     print(f\"- `{name}`\") --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n- `1943____`\n- `1row`\n- `3-d`\n- `3d-ascii`\n- `3d_diagonal`\n- `3x5`\n- `4max`\n- `4x4_offr`\n- `5lineoblique`\n- `5x7`\n- `5x8`\n- `64f1____`\n- `6x10`\n- `6x9`\n- `a_zooloo`\n- `acrobatic`\n- `advenger`\n- `alligator`\n- `alligator2`\n- `alpha`\n- `alphabet`\n- `amc_3_line`\n- `amc_3_liv1`\n- `amc_aaa01`\n- `amc_neko`\n- `amc_razor`\n- `amc_razor2`\n- `amc_slash`\n- `amc_slider`\n- `amc_thin`\n- `amc_tubes`\n- `amc_untitled`\n- `ansi_regular`\n- `ansi_shadow`\n- `aquaplan`\n- `arrows`\n- `asc_____`\n- `ascii___`\n- `ascii_new_roman`\n- `assalt_m`\n- `asslt__m`\n- `atc_____`\n- `atc_gran`\n- `avatar`\n- `b1ff`\n- `b_m__200`\n- `banner`\n- `banner3`\n- `banner3-D`\n- `banner4`\n- `barbwire`\n- `basic`\n- `battle_s`\n- `battlesh`\n- `baz__bil`\n- `bear`\n- `beer_pub`\n- `bell`\n- `benjamin`\n- `big`\n- `big_money-ne`\n- `big_money-nw`\n- `big_money-se`\n- `big_money-sw`\n- `bigchief`\n- `bigfig`\n- `binary`\n- `block`\n- `blocks`\n- `blocky`\n- `bloody`\n- `bolger`\n- `braced`\n- `bright`\n- `brite`\n- `briteb`\n- `britebi`\n- `britei`\n- `broadway`\n- `broadway_kb`\n- `bubble`\n- `bubble__`\n- `bubble_b`\n- `bulbhead`\n- `c1______`\n- `c2______`\n- `c_ascii_`\n- `c_consen`\n- `calgphy2`\n- `caligraphy`\n- `calvin_s`\n- `cards`\n- `catwalk`\n- `caus_in_`\n- `char1___`\n- `char2___`\n- `char3___`\n- `char4___`\n- `charact1`\n- `charact2`\n- `charact3`\n- `charact4`\n- `charact5`\n- `charact6`\n- `characte`\n- `charset_`\n- `chartr`\n- `chartri`\n- `chiseled`\n- `chunky`\n- `clb6x10`\n- `clb8x10`\n- `clb8x8`\n- `cli8x8`\n- `clr4x6`\n- `clr5x10`\n- `clr5x6`\n- `clr5x8`\n- `clr6x10`\n- `clr6x6`\n- `clr6x8`\n- `clr7x10`\n- `clr7x8`\n- `clr8x10`\n- `clr8x8`\n- `coil_cop`\n- `coinstak`\n- `cola`\n- `colossal`\n- `com_sen_`\n- `computer`\n- `contessa`\n- `contrast`\n- `convoy__`\n- `cosmic`\n- `cosmike`\n- `cour`\n- `courb`\n- `courbi`\n- `couri`\n- `crawford`\n- `crawford2`\n- `crazy`\n- `cricket`\n- `cursive`\n- `cyberlarge`\n- `cybermedium`\n- `cybersmall`\n- `cygnet`\n- `d_dragon`\n- `danc4`\n- `dancing_font`\n- `dcs_bfmo`\n- `decimal`\n- `deep_str`\n- `def_leppard`\n- `defleppard`\n- `delta_corps_priest_1`\n- `demo_1__`\n- `demo_2__`\n- `demo_m__`\n- `devilish`\n- `diamond`\n- `diet_cola`\n- `digital`\n- `doh`\n- `doom`\n- `dos_rebel`\n- `dotmatrix`\n- `double`\n- `double_shorts`\n- `drpepper`\n- `druid___`\n- `dwhistled`\n- `e__fist_`\n- `ebbs_1__`\n- `ebbs_2__`\n- `eca_____`\n- `efti_robot`\n- `eftichess`\n- `eftifont`\n- `eftipiti`\n- `eftirobot`\n- `eftitalic`\n- `eftiwall`\n- `eftiwater`\n- `electronic`\n- `elite`\n- `epic`\n- `etcrvs__`\n- `f15_____`\n- `faces_of`\n- `fair_mea`\n- `fairligh`\n- `fantasy_`\n- `fbr12___`\n- `fbr1____`\n- `fbr2____`\n- `fbr_stri`\n- `fbr_tilt`\n- `fender`\n- `filter`\n- `finalass`\n- `fire_font-k`\n- `fire_font-s`\n- `fireing_`\n- `flipped`\n- `flower_power`\n- `flyn_sh`\n- `fourtops`\n- `fp1_____`\n- `fp2_____`\n- `fraktur`\n- `fun_face`\n- `fun_faces`\n- `funky_dr`\n- `future_1`\n- `future_2`\n- `future_3`\n- `future_4`\n- `future_5`\n- `future_6`\n- `future_7`\n- `future_8`\n- `fuzzy`\n- `gauntlet`\n- `georgi16`\n- `georgia11`\n- `ghost`\n- `ghost_bo`\n- `ghoulish`\n- `glenyn`\n- `goofy`\n- `gothic`\n- `gothic__`\n- `graceful`\n- `gradient`\n- `graffiti`\n- `grand_pr`\n- `greek`\n- `green_be`\n- `hades___`\n- `heart_left`\n- `heart_right`\n- `heavy_me`\n- `helv`\n- `helvb`\n- `helvbi`\n- `helvi`\n- `henry_3d`\n- `heroboti`\n- `hex`\n- `hieroglyphs`\n- `high_noo`\n- `hills___`\n- `hollywood`\n- `home_pak`\n- `horizontal_left`\n- `horizontal_right`\n- `house_of`\n- `hypa_bal`\n- `hyper___`\n- `icl-1900`\n- `impossible`\n- `inc_raw_`\n- `invita`\n- `isometric1`\n- `isometric2`\n- `isometric3`\n- `isometric4`\n- `italic`\n- `italics_`\n- `ivrit`\n- `jacky`\n- `jazmine`\n- `jerusalem`\n- `joust___`\n- `js_block_letters`\n- `js_bracket_letters`\n- `js_capital_curves`\n- `js_cursive`\n- `js_stick_letters`\n- `katakana`\n- `kban`\n- `keyboard`\n- `kgames_i`\n- `kik_star`\n- `knob`\n- `konto`\n- `konto_slant`\n- `krak_out`\n- `larry3d`\n- `lazy_jon`\n- `lcd`\n- `lean`\n- `letter_w`\n- `letters`\n- `letterw3`\n- `lexible_`\n- `lil_devil`\n- `line_blocks`\n- `linux`\n- `lockergnome`\n- `mad_nurs`\n- `madrid`\n- `magic_ma`\n- `marquee`\n- `master_o`\n- `maxfour`\n- `mayhem_d`\n- `mcg_____`\n- `merlin1`\n- `merlin2`\n- `mig_ally`\n- `mike`\n- `mini`\n- `mirror`\n- `mnemonic`\n- `modern__`\n- `modular`\n- `morse`\n- `morse2`\n- `moscow`\n- `mshebrew210`\n- `muzzle`\n- `nancyj`\n- `nancyj-fancy`\n- `nancyj-improved`\n- `nancyj-underlined`\n- `new_asci`\n- `nfi1____`\n- `nipples`\n- `notie_ca`\n- `npn_____`\n- `nscript`\n- `ntgreek`\n- `nvscript`\n- `o8`\n- `octal`\n- `odel_lak`\n- `ogre`\n- `ok_beer_`\n- `old_banner`\n- `os2`\n- `outrun__`\n- `p_s_h_m_`\n- `p_skateb`\n- `pacos_pe`\n- `panther_`\n- `patorjk's_cheese`\n- `patorjk-hex`\n- `pawn_ins`\n- `pawp`\n- `peaks`\n- `pebbles`\n- `pepper`\n- `phonix__`\n- `platoon2`\n- `platoon_`\n- `pod_____`\n- `poison`\n- `puffy`\n- `puzzle`\n- `pyramid`\n- `r2-d2___`\n- `rad_____`\n- `rad_phan`\n- `radical_`\n- `rainbow_`\n- `rally_s2`\n- `rally_sp`\n- `rammstein`\n- `rampage_`\n- `rastan__`\n- `raw_recu`\n- `rci_____`\n- `rectangles`\n- `red_phoenix`\n- `relief`\n- `relief2`\n- `rev`\n- `ripper!_`\n- `road_rai`\n- `rockbox_`\n- `rok_____`\n- `roman`\n- `roman___`\n- `rot13`\n- `rotated`\n- `rounded`\n- `rowancap`\n- `rozzo`\n- `runic`\n- `runyc`\n- `sans`\n- `sansb`\n- `sansbi`\n- `sansi`\n- `santa_clara`\n- `sblood`\n- `sbook`\n- `sbookb`\n- `sbookbi`\n- `sbooki`\n- `script`\n- `script__`\n- `serifcap`\n- `shadow`\n- `shimrod`\n- `short`\n- `skate_ro`\n- `skateord`\n- `skateroc`\n- `sketch_s`\n- `sl_script`\n- `slant`\n- `slant_relief`\n- `slide`\n- `slscript`\n- `sm______`\n- `small`\n- `small_caps`\n- `small_poison`\n- `small_shadow`\n- `small_slant`\n- `smisome1`\n- `smkeyboard`\n- `smscript`\n- `smshadow`\n- `smslant`\n- `smtengwar`\n- `soft`\n- `space_op`\n- `spc_demo`\n- `speed`\n- `spliff`\n- `stacey`\n- `stampate`\n- `stampatello`\n- `standard`\n- `star_strips`\n- `star_war`\n- `starwars`\n- `stealth_`\n- `stellar`\n- `stencil1`\n- `stencil2`\n- `stforek`\n- `stick_letters`\n- `stop`\n- `straight`\n- `street_s`\n- `stronger_than_all`\n- `sub-zero`\n- `subteran`\n- `super_te`\n- `swamp_land`\n- `swan`\n- `sweet`\n- `t__of_ap`\n- `tanja`\n- `tav1____`\n- `taxi____`\n- `tec1____`\n- `tec_7000`\n- `tecrvs__`\n- `tengwar`\n- `term`\n- `test1`\n- `the_edge`\n- `thick`\n- `thin`\n- `this`\n- `thorned`\n- `threepoint`\n- `ti_pan__`\n- `ticks`\n- `ticksslant`\n- `tiles`\n- `times`\n- `timesofl`\n- `tinker-toy`\n- `tomahawk`\n- `tombstone`\n- `top_duck`\n- `train`\n- `trashman`\n- `trek`\n- `triad_st`\n- `ts1_____`\n- `tsalagi`\n- `tsm_____`\n- `tsn_base`\n- `tty`\n- `ttyb`\n- `tubular`\n- `twin_cob`\n- `twisted`\n- `twopoint`\n- `type_set`\n- `ucf_fan_`\n- `ugalympi`\n- `unarmed_`\n- `univers`\n- `usa_____`\n- `usa_pq__`\n- `usaflag`\n- `utopia`\n- `utopiab`\n- `utopiabi`\n- `utopiai`\n- `varsity`\n- `vortron_`\n- `war_of_w`\n- `wavy`\n- `weird`\n- `wet_letter`\n- `whimsy`\n- `wow`\n- `xbrite`\n- `xbriteb`\n- `xbritebi`\n- `xbritei`\n- `xchartr`\n- `xchartri`\n- `xcour`\n- `xcourb`\n- `xcourbi`\n- `xcouri`\n- `xhelv`\n- `xhelvb`\n- `xhelvbi`\n- `xhelvi`\n- `xsans`\n- `xsansb`\n- `xsansbi`\n- `xsansi`\n- `xsbook`\n- `xsbookb`\n- `xsbookbi`\n- `xsbooki`\n- `xtimes`\n- `xtty`\n- `xttyb`\n- `yie-ar__`\n- `yie_ar_k`\n- `z-pilot_`\n- `zig_zag_`\n- `zone7___`\n\n\u003c!-- OUTPUT:END --\u003e\n\u003c/details\u003e\n\n**Available Gradients:**\n\nYou can choose from a variety of gradients Tuitorial provides. These are:\n\n\u003c!-- CODE:START --\u003e\n\u003c!-- import tuitorial.widgets --\u003e\n\u003c!-- for name in tuitorial.widgets.GRADIENTS: --\u003e\n\u003c!--     print(f\"- `{name}`\") --\u003e\n\u003c!-- CODE:END --\u003e\n\u003c!-- OUTPUT:START --\u003e\n\u003c!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --\u003e\n- `lava`\n- `blue`\n- `green`\n- `rainbow`\n- `pink`\n- `ocean`\n\n\u003c!-- OUTPUT:END --\u003e\n\n### 📖 Helper Functions\n\n#### `create_bullet_point_chapter`\n\nThis helper function simplifies the creation of chapters that present information in a bullet-point format.\nIt automatically generates the code content from the list of bullet points, and each step in the generated chapter will highlight a different bullet point.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePython\u003c/b\u003e\u003c/summary\u003e\n\n```python markdown-code-runner\nfrom rich.style import Style\nfrom tuitorial import TuitorialApp\nfrom tuitorial.helpers import create_bullet_point_chapter\n\nbullet_points = [\n    \"This is the first point.\",\n    \"Here is the second point.\",\n    \"And finally, the third point.\",\n]\n\n# Create a chapter with bullet points\nbullet_point_chapter = create_bullet_point_chapter(\n    \"My Bullet Points\",\n    bullet_points,\n    style=Style(color=\"magenta\", bold=True),\n)\n\n# You can also add extra descriptive text per step:\nbullet_point_chapter_with_extras = create_bullet_point_chapter(\n    \"My Bullet Points with Extras\",\n    bullet_points,\n    extras=[\n        \"Extra info for point 1.\",\n        \"More details about point 2.\",\n        \"Final thoughts on point 3.\",\n    ],\n    style=Style(color=\"green\", bold=True),\n)\n\napp = TuitorialApp([bullet_point_chapter, bullet_point_chapter_with_extras])\napp.run()\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eYAML\u003c/b\u003e\u003c/summary\u003e\n\n```yaml\nchapters:\n  - title: \"My Bullet Points\"\n    type: bullet_points\n    bullet_points:\n      - \"This is the first point.\"\n      - \"Here is the second point.\"\n      - \"And finally, the third point.\"\n    style: \"magenta bold\"\n\n  - title: \"My Bullet Points with Extras\"\n    type: bullet_points\n    bullet_points:\n      - text: \"This is the first point.\"\n        extra: \"Extra info for point 1.\"\n      - text: \"Here is the second point.\"\n        extra: \"More details about point 2.\"\n      - text: \"And finally, the third point.\"\n        extra: \"Final thoughts on point 3.\"\n      - Fourth point without extra\n    style: \"green bold\"\n```\n\n\u003c/details\u003e\n\n## ⌨️ Controls\n\n- `↑` Previous step in current chapter\n- `↓` Next step in current chapter\n- `→` Next chapter\n- `←` Previous chapter\n- `r` Reset to first step of current chapter\n- `d` Toggle dim/bright background\n- `q` Quit `tuitorial`\n\n## 🧪 Development\n\n1. Clone the repository:\n\n   ```bash\n   git clone https://github.com/basnijholt/tuitorial.git\n   cd tuitorial\n   ```\n\n2. Create a virtual environment:\n\n   ```bash\n   python -m venv venv\n   source venv/bin/activate  # or `venv\\Scripts\\activate` on Windows\n   ```\n\n3. Install development dependencies:\n\n   ```bash\n   pip install -e \".[test]\"\n   ```\n\n4. Enable pre-commit hooks:\n\n   ```bash\n   pre-commit install\n   ```\n\n5. Run tests:\n\n   ```bash\n   pytest\n   ```\n\n## 🤝 Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/AmazingFeature`)\n3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)\n4. Push to the branch (`git push origin feature/AmazingFeature`)\n5. Open a Pull Request\n\n## 📝 License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## 🙏 Acknowledgments\n\n- [Textual](https://textual.textualize.io/) for the amazing TUI framework\n- [Rich](https://rich.readthedocs.io/) for beautiful terminal formatting\n\n## 📚 Similar Projects\n\n- [Spiel](https://github.com/JoshKarpel/spiel) Display richly-styled presentations using your terminal (also Textual-based, more for general presentations, no focus modes like `tuitorial`).\n- [present](https://github.com/vinayak-mehta/present) A terminal-based presentation tool with markdown support (more focused on general presentations, less on code tutorials) also the last commit was in 2020.\n\n## 🐛 Troubleshooting\n\n**Q: The colors don't show up correctly in my terminal.**\nA: Make sure your terminal supports true color and has a compatible color scheme.\n\n**Q: The tutorial doesn't respond to keyboard input.**\nA: Verify that your terminal emulator is properly forwarding keyboard events.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbasnijholt%2Ftuitorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbasnijholt%2Ftuitorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbasnijholt%2Ftuitorial/lists"}