Skip to main content

Clickable file links with icons, toggles, and command orchestration widget for Textual TUI applications.

Project description

textual-filelink

CI PyPI Python Versions Downloads Coverage License

Clickable file links for Textual applications. Open files in your editor directly from your TUI.

Features

  • ๐Ÿ”— Clickable file links that open in your preferred editor (VSCode, vim, nano, etc.)
  • โ˜‘๏ธ Toggle controls for selecting/deselecting files
  • โŒ Remove buttons for file management interfaces
  • ๐ŸŽจ Multiple status icons with unicode support for rich visual feedback
  • ๐Ÿ“ Icon positioning - place icons before or after filenames
  • ๐Ÿ”ข Icon ordering - control display order with explicit indices
  • ๐Ÿ‘† Clickable icons - make icons interactive with click events
  • ๐Ÿ‘๏ธ Dynamic visibility - show/hide icons on the fly
  • ๐ŸŽฏ Jump to specific line and column in your editor
  • ๐Ÿ”ง Customizable command builders for any editor
  • ๐ŸŽญ Flexible layouts - show/hide controls as needed
  • ๐Ÿ’ฌ Tooltips for all interactive elements
  • ๐Ÿš€ Command orchestration with play/stop controls and animated spinners
  • โŒจ๏ธ Keyboard accessible - fully tabbable and navigable without a mouse
  • ๐Ÿ”‘ Customizable keyboard shortcuts - configure your own key bindings

Installation

pip install textual-filelink

Or with PDM:

pdm add textual-filelink

Quick Start

Basic FileLink

from pathlib import Path
from textual.app import App, ComposeResult
from textual_filelink import FileLink

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield FileLink("README.md", line=10, column=5)
    
    def on_file_link_opened(self, event: FileLink.Opened):
        self.notify(f"Opened {event.path.name} at line {event.line}")

if __name__ == "__main__":
    MyApp().run()

ToggleableFileLink with Multiple Icons

from textual_filelink import ToggleableFileLink

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield ToggleableFileLink(
            "script.py",
            initial_toggle=True,
            show_toggle=True,
            show_remove=True,
            icons=[
                {"name": "status", "icon": "โœ“", "clickable": True, "tooltip": "Validated"},
                {"name": "lock", "icon": "๐Ÿ”’", "position": "after", "tooltip": "Read-only"},
                {"name": "modified", "icon": "๐Ÿ“", "visible": False, "tooltip": "Modified"},
            ],
            toggle_tooltip="Toggle selection",
            remove_tooltip="Remove file",
            line=42
        )
    
    def on_toggleable_file_link_toggled(self, event: ToggleableFileLink.Toggled):
        self.notify(f"{event.path.name} toggled: {event.is_toggled}")
    
    def on_toggleable_file_link_removed(self, event: ToggleableFileLink.Removed):
        self.notify(f"{event.path.name} removed")
    
    def on_toggleable_file_link_icon_clicked(self, event: ToggleableFileLink.IconClicked):
        self.notify(f"Clicked '{event.icon_name}' icon on {event.path.name}")
        
        # Example: dynamically update icon
        link = event.control
        link.update_icon("status", icon="โณ", tooltip="Processing...")

if __name__ == "__main__":
    MyApp().run()

CommandLink for Command Orchestration

โœ… โ–ถ๏ธ Build โš™๏ธ - last run successful, play button to start again, command name ("Build"), settings icon โŒ โ–ถ๏ธ Build โš™๏ธ - last run failed, play button to start again, command name ("Build"), settings icon โ ง โน๏ธ Build โš™๏ธ - spinner shows command running, stop button to cancel run, command name ("Build"), settings icon

from textual_filelink import CommandLink

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield CommandLink(
            "Run Tests",
            output_path="test_output.log",
            initial_status_icon="โ—‹",
            initial_status_tooltip="Not run yet",
            show_settings=True,
        )

    def on_command_link_play_clicked(self, event: CommandLink.PlayClicked):
        link = self.query_one(CommandLink)
        link.set_status(running=True, tooltip="Running tests...")
        # Start your command here

    def on_command_link_stop_clicked(self, event: CommandLink.StopClicked):
        link = self.query_one(CommandLink)
        link.set_status(icon="โน", running=False, tooltip="Stopped")

    def on_command_link_settings_clicked(self, event: CommandLink.SettingsClicked):
        self.notify(f"Settings for {event.name}")

if __name__ == "__main__":
    MyApp().run()

FileLinkList for Managing Collections

from textual_filelink import FileLinkList, FileLink

class MyApp(App):
    def compose(self) -> ComposeResult:
        file_list = FileLinkList(show_toggles=True, show_remove=True)
        
        # Add items (all items must have explicit IDs)
        file_list.add_item(FileLink("test.py", id="test-py"), toggled=True)
        file_list.add_item(FileLink("main.py", id="main-py"))
        
        yield file_list
    
    def on_file_link_list_item_toggled(self, event: FileLinkList.ItemToggled):
        self.notify(f"Toggled: {event.item.path}")
    
    def on_file_link_list_item_removed(self, event: FileLinkList.ItemRemoved):
        self.notify(f"Removed: {event.item.path}")

if __name__ == "__main__":
    MyApp().run()

FileLinkWithIcons for Composable File Links

from textual_filelink import FileLinkWithIcons, Icon

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield FileLinkWithIcons(
            "script.py",
            line=42,
            icons_before=[
                Icon(name="status", icon="โœ…", tooltip="Validated"),
                Icon(name="type", icon="๐Ÿ", tooltip="Python file"),
            ],
            icons_after=[
                Icon(name="lock", icon="๐Ÿ”’", clickable=True, key="l", tooltip="Toggle lock"),
            ],
        )
    
    def on_file_link_with_icons_icon_clicked(self, event: FileLinkWithIcons.IconClicked):
        self.notify(f"Clicked icon: {event.icon_name}")

if __name__ == "__main__":
    MyApp().run()

Keyboard Navigation

Tab Navigation

All FileLink widgets are fully keyboard accessible and can be navigated using standard terminal keyboard shortcuts:

  • Tab - Move focus to the next widget
  • Shift+Tab - Move focus to the previous widget

When a FileLink widget has focus, it displays a visual indicator (border with accent color). You can customize the focus appearance using CSS.

Built-in Keyboard Shortcuts

All FileLink widgets support keyboard activation:

FileLink:

  • enter or o - Open file in editor

ToggleableFileLink:

  • enter or o - Open file in editor
  • space or t - Toggle checkbox
  • delete or x - Remove widget
  • 1-9 - Activate clickable icons (in order of appearance)

CommandLink:

  • enter or o - Open output file (if path is set)
  • space or p - Play/Stop command
  • s - Settings (if show_settings=True)

FileLinkWithIcons:

  • enter or o - Open file in editor
  • Custom keys - If icons have key parameter set

Default Keyboard Shortcuts

All widgets have class-level default keyboard shortcuts that can be customized:

FileLink:

FileLink.DEFAULT_OPEN_KEYS = ["enter", "o"]

CommandLink:

CommandLink.DEFAULT_OPEN_KEYS = ["enter", "o"]
CommandLink.DEFAULT_PLAY_STOP_KEYS = ["space", "p"]
CommandLink.DEFAULT_SETTINGS_KEYS = ["s"]

These defaults apply to all instances unless overridden with the open_keys, play_stop_keys, or settings_keys parameters.

Customizing Keyboard Shortcuts

You can customize keyboard shortcuts per-widget using the open_keys, play_stop_keys, and settings_keys parameters:

# FileLink with custom open keys
link = FileLink(
    "file.py",
    open_keys=["f2", "ctrl+o"]  # Override default "enter"/"o"
)

# CommandLink with custom shortcuts
cmd = CommandLink(
    "Build",
    open_keys=["enter"],
    play_stop_keys=["f5", "ctrl+r"],
    settings_keys=["f2"]
)

Dynamic App-Level Bindings

Bind number keys to activate specific widgets in a list without requiring focus (useful for scrollable lists of commands):

from textual import events
from textual.app import App, ComposeResult
from textual.containers import ScrollableContainer
from textual_filelink import CommandLink

class MyApp(App):
    def compose(self) -> ComposeResult:
        with ScrollableContainer():
            yield CommandLink("Build", id="cmd-1")
            yield CommandLink("Test", id="cmd-2")
            yield CommandLink("Deploy", id="cmd-3")

    def on_key(self, event: events.Key) -> None:
        """Route number keys to commands - triggers play/stop toggle."""
        if event.key.isdigit():
            num = int(event.key)
            commands = list(self.query(CommandLink))
            if 0 < num <= len(commands):
                cmd = commands[num - 1]
                # Use action method to toggle play/stop automatically
                cmd.action_play_stop()
                event.prevent_default()

Keyboard Shortcut Discoverability

All interactive elements automatically display their keyboard shortcuts in tooltips. This makes keyboard navigation discoverable without reading documentation.

Examples:

  • Toggle checkbox: "Click to toggle (space/t)"
  • Remove button: "Remove (delete/x)"
  • Play button: "Run command (space/p)"
  • Settings: "Settings (s)"
  • Clickable icon 1: "Status (1)"

FileLink API

Constructor

FileLink(
    path: Path | str,
    display_name: str | None = None,
    *,
    line: int | None = None,
    column: int | None = None,
    command_builder: Callable | None = None,
    open_keys: list[str] | None = None,
    name: str | None = None,
    id: str | None = None,
    classes: str | None = None,
    _embedded: bool = False,
    tooltip: str | None = None,
)

Parameters:

  • path: Full path to the file
  • display_name: Text to display for the link. If None, defaults to the filename
  • line: Optional line number to jump to
  • column: Optional column number to jump to
  • command_builder: Custom function to build the editor command
  • open_keys: Custom keyboard shortcuts for opening (default: ["enter", "o"])
  • name: Widget name
  • id: Widget ID
  • classes: CSS classes
  • _embedded: Internal use only. Internal use only. If True, disables focus to prevent stealing focus from parent widgets (used when FileLink is embedded in CommandLink or FileLinkWithIcons)
  • tooltip: Optional tooltip text

Properties

  • path: Path - The file path
  • display_name: str - The display name
  • line: int | None - The line number
  • column: int | None - The column number

Methods

open_file()

Open the file in the configured editor (can be called programmatically).

Messages

FileLink.Opened

Posted when the link is clicked or opened via keyboard.

Attributes:

  • widget: FileLink - The FileLink widget that was opened
  • path: Path - The file path that was opened
  • line: int | None - The line number to navigate to (or None)
  • column: int | None - The column number to navigate to (or None)

Note: FileLink.Clicked is deprecated but still available for backwards compatibility. Use FileLink.Opened instead.

Class-Level Configuration

# Set default command builder for all FileLink instances
FileLink.default_command_builder = FileLink.vim_command

# Set default open keys for all FileLink instances
FileLink.DEFAULT_OPEN_KEYS = ["enter", "f2"]

ToggleableFileLink API

โš ๏ธ Deprecation Notice: ToggleableFileLink is deprecated as of v0.4.0 and will be removed in v0.5.0. Use FileLinkWithIcons for icon support and FileLinkList for toggle/remove controls instead.

Constructor

ToggleableFileLink(
    path: Path | str,
    *,
    initial_toggle: bool = False,
    show_toggle: bool = True,
    show_remove: bool = True,
    icons: list[IconConfig | dict] | None = None,
    line: int | None = None,
    column: int | None = None,
    command_builder: Callable | None = None,
    disable_on_untoggle: bool = False,
    toggle_tooltip: str | None = None,
    remove_tooltip: str | None = None,
    link_tooltip: str | None = None,
    name: str | None = None,
    id: str | None = None,
    classes: str | None = None,
)

Parameters:

  • path: Full path to the file
  • initial_toggle: Whether the item starts toggled (checked)
  • show_toggle: Whether to display the toggle control (โ˜/โ˜‘)
  • show_remove: Whether to display the remove button (ร—)
  • icons: List of icon configurations (see IconConfig below)
  • line: Optional line number to jump to
  • column: Optional column number to jump to
  • command_builder: Custom function to build the editor command
  • disable_on_untoggle: If True, dim/disable the link when untoggled
  • toggle_tooltip: Optional tooltip text for the toggle button
  • remove_tooltip: Optional tooltip text for the remove button
  • link_tooltip: Optional tooltip text for the filename/link

IconConfig

Icons can be specified as dicts or IconConfig dataclasses:

from textual_filelink.toggleable_file_link import IconConfig

# As dict
{"name": "status", "icon": "โœ“", "clickable": True, "tooltip": "Done"}

# As dataclass
IconConfig(name="status", icon="โœ“", clickable=True, tooltip="Done")

IconConfig Properties:

  • name (str, required): Unique identifier for the icon
  • icon (str, required): Unicode character to display
  • position (str): "before" or "after" the filename (default: "before")
  • index (int | None): Explicit ordering index (default: None = use list order)
  • visible (bool): Whether icon is initially visible (default: True)
  • clickable (bool): Whether icon posts IconClicked messages (default: False)
  • tooltip (str | None): Tooltip text (default: None)

Properties

  • path: Path - The file path
  • is_toggled: bool - Current toggle state
  • icons: list[dict] - List of all icon configurations
  • file_link: FileLink - The internal FileLink widget

Methods

set_icon_visible(name: str, visible: bool)

Show or hide a specific icon.

link.set_icon_visible("warning", True)   # Show icon
link.set_icon_visible("warning", False)  # Hide icon

update_icon(name: str, **kwargs)

Update any properties of an existing icon.

link.update_icon("status", icon="โœ“", tooltip="Complete")
link.update_icon("status", visible=False)
link.update_icon("status", position="after", index=5)

Updatable properties: icon, position, index, visible, clickable, tooltip

get_icon(name: str) -> dict | None

Get a copy of an icon's configuration.

config = link.get_icon("status")
if config:
    print(f"Icon: {config['icon']}, Visible: {config['visible']}")

set_toggle_tooltip(tooltip: str | None)

Update the toggle button tooltip.

set_remove_tooltip(tooltip: str | None)

Update the remove button tooltip.

set_link_tooltip(tooltip: str | None)

Update the filename/link tooltip.

Messages

ToggleableFileLink.Toggled

Posted when the toggle state changes.

Attributes:

  • path: Path
  • is_toggled: bool

ToggleableFileLink.Removed

Posted when the remove button is clicked.

Attributes:

  • path: Path

ToggleableFileLink.IconClicked

Posted when a clickable icon is clicked.

Attributes:

  • path: Path
  • icon_name: str - The name of the clicked icon
  • icon: str - The unicode character of the icon

CommandLink API

CommandLink is a widget for command orchestration and status display. It provides play/stop controls, animated spinner, status icons, and optional settings.

Architecture

CommandLink is a standalone widget (extends Horizontal, not ToggleableFileLink). It has a flat composition:

  • Status icon (or animated spinner when running)
  • Play/stop button (โ–ถ๏ธ/โธ๏ธ)
  • Command name (clickable FileLink if output_path is set)
  • Settings icon (optional, if show_settings=True)

Note: Toggle and remove controls are NOT part of CommandLink. If you need those, add the CommandLink to a FileLinkList with show_toggles=True and show_remove=True.

Quick Start

from textual_filelink import CommandLink

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield CommandLink(
            "Run Tests",
            output_path="test_output.log",
            initial_status_icon="โ—‹",
            initial_status_tooltip="Not run yet",
            show_settings=True,
        )

    def on_command_link_play_clicked(self, event: CommandLink.PlayClicked):
        link = self.query_one(CommandLink)
        link.set_status(running=True, tooltip="Running tests...")
        self.run_worker(self.run_tests(link))

    def on_command_link_stop_clicked(self, event: CommandLink.StopClicked):
        link = self.query_one(CommandLink)
        link.set_status(icon="โน", running=False, tooltip="Stopped")

    def on_command_link_settings_clicked(self, event: CommandLink.SettingsClicked):
        self.notify(f"Settings for {event.name}")

    async def run_tests(self, link: CommandLink):
        # Simulate test run
        await asyncio.sleep(2)
        link.set_status(icon="โœ…", running=False, tooltip="All tests passed")

Constructor

CommandLink(
    name: str,
    *,
    output_path: Path | str | None = None,
    command_builder: Callable | None = None,
    initial_status_icon: str = "โ—‹",
    initial_status_tooltip: str | None = None,
    show_settings: bool = False,
    open_keys: list[str] | None = None,
    play_stop_keys: list[str] | None = None,
    settings_keys: list[str] | None = None,
    id: str | None = None,
    classes: str | None = None,
)

Parameters:

  • name: Command display name (also used to generate widget ID if not provided)
  • output_path: Path to output file. If set, clicking command name opens the file
  • command_builder: Custom command builder for opening output files
  • initial_status_icon: Initial status icon (default: "โ—‹")
  • initial_status_tooltip: Initial tooltip for status icon
  • show_settings: Whether to show the settings icon (default: False)
  • open_keys: Custom keyboard shortcuts for opening output (default: ["enter", "o"])
  • play_stop_keys: Custom keyboard shortcuts for play/stop (default: ["space", "p"])
  • settings_keys: Custom keyboard shortcuts for settings (default: ["s"])
  • id: Widget ID. If None, auto-generated from name
  • classes: CSS classes

Layout

[status/spinner] [โ–ถ๏ธ/โธ๏ธ] command_name [โš™๏ธ]
  • status/spinner: Shows status icon, or animated spinner when running
  • play/stop: โ–ถ๏ธ when stopped, โธ๏ธ when running
  • command_name: Clickable link to output file (if output_path is set)
  • settings: โš™๏ธ icon (only if show_settings=True)

Properties

  • name: str - The command name
  • output_path: Path | None - Current output file path
  • is_running: bool - Whether the command is currently running

Methods

set_status(icon: str | None = None, running: bool | None = None, tooltip: str | None = None)

Update command status display.

# Start running (shows spinner)
link.set_status(running=True, tooltip="Running tests...")

# Complete with success
link.set_status(icon="โœ…", running=False, tooltip="All tests passed")

# Complete with failure
link.set_status(icon="โŒ", running=False, tooltip="3 tests failed")

# Update tooltip only
link.set_status(tooltip="Still running...")

set_output_path(output_path: Path | str | None)

Update the output file path.

link.set_output_path(Path("output.log"))
link.set_output_path(None)  # Clear output path

Messages

CommandLink.PlayClicked

Posted when play button (โ–ถ๏ธ) is clicked.

Attributes:

  • widget: CommandLink - The CommandLink widget that was clicked
  • name: str - The command name
  • output_path: Path | None - The output file path (or None if not set)

CommandLink.StopClicked

Posted when stop button (โธ๏ธ) is clicked.

Attributes:

  • widget: CommandLink - The CommandLink widget that was clicked
  • name: str - The command name
  • output_path: Path | None - The output file path (or None if not set)

CommandLink.SettingsClicked

Posted when settings icon (โš™๏ธ) is clicked (only if show_settings=True).

Attributes:

  • widget: CommandLink - The CommandLink widget that was clicked
  • name: str - The command name
  • output_path: Path | None - The output file path (or None if not set)

CommandLink.OutputClicked

Posted when command name is clicked (opens output file).

Attributes:

  • output_path: Path - The output file path

Status Icons

Common status icons for commands:

"โ—‹"  # Not run / Unknown
"โœ…"  # Success / Passed
"โŒ"  # Failed / Error
"โš ๏ธ"  # Warning
"โญ๏ธ"  # Skipped
"๐Ÿ”„"  # Needs rerun
"โน"  # Stopped

Adding Toggle/Remove to CommandLink

CommandLink doesn't have built-in toggle/remove controls. Use FileLinkList to add them:

from textual_filelink import FileLinkList, CommandLink

class MyApp(App):
    def compose(self) -> ComposeResult:
        file_list = FileLinkList(show_toggles=True, show_remove=True)
        
        # Add CommandLinks (must have explicit IDs)
        file_list.add_item(
            CommandLink("Build", id="cmd-build"),
            toggled=True
        )
        file_list.add_item(
            CommandLink("Test", id="cmd-test"),
            toggled=False
        )
        
        yield file_list

Complete Example

from pathlib import Path
import asyncio
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.widgets import Header, Footer, Static
from textual_filelink import CommandLink

class CommandRunnerApp(App):
    CSS = """
    Screen {
        align: center middle;
    }
    Vertical {
        width: 60;
        height: auto;
        border: solid green;
        padding: 1;
    }
    """

    def compose(self) -> ComposeResult:
        yield Header()

        with Vertical():
            yield Static("๐Ÿš€ Command Runner")

            yield CommandLink(
                "Unit Tests",
                initial_status_icon="โ—‹",
                initial_status_tooltip="Not run",
                show_settings=True,
                id="unit-tests",
            )

            yield CommandLink(
                "Lint",
                initial_status_icon="โ—‹",
                initial_status_tooltip="Not run",
                show_settings=False,
                id="lint",
            )

            yield CommandLink(
                "Build",
                initial_status_icon="โ—‹",
                initial_status_tooltip="Not run",
                show_settings=True,
                id="build",
            )

        yield Footer()

    def on_command_link_play_clicked(self, event: CommandLink.PlayClicked):
        link = self.query_one(f"#{event.name}", CommandLink)
        link.set_status(running=True, tooltip=f"Running {event.name}...")
        self.run_worker(self.simulate_command(link, event.name))

    def on_command_link_stop_clicked(self, event: CommandLink.StopClicked):
        link = self.query_one(f"#{event.name}", CommandLink)
        link.set_status(icon="โน", running=False, tooltip="Stopped")
        self.notify(f"Stopped {event.name}", severity="warning")

    def on_command_link_settings_clicked(self, event: CommandLink.SettingsClicked):
        self.notify(f"Settings for {event.name}")

    async def simulate_command(self, link: CommandLink, name: str):
        await asyncio.sleep(2)
        # Simulate success/failure
        import random
        if random.random() > 0.3:
            link.set_status(icon="โœ…", running=False, tooltip="Passed")
            link.set_output_path(Path(f"{name.lower().replace(' ', '_')}.log"))
            self.notify(f"{name} passed!", severity="information")
        else:
            link.set_status(icon="โŒ", running=False, tooltip="Failed")
            self.notify(f"{name} failed!", severity="error")

if __name__ == "__main__":
    CommandRunnerApp().run()

FileLinkList API

FileLinkList is a container for managing collections of file link widgets with uniform controls.

Features

  • Automatic scrolling via VerticalScroll
  • Optional toggle checkboxes for each item
  • Optional remove buttons for each item
  • ID validation (all items must have explicit IDs, no duplicates)
  • Batch operations: toggle_all(), remove_selected()
  • Works with FileLink, FileLinkWithIcons, CommandLink, and ToggleableFileLink

Constructor

FileLinkList(
    *,
    show_toggles: bool = False,
    show_remove: bool = False,
    id: str | None = None,
    classes: str | None = None,
)

Parameters:

  • show_toggles: Whether to show toggle checkboxes for all items
  • show_remove: Whether to show remove buttons for all items
  • id: Widget ID
  • classes: CSS classes

Methods

add_item(item: Widget, *, toggled: bool = False)

Add an item to the list.

file_list.add_item(FileLink("test.py", id="test-py"), toggled=True)
file_list.add_item(CommandLink("Build", id="cmd-build"))

Raises:

  • ValueError if item has no ID or ID is duplicate

remove_item(item: Widget)

Remove an item from the list.

file_list.remove_item(item)

clear_items()

Remove all items from the list.

file_list.clear_items()

toggle_all(value: bool)

Set all toggle checkboxes to the same value.

file_list.toggle_all(True)   # Check all
file_list.toggle_all(False)  # Uncheck all

remove_selected()

Remove all toggled items from the list.

file_list.remove_selected()

get_toggled_items() -> list[Widget]

Get all currently toggled items.

selected = file_list.get_toggled_items()
for item in selected:
    print(item.path)

get_items() -> list[Widget]

Get all items in the list.

all_items = file_list.get_items()

Properties

  • len(file_list) - Number of items in the list
  • iter(file_list) - Iterate over items

Messages

FileLinkList.ItemToggled

Posted when an item's toggle state changes.

Attributes:

  • item: Widget - The item that was toggled
  • is_toggled: bool - New toggle state

FileLinkList.ItemRemoved

Posted when an item is removed.

Attributes:

  • item: Widget - The item that was removed

Example

from textual_filelink import FileLinkList, FileLink, CommandLink

class MyApp(App):
    def compose(self) -> ComposeResult:
        file_list = FileLinkList(show_toggles=True, show_remove=True)
        
        # Mix different widget types (all need IDs)
        file_list.add_item(FileLink("test.py", id="test-py"), toggled=True)
        file_list.add_item(FileLink("main.py", id="main-py"))
        file_list.add_item(CommandLink("Build", id="cmd-build"))
        
        yield file_list
    
    def on_mount(self):
        file_list = self.query_one(FileLinkList)
        
        # Batch operations
        file_list.toggle_all(True)
        
        selected = file_list.get_toggled_items()
        self.notify(f"Selected: {len(selected)} items")

FileLinkWithIcons API

FileLinkWithIcons composes a FileLink with customizable icon indicators before and after the filename.

Layout

[icons_before] FileLink [icons_after]

Constructor

FileLinkWithIcons(
    path: Path | str,
    display_name: str | None = None,
    *,
    line: int | None = None,
    column: int | None = None,
    command_builder: Callable | None = None,
    icons_before: list[Icon] | None = None,
    icons_after: list[Icon] | None = None,
    name: str | None = None,
    id: str | None = None,
    classes: str | None = None,
    tooltip: str | None = None,
)

Parameters:

  • path: Full path to the file
  • display_name: Text to display for the link. If None, defaults to filename
  • line: Optional line number to jump to
  • column: Optional column number to jump to
  • command_builder: Function to build the editor command
  • icons_before: Icons to display before the filename (order preserved)
  • icons_after: Icons to display after the filename (order preserved)
  • name: Widget name
  • id: Widget ID
  • classes: CSS classes
  • tooltip: Optional tooltip for the entire widget

Icon Class

Icons are specified using the Icon dataclass:

from textual_filelink import Icon

Icon(
    name: str,           # REQUIRED: Unique identifier
    icon: str,           # REQUIRED: Unicode character
    tooltip: str | None = None,
    clickable: bool = False,
    key: str | None = None,
    visible: bool = True,
)

Icon Properties:

  • name (str, required): Unique identifier for this icon within the widget
  • icon (str, required): Unicode character to display (e.g., "โœ…", "โš™๏ธ", "๐Ÿ”’")
  • tooltip (str | None): Optional tooltip text shown on hover
  • clickable (bool): Whether clicking this icon emits IconClicked events (default: False)
  • key (str | None): Optional keyboard shortcut to trigger this icon (e.g., "1", "s", "ctrl+x")
  • visible (bool): Whether the icon is initially visible (default: True)

Icon Validation:

  • Duplicate icon names raise ValueError
  • Duplicate icon keys raise ValueError
  • Icon keys cannot conflict with FileLink bindings ("o", "enter")

Properties

  • path: Path - The file path
  • line: int | None - The line number
  • column: int | None - The column number
  • file_link: FileLink - The internal FileLink widget (read-only access)

Methods

update_icon(name: str, **kwargs)

Update an icon's properties.

widget.update_icon("status", icon="โœ…", tooltip="Passed")
widget.update_icon("warning", visible=True)
widget.update_icon("lock", clickable=True, key="l")

Updatable properties: icon, tooltip, clickable, visible, key

Raises:

  • ValueError if icon name not found or invalid property provided

set_icon_visible(name: str, visible: bool)

Set icon visibility.

widget.set_icon_visible("warning", True)   # Show
widget.set_icon_visible("warning", False)  # Hide

Raises:

  • ValueError if icon name not found

get_icon(name: str) -> Icon | None

Get icon by name.

icon = widget.get_icon("status")
if icon:
    print(f"Icon: {icon.icon}, Visible: {icon.visible}")

Messages

FileLinkWithIcons.IconClicked

Posted when a clickable icon is clicked.

Attributes:

  • widget: FileLinkWithIcons - The widget containing the clicked icon
  • path: Path - The file path associated with the FileLink
  • icon_name: str - The name identifier of the clicked icon
  • icon_char: str - The unicode character displayed for the icon

Icon Examples

Basic Icons

from textual_filelink import FileLinkWithIcons, Icon

# Icons before filename
link = FileLinkWithIcons(
    "script.py",
    icons_before=[
        Icon(name="type", icon="๐Ÿ", tooltip="Python file"),
        Icon(name="status", icon="โœ…", tooltip="Validated"),
    ]
)
# Display: ๐Ÿ โœ… script.py

# Icons after filename
link = FileLinkWithIcons(
    "script.py",
    icons_after=[
        Icon(name="size", icon="๐Ÿ“Š", tooltip="Large file"),
        Icon(name="sync", icon="โ˜๏ธ", tooltip="Synced"),
    ]
)
# Display: script.py ๐Ÿ“Š โ˜๏ธ

# Mixed positions
link = FileLinkWithIcons(
    "script.py",
    icons_before=[
        Icon(name="type", icon="๐Ÿ"),
    ],
    icons_after=[
        Icon(name="lock", icon="๐Ÿ”’"),
    ]
)
# Display: ๐Ÿ script.py ๐Ÿ”’

Clickable Icons

from textual_filelink import FileLinkWithIcons, Icon

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield FileLinkWithIcons(
            "config.json",
            icons_before=[
                Icon(
                    name="edit",
                    icon="โœ๏ธ",
                    clickable=True,
                    key="e",
                    tooltip="Edit"
                ),
                Icon(
                    name="refresh",
                    icon="๐Ÿ”„",
                    clickable=True,
                    key="r",
                    tooltip="Refresh"
                ),
            ]
        )
    
    def on_file_link_with_icons_icon_clicked(
        self, 
        event: FileLinkWithIcons.IconClicked
    ):
        if event.icon_name == "edit":
            self.notify(f"Editing {event.path.name}")
        elif event.icon_name == "refresh":
            self.notify(f"Refreshing {event.path.name}")

Dynamic Icon Updates

from textual_filelink import FileLinkWithIcons, Icon

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield FileLinkWithIcons(
            "process.py",
            id="task-file",
            icons_before=[
                Icon(name="status", icon="โณ", tooltip="Pending"),
            ],
            icons_after=[
                Icon(name="result", icon="โšช", visible=False),
            ]
        )
    
    def on_mount(self):
        # Simulate processing
        self.set_timer(2.0, self.complete_task)
    
    def complete_task(self):
        widget = self.query_one("#task-file", FileLinkWithIcons)
        widget.update_icon("status", icon="โœ“", tooltip="Complete")
        widget.set_icon_visible("result", True)
        widget.update_icon("result", icon="๐ŸŸข", tooltip="Success")

Hidden Icons

from textual_filelink import FileLinkWithIcons, Icon

# Start with hidden warning icon
link = FileLinkWithIcons(
    "data.csv",
    id="data-file",
    icons_before=[
        Icon(name="type", icon="๐Ÿ“Š"),
        Icon(name="warning", icon="โš ๏ธ", visible=False),  # Hidden initially
    ]
)

# Show warning later
def show_warning():
    widget = self.query_one("#data-file", FileLinkWithIcons)
    widget.set_icon_visible("warning", True)
    widget.update_icon("warning", tooltip="Validation failed!")

Complete Example

from pathlib import Path
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.widgets import Header, Footer, Static
from textual_filelink import FileLinkWithIcons, Icon

class IconFileApp(App):
    CSS = """
    Screen {
        align: center middle;
    }
    Vertical {
        width: 80;
        height: auto;
        border: solid green;
        padding: 1;
    }
    """
    
    def compose(self) -> ComposeResult:
        yield Header()
        
        with Vertical():
            yield Static("๐Ÿ“‚ Project Files with Icons")
            
            # Python file with status
            yield FileLinkWithIcons(
                Path("main.py"),
                line=42,
                icons_before=[
                    Icon(name="type", icon="๐Ÿ", tooltip="Python file"),
                    Icon(name="status", icon="โœ…", tooltip="All checks passed"),
                ],
                icons_after=[
                    Icon(name="coverage", icon="๐Ÿ’ฏ", tooltip="100% coverage"),
                ]
            )
            
            # Config file with clickable edit icon
            yield FileLinkWithIcons(
                Path("config.json"),
                id="config-file",
                icons_before=[
                    Icon(name="type", icon="โš™๏ธ", tooltip="Config file"),
                    Icon(
                        name="edit",
                        icon="โœ๏ธ",
                        clickable=True,
                        key="e",
                        tooltip="Edit config"
                    ),
                ],
                icons_after=[
                    Icon(name="lock", icon="๐Ÿ”’", tooltip="Read-only"),
                ]
            )
            
            # Data file with processing status
            yield FileLinkWithIcons(
                Path("data.csv"),
                id="data-file",
                icons_before=[
                    Icon(name="type", icon="๐Ÿ“Š", tooltip="Data file"),
                    Icon(name="status", icon="โณ", tooltip="Processing..."),
                ],
                icons_after=[
                    Icon(name="result", icon="โšช", visible=False),
                ]
            )
        
        yield Footer()
    
    def on_mount(self):
        # Simulate processing completion after 3 seconds
        self.set_timer(3.0, self.complete_processing)
    
    def complete_processing(self):
        widget = self.query_one("#data-file", FileLinkWithIcons)
        widget.update_icon("status", icon="โœ“", tooltip="Processing complete")
        widget.set_icon_visible("result", True)
        widget.update_icon("result", icon="๐ŸŸข", tooltip="Success")
    
    def on_file_link_with_icons_icon_clicked(
        self, 
        event: FileLinkWithIcons.IconClicked
    ):
        if event.icon_name == "edit":
            self.notify(f"โœ๏ธ Editing {event.path.name}")
            # You could open an editor, show a modal, etc.

if __name__ == "__main__":
    IconFileApp().run()

Custom Editor Commands

Using Built-in Command Builders

from textual_filelink import FileLink

# Set default for all FileLink instances
FileLink.default_command_builder = FileLink.vim_command

# Or per instance
link = FileLink(path, command_builder=FileLink.nano_command)

Available builders:

  • FileLink.vscode_command - VSCode (default)
  • FileLink.vim_command - Vim
  • FileLink.nano_command - Nano
  • FileLink.eclipse_command - Eclipse
  • FileLink.copy_path_command - Copy path to clipboard

Custom Command Builder

def my_editor_command(path: Path, line: int | None, column: int | None) -> list[str]:
    """Build command for my custom editor."""
    cmd = ["myeditor"]
    if line:
        cmd.extend(["--line", str(line)])
    if column:
        cmd.extend(["--column", str(column)])
    cmd.append(str(path))
    return cmd

link = FileLink(path, command_builder=my_editor_command)

Common Unicode Icons

# Status indicators
"โœ“"  # Success/Complete
"โš "  # Warning
"โœ—"  # Error/Failed
"โณ"  # In progress
"๐Ÿ”’"  # Locked
"๐Ÿ“"  # Modified
"โž•"  # New/Added
"โž–"  # Deleted
"๐Ÿ”„"  # Syncing

# File types
"๐Ÿ“„"  # Document
"๐Ÿ“"  # Folder
"๐Ÿ"  # Python file
"๐Ÿ“Š"  # Data file
"โš™๏ธ"  # Config file
"๐ŸŒ"  # Web file
"๐ŸŽจ"  # Image file
"๐Ÿ“ฆ"  # Package/Archive

# Actions
"โœ๏ธ"  # Edit
"๐Ÿ‘๏ธ"  # View
"๐Ÿ—‘๏ธ"  # Delete
"๐Ÿ’พ"  # Save
"๐Ÿ“‹"  # Copy
"๐Ÿ”"  # Search

# States
"๐ŸŸข"  # Success/Green
"๐ŸŸก"  # Warning/Yellow
"๐Ÿ”ด"  # Error/Red
"โšช"  # Neutral/White
"๐ŸŸฃ"  # Info/Purple
"โšซ"  # Disabled/Black
"๐Ÿ’ฏ"  # Perfect score
"โ—‹"   # Empty/Not started

Utility Functions

sanitize_id

from textual_filelink import sanitize_id

# Convert name to valid widget ID
widget_id = sanitize_id("Run Tests")  # Returns: "run-tests"
widget_id = sanitize_id("src/main.py")  # Returns: "src-main-py"
widget_id = sanitize_id("Build Project!")  # Returns: "build-project-"

Description: Converts a name to a valid Textual widget ID by:

  • Converting to lowercase
  • Replacing spaces and path separators with hyphens
  • Keeping only alphanumeric characters, hyphens, and underscores

Development

# Clone the repository
git clone https://github.com/eyecantell/textual-filelink.git
cd textual-filelink

# Install with dev dependencies
pdm install -d

# Run tests
pdm run pytest

# Run tests with coverage
pdm run pytest --cov

# Lint
pdm run ruff check .

# Format
pdm run ruff format .

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Built with Textual by Textualize
  • Inspired by the need for better file navigation in terminal applications

Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

textual_filelink-0.4.0.tar.gz (62.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

textual_filelink-0.4.0-py3-none-any.whl (35.5 kB view details)

Uploaded Python 3

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page