Skip to content

Latest commit

 

History

65 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

teams-tui

An intuitive terminal UI for Microsoft Teams chats, built in Go with Bubble Tea v2. It focuses on one-to-one, group, and meeting chats, and gives you a heads-up when a meeting is about to start.

 Teams — Ada Lovelace
┌──────────────────────────────┐┌────────────────────────────────────────────┐
│ Chats                        ││ Grace Hopper          14:02                  │
│ [>] Grace Hopper             ││ Did you push the nanosecond demo?            │
│     Grace: did you push…     ││                                              │
│ [#] Compiler Team            ││ You                   14:03                  │
│     You: shipping v2 now     ││ Yes — merged and shipping v2 now.            │
│ [@] Standup                  ││                                              │
│     Alan: notes attached     │└──────────────────────────────────────────────┘
│                              │┌────────────────────────────────────────────┐
│                              ││ Type a message… (enter to send)              │
└──────────────────────────────┘└────────────────────────────────────────────┘
 COMPOSE · 3 chats
 tab next pane · enter send · ctrl+r refresh · ? help · ctrl+c quit

Features

  • Chats only, done well: one-to-one, multi-person group, and meeting chats.
  • Live updates via lightweight polling of Microsoft Graph.
  • Send messages inline from the compose box.
  • Code blocks: incoming code blocks render as styled panels with per-language syntax highlighting (via chroma), plus inline `code`. Teams' own <codeblock> element and <pre>/<code> from other clients are both recognized, and the language is detected from the message (e.g. a class="Php" block highlights as PHP). When composing, type a Markdown fence (```, optionally with a language like ```go) to start a code block: enter then adds new lines instead of sending, so you can type multiple lines, and a closing ``` line lets enter send as usual. Both render as real code blocks for everyone, including the native Teams client.
  • Emoji: incoming Teams emoji render as Unicode glyphs. When composing, classic emoticons (:-), <3, :D) are auto-replaced with the emoji as you type, like the desktop client. Colon-style emoticons (:), :p, :D) convert only once you type a space after them, so they don't get in the way of shortcodes that share the same start (e.g. :party). Typing : followed by at least two characters opens an inline shortcode picker (:thumbsup:) above the compose box (/ to select, tab/enter to insert, esc to dismiss) without interrupting your typing. The full standard emoji set is supported, and shortcodes match with or without separators — :nauseatedface: (the Teams form) and :nauseated_face: both work. Press ctrl+j to open a browser of every emoji and filter it interactively.
  • @-mentions: in a group or meeting chat, type @ to open a participant picker and tab/enter to complete a name (/ to choose, keep typing to filter by first name or surname). The completed name is sent as a real Teams mention, so the person is notified and the mention highlights for everyone.
  • Spell check: as you type, misspelled words and their top corrections appear on a strip beneath the compose box. Press ctrl+f while composing to open a correction picker (/ to choose a word → suggestion, enter to apply, esc to cancel). It uses the system spell checker (enchant-2, the GNOME/gspell layer, falling back to hunspell), so it honours your installed dictionaries and personal word list. Configure the dictionary with spell_language (e.g. en_GB) or turn it off with disable_spell_check; it's silently inactive if no helper is installed.
  • Presence: see each participant's Teams status next to their name, and set your own status from a popup (ctrl+s). While running, the app maintains a presence session so your chosen status persists like a native client.
  • Meeting alerts: an in-TUI banner and a desktop notification when a meeting is starting within your lookahead window.
  • Message notifications: a desktop notification for new incoming messages across all your chats (your own messages and the chat you're actively viewing while the terminal is focused are skipped). Disable with disable_desktop_notify, which also turns off meeting alerts.
  • Click to jump to the chat: on Linux, clicking a message notification raises the terminal window, selects teams-tui's tmux pane (auto-detected from $TMUX_PANE), and switches to the sending chat. Window raising uses the focus_command config (default targets GNOME's Ptyxis via its D-Bus activation, passing the notification's XDG activation token so Wayland allows the raise); set your own command for other terminals/WMs — e.g. an X11 wmctrl -a/xdotool invocation — or "-" to disable raising while still switching the chat. Clickable notifications need a D-Bus notification daemon that advertises the actions capability; elsewhere the notification still appears but isn't clickable.
  • Hybrid-friendly auth: uses the OAuth 2.0 device authorization grant, so sign-in happens in your real browser. This works for both fully Entra-hosted tenants and hybrid Entra/Active Directory federated setups — if your tenant redirects to a company-hosted web login (ADFS or similar), that page simply appears during the browser step.
  • Secure token cache: refresh tokens are stored in your OS keyring (Keychain / Secret Service / Credential Manager), so you don't re-auth every launch.
  • Built almost entirely from off-the-shelf Bubbles components (list, viewport, textarea, spinner, help, key) and Lip Gloss styling.

Requirements

  • Go 1.24+
  • A registered Microsoft Entra application (see below)
  • On Linux, a Secret Service provider (e.g. GNOME Keyring / KWallet) for the token cache

Register an Entra application

The device-code flow needs a public client application registered in your tenant. An administrator performs these one-time steps in the Entra admin center:

  1. Identity → Applications → App registrations → New registration.

    • Name: teams-tui (anything).
    • Supported account types: Accounts in this organizational directory only (single tenant) is recommended for corporate/hybrid setups.
    • Leave the Redirect URI blank.
  2. Open the new app → Authentication.

    • Under Advanced settings, set Allow public client flows to Yes. (This enables the device-code grant.)
  3. Open API permissions → Add a permission → Microsoft Graph → Delegated permissions and add:

    • User.Read
    • Chat.ReadWrite
    • People.Read (to list contacts and start new chats from them)
    • Calendars.Read (for meeting notifications; optional)
    • Presence.Read.All (to show participants' Teams status; needs admin consent)
    • Presence.ReadWrite (to show and set your own status)
    • Click Grant admin consent if required by your tenant.

    Note: openid, profile, and offline_access are OpenID Connect scopes, not Graph resource permissions — they won't appear in the main Graph permissions list. You do not need to add them in the portal: teams-tui requests them as scopes during sign-in and Entra grants them automatically. (If you want them listed explicitly, they live under the collapsed "OpenId permissions" group on the same Microsoft Graph → Delegated permissions screen.)

  4. Note the Application (client) ID and the Directory (tenant) ID from the app's Overview page.

For hybrid Entra/AD federated tenants, always use the specific tenant GUID (not common/organizations) so the browser sign-in is routed to your federation/web login.

Configuration

teams-tui reads configuration from environment variables and/or a JSON file. Environment variables take precedence.

Environment variables

Variable Description
TEAMS_TUI_CLIENT_ID Application (client) ID. Required.
TEAMS_TUI_TENANT_ID Directory (tenant) ID or GUID. Default organizations.
TEAMS_TUI_AUTH_HOST Login host. Default https://login.microsoftonline.com.
TEAMS_TUI_GRAPH_BASE_URL Graph base URL. Default https://graph.microsoft.com/v1.0.
TEAMS_TUI_SCOPES Space-separated scope override.
TEAMS_TUI_CONFIG Path to the JSON config file.

Config file

Default location: $XDG_CONFIG_HOME/teams-tui/config.json (typically ~/.config/teams-tui/config.json).

{
  "client_id": "00000000-0000-0000-0000-000000000000",
  "tenant_id": "11111111-1111-1111-1111-111111111111",
  "poll_interval_seconds": 10,
  "max_chats": 200,
  "meeting_lookahead_minutes": 5,
  "disable_desktop_notify": false,
  "code_block_style": "monokai",
  "disable_spell_check": false,
  "spell_language": "",
  "focus_command": "gdbus call --session --dest org.gnome.Ptyxis --object-path /org/gnome/Ptyxis --method org.freedesktop.Application.Activate \"{'activation-token': <'{token}'>}\""
}

max_chats caps how many chats are loaded into the sidebar. Graph returns them most-recently-active first, 50 per page, and teams-tui follows the paging links until it has this many, so raising it costs one extra request per 50 chats on every poll. Defaults to 200; overridable via TEAMS_TUI_MAX_CHATS.

code_block_style selects the chroma theme used to syntax-highlight code blocks (e.g. monokai, dracula, github-dark, nord, solarized-dark). It defaults to monokai; an unknown name falls back to that default. It can also be set via TEAMS_TUI_CODE_BLOCK_STYLE.

disable_spell_check turns off compose-box spell checking, and spell_language selects the dictionary (e.g. en_US, en_GB; empty uses the system default). Spell checking shells out to the system helper — enchant-2 (the GNOME/gspell spell layer), falling back to hunspell — so on Fedora/GNOME install the enchant2 and hunspell packages plus a dictionary (hunspell-en) to enable it. Misspelled words and their top suggestions appear on a strip beneath the compose box; the feature is silently disabled when no helper is installed. These can also be set via TEAMS_TUI_DISABLE_SPELL_CHECK and TEAMS_TUI_SPELL_LANGUAGE.

focus_command is the shell command run when a desktop notification is clicked, to raise the terminal window (see Click to jump to the chat above). The literal {token} is replaced with the notification's XDG activation token, which is also exported to the command as XDG_ACTIVATION_TOKEN. It defaults to the GNOME Ptyxis command shown above; set it to another terminal/WM command (X11 users can use wmctrl -a teams-tui or an xdotool invocation) or "-" to disable window raising while still switching chats and selecting the tmux pane. Overridable via TEAMS_TUI_FOCUS_COMMAND.

For a sovereign cloud or a custom federation host, override auth_host and graph_base_url accordingly.

Build & run

go build -o teams-tui ./cmd/teams-tui
TEAMS_TUI_CLIENT_ID=<client-id> TEAMS_TUI_TENANT_ID=<tenant-id> ./teams-tui

On first launch you'll see a verification URL and a short code. Open the URL in any browser, sign in (your company login page will appear if your tenant is federated), enter the code, and the TUI loads your chats. Subsequent launches reuse the cached refresh token from your keyring.

Re-authenticating after adding a permission

If you add a new Graph permission (e.g. Presence.Read.All), the cached token won't include it and calls will fail with 403 InsufficientPrivileges. The app detects this automatically — on the next launch it sees the cached token no longer covers the requested scopes and starts a fresh sign-in (with the new consent prompt). To force it immediately:

./teams-tui --logout   # clears the cached token from the keyring
./teams-tui            # next launch prompts for sign-in + new consent

Make sure admin consent is granted for the new permission in the app registration first if your tenant requires it.

Key bindings

Key Action
tab / shift+tab Move focus between Chats / Messages / Compose
/ j/k Navigate the focused pane; in Messages, move the message selection
click Focus the clicked pane: a chat row opens that chat (Chats), a message selects it (Messages), the compose box puts the cursor on the clicked character
click + drag Highlight text in the Messages pane or the compose box (drag over the text you want to select)
scroll ↑/↓ Scroll the conversation (the sidebar is not wheel-scrolled: use the keys, a click, or page it with a horizontal scroll)
scroll ←/→ Page the chat list back/forward — one page per swipe — landing on the chat at the top of that page (works anywhere on screen; needs a tilt wheel or a trackpad's sideways swipe)
y / c Copy the highlighted text selection to the system clipboard (Messages)
ctrl+c Copy the highlighted compose-box selection to the system clipboard; with nothing highlighted, quits
enter Open selected chat (Chats) / send message (Compose) / start chat (Contacts)
r React to the selected message (Messages): opens a searchable emoji picker; reacting with an emoji you already used removes it
q Quote-reply (Messages): with no text highlighted, starts a native Teams reply to the whole message (shows a preview banner above the composer; enter to send, esc to cancel). With text highlighted, prefills just that selection into the composer as a quote (sent as an inline quote block)
alt+enter Insert a newline in the compose box
: + 2 chars Open the inline emoji picker while composing (/ select, tab/enter insert, esc close)
ctrl+j Open the full emoji browser while composing (mnemonic: emoJi): lists every emoji and filters as you type (/ select, enter insert at cursor, esc close)
@ In a group/meeting chat, open the participant picker to @-mention someone (/ select, tab/enter complete, keep typing to filter, esc dismiss)
ctrl+f Open the spelling correction picker while composing: choose a word → suggestion (/ select, enter apply, esc close)
/ Filter the chat list / search contacts
ctrl+o Toggle the sidebar between Chats and Contacts (start a new chat)
ctrl+e Edit a message: the selected message in the Messages pane if it's yours, otherwise your most recent message
ctrl+y / click Open an image in your default viewer/browser (ctrl+y = newest; click a placeholder for that one)
ctrl+v Paste an image from the clipboard and attach it to the next message (type a caption, then enter to send; esc to discard)
esc Drop a compose-box text selection; otherwise clear the compose box (empties typed text, cancels an edit or reply, discards a staged image)
ctrl+r Refresh now
ctrl+s Open the status picker (set your presence)
ctrl+g Toggle full help (esc also closes it): all key bindings, plus a legend for the sidebar's symbols
ctrl+c Quit (see above: copies first when compose text is highlighted)

Sidebar symbols

The expanded help (ctrl+g) ends with this legend, so you don't have to come back here for it.

Symbol Meaning
[>] One-to-one chat
[#] Group chat
[@] Meeting chat
bold orange row Chat with unread messages
green Available
red Busy, in a call, in a meeting, or presenting
red Do not disturb / focusing
amber Away, be right back, or out of office
grey Offline or unknown

The presence dots appear on rows in Contacts mode (ctrl+o), in the conversation header, and next to your own status in the footer.

Architecture

cmd/teams-tui        program entrypoint, signal handling, wiring
internal/config      config loading (env + JSON) and endpoint URLs
internal/auth        OAuth device-code flow, refresh, keyring token store
internal/graph       Microsoft Graph client (chats, messages, calendar) + types
internal/notify      desktop notifications (beeep + D-Bus actions), degrade-gracefully
internal/focus       raise terminal window + select tmux pane on notification click
internal/ui          Bubble Tea v2 model/update/view, commands, components
internal/ui/styles   Lip Gloss styles

The UI follows The Elm Architecture: all Graph/auth I/O happens inside tea.Cmds that return typed messages, which the single Update function folds into the model. Polling is driven by tea.Tick.

Code blocks round-trip through three places that share the same fence convention: graph/code.go parses Teams' stored HTML (its <codeblock> element and <pre>/<code>) into Markdown-ish fences, graph/compose.go converts outgoing compose text back into the <pre><code> HTML Teams expects, and ui/highlight.go syntax-highlights the rendered block using a chroma theme (code_block_style).

Notes & limitations

  • Reading tokens is intentionally avoided in code; tokens are treated as opaque per Microsoft guidance.
  • Message history is fetched per chat on demand and refreshed on the poll tick.
  • Quote-replies default to Teams' native format: pressing q on a message (with nothing highlighted) sends a messageReference attachment (graph/quote.go, Client.SendMessageReply) — the same structure the desktop client stores, so it shows as a real threaded reply. Incoming replies (from any client) carry the quoted message as a messageReference attachment too; Message.PlainText() resolves it into > Sender wrote: / > quoted text lines, which the renderer styles with a left bar (ui/view.go).
  • The native format references a whole message by id, so it can't quote a sub-range. When text is highlighted and you press q, the selection is instead prefilled into the composer as > -prefixed lines and sent as an inline <blockquote> (graph/compose.go) — quoted text in every client, just not a threaded reply. Incoming/older <blockquote> quotes are understood on the receive side too (graph/text.go).
  • Chats with unread messages are shown in orange text in the sidebar (the selected chat keeps its pink highlight). Unread state comes from Graph's per-chat viewpoint read marker; opening a chat marks it read locally and on the server (via markChatReadForUser, covered by Chat.ReadWrite), so it syncs to your other devices. Your own most recent message never counts as unread.
  • This is a chat-focused client; it does not implement calls, channels/teams browsing, files, or app tabs.
  • Reactions are rendered and can be added/removed (r on a selected message, via setReaction/unsetReaction, covered by Chat.ReadWrite). Quote-replies are sent as a native Teams messageReference and rendered with a left bar, so they show as a proper reply for every participant.
  • Images in messages are shown as placeholders (🖼 [n] name); press ctrl+y to open the newest one, or click a placeholder to open that specific image, in your OS default image viewer/browser (inline Graph hosted content is downloaded with your token first). Other attachment types are not yet rendered.
  • You can highlight message text with a click-and-drag in the Messages pane, then y/c to copy it to the system clipboard or q to quote just that selection into a reply. Copying shells out to a platform clipboard helper, so the relevant tool must be installed: wl-copy (Wayland) or xclip (X11) on Linux, pbcopy on macOS, and clip on Windows. (The app captures the mouse for its own selection; to fall back to your terminal's native selection, hold the modifier your terminal uses to bypass app mouse reporting — often shift.)
  • Clicking any pane focuses it, so the keyboard follows the mouse; clicking a chat row also opens that chat. The wheel scrolls the conversation, and a horizontal wheel (tilt wheel or a trackpad's sideways swipe) pages the chat list from anywhere on screen, opening the chat at the top of the page it lands on — from there, arrow through the page or click a row. A trackpad reports one swipe as a burst of scroll events, so paging is throttled to one page per gesture (swiping back the other way takes effect immediately). The wheel deliberately doesn't scroll the sidebar itself: moving the highlight opens the chat it lands on, so free-running wheel scrolling would be a stream of chat loads. Typing a printable character while the sidebar has focus still jumps straight to the compose box.
  • The compose box takes the mouse too: click to put the cursor on a character, or click-and-drag to highlight text. ctrl+c copies the highlight (and only quits when nothing is highlighted), and typing, pasting or backspace/delete replaces it, as in any editor. esc drops the highlight.
  • You can paste an image from the clipboard with ctrl+v to send it inline. This shells out to a platform clipboard helper, so the relevant tool must be installed: wl-paste (Wayland) or xclip (X11) on Linux, osascript on macOS, and powershell on Windows. The image is sent as Teams hosted content via Chat.ReadWrite (no extra scope required); any text you type becomes the image's caption.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages