You are not logged in.
It would be great if somebody was willing to test what I have so far. I am posting the latest scripts but first you need to create the folder ~/.local/share/vai and drop all these scripts in there and drop the .desktop file into ~/.local/share/applications, and here is the order i'm posting them:
1. The wrapper script that must be named vai.sh
2. The scraper script that must be named scrape12.sh
3. The GUI raw C code, vai12.c you'll need to name vai12.c and compile with :gcc -o ~/.local/share/vai/vai12 ~/.local/share/vai/vai12.c $(pkg-config --cflags --libs gtk+-3.0)
4. The .desktop that goes in ~/.local/share/applications, you'll need to edit this file's path to use your username. But after that it should appear in your menu.
Make sure the scripts are made executable. Be aware that the scraper takes a long time, about 3.5 minutes on my ancient low-spec machine, probably a LOT faster on a modern machine with 4 cores or more. After that first run though, the app opens super fast.
Scripts:
#!/bin/sh
# Wrapper script for Vuu-do App Info (VAI)
# This application is for viewing information about installed apps.
# Copyleft 2025 greenjeans. Use as you see fit.
# This is free software with no warranty, use at your own risk.
# Paths
VAI_DIR="$HOME/.local/share/vai"
SCRAPED_APPS="$VAI_DIR/scraped_apps.txt"
SCRAPER="$HOME/.local/share/vai/scrape12.sh"
GUI="$HOME/.local/share/vai/vai12"
# Check if scraped_apps.txt exists, run scraper if not
if [ ! -f "$SCRAPED_APPS" ]; then
if [ ! -x "$SCRAPER" ]; then
echo "Error: Scraper script $SCRAPER not found or not executable" >&2
exit 1
fi
"$SCRAPER" || {
echo "Error: Scraper failed" >&2
exit 1
}
fi
# Run the GUI
if [ ! -x "$GUI" ]; then
echo "Error: GUI binary $GUI not found or not executable" >&2
exit 1
fi
exec "$GUI"
#!/bin/bash
# Copyleft 2025 greenjeans. Use as you see fit.
# This script scrapes your application files, parses their name and description
# and outputs the info into a text database to be read by the Vuu-do App Info GUI app.
# This is free software with no warranty, use at your own risk.
# Output file for scraped data
output_file="$HOME/.local/share/vai/scraped_apps.txt"
debug_log="$HOME/.local/share/vai/scrape_debug.log"
ensure_vai_dir
mkdir -p "$(dirname "$output_file")"
# Check and create ~/.local/share/vai directory
ensure_vai_dir() {
local dir="$HOME/.local/share/vai"
if [ ! -d "$dir" ]; then
mkdir -p "$dir" || {
echo "Error: Failed to create directory $dir" >&2
exit 1
}
echo "Created directory $dir" >> "$debug_log"
fi
}
# 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
if [[ -n "$current_pkg" && -n "$desc" ]]; then
dpkg_desc["$current_pkg"]="$desc"
fi
current_pkg="${BASH_REMATCH[1]}"
desc=""
elif [[ "$line" =~ ^Description:\ (.+) ]]; then
desc="${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^\ (.+) ]] && [[ -n "$current_pkg" ]] && [[ -n "$desc" ]]; then
# Normalize bullet markers to *
line="${BASH_REMATCH[1]}"
if [[ "$line" =~ ^[-.] ]]; then
line="*${line:1}"
fi
desc+=$'\n'"$line"
elif [[ -z "$line" ]] && [[ -n "$current_pkg" ]] && [[ -n "$desc" ]]; then
dpkg_desc["$current_pkg"]="$desc"
current_pkg=""
desc=""
fi
done < /var/lib/dpkg/status
if [[ -n "$current_pkg" && -n "$desc" ]]; then
dpkg_desc["$current_pkg"]="$desc"
fi
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}"
# Add period to first line if missing
first_line=$(echo "$desc" | head -n 1)
if [[ ! "$first_line" =~ \.$ ]]; then
desc=$(echo -e "$first_line.\n$(echo "$desc" | tail -n +2)")
fi
# Escape newlines for storage
desc=$(echo "$desc" | tr '\n' '\v' | sed 's/\v/\\n/g')
# 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"
#define _GNU_SOURCE
#include <gtk/gtk.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#define MAX_LINE 4096
// 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("/.local/share/vai/scraped_apps.txt") + 1);
sprintf(path, "%s/.local/share/vai/scraped_apps.txt", home);
return path;
}
// Capitalize first letter of the description
char *capitalize_first_letter(const char *desc) {
if (!desc || strlen(desc) == 0) return strdup(desc);
char *result = strdup(desc);
if (islower(result[0])) {
result[0] = toupper(result[0]);
}
return result;
}
// Process description to format bullet points and paragraph separators
char *format_description(const char *desc) {
if (!desc || strlen(desc) == 0) return capitalize_first_letter("No description available");
// Allocate enough space for formatted description
char *result = malloc(strlen(desc) * 2 + MAX_LINE);
char *current = result;
const char *p = desc;
int line_start = 1;
char indent[] = " ";
char bullet[] = "• ";
while (*p) {
if (line_start && (*p == '*' || *p == '.')) {
// Peek ahead to check if this is a bullet point or paragraph separator
const char *next = p + 1;
int spaces = 0;
while (*next == ' ') {
spaces++;
next++;
}
if (*next == '\n' || *next == '\0' || (*next == '\\' && *(next + 1) == 'n')) {
// Paragraph separator: add fat bullet without indent
strcpy(current, bullet);
current += strlen(bullet);
p += 1 + spaces; // Skip marker and spaces
if (*p == '\\' && *(p + 1) == 'n') {
p += 2; // Skip \n
*current++ = '\n'; // Add newline after separator
}
line_start = 1;
} else {
// Bullet point: add indent and bullet
strcpy(current, indent);
current += strlen(indent);
strcpy(current, bullet);
current += strlen(bullet);
p += 1 + spaces; // Skip marker and spaces
line_start = 0;
// Copy the rest of the line
while (*p && !(*p == '\\' && *(p + 1) == 'n') && *p != '\n') {
*current++ = *p++;
}
*current++ = '\n'; // Add newline after bullet point
line_start = 1;
}
} else if (*p == '\\' && *(p + 1) == 'n') {
// Handle explicit newlines
p += 2; // Skip \n
*current++ = '\n';
line_start = 1;
} else {
// Copy character as-is, preserving spaces
*current++ = *p++;
line_start = 0;
}
}
*current = '\0';
// Capitalize first letter
char *capitalized = capitalize_first_letter(result);
free(result);
// Debug output
g_print("Formatted description:\n%s\n---\n", capitalized);
return capitalized;
}
// 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";
// Debug raw description
g_print("Raw description for %s:\n%s\n---\n", name, desc);
// Format description
char *clean_desc = format_description(desc);
if (num_apps >= capacity) {
capacity *= 2;
apps = realloc(apps, capacity * sizeof(AppData));
}
apps[num_apps].name = strdup(name);
apps[num_apps].description = clean_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), 950, 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); // Use WORD wrap for natural text flow
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;
}
[Desktop Entry]
Version=1.0
Type=Application
Terminal=false
Name=Vuu-do App Info
Exec=/home/(username)/.local/share/vai/vai.sh
Icon=edit-find
Name=Vuu-do App Info
Comment=Information about installed applications.