2025-12-28 02:14:31 +00:00
|
|
|
/*
|
|
|
|
|
* DWN - Desktop Window Manager
|
2025-12-28 03:30:10 +00:00
|
|
|
* retoor <retoor@molodetz.nl>
|
2026-02-07 12:04:52 +00:00
|
|
|
* AI Integration implementation - Production Hardened
|
2025-12-28 02:14:31 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "ai.h"
|
|
|
|
|
#include "config.h"
|
|
|
|
|
#include "client.h"
|
|
|
|
|
#include "workspace.h"
|
|
|
|
|
#include "notifications.h"
|
|
|
|
|
#include "util.h"
|
|
|
|
|
#include "cJSON.h"
|
|
|
|
|
|
|
|
|
|
#include <curl/curl.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <stdio.h>
|
2025-12-28 08:26:53 +00:00
|
|
|
#include <pthread.h>
|
|
|
|
|
#include <stdatomic.h>
|
2025-12-28 02:14:31 +00:00
|
|
|
|
|
|
|
|
#define OPENROUTER_URL "https://openrouter.ai/api/v1/chat/completions"
|
|
|
|
|
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Request queue management */
|
|
|
|
|
typedef struct {
|
|
|
|
|
AIRequest *head;
|
|
|
|
|
AIRequest *tail;
|
|
|
|
|
size_t count;
|
|
|
|
|
} AIRequestQueue;
|
|
|
|
|
|
|
|
|
|
static AIRequestQueue ai_queue = {NULL, NULL, 0};
|
|
|
|
|
static ExaRequest *exa_queue_head = NULL;
|
|
|
|
|
static ExaRequest *exa_queue_tail = NULL;
|
|
|
|
|
static size_t exa_queue_count = 0;
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
static CURLM *curl_multi = NULL;
|
|
|
|
|
static AIContext current_context;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Thread synchronization - unified mutex hierarchy to prevent deadlocks */
|
|
|
|
|
static pthread_mutex_t ai_mutex = PTHREAD_MUTEX_INITIALIZER;
|
2025-12-28 08:26:53 +00:00
|
|
|
static atomic_int ai_shutting_down = 0;
|
2025-12-28 02:14:31 +00:00
|
|
|
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Thread-safe buffer for JSON escaping */
|
2025-12-28 02:14:31 +00:00
|
|
|
typedef struct {
|
|
|
|
|
char *data;
|
|
|
|
|
size_t size;
|
2026-02-07 12:04:52 +00:00
|
|
|
size_t capacity;
|
2025-12-28 02:14:31 +00:00
|
|
|
} ResponseBuffer;
|
|
|
|
|
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Safe memory reallocation with overflow checking */
|
|
|
|
|
static void *safe_realloc(void *ptr, size_t new_size, size_t old_size)
|
|
|
|
|
{
|
|
|
|
|
(void)old_size; /* Used for debugging/logging if needed */
|
|
|
|
|
if (new_size == 0) {
|
|
|
|
|
dwn_free(ptr);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
/* Check for overflow in multiplication scenarios */
|
|
|
|
|
if (new_size > (size_t)-1 / 2) {
|
|
|
|
|
LOG_ERROR("Allocation size overflow: %zu", new_size);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
return dwn_realloc(ptr, new_size);
|
|
|
|
|
}
|
2025-12-28 02:14:31 +00:00
|
|
|
|
2026-02-07 12:04:52 +00:00
|
|
|
/* JSON string escaping with proper buffer management */
|
|
|
|
|
static char *escape_json_string(const char *input, size_t *out_len)
|
|
|
|
|
{
|
|
|
|
|
if (input == NULL) {
|
|
|
|
|
if (out_len) *out_len = 0;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t input_len = strlen(input);
|
|
|
|
|
size_t max_escaped_len = input_len * 2 + 1; /* Worst case: every char escaped */
|
|
|
|
|
|
|
|
|
|
char *escaped = dwn_malloc(max_escaped_len);
|
|
|
|
|
if (escaped == NULL) {
|
|
|
|
|
if (out_len) *out_len = 0;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t j = 0;
|
|
|
|
|
for (size_t i = 0; i < input_len && j < max_escaped_len - 1; i++) {
|
|
|
|
|
unsigned char c = (unsigned char)input[i];
|
|
|
|
|
switch (c) {
|
|
|
|
|
case '"':
|
|
|
|
|
if (j + 2 < max_escaped_len) { escaped[j++] = '\\'; escaped[j++] = '"'; }
|
|
|
|
|
break;
|
|
|
|
|
case '\\':
|
|
|
|
|
if (j + 2 < max_escaped_len) { escaped[j++] = '\\'; escaped[j++] = '\\'; }
|
|
|
|
|
break;
|
|
|
|
|
case '\n':
|
|
|
|
|
if (j + 2 < max_escaped_len) { escaped[j++] = '\\'; escaped[j++] = 'n'; }
|
|
|
|
|
break;
|
|
|
|
|
case '\r':
|
|
|
|
|
if (j + 2 < max_escaped_len) { escaped[j++] = '\\'; escaped[j++] = 'r'; }
|
|
|
|
|
break;
|
|
|
|
|
case '\t':
|
|
|
|
|
if (j + 2 < max_escaped_len) { escaped[j++] = '\\'; escaped[j++] = 't'; }
|
|
|
|
|
break;
|
|
|
|
|
case '\b':
|
|
|
|
|
if (j + 2 < max_escaped_len) { escaped[j++] = '\\'; escaped[j++] = 'b'; }
|
|
|
|
|
break;
|
|
|
|
|
case '\f':
|
|
|
|
|
if (j + 2 < max_escaped_len) { escaped[j++] = '\\'; escaped[j++] = 'f'; }
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
if (c < 0x20) {
|
|
|
|
|
/* Control characters - use unicode escape if room */
|
|
|
|
|
if (j + 6 < max_escaped_len) {
|
|
|
|
|
snprintf(&escaped[j], 7, "\\u%04x", c);
|
|
|
|
|
j += 6;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
escaped[j++] = (char)c;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
escaped[j] = '\0';
|
|
|
|
|
|
|
|
|
|
if (out_len) *out_len = j;
|
|
|
|
|
return escaped;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Calculate required buffer size for JSON payload safely */
|
|
|
|
|
static size_t calculate_json_buffer_size(const char *model, const char *escaped_prompt)
|
|
|
|
|
{
|
|
|
|
|
size_t model_len = (model != NULL) ? strlen(model) : 0;
|
|
|
|
|
size_t prompt_len = (escaped_prompt != NULL) ? strlen(escaped_prompt) : 0;
|
|
|
|
|
|
|
|
|
|
/* Base JSON structure size + model + prompt + safety margin */
|
|
|
|
|
size_t base_size = 256; /* {"model":"","messages":[{"role":"user","content":""}]} */
|
|
|
|
|
size_t total = base_size + model_len + prompt_len;
|
|
|
|
|
|
|
|
|
|
/* Overflow check */
|
|
|
|
|
if (total < base_size || total < model_len || total < prompt_len) {
|
|
|
|
|
return 0; /* Overflow detected */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return total;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* CURL write callback with dynamic buffer */
|
2025-12-28 02:14:31 +00:00
|
|
|
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp)
|
|
|
|
|
{
|
|
|
|
|
size_t realsize = size * nmemb;
|
|
|
|
|
ResponseBuffer *buf = (ResponseBuffer *)userp;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
if (buf == NULL) return 0;
|
|
|
|
|
|
|
|
|
|
/* Overflow check for addition */
|
|
|
|
|
if (realsize > (size_t)-1 - buf->size - 1) {
|
|
|
|
|
LOG_ERROR("Response buffer size overflow");
|
2025-12-28 02:14:31 +00:00
|
|
|
return 0;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
size_t new_size = buf->size + realsize + 1;
|
|
|
|
|
char *ptr = safe_realloc(buf->data, new_size, buf->capacity);
|
|
|
|
|
if (ptr == NULL) {
|
|
|
|
|
LOG_ERROR("Failed to grow response buffer");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
buf->data = ptr;
|
2026-02-07 12:04:52 +00:00
|
|
|
memcpy(buf->data + buf->size, contents, realsize);
|
2025-12-28 02:14:31 +00:00
|
|
|
buf->size += realsize;
|
|
|
|
|
buf->data[buf->size] = '\0';
|
2026-02-07 12:04:52 +00:00
|
|
|
buf->capacity = new_size;
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
return realsize;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Remove request from queue - must hold ai_mutex */
|
|
|
|
|
static void ai_queue_remove(AIRequest *req)
|
|
|
|
|
{
|
|
|
|
|
if (req == NULL) return;
|
|
|
|
|
|
|
|
|
|
AIRequest **current = &ai_queue.head;
|
|
|
|
|
while (*current != NULL) {
|
|
|
|
|
if (*current == req) {
|
|
|
|
|
*current = req->next;
|
|
|
|
|
if (ai_queue.tail == req) {
|
|
|
|
|
ai_queue.tail = (req->next == NULL) ?
|
|
|
|
|
(ai_queue.head != NULL ? ai_queue.head : NULL) : NULL;
|
|
|
|
|
/* Find new tail if needed */
|
|
|
|
|
if (ai_queue.head != NULL && ai_queue.tail == NULL) {
|
|
|
|
|
AIRequest *t = ai_queue.head;
|
|
|
|
|
while (t->next != NULL) t = t->next;
|
|
|
|
|
ai_queue.tail = t;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ai_queue.count--;
|
|
|
|
|
req->next = NULL;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
current = &(*current)->next;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Free AI request resources safely */
|
|
|
|
|
static void ai_request_free(AIRequest *req)
|
|
|
|
|
{
|
|
|
|
|
if (req == NULL) return;
|
|
|
|
|
|
|
|
|
|
/* Free headers first */
|
|
|
|
|
if (req->headers != NULL) {
|
|
|
|
|
curl_slist_free_all(req->headers);
|
|
|
|
|
req->headers = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Clean up CURL handle - should already be removed from multi */
|
|
|
|
|
if (req->curl_handle != NULL) {
|
|
|
|
|
curl_easy_cleanup(req->curl_handle);
|
|
|
|
|
req->curl_handle = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Free user data (ResponseBuffer) */
|
|
|
|
|
if (req->user_data != NULL) {
|
|
|
|
|
ResponseBuffer *buf = (ResponseBuffer *)req->user_data;
|
|
|
|
|
if (buf->data != NULL) {
|
|
|
|
|
dwn_free(buf->data);
|
|
|
|
|
buf->data = NULL;
|
|
|
|
|
}
|
|
|
|
|
dwn_free(buf);
|
|
|
|
|
req->user_data = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Free strings */
|
|
|
|
|
if (req->prompt != NULL) {
|
|
|
|
|
dwn_free(req->prompt);
|
|
|
|
|
req->prompt = NULL;
|
|
|
|
|
}
|
|
|
|
|
if (req->response != NULL) {
|
|
|
|
|
dwn_free(req->response);
|
|
|
|
|
req->response = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dwn_free(req);
|
|
|
|
|
}
|
2025-12-28 02:14:31 +00:00
|
|
|
|
|
|
|
|
bool ai_init(void)
|
|
|
|
|
{
|
|
|
|
|
if (dwn == NULL || dwn->config == NULL) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 08:26:53 +00:00
|
|
|
atomic_store(&ai_shutting_down, 0);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (dwn->config->openrouter_api_key[0] == '\0') {
|
|
|
|
|
LOG_INFO("AI features disabled (no OPENROUTER_API_KEY)");
|
|
|
|
|
dwn->ai_enabled = false;
|
2025-12-28 04:01:46 +00:00
|
|
|
return true;
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
pthread_mutex_lock(&ai_mutex);
|
2025-12-28 08:26:53 +00:00
|
|
|
curl_multi = curl_multi_init();
|
2025-12-28 02:14:31 +00:00
|
|
|
if (curl_multi == NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
2025-12-28 02:14:31 +00:00
|
|
|
LOG_ERROR("Failed to initialize curl multi handle");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
dwn->ai_enabled = true;
|
|
|
|
|
LOG_INFO("AI features enabled");
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ai_cleanup(void)
|
|
|
|
|
{
|
2025-12-28 08:26:53 +00:00
|
|
|
atomic_store(&ai_shutting_down, 1);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
pthread_mutex_lock(&ai_mutex);
|
|
|
|
|
|
|
|
|
|
/* Cancel and free all pending AI requests */
|
|
|
|
|
AIRequest *req = ai_queue.head;
|
|
|
|
|
while (req != NULL) {
|
|
|
|
|
AIRequest *next = req->next;
|
|
|
|
|
|
|
|
|
|
/* Remove from curl multi if present */
|
|
|
|
|
if (curl_multi != NULL && req->curl_handle != NULL) {
|
|
|
|
|
curl_multi_remove_handle(curl_multi, req->curl_handle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ai_request_free(req);
|
|
|
|
|
req = next;
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
ai_queue.head = NULL;
|
|
|
|
|
ai_queue.tail = NULL;
|
|
|
|
|
ai_queue.count = 0;
|
|
|
|
|
|
|
|
|
|
/* Cancel and free all pending Exa requests */
|
|
|
|
|
ExaRequest *exa_req = exa_queue_head;
|
|
|
|
|
while (exa_req != NULL) {
|
|
|
|
|
ExaRequest *next = exa_req->next;
|
|
|
|
|
|
|
|
|
|
if (curl_multi != NULL && exa_req->curl_handle != NULL) {
|
|
|
|
|
curl_multi_remove_handle(curl_multi, exa_req->curl_handle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (exa_req->headers != NULL) {
|
|
|
|
|
curl_slist_free_all(exa_req->headers);
|
|
|
|
|
}
|
|
|
|
|
if (exa_req->curl_handle != NULL) {
|
|
|
|
|
curl_easy_cleanup(exa_req->curl_handle);
|
|
|
|
|
}
|
|
|
|
|
if (exa_req->user_data != NULL) {
|
|
|
|
|
ResponseBuffer *buf = (ResponseBuffer *)exa_req->user_data;
|
|
|
|
|
if (buf->data != NULL) dwn_free(buf->data);
|
|
|
|
|
dwn_free(buf);
|
|
|
|
|
}
|
|
|
|
|
if (exa_req->query != NULL) dwn_free(exa_req->query);
|
|
|
|
|
dwn_free(exa_req);
|
|
|
|
|
|
|
|
|
|
exa_req = next;
|
|
|
|
|
}
|
|
|
|
|
exa_queue_head = NULL;
|
|
|
|
|
exa_queue_tail = NULL;
|
|
|
|
|
exa_queue_count = 0;
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (curl_multi != NULL) {
|
|
|
|
|
curl_multi_cleanup(curl_multi);
|
|
|
|
|
curl_multi = NULL;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
curl_global_cleanup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ai_is_available(void)
|
|
|
|
|
{
|
2026-02-07 12:04:52 +00:00
|
|
|
return dwn != NULL && dwn->ai_enabled && !atomic_load(&ai_shutting_down);
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
|
|
|
|
|
{
|
|
|
|
|
if (!ai_is_available() || prompt == NULL) {
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Escape the prompt for JSON */
|
|
|
|
|
size_t escaped_len = 0;
|
|
|
|
|
char *escaped_prompt = escape_json_string(prompt, &escaped_len);
|
|
|
|
|
if (escaped_prompt == NULL) {
|
|
|
|
|
LOG_ERROR("Failed to escape prompt");
|
2025-12-28 08:26:53 +00:00
|
|
|
return NULL;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Calculate safe buffer size including model name */
|
|
|
|
|
size_t json_size = calculate_json_buffer_size(dwn->config->ai_model, escaped_prompt);
|
|
|
|
|
if (json_size == 0) {
|
|
|
|
|
LOG_ERROR("JSON buffer size calculation overflow");
|
|
|
|
|
dwn_free(escaped_prompt);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char *json_payload = dwn_malloc(json_size);
|
|
|
|
|
if (json_payload == NULL) {
|
|
|
|
|
LOG_ERROR("Failed to allocate JSON payload buffer");
|
|
|
|
|
dwn_free(escaped_prompt);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Build JSON payload safely */
|
|
|
|
|
int written = snprintf(json_payload, json_size,
|
|
|
|
|
"{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}]}",
|
|
|
|
|
dwn->config->ai_model, escaped_prompt);
|
|
|
|
|
|
|
|
|
|
dwn_free(escaped_prompt);
|
|
|
|
|
|
|
|
|
|
if (written < 0 || (size_t)written >= json_size) {
|
|
|
|
|
LOG_ERROR("JSON payload truncation detected");
|
|
|
|
|
dwn_free(json_payload);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Allocate request structure */
|
2025-12-28 02:14:31 +00:00
|
|
|
AIRequest *req = dwn_calloc(1, sizeof(AIRequest));
|
2026-02-07 12:04:52 +00:00
|
|
|
if (req == NULL) {
|
|
|
|
|
dwn_free(json_payload);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
req->prompt = dwn_strdup(prompt);
|
|
|
|
|
req->state = AI_STATE_PENDING;
|
|
|
|
|
req->callback = callback;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Allocate response buffer */
|
|
|
|
|
ResponseBuffer *response = dwn_calloc(1, sizeof(ResponseBuffer));
|
|
|
|
|
if (response == NULL) {
|
|
|
|
|
dwn_free(json_payload);
|
2025-12-28 02:14:31 +00:00
|
|
|
dwn_free(req->prompt);
|
|
|
|
|
dwn_free(req);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
req->user_data = response;
|
|
|
|
|
|
|
|
|
|
/* Create CURL handle */
|
|
|
|
|
CURL *easy = curl_easy_init();
|
|
|
|
|
if (easy == NULL) {
|
|
|
|
|
LOG_ERROR("Failed to initialize CURL easy handle");
|
|
|
|
|
dwn_free(json_payload);
|
|
|
|
|
ai_request_free(req);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
req->curl_handle = easy;
|
|
|
|
|
|
|
|
|
|
/* Build headers - store in request for later cleanup */
|
2025-12-28 02:14:31 +00:00
|
|
|
struct curl_slist *headers = NULL;
|
2026-02-07 12:04:52 +00:00
|
|
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
char auth_header[300];
|
|
|
|
|
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s",
|
|
|
|
|
dwn->config->openrouter_api_key);
|
|
|
|
|
headers = curl_slist_append(headers, auth_header);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
req->headers = headers;
|
|
|
|
|
|
|
|
|
|
/* Configure CURL */
|
2025-12-28 02:14:31 +00:00
|
|
|
curl_easy_setopt(easy, CURLOPT_URL, OPENROUTER_URL);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers);
|
2026-02-07 12:04:52 +00:00
|
|
|
curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, json_payload);
|
2025-12-28 02:14:31 +00:00
|
|
|
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_WRITEDATA, response);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 30L);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_PRIVATE, req);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
|
2026-02-07 12:04:52 +00:00
|
|
|
curl_easy_setopt(easy, CURLOPT_NOSIGNAL, 1L); /* Prevent SIGALRM issues */
|
|
|
|
|
|
|
|
|
|
/* Add to queue and multi handle under lock */
|
|
|
|
|
pthread_mutex_lock(&ai_mutex);
|
|
|
|
|
|
2025-12-28 08:26:53 +00:00
|
|
|
if (curl_multi != NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
CURLMcode mc = curl_multi_add_handle(curl_multi, easy);
|
|
|
|
|
if (mc != CURLM_OK) {
|
|
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
|
|
|
|
LOG_ERROR("Failed to add handle to curl multi: %s", curl_multi_strerror(mc));
|
|
|
|
|
dwn_free(json_payload);
|
|
|
|
|
ai_request_free(req);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
|
|
|
|
LOG_ERROR("CURL multi not initialized");
|
|
|
|
|
dwn_free(json_payload);
|
|
|
|
|
ai_request_free(req);
|
|
|
|
|
return NULL;
|
2025-12-28 08:26:53 +00:00
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Add to queue tail for FIFO processing */
|
|
|
|
|
req->next = NULL;
|
|
|
|
|
if (ai_queue.tail != NULL) {
|
|
|
|
|
ai_queue.tail->next = req;
|
|
|
|
|
} else {
|
|
|
|
|
ai_queue.head = req;
|
|
|
|
|
}
|
|
|
|
|
ai_queue.tail = req;
|
|
|
|
|
ai_queue.count++;
|
|
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
|
|
|
|
|
|
|
|
|
/* json_payload is copied by CURL, we can free our copy */
|
|
|
|
|
dwn_free(json_payload);
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
LOG_DEBUG("AI request sent: %.50s...", prompt);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
return req;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ai_cancel_request(AIRequest *req)
|
|
|
|
|
{
|
2026-02-07 12:04:52 +00:00
|
|
|
if (req == NULL) return;
|
|
|
|
|
|
|
|
|
|
pthread_mutex_lock(&ai_mutex);
|
|
|
|
|
|
|
|
|
|
/* Remove from curl multi if still active */
|
|
|
|
|
if (curl_multi != NULL && req->curl_handle != NULL) {
|
|
|
|
|
curl_multi_remove_handle(curl_multi, req->curl_handle);
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Remove from queue */
|
|
|
|
|
ai_queue_remove(req);
|
|
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
|
|
|
|
|
|
|
|
|
/* Mark as cancelled and free */
|
2025-12-28 02:14:31 +00:00
|
|
|
req->state = AI_STATE_ERROR;
|
2026-02-07 12:04:52 +00:00
|
|
|
ai_request_free(req);
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG("AI request cancelled");
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ai_process_pending(void)
|
|
|
|
|
{
|
2025-12-28 08:26:53 +00:00
|
|
|
if (atomic_load(&ai_shutting_down)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
pthread_mutex_lock(&ai_mutex);
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (curl_multi == NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
2025-12-28 02:14:31 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Perform CURL operations */
|
2025-12-28 02:14:31 +00:00
|
|
|
int running_handles;
|
|
|
|
|
curl_multi_perform(curl_multi, &running_handles);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Process completed transfers */
|
2025-12-28 02:14:31 +00:00
|
|
|
CURLMsg *msg;
|
|
|
|
|
int msgs_left;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
while ((msg = curl_multi_info_read(curl_multi, &msgs_left)) != NULL) {
|
|
|
|
|
if (msg->msg == CURLMSG_DONE) {
|
|
|
|
|
CURL *easy = msg->easy_handle;
|
|
|
|
|
AIRequest *req = NULL;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (req != NULL) {
|
|
|
|
|
ResponseBuffer *buf = (ResponseBuffer *)req->user_data;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (msg->data.result == CURLE_OK && buf != NULL && buf->data != NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Parse JSON response */
|
2025-12-28 02:14:31 +00:00
|
|
|
cJSON *root = cJSON_Parse(buf->data);
|
|
|
|
|
if (root != NULL) {
|
|
|
|
|
cJSON *choices = cJSON_GetObjectItemCaseSensitive(root, "choices");
|
|
|
|
|
if (cJSON_IsArray(choices) && cJSON_GetArraySize(choices) > 0) {
|
|
|
|
|
cJSON *first_choice = cJSON_GetArrayItem(choices, 0);
|
|
|
|
|
cJSON *message = cJSON_GetObjectItemCaseSensitive(first_choice, "message");
|
|
|
|
|
if (message != NULL) {
|
|
|
|
|
cJSON *content = cJSON_GetObjectItemCaseSensitive(message, "content");
|
|
|
|
|
if (cJSON_IsString(content) && content->valuestring != NULL) {
|
|
|
|
|
req->response = dwn_strdup(content->valuestring);
|
|
|
|
|
req->state = AI_STATE_COMPLETED;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (req->state != AI_STATE_COMPLETED) {
|
|
|
|
|
cJSON *error = cJSON_GetObjectItemCaseSensitive(root, "error");
|
|
|
|
|
if (error != NULL) {
|
|
|
|
|
cJSON *err_msg = cJSON_GetObjectItemCaseSensitive(error, "message");
|
2026-02-07 12:04:52 +00:00
|
|
|
if (cJSON_IsString(err_msg) && err_msg->valuestring != NULL) {
|
2025-12-28 02:14:31 +00:00
|
|
|
req->response = dwn_strdup(err_msg->valuestring);
|
|
|
|
|
req->state = AI_STATE_ERROR;
|
|
|
|
|
LOG_ERROR("AI API error: %s", err_msg->valuestring);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (req->state != AI_STATE_COMPLETED && req->state != AI_STATE_ERROR) {
|
|
|
|
|
req->response = dwn_strdup(buf->data);
|
|
|
|
|
req->state = AI_STATE_COMPLETED;
|
|
|
|
|
LOG_WARN("Could not parse AI response, returning raw");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
req->state = AI_STATE_ERROR;
|
|
|
|
|
LOG_ERROR("AI request failed: %s", curl_easy_strerror(msg->data.result));
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Remove from curl multi */
|
|
|
|
|
curl_multi_remove_handle(curl_multi, easy);
|
|
|
|
|
req->curl_handle = NULL; /* Mark as removed */
|
|
|
|
|
|
|
|
|
|
/* Invoke callback */
|
2025-12-28 02:14:31 +00:00
|
|
|
if (req->callback != NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Unlock for callback to prevent deadlock */
|
|
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
2025-12-28 02:14:31 +00:00
|
|
|
req->callback(req);
|
2026-02-07 12:04:52 +00:00
|
|
|
pthread_mutex_lock(&ai_mutex);
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Remove from queue and free */
|
|
|
|
|
ai_queue_remove(req);
|
|
|
|
|
ai_request_free(req);
|
|
|
|
|
} else {
|
|
|
|
|
/* Unknown request - just cleanup */
|
|
|
|
|
curl_multi_remove_handle(curl_multi, easy);
|
|
|
|
|
curl_easy_cleanup(easy);
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Context update and analysis functions */
|
2025-12-28 02:14:31 +00:00
|
|
|
void ai_update_context(void)
|
|
|
|
|
{
|
|
|
|
|
memset(¤t_context, 0, sizeof(current_context));
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
Workspace *ws = workspace_get_current();
|
|
|
|
|
if (ws != NULL && ws->focused != NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
strncpy(current_context.focused_window, ws->focused->title,
|
|
|
|
|
sizeof(current_context.focused_window) - 1);
|
|
|
|
|
current_context.focused_window[sizeof(current_context.focused_window) - 1] = '\0';
|
|
|
|
|
strncpy(current_context.focused_class, ws->focused->class,
|
|
|
|
|
sizeof(current_context.focused_class) - 1);
|
|
|
|
|
current_context.focused_class[sizeof(current_context.focused_class) - 1] = '\0';
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
size_t offset = 0;
|
2025-12-28 02:14:31 +00:00
|
|
|
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
|
|
|
|
|
if (c->workspace == (unsigned int)dwn->current_workspace) {
|
|
|
|
|
int written = snprintf(current_context.workspace_windows + offset,
|
|
|
|
|
sizeof(current_context.workspace_windows) - offset,
|
|
|
|
|
"%s%s", offset > 0 ? ", " : "", c->title);
|
2026-02-07 12:04:52 +00:00
|
|
|
if (written > 0 && (size_t)written < sizeof(current_context.workspace_windows) - offset) {
|
|
|
|
|
offset += (size_t)written;
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
current_context.window_count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char *ai_analyze_task(void)
|
|
|
|
|
{
|
|
|
|
|
const char *class = current_context.focused_class;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (strstr(class, "code") || strstr(class, "Code") ||
|
|
|
|
|
strstr(class, "vim") || strstr(class, "emacs")) {
|
|
|
|
|
return "coding";
|
|
|
|
|
}
|
|
|
|
|
if (strstr(class, "firefox") || strstr(class, "chrome") ||
|
|
|
|
|
strstr(class, "Firefox") || strstr(class, "Chrome")) {
|
|
|
|
|
return "browsing";
|
|
|
|
|
}
|
|
|
|
|
if (strstr(class, "slack") || strstr(class, "discord") ||
|
|
|
|
|
strstr(class, "Slack") || strstr(class, "Discord")) {
|
|
|
|
|
return "communication";
|
|
|
|
|
}
|
|
|
|
|
if (strstr(class, "terminal") || strstr(class, "Terminal")) {
|
|
|
|
|
return "terminal";
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
return "general";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char *ai_suggest_window(void)
|
|
|
|
|
{
|
|
|
|
|
const char *task = ai_analyze_task();
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (strcmp(task, "coding") == 0) {
|
|
|
|
|
return "Consider opening a terminal for testing";
|
|
|
|
|
}
|
|
|
|
|
if (strcmp(task, "browsing") == 0) {
|
|
|
|
|
return "Documentation or reference material available?";
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char *ai_suggest_app(void)
|
|
|
|
|
{
|
2025-12-28 04:01:46 +00:00
|
|
|
return NULL;
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Command palette callback */
|
2025-12-28 02:14:31 +00:00
|
|
|
static void ai_command_response_callback(AIRequest *req)
|
|
|
|
|
{
|
2026-02-07 12:04:52 +00:00
|
|
|
if (req == NULL) return;
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (req->state == AI_STATE_COMPLETED && req->response != NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Parse and execute WM commands */
|
2026-01-08 22:58:19 +00:00
|
|
|
char *wm_cmd = strstr(req->response, "[WM_CMD:");
|
|
|
|
|
if (wm_cmd != NULL) {
|
|
|
|
|
char *cmd_start = strchr(wm_cmd, ':');
|
|
|
|
|
if (cmd_start != NULL) {
|
|
|
|
|
cmd_start++;
|
|
|
|
|
while (*cmd_start == ' ') cmd_start++;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2026-01-08 22:58:19 +00:00
|
|
|
char *cmd_end = strchr(cmd_start, ']');
|
|
|
|
|
if (cmd_end != NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
size_t cmd_len = (size_t)(cmd_end - cmd_start);
|
|
|
|
|
if (cmd_len > 0 && cmd_len < 4096) { /* Sanity check */
|
|
|
|
|
char *cmd_json = dwn_malloc(cmd_len + 1);
|
|
|
|
|
if (cmd_json != NULL) {
|
|
|
|
|
memcpy(cmd_json, cmd_start, cmd_len);
|
|
|
|
|
cmd_json[cmd_len] = '\0';
|
|
|
|
|
|
|
|
|
|
LOG_INFO("AI executing WM command: %s", cmd_json);
|
|
|
|
|
notification_show("DWN AI", "Executing WM Command", cmd_json, NULL, 2000);
|
|
|
|
|
|
|
|
|
|
extern void api_handle_json_command(const char *json_str);
|
|
|
|
|
api_handle_json_command(cmd_json);
|
|
|
|
|
|
|
|
|
|
dwn_free(cmd_json);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-08 22:58:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Parse and execute shell commands */
|
2025-12-28 02:14:31 +00:00
|
|
|
char *run_cmd = strstr(req->response, "[RUN:");
|
|
|
|
|
if (run_cmd == NULL) {
|
|
|
|
|
run_cmd = strstr(req->response, "[EXEC:");
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (run_cmd != NULL) {
|
|
|
|
|
char *cmd_start = strchr(run_cmd, ':');
|
|
|
|
|
if (cmd_start != NULL) {
|
|
|
|
|
cmd_start++;
|
|
|
|
|
while (*cmd_start == ' ') cmd_start++;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
char *cmd_end = strchr(cmd_start, ']');
|
|
|
|
|
if (cmd_end != NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
size_t cmd_len = (size_t)(cmd_end - cmd_start);
|
|
|
|
|
if (cmd_len > 0 && cmd_len < 4096) { /* Sanity check */
|
|
|
|
|
char *cmd = dwn_malloc(cmd_len + 1);
|
|
|
|
|
if (cmd != NULL) {
|
|
|
|
|
memcpy(cmd, cmd_start, cmd_len);
|
|
|
|
|
cmd[cmd_len] = '\0';
|
|
|
|
|
|
|
|
|
|
/* Trim trailing whitespace */
|
|
|
|
|
while (cmd_len > 0 && (cmd[cmd_len - 1] == ' ' || cmd[cmd_len - 1] == '\t')) {
|
|
|
|
|
cmd[--cmd_len] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("AI executing command: %s", cmd);
|
|
|
|
|
notification_show("DWN AI", "Running", cmd, NULL, 2000);
|
|
|
|
|
spawn_async(cmd);
|
|
|
|
|
dwn_free(cmd);
|
|
|
|
|
}
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
notification_show("DWN AI", "Response", req->response, NULL, 8000);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
notification_show("DWN AI", "Error", "Failed to get AI response", NULL, 3000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ai_show_command_palette(void)
|
|
|
|
|
{
|
|
|
|
|
if (!ai_is_available()) {
|
|
|
|
|
notification_show("DWN", "AI Unavailable",
|
|
|
|
|
"Set OPENROUTER_API_KEY to enable AI features",
|
|
|
|
|
NULL, 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
char *input = NULL;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (spawn("command -v dmenu >/dev/null 2>&1") == 0) {
|
|
|
|
|
input = spawn_capture("echo '' | dmenu -p 'Ask AI:'");
|
|
|
|
|
} else if (spawn("command -v rofi >/dev/null 2>&1") == 0) {
|
|
|
|
|
input = spawn_capture("rofi -dmenu -p 'Ask AI:'");
|
|
|
|
|
} else {
|
|
|
|
|
notification_show("DWN AI", "Missing Dependency",
|
|
|
|
|
"Install dmenu or rofi for AI command palette:\n"
|
|
|
|
|
"sudo apt install dmenu",
|
|
|
|
|
NULL, 5000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (input == NULL || input[0] == '\0') {
|
|
|
|
|
if (input != NULL) {
|
|
|
|
|
dwn_free(input);
|
|
|
|
|
}
|
|
|
|
|
LOG_DEBUG("AI command palette cancelled");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
LOG_DEBUG("AI command palette input: %s", input);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
notification_show("DWN AI", "Processing...", input, NULL, 2000);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
ai_update_context();
|
|
|
|
|
const char *task = ai_analyze_task();
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Build prompt with bounds checking */
|
|
|
|
|
char prompt[4096];
|
|
|
|
|
const char *prompt_template =
|
|
|
|
|
"You are an AI assistant integrated into a Linux window manager called DWN. "
|
|
|
|
|
"You can execute shell commands for the user.\n\n"
|
|
|
|
|
"IMPORTANT: When the user asks you to run, open, launch, or start an application, "
|
|
|
|
|
"respond with the command in this exact format: [RUN: command]\n"
|
|
|
|
|
"When the user asks for window management tasks (switching workspaces, focusing windows), "
|
|
|
|
|
"respond with the JSON command in this exact format: [WM_CMD: {\"command\": \"...\", ...}]\n"
|
|
|
|
|
"Available WM Commands:\n"
|
|
|
|
|
"- switch_workspace (e.g. [WM_CMD: {\"command\": \"switch_workspace\", \"workspace\": 2}])\n"
|
|
|
|
|
"- focus_client (e.g. [WM_CMD: {\"command\": \"focus_client\", \"window\": <window_id>}])\n"
|
|
|
|
|
"Examples:\n"
|
|
|
|
|
"- User: 'open chrome' -> [RUN: google-chrome]\n"
|
|
|
|
|
"- User: 'switch to workspace 3' -> [WM_CMD: {\"command\": \"switch_workspace\", \"workspace\": 2}]\n"
|
|
|
|
|
"- User: 'go to desktop 1' -> [WM_CMD: {\"command\": \"switch_workspace\", \"workspace\": 0}]\n"
|
|
|
|
|
"For questions or non-command requests, respond briefly (1-2 sentences) without the [RUN:] or [WM_CMD:] format.\n\n"
|
|
|
|
|
"User's current task: %s\n"
|
|
|
|
|
"User's request: %s";
|
|
|
|
|
|
|
|
|
|
int written = snprintf(prompt, sizeof(prompt), prompt_template, task, input);
|
|
|
|
|
if (written < 0 || (size_t)written >= sizeof(prompt)) {
|
|
|
|
|
LOG_ERROR("Prompt too long, truncating");
|
|
|
|
|
prompt[sizeof(prompt) - 1] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
dwn_free(input);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
ai_send_request(prompt, ai_command_response_callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ai_execute_command(const char *command)
|
|
|
|
|
{
|
|
|
|
|
if (!ai_is_available() || command == NULL) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
LOG_DEBUG("AI executing command: %s", command);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
char prompt[512];
|
|
|
|
|
snprintf(prompt, sizeof(prompt),
|
|
|
|
|
"User command: %s\nCurrent task: %s\nRespond with a single action to take.",
|
|
|
|
|
command, ai_analyze_task());
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
ai_send_request(prompt, NULL);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Placeholder functions */
|
2025-12-28 02:14:31 +00:00
|
|
|
void ai_auto_organize_workspace(void)
|
|
|
|
|
{
|
|
|
|
|
LOG_DEBUG("AI auto-organize (placeholder)");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ai_suggest_layout(void)
|
|
|
|
|
{
|
|
|
|
|
LOG_DEBUG("AI layout suggestion (placeholder)");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ai_analyze_workflow(void)
|
|
|
|
|
{
|
|
|
|
|
LOG_DEBUG("AI workflow analysis (placeholder)");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ai_should_show_notification(const char *app, const char *summary)
|
|
|
|
|
{
|
|
|
|
|
(void)app;
|
|
|
|
|
(void)summary;
|
2025-12-28 04:01:46 +00:00
|
|
|
return true;
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int ai_notification_priority(const char *app, const char *summary)
|
|
|
|
|
{
|
|
|
|
|
if (strstr(summary, "urgent") || strstr(summary, "Urgent") ||
|
|
|
|
|
strstr(summary, "error") || strstr(summary, "Error")) {
|
|
|
|
|
return 3;
|
|
|
|
|
}
|
|
|
|
|
if (strstr(app, "slack") || strstr(app, "Slack") ||
|
|
|
|
|
strstr(app, "discord") || strstr(app, "Discord")) {
|
|
|
|
|
return 2;
|
|
|
|
|
}
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ai_monitor_performance(void)
|
|
|
|
|
{
|
|
|
|
|
LOG_DEBUG("AI performance monitoring (placeholder)");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char *ai_performance_suggestion(void)
|
|
|
|
|
{
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Exa search implementation */
|
2025-12-28 02:14:31 +00:00
|
|
|
#define EXA_API_URL "https://api.exa.ai/search"
|
|
|
|
|
|
|
|
|
|
bool exa_is_available(void)
|
|
|
|
|
{
|
|
|
|
|
return dwn != NULL && dwn->config != NULL &&
|
2026-02-07 12:04:52 +00:00
|
|
|
dwn->config->exa_api_key[0] != '\0' && !atomic_load(&ai_shutting_down);
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void exa_parse_response(ExaRequest *req, const char *json)
|
|
|
|
|
{
|
|
|
|
|
if (req == NULL || json == NULL) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
req->result_count = 0;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
cJSON *root = cJSON_Parse(json);
|
|
|
|
|
if (root == NULL) {
|
|
|
|
|
LOG_WARN("Failed to parse Exa response JSON");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
cJSON *results = cJSON_GetObjectItemCaseSensitive(root, "results");
|
|
|
|
|
if (!cJSON_IsArray(results)) {
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
int array_size = cJSON_GetArraySize(results);
|
|
|
|
|
for (int i = 0; i < array_size && req->result_count < 10; i++) {
|
|
|
|
|
cJSON *item = cJSON_GetArrayItem(results, i);
|
|
|
|
|
if (!cJSON_IsObject(item)) continue;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
ExaSearchResult *res = &req->results[req->result_count];
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
cJSON *title = cJSON_GetObjectItemCaseSensitive(item, "title");
|
|
|
|
|
if (cJSON_IsString(title) && title->valuestring != NULL) {
|
|
|
|
|
strncpy(res->title, title->valuestring, sizeof(res->title) - 1);
|
|
|
|
|
res->title[sizeof(res->title) - 1] = '\0';
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
cJSON *url = cJSON_GetObjectItemCaseSensitive(item, "url");
|
|
|
|
|
if (cJSON_IsString(url) && url->valuestring != NULL) {
|
|
|
|
|
strncpy(res->url, url->valuestring, sizeof(res->url) - 1);
|
|
|
|
|
res->url[sizeof(res->url) - 1] = '\0';
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
cJSON *text = cJSON_GetObjectItemCaseSensitive(item, "text");
|
|
|
|
|
if (cJSON_IsString(text) && text->valuestring != NULL) {
|
|
|
|
|
strncpy(res->snippet, text->valuestring, sizeof(res->snippet) - 1);
|
|
|
|
|
res->snippet[sizeof(res->snippet) - 1] = '\0';
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
req->result_count++;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
cJSON_Delete(root);
|
|
|
|
|
LOG_DEBUG("Parsed %d Exa results", req->result_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
|
|
|
|
|
{
|
|
|
|
|
if (!exa_is_available() || query == NULL) {
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Escape query for JSON */
|
|
|
|
|
size_t escaped_len = 0;
|
|
|
|
|
char *escaped = escape_json_string(query, &escaped_len);
|
|
|
|
|
if (escaped == NULL) {
|
2025-12-28 08:26:53 +00:00
|
|
|
return NULL;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Calculate buffer size */
|
|
|
|
|
size_t json_size = escaped_len + 256;
|
|
|
|
|
if (json_size < escaped_len) { /* Overflow check */
|
|
|
|
|
dwn_free(escaped);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char *json_query = dwn_malloc(json_size);
|
|
|
|
|
if (json_query == NULL) {
|
|
|
|
|
dwn_free(escaped);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int written = snprintf(json_query, json_size,
|
|
|
|
|
"{\"query\":\"%s\",\"type\":\"auto\",\"numResults\":10,\"contents\":{\"text\":true}}",
|
|
|
|
|
escaped);
|
|
|
|
|
dwn_free(escaped);
|
|
|
|
|
|
|
|
|
|
if (written < 0 || (size_t)written >= json_size) {
|
|
|
|
|
dwn_free(json_query);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
ExaRequest *req = dwn_calloc(1, sizeof(ExaRequest));
|
2026-02-07 12:04:52 +00:00
|
|
|
if (req == NULL) {
|
|
|
|
|
dwn_free(json_query);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
req->query = dwn_strdup(query);
|
|
|
|
|
req->state = AI_STATE_PENDING;
|
|
|
|
|
req->callback = callback;
|
|
|
|
|
req->result_count = 0;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
ResponseBuffer *response = dwn_calloc(1, sizeof(ResponseBuffer));
|
|
|
|
|
if (response == NULL) {
|
2025-12-28 02:14:31 +00:00
|
|
|
dwn_free(json_query);
|
|
|
|
|
dwn_free(req->query);
|
|
|
|
|
dwn_free(req);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
req->user_data = response;
|
|
|
|
|
|
|
|
|
|
CURL *easy = curl_easy_init();
|
|
|
|
|
if (easy == NULL) {
|
|
|
|
|
dwn_free(json_query);
|
|
|
|
|
dwn_free(req->query);
|
|
|
|
|
dwn_free(response);
|
|
|
|
|
dwn_free(req);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
req->curl_handle = easy;
|
|
|
|
|
|
|
|
|
|
/* Build headers */
|
2025-12-28 02:14:31 +00:00
|
|
|
struct curl_slist *headers = NULL;
|
2026-02-07 12:04:52 +00:00
|
|
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
char api_header[300];
|
|
|
|
|
snprintf(api_header, sizeof(api_header), "x-api-key: %s",
|
|
|
|
|
dwn->config->exa_api_key);
|
|
|
|
|
headers = curl_slist_append(headers, api_header);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
req->headers = headers;
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
curl_easy_setopt(easy, CURLOPT_URL, EXA_API_URL);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_POSTFIELDS, json_query);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_WRITEDATA, response);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 15L);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_PRIVATE, req);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
|
|
|
|
|
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
|
2026-02-07 12:04:52 +00:00
|
|
|
curl_easy_setopt(easy, CURLOPT_NOSIGNAL, 1L);
|
|
|
|
|
|
|
|
|
|
pthread_mutex_lock(&ai_mutex);
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (curl_multi == NULL) {
|
|
|
|
|
curl_multi = curl_multi_init();
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 08:26:53 +00:00
|
|
|
if (curl_multi != NULL) {
|
|
|
|
|
curl_multi_add_handle(curl_multi, easy);
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Add to queue */
|
|
|
|
|
req->next = NULL;
|
|
|
|
|
if (exa_queue_tail != NULL) {
|
|
|
|
|
exa_queue_tail->next = req;
|
|
|
|
|
} else {
|
|
|
|
|
exa_queue_head = req;
|
|
|
|
|
}
|
|
|
|
|
exa_queue_tail = req;
|
|
|
|
|
exa_queue_count++;
|
|
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
|
|
|
|
|
|
|
|
|
dwn_free(json_query);
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
LOG_DEBUG("Exa search sent: %s", query);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
return req;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void exa_process_pending(void)
|
|
|
|
|
{
|
2025-12-28 08:26:53 +00:00
|
|
|
if (atomic_load(&ai_shutting_down)) {
|
2025-12-28 02:14:31 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
pthread_mutex_lock(&ai_mutex);
|
|
|
|
|
|
2025-12-28 08:26:53 +00:00
|
|
|
if (curl_multi == NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
2025-12-28 08:26:53 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
if (exa_queue_head == NULL) {
|
|
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
2025-12-28 08:26:53 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
int running_handles;
|
|
|
|
|
curl_multi_perform(curl_multi, &running_handles);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
CURLMsg *msg;
|
|
|
|
|
int msgs_left;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
while ((msg = curl_multi_info_read(curl_multi, &msgs_left)) != NULL) {
|
|
|
|
|
if (msg->msg == CURLMSG_DONE) {
|
|
|
|
|
CURL *easy = msg->easy_handle;
|
|
|
|
|
ExaRequest *req = NULL;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
if (req != NULL) {
|
2025-12-28 02:14:31 +00:00
|
|
|
ResponseBuffer *buf = (ResponseBuffer *)req->user_data;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (msg->data.result == CURLE_OK && buf != NULL && buf->data != NULL) {
|
|
|
|
|
exa_parse_response(req, buf->data);
|
|
|
|
|
req->state = AI_STATE_COMPLETED;
|
|
|
|
|
} else {
|
|
|
|
|
req->state = AI_STATE_ERROR;
|
|
|
|
|
LOG_ERROR("Exa request failed: %s", curl_easy_strerror(msg->data.result));
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
curl_multi_remove_handle(curl_multi, easy);
|
|
|
|
|
req->curl_handle = NULL;
|
|
|
|
|
|
|
|
|
|
/* Invoke callback */
|
2025-12-28 02:14:31 +00:00
|
|
|
if (req->callback != NULL) {
|
2026-02-07 12:04:52 +00:00
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
2025-12-28 02:14:31 +00:00
|
|
|
req->callback(req);
|
2026-02-07 12:04:52 +00:00
|
|
|
pthread_mutex_lock(&ai_mutex);
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Remove from queue and free */
|
|
|
|
|
ExaRequest **pp = &exa_queue_head;
|
2025-12-28 02:14:31 +00:00
|
|
|
while (*pp != NULL) {
|
|
|
|
|
if (*pp == req) {
|
|
|
|
|
*pp = req->next;
|
2026-02-07 12:04:52 +00:00
|
|
|
if (exa_queue_tail == req) {
|
|
|
|
|
exa_queue_tail = (pp == &exa_queue_head) ? NULL : exa_queue_head;
|
|
|
|
|
if (exa_queue_head != NULL && exa_queue_tail == NULL) {
|
|
|
|
|
ExaRequest *t = exa_queue_head;
|
|
|
|
|
while (t->next != NULL) t = t->next;
|
|
|
|
|
exa_queue_tail = t;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
exa_queue_count--;
|
2025-12-28 02:14:31 +00:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
pp = &(*pp)->next;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Cleanup */
|
|
|
|
|
if (req->headers != NULL) curl_slist_free_all(req->headers);
|
|
|
|
|
if (easy != NULL) curl_easy_cleanup(easy);
|
|
|
|
|
if (req->user_data != NULL) {
|
|
|
|
|
ResponseBuffer *b = (ResponseBuffer *)req->user_data;
|
|
|
|
|
if (b->data != NULL) dwn_free(b->data);
|
|
|
|
|
dwn_free(b);
|
|
|
|
|
}
|
|
|
|
|
if (req->query != NULL) dwn_free(req->query);
|
|
|
|
|
dwn_free(req);
|
|
|
|
|
} else {
|
|
|
|
|
curl_multi_remove_handle(curl_multi, easy);
|
|
|
|
|
curl_easy_cleanup(easy);
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
pthread_mutex_unlock(&ai_mutex);
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-07 12:04:52 +00:00
|
|
|
/* Exa launcher callback and UI */
|
2025-12-28 02:14:31 +00:00
|
|
|
static void exa_launcher_callback(ExaRequest *req)
|
|
|
|
|
{
|
|
|
|
|
if (req == NULL || req->state != AI_STATE_COMPLETED) {
|
|
|
|
|
notification_show("Exa Search", "Error", "Search failed", NULL, 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (req->result_count == 0) {
|
|
|
|
|
notification_show("Exa Search", "No Results", req->query, NULL, 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
size_t choices_size = (size_t)(req->result_count * 300);
|
2025-12-28 02:14:31 +00:00
|
|
|
char *choices = dwn_malloc(choices_size);
|
2026-02-07 12:04:52 +00:00
|
|
|
if (choices == NULL) {
|
|
|
|
|
notification_show("Exa Search", "Error", "Memory allocation failed", NULL, 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
size_t offset = 0;
|
|
|
|
|
choices[0] = '\0';
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
for (int i = 0; i < req->result_count; i++) {
|
2026-02-07 12:04:52 +00:00
|
|
|
int w = snprintf(choices + offset, choices_size - offset,
|
|
|
|
|
"%s%s", offset > 0 ? "\n" : "", req->results[i].title);
|
|
|
|
|
if (w > 0 && (size_t)w < choices_size - offset) {
|
|
|
|
|
offset += (size_t)w;
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
char *escaped_choices = shell_escape(choices);
|
2026-02-07 12:04:52 +00:00
|
|
|
size_t cmd_size = (escaped_choices != NULL) ? strlen(escaped_choices) + 64 : 64;
|
|
|
|
|
char *cmd = dwn_malloc(cmd_size);
|
|
|
|
|
|
|
|
|
|
if (cmd != NULL && escaped_choices != NULL) {
|
|
|
|
|
snprintf(cmd, cmd_size, "echo %s | dmenu -l 10 -p 'Results:'", escaped_choices);
|
|
|
|
|
|
|
|
|
|
char *selected = spawn_capture(cmd);
|
|
|
|
|
|
|
|
|
|
if (selected != NULL && selected[0] != '\0') {
|
|
|
|
|
for (int i = 0; i < req->result_count; i++) {
|
|
|
|
|
size_t title_len = strlen(req->results[i].title);
|
|
|
|
|
if (strncmp(selected, req->results[i].title, title_len) == 0) {
|
|
|
|
|
char *escaped_url = shell_escape(req->results[i].url);
|
|
|
|
|
if (escaped_url != NULL) {
|
|
|
|
|
size_t open_cmd_size = strlen(escaped_url) + 32;
|
|
|
|
|
char *open_cmd = dwn_malloc(open_cmd_size);
|
|
|
|
|
if (open_cmd != NULL) {
|
|
|
|
|
snprintf(open_cmd, open_cmd_size, "xdg-open %s &", escaped_url);
|
|
|
|
|
spawn_async(open_cmd);
|
|
|
|
|
dwn_free(open_cmd);
|
|
|
|
|
}
|
|
|
|
|
dwn_free(escaped_url);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
dwn_free(selected);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
dwn_free(cmd);
|
|
|
|
|
dwn_free(escaped_choices);
|
|
|
|
|
dwn_free(choices);
|
2026-02-07 12:04:52 +00:00
|
|
|
|
|
|
|
|
/* Cleanup request */
|
|
|
|
|
if (req->query != NULL) dwn_free(req->query);
|
|
|
|
|
if (req->user_data != NULL) {
|
|
|
|
|
ResponseBuffer *b = (ResponseBuffer *)req->user_data;
|
|
|
|
|
if (b->data != NULL) dwn_free(b->data);
|
|
|
|
|
dwn_free(b);
|
|
|
|
|
}
|
|
|
|
|
if (req->headers != NULL) curl_slist_free_all(req->headers);
|
2025-12-28 02:14:31 +00:00
|
|
|
dwn_free(req);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void exa_show_app_launcher(void)
|
|
|
|
|
{
|
|
|
|
|
if (!exa_is_available()) {
|
|
|
|
|
notification_show("Exa", "Unavailable",
|
|
|
|
|
"Set EXA_API_KEY in config to enable semantic search",
|
|
|
|
|
NULL, 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
char *query = NULL;
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (spawn("command -v dmenu >/dev/null 2>&1") == 0) {
|
|
|
|
|
query = spawn_capture("echo '' | dmenu -p 'Exa Search:'");
|
|
|
|
|
} else if (spawn("command -v rofi >/dev/null 2>&1") == 0) {
|
|
|
|
|
query = spawn_capture("rofi -dmenu -p 'Exa Search:'");
|
|
|
|
|
} else {
|
|
|
|
|
notification_show("Exa", "Missing Dependency",
|
|
|
|
|
"Install dmenu or rofi", NULL, 3000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
if (query == NULL || query[0] == '\0') {
|
|
|
|
|
if (query != NULL) dwn_free(query);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
query[strcspn(query, "\n")] = '\0';
|
2026-02-07 12:04:52 +00:00
|
|
|
|
2025-12-28 02:14:31 +00:00
|
|
|
notification_show("Exa", "Searching...", query, NULL, 2000);
|
|
|
|
|
exa_search(query, exa_launcher_callback);
|
2026-02-07 12:04:52 +00:00
|
|
|
dwn_free(query);
|
2025-12-28 02:14:31 +00:00
|
|
|
}
|