Skip to content

Instantly share code, notes, and snippets.

@ericboehs
Last active December 11, 2025 19:13
Show Gist options
  • Select an option

  • Save ericboehs/7c741ccb7db890eaf5eb88e8c50dfb65 to your computer and use it in GitHub Desktop.

Select an option

Save ericboehs/7c741ccb7db890eaf5eb88e8c50dfb65 to your computer and use it in GitHub Desktop.
CLI tool to set Slack status from the command line (supports multiple workspaces, presets, age encryption)
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.1.0"
SCRIPT_NAME="slack-status"
# XDG Base Directory support
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/slack-status"
TOKENS_FILE="$CONFIG_DIR/tokens.json"
TOKENS_FILE_ENCRYPTED="$CONFIG_DIR/tokens.age"
CONFIG_FILE="$CONFIG_DIR/config.json"
PRESETS_FILE="$CONFIG_DIR/presets.json"
# Colors (disabled if not a terminal)
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC=''
fi
# Verbosity
VERBOSE=false
QUIET=false
#######################################
# Utility Functions
#######################################
log_info() {
$QUIET || echo -e "${GREEN}βœ“${NC} $*"
}
log_warn() {
$QUIET || echo -e "${YELLOW}⚠${NC} $*" >&2
}
log_error() {
echo -e "${RED}βœ—${NC} $*" >&2
}
log_verbose() {
$VERBOSE && echo -e "${BLUE}β†’${NC} $*" || true
}
die() {
log_error "$@"
exit 1
}
check_dependency() {
command -v "$1" &>/dev/null
}
ensure_jq() {
check_dependency jq || die "jq is required but not installed. Install it with: brew install jq (macOS) or apt install jq (Linux)"
}
ensure_curl() {
check_dependency curl || die "curl is required but not installed."
}
has_age() {
check_dependency age
}
#######################################
# Duration Parsing
#######################################
parse_duration() {
local input="$1"
local total_seconds=0
# If it's just a number, treat as seconds
if [[ "$input" =~ ^[0-9]+$ ]]; then
echo "$input"
return
fi
# Parse human-readable format like 1h30m, 2h, 45m
local remaining="$input"
while [[ -n "$remaining" ]]; do
if [[ "$remaining" =~ ^([0-9]+)h(.*)$ ]]; then
total_seconds=$((total_seconds + ${BASH_REMATCH[1]} * 3600))
remaining="${BASH_REMATCH[2]}"
elif [[ "$remaining" =~ ^([0-9]+)m(.*)$ ]]; then
total_seconds=$((total_seconds + ${BASH_REMATCH[1]} * 60))
remaining="${BASH_REMATCH[2]}"
elif [[ "$remaining" =~ ^([0-9]+)s(.*)$ ]]; then
total_seconds=$((total_seconds + ${BASH_REMATCH[1]}))
remaining="${BASH_REMATCH[2]}"
else
die "Invalid duration format: $input (use format like 1h30m, 2h, 45m, or seconds)"
fi
done
echo "$total_seconds"
}
format_duration() {
local seconds="$1"
if [[ "$seconds" -eq 0 ]]; then
echo "no expiration"
return
fi
local hours=$((seconds / 3600))
local minutes=$(((seconds % 3600) / 60))
local result=""
[[ $hours -gt 0 ]] && result="${hours}h"
[[ $minutes -gt 0 ]] && result="${result}${minutes}m"
[[ -z "$result" ]] && result="${seconds}s"
echo "$result"
}
#######################################
# SSH Key Detection
#######################################
detect_ssh_key() {
local ed25519_key="$HOME/.ssh/id_ed25519"
local rsa_key="$HOME/.ssh/id_rsa"
local has_ed25519=false
local has_rsa=false
[[ -f "$ed25519_key" ]] && has_ed25519=true
[[ -f "$rsa_key" ]] && has_rsa=true
if $has_ed25519 && $has_rsa; then
# Both exist, prompt with ed25519 as default
echo "both:$ed25519_key:$rsa_key"
elif $has_ed25519; then
echo "ed25519:$ed25519_key"
elif $has_rsa; then
echo "rsa:$rsa_key"
else
echo "none"
fi
}
#######################################
# Token Encryption/Decryption
#######################################
encrypt_tokens() {
local ssh_key="$1"
local tokens_json="$2"
if has_age; then
echo "$tokens_json" | age -R "$ssh_key.pub" -o "$TOKENS_FILE_ENCRYPTED"
rm -f "$TOKENS_FILE"
log_verbose "Tokens encrypted with age"
else
echo "$tokens_json" > "$TOKENS_FILE"
chmod 600 "$TOKENS_FILE"
log_warn "age not found. Tokens stored unencrypted in $TOKENS_FILE"
fi
}
decrypt_tokens() {
if [[ -f "$TOKENS_FILE_ENCRYPTED" ]]; then
local ssh_key
ssh_key=$(get_config_value ".ssh_key" "")
if [[ -z "$ssh_key" ]]; then
die "No SSH key configured. Run '$SCRIPT_NAME config' to set up."
fi
if ! has_age; then
die "Tokens are encrypted but age is not installed. Install it with: brew install age (macOS) or apt install age (Linux)"
fi
age -d -i "$ssh_key" "$TOKENS_FILE_ENCRYPTED" 2>/dev/null || die "Failed to decrypt tokens. Check your SSH key."
elif [[ -f "$TOKENS_FILE" ]]; then
cat "$TOKENS_FILE"
else
echo "{}"
fi
}
#######################################
# Config Management
#######################################
ensure_config_dir() {
mkdir -p "$CONFIG_DIR"
}
get_config_value() {
local key="$1"
local default="${2:-}"
if [[ -f "$CONFIG_FILE" ]]; then
local value
value=$(jq -r "$key // empty" "$CONFIG_FILE" 2>/dev/null)
if [[ -n "$value" && "$value" != "null" ]]; then
echo "$value"
return
fi
fi
echo "$default"
}
set_config_value() {
local key="$1"
local value="$2"
ensure_config_dir
if [[ -f "$CONFIG_FILE" ]]; then
local tmp
tmp=$(mktemp)
jq "$key = $value" "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE"
else
echo "{}" | jq "$key = $value" > "$CONFIG_FILE"
fi
}
get_primary_workspace() {
get_config_value ".primary_workspace" ""
}
set_primary_workspace() {
set_config_value ".primary_workspace" "\"$1\""
}
#######################################
# Workspace Management
#######################################
get_workspaces() {
decrypt_tokens | jq -r 'keys[]' 2>/dev/null
}
get_workspace_token() {
local workspace="$1"
decrypt_tokens | jq -r ".\"$workspace\".token // empty"
}
get_workspace_cookie() {
local workspace="$1"
decrypt_tokens | jq -r ".\"$workspace\".cookie // empty"
}
workspace_exists() {
local workspace="$1"
local token
token=$(get_workspace_token "$workspace")
[[ -n "$token" ]]
}
add_workspace() {
local name="$1"
local token="$2"
local cookie="${3:-}"
local current_tokens
current_tokens=$(decrypt_tokens)
local new_tokens
if [[ -n "$cookie" ]]; then
new_tokens=$(echo "$current_tokens" | jq ".\"$name\" = {\"token\": \"$token\", \"cookie\": \"$cookie\"}")
else
new_tokens=$(echo "$current_tokens" | jq ".\"$name\" = {\"token\": \"$token\"}")
fi
local ssh_key
ssh_key=$(get_config_value ".ssh_key" "")
if [[ -n "$ssh_key" ]] && has_age; then
encrypt_tokens "$ssh_key" "$new_tokens"
else
echo "$new_tokens" > "$TOKENS_FILE"
chmod 600 "$TOKENS_FILE"
fi
}
remove_workspace() {
local name="$1"
local current_tokens
current_tokens=$(decrypt_tokens)
local new_tokens
new_tokens=$(echo "$current_tokens" | jq "del(.\"$name\")")
local ssh_key
ssh_key=$(get_config_value ".ssh_key" "")
if [[ -n "$ssh_key" ]] && has_age; then
encrypt_tokens "$ssh_key" "$new_tokens"
else
echo "$new_tokens" > "$TOKENS_FILE"
chmod 600 "$TOKENS_FILE"
fi
}
#######################################
# Preset Management
#######################################
ensure_default_presets() {
if [[ ! -f "$PRESETS_FILE" ]]; then
ensure_config_dir
cat > "$PRESETS_FILE" << 'EOF'
{
"meeting": {
"text": "In a meeting",
"emoji": ":spiral_calendar_pad:",
"duration": "1h",
"dnd": "1h"
},
"lunch": {
"text": "Out to lunch",
"emoji": ":sandwich:",
"duration": "1h"
},
"focus": {
"text": "Deep work",
"emoji": ":headphones:",
"duration": "2h",
"presence": "away",
"dnd": "2h"
},
"afk": {
"text": "Away from keyboard",
"emoji": ":walking:",
"duration": "30m"
},
"brb": {
"text": "Be right back",
"emoji": ":brb:",
"duration": "15m"
},
"pto": {
"text": "Out of office",
"emoji": ":palm_tree:",
"duration": "0"
}
}
EOF
fi
}
get_preset() {
local name="$1"
ensure_default_presets
jq -r ".\"$name\" // empty" "$PRESETS_FILE"
}
list_presets() {
ensure_default_presets
jq -r 'to_entries[] | "\(.key): \(.value.emoji) \(.value.text) (\(.value.duration // "no expiration"))\(if .value.presence then " [presence: \(.value.presence)]" else "" end)\(if .value.dnd then " [dnd: \(.value.dnd)]" else "" end)"' "$PRESETS_FILE"
}
add_preset() {
local name="$1"
local text="$2"
local emoji="$3"
local duration="${4:-0}"
local presence="${5:-}"
local dnd="${6:-}"
ensure_default_presets
local tmp
tmp=$(mktemp)
local preset_obj="{\"text\": \"$text\", \"emoji\": \"$emoji\", \"duration\": \"$duration\""
[[ -n "$presence" ]] && preset_obj="$preset_obj, \"presence\": \"$presence\""
[[ -n "$dnd" ]] && preset_obj="$preset_obj, \"dnd\": \"$dnd\""
preset_obj="$preset_obj}"
jq ".\"$name\" = $preset_obj" "$PRESETS_FILE" > "$tmp" && mv "$tmp" "$PRESETS_FILE"
}
delete_preset() {
local name="$1"
ensure_default_presets
local tmp
tmp=$(mktemp)
jq "del(.\"$name\")" "$PRESETS_FILE" > "$tmp" && mv "$tmp" "$PRESETS_FILE"
}
#######################################
# Slack API
#######################################
slack_api_call() {
local workspace="$1"
local method="$2"
local data="${3:-}"
local token cookie
token=$(get_workspace_token "$workspace")
cookie=$(get_workspace_cookie "$workspace")
if [[ -z "$token" ]]; then
die "No token found for workspace: $workspace"
fi
local curl_args=(
-sS
-X POST
"https://slack.com/api/$method"
-H "Authorization: Bearer $token"
-H "Content-Type: application/json; charset=utf-8"
)
# Add cookie header if present (for xoxc tokens)
if [[ -n "$cookie" ]]; then
curl_args+=(-H "Cookie: d=$cookie")
fi
if [[ -n "$data" ]]; then
curl_args+=(--data "$data")
fi
curl "${curl_args[@]}"
}
set_status() {
local workspace="$1"
local text="$2"
local emoji="$3"
local duration="$4"
local expiration=0
if [[ "$duration" -gt 0 ]]; then
expiration=$(($(date +%s) + duration))
fi
local payload
payload=$(jq -n \
--arg text "$text" \
--arg emoji "$emoji" \
--argjson exp "$expiration" \
'{profile: {status_text: $text, status_emoji: $emoji, status_expiration: $exp}}')
log_verbose "Setting status on $workspace: $text $emoji ($(format_duration "$duration"))"
log_verbose "Payload: $payload"
local response
response=$(slack_api_call "$workspace" "users.profile.set" "$payload")
log_verbose "Response: $response"
local ok
ok=$(echo "$response" | jq -r '.ok')
if [[ "$ok" == "true" ]]; then
return 0
else
local error
error=$(echo "$response" | jq -r '.error // "unknown error"')
log_error "Failed to set status on $workspace: $error"
return 1
fi
}
get_status() {
local workspace="$1"
local response
response=$(slack_api_call "$workspace" "users.profile.get")
log_verbose "Response: $response"
local ok
ok=$(echo "$response" | jq -r '.ok')
if [[ "$ok" == "true" ]]; then
local text emoji expiration
text=$(echo "$response" | jq -r '.profile.status_text // ""')
emoji=$(echo "$response" | jq -r '.profile.status_emoji // ""')
expiration=$(echo "$response" | jq -r '.profile.status_expiration // 0')
if [[ -z "$text" && -z "$emoji" ]]; then
echo "No status set"
else
local exp_str=""
if [[ "$expiration" -gt 0 ]]; then
local now remaining
now=$(date +%s)
remaining=$((expiration - now))
if [[ $remaining -gt 0 ]]; then
exp_str=" (expires in $(format_duration "$remaining"))"
else
exp_str=" (expired)"
fi
fi
echo "$emoji $text$exp_str"
fi
return 0
else
local error
error=$(echo "$response" | jq -r '.error // "unknown error"')
log_error "Failed to get status from $workspace: $error"
return 1
fi
}
clear_status() {
local workspace="$1"
set_status "$workspace" "" "" 0
}
#######################################
# Presence API
#######################################
set_presence() {
local workspace="$1"
local presence="$2" # "away" or "auto"
local payload
payload=$(jq -n --arg presence "$presence" '{presence: $presence}')
log_verbose "Setting presence on $workspace: $presence"
local response
response=$(slack_api_call "$workspace" "users.setPresence" "$payload")
log_verbose "Response: $response"
local ok
ok=$(echo "$response" | jq -r '.ok')
if [[ "$ok" == "true" ]]; then
return 0
else
local error
error=$(echo "$response" | jq -r '.error // "unknown error"')
log_error "Failed to set presence on $workspace: $error"
return 1
fi
}
get_presence() {
local workspace="$1"
local response
response=$(slack_api_call "$workspace" "users.getPresence")
log_verbose "Response: $response"
local ok
ok=$(echo "$response" | jq -r '.ok')
if [[ "$ok" == "true" ]]; then
local presence manual_away online
presence=$(echo "$response" | jq -r '.presence // "unknown"')
manual_away=$(echo "$response" | jq -r '.manual_away // false')
online=$(echo "$response" | jq -r '.online // false')
local display_status
if [[ "$manual_away" == "true" ]]; then
display_status="Away (manual)"
elif [[ "$online" == "true" ]]; then
display_status="Auto (online)"
elif [[ "$presence" == "away" ]]; then
display_status="Away"
else
display_status="Active"
fi
echo "$display_status"
return 0
else
local error
error=$(echo "$response" | jq -r '.error // "unknown error"')
log_error "Failed to get presence from $workspace: $error"
return 1
fi
}
#######################################
# DND (Do Not Disturb) API
#######################################
set_dnd_snooze() {
local workspace="$1"
local minutes="$2"
log_verbose "Setting DND snooze on $workspace for $minutes minutes"
local response
response=$(slack_api_call "$workspace" "dnd.setSnooze" "{\"num_minutes\": $minutes}")
log_verbose "Response: $response"
local ok
ok=$(echo "$response" | jq -r '.ok')
if [[ "$ok" == "true" ]]; then
return 0
else
local error
error=$(echo "$response" | jq -r '.error // "unknown error"')
log_error "Failed to set DND on $workspace: $error"
return 1
fi
}
end_dnd_snooze() {
local workspace="$1"
log_verbose "Ending DND snooze on $workspace"
local response
response=$(slack_api_call "$workspace" "dnd.endSnooze")
log_verbose "Response: $response"
local ok
ok=$(echo "$response" | jq -r '.ok')
if [[ "$ok" == "true" ]]; then
return 0
else
local error
error=$(echo "$response" | jq -r '.error // "unknown error"')
# "snooze_not_active" is not really an error
if [[ "$error" == "snooze_not_active" ]]; then
return 0
fi
log_error "Failed to end DND on $workspace: $error"
return 1
fi
}
get_dnd_info() {
local workspace="$1"
local response
response=$(slack_api_call "$workspace" "dnd.info")
log_verbose "Response: $response"
local ok
ok=$(echo "$response" | jq -r '.ok')
if [[ "$ok" == "true" ]]; then
local snooze_enabled snooze_endtime dnd_enabled next_dnd_start next_dnd_end
snooze_enabled=$(echo "$response" | jq -r '.snooze_enabled // false')
snooze_endtime=$(echo "$response" | jq -r '.snooze_endtime // 0')
dnd_enabled=$(echo "$response" | jq -r '.dnd_enabled // false')
next_dnd_start=$(echo "$response" | jq -r '.next_dnd_start_ts // 0')
next_dnd_end=$(echo "$response" | jq -r '.next_dnd_end_ts // 0')
if [[ "$snooze_enabled" == "true" ]]; then
local now remaining
now=$(date +%s)
remaining=$((snooze_endtime - now))
if [[ $remaining -gt 0 ]]; then
echo "Snoozed ($(format_duration "$remaining") remaining)"
else
echo "Snooze expired"
fi
elif [[ "$dnd_enabled" == "true" ]]; then
echo "DND scheduled"
else
echo "Not snoozed"
fi
return 0
else
local error
error=$(echo "$response" | jq -r '.error // "unknown error"')
log_error "Failed to get DND info from $workspace: $error"
return 1
fi
}
#######################################
# Setup Wizard
#######################################
run_setup_wizard() {
echo -e "${BOLD}Welcome to slack-status setup!${NC}"
echo ""
ensure_config_dir
# Check for age
if ! has_age; then
echo -e "${YELLOW}Warning:${NC} 'age' encryption tool not found."
echo "Your Slack tokens will be stored unencrypted."
echo "Install age for encrypted storage: brew install age (macOS) or apt install age (Linux)"
echo ""
fi
# SSH key selection
local ssh_key=""
if has_age; then
echo -e "${BOLD}SSH Key for Encryption${NC}"
local key_info
key_info=$(detect_ssh_key)
case "$key_info" in
both:*)
local ed25519_path rsa_path
ed25519_path=$(echo "$key_info" | cut -d: -f2)
rsa_path=$(echo "$key_info" | cut -d: -f3)
echo "Found multiple SSH keys:"
echo " 1) $ed25519_path (ed25519, recommended)"
echo " 2) $rsa_path (rsa)"
read -rp "Select key [1]: " key_choice
key_choice="${key_choice:-1}"
if [[ "$key_choice" == "2" ]]; then
ssh_key="$rsa_path"
else
ssh_key="$ed25519_path"
fi
;;
ed25519:*|rsa:*)
ssh_key=$(echo "$key_info" | cut -d: -f2)
echo "Found SSH key: $ssh_key"
read -rp "Use this key? [Y/n]: " use_key
if [[ "${use_key,,}" == "n" ]]; then
read -rp "Enter path to SSH key: " ssh_key
fi
;;
none)
echo "No SSH key found at ~/.ssh/id_ed25519 or ~/.ssh/id_rsa"
read -rp "Enter path to SSH key (or leave empty to skip encryption): " ssh_key
;;
esac
if [[ -n "$ssh_key" ]]; then
if [[ ! -f "$ssh_key" ]]; then
log_warn "SSH key not found: $ssh_key"
log_warn "Tokens will be stored unencrypted."
ssh_key=""
elif [[ ! -f "$ssh_key.pub" ]]; then
log_warn "Public key not found: $ssh_key.pub"
log_warn "Tokens will be stored unencrypted."
ssh_key=""
else
set_config_value ".ssh_key" "\"$ssh_key\""
echo -e "${GREEN}βœ“${NC} Will use $ssh_key for encryption"
fi
fi
echo ""
fi
# Add workspaces
local workspace_count=0
local add_more="y"
while [[ "${add_more,,}" == "y" ]]; do
echo -e "${BOLD}Add Slack Workspace${NC}"
read -rp "Workspace name (e.g., 'oddball', 'boehs'): " ws_name
if [[ -z "$ws_name" ]]; then
log_warn "Workspace name cannot be empty"
continue
fi
# Normalize to lowercase
ws_name="${ws_name,,}"
echo ""
echo "Enter your Slack token. This can be:"
echo " - A user token (xoxp-...)"
echo " - A client token with cookie (xoxc-...:xoxd-...)"
echo ""
read -rp "Token: " ws_token
if [[ -z "$ws_token" ]]; then
log_warn "Token cannot be empty"
continue
fi
# Parse token:cookie format
local token cookie=""
if [[ "$ws_token" == *":"* ]]; then
token="${ws_token%%:*}"
cookie="${ws_token#*:}"
else
token="$ws_token"
fi
add_workspace "$ws_name" "$token" "$cookie"
((workspace_count++))
if [[ $workspace_count -eq 1 ]]; then
set_primary_workspace "$ws_name"
echo -e "${GREEN}βœ“${NC} Added workspace '$ws_name' as primary"
else
echo -e "${GREEN}βœ“${NC} Added workspace '$ws_name'"
fi
echo ""
read -rp "Add another workspace? [y/N]: " add_more
add_more="${add_more:-n}"
echo ""
done
if [[ $workspace_count -eq 0 ]]; then
log_warn "No workspaces configured. Run '$SCRIPT_NAME config' to add workspaces."
else
# Initialize default presets
ensure_default_presets
echo -e "${GREEN}Setup complete!${NC}"
echo ""
echo "Configured workspaces:"
get_workspaces | while read -r ws; do
local primary_indicator=""
[[ "$ws" == "$(get_primary_workspace)" ]] && primary_indicator=" (primary)"
echo " - $ws$primary_indicator"
done
echo ""
echo "Try it out:"
echo " $SCRIPT_NAME set \"Working\" :computer:"
echo " $SCRIPT_NAME preset lunch"
echo " $SCRIPT_NAME get"
echo " $SCRIPT_NAME clear"
fi
}
#######################################
# Command Handlers
#######################################
cmd_set() {
local workspace="" text="" emoji=":speech_balloon:" duration="0"
local all_workspaces=false
local presence="" dnd=""
while [[ $# -gt 0 ]]; do
case "$1" in
-w|--workspace)
workspace="$2"
shift 2
;;
--all)
all_workspaces=true
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-q|--quiet)
QUIET=true
shift
;;
-p|--presence)
presence="$2"
shift 2
;;
-d|--dnd)
dnd="$2"
shift 2
;;
-*)
die "Unknown option: $1"
;;
*)
if [[ -z "$text" ]]; then
text="$1"
elif [[ "$1" == :* ]]; then
emoji="$1"
else
duration=$(parse_duration "$1")
fi
shift
;;
esac
done
if [[ -z "$text" ]]; then
die "Usage: $SCRIPT_NAME set <text> [emoji] [duration] [-w workspace|--all] [-p presence] [-d dnd]"
fi
local workspaces=()
if $all_workspaces; then
while IFS= read -r ws; do
workspaces+=("$ws")
done < <(get_workspaces)
elif [[ -n "$workspace" ]]; then
workspaces=("$workspace")
else
local primary
primary=$(get_primary_workspace)
if [[ -z "$primary" ]]; then
die "No primary workspace configured. Run '$SCRIPT_NAME config' or use -w/--workspace."
fi
workspaces=("$primary")
fi
local exit_code=0
local duration_display
duration_display=$(format_duration "$duration")
for ws in "${workspaces[@]}"; do
if ! workspace_exists "$ws"; then
log_error "Workspace not found: $ws"
exit_code=1
continue
fi
if set_status "$ws" "$text" "$emoji" "$duration"; then
if [[ "$duration" -eq 0 ]]; then
log_info "Status set on ${BOLD}$ws${NC}: $emoji $text (${YELLOW}no expiration${NC})"
else
log_info "Status set on ${BOLD}$ws${NC}: $emoji $text (expires in $duration_display)"
fi
else
exit_code=1
fi
# Apply presence if specified
if [[ -n "$presence" ]]; then
local p="$presence"
[[ "$p" == "active" ]] && p="auto"
if set_presence "$ws" "$p"; then
local display_p="$p"
[[ "$p" == "auto" ]] && display_p="active (auto)"
log_info "Presence set on ${BOLD}$ws${NC}: $display_p"
else
exit_code=1
fi
fi
# Apply DND if specified
if [[ -n "$dnd" ]]; then
if [[ "$dnd" == "off" || "$dnd" == "end" ]]; then
if end_dnd_snooze "$ws"; then
log_info "Notifications resumed on ${BOLD}$ws${NC}"
else
exit_code=1
fi
else
local dnd_seconds dnd_minutes
dnd_seconds=$(parse_duration "$dnd")
dnd_minutes=$((dnd_seconds / 60))
[[ $dnd_minutes -lt 1 ]] && dnd_minutes=1
if set_dnd_snooze "$ws" "$dnd_minutes"; then
log_info "Notifications snoozed on ${BOLD}$ws${NC} for $(format_duration "$dnd_seconds")"
else
exit_code=1
fi
fi
fi
done
return $exit_code
}
cmd_get() {
local workspace=""
local all_workspaces=false
while [[ $# -gt 0 ]]; do
case "$1" in
-w|--workspace)
workspace="$2"
shift 2
;;
--all)
all_workspaces=true
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-q|--quiet)
QUIET=true
shift
;;
*)
die "Unknown option: $1"
;;
esac
done
local workspaces=()
if $all_workspaces; then
while IFS= read -r ws; do
workspaces+=("$ws")
done < <(get_workspaces)
elif [[ -n "$workspace" ]]; then
workspaces=("$workspace")
else
local primary
primary=$(get_primary_workspace)
if [[ -z "$primary" ]]; then
die "No primary workspace configured. Run '$SCRIPT_NAME config' or use -w/--workspace."
fi
workspaces=("$primary")
fi
local exit_code=0
for ws in "${workspaces[@]}"; do
if ! workspace_exists "$ws"; then
log_error "Workspace not found: $ws"
exit_code=1
continue
fi
local status
if status=$(get_status "$ws"); then
echo -e "${BOLD}$ws${NC}: $status"
else
exit_code=1
fi
done
return $exit_code
}
cmd_clear() {
local workspace=""
local all_workspaces=false
while [[ $# -gt 0 ]]; do
case "$1" in
-w|--workspace)
workspace="$2"
shift 2
;;
--all)
all_workspaces=true
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-q|--quiet)
QUIET=true
shift
;;
*)
die "Unknown option: $1"
;;
esac
done
local workspaces=()
if $all_workspaces; then
while IFS= read -r ws; do
workspaces+=("$ws")
done < <(get_workspaces)
elif [[ -n "$workspace" ]]; then
workspaces=("$workspace")
else
local primary
primary=$(get_primary_workspace)
if [[ -z "$primary" ]]; then
die "No primary workspace configured. Run '$SCRIPT_NAME config' or use -w/--workspace."
fi
workspaces=("$primary")
fi
local exit_code=0
for ws in "${workspaces[@]}"; do
if ! workspace_exists "$ws"; then
log_error "Workspace not found: $ws"
exit_code=1
continue
fi
if clear_status "$ws"; then
log_info "Status cleared on ${BOLD}$ws${NC}"
else
exit_code=1
fi
done
return $exit_code
}
cmd_preset() {
local action="${1:-list}"
shift || true
case "$action" in
list|ls)
ensure_default_presets
echo -e "${BOLD}Available presets:${NC}"
list_presets
;;
add)
local name="${1:-}"
if [[ -z "$name" ]]; then
read -rp "Preset name: " name
fi
if [[ -z "$name" ]]; then
die "Preset name is required"
fi
local text emoji duration presence dnd
read -rp "Status text: " text
read -rp "Emoji (e.g., :coffee:): " emoji
read -rp "Duration (e.g., 1h, 30m, or 0 for no expiration): " duration
duration="${duration:-0}"
read -rp "Presence (away/auto, or blank to skip): " presence
read -rp "DND snooze (e.g., 1h, or 'off', or blank to skip): " dnd
add_preset "$name" "$text" "$emoji" "$duration" "$presence" "$dnd"
log_info "Preset '$name' added"
;;
edit)
local name="${1:-}"
if [[ -z "$name" ]]; then
die "Usage: $SCRIPT_NAME preset edit <name>"
fi
local existing
existing=$(get_preset "$name")
if [[ -z "$existing" ]]; then
die "Preset '$name' not found"
fi
local current_text current_emoji current_duration current_presence current_dnd
current_text=$(echo "$existing" | jq -r '.text')
current_emoji=$(echo "$existing" | jq -r '.emoji')
current_duration=$(echo "$existing" | jq -r '.duration // "0"')
current_presence=$(echo "$existing" | jq -r '.presence // ""')
current_dnd=$(echo "$existing" | jq -r '.dnd // ""')
echo "Editing preset '$name' (press Enter to keep current value, '-' to clear)"
read -rp "Status text [$current_text]: " text
text="${text:-$current_text}"
read -rp "Emoji [$current_emoji]: " emoji
emoji="${emoji:-$current_emoji}"
read -rp "Duration [$current_duration]: " duration
duration="${duration:-$current_duration}"
read -rp "Presence [$current_presence]: " presence
[[ "$presence" == "-" ]] && presence="" || presence="${presence:-$current_presence}"
read -rp "DND snooze [$current_dnd]: " dnd
[[ "$dnd" == "-" ]] && dnd="" || dnd="${dnd:-$current_dnd}"
add_preset "$name" "$text" "$emoji" "$duration" "$presence" "$dnd"
log_info "Preset '$name' updated"
;;
delete|rm)
local name="${1:-}"
if [[ -z "$name" ]]; then
die "Usage: $SCRIPT_NAME preset delete <name>"
fi
local existing
existing=$(get_preset "$name")
if [[ -z "$existing" ]]; then
die "Preset '$name' not found"
fi
delete_preset "$name"
log_info "Preset '$name' deleted"
;;
*)
# Assume it's a preset name to apply
local preset_name="$action"
local preset_data
preset_data=$(get_preset "$preset_name")
if [[ -z "$preset_data" ]]; then
die "Preset '$preset_name' not found. Use '$SCRIPT_NAME preset list' to see available presets."
fi
local text emoji duration_str duration presence dnd
text=$(echo "$preset_data" | jq -r '.text')
emoji=$(echo "$preset_data" | jq -r '.emoji')
duration_str=$(echo "$preset_data" | jq -r '.duration // "0"')
duration=$(parse_duration "$duration_str")
presence=$(echo "$preset_data" | jq -r '.presence // empty')
dnd=$(echo "$preset_data" | jq -r '.dnd // empty')
# Apply status first
cmd_set "$text" "$emoji" "$duration" "$@"
# Apply presence if set
if [[ -n "$presence" ]]; then
cmd_presence "$presence" "$@"
fi
# Apply DND if set
if [[ -n "$dnd" ]]; then
if [[ "$dnd" == "off" || "$dnd" == "end" ]]; then
cmd_dnd off "$@"
else
cmd_dnd on "$dnd" "$@"
fi
fi
;;
esac
}
cmd_workspaces() {
local action="${1:-list}"
shift || true
case "$action" in
list|ls)
local primary
primary=$(get_primary_workspace)
echo -e "${BOLD}Configured workspaces:${NC}"
get_workspaces | while read -r ws; do
local primary_indicator=""
[[ "$ws" == "$primary" ]] && primary_indicator=" ${GREEN}(primary)${NC}"
echo -e " - $ws$primary_indicator"
done
;;
add)
echo -e "${BOLD}Add Slack Workspace${NC}"
read -rp "Workspace name: " ws_name
ws_name="${ws_name,,}"
if workspace_exists "$ws_name"; then
die "Workspace '$ws_name' already exists"
fi
read -rp "Token (xoxp-... or xoxc-...:xoxd-...): " ws_token
local token cookie=""
if [[ "$ws_token" == *":"* ]]; then
token="${ws_token%%:*}"
cookie="${ws_token#*:}"
else
token="$ws_token"
fi
add_workspace "$ws_name" "$token" "$cookie"
log_info "Workspace '$ws_name' added"
;;
remove|rm)
local name="${1:-}"
if [[ -z "$name" ]]; then
die "Usage: $SCRIPT_NAME workspaces remove <name>"
fi
if ! workspace_exists "$name"; then
die "Workspace '$name' not found"
fi
remove_workspace "$name"
log_info "Workspace '$name' removed"
# If we removed the primary, clear it
if [[ "$name" == "$(get_primary_workspace)" ]]; then
set_config_value ".primary_workspace" '""'
log_warn "Removed primary workspace. Set a new one with: $SCRIPT_NAME workspaces primary <name>"
fi
;;
primary)
local name="${1:-}"
if [[ -z "$name" ]]; then
local current
current=$(get_primary_workspace)
if [[ -n "$current" ]]; then
echo "Current primary workspace: $current"
else
echo "No primary workspace set"
fi
return
fi
if ! workspace_exists "$name"; then
die "Workspace '$name' not found"
fi
set_primary_workspace "$name"
log_info "Primary workspace set to '$name'"
;;
*)
die "Unknown workspaces action: $action"
;;
esac
}
cmd_config() {
run_setup_wizard
}
cmd_presence() {
local workspace=""
local all_workspaces=false
local presence=""
while [[ $# -gt 0 ]]; do
case "$1" in
-w|--workspace)
workspace="$2"
shift 2
;;
--all)
all_workspaces=true
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-q|--quiet)
QUIET=true
shift
;;
away|auto|active)
# "active" is an alias for "auto"
[[ "$1" == "active" ]] && presence="auto" || presence="$1"
shift
;;
*)
die "Unknown option or presence value: $1 (use 'away' or 'auto')"
;;
esac
done
local workspaces=()
if $all_workspaces; then
while IFS= read -r ws; do
workspaces+=("$ws")
done < <(get_workspaces)
elif [[ -n "$workspace" ]]; then
workspaces=("$workspace")
else
local primary
primary=$(get_primary_workspace)
if [[ -z "$primary" ]]; then
die "No primary workspace configured. Run '$SCRIPT_NAME config' or use -w/--workspace."
fi
workspaces=("$primary")
fi
local exit_code=0
# If no presence specified, show current presence
if [[ -z "$presence" ]]; then
for ws in "${workspaces[@]}"; do
if ! workspace_exists "$ws"; then
log_error "Workspace not found: $ws"
exit_code=1
continue
fi
local current_presence
if current_presence=$(get_presence "$ws"); then
echo -e "${BOLD}$ws${NC}: $current_presence"
else
exit_code=1
fi
done
else
for ws in "${workspaces[@]}"; do
if ! workspace_exists "$ws"; then
log_error "Workspace not found: $ws"
exit_code=1
continue
fi
if set_presence "$ws" "$presence"; then
local display_presence="$presence"
[[ "$presence" == "auto" ]] && display_presence="active (auto)"
log_info "Presence set on ${BOLD}$ws${NC}: $display_presence"
else
exit_code=1
fi
done
fi
return $exit_code
}
cmd_dnd() {
local workspace=""
local all_workspaces=false
local action=""
local duration=""
while [[ $# -gt 0 ]]; do
case "$1" in
-w|--workspace)
workspace="$2"
shift 2
;;
--all)
all_workspaces=true
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-q|--quiet)
QUIET=true
shift
;;
on|snooze)
action="snooze"
shift
# Check if next arg is a duration
if [[ $# -gt 0 && ! "$1" =~ ^- ]]; then
duration="$1"
shift
fi
;;
off|end)
action="end"
shift
;;
status|info)
action="status"
shift
;;
*)
# Could be a duration for snooze
if [[ "$1" =~ ^[0-9] ]]; then
action="snooze"
duration="$1"
shift
else
die "Unknown option: $1"
fi
;;
esac
done
local workspaces=()
if $all_workspaces; then
while IFS= read -r ws; do
workspaces+=("$ws")
done < <(get_workspaces)
elif [[ -n "$workspace" ]]; then
workspaces=("$workspace")
else
local primary
primary=$(get_primary_workspace)
if [[ -z "$primary" ]]; then
die "No primary workspace configured. Run '$SCRIPT_NAME config' or use -w/--workspace."
fi
workspaces=("$primary")
fi
# Default action is status
[[ -z "$action" ]] && action="status"
# Default duration for snooze is 1 hour
if [[ "$action" == "snooze" && -z "$duration" ]]; then
duration="1h"
fi
local exit_code=0
case "$action" in
status)
for ws in "${workspaces[@]}"; do
if ! workspace_exists "$ws"; then
log_error "Workspace not found: $ws"
exit_code=1
continue
fi
local dnd_info
if dnd_info=$(get_dnd_info "$ws"); then
echo -e "${BOLD}$ws${NC}: $dnd_info"
else
exit_code=1
fi
done
;;
snooze)
local seconds minutes
seconds=$(parse_duration "$duration")
minutes=$((seconds / 60))
[[ $minutes -lt 1 ]] && minutes=1
for ws in "${workspaces[@]}"; do
if ! workspace_exists "$ws"; then
log_error "Workspace not found: $ws"
exit_code=1
continue
fi
if set_dnd_snooze "$ws" "$minutes"; then
log_info "Notifications snoozed on ${BOLD}$ws${NC} for $(format_duration "$seconds")"
else
exit_code=1
fi
done
;;
end)
for ws in "${workspaces[@]}"; do
if ! workspace_exists "$ws"; then
log_error "Workspace not found: $ws"
exit_code=1
continue
fi
if end_dnd_snooze "$ws"; then
log_info "Notifications resumed on ${BOLD}$ws${NC}"
else
exit_code=1
fi
done
;;
esac
return $exit_code
}
cmd_help() {
echo -e "${BOLD}slack-status${NC} v$VERSION - Set your Slack status from the command line
${BOLD}USAGE${NC}
$SCRIPT_NAME <command> [options]
${BOLD}COMMANDS${NC}
set <text> [emoji] [duration] Set your status
get Get current status
clear Clear your status
preset <name|action> Use or manage presets
presence [away|auto] Get or set presence (away/active)
dnd [on|off] [duration] Snooze or resume notifications
workspaces <action> Manage workspaces
config Run setup wizard
help Show this help
${BOLD}OPTIONS${NC}
-w, --workspace <name> Target a specific workspace
--all Target all workspaces
-p, --presence <value> Also set presence (away/auto/active)
-d, --dnd <duration> Also snooze notifications (or 'off')
-v, --verbose Show detailed output
-q, --quiet Suppress output (for scripting)
${BOLD}EXAMPLES${NC}
$SCRIPT_NAME set \"Deep work\" :headphones: 2h
$SCRIPT_NAME set \"In a meeting\" :calendar: 1h30m -w oddball
$SCRIPT_NAME set \"On PTO\" :palm_tree: --all
$SCRIPT_NAME set \"Focus time\" :headphones: 2h -p away -d 2h
$SCRIPT_NAME preset lunch
$SCRIPT_NAME preset lunch --all
$SCRIPT_NAME get --all
$SCRIPT_NAME clear
${BOLD}PRESET COMMANDS${NC}
preset list List all presets
preset add Add a new preset (interactive)
preset edit <name> Edit a preset
preset delete <name> Delete a preset
preset <name> Apply a preset
${BOLD}WORKSPACE COMMANDS${NC}
workspaces list List configured workspaces
workspaces add Add a workspace (interactive)
workspaces remove <name> Remove a workspace
workspaces primary [name] Get or set primary workspace
${BOLD}PRESENCE COMMANDS${NC}
presence Show current presence
presence away Set presence to away
presence auto Set presence to active (auto)
presence active Alias for 'presence auto'
${BOLD}DND (DO NOT DISTURB) COMMANDS${NC}
dnd Show current DND status
dnd on [duration] Snooze notifications (default: 1h)
dnd off Resume notifications
dnd 2h Snooze for specific duration
${BOLD}DURATION FORMAT${NC}
Seconds: 3600
Human-readable: 1h, 30m, 1h30m, 2h15m
${BOLD}CONFIGURATION${NC}
Config directory: $CONFIG_DIR
Run '$SCRIPT_NAME config' to reconfigure
"
}
#######################################
# Main
#######################################
main() {
ensure_jq
ensure_curl
local command="${1:-help}"
shift || true
# Check if setup is needed
if [[ ! -f "$CONFIG_FILE" && "$command" != "config" && "$command" != "help" && "$command" != "--help" && "$command" != "-h" ]]; then
echo "No configuration found. Let's set things up!"
echo ""
run_setup_wizard
exit 0
fi
case "$command" in
set)
cmd_set "$@"
;;
get)
cmd_get "$@"
;;
clear)
cmd_clear "$@"
;;
preset)
cmd_preset "$@"
;;
presence)
cmd_presence "$@"
;;
dnd|snooze)
cmd_dnd "$@"
;;
workspaces|workspace|ws)
cmd_workspaces "$@"
;;
config|setup)
cmd_config "$@"
;;
help|--help|-h)
cmd_help
;;
version|--version|-V)
echo "$SCRIPT_NAME v$VERSION"
;;
*)
# Maybe it's a preset name as a shortcut?
local preset_data
preset_data=$(get_preset "$command" 2>/dev/null || true)
if [[ -n "$preset_data" ]]; then
cmd_preset "$command" "$@"
else
die "Unknown command: $command. Run '$SCRIPT_NAME help' for usage."
fi
;;
esac
}
main "$@"
@ericboehs
Copy link
Author

ericboehs commented Dec 11, 2025

slack-status

A CLI tool to set your Slack status from the command line. Supports multiple workspaces, presets, presence, DND (snooze), and optional token encryption with age.

Features

  • Multiple workspaces - Configure and switch between work/personal Slacks
  • Presets - Quick shortcuts like slack-status lunch or slack-status focus
  • Presence control - Set yourself as away or active
  • DND / Snooze - Pause notifications for a duration
  • Human-readable durations - Use 1h30m instead of seconds
  • Encrypted token storage - Uses age with your SSH key (falls back to plaintext if age not installed)
  • XDG compliant - Config stored in ~/.config/slack-status/

Installation

curl -o ~/bin/slack-status https://gist.githubusercontent.com/ericboehs/7c741ccb7db890eaf5eb88e8c50dfb65/raw/slack-status
chmod +x ~/bin/slack-status

Dependencies

  • jq - JSON parsing (pre-installed on macOS, apt install jq on Linux)
  • curl - API calls
  • age (optional) - Token encryption (brew install age or apt install age)

Setup

Run slack-status and follow the wizard. You'll need a Slack token:

  • User token (xoxp-...) - From a Slack app with users.profile:write scope
  • Client token (xoxc-...:xoxd-...) - From browser dev tools (token:cookie format)

Usage

# Set status
slack-status set "Deep work" :headphones: 2h
slack-status set "In a meeting" :calendar: 1h30m -w work
slack-status set "On PTO" :palm_tree: --all

# Set status with presence and DND
slack-status set "Focus time" :headphones: 2h -p away -d 2h

# Use presets (can include presence + DND)
slack-status lunch                  # shortcut
slack-status focus                  # sets away + snoozes notifications
slack-status preset lunch --all     # all workspaces

# Check/clear status
slack-status get
slack-status get --all
slack-status clear

# Presence (away/active)
slack-status presence               # show current
slack-status presence away          # set away
slack-status presence auto          # set active

# DND / Snooze notifications
slack-status dnd                    # show current
slack-status dnd on 2h              # snooze for 2 hours
slack-status dnd off                # resume notifications

# Manage presets
slack-status preset list
slack-status preset add
slack-status preset edit focus
slack-status preset delete brb

# Manage workspaces  
slack-status workspaces list
slack-status workspaces add
slack-status workspaces primary personal

Default Presets

Name Status Emoji Duration Presence DND
meeting In a meeting :spiral_calendar_pad: 1h - 1h
lunch Out to lunch πŸ₯ͺ 1h - -
focus Deep work 🎧 2h away 2h
afk Away from keyboard 🚢 30m - -
brb Be right back :brb: 15m - -
pto Out of office 🌴 no expiration - -

Duration Format

  • Seconds: 3600
  • Human-readable: 1h, 30m, 1h30m, 2h15m
  • Omit for no expiration (status stays until cleared)

Config Files

~/.config/slack-status/
β”œβ”€β”€ tokens.age      # Encrypted tokens (or tokens.json if no age)
β”œβ”€β”€ config.json     # Primary workspace, SSH key path
└── presets.json    # Custom presets (can include presence/dnd)

Preset JSON Format

Presets can optionally include presence and dnd fields:

{
  "focus": {
    "text": "Deep work",
    "emoji": ":headphones:",
    "duration": "2h",
    "presence": "away",
    "dnd": "2h"
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment