textual-vimkeys-input


Nametextual-vimkeys-input JSON
Version 0.3.2 PyPI version JSON
download
home_pageNone
SummaryComprehensive Vim-style modal editing widget for Textual with operator+motion, text objects, and visual mode
upload_time2025-11-02 02:51:40
maintainerNone
docs_urlNone
authorNone
requires_python>=3.11
licenseMIT
keywords command-mode input keybindings modal-editing terminal text-editor textual tui vi vim visual-mode widget
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # VimKeys Input

A comprehensive Vim-style modal editing widget for [Textual](https://textual.textualize.io/), bringing the power of Vim keybindings to your terminal applications.

[![Python Version](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
[![Textual Version](https://img.shields.io/badge/textual-0.47%2B-purple.svg)](https://textual.textualize.io/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

### Core Vim Functionality
- **Modal Editing**: Full INSERT, COMMAND, and VISUAL mode support
- **Operator + Motion System**: Complete implementation of vim's composable commands (e.g., `dw`, `c$`, `y3j`)
- **Count Support**: Numeric prefixes for commands (e.g., `5j`, `3dd`, `2d3w`)
- **Text Objects**: Semantic editing with `iw`, `i"`, `i(`, `da{`, etc.
- **Marks**: Position bookmarks with `ma`, `'a`, `` `a ``
- **Visual Selection**: Character-wise selection with operators

### Navigation
- **Basic Movement**: `hjkl` for directional navigation
- **Word Motion**: `w`, `b`, `e` for word-based navigation
- **Line Navigation**: `0`, `$`, `^` for line start/end/first non-whitespace
- **Document Navigation**: `gg`, `G` for document start/end
- **Count Support**: All motions support counts (e.g., `5j`, `3w`)

### Editing
- **Delete**: `x`, `dd`, `d<motion>` (e.g., `dw`, `d$`, `d3e`)
- **Change**: `cc`, `c<motion>` (e.g., `cw`, `c$`, `cb`)
- **Yank (Copy)**: `yy`, `y<motion>` (e.g., `y3j`, `yw`)
- **Paste**: `p`, `P` for paste after/before cursor
- **Undo/Redo**: `u`, `Ctrl+r` with full history
- **Insert Modes**: `i`, `a`, `I`, `A`, `o`, `O` for various insert positions

### Advanced Features
- **Search**: `/` for forward search, `n`/`N` for next/previous
- **Replace**: `r` for single character replacement
- **Join Lines**: `J` to join current line with next
- **Visual Mode**: `v` for character-wise selection, operators work in visual mode
- **Case Change**: `~` to toggle case (planned)
- **Line Change**: `S`, `C` for line/tail change (planned)

### Visual Feedback
- **Mode Indicators**: Border colors change based on current mode
  - Green: INSERT mode
  - Blue: COMMAND mode
  - Yellow: VISUAL mode
- **CSS Customization**: Full control over mode styling via CSS classes

### Events
- **Submitted**: Fires when user presses Enter in INSERT mode
- **ModeChanged**: Fires whenever vim mode changes

## Installation

### From PyPI (when published)
```bash
pip install vimkeys-input
```

### From Source (Development)
```bash
# Clone the repository
git clone https://github.com/yourusername/vimkeys-input.git
cd vimkeys-input

# Using uv (recommended)
uv venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
uv pip install -e ".[dev]"

# Or using pip
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```

## Quick Start

### Basic Example

```python
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer
from vimkeys_input import VimTextArea

class MyApp(App):
    """A simple app with vim-style input."""

    def compose(self) -> ComposeResult:
        yield Header()
        yield VimTextArea(id="input")
        yield Footer()

    def on_vim_text_area_submitted(self, event: VimTextArea.Submitted):
        """Handle text submission."""
        self.notify(f"You entered: {event.text}")
        event.text_area.clear()

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

### Chat Application Example

```python
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, RichLog
from textual.containers import Vertical
from vimkeys_input import VimTextArea

class ChatApp(App):
    """A chat application with vim-style input."""

    CSS = """
    #history {
        height: 1fr;
        border: solid $primary;
    }

    #input {
        height: auto;
        max-height: 10;
        margin: 1;
    }
    """

    def compose(self) -> ComposeResult:
        yield Header()
        with Vertical():
            yield RichLog(id="history", markup=True, wrap=True)
            yield VimTextArea(id="input")
        yield Footer()

    def on_mount(self):
        history = self.query_one("#history", RichLog)
        history.write("Welcome to the chat!")

    def on_vim_text_area_submitted(self, event: VimTextArea.Submitted):
        if not event.text.strip():
            return

        history = self.query_one("#history", RichLog)
        history.write(f"[bold cyan]You:[/bold cyan] {event.text}")
        event.text_area.clear()

if __name__ == "__main__":
    app = ChatApp()
    app.run()
```

## Complete Keybinding Reference

### Mode Switching

| Key | Action | Description |
|-----|--------|-------------|
| `ESC` | Enter COMMAND mode | Exit INSERT/VISUAL mode |
| `i` | Insert at cursor | Enter INSERT mode at cursor |
| `a` | Append after cursor | Enter INSERT mode after cursor |
| `I` | Insert at line start | Enter INSERT mode at first non-whitespace |
| `A` | Append at line end | Enter INSERT mode at end of line |
| `o` | Open line below | Create new line below and enter INSERT mode |
| `O` | Open line above | Create new line above and enter INSERT mode |
| `v` | Visual mode | Enter character-wise VISUAL mode |

### Navigation (COMMAND Mode)

| Key | Action | Count Support | Description |
|-----|--------|---------------|-------------|
| `h` | Left | ✓ | Move cursor left |
| `j` | Down | ✓ | Move cursor down |
| `k` | Up | ✓ | Move cursor up |
| `l` | Right | ✓ | Move cursor right |
| `w` | Next word | ✓ | Move to start of next word |
| `b` | Previous word | ✓ | Move to start of previous word |
| `e` | End of word | ✓ | Move to end of current/next word |
| `0` | Line start | - | Move to beginning of line |
| `$` | Line end | - | Move to end of line |
| `^` | First non-blank | - | Move to first non-whitespace character |
| `gg` | Document start | - | Move to first line |
| `G` | Document end | - | Move to last line |

### Operators (COMMAND Mode)

Operators can be combined with motions and counts for powerful editing:

| Operator | Description | Example | Result |
|----------|-------------|---------|--------|
| `d` | Delete | `dw` | Delete word |
| | | `d$` | Delete to end of line |
| | | `d3j` | Delete 3 lines down |
| | | `dd` | Delete current line |
| `c` | Change (delete + INSERT) | `cw` | Change word |
| | | `c$` | Change to end of line |
| | | `cb` | Change previous word |
| | | `cc` | Change entire line |
| `y` | Yank (copy) | `yw` | Yank word |
| | | `y$` | Yank to end of line |
| | | `y3j` | Yank 3 lines down |
| | | `yy` | Yank current line |

### Text Objects (COMMAND Mode)

Use with operators for semantic editing:

| Text Object | Description | Example | Result |
|-------------|-------------|---------|--------|
| `iw` | Inner word | `diw` | Delete word under cursor |
| `aw` | A word (with space) | `daw` | Delete word + surrounding space |
| `i"` | Inside quotes | `di"` | Delete text inside quotes |
| `a"` | Around quotes | `da"` | Delete text + quotes |
| `i(` / `i)` | Inside parens | `di(` | Delete text inside parentheses |
| `a(` / `a)` | Around parens | `da(` | Delete text + parentheses |
| `i{` / `i}` | Inside braces | `di{` | Delete text inside braces |
| `a{` / `a}` | Around braces | `da{` | Delete text + braces |
| `i[` / `i]` | Inside brackets | `di[` | Delete text inside brackets |
| `a[` / `a]` | Around brackets | `da[` | Delete text + brackets |

### Editing (COMMAND Mode)

| Key | Action | Count Support | Description |
|-----|--------|---------------|-------------|
| `x` | Delete char | ✓ | Delete character under cursor |
| `r` | Replace char | - | Replace character under cursor |
| `p` | Paste after | ✓ | Paste after cursor/line |
| `P` | Paste before | ✓ | Paste before cursor/line |
| `u` | Undo | - | Undo last change |
| `Ctrl+r` | Redo | - | Redo last undone change |
| `J` | Join lines | - | Join current line with next |
| `~` | Toggle case | ✓ | Toggle case of character(s) |

### Search (COMMAND Mode)

| Key | Action | Description |
|-----|--------|-------------|
| `/` | Search forward | Enter search pattern |
| `n` | Next match | Jump to next search result |
| `N` | Previous match | Jump to previous search result |

### Marks (COMMAND Mode)

| Key | Action | Description |
|-----|--------|-------------|
| `m{a-z}` | Set mark | Set mark at cursor position (e.g., `ma`) |
| `'{a-z}` | Jump to mark line | Jump to line of mark (e.g., `'a`) |
| `` `{a-z} `` | Jump to mark pos | Jump to exact position of mark (e.g., `` `a ``) |

### Visual Mode

| Key | Action | Description |
|-----|--------|-------------|
| `v` | Enter visual | Start character-wise selection |
| `h/j/k/l` | Extend selection | Extend selection with movement |
| `w/b/e` | Extend by word | Extend selection by words |
| `d` | Delete selection | Delete selected text |
| `c` | Change selection | Delete selected text and enter INSERT |
| `y` | Yank selection | Copy selected text |
| `ESC` | Exit visual | Return to COMMAND mode |

### Count System

Vim counts work in multiple ways:

```
{count}{motion}     Example: 5j (move down 5 lines)
{count}{operator}   Example: 3dd (delete 3 lines)
{operator}{count}{motion}   Example: d3w (delete 3 words)
{count1}{operator}{count2}{motion}   Example: 2d3w (delete 6 words total)
```

### INSERT Mode

| Key | Action | Description |
|-----|--------|-------------|
| `Enter` | Submit | Fire Submitted event (customizable) |
| `ESC` | Exit INSERT | Return to COMMAND mode |
| All standard keys | Insert text | Normal text input |
| Arrow keys, Backspace, Delete, etc. | Edit | Standard editing operations |

## Examples

The package includes several example applications demonstrating different use cases:

### 01_spike.py - Basic Functionality
```bash
uv run python examples/01_spike.py
```
Demonstrates basic vim modal editing with a simple text input.

### 02_simple_chat.py - Chat Bot
```bash
uv run python examples/02_simple_chat.py
```
A simple chat bot application showing how to integrate VimTextArea in a conversational UI.

### 03_streaming_chat.py - Advanced Chat
```bash
uv run python examples/03_streaming_chat.py
```
An advanced chat application featuring:
- Token-by-token streaming responses
- Animated "thinking" indicator
- Input history navigation (up/down arrows)
- Text wrapping
- Command palette integration

## API Reference

### VimTextArea

The main widget class extending Textual's `TextArea`.

#### Constructor

```python
VimTextArea(
    text: str = "",
    language: str | None = None,
    theme: str = "css",
    *,
    id: str | None = None,
    classes: str | None = None,
)
```

All parameters from Textual's `TextArea` are supported.

#### Properties

```python
@property
def mode(self) -> VimMode:
    """Get current vim mode."""

@mode.setter
def mode(self, new_mode: VimMode) -> None:
    """Set vim mode and update visual feedback."""

@property
def yank_register(self) -> str:
    """Get contents of yank (copy) register."""
```

#### Methods

```python
def clear(self) -> None:
    """Clear the text area and reset to INSERT mode."""

def get_line(self, row: int) -> Text:
    """Get the text content of a specific line."""

def set_cursor(self, row: int, col: int) -> None:
    """Set cursor position."""
```

#### Events

```python
class Submitted(Message):
    """Posted when user presses Enter in INSERT mode."""

    @property
    def text(self) -> str:
        """The submitted text."""

    @property
    def text_area(self) -> VimTextArea:
        """The VimTextArea that was submitted."""

class ModeChanged(Message):
    """Posted when vim mode changes."""

    @property
    def mode(self) -> VimMode:
        """The new mode."""

    @property
    def previous_mode(self) -> VimMode:
        """The previous mode."""
```

### VimMode

Enum representing vim modes:

```python
class VimMode(Enum):
    INSERT = "insert"     # Normal text input
    COMMAND = "command"   # Vim command mode
    VISUAL = "visual"     # Visual selection mode
```

### CSS Classes

VimTextArea automatically applies CSS classes based on mode:

- `.insert-mode` - Applied in INSERT mode
- `.command-mode` - Applied in COMMAND mode
- `.visual-mode` - Applied in VISUAL mode

## Customization

### Styling Modes

Customize the appearance of different modes with CSS:

```python
class MyApp(App):
    CSS = """
    VimTextArea.insert-mode {
        border: solid green;
        background: $surface;
    }

    VimTextArea.command-mode {
        border: solid blue;
        background: $surface;
    }

    VimTextArea.visual-mode {
        border: solid yellow;
        background: $boost;
    }

    VimTextArea {
        height: auto;
        max-height: 20;
        min-height: 3;
    }
    """
```

### Handling Events

Listen to vim-specific events:

```python
def on_vim_text_area_mode_changed(self, event: VimTextArea.ModeChanged):
    """React to mode changes."""
    if event.mode == VimMode.INSERT:
        self.sub_title = "Editing"
    elif event.mode == VimMode.COMMAND:
        self.sub_title = "Command"

def on_vim_text_area_submitted(self, event: VimTextArea.Submitted):
    """Handle text submission."""
    self.process_input(event.text)
    event.text_area.clear()
```

### Extending VimTextArea

Subclass to add custom functionality:

```python
from vimkeys_input import VimTextArea
from textual import events

class HistoryVimTextArea(VimTextArea):
    """VimTextArea with input history navigation."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.input_history = []
        self.history_index = -1

    def add_to_history(self, text: str):
        """Add entry to history."""
        if text.strip():
            self.input_history.append(text)

    def on_key(self, event: events.Key) -> None:
        """Handle up/down for history navigation."""
        if event.key == "up" and self.cursor_location == (0, 0):
            # Load previous history entry
            if self.input_history and self.history_index < len(self.input_history) - 1:
                self.history_index += 1
                self.text = self.input_history[-(self.history_index + 1)]
                event.prevent_default()
                return

        return super().on_key(event)
```

## Architecture

VimTextArea is built using a modular mixin architecture:

```
VimTextArea (vim_textarea.py)
├── NavigationMixin (operations/navigation.py)
│   └── Handles h, j, k, l, w, b, e, 0, $, gg, G
├── EditingMixin (operations/editing.py)
│   └── Handles x, r, p, P, u, Ctrl+r, J
├── VisualMixin (operations/visual.py)
│   └── Handles visual mode selection and operations
├── SearchMixin (operations/search.py)
│   └── Handles /, n, N search operations
├── TextObjectMixin (operations/text_objects.py)
│   └── Handles iw, aw, i", a", i(, a(, etc.
├── CountHandler (count_handler.py)
│   └── Manages numeric count state
├── OperatorPendingState (operator_pending.py)
│   └── Manages operator+motion combinations
└── MarksManager (marks.py)
    └── Manages position bookmarks

All built on Textual's TextArea widget
```

This modular design keeps the codebase maintainable and extensible.

## Development

### Project Structure

```
vimkeys-input/
├── vimkeys_input/           # Main package
│   ├── __init__.py         # Public API exports
│   ├── vim_modes.py        # VimMode enum
│   ├── vim_textarea.py     # Main widget class
│   ├── count_handler.py    # Count support
│   ├── operator_pending.py # Operator+motion system
│   ├── marks.py            # Position bookmarks
│   └── operations/         # Modular operation mixins
│       ├── __init__.py
│       ├── navigation.py   # Movement commands
│       ├── editing.py      # Edit commands
│       ├── visual.py       # Visual mode
│       ├── search.py       # Search functionality
│       └── text_objects.py # Text object support
├── examples/               # Example applications
│   ├── 01_spike.py
│   ├── 02_simple_chat.py
│   └── 03_streaming_chat.py
├── tests/                  # Test suite (124 tests)
├── docs/                   # Additional documentation
├── plans/                  # Development planning docs
├── pyproject.toml         # Package configuration
└── README.md              # This file
```

### Running Tests

```bash
# Install development dependencies
uv pip install -e ".[dev]"

# Run all tests
pytest tests/

# Run specific test file
pytest tests/test_navigation.py

# Run with coverage
pytest --cov=vimkeys_input tests/

# Run with verbose output
pytest -v tests/
```

### Development Mode

Use Textual's dev mode for live reload during development:

```bash
textual run --dev examples/01_spike.py

# Or with console for debugging
textual console
textual run --dev examples/01_spike.py
```

### Code Quality

The project uses:
- **pytest** for testing
- **ruff** for linting and formatting
- **mypy** for type checking (planned)

```bash
# Format code
ruff format vimkeys_input/

# Lint code
ruff check vimkeys_input/

# Type check
mypy vimkeys_input/
```

## Implementation Status

### ✅ Phase 0: Spike (Complete)
- Basic structure and mode switching
- Core navigation (hjkl)
- Basic editing (x, dd, yy, p)
- Visual selection
- Example applications

### ✅ Phase 1: Refactoring (Complete)
- Modular mixin architecture
- Comprehensive test suite (124 tests, 82% passing)
- All navigation commands
- All mode transitions
- Undo/redo integration

### ✅ Phase 2: Advanced Features (Complete)
- Operator + motion system (dw, c$, y3j)
- Count support (5j, 3dd, 2d3w)
- Text objects (diw, ci", da()
- Marks system (ma, 'a, `a)
- Search functionality (/, n, N)
- Join lines (J)
- Replace character (r)

### 🚧 Phase 3: Polish (In Progress)
- Comprehensive documentation
- Additional examples
- Performance optimization
- PyPI package preparation
- Case toggle (~)
- Line operations (S, C)
- Visual line mode (V)

## Contributing

We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

### Quick Contribution Guide

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Add tests for new functionality
5. Ensure tests pass (`pytest tests/`)
6. Commit with descriptive messages
7. Push to your fork
8. Open a Pull Request

## Roadmap

- [ ] Visual line mode (V)
- [ ] Visual block mode (Ctrl+v)
- [ ] Case change operations (~, gu, gU)
- [ ] Line operations (S, C)
- [ ] Repeat command (.)
- [ ] Macros (q, @)
- [ ] More text objects (sentences, paragraphs)
- [ ] Ex commands (`:w`, `:q`)
- [ ] Configuration system
- [ ] Plugin architecture

## FAQ

**Q: Does this support all vim commands?**
A: No, this implements core vim modal editing and the most common commands. It's designed for text input in terminal apps, not as a full vim replacement.

**Q: Can I use this with Textual's built-in TextArea features?**
A: Yes! VimTextArea extends TextArea, so features like syntax highlighting, themes, and language support all work.

**Q: How do I disable vim mode temporarily?**
A: Set `text_area.mode = VimMode.INSERT` to keep the widget in INSERT mode, or subclass and override key handling.

**Q: Does this work on Windows?**
A: Yes! VimTextArea works on all platforms supported by Textual (Linux, macOS, Windows).

**Q: Can I customize which keys trigger which commands?**
A: Currently no, but this is planned for a future release. For now, you can subclass and override key handlers.

## License

MIT License - see [LICENSE](LICENSE) file for details.

## Credits

- Built with [Textual](https://textual.textualize.io/) by [Textualize](https://www.textualize.io/)
- Inspired by [Vim](https://www.vim.org/) by Bram Moolenaar
- Created and maintained by contributors

## Links

- **Documentation**: [Full documentation](https://github.com/yourusername/vimkeys-input/docs)
- **Examples**: [Example applications](https://github.com/yourusername/vimkeys-input/tree/main/examples)
- **Issues**: [Bug reports and feature requests](https://github.com/yourusername/vimkeys-input/issues)
- **Discussions**: [Community discussions](https://github.com/yourusername/vimkeys-input/discussions)

---

Made with ❤️ for the Textual community

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "textual-vimkeys-input",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.11",
    "maintainer_email": "Jason Cronquist <jason@example.com>",
    "keywords": "command-mode, input, keybindings, modal-editing, terminal, text-editor, textual, tui, vi, vim, visual-mode, widget",
    "author": null,
    "author_email": "Jason Cronquist <jason@example.com>",
    "download_url": "https://files.pythonhosted.org/packages/8a/0f/c32bf6fdcf8bcef554d4366f3a360297bc362a64d858074d24b534310d75/textual_vimkeys_input-0.3.2.tar.gz",
    "platform": null,
    "description": "# VimKeys Input\n\nA comprehensive Vim-style modal editing widget for [Textual](https://textual.textualize.io/), bringing the power of Vim keybindings to your terminal applications.\n\n[![Python Version](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)\n[![Textual Version](https://img.shields.io/badge/textual-0.47%2B-purple.svg)](https://textual.textualize.io/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n## Features\n\n### Core Vim Functionality\n- **Modal Editing**: Full INSERT, COMMAND, and VISUAL mode support\n- **Operator + Motion System**: Complete implementation of vim's composable commands (e.g., `dw`, `c$`, `y3j`)\n- **Count Support**: Numeric prefixes for commands (e.g., `5j`, `3dd`, `2d3w`)\n- **Text Objects**: Semantic editing with `iw`, `i\"`, `i(`, `da{`, etc.\n- **Marks**: Position bookmarks with `ma`, `'a`, `` `a ``\n- **Visual Selection**: Character-wise selection with operators\n\n### Navigation\n- **Basic Movement**: `hjkl` for directional navigation\n- **Word Motion**: `w`, `b`, `e` for word-based navigation\n- **Line Navigation**: `0`, `$`, `^` for line start/end/first non-whitespace\n- **Document Navigation**: `gg`, `G` for document start/end\n- **Count Support**: All motions support counts (e.g., `5j`, `3w`)\n\n### Editing\n- **Delete**: `x`, `dd`, `d<motion>` (e.g., `dw`, `d$`, `d3e`)\n- **Change**: `cc`, `c<motion>` (e.g., `cw`, `c$`, `cb`)\n- **Yank (Copy)**: `yy`, `y<motion>` (e.g., `y3j`, `yw`)\n- **Paste**: `p`, `P` for paste after/before cursor\n- **Undo/Redo**: `u`, `Ctrl+r` with full history\n- **Insert Modes**: `i`, `a`, `I`, `A`, `o`, `O` for various insert positions\n\n### Advanced Features\n- **Search**: `/` for forward search, `n`/`N` for next/previous\n- **Replace**: `r` for single character replacement\n- **Join Lines**: `J` to join current line with next\n- **Visual Mode**: `v` for character-wise selection, operators work in visual mode\n- **Case Change**: `~` to toggle case (planned)\n- **Line Change**: `S`, `C` for line/tail change (planned)\n\n### Visual Feedback\n- **Mode Indicators**: Border colors change based on current mode\n  - Green: INSERT mode\n  - Blue: COMMAND mode\n  - Yellow: VISUAL mode\n- **CSS Customization**: Full control over mode styling via CSS classes\n\n### Events\n- **Submitted**: Fires when user presses Enter in INSERT mode\n- **ModeChanged**: Fires whenever vim mode changes\n\n## Installation\n\n### From PyPI (when published)\n```bash\npip install vimkeys-input\n```\n\n### From Source (Development)\n```bash\n# Clone the repository\ngit clone https://github.com/yourusername/vimkeys-input.git\ncd vimkeys-input\n\n# Using uv (recommended)\nuv venv .venv\nsource .venv/bin/activate  # On Windows: .venv\\Scripts\\activate\nuv pip install -e \".[dev]\"\n\n# Or using pip\npython -m venv .venv\nsource .venv/bin/activate\npip install -e \".[dev]\"\n```\n\n## Quick Start\n\n### Basic Example\n\n```python\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nfrom vimkeys_input import VimTextArea\n\nclass MyApp(App):\n    \"\"\"A simple app with vim-style input.\"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        yield VimTextArea(id=\"input\")\n        yield Footer()\n\n    def on_vim_text_area_submitted(self, event: VimTextArea.Submitted):\n        \"\"\"Handle text submission.\"\"\"\n        self.notify(f\"You entered: {event.text}\")\n        event.text_area.clear()\n\nif __name__ == \"__main__\":\n    app = MyApp()\n    app.run()\n```\n\n### Chat Application Example\n\n```python\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer, RichLog\nfrom textual.containers import Vertical\nfrom vimkeys_input import VimTextArea\n\nclass ChatApp(App):\n    \"\"\"A chat application with vim-style input.\"\"\"\n\n    CSS = \"\"\"\n    #history {\n        height: 1fr;\n        border: solid $primary;\n    }\n\n    #input {\n        height: auto;\n        max-height: 10;\n        margin: 1;\n    }\n    \"\"\"\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        with Vertical():\n            yield RichLog(id=\"history\", markup=True, wrap=True)\n            yield VimTextArea(id=\"input\")\n        yield Footer()\n\n    def on_mount(self):\n        history = self.query_one(\"#history\", RichLog)\n        history.write(\"Welcome to the chat!\")\n\n    def on_vim_text_area_submitted(self, event: VimTextArea.Submitted):\n        if not event.text.strip():\n            return\n\n        history = self.query_one(\"#history\", RichLog)\n        history.write(f\"[bold cyan]You:[/bold cyan] {event.text}\")\n        event.text_area.clear()\n\nif __name__ == \"__main__\":\n    app = ChatApp()\n    app.run()\n```\n\n## Complete Keybinding Reference\n\n### Mode Switching\n\n| Key | Action | Description |\n|-----|--------|-------------|\n| `ESC` | Enter COMMAND mode | Exit INSERT/VISUAL mode |\n| `i` | Insert at cursor | Enter INSERT mode at cursor |\n| `a` | Append after cursor | Enter INSERT mode after cursor |\n| `I` | Insert at line start | Enter INSERT mode at first non-whitespace |\n| `A` | Append at line end | Enter INSERT mode at end of line |\n| `o` | Open line below | Create new line below and enter INSERT mode |\n| `O` | Open line above | Create new line above and enter INSERT mode |\n| `v` | Visual mode | Enter character-wise VISUAL mode |\n\n### Navigation (COMMAND Mode)\n\n| Key | Action | Count Support | Description |\n|-----|--------|---------------|-------------|\n| `h` | Left | \u2713 | Move cursor left |\n| `j` | Down | \u2713 | Move cursor down |\n| `k` | Up | \u2713 | Move cursor up |\n| `l` | Right | \u2713 | Move cursor right |\n| `w` | Next word | \u2713 | Move to start of next word |\n| `b` | Previous word | \u2713 | Move to start of previous word |\n| `e` | End of word | \u2713 | Move to end of current/next word |\n| `0` | Line start | - | Move to beginning of line |\n| `$` | Line end | - | Move to end of line |\n| `^` | First non-blank | - | Move to first non-whitespace character |\n| `gg` | Document start | - | Move to first line |\n| `G` | Document end | - | Move to last line |\n\n### Operators (COMMAND Mode)\n\nOperators can be combined with motions and counts for powerful editing:\n\n| Operator | Description | Example | Result |\n|----------|-------------|---------|--------|\n| `d` | Delete | `dw` | Delete word |\n| | | `d$` | Delete to end of line |\n| | | `d3j` | Delete 3 lines down |\n| | | `dd` | Delete current line |\n| `c` | Change (delete + INSERT) | `cw` | Change word |\n| | | `c$` | Change to end of line |\n| | | `cb` | Change previous word |\n| | | `cc` | Change entire line |\n| `y` | Yank (copy) | `yw` | Yank word |\n| | | `y$` | Yank to end of line |\n| | | `y3j` | Yank 3 lines down |\n| | | `yy` | Yank current line |\n\n### Text Objects (COMMAND Mode)\n\nUse with operators for semantic editing:\n\n| Text Object | Description | Example | Result |\n|-------------|-------------|---------|--------|\n| `iw` | Inner word | `diw` | Delete word under cursor |\n| `aw` | A word (with space) | `daw` | Delete word + surrounding space |\n| `i\"` | Inside quotes | `di\"` | Delete text inside quotes |\n| `a\"` | Around quotes | `da\"` | Delete text + quotes |\n| `i(` / `i)` | Inside parens | `di(` | Delete text inside parentheses |\n| `a(` / `a)` | Around parens | `da(` | Delete text + parentheses |\n| `i{` / `i}` | Inside braces | `di{` | Delete text inside braces |\n| `a{` / `a}` | Around braces | `da{` | Delete text + braces |\n| `i[` / `i]` | Inside brackets | `di[` | Delete text inside brackets |\n| `a[` / `a]` | Around brackets | `da[` | Delete text + brackets |\n\n### Editing (COMMAND Mode)\n\n| Key | Action | Count Support | Description |\n|-----|--------|---------------|-------------|\n| `x` | Delete char | \u2713 | Delete character under cursor |\n| `r` | Replace char | - | Replace character under cursor |\n| `p` | Paste after | \u2713 | Paste after cursor/line |\n| `P` | Paste before | \u2713 | Paste before cursor/line |\n| `u` | Undo | - | Undo last change |\n| `Ctrl+r` | Redo | - | Redo last undone change |\n| `J` | Join lines | - | Join current line with next |\n| `~` | Toggle case | \u2713 | Toggle case of character(s) |\n\n### Search (COMMAND Mode)\n\n| Key | Action | Description |\n|-----|--------|-------------|\n| `/` | Search forward | Enter search pattern |\n| `n` | Next match | Jump to next search result |\n| `N` | Previous match | Jump to previous search result |\n\n### Marks (COMMAND Mode)\n\n| Key | Action | Description |\n|-----|--------|-------------|\n| `m{a-z}` | Set mark | Set mark at cursor position (e.g., `ma`) |\n| `'{a-z}` | Jump to mark line | Jump to line of mark (e.g., `'a`) |\n| `` `{a-z} `` | Jump to mark pos | Jump to exact position of mark (e.g., `` `a ``) |\n\n### Visual Mode\n\n| Key | Action | Description |\n|-----|--------|-------------|\n| `v` | Enter visual | Start character-wise selection |\n| `h/j/k/l` | Extend selection | Extend selection with movement |\n| `w/b/e` | Extend by word | Extend selection by words |\n| `d` | Delete selection | Delete selected text |\n| `c` | Change selection | Delete selected text and enter INSERT |\n| `y` | Yank selection | Copy selected text |\n| `ESC` | Exit visual | Return to COMMAND mode |\n\n### Count System\n\nVim counts work in multiple ways:\n\n```\n{count}{motion}     Example: 5j (move down 5 lines)\n{count}{operator}   Example: 3dd (delete 3 lines)\n{operator}{count}{motion}   Example: d3w (delete 3 words)\n{count1}{operator}{count2}{motion}   Example: 2d3w (delete 6 words total)\n```\n\n### INSERT Mode\n\n| Key | Action | Description |\n|-----|--------|-------------|\n| `Enter` | Submit | Fire Submitted event (customizable) |\n| `ESC` | Exit INSERT | Return to COMMAND mode |\n| All standard keys | Insert text | Normal text input |\n| Arrow keys, Backspace, Delete, etc. | Edit | Standard editing operations |\n\n## Examples\n\nThe package includes several example applications demonstrating different use cases:\n\n### 01_spike.py - Basic Functionality\n```bash\nuv run python examples/01_spike.py\n```\nDemonstrates basic vim modal editing with a simple text input.\n\n### 02_simple_chat.py - Chat Bot\n```bash\nuv run python examples/02_simple_chat.py\n```\nA simple chat bot application showing how to integrate VimTextArea in a conversational UI.\n\n### 03_streaming_chat.py - Advanced Chat\n```bash\nuv run python examples/03_streaming_chat.py\n```\nAn advanced chat application featuring:\n- Token-by-token streaming responses\n- Animated \"thinking\" indicator\n- Input history navigation (up/down arrows)\n- Text wrapping\n- Command palette integration\n\n## API Reference\n\n### VimTextArea\n\nThe main widget class extending Textual's `TextArea`.\n\n#### Constructor\n\n```python\nVimTextArea(\n    text: str = \"\",\n    language: str | None = None,\n    theme: str = \"css\",\n    *,\n    id: str | None = None,\n    classes: str | None = None,\n)\n```\n\nAll parameters from Textual's `TextArea` are supported.\n\n#### Properties\n\n```python\n@property\ndef mode(self) -> VimMode:\n    \"\"\"Get current vim mode.\"\"\"\n\n@mode.setter\ndef mode(self, new_mode: VimMode) -> None:\n    \"\"\"Set vim mode and update visual feedback.\"\"\"\n\n@property\ndef yank_register(self) -> str:\n    \"\"\"Get contents of yank (copy) register.\"\"\"\n```\n\n#### Methods\n\n```python\ndef clear(self) -> None:\n    \"\"\"Clear the text area and reset to INSERT mode.\"\"\"\n\ndef get_line(self, row: int) -> Text:\n    \"\"\"Get the text content of a specific line.\"\"\"\n\ndef set_cursor(self, row: int, col: int) -> None:\n    \"\"\"Set cursor position.\"\"\"\n```\n\n#### Events\n\n```python\nclass Submitted(Message):\n    \"\"\"Posted when user presses Enter in INSERT mode.\"\"\"\n\n    @property\n    def text(self) -> str:\n        \"\"\"The submitted text.\"\"\"\n\n    @property\n    def text_area(self) -> VimTextArea:\n        \"\"\"The VimTextArea that was submitted.\"\"\"\n\nclass ModeChanged(Message):\n    \"\"\"Posted when vim mode changes.\"\"\"\n\n    @property\n    def mode(self) -> VimMode:\n        \"\"\"The new mode.\"\"\"\n\n    @property\n    def previous_mode(self) -> VimMode:\n        \"\"\"The previous mode.\"\"\"\n```\n\n### VimMode\n\nEnum representing vim modes:\n\n```python\nclass VimMode(Enum):\n    INSERT = \"insert\"     # Normal text input\n    COMMAND = \"command\"   # Vim command mode\n    VISUAL = \"visual\"     # Visual selection mode\n```\n\n### CSS Classes\n\nVimTextArea automatically applies CSS classes based on mode:\n\n- `.insert-mode` - Applied in INSERT mode\n- `.command-mode` - Applied in COMMAND mode\n- `.visual-mode` - Applied in VISUAL mode\n\n## Customization\n\n### Styling Modes\n\nCustomize the appearance of different modes with CSS:\n\n```python\nclass MyApp(App):\n    CSS = \"\"\"\n    VimTextArea.insert-mode {\n        border: solid green;\n        background: $surface;\n    }\n\n    VimTextArea.command-mode {\n        border: solid blue;\n        background: $surface;\n    }\n\n    VimTextArea.visual-mode {\n        border: solid yellow;\n        background: $boost;\n    }\n\n    VimTextArea {\n        height: auto;\n        max-height: 20;\n        min-height: 3;\n    }\n    \"\"\"\n```\n\n### Handling Events\n\nListen to vim-specific events:\n\n```python\ndef on_vim_text_area_mode_changed(self, event: VimTextArea.ModeChanged):\n    \"\"\"React to mode changes.\"\"\"\n    if event.mode == VimMode.INSERT:\n        self.sub_title = \"Editing\"\n    elif event.mode == VimMode.COMMAND:\n        self.sub_title = \"Command\"\n\ndef on_vim_text_area_submitted(self, event: VimTextArea.Submitted):\n    \"\"\"Handle text submission.\"\"\"\n    self.process_input(event.text)\n    event.text_area.clear()\n```\n\n### Extending VimTextArea\n\nSubclass to add custom functionality:\n\n```python\nfrom vimkeys_input import VimTextArea\nfrom textual import events\n\nclass HistoryVimTextArea(VimTextArea):\n    \"\"\"VimTextArea with input history navigation.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.input_history = []\n        self.history_index = -1\n\n    def add_to_history(self, text: str):\n        \"\"\"Add entry to history.\"\"\"\n        if text.strip():\n            self.input_history.append(text)\n\n    def on_key(self, event: events.Key) -> None:\n        \"\"\"Handle up/down for history navigation.\"\"\"\n        if event.key == \"up\" and self.cursor_location == (0, 0):\n            # Load previous history entry\n            if self.input_history and self.history_index < len(self.input_history) - 1:\n                self.history_index += 1\n                self.text = self.input_history[-(self.history_index + 1)]\n                event.prevent_default()\n                return\n\n        return super().on_key(event)\n```\n\n## Architecture\n\nVimTextArea is built using a modular mixin architecture:\n\n```\nVimTextArea (vim_textarea.py)\n\u251c\u2500\u2500 NavigationMixin (operations/navigation.py)\n\u2502   \u2514\u2500\u2500 Handles h, j, k, l, w, b, e, 0, $, gg, G\n\u251c\u2500\u2500 EditingMixin (operations/editing.py)\n\u2502   \u2514\u2500\u2500 Handles x, r, p, P, u, Ctrl+r, J\n\u251c\u2500\u2500 VisualMixin (operations/visual.py)\n\u2502   \u2514\u2500\u2500 Handles visual mode selection and operations\n\u251c\u2500\u2500 SearchMixin (operations/search.py)\n\u2502   \u2514\u2500\u2500 Handles /, n, N search operations\n\u251c\u2500\u2500 TextObjectMixin (operations/text_objects.py)\n\u2502   \u2514\u2500\u2500 Handles iw, aw, i\", a\", i(, a(, etc.\n\u251c\u2500\u2500 CountHandler (count_handler.py)\n\u2502   \u2514\u2500\u2500 Manages numeric count state\n\u251c\u2500\u2500 OperatorPendingState (operator_pending.py)\n\u2502   \u2514\u2500\u2500 Manages operator+motion combinations\n\u2514\u2500\u2500 MarksManager (marks.py)\n    \u2514\u2500\u2500 Manages position bookmarks\n\nAll built on Textual's TextArea widget\n```\n\nThis modular design keeps the codebase maintainable and extensible.\n\n## Development\n\n### Project Structure\n\n```\nvimkeys-input/\n\u251c\u2500\u2500 vimkeys_input/           # Main package\n\u2502   \u251c\u2500\u2500 __init__.py         # Public API exports\n\u2502   \u251c\u2500\u2500 vim_modes.py        # VimMode enum\n\u2502   \u251c\u2500\u2500 vim_textarea.py     # Main widget class\n\u2502   \u251c\u2500\u2500 count_handler.py    # Count support\n\u2502   \u251c\u2500\u2500 operator_pending.py # Operator+motion system\n\u2502   \u251c\u2500\u2500 marks.py            # Position bookmarks\n\u2502   \u2514\u2500\u2500 operations/         # Modular operation mixins\n\u2502       \u251c\u2500\u2500 __init__.py\n\u2502       \u251c\u2500\u2500 navigation.py   # Movement commands\n\u2502       \u251c\u2500\u2500 editing.py      # Edit commands\n\u2502       \u251c\u2500\u2500 visual.py       # Visual mode\n\u2502       \u251c\u2500\u2500 search.py       # Search functionality\n\u2502       \u2514\u2500\u2500 text_objects.py # Text object support\n\u251c\u2500\u2500 examples/               # Example applications\n\u2502   \u251c\u2500\u2500 01_spike.py\n\u2502   \u251c\u2500\u2500 02_simple_chat.py\n\u2502   \u2514\u2500\u2500 03_streaming_chat.py\n\u251c\u2500\u2500 tests/                  # Test suite (124 tests)\n\u251c\u2500\u2500 docs/                   # Additional documentation\n\u251c\u2500\u2500 plans/                  # Development planning docs\n\u251c\u2500\u2500 pyproject.toml         # Package configuration\n\u2514\u2500\u2500 README.md              # This file\n```\n\n### Running Tests\n\n```bash\n# Install development dependencies\nuv pip install -e \".[dev]\"\n\n# Run all tests\npytest tests/\n\n# Run specific test file\npytest tests/test_navigation.py\n\n# Run with coverage\npytest --cov=vimkeys_input tests/\n\n# Run with verbose output\npytest -v tests/\n```\n\n### Development Mode\n\nUse Textual's dev mode for live reload during development:\n\n```bash\ntextual run --dev examples/01_spike.py\n\n# Or with console for debugging\ntextual console\ntextual run --dev examples/01_spike.py\n```\n\n### Code Quality\n\nThe project uses:\n- **pytest** for testing\n- **ruff** for linting and formatting\n- **mypy** for type checking (planned)\n\n```bash\n# Format code\nruff format vimkeys_input/\n\n# Lint code\nruff check vimkeys_input/\n\n# Type check\nmypy vimkeys_input/\n```\n\n## Implementation Status\n\n### \u2705 Phase 0: Spike (Complete)\n- Basic structure and mode switching\n- Core navigation (hjkl)\n- Basic editing (x, dd, yy, p)\n- Visual selection\n- Example applications\n\n### \u2705 Phase 1: Refactoring (Complete)\n- Modular mixin architecture\n- Comprehensive test suite (124 tests, 82% passing)\n- All navigation commands\n- All mode transitions\n- Undo/redo integration\n\n### \u2705 Phase 2: Advanced Features (Complete)\n- Operator + motion system (dw, c$, y3j)\n- Count support (5j, 3dd, 2d3w)\n- Text objects (diw, ci\", da()\n- Marks system (ma, 'a, `a)\n- Search functionality (/, n, N)\n- Join lines (J)\n- Replace character (r)\n\n### \ud83d\udea7 Phase 3: Polish (In Progress)\n- Comprehensive documentation\n- Additional examples\n- Performance optimization\n- PyPI package preparation\n- Case toggle (~)\n- Line operations (S, C)\n- Visual line mode (V)\n\n## Contributing\n\nWe welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.\n\n### Quick Contribution Guide\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Make your changes\n4. Add tests for new functionality\n5. Ensure tests pass (`pytest tests/`)\n6. Commit with descriptive messages\n7. Push to your fork\n8. Open a Pull Request\n\n## Roadmap\n\n- [ ] Visual line mode (V)\n- [ ] Visual block mode (Ctrl+v)\n- [ ] Case change operations (~, gu, gU)\n- [ ] Line operations (S, C)\n- [ ] Repeat command (.)\n- [ ] Macros (q, @)\n- [ ] More text objects (sentences, paragraphs)\n- [ ] Ex commands (`:w`, `:q`)\n- [ ] Configuration system\n- [ ] Plugin architecture\n\n## FAQ\n\n**Q: Does this support all vim commands?**\nA: No, this implements core vim modal editing and the most common commands. It's designed for text input in terminal apps, not as a full vim replacement.\n\n**Q: Can I use this with Textual's built-in TextArea features?**\nA: Yes! VimTextArea extends TextArea, so features like syntax highlighting, themes, and language support all work.\n\n**Q: How do I disable vim mode temporarily?**\nA: Set `text_area.mode = VimMode.INSERT` to keep the widget in INSERT mode, or subclass and override key handling.\n\n**Q: Does this work on Windows?**\nA: Yes! VimTextArea works on all platforms supported by Textual (Linux, macOS, Windows).\n\n**Q: Can I customize which keys trigger which commands?**\nA: Currently no, but this is planned for a future release. For now, you can subclass and override key handlers.\n\n## License\n\nMIT License - see [LICENSE](LICENSE) file for details.\n\n## Credits\n\n- Built with [Textual](https://textual.textualize.io/) by [Textualize](https://www.textualize.io/)\n- Inspired by [Vim](https://www.vim.org/) by Bram Moolenaar\n- Created and maintained by contributors\n\n## Links\n\n- **Documentation**: [Full documentation](https://github.com/yourusername/vimkeys-input/docs)\n- **Examples**: [Example applications](https://github.com/yourusername/vimkeys-input/tree/main/examples)\n- **Issues**: [Bug reports and feature requests](https://github.com/yourusername/vimkeys-input/issues)\n- **Discussions**: [Community discussions](https://github.com/yourusername/vimkeys-input/discussions)\n\n---\n\nMade with \u2764\ufe0f for the Textual community\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Comprehensive Vim-style modal editing widget for Textual with operator+motion, text objects, and visual mode",
    "version": "0.3.2",
    "project_urls": {
        "Bug Tracker": "https://github.com/jcronq/textual-vimkeys-input/issues",
        "Changelog": "https://github.com/jcronq/textual-vimkeys-input/blob/main/CHANGELOG.md",
        "Documentation": "https://github.com/jcronq/textual-vimkeys-input/blob/main/README.md",
        "Homepage": "https://github.com/jcronq/textual-vimkeys-input",
        "Repository": "https://github.com/jcronq/textual-vimkeys-input"
    },
    "split_keywords": [
        "command-mode",
        " input",
        " keybindings",
        " modal-editing",
        " terminal",
        " text-editor",
        " textual",
        " tui",
        " vi",
        " vim",
        " visual-mode",
        " widget"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "60ccb1207ece30359ecd8c72a2d25cc5ee5f37c81dd8774e16b16b656f847e7c",
                "md5": "d41b6f45bb10c907f635d47e0463c639",
                "sha256": "8fad9616fdafe6e38065c72c87b2766957f573c694051a2990e7a2297b6210ee"
            },
            "downloads": -1,
            "filename": "textual_vimkeys_input-0.3.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d41b6f45bb10c907f635d47e0463c639",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.11",
            "size": 28978,
            "upload_time": "2025-11-02T02:51:38",
            "upload_time_iso_8601": "2025-11-02T02:51:38.861721Z",
            "url": "https://files.pythonhosted.org/packages/60/cc/b1207ece30359ecd8c72a2d25cc5ee5f37c81dd8774e16b16b656f847e7c/textual_vimkeys_input-0.3.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "8a0fc32bf6fdcf8bcef554d4366f3a360297bc362a64d858074d24b534310d75",
                "md5": "7bd6d260c21571e80f780a847bd588af",
                "sha256": "4edb1d0af5a4d107c4cb15de2279449b2e8a47f717eeb1b87a8ce2c940e9f0d8"
            },
            "downloads": -1,
            "filename": "textual_vimkeys_input-0.3.2.tar.gz",
            "has_sig": false,
            "md5_digest": "7bd6d260c21571e80f780a847bd588af",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.11",
            "size": 116436,
            "upload_time": "2025-11-02T02:51:40",
            "upload_time_iso_8601": "2025-11-02T02:51:40.539481Z",
            "url": "https://files.pythonhosted.org/packages/8a/0f/c32bf6fdcf8bcef554d4366f3a360297bc362a64d858074d24b534310d75/textual_vimkeys_input-0.3.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-11-02 02:51:40",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "jcronq",
    "github_project": "textual-vimkeys-input",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "textual-vimkeys-input"
}
        
Elapsed time: 1.95064s