You know the feeling: dozens — hundreds — of Telegram chats, groups, channels and DMs, and finding the right one means scrolling, half-remembering names, and searching inside the app. It's like having a thousand browser tabs open, except worse — they're spread across multiple accounts and buried under folders and archives.
telepad is a rofi quick-switcher for Telegram — hit a hotkey from anywhere, fuzzy-type a few letters, and your running Telegram Desktop (or a fork like AyuGram) jumps straight to that chat, group, channel, contact, or forum topic — across all your accounts.
It's Discord's Ctrl+K quick-switcher, reimagined for the desktop Telegram client — and arguably better. Because it's a global rofi keybinding (bound in your WM), you trigger it from any window: your editor, terminal, browser, anything. No need to first focus the app. Where Discord makes you focus Discord → Ctrl+K → type → jump, telepad is just hotkey → type → jump from wherever you already are — fewer keystrokes, zero context switch.
┌─ data layer (grammers / MTProto) ─┐ ┌─ front-end (rofi) ─────────────────┐
│ per account, once: log in │ │ flat fuzzy list across accounts │
│ sync: dialogs + contacts + forum │ ─cache→ │ ↳ forum? → topic submenu │
│ topics → JSON cache │ │ → (inject_cmd: switch account) │
└────────────────────────────────────┘ │ → D-Bus Open → client navigates │
└────────────────────────────────────┘
Status: works day-to-day for the author on X11 + i3 + AyuGram. Rough edges remain — see Caveats and Roadmap. The client is now configurable (
client = "telegram" | "ayugram"), defaulting to vanilla Telegram Desktop, but has only been exercised in anger against AyuGram so far — sometg://id-based opens may still differ on vanilla (see Caveats).
- Fuzzy jump to any chat, group, channel, or contact, across all accounts —
searchable by display name or
@handle(public usernames show in the row) - Forum topics: every topic is a flat, searchable row (
Forum ▸ Topic · topic) so you can jump straight to one on the first keystroke — or select the forum itself to browse its topics in a submenu - Chat folders: a
📁 <Folder>entry per account expands into a submenu of that folder's chats - Archive: a
🗄 Archivedentry per account — open the archive folder, or jump straight to any archived chat - Contacts included — even blocked users or people you have no open chat with
- Cross-account: switches to the target account before opening
- Frecency ranking: chats you jump to often (and recently) float to the top, so the list stays sorted around how you actually use it
- Saved Messages: a
⭐ Saved Messagesrow per account, always a keystroke away - Avatars (opt-in):
sync --avatarscaches profile photos and rofi shows them as row icons - Fullscreen rofi menu; simple
login/sync/menucommands
Two halves that talk through a small on-disk cache:
- Reading your chats uses grammers, a Rust
MTProto client. This is a separate login from AyuGram (it can't read AyuGram's
encrypted
tdata), so each account authenticates once and its session is cached.syncpulls the dialog list, your contacts, each forum's topics, and your chat folders into a JSON cache the menu reads. - Navigating hands a
tg://link to the running client in-process over D-Bus (org.freedesktop.Application.Openon the client's well-known name —org.telegram.desktopfor Telegram,com.ayugram.desktopfor AyuGram, picked by theclientsetting). Public peers resolve by@username; private channels/supergroups open viaprivatepost?channel=<raw>; other username-less peers (users, legacy groups) open by internal peer id viatg://chat?id=; forum topics useresolve?domain=X&topic=(public) orprivatepost?channel=<raw>&topic=(private).
Switching account is done by injecting the keypress you bound in the client
(e.g. alt+2) via a configurable key-injector (inject_cmd, default
xdotool), not by any tg:// parameter. This is deliberate and worth
understanding:
tg://…&acc=Ncrashes AyuGram. The deep-link account-switch path segfaults in the builds tested (on Nix it dies silently — no crash dump, truncated log).xdg-open tg://…spawns a second client process; its single-instance handoff races and can kill the running window. So navigation uses the in-process D-BusOpeninstead, which never spawns anything.- That leaves no safe URL-based way to switch accounts, so telepad drives the client's own account-switch shortcut with a real keypress — the same switch you'd do by hand, which is safe.
Consequently, cross-account jumping needs two configurable shell commands — a
window-focuser (focus_cmd, default i3-msg) and a key-injector
(inject_cmd, default xdotool) — plus the account-switch keys bound inside
the client. Both default to X11 + i3 but are overridable for any WM, X11 or
Wayland (see Configuration). Leave switch_key empty to stay
same-account only (rock-solid, no focuser/injector needed).
- Rust (to build)
- rofi — the menu
- gdbus (GLib) — delivers the URL to the running client
- for cross-account switching only: a window-focuser and a key-injector —
defaults
i3-msg+xdotool(X11); overridefocus_cmd/inject_cmdfor other WMs (e.g.swaymsg/hyprctl+ydotoolon Wayland)
cargo build --release
# put target/release/telepad on your PATH- API credentials (free): https://my.telegram.org → API development tools → note
your
api_idandapi_hash. - Bind account-switch keys in the client (for cross-account): AyuGram/Telegram
Desktop → Settings → Advanced → keyboard shortcuts → bind
account1,account2, … to keys (e.g.alt+1,alt+2). - Configure telepad:
Set
mkdir -p ~/.config/telepad cp config.example.toml ~/.config/telepad/config.toml $EDITOR ~/.config/telepad/config.toml
api_id/api_hashand one[[accounts]]block per account, giving each itsswitch_key. - Log in each account once:
telepad login personal
The login code arrives inside your already-logged-in Telegram/AyuGram (the Telegram service chat), not by SMS. 2FA password is prompted if set.
- Build the cache:
telepad sync # all accounts (dialogs + contacts + forum topics) - Bind the menu to a key (i3 example):
bindsym $mod+g exec --no-startup-id telepad
telepad # the quick-switcher (flat, all accounts)
telepad menu # same thing
telepad sync [acct] # refresh the cache (run periodically / via cron)
telepad sync --avatars # also download profile photos (slower; shown as row icons)
telepad login <acct> # (re)authenticate an accountRows are tagged [Account], so type an account name to scope (work signals) or
just the chat name (signals). Peers with a public username show their @handle
in the row, so you can search by either the display name or the handle
(@durov). Forum rows show forum ▸, and each of their topics
appears as its own Forum ▸ Topic · topic row for direct jumping. Chat folders
show as 📁 <Folder> ▸ rows that expand into that folder's chats.
| Field | Meaning |
|---|---|
api_id / api_hash |
One app from my.telegram.org, shared by all accounts |
client |
Which client to drive: telegram (default) or ayugram. Sets the D-Bus name + window class |
dbus_service |
Override the client's D-Bus name (for other TDesktop forks); object path is derived from it |
window_class |
Override the X11 class of the client, focused before the switch key (else from client) |
focus_cmd |
Shell command to focus the client; {class} substituted. Default i3-msg [class="{class}"] focus. Override for other WMs (wmctrl, swaymsg, hyprctl) |
inject_cmd |
Shell command to press a key chord; {key} substituted. Default xdotool key --clearmodifiers {key} (X11). Override for Wayland (ydotool, wtype) |
accounts[].acc |
1-based slot in the client's account list (display/ordering) |
accounts[].label |
Name shown in rofi |
accounts[].session |
Session file name under ~/.local/share/telepad/ |
accounts[].phone |
International format; used only for telepad login |
accounts[].switch_key |
Key chord that switches to this account (e.g. alt+1), sent via inject_cmd; empty = never switch |
- Client is configurable but only battle-tested on AyuGram. The D-Bus name and
window class now follow the
clientsetting (telegram/ayugram), so vanilla Telegram Desktop is wired up and the default. But thetg://handlers differ between builds: AyuGram has atg://chat?id=handler used for username-less users/legacy groups, which vanilla Telegram Desktop may not — those specific jumps could still fail there (public@usernamepeers and private channels viaprivatepostshould work on both). Real-world vanilla testing is still on the TODO. - Cross-account depends on a focuser + key-injector and on window focus/timing.
Defaults are X11 (
i3-msg+xdotool); Wayland works by pointingfocus_cmd/inject_cmdatswaymsg/hyprctl+ydotool. Same-account is dependency-light. - Separate sessions: grammers logs in independently, so each account shows as an extra device in your Telegram sessions list.
- Stale cache: the list is a snapshot; re-run
telepad syncto pick up new chats. - "Recents" that aren't saved contacts won't appear (only dialogs + saved contacts are indexed).
- Chat folders list their explicitly-added chats. Folders defined purely by rules ("all groups", "unread", "non-contacts") with no named members won't populate, and selecting a folder chat doesn't switch the client's sidebar tab.
- "Creative" names are hard to find. As always, if someone gives a channel or
nickname a name in a fancy font — those Unicode "𝔤𝔬𝔱𝔥𝔦𝔠" / "𝓼𝓬𝓻𝓲𝓹𝓽" /
fullwidth pseudo-fonts that aren't actually plain letters but styled math/Unicode
codepoints — it won't match a normal-text fuzzy search. Typing
signalswon't find𝓼𝓲𝓰𝓷𝓪𝓵𝓼, because to the matcher they're entirely different characters. telepad indexes the raw name as-is and does no Unicode font-normalization, so you'd have to recognize and scroll to those visually.
-
Test against vanilla Telegram Desktop. The D-Bus name and window class are now configurable via
client(done); what's left is real-world verification and handling anytg://handler gaps on vanilla (notably the id-basedchat?id=open, which AyuGram provides but vanilla may not). -
Decouple from rofi — become composable plumbing. The WM/injector are already configurable (
focus_cmd/inject_cmd); the picker (rofi) is the last hardcoded touchpoint. Turn telepad into Unix-pipe primitives so anyone can bring their own picker too: -telepad listprints every jumpable target as a flat, machine-readable stream (one line per chat / topic / folder-chat / archived-chat). -telepad opentakes a selected line back (stdin/arg), maps it to its target by re-deriving the same list from cache, then runs switch + open. -telepad menustays the batteries-included default (pipeslistinto the menu command and dispatchesopen), so it's still zero-config — but power users can just dotelepad list | fzf | telepad open.All four external touchpoints become configurable commands with `{…}` placeholder substitution, exactly like today's `focus_cmd` / `{class}`: | hook | default | unlocks | |------|---------|---------| | `menu_cmd` | `rofi -dmenu` | dmenu / fzf / wofi / any picker | | `focus_cmd` | i3-msg *(exists today)* | other WMs | | `inject_cmd` | xdotool *(exists today)* | ydotool / Wayland | | `open_cmd` | gdbus … `{service}` … `{url}` | any opener (already: `client`/`dbus_service` pick the D-Bus name) | A single flat `list` stream implies folders/archive would render as flat rows (`📁 Folder ▸ Chat`, `🗄 Chat`) instead of interactive submenus — mirroring how forum topics are already flattened. The D-Bus name is already configurable (`client` / `dbus_service`); `open_cmd` would generalize the whole open command. Goal: few/no hardcoded external CLI deps. -
Investigate a non-crashing account switch (upstream fix to the
tg://…&acc=handler would remove the whole xdotool detour). -
Include recent/top peers (
contacts.getTopPeers) for non-contact recents. -
Optional
sync-on-open so the menu is always fresh.
Supersedes two earlier personal experiments (tg-rofi, rofi-tg-switcher).
MIT OR Apache-2.0.