בס״ד

Instantly Responsive Sway Status using Unix Socket
2025-06-16

In the Sway window manager, we can provide a program to be run to display system information by setting status_command in the bar section of our configuration file. The comment above it, in the default configuration file, explains "when the status_command prints a new line to stdout, swaybar updates."

Given that information, you may instinctively reach for a loop with a brief delay in order to keep the bar data up to date. Such an approach is commonly suggested, perhaps in part because the default command does exactly that:

  status_command while date +'%Y-%m-%d %X'; do sleep 1; done

The trouble here is twofold.

First, as the bar command increases in complexity, it continues to be run every second, burning processing resources it might otherwise conserve.

Second, and more pressing, if you include volume, or brightness, or any other user changeable information, you will sometime experience a noticeable delay, even running the command once a second.

We will solve this delay using simple shell scripting and basic Linux command line tools, but first, we must write a status bar text generator shell script.

Generating the Status Text

The entire file is found below, broken into sections and explained.

Using a Nerd Font for Icons

Before we begin with the script itself, a prerequisite for including icons is a font with the correct symbols included. I use a nerd font to do this, specifically Meslo LGS (line-gap small) NF (nerd font), which I also include in this page to ensure the icons display correctly below.

Coloring the Status Icons

To ensure our small icons are easily differentiable we apply color to them.

Defining the Colors

The colors are from a custom theme I created called workmanlike. Many of them are not used below but are included for completeness and potential future use.

# Dark Colors
 dBlack="#1e1c1a"
  dGray="#7e7c7a"
 dWhite="#ceccca"
  dBlue="#20419a"
dPurple="#5d377b"
  dPink="#883067"
   dRed="#962a1c"
dYellow="#916518"
 dGreen="#406d00"
  dTeal="#2d746e"

# Bright Colors
 bBlack="#4e4c4a"
  bGray="#9e9c9a"
 bWhite="#eeecea"
  bBlue="#5273cd"
bPurple="#8f69ad"
  bPink="#ba6299"
   bRed="#c85c4e"
bYellow="#c3974a"
 bGreen="#729f32"
  bTeal="#5fa6a0"

Applying the Colors

We apply color to an icon with a small function. Argument $1 is the color, $2 is the icon to be colored, and $3 is the text value. All of this appears on an off-white background.

colorize() {
  echo "<span background='$bWhite' foreground='$dBlack'><span foreground='$1'> $2 </span>$3 </span>"
}

Note, colorize uses Pango markup instead of ANSI control sequences, making the colors and output results easier to reason about.

Backlight Level

We color the icon yellow once light is used to get the brightness level, and awk to retain just the integer part from before the decimal.

glow=$(colorize $dYellow "󰃞" $(light -G | awk -F '.' '{print $1}'))

Battery State

For the battery we use twenty-two separate icons to indicate the state granularly.

Discharging Icons

Here, $1 is the battery level, and we use case 1 in to seek the first true check for what the level is less than or equal to.

battery_icon() {
  case 1 in
    $(($1<=5)) ) echo "󱃍";;
    $(($1<=15))) echo "󰁺";;
    $(($1<=25))) echo "󰁻";;
    $(($1<=35))) echo "󰁼";;
    $(($1<=45))) echo "󰁽";;
    $(($1<=55))) echo "󰁾";;
    $(($1<=65))) echo "󰁿";;
    $(($1<=75))) echo "󰂀";;
    $(($1<=85))) echo "󰂁";;
    $(($1<=95))) echo "󰂂";;
    *          ) echo "󰁹";;
  esac
}

Charging Icons

charging_icon() {
  case 1 in
    $(($1<=5)) ) echo "󰢟";;
    $(($1<=15))) echo "󰢜";;
    $(($1<=25))) echo "󰂆";;
    $(($1<=35))) echo "󰂇";;
    $(($1<=45))) echo "󰂈";;
    $(($1<=55))) echo "󰢝";;
    $(($1<=65))) echo "󰂉";;
    $(($1<=75))) echo "󰢞";;
    $(($1<=85))) echo "󰂊";;
    $(($1<=95))) echo "󰂋";;
    *          ) echo "󰂅";;
  esac
}

Low Battery Color

When at ten percent or below the color switches from green to red as a warning.

charge_color() {
  if [[ $(($1<=10)) == 1 ]]
  then
    echo $dRed
  else
    echo $dGreen
  fi
}

Formulate Return

The battery charge and status can is retrieved from /sys files, the icon is selected based on the status and charge, and the icon is colored.

battery() {
  charge=$(cat /sys/class/power_supply/BAT1/capacity)
  status=$(cat /sys/class/power_supply/BAT1/status)
  
  if [ "$status" = "Charging" ]
  then
    icon=$(charging_icon $charge)
  else
    icon=$(battery_icon $charge)
  fi
  
  echo "$(colorize $(charge_color $charge) $icon $charge)"
}

Volume State

Level Icon

Uses the same approach as the battery icons above.

volume_icon() {
  case 1 in
    $(($1<=34))) echo "󰕿";;
    $(($1<=68))) echo "󰖀";;
    *          ) echo "󰕾";;
  esac
}

Formulate Return

Getting the volume level and mute status lacks a universal approach. Here, I use pactl with awk to pull just the needed information from the returned strings.

Based on the mute status the correct icon is selected. The result is colored and returned with echo.

volume() {
  level=$(
    pactl get-sink-volume @DEFAULT_SINK@ | head -n1 |
    awk '{print substr($5, 1, length($5)-1)}'
  )
  mute=$(pactl get-sink-mute @DEFAULT_SINK@ | awk '{print $2}')
  case $mute in
    yes) icon="󰸈";;
    no)  icon=$(volume_icon $level);;
  esac
  echo "$(colorize $dPink $icon $level)"
}

Time

Time is separate from date, so each has its own section and icon, which is visually preferable.

Clock Icon

For each hour, which date +%-I provides, a distinct clock icon with the hands in the correct position is used.

clock_icon() {
  case `date +%-I` in
     1) echo "󱐿";;
     2) echo "󱑀";;
     3) echo "󱑁";;
     4) echo "󱑂";;
     5) echo "󱑃";;
     6) echo "󱑄";;
     7) echo "󱑅";;
     8) echo "󱑆";;
     9) echo "󱑇";;
    10) echo "󱑈";;
    11) echo "󱑉";;
    12) echo "󱑊";;
  esac
}

Formulate Return

The time format is standard American am/pm.

time=$(colorize $dPurple `clock_icon` $(date +"%-I:%M%P"))

Date

Includes the day of the week and day of the month.

Suffix

The day of the month looks nicer with a suffix, and it is simple to add due to the limited set of numbers.

day_suffix() {
  case `date +%-d` in
    1|21|31) echo "st";;
    2|22)    echo "nd";;
    3|23)    echo "rd";;
    *)       echo "th";;
  esac
}

Formulate Return

The date is formatted abbreviated day of the week name, abbreviated month name, day of the month number with suffix.

date=$(colorize $dBlue "󰃭" "$(date +"%a %b %-d`day_suffix`")")

Socket Writing

When we run all of this and build our status bar string, we ultimately return it not to standard out, but rather to a Unix socket.

We do this with netcat, in my case the OpenBSD variant, run as the nc command, to write the information to the status_pipe file, which we will read in our second shell script.

printf " $glow `volume` `battery` $time $date \n" | nc -N -U /tmp/status_pipe

Triggering a Status Text Update

Cleaning on Exit

To avoid leaving a service running and the pipe file lying around, we make use of trap to run our cleanup function on EXIT and SIGINT.

cleanup() {
  rm -f /tmp/status_pipe
  systemctl --user stop Sway_Status.{service,timer}
  systemctl --user reset-failed Sway_Status.{service,timer}
}

trap cleanup EXIT
trap "exit" INT

Listen to the Socket

We again use netcat, this time to -keep -listening on our -Unix socket, and for each newline we print the generated status.

nc -k -l -U /tmp/status_pipe | while IFS='\n' read -r current_status; do
  printf "$current_status\n"
done

Calling Status Generation

At this point we load the generation shell script path and run it immediately to get our initial status bar.

generate_status="$HOME/.local/bin/sway-status/socket-generate.sh"

$generate_status

on brightness with inotify

We -monitor the close_write -event on the brightness file with inotifywait. This will be immediately updated when any change to brightness occurs, and will therefore regenerate the status.

(
  inotifywait -m -e close_write /sys/class/backlight/intel_backlight/brightness |
  while read -r; do $generate_status; done
) &

on audio with pactl

By using the subscribe command we can use pactl to monitor all changes to the audio system. We use ripgerp (rg) to continue the pipe only on lines that indicate a change to the audio sink. This will appear on volume changes and mute.

(
  pactl subscribe |
  rg --line-buffered 'change.*sink' |
  while IFS= read -r line; do $generate_status; done
) &

on time with systemd-run

Finally, we create a systemd transient service which runs every minute on the minute to update our status.

systemd-run                        \
  --unit=Sway_Status               \
  --quiet                          \
  --user                           \
  --on-calendar '*:0/1'            \
  $generate_status

Of all the monitors, this has tended to be the least stable.

Summary

While there are many existing flexible sway status bar programs you can easily customize, there is something valuable about writing your own, understanding how the pieces fit together. So you shouldn't take what I've written directly, but rather learn from it to build for yourself, to grow in knowledge and skill.

Further, what I have here is far from perfect. For example, instead of re-generating the entire status bar text, we could just regenerate the changed bit. I may in fact do this in the future, but for now I leave it as an exercise for the reader.

share this post