telegram.nvim

August 9, 2026 ยท View on GitHub

License: MIT Node CI Neovim

A Telegram chat tool for neovim, similar to telega.el

Backend powered by TDLib + Node.js (TypeScript), frontend in pure Lua with HTTP + WebSocket communication.

๐Ÿ’ฌ Join the discussion on Telegram: t.me/+h4aEOaABJJ1mMzhl

Screenshots

Feature Status

Messages

  • Read text, media, links, code blocks, service messages in real-time via WebSocket
  • Rich text highlighting: bold, italic, code, strikethrough, ||spoiler||, links with distinct highlight groups
  • Rich display for contacts (๐Ÿ‘ค), venues (๐Ÿ“ with map link), locations, invoices, gifts, calls, dice, and more
  • Full service message coverage: screenshot, contact registered, proximity alert, theme/background changes, boosts, payments, web apps, admin events, and more
  • Send/edit/delete/forward/reply with markdown formatting
  • Polls: display with progress bars, vote (@vote), create (@createpoll), view voters (@voters), stop (@stoppoll). Supports multi-answer and timed polls.
  • Search messages, copy text, save to Favorites
  • Jump to any date with @jump_to_date โ€” supports YYYY-MM-DD, today, yesterday
  • Auto-download media with @refreshmedia, inline previews (photo/video/sticker/file)
  • Read receipts, edited indicators, view counts, typing indicators

Chat management

  • Group, channel, and private chat (DM) support
  • Member management: promote/demote, ban/unban, restrict, add by @username
  • Invite links with member limit and expiration
  • Group settings: title, description, granular permissions editor (14 types)
  • Pin/unpin messages, react with emojis (40+), mark unread, archive chats
  • Favorites (Saved Messages)

UI & UX

  • Configurable panel position (right/left/bottom/top)
  • Rich text highlighting: bold, italic, code, strikethrough, ||spoiler||, links in messages
  • Footer metadata ([edited], views, reactions) styled with distinct highlight group
  • Floating input editor with markdown treesitter highlight and reply preview
  • Cursor persistence per chat, unread-aware loading with divider
  • Scroll infinitely in both directions, date separators
  • Statusline integration (lualine/heirline), help popup
  • Theme adaptation (all highlights from your Neovim theme)
  • Customizable keymaps, toggleable title bar with connection status
  • Wake-up safe: batches messages received during sleep

Authentication & connectivity

  • Phone โ†’ code โ†’ 2FA flow, session persists across restarts
  • :TgLogout to clear auth
  • Online status with periodic heartbeat (shows as telegram.nvim)
  • Real-time sync between devices
  • Proxy support (SOCKS5 / HTTP) for restricted regions
  • :TgPr โ€” create/merge PRs with branch picker, squash option, auto-delete
  • :TgIssue โ€” browse issues, close, assign, create branches, open in browser

Known limitations

  • Send media (photos/videos/files/audio), stickers/GIFs
  • Scheduled messages

blink.cmp integration

telegram.nvim provides a blink.cmp source for auto-completion in the input editor:

TriggerInputCompletes
::heartโค๏ธ emoji (60+ names)
@@alice๐Ÿ‘ค chat member mentions
//start๐Ÿค– bot commands (from group info)
``` ```lua๐Ÿ–ฅ๏ธ code block language (42 langs)

Add to your blink.cmp setup:

sources = {
  { name = 'telegram', module = 'telegram.blink' },
}

@ Tools

Available tools via the tool picker (@ or :TgTool):

ToolDescription
@archiveArchive/unarchive current chat
@blockedList and manage blocked users
@channelsSwitch to a channel (filtered)
@chatsSwitch to another chat
@contactsBrowse your contacts list
@createpollCreate a poll in current chat
@dmSwitch to a private chat (filtered)
@draftSave draft to server / clear draft
@eventlogView recent admin events (member changes, edits, etc.)
@foldersSwitch chat folder
@groupsSwitch to a group (filtered)
@groupsettingsGroup / channel settings (title, description, permissions, etc.)
@invitelinksManage invite links
@joinrequestsView and manage pending join requests
@jump_to_dateJump to messages on a specific date
@markunreadMark current chat as unread / read
@membersView and manage chat members
@mentionsSearch @mentions in current chat
@messagelinkCopy shareable link of message under cursor
@muteMute / unmute current chat
@myprofileView and edit your profile name and bio
@newchatStart a new private chat by @username
@openlinkOpen URL or media file under cursor
@opensharedOpen shared chat or user DM
@pinchatPin / unpin current chat
@reactionReact to message
@refreshRefresh messages
@refreshmediaDownload and update image for message under cursor
@savedOpen Saved Messages
@searchSearch message history
@sendSend a message to current chat
@showarchivedToggle archived chats in picker
@stoppollStop a poll
@toggleheaderToggle floating title bar visibility
@translateTranslate message under cursor
@translate_zhTranslate message under cursor to Chinese
@userinfoView profile of message sender
@voteVote on the poll message under cursor
@votersList who voted on each poll option

Customizing keys

require("telegram").setup({
  keys = {
    input_editor = "I",  -- rebind i โ†’ I
    refresh = "<F5>",
    help = "<F1>",
    ban = false,  -- disable ban key
  },
})

All available keys and their defaults:

Key nameDefaultAction
translate_zhtttranslate message to Chinese
tool_picker@open tool picker
input_editoriopen input editor
reply<CR>reply / jump to original
editeedit own message
deleteddelete / revoke
forwardfforward message
forward_with_replyFforward with reply context
pinppin / unpin message
savessave to Favorites
copyyycopy message text
refreshGrefresh + jump to bottom
banBban message sender
open_dmcopen DM with message sender
help?toggle this help
editor_submit<CR>submit message in editor
editor_cancel<Esc>cancel editing
help_close<Esc>close this help
help_close_qqclose this help (alt)
goto_last<C-o>switch to previous chat
reactionrreact to message
archiveaarchive/unarchive chat
mark_unreadumark unread / mark as read
message_linkLcopy message link
user_profileUview user profile
mutemmute / unmute chat
perms_downjpermission editor: move down
perms_upkpermission editor: move up
perms_toggle<Tab>permission editor: toggle item
perms_up_alt<S-Tab>permission editor: move up (alt)
perms_save<CR>permission editor: save
perms_discard<Esc>permission editor: discard

Set any key to false to disable it.

Service messages

System messages (members added, group renamed, etc.) are rendered as readable text with a prefix symbol. The text color follows the Comment highlight group.

PrefixDisplayExample
[+]Member joined[+] Kitty joined this group via invite link at 2026-05-28 19:49
[+]Member added[+] Kitty added Bob at 2026-05-28 19:49
[-]Member left[-] Kitty left the group at 2026-05-28 19:49
[~]Group changed[~] Kitty changed the group name at 2026-05-28 19:49
[~]Group photo changed[~] Kitty changed the group photo at 2026-05-28 19:49
[~]Group upgraded[~] Kitty upgraded from a basic group at 2026-05-28 19:49
[*]Message pinned[*] Kitty pinned a message at 2026-05-28 19:49
[>]Group/topic created[>] Kitty created this group at 2026-05-28 19:49
[!]Auto-delete timer set[!] Kitty set auto-delete timer at 2026-05-28 19:49

Media labels

Media messages are shown as thumbnails or tags:

TagMeaning
![Photo](/path)Photo sent (clickable, HD via @refreshmedia)
![Video](/path)Video sent (clickable)
![Animation](/path)GIF sent (clickable)
![Document](/path)File sent (clickable)
![Audio](/path)Music sent (clickable)
![Voice](/path)Voice message (clickable)
![Video Note](/path)Video message (clickable)
![Sticker](/path)Sticker sent (clickable)
[Poll]Poll with progress bars. @vote to vote, @voters to see voters, @stoppoll to close
[Contact]Contact shared
[Location]Location shared
๐ŸŽฒ 5 / ๐Ÿ€ 3 etc.Dice / emoji roll
๐ŸŽฎ TitleGame played
[Call]Voice/video call
emoji characterAnimated emoji (inline text)
![Video](/thumbnail)Video thumbnail preview (click @openlink to play)
๐Ÿ‘ค Name / ๐Ÿ“ž phoneContact shared
๐Ÿ“ Name / address / ๐Ÿ—บ๏ธ linkVenue shared (click @openlink for map)
๐Ÿ“ Live: lat, lngLive location with expiry
๐Ÿ’ฌ Chat shared: nameChat shared (click @openshared to open)
๐Ÿ‘ฅ Users shared: ...Users shared (click @openshared for DM)
โญ Alice gifted PremiumPremium gift / Stars / Gift code
๐ŸŽ Alice sent a giftGift message
๐Ÿ“ฑ StoryStory share
๐Ÿ”‹ Chat boosted ร—NChat boost
๐ŸŽฎ Score: +NGame score
โœ… Payment: N currSuccessful payment
๐Ÿ“ธ Screenshot takenScreenshot notification
๐Ÿ“… / ๐Ÿ”Š / ๐Ÿ”‡Video chat scheduled / started / ended
๐Ÿ“ Proximity alertProximity trigger
๐Ÿ“Œ Topic renamed: ...Forum topic edited
๐ŸŽฒ 5 etc.Stake dice (๐ŸŽฏ๐ŸŽณ๐ŸŽฐ etc.)
๐ŸŽ‰ Giveaway created / ๐Ÿ† Winners / โœ… CompletedGiveaway lifecycle
โญ Giveaway prize: N StarsGiveaway star prize
๐Ÿ’Ž Alice gifted N TONTON gift
๐Ÿ“ฑ Joined TelegramContact registered notification
๐Ÿ”Š Group call started / ๐Ÿ“ž Missed / ๐Ÿ”‡ EndedGroup call events
๐ŸŒ WebApp: buttonWebApp data sent
๐ŸŽจ Theme set: nameChat theme changed
๐Ÿ–ผ๏ธ Background changedChat background changed

Requirements

  • Node.js (>= 18)
  • curl
  • libtdjson โ€” TDLib shared library (minimum version 1.8.64) โ€” libtdjson.so (Linux), libtdjson.dylib (macOS), tdjson.dll (Windows)
  • snacks.nvim โ€” optional, used for the chat picker with fuzzy search (falls back to vim.ui.select if not installed)
  • ImageMagick โ€” optional, required by snacks.nvim image module to display non-PNG images (e.g. JPEG photos). Install with brew install imagemagick on macOS
  • gh (GitHub CLI) โ€” optional, required for :TgPr and :TgIssue commands

Installing libtdjson

git clone https://github.com/tdlib/td.git
cd td
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_INSTALL_PREFIX=~/.local \
    -DCMAKE_CXX_FLAGS="-O2 -g0" \
    ..
cmake --build . --target install -j$(nproc)
ldconfig 2>/dev/null || true

Installation

lazy.nvim

{
  "ChuYanLon/telegram.nvim",
  build = "npm i",
  event = "VeryLazy",
  dependencies = {
    -- "folke/snacks.nvim",   -- optional: enables fuzzy-find chat picker
  },
  keys = {
    { "<leader>tt", "<cmd>Tg<Cr>", desc = "Toggle Telegram" },
    { "<leader>tL", "<cmd>TgLogout<Cr>", desc = "Logout Telegram" },
    { "<leader>tp", "<cmd>TgPr<Cr>", desc = "Create PR" },
    { "<leader>ti", "<cmd>TgIssue<Cr>", desc = "Manage Issues" },
  },
  cmd = {
    "Tg",
    "TgLogout",
    "TgPr",
    "TgIssue",
  },
  opts = {
    -- tdlib_path = "/path/to/libtdjson.so",         -- optional: .so (Linux) / .dylib (macOS) / .dll (Windows)
    -- proxy = "socks5://127.0.0.1:7890",             -- optional: for regions where Telegram is blocked
  },
}

build = "npm i" installs Node.js dependencies automatically on first install.

The server auto-detects libtdjson on startup via ldconfig, LD_LIBRARY_PATH, and common paths. See Configuration for details.

Lua API

Statusline

require("telegram").lualine is a pre-built lualine component:

require("lualine").setup({
  sections = {
    lualine_x = { require("telegram").lualine },
  },
})

For other statuslines (heirline, feline, etc.):

require("telegram").status()       -- "disconnected" | "connecting" | "connected" | "error"
require("telegram").status_color() -- { fg = "#..." }   -- color matching current status
require("telegram").total_unread() -- total, mentions   -- unread counts across all chats

Displays ๏‡˜ with:

  • ๐ŸŸข green โ€” connected, no unread
  • ๐ŸŸก yellow โ€” connecting
  • โšซ gray โ€” disconnected
  • ๐Ÿ”ด red โ€” error or has @mentions
  • Shows unread count after icon when there are new messages, e.g. ๏‡˜ 5
  • Appends ! when there are @mentions, e.g. ๏‡˜ 3!

Commands

CommandDescription
:TgGlobal toggle: opens tg window if closed, hides it if open (from any buffer). First run: server + auth, then opens last chat
:TgLogoutLog out, clear auth data, next :Tg starts fresh
:TgSendSend a message: :TgSend <text> to current chat, or :TgSend <chatId> <text> to specific chat
:TgToolOpen tool picker (@ equivalent)
:TgPrPropose changes from a feature branch to main โ€” choose squash or full merge, branch auto-deletes on completion
:TgIssueBrowse your assigned issues โ€” create, close, assign, and create branches directly from an issue

The server runs on ports 8080/8081 (configurable via setup({ http_port, ws_port }) or TG_PORT/TG_WS_PORT env vars). Opening :Tg in another Neovim instance will connect to the same server โ€” only the instance that started it will stop it on exit.

Neovim Keymaps

-- Configure inside lazy.nvim `keys`, or map manually:
vim.keymap.set("n", "<leader>tt", "<cmd>Tg<Cr>", { desc = "Toggle Telegram" })
vim.keymap.set("n", "<leader>tL", "<cmd>TgLogout<Cr>", { desc = "Logout Telegram" })
vim.keymap.set("n", "<leader>tp", "<cmd>TgPr<Cr>", { desc = "Create PR" })
vim.keymap.set("n", "<leader>ti", "<cmd>TgIssue<Cr>", { desc = "Manage Issues" })

In the chat picker (@ โ†’ chats):

  • Built-in fuzzy search (Snacks picker when available, vim.ui.select fallback)
  • <CR> โ€” select chat
  • <Esc> โ€” close

Input Editor

KeyAction
<CR>Send message / confirm edit
EscCancel reply/edit/forward mode

Mouse

Scrolling near the top/bottom of the buffer automatically loads older/newer messages.

Auth Flow

First run of :Tg:

  1. Backend starts on port 8080
  2. TDLib enters authentication flow
  3. Neovim shows an input prompt โ€” async and non-blocking, you can keep editing
  4. Enter: phone number โ†’ verification code โ†’ (optional) 2FA password
  5. On success, the group list opens automatically

Cancelling the input prompt (ESC / close dialog) aborts auth and cleans cached state. The next :Tg starts from scratch.

Configuration

Pass options via setup():

require("telegram").setup({
  -- tdlib_path = "/path/to/libtdjson.so",  -- only if auto-detection fails
  -- proxy = "socks5://127.0.0.1:7890",     -- proxy for TDLib connections
  -- data_dir = "/path/to/data",            -- default: plugin root
  -- http_port = 8080,                      -- HTTP server port
  -- ws_port = 8081,                        -- WebSocket server port
  -- notify_chat_types = { "private", "mention" },  -- types: "private", "group", "channel"; add "mention" for @mentions
  -- hide_title = false,  -- start with floating title bar hidden
  -- panel_position = "right",  -- "right" | "left" | "bottom" | "top"
})

Environment variable overrides:

Env varOverrides
TG_TDLIB_PATHtdlib_path
TG_PROXYproxy
TG_PORTHTTP server port (default: 8080)
TG_WS_PORTWebSocket server port (default: 8081)
TG_DATA_DIRData directory for tdlib_db/ and tdlib_files/ (default: plugin root)

The server auto-detects libtdjson on startup via:

  • Linux: ldconfig -p, common paths (/usr/lib, /usr/local/lib, ~/.local/lib, /usr/lib64, /opt/lib), LD_LIBRARY_PATH, and find
  • macOS: mdfind and common paths (/opt/homebrew/lib, /usr/local/lib)
  • Windows: where tdjson.dll and common paths (%LOCALAPPDATA%, %PROGRAMFILES%)

Override with setup({ tdlib_path = "..." }) or the TG_TDLIB_PATH env var.

Note on proxy: In regions where Telegram is blocked (e.g. China), TDLib cannot connect to Telegram's servers directly. Set a SOCKS5 or HTTP proxy here. Supported formats:

  • socks5://127.0.0.1:7890
  • socks5://user:pass@127.0.0.1:7890
  • http://127.0.0.1:8080

Database

TDLib stores data in data_dir/tdlib_db/ (SQLite + binlog) and files in data_dir/tdlib_files/.
Delete these directories to force re-authentication.
:TgLogout does this automatically.

FAQ

Q: Verification code never arrives (SMS not received) A: If you're in a region where Telegram is blocked (e.g. China), TDLib needs a proxy to connect. Set proxy in your config:

require("telegram").setup({
  proxy = "socks5://127.0.0.1:7890",
})

Your proxy needs to support SOCKS5 (e.g. ClashX, V2Ray, Shadowsocks). On Windows, a system-level VPN/proxy may already cover TDLib's traffic; on macOS, TDLib ignores system proxy settings and must be configured explicitly.

Q: "libtdjson.so not found" / "Cannot find libtdjson" A: The server auto-detects the library on startup. If auto-detection fails, install TDLib (see "Installing libtdjson" above) or set a custom path via setup({ tdlib_path = "..." }) or the TG_TDLIB_PATH env var.

Q: Do I need to re-authenticate every time Neovim restarts? A: No. TDLib caches session state in tdlib_db/. Auth persists across restarts.

Q: Why does the server use TypeScript? A: The backend was migrated from JavaScript to TypeScript (v0.3.0) for better type safety and maintainability in a multi-contributor project. The server runs via tsx, which is installed automatically by npm install โ€” no extra setup needed.

Q: How do I switch accounts? A: Run :TgLogout, or manually delete the tdlib_db/ and tdlib_files/ directories.

Q: Port conflict? A: Default ports are 8080/8081. Configure via setup({ http_port = ..., ws_port = ... }) or TG_PORT/TG_WS_PORT env vars. The plugin checks if a server is already running and reconnects if it's ours. If occupied by another process, startup fails โ€” change to different ports. Server process is terminated on Neovim exit.

Development Workflow

  • main โ€” stable branch, protected, no direct pushes
  • feat/* / fix/* / chore/* โ€” feature/fix branches, created from main
  • PRs target main โ€” use :TgPr to create and optionally merge
  • Merge options: squash or commit
  • After merge, GitHub auto-deletes the source branch (set in repo settings)
  • CI runs on every push and PR (test + typecheck)

Contributing

All contributions are welcome! Just open a pull request targeting main. See the full guide for details.

License

MIT