config

package
v0.0.12 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(): lowercased, with modifier and key aliases resolved and modifiers 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++".

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 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. Quit (and the hardcoded
	// ctrl+q) work from everywhere including the composer. Default "q".
	//
	// 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.
	QuitBrowsing  string `toml:"quit_browsing"`
	FocusChatList string `toml:"focus_chat_list"`
	FocusChatView string `toml:"focus_chat_view"`
	FocusComposer string `toml:"focus_composer"`
	Search        string `toml:"search"`
	Contacts      string `toml:"contacts"`
	// ContactsAlt is a second, alt-free binding for the contacts overlay,
	// so the overlay stays reachable on terminals that cannot report Alt
	// (see the macOS notes below). Default f4.
	ContactsAlt string `toml:"contacts_alt"`
	// Help opens the keybinding overlay. Default "?".
	Help string `toml:"help"`
	// GlobalSearch searches every chat. Search ("/") does the same from
	// every panel except the chat view, where vi convention makes "/" mean
	// "find in this buffer"; GlobalSearch is the panel-independent binding.
	// Default ctrl+g.
	GlobalSearch  string `toml:"global_search"`
	NextChat      string `toml:"next_chat"`
	PrevChat      string `toml:"prev_chat"`
	Reply         string `toml:"reply"`
	EditMessage   string `toml:"edit_message"`
	DeleteMessage string `toml:"delete_message"`
	Forward       string `toml:"forward"`
	ScrollUp      string `toml:"scroll_up"`
	ScrollDown    string `toml:"scroll_down"`
	PageUp        string `toml:"page_up"`
	PageDown      string `toml:"page_down"`
	// NextFolder/PrevFolder cycle the chat list's folder tabs.
	NextFolder string `toml:"next_folder"`
	PrevFolder string `toml:"prev_folder"`
}

KeyConfig lists user-configurable key bindings. Not every field is currently consulted:

  • Wired, dispatched by internal/app itself (which normalizes the value via NormalizeKey and falls back to its built-in default when empty): Quit, QuitBrowsing, FocusChatList, FocusChatView, FocusComposer, Search, GlobalSearch, Contacts, ContactsAlt, Help, NextFolder, PrevFolder, NextChat, PrevChat. Note FocusChatList/View/Composer are wired *in addition to* their hardcoded alt+1/2/3 shortcuts, which always work regardless of configuration.
  • Wired, resolved by internal/app and handed to the chat view, which implements them: Reply, EditMessage, DeleteMessage, ScrollUp, ScrollDown, PageUp, PageDown. Same normalization and defaulting; the difference is only which layer matches the key. Two rules apply there and nowhere else. Reply/EditMessage/DeleteMessage are mnemonics: a value REPLACES the built-in r/e/d. ScrollUp/ScrollDown/ PageUp/PageDown are motions: a value is ADDED to the built-in j/k, the arrows and pgup/pgdown, which always keep working. And a value that collides with a key the chat view already claims is dropped rather than allowed to shadow it. The chat view's remaining keys (g/G, ctrl+u/ctrl+d, n/N, ctrl+f, enter/o/s) are hardcoded there, as are the chat list's arrows and digits and the composer's line editing — see the keymap table in internal/app/keymap.go.
  • Unwired (parsed and preserved on save, but not consulted anywhere — kept so existing config.toml files round-trip cleanly; a value here is silently inert): Forward. There is no forward-a-message feature to bind it to; the field exists only so config files that set it still load.

A component-dispatched 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 now prevent that: the chat view is told what the app has claimed (keys.AppReserved) and refuses such a binding, keeping its built-in letter; and DetectKeyCollisions reports it, so the refusal is explained rather than silent.

Wired bindings are matched before the focused panel sees the key, so a binding here shadows that key in the chat list and chat view. It does not shadow it in the composer: typing is only ever entered deliberately (i, c, Tab, the focus keys, or a click), and once the composer has focus almost nothing is claimed at app level — see the exception list in the keymap table in internal/app/keymap.go. A bare printable is therefore a reasonable binding here, though a modifier still reads more clearly.

macOS: Alt bindings and the Option key

The default alt+… bindings only reach the app if the terminal reports Option as a modifier. Terminals differ:

  • Ghostty: macos-option-as-alt = true (default "false" on macOS — confirmed in field testing to be why alt bindings work in kitty but not in a stock Ghostty).
  • Terminal.app: Settings → Profiles → Keyboard → "Use Option as Meta key" (off by default).
  • iTerm2: Settings → Profiles → Keys → Left/Right Option key → "Esc+".
  • kitty/WezTerm/Alacritty report Option as Alt by default.

While Option is not reported as a modifier, macOS composes the character itself and the terminal sends only that: Option+1 arrives as a bare "¡" with no modifier bit, indistinguishable from the user typing "¡". This is not something the Kitty keyboard protocol fixes — the composition happens before the terminal builds the key event — and no amount of key matching can recover the binding.

So every alt binding has an alt-free alternative: f1/f2/f3 for panel focus, f4 for contacts (ContactsAlt), ctrl+g for global search (GlobalSearch), and "[" / "]", the left/right arrows or the 1-9 jump for the folder tabs while the chat list is focused. (Bare h/l used to be that fallback for the folder tabs; they now move between panels, which is what left/right means in a two-column layout — see internal/app/keymap.go.) Rebinding here works too — prefer ctrl+… or a function 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