An open API service indexing awesome lists of open source software.

https://github.com/ChuYanLon/telegram.nvim

A Telegram chat client inside Neovim, powered by TDLib + TypeScript + Lua
https://github.com/ChuYanLon/telegram.nvim

chat lua neovim neovim-plugin plugin tdlib telegram tui typescript

Last synced: 14 days ago
JSON representation

A Telegram chat client inside Neovim, powered by TDLib + TypeScript + Lua

Awesome Lists containing this project

README

          

# telegram.nvim

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](package.json)
[![CI](https://github.com/ChuYanLon/telegram.nvim/actions/workflows/ci.yml/badge.svg)](https://github.com/ChuYanLon/telegram.nvim/actions/workflows/ci.yml)
[![Neovim](https://img.shields.io/badge/neovim-%3E%3D0.9-blueviolet)](https://neovim.io)

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](https://t.me/+h4aEOaABJJ1mMzhl)

## Screenshots

> Partial screenshots — see Feature Status below for the full list.

| | |
|-|-|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |

## Feature Status

### What works

- [x] Login with phone number, verification code, and 2FA password
- [x] Session persists across restarts (no re-login)
- [x] `:TgLogout` to clear auth and start fresh
- [x] Group list with unread badges, inline fuzzy search (Snacks picker with `vim.ui.select` fallback)
- [x] Open/close chats, switch between groups
- [x] Scroll infinitely in both directions (older and newer messages)
- [x] Receive new messages in real-time via WebSocket
- [x] Typing indicators and online member count
- [x] Cursor position is remembered per chat (tracked by message ID)
- [x] Messages are marked as read when opening a chat; per-message read tracking with `last_read_id` persistence
- [x] Unread-aware loading — opens at first unread message with unread divider
- [x] Date separators between messages, loading indicators, and empty state
- [x] Send plain text messages (with reply context)
- [x] Send messages with formatting — type markdown syntax (`**bold**`, `### heading`) in input; Telegram clients (Android, iOS, Desktop) parse markdown natively, Neovim buffer renders via markdown treesitter
- [x] Edit your own messages
- [x] Delete your own messages (Delete for me / Revoke for everyone)
- [x] Forward messages to another chat
- [x] Reply to a message with quote context
- [x] Search messages and jump to the result position
- [x] URLs are highlighted and clickable
- [x] Code blocks (backtick) are detected and formatted
- [x] Single-panel chat layout with floating input popup
- [x] Adjustable panel position — `panel_position` option (`"right"`, `"left"`, `"bottom"`, `"top"`); width via `g:telegram_width` (default 50), height via `g:telegram_height` (default 15)
- [x] `?` opens a help popup with all keybindings
- [x] Target line highlighting (reply/edit/delete/forward) using theme's `Diff*` colors
- [x] `:TgPr` — create GitHub PR with branch picker, auto-fill, optional merge
- [x] `:TgIssue` — list, create branch, close, assign, open in browser
- [x] Proxy support (SOCKS5 / HTTP) for restricted regions
- [x] Service messages shown as readable text with prefix symbols
- [x] Download HD media — `@refreshmedia` downloads highest-quality version of photos/videos under cursor (async, non-blocking)
- [x] Context-aware tool picker — `@` only shows applicable tools (e.g. `refreshmedia` only on media messages)
- [x] Wake-up safe — messages received after sleep are batched and rendered at once, no Neovim freeze
- [x] Admin custom titles — shows `[头衔]` next to admin names in messages and member list; admins without title show `[Administrator]`
- [x] Photo / sticker / video / file inline preview — rendered as `![Photo](/path)` markdown; works with image renderers like `snacks.nvim` image module
- [x] Private chats (direct 1-on-1 messages) — press `c` on a message to open DM with the sender
- [x] Channel support — view channels and their messages; admin tools (member list, change info) shown based on permissions
- [x] Group management — view members (including admins and creator), ban/unban, restrict/unrestrict, promote/demote admins, add members by @username
- [x] Group settings — change title/description, granular default permissions editor (14 permission types with toggle-all), leave group, unsubscribe from channel, delete history
- [x] Invite links — create (with optional member limit and expiration), view, edit, and revoke invite links
- [x] Pin / unpin messages — press `p` on a message to pin/unpin; permission check for `can_pin_messages`
- [x] React to messages — press `r` to open reaction picker with 40+ verified emojis; same emoji toggles off, different auto-switches; real-time sync via WebSocket
- [x] Real-time sync between devices — edits, deletions, reactions, group info changes, user name/status changes sync via WebSocket from other clients
- [x] Online status — session reports as online with periodic heartbeat; device shown as `telegram.nvim` in Telegram's active sessions list
- [x] Favorites (Saved Messages) — dedicated chat with 📌 icon in picker; press `s` on any message to save with confirmation
- [x] View counts — channel messages show `👀 N` footer with k/M formatting; real-time update via WebSocket
- [x] Read receipts — outgoing private messages show `(read HH:MM)` in header when read by recipient
- [x] Edited indicator — edited messages show `[edited]` footer
- [x] Copy message text — press `yy` to copy message text to system clipboard
- [x] Toggle title bar — `@toggleheader` shows/hides the floating title bar; configurable via `hide_title` option
- [x] Connection status — title bar shows red dot when disconnected from Telegram
- [x] Input editor redesign — bottom panel with context preview; markdown syntax highlighting while typing
- [x] Customizable keymaps — all keys configurable via `setup({ keys = { ... } })`
- [x] Configurable panel position — `panel_position = "right" | "left" | "bottom" | "top"`
- [x] Theme adaptation — all highlight groups derive from your Neovim theme (`Comment`, `DiffAdd`, `DiagnosticOk`, etc.)

### What doesn't work yet

- [ ] **Send media** (photos, videos, files, audio) — can't upload anything yet
- [ ] **Send stickers / GIFs**
- [ ] **Create polls**
- [ ] **Scheduled messages**
- [ ] **Poll, contact, location, dice, game, call display** — fallback shows label, content not interactive
- [ ] **Inline bots** / bot commands

### Customizing keys

```lua
require("telegram").setup({
keys = {
input_editor = "I", -- rebind i → I
refresh = "",
help = "",
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` | `` | 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` | `` | Submit message in editor |
| `editor_cancel` | `` | Cancel editing |
| `help_close` | `` | 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` | `` | Permission editor: toggle item |
| `perms_up_alt` | `` | Permission editor: move up (alt) |
| `perms_save` | `` | Permission editor: save |
| `perms_discard` | `` | 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.

| 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 labels

Media messages are shown as thumbnails or tags:

| Tag | Meaning |
|-----|---------|
| `![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 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)` | Video thumbnail preview (click `@openlink` to play) |

## 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

```bash
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

```lua
{
"ChuYanLon/telegram.nvim",
build = "npm i",
event = "VeryLazy",
dependencies = {
-- "folke/snacks.nvim", -- optional: enables fuzzy-find chat picker
},
keys = {
{ "tt", "Tg", desc = "Toggle Telegram" },
{ "tL", "TgLogout", desc = "Logout Telegram" },
{ "tp", "TgPr", desc = "Create PR" },
{ "ti", "TgIssue", 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.

## Lua API

### Statusline

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

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

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

```lua
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

| 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 ` to current chat, or `:TgSend ` 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 })` 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

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

In the chat picker (`@` → chats):
- Built-in fuzzy search (Snacks picker when available, `vim.ui.select` fallback)
- `` — select chat
- `` — close

## 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()`:

```lua
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:
- **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`

## 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:

```lua
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](CONTRIBUTING.md) for details.

## License

MIT