Graduately/util.c
pepaskopec-blip 91a848b408 Add a self-updater that installs new releases.
Bake the release tag into the binary so the app knows what it is running;
plain builds keep the "dev" default and never offer an update. The CI
workflow already exported the tag on Linux and macOS, so only the Windows
job needed it.

Read the newest version from the redirect GitHub serves for
/releases/latest, which avoids the REST API and its unauthenticated rate
limit and needs no JSON parser. Networking goes through the curl command
rather than a linked library: curl handles TLS, redirects and proxies, it
ships on all three platforms, and it keeps the bundling scripts free of
another shared library.

An application cannot reliably replace its own files while running, so
every platform follows one shape: unpack beside the current install,
write a script that waits for this process to exit before swapping, then
launch it detached and quit. Staging beside the install keeps the move on
one filesystem and fails early when the location is read-only. The
Windows script copies without mirroring so the progress files kept inside
the install directory survive.

Check quietly a few seconds after startup and offer the result in a
banner above the pages, with a manual check in the settings popover.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 15:04:55 +02:00

333 lines
9.8 KiB
C

#include "maturita.h"
#ifdef _WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <mach-o/dyld.h>
#include <unistd.h>
#else
#include <unistd.h>
#endif
/* ------------------------------------------------------------------ */
/* Portable data / progress paths */
/* ------------------------------------------------------------------ */
char *app_progress_dir;
char *app_progress_u1;
char *app_progress_u2;
char *app_progress_u3;
char *app_settings_file;
char *app_progress_net;
char *app_progress_hw;
char *app_progress_mluvnice;
char *app_progress_cetba;
static char *dup_dirname(const char *path) {
char *tmp = g_strdup(path);
char *dir;
if (!tmp)
return NULL;
dir = g_path_get_dirname(tmp);
g_free(tmp);
return dir;
}
/* Absolute path of this process's executable, or NULL. */
char *app_executable_path(void) {
#ifdef _WIN32
wchar_t wbuf[MAX_PATH];
DWORD n = GetModuleFileNameW(NULL, wbuf, MAX_PATH);
char *out;
if (n == 0 || n >= MAX_PATH)
return NULL;
out = g_utf16_to_utf8((const gunichar2 *)wbuf, -1, NULL, NULL, NULL);
return out;
#elif defined(__APPLE__)
char *buf;
uint32_t size = 0;
_NSGetExecutablePath(NULL, &size);
if (size == 0)
return NULL;
buf = g_malloc(size);
if (_NSGetExecutablePath(buf, &size) != 0) {
g_free(buf);
return NULL;
}
{
char *real = g_canonicalize_filename(buf, NULL);
g_free(buf);
return real ? real : NULL;
}
#else
return g_file_read_link("/proc/self/exe", NULL);
#endif
}
static gboolean dir_has_stylesheet(const char *dir) {
char *css;
gboolean ok;
if (!dir || !dir[0])
return FALSE;
css = g_build_filename(dir, CSS_FILE, NULL);
ok = g_file_test(css, G_FILE_TEST_IS_REGULAR);
g_free(css);
return ok;
}
static char *macos_resources_dir(const char *exe) {
char *macos_dir;
char *contents;
char *resources;
if (!exe)
return NULL;
macos_dir = dup_dirname(exe);
if (!macos_dir)
return NULL;
if (!g_str_has_suffix(macos_dir, "MacOS")) {
g_free(macos_dir);
return NULL;
}
contents = dup_dirname(macos_dir);
g_free(macos_dir);
if (!contents)
return NULL;
resources = g_build_filename(contents, "Resources", NULL);
g_free(contents);
if (!dir_has_stylesheet(resources)) {
g_free(resources);
return NULL;
}
return resources;
}
/* Directory next to Foo.app when exe is .../Foo.app/Contents/MacOS/maturita. */
static char *macos_app_parent_dir(const char *exe) {
char *macos_dir;
char *contents;
char *app_bundle;
char *parent;
if (!exe)
return NULL;
macos_dir = dup_dirname(exe);
if (!macos_dir || !g_str_has_suffix(macos_dir, "MacOS")) {
g_free(macos_dir);
return NULL;
}
contents = dup_dirname(macos_dir);
g_free(macos_dir);
if (!contents)
return NULL;
app_bundle = dup_dirname(contents);
g_free(contents);
if (!app_bundle)
return NULL;
parent = dup_dirname(app_bundle);
g_free(app_bundle);
return parent;
}
static void set_progress_paths(const char *write_root) {
g_free(app_progress_dir);
g_free(app_progress_u1);
g_free(app_progress_u2);
g_free(app_progress_u3);
g_free(app_settings_file);
g_free(app_progress_net);
g_free(app_progress_hw);
g_free(app_progress_mluvnice);
g_free(app_progress_cetba);
app_progress_dir = g_build_filename(write_root, PROGRESS_DIR_NAME, NULL);
app_progress_u1 = g_build_filename(app_progress_dir, "unit1.conf", NULL);
app_progress_u2 = g_build_filename(app_progress_dir, "unit2.conf", NULL);
app_progress_u3 = g_build_filename(app_progress_dir, "unit3.conf", NULL);
app_settings_file = g_build_filename(app_progress_dir, "settings.conf", NULL);
app_progress_net = g_build_filename(app_progress_dir, "net.conf", NULL);
app_progress_hw = g_build_filename(app_progress_dir, "hw.conf", NULL);
app_progress_mluvnice = g_build_filename(app_progress_dir, "mluvnice.conf", NULL);
app_progress_cetba = g_build_filename(app_progress_dir, "cetba.conf", NULL);
}
/* Resolve read-only asset root, chdir there, and point progress at a writable
* location (next to the AppImage / .app, or beside the binary / source tree). */
void setup_portable_paths(void) {
const char *appdir = g_getenv("APPDIR");
const char *appimage = g_getenv("APPIMAGE");
char *exe = app_executable_path();
char *exe_dir = exe ? dup_dirname(exe) : NULL;
char *data_dir = NULL;
char *write_root = NULL;
char *cwd;
if (appdir && dir_has_stylesheet(appdir))
data_dir = g_strdup(appdir);
if (!data_dir)
data_dir = macos_resources_dir(exe);
if (!data_dir && exe_dir && dir_has_stylesheet(exe_dir))
data_dir = g_strdup(exe_dir);
cwd = g_get_current_dir();
if (!data_dir && dir_has_stylesheet(cwd))
data_dir = g_strdup(cwd);
if (!data_dir && cwd)
data_dir = g_strdup(cwd);
if (appimage && appimage[0])
write_root = dup_dirname(appimage);
if (!write_root)
write_root = macos_app_parent_dir(exe);
if (!write_root && exe_dir)
write_root = g_strdup(exe_dir);
if (!write_root && data_dir)
write_root = g_strdup(data_dir);
if (!write_root)
write_root = g_strdup(".");
set_progress_paths(write_root);
if (data_dir && g_chdir(data_dir) != 0)
g_warning("Could not chdir to data directory '%s'", data_dir);
g_free(cwd);
g_free(write_root);
g_free(data_dir);
g_free(exe_dir);
g_free(exe);
}
char *normalize_answer(const char *input) {
GString *pre = g_string_new(NULL);
GString *out = g_string_new(NULL);
const gchar *down = g_utf8_strdown(input ? input : "", -1);
gchar *decomp;
const gchar *p;
gboolean prev_space = FALSE;
/* German special letters first, so ä/ö/ü/ß keep their two-letter
* keyboard fallbacks (ae/oe/ue/ss). */
for (p = down; *p; p = g_utf8_next_char(p)) {
gunichar ch = g_utf8_get_char(p);
switch (ch) {
case 0x00e4: g_string_append(pre, "ae"); break; /* ä */
case 0x00f6: g_string_append(pre, "oe"); break; /* ö */
case 0x00fc: g_string_append(pre, "ue"); break; /* ü */
case 0x00df: g_string_append(pre, "ss"); break; /* ß */
default: g_string_append_unichar(pre, ch); break;
}
}
/* Decompose and drop combining accent marks so Czech letters such as
* é/ě/ř/ů/ž/š/č are treated as their base letters. */
decomp = g_utf8_normalize(pre->str, -1, G_NORMALIZE_NFD);
if (!decomp)
decomp = g_strdup(pre->str);
for (p = decomp; *p; p = g_utf8_next_char(p)) {
gunichar ch = g_utf8_get_char(p);
if (g_unichar_combining_class(ch) != 0)
continue; /* accent mark */
if (g_unichar_isspace(ch)) {
if (out->len > 0 && !prev_space)
g_string_append_c(out, ' ');
prev_space = TRUE;
continue;
}
if (!g_unichar_isalnum(ch))
continue; /* ignore punctuation and symbols */
prev_space = FALSE;
g_string_append_unichar(out, ch);
}
if (out->len > 0 && out->str[out->len - 1] == ' ')
g_string_truncate(out, out->len - 1);
g_free(decomp);
g_free((gpointer)down);
g_string_free(pre, TRUE);
return g_string_free(out, FALSE);
}
void shuffle_indices(int *arr, int n) {
for (int i = n - 1; i > 0; i--) {
int j = g_random_int_range(0, i + 1);
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
void flow_clear(GtkFlowBox *fb) {
GtkWidget *c = gtk_widget_get_first_child(GTK_WIDGET(fb));
while (c) {
GtkWidget *next = gtk_widget_get_next_sibling(c);
gtk_flow_box_remove(fb, c);
c = next;
}
}
void answer_mark(GtkWidget *widget, gboolean ok) {
if (ok) {
gtk_widget_remove_css_class(widget, "answer-wrong");
gtk_widget_add_css_class(widget, "answer-ok");
} else {
gtk_widget_remove_css_class(widget, "answer-ok");
gtk_widget_add_css_class(widget, "answer-wrong");
}
}
void set_feedback(GtkWidget *label, gboolean ok, const char *text) {
gtk_widget_remove_css_class(label, "feedback-ok");
gtk_widget_remove_css_class(label, "feedback-err");
gtk_widget_remove_css_class(label, "feedback-warn");
gtk_widget_add_css_class(label, ok ? "feedback-ok" : "feedback-err");
gtk_label_set_text(GTK_LABEL(label), text);
}
/* Yellow "almost there" note: close to a correct answer but incomplete. */
void set_feedback_warn(GtkWidget *label, const char *text) {
gtk_widget_remove_css_class(label, "feedback-ok");
gtk_widget_remove_css_class(label, "feedback-err");
gtk_widget_remove_css_class(label, "feedback-warn");
gtk_widget_add_css_class(label, "feedback-warn");
gtk_label_set_text(GTK_LABEL(label), text);
}
/* True if `s` contains any German special letter (ä, ö, ü or ß). */
gboolean text_has_german_umlaut(const char *s) {
const char *p = s ? s : "";
while (*p) {
gunichar c = g_utf8_get_char(p);
if (c == 0x00e4 || c == 0x00f6 || c == 0x00fc || c == 0x00df)
return TRUE;
p = g_utf8_next_char(p);
}
return FALSE;
}
/* Small bilingual note shown under the exercise title whenever the answer
* the student has to type may contain a German special letter. */
void add_umlaut_note(GtkWidget *body) {
GtkWidget *n = gtk_label_new(NULL);
i18n_bind(n, "hint_umlauts", 0);
gtk_widget_set_halign(n, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(n), TRUE);
gtk_widget_set_margin_bottom(n, 2);
gtk_widget_add_css_class(n, "hint");
gtk_box_append(GTK_BOX(body), n);
}