Clickable file links for Textual applications.
Project description
textual-filelink
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
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_clicked(self, event: FileLink.Clicked):
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 ot start again, command name ("Build"), settings icon, remove icon โ โถ๏ธ Build โ๏ธ ร - last run failed, play button to start agai1n, command name ("Build"), settings icon, remove icon โ ง โน๏ธ Build โ๏ธ ร - spinner, stop button to cancel run, command name ("Build"), settings icon, remove icon
from textual_filelink import CommandLink
class MyApp(App):
def compose(self) -> ComposeResult:
yield CommandLink(
"Run Tests",
initial_status_icon="โ",
initial_status_tooltip="Not run yet",
)
def on_command_link_play_clicked(self, event: CommandLink.PlayClicked):
link = self.query_one(f"#{event.name}", CommandLink)
link.set_status(running=True, tooltip="Running...")
# Start your command here
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")
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:
o- Open file in editor
ToggleableFileLink:
o- Open file in editorSpaceort- Toggle checkboxDeleteorx- Remove widget1-9- Activate clickable icons (in order of appearance)
CommandLink:
o- Open output file (if path is set)Spaceorp- Play/Stop commands- Settingst- Toggle checkboxDeleteorx- Remove widget
Customizing Keyboard Shortcuts
Override the BINDINGS class variable in a subclass to customize keyboard shortcuts:
from textual.binding import Binding
from textual_filelink import FileLink
class MyFileLink(FileLink):
BINDINGS = [
Binding("enter", "open_file", "Open"), # Use Enter instead of 'o'
Binding("ctrl+o", "open_file", "Open"), # Add Ctrl+O
]
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") # Press 1 to toggle play/stop
yield CommandLink("Test") # Press 2 to toggle play/stop
yield CommandLink("Deploy") # Press 3 to toggle play/stop
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()
How it works:
- Number keys work globally - no need to Tab to the widget first
- Pressing '1' toggles the first command between play and stop
- Pressing '2' toggles the second command, etc.
- Uses the widget's
action_play_stop()method which handles state checking internally (checks if running and posts appropriate message)
Alternative approaches:
If you need more control over the behavior, you can manually post messages:
def on_key(self, event: events.Key) -> None:
if event.key.isdigit():
num = int(event.key)
commands = list(self.query(CommandLink))
if 0 < num <= len(commands):
cmd = commands[num - 1]
# Option 1: Always play (ignore if already running)
cmd.post_message(CommandLink.PlayClicked(
cmd.output_path, cmd.name, cmd.output_path, cmd.is_toggled
))
# Option 2: Always stop (ignore if not running)
cmd.post_message(CommandLink.StopClicked(
cmd.output_path, cmd.name, cmd.output_path, cmd.is_toggled
))
# Option 3: Custom logic based on state
if cmd.is_running:
cmd.post_message(CommandLink.StopClicked(
cmd.output_path, cmd.name, cmd.output_path, cmd.is_toggled
))
else:
cmd.post_message(CommandLink.PlayClicked(
cmd.output_path, cmd.name, cmd.output_path, cmd.is_toggled
))
event.prevent_default()
For ToggleableFileLink:
The same pattern works for other widget actions:
from textual_filelink import ToggleableFileLink
class MyApp(App):
def on_key(self, event: events.Key) -> None:
if event.key.isdigit():
num = int(event.key)
links = list(self.query(ToggleableFileLink))
if 0 < num <= len(links):
link = links[num - 1]
# Open the file
link.action_open_file()
# Or toggle checkbox
link.action_toggle()
# Or remove
link.action_remove()
event.prevent_default()
Complete Example
class KeyboardAccessibleApp(App):
def compose(self) -> ComposeResult:
yield FileLink("file1.py", name="link1")
yield FileLink("file2.py", name="link2")
yield ToggleableFileLink("file3.py", name="link3")
yield CommandLink("Run Tests", name="cmd1")
if __name__ == "__main__":
# Now fully keyboard accessible!
# Tab to navigate, o/space/p/etc to activate
KeyboardAccessibleApp().run()
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 (p/space)"
- Settings: "Settings (s)"
- Clickable icon 1: "Status (1)"
How it works: Tooltips are automatically enhanced with keyboard shortcuts based on the widget's BINDINGS. When you hover over or focus on an interactive element, the tooltip displays both the description and the available keyboard shortcuts.
Custom Bindings:
If you override BINDINGS in a subclass, tooltips will automatically reflect your custom keys:
class CustomFileLink(FileLink):
BINDINGS = [
Binding("enter", "open_file", "Open"),
]
# Tooltip will show "(enter)" instead of "(o)"
link = CustomFileLink("file.txt")
FileLink API
Constructor
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 fileline: Optional line number to jump tocolumn: Optional column number to jump tocommand_builder: Custom function to build the editor command
Properties
path: Path- The file pathline: int | None- The line numbercolumn: int | None- The column number
Messages
FileLink.Clicked
Posted when the link is clicked.
Attributes:
path: Pathline: int | Nonecolumn: int | None
ToggleableFileLink API
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,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
)
Parameters:
path: Full path to the fileinitial_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 tocolumn: Optional column number to jump tocommand_builder: Custom function to build the editor commanddisable_on_untoggle: If True, dim/disable the link when untoggledtoggle_tooltip: Optional tooltip text for the toggle buttonremove_tooltip: Optional tooltip text for the remove button
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 iconicon(str, required): Unicode character to displayposition(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 postsIconClickedmessages (default: False)tooltip(str | None): Tooltip text (default: None)
Properties
path: Path- The file pathis_toggled: bool- Current toggle stateicons: list[dict]- List of all icon configurationsfile_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.
Messages
ToggleableFileLink.Toggled
Posted when the toggle state changes.
Attributes:
path: Pathis_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: Pathicon_name: str- The name of the clicked iconicon: str- The unicode character of the icon
CommandLink API
CommandLink is a specialized widget for command orchestration and status display, extending ToggleableFileLink. It's designed for single-instance commands (not multiple concurrent runs of the same command).
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_toggle=True,
show_settings=True,
show_remove=True,
)
def on_command_link_play_clicked(self, event: CommandLink.PlayClicked):
# Event provides full context: name, path, output_path, is_toggled
link = self.query_one(f"#{event.name}", 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):
# Event provides full context including toggle state
self.notify(f"Stopping {event.name}")
def on_command_link_settings_clicked(self, event: CommandLink.SettingsClicked):
# Event provides full context for configuration
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")
link.set_output_path(Path("test_output.log"), tooltip="Click to view results")
Constructor
CommandLink(
name: str,
output_path: Path | str | None = None,
*,
initial_toggle: bool = False,
initial_status_icon: str = "โ",
initial_status_tooltip: str | None = None,
running: bool = False,
show_toggle: bool = True,
show_settings: bool = True,
show_remove: bool = True,
toggle_tooltip: str | None = None,
settings_tooltip: str | None = None,
remove_tooltip: str | None = None,
command_builder: Callable | None = None,
disable_on_untoggle: bool = False,
)
Parameters:
name: Command display name (also used as widget ID)output_path: Path to output file. If None, clicking command name does nothinginitial_toggle: Whether the command starts toggled/selectedinitial_status_icon: Initial status icon (default: "โ")initial_status_tooltip: Initial tooltip for status iconrunning: Whether command is currently running. If True, shows spinner and stop buttonshow_toggle: Whether to show the toggle checkboxshow_settings: Whether to show the settings iconshow_remove: Whether to show the remove buttontoggle_tooltip: Tooltip for toggle checkboxsettings_tooltip: Tooltip for settings iconremove_tooltip: Tooltip for remove buttoncommand_builder: Custom command builder for opening output filesdisable_on_untoggle: If True, dim/disable when untoggled
Layout
[toggle] [status/spinner] [play/stop] command_name [settings] [remove]
- toggle: Checkbox for selecting commands (inherited from ToggleableFileLink)
- status/spinner: Shows status icon, or animated spinner when running
- play/stop: โถ when stopped, โน when running
- command_name: Clickable link to output file (if set)
- settings: โ icon for configuration
- remove: ร button (inherited from ToggleableFileLink)
Properties
name: str- The command name (alias for display_name)display_name: str- The command display name (e.g., "Test", "Build")output_path: Path | None- Current output file pathpath: Path | None- The output file path (returns the actual output path, not a display path)is_running: bool- Whether the command is currently runningis_toggled: bool- Current toggle state (inherited)
Methods
set_status(icon: str | None = None, tooltip: str | None = None, running: bool | 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(path: Path | str | None, tooltip: str | None = None)
Update the output file path.
link.set_output_path(Path("output.log"), tooltip="Click to view output")
link.set_output_path(None) # Clear output path
set_toggle(toggled: bool, tooltip: str | None = None)
Update toggle state programmatically.
link.set_toggle(True, tooltip="Selected for batch run")
link.set_toggle(False)
set_settings_tooltip(tooltip: str | None)
Update settings icon tooltip.
link.set_settings_tooltip("Configure test options")
Messages
CommandLink.PlayClicked
Posted when play button (โถ) is clicked.
Attributes:
name: str- The command namepath: Path | None- The output file path (or None if not set)output_path: Path | None- The output file path (same as path)is_toggled: bool- Whether the command is selected for batch run
CommandLink.StopClicked
Posted when stop button (โน) is clicked.
Attributes:
name: str- The command namepath: Path | None- The output file path (or None if not set)output_path: Path | None- The output file path (same as path)is_toggled: bool- Whether the command is selected for batch run
CommandLink.SettingsClicked
Posted when settings icon (โ) is clicked.
Attributes:
name: str- The command namepath: Path | None- The output file path (or None if not set)output_path: Path | None- The output file path (same as path)is_toggled: bool- Whether the command is selected for batch run
Inherited Messages:
ToggleableFileLink.Toggled- When toggle state changesToggleableFileLink.Removed- When remove button is clicked
Status Icons
Common status icons for commands:
"โ" # Not run / Unknown
"โ
" # Success / Passed
"โ" # Failed / Error
"โ ๏ธ" # Warning
"โญ๏ธ" # Skipped
"๐" # Needs rerun
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",
settings_tooltip="Configure test options",
)
yield CommandLink(
"Lint",
initial_status_icon="โ",
initial_status_tooltip="Not run",
show_settings=False,
)
yield CommandLink(
"Build",
initial_status_icon="โ",
initial_status_tooltip="Not run",
settings_tooltip="Build configuration",
)
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()
Using Enriched Message Properties
As of version 0.2.0, CommandLink messages include enriched context that eliminates the need to query widgets in event handlers:
class SmartCommandApp(App):
def compose(self) -> ComposeResult:
yield CommandLink("Tests", initial_status_icon="โ", show_toggle=True)
yield CommandLink("Build", initial_status_icon="โ", initial_toggle=True)
def on_command_link_play_clicked(self, event: CommandLink.PlayClicked):
"""Event provides full context about the command."""
# Instead of querying: link = self.query_one(f"#{event.name}", CommandLink)
# Just use the event properties:
self.log(f"Playing {event.name}")
self.log(f"Output path: {event.path}") # Path to output file
self.log(f"Is toggled: {event.is_toggled}") # Selected for batch run?
# You still need to query for widget methods (like set_status),
# but now you have context directly from the message
link = self.query_one(f"#{event.name}", CommandLink)
link.set_status(running=True, tooltip="Running...")
def on_command_link_stop_clicked(self, event: CommandLink.StopClicked):
"""Stop button includes state context."""
# All message types (PlayClicked, StopClicked, SettingsClicked)
# include: name, path, output_path, is_toggled
self.notify(f"Stopping {event.name} (toggled={event.is_toggled})")
def on_command_link_settings_clicked(self, event: CommandLink.SettingsClicked):
"""Settings event has full context."""
# Can now make decisions based on command state without querying
if event.is_toggled:
self.notify(f"Settings for {event.name} (part of batch run)")
else:
self.notify(f"Settings for {event.name} (standalone)")
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- VimFileLink.nano_command- NanoFileLink.eclipse_command- EclipseFileLink.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)
Icon Examples
Icon Positioning
# Icons before filename
ToggleableFileLink(
path,
icons=[
{"name": "type", "icon": "๐", "position": "before"},
{"name": "status", "icon": "โ", "position": "before"},
]
)
# Display: ๐ โ script.py
# Icons after filename
ToggleableFileLink(
path,
icons=[
{"name": "size", "icon": "๐", "position": "after"},
{"name": "sync", "icon": "โ๏ธ", "position": "after"},
]
)
# Display: script.py ๐ โ๏ธ
# Mixed positions
ToggleableFileLink(
path,
icons=[
{"name": "type", "icon": "๐", "position": "before"},
{"name": "sync", "icon": "โ๏ธ", "position": "after"},
]
)
# Display: ๐ script.py โ๏ธ
Icon Ordering
# Explicit ordering with index
ToggleableFileLink(
path,
icons=[
{"name": "third", "icon": "3๏ธโฃ", "index": 3},
{"name": "first", "icon": "1๏ธโฃ", "index": 1},
{"name": "second", "icon": "2๏ธโฃ", "index": 2},
]
)
# Display: 1๏ธโฃ 2๏ธโฃ 3๏ธโฃ filename.py
# Auto ordering (maintains list order)
ToggleableFileLink(
path,
icons=[
{"name": "first", "icon": "A"},
{"name": "second", "icon": "B"},
{"name": "third", "icon": "C"},
]
)
# Display: A B C filename.py
Dynamic Icon Updates
class MyApp(App):
def compose(self) -> ComposeResult:
yield ToggleableFileLink(
"process.py",
id="task-file",
icons=[
{"name": "status", "icon": "โณ", "tooltip": "Pending"},
{"name": "result", "icon": "โช", "visible": False},
]
)
def on_mount(self):
# Simulate processing
self.set_timer(2.0, self.complete_task)
def complete_task(self):
link = self.query_one("#task-file", ToggleableFileLink)
link.update_icon("status", icon="โ", tooltip="Complete")
link.set_icon_visible("result", True)
link.update_icon("result", icon="๐ข", tooltip="Success")
Clickable Icons
class MyApp(App):
def compose(self) -> ComposeResult:
yield ToggleableFileLink(
path,
icons=[
{"name": "edit", "icon": "โ๏ธ", "clickable": True, "tooltip": "Edit settings"},
{"name": "refresh", "icon": "๐", "clickable": True, "tooltip": "Refresh"},
{"name": "info", "icon": "โน๏ธ", "clickable": True, "tooltip": "Show info"},
]
)
def on_toggleable_file_link_icon_clicked(self, event: ToggleableFileLink.IconClicked):
if event.icon_name == "edit":
self.edit_file(event.path)
elif event.icon_name == "refresh":
self.refresh_file(event.path)
elif event.icon_name == "info":
self.show_info(event.path)
Layout Configurations
Toggle Only
ToggleableFileLink(path, show_toggle=True, show_remove=False)
Display: โ filename.txt
Remove Only
ToggleableFileLink(path, show_toggle=False, show_remove=True)
Display: filename.txt ร
Both Controls
ToggleableFileLink(path, show_toggle=True, show_remove=True)
Display: โ filename.txt ร
Plain Link with Icons
ToggleableFileLink(
path,
show_toggle=False,
show_remove=False,
icons=[{"name": "type", "icon": "๐"}]
)
Display: ๐ filename.txt
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
# Actions
"โ๏ธ" # Edit
"๐๏ธ" # View
"๐๏ธ" # Delete
"๐พ" # Save
"๐" # Copy
# States
"๐ข" # Success/Green
"๐ก" # Warning/Yellow
"๐ด" # Error/Red
"โช" # Neutral/White
"๐ฃ" # Info/Purple
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 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 compose(self) -> ComposeResult:
yield Header()
with Vertical():
yield Static("๐ Project Files")
# Validated file with multiple icons
yield ToggleableFileLink(
Path("main.py"),
initial_toggle=True,
icons=[
{"name": "status", "icon": "โ", "tooltip": "Validated", "clickable": True},
{"name": "type", "icon": "๐", "tooltip": "Python file"},
{"name": "lock", "icon": "๐", "position": "after", "tooltip": "Read-only"},
]
)
# File needing review
yield ToggleableFileLink(
Path("config.json"),
icons=[
{"name": "status", "icon": "โ ", "tooltip": "Needs review", "clickable": True},
{"name": "type", "icon": "โ๏ธ", "tooltip": "Config file"},
]
)
# File being processed
yield ToggleableFileLink(
Path("data.csv"),
id="processing-file",
icons=[
{"name": "status", "icon": "โณ", "tooltip": "Processing...", "clickable": True},
{"name": "type", "icon": "๐", "tooltip": "Data file"},
{"name": "result", "icon": "โช", "visible": False, "position": "after"},
]
)
yield Footer()
def on_toggleable_file_link_toggled(self, event: ToggleableFileLink.Toggled):
state = "selected" if event.is_toggled else "deselected"
self.notify(f"๐ {event.path.name} {state}")
def on_toggleable_file_link_removed(self, event: ToggleableFileLink.Removed):
# 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_icon_clicked(self, event: ToggleableFileLink.IconClicked):
# Find the link by path
link = None
for child in self.query(ToggleableFileLink):
if child.path == event.path:
link = child
break
if not link:
return
if event.icon_name == "status":
# Toggle processing status
if event.icon == "โณ":
# Complete processing
link.update_icon("status", icon="โ", tooltip="Complete")
# Only update result icon if it exists (for data.csv)
if link.get_icon("result"):
link.set_icon_visible("result", True)
link.update_icon("result", icon="๐ข", tooltip="Success")
self.notify(f"โ
{event.path.name} processing complete")
else:
# Start processing
link.update_icon("status", icon="โณ", tooltip="Processing...")
# Only hide result icon if it exists (for data.csv)
if link.get_icon("result"):
link.set_icon_visible("result", False)
self.notify(f"โณ Processing {event.path.name}...")
if __name__ == "__main__":
FileManagerApp().run()
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.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - 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
- PyPI: https://pypi.org/project/textual-filelink/
- GitHub: https://github.com/eyecantell/textual-filelink
- Issues: https://github.com/eyecantell/textual-filelink/issues
- Changelog: https://github.com/eyecantell/textual-filelink/blob/main/CHANGELOG.md
- Textual Documentation: https://textual.textualize.io/
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file textual_filelink-0.3.0.tar.gz.
File metadata
- Download URL: textual_filelink-0.3.0.tar.gz
- Upload date:
- Size: 48.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: pdm/2.26.2 CPython/3.10.16 Linux/5.15.167.4-microsoft-standard-WSL2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67a6e2fa68b8fea19750af096fd875f7532dc220958ecd217f23b21442f7ff0e
|
|
| MD5 |
2497f8a2e0f3001764bb9d0a3859fb8c
|
|
| BLAKE2b-256 |
8b59fbd11414c9c03b2a70aa27ec11ff4a3140a250fea763c0d4d39a8d7f73fe
|
File details
Details for the file textual_filelink-0.3.0-py3-none-any.whl.
File metadata
- Download URL: textual_filelink-0.3.0-py3-none-any.whl
- Upload date:
- Size: 26.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: pdm/2.26.2 CPython/3.10.16 Linux/5.15.167.4-microsoft-standard-WSL2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b4095d1965eb4c5e4befcfaa4d3df638b3530870a366a3ec83ff5e9698810e84
|
|
| MD5 |
43391c574c634041bd646d7fb5aefb7c
|
|
| BLAKE2b-256 |
39e8a5fa117bf8bd30e18304f7853e1483f07b6f1e1ea58add8a1bc62a39e246
|