composer

package
v0.0.14 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AppMode

type AppMode int

AppMode is the interaction mode the badge reports.

It is a copy of the app's InteractionMode rather than a shared type because the app imports this package and not the other way round. The mapping is one switch in internal/app with a test that walks every value, which is a cheaper coupling than a third package existing to hold four constants.

The badge REPORTS the mode; it never decides one. Decision 3: a badge that altered key routing would be a second source of truth for something Model.Mode() already derives.

const (
	// AppNormal: printable keys navigate. A browsing panel, or an overlay
	// that navigates rather than collects text.
	AppNormal AppMode = iota
	// AppInsert: printable keys type.
	AppInsert
	// AppVi: this composer is in vi editing and has returned to its
	// command state, so the next letter runs a vi command on the draft
	// (decision I-12). It shared NORMAL with the browsing panels for a
	// release while sharing none of their keys.
	AppVi
	// AppCommand: the palette owns the keyboard.
	AppCommand
)

func (AppMode) String

func (a AppMode) String() string

type AttachRequestedMsg

type AttachRequestedMsg struct{}

AttachRequestedMsg is emitted when the user asks to attach a file (Ctrl+T).

type AttachmentDiscardedMsg

type AttachmentDiscardedMsg struct {
	Path string
}

AttachmentDiscardedMsg is emitted when a pending attachment is dropped without being sent (Escape), so the owner can delete the spooled file.

type EditingMode

type EditingMode int

EditingMode selects which line-editing keymap the composer speaks. The default is ModeEmacs, so a Model that is never configured behaves exactly as it did before vi mode existed.

Only the composer is modal. Every other widgets.TextArea user (the search overlay, chatview's find bar, the auth prompts) keeps emacs semantics unconditionally — a one-line search field has nothing to gain from a normal mode, and Escape there already means "close the overlay".

const (
	// ModeEmacs is the readline keymap: ctrl+a/e move to the ends of the
	// line, ctrl+b/f a character, ctrl+k/u kill to either end, ctrl+w kills
	// the previous word, ctrl+d deletes forward.
	ModeEmacs EditingMode = iota
	// ModeVi is the modal keymap: Escape leaves insert mode for a normal
	// mode with h/j/k/l, w/b, 0/$, x, dd, D and i/a/A/o/O.
	ModeVi
)

func EditingModeFor

func EditingModeFor(name string) EditingMode

EditingModeFor maps a configured keymap name onto an EditingMode. It is the bridge from config.ResolveComposeEditing (which returns "emacs" or "vi") and, being total, treats anything it does not recognise as emacs rather than leaving the composer in a mode the user did not ask for.

func (EditingMode) String

func (e EditingMode) String() string

type MessageSubmittedMsg

type MessageSubmittedMsg struct {
	ChatId        int64
	Text          string
	ReplyToId     int64
	EditMessageId int64
	Attachment    string // local file path, empty if none
	AsPhoto       bool   // send the attachment as an inline photo, not a document
}

MessageSubmittedMsg is emitted when the user submits a message.

type Mode

type Mode int

Mode represents the composer's current mode.

const (
	ModeNormal Mode = iota
	ModeReply
	ModeEdit
)

type Model

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

Model is the message composer component.

func New

func New(r theme.Roles) Model

New creates a new composer model.

func (Model) Attachment

func (m Model) Attachment() string

Attachment returns the pending attachment path, empty when there is none.

func (Model) ChatId

func (m Model) ChatId() int64

ChatId returns the chat the composer is currently sending to, 0 when none.

func (Model) Draft

func (m Model) Draft() string

Draft is the text currently in the composer, for a host that needs to see what was typed rather than only whether anything was.

func (Model) DraftChats

func (m Model) DraftChats() map[int64]bool

DraftChats is the set of chats holding parked drafts, for the chat list.

func (Model) EditingMode

func (m Model) EditingMode() EditingMode

EditingMode reports the composer's current line-editing keymap.

func (*Model) EnterEditMode

func (m *Model) EnterEditMode(messageID int64, currentText string) string

EnterEditMode starts editing a message. An edit cannot carry media, so any pending attachment is discarded and its path returned for the caller to delete.

The draft on screen is PARKED first (decision I-3): loading the message text over it used to destroy whatever was half-written, without a confirm and without a way back. Cancelling the edit or sending it puts the draft back — see unparkEdit.

func (*Model) EnterReplyMode

func (m *Model) EnterReplyMode(messageID int64, previewText string)

EnterReplyMode starts replying to a message.

func (Model) Expanded

func (m Model) Expanded() bool

Expanded reports whether the eight-row form is showing.

func (Model) HasDraft

func (m Model) HasDraft() bool

HasDraft reports whether the composer holds unsent text.

It exists for app.go's quit-confirm rule: "q" from a browsing panel quits outright, unless there is work in the composer to lose, in which case it asks first. Whitespace alone is not work, so it is trimmed.

Not replaceable by IsComposing: that answers a different question (does Escape belong to the composer) and is true for reply/edit mode and, in vi mode, for insert mode — regardless of whether anything has been typed.

func (Model) HasDraftFor

func (m Model) HasDraftFor(chatID int64) bool

HasDraftFor reports whether a chat has unsent work parked. The chat list shows it in the preview row.

The open chat never has one: restoreDraft consumed its entry on the way in, because its draft is on screen in the composer rather than parked. Saying "draft: saved locally" about the thing you are looking at would tell the reader nothing and cost them the message preview.

func (Model) IsComposing

func (m Model) IsComposing() bool

IsComposing reports whether Escape belongs to the composer rather than to app.go's focus-back handler: reply/edit mode or a pending attachment needs clearing first, and in vi mode an Escape pressed in insert mode has to reach the composer so it can switch to normal mode.

func (Model) IsEditing

func (m Model) IsEditing() bool

IsEditing reports whether the composer is editing an existing message. Attachments cannot be added to an edit.

func (Model) IsViNormalMode

func (m Model) IsViNormalMode() bool

IsViNormalMode reports whether vi mode is active and currently in normal mode. Exposed for the status/hint rendering and for tests.

func (*Model) Reset

func (m *Model) Reset()

Reset clears the composer state, text included.

func (Model) Rows

func (m Model) Rows() int

Rows is how many terminal rows the composer needs.

The host asks before computing the layout, so the thread gives up exactly the rows the composer is about to use — a composer that rendered more rows than it asked for would push the bottom of the history off screen, and one that rendered fewer would leave a hole. Rows is how many rows the composer WANTS: one to type into, and one each for the context above it.

What it wants, not what it was given — the layout budgets from this, and a Rows that reported its own budget back would be a loop that settles on one row and never grows. What it actually DRAWS is bounded separately; see View.

func (*Model) SetAttachment

func (m *Model) SetAttachment(path string, asPhoto bool) string

SetAttachment sets the pending attachment path shown above the composer. asPhoto requests that an image be sent inline rather than as a document. It returns the attachment it replaced (empty when there was none) so the caller can clean up a spool file that is no longer referenced.

func (*Model) SetChatId

func (m *Model) SetChatId(chatID int64) string

SetChatId switches the composer to another chat, parking the current chat's draft and restoring that chat's own (decision 13).

It used to discard: the draft and any staged attachment went in the bin and the path came back for cleanup. That made the chat list unusable while half-way through a message — checking who else had written cost you what you had typed — so switching now costs nothing. Drafts live for the session only and are never synced to Telegram, so nothing here can surprise another client.

The return value is the attachment path the caller should delete. It is now always empty, since nothing is displaced; the signature is unchanged so callers keep cleaning up after the paths that ARE dropped, by Escape and by entering edit mode.

func (*Model) SetEditingMode

func (m *Model) SetEditingMode(mode EditingMode)

SetEditingMode selects the composer's line-editing keymap. Switching to vi starts in insert mode, which is what a chat composer wants: typing a message is the common case and a normal-mode landing would swallow the first word.

func (*Model) SetExpanded

func (m *Model) SetExpanded(on bool)

SetExpanded switches between the inline row and the eight-row form.

func (*Model) SetFocused

func (m *Model) SetFocused(focused bool)

SetFocused sets focus state. SetFocused moves the keyboard into or out of the composer.

Arriving in the composer puts vi mode back into INSERT. The reason is the one SetEditingMode already gives for starting there: typing a message is the common case, and a normal-mode landing swallows the first word — but the state was only initialised once, so the composer remembered whatever mode it was left in.

A vi user leaves the composer with two escapes: the first goes to normal mode, the second gives the panel back. So the composer was ALWAYS in normal mode by the time it was next entered, and r, e and i each landed there. Pressing r and typing "abc" produced "bc" — the "a" was vi's append. ctrl+j did not insert a newline for the same reason, deliberately (see isNewlineChord); ctrl+o worked because it is handled before the modal dispatch, which is why the external editor looked like the only way in.

Only on the transition, so a setFocus while already focused cannot pull a vi user out of normal mode mid-command.

func (*Model) SetMode

func (m *Model) SetMode(mode AppMode)

SetMode tells the composer which interaction mode to report for the states it cannot see itself: COMMAND, and the NORMAL of another panel holding focus. The app resolves it; see internal/app/mode.go.

func (*Model) SetNotice

func (m *Model) SetNotice(notice string)

SetNotice shows a transient message on the composer's hint line.

func (*Model) SetParseMarkdown

func (m *Model) SetParseMarkdown(on bool)

SetParseMarkdown tells the composer whether outgoing markdown parsing is on, which decides both the "md" label and whether the expanded form's preview shows parsed text or the text verbatim.

The label is only honest when it reflects the setting: showing "md" with parsing off would promise a transformation that will not happen.

func (*Model) SetRoles

func (m *Model) SetRoles(r theme.Roles)

SetRoles supplies the TUI 2.0 semantic palette.

func (*Model) SetSize

func (m *Model) SetSize(width, height int)

SetSize sets the component dimensions.

func (Model) Update

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd)

Update handles messages.

Key dispatch matches on tea.KeyPressMsg.Keystroke() for every chord, and on String() only for the unmodified printables that drive vi's normal mode. String() returns Key.Text whenever the terminal attached any: a Kitty-protocol shift+enter arrives as CSI 13;2;13u, so String() is "\r" while Keystroke() is "shift+enter". Matching the newline chord on String() would have made it invisible in exactly the terminals that can report it. See the keyPress doc comment in internal/app/keymap.go.

func (Model) View

func (m Model) View() string

View renders the composer as exactly Rows() lines, each exactly the panel width.

type PasteRequestedMsg

type PasteRequestedMsg struct{}

PasteRequestedMsg is emitted when the user asks to attach whatever image is on the system clipboard (Ctrl+V).

type ResizedMsg

type ResizedMsg struct{}

ResizedMsg is emitted when the composer's row count changes, so the host can recompute the layout before the next paint.

The composer cannot resize itself: the rows it takes come out of the thread's budget, and only the host knows what the rest of the screen is doing. Emitting rather than assuming is what keeps the two from disagreeing about where the composer starts.

Jump to

Keyboard shortcuts

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