# Modern Input Plugin Specification

## Overview

Migrate useful features from `enhanced_input` plugin to a new `modern_input` plugin that follows the design system (solid blocks ▄▀) while preserving customization options.

## Goals

1. Remove `enhanced_input` plugin (deprecated box-drawing characters)
2. Create `modern_input` plugin using design system
3. Preserve customization features users want
4. Support multi-line input display

## Features to Migrate

### From enhanced_input (keep these):
- Cursor blinking (configurable rate)
- Placeholder text when empty
- Dynamic width (auto, percentage, fixed)
- Min/max width constraints
- Color customization (text, cursor, placeholder)
- Show/hide status in status area

### From enhanced_input (drop these):
- Box-drawing character styles (╭╮╰╯│─) - violates design system
- Style randomization - unnecessary complexity
- Gradient modes on borders - design system handles this

### New features:
- Multi-line display (max 3 visible lines)
- Scroll indicators when content exceeds visible area
- Continuation prompt for subsequent lines

## File Structure

```
plugins/
  modern_input/
    __init__.py          # Package exports
    config.py            # Plugin configuration
    cursor_manager.py    # Cursor blinking logic
    renderer.py          # Multi-line box rendering
  modern_input_plugin.py # Main plugin class
```

## Implementation

### config.py

```python
from dataclasses import dataclass
from typing import Optional

@dataclass
class ModernInputConfig:
    enabled: bool = True

    # Sizing
    width_mode: str = "auto"  # auto, percentage (e.g. "80%"), fixed (e.g. "60")
    min_width: int = 40
    max_width: int = 76
    max_visible_lines: int = 3

    # Placeholder
    show_placeholder: bool = True
    placeholder: str = "Type your message..."

    # Cursor
    cursor_blink: bool = True
    cursor_blink_rate: float = 0.5  # seconds

    # Colors (use theme by default, allow overrides)
    use_theme_colors: bool = True
    custom_text_color: Optional[str] = None
    custom_placeholder_color: Optional[str] = None

    # Status
    show_status: bool = False

    @classmethod
    def from_config_manager(cls, config_manager) -> "ModernInputConfig":
        """Load config from config manager."""
        prefix = "plugins.modern_input"
        return cls(
            enabled=config_manager.get(f"{prefix}.enabled", True),
            width_mode=config_manager.get(f"{prefix}.width_mode", "auto"),
            min_width=config_manager.get(f"{prefix}.min_width", 40),
            max_width=config_manager.get(f"{prefix}.max_width", 76),
            max_visible_lines=config_manager.get(f"{prefix}.max_visible_lines", 3),
            show_placeholder=config_manager.get(f"{prefix}.show_placeholder", True),
            placeholder=config_manager.get(f"{prefix}.placeholder", "Type your message..."),
            cursor_blink=config_manager.get(f"{prefix}.cursor_blink", True),
            cursor_blink_rate=config_manager.get(f"{prefix}.cursor_blink_rate", 0.5),
            use_theme_colors=config_manager.get(f"{prefix}.use_theme_colors", True),
            show_status=config_manager.get(f"{prefix}.show_status", False),
        )
```

### cursor_manager.py

```python
import time

class CursorManager:
    def __init__(self, blink_rate: float = 0.5):
        self.blink_rate = blink_rate
        self._last_blink = time.time()
        self._cursor_visible = True

    def update(self) -> None:
        """Update cursor blink state."""
        now = time.time()
        if now - self._last_blink >= self.blink_rate:
            self._cursor_visible = not self._cursor_visible
            self._last_blink = now

    def get_cursor_char(self, is_active: bool) -> str:
        """Get cursor character based on state."""
        if not is_active:
            return ""
        return "█" if self._cursor_visible else " "

    def reset(self) -> None:
        """Reset cursor to visible state."""
        self._cursor_visible = True
        self._last_blink = time.time()
```

### renderer.py

```python
from typing import List, Dict, Any
from core.ui.design_system import T, C, solid_fg, gradient

class ModernInputRenderer:
    """Renders multi-line input box using design system."""

    def __init__(self, config):
        self.config = config

    def render(
        self,
        buffer_content: str,
        cursor_position: int,
        cursor_char: str,
        width: int,
        is_thinking: bool = False,
    ) -> List[str]:
        """Render the input box.

        Args:
            buffer_content: Raw input buffer (may contain newlines)
            cursor_position: Absolute cursor position in buffer
            cursor_char: Cursor character to display
            width: Box width
            is_thinking: Whether LLM is processing (hide cursor)

        Returns:
            List of rendered lines
        """
        lines = []

        # Determine prompt
        is_shell = buffer_content.lstrip().startswith("!")
        prompt = "!" if is_shell else "⠠⠵"
        continuation = "..."

        # Process buffer for display
        display_buffer, display_cursor = self._process_buffer(
            buffer_content, cursor_position, is_shell
        )

        # Split into lines
        input_lines = display_buffer.split("\n") if display_buffer else [""]

        # Calculate visible window
        cursor_line, cursor_col = self._get_cursor_line_col(
            display_buffer, display_cursor, input_lines
        )
        visible_start, visible_end = self._get_visible_window(
            cursor_line, len(input_lines)
        )

        visible_lines = input_lines[visible_start:visible_end]
        has_above = visible_start > 0
        has_below = visible_end < len(input_lines)

        # Top border
        border_top = "▄" * width
        if has_above:
            border_top = "▄" * (width - 2) + " ^"
        lines.append(solid_fg(border_top, T().dark[0]))

        # Content lines
        for i, line_text in enumerate(visible_lines):
            actual_line_idx = visible_start + i
            line_prompt = prompt if actual_line_idx == 0 else continuation

            # Add cursor if on this line
            if not is_thinking and actual_line_idx == cursor_line:
                col = min(cursor_col, len(line_text))
                text = line_text[:col] + cursor_char + line_text[col:]
            elif not is_thinking and not display_buffer and i == 0:
                # Empty buffer - show placeholder or just cursor
                if self.config.show_placeholder:
                    text = cursor_char + self._dim_text(self.config.placeholder)
                else:
                    text = cursor_char
            else:
                text = line_text

            content = f" {line_prompt} {text}".ljust(width)[:width]
            lines.append(gradient(content, T().input_bg, T().text, width))

        # Bottom border
        border_bottom = "▀" * width
        if has_below:
            border_bottom = "▀" * (width - 2) + " v"
        lines.append(solid_fg(border_bottom, T().dark[0]))

        return lines

    def _process_buffer(self, buffer: str, cursor_pos: int, is_shell: bool):
        """Process buffer for display (strip shell ! prefix)."""
        if not is_shell or not buffer:
            return buffer, cursor_pos

        stripped = buffer.lstrip()
        leading_ws = len(buffer) - len(stripped)

        if stripped.startswith("!"):
            display = buffer[:leading_ws] + stripped[1:]
            if cursor_pos > leading_ws + 1:
                cursor_pos -= 1
            elif cursor_pos > leading_ws:
                cursor_pos = leading_ws
            return display, cursor_pos
        return buffer, cursor_pos

    def _get_cursor_line_col(self, buffer: str, cursor_pos: int, lines: List[str]):
        """Calculate cursor line and column."""
        chars = 0
        for i, line in enumerate(lines):
            line_end = chars + len(line)
            if cursor_pos <= line_end:
                return i, cursor_pos - chars
            chars = line_end + 1
        return len(lines) - 1, len(lines[-1]) if lines else 0

    def _get_visible_window(self, cursor_line: int, total_lines: int):
        """Calculate visible line window."""
        max_visible = self.config.max_visible_lines

        if total_lines <= max_visible:
            return 0, total_lines

        # Center cursor in visible window when possible
        half = max_visible // 2
        start = max(0, cursor_line - half)
        end = start + max_visible

        if end > total_lines:
            end = total_lines
            start = max(0, end - max_visible)

        return start, end

    def _dim_text(self, text: str) -> str:
        """Apply dim styling to text."""
        return f"\033[2m{text}\033[22m"
```

### modern_input_plugin.py

```python
"""Modern Input Plugin for Kollabor CLI.

Provides customizable input rendering using the design system.
Supports multi-line input with scroll indicators.
"""

import logging
from typing import Any, Dict, List, TYPE_CHECKING

from core.events import Event, EventType, Hook, HookPriority

from plugins.modern_input.config import ModernInputConfig
from plugins.modern_input.cursor_manager import CursorManager
from plugins.modern_input.renderer import ModernInputRenderer

if TYPE_CHECKING:
    from core.config import ConfigManager
    from core.events import EventBus
    from core.io.terminal_renderer import TerminalRenderer

logger = logging.getLogger(__name__)


class ModernInputPlugin:
    """Plugin that renders input box using design system with customization."""

    @staticmethod
    def get_default_config() -> Dict[str, Any]:
        """Get default configuration."""
        return {
            "enabled": True,
            "width_mode": "auto",
            "min_width": 40,
            "max_width": 76,
            "max_visible_lines": 3,
            "show_placeholder": True,
            "placeholder": "Type your message...",
            "cursor_blink": True,
            "cursor_blink_rate": 0.5,
            "use_theme_colors": True,
            "show_status": False,
        }

    def __init__(
        self,
        name: str,
        event_bus: "EventBus",
        renderer: "TerminalRenderer",
        config: "ConfigService",
    ) -> None:
        self.name = name
        self.event_bus = event_bus
        self.renderer = renderer
        self.config_service = config

        self.config = ModernInputConfig.from_config_manager(config.config_manager)
        self.cursor_manager = CursorManager(self.config.cursor_blink_rate)
        self.input_renderer = ModernInputRenderer(self.config)

        self.hooks = [
            Hook(
                name="render_modern_input",
                plugin_name=self.name,
                event_type=EventType.INPUT_RENDER,
                priority=HookPriority.DISPLAY.value,
                callback=self._render_input,
            )
        ]

    async def _render_input(self, data: Dict[str, Any], event: Event) -> Dict[str, Any]:
        """Render the input box."""
        if not self.config.enabled:
            return {"status": "disabled"}

        # Get state from renderer
        buffer = getattr(self.renderer, "input_buffer", "")
        cursor_pos = getattr(self.renderer, "cursor_position", len(buffer))
        is_thinking = getattr(self.renderer, "thinking_active", False)

        # Update cursor blink
        if self.config.cursor_blink:
            self.cursor_manager.update()

        cursor_char = self.cursor_manager.get_cursor_char(not is_thinking)

        # Calculate width
        terminal_width, _ = self.renderer.terminal_state.get_size()
        width = self._calculate_width(terminal_width)

        # Render
        lines = self.input_renderer.render(
            buffer_content=buffer,
            cursor_position=cursor_pos,
            cursor_char=cursor_char,
            width=width,
            is_thinking=is_thinking,
        )

        return {
            "status": "rendered",
            "fancy_input_lines": lines,
            "lines": len(lines),
        }

    def _calculate_width(self, terminal_width: int) -> int:
        """Calculate box width based on config."""
        mode = self.config.width_mode

        if mode == "auto":
            width = min(terminal_width, self.config.max_width)
        elif mode.endswith("%"):
            pct = int(mode[:-1]) / 100
            width = int(terminal_width * pct)
        else:
            try:
                width = int(mode)
            except ValueError:
                width = self.config.max_width

        return max(self.config.min_width, min(width, self.config.max_width))

    async def initialize(self) -> None:
        """Initialize plugin."""
        self.config_service.register_reload_callback(self._reload_config)
        logger.info("Modern input plugin initialized")

    def _reload_config(self) -> None:
        """Reload configuration."""
        self.config = ModernInputConfig.from_config_manager(
            self.config_service.config_manager
        )
        self.cursor_manager = CursorManager(self.config.cursor_blink_rate)
        self.input_renderer = ModernInputRenderer(self.config)
        logger.info("Modern input config reloaded")

    async def register_hooks(self) -> None:
        """Register hooks."""
        for hook in self.hooks:
            await self.event_bus.register_hook(hook)

    async def shutdown(self) -> None:
        """Shutdown plugin."""
        pass
```

## Migration Steps

1. **Create new plugin structure:**
   ```
   mkdir -p plugins/modern_input
   touch plugins/modern_input/__init__.py
   ```

2. **Create files:**
   - `plugins/modern_input/config.py`
   - `plugins/modern_input/cursor_manager.py`
   - `plugins/modern_input/renderer.py`
   - `plugins/modern_input_plugin.py`

3. **Remove old plugin:**
   ```
   rm -rf plugins/enhanced_input/
   rm plugins/enhanced_input_plugin.py
   ```

4. **Update any imports** that reference enhanced_input

5. **Test:**
   - Plugin loads without errors
   - Input renders with design system style
   - Multi-line works (Shift+Enter)
   - Cursor blinks
   - Placeholder shows when empty
   - Scroll indicators appear with 4+ lines

## Config Widget Registration

The plugin should also provide config widgets for the /config modal:

```python
@staticmethod
def get_config_widgets() -> Dict[str, Any]:
    return {
        "title": "Modern Input",
        "widgets": [
            {
                "type": "checkbox",
                "label": "Enabled",
                "config_path": "plugins.modern_input.enabled",
            },
            {
                "type": "slider",
                "label": "Max Visible Lines",
                "config_path": "plugins.modern_input.max_visible_lines",
                "min_value": 1,
                "max_value": 10,
                "step": 1,
            },
            {
                "type": "checkbox",
                "label": "Show Placeholder",
                "config_path": "plugins.modern_input.show_placeholder",
            },
            {
                "type": "text_input",
                "label": "Placeholder Text",
                "config_path": "plugins.modern_input.placeholder",
            },
            {
                "type": "checkbox",
                "label": "Cursor Blink",
                "config_path": "plugins.modern_input.cursor_blink",
            },
            {
                "type": "slider",
                "label": "Blink Rate",
                "config_path": "plugins.modern_input.cursor_blink_rate",
                "min_value": 0.1,
                "max_value": 2.0,
                "step": 0.1,
            },
        ]
    }
```

## Notes

- Plugin is **disabled by default** - core renderer handles input
- When enabled, overrides INPUT_RENDER event
- Uses same buffer_manager multi-line logic as core
- Follows design system strictly (solid blocks only)
- Preserves user customization options
