The officially official Devuan Forum!

You are not logged in.

#26 Re: DIY » Working on a new app, the learning curve continues... » 2025-06-29 03:48:14

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.

Board footer

Forum Software