General concept
Managing multiple Tailwind CSS projects can quickly become cumbersome. Each project requires its own watcher process that needs to run in the background, consuming terminal sessions or requiring complex window managers. This becomes especially challenging when working with multiple codebases or when you need your watchers to persist after logging out.
In this guide, we'll explore how to create a script that leverages systemd user services to manage Tailwind CSS processes. This approach provides several advantages over traditional terminal-based watchers, including automatic startup, persistent background operation, centralised logging, and graceful failure handling.
For those working on multiple Tailwind CSS projects simultaneously this can offer a solid solution. You should have basic familiarity with Linux command line tools and Tailwind CSS. By the end of this article, you'll have a practical tool that simplifies your development workflow and a better understanding of how to leverage systemd for development tasks.
Understanding the requirements
Before diving into the code, let's clarify what our Tailwind manager needs to accomplish:
- Add new Tailwind CSS projects to be managed
- Start and stop individual project watchers
- List all configured projects and their status
- Remove projects when no longer needed
- View logs for troubleshooting
- Check status of running services
Most importantly, our solution needs to maintain these watchers reliably in the background, survive terminal closures, and potentially restart automatically at login.
Setting up the basic structure
Our script, which we'll call twmanage (short for Tailwind Manager), will need to manage configuration storage and create the necessary systemd service files. We'll start by establishing the directory structure and configuration format:
#!/usr/bin/env bash
# Tailwind User Service Manager - Uses systemd user services
CONFIG_DIR="$HOME/.config/twmanage"
SERVICES_DIR="$HOME/.config/systemd/user"
# Create required directories
mkdir -p "$CONFIG_DIR"
mkdir -p "$SERVICES_DIR"
# Create config if it doesn't exist
if [ ! -f "$CONFIG_DIR/config.yaml" ]; then
echo "projects: []" > "$CONFIG_DIR/config.yaml"
fi
This portion ensures we have the necessary directories for storing configuration and systemd service files. We're using YAML for configuration as it provides good readability while supporting hierarchical data. When the script first runs, it creates an empty projects list if the configuration doesn't exist.
Note
Adding new projects
The core functionality of our manager is the ability to add new Tailwind projects. This requires capturing project details and creating both a configuration entry and a systemd service file:
add_project() {
local name="$1"
local directory="$2"
local input="$3"
local output="$4"
local args="${5:-}"
# Add to config.yaml
yq -i '.projects += [{"name": "'"$name"'", "directory": "'"$directory"'", "input": "'"$input"'", "output": "'"$output"'", "args": "'"$args"'"}]' "$CONFIG_DIR/config.yaml"
# Create systemd service file
cat > "$SERVICES_DIR/tailwind-$name.service" << EOF
[Unit]
Description=Tailwind CSS watcher for $name
After=network.target
[Service]
Type=simple
# Enable colour output
Environment="FORCE_COLOR=1"
WorkingDirectory=$directory
# Using the critical --watch=always flag for tailwindcss v4
ExecStart=/bin/bash -c 'tailwindcss -i $input -o $output --watch=always $args'
# Clean shutdown on service stop
KillSignal=SIGINT
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
EOF
# Reload systemd
systemctl --user daemon-reload
echo "Project '$name' added successfully"
}
This function performs several important tasks:
First, it uses the yq tool (a YAML processor) to add a new project to our configuration file. This keeps all project metadata in one place for later reference.
Then, it creates a systemd service file with several noteworthy features:
- The service is named based on the project name for easy identification
- It sets the working directory to the project directory
- It configures Tailwind CSS to watch for changes with the appropriate input and output files
- It enables colour output for better log readability
- It ensures proper shutdown by using SIGINT (equivalent to Ctrl+C)
- It automatically restarts on failure after a 5-second delay
- It directs all output to the systemd journal for centralised logging
Finally, it reloads the systemd configuration to recognize the new service file.
Managing project lifecycles
Once projects are added, we need functions to start, stop, and remove them. These functions interact with systemd's user service commands:
start_project() {
local name="$1"
systemctl --user start "tailwind-$name.service"
systemctl --user enable "tailwind-$name.service"
echo "Started Tailwind service for '$name'"
}
stop_project() {
local name="$1"
systemctl --user stop "tailwind-$name.service"
systemctl --user disable "tailwind-$name.service"
echo "Stopped Tailwind service for '$name'"
}
remove_project() {
local name="$1"
# Stop service if running
systemctl --user stop "tailwind-$name.service" 2>/dev/null || true
systemctl --user disable "tailwind-$name.service" 2>/dev/null || true
# Remove service file
rm -f "$SERVICES_DIR/tailwind-$name.service"
# Remove from config
yq -i 'del(.projects[] | select(.name == "'"$name"'"))' "$CONFIG_DIR/config.yaml"
# Reload systemd
systemctl --user daemon-reload
echo "Project '$name' removed successfully"
}
The start function both starts the service immediately and enables it to start automatically at login. This ensures the service persists through restarts.
The stop function does the opposite, stopping the current service and disabling automatic startup.
The remove function combines several operations: stopping the service (if running), removing the service file, and removing the project from our configuration file. We use error suppression (2>/dev/null || true) to handle cases where the service was already stopped or not running.
Tip
Monitoring and information functions
To complete our tool, we need ways to view project status, list all configured projects, and access logs:
list_projects() {
echo "Tailwind Projects:"
echo "-----------------"
printf "%-20s %-10s %-40s\n" "NAME" "STATUS" "DIRECTORY"
echo "-----------------"
yq -r '.projects[] | .name' "$CONFIG_DIR/config.yaml" 2>/dev/null | while read -r name; do
status=$(systemctl --user is-active "tailwind-$name.service" 2>/dev/null || echo "INACTIVE")
directory=$(yq -r '.projects[] | select(.name == "'"$name"'") | .directory' "$CONFIG_DIR/config.yaml")
printf "%-20s %-10s %-40s\n" "$name" "$status" "$directory"
done
}
view_logs() {
local name="$1"
journalctl --user -u "tailwind-$name.service" -f
}
show_status() {
local name="$1"
if [ -z "$name" ]; then
echo "Checking all Tailwind services..."
services=$(find "$SERVICES_DIR" -name "tailwind-*.service" -type f -exec basename {} \; 2>/dev/null)
if [ -z "$services" ]; then
echo "No Tailwind services found."
return
fi
printf "%-25s %-10s %-40s\n" "SERVICE" "STATUS" "PROJECT"
echo "----------------------------------------------------------------------"
for service in $services; do
project_name=${service#tailwind-}
project_name=${project_name%.service}
status=$(systemctl --user is-active "$service" 2>/dev/null || echo "inactive")
printf "%-25s %-10s %-40s\n" "$service" "$status" "$project_name"
done
else
echo "Service: tailwind-$name.service"
status=$(systemctl --user is-active "tailwind-$name.service" 2>/dev/null || echo "inactive")
echo "Status: $status"
if [ "$status" = "active" ]; then
echo "Service details:"
systemctl --user status "tailwind-$name.service" --no-pager
else
echo "Service is not active"
fi
fi
}
The list_projects function reads our configuration file and queries systemd for the status of each service, presenting a formatted table of results.
The view_logs function leverages systemd's journalctl command to stream logs for a specific project, using the -f flag to follow new entries as they arrive.
The show_status function provides more detailed information about service status. It has two modes: when called without a project name, it shows a summary of all services; when called with a project name, it shows detailed status for that specific service.
Command parsing and main function
Finally, we need to wire up our functions to the command line interface:
case "$1" in
add)
shift
add_project "$@"
;;
start)
start_project "$2"
;;
stop)
stop_project "$2"
;;
list)
list_projects
;;
remove)
remove_project "$2"
;;
logs)
view_logs "$2"
;;
status)
show_status "$2"
;;
*)
echo "Usage: $0 {add|start|stop|list|remove|logs|status} [arguments]"
echo " add NAME DIRECTORY INPUT OUTPUT [ARGS] Add a new project"
echo " start NAME Start a project"
echo " stop NAME Stop a project"
echo " list List all projects"
echo " remove NAME Remove a project"
echo " logs NAME View logs for a project"
echo " status [NAME] Show status of a project or all projects"
;;
esac
This case statement routes the first command line argument to the appropriate function and passes along any additional arguments. The default case provides helpful usage information.
Using the script in practice
Now that we understand how the script works, let's see it in action with a few practical examples:
Adding a new Tailwind project:
./twmanage add myproject ~/projects/myproject/src css/input.css css/output.css "--minify"
This command adds a project named "myproject" located in your home directory's projects folder, with input CSS from css/input.css and output to css/output.css, with the additional flag to minify the output.
Starting the project:
./twmanage start myproject
This command starts the Tailwind watcher for your project and enables it to start automatically at login.
Checking all projects:
./twmanage list
This displays a table of all your projects and their current status.
Viewing logs for troubleshooting:
./twmanage logs myproject
This shows a continuous stream of log output from your Tailwind process, which is helpful for diagnosing issues with your CSS.
Important
./twmanage stop myproject && ./twmanage start myproject
This ensures changes to your Tailwind configuration are applied properly.
Technical considerations and improvements
There are a few technical points worth mentioning:
The script uses systemd user services rather than system services, meaning they run in the user's session without requiring root privileges. This is the appropriate scope for development tools.
We're leveraging the yq command-line tool for YAML processing. This is a dependency that you'll need to install if you plan to use this script.
The --watch=always flag is specifically for Tailwind CSS v4, which aligns with our target technical stack. For other versions, you might need to adjust this flag.
Potential improvements to this script could include:
- Adding validation for input parameters
- Supporting more Tailwind configuration options
- Adding a command to restart a service
- Implementing a way to update existing project configurations
Conclusion
We've created a practical tool that simplifies managing multiple Tailwind CSS projects by leveraging systemd user services. This approach provides several advantages over traditional terminal-based solutions, including reliability, persistence, and centralised logging.
By using this script, you can reclaim terminal windows, ensure your CSS is always compiled, and have a more robust development environment. The techniques used here can also be applied to other development tasks that require long-running processes.
For your next steps, consider extending this script to match your specific workflow, or explore other development tasks that could benefit from systemd integration. You might also look into more advanced systemd features like socket activation or dependency management to further enhance your development environment.