editor

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Feb 23, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// MaxLines is the maximum number of lines in a message (100 lines, 1-based indexing)
	MaxLines = 100
	// MaxLineLength is the maximum length of a line before word wrap (79 chars)
	MaxLineLength = 79
)
View Source
const (
	// WordStar navigation commands
	KeyCtrlE = 0x05 // Up
	KeyCtrlX = 0x18 // Down
	KeyCtrlS = 0x13 // Left
	KeyCtrlD = 0x04 // Right
	KeyCtrlW = 0x17 // Home (start of line)
	KeyCtrlP = 0x10 // End (end of line)
	KeyCtrlR = 0x12 // Page Up
	KeyCtrlC = 0x03 // Page Down

	// Command shortcuts (shown in footer: CTRL (A)Abort (Z)Save (Q)Quote)
	KeyCtrlA = 0x01 // Abort (formerly Word Left)
	KeyCtrlZ = 0x1A // Save
	KeyCtrlQ = 0x11 // Quote

	// Word navigation
	KeyCtrlF = 0x06 // Word Right

	// Edit commands
	KeyCtrlV = 0x16 // Toggle Insert/Overwrite
	KeyCtrlG = 0x07 // Delete character at cursor
	KeyCtrlT = 0x14 // Delete word
	KeyCtrlY = 0x19 // Delete line
	KeyCtrlJ = 0x0A // Join lines (also Enter/LF in some contexts)
	KeyCtrlN = 0x0E // Split line (new line)
	KeyCtrlB = 0x02 // Reformat paragraph
	KeyCtrlL = 0x0C // Redisplay screen

	// Special keys
	KeyEsc       = 0x1B // Escape
	KeyEnter     = 0x0D // Carriage Return
	KeyBackspace = 0x08 // Backspace
	KeyTab       = 0x09 // Tab
	KeyDelete    = 0x7F // Delete (DEL character)

	// Special internal codes for arrow keys (outside normal byte range)
	KeyArrowUp    = 0x100 // Internal code for up arrow
	KeyArrowDown  = 0x101 // Internal code for down arrow
	KeyArrowLeft  = 0x102 // Internal code for left arrow
	KeyArrowRight = 0x103 // Internal code for right arrow
	KeyPageUp     = 0x104 // Internal code for page up
	KeyPageDown   = 0x105 // Internal code for page down
	KeyHome       = 0x106 // Internal code for home
	KeyEnd        = 0x107 // Internal code for end
	KeyInsert     = 0x108 // Internal code for insert
	KeyDeleteKey  = 0x109 // Internal code for delete key
)

Special key codes for editor commands (using WordStar-style control characters)

Variables

View Source
var ErrIdleTimeout = errors.New("idle timeout")

ErrIdleTimeout is returned by ReadKeyWithTimeout when no input arrives before the caller-supplied deadline. It is distinct from the internal inter-byte escape-sequence timeout so that callers can handle user-visible idle disconnects without false-positive matches on sequence parsing.

Functions

func IsControlKey

func IsControlKey(key int) bool

IsControlKey returns true if the key is a control character

func IsPrintable

func IsPrintable(key int) bool

IsPrintable returns true if the key is a printable character

func KeyName

func KeyName(key int) string

KeyName returns a human-readable name for a key code

func RunEditor

func RunEditor(initialContent string, input io.Reader, output io.Writer, outputMode ansi.OutputMode) (content string, saved bool, err error)

RunEditor takes initial text, the input/output streams from the SSH session, the output mode (CP437 or UTF-8), runs a full-screen editor, and returns the final text content, whether it was saved, and any error.

func RunEditorWithMetadata

func RunEditorWithMetadata(initialContent string, input io.Reader, output io.Writer, outputMode ansi.OutputMode,
	subject, recipient, fromName string, isAnon bool, quoteFrom, quoteTitle, quoteDate, quoteTime string, quoteIsAnon bool, quoteLines []string, ih *InputHandler, ctx ...EditorContext) (content string, saved bool, err error)

RunEditorWithMetadata is an extended version that accepts message metadata. fromName is the sender display name shown in the @F@ header field: the user's handle by default, their real name when the area requires it, or the configured anonymous string when the user chose to post anonymously.

ih is an optional pre-created *InputHandler to share with the caller's reader. Pass nil to create a new one internally. Passing a shared InputHandler prevents the editor's goroutine from consuming bytes after the editor exits.

func TranslateToWordStar

func TranslateToWordStar(key int) int

TranslateToWordStar translates arrow keys to WordStar equivalents

Types

type CommandHandler

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

CommandHandler handles special slash commands

func NewCommandHandler

func NewCommandHandler(screen *Screen, buffer *MessageBuffer, menuSetPath, yesNoHi, yesNoLo, yesText, noText, abortText string) *CommandHandler

NewCommandHandler creates a new command handler

func (*CommandHandler) HandleAbort

func (ch *CommandHandler) HandleAbort(inputHandler *InputHandler) bool

HandleAbort handles the Abort command (CTRL-A). Displays a lightbar Yes/No confirmation in the last footer row (PromptRow). Returns true to signal abort and exit; false restores the footer row and continues.

func (*CommandHandler) HandleHelp

func (ch *CommandHandler) HandleHelp(inputHandler *InputHandler)

HandleHelp handles the /H (help) command Displays the help screen

func (*CommandHandler) HandleQuote

func (ch *CommandHandler) HandleQuote(inputHandler *InputHandler, currentLine, currentCol int) (int, int)

HandleQuote handles the Quote command (CTRL-Q). Follows Pascal flow: display message inline, prompt for line range, insert quote. Prompts appear in PromptRow (last footer row); footer is restored by the caller's FullRedraw.

func (*CommandHandler) HandleSave

func (ch *CommandHandler) HandleSave() bool

HandleSave handles the Save command (CTRL-Z). Returns true to signal save and exit; on false, an error is written to PromptRow.

func (*CommandHandler) HandleView

func (ch *CommandHandler) HandleView(inputHandler *InputHandler)

HandleView handles the /V (view) command Displays the current message (not fully implemented)

func (*CommandHandler) SetQuoteData

func (ch *CommandHandler) SetQuoteData(data *QuoteData)

SetQuoteData sets the message data to be used for the /Q quote command

func (*CommandHandler) ShowEscapeMenu

func (ch *CommandHandler) ShowEscapeMenu(inputHandler *InputHandler) CommandType

ShowEscapeMenu displays a lightbar selection menu at PromptRow when Escape is pressed. Items: Save, Abort, Edit (continue), Help, Quote. Returns the selected CommandType, or CommandNone to continue editing.

type CommandType

type CommandType int

CommandType represents a special editor command

const (
	CommandNone  CommandType = iota
	CommandSave              // /S - Save and exit
	CommandAbort             // /A - Abort editing
	CommandQuote             // /Q - Quote previous message
	CommandHelp              // /H or /? - Show help
	CommandView              // /V - View message (not implemented in this version)
)

type EditorContext

type EditorContext struct {
	NodeNumber int    // Current node number (@K@)
	NextMsgNum int    // Next message number in the area (@#@)
	ConfArea   string // "Conference > Area" combined string (@Z@)
}

EditorContext carries optional display-only metadata for the editor header. Pass this to RunEditorWithMetadata to populate @K@ (node), @#@ (msg num), and @Z@ (conference > area) placeholders in FSEDITOR.ANS.

type FSEditor

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

FSEditor is the full-screen message editor

func NewFSEditor

func NewFSEditor(session ssh.Session, terminal io.Writer, outputMode ansi.OutputMode,
	termWidth, termHeight int, menuSetPath, yesNoHi, yesNoLo, yesText, noText, abortText string,
	ih *InputHandler) *FSEditor

NewFSEditor creates a new full-screen editor instance. ih is an optional pre-created InputHandler to reuse (pass nil to create a new one). Passing a shared InputHandler prevents the editor's background goroutine from racing with the caller's reader for bytes after the editor exits.

func (*FSEditor) GetBuffer

func (e *FSEditor) GetBuffer() *MessageBuffer

GetBuffer returns the message buffer (for testing)

func (*FSEditor) GetContent

func (e *FSEditor) GetContent() string

GetContent returns the editor content

func (*FSEditor) HandleResize

func (e *FSEditor) HandleResize(newWidth, newHeight int)

HandleResize handles terminal resize events

func (*FSEditor) IsModified

func (e *FSEditor) IsModified() bool

IsModified returns whether the content was modified

func (*FSEditor) IsSaved

func (e *FSEditor) IsSaved() bool

IsSaved returns whether the message was saved

func (*FSEditor) LoadContent

func (e *FSEditor) LoadContent(content string)

LoadContent loads initial content into the editor

func (*FSEditor) Run

func (e *FSEditor) Run() (string, bool, error)

Run starts the editor main loop

func (*FSEditor) SetBoardName

func (e *FSEditor) SetBoardName(name string)

SetBoardName sets the BBS board name substituted into the footer @B@ placeholder.

func (*FSEditor) SetEditorContext

func (e *FSEditor) SetEditorContext(ctx EditorContext)

SetEditorContext sets optional context fields displayed in the editor header (node number, next message number, conference > area name).

func (*FSEditor) SetMetadata

func (e *FSEditor) SetMetadata(subject, recipient, fromName string, isAnon bool)

SetMetadata sets the message metadata (subject, recipient, sender, etc.)

func (*FSEditor) SetQuoteData

func (e *FSEditor) SetQuoteData(data *QuoteData)

SetQuoteData sets message data to be used for the /Q quote command

func (*FSEditor) SetTimezone

func (e *FSEditor) SetTimezone(configTZ string)

SetTimezone configures the timezone used for date/time display in the editor header.

type InputHandler

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

InputHandler handles keyboard input and escape sequence parsing. A background goroutine continuously reads raw bytes from the underlying reader into a buffered channel. This makes select-based timeouts reliable regardless of whether the reader supports SetReadDeadline (e.g. ssh.Session).

func NewInputHandler

func NewInputHandler(input io.Reader) *InputHandler

NewInputHandler creates a new input handler. A goroutine is started to read from input; it exits when input returns an error.

func (*InputHandler) Close

func (ih *InputHandler) Close()

Close stops the background read loop for handlers backed by sessions that support SetReadInterrupt. It is safe to call multiple times.

func (*InputHandler) CloseAndWait

func (ih *InputHandler) CloseAndWait()

CloseAndWait stops the background goroutine and blocks until it has fully exited (including the deferred setReadInterrupt(nil) call). This eliminates the race where a door's SetReadInterrupt call is overwritten by the outgoing handler's deferred cleanup. For sessions without SetReadInterrupt support (e.g. telnet), it returns immediately after signaling close.

func (*InputHandler) Read

func (ih *InputHandler) Read(p []byte) (int, error)

Read implements io.Reader. It reads exactly one byte from the incoming channel, blocking until a byte is available or the channel is closed (EOF). This allows InputHandler to be wrapped by bufio.NewReader and shared between callers (e.g. menu loops and the full-screen editor) so that the background goroutine's bytes are not lost when the editor returns.

func (*InputHandler) ReadKey

func (ih *InputHandler) ReadKey() (int, error)

ReadKey reads a single key, handling escape sequences. Returns an integer code (may be > 255 for special keys).

func (*InputHandler) ReadKeyTranslated

func (ih *InputHandler) ReadKeyTranslated() (int, error)

ReadKeyTranslated reads a key and translates arrow keys to WordStar commands

func (*InputHandler) ReadKeyWithTimeout

func (ih *InputHandler) ReadKeyWithTimeout(idleTimeout time.Duration) (int, error)

ReadKeyWithTimeout is identical to ReadKey but waits at most idleTimeout for the very first byte. If no input arrives within that window it returns (0, ErrIdleTimeout). Inter-byte timeouts for escape-sequence parsing are unaffected. This is the extensible primitive for idle-disconnect logic.

func (*InputHandler) SetSessionIdleTimeout

func (ih *InputHandler) SetSessionIdleTimeout(d time.Duration)

SetSessionIdleTimeout sets the session-level idle timeout applied to every ReadKey call. Pass 0 to disable. Thread-safe.

type MessageBuffer

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

MessageBuffer manages the text content of the message being edited

func NewMessageBuffer

func NewMessageBuffer() *MessageBuffer

NewMessageBuffer creates a new message buffer

func (*MessageBuffer) Clear

func (mb *MessageBuffer) Clear()

Clear clears the buffer

func (*MessageBuffer) DeleteChar

func (mb *MessageBuffer) DeleteChar(lineNum, col int) bool

DeleteChar deletes a character at the specified position (1-based)

func (*MessageBuffer) DeleteLine

func (mb *MessageBuffer) DeleteLine(lineNum int) bool

DeleteLine deletes a line at the specified position (1-based)

func (*MessageBuffer) GetCharAt

func (mb *MessageBuffer) GetCharAt(lineNum, col int) rune

GetCharAt returns the character at a specific position (1-based)

func (*MessageBuffer) GetContent

func (mb *MessageBuffer) GetContent() string

GetContent returns the entire buffer content as a string

func (*MessageBuffer) GetLastChar

func (mb *MessageBuffer) GetLastChar(lineNum int) rune

GetLastChar returns the last character on a line

func (*MessageBuffer) GetLine

func (mb *MessageBuffer) GetLine(lineNum int) string

GetLine returns the content of a line (1-based)

func (*MessageBuffer) GetLineCount

func (mb *MessageBuffer) GetLineCount() int

GetLineCount returns the current number of lines

func (*MessageBuffer) GetLineLength

func (mb *MessageBuffer) GetLineLength(lineNum int) int

GetLineLength returns the length of a line

func (*MessageBuffer) InsertChar

func (mb *MessageBuffer) InsertChar(lineNum, col int, ch rune) bool

InsertChar inserts a character at the specified position (1-based line and col)

func (*MessageBuffer) InsertLine

func (mb *MessageBuffer) InsertLine(lineNum int) bool

InsertLine inserts a new blank line at the specified position (1-based)

func (*MessageBuffer) IsLineEmpty

func (mb *MessageBuffer) IsLineEmpty(lineNum int) bool

IsLineEmpty returns true if a line is empty or contains only whitespace

func (*MessageBuffer) JoinLines

func (mb *MessageBuffer) JoinLines(lineNum int) bool

JoinLines joins the current line with the next line

func (*MessageBuffer) LoadContent

func (mb *MessageBuffer) LoadContent(content string)

LoadContent loads initial content into the buffer

func (*MessageBuffer) OverwriteChar

func (mb *MessageBuffer) OverwriteChar(lineNum, col int, ch rune) bool

OverwriteChar overwrites a character at the specified position (1-based)

func (*MessageBuffer) RemoveTrailingSpaces

func (mb *MessageBuffer) RemoveTrailingSpaces(lineNum int)

RemoveTrailingSpaces removes trailing spaces from a line

func (*MessageBuffer) SetLine

func (mb *MessageBuffer) SetLine(lineNum int, content string)

SetLine sets the content of a line (1-based)

func (*MessageBuffer) SplitLine

func (mb *MessageBuffer) SplitLine(lineNum, col int) bool

SplitLine splits a line at the specified column (1-based) Returns true if successful

type QuoteData

type QuoteData struct {
	From   string   // Message author
	Title  string   // Message subject/title
	Date   string   // Message date
	Time   string   // Message time
	IsAnon bool     // Anonymous flag
	Lines  []string // Message content lines
}

QuoteData holds message metadata for quoting

type Screen

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

Screen handles all screen rendering and ANSI control

func NewScreen

func NewScreen(terminal io.Writer, outputMode ansi.OutputMode, termWidth, termHeight int) *Screen

NewScreen creates a new screen manager

func (*Screen) ClearCache

func (s *Screen) ClearCache()

ClearCache clears the screen cache to force a full redraw on next refresh

func (*Screen) ClearEOL

func (s *Screen) ClearEOL()

ClearEOL clears from cursor to end of line

func (*Screen) ClearScreen

func (s *Screen) ClearScreen()

ClearScreen clears the entire screen and homes the cursor

func (*Screen) DisplayFooter

func (s *Screen) DisplayFooter()

DisplayFooter renders the footer template at the bottom of the terminal. Must be called after ClearScreen (e.g. within FullRedraw) to repaint the footer.

func (*Screen) DisplayHeader

func (s *Screen) DisplayHeader()

DisplayHeader displays the header template, then overlays dynamic row-4 fields.

func (*Screen) DisplayStatusLine

func (s *Screen) DisplayStatusLine(insertMode bool, currentLine, totalLines int)

DisplayStatusLine displays the status line at the bottom

func (*Screen) FullRedraw

func (s *Screen) FullRedraw(buffer *MessageBuffer, topLine, currentLine, currentCol int, insertMode bool)

FullRedraw performs a complete screen redraw

func (*Screen) GetEditingStartY

func (s *Screen) GetEditingStartY() int

GetEditingStartY returns the Y position where text editing begins

func (*Screen) GetScreenLines

func (s *Screen) GetScreenLines() int

GetScreenLines returns the number of available editing lines

func (*Screen) GoXY

func (s *Screen) GoXY(x, y int)

GoXY moves the cursor to the specified position (1-based)

func (*Screen) HasFooter

func (s *Screen) HasFooter() bool

HasFooter reports whether a footer template is loaded.

func (*Screen) LoadFooterTemplate

func (s *Screen) LoadFooterTemplate(menuSetPath string) error

LoadFooterTemplate loads and processes the FSEDITORF.ANS footer template. The footer is always 2 rows tall and is positioned at the bottom of the screen. Screen geometry (statusLineY) is adjusted to prevent the editing area from overwriting footer rows.

func (*Screen) LoadHeaderTemplate

func (s *Screen) LoadHeaderTemplate(menuSetPath, subject, recipient, fromName string, isAnon bool) error

LoadHeaderTemplate loads and processes the FSEDITOR.ANS template. fromName is the sender display name: handle, real name, or anonymous string.

func (*Screen) PromptRow

func (s *Screen) PromptRow() int

PromptRow returns the terminal row used for ephemeral command prompts (abort confirmation, save notice, quote range input, etc.). With a footer this is the last terminal row (the "ViSiON/3 Edit" tagline row), which is restored by DisplayFooter after the command completes. Without a footer it falls back to statusLineY.

func (*Screen) RefreshLine

func (s *Screen) RefreshLine(lineNum int, lineContent string, topLine int)

RefreshLine redraws a single line if it has changed

func (*Screen) RefreshScreen

func (s *Screen) RefreshScreen(buffer *MessageBuffer, topLine, currentLine, currentCol int, insertMode bool, forceStatusUpdate bool)

RefreshScreen redraws all visible lines (incremental update)

func (*Screen) Reposition

func (s *Screen) Reposition(currentLine, currentCol, topLine int)

Reposition moves cursor to the current editing position

func (*Screen) Resize

func (s *Screen) Resize(newWidth, newHeight int)

Resize handles terminal resize events

func (*Screen) ScrollDown

func (s *Screen) ScrollDown(lines int)

ScrollDown scrolls the display down by the specified number of lines

func (*Screen) ScrollUp

func (s *Screen) ScrollUp(lines int)

ScrollUp scrolls the display up by the specified number of lines

func (*Screen) UpdateDynamicFields

func (s *Screen) UpdateDynamicFields(insertMode bool, currentLine, totalLines int)

UpdateDynamicFields updates dynamic fields in the status line (like Insert/Line indicators)

func (*Screen) WriteDirect

func (s *Screen) WriteDirect(text string)

WriteDirect writes directly to the terminal (for special messages)

func (*Screen) WriteDirectProcessed

func (s *Screen) WriteDirectProcessed(text string)

WriteDirectProcessed writes directly to the terminal with pipe code processing

type WordWrapper

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

WordWrapper handles word wrapping and paragraph formatting

func NewWordWrapper

func NewWordWrapper(buffer *MessageBuffer) *WordWrapper

NewWordWrapper creates a new word wrapper

func (*WordWrapper) CheckAndWrap

func (ww *WordWrapper) CheckAndWrap(lineNum, col int) (int, int)

CheckAndWrap checks if the current line exceeds the maximum length and performs word wrapping if necessary Returns the new cursor position (line, col)

func (*WordWrapper) DeleteWord

func (ww *WordWrapper) DeleteWord(lineNum, col int) bool

DeleteWord deletes the word to the right of the cursor

func (*WordWrapper) FindWordLeft

func (ww *WordWrapper) FindWordLeft(lineNum, col int) int

FindWordLeft finds the start of the word to the left

func (*WordWrapper) FindWordRight

func (ww *WordWrapper) FindWordRight(lineNum, col int) int

FindWordRight finds the start of the word to the right

func (*WordWrapper) HandleBackspace

func (ww *WordWrapper) HandleBackspace(lineNum, col int) (int, int, bool)

HandleBackspace handles backspace at the given position May trigger line joining if at start of line Returns new cursor position (line, col)

func (*WordWrapper) HandleDelete

func (ww *WordWrapper) HandleDelete(lineNum, col int) bool

HandleDelete handles delete at the given position May trigger line joining if at end of line Returns whether delete was successful

func (*WordWrapper) IsAtWordBoundary

func (ww *WordWrapper) IsAtWordBoundary(lineNum, col int) bool

IsAtWordBoundary returns true if cursor is at a word boundary

func (*WordWrapper) ReformatParagraph

func (ww *WordWrapper) ReformatParagraph(startLine int) int

ReformatParagraph reformats a paragraph starting at the specified line This joins lines and redistributes words to maintain proper wrapping

Jump to

Keyboard shortcuts

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