Documentation
¶
Index ¶
- type Keys
- type LoadMoreHistoryMsg
- type MediaPlayMsg
- type MessageActionMsg
- type Model
- func (m Model) ActiveKeys() Keys
- func (m *Model) ApplyMedia(cfg config.MediaConfig)
- func (m *Model) ApplyStorage(cfg config.StorageConfig)
- func (m *Model) ApplyUI(cfg config.UIConfig)
- func (m Model) ChatId() int64
- func (m *Model) ClickAt(row int) bool
- func (m Model) DownloadCmd() tea.Cmd
- func (m Model) HasSearchResults() bool
- func (m *Model) LazyMediaCmd() tea.Cmd
- func (m *Model) MarkLoadedForTest()
- func (m *Model) MarkReadCmd() tea.Cmd
- func (m *Model) OpenChat(chatID int64, title string) tea.Cmd
- func (m *Model) OpenChatAt(chatID int64, title string, targetMsgID int64) tea.Cmd
- func (m Model) OpenExternallyCmd() tea.Cmd
- func (m *Model) OpenFind()
- func (m Model) OverlayPhotoCmd() tea.Cmd
- func (m Model) PlayVoiceCmd() tea.Cmd
- func (m *Model) ScrollByLines(n int)
- func (m Model) SearchActive() bool
- func (m *Model) SetBufferIndex(n int)
- func (m *Model) SetFocused(focused bool)
- func (m *Model) SetKeys(k Keys)
- func (m *Model) SetMyUserId(id int64)
- func (m *Model) SetReservedKeys(reserved []string)
- func (m *Model) SetSize(w, h int)
- func (m Model) TargetMessageId() int64
- func (m Model) Update(msg tea.Msg) (Model, tea.Cmd)
- func (m Model) View() string
- func (m Model) YankCmd() tea.Cmd
- type OpenPhotoMsg
- type OpenedPhotoMsg
- type ScrollToBottomMsg
- type YankMsg
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Keys ¶
Keys is the subset of config.KeyConfig that chatview consults directly. chatview cannot import internal/config or internal/app (app imports this package), so the caller — internal/app, in New() — is responsible for reading config.toml's keys table, running each field through config.NormalizeKey, and passing the result here via SetKeys.
A zero/empty field means "keep the built-in default", so a Model that never has SetKeys called behaves exactly as it did before chatview became configurable (New calls SetKeys(Keys{}) itself for exactly this reason).
Every field is a mnemonic and every field REPLACES its built-in letter (decision I-13). The motion fields that used to live here — ScrollUp/ScrollDown/PageUp/PageDown, which ADDED a spelling alongside j/k, the arrows and the page keys — are gone with them: motions are vi's, and a second semantic in one table cost more to explain than the configurability was worth.
type LoadMoreHistoryMsg ¶
type LoadMoreHistoryMsg struct {
ChatId int64
}
type MediaPlayMsg ¶
type MediaPlayMsg struct {
Status string // "playing", "downloading", "error", "opened"
Info string
}
MediaPlayMsg is sent when media playback starts.
type MessageActionMsg ¶
type Model ¶
type Model struct {
// contains filtered or unexported fields
}
func (Model) ActiveKeys ¶
ActiveKeys reports what handleKey actually matches right now, after SetKeys's defaulting and collision resolution — not the raw Keys a caller last passed in. This is the source of truth for anything outside this package that advertises a chatview binding (the "?" help card, the hint bar, and so on): it exists precisely so nothing has to reimplement SetKeys's collision rule to stay honest. A caller that instead re-derived a "resolved" reply by re-running the config through its own defaulting logic could end up advertising a binding that collided and was dropped — e.g. showing "j" for reply after a user sets reply = "j", when the panel still (correctly) matches "r". Always read this instead.
A field comes back as the single spelling that is live: the configured value if it was accepted, the built-in letter if it fell back to that, or "" if SetKeys's pass 3 found the action UNREACHABLE. A caller rendering a help card should show "" as "unbound", not as an empty row that looks like an oversight.
func (*Model) ApplyMedia ¶
func (m *Model) ApplyMedia(cfg config.MediaConfig)
ApplyMedia applies media config: image protocol and bubble size, external players, and photo auto-download. Voice notes download on play regardless of AutoDownloadVoice; they are not eagerly prefetched.
func (*Model) ApplyStorage ¶
func (m *Model) ApplyStorage(cfg config.StorageConfig)
ApplyStorage takes the [storage] settings this panel needs: where `s` saves to. The media cache is the client's business, not this panel's.
func (*Model) ApplyUI ¶
ApplyUI applies the [ui] settings the renderer needs: the inline-image policy and whether links carry OSC 8. The rail preference is the host's own business.
func (*Model) ClickAt ¶ added in v0.0.13
ClickAt moves the cursor to the message drawn on a panel-local row, and reports whether it moved anything.
A mouse user had no way to choose what r, y or + would act on: a click focused the panel and left the cursor wherever the keyboard had put it (decision I-11). The row is resolved through the same line index View draws from, so what the click selects is what is under the pointer.
A click on the header, on a day or unread divider, or on the blank space above a short history moves nothing. Those are not messages, and moving the cursor to the nearest one instead would be a guess the reader did not make.
func (Model) DownloadCmd ¶
DownloadCmd and OpenExternallyCmd are the media overlay's two other keys, exported because while the overlay is up the host owns the keyboard and the chat view never sees the press. They are the same actions s and o perform in the thread, deliberately: the overlay's hint row advertises the same letters, and two spellings of "save this" would be two things to keep in agreement.
func (Model) HasSearchResults ¶
HasSearchResults reports whether a completed in-chat search still holds hits, i.e. whether n/N are meaningful.
func (*Model) LazyMediaCmd ¶
LazyMediaCmd starts the thumbnail download for any photo that the current scroll position brought near the viewport but that the open-time prefetch cap skipped. The keyboard scroll handlers call it themselves; it is exported for the host's mouse-wheel path, which drives ScrollByLines and would otherwise never trigger a lazy load.
func (*Model) MarkLoadedForTest ¶
func (m *Model) MarkLoadedForTest()
MarkLoadedForTest puts the panel into the state a finished history load puts it in, for messages a test has already put in the store.
historyLoadedMsg is unexported and arrives from a command this component starts itself, so a test in another package cannot get the panel past its "Loading messages..." line without a seam. Same reason and same shape as chatlist.MarkLoadedForTest.
func (*Model) MarkReadCmd ¶
MarkReadCmd marks the open chat read up to its newest loaded message, without moving the scroll position — the point of an explicit mark-read is to clear the badge while you keep reading where you are.
Unlike the automatic read receipts in Update, this does NOT wait for terminal focus. Those are inferred from "the user is looking at it", which is only true when the terminal has focus; this one was asked for explicitly, so deferring it would just make the command look broken.
Returns nil when there is nothing to mark, so a caller can treat a nil Cmd as "no chat open or no messages loaded".
func (*Model) OpenChatAt ¶
OpenChatAt opens a chat and, once history has loaded, scrolls so that targetMsgID is visible (roughly centred). targetMsgID 0 means "newest message at the bottom", i.e. plain OpenChat behaviour. If the target is not in the first page, up to maxTargetPages further pages are fetched backwards; if it is still not found the view settles at the oldest loaded message and a notice is shown in the header.
func (Model) OpenExternallyCmd ¶
func (*Model) OpenFind ¶
func (m *Model) OpenFind()
OpenFind opens the in-chat search input, exactly as the panel's own ctrl+f binding does. It exists so the host can route a contextual binding (e.g. "/" pressed with the chat view focused) straight into this component with a method call. Re-emitting a synthetic ctrl+f key event through the command loop is the alternative, and it livelocks the moment a user configures keys.search = "ctrl+f": the forwarded key re-matches the host binding, which forwards it again, forever.
A no-op when the input is already open (so a repeated press does not wipe a half-typed query) or when no chat is open (there is nothing to search, and the input would render over the placeholder view).
func (Model) OverlayPhotoCmd ¶
OverlayPhotoCmd raises the media overlay on the cursored message and downloads the picture for it.
Photos only. A video, a document or a voice note has no in-terminal representation this client can draw, so those keep the existing behaviour of handing the file to the platform — an overlay that says "cannot draw this" is worse than the thing that already works.
Returns nil when the cursor is not on a photo, which is the host's signal to fall through to that existing behaviour.
func (Model) PlayVoiceCmd ¶
PlayVoiceCmd plays the cursored voice note or audio message.
Voice only, deliberately. Space is a big, easy key and the point of it is that a voice note is one press away; making it also open documents and spawn video players would make "the big key" mean "do whatever this message implies", which is what enter is for.
func (*Model) ScrollByLines ¶
ScrollByLines scrolls the message view by n lines (positive = up/older).
func (Model) SearchActive ¶
SearchActive reports whether the in-chat search input is open. While it is, every key belongs to the input: the host must route input events to this panel without consuming them first (esc, quick-type, etc.).
func (*Model) SetBufferIndex ¶
SetBufferIndex tells the header which row of the chat list this thread is. Zero clears it.
func (*Model) SetFocused ¶
func (*Model) SetKeys ¶
SetKeys resolves and stores the bindings handleKey consults. See SetReservedKeys's doc comment for the app-level surface this also protects — call that first if the caller has one.
Resolution runs in three passes over a single "claimed" set, so that explicit configuration always outranks a built-in default regardless of field order, and a collision never produces a silent double-bind:
- Claim chatViewFixedKeys() and every key from SetReservedKeys. These are claimed unconditionally, before anything from k is considered.
- For each field in order, if it was EXPLICITLY configured (non-empty), accept it and claim it — unless it is already claimed, in which case the configured value is rejected outright (not partially honored) and the field is treated as unconfigured for pass 3.
- For any field that did not get an accepted binding in pass 2, try its built-in letter ("r"/"e"/"d"/"m"). If that letter is itself already claimed — by a fixed key, a reserved key, or another field's pass-2 binding — the action is UNREACHABLE this call: it is left as "" rather than bound anyway, and ActiveKeys reports the same "" so a caller can advertise it honestly as unbound instead of lying about which key fires it. This never happens with an unconfigured Keys{}, since r/e/d/m never collide with each other or with anything in chatViewFixedKeys().
Two collisions this resolves that a naive single-pass, field-order resolver got wrong:
- reply = "e" (edit_message left at its default "e") used to leave both reply and edit bound to "e", with edit silently unreachable because reply's switch case came first — and nothing reported it. Now edit resolves to "" (unreachable) instead of double-binding.
- edit_message = "r" (reply left at its default "r") used to be silently discarded, because reply's default claimed "r" before edit_message's explicit config was ever considered. Now pass 2 runs every explicit config before pass 3 tries any default, so the explicit edit_message = "r" wins and reply falls back to "" instead (there is no other letter for it to try).
func (*Model) SetMyUserId ¶
func (*Model) SetReservedKeys ¶
SetReservedKeys tells chatview which keys are already claimed by bindings outside this package — every app-level key (quit, panel focus, tab, i/c into the composer, h/l panel movement, /, ?, and so on) — so SetKeys's collision resolution can refuse to accept a configured mnemonic that would silently shadow one of them. Without this, e.g. reply = "q" would be accepted, advertised on the help card as Reply, and quit the app the moment it fired, because chatview had no way to know app.go had already claimed "q".
Call order: SetReservedKeys must be called before SetKeys, since SetKeys resolves and freezes the claimed-key set immediately when it runs. Calling SetKeys without ever calling SetReservedKeys first is safe: the app-level surface is simply treated as empty, which is this package's original, pre-reservation behavior.
func (*Model) SetSize ¶
SetSize resizes the view. A width change invalidates every cached bubble, since bubbles are laid out for a specific panel width.
func (Model) TargetMessageId ¶
TargetMessageId is the message the panel is still trying to scroll to, or 0 when it is simply showing the newest.
Exported for the callers that OPEN a chat at a message — a search result, a channel post's discussion — because "did it aim at the right message" is otherwise only observable after the history has loaded and scrolled, which is two round trips a test does not have.
func (Model) YankCmd ¶
YankCmd copies the cursored message's text to the system clipboard.
The message's TEXT, exactly as Telegram sent it — not the rendered body. The render has a gutter, wraps at the pane width, and draws a code block inside a frame; none of that is wanted in a paste buffer. Taking the source also means a message that is nothing but a code block yanks as the code, which is what "copy a message or a code block" comes down to once there is no second binding for it.
Returns nil when there is nothing to copy, which the host reads as "say so" rather than as success.
type OpenPhotoMsg ¶
type OpenPhotoMsg struct {
Caption string
}
OpenPhotoMsg asks the host to raise the media overlay. It carries the caption for the overlay's header; the download follows in OpenedPhotoMsg.
type OpenedPhotoMsg ¶
OpenedPhotoMsg is the downloaded file for the overlay, or why there is none.
type ScrollToBottomMsg ¶
type ScrollToBottomMsg struct{}
type YankMsg ¶
type YankMsg struct {
// Runes is how much was copied, so the notice can say so. A reader who
// yanks a long message and gets a silent confirmation has no way to
// tell it from a yank that copied an empty caption.
Runes int
Err error
}
YankMsg reports what a copy did. The host phrases the notice, because the hint bar is the host's row and the wording belongs with the other notices rather than beside the clipboard call.