Metadata-Version: 2.1
Name: textual-filelink
Version: 0.2.0
Summary: Clickable file links for Textual applications.
Keywords: textual,tui,terminal,file,link,widget,clickable,file-manager,developer-tools
Author-Email: eyecantell <paul@pneuma.solutions>
License: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Terminals
Classifier: Topic :: Software Development :: User Interfaces
Project-URL: Homepage, https://github.com/eyecantell/textual-filelink
Project-URL: Repository, https://github.com/eyecantell/textual-filelink
Project-URL: Issues, https://github.com/eyecantell/textual-filelink/issues
Project-URL: Documentation, https://github.com/eyecantell/textual-filelink#readme
Project-URL: Changelog, https://github.com/eyecantell/textual-filelink/releases
Requires-Python: >=3.9
Requires-Dist: textual>=6.6.0
Description-Content-Type: text/markdown

# textual-filelink
[![CI](https://github.com/eyecantell/textual-filelink/actions/workflows/ci.yml/badge.svg)](https://github.com/eyecantell/textual-filelink/actions/runs/19725973735)
[![PyPI](https://img.shields.io/pypi/v/textual-filelink.svg)](https://pypi.org/project/textual-filelink/)
[![Python Versions](https://img.shields.io/pypi/pyversions/textual-filelink.svg)](https://pypi.org/project/textual-filelink/)
[![Downloads](https://pepy.tech/badge/textual-filelink)](https://pepy.tech/project/textual-filelink)
[![License](https://img.shields.io/pypi/l/textual-filelink.svg)](https://github.com/eyecantell/textual-filelink/blob/main/LICENSE)

Clickable file links for [Textual](https://github.com/Textualize/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
- 🎨 **Status icons** with unicode support for visual feedback (optionally clickable)
- 🎯 **Jump to specific line and column** in your editor
- 🔧 **Customizable command builders** for any editor
- 🎭 **Flexible layouts** - show/hide controls as needed
- 💬 **Tooltips** for toggle, status icon, and remove button

## Installation

```bash
pip install textual-filelink
```

Or with PDM:

```bash
pdm add textual-filelink
```

## Quick Start

### Basic FileLink

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

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

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

### ToggleableFileLink with Status Icons

```python
from textual_filelink import ToggleableFileLink

class MyApp(App):
    def compose(self) -> ComposeResult:
        yield ToggleableFileLink(
            Path("script.py"),
            initial_toggle=True,
            show_toggle=True,
            show_remove=True,
            status_icon="✓",
            status_icon_clickable=True,
            toggle_tooltip="Toggle selection",
            status_tooltip="File status",
            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_status_icon_clicked(self, event: ToggleableFileLink.StatusIconClicked):
        self.notify(f"Status icon '{event.icon}' clicked for {event.path.name}")

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

## FileLink API

### Constructor

```python
FileLink(
    path: Path | str,
    *,
    line: int | None = None,
    column: int | None = None,
    command_builder: Callable | None = None,
    name: str | None = None,
    id: str | None = None,
    classes: str | None = None,
)
```

**Parameters:**
- `path`: Full path to the file
- `line`: Optional line number to jump to
- `column`: Optional column number to jump to
- `command_builder`: Custom function to build the editor command

### Properties

- `path: Path` - The file path
- `line: int | None` - The line number
- `column: int | None` - The column number

### Messages

#### `FileLink.Clicked`
Posted when the link is clicked.

**Attributes:**
- `path: Path`
- `line: int | None`
- `column: int | None`

## ToggleableFileLink API

### Constructor

```python
ToggleableFileLink(
    path: Path | str,
    *,
    initial_toggle: bool = False,
    show_toggle: bool = True,
    show_remove: bool = True,
    status_icon: str | None = None,
    status_icon_clickable: bool = False,
    line: int | None = None,
    column: int | None = None,
    command_builder: Callable | None = None,
    disable_on_untoggle: bool = False,
    toggle_tooltip: str | None = None,
    status_tooltip: str | None = None,
    remove_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 (×)
- `status_icon`: Optional unicode icon to display before the filename
- `status_icon_clickable`: Whether the status icon is clickable (posts `StatusIconClicked` message)
- `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
- `status_tooltip`: Optional tooltip text for the status icon
- `remove_tooltip`: Optional tooltip text for the remove button

### Properties

- `path: Path` - The file path
- `is_toggled: bool` - Current toggle state
- `status_icon: str | None` - Current status icon

### Methods

#### `set_status_icon(icon: str | None, tooltip: str | None = None)`
Update the status icon and optionally its tooltip.

```python
link.set_status_icon("⚠", tooltip="Warning: File modified")  # Warning
link.set_status_icon("✓", tooltip="Validated")  # Success
link.set_status_icon("⏳", tooltip="Processing...")  # In progress
link.set_status_icon(None) # Hide icon
```

#### `set_toggle_tooltip(tooltip: str | None)`
Update the toggle button tooltip.

#### `set_remove_tooltip(tooltip: str | None)`
Update the remove button 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.StatusIconClicked`
Posted when the status icon is clicked (if `status_icon_clickable=True`).

**Attributes:**
- `path: Path`
- `icon: str`

## Custom Editor Commands

### Using Built-in Command Builders

```python
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

```python
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)
```

## Layout Configurations

### Toggle Only
```python
ToggleableFileLink(path, show_toggle=True, show_remove=False)
```
Display: `☐ filename.txt`

### Remove Only
```python
ToggleableFileLink(path, show_toggle=False, show_remove=True)
```
Display: `filename.txt ×`

### Both Controls
```python
ToggleableFileLink(path, show_toggle=True, show_remove=True)
```
Display: `☐ filename.txt ×`

### Neither (Plain Link with Status)
```python
ToggleableFileLink(path, show_toggle=False, show_remove=False, status_icon="📄")
```
Display: `📄 filename.txt`

## Status Icons

Common unicode icons you can use:

```python
# 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
```

## Complete Example

```python
from pathlib import Path
from textual.app import App, ComposeResult
from textual.containers import Vertical, Horizontal
from textual.widgets import Header, Footer, Static
from textual_filelink import ToggleableFileLink

class FileManagerApp(App):
    CSS = """
    Screen {
        align: center middle;
    }
    Vertical {
        width: 80;
        height: auto;
        border: solid green;
        padding: 1;
    }
    Static {
        width: 100%;
        content-align: center middle;
        text-style: bold;
    }
    """
    
    def __init__(self):
        super().__init__()
        self.selected_files = set()
    
    def compose(self) -> ComposeResult:
        yield Header()
        
        with Vertical():
            yield Static("📁 Project Files")
            
            files = [
                ("main.py", "✓", True, "Validated", True),
                ("config.json", "⚠", False, "Needs review", True),
                ("README.md", "📝", False, "Draft", False),
                ("tests.py", "⏳", False, "Running tests", False),
            ]
            
            for filename, icon, toggled, tooltip, clickable in files:
                yield ToggleableFileLink(
                    Path(filename),
                    initial_toggle=toggled,
                    show_toggle=True,
                    show_remove=True,
                    status_icon=icon,
                    status_icon_clickable=clickable,
                    toggle_tooltip="Toggle selection",
                    status_tooltip=tooltip,
                    remove_tooltip="Remove file",
                    line=1,
                )
        
        yield Footer()
    
    def on_toggleable_file_link_toggled(self, event: ToggleableFileLink.Toggled):
        if event.is_toggled:
            self.selected_files.add(event.path)
            self.notify(f"✓ Selected {event.path.name}")
        else:
            self.selected_files.discard(event.path)
            self.notify(f"☐ Deselected {event.path.name}")
    
    def on_toggleable_file_link_removed(self, event: ToggleableFileLink.Removed):
        self.selected_files.discard(event.path)
        # Find and remove the widget
        for child in self.query(ToggleableFileLink):
            if child.path == event.path:
                child.remove()
        self.notify(f"❌ Removed {event.path.name}", severity="warning")
    
    def on_toggleable_file_link_status_icon_clicked(self, event: ToggleableFileLink.StatusIconClicked):
        self.notify(f"Clicked status icon '{event.icon}' for {event.path.name}")
    
    def on_file_link_clicked(self, event):
        self.notify(f"Opening {event.path.name}...")

if __name__ == "__main__":
    FileManagerApp().run()
```

## Development

```bash
# 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](LICENSE) file for details.

## Acknowledgments

- Built with [Textual](https://github.com/Textualize/textual) by Textualize
- Inspired by the need for better file navigation in terminal applications

## Links

- **PyPI**: https://pypi.org/project/textual-filelink/
- **GitHub**: https://github.com/eyecantell/textual-filelink
- **Issues**: https://github.com/eyecantell/textual-filelink/issues
- **Textual Documentation**: https://textual.textualize.io/