portalis

package module
v0.1.0 Latest Latest
Warning

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

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

README

Portalis

A built-in terminal emulator for Go. Runs a shell in a PTY, parses ANSI output, and renders the result through Bubble Tea. Designed to be embedded into a host application that allocates a rectangle and forwards keyboard, mouse and resize events.

Features

  • PTY-backed session via creack/pty
  • ANSI/VT parser: CSI, OSC, SGR colors (16 / 256 / 24-bit), UTF-8
  • xterm-compatible key encoding (Ctrl/Alt/Shift/F-keys, application cursor mode)
  • DEC modes: ?1 application cursor, ?25 cursor visibility, ?1049 alt screen, ?2004 bracketed paste, ?2026 synchronized output
  • Editing sequences: ICH (CSI @), DCH (CSI P), ECH (CSI X), IL (CSI L), DL (CSI M), SU (CSI S), SD (CSI T), VPA (CSI d), HPA (CSI G)
  • Scroll regions, index/reverse index, DEC Special Graphics charset
  • OSC 7 working-directory tracking with callbacks
  • OSC 52 clipboard integration (macOS, Wayland, X11)
  • Synchronized output (CSI ? 2026 h/l)
  • Selection with mouse drag, scrollback up to 10 000 lines
  • Alt screen, bracketed paste, command history
  • Render dirty-cache and PTY output coalescing for performance
  • Framework-agnostic core: feed events, call View(w, h) to render

Installation

go get github.com/starframe-dev/portalis

Requires Go 1.22 or later.

Quick start

import (
    tea "github.com/charmbracelet/bubbletea"
    "github.com/starframe-dev/portalis"
)

em := portalis.NewEmulator("session-1", "build", "bash", []string{"-l"})

em.OnCWDChange = func(dir string) {
    fmt.Println("cwd:", dir)
}

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

The host calls Update(msg) with tea.KeyMsg, tea.MouseMsg, ResizeMsg, PtyOutputMsg, PtyExitMsg, and CursorBlinkMsg. Render any time with View(width, height).

Architecture

File Responsibility
emulator.go Top-level controller. Coordinates Screen, Parser and PTY.
screen.go 2D cell grid, scrollback, selection, rendering, dirty cache.
ansi.go ANSI/VT escape parser (CSI, OSC, SGR, UTF-8, DEC modes).
pty.go PTY spawn / read / write / resize / output coalescing.
clipboard.go OSC 52 with platform backends.
┌────────────────── Emulator ──────────────────┐
│  Parser  ──▶  Screen  ◀──  PTY (process I/O) │
└──────────────────────────────────────────────┘

Documentation

Project specs live under specs/ and per-file specs under code-specs/:

Testing

go test ./...
go test -race ./...

Unit tests cover ANSI sequences, screen operations, key encoding, and PTY resize/coalescing. Visual E2E with tmux is run via cuetty-cli (see cuetty-artifacts/portalis-tmux/).

Note: clipboard_mac_test.go runs only on macOS; other tests are portable.

License

MIT (see project root).


About this fork

This is a fork of starframe-dev/portalis.

The only change is the module path. Upstream declares itself as github.com/Starframe/portalis, which no longer resolves — that repository returns 404, so the module cannot be fetched by its own declared path and every consumer needs a replace directive. A replace makes go install example.com/consumer@latest fail outright, so live-pr depends on this fork instead.

No functional changes. If upstream corrects its module path and tags a release, this fork becomes unnecessary.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultShell

func DefaultShell() (string, []string)

DefaultShell returns a usable login shell, preferring absolute paths so it works even when the parent PTY provides a minimal or empty PATH.

func SendBytes

func SendBytes(p *Pty, data []byte) tea.Cmd

SendBytes sends raw bytes to the PTY.

Types

type Cell

type Cell struct {
	Rune         rune
	Combining    string
	FG           lipgloss.Color
	BG           lipgloss.Color
	Style        StyleBits
	Continuation bool
}

Cell is a single character cell on the terminal screen.

func (Cell) Empty

func (c Cell) Empty() bool

Empty returns true if the cell has no visible content.

type Cursor

type Cursor struct {
	Row   int
	Col   int
	FG    lipgloss.Color
	BG    lipgloss.Color
	Style StyleBits
}

Cursor represents the terminal cursor position.

type CursorBlinkMsg

type CursorBlinkMsg struct{}

CursorBlinkMsg is sent by the host to toggle the cursor blink state. A single host-level timer should broadcast this message to all visible terminal emulators so their cursors blink in sync.

type Emulator

type Emulator struct {
	SessionID string
	ChatName  string

	// Callbacks invoked when cwd or command history changes.
	OnCWDChange             func(string)
	OnCommandHistoryChanged func([]string)
	// contains filtered or unexported fields
}

Emulator is a built-in terminal emulator. It runs a shell/command in a PTY and renders the output. It is independent of any UI framework; hosts feed it keyboard/mouse/resize messages and call View(width, height) to render it.

func NewEmulator

func NewEmulator(sessionID, chatName, command string, args []string) *Emulator

func (*Emulator) Blur

func (e *Emulator) Blur()

Blur marks the emulator as blurred.

func (*Emulator) CWD

func (e *Emulator) CWD() string

CWD returns the current working directory of the PTY process. Falls back to InitialCWD if the PTY hasn't reported OSC 7 yet.

func (*Emulator) Close

func (e *Emulator) Close()

Close closes the PTY.

func (*Emulator) Focus

func (e *Emulator) Focus()

Focus marks the emulator as focused.

func (*Emulator) Listen

func (e *Emulator) Listen() tea.Cmd

Listen returns a command that waits for the next PTY output. At most one returned command may be waiting for a given Emulator.

func (*Emulator) Pty

func (e *Emulator) Pty() *Pty

Pty returns the underlying PTY for debug purposes.

func (*Emulator) SetCommandHistory

func (e *Emulator) SetCommandHistory(history []string)

SetCommandHistory restores a previously saved command history.

func (*Emulator) SetInitialCWD

func (e *Emulator) SetInitialCWD(dir string)

SetInitialCWD sets the directory in which the PTY process starts.

func (*Emulator) SetScrollbackLimit

func (e *Emulator) SetScrollbackLimit(limit int)

SetScrollbackLimit sets the maximum number of scrollback lines. Non-positive values disable the limit. Call before Start to take effect; calling after Start updates the existing screen immediately.

func (*Emulator) SetStartEnv

func (e *Emulator) SetStartEnv(env []string)

SetStartEnv records extra environment variables to use when the PTY is spawned without explicit env vars (Start, StartWithEnv(nil), StartSync(nil)). Must be called before the process starts; later calls have no effect on an already running PTY.

func (*Emulator) Start

func (e *Emulator) Start() tea.Cmd

Start begins spawning the PTY process. Returns PtyReadyMsg when done.

func (*Emulator) StartEnv

func (e *Emulator) StartEnv() []string

StartEnv returns the env vars recorded via SetStartEnv (nil when unset).

func (*Emulator) StartSync

func (e *Emulator) StartSync(extraEnv []string) error

StartSync spawns the PTY process synchronously with extra environment variables. Unlike StartWithEnv, it does not return a tea.Cmd — the PTY is ready immediately. Returns an error if spawning fails.

func (*Emulator) StartWithEnv

func (e *Emulator) StartWithEnv(extraEnv []string) tea.Cmd

StartWithEnv begins spawning the PTY process with extra environment variables.

func (*Emulator) Stop

func (e *Emulator) Stop()

Stop terminates the session and switches the panel to the idle ASCII art view.

func (*Emulator) Update

func (e *Emulator) Update(msg tea.Msg) tea.Cmd

Update handles messages.

func (*Emulator) View

func (e *Emulator) View(width, height int) string

View renders the terminal screen at the given panel size. The screen size is kept in sync by ResizeMsg from the warp layout engine; during a drag the panel is filled by warp's padContent and the real resize happens on release.

type Parser

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

Parser parses ANSI escape sequences and updates a Screen.

func NewParser

func NewParser(screen *Screen) *Parser

NewParser creates a new ANSI parser for the given screen.

func (*Parser) Feed

func (p *Parser) Feed(data []byte)

Feed feeds data into the parser.

func (*Parser) SetCWDCallback

func (p *Parser) SetCWDCallback(fn func(string))

SetCWDCallback sets the callback invoked when the working directory changes.

type Pty

type Pty struct {
	Output chan []byte
	Errors chan error
	// contains filtered or unexported fields
}

Pty wraps a pseudoterminal and forwards output to a channel.

func Spawn

func Spawn(command string, args []string, env ...string) (*Pty, error)

Spawn starts a new PTY with the given command, arguments and optional environment variables.

func SpawnInDir

func SpawnInDir(command string, args []string, dir string, env ...string) (*Pty, error)

SpawnInDir starts a new PTY in the given working directory.

func (*Pty) Close

func (p *Pty) Close() error

Close closes the PTY and kills the process.

func (*Pty) Listen

func (p *Pty) Listen(sessionID string) tea.Cmd

Listen returns a bubbletea command that waits for one ordered PTY read. readLoop already limits reads to 4 KiB; keeping that boundary prevents a 64 KiB Parser.Feed call from blocking the UI update loop.

func (*Pty) Resize

func (p *Pty) Resize(rows, cols int) error

Resize resizes the PTY and signals the child process about the change.

func (*Pty) Write

func (p *Pty) Write(data []byte) error

Write sends data to the PTY.

type PtyExitMsg

type PtyExitMsg struct {
	SessionID string
	Err       error
}

PtyExitMsg is sent when the PTY process exits.

type PtyOutputMsg

type PtyOutputMsg struct {
	SessionID string
	Data      []byte
}

PtyOutputMsg is sent when new data arrives from the PTY.

type PtyReadyMsg

type PtyReadyMsg struct {
	SessionID      string
	AlreadyRunning bool
}

PtyReadyMsg is sent when the PTY is ready for listening.

type RenderTickMsg

type RenderTickMsg struct{ SessionID string }

RenderTickMsg is kept for compatibility with hosts that may still route queued tick messages from an older emulator instance.

type ResizeMsg

type ResizeMsg struct {
	Width  int
	Height int
}

ResizeMsg is sent by a host when the emulator's allocated rectangle changes. It carries the content size in cells (without borders or padding).

type Screen

type Screen struct {
	Rows   int
	Cols   int
	Cells  [][]Cell
	Cursor Cursor

	CursorVisible      bool
	CursorBlinkVisible bool // toggled by emulator for blinking cursor
	// contains filtered or unexported fields
}

Screen is a 2D grid of cells.

func NewScreen

func NewScreen(rows, cols int) *Screen

NewScreen creates a new screen with the given dimensions. Scrollback is capped to defaultScrollbackLimit lines by default.

func (*Screen) Clear

func (s *Screen) Clear()

Clear clears the entire screen.

func (*Screen) ClearLine

func (s *Screen) ClearLine()

ClearLine clears the current line from cursor to end.

func (*Screen) ClearLineAll

func (s *Screen) ClearLineAll()

ClearLineAll clears the entire current line.

func (*Screen) ClearLineLeft

func (s *Screen) ClearLineLeft()

ClearLineLeft clears from start of line to cursor.

func (*Screen) ClearSelection

func (s *Screen) ClearSelection()

ClearSelection clears any active selection.

func (*Screen) ClearWrapPending

func (s *Screen) ClearWrapPending()

ClearWrapPending resets the wrap-pending flag.

func (*Screen) CursorBackward

func (s *Screen) CursorBackward(n int)

CursorBackward moves the cursor left n columns.

func (*Screen) CursorDown

func (s *Screen) CursorDown(n int)

CursorDown moves the cursor down n rows.

func (*Screen) CursorForward

func (s *Screen) CursorForward(n int)

CursorForward moves the cursor right n columns.

func (*Screen) CursorNextLine

func (s *Screen) CursorNextLine(n int)

CursorNextLine moves the cursor down n rows and to column 0.

func (*Screen) CursorPos

func (s *Screen) CursorPos() (row, col int)

CursorPos returns the current cursor position.

func (*Screen) CursorPrevLine

func (s *Screen) CursorPrevLine(n int)

CursorPrevLine moves the cursor up n rows and to column 0.

func (*Screen) CursorUp

func (s *Screen) CursorUp(n int)

CursorUp moves the cursor up n rows.

func (*Screen) DeleteChars

func (s *Screen) DeleteChars(n int)

DeleteChars deletes n cells at the cursor and shifts the remainder left.

func (*Screen) DeleteLines

func (s *Screen) DeleteLines(n int)

DeleteLines deletes n lines at the cursor inside the active region.

func (*Screen) EnterAltScreen

func (s *Screen) EnterAltScreen()

EnterAltScreen saves the current screen and clears it.

func (*Screen) EraseChars

func (s *Screen) EraseChars(n int)

EraseChars clears n cells starting at the cursor without shifting text.

func (*Screen) ExitAltScreen

func (s *Screen) ExitAltScreen()

ExitAltScreen restores the saved screen.

func (*Screen) ExtendSelection

func (s *Screen) ExtendSelection(row, col int)

ExtendSelection updates the selection end to the given cell.

func (*Screen) Index

func (s *Screen) Index()

Index moves the cursor down, scrolling the active region at its bottom.

func (*Screen) InsertChars

func (s *Screen) InsertChars(n int)

InsertChars inserts n blank cells at the cursor.

func (*Screen) InsertLines

func (s *Screen) InsertLines(n int)

InsertLines inserts n blank lines at the cursor inside the active region.

func (*Screen) LineText

func (s *Screen) LineText(row int) string

LineText returns the plain text of the given screen row, ignoring trailing empty cells. It is used to capture the current command line before sending Enter to the PTY.

func (*Screen) NextLine

func (s *Screen) NextLine()

NextLine moves to column zero on the next line.

func (*Screen) Put

func (s *Screen) Put(r rune)

Put writes a rune at the cursor position and advances the cursor by its terminal cell width. Wide runes occupy a base cell plus a continuation cell; combining runes extend the previous grapheme without moving the cursor.

func (*Screen) PutBytes

func (s *Screen) PutBytes(data []byte) int

PutBytes bulk-writes printable ASCII bytes to the screen with a single markDirty and without allocating a temporary slice. It mirrors Put's per-byte semantics (wrap at end of row, cursor advance, wrapPending) but does it in one tight pass instead of N function calls.

The caller must guarantee data is all printable ASCII (0x20..0x7e). Returns the number of bytes consumed. Callers that discover a non-printable byte must fall back to Put for that byte.

func (*Screen) Render

func (s *Screen) Render() string

Render returns the screen as a string. If the view is scrolled back, lines are taken from the scrollback buffer; otherwise the live screen is rendered.

func (*Screen) ResetView

func (s *Screen) ResetView()

ResetView resets the view offset so the live screen is shown.

func (*Screen) Resize

func (s *Screen) Resize(rows, cols int)

Resize resizes the screen, preserving existing content.

func (*Screen) RestoreCursor

func (s *Screen) RestoreCursor()

RestoreCursor restores the saved cursor position.

func (*Screen) ReverseIndex

func (s *Screen) ReverseIndex()

ReverseIndex moves the cursor up, scrolling the active region down at its top.

func (*Screen) SaveCursor

func (s *Screen) SaveCursor()

SaveCursor saves the current cursor position.

func (*Screen) ScrollBottom

func (s *Screen) ScrollBottom() int

ScrollBottom returns the bottom of the scroll region (0-indexed).

func (*Screen) ScrollRegionDown

func (s *Screen) ScrollRegionDown(n int)

ScrollRegionDown scrolls the active region down by n lines.

func (*Screen) ScrollRegionUp

func (s *Screen) ScrollRegionUp(n int)

ScrollRegionUp scrolls the active region up by n lines.

func (*Screen) ScrollTop

func (s *Screen) ScrollTop() int

ScrollTop returns the top of the scroll region (0-indexed).

func (*Screen) ScrollUp

func (s *Screen) ScrollUp()

ScrollUp scrolls the active region up by one line.

func (*Screen) ScrollViewDown

func (s *Screen) ScrollViewDown(n int)

ScrollViewDown moves the view down towards the live screen by n lines.

func (*Screen) ScrollViewUp

func (s *Screen) ScrollViewUp(n int)

ScrollViewUp moves the view up into the scrollback by n lines.

func (*Screen) SelectionText

func (s *Screen) SelectionText() []string

SelectionText returns the text currently highlighted by the selection, with each line as one string in the slice. Trailing whitespace trimmed. When dragging left on a single row, the column under the mouse (the lower col index) is excluded.

func (*Screen) SetCursor

func (s *Screen) SetCursor(row, col int)

SetCursor sets the cursor position (1-indexed in ANSI, 0-indexed here).

func (*Screen) SetScrollRegion

func (s *Screen) SetScrollRegion(top, bottom int)

SetScrollRegion sets the scrolling region (DECSTBM). top and bottom are 1-indexed (as sent by ANSI).

func (*Screen) SetScrollbackLimit

func (s *Screen) SetScrollbackLimit(limit int)

SetScrollbackLimit sets the maximum number of scrollback lines. A value of zero or less disables the limit (not recommended for long-running sessions).

func (*Screen) SetSync

func (s *Screen) SetSync(active bool)

SetSync enables or disables synchronized output (ESC[?2026h/l). While synchronized output is active, Render() returns the last committed frame so intermediate redraw states are not visible.

func (*Screen) StartSelection

func (s *Screen) StartSelection(row, col int)

StartSelection begins a selection at the given cell.

func (*Screen) ViewOffset

func (s *Screen) ViewOffset() int

ViewOffset returns how many lines the view is scrolled back.

type StyleBits

type StyleBits uint8

StyleBits holds text style attributes.

const (
	StyleBold StyleBits = 1 << iota
	StyleDim
	StyleItalic
	StyleUnderline
	StyleBlink
	StyleReverse
	StyleHidden
	StyleStrikethrough
)

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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