Created
February 6, 2025 14:51
-
-
Save EricsonWillians/eadef25d6c131fdd8d564bd11ff76387 to your computer and use it in GitHub Desktop.
A perl script for migrating shortcuts from flatpak to lxde menu / applications folder.
This file contains 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/env perl | |
use strict; | |
use warnings; | |
use File::Find; | |
use File::Path qw(make_path); | |
use File::Copy qw(copy); | |
use File::Compare; | |
use File::Basename; | |
use IPC::System::Simple qw(system capture); | |
use Cwd qw(abs_path); | |
############################################################################### | |
# Global Directories and Configuration | |
############################################################################### | |
# Home directory (resolve from environment or the current user) | |
my $HOME = $ENV{HOME} || (getpwuid($<))[7]; | |
# LXDE and GNOME menu paths | |
my $LXDE_MENU_DIR = "$HOME/.local/share/lxpanel/LXDE/menus"; | |
my $GNOME_MENU_DIR = "$HOME/.config/menus"; | |
my $MENU_FILENAME = "flatpak-apps.menu"; | |
my $APPLICATIONS_DIR = "$HOME/.local/share/applications"; | |
# Typical Flatpak .desktop directories | |
my $FLATPAK_EXPORTS = "$HOME/.local/share/flatpak/exports/share/applications"; | |
my $FLATPAK_SYSTEM = "/var/lib/flatpak/exports/share/applications"; | |
my $FLATPAK_APPS = "$HOME/.local/share/flatpak/app"; # Contains installed Flatpak app directories | |
# Icon directories | |
my $FLATPAK_ICONS = "$HOME/.local/share/flatpak/exports/share/icons"; | |
my $SYSTEM_ICONS = "/var/lib/flatpak/exports/share/icons"; | |
my $LOCAL_ICONS = "$HOME/.local/share/icons"; | |
# Potential image extensions to search for, locally and remotely | |
my @ICON_EXTS = ('.png', '.svg', '.jpg', '.jpeg', '.xpm', '.ico'); | |
# Common icon sizes for typical hicolor theme directories | |
my @ICON_SIZES = qw( | |
16x16 22x22 24x24 32x32 48x48 64x64 96x96 128x128 256x256 | |
); | |
############################################################################### | |
# Subroutines | |
############################################################################### | |
#------------------------------------------------------------------------------ | |
# parse_desktop_file($desktop_file) -> \%desktop_data | |
# Reads a .desktop file line by line, returns a hashref of { KEY => VALUE }. | |
#------------------------------------------------------------------------------ | |
sub parse_desktop_file { | |
my ($desktop_file) = @_; | |
my %data; | |
open my $fh, '<', $desktop_file or return {}; | |
while (my $line = <$fh>) { | |
chomp($line); | |
# Skip comments and blank lines | |
next if $line =~ /^\s*#/ || $line =~ /^\s*$/; | |
# Accept lines that match KEY=VALUE | |
if ($line =~ /^([^=]+)=(.*)$/) { | |
my ($key, $val) = ($1, $2); | |
$data{$key} = $val; | |
} | |
} | |
close $fh; | |
return \%data; | |
} | |
#------------------------------------------------------------------------------ | |
# find_or_download_icon($icon_name) -> $final_icon_name | |
# | |
# 1) Attempts to resolve $icon_name locally. | |
# 2) If not found, tries searching for a better match among installed app folders. | |
# 3) If still not found, tries minimal "web scraping" for an icon match (GitHub, etc.). | |
# 4) Copies found icon to $LOCAL_ICONS/hicolor/<size>/apps. | |
# 5) Returns a short icon name (without path) so that .desktop files can reference it. | |
#------------------------------------------------------------------------------ | |
sub find_or_download_icon { | |
my ($icon_name) = @_; | |
return '' unless $icon_name; # No icon specified at all | |
# Break down the icon_name to handle potential paths or extension | |
my ($basename, $path, $orig_ext) = fileparse($icon_name, @ICON_EXTS); | |
# Create an array of possible filenames to look for (with or without extension) | |
my @possible_names; | |
if ($orig_ext) { | |
push @possible_names, $basename . $orig_ext; | |
} else { | |
push @possible_names, $basename; # no extension scenario | |
foreach my $ext (@ICON_EXTS) { | |
push @possible_names, $basename . $ext; | |
} | |
} | |
# 1) Try local icon directories (Flatpak/system/local). | |
if (my $found_local = find_icon_locally(\@possible_names)) { | |
return $found_local; | |
} | |
# 2) Try searching inside $FLATPAK_APPS for potential images that match | |
# the app's ID or a subdirectory name (recursive). | |
# This might yield a better guess if there's e.g. "icon.png" inside | |
# /home/ericsonwillians/.local/share/flatpak/app/SomeAppDir/ | |
if (my $found_in_apps = find_icon_in_flatpak_apps(\@possible_names)) { | |
return $found_in_apps; | |
} | |
# 3) If not found, attempt a naive web scraping approach. We demonstrate | |
# minimal GitHub + general web fetch. In practice, you'd tailor these | |
# calls to known icon repositories or official sources. | |
# if (my $found_online = find_icon_online($basename)) { | |
# return $found_online; | |
# } | |
# If everything fails, return the original name. The system might still find | |
# a fallback through existing icon themes or end up with a missing icon. | |
return $icon_name; | |
} | |
#------------------------------------------------------------------------------ | |
# find_icon_locally(\@candidate_names) -> $final_short_name | |
# Tries to locate any candidate icon name in local known directories and subdirs. | |
# If found, copies it into local hicolor and returns the short name. | |
# Returns undef if not found. | |
#------------------------------------------------------------------------------ | |
sub find_icon_locally { | |
my ($candidate_names) = @_; | |
my @search_paths = ( | |
$FLATPAK_ICONS, | |
$SYSTEM_ICONS, | |
"$LOCAL_ICONS/hicolor", | |
); | |
# For each search path, check typical hicolor subdirectories | |
foreach my $sp (@search_paths) { | |
next unless -d $sp; | |
foreach my $size (@ICON_SIZES) { | |
# Some icons are directly in e.g. $sp/48x48/apps, or nested further | |
my @possible_dirs = ( | |
"$sp/hicolor/$size/apps", | |
"$sp/$size/apps", | |
"$sp/$size", # fallback | |
$sp, # fallback | |
); | |
foreach my $dir_to_try (@possible_dirs) { | |
next unless -d $dir_to_try; | |
foreach my $cand (@$candidate_names) { | |
my $candidate_path = "$dir_to_try/$cand"; | |
if (-f $candidate_path) { | |
return copy_icon_to_local($candidate_path, $cand); | |
} | |
} | |
} | |
} | |
} | |
return; | |
} | |
#------------------------------------------------------------------------------ | |
# find_icon_in_flatpak_apps(\@candidate_names) -> $final_short_name | |
# | |
# Recursively examines $FLATPAK_APPS (e.g. ~/.local/share/flatpak/app/) for | |
# any file matching the candidate names or typical icon extensions. | |
# Returns a short icon name for the best match, or undef if not found. | |
#------------------------------------------------------------------------------ | |
sub find_icon_in_flatpak_apps { | |
my ($candidate_names) = @_; | |
my $found_path; | |
# We trap the '__ICON_FOUND__' exception inside an eval block to | |
# avoid printing a backtrace to STDOUT. | |
if (-d $FLATPAK_APPS) { | |
eval { | |
find( | |
sub { | |
return if -d $_; # Skip directories | |
my $filename = $_; | |
# Check each candidate possibility | |
foreach my $cand (@$candidate_names) { | |
if ($filename eq $cand) { | |
# Build absolute path and store it | |
$found_path = $File::Find::name; | |
# Stop the 'find' recursion using an exception | |
die '__ICON_FOUND__'; | |
} | |
} | |
}, | |
$FLATPAK_APPS | |
); | |
}; | |
# If the eval died with something other than '__ICON_FOUND__', rethrow | |
if ($@ && $@ !~ /__ICON_FOUND__/) { | |
die $@; | |
} | |
} | |
# If we found a path, copy it locally | |
if ($found_path) { | |
my ($basename, $dir, $ext) = fileparse($found_path, @ICON_EXTS); | |
my $cand = $ext ? $basename . $ext : $basename; | |
return copy_icon_to_local($found_path, $cand); | |
} | |
return; | |
} | |
#------------------------------------------------------------------------------ | |
# find_icon_online($basename) -> $short_icon_name | |
# | |
# Demonstrates a naive web-scraping approach. We attempt: | |
# 1) Searching GitHub for $basename or something similar | |
# 2) Searching a generic web location for an icon named $basename | |
# If we can fetch a valid image file, we copy it locally and return its name. | |
# | |
# Real-world scraping or API usage will vary widely. This is a placeholder. | |
#------------------------------------------------------------------------------ | |
sub find_icon_online { | |
my ($basename) = @_; | |
return unless $basename; | |
# Use local sub to attempt a download from a known or hypothetical list of URLs | |
# For demonstration, we build some possible URLs. Adjust as needed. | |
my @remote_urls = build_possible_icon_urls($basename); | |
# Attempt to download from each in turn | |
foreach my $url (@remote_urls) { | |
if (my $local_path = attempt_download($url)) { | |
# If an actual file was saved, copy it to local hicolor | |
return copy_icon_to_local($local_path, basename($local_path)); | |
} | |
} | |
return; # Nothing worked | |
} | |
#------------------------------------------------------------------------------ | |
# build_possible_icon_urls($basename) -> @urls | |
# | |
# Constructs a naive set of potential URLs for $basename, using various extensions | |
# and hypothetical hosting sites. Modify to suit your environment. | |
#------------------------------------------------------------------------------ | |
sub build_possible_icon_urls { | |
my ($basename) = @_; | |
warn "DEBUG: building URLs for basename=<$basename>\n"; | |
my @urls; | |
# Example: Attempt GitHub raw URLs from a known repository structure | |
# Replace 'someuser/somerepo' with your real path. We'll guess subfolders, etc. | |
foreach my $ext (@ICON_EXTS) { | |
push @urls, "https://raw.githubusercontent.com/someuser/somerepo/main/$basename$ext"; | |
push @urls, "https://raw.githubusercontent.com/someuser/somerepo/main/icons/$basename$ext"; | |
} | |
# Add your own fallback URLs or other known sources | |
# For instance, a separate "icons" domain or your local server | |
# push @urls, "https://example.com/icons/$basename.png", etc. | |
return @urls; | |
} | |
#------------------------------------------------------------------------------ | |
# attempt_download($url) -> $local_path or undef | |
# | |
# Tries to download from $url using 'curl' or 'wget'. If successful, returns the | |
# path to the downloaded file in /tmp/. Otherwise returns undef. The caller then | |
# can copy it into local hicolor or discard it. | |
#------------------------------------------------------------------------------ | |
sub attempt_download { | |
my ($url) = @_; | |
# We save to a temporary file in /tmp | |
my $tmp_file = "/tmp/$$.icon_download"; | |
unlink $tmp_file if -f $tmp_file; # remove if leftover | |
# If 'curl' is available, try that first | |
my $curl_path = `which curl 2>/dev/null`; chomp($curl_path); | |
if ($curl_path) { | |
my $status = system("$curl_path -fLs --connect-timeout 5 -o '$tmp_file' '$url'"); | |
if ($status == 0 && -s $tmp_file) { | |
return $tmp_file; | |
} | |
else { | |
# Gracefully note the failure rather than throw | |
warn "HTTP fetch failed for $url (curl exit code: $? >> 8)\n"; | |
} | |
} | |
# Otherwise, if 'wget' is available, try that | |
my $wget_path = `which wget 2>/dev/null`; chomp($wget_path); | |
if ($wget_path) { | |
my $status = system("$wget_path -q -T 5 -O '$tmp_file' '$url'"); | |
if ($status == 0 && -s $tmp_file) { | |
return $tmp_file; | |
} | |
} | |
return; # Download failed | |
} | |
#------------------------------------------------------------------------------ | |
# copy_icon_to_local($source_path, $candidate_filename) -> $icon_short_name | |
# | |
# Copies $source_path into $LOCAL_ICONS/hicolor/48x48/apps by default (adjust | |
# if you prefer a different size). Returns a short icon name (without path/extension). | |
#------------------------------------------------------------------------------ | |
sub copy_icon_to_local { | |
my ($source_path, $candidate_filename) = @_; | |
my $icon_target_dir = "$LOCAL_ICONS/hicolor/48x48/apps"; | |
unless (-d $icon_target_dir) { | |
make_path($icon_target_dir); | |
} | |
my $target_path = "$icon_target_dir/$candidate_filename"; | |
# Skip if source == target (meaning it's already there) | |
return basename($target_path) if $source_path eq $target_path; | |
# Or skip if file contents are identical: | |
# return basename($target_path) if -f $target_path && compare($source_path, $target_path) == 0; | |
copy($source_path, $target_path) | |
or warn "Failed to copy $source_path -> $target_path: $!"; | |
my ($base, $dir, $ext) = fileparse($candidate_filename, @ICON_EXTS); | |
return $ext ? $base : $candidate_filename; | |
} | |
#------------------------------------------------------------------------------ | |
# process_directory($dir_path, $menu_fh) -> void | |
# | |
# Recursively finds all .desktop files in $dir_path, validates them, appends them | |
# to the <Include> section of the menu. Copies the .desktop file to $APPLICATIONS_DIR | |
# if missing or different. Also attempts to rewrite the Icon= line if we can locate | |
# a better icon match. | |
#------------------------------------------------------------------------------ | |
sub process_directory { | |
my ($dir_path, $menu_fh) = @_; | |
return unless (-d $dir_path); | |
my @desktop_files; | |
find( | |
sub { | |
return unless -f $_ && /\.desktop$/i; | |
push @desktop_files, $File::Find::name; | |
}, | |
$dir_path | |
); | |
foreach my $desktop_file (@desktop_files) { | |
my $data = parse_desktop_file($desktop_file); | |
next unless $data->{Exec}; # Skip if there's no Exec line | |
if (exists $data->{Icon} && defined $data->{Icon}) { | |
$data->{Icon} =~ s/\#.*$//; # remove everything from '#' onward | |
$data->{Icon} =~ s/^\s+|\s+$//g; # trim whitespace | |
} | |
# Very rough check to see if parse_desktop_file returned anything | |
if (! %{$data}) { | |
print "⚠️ Invalid .desktop file or unreadable: $desktop_file\n"; | |
next; | |
} | |
# Extract filename | |
my $filename = fileparse($desktop_file); | |
# Include in the menu | |
print $menu_fh " <Filename>$filename</Filename>\n"; | |
# Copy to ~/.local/share/applications if new or changed | |
my $dest_path = "$APPLICATIONS_DIR/$filename"; | |
if (! -f $dest_path || compare($desktop_file, $dest_path) != 0) { | |
copy($desktop_file, $dest_path) or warn "Copy failed: $desktop_file -> $dest_path: $!"; | |
chmod 0755, $dest_path; # Mark executable (some desktop files require +x) | |
print "🔄 Processed: $filename\n"; | |
} | |
# Attempt to find/improve the icon | |
if ($data->{Icon}) { | |
my $best_icon = find_or_download_icon($data->{Icon}); | |
if ($best_icon && $best_icon ne $data->{Icon}) { | |
rewrite_desktop_icon($dest_path, $best_icon); | |
} | |
} | |
} | |
} | |
#------------------------------------------------------------------------------ | |
# rewrite_desktop_icon($desktop_path, $new_icon_name) -> void | |
# | |
# Rewrites the Icon= line in the .desktop file to use $new_icon_name if found. | |
#------------------------------------------------------------------------------ | |
sub rewrite_desktop_icon { | |
my ($desktop_path, $new_icon_name) = @_; | |
return unless (-f $desktop_path); | |
my @lines; | |
{ | |
open my $in, '<', $desktop_path or return; | |
@lines = <$in>; | |
close $in; | |
} | |
foreach my $line (@lines) { | |
if ($line =~ /^Icon=/) { | |
$line = "Icon=$new_icon_name\n"; | |
} | |
} | |
{ | |
open my $out, '>', $desktop_path or return; | |
print $out @lines; | |
close $out; | |
} | |
return; | |
} | |
############################################################################### | |
# Main Script Execution | |
############################################################################### | |
# 1) Ensure local directories exist | |
make_path($LXDE_MENU_DIR) unless (-d $LXDE_MENU_DIR); | |
make_path($GNOME_MENU_DIR) unless (-d $GNOME_MENU_DIR); | |
make_path($APPLICATIONS_DIR) unless (-d $APPLICATIONS_DIR); | |
make_path($LOCAL_ICONS) unless (-d $LOCAL_ICONS); | |
# Create subdirectories for hicolor theme | |
foreach my $sz (@ICON_SIZES) { | |
make_path("$LOCAL_ICONS/hicolor/$sz/apps") | |
unless -d "$LOCAL_ICONS/hicolor/$sz/apps"; | |
} | |
# 2) Generate menus for LXDE and GNOME | |
foreach my $menu_dir ($LXDE_MENU_DIR, $GNOME_MENU_DIR) { | |
my $menu_file = "$menu_dir/$MENU_FILENAME"; | |
open my $mf, '>', $menu_file or die "Cannot open $menu_file: $!"; | |
print $mf <<'EOF'; | |
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" | |
"http://www.freedesktop.org/standards/menu-spec/menu-1.0.dtd"> | |
<Menu> | |
<Name>Flatpak Applications</Name> | |
<Directory>flatpak-apps.directory</Directory> | |
<DefaultDirectoryDirs/> | |
<DefaultAppDirs/> | |
<Include> | |
EOF | |
# Migrate .desktop files from these directories in order | |
process_directory($APPLICATIONS_DIR, $mf); | |
process_directory($FLATPAK_EXPORTS, $mf); | |
process_directory($FLATPAK_SYSTEM, $mf); | |
process_directory($FLATPAK_APPS, $mf); | |
print $mf <<'EOF'; | |
</Include> | |
</Menu> | |
EOF | |
close $mf; | |
print "✅ Menu generated at: $menu_file\n"; | |
} | |
# 3) Create a basic hicolor index.theme if missing | |
my $theme_index = "$LOCAL_ICONS/hicolor/index.theme"; | |
if (! -f $theme_index) { | |
open my $th, '>', $theme_index or die "Cannot create $theme_index: $!"; | |
print $th <<'EOF'; | |
[Icon Theme] | |
Name=Hicolor | |
Comment=Fallback icon theme | |
Directories=16x16/apps,22x22/apps,24x24/apps,32x32/apps,48x48/apps,64x64/apps,96x96/apps,128x128/apps,256x256/apps | |
[16x16/apps] | |
Size=16 | |
Context=Applications | |
Type=Fixed | |
[22x22/apps] | |
Size=22 | |
Context=Applications | |
Type=Fixed | |
[24x24/apps] | |
Size=24 | |
Context=Applications | |
Type=Fixed | |
[32x32/apps] | |
Size=32 | |
Context=Applications | |
Type=Fixed | |
[48x48/apps] | |
Size=48 | |
Context=Applications | |
Type=Fixed | |
[64x64/apps] | |
Size=64 | |
Context=Applications | |
Type=Fixed | |
[96x96/apps] | |
Size=96 | |
Context=Applications | |
Type=Fixed | |
[128x128/apps] | |
Size=128 | |
Context=Applications | |
Type=Fixed | |
[256x256/apps] | |
Size=256 | |
Context=Applications | |
Type=Fixed | |
EOF | |
close $th; | |
} | |
# 4) Update system caches so that new icons and desktop entries are recognized | |
print "🔄 Updating system caches...\n"; | |
eval { system("gtk-update-icon-cache", "$LOCAL_ICONS/hicolor"); }; | |
eval { system("update-desktop-database", $APPLICATIONS_DIR); }; | |
print "✅ System caches updated.\n"; | |
# 5) Restart LXDE panel if the command is available | |
my $lxpanelctl = `which lxpanelctl 2>/dev/null`; chomp($lxpanelctl); | |
if ($lxpanelctl) { | |
system("$lxpanelctl restart"); | |
print "🔄 LXPanel restarted.\n"; | |
} else { | |
print "⚠️ LXPanel restart command not found; please restart manually.\n"; | |
} | |
print "🚀 Integration complete. Flatpak applications should now appear in LXDE/GNOME menus.\n"; | |
exit 0; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment