Created
April 4, 2026 17:24
-
-
Save smooker/b15ed5da0eca0c25a6ef34c7da8b9d19 to your computer and use it in GitHub Desktop.
XFCE4 displays cleanup + backdrop tool (--recreate for one-shot fix)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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/"/"/g; | |
| $a{value} =~ s/&/&/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