Conky system monitor for Qubes OS (Disks, RAM, CPU)

Yesterday I tried creating a Conky monitor for an overview of the load across all my qubes. Conky runs three scripts that you can save in dom0 - for example, disk.sh, ram.sh, cpu.sh. Or save these scripts in /etc/conky (then change the path in the config from ~/disk.sh to /etc/conky/disk.sh). All scripts are set up to show max accuracy data (improved standard utilities including xentop and similar tools).
You can also use these scripts with a Generic Monitor on xfce4-panel.

  1. Script for private storages:
#!/bin/bash
# Qubes Private Storage Monitor for Conky

# Arrays to store output
declare -a vm_lines
declare -a dom0_lines

# Get list of running VMs from xl list (full names, not truncated like xentop)
while IFS= read -r line; do
    # Skip header
    [[ -z "$line" ]] && continue
    [[ "$line" =~ ^Name ]] && continue
    
    # Parse: Name ID Mem VCPUs State Time(s)
    vm_name=$(echo "$line" | awk '{print $1}')
    
    [[ -z "$vm_name" ]] && continue
    
    if [[ "$vm_name" == "Domain-0" ]]; then
        vm_name="dom0"
    fi
    
    # Get disk info for this VM from qvm-ls
    disk_info=$(qvm-ls --fields=name,priv-curr,priv-max | awk -v name="$vm_name" '$1 == name {print $2, $3}')
    priv_curr=$(echo "$disk_info" | awk '{print $1}')
    priv_max=$(echo "$disk_info" | awk '{print $2}')
    
    if [[ "$vm_name" == "dom0" ]]; then
        # dom0 doesn't have private storage like VMs, use root filesystem
        used_mb=$(df -m / 2>/dev/null | awk 'NR==2 {print $3}')
        total_mb=$(df -m / 2>/dev/null | awk 'NR==2 {print $2}')
    else
        # Use values from qvm-ls (already in MiB)
        used_mb="$priv_curr"
        total_mb="$priv_max"
    fi
    
    # Skip if no private storage
    [[ -z "$total_mb" || "$total_mb" == "0" ]] && continue
    
    # Calculate percentage
    if [[ -n "$total_mb" && "$total_mb" -gt 0 ]]; then
        pct=$(awk "BEGIN {printf \"%.1f\", ($used_mb / $total_mb) * 100}")
    else
        pct="0.0"
    fi
    
    # Convert to human readable
    if [[ "$total_mb" -gt 1048576 ]]; then
        total_hr=$(awk "BEGIN {printf \"%.1fG\", $total_mb/1048576}")
        used_hr=$(awk "BEGIN {printf \"%.1fG\", $used_mb/1048576}")
    elif [[ "$total_mb" -gt 1024 ]]; then
        total_hr=$(awk "BEGIN {printf \"%.1fG\", $total_mb/1024}")
        used_hr=$(awk "BEGIN {printf \"%.1fG\", $used_mb/1024}")
    else
        total_hr="${total_mb}M"
        used_hr="${used_mb}M"
    fi
    
    # Format: name (20 chars), 2 spaces, pct (5 chars), used/total
    formatted=$(printf '%-20s  %5s%%  %6s / %-6s' "$vm_name" "$pct" "$used_hr" "$total_hr")
    
    if [[ "$vm_name" == "dom0" ]]; then
        dom0_lines+=("$formatted")
    else
        vm_lines+=("$formatted")
    fi
    
done < <(xl list 2>/dev/null)

# Print header
echo "Domain                 Disk%    Used / Max"
echo "──────────────────────────────────────────────"

# Print dom0 first
for line in "${dom0_lines[@]}"; do
    echo "$line"
done

# Then other VMs
for line in "${vm_lines[@]}"; do
    echo "$line"
done

# Calculate totals for running VMs only
total_used=0
total_size=0

while IFS= read -r line; do
    [[ -z "$line" ]] && continue
    [[ "$line" =~ ^Name ]] && continue
    
    vm_name=$(echo "$line" | awk '{print $1}')
    
    [[ "$vm_name" == "Domain-0" ]] && continue
    [[ -z "$vm_name" ]] && continue
    
    disk_info=$(qvm-ls --fields=name,priv-curr,priv-max | awk -v name="$vm_name" '$1 == name {print $2, $3}')
    priv_curr=$(echo "$disk_info" | awk '{print $1}')
    priv_max=$(echo "$disk_info" | awk '{print $2}')
    
    [[ -z "$priv_max" || "$priv_max" == "0" ]] && continue
    
    total_used=$(awk "BEGIN {printf \"%.0f\", $total_used + $priv_curr}")
    total_size=$(awk "BEGIN {printf \"%.0f\", $total_size + $priv_max}")
done < <(xl list 2>/dev/null)

# Add dom0
root_used=$(df -m / 2>/dev/null | awk 'NR==2 {print $3}')
root_total=$(df -m / 2>/dev/null | awk 'NR==2 {print $2}')
total_used=$(awk "BEGIN {printf \"%.0f\", $total_used + $root_used}")
total_size=$(awk "BEGIN {printf \"%.0f\", $total_size + $root_total}")

if [[ "$total_size" -gt 0 ]]; then
    total_pct=$(awk "BEGIN {printf \"%.1f\", ($total_used / $total_size) * 100}")
else
    total_pct="0.0"
fi

# Convert totals to human readable
if [[ "$total_size" -gt 1048576 ]]; then
    total_size_hr=$(awk "BEGIN {printf \"%.1fG\", $total_size/1048576}")
    total_used_hr=$(awk "BEGIN {printf \"%.1fG\", $total_used/1048576}")
elif [[ "$total_size" -gt 1024 ]]; then
    total_size_hr=$(awk "BEGIN {printf \"%.1fG\", $total_size/1024}")
    total_used_hr=$(awk "BEGIN {printf \"%.1fG\", $total_used/1024}")
else
    total_size_hr="${total_size}M"
    total_used_hr="${total_used}M"
fi

echo "──────────────────────────────────────────────"
printf '%-20s  %5s%%  %6s / %-6s\n' 'TOTAL' "$total_pct" "$total_used_hr" "$total_size_hr"

# Physical disk info
phys_total=$(df -m / 2>/dev/null | awk 'NR==2 {print $2}')
phys_avail=$(df -m / 2>/dev/null | awk 'NR==2 {print $4}')
phys_used=$(df -m / 2>/dev/null | awk 'NR==2 {print $3}')

if [[ "$phys_total" -gt 1048576 ]]; then
    phys_total_hr=$(awk "BEGIN {printf \"%.1fG\", $phys_total/1048576}")
    phys_used_hr=$(awk "BEGIN {printf \"%.1fG\", $phys_used/1048576}")
    phys_avail_hr=$(awk "BEGIN {printf \"%.1fG\", $phys_avail/1048576}")
elif [[ "$phys_total" -gt 1024 ]]; then
    phys_total_hr=$(awk "BEGIN {printf \"%.1fG\", $phys_total/1024}")
    phys_used_hr=$(awk "BEGIN {printf \"%.1fG\", $phys_used/1024}")
    phys_avail_hr=$(awk "BEGIN {printf \"%.1fG\", $phys_avail/1024}")
else
    phys_total_hr="${phys_total}M"
    phys_used_hr="${phys_used}M"
    phys_avail_hr="${phys_avail}M"
fi

df_used=$(df -h / 2>/dev/null | awk 'NR==2 {print $3}')
df_total=$(df -h / 2>/dev/null | awk 'NR==2 {print $2}')
df_avail=$(df -h / 2>/dev/null | awk 'NR==2 {print $4}')
df_pct=$(df -h / 2>/dev/null | awk 'NR==2 {print $5}')
printf '%-23s  %3s     %-1s / %s\n' 'dom0 (df -h)' "$df_pct" "$df_used" "$df_total"

  1. Script for RAM:
#!/bin/bash
# Qubes RAM Monitor for Conky

# Convert KB to GB with one decimal, always show leading zero
kb_to_gb() {
    local kb=$1
    if [ -z "$kb" ] || [ "$kb" = "0" ]; then
        echo "0.0"
    else
        awk "BEGIN {printf \"%.1f\", $kb/1048576}"
    fi
}

mb_to_gb() {
    local mb=$1
    if [ -z "$mb" ] || [ "$mb" = "0" ]; then
        echo "0.0"
    else
        awk "BEGIN {printf \"%.1f\", $mb/1024}"
    fi
}

# Initialize totals
total_meminfo=0
total_static_max=0

# Arrays for output ordering
declare -a dom0_lines
declare -a vm_lines

# Get all running domains including dom0, skip -dm domains
while read vm_name domid rest; do
    [ -z "$vm_name" ] && continue
    [ -z "$domid" ] && continue

    # Skip -dm (Device Model) domains
    [[ "$vm_name" == *"-dm" ]] && continue

    if [ "$domid" = "0" ]; then
        vm_name="dom0"
    fi

    # Read xenstore memory values
    meminfo_kb=$(xenstore-read /local/domain/$domid/memory/meminfo 2>/dev/null || echo "0")
    static_max_kb=$(xenstore-read /local/domain/$domid/memory/static-max 2>/dev/null || echo "0")

    # Convert to GB
    meminfo_gb=$(kb_to_gb $meminfo_kb)
    static_max_gb=$(kb_to_gb $static_max_kb)

    # Calculate RAM% (meminfo / static_max * 100)
    if [ "$static_max_kb" != "0" ] && [ "$meminfo_kb" != "0" ]; then
        ram_pct=$(awk "BEGIN {printf \"%.1f\", ($meminfo_kb / $static_max_kb) * 100}")
    else
        ram_pct="0.0"
    fi

    # Add to totals
    if [ "$meminfo_kb" != "0" ]; then
        total_meminfo=$((total_meminfo + meminfo_kb))
    fi
    if [ "$static_max_kb" != "0" ]; then
        total_static_max=$((total_static_max + static_max_kb))
    fi

    # Format line: name (20 chars), 2 spaces, RAM%, (actG/maxG)
    formatted=$(printf '%-20s  %5s%%    %sG / %sG' "$vm_name" "$ram_pct" "$meminfo_gb" "$static_max_gb")

    if [ "$vm_name" = "dom0" ]; then
        dom0_lines+=("$formatted")
    else
        vm_lines+=("$formatted")
    fi
done < <(xl list | tail -n +2)

# Get system total memory
system_total_mb=$(xl info | grep total_memory | awk '{print $3}')
system_total_gb=$(mb_to_gb $system_total_mb)

# Convert totals to GB
total_meminfo_gb=$(kb_to_gb $total_meminfo)
total_static_max_gb=$(kb_to_gb $total_static_max)

# Calculate total RAM%
if [ "$total_static_max" != "0" ] && [ "$total_meminfo" != "0" ]; then
    total_ram_pct=$(awk "BEGIN {printf \"%.1f\", ($total_meminfo / $total_static_max) * 100}")
else
    total_ram_pct="0.0"
fi

# Conky output
echo "Domain                  RAM%    Used / Max"
echo "──────────────────────────────────────────────"

for line in "${dom0_lines[@]}"; do
    echo "$line"
done

for line in "${vm_lines[@]}"; do
    echo "$line"
done

echo "──────────────────────────────────────────────"
printf '%-20s  %5s%%    %sG / %sG\n' 'TOTAL' "$total_ram_pct" "$total_meminfo_gb" "$total_static_max_gb"
#printf '%-20s  %5sG\n' 'System Total' "$system_total_gb"

  1. Script for CPU:

CPU values are normalized per vCPU, showing average utilization across each virtual core rather than total consumption. Normalized values are now scaled to a 0-100% range.

#!/bin/bash
# Qubes CPU Monitor for Conky

# [load] config
cpu_bar() {
    local pct=$1
    local max=100
    local width=6
    local filled=$(awk "BEGIN {printf \"%d\", ($pct / $max) * $width}")
    local empty=$((width - filled))
    
    local bars=""
    for ((i=0; i<filled; i++)); do bars+="β–ˆ"; done
    for ((i=0; i<empty; i++)); do bars+="β–‘"; done
    
    echo "$bars"
}

total_cpu=0
total_norm=0

declare -a vm_lines
declare -a dom0_lines

# Cache xentop output (second iteration only)
xentop_data=$(xentop -bi2 -d1 2>/dev/null)

# Extract second iteration: after second NAME header
xentop_second=$(echo "$xentop_data" | awk '
    /^[[:space:]]*NAME[[:space:]]+STATE/ {header_count++; next}
    header_count == 2 {print}
')

# Get running VMs from xl list (full names)
while IFS= read -r line; do
    [[ -z "$line" ]] && continue
    [[ "$line" =~ ^Name ]] && continue
    
    vm_name=$(echo "$line" | awk '{print $1}')
    [[ -z "$vm_name" ]] && continue
    
    # Skip -dm (Device Model) domains
    [[ "$vm_name" == *"-dm" ]] && continue
    
    if [[ "$vm_name" == "Domain-0" ]]; then
        vm_name="dom0"
    fi
    
    # Find CPU info in xentop second iteration
    xentop_name="$vm_name"
    if [[ ${#vm_name} -gt 10 ]]; then
        xentop_name="${vm_name:0:10}"
    fi
    
    xentop_line=$(echo "$xentop_second" | awk -v name="$xentop_name" '$1 == name {print; exit}')
    
    # If not found, try Domain-0 for dom0
    if [[ -z "$xentop_line" && "$vm_name" == "dom0" ]]; then
        xentop_line=$(echo "$xentop_second" | awk '$1 == "Domain-0" {print; exit}')
    fi
    
    cpu_pct=$(echo "$xentop_line" | awk '{print $4}')
    vcpus=$(echo "$xentop_line" | awk '{print $9}')
    
    [[ -z "$cpu_pct" ]] && cpu_pct="0.0"
    [[ -z "$vcpus" ]] || [[ "$vcpus" == "0" ]] && vcpus=1
    
    norm_cpu=$(awk "BEGIN {printf \"%.1f\", $cpu_pct / $vcpus}")
    
    if [[ "$cpu_pct" != "0.0" ]] && [[ -n "$cpu_pct" ]]; then
        total_cpu=$(awk "BEGIN {printf \"%.1f\", $total_cpu + $cpu_pct}")
        total_norm=$(awk "BEGIN {printf \"%.1f\", $total_norm + $norm_cpu}")
    fi
    
    bar=$(cpu_bar "$norm_cpu")
    formatted=$(printf '%-20s  %5s%%    (%1s)  [%s]' "$vm_name" "$norm_cpu" "$vcpus" "$bar")
    
    if [[ "$vm_name" == "dom0" ]]; then
        dom0_lines+=("$formatted")
    else
        vm_lines+=("$formatted")
    fi
done < <(xl list 2>/dev/null)

# Print dom0 first
echo "Domain                  CPU%   Cores  [load]"
echo "──────────────────────────────────────────────"
for line in "${dom0_lines[@]}"; do
    echo "$line"
done

# Then other VMs
for line in "${vm_lines[@]}"; do
    echo "$line"
done

echo "──────────────────────────────────────────────"
#printf '%-20s  %5s%%\n' 'TOTAL' "${total_norm}"

#system_cpus=$(xl info 2>/dev/null | grep nr_cpus | awk '{print $3}')
#printf '%-20s  %5s\n' 'Physical CPUs' "${system_cpus}"

My /etc/conky/conky.conf:

You can edit conky.config and change gaps, size, font size, color, and any other parameters. dom0 mode is for those who use Qubes OS with amnesic modes

conky.config = {
    alignment = 'top_right',
    background = false,
    own_window = true,
    --## me change, added next 2 lines
    --## own_window_type override
    own_window_type = 'normal',
    own_window_argb_visual = true,
    own_window_transparent = true,
    own_window_hints = 'undecorated,sticky,skip_taskbar,skip_pager,below',
    own_window_argb_value = 12,
    own_window_colour = '000000',
    border_width = 1,
    cpu_avg_samples = 2,
    default_color = '63979a',
    default_outline_color = 'red',
    draw_borders = false,
    draw_graph_borders = true,
    draw_outline = false,
    draw_shadows = false,
    use_xft = true,
    font = 'Noto Sans Mono:size=8',
    gap_x = 10,
    gap_y = 45,
    minimum_height = 5,
    minimum_width = 5,
    net_avg_samples = 2,
    no_buffers = true,
    out_to_console = false,
    out_to_stderr = false,
    extra_newline = false,
    own_window = true,
    own_window_class = 'Conky',
    own_window_type = 'desktop',
    own_window_transparent = true,
    own_window_argb_visual = true,
    stentch = 1,
    double_buffer = true,
    update_interval = 2.0,
    uppercase = false,
    use_spacer = 'none',
    show_graph_scale = false,
    show_graph_range = false
}

conky.text = [[
dom0 mode: ${exec findmnt -n -o SOURCE /}

${execi 5 ~/disk.sh}

${execi 2 ~/ram.sh}

${execi 2 ~/cpu.sh}
]]

:check_mark: License:Unlicense

6 Likes
1 Like

If you Salt your scripts, there’s a better chance them to be adopted by the devs, even before v5.

2 Likes

Did I understand this correctly: Do I have to run all these scripts in dom0?

To install conky in Debian, I use sudo apt install conky-all.

But how do I install conky in dom0?

A very large config, no Disks data, no RAM data, CPU usage not normalized (200-300%)

1 Like

Try sudo qubes-dom0-update conky

1 Like

Pretty cool, heres my colors. I was planning on making a similar conky script until I upgraded to 4.3 and saw they already added per qube resource monitoring (The grey box on your panel next to your whonix lock. But if you’re going to do all the work… :slight_smile: I might later color code each VM or each script, see how bored I am

1 Like

Thanks) Any changes and improvements are welcome)