From cf197c5bf2d8e741e76d92fd98efaf933863121b Mon Sep 17 00:00:00 2001 From: cheerfulScumbag <164391367+cheerfulScumbag@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:26:25 +0000 Subject: [PATCH] Add opt-in session save and restore support --- assets/config.conf | 11 + meson.build | 1 + src/config/parse_config.h | 89 +++++++ src/mango.c | 258 ++++++++++++++++++++ src/session/session.c | 489 ++++++++++++++++++++++++++++++++++++++ src/session/session.h | 39 +++ 6 files changed, 887 insertions(+) create mode 100644 src/session/session.c create mode 100644 src/session/session.h diff --git a/assets/config.conf b/assets/config.conf index 15b654c1..22708cff 100644 --- a/assets/config.conf +++ b/assets/config.conf @@ -78,6 +78,17 @@ no_border_when_single=0 axis_bind_apply_timeout=100 focus_on_activate=1 idleinhibit_ignore_visible=0 +# Restore and save session state across compositor restarts. +# Disabled by default to preserve Mango's current behavior. +# Minimized restore is currently unsupported. +# Restore expects target outputs to already exist before clients map. +session_restore=0 +# Relaunch mapping used when Mango cannot infer a launch command from a client. +# Format: session_launch=app_id|command +# or: session_launch=app_id|title|command +# session_launch=foot|foot +# session_launch=foot|gamma|foot -a foot -T gamma -e sh -lc "sleep 600" +# session_launch=org.kde.dolphin|dolphin . sloppyfocus=1 warpcursor=1 focus_cross_monitor=0 diff --git a/meson.build b/meson.build index 5d09d536..ca79fa3b 100644 --- a/meson.build +++ b/meson.build @@ -97,6 +97,7 @@ endif executable('mango', 'src/mango.c', 'src/common/util.c', + 'src/session/session.c', 'src/ext-protocol/wlr_ext_workspace_v1.c', wayland_sources, dependencies : [ diff --git a/src/config/parse_config.h b/src/config/parse_config.h index fda401d9..c1359ac9 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -52,6 +52,12 @@ typedef struct { char *value; } ConfigEnv; +typedef struct { + char app_id[256]; + char title[512]; + char command[1024]; +} ConfigSessionLaunchRule; + typedef struct { const char *id; const char *title; @@ -251,6 +257,7 @@ typedef struct { uint32_t axis_bind_apply_timeout; uint32_t focus_on_activate; + int32_t session_restore; int32_t idleinhibit_ignore_visible; int32_t sloppyfocus; int32_t warpcursor; @@ -352,6 +359,8 @@ typedef struct { char **exec_once; int32_t exec_once_count; + ConfigSessionLaunchRule *session_launch_rules; + int32_t session_launch_rules_count; char *cursor_theme; uint32_t cursor_size; @@ -1426,6 +1435,71 @@ bool parse_option(Config *config, char *key, char *value) { config->allow_shortcuts_inhibit = atoi(value); } else if (strcmp(key, "allow_lock_transparent") == 0) { config->allow_lock_transparent = atoi(value); + } else if (strcmp(key, "session_restore") == 0) { + config->session_restore = atoi(value); + } else if (strcmp(key, "session_launch") == 0) { + ConfigSessionLaunchRule rule = {0}; + ConfigSessionLaunchRule *new_rules = NULL; + char *first_sep = strchr(value, '|'); + char *second_sep = NULL; + size_t app_len, title_len = 0, cmd_len; + + if (!first_sep) { + fprintf(stderr, + "\033[1m\033[31m[ERROR]:\033[33m Invalid session_launch " + "format. Expected app_id|command or " + "app_id|title|command\033[0m\n"); + return false; + } + + second_sep = strchr(first_sep + 1, '|'); + app_len = (size_t)(first_sep - value); + if (second_sep) { + title_len = (size_t)(second_sep - (first_sep + 1)); + cmd_len = strlen(second_sep + 1); + } else { + cmd_len = strlen(first_sep + 1); + } + + if (app_len == 0 || cmd_len == 0 || app_len >= sizeof(rule.app_id) || + cmd_len >= sizeof(rule.command) || + title_len >= sizeof(rule.title)) { + fprintf(stderr, + "\033[1m\033[31m[ERROR]:\033[33m Invalid session_launch " + "entry length\033[0m\n"); + return false; + } + + memcpy(rule.app_id, value, app_len); + rule.app_id[app_len] = '\0'; + if (second_sep) { + memcpy(rule.title, first_sep + 1, title_len); + rule.title[title_len] = '\0'; + memcpy(rule.command, second_sep + 1, cmd_len + 1); + } else { + memcpy(rule.command, first_sep + 1, cmd_len + 1); + } + trim_whitespace(rule.app_id); + trim_whitespace(rule.title); + trim_whitespace(rule.command); + if (rule.app_id[0] == '\0' || rule.command[0] == '\0') { + fprintf(stderr, + "\033[1m\033[31m[ERROR]:\033[33m session_launch requires " + "both app_id and command\033[0m\n"); + return false; + } + + new_rules = realloc(config->session_launch_rules, + sizeof(*config->session_launch_rules) * + (config->session_launch_rules_count + 1)); + if (!new_rules) { + fprintf(stderr, + "\033[1m\033[31m[ERROR]:\033[33m Failed to allocate " + "session_launch rules\033[0m\n"); + return false; + } + config->session_launch_rules = new_rules; + config->session_launch_rules[config->session_launch_rules_count++] = rule; } else if (strcmp(key, "no_border_when_single") == 0) { config->no_border_when_single = atoi(value); } else if (strcmp(key, "no_radius_when_single") == 0) { @@ -2826,6 +2900,14 @@ void free_circle_layout(Config *config) { config->circle_layout_count = 0; // 重置计数 } +void free_session_launch_rules(Config *config) { + if (config->session_launch_rules) { + free(config->session_launch_rules); + config->session_launch_rules = NULL; + } + config->session_launch_rules_count = 0; +} + void free_baked_points(void) { if (baked_points_move) { free(baked_points_move); @@ -3096,6 +3178,7 @@ void free_config(void) { // 释放 circle_layout free_circle_layout(&config); + free_session_launch_rules(&config); // 释放动画资源 free_baked_points(); @@ -3171,6 +3254,7 @@ void override_config(void) { config.axis_bind_apply_timeout = CLAMP_INT(config.axis_bind_apply_timeout, 0, 1000); config.focus_on_activate = CLAMP_INT(config.focus_on_activate, 0, 1); + config.session_restore = CLAMP_INT(config.session_restore, 0, 1); config.idleinhibit_ignore_visible = CLAMP_INT(config.idleinhibit_ignore_visible, 0, 1); config.sloppyfocus = CLAMP_INT(config.sloppyfocus, 0, 1); @@ -3275,6 +3359,9 @@ void set_value_default() { config.axis_bind_apply_timeout = 100; config.focus_on_activate = 1; + config.session_restore = 0; + config.session_launch_rules = NULL; + config.session_launch_rules_count = 0; config.new_is_master = 1; config.default_mfact = 0.55f; config.default_nmaster = 1; @@ -3511,6 +3598,8 @@ bool parse_config(void) { config.exec_count = 0; config.exec_once = NULL; config.exec_once_count = 0; + config.session_launch_rules = NULL; + config.session_launch_rules_count = 0; config.scroller_proportion_preset = NULL; config.scroller_proportion_preset_count = 0; config.circle_layout = NULL; diff --git a/src/mango.c b/src/mango.c index 93c049f4..16830e51 100644 --- a/src/mango.c +++ b/src/mango.c @@ -407,6 +407,7 @@ struct Client { float focused_opacity; float unfocused_opacity; char oldmonname[128]; + char session_launch_command[1024]; int32_t noblur; double master_mfact_per, master_inner_per, stack_inner_per; double old_master_mfact_per, old_master_inner_per, old_stack_inner_per; @@ -941,6 +942,7 @@ struct Pertag { }; #include "config/parse_config.h" +#include "session/session.h" static struct wl_signal mango_print_status; @@ -1011,6 +1013,257 @@ static struct wl_event_source *sync_keymap; #include "layout/horizontal.h" #include "layout/vertical.h" +static void session_write_json_string(FILE *out, const char *str) { + const unsigned char *p = (const unsigned char *)(str ? str : ""); + + fputc('"', out); + for (; *p != '\0'; ++p) { + switch (*p) { + case '\\': + fputs("\\\\", out); + break; + case '"': + fputs("\\\"", out); + break; + case '\n': + fputs("\\n", out); + break; + case '\r': + fputs("\\r", out); + break; + case '\t': + fputs("\\t", out); + break; + default: + if (*p < 0x20) + fprintf(out, "\\u%04x", *p); + else + fputc(*p, out); + } + } + fputc('"', out); +} + +const char *mango_session_client_appid(Client *c) { return client_get_appid(c); } + +const char *mango_session_client_title(Client *c) { return client_get_title(c); } + +const char *mango_session_client_monitor(Client *c) { + return (c && c->mon && c->mon->wlr_output) ? c->mon->wlr_output->name : ""; +} + +const char *mango_session_lookup_launch_command(const char *app_id, + const char *title) { + if (!app_id || app_id[0] == '\0') + return ""; + + for (int32_t i = 0; i < config.session_launch_rules_count; ++i) { + ConfigSessionLaunchRule *rule = &config.session_launch_rules[i]; + if (strcmp(rule->app_id, app_id) != 0) + continue; + if (rule->title[0] == '\0') + continue; + if (title && strcmp(rule->title, title) == 0) + return rule->command; + } + + for (int32_t i = 0; i < config.session_launch_rules_count; ++i) { + ConfigSessionLaunchRule *rule = &config.session_launch_rules[i]; + if (strcmp(rule->app_id, app_id) == 0 && rule->title[0] == '\0') + return rule->command; + } + + return ""; +} + +void mango_session_remember_client_launch_command(Client *c, const char *command) { + if (!c) + return; + + memset(c->session_launch_command, 0, sizeof(c->session_launch_command)); + if (!command || command[0] == '\0') + return; + + strncpy(c->session_launch_command, command, + sizeof(c->session_launch_command) - 1); + c->session_launch_command[sizeof(c->session_launch_command) - 1] = '\0'; +} + +void mango_session_spawn_command(const char *command) { + if (!command || command[0] == '\0') + return; + + if (fork() == 0) { + signal(SIGSEGV, SIG_IGN); + signal(SIGABRT, SIG_IGN); + signal(SIGILL, SIG_IGN); + + dup2(STDERR_FILENO, STDOUT_FILENO); + setsid(); + + execlp("sh", "sh", "-c", command, (char *)NULL); + execlp("bash", "bash", "-c", command, (char *)NULL); + _exit(EXIT_FAILURE); + } +} + +static bool session_client_should_save(Client *c) { + const char *appid; + + if (!c || c->iskilling || !c->mon || c->tags == 0) + return false; + if (client_is_unmanaged(c) || client_is_x11_popup(c)) + return false; + if (client_get_parent(c) != NULL) + return false; + if (c->is_in_scratchpad || c->is_scratchpad_show || c->isnamedscratchpad) + return false; + if (c->swallowing || c->swallowedby) + return false; + + appid = client_get_appid(c); + return appid && appid[0] != '\0' && strcmp(appid, "broken") != 0; +} + +int32_t mango_session_is_config_enabled(void) { return config.session_restore; } + +static const char *session_client_launch_command(Client *c) { + const char *appid; + const char *title; + const char *mapped_command; + + if (c && c->session_launch_command[0] != '\0') + return c->session_launch_command; + + appid = client_get_appid(c); + + if (!appid || appid[0] == '\0' || strcmp(appid, "broken") == 0) + return ""; + + title = client_get_title(c); + mapped_command = mango_session_lookup_launch_command(appid, title); + if (mapped_command && mapped_command[0] != '\0') + return mapped_command; + + /* First relaunch pass: use app_id as the best-effort launch command. + * Explicit config mappings will override this for wrappers and PWAs. */ + return appid; +} + +int32_t mango_session_write_snapshot(FILE *out) { + Client *c; + int32_t count = 0; + + fputs("[\n", out); + wl_list_for_each(c, &clients, link) { + const char *monitor_name; + const char *appid; + const char *launch_command; + const char *title; + const char *separator; + + if (!session_client_should_save(c)) + continue; + + monitor_name = + (c->mon && c->mon->wlr_output) ? c->mon->wlr_output->name : ""; + appid = client_get_appid(c); + launch_command = session_client_launch_command(c); + title = client_get_title(c); + separator = count == 0 ? "" : ",\n"; + + fputs(separator, out); + fputs(" {\n", out); + fputs(" \"app_id\": ", out); + session_write_json_string(out, appid); + fputs(",\n \"title\": ", out); + session_write_json_string(out, title); + fprintf(out, + ",\n \"pid\": %d,\n \"monitor\": ", + client_get_pid(c)); + session_write_json_string(out, monitor_name); + fputs(",\n \"launch_command\": ", out); + session_write_json_string(out, launch_command); + fprintf(out, + ",\n \"tags\": %u,\n \"is_floating\": %d,\n " + "\"is_fullscreen\": %d,\n \"is_minimized\": %d,\n " + "\"geom\": {\"x\": %d, \"y\": %d, \"width\": %d, \"height\": %d}," + "\n \"float_geom\": {\"x\": %d, \"y\": %d, \"width\": %d, " + "\"height\": %d}\n }", + c->tags, c->isfloating, c->isfullscreen, c->isminimized, c->geom.x, + c->geom.y, c->geom.width, c->geom.height, c->float_geom.x, + c->float_geom.y, c->float_geom.width, c->float_geom.height); + count++; + } + fputs("\n]\n", out); + + return count; +} + +void mango_session_apply_restore_entry(Client *c, + const SessionRestoreEntry *entry) { + Monitor *target = NULL, *m = NULL; + uint32_t tags; + + if (!c || !entry || !c->mon) + return; + + tags = entry->tags; + if (tags == 0) + tags = c->tags ? c->tags : c->mon->tagset[c->mon->seltags]; + + if (entry->monitor[0] != '\0') { + wl_list_for_each(m, &mons, link) { + if (!m->wlr_output->enabled) + continue; + if (strcmp(m->wlr_output->name, entry->monitor) == 0) { + target = m; + break; + } + } + } + + if (target && target != c->mon) { + setmon(c, target, tags, false); + } else { + c->tags = tags; + } + + arrange(c->mon, false, false); + + if (entry->is_floating) { + c->float_geom = (struct wlr_box){ + .x = entry->float_geom.x, + .y = entry->float_geom.y, + .width = entry->float_geom.width, + .height = entry->float_geom.height, + }; + if (c->float_geom.width <= 0 || c->float_geom.height <= 0) { + c->float_geom = (struct wlr_box){ + .x = entry->geom.x, + .y = entry->geom.y, + .width = entry->geom.width, + .height = entry->geom.height, + }; + } + c->geom = c->float_geom; + setfloating(c, 1); + resize(c, c->geom, 0); + } else if (c->isfloating) { + setfloating(c, 0); + } + + if (entry->is_fullscreen && !c->isfullscreen) { + setfullscreen(c, 1); + } else if (!entry->is_fullscreen && c->isfullscreen) { + setfullscreen(c, 0); + } + + /* Skip minimized restore for now to avoid introducing new focus changes. */ + client_update_oldmonname_record(c, c->mon); + printstatus(); +} + void client_change_mon(Client *c, Monitor *m) { setmon(c, m, c->tags, true); if (c->isfloating) { @@ -2264,6 +2517,8 @@ void cleanuplisteners(void) { } void cleanup(void) { + session_save_now(true); + session_shutdown(); cleanuplisteners(); #ifdef XWAYLAND wlr_xwayland_destroy(xwayland); @@ -4208,6 +4463,7 @@ mapnotify(struct wl_listener *listener, void *data) { // make sure the animation is open type c->is_pending_open_animation = true; resize(c, c->geom, 0); + session_handle_client_mapped(c); printstatus(); } @@ -4978,6 +5234,7 @@ run(char *startup_cmd) { run_exec(); run_exec_once(); + session_maybe_restore_startup(); /* Run the Wayland event loop. This does not return until you exit the * compositor. Starting the backend rigged up all of the necessary event @@ -5494,6 +5751,7 @@ void setup(void) { setenv("_JAVA_AWT_WM_NONREPARENTING", "1", 1); parse_config(); + session_init(); if (cli_debug_log) { config.log_level = WLR_DEBUG; } diff --git a/src/session/session.c b/src/session/session.c new file mode 100644 index 00000000..596349f2 --- /dev/null +++ b/src/session/session.c @@ -0,0 +1,489 @@ +#include "session.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/util.h" + +extern int32_t mango_session_is_config_enabled(void); +extern int32_t mango_session_write_snapshot(FILE *out); +extern const char *mango_session_client_appid(Client *c); +extern const char *mango_session_client_title(Client *c); +extern const char *mango_session_client_monitor(Client *c); +extern const char *mango_session_lookup_launch_command(const char *app_id, + const char *title); +extern void mango_session_remember_client_launch_command(Client *c, + const char *command); +extern void mango_session_spawn_command(const char *command); +extern void mango_session_apply_restore_entry(Client *c, + const SessionRestoreEntry *entry); + +typedef struct { + SessionRestoreEntry entry; + bool used; +} PendingSessionEntry; + +static PendingSessionEntry *pending_entries; +static size_t pending_count; +static bool restore_started; + +static bool mkdir_p(const char *dir) { + char tmp[PATH_MAX]; + size_t len; + + if (!dir || dir[0] == '\0') + return false; + + len = strlen(dir); + if (len >= sizeof(tmp)) + return false; + + memcpy(tmp, dir, len + 1); + + for (char *p = tmp + 1; *p != '\0'; ++p) { + if (*p != '/') + continue; + *p = '\0'; + if (mkdir(tmp, 0755) < 0 && errno != EEXIST) + return false; + *p = '/'; + } + + return mkdir(tmp, 0755) == 0 || errno == EEXIST; +} + +static char *session_data_dir(void) { + const char *xdg_data = getenv("XDG_DATA_HOME"); + if (xdg_data && xdg_data[0] != '\0') + return string_printf("%s/mango", xdg_data); + + const char *home = getenv("HOME"); + if (!home || home[0] == '\0') + return NULL; + + return string_printf("%s/.local/share/mango", home); +} + +static char *session_file_path(void) { + char *dir = session_data_dir(); + char *path; + if (!dir) + return NULL; + path = string_printf("%s/session.json", dir); + free(dir); + return path; +} + +static void free_pending_entries(void) { + free(pending_entries); + pending_entries = NULL; + pending_count = 0; +} + +static const char *skip_ws(const char *p) { + while (p && (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')) + ++p; + return p; +} + +static const char *find_matching_brace(const char *start) { + int depth = 0; + bool in_string = false; + bool escaped = false; + + for (const char *p = start; p && *p != '\0'; ++p) { + if (in_string) { + if (escaped) { + escaped = false; + } else if (*p == '\\') { + escaped = true; + } else if (*p == '"') { + in_string = false; + } + continue; + } + + if (*p == '"') { + in_string = true; + } else if (*p == '{') { + depth++; + } else if (*p == '}') { + depth--; + if (depth == 0) + return p; + } + } + + return NULL; +} + +static bool parse_json_string_value(const char *value, char *dest, size_t dest_size) { + size_t i = 0; + const char *p = skip_ws(value); + + if (!p || *p != '"' || dest_size == 0) + return false; + + ++p; + while (*p != '\0' && *p != '"' && i + 1 < dest_size) { + if (*p == '\\') { + ++p; + if (*p == '\0') + break; + switch (*p) { + case 'n': + dest[i++] = '\n'; + break; + case 'r': + dest[i++] = '\r'; + break; + case 't': + dest[i++] = '\t'; + break; + case '\\': + case '"': + case '/': + dest[i++] = *p; + break; + default: + dest[i++] = *p; + break; + } + ++p; + continue; + } + dest[i++] = *p++; + } + + dest[i] = '\0'; + return true; +} + +static bool extract_json_string(const char *obj, const char *key, char *dest, + size_t dest_size) { + const char *p = strstr(obj, key); + if (!p) + return false; + p = strchr(p, ':'); + if (!p) + return false; + return parse_json_string_value(p + 1, dest, dest_size); +} + +static bool extract_json_int(const char *obj, const char *key, int32_t *out) { + char *end = NULL; + const char *p = strstr(obj, key); + long value; + + if (!p) + return false; + p = strchr(p, ':'); + if (!p) + return false; + p = skip_ws(p + 1); + if (!p) + return false; + + value = strtol(p, &end, 10); + if (end == p) + return false; + + *out = (int32_t)value; + return true; +} + +static bool extract_json_object_range(const char *obj, const char *key, + const char **start, const char **end) { + const char *p = strstr(obj, key); + if (!p) + return false; + p = strchr(p, ':'); + if (!p) + return false; + p = skip_ws(p + 1); + if (!p || *p != '{') + return false; + *start = p; + *end = find_matching_brace(p); + return *end != NULL; +} + +static bool parse_geom_object(const char *obj, const char *key, SessionRect *geom) { + const char *start = NULL, *end = NULL; + char *sub = NULL; + bool ok; + size_t len; + + if (!extract_json_object_range(obj, key, &start, &end)) + return false; + + len = (size_t)(end - start + 1); + sub = ecalloc(len + 1, 1); + memcpy(sub, start, len); + + ok = extract_json_int(sub, "\"x\"", &geom->x) && + extract_json_int(sub, "\"y\"", &geom->y) && + extract_json_int(sub, "\"width\"", &geom->width) && + extract_json_int(sub, "\"height\"", &geom->height); + + free(sub); + return ok; +} + +static bool parse_session_entry(const char *obj, SessionRestoreEntry *entry) { + int32_t tags = 0; + + memset(entry, 0, sizeof(*entry)); + + if (!extract_json_string(obj, "\"app_id\"", entry->app_id, + sizeof(entry->app_id))) + return false; + + extract_json_string(obj, "\"title\"", entry->title, sizeof(entry->title)); + extract_json_string(obj, "\"monitor\"", entry->monitor, + sizeof(entry->monitor)); + extract_json_string(obj, "\"launch_command\"", entry->launch_command, + sizeof(entry->launch_command)); + extract_json_int(obj, "\"pid\"", &entry->pid); + extract_json_int(obj, "\"tags\"", &tags); + entry->tags = (uint32_t)tags; + extract_json_int(obj, "\"is_floating\"", &entry->is_floating); + extract_json_int(obj, "\"is_fullscreen\"", &entry->is_fullscreen); + extract_json_int(obj, "\"is_minimized\"", &entry->is_minimized); + parse_geom_object(obj, "\"geom\"", &entry->geom); + parse_geom_object(obj, "\"float_geom\"", &entry->float_geom); + + return true; +} + +static bool load_pending_entries(void) { + char *path = session_file_path(); + char *contents = NULL; + FILE *in = NULL; + const char *cursor; + bool loaded = false; + + free_pending_entries(); + if (!path) + return false; + + in = fopen(path, "r"); + if (!in) + goto cleanup; + + if (fseek(in, 0, SEEK_END) != 0) + goto cleanup; + long size = ftell(in); + if (size < 0 || fseek(in, 0, SEEK_SET) != 0) + goto cleanup; + + contents = ecalloc((size_t)size + 1, 1); + if (fread(contents, 1, (size_t)size, in) != (size_t)size) + goto cleanup; + contents[size] = '\0'; + + cursor = contents; + while ((cursor = strchr(cursor, '{')) != NULL) { + const char *end = find_matching_brace(cursor); + SessionRestoreEntry entry; + char *obj; + size_t len; + + if (!end) + break; + + len = (size_t)(end - cursor + 1); + obj = ecalloc(len + 1, 1); + memcpy(obj, cursor, len); + + if (parse_session_entry(obj, &entry)) { + PendingSessionEntry *new_entries = realloc( + pending_entries, sizeof(*pending_entries) * (pending_count + 1)); + if (!new_entries) { + free(obj); + goto cleanup; + } + pending_entries = new_entries; + pending_entries[pending_count].entry = entry; + pending_entries[pending_count].used = false; + pending_count++; + loaded = true; + } + + free(obj); + cursor = end + 1; + } + +cleanup: + if (in) + fclose(in); + free(contents); + free(path); + return loaded; +} + +static PendingSessionEntry *find_pending_match(const char *appid, const char *title, + const char *monitor) { + PendingSessionEntry *fallback = NULL; + PendingSessionEntry *monitor_match = NULL; + + for (size_t i = 0; i < pending_count; ++i) { + PendingSessionEntry *candidate = &pending_entries[i]; + if (candidate->used) + continue; + if (strcmp(candidate->entry.app_id, appid) != 0) + continue; + + if (title && title[0] != '\0' && + strcmp(candidate->entry.title, title) == 0) { + if (monitor && monitor[0] != '\0' && + strcmp(candidate->entry.monitor, monitor) == 0) + return candidate; + if (!fallback) + fallback = candidate; + } + + if (!monitor_match && monitor && monitor[0] != '\0' && + strcmp(candidate->entry.monitor, monitor) == 0) { + monitor_match = candidate; + } + + if (!fallback) + fallback = candidate; + } + + return monitor_match ? monitor_match : fallback; +} + +static bool session_resolve_launch_command(SessionRestoreEntry *entry) { + const char *mapped_command; + + if (!entry || entry->app_id[0] == '\0') + return false; + + /* Explicit user mapping wins over best-effort persisted launch data. */ + mapped_command = mango_session_lookup_launch_command(entry->app_id, + entry->title); + if (mapped_command && mapped_command[0] != '\0') { + strncpy(entry->launch_command, mapped_command, + sizeof(entry->launch_command) - 1); + entry->launch_command[sizeof(entry->launch_command) - 1] = '\0'; + return true; + } + + return entry->launch_command[0] != '\0'; +} + +static void session_spawn_restore_entries(void) { + for (size_t i = 0; i < pending_count; ++i) { + SessionRestoreEntry *entry = &pending_entries[i].entry; + + if (!session_resolve_launch_command(entry)) + continue; + + mango_session_spawn_command(entry->launch_command); + } +} + +void session_init(void) {} + +void session_shutdown(void) { + free_pending_entries(); + restore_started = false; +} + +void session_maybe_restore_startup(void) { + if (!session_is_enabled() || restore_started) + return; + + restore_started = true; + if (!load_pending_entries()) + return; + + session_spawn_restore_entries(); +} + +void session_handle_client_mapped(Client *c) { + PendingSessionEntry *match; + const char *appid; + const char *title; + const char *monitor; + + if (!restore_started || pending_count == 0 || !c) + return; + + appid = mango_session_client_appid(c); + if (!appid || appid[0] == '\0' || strcmp(appid, "broken") == 0) + return; + + title = mango_session_client_title(c); + monitor = mango_session_client_monitor(c); + match = find_pending_match(appid, title, monitor); + if (!match) + return; + + mango_session_remember_client_launch_command(c, match->entry.launch_command); + mango_session_apply_restore_entry(c, &match->entry); + match->used = true; +} + +void session_handle_client_destroyed(Client *c) { + (void)c; +} + +void session_save_now(bool is_final_shutdown) { + FILE *out = NULL; + char *dir = NULL, *tmp_path = NULL, *final_path = NULL; + int32_t count = 0; + + if (!session_is_enabled()) + return; + + dir = session_data_dir(); + if (!dir || !mkdir_p(dir)) + goto cleanup; + + tmp_path = string_printf("%s/session.json.tmp", dir); + final_path = string_printf("%s/session.json", dir); + if (!tmp_path || !final_path) + goto cleanup; + + out = fopen(tmp_path, "w"); + if (!out) + goto cleanup; + + count = mango_session_write_snapshot(out); + if (fclose(out) != 0) { + out = NULL; + goto cleanup; + } + out = NULL; + + if (count <= 0 && is_final_shutdown) { + unlink(tmp_path); + goto cleanup; + } + + if (rename(tmp_path, final_path) != 0) + unlink(tmp_path); + +cleanup: + if (out) + fclose(out); + free(dir); + free(tmp_path); + free(final_path); +} + +bool session_is_enabled(void) { return mango_session_is_config_enabled() != 0; } + +bool session_is_restorable_client(Client *c) { + (void)c; + return false; +} diff --git a/src/session/session.h b/src/session/session.h new file mode 100644 index 00000000..7306cad9 --- /dev/null +++ b/src/session/session.h @@ -0,0 +1,39 @@ +#ifndef SESSION_H +#define SESSION_H + +#include +#include + +typedef struct Client Client; + +typedef struct { + int32_t x; + int32_t y; + int32_t width; + int32_t height; +} SessionRect; + +typedef struct { + char app_id[256]; + char title[512]; + char monitor[128]; + char launch_command[1024]; + int32_t pid; + uint32_t tags; + int32_t is_floating; + int32_t is_fullscreen; + int32_t is_minimized; + SessionRect geom; + SessionRect float_geom; +} SessionRestoreEntry; + +void session_init(void); +void session_shutdown(void); +void session_maybe_restore_startup(void); +void session_handle_client_mapped(Client *c); +void session_handle_client_destroyed(Client *c); +void session_save_now(bool is_final_shutdown); +bool session_is_enabled(void); +bool session_is_restorable_client(Client *c); + +#endif