You are not logged in.
Been screwing around most of the day with this, had never messed with C before, or compiling it so this is a first for me, awesome learning experience but omg does it ever take me forever to figure out how to do simple stuff, lol, RRQ probably could do it in 5 minutes on leafpad!
Simple app to list applications installed and a description, also has a search function that starts working as soon as you start typing, it scrapes /usr/share/applications then cross-references that list with /var/lib/dpkg/status for the description. Simple but possibly useful to the new folks I convert to linux around here. Here's a screenshot, nothing fancy and this is just the working prototype, I have a lot of work to do yet polishing up the text area as it's kinda shoved all together right now:
https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded April 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate
Offline
Looks interesting!
Offline
Looks cool. What toolkit are you using?
Apart from probably the search bar, you could also create this using YAD (maybe even Zenity). I've been studying up on some custom GUI boxes using this method. This example for reading system information is one of my favorites I've come across so far (a nice drop-in replacement for Hardinfo). It looks good on GTK2.
Offline
Thanks guys!
Yeah Yad + shellscript was my first instinct as it's my go-to for so many things, but just couldn't get it to look and act right. Also tried using webkit to make a little browser basically as i've had some luck on another app doing that and python for the rest, but still no luck. Finally used C and just plain ol gtk and that worked insanely well and fast for the GUI, took a fair amount of tries but i'm happy with it. The separate scraper script is done in bash.
Got to looking at yad while trying to work that out, fired up the yad-icon-browser which was super-close to what I wanted, and I always figured since it came packaged with yad that it was made with yad, lol, nope. Binary file written in C so I really don't understand why it's packaged with yad as there is almost no yad in it whatsoever. But....
The source code for it is very small, and it was just a matter of stripping out a lot of it that I didn't need, then re-writing it a bit to scrape a text file for content rather than the icons. I'm using that text file created by the scraper script instead of sqlite for a database. One of the goals here was to NOT have to install extra depends for this over and above what's typically on a basic system, keeping it simple, small, and fast and doing one job well.
Still need to fine-tune the scraper script, it's missing the description portion in about 10% of the apps, mainly ones with weird names for their .desktop files instead of straightforward ones, like Handbrake ought to be handbrake.desktop, but instead it's "fr.handbrake.ghb.desktop" and Hexchat is "io.github.hexchat.desktop", somehow that's defeating me, currently trying some new mods to the script.
Last edited by greenjeans (2025-06-25 23:02:42)
https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded April 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate
Offline
Here's the scraper script if anyone has any suggestions for my little issue with it not grabbing the description for some apps, ignore the paths at the top, it's not an Appimage, that's just a directory that wound up to be my area for all experiments:
EDIT: Been messing with this all day today, and am soooooo close to now to 100% with the scraper, fixed like 90% of the issues. But a few minutes ago I had a forehead slapper moment, I think i've been going about this the hard way, gonna try a little different tack.
Last edited by greenjeans (2025-06-26 21:38:21)
https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded April 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate
Offline
Holy cow has it ever been a day, I don't know how y'all that do this for a living do it, just grateful that you do!
After tons of adding functions of various types I finally have it nailed down 99.99%. Had to add some Mate-specific exclusions along with others to get rid of the simple utilities that are self-explanatory, Openbox won't have those issues but I imagine if someone tried this with another DE that more exclusions would have to be added. Scraper script is pretty complex now, and takes quite a while to run, it's only a one-time run needed for this, honestly I could have just typed out the database in short order, but that's not the point of this exercise.
End goal is to integrate this with the release notes/manual I do in Vuu-do, and provide an all-in-one help app for folks brand new to linux to get oriented quickly.
Still needs a little work on text-formatting, i'm tempted to try and see if parallel-processing can speed up the scraper script, but that's low priority since it only needs to run once, just considering it for the experience and furthering my knowledge-base.
If anybody is interested let me know and i'll post up the two scripts, you'll have to compile the C yourself but that's super-easy and only takes a few seconds, just need GCC and libgtk3-dev.
https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded April 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate
Offline
Scraper script is pretty complex now, and takes quite a while to run, it's only a one-time run needed for this,
Next step, rewrite the scraper in C
:-)
Offline
^^^ Oh hell no, lol! Been fighting with the main script (C) all morning trying to get the text and bullet points properly formatted, so far all i've succeeded in doing is changing the font and spacing which does look much nicer, but still, i'm no C wiz, no way i'm ready to take on doing the scraper in C, I just now got it whipped into beautiful working shape and even that took a ton of trial and error despite shellscript being something i'm decent at now.
Here is the two scripts, scraper and the raw C code for the GUI and the bit to compile if someone wants to try it out, note that you'll need to change the paths at the top of the scripts and the compile line. Just look at that scraper script and all the pattern-matching crap I had to do, this is because some folks just refuse to do some common logical naming of things. the gnome-disk-utility is one of the worst offenders in that regard (not surprising that), they literally have a different name for every identifier, .desktop name, name, exec and package name, whereas most apps use the same basic name for all those things making it a breeze to parse and connect the name to the description.
gcc -o ~/path/to/appbrowser ~/path/to/appbrowser.c $(pkg-config --cflags --libs gtk+-3.0)
GUI
#define _GNU_SOURCE
#include <gtk/gtk.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_LINE 1024
// Struct to hold app data
typedef struct {
char *name;
char *description;
} AppData;
// Global app list
AppData *apps = NULL;
int num_apps = 0;
// GTK widgets
GtkWidget *window;
GtkWidget *app_list_view;
GtkListStore *app_store;
GtkWidget *desc_view;
GtkTextBuffer *desc_buffer;
GtkWidget *search_entry;
// Get dynamic data file path
char *get_data_file_path() {
char *home = getenv("HOME");
if (!home) {
g_print("Error: Cannot get HOME environment variable\n");
return NULL;
}
char *path = malloc(strlen(home) + strlen("/path/to/scraped_apps.txt") + 1);
sprintf(path, "%s/path/to/scraped_apps.txt", home);
return path;
}
// Load apps from scraped_apps.txt
void load_apps() {
char *data_file = get_data_file_path();
if (!data_file) {
gtk_text_buffer_set_text(desc_buffer, "Error: Cannot get data file path", -1);
return;
}
FILE *file = fopen(data_file, "r");
if (!file) {
char err_msg[256];
snprintf(err_msg, 256, "Error: Cannot open %s", data_file);
gtk_text_buffer_set_text(desc_buffer, err_msg, -1);
free(data_file);
return;
}
char line[MAX_LINE];
int capacity = 100;
apps = malloc(capacity * sizeof(AppData));
num_apps = 0;
while (fgets(line, MAX_LINE, file)) {
// Remove trailing newline
line[strcspn(line, "\n")] = '\0';
// Parse line: Name|Exec|Description
char *name = strtok(line, "|");
if (!name) continue;
char *exec = strtok(NULL, "|");
if (!exec) continue;
char *desc = strtok(NULL, "\0"); // Capture entire description
if (!desc) desc = "No description available";
if (num_apps >= capacity) {
capacity *= 2;
apps = realloc(apps, capacity * sizeof(AppData));
}
apps[num_apps].name = strdup(name);
apps[num_apps].description = strdup(desc);
num_apps++;
}
fclose(file);
free(data_file);
g_print("Loaded %d apps from %s\n", num_apps, data_file);
}
// Filter apps based on search
gboolean filter_apps(GtkTreeModel *model, GtkTreeIter *iter, gpointer data) {
const char *search_text = gtk_entry_get_text(GTK_ENTRY(search_entry));
if (!search_text || strlen(search_text) == 0) return TRUE;
char *name;
gtk_tree_model_get(model, iter, 0, &name, -1);
if (!name) return FALSE;
gboolean visible = strcasestr(name, search_text) != NULL;
g_free(name);
return visible;
}
// Update description pane
void on_app_selected(GtkTreeSelection *selection, gpointer data) {
GtkTreeIter filter_iter;
GtkTreeModel *filter_model;
if (gtk_tree_selection_get_selected(selection, &filter_model, &filter_iter)) {
GtkTreeIter child_iter;
GtkTreeModel *child_model = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter_model));
gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter_model), &child_iter, &filter_iter);
char *name;
gtk_tree_model_get(child_model, &child_iter, 0, &name, -1);
g_print("Selected app: %s\n", name ? name : "NULL");
if (name) {
for (int i = 0; i < num_apps; i++) {
if (apps[i].name && strcmp(apps[i].name, name) == 0) {
g_print("Setting description for %s\n", name);
gtk_text_buffer_set_text(desc_buffer, apps[i].description ? apps[i].description : "No description available", -1);
g_free(name);
return;
}
}
g_print("No matching app found for %s\n", name);
g_free(name);
gtk_text_buffer_set_text(desc_buffer, "No description available", -1);
} else {
g_print("Failed to get app name\n");
gtk_text_buffer_set_text(desc_buffer, "Error: Invalid selection", -1);
}
} else {
g_print("No selection\n");
gtk_text_buffer_set_text(desc_buffer, "Select an app to view its description", -1);
}
}
// Update filter on search change
void on_search_changed(GtkEntry *entry, gpointer data) {
gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(data));
}
int main(int argc, char *argv[]) {
gtk_init(&argc, &argv);
// Create window
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), "Vuu-do App Info");
gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
// Create main container
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_container_add(GTK_CONTAINER(window), vbox);
// Search entry
search_entry = gtk_entry_new();
gtk_entry_set_placeholder_text(GTK_ENTRY(search_entry), "Search apps by name...");
gtk_box_pack_start(GTK_BOX(vbox), search_entry, FALSE, FALSE, 5);
// Paned layout
GtkWidget *paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
gtk_paned_set_position(GTK_PANED(paned), 300); // Wide app list
gtk_box_pack_start(GTK_BOX(vbox), paned, TRUE, TRUE, 5);
// App list (left pane)
app_store = gtk_list_store_new(1, G_TYPE_STRING);
GtkTreeModel *filter_model = gtk_tree_model_filter_new(GTK_TREE_MODEL(app_store), NULL);
gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filter_model), filter_apps, NULL, NULL);
app_list_view = gtk_tree_view_new_with_model(filter_model);
GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("Applications", renderer, "text", 0, NULL);
gtk_tree_view_append_column(GTK_TREE_VIEW(app_list_view), column);
gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(app_list_view), TRUE);
// Populate app list
load_apps();
for (int i = 0; i < num_apps; i++) {
GtkTreeIter iter;
gtk_list_store_append(app_store, &iter);
gtk_list_store_set(app_store, &iter, 0, apps[i].name, -1);
}
GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_container_add(GTK_CONTAINER(scroll), app_list_view);
gtk_paned_pack1(GTK_PANED(paned), scroll, FALSE, FALSE);
// Description pane (right)
desc_view = gtk_text_view_new();
gtk_text_view_set_editable(GTK_TEXT_VIEW(desc_view), FALSE);
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(desc_view), GTK_WRAP_WORD);
gtk_text_view_set_left_margin(GTK_TEXT_VIEW(desc_view), 10);
gtk_text_view_set_right_margin(GTK_TEXT_VIEW(desc_view), 10);
gtk_text_view_set_top_margin(GTK_TEXT_VIEW(desc_view), 10);
gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(desc_view), 10);
desc_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(desc_view));
// Apply CSS for font and spacing
GtkCssProvider *provider = gtk_css_provider_new();
gtk_css_provider_load_from_data(provider,
"textview { line-height: 1.5; padding: 10px; font-family: Sans; }", -1, NULL);
gtk_style_context_add_provider(gtk_widget_get_style_context(desc_view),
GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g_object_unref(provider);
gtk_text_buffer_set_text(desc_buffer, "Select an app to view its description", -1);
scroll = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_container_add(GTK_CONTAINER(scroll), desc_view);
gtk_paned_pack2(GTK_PANED(paned), scroll, TRUE, FALSE);
// Connect signals
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(app_list_view));
gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
g_signal_connect(selection, "changed", G_CALLBACK(on_app_selected), NULL);
g_signal_connect(search_entry, "changed", G_CALLBACK(on_search_changed), filter_model);
// Show all
gtk_widget_show_all(window);
gtk_main();
// Cleanup
for (int i = 0; i < num_apps; i++) {
free(apps[i].name);
free(apps[i].description);
}
free(apps);
return 0;
}
Scraper
#!/bin/bash
# Output file for scraped data
output_file="$HOME/path/to/scraped_apps.txt"
debug_log="$HOME/path/to/scrape_debug.log"
mkdir -p "$(dirname "$output_file")"
# Check if output file exists and is non-empty
if [ -s "$output_file" ]; then
echo "Using existing $output_file" >> "$debug_log"
exit 0
fi
# Initialize output and debug files
: > "$output_file"
: > "$debug_log"
# Parse /var/lib/dpkg/status for package descriptions
echo "Scraping /var/lib/dpkg/status..." >> "$debug_log"
declare -A dpkg_desc
current_pkg=""
desc=""
if [ ! -r "/var/lib/dpkg/status" ]; then
echo "Error: Cannot read /var/lib/dpkg/status" >> "$debug_log"
exit 1
fi
while IFS= read -r line; do
if [[ "$line" =~ ^Package:\ (.+) ]]; then
current_pkg="${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^Description:\ (.+) ]]; then
desc="${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^\ (.+) ]] && [[ -n "$current_pkg" ]] && [[ -n "$desc" ]]; then
desc+=" ${BASH_REMATCH[1]}"
elif [[ -z "$line" ]] && [[ -n "$current_pkg" ]] && [[ -n "$desc" ]]; then
dpkg_desc["$current_pkg"]="$desc"
current_pkg=""
desc=""
fi
done < /var/lib/dpkg/status
echo "Found ${#dpkg_desc[@]} packages in /var/lib/dpkg/status" >> "$debug_log"
# Function to find a matching term from .desktop filename, Name, and Exec fields
find_matching_term() {
local desktop_file="$1"
local package_name="$2"
# Extract Name and Exec fields, convert to lowercase
name_field=$(grep -m1 -i "^Name=" "$desktop_file" | sed 's/^Name=//' | tr '[:upper:]' '[:lower:]' | tr -s ' ')
exec_field=$(grep -m1 -i "^Exec=" "$desktop_file" | sed 's/^Exec=//' | tr '[:upper:]' '[:lower:]' | awk '{print $1}' | xargs basename)
# Get the .desktop filename without extension, converted to lowercase
desktop_base=$(basename "$desktop_file" .desktop | tr '[:upper:]' '[:lower:]')
# Split filename on dots and dashes
desktop_terms=(${desktop_base//[.-]/ }) # Split on . or -
# Find common terms across fields
for term in "${desktop_terms[@]}"; do
if [[ "$name_field" =~ $term || "$exec_field" =~ $term ]]; then
echo "Found matching term '$term' for $desktop_file" >> "$debug_log"
if [[ -n "${dpkg_desc[$term]}" ]]; then
echo "$term"
return
fi
fi
done
# Try common suffixes
for suffix in "-all" "-gui" "-common"; do
local test_pkg="${package_name}${suffix}"
if [[ -n "${dpkg_desc[$test_pkg]}" ]]; then
echo "Found package with suffix '$test_pkg' for $desktop_file" >> "$debug_log"
echo "$test_pkg"
return
fi
done
# Fallback to dpkg -S
local dpkg_pkg
dpkg_pkg=$(dpkg -S "$(basename "$desktop_file")" 2>/dev/null | cut -d':' -f1)
if [[ -n "$dpkg_pkg" && -n "${dpkg_desc[$dpkg_pkg]}" ]]; then
echo "Found package '$dpkg_pkg' via dpkg -S for $desktop_file" >> "$debug_log"
echo "$dpkg_pkg"
return
fi
# Final fallback: return original package name
echo "No matching term, suffix, or dpkg -S found for $desktop_file, using $package_name" >> "$debug_log"
echo "$package_name"
}
# Scrape .desktop files
desktop_dir="/usr/share/applications"
echo "Scraping $desktop_dir..." >> "$debug_log"
if [ ! -d "$desktop_dir" ]; then
echo "Error: Directory $desktop_dir does not exist" >> "$debug_log"
exit 1
fi
count=0
shopt -s nullglob # Handle case where no .desktop files exist
for desktop_file in "$desktop_dir"/*.desktop; do
if [ ! -f "$desktop_file" ] || [ ! -r "$desktop_file" ]; then
echo "Skipping $desktop_file: File does not exist or is not readable" >> "$debug_log"
continue
fi
echo "Processing $desktop_file" >> "$debug_log"
# Check owning package with dpkg -S
owning_pkg=$(dpkg -S "$(basename "$desktop_file")" 2>/dev/null | cut -d':' -f1)
if [[ "$owning_pkg" == "mate-utils" || "$owning_pkg" == "mate-control-center" || "$owning_pkg" == "mate-desktop" ]]; then
echo "Skipping $desktop_file: Owned by $owning_pkg" >> "$debug_log"
continue
fi
# Extract Name, Exec, Comment, NoDisplay, and Categories using grep
name=$(grep -m1 -i "^Name=" "$desktop_file" | sed 's/^Name=//')
exec_cmd=$(grep -m1 -i "^Exec=" "$desktop_file" | sed 's/^Exec=//')
comment=$(grep -m1 -i "^Comment=" "$desktop_file" | sed 's/^Comment=//')
nodisplay=$(grep -m1 -i "^NoDisplay=" "$desktop_file" | sed 's/^NoDisplay=//')
categories=$(grep -m1 -i "^Categories=" "$desktop_file" | sed 's/^Categories=//')
# Skip if Name or Exec is empty
if [ -z "$name" ] || [ -z "$exec_cmd" ]; then
echo "Skipping $desktop_file: Missing Name or Exec" >> "$debug_log"
continue
fi
# Skip if NoDisplay=true
if [ "$nodisplay" = "true" ]; then
echo "Skipping $desktop_file: NoDisplay=true" >> "$debug_log"
continue
fi
# Skip if Categories contains "Settings" (case-insensitive)
if echo "$categories" | grep -qi "Settings"; then
echo "Skipping $desktop_file: Categories contains 'Settings'" >> "$debug_log"
continue
fi
# Get package name (strip .desktop)
pkg_name=$(basename "$desktop_file" .desktop | tr '[:upper:]' '[:lower:]')
# Get description (dpkg or fallback to Comment)
desc="${dpkg_desc[$pkg_name]:-$comment}"
# If no description from dpkg or falls back to comment, try matching term
if [ -z "$desc" ] || [ "$desc" = "$comment" ]; then
new_pkg_name=$(find_matching_term "$desktop_file" "$pkg_name")
desc="${dpkg_desc[$new_pkg_name]:-$comment}"
echo "Retried description for $name with package $new_pkg_name" >> "$debug_log"
fi
# Final fallback
desc="${desc:-No description available}"
# Write to output file (format: Name|Exec|Description)
echo "$name|$exec_cmd|$desc" >> "$output_file"
((count++))
echo "Added $name to output" >> "$debug_log"
done
shopt -u nullglob
echo "Scraped $count apps from $desktop_dir" >> "$debug_log"
echo "Output written to $output_file" >> "$debug_log"
echo "Debug log: $debug_log"
# Add test app if no apps were scraped
if [ "$count" -eq 0 ]; then
echo "No apps scraped, adding test app" >> "$debug_log"
echo "TestApp|echo 'Test App'|Test application" >> "$output_file"
((count++))
echo "Added TestApp to output" >> "$debug_log"
fi
echo "Total apps scraped: $count" >> "$debug_log"
https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded April 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate
Offline
I got rid of all the utilities that didn't need to be on there with some creative exclusions, so that cleaned up a lot.
https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded April 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate
Offline