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
Partial screenshots — see Feature Status below for the full list.
:TgLogout to clear auth and start freshvim.ui.select fallback)last_read_id persistence**bold**, ### heading) in input; Telegram clients (Android, iOS, Desktop) parse markdown natively, Neovim buffer renders via markdown treesitterpanel_position option ("right", "left", "bottom", "top"); width via g:telegram_width (default 50), height via g:telegram_height (default 15)? opens a help popup with all keybindingsDiff* colors:TgPr — create GitHub PR with branch picker, auto-fill, optional merge:TgIssue — list, create branch, close, assign, open in browser@refreshmedia downloads highest-quality version of photos/videos under cursor (async, non-blocking)@ only shows applicable tools (e.g. refreshmedia only on media messages)[头衔] next to admin names in messages and member list; admins without title show [Administrator] markdown; works with image renderers like snacks.nvim image modulec on a message to open DM with the senderp on a message to pin/unpin; permission check for can_pin_messagesr to open reaction picker with 40+ verified emojis; same emoji toggles off, different auto-switches; real-time sync via WebSockettelegram.nvim in Telegram's active sessions lists on any message to save with confirmation👀 N footer with k/M formatting; real-time update via WebSocket(read HH:MM) in header when read by recipient[edited] footeryy to copy message text to system clipboard@toggleheader shows/hides the floating title bar; configurable via hide_title optionsetup({ keys = { ... } })panel_position = "right" | "left" | "bottom" | "top"Comment, DiffAdd, DiagnosticOk, etc.)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 name | Default | Action |
|---|---|---|
tool_picker |
@ |
Open tool picker |
input_editor |
i |
Open message input editor |
reply |
<CR> |
Reply to / jump to message |
edit |
e |
Edit own message |
delete |
d |
Delete / revoke message |
forward |
f |
Forward message |
pin |
p |
Pin / unpin message |
reaction |
r |
React to message (opens emoji picker) |
save |
s |
Save message to Favorites (with confirmation) |
copy |
yy |
Copy message text to clipboard |
refresh |
G |
Refresh messages, jump to bottom |
ban |
B |
Ban message sender |
open_dm |
c |
Open DM with message sender |
help |
? |
Toggle help popup |
editor_submit |
<CR> |
Submit message in editor |
editor_cancel |
<Esc> |
Cancel editing |
help_close |
<Esc> |
Close help popup |
help_close_q |
q |
Close help popup (alt) |
perms_down |
j |
Permission editor: move down |
perms_up |
k |
Permission 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.
System messages (members added, group renamed, etc.) are rendered as readable text with a prefix symbol. The text color follows the Comment highlight group.
| Prefix | Display | Example |
|---|---|---|
[+] |
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 messages are shown as thumbnails or tags:
| Tag | Meaning |
|---|---|
 |
Photo sent (clickable, HD via @refreshmedia) |
 |
Video sent (clickable) |
 |
GIF sent (clickable) |
 |
File sent (clickable) |
 |
Music sent (clickable) |
 |
Voice message (clickable) |
 |
Video message (clickable) |
 |
Sticker sent (clickable) |
[Poll] |
Poll created |
[Contact] |
Contact shared |
[Location] |
Location shared |
[Dice] |
Dice rolled |
[Game] |
Game played |
[Call] |
Voice/video call |
| emoji character | Animated emoji (inline text) |
 |
Video thumbnail preview (click @openlink to play) |
libtdjson.so (Linux), libtdjson.dylib (macOS), tdjson.dll (Windows)vim.ui.select if not installed)brew install imagemagick on macOS:TgPr and :TgIssue commandsgit 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
{
"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.
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:
 5! when there are @mentions, e.g.  3!| Command | Description |
|---|---|
:Tg |
Global toggle: opens tg window if closed, hides it if open (from any buffer). First run: server + auth, then opens last chat |
:TgLogout |
Log out, clear auth data, next :Tg starts fresh |
:TgSend |
Send a message: :TgSend <text> to current chat, or :TgSend <chatId> <text> to specific chat |
:TgTool |
Open tool picker (@ equivalent) |
:TgPr |
Propose changes from a feature branch to main — choose squash or full merge, branch auto-deletes on completion |
:TgIssue |
Browse 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 })orTG_PORT/TG_WS_PORTenv vars). Opening:Tgin another Neovim instance will connect to the same server — only the instance that started it will stop it on exit.
-- 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):
vim.ui.select fallback)<CR> — select chat<Esc> — closeFirst run of :Tg:
Cancelling the input prompt (ESC / close dialog) aborts auth and cleans cached state. The next :Tg starts from scratch.
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 var | Overrides |
|---|---|
TG_TDLIB_PATH |
tdlib_path |
TG_PROXY |
proxy |
TG_PORT |
HTTP server port (default: 8080) |
TG_WS_PORT |
WebSocket server port (default: 8081) |
TG_DATA_DIR |
Data directory for tdlib_db/ and tdlib_files/ (default: plugin root) |
The server auto-detects libtdjson on startup via:
ldconfig -p, common paths (/usr/lib, /usr/local/lib, ~/.local/lib, /usr/lib64, /opt/lib), LD_LIBRARY_PATH, and findmdfind and common paths (/opt/homebrew/lib, /usr/local/lib)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:7890socks5://user:pass@127.0.0.1:7890http://127.0.0.1:8080
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.
main — stable branch, protected, no direct pushesfeat/* / fix/* / chore/* — feature/fix branches, created from mainmain — use :TgPr to create and optionally mergeAll contributions are welcome! Just open a pull request targeting main. See the full guide for details.
MIT