Skip to content

display

appimage_updater.ui.display

Display and formatting functions for the AppImage Updater CLI.

This module contains all the functions responsible for formatting and displaying information to the user via the console, including tables, panels, file information, and symlink details.

console = Console(no_color=(bool(os.environ.get('NO_COLOR')))) module-attribute

add_checksum_config_lines(app, config_lines)

Add checksum configuration lines if applicable.

Source code in src/appimage_updater/ui/display.py
def add_checksum_config_lines(app: Any, config_lines: list[str]) -> None:
    """Add checksum configuration lines if applicable."""
    if not _has_checksum_config(app):
        return
    _add_checksum_status_line(app, config_lines)
    if app.checksum.enabled:
        _add_checksum_details(app, config_lines)

add_optional_config_lines(app, config_lines)

Add optional configuration lines (prerelease, symlink_path).

Source code in src/appimage_updater/ui/display.py
def add_optional_config_lines(app: Any, config_lines: list[str]) -> None:
    """Add optional configuration lines (prerelease, symlink_path)."""
    if hasattr(app, "prerelease"):
        config_lines.append(f"[bold]Prerelease:[/bold] {'Yes' if app.prerelease else 'No'}")

    if hasattr(app, "symlink_path") and app.symlink_path:
        display_symlink = _replace_home_with_tilde(str(app.symlink_path))
        config_lines.append(f"[bold]Symlink Path:[/bold] {display_symlink}")

add_rotation_config_lines(app, config_lines)

Add file rotation configuration lines if applicable.

Source code in src/appimage_updater/ui/display.py
def add_rotation_config_lines(app: Any, config_lines: list[str]) -> None:
    """Add file rotation configuration lines if applicable."""
    if hasattr(app, "rotation_enabled"):
        _add_rotation_status_line(app, config_lines)
        if app.rotation_enabled:
            _add_retain_count_line(app, config_lines)
            _add_managed_symlink_line(app, config_lines)

Check if the configured symlink exists and points to an AppImage in the download directory.

Source code in src/appimage_updater/ui/display.py
def check_configured_symlink(symlink_path: Path, download_dir: Path) -> tuple[Path, Path] | None:
    """Check if the configured symlink exists and points to an AppImage in the download directory."""
    if not _is_valid_symlink(symlink_path):
        return None

    try:
        target = symlink_path.resolve()
        # Check if target is in download directory and is an AppImage
        if _is_valid_appimage_target(target, download_dir):
            return (symlink_path, target)
        # If we get here, symlink doesn't point to expected location
        logger.debug(f"Symlink {symlink_path} points to {target}, not an AppImage in download directory")
    except (OSError, RuntimeError) as e:
        logger.debug(f"Failed to resolve configured symlink {symlink_path}: {e}")

    return None

display_application_details(app, config_source_info=None)

Display detailed information about a specific application.

Source code in src/appimage_updater/ui/display.py
def display_application_details(app: Any, config_source_info: dict[str, str] | None = None) -> None:
    """Display detailed information about a specific application."""
    output_formatter = get_output_formatter()

    if output_formatter and not hasattr(output_formatter, "console"):
        # Only use structured format for non-Rich formatters (JSON, Plain, HTML)
        # Rich formatter should use the original Rich panel display
        app_details = {
            "name": app.name,
            "enabled": getattr(app, "enabled", True),
            "url": getattr(app, "url", ""),
            "download_dir": str(getattr(app, "download_dir", "")),
            "source_type": getattr(app, "source_type", ""),
            "pattern": getattr(app, "pattern", ""),
            "config_source": config_source_info or {},
        }

        # Add basic file and symlink information
        app_details["files"] = {"status": "File information available in Rich format"}
        app_details["symlinks"] = {"status": "Symlink information available in Rich format"}

        # Use a generic method for application details (we can add this to the interface later)
        if hasattr(output_formatter, "print_application_details"):
            output_formatter.print_application_details(app_details)
        else:
            # Fallback to table format
            output_formatter.print_table([app_details], title=f"Application Details: {app.name}")
    else:
        # Fallback to Rich panel display
        console.print(f"\n[bold cyan]Application: {app.name}[/bold cyan]")
        console.print("=" * (len(app.name) + 14))

        # Configuration section
        config_info = get_configuration_info(app, config_source_info)
        config_panel = Panel(config_info, title="Configuration", border_style="blue")

        # Files section
        files_info = get_files_info(app)
        files_panel = Panel(files_info, title="Files", border_style="green")

        # Symlinks section
        symlinks_info = get_symlinks_info(app)
        symlinks_panel = Panel(symlinks_info, title="Symlinks", border_style="yellow")

        console.print(config_panel)
        console.print(files_panel)
        console.print(symlinks_panel)

display_applications_list(applications)

Display applications list in a table.

Source code in src/appimage_updater/ui/display.py
def display_applications_list(applications: list[Any]) -> None:
    """Display applications list in a table."""
    output_formatter = get_output_formatter()

    if output_formatter:
        _display_applications_with_formatter(applications, output_formatter)
    else:
        _display_applications_with_rich_table(applications)

display_check_results(results, show_urls=False)

Display check results in a table.

Source code in src/appimage_updater/ui/display.py
def display_check_results(results: list[CheckResult], show_urls: bool = False) -> None:
    """Display check results in a table."""
    table = _create_results_table(show_urls)

    for result in results:
        row = _create_result_row(result, show_urls)
        table.add_row(*row)

    console.print(table)

    if show_urls:
        _display_url_table(results)

display_download_results(results)

Display download results.

Source code in src/appimage_updater/ui/display.py
def display_download_results(results: list[Any]) -> None:
    """Display download results."""
    successful = [r for r in results if r.success]
    failed = [r for r in results if not r.success]

    display_successful_downloads(successful)
    display_failed_downloads(failed)

display_edit_summary(app_name, changes)

Display a summary of changes made during edit operation.

Source code in src/appimage_updater/ui/display.py
def display_edit_summary(app_name: str, changes: list[str]) -> None:
    """Display a summary of changes made during edit operation."""
    console.print(f"\n[green]Successfully updated configuration for '{app_name}'[/green]")
    console.print("[blue]Changes made:[/blue]")
    for change in changes:
        console.print(f"  • {change}")

display_failed_downloads(failed)

Display failed download results.

Source code in src/appimage_updater/ui/display.py
def display_failed_downloads(failed: list[Any]) -> None:
    """Display failed download results."""
    if not failed:
        return

    console.print(f"\n[red]Failed to download {len(failed)} updates:")
    for result in failed:
        console.print(f"  Failed: {result.app_name}: {result.error_message}")

display_successful_downloads(successful)

Display successful download results.

Source code in src/appimage_updater/ui/display.py
def display_successful_downloads(successful: list[Any]) -> None:
    """Display successful download results."""
    if not successful:
        return

    console.print(f"\n[green]Successfully downloaded {len(successful)} updates:")
    for result in successful:
        size_mb = result.download_size / (1024 * 1024)
        checksum_status = get_checksum_status(result)
        console.print(f"  Downloaded: {result.app_name} ({size_mb:.1f} MB){checksum_status}")

Find symlinks pointing to AppImage files in the download directory.

Uses the same search paths as go-appimage's appimaged: - /usr/local/bin - /opt - ~/Applications - ~/.local/bin - ~/Downloads - $PATH directories

Source code in src/appimage_updater/ui/display.py
def find_appimage_symlinks(download_dir: Path, configured_symlink_path: Path | None = None) -> list[tuple[Path, Path]]:
    """Find symlinks pointing to AppImage files in the download directory.

    Uses the same search paths as go-appimage's appimaged:
    - /usr/local/bin
    - /opt
    - ~/Applications
    - ~/.local/bin
    - ~/Downloads
    - $PATH directories
    """
    found_symlinks = _check_configured_symlink_if_provided(configured_symlink_path, download_dir)
    search_locations = _get_search_locations(download_dir)
    found_symlinks.extend(_scan_all_locations(search_locations, download_dir))
    return _remove_duplicate_symlinks(found_symlinks)

find_matching_appimage_files(download_dir, pattern)

Find AppImage files matching the pattern in the download directory.

Returns:

Type Description
list[Path] | str

List of matching files, or error message string if there was an error.

Source code in src/appimage_updater/ui/display.py
def find_matching_appimage_files(download_dir: Path, pattern: str) -> list[Path] | str:
    """Find AppImage files matching the pattern in the download directory.

    Returns:
        List of matching files, or error message string if there was an error.
    """
    try:
        pattern_compiled = re.compile(pattern)
        return _collect_matching_files(download_dir, pattern_compiled)
    except PermissionError:
        return "[red]Permission denied accessing download directory[/red]"

format_file_groups(rotation_groups)

Format file groups into display strings.

Source code in src/appimage_updater/ui/display.py
def format_file_groups(rotation_groups: dict[str, list[Path]]) -> str:
    """Format file groups into display strings."""
    file_lines: list[str] = []

    for group_name, files in rotation_groups.items():
        _sort_files_by_modification_time(files)
        _add_group_header(file_lines, group_name)
        _add_file_info_lines(file_lines, files)
        _add_group_separator(file_lines, group_name)

    _remove_trailing_empty_lines(file_lines)
    return "\n".join(file_lines)

format_single_file_info(file_path)

Format information for a single file.

Source code in src/appimage_updater/ui/display.py
def format_single_file_info(file_path: Path) -> list[str]:
    """Format information for a single file."""
    stat_info = file_path.stat()
    size_mb = stat_info.st_size / (1024 * 1024)
    mtime = os.path.getmtime(file_path)
    mtime_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime))

    # Check if file is executable
    executable = "[green]executable[/green]" if os.access(file_path, os.X_OK) else "[red]not executable[/red]"

    # Identify rotation suffix for better display
    rotation_indicator = get_rotation_indicator(file_path.name)

    return [
        f"[bold]{file_path.name}[/bold]{rotation_indicator}",
        f"  [dim]Size:[/dim] {size_mb:.1f} MB",
        f"  [dim]Modified:[/dim] {mtime_str}",
        f"  [dim]Executable:[/dim] {executable}",
    ]

Format information for a single symlink.

Source code in src/appimage_updater/ui/display.py
def format_single_symlink(symlink_path: Path, target_path: Path) -> list[str]:
    """Format information for a single symlink."""
    target_status = _get_target_status(target_path)
    status_icon = _get_status_icon(target_status)

    # Apply home path replacement for display
    display_symlink = _replace_home_with_tilde(str(symlink_path))
    display_target = _replace_home_with_tilde(str(target_path))

    lines = [f"[bold]{display_symlink}[/bold] {status_icon}", f"  [dim]→[/dim] {display_target}"]

    status_message = _get_status_message(target_status)
    if status_message:
        lines.append(status_message)
    return lines

Format symlink information for display.

Source code in src/appimage_updater/ui/display.py
def format_symlink_info(found_symlinks: list[tuple[Path, Path]]) -> str:
    """Format symlink information for display."""
    symlink_lines = []
    for symlink_path, target_path in found_symlinks:
        symlink_lines.extend(format_single_symlink(symlink_path, target_path))
        symlink_lines.append("")  # Empty line between symlinks

    # Remove last empty line
    if symlink_lines and symlink_lines[-1] == "":
        symlink_lines.pop()

    return "\n".join(symlink_lines)

get_base_appimage_name(filename)

Extract the base name from an AppImage filename, removing rotation suffixes.

Examples:

'app.AppImage' -> 'app' 'app.AppImage.current' -> 'app' 'app.AppImage.old' -> 'app' 'MyApp-v1.0.AppImage.old2' -> 'MyApp-v1.0'

Source code in src/appimage_updater/ui/display.py
def get_base_appimage_name(filename: str) -> str:
    """Extract the base name from an AppImage filename, removing rotation suffixes.

    Examples:
        'app.AppImage' -> 'app'
        'app.AppImage.current' -> 'app'
        'app.AppImage.old' -> 'app'
        'MyApp-v1.0.AppImage.old2' -> 'MyApp-v1.0'
    """
    # Remove .AppImage and any rotation suffix
    if ".AppImage" in filename:
        base = filename.split(".AppImage")[0]
        return base
    return filename

get_basic_config_lines(app)

Get basic configuration lines for an application.

Source code in src/appimage_updater/ui/display.py
def get_basic_config_lines(app: Any) -> list[str]:
    """Get basic configuration lines for an application."""
    config_lines = [
        f"[bold]Name:[/bold] {app.name}",
        f"[bold]Status:[/bold] {'[green]Enabled[/green]' if app.enabled else '[red]Disabled[/red]'}",
        f"[bold]Source:[/bold] {app.source_type.title()}",
        f"[bold]URL:[/bold] {app.url}",
        f"[bold]Download Directory:[/bold] {_replace_home_with_tilde(str(app.download_dir))}",
        f"[bold]File Pattern:[/bold] {app.pattern}",
    ]

    # Add basename if it exists and is not None and is a string
    if hasattr(app, "basename") and app.basename and isinstance(app.basename, str):
        config_lines.append(f"[bold]Base Name:[/bold] {app.basename}")

    return config_lines

get_checksum_status(result)

Get checksum status indicator for a download result.

Source code in src/appimage_updater/ui/display.py
def get_checksum_status(result: Any) -> str:
    """Get checksum status indicator for a download result."""
    if not result.checksum_result:
        return ""

    if result.checksum_result.verified:
        return " [green]verified[/green]"
    else:
        return " [yellow]unverified[/yellow]"

get_configuration_info(app, config_source_info=None)

Get formatted configuration information for an application.

Source code in src/appimage_updater/ui/display.py
def get_configuration_info(app: Any, config_source_info: dict[str, str] | None = None) -> str:
    """Get formatted configuration information for an application."""
    config_lines = get_basic_config_lines(app)

    # Add config file path if available
    if config_source_info:
        config_path = _get_app_config_path(app, config_source_info)
        if config_path:
            config_lines.append(f"[bold]Config File:[/bold] {config_path}")

    add_optional_config_lines(app, config_lines)
    add_checksum_config_lines(app, config_lines)
    add_rotation_config_lines(app, config_lines)

    return "\n".join(config_lines)

get_files_info(app)

Get information about AppImage files for an application.

Source code in src/appimage_updater/ui/display.py
def get_files_info(app: Any) -> str:
    """Get information about AppImage files for an application."""
    download_dir = Path(app.download_dir)

    if not download_dir.exists():
        return "[yellow]Download directory does not exist[/yellow]"

    matching_files = find_matching_appimage_files(download_dir, app.pattern)
    if isinstance(matching_files, str):  # Error message
        return matching_files

    if not matching_files:
        return "[yellow]No AppImage files found matching the pattern[/yellow]"

    # Group files by rotation status
    rotation_groups = group_files_by_rotation(matching_files)

    return format_file_groups(rotation_groups)

get_rotation_indicator(filename)

Get a visual indicator for rotation status.

Source code in src/appimage_updater/ui/display.py
def get_rotation_indicator(filename: str) -> str:
    """Get a visual indicator for rotation status."""
    if _is_current_file(filename):
        return " [green](current)[/green]"
    elif _is_previous_file(filename):
        return " [yellow](previous)[/yellow]"
    elif _is_numbered_old_file(filename):
        return _get_numbered_old_indicator(filename)
    elif has_rotation_suffix(filename):
        return " [blue](rotated)[/blue]"
    return ""

Get information about symlinks pointing to AppImage files.

Source code in src/appimage_updater/ui/display.py
def get_symlinks_info(app: Any) -> str:
    """Get information about symlinks pointing to AppImage files."""
    download_dir = Path(app.download_dir)

    if not download_dir.exists():
        return "[yellow]Download directory does not exist[/yellow]"

    # Find symlinks including configured symlink_path
    found_symlinks = find_appimage_symlinks(download_dir, getattr(app, "symlink_path", None))

    if not found_symlinks:
        return "[yellow]No symlinks found pointing to AppImage files[/yellow]"

    return format_symlink_info(found_symlinks)

Check if symlink points to a valid AppImage file and return the target.

Source code in src/appimage_updater/ui/display.py
def get_valid_symlink_target(symlink: Path, download_dir: Path) -> Path | None:
    """Check if symlink points to a valid AppImage file and return the target."""
    try:
        target = symlink.resolve()
        # Check if symlink points to a file in our download directory
        # Accept files that contain ".AppImage" (handles .current, .old suffixes)
        if _is_valid_target_location(target, download_dir) or _is_valid_symlink_location(symlink, download_dir):
            return target
        # If we get here, symlink doesn't point to expected location
        logger.debug(f"Symlink {symlink} points to {target}, not a valid AppImage in download directory")
    except (OSError, RuntimeError) as e:
        logger.debug(f"Failed to resolve symlink {symlink}: {e}")
    return None

group_files_by_rotation(files)

Group files by their rotation status.

Groups files into: - 'rotated': Files that are part of a rotation group (have .current, .old, etc.) - 'standalone': Files that don't appear to be part of rotation

Source code in src/appimage_updater/ui/display.py
def group_files_by_rotation(files: list[Path]) -> dict[str, list[Path]]:
    """Group files by their rotation status.

    Groups files into:
    - 'rotated': Files that are part of a rotation group (have .current, .old, etc.)
    - 'standalone': Files that don't appear to be part of rotation
    """
    base_name_groups = _create_base_name_groups(files)
    rotation_groups = _classify_file_groups(base_name_groups)

    # Remove empty groups
    return {k: v for k, v in rotation_groups.items() if v}

has_rotation_suffix(filename)

Check if filename has a rotation suffix like .current, .old, .old2, etc.

Source code in src/appimage_updater/ui/display.py
def has_rotation_suffix(filename: str) -> bool:
    """Check if filename has a rotation suffix like .current, .old, .old2, etc."""
    return _has_numbered_old_suffix(filename) or _has_basic_rotation_suffix(filename)

Scan a directory for symlinks pointing to AppImage files.

Source code in src/appimage_updater/ui/display.py
def scan_directory_for_symlinks(location: Path, download_dir: Path) -> list[tuple[Path, Path]]:
    """Scan a directory for symlinks pointing to AppImage files."""
    symlinks = []
    try:
        for item in location.iterdir():
            if item.is_symlink():
                symlink_target = get_valid_symlink_target(item, download_dir)
                if symlink_target:
                    symlinks.append((item, symlink_target))
    except PermissionError as e:
        logger.debug(f"Permission denied reading directory {location}: {e}")
    return symlinks