|
/*
|
|
* DWN - Desktop Window Manager
|
|
* retoor <retoor@molodetz.nl>
|
|
* Application launcher with .desktop file support
|
|
*/
|
|
|
|
#include "applauncher.h"
|
|
#include "config.h"
|
|
#include "util.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <dirent.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <ctype.h>
|
|
#include <pwd.h>
|
|
#include <limits.h>
|
|
|
|
static AppLauncherState launcher_state = {0};
|
|
|
|
static char recent_file_path[PATH_MAX] = {0};
|
|
|
|
static void scan_desktop_dir(const char *dir);
|
|
static int parse_desktop_file(const char *path, AppEntry *entry);
|
|
static void load_recent_apps(void);
|
|
static void save_recent_apps(void);
|
|
static void add_to_recent(const char *desktop_id);
|
|
static char *strip_field_codes(const char *exec);
|
|
static int compare_apps(const void *a, const void *b);
|
|
|
|
|
|
void applauncher_init(void)
|
|
{
|
|
memset(&launcher_state, 0, sizeof(launcher_state));
|
|
|
|
const char *home = getenv("HOME");
|
|
if (home == NULL) {
|
|
struct passwd *pw = getpwuid(getuid());
|
|
home = pw ? pw->pw_dir : "/tmp";
|
|
}
|
|
snprintf(recent_file_path, sizeof(recent_file_path),
|
|
"%s/.local/share/dwn/recent_apps", home);
|
|
|
|
char dir_path[PATH_MAX];
|
|
snprintf(dir_path, sizeof(dir_path), "%s/.local/share/dwn", home);
|
|
mkdir(dir_path, 0755);
|
|
|
|
load_recent_apps();
|
|
|
|
applauncher_refresh();
|
|
|
|
LOG_INFO("App launcher initialized with %d applications", launcher_state.app_count);
|
|
}
|
|
|
|
void applauncher_cleanup(void)
|
|
{
|
|
save_recent_apps();
|
|
}
|
|
|
|
void applauncher_refresh(void)
|
|
{
|
|
launcher_state.app_count = 0;
|
|
|
|
scan_desktop_dir("/usr/share/applications");
|
|
|
|
const char *home = getenv("HOME");
|
|
if (home) {
|
|
char user_apps[PATH_MAX];
|
|
snprintf(user_apps, sizeof(user_apps), "%s/.local/share/applications", home);
|
|
scan_desktop_dir(user_apps);
|
|
}
|
|
|
|
qsort(launcher_state.apps, launcher_state.app_count,
|
|
sizeof(AppEntry), compare_apps);
|
|
|
|
LOG_DEBUG("Refreshed app list: %d applications found", launcher_state.app_count);
|
|
}
|
|
|
|
|
|
static void scan_desktop_dir(const char *dir)
|
|
{
|
|
DIR *d = opendir(dir);
|
|
if (d == NULL) {
|
|
return;
|
|
}
|
|
|
|
struct dirent *entry;
|
|
while ((entry = readdir(d)) != NULL) {
|
|
if (launcher_state.app_count >= MAX_APPS) {
|
|
break;
|
|
}
|
|
|
|
size_t len = strlen(entry->d_name);
|
|
if (len < 9 || strcmp(entry->d_name + len - 8, ".desktop") != 0) {
|
|
continue;
|
|
}
|
|
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/%s", dir, entry->d_name);
|
|
|
|
bool exists = false;
|
|
for (int i = 0; i < launcher_state.app_count; i++) {
|
|
if (strcmp(launcher_state.apps[i].desktop_id, entry->d_name) == 0) {
|
|
exists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (exists) {
|
|
continue;
|
|
}
|
|
|
|
AppEntry app = {0};
|
|
if (parse_desktop_file(path, &app) == 0) {
|
|
snprintf(app.desktop_id, sizeof(app.desktop_id), "%s", entry->d_name);
|
|
launcher_state.apps[launcher_state.app_count++] = app;
|
|
}
|
|
}
|
|
|
|
closedir(d);
|
|
}
|
|
|
|
|
|
static int parse_desktop_file(const char *path, AppEntry *entry)
|
|
{
|
|
FILE *f = fopen(path, "r");
|
|
if (f == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
char line[1024];
|
|
bool in_desktop_entry = false;
|
|
bool has_name = false;
|
|
bool has_exec = false;
|
|
|
|
entry->hidden = false;
|
|
entry->terminal = false;
|
|
|
|
while (fgets(line, sizeof(line), f) != NULL) {
|
|
size_t len = strlen(line);
|
|
while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r' ||
|
|
line[len-1] == ' ' || line[len-1] == '\t')) {
|
|
line[--len] = '\0';
|
|
}
|
|
|
|
if (line[0] == '[') {
|
|
in_desktop_entry = (strcmp(line, "[Desktop Entry]") == 0);
|
|
continue;
|
|
}
|
|
|
|
if (!in_desktop_entry) {
|
|
continue;
|
|
}
|
|
|
|
char *eq = strchr(line, '=');
|
|
if (eq == NULL) {
|
|
continue;
|
|
}
|
|
|
|
*eq = '\0';
|
|
char *key = line;
|
|
char *value = eq + 1;
|
|
|
|
if (strchr(key, '[') != NULL) {
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(key, "Name") == 0 && !has_name) {
|
|
strncpy(entry->name, value, sizeof(entry->name) - 1);
|
|
has_name = true;
|
|
} else if (strcmp(key, "Exec") == 0 && !has_exec) {
|
|
strncpy(entry->exec, value, sizeof(entry->exec) - 1);
|
|
has_exec = true;
|
|
} else if (strcmp(key, "Icon") == 0) {
|
|
strncpy(entry->icon, value, sizeof(entry->icon) - 1);
|
|
} else if (strcmp(key, "Terminal") == 0) {
|
|
entry->terminal = (strcasecmp(value, "true") == 0);
|
|
} else if (strcmp(key, "Type") == 0) {
|
|
if (strcmp(value, "Application") != 0) {
|
|
fclose(f);
|
|
return -1;
|
|
}
|
|
} else if (strcmp(key, "Hidden") == 0 || strcmp(key, "NoDisplay") == 0) {
|
|
if (strcasecmp(value, "true") == 0) {
|
|
entry->hidden = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
|
|
if (!has_name || !has_exec || entry->hidden) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static void load_recent_apps(void)
|
|
{
|
|
launcher_state.recent_count = 0;
|
|
|
|
FILE *f = fopen(recent_file_path, "r");
|
|
if (f == NULL) {
|
|
return;
|
|
}
|
|
|
|
char line[256];
|
|
while (fgets(line, sizeof(line), f) != NULL &&
|
|
launcher_state.recent_count < MAX_RECENT_APPS) {
|
|
size_t len = strlen(line);
|
|
if (len > 0 && line[len-1] == '\n') {
|
|
line[len-1] = '\0';
|
|
}
|
|
if (line[0] != '\0') {
|
|
snprintf(launcher_state.recent[launcher_state.recent_count],
|
|
sizeof(launcher_state.recent[0]), "%s", line);
|
|
launcher_state.recent_count++;
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
LOG_DEBUG("Loaded %d recent apps", launcher_state.recent_count);
|
|
}
|
|
|
|
static void save_recent_apps(void)
|
|
{
|
|
FILE *f = fopen(recent_file_path, "w");
|
|
if (f == NULL) {
|
|
LOG_WARN("Could not save recent apps to %s", recent_file_path);
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < launcher_state.recent_count; i++) {
|
|
fprintf(f, "%s\n", launcher_state.recent[i]);
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
|
|
static void add_to_recent(const char *desktop_id)
|
|
{
|
|
for (int i = 0; i < launcher_state.recent_count; i++) {
|
|
if (strcmp(launcher_state.recent[i], desktop_id) == 0) {
|
|
for (int j = i; j > 0; j--) {
|
|
memcpy(launcher_state.recent[j], launcher_state.recent[j-1],
|
|
sizeof(launcher_state.recent[0]));
|
|
}
|
|
snprintf(launcher_state.recent[0], sizeof(launcher_state.recent[0]),
|
|
"%s", desktop_id);
|
|
save_recent_apps();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (launcher_state.recent_count < MAX_RECENT_APPS) {
|
|
launcher_state.recent_count++;
|
|
}
|
|
|
|
for (int i = launcher_state.recent_count - 1; i > 0; i--) {
|
|
memcpy(launcher_state.recent[i], launcher_state.recent[i-1],
|
|
sizeof(launcher_state.recent[0]));
|
|
}
|
|
snprintf(launcher_state.recent[0], sizeof(launcher_state.recent[0]),
|
|
"%s", desktop_id);
|
|
|
|
save_recent_apps();
|
|
}
|
|
|
|
|
|
static char *strip_field_codes(const char *exec)
|
|
{
|
|
static char result[PATH_MAX];
|
|
size_t j = 0;
|
|
|
|
for (size_t i = 0; exec[i] && j < sizeof(result) - 1; i++) {
|
|
if (exec[i] == '%' && exec[i+1]) {
|
|
char code = exec[i+1];
|
|
if (code == 'f' || code == 'F' || code == 'u' || code == 'U' ||
|
|
code == 'd' || code == 'D' || code == 'n' || code == 'N' ||
|
|
code == 'i' || code == 'c' || code == 'k') {
|
|
i++;
|
|
if (exec[i+1] == ' ') {
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
result[j++] = exec[i];
|
|
}
|
|
result[j] = '\0';
|
|
|
|
while (j > 0 && (result[j-1] == ' ' || result[j-1] == '\t')) {
|
|
result[--j] = '\0';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static int compare_apps(const void *a, const void *b)
|
|
{
|
|
const AppEntry *app_a = (const AppEntry *)a;
|
|
const AppEntry *app_b = (const AppEntry *)b;
|
|
return strcasecmp(app_a->name, app_b->name);
|
|
}
|
|
|
|
static AppEntry *find_app_by_id(const char *desktop_id)
|
|
{
|
|
for (int i = 0; i < launcher_state.app_count; i++) {
|
|
if (strcmp(launcher_state.apps[i].desktop_id, desktop_id) == 0) {
|
|
return &launcher_state.apps[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static AppEntry *find_app_by_name(const char *name)
|
|
{
|
|
for (int i = 0; i < launcher_state.app_count; i++) {
|
|
if (strcmp(launcher_state.apps[i].name, name) == 0) {
|
|
return &launcher_state.apps[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
void applauncher_show(void)
|
|
{
|
|
if (launcher_state.app_count == 0) {
|
|
LOG_WARN("No applications found");
|
|
return;
|
|
}
|
|
|
|
size_t buf_size = launcher_state.app_count * 140;
|
|
char *choices = dwn_malloc(buf_size);
|
|
if (choices == NULL) {
|
|
LOG_ERROR("Failed to allocate memory for app list");
|
|
return;
|
|
}
|
|
choices[0] = '\0';
|
|
|
|
bool *added = dwn_calloc(launcher_state.app_count, sizeof(bool));
|
|
if (added == NULL) {
|
|
dwn_free(choices);
|
|
return;
|
|
}
|
|
|
|
size_t pos = 0;
|
|
|
|
for (int i = 0; i < launcher_state.recent_count; i++) {
|
|
AppEntry *app = find_app_by_id(launcher_state.recent[i]);
|
|
if (app != NULL) {
|
|
for (int j = 0; j < launcher_state.app_count; j++) {
|
|
if (&launcher_state.apps[j] == app) {
|
|
added[j] = true;
|
|
break;
|
|
}
|
|
}
|
|
size_t name_len = strlen(app->name);
|
|
if (pos + name_len + 2 < buf_size) {
|
|
memcpy(choices + pos, app->name, name_len);
|
|
pos += name_len;
|
|
choices[pos++] = '\n';
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < launcher_state.app_count; i++) {
|
|
if (added[i]) {
|
|
continue;
|
|
}
|
|
size_t name_len = strlen(launcher_state.apps[i].name);
|
|
if (pos + name_len + 2 < buf_size) {
|
|
memcpy(choices + pos, launcher_state.apps[i].name, name_len);
|
|
pos += name_len;
|
|
choices[pos++] = '\n';
|
|
}
|
|
}
|
|
|
|
if (pos > 0) {
|
|
choices[pos-1] = '\0';
|
|
}
|
|
|
|
dwn_free(added);
|
|
|
|
char tmp_path[] = "/tmp/dwn_apps_XXXXXX";
|
|
int tmp_fd = mkstemp(tmp_path);
|
|
if (tmp_fd < 0) {
|
|
dwn_free(choices);
|
|
LOG_ERROR("Failed to create temp file for app list");
|
|
return;
|
|
}
|
|
|
|
ssize_t written = write(tmp_fd, choices, strlen(choices));
|
|
close(tmp_fd);
|
|
dwn_free(choices);
|
|
|
|
if (written < 0) {
|
|
unlink(tmp_path);
|
|
LOG_ERROR("Failed to write app list to temp file");
|
|
return;
|
|
}
|
|
|
|
char cmd[1024];
|
|
snprintf(cmd, sizeof(cmd),
|
|
"cat '%s' | dmenu -i -l 10 -p 'Run:'; rm -f '%s'",
|
|
tmp_path, tmp_path);
|
|
|
|
FILE *p = popen(cmd, "r");
|
|
|
|
if (p == NULL) {
|
|
LOG_ERROR("Failed to run dmenu");
|
|
return;
|
|
}
|
|
|
|
char selected[256] = {0};
|
|
if (fgets(selected, sizeof(selected), p) != NULL) {
|
|
size_t len = strlen(selected);
|
|
if (len > 0 && selected[len-1] == '\n') {
|
|
selected[len-1] = '\0';
|
|
}
|
|
}
|
|
pclose(p);
|
|
|
|
if (selected[0] == '\0') {
|
|
return;
|
|
}
|
|
|
|
AppEntry *app = find_app_by_name(selected);
|
|
if (app != NULL) {
|
|
applauncher_launch(app->desktop_id);
|
|
} else {
|
|
spawn_async(selected);
|
|
}
|
|
}
|
|
|
|
void applauncher_launch(const char *desktop_id)
|
|
{
|
|
AppEntry *app = find_app_by_id(desktop_id);
|
|
if (app == NULL) {
|
|
LOG_WARN("App not found: %s", desktop_id);
|
|
return;
|
|
}
|
|
|
|
char *exec_cmd = strip_field_codes(app->exec);
|
|
|
|
char cmd[1024];
|
|
if (app->terminal) {
|
|
const char *terminal = config_get_terminal();
|
|
snprintf(cmd, sizeof(cmd), "%s -e %s", terminal, exec_cmd);
|
|
} else {
|
|
snprintf(cmd, sizeof(cmd), "%s", exec_cmd);
|
|
}
|
|
|
|
LOG_INFO("Launching: %s (%s)", app->name, cmd);
|
|
spawn_async(cmd);
|
|
|
|
add_to_recent(desktop_id);
|
|
}
|