2025-10-18

xbar: Customizing Your Mac Menu Bar

Put Any Information in Your Mac Menu Bar with Simple Shell Scripts

Table of Contents

What is xbar?

favicon
xbar is a free and open-source MacOS application that lets you put the output of any script or program into your Mac menu bar. With just a few lines of shell script, you can display system information, check email, monitor server status, or create custom utilities.

Key Features

  • 🚀 Simple: Write scripts in any language (bash, Python, JavaScript, etc.)
  • 🔄 Auto-refresh: Set custom refresh intervals
  • 📦 Plugin Repository: Browse hundreds of community plugins
  • ⚙️ Customizable: Full control over appearance and behavior

Installation

Method 1: Homebrew

brew install --cask xbar

Method 2: Direct Download

Download the latest release from xbarapp.com and drag it to your Applications folder.

Browse and Install Plugins

  1. Open xbar and click on the menu bar icon
  2. Select "Plugin Browser"
  3. Browse or search for plugins
  4. Click "Install" to add a plugin
Tip
If you have many plugins installed, menu bar icons may overflow.Change the spacing between icons in menu bar by running the following command in Terminal:
defaults -currentHost write -globalDomain NSStatusItemSpacing -int 6
Increase the number to widen the spacing, decrease to narrow it. You need to sign out and sign in again for the change to take effect.

Understanding xbar Plugin Format

Filename Convention

xbar uses the filename to determine refresh intervals:
plugin-name.{time}.{extension}
Refresh intervals:
  • 1s - Every second
  • 10s - Every 10 seconds
  • 1m - Every minute
  • 5m - Every 5 minutes
  • 1h - Every hour
  • 1d - Every day
Examples:
weather.5m.sh        # Refreshes every 5 minutes
email.1m.py          # Refreshes every minute
stocks.30s.js        # Refreshes every 30 seconds

Output Format

xbar plugins use a simple text-based format:
# Menu bar display (first line)
echo "Display Text"

# Dropdown menu (lines after "---")
echo "---"
echo "Menu Item 1"
echo "Menu Item 2"

Script Examples

Let's examine the three xbar scripts:

1. Email Unread Count Monitor

email_unread.1m.sh
1#!/bin/bash
2#  <xbar.title>Mail</xbar.title>
3#  <xbar.desc>Show unread count from Mail.app in menubar (with Open Mail option)</xbar.desc>
4#  <xbar.dependencies>bash,osascript,open</xbar.dependencies>
5set -e
6
7# get unread count (fall back to 0 if empty)
8OUTPUT=$(osascript -e 'tell application "Mail"' -e 'unread count of inbox' -e 'end tell')
9OUTPUT=${OUTPUT:-0}
10
11echo -n "📥"
12echo " ${OUTPUT}"
13
14echo "---"
15
16if [[ $OUTPUT -gt 0 ]]
17then
18  echo "Open Mail | bash=/usr/bin/open param1=-a param2=Mail terminal=false"
19else
20  echo "No unread mail"
21fi

How It Works

  1. AppleScript Integration: Uses osascript to query Mail.app's unread count
  2. Menu Bar Display: Shows 📥 icon with unread count
  3. Interactive Menu: Provides "Open Mail" button when unread messages exist
  4. Refresh: Updates every minute (1m in filename)

Features

  • ✅ Quick glance at unread emails without opening Mail.app
  • ✅ Click to open Mail.app directly
  • ✅ Handles Mail.app not running gracefully

2. USB-C Charger Wattage Display

power_wattage.30s.sh
1#!/bin/bash
2# <xbar.title>USB-C Charger Wattage with Battery</xbar.title>
3# <xbar.desc>Displays current USB-C charger wattage and battery percentage.</xbar.desc>
4
5# Get battery percentage
6BATTERY=$(pmset -g batt | grep -Eo "\d+%" | cut -d% -f1)
7
8# Get charger wattage
9WATTAGE=$(system_profiler SPPowerDataType | grep "Wattage" | awk '{print $3}')
10
11if [ -z "$WATTAGE" ]; then
12  # On battery
13  echo "🔋 ${BATTERY}%"
14else
15  # Charging
16  echo "⚡ ${WATTAGE}W ${BATTERY}%"
17fi

How It Works

  1. Battery Status: Uses pmset to get battery percentage
  2. Charger Detection: Uses system_profiler to detect connected charger wattage
  3. Smart Display: Shows different icons for battery vs charging state
  4. Refresh: Updates every 30 seconds

Use Cases

  • Monitor charging power, e.g., ⚡ 65W (useful for checking if using correct charger)
  • Battery level, e.g., 🔋 50%

3. Advanced Countdown Timer

simple-timer.1s.sh
1#!/bin/bash
2# <xbar.title>Simple Timer (Fixed-width)</xbar.title>
3# <xbar.version>v1.2</xbar.version>
4# <xbar.author>Kensuke</xbar.author>
5# <xbar.desc>Countdown timer with fixed-width digits (uses fullwidth Unicode digits).</xbar.desc>
6# <xbar.dependencies>bash, osascript</xbar.dependencies>
7# <xbar.refresh>1</xbar.refresh>
8
9STATE_FILE="/tmp/xbar_timer_state"
10
11now() { date +%s; }
12
13save_state() {
14  cat >"$STATE_FILE" <<EOF
15END_TIME=$END_TIME
16RUNNING=$RUNNING
17REMAINING=$REMAINING
18NOTIFIED=$NOTIFIED
19EOF
20}
21
22load_state() {
23  if [ -f "$STATE_FILE" ]; then
24    source "$STATE_FILE"
25  else
26    END_TIME=0
27    RUNNING=0
28    REMAINING=0
29    NOTIFIED=0
30  fi
31}
32
33# convert ASCII digits and colon to fullwidth characters (fixed width)
34to_fullwidth() {
35  s="$1"
36  # array of fullwidth digits 0-9
37  digits=(0 1 2 3 4 5 6 7 8 9)
38  for i in 0 1 2 3 4 5 6 7 8 9; do
39    s="${s//${i}/${digits[i]}}"
40  done
41  # convert colon to fullwidth colon
42  s="${s//:/:}"
43  echo "$s"
44}
45
46# parse human-friendly durations: 25m, 5m, 1m, 90s, 1h, or plain minutes (e.g. 25)
47parse_seconds() {
48  arg="$1"
49  if [[ "$arg" =~ ^([0-9]+)h$ ]]; then
50    echo $(( ${BASH_REMATCH[1]} * 3600 ))
51  elif [[ "$arg" =~ ^([0-9]+)m$ ]]; then
52    echo $(( ${BASH_REMATCH[1]} * 60 ))
53  elif [[ "$arg" =~ ^([0-9]+)s$ ]]; then
54    echo "${BASH_REMATCH[1]}"
55  elif [[ "$arg" =~ ^[0-9]+$ ]]; then
56    # treat plain number as minutes
57    echo $(( arg * 60 ))
58  else
59    # fallback: 60s
60    echo 60
61  fi
62}
63
64notify_finished() {
65  /usr/bin/osascript -e 'beep 3'
66  /usr/bin/osascript -e 'display dialog "Timer finished!" with title "xbar Timer" buttons {"OK"} default button "OK" with icon caution' &>/dev/null
67}
68
69# Handle action invocations
70if [ "$1" = "start" ]; then
71  DURATION_STR="${2:-25m}"
72  DURATION=$(parse_seconds "$DURATION_STR")
73  END_TIME=$(( $(now) + DURATION ))
74  RUNNING=1
75  REMAINING=0
76  NOTIFIED=0
77  save_state
78  exit 0
79elif [ "$1" = "custom" ]; then
80  # Show input dialog for custom timer
81  CUSTOM_INPUT=$(/usr/bin/osascript -e 'display dialog "Enter timer duration:\n(e.g., 10m, 30s, 1h, or just 15 for minutes)" default answer "10m" with title "Custom Timer" buttons {"Cancel", "Start"} default button "Start"' -e 'text returned of result' 2>/dev/null)
82  if [ -n "$CUSTOM_INPUT" ]; then
83    DURATION=$(parse_seconds "$CUSTOM_INPUT")
84    END_TIME=$(( $(now) + DURATION ))
85    RUNNING=1
86    REMAINING=0
87    NOTIFIED=0
88    save_state
89  fi
90  exit 0
91elif [ "$1" = "stop" ]; then
92  load_state
93  if [ "$RUNNING" -eq 1 ]; then
94    REM=$(( END_TIME - $(now) ))
95    if [ "$REM" -lt 0 ]; then REM=0; fi
96    REMAINING=$REM
97    END_TIME=0
98  fi
99  RUNNING=0
100  NOTIFIED=0
101  save_state
102  exit 0
103elif [ "$1" = "reset" ]; then
104  END_TIME=0
105  RUNNING=0
106  REMAINING=0
107  NOTIFIED=0
108  save_state
109  exit 0
110fi
111
112# Normal display run (no args)
113load_state
114
115# Determine display text and icon
116if [ "$RUNNING" -eq 1 ] && [ "$END_TIME" -gt 0 ]; then
117  REM=$(( END_TIME - $(now) ))
118  if [ "$REM" -eq 0 ]; then
119    # Show 00:00 first
120    DISPLAY="00:00"
121    ICON="⏰"
122  elif [ "$REM" -lt 0 ]; then
123    # Time is up, show alert
124    if [ "$NOTIFIED" -eq 0 ]; then
125      notify_finished
126      NOTIFIED=1
127    fi
128    RUNNING=0
129    REMAINING=0
130    END_TIME=0
131    save_state
132    DISPLAY="Done"
133    ICON=""
134  else
135    # Running timer - format time
136    H=$(( REM / 3600 ))
137    M=$(( (REM % 3600) / 60 ))
138    S=$(( REM % 60 ))
139    if [ "$H" -gt 0 ]; then
140      printf -v DISPLAY "%02d:%02d:%02d" "$H" "$M" "$S"
141    else
142      printf -v DISPLAY "%02d:%02d" "$M" "$S"
143    fi
144    ICON="⏰"
145  fi
146else
147  # Stopped/paused timer
148  REM=$REMAINING
149  if [ "$REM" -gt 0 ]; then
150    # Paused with remaining time
151    H=$(( REM / 3600 ))
152    M=$(( (REM % 3600) / 60 ))
153    S=$(( REM % 60 ))
154    if [ "$H" -gt 0 ]; then
155      printf -v DISPLAY "%02d:%02d:%02d" "$H" "$M" "$S"
156    else
157      printf -v DISPLAY "%02d:%02d" "$M" "$S"
158    fi
159    ICON="⏸"
160  else
161    # No timer set
162    DISPLAY="⏰"
163    ICON=""
164  fi
165fi
166
167# Convert to fullwidth if contains digits/colon
168if [[ "$DISPLAY" =~ [0-9] ]]; then
169  DISPLAY=$(to_fullwidth "$DISPLAY")
170fi
171
172# Combine icon and display
173if [ -n "$ICON" ]; then
174  DISPLAY="${ICON} ${DISPLAY}"
175fi
176
177# Single output point for menu bar
178echo "$DISPLAY"
179
180# Menu items
181echo '---'
182SCRIPT_PATH="$0"
183echo "Start 1h  | bash='$SCRIPT_PATH' param1='start' param2='1h' terminal=false refresh=true"
184echo "Start 30m  | bash='$SCRIPT_PATH' param1='start' param2='30m' terminal=false refresh=true"
185echo "Start 3m  | bash='$SCRIPT_PATH' param1='start' param2='3m' terminal=false refresh=true"
186echo "Set Custom Timer | bash='$SCRIPT_PATH' param1='custom' terminal=false refresh=true"
187if [ "$RUNNING" -eq 1 ]; then
188  echo "Stop      | bash='$SCRIPT_PATH' param1='stop' terminal=false refresh=true"
189fi
190echo "Reset     | bash='$SCRIPT_PATH' param1='reset' terminal=false refresh=true"

Advanced Features

This timer script demonstrates several advanced xbar techniques:
  1. State Persistence: Uses /tmp/xbar_timer_state to maintain timer state between refreshes
  2. Full-width Unicode: Converts digits to full-width characters (0123) for consistent spacing
  3. Interactive Controls: Provides start, stop, and reset buttons in dropdown menu
  4. Flexible Duration Input: Accepts formats like 25m, 90s, 1h, or plain numbers (treated as minutes)
  5. Custom Timer Dialog: Shows macOS dialog for entering custom durations
  6. Notifications: Audio beep and visual dialog when timer completes

Key Implementation Details

State Management:
# Save timer state to file
save_state() {
  cat >"$STATE_FILE" <<EOF
END_TIME=$END_TIME
RUNNING=$RUNNING
REMAINING=$REMAINING
NOTIFIED=$NOTIFIED
EOF
}
Duration Parsing:
parse_seconds() {
  arg="$1"
  if [[ "$arg" =~ ^([0-9]+)h$ ]]; then
    echo $(( ${BASH_REMATCH[1]} * 3600 ))
  elif [[ "$arg" =~ ^([0-9]+)m$ ]]; then
    echo $(( ${BASH_REMATCH[1]} * 60 ))
  # ... more formats
}
Fixed-width Display:
# Converts "12:34" to "12:34" for consistent menu bar width
to_fullwidth() {
  digits=(0 1 2 3 4 5 6 7 8 9)
  # ... conversion logic
}

Advanced xbar Plugin Techniques

xbar supports interactive menu items with various actions:
# Open URL
echo "GitHub | href=https://github.com"

# Run bash command
echo "Restart | bash=/usr/bin/killall param1=SystemUIServer terminal=false refresh=true"

# Run with parameters
echo "Process File | bash='./process.sh' param1='file.txt' terminal=false"

# Show in terminal
echo "Run Script | bash='./script.sh' terminal=true"

# Refresh after action
echo "Action | bash='command' refresh=true"

Text Formatting

# Colors
echo "Red Text | color=red"
echo "Custom Color | color=#FF5733"

# Font styles  
echo "Bold Text | font=Monaco size=14"
echo "Custom Font | font='Helvetica Neue' size=12"

# Combine attributes
echo "Styled Item | color=blue font=Monaco size=14"

Icons and Emojis

# Use emojis
echo "⚠️ Warning"
echo "✅ Success"
echo "📧 Email"

# Use SF Symbols (macOS 11+)
echo "Symbol | sfimage=star.fill"

# Custom image
echo "Item | image=base64encodedimage"
echo "Parent Menu"
echo "--Submenu Item 1"
echo "--Submenu Item 2"
echo "----Nested Item"

Separators

echo "---"  # Separator line

Writing Your Own xbar Plugin

Let's create a simple weather plugin:
weather.30m.sh
1#!/bin/bash
2
3# Get weather data (using wttr.in API)
4LOCATION="Tokyo"
5WEATHER=$(curl -s "wttr.in/${LOCATION}?format=%c+%t" | head -n 1)
6
7# Menu bar display
8echo "🌡️ ${WEATHER}"
9
10# Dropdown menu
11echo "---"
12echo "Location: ${LOCATION}"
13echo "---"
14echo "Refresh | refresh=true"
15echo "Open wttr.in | href=https://wttr.in/${LOCATION}"

Steps to Install

  1. Create the file:
    touch ~/plugins/weather.30m.sh
  2. Make it executable:
    chmod +x ~/plugins/weather.30m.sh
  3. Refresh xbar:
    • Click xbar menu → "Refresh All"

Best Practices

1. Error Handling

#!/bin/bash
set -e  # Exit on error

# Handle missing commands
if ! command -v jq &> /dev/null; then
    echo "❌ Error"
    echo "---"
    echo "jq not installed"
    exit 1
fi

2. Performance

Warning
Avoid slow operations in frequently refreshed plugins. For example, don't run a plugin that makes network requests every second.
# Cache results for expensive operations
CACHE_FILE="/tmp/xbar_cache_${PLUGIN_NAME}"
CACHE_TTL=300  # 5 minutes

if [ -f "$CACHE_FILE" ]; then
    AGE=$(($(date +%s) - $(stat -f %m "$CACHE_FILE")))
    if [ $AGE -lt $CACHE_TTL ]; then
        cat "$CACHE_FILE"
        exit 0
    fi
fi

# Fetch new data
DATA=$(expensive_operation)
echo "$DATA" | tee "$CACHE_FILE"

3. Security

# Don't hardcode sensitive data
API_KEY="${MY_API_KEY:-}"
if [ -z "$API_KEY" ]; then
    echo "❌ No API key"
    exit 1
fi

# Use environment variables or keychain
PASSWORD=$(security find-generic-password -s "my-service" -w)

Plugin Ideas

Here are some ideas for custom xbar plugins:

System Monitoring

  • CPU/Memory usage
  • Disk space
  • Network speed
  • Running processes

Productivity

  • Simer ✅ (already have this!)
  • Task list from Things/Todoist
  • Calendar events
  • Clipboard history

Communication

  • Unread messages (Slack, Discord)
  • Email count ✅ (already have this!)
  • GitHub notifications

Development

  • Git repository status
  • CI/CD build status
  • Server uptime
  • Docker container status

Personal

  • Bitcoin price
  • Stock prices
  • Weather ✅ (example above)
  • Calendar events
  • Habit tracker

Troubleshooting

  1. Check permissions:
    chmod +x ~/plugins/your-plugin.sh
  2. Verify shebang:
    #!/bin/bash
    # Must be first line
  3. Check syntax:
    bash -n your-plugin.sh
  4. Filename format:
    plugin-name.INTERVAL.extension
    For example: timer.1s.sh (refreshes every second)

Advanced Examples

API Integration

github-notifications.5m.sh
1#!/bin/bash
2
3GITHUB_TOKEN="${GITHUB_TOKEN}"
4API_URL="https://api.github.com/notifications"
5
6COUNT=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" | jq length)
7
8if [ "$COUNT" -gt 0 ]; then
9    echo "🔔 ${COUNT}"
10else
11    echo "🔕"
12fi
13
14echo "---"
15echo "GitHub Notifications | href=https://github.com/notifications"

System Information

system-info.1h.sh
1#!/bin/bash
2
3# Get system info
4UPTIME=$(uptime | awk '{print $3,$4}' | sed 's/,//')
5CPU=$(top -l 1 | grep "CPU usage" | awk '{print $3}')
6MEMORY=$(top -l 1 | grep "PhysMem" | awk '{print $2}')
7
8echo "💻 System"
9echo "---"
10echo "Uptime: ${UPTIME}"
11echo "CPU: ${CPU}"
12echo "Memory: ${MEMORY}"

Resources

favicon

favicon

Conclusion

xbar is a powerful tool for customizing your Mac menu bar. With just a few lines of shell script, you can:
  • 📊 Monitor system resources
  • 📧 Check email and notifications
  • ⏱️ Create productivity tools
  • 🌐 Integrate with APIs
Start with simple plugins and gradually build more complex ones as you learn. The xbar community has created hundreds of plugins you can use as references and inspiration.