Skip to content

Configuration

Toni Leino edited this page Jun 12, 2026 · 1 revision

Configuration

Config lives at ~/.config/slk/config.toml.

Full example

[general]
default_workspace = "work"      # the slug, not the team ID
use_slack_sections = true       # use real Slack sidebar sections (default).
                                # set false to use [sections.*] globs instead.
keep_focus_on_list = false      # keep focus on the list when you select a
                                # channel / open a thread (default false).
                                # See "Browsing without losing focus" below.

[appearance]
theme = "dracula"
timestamp_format = "3:04 PM"
image_protocol = "auto"   # auto | kitty | sixel | halfblock | off
max_image_rows = 20       # cap inline image height in terminal rows
panel_borders = true      # draw the box border around each pane;
                          # false = a thin left accent bar instead

[animations]
enabled = true
smooth_scrolling = true
typing_indicators = true

[notifications]
enabled = true
on_mention = true
on_dm = true
on_keyword = ["deploy", "incident"]
quiet_hours = "22:00-08:00"   # planned

[cache]
message_retention_days = 30
max_db_size_mb = 500
max_image_cache_mb = 200
identity_ttl_days = 7     # re-fetch a user/bot's name + avatar after N days
                          # so renames and new avatars show up; 0 = never refresh

# Glob-based channel sections — only consulted when use_slack_sections
# is false (globally or per-workspace), or when Slack's section API is
# unreachable. Otherwise slk reads the user's actual Slack sections.
[sections.Alerts]
channels = ["alerts", "ops", "*-alerts"]
order = 1

# Channels can carry an optional ":<N>" suffix to pin their order
# within the section. Lower numbers sort higher. Entries without a
# suffix fall after annotated ones, in the order they appear.
# This syntax is only honored when use_slack_sections = false;
# in Slack-native mode, channel order comes from Slack.
[sections.Engineering]
channels = ["eng-general:1", "eng-alerts:2", "eng-*"]
order = 2

# Per-workspace settings: keyed by a slug you choose at --add-workspace
# time. team_id ties the slug to the underlying Slack workspace.
[workspaces.work]
team_id = "T01ABCDEF"
order   = 1                     # rail position; 1-based, used by 1-9 keys
theme   = "dracula"             # overrides [appearance].theme
use_slack_sections = false      # this workspace uses [sections.*] globs;
                                # other workspaces still use Slack sections

[workspaces.work.sections.Alerts]
channels = ["alerts", "*-alerts"]
order = 1

[workspaces.work.sections.Engineering]
channels = ["eng-*", "deploys"]
order = 2

# A second workspace with no per-workspace sections — falls back to
# the global [sections.*] above.
[workspaces.side]
team_id = "T02XYZ"
order   = 2

# Inline color overrides on top of the active theme
[theme]
primary = "#4A9EFF"
accent = "#50C878"
background = "#1A1A2E"
text = "#E0E0E0"

External commands

Run a program against the selected message with the x key (pick from a list), or automatically when an incoming message matches a trigger.

# Manual: select a message, press `x`, pick this command.
[[external_commands]]
name = "Create task"
argv = ["/home/me/bin/slk-task"]   # run directly (no shell)

[[external_commands]]
name = "OCR image"
argv = ["tesseract", "-", "-"]
capture_output = true              # show stdout in an overlay (else a toast)

[[external_commands]]
name = "Edit in editor"
argv = ["nvim"]
interactive = true                 # suspend the TUI and attach the terminal
confirm = true                     # ask before running

# Auto: runs in the background when an incoming message matches.
[[external_commands]]
name = "Notify on mention"
argv = ["/home/me/bin/notify"]
on_mention = true                  # fires when you are @-mentioned

[[external_commands]]
name = "Page on deploy fail"
argv = ["/home/me/bin/page"]
match = "deploy failed"            # case-insensitive substring
# match_regex = "deploy (failed|errored)"   # alternative: RE2 regex
channels = ["alerts", "ops"]       # optional: only these channel names
include_self = false               # set true to also fire on your own messages

How the message reaches the command. argv is executed directly (no shell, so no quoting hazards). The message text is piped to stdin, and metadata is provided via environment variables:

Variable Value
SLK_TEXT message text (also on stdin)
SLK_USER / SLK_USER_ID author display name / ID
SLK_TS message timestamp
SLK_CHANNEL / SLK_CHANNEL_NAME channel ID / name
SLK_PERMALINK archive link (when known)
SLK_IMAGE_PATHS newline-separated on-disk paths of the message's cached images
SLK_IMAGE_COUNT number of image paths

Execution flags (manual or auto): capture_output shows stdout in a scrollable overlay instead of a completion toast; interactive releases the terminal to the command (editors / TUI tools) and restores slk on exit; confirm prompts first. Auto-runs are always in the background (never interactive).

Auto-triggers. A command with on_mention, match, and/or match_regex also runs automatically on incoming messages (across all workspaces and channels), firing if any of its triggers match. channels limits it to named channels. Your own messages are skipped unless include_self = true. A command can be both manual (x) and auto.

Browsing without losing focus

By default, picking a channel from the sidebar — or opening a thread — moves keyboard focus into the content pane, so you can immediately j/k through messages, react, and reply. The trade-off: to try another channel you have to Tab/h back to the sidebar first.

Set keep_focus_on_list = true under [general] to keep focus on the list instead. Selecting a channel or opening a thread still loads the content, but keyboard focus stays put — so you can keep walking the list with j/k and Enter without tabbing back each time. When you want to read or reply, step into the content with Tab, l, or . This applies to all three list→content jumps: sidebar → channel, threads view → thread, and a channel message → its thread. The default is false (focus follows selection).

Section resolution

When use_slack_sections = true (the default) and Slack's section endpoint is reachable, slk reads the user's actual sidebar sections — names, emoji, linked-list order, and channel membership — directly from Slack and keeps them live via WebSocket events. Any [sections.*] or [workspaces.<slug>.sections.*] blocks in config.toml are ignored in this mode (a one-line info note is emitted to the debug log on first connect so the shadowing isn't silent). Set use_slack_sections = false globally, or per-workspace, to opt into glob-based sections instead.

Per-workspace [workspaces.<slug>.sections.*] blocks fully replace the global [sections.*] for that workspace. Workspaces that define no sections of their own fall back to the global table.

Ordering channels within a section

Each entry in a section's channels list may carry an optional :<N> suffix where N is a non-negative integer. Channels matched by an annotated pattern sort ahead of channels matched by un-annotated patterns; among annotated channels, lower N wins; un-annotated channels keep the order Slack returned them in.

[sections.Engineering]
channels = ["eng-general:1", "eng-alerts:2", "eng-*"]
order = 2

In the example above, #eng-general is pinned to the top of the Engineering section, followed by #eng-alerts, followed by every other eng-* channel in Slack-API order.

This syntax is only honored when use_slack_sections = false (or when Slack's section endpoint is unreachable and slk falls back to glob mode). In Slack-native mode, channel order within a section comes from Slack and the :<N> suffix is ignored along with the rest of the [sections.*] block.

v1 limitations of Slack-native sections

v1 is read-only — section editing still happens in the official client; slk reflects the results. Your Starred conversations (Slack's stars section) are surfaced read-only and pinned to the top of the sidebar: star or unstar in the official client and slk reflects it live. Other system sections (slack_connect, salesforce_records, agents) remain hidden. Because this comes from Slack's section data, the Starred section only appears in Slack-native mode (use_slack_sections = true, the default); it is not available in glob-section mode. Sections with more than 10 channels may be returned only partially by Slack's API on initial load; the missing channels temporarily fall into the catch-all bucket and migrate into their correct section as WebSocket events fire or the workspace reconnects. A debug-log warning identifies which sections were truncated.

Workspace order

The order field controls workspace position in the rail and the mapping for the 19 digit keys. Positive values sort ascending (lowest first); workspaces without an order (or with order = 0) sort after explicitly ordered ones, alphabetically by slug. Tokens on disk that have no [workspaces.<slug>] block at all sort last, alphabetically by team ID. The order is stable across runs. Previously the rail order depended on which workspace's WebSocket connected first; it is now deterministic regardless of network timing, even without an explicit order set.

Legacy configs that key the block by raw team ID ([workspaces.T01ABCDEF]) keep working unchanged.

Terminal-palette themes (ANSI Dark, ANSI Light)

Two built-in themes use ANSI 16 color codes exclusively rather than fixed RGB values. They inherit the user's terminal color palette, so changing your terminal colorscheme (light/dark, solarized, accessibility palettes, etc.) immediately changes slk's UI colors to match.

[appearance]
theme = "ANSI Dark"   # or "ANSI Light"

Pick the variant whose background matches your terminal's background.

Trade-off: selection-row highlights and compose-input tints are still computed as RGB approximations, so the tint regions of those elements use truecolor rather than your palette. The rest of the UI honors the palette.

Custom themes

Drop .toml files into ~/.config/slk/themes/:

name = "My Theme"

[colors]
primary      = "#BD93F9"
accent       = "#50FA7B"
warning      = "#FFB86C"
error        = "#FF5555"
background   = "#282A36"
surface      = "#343746"
surface_dark = "#21222C"
text         = "#F8F8F2"
text_muted   = "#6272A4"
border       = "#44475A"

# Optional sidebar/rail overrides — lets you have a darker sidebar with a
# lighter message pane (Slack's default look). Fall back to
# background/text/text_muted/surface_dark when omitted.
sidebar_background = "#19171D"
sidebar_text       = "#D1D2D3"
sidebar_text_muted = "#9A9B9E"
rail_background    = "#19171D"

Every built-in theme now sets a channels-panel (sidebar) background that is perceptibly distinct from the message pane. When writing a custom theme, set sidebar_background to a clearly darker (or, on near-black themes, a slightly lighter) shade than background for the same effect.

Switch themes live with Ctrl+y.

Data paths (XDG)

Path Contents
~/.config/slk/ Configuration, custom themes
~/.local/share/slk/ SQLite cache, tokens
~/.cache/slk/ Avatars, image cache

Clone this wiki locally