vimtea

package module
v0.0.0-...-23a552a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 19, 2026 License: MIT Imports: 19 Imported by: 0

README

VimTea - Vim-like Text Editor for TUIs

VimTea is a lightweight, Vim-inspired text editor for the terminal, built with Go and the Bubble Tea TUI framework. It provides a modular, extensible foundation for building Vim-like text editors in your terminal applications.

Go Reference Go Report Card

VimTea Demo

Features

  • Multiple editing modes (Normal, Insert, Visual, Command)
  • Vim-like keybindings and commands
  • Line numbers (absolute and relative)
  • Count-based movement commands (e.g. 5j, 10k)
  • Undo/redo functionality
  • Visual mode selection (character and line-wise)
  • Command mode
  • Clipboard operations (yank, delete, paste)
  • Word operations
  • Operator-pending motions and text objects for d, c, and y
  • Extensible architecture
  • Custom key bindings
  • Customizable highlighting

Installation

go get github.com/eilifhl/vimtea

Code Structure

The codebase has been organized into modular components:

  • model.go: Main editor model and public interfaces
  • buffer.go: Text buffer with undo/redo operations
  • cursor.go: Cursor and text range operations
  • bindings.go: Key binding registry
  • commands.go: Command implementations
  • view.go: Rendering functions
  • highlight.go: Syntax highlighting
  • styles.go: UI style definitions

Usage

Basic Usage
package main

import (
    "log"

    tea "github.com/charmbracelet/bubbletea"
    "github.com/kujtimiihoxha/vimtea"
)

func main() {
    // Create a new editor with default options
    editor := vimtea.NewEditor(vimtea.WithFullScreen())

    // Run the editor
    p := tea.NewProgram(editor)
    if _, err := p.Run(); err != nil {
        log.Fatal(err)
    }
}
Load Content
package main

import (
    "log"

    tea "github.com/charmbracelet/bubbletea"
    "github.com/kujtimiihoxha/vimtea"
)

func main() {
    content := `This is a sample file
         with multiple lines
         for testing the editor`
    }

    // Create editor with content
    editor := vimtea.NewEditor(
        vimtea.WithContent(content),
        vimtea.WithFileName("example.txt"),
        vimtea.WithFullScreen(),
    )

    p := tea.NewProgram(editor)
    if _, err := p.Run(); err != nil {
        log.Fatal(err)
    }
}
Custom Key Bindings
package main

import (
    "log"

    tea "github.com/charmbracelet/bubbletea"
    "github.com/kujtimiihoxha/vimtea"
)

func main() {
    // Create editor
    editor := vimtea.NewEditor(vimtea.WithFullScreen())

    // Add custom binding
    editor.AddBinding(vimtea.KeyBinding{
        Key:         "ctrl+s",
        Mode:        vimtea.ModeNormal,
        Description: "Save file",
        Handler: func(b vimtea.Buffer) tea.Cmd {
            return vimtea.SetStatusMsg("File saved!")
        },
    })

    p := tea.NewProgram(editor)
    if _, err := p.Run(); err != nil {
        log.Fatal(err)
    }
}
Custom Commands
package main

import (
    "log"

    tea "github.com/charmbracelet/bubbletea"
    "github.com/kujtimiihoxha/vimtea"
)

func main() {
    // Create editor
    editor := vimtea.NewEditor(vimtea.WithFullScreen())

    // Add custom command
    editor.AddCommand("mysave", func(b vimtea.Buffer, args []string) tea.Cmd {
        return vimtea.SetStatusMsg("Custom save executed!")
    })

    p := tea.NewProgram(editor)
    if _, err := p.Run(); err != nil {
        log.Fatal(err)
    }
}
Custom Styling
package main

import (
    "log"

    tea "github.com/charmbracelet/bubbletea"
    "github.com/charmbracelet/lipgloss"
    "github.com/kujtimiihoxha/vimtea"
)

func main() {
    // Custom styles
    lineNumberStyle := lipgloss.NewStyle().
        Foreground(lipgloss.Color("#888888")).
        Background(lipgloss.Color("#222222")).
        PaddingRight(1)

    currentLineStyle := lipgloss.NewStyle().
        Foreground(lipgloss.Color("white")).
        Background(lipgloss.Color("#444444")).
        Bold(true).
        PaddingRight(1)

    cursorStyle := lipgloss.NewStyle().
        Background(lipgloss.Color("#CC8800")).
        Foreground(lipgloss.Color("black"))

    // Create editor with custom styles
    editor := vimtea.NewEditor(
        vimtea.WithLineNumberStyle(lineNumberStyle),
        vimtea.WithCurrentLineNumberStyle(currentLineStyle),
        vimtea.WithCursorStyle(cursorStyle),
        vimtea.WithRelativeNumbers(true),
        vimtea.WithFullScreen(),
    )

    p := tea.NewProgram(editor)
    if _, err := p.Run(); err != nil {
        log.Fatal(err)
    }
}

Default Key Bindings

Normal Mode
  • h, j, k, l: Basic movement (left, down, up, right)
  • Number prefixes: 5j, 10k: Move multiple lines at once
  • w: Move to next word start
  • b: Move to previous word start
  • W / B: Move by whitespace-delimited WORDs
  • e / E: Move to the end of the current word or WORD
  • ge / gE: Move to the end of the previous word or WORD
  • 0: Move to start of line
  • ^: Move to first non-whitespace character in line
  • $: Move to end of line
  • g_: Move to the last non-whitespace character in line
  • gg: Move to start of document
  • G: Move to end of document
  • %: Jump to the matching bracket
  • { / }: Move between paragraphs
  • f<char> / F<char>: Find a character forward or backward
  • t<char> / T<char>: Move just before or after a character
  • ; / ,: Repeat the last find motion
  • H / M / L: Move to the top, middle, or bottom visible line
  • i: Enter insert mode
  • a: Append after cursor
  • A: Append at end of line
  • I: Insert at start of line
  • v: Enter visual mode
  • V: Enter visual line mode
  • :: Enter command mode
  • x: Delete character at cursor
  • dd: Delete line
  • D: Delete from cursor to end of line
  • dw: Delete word
  • d0, d$, dgg, dG: Delete with motions
  • yy: Yank (copy) line
  • p: Paste after cursor
  • P: Paste before cursor
  • u: Undo
  • ctrl+r: Redo
  • o: Open line below and enter insert mode
  • O: Open line above and enter insert mode
  • diw: Delete inner word
  • yiw: Yank inner word
  • ciw: Change inner word
  • daw / caw / yaw: Around-word text object
  • di(: Delete inner parentheses
  • yi(: Yank inner parentheses
  • ci(: Change inner parentheses
  • da( / ca( / ya(: Around-parentheses text object
  • dib / yib / cib: Same inner-parentheses operations using b
  • cw: Change word
  • r<char>: Replace the character under the cursor
  • zr: Toggle relative line numbers
  • q: Quit
Insert Mode
  • esc: Return to normal mode
  • Arrow keys: Navigate
  • ctrl+a / ctrl+e: Jump to start or end of line
  • ctrl+b / ctrl+f: Move left or right one character
  • ctrl+left / ctrl+right: Move backward or forward by one word
  • ctrl+p / ctrl+n: Move up or down one line
  • ctrl+d / delete: Delete the character under the cursor
  • ctrl+k: Kill to end of line
  • ctrl+u: Kill to start of line
  • ctrl+w / alt+backspace: Delete previous word
  • alt+d / alt+delete: Delete next word
  • ctrl+y: Yank text back into the buffer
  • alt+y: Cycle through the kill ring after a yank
  • ctrl+t: Transpose adjacent characters
  • alt+t: Transpose adjacent words
  • ctrl+o: Open a new line without leaving insert mode
  • alt+b / alt+f: Move backward or forward by one word
  • alt+< / alt+>: Jump to the start or end of the document
  • Regular typing inserts text
Visual Mode
  • esc: Return to normal mode
  • h, j, k, l: Expand selection
  • y: Yank selection
  • d, x: Delete selection
  • p: Replace selection with yanked text
Command Mode
  • esc: Cancel command
  • enter: Execute command

Extending VimTea

VimTea is designed to be easily extendable. You can:

  1. Add custom key bindings with editor.AddBinding()
  2. Create new commands with editor.AddCommand()
  3. Modify the rendering style with custom style options
  4. Access buffer operations directly via the Buffer interface
  5. Create custom views by implementing the View interface
  6. Customize the editor appearance with style options (WithTextStyle, WithLineNumberStyle, WithCurrentLineNumberStyle, etc.)

Contributing

Contributions are welcome! Here's how you can contribute:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Please make sure to update tests as appropriate and follow the existing code style.

Development Workflow

  1. Clone the repository
  2. Install dependencies: go mod download
  3. Make your changes
  4. Format code: go fmt ./...
  5. Verify imports: goimports -w .
  6. Run the example: cd example && go run main.go
  7. Create tests for your changes

Architecture

VimTea follows a modular architecture centered around these core components:

  • Editor: The main interface that integrates all components
  • Buffer: Manages text content with undo/redo operations
  • Cursor: Handles positioning and selection
  • Bindings: Registers and manages key bindings
  • Commands: Implements editor commands (like Vim ex commands)
  • View: Renders the editor to the terminal

These components follow clean separation of concerns, making it easier to:

  • Add new features
  • Test individual components
  • Understand the codebase
  • Customize functionality

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Package vimtea provides a Vim-like text editor component for terminal applications

Package vimtea provides a Vim-like text editor component for terminal applications

Package vimtea provides a Vim-like text editor component for terminal applications

Package vimtea provides a Vim-like text editor component for terminal applications

Package vimtea provides a Vim-like text editor component for terminal applications

Package vimtea provides a Vim-like text editor component for terminal applications built with Bubble Tea. It supports normal, insert, visual, and command modes with key bindings similar to Vim.

Package vimtea provides a Vim-like text editor component for terminal applications

Package vimtea provides a Vim-like text editor component for terminal applications

Package vimtea provides a Vim-like text editor component for terminal applications built with Bubble Tea (github.com/charmbracelet/bubbletea).

Features

  • Vim-like modal editing with normal, insert, visual, and command modes
  • Familiar key bindings for Vim users (h,j,k,l navigation, d/y/p for delete/yank/paste, etc.)
  • Command mode with colon commands
  • Visual mode for selecting text
  • Undo/redo functionality
  • Line numbers (regular and relative)
  • Syntax highlighting
  • Customizable styles and themes
  • Extensible key binding system

Getting Started

Create a new editor with default settings:

editor := vimtea.NewEditor()

Or customize it with options:

editor := vimtea.NewEditor(
	vimtea.WithContent("Initial content"),
	vimtea.WithEnableStatusBar(true),
	vimtea.WithDefaultSyntaxTheme("catppuccin-macchiato"),
	vimtea.WithRelativeNumbers(true),
)

Use it in a Bubble Tea application:

p := tea.NewProgram(editor)
if _, err := p.Run(); err != nil {
	log.Fatal(err)
}

Extending Functionality

Add custom key bindings:

editor.AddBinding(vimtea.KeyBinding{
	Key:         "ctrl+s",
	Mode:        vimtea.ModeNormal,
	Description: "Save file",
	Handler: func(buf vimtea.Buffer) tea.Cmd {
		// Your save logic here
		return nil
	},
})

Add custom command:

editor.AddCommand("write", func(buf vimtea.Buffer, args []string) tea.Cmd {
	// Your save logic here
	return nil
})

Styling

Customize the appearance with style options:

customStyle := lipgloss.NewStyle().
	Foreground(lipgloss.Color("#FFFFFF")).
	Background(lipgloss.Color("#333333"))

editor := vimtea.NewEditor(
	vimtea.WithTextStyle(customStyle),
	vimtea.WithLineNumberStyle(numberStyle),
	vimtea.WithCursorStyle(cursorStyle),
)

Package vimtea provides a Vim-like text editor component for terminal applications

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SetStatusMsg

func SetStatusMsg(msg string) tea.Cmd

SetStatusMsg creates a command that sets the status message This can be used by external components to update the editor's status message

Types

type BindingRegistry

type BindingRegistry struct {
	// contains filtered or unexported fields
}

BindingRegistry manages key bindings for the editor It supports exact matches and prefix detection for multi-key sequences

func (*BindingRegistry) Add

func (r *BindingRegistry) Add(key string, cmd Command, mode EditorMode, help string)

Add registers a new key binding with the registry It automatically builds prefix maps for multi-key sequences

func (*BindingRegistry) FindExact

func (r *BindingRegistry) FindExact(keySeq string, mode EditorMode) *internalKeyBinding

FindExact looks for an exact match for the given key sequence in the specified mode It can handle numeric prefixes by ignoring them when looking for the command

func (*BindingRegistry) GetAll

func (r *BindingRegistry) GetAll() []internalKeyBinding

GetAll returns all registered key bindings

func (*BindingRegistry) GetForMode

func (r *BindingRegistry) GetForMode(mode EditorMode) []internalKeyBinding

GetForMode returns all key bindings for the specified mode

func (*BindingRegistry) IsPrefix

func (r *BindingRegistry) IsPrefix(keySeq string, mode EditorMode) bool

IsPrefix checks if the key sequence is a prefix of any registered binding This is used to determine if we should wait for more input

type Buffer

type Buffer interface {
	// Text returns the entire buffer content as a string
	Text() string

	// Lines returns all lines in the buffer as a string slice
	Lines() []string

	// LineCount returns the number of lines in the buffer
	LineCount() int

	// LineLength returns the length of the line at the given row
	LineLength(row int) int

	// VisualLineLength returns the visual length of the line at the given row
	// counting tabs as tabWidth spaces
	VisualLineLength(row int) int

	// InsertAt inserts text at the specified position
	InsertAt(row, col int, text string)

	// DeleteAt deletes text between the specified positions
	DeleteAt(startRow, startCol, endRow, endCol int)

	// Undo reverts the last change and returns a command with the new cursor position
	Undo() tea.Cmd

	// Redo reapplies a previously undone change
	Redo() tea.Cmd

	// CanUndo returns whether there are changes that can be undone
	CanUndo() bool

	// CanRedo returns whether there are changes that can be redone
	CanRedo() bool

	// Clear removes all content from the buffer and resets to empty state
	Clear() tea.Cmd
}

Buffer defines the interface for text buffer operations It provides methods for manipulating text content and undo/redo functionality

type Command

type Command func(m *editorModel) tea.Cmd

Command is a function that performs an action on the editor model and returns a bubbletea command

type CommandFn

type CommandFn func(Buffer, []string) tea.Cmd

CommandFn is a function that can be executed when a command is run in command mode It takes a buffer reference and command arguments, and returns a bubbletea command

type CommandMsg

type CommandMsg struct {
	Command string // Command name without arguments
}

CommandMsg is sent when a command is executed from command mode It contains the command name that should be looked up in the CommandRegistry

type CommandRegistry

type CommandRegistry struct {
	// contains filtered or unexported fields
}

CommandRegistry stores and manages commands that can be executed in command mode Commands are invoked by typing ":command" in command mode

func (*CommandRegistry) Get

func (r *CommandRegistry) Get(name string) Command

Get retrieves a command by name, returning nil if not found

func (*CommandRegistry) GetAll

func (r *CommandRegistry) GetAll() map[string]Command

GetAll returns all registered commands as a map

func (*CommandRegistry) Register

func (r *CommandRegistry) Register(name string, cmd Command)

Register adds a command to the registry with the given name

type Cursor

type Cursor struct {
	Row int // Zero-based line index
	Col int // Zero-based column index
}

Cursor represents a position in the text buffer with row and column coordinates

func (Cursor) Clone

func (c Cursor) Clone() Cursor

Clone creates a copy of the cursor

type Editor

type Editor interface {
	// Implements the bubbletea.Model interface
	tea.Model

	// AddBinding registers a new key binding
	AddBinding(binding KeyBinding)

	// AddCommand registers a new command that can be executed in command mode
	AddCommand(name string, cmd CommandFn)

	// GetBuffer returns the current buffer
	GetBuffer() Buffer

	// GetMode returns the current editor mode
	GetMode() EditorMode

	// SetMode changes the current editor mode
	SetMode(mode EditorMode) tea.Cmd

	// SetStatusMessage sets the status message displayed in the status bar
	SetStatusMessage(msg string) tea.Cmd

	// SetSize updates the editor's dimensions when the terminal window is resized
	SetSize(width, height int) (tea.Model, tea.Cmd)

	// Tick sends a tick message to the editor
	Tick() tea.Cmd

	// Reset restores the editor to its initial state
	Reset() tea.Cmd
}

Editor defines the interface for interacting with the editor component

func NewEditor

func NewEditor(opts ...EditorOption) Editor

NewEditor creates a new editor instance with the provided options

type EditorMode

type EditorMode int

EditorMode represents the current mode of the editor

const (
	// ModeNormal is the default mode for navigation and commands
	ModeNormal EditorMode = iota
	// ModeInsert is for inserting and editing text
	ModeInsert
	// ModeVisual is for selecting text
	ModeVisual
	// ModeCommand is for entering commands with a colon prompt
	ModeCommand
)

func (EditorMode) String

func (m EditorMode) String() string

String returns the string representation of the editor mode

type EditorModeMsg

type EditorModeMsg struct {
	Mode EditorMode
}

type EditorOption

type EditorOption func(*options)

EditorOption is a function that modifies the editor options

func WithBlinkInterval

func WithBlinkInterval(interval time.Duration) EditorOption

WithBlinkInterval sets the cursor blink interval duration

func WithCommandStyle

func WithCommandStyle(style lipgloss.Style) EditorOption

WithCommandStyle sets the style for the command line

func WithContent

func WithContent(content string) EditorOption

WithContent sets the initial content for the editor

func WithCurrentLineNumberStyle

func WithCurrentLineNumberStyle(style lipgloss.Style) EditorOption

WithCurrentLineNumberStyle sets the style for the current line number

func WithCursorStyle

func WithCursorStyle(style lipgloss.Style) EditorOption

WithCursorStyle sets the style for the cursor

func WithDefaultSyntaxTheme

func WithDefaultSyntaxTheme(theme string) EditorOption

WithDefaultSyntaxTheme sets the syntax highlighting theme Available themes include "catppuccin-macchiato" and others

func WithEnableModeCommand

func WithEnableModeCommand(enable bool) EditorOption

WithEnableModeCommand enables or disables command mode (:commands)

func WithEnableStatusBar

func WithEnableStatusBar(enable bool) EditorOption

WithEnableStatusBar enables or disables the status bar at the bottom

func WithFileName

func WithFileName(fileName string) EditorOption

WithFileName sets the filename for syntax highlighting

func WithFullScreen

func WithFullScreen() EditorOption

func WithLineNumberStyle

func WithLineNumberStyle(style lipgloss.Style) EditorOption

WithLineNumberStyle sets the style for line numbers

func WithRelativeNumbers

func WithRelativeNumbers(enable bool) EditorOption

WithRelativeNumbers enables or disables relative line numbering When enabled, line numbers show the distance from the current line

func WithSelectedStyle

func WithSelectedStyle(style lipgloss.Style) EditorOption

WithSelectedStyle sets the style for selected text

func WithStatusStyle

func WithStatusStyle(style lipgloss.Style) EditorOption

WithStatusStyle sets the style for the status bar

func WithTextStyle

func WithTextStyle(style lipgloss.Style) EditorOption

WithTextStyle sets the style for regular text

type KeyBinding

type KeyBinding struct {
	Key         string               // The key sequence to bind (e.g. "j", "dd", "ctrl+f")
	Mode        EditorMode           // Which editor mode this binding is active in
	Description string               // Human-readable description for help screens
	Handler     func(Buffer) tea.Cmd // Function to execute when the key is pressed
}

KeyBinding represents a key binding that can be registered with the editor This is the public API for adding key bindings

type TextRange

type TextRange struct {
	Start Cursor // Starting position (inclusive)
	End   Cursor // Ending position (inclusive)
}

TextRange represents a range of text with start and end positions

type UndoRedoMsg

type UndoRedoMsg struct {
	NewCursor Cursor // New cursor position after undo/redo
	Success   bool   // Whether the operation succeeded
	IsUndo    bool   // True for undo, false for redo
}

UndoRedoMsg is sent when an undo or redo operation is performed It contains the new cursor position and operation status

Directories

Path Synopsis
Example application demonstrating the use of vimtea This opens itself and provides a Vim-like interface to edit the file
Example application demonstrating the use of vimtea This opens itself and provides a Vim-like interface to edit the file

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL