config

package
v0.0.13 Latest Latest
Warning

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

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

Documentation

Index

Constants

View Source
const (
	DefaultSessionFile = "~/.local/share/tele-tui/session.json"
	DefaultFilesDir    = "~/.local/share/tele-tui/files"
	DefaultDownloadDir = "~/Downloads"
)

Default storage locations, in the portable "~/" form. defaultConfig expands them for the running app; -migrate-config writes these literals so a generated config stays portable instead of hardcoding one machine's home directory.

View Source
const (
	// InlineImagesNever shows the metadata card in the thread and hands
	// the picture to the platform viewer on Enter. The right answer over a
	// slow link, and on a terminal whose image support is a guess.
	InlineImagesNever = "never"
	// InlineImagesOnOpen shows the card in the thread and draws the picture
	// full-pane when the reader OPENS it. The default, and the name is
	// literal: "on open" is when the art appears, not a condition under
	// which it appears in the history.
	InlineImagesOnOpen = "on_open"
	// InlineImagesAlways also draws an eight-row preview in the thread,
	// which is the only setting that puts art in the history at all.
	InlineImagesAlways = "always"
)

Inline-image policies for UIConfig.InlineImages.

View Source
const (
	// HyperlinksAuto emits OSC 8 only on terminals known to understand it
	// (theme.SupportsHyperlinks). The default, and an allowlist: a
	// terminal that prints the sequence instead of acting on it puts a URL
	// in the middle of somebody's message.
	HyperlinksAuto = "auto"
	// HyperlinksNever never emits them. Links stay cyan and underlined,
	// which is the affordance; OSC 8 only adds the click.
	HyperlinksNever = "never"
	// HyperlinksAlways emits them regardless — for a terminal the
	// allowlist does not know, or for tmux with allow-passthrough on,
	// which cannot be detected from the environment.
	HyperlinksAlways = "always"
)

Hyperlink policies for UIConfig.Hyperlinks.

View Source
const (
	// NotifyMethodAuto asks the terminal where it is known to understand
	// the sequence and the system otherwise. The default.
	NotifyMethodAuto = "auto"
	// NotifyMethodTerminal always asks the terminal, for one the allowlist
	// does not know. A terminal that does not understand it prints it.
	NotifyMethodTerminal = "terminal"
	// NotifyMethodSystem always uses the platform notifier: notify-send on
	// Linux, osascript on macOS — which posts as Script Editor, because a
	// command-line binary has no bundle of its own to post from.
	NotifyMethodSystem = "system"
)

Delivery methods for NotificationConfig.Method.

The strings are defined here, beside the other policy fields, so this package stays free of the one that implements them — internal/notification parses the same three values.

View Source
const (
	// EmojiWidthAuto measures with the Unicode tables and reserves a cell
	// on top for every composition rule, so an over-reservation shows as a
	// gap rather than as a row overwriting its neighbour. The default, and
	// the only value that is a guess.
	EmojiWidthAuto = "auto"
	// EmojiWidthComposed says this terminal applies every composition
	// rule. The tables are then right and nothing is reserved on top —
	// which is what closes the gap between the folder tabs and the clock.
	EmojiWidthComposed = "composed"
	// EmojiWidthSeparate says it applies none of them: U+FE0F is ignored
	// and joined or paired sequences are drawn as their parts.
	EmojiWidthSeparate = "separate"
)

Emoji-width declarations for UIConfig.EmojiWidth.

This is one setting rather than two because the terminals that get it wrong get it wrong consistently: one that honours U+FE0F also composes ZWJ sequences and flags. What it cannot be is inferred — the widths differ in opposite directions, so no single "narrow" or "wide" describes them.

View Source
const (
	// ComposeEditingEmacs is the readline keymap (ctrl+a/e/b/f/k/u/w/d).
	ComposeEditingEmacs = "emacs"
	// ComposeEditingVi is the modal vi keymap.
	ComposeEditingVi = "vi"
	// ComposeEditingAuto infers the keymap from the user's $EDITOR.
	ComposeEditingAuto = "auto"
)

Line-editing keymaps for UIConfig.ComposeEditing.

View Source
const DefaultQuitKey = "ctrl+q"

DefaultQuitKey is what ResolveQuitKey falls back to. It is a chord on purpose: quit is matched before every focus gate, so a bare letter here is a letter that cannot be typed anywhere in the client.

Variables

This section is empty.

Functions

func BackupFile

func BackupFile(path string) (string, error)

BackupFile copies path to path+".bak", byte for byte, at mode 0600. The migration re-marshals the config through the TOML encoder, which silently drops comments and reorders tables; the backup is the only copy of what the user actually wrote — and it holds the same api_hash and phone number as the original, so it gets the same restrictive mode.

The backup lands beside the *resolved* file. With the usual dotfiles layout (~/.config/tele-tui/config.toml symlinked into ~/dotfiles) the backup belongs next to the real file in the dotfiles directory, where the content it is protecting actually lives — not next to the symlink.

An existing backup is never overwritten. Someone who has already migrated once and is migrating again would otherwise lose their real original to a copy of the already-migrated file; the second backup gets a timestamp suffix instead.

func ConfigPath

func ConfigPath() string

ConfigPath returns the config file the app would load: TELETUI_CONFIG when set, otherwise the default location — whether or not it exists. Callers that need to know if it exists should stat it.

func DetectKeyCollisions

func DetectKeyCollisions(cfg *Config) []string

DetectKeyCollisions reports keys fields whose bindings cannot all work, as human-readable lines. Two kinds are found.

**Within config.** Two fields the same dispatcher matches, set to one key. Filling in new fields can create a collision the user never made: someone who bound search to "?" gets help = "?" from this migration, and only one of the two can win. The migration will not silently rewrite a deliberate choice, so the honest thing is to name the clash and let the user decide.

**Across the package boundary.** A component-dispatched field (see componentDispatchedFields) set to a key internal/app claims first. This is the case that shipped broken: reply = "q" was accepted, advertised on the help card as Reply, and quit the application when pressed, because app-level dispatch matched "q" before the chat view ever saw the event. The reservation is measured against keys.AppReserved, so it follows the user's own config — moving quit_browsing to f9 frees "q", and this stops reporting it.

What remains unchecked, and why:

  • The one inert field (forward) reaches no dispatcher, so a shared value there means nothing and is ignored.
  • Keys the OTHER components hardcode: chatview's g/G, n/N, ctrl+f, ctrl+u/ctrl+d, enter/o/s; chatlist's arrows, [ / ] and 1-9; the composer's readline and vi chords. Binding a keys field onto one of those still collides silently here. The chat view resolves its own share at runtime — a configured binding that would shadow a key it already owns is dropped rather than allowed to win — but it does so quietly, and this function is where that ought to become a message. Naming those sets here would mean a second, hand-copied record of them; the honest fix is for each component to publish its claimed set the way internal/app now does through keys.AppFixed.
  • Whether the *combination* is usable. Two bindings can be collision-free and still miserable.

The help overlay shows the real, merged map; this checks only what config.toml can express.

func IsBarePrintableKey added in v0.0.12

func IsBarePrintableKey(key string) bool

IsBarePrintableKey reports whether a NORMALIZED binding types a character when it is pressed: a single unmodified printable ("x", "?", "+"), or "space". A chord ("ctrl+x"), a named key that produces no text ("esc", "f1", "pgup") and the empty string are all false.

It answers one question: would binding this shadow a character somebody types? Only the bindings matched ahead of the composer have to ask it.

func NormalizeKey

func NormalizeKey(s string) string

NormalizeKey canonicalizes a user-configured key string to the form produced by bubbletea's Key.Keystroke(): modifier and key aliases resolved, modifiers lowercased and emitted in Keystroke's fixed order (ctrl, alt, shift, meta, hyper, super). An empty input returns empty, so callers can detect "not configured" and fall back to a built-in default.

Examples: "ALT+L" -> "alt+l", "Option+1" -> "alt+1", "shift+ctrl+a" -> "ctrl+shift+a", "Escape" -> "esc", "ctrl++" -> "ctrl++".

A lone printable keeps its case

"J" stays "J". Case is not decoration on an unmodified letter, it is the binding: a shift+j press reports Keystroke() "shift+j" and String() "J", and keys.Press matches an unmodified key on either spelling — so "J" matches it and "j" does not, while a plain j press matches "j" and not "J". next_chat = "J" and the chat list's own j are two different keys, and lowercasing the first turns the second into chat navigation.

That is a real regression rather than a hypothetical one: the shipped defaults set next_chat/prev_chat to J/K, Load fills them into every config, and with them folded to j/k the app-level handler — which runs before the focused panel — took the chat list's cursor motion.

Only the lone-printable case is preserved. Anything with a modifier goes on being lowercased, because there Keystroke() is the only spelling that can match and it is lowercase: "ALT+L" is alt+l, not alt+L. A user who means the shifted letter with a modifier writes the modifier out ("alt+shift+l"), which is what the terminal reports.

Anything that is not a recognized modifier terminates the modifier prefix and is taken (together with the rest of the string) as the key name, so a literal "+" binding survives intact.

func ResolveComposeEditing

func ResolveComposeEditing(setting string) string

ResolveComposeEditing turns a configured UIConfig.ComposeEditing value into a concrete ComposeEditingEmacs or ComposeEditingVi.

An explicit "emacs" or "vi" wins. Everything else — "auto", empty (an older config.toml predating the field), or an unrecognized value — infers the keymap from $VISUAL, falling back to $EDITOR: if the editor's command name contains "vi" (vi, vim, nvim, gvim, view) the answer is vi, otherwise emacs. That also makes emacs the answer when no editor is set, matching the shell convention that readline bindings are the default.

An unrecognized value is treated as "auto" rather than rejected, so a typo in config.toml degrades to a sensible keymap instead of breaking startup.

func ResolveEmojiWidth

func ResolveEmojiWidth(v string) string

ResolveEmojiWidth normalises UIConfig.EmojiWidth, falling back to the default for an empty or unrecognised value — a typo here should cost the user the setting, not the client.

func ResolveHyperlinks(v string) string

ResolveHyperlinks normalises UIConfig.Hyperlinks to one of the three policies, treating anything unrecognised as the default rather than failing: a typo in a cosmetic setting should not stop the client starting.

func ResolveInlineImages

func ResolveInlineImages(v string) string

ResolveInlineImages normalises UIConfig.InlineImages, falling back to the default for an empty or unrecognised value.

Unrecognised falls back rather than failing: a typo in this field should cost the user the setting, not the client.

func ResolveQuitKey added in v0.0.12

func ResolveQuitKey(configured string) (key string, refused bool)

ResolveQuitKey is the binding KeyConfig.Quit resolves to, and whether the configured value was refused (decision I-13).

quit is the one field where a bare printable is not merely unwise but broken: it is matched before every focus gate, so quit = "x" meant that pressing x while writing a message quit the application instead of typing an x. Nothing rejected that — DetectKeyCollisions only compares bindings against each other, never against "is this a character someone types" — so the documented advice was to avoid it, which is not the same thing as it not happening.

The refusal keeps the default rather than leaving quit unbound: a client with no way out is worse than one that ignored a line of config, and StartupWarnings says which it did.

func Save

func Save(cfg *Config) error

Save writes the config to the path the app will read back: $TELETUI_CONFIG when set, otherwise the default location. See ConfigPath.

Writing to the default location unconditionally would break the first-run setup wizard under $TELETUI_CONFIG: the credentials it collects would land in a file Load never looks at, and the next launch would ask for them again.

func SaveTo

func SaveTo(path string, cfg *Config) error

SaveTo writes the config to an explicit path. Save always writes to the default location; migration has to write back to the file it read, which TELETUI_CONFIG can move.

func SortChanges

func SortChanges(changes []MigrationChange)

SortChanges orders a migration summary by field name, so successive runs and successive versions produce comparable output.

func StartupWarnings added in v0.0.12

func StartupWarnings(cfg *Config) []string

StartupWarnings is everything about the keys table worth telling the user before the TUI takes the screen: a refused quit binding, and bindings that cannot all work.

At startup, not only under -migrate-config (decision I-13). A warning somebody sees only if they happen to run a migration is a warning about a client they are already using with the broken binding in it.

Types

type Config

type Config struct {
	Telegram      TelegramConfig     `toml:"telegram"`
	Storage       StorageConfig      `toml:"storage"`
	UI            UIConfig           `toml:"ui"`
	Media         MediaConfig        `toml:"media"`
	Notifications NotificationConfig `toml:"notifications"`
	Keys          KeyConfig          `toml:"keys"`
}

func Load

func Load() (*Config, error)

type KeyConfig

type KeyConfig struct {
	// Quit quits from anywhere, without asking. A bare printable is
	// refused here: it is matched before every focus gate, so it would be
	// untypable in a message. See [ResolveQuitKey]. Default ctrl+q.
	Quit string `toml:"quit"`
	// QuitBrowsing quits from the chat list and the chat view only, where
	// a bare letter cannot be mistaken for typing — the composer owns
	// printables and never sees it. An unsent draft or a pending
	// attachment turns it into a confirm rather than an immediate exit, so
	// a single keystroke cannot discard a message being written.
	// Default "q".
	QuitBrowsing string `toml:"quit_browsing"`
	// Search searches the buffer in front of you: in-chat find from the
	// chat view, a live filter over the chat list. Default "/".
	Search string `toml:"search"`
	// GlobalSearch searches every chat, from any panel — the
	// panel-independent binding Search cannot be, since vi convention
	// gives "/" to the buffer you are looking at. Default ctrl+g.
	GlobalSearch string `toml:"global_search"`
	// Contacts toggles the contacts overlay. Default "c".
	Contacts string `toml:"contacts"`
	// Compose moves focus to the composer, opening the cursored chat
	// first when it is not already the open one. Default "i".
	Compose string `toml:"compose"`
	// Help opens the keybinding overlay. Default "?".
	Help string `toml:"help"`
	// NextChat/PrevChat open the next and previous chat outright, unlike
	// the chat list's own j/k which only move the cursor. Defaults "J"
	// and "K".
	NextChat string `toml:"next_chat"`
	PrevChat string `toml:"prev_chat"`
	// NextUnread opens the next chat with unread messages, searching down
	// from the cursor within the active folder and wrapping once.
	// Default "u".
	NextUnread string `toml:"next_unread"`
	// NextFolder/PrevFolder cycle the folder tabs from either browsing
	// panel. Defaults "]" and "[".
	NextFolder string `toml:"next_folder"`
	PrevFolder string `toml:"prev_folder"`
	// Reply/EditMessage/DeleteMessage act on the cursored message in the
	// chat view. Defaults "r", "e", "d".
	Reply         string `toml:"reply"`
	EditMessage   string `toml:"edit_message"`
	DeleteMessage string `toml:"delete_message"`
	// MarkRead marks the open chat read without moving the scroll or the
	// unread divider. Default "m".
	MarkRead string `toml:"mark_read"`
}

KeyConfig lists the user-configurable key bindings. Every field follows one rule (decision I-13):

A value REPLACES the default. A value that collides with anything
already bound — another field, or a key a panel owns outright — is
REFUSED, the default is kept, and the refusal is reported at startup.

There used to be three rules. Some fields replaced their default, some were an extra spelling ADDED alongside it, and one was accepted, saved and never consulted at all. The three needed a page of documentation to tell apart, and the third let `forward` sit in the shipped example file bound to nothing.

Which layer matches a binding still differs, and still does not change the rule. internal/app dispatches Quit, QuitBrowsing, Search, GlobalSearch, Contacts, Compose, Help, NextChat, PrevChat, NextUnread, NextFolder and PrevFolder itself; it resolves Reply, EditMessage, DeleteMessage and MarkRead and hands them to the chat view, which implements them (see chatview.Keys).

The motions are not configurable, and are not meant to be: j/k, the arrows, g/G, ctrl+e/ctrl+y, ctrl+d/ctrl+u and the page keys are vi's, the chat list's digits and the composer's line editing belong to their components, and a client that let them move would be a client whose documentation could not describe it. See the keymap in docs/interaction-model.md.

A field set to a key internal/app claims first is not merely shadowed — it is dead, because app-level dispatch runs before the focused panel sees the event. reply = "q" used to be accepted, advertised on the help card as Reply, and quit the application when pressed. Two things prevent it now: the chat view is told what the app has claimed (keys.AppReserved) and refuses such a binding, keeping its built-in letter; and StartupWarnings reports it, so the refusal is explained rather than silent. The help card shows an action left with no key as "(unbound)".

A binding here shadows that key in the chat list and the chat view. It does not shadow it in the composer: typing is only ever entered deliberately (i, Tab, an action that needs text, or a click), and once the composer has focus almost nothing is claimed at app level. Quit is the exception, and the one field where a bare printable is refused outright — see ResolveQuitKey.

Values are read through NormalizeKey: modifiers and key names are case-insensitive and aliased, but a lone printable letter keeps its case, because on an unmodified key the case is the binding. NextChat is "J" and the chat list's own motion is "j"; they are not the same key.

type MediaConfig

type MediaConfig struct {
	ImageProtocol       string `toml:"image_protocol"`
	MaxImageWidth       int    `toml:"max_image_width"`
	MaxImageHeight      int    `toml:"max_image_height"`
	VoicePlayer         string `toml:"voice_player"`
	VideoPlayer         string `toml:"video_player"`
	AutoDownloadPhotos  bool   `toml:"auto_download_photos"`
	AutoDownloadLimitMB int    `toml:"auto_download_limit_mb"`

	// AutoDownloadVoice is read back and written out, and nothing consults
	// it. Voice notes are never prefetched: one is fetched when you press
	// space on it, which is the only moment anybody wants the bytes, and
	// turning that off would mean a key that does nothing. Kept for config
	// round-tripping — see TimestampFormat for the same reasoning at
	// length.
	AutoDownloadVoice bool `toml:"auto_download_voice"`
}

type MigrationChange

type MigrationChange struct {
	// Field is the TOML key, qualified by its table (e.g. "keys.contacts").
	Field string
	// Old is the value the config carried, empty when Absent.
	Old string
	// Absent distinguishes a key the file never had from one it set to the
	// empty string. Both read as "" in Old, but they are different
	// mistakes: the first is an old config missing a field added since, the
	// second is someone who wrote `contacts = ""` and deserves to see that
	// their (broken) setting was replaced rather than merely filled in.
	Absent bool
	// New is the value written in its place.
	New string
	// Removed marks a field the client no longer has. The key is dropped
	// from the rewritten file rather than replaced, and the summary says so
	// — a user who tuned it deserves to learn it stopped doing anything,
	// which is not the same news as a value being changed.
	Removed bool
}

MigrationChange records one field the migration rewrote.

func Migrate

func Migrate(cfg *Config, raw *RawFile) []MigrationChange

Migrate brings an older config up to the current defaults in place and reports what it changed. It is pure apart from mutating cfg — no file is read or written — so the caller owns backup and save ordering.

cfg is the config to rewrite and save. raw is the same file parsed without defaults applied (LoadRawFile), and is consulted only to tell "the file did not have this key" from "the file set it to the current default". Load fills absent fields from defaultConfig, so without raw a config written before help/global_search/contacts_alt existed would look like it already had them and the summary would not mention the keys it gained. raw may be nil, in which case cfg's own empty fields count as absent.

Per field, in the keys table:

  • absent: filled with the current default, and reported so the user can see which keys the file gained.
  • equal to a known stale default (see staleKeyDefaults): replaced with the current default, because the user inherited it rather than chose it. Comparison is through NormalizeKey, so "CTRL+K" counts.
  • anything else: left alone. A deliberate customization survives an upgrade even when it collides with something.

It also fills two fields outside keys that were introduced later: ui.compose_editing and storage.state_file. state_file is written as the path the client would otherwise derive, so the location becomes explicit rather than implied.

func (MigrationChange) String

func (c MigrationChange) String() string

String renders a change for the migration summary.

type NotificationConfig

type NotificationConfig struct {
	Enabled     bool `toml:"enabled"`
	Sound       bool `toml:"sound"`
	ShowPreview bool `toml:"show_preview"`

	// Method is who posts the notification: "auto" (the default — the
	// terminal where it is known to understand the sequence, the system
	// otherwise), "terminal", or "system". See internal/notification for
	// why the terminal is usually the right answer, and why macOS labels
	// the system path "Script Editor".
	Method string `toml:"method"`
}

type RawFile

type RawFile struct {
	// Config is the file parsed with no defaults applied.
	Config *Config
	// contains filtered or unexported fields
}

RawFile is a config file as written: parsed without defaults, plus the structure of what was actually in it.

Load applies defaults, which erases the difference between "the file did not have this key" and "the file set it to today's default". The migration needs that difference to report honestly, and needs the unexpanded path strings so it does not rewrite a user's "~/..." into an absolute path.

func LoadRawFile

func LoadRawFile(path string) (*RawFile, error)

LoadRawFile parses a config file without applying any defaults. See RawFile; Load is what the app itself wants.

func (*RawFile) Has

func (r *RawFile) Has(section, field string) bool

Has reports whether the file contained section.field.

func (*RawFile) MissingSections

func (r *RawFile) MissingSections() []string

MissingSections returns the config tables the file does not have, which a rewrite will add in full. Sorted.

func (*RawFile) Removed

func (r *RawFile) Removed() map[string]string

Removed returns the deliberately-dropped keys the file carried, as "section.key" -> the value it had.

func (*RawFile) Unknown

func (r *RawFile) Unknown() []string

Unknown returns the keys the current schema does not recognize, which a rewrite will drop. Sorted.

type StorageConfig

type StorageConfig struct {
	SessionFile string `toml:"session_file"`

	// FilesDir is the media CACHE: where downloads land so a photo drawn
	// twice is fetched once. It is not where "save this" saves to — see
	// DownloadDir — and a user who set files_dir expecting the latter got
	// a cache directory full of files with server-side names.
	FilesDir string `toml:"files_dir"`

	// DownloadDir is where `s` puts a copy, under the sender's own
	// filename. Defaults to the platform download folder, because that is
	// where a person looks for a thing they just saved.
	DownloadDir string `toml:"download_dir"`
	// StateFile is the bbolt database holding the update-sequence state
	// (pts/qts/seq/date) and the peer access-hash cache, so updates that
	// arrived while the app was offline can be recovered on the next start.
	// Empty (the default) means "state.db" next to SessionFile.
	StateFile string `toml:"state_file"`
}

type TelegramConfig

type TelegramConfig struct {
	APIID   int32  `toml:"api_id"`
	APIHash string `toml:"api_hash"`
	Phone   string `toml:"phone"`
}

type UIConfig

type UIConfig struct {
	Theme string `toml:"theme"`

	// TimestampFormat and DateFormat are READ BACK AND WRITTEN OUT, and
	// nothing consults them. Every time on screen has a fixed form chosen
	// for the column it sits in: the thread's clock is 15:04 because the
	// grid gives it five cells and puts the date in a day divider, the
	// chat list is relative because a chat list is read for recency, and a
	// day divider names the day. A Go layout string cannot express those,
	// and one that overrode all three would break the column widths the
	// frame is built on.
	//
	// Kept so an existing config round-trips rather than losing keys on
	// -migrate-config. Marked here, and in config.example.toml, so nobody
	// spends an afternoon finding out they do nothing.
	TimestampFormat string `toml:"timestamp_format"`
	DateFormat      string `toml:"date_format"`

	// InlineImages governs WHERE a photo is drawn:
	// [InlineImagesNever], [InlineImagesOnOpen] (the default), or
	// [InlineImagesAlways]. Only the last puts art in the thread, and only
	// bounded — see render.inlineArtRows for why the bound is not
	// negotiable.
	InlineImages string `toml:"inline_images"`

	// Hyperlinks governs OSC 8 terminal hyperlinks on links in a message:
	// [HyperlinksAuto] (the default), [HyperlinksNever], or
	// [HyperlinksAlways].
	Hyperlinks string `toml:"hyperlinks"`

	// EmojiWidth declares how this terminal draws emoji sequences that
	// have a composition rule: [EmojiWidthAuto] (the default),
	// [EmojiWidthComposed], or [EmojiWidthSeparate]. It is a declaration
	// because it cannot be detected — see internal/ui/cell for why.
	EmojiWidth string `toml:"emoji_width"`

	// Rail shows the right-hand context rail — pinned message, members,
	// shared files — on a terminal wide enough for it. Off by default: it
	// costs 30 columns, and they come out of the thread.
	Rail bool `toml:"rail"`
	// ComposeEditing selects the composer's line-editing keymap:
	// [ComposeEditingEmacs], [ComposeEditingVi], or [ComposeEditingAuto]
	// (the default) to infer it from $VISUAL/$EDITOR. Resolve it with
	// [ResolveComposeEditing]; never read the raw value.
	ComposeEditing string `toml:"compose_editing"`
	// ParseMarkdown enables the Telegram Desktop markdown subset in
	// outgoing messages and captions (**bold**, __italic__, `code`,
	// “`pre“`, ~~strike~~, ||spoiler||, [text](url)).
	//
	// Defaults to FALSE: what you typed is what gets sent. The composer has
	// no preview, so with parsing on silently by default the first time a
	// user notices is when a message has already left — and the syntax
	// overlaps with things people paste verbatim. __init__ arrives as
	// init, a snippet full of ** loses it, a table of || collapses. Opting
	// in means knowing that transformation happens.
	//
	// -migrate-config turns it on for existing configs, where it reports
	// the change, so upgraders get the feature but are told about it.
	ParseMarkdown bool `toml:"parse_markdown"`
}

Jump to

Keyboard shortcuts

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