/*
* 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);
}