foxygit / Hush Log in
commit 1d19404f14583e32d36ef4cec0edf46ba5f97c66
Author:     MrJensK <jens.se@icloud.com>
AuthorDate: Mon Aug 24 19:25:18 2026 +0200
Commit:     MrJensK <jens.se@icloud.com>
CommitDate: Mon Aug 24 19:25:18 2026 +0200

    Add `cmake --install` support so hush can live on PATH

    Installs the binary to <prefix>/bin and its fonts/language files to
    <prefix>/share/hush/assets. A new src/paths.c resolves the assets
    directory at runtime relative to the running executable (via
    /proc/self/exe), falling back to the compile-time source-tree path
    when no such install layout is found next to the binary -- so the
    install is fully relocatable, and `./build/hush` from a plain build
    directory keeps working unmodified.

    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
 CMakeLists.txt | 13 +++++++++++++
 README.md      | 16 +++++++++++++++-
 src/fonts.c    | 23 ++++++++++++++---------
 src/lang.c     |  3 ++-
 src/main.c     |  2 ++
 src/paths.c    | 41 +++++++++++++++++++++++++++++++++++++++++
 src/paths.h    | 15 +++++++++++++++
 7 files changed, 102 insertions(+), 11 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index d5d20a5..1ac4e0e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -10,6 +10,7 @@ if(NOT CMAKE_BUILD_TYPE)
 endif()

 include(FetchContent)
+include(GNUInstallDirs)

 set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
 set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
@@ -36,6 +37,7 @@ add_executable(hush
     src/undo.c
     src/config.c
     src/lang.c
+    src/paths.c
     src/editor.c
     src/render.c
     src/markdown_io.c
@@ -52,6 +54,16 @@ endif()

 target_compile_options(hush PRIVATE -Wall -Wextra -Wno-unused-parameter)

+# `cmake --install build` (or `sudo make install` from build/) puts hush on PATH: the binary
+# goes to <prefix>/bin (CMAKE_INSTALL_PREFIX defaults to /usr/local, whose bin/ is already on
+# PATH on virtually every Linux distro) and its fonts/language files go to
+# <prefix>/share/hush/assets, found at runtime relative to the installed binary (see
+# src/paths.c) -- so the install is fully relocatable and doesn't depend on this source tree
+# still being around afterward. Pass -DCMAKE_INSTALL_PREFIX=$HOME/.local to install for just
+# your user instead (no sudo needed, as long as ~/.local/bin is already on your PATH).
+install(TARGETS hush RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+install(DIRECTORY assets/ DESTINATION ${CMAKE_INSTALL_DATADIR}/hush/assets)
+
 # Headless logic test: no window/GPU context is created, so it can run in CI or over SSH.
 add_executable(hush_test
     src/clay_impl.c
@@ -66,6 +78,7 @@ add_executable(hush_test
     src/undo.c
     src/config.c
     src/lang.c
+    src/paths.c
     src/editor.c
     src/render.c
     src/markdown_io.c
diff --git a/README.md b/README.md
index d2f7002..05866d4 100644
--- a/README.md
+++ b/README.md
@@ -28,13 +28,27 @@ This produces two binaries in `build/`:
 - `hush` — the editor itself
 - `hush_test` — a headless test suite for the document/editor logic (no window or GPU context needed, so it can run over SSH or in CI)

+### Installing (optional)
+
+To put `hush` on your `PATH`:
+
+```sh
+sudo cmake --install build
+```
+
+This installs the binary to `<prefix>/bin` and its fonts/language files to `<prefix>/share/hush/assets`, found at runtime relative to the installed binary — so it's a fully relocatable install, not tied to this source directory. `CMAKE_INSTALL_PREFIX` defaults to `/usr/local`, whose `bin/` is already on `PATH` on virtually every Linux distro, hence the `sudo`. To install for just your own user instead (no `sudo` needed, as long as `~/.local/bin` is already on your `PATH`):
+
+```sh
+cmake --install build --prefix ~/.local
+```
+
 ## Usage

 ```sh
 ./build/hush [file.md]
 ```

-Run without an argument to start with an empty, untitled document.
+(or just `hush [file.md]` if you installed it, see above). Run without an argument to start with an empty, untitled document.

 ### Keyboard shortcuts

diff --git a/src/fonts.c b/src/fonts.c
index b158e52..43aecc7 100644
--- a/src/fonts.c
+++ b/src/fonts.c
@@ -1,5 +1,7 @@
 #include "fonts.h"
+#include "paths.h"
 #include <stdlib.h>
+#include <stdio.h>

 #define FONT_BASE_SIZE 48

@@ -22,7 +24,10 @@ static int *build_codepoints(int *count) {
     return cps;
 }

-static void load_one(Font *font, const char *overridePath, const char *defaultPath, const int *codepoints, int codepointCount) {
+static void load_one(Font *font, const char *overridePath, const char *defaultFileName, const int *codepoints, int codepointCount) {
+    char defaultPath[1024];
+    snprintf(defaultPath, sizeof defaultPath, "%s/fonts/%s", paths_assets_dir(), defaultFileName);
+
     const char *path = (overridePath && overridePath[0]) ? overridePath : defaultPath;
     *font = LoadFontEx(path, FONT_BASE_SIZE, (int *)codepoints, codepointCount);
     if (font->texture.id == 0 && overridePath) {
@@ -44,14 +49,14 @@ void fonts_load(Font *fonts, char *const *overridePaths) {
     char *none[FONT_COUNT] = {0};
     char *const *ov = overridePaths ? overridePaths : none;

-    load_one(&fonts[FONT_SANS_REGULAR], ov[FONT_SANS_REGULAR], ASSETS_DIR "/fonts/DejaVuSans.ttf", codepoints, codepointCount);
-    load_one(&fonts[FONT_SANS_BOLD], ov[FONT_SANS_BOLD], ASSETS_DIR "/fonts/DejaVuSans-Bold.ttf", codepoints, codepointCount);
-    load_one(&fonts[FONT_SANS_ITALIC], ov[FONT_SANS_ITALIC], ASSETS_DIR "/fonts/DejaVuSans-Oblique.ttf", codepoints, codepointCount);
-    load_one(&fonts[FONT_SANS_BOLD_ITALIC], ov[FONT_SANS_BOLD_ITALIC], ASSETS_DIR "/fonts/DejaVuSans-BoldOblique.ttf", codepoints, codepointCount);
-    load_one(&fonts[FONT_MONO_REGULAR], ov[FONT_MONO_REGULAR], ASSETS_DIR "/fonts/DejaVuSansMono.ttf", codepoints, codepointCount);
-    load_one(&fonts[FONT_MONO_BOLD], ov[FONT_MONO_BOLD], ASSETS_DIR "/fonts/DejaVuSansMono-Bold.ttf", codepoints, codepointCount);
-    load_one(&fonts[FONT_MONO_ITALIC], ov[FONT_MONO_ITALIC], ASSETS_DIR "/fonts/DejaVuSansMono-Oblique.ttf", codepoints, codepointCount);
-    load_one(&fonts[FONT_MONO_BOLD_ITALIC], ov[FONT_MONO_BOLD_ITALIC], ASSETS_DIR "/fonts/DejaVuSansMono-BoldOblique.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_SANS_REGULAR], ov[FONT_SANS_REGULAR], "DejaVuSans.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_SANS_BOLD], ov[FONT_SANS_BOLD], "DejaVuSans-Bold.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_SANS_ITALIC], ov[FONT_SANS_ITALIC], "DejaVuSans-Oblique.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_SANS_BOLD_ITALIC], ov[FONT_SANS_BOLD_ITALIC], "DejaVuSans-BoldOblique.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_MONO_REGULAR], ov[FONT_MONO_REGULAR], "DejaVuSansMono.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_MONO_BOLD], ov[FONT_MONO_BOLD], "DejaVuSansMono-Bold.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_MONO_ITALIC], ov[FONT_MONO_ITALIC], "DejaVuSansMono-Oblique.ttf", codepoints, codepointCount);
+    load_one(&fonts[FONT_MONO_BOLD_ITALIC], ov[FONT_MONO_BOLD_ITALIC], "DejaVuSansMono-BoldOblique.ttf", codepoints, codepointCount);

     free(codepoints);
 }
diff --git a/src/lang.c b/src/lang.c
index 74cb0be..ee48f18 100644
--- a/src/lang.c
+++ b/src/lang.c
@@ -1,4 +1,5 @@
 #include "lang.h"
+#include "paths.h"
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -95,7 +96,7 @@ void lang_load(const char *code) {
     if (!code || !code[0]) return;

     char path[1024];
-    snprintf(path, sizeof path, "%s/lang/%s.lang", ASSETS_DIR, code);
+    snprintf(path, sizeof path, "%s/lang/%s.lang", paths_assets_dir(), code);

     FILE *f = fopen(path, "rb");
     if (!f) return;
diff --git a/src/main.c b/src/main.c
index 31758eb..84f7091 100644
--- a/src/main.c
+++ b/src/main.c
@@ -8,6 +8,7 @@
 #include "app_mode.h"
 #include "config.h"
 #include "lang.h"
+#include "paths.h"
 #include <stdlib.h>
 #include <string.h>
 #include <stdio.h>
@@ -31,6 +32,7 @@ static void update_window_title(EditorState *ed) {
 }

 int main(int argc, char **argv) {
+    paths_init();
     Config cfg;
     config_load(&cfg); /* pure file I/O, no raylib dependency -- safe to run before window init */
     lang_load(cfg.language);
diff --git a/src/paths.c b/src/paths.c
new file mode 100644
index 0000000..b10234b
--- /dev/null
+++ b/src/paths.c
@@ -0,0 +1,41 @@
+#include "paths.h"
+#include <stdio.h>
+#include <string.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <limits.h>
+#include <sys/stat.h>
+
+#define ASSETS_PATH_MAX (PATH_MAX + 64)
+
+static char g_assetsDir[ASSETS_PATH_MAX];
+
+static bool is_dir(const char *path) {
+    struct stat st;
+    return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
+}
+
+void paths_init(void) {
+    char exePath[PATH_MAX];
+    ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
+    if (n > 0) {
+        exePath[n] = '\0';
+        char *slash = strrchr(exePath, '/');
+        if (slash) {
+            *slash = '\0'; /* exePath is now the executable's own directory, e.g. .../bin */
+            char candidate[ASSETS_PATH_MAX];
+            snprintf(candidate, sizeof candidate, "%s/../share/hush/assets", exePath);
+            if (is_dir(candidate)) {
+                snprintf(g_assetsDir, sizeof g_assetsDir, "%s", candidate);
+                return;
+            }
+        }
+    }
+    /* No install found next to the executable (or /proc/self/exe isn't available on this
+       platform) -- fall back to the source tree, so `./build/hush` keeps working unmodified. */
+    snprintf(g_assetsDir, sizeof g_assetsDir, "%s", ASSETS_DIR);
+}
+
+const char *paths_assets_dir(void) {
+    return g_assetsDir;
+}
diff --git a/src/paths.h b/src/paths.h
new file mode 100644
index 0000000..6b841b7
--- /dev/null
+++ b/src/paths.h
@@ -0,0 +1,15 @@
+#ifndef HUSH_PATHS_H
+#define HUSH_PATHS_H
+
+/* Resolves and caches the directory hush should load fonts/lang files from. Call once at
+   startup, before fonts_load()/lang_load(). Prefers <prefix>/share/hush/assets next to the
+   running executable (so `cmake --install` produces a fully relocatable install that works
+   from anywhere on PATH); falls back to the source-tree assets/ directory baked in at compile
+   time (ASSETS_DIR) whenever that install layout isn't found -- e.g. running straight from the
+   build directory during development, which keeps working exactly as before. */
+void paths_init(void);
+
+/* Returns the resolved assets directory (no trailing slash). Valid after paths_init(). */
+const char *paths_assets_dir(void);
+
+#endif