Skip to content

Instantly share code, notes, and snippets.

@EricsonWillians
Created February 6, 2025 14:51
Show Gist options
  • Save EricsonWillians/eadef25d6c131fdd8d564bd11ff76387 to your computer and use it in GitHub Desktop.
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.
#!/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