Skip to content

Instantly share code, notes, and snippets.

@smooker
Created April 4, 2026 17:24
Show Gist options
  • Select an option

  • Save smooker/b15ed5da0eca0c25a6ef34c7da8b9d19 to your computer and use it in GitHub Desktop.

Select an option

Save smooker/b15ed5da0eca0c25a6ef34c7da8b9d19 to your computer and use it in GitHub Desktop.
XFCE4 displays cleanup + backdrop tool (--recreate for one-shot fix)
#!/usr/bin/perl
# xfce_displays_clean.pl - Parse, clean and apply XFCE4 displays.xml
# + Set wallpaper/color on all connected monitors
# NO EXTERNAL DEPENDENCIES - pure Perl 5 core only
# Usage: ./xfce_displays_clean.pl [displays.xml]
use strict;
use warnings;
use File::Copy;
use POSIX qw(strftime);
my $XFCONF_Q = '/usr/bin/xfconf-query';
my $XFDESKTOP = '/usr/bin/xfdesktop';
my $XRANDR = '/usr/bin/xrandr';
my $PIDOF = '/usr/bin/pidof';
my $KILL = '/bin/kill';
# Auto-detect pidof
unless (-x $PIDOF) {
for my $p ('/bin/pidof', '/usr/bin/pidof', '/sbin/pidof') {
if (-x $p) { $PIDOF = $p; last; }
}
}
# Auto-detect kill
unless (-x $KILL) {
for my $p ('/bin/kill', '/usr/bin/kill') {
if (-x $p) { $KILL = $p; last; }
}
}
my $default_path = "$ENV{HOME}/.config/xfce4/xfconf/xfce-perchannel-xml/displays.xml";
my $file = $ARGV[0] || $default_path;
# --- Get connected monitors from xrandr ---
sub get_connected_monitors {
my @mons;
my $xr = `$XRANDR 2>/dev/null`;
while ($xr =~ /^(\S+)\s+connected\s+(?:primary\s+)?(\d+x\d+)/mg) {
push @mons, { port => $1, res => $2 };
}
return @mons;
}
# --- Parse hex color #RRGGBB to XFCE rgba doubles ---
sub hex_to_rgba {
my $hex = shift;
$hex =~ s/^#//;
if (length($hex) == 6) {
my @c = map { hex($_) / 255.0 } ($hex =~ /../g);
return (@c, 1.0);
}
return (0.2, 0.2, 0.2, 1.0); # default dark gray
}
# --- Named colors ---
my %named_colors = (
black => '000000', white => 'ffffff', red => 'cc0000',
green => '00cc00', blue => '3333cc', yellow => 'cccc00',
cyan => '00cccc', purple => '8833aa', gray => '333333',
darkblue => '1a1a4e', darkgreen => '1a3a1a', darkred => '4e1a1a',
);
# --- Set backdrop on all monitors ---
sub set_backdrop {
my @mons = get_connected_monitors();
if (!@mons) {
print "No connected monitors found via xrandr!\n";
return;
}
print "\nConnected monitors:\n";
for my $i (0..$#mons) {
printf " %d) %s (%s)\n", $i+1, $mons[$i]{port}, $mons[$i]{res};
}
print "\nBackdrop type:\n";
print " 1) Solid color (all monitors same color)\n";
print " 2) Solid color (different per monitor)\n";
print " 3) Wallpaper image (all monitors same)\n";
print " 4) Wallpaper image (different per monitor)\n";
print "Choice [1-4]: ";
my $bchoice = <STDIN>; chomp $bchoice;
if ($bchoice eq '1') {
print "Color (#RRGGBB or name: " . join(', ', sort keys %named_colors) . "): ";
my $col = <STDIN>; chomp $col;
$col = $named_colors{lc($col)} if $named_colors{lc($col)};
my @rgba = hex_to_rgba($col);
for my $m (@mons) {
apply_solid($m->{port}, @rgba);
}
}
elsif ($bchoice eq '2') {
for my $m (@mons) {
printf "Color for %s (#RRGGBB or name): ", $m->{port};
my $col = <STDIN>; chomp $col;
$col = $named_colors{lc($col)} if $named_colors{lc($col)};
my @rgba = hex_to_rgba($col);
apply_solid($m->{port}, @rgba);
}
}
elsif ($bchoice eq '3') {
print "Image path: ";
my $img = <STDIN>; chomp $img;
if (! -f $img) { print "File not found: $img\n"; return; }
print "Style (1=centered, 2=tiled, 3=stretched, 4=scaled, 5=zoomed) [5]: ";
my $style = <STDIN>; chomp $style;
$style = 5 unless $style =~ /^[1-5]$/;
for my $m (@mons) {
apply_wallpaper($m->{port}, $img, $style);
}
}
elsif ($bchoice eq '4') {
for my $m (@mons) {
printf "Image for %s: ", $m->{port};
my $img = <STDIN>; chomp $img;
if (! -f $img) { print " File not found: $img, skipping.\n"; next; }
print " Style (1=centered, 2=tiled, 3=stretched, 4=scaled, 5=zoomed) [5]: ";
my $style = <STDIN>; chomp $style;
$style = 5 unless $style =~ /^[1-5]$/;
apply_wallpaper($m->{port}, $img, $style);
}
}
else { print "Invalid.\n"; return; }
# Reload
print "\nReloading xfdesktop... ";
system("$KILL `$PIDOF xfdesktop` 2>/dev/null");
select(undef, undef, undef, 1);
system("$XFDESKTOP >/dev/null 2>&1 &");
print "done.\n";
}
sub apply_solid {
my ($port, @rgba) = @_;
my $base = "/backdrop/screen0/monitor$port/workspace0";
my @cmds = (
"$XFCONF_Q -c xfce4-desktop -p $base/color-style -s 0 --create -t int",
"$XFCONF_Q -c xfce4-desktop -p $base/image-style -s 0 --create -t int",
"$XFCONF_Q -c xfce4-desktop -p $base/rgba1"
. " -s $rgba[0] -s $rgba[1] -s $rgba[2] -s $rgba[3]"
. " --create -t double -t double -t double -t double",
);
for my $cmd (@cmds) {
system($cmd);
}
printf " %s -> solid #%02x%02x%02x\n", $port,
int($rgba[0]*255), int($rgba[1]*255), int($rgba[2]*255);
}
sub apply_wallpaper {
my ($port, $img, $style) = @_;
my $base = "/backdrop/screen0/monitor$port/workspace0";
my @cmds = (
"$XFCONF_Q -c xfce4-desktop -p $base/image-style -s $style --create -t int",
"$XFCONF_Q -c xfce4-desktop -p $base/last-image -s '$img' --create -t string",
);
for my $cmd (@cmds) {
system($cmd);
}
printf " %s -> %s (style %d)\n", $port, $img, $style;
}
# === MAIN ===
# --recreate [color] : kill xfconfd, nuke Fallback, set solid color on all monitors, reload
# --backdrop : interactive backdrop setup
if (@ARGV && $ARGV[0] eq '--recreate') {
my $color = $ARGV[1] || 'gray';
$color = $named_colors{lc($color)} if $named_colors{lc($color)};
my @rgba = hex_to_rgba($color);
print "\033[1;36m=== RECREATE MODE ===\033[0m\n\n";
# 1. Kill xfconfd
my $pid = `$PIDOF xfconfd 2>/dev/null`; chomp $pid;
if ($pid) {
system("$KILL $pid 2>/dev/null");
select(undef, undef, undef, 0.5);
my $check = `$PIDOF xfconfd 2>/dev/null`; chomp $check;
system("$KILL -9 $check 2>/dev/null") if $check;
printf " xfconfd killed (was PID %s)\n", $pid;
} else {
print " xfconfd not running\n";
}
# 2. Clean displays.xml — remove everything except Default
if (-f $file) {
open my $fh, '<', $file or die "Cannot open $file: $!\n";
my @lines = <$fh>;
close $fh;
# Backup
my $ts = strftime("%Y%m%d_%H%M%S", localtime);
my $backup = "${file}.bak_${ts}";
copy($file, $backup);
print " Backup: $backup\n";
# Remove non-Default sections
my @out;
my $d = 0;
my $skipping = 0;
my $skip_d = 0;
my %rm_sections;
for my $line (@lines) {
my $is_sc = ($line =~ m{<property\s+[^>]*/\s*>});
my $is_open = (!$is_sc && $line =~ m{<property\s+([^>]*)>});
my $oattrs = $1 // '';
my $cc = 0;
$cc++ while $line =~ m{</property>}g;
if ($skipping) {
$d++ if $is_open;
$d -= $cc; $d = 0 if $d < 0;
$skipping = 0 if $d < $skip_d;
next;
}
if ($is_open) {
$d++;
if ($d == 1) {
my ($n) = $oattrs =~ /name="([^"]*)"/;
if ($n && $n ne 'Default' && $n ne 'ActiveProfile' &&
$n ne 'IdentityPopups' && $n ne 'Notify' && $n ne 'AutoEnableProfiles') {
$rm_sections{$n} = 1;
$skipping = 1; $skip_d = $d - 1;
next;
}
}
}
$d -= $cc; $d = 0 if $d < 0;
push @out, $line;
}
my $output = join('', @out);
$output =~ s/\n{3,}/\n\n/g;
open my $of, '>', $file or die "Cannot write: $!\n";
print $of $output;
close $of;
if (%rm_sections) {
printf " Removed sections: %s\n", join(', ', sort keys %rm_sections);
}
print " displays.xml cleaned\n";
} else {
print " displays.xml not found (will be auto-created)\n";
}
# 3. Clean xfce4-desktop.xml — nuke old monitor entries
my $desk_xml = "$ENV{HOME}/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml";
if (-f $desk_xml) {
my $ts = strftime("%Y%m%d_%H%M%S", localtime);
copy($desk_xml, "${desk_xml}.bak_${ts}");
unlink $desk_xml;
print " xfce4-desktop.xml removed (old backdrop entries)\n";
}
# 4. Restart xfconfd
print " Restarting xfconfd... ";
system("$XFCONF_Q -c displays -p /ActiveProfile >/dev/null 2>&1");
my $new_pid = `$PIDOF xfconfd 2>/dev/null`; chomp $new_pid;
print $new_pid ? "OK (PID $new_pid)\n" : "will auto-start\n";
# 5. Set solid color on all connected monitors
my @mons = get_connected_monitors();
if (@mons) {
printf "\n Setting solid color #%02x%02x%02x on %d monitors:\n",
int($rgba[0]*255), int($rgba[1]*255), int($rgba[2]*255), scalar @mons;
for my $m (@mons) {
apply_solid($m->{port}, @rgba);
}
} else {
print "\n No connected monitors found via xrandr\n";
}
# 6. Restart xfdesktop
print "\n Restarting xfdesktop... ";
system("$KILL `$PIDOF xfdesktop 2>/dev/null` 2>/dev/null");
select(undef, undef, undef, 1);
system("$XFDESKTOP >/dev/null 2>&1 &");
print "done.\n";
printf "\n\033[1;32mRecreate complete!\033[0m\n";
exit 0;
}
if (@ARGV && $ARGV[0] eq '--backdrop') {
set_backdrop();
exit 0;
}
# Check if displays.xml exists (not needed for --backdrop/--recreate)
die "File not found: $file\n" unless -f $file;
open my $fh, '<', $file or die "Cannot open $file: $!\n";
my @lines = <$fh>;
close $fh;
# --- Parse XML ---
my $depth = 0;
my %skip_props = map { $_ => 1 } qw(ActiveProfile IdentityPopups Notify AutoEnableProfiles);
my %sections;
my @section_order;
my ($cur_section, $cur_monitor, $cur_prop);
my %cur_mon;
my @cur_mons;
sub parse_attrs {
my $s = shift;
my %a;
while ($s =~ /(\w+)="([^"]*)"/g) { $a{$1} = $2; }
$a{value} //= '';
$a{value} =~ s/&quot;/"/g;
$a{value} =~ s/&amp;/&/g;
return %a;
}
for my $line (@lines) {
if ($line =~ m{<property\s+([^>]*)/\s*>}) {
my %a = parse_attrs($1);
if ($depth == 2 && $cur_monitor) {
my ($n, $v) = ($a{name}//'', $a{value});
if ($n eq 'Active') { $cur_mon{active} = $v eq 'true' ? 'YES' : 'no'; }
elsif ($n eq 'EDID') { $cur_mon{edid} = $v ? substr($v, 0, 10) : ''; }
elsif ($n eq 'Resolution') { $cur_mon{res} = $v; }
elsif ($n eq 'RefreshRate') { $cur_mon{refresh} = $v; }
elsif ($n eq 'Primary') { $cur_mon{primary} = $v eq 'true' ? '*PRI*' : ''; }
}
elsif ($depth == 3 && $cur_prop && $cur_prop eq 'Position') {
my ($n, $v) = ($a{name}//'', $a{value});
if ($n eq 'X') { $cur_mon{pos_x} = $v; }
elsif ($n eq 'Y') { $cur_mon{pos_y} = $v; }
}
next;
}
if ($line =~ m{<property\s+([^>]*)>}) {
my %a = parse_attrs($1);
$depth++;
if ($depth == 1) {
my $n = $a{name} // '';
if (!$skip_props{$n}) { $cur_section = $n; @cur_mons = (); }
else { $cur_section = undef; }
}
elsif ($depth == 2 && $cur_section) {
$cur_monitor = $a{name};
%cur_mon = ( port => $a{name}, desc => $a{value}||$a{name},
active => '?', edid => '', res => '', refresh => '',
primary => '', pos_x => 0, pos_y => 0 );
}
elsif ($depth == 3 && $cur_monitor) {
$cur_prop = $a{name};
}
next;
}
while ($line =~ m{</property>}g) {
if ($depth == 3) { $cur_prop = undef; }
elsif ($depth == 2 && $cur_monitor) { push @cur_mons, { %cur_mon }; $cur_monitor = undef; }
elsif ($depth == 1 && $cur_section) {
if (@cur_mons) {
$sections{$cur_section} = { monitors => [ @cur_mons ] };
push @section_order, $cur_section;
}
$cur_section = undef;
}
$depth--;
$depth = 0 if $depth < 0;
}
}
# --- Display ---
print "=" x 75, "\n";
print " XFCE4 Display Configuration: $file\n";
print "=" x 75, "\n\n";
if (!@section_order) {
print "No monitor sections found!\n";
}
for my $sect_name (@section_order) {
my @mons = @{$sections{$sect_name}{monitors}};
printf "\033[1;33m[ %s ]\033[0m (%d monitors)\n", $sect_name, scalar @mons;
print "-" x 75, "\n";
printf " %-3s %-14s %-26s %-6s %-11s %-6s %s\n",
'#', 'Port', 'Monitor', 'Active', 'Resolution', 'Hz', 'Position';
print " ", "-" x 72, "\n";
my $i = 1;
for my $m (@mons) {
my $color = $m->{active} eq 'YES' ? "\033[32m" : "\033[90m";
printf " ${color}%-3d %-14s %-26s %-6s %-11s %-6s %d,%d %s\033[0m\n",
$i++, $m->{port}, $m->{desc}, $m->{active},
$m->{res}, $m->{refresh}, $m->{pos_x}, $m->{pos_y}, $m->{primary};
}
print "\n";
}
# --- Menu ---
print "=" x 75, "\n";
print " OPTIONS:\n";
print "=" x 75, "\n";
print " 1) Delete entire Fallback section (most common fix)\n";
print " 2) Delete specific monitors from a section\n";
print " 3) Delete entire section by name\n";
print " 4) Nuke everything except Default (clean start)\n";
print " 5) Set backdrop (wallpaper/color) on connected monitors\n";
print " 6) Just show info (exit)\n";
print "\nChoice [1-6]: ";
my $choice = <STDIN>;
chomp $choice;
exit 0 if $choice eq '6';
if ($choice eq '5') {
set_backdrop();
exit 0;
}
# --- Stop xfconfd ---
my $killed_xfconfd = 0;
my $xfconfd_pid = `$PIDOF xfconfd 2>/dev/null`;
chomp $xfconfd_pid;
if ($xfconfd_pid) {
print "\n\033[1;31m*** xfconfd (PID $xfconfd_pid) is running ***\033[0m\n";
print " Must kill it first or it overwrites changes from RAM.\n";
print " Kill xfconfd? [Y/n]: ";
my $ans = <STDIN>; chomp $ans;
if (lc($ans) ne 'n') {
system("$KILL $xfconfd_pid");
select(undef, undef, undef, 0.5);
my $check = `$PIDOF xfconfd 2>/dev/null`; chomp $check;
system("$KILL -9 $check") if $check;
print " xfconfd killed.\n";
$killed_xfconfd = 1;
}
}
# --- Backup ---
my $ts = strftime("%Y%m%d_%H%M%S", localtime);
my $backup = "${file}.bak_${ts}";
copy($file, $backup) or die "Cannot backup: $!\n";
print "Backup: $backup\n\n";
# --- Determine what to delete ---
my %del_sect;
my %del_mon;
if ($choice eq '1') {
if (exists $sections{'Fallback'}) {
$del_sect{'Fallback'} = 1;
printf "Deleting Fallback (%d monitors).\n", scalar @{$sections{'Fallback'}{monitors}};
} else { print "No Fallback section.\n"; exit 0; }
}
elsif ($choice eq '2') {
print "Which section? [" . join(', ', @section_order) . "]: ";
my $sect = <STDIN>; chomp $sect;
die "Not found.\n" unless exists $sections{$sect};
my @mons = @{$sections{$sect}{monitors}};
print "Enter numbers to DELETE (comma-separated): ";
my $nums = <STDIN>; chomp $nums;
for my $n (map { int($_) } split /[,\s]+/, $nums) {
if ($n >= 1 && $n <= @mons) {
$del_mon{$sect}{$mons[$n-1]{port}} = 1;
printf " Delete: #%d %s (%s)\n", $n, $mons[$n-1]{port}, $mons[$n-1]{desc};
}
}
}
elsif ($choice eq '3') {
print "Which section? [" . join(', ', @section_order) . "]: ";
my $sect = <STDIN>; chomp $sect;
die "Not found.\n" unless exists $sections{$sect};
$del_sect{$sect} = 1;
}
elsif ($choice eq '4') {
for my $s (@section_order) {
next if $s eq 'Default';
$del_sect{$s} = 1;
printf "Deleting '%s' (%d monitors).\n", $s, scalar @{$sections{$s}{monitors}};
}
}
else { die "Invalid.\n"; }
# --- Rebuild XML ---
my @out;
$depth = 0;
my $skipping = 0;
my $skip_until_depth = 0;
my ($d1_name, $d2_name) = ('', '');
for my $line (@lines) {
my $is_selfclose = ($line =~ m{<property\s+[^>]*/\s*>});
my $is_open = (!$is_selfclose && $line =~ m{<property\s+([^>]*)>});
my $open_attrs = $1 // '';
my $close_count = 0;
$close_count++ while $line =~ m{</property>}g;
if ($skipping) {
if ($is_open) { $depth++; }
$depth -= $close_count;
$depth = 0 if $depth < 0;
$skipping = 0 if $depth < $skip_until_depth;
next;
}
if ($is_open) {
$depth++;
my %a;
while ($open_attrs =~ /(\w+)="([^"]*)"/g) { $a{$1} = $2; }
if ($depth == 1) {
$d1_name = $a{name} // '';
if ($del_sect{$d1_name}) {
$skipping = 1; $skip_until_depth = $depth - 1; next;
}
}
elsif ($depth == 2) {
$d2_name = $a{name} // '';
if ($del_mon{$d1_name} && $del_mon{$d1_name}{$d2_name}) {
$skipping = 1; $skip_until_depth = $depth - 1; next;
}
}
}
$depth -= $close_count;
$depth = 0 if $depth < 0;
$d1_name = '' if $close_count > 0 && $depth == 0;
$d2_name = '' if $close_count > 0 && $depth <= 1;
push @out, $line;
}
my $output = join('', @out);
$output =~ s/\n{3,}/\n\n/g;
open my $of, '>', $file or die "Cannot write: $!\n";
print $of $output;
close $of;
print "\n\033[1;32mSaved: $file\033[0m\n";
if ($killed_xfconfd) {
print "\nRestarting xfconfd... ";
system("$XFCONF_Q -c displays -p /ActiveProfile >/dev/null 2>&1");
my $new_pid = `$PIDOF xfconfd 2>/dev/null`; chomp $new_pid;
print $new_pid ? "OK (PID $new_pid)\n" : "will start on next access.\n";
}
print "Reloading xfdesktop... ";
system("$XFDESKTOP --reload >/dev/null 2>&1 &");
print "done.\n";
print "\nBackup: $backup\n";
print "Rollback: cp '$backup' '$file' && $XFDESKTOP --reload\n";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment