foxygit / sxbar Log in
commit 5df6f7f680489fbefb214ce701a5d2bac7f5b6da
Author:     MrJensK <jens.se@icloud.com>
AuthorDate: Wed Aug 12 20:47:38 2026 +0200
Commit:     MrJensK <jens.se@icloud.com>
CommitDate: Wed Aug 12 20:47:38 2026 +0200

    scrips install updates
---
 README.md    | 38 ++++++++++++++++++++++--
 src/parser.c | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 132 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 806d331..5dac418 100644
--- a/README.md
+++ b/README.md
@@ -195,8 +195,9 @@ prefix_cmd : battery : "~/.config/sxbar/scripts/battery_icon.sh"
 This and a matching `volume_icon.sh` live under `scripts/` in this repo and
 are installed by `make install` to `$(PREFIX)/share/sxbar/scripts/` as
 reference copies — `sxbarc` still points at your own copy under
-`~/.config/sxbar/scripts/`, so copy and edit from there rather than the
-installed ones.
+`~/.config/sxbar/scripts/`, which `sxbar` seeds from those reference
+copies on first run (see "Auto-seeded `~/.config/sxbar/scripts/`" below),
+so just edit the copy that ends up there rather than the installed ones.

 ### Result

@@ -1108,6 +1109,39 @@ for every module.
   it silently resolves to the no-op fallback, same as any other name
   without a matching script.

+## Auto-seeded `~/.config/sxbar/scripts/`
+
+`make install` only ever populated `$(PREFIX)/share/sxbar/scripts/` (the
+system-wide reference copies) — nothing created or filled in
+`~/.config/sxbar/scripts/`, the copy `resolve_script()` actually prefers
+and the one users are told to edit. On a genuinely fresh install that
+directory didn't exist at all, so every built-in module silently fell
+back to the reference copy (or, if `PREFIX` wasn't `/usr/local`, to a
+no-op) until the user found the docs and copied the scripts over by hand.
+
+### Changes
+- Added `seed_user_scripts()` (`src/parser.c`), called once at the top of
+  `parse_config()`. It walks `/usr/local/share/sxbar/scripts/*.sh` (the
+  same hardcoded reference path `resolve_script()` already falls back to)
+  and, for each script not already present under
+  `~/.config/sxbar/scripts/`, creates that directory if needed and copies
+  the file in with mode `0755`.
+- Never overwrites a file that's already there, so hand-edited scripts
+  are untouched — it only fills in gaps, which also means a script added
+  for a new built-in module in a later `sxbar` version appears
+  automatically on next launch without clobbering everything else.
+- Runs as whatever user launches `sxbar` (never the installer, which is
+  typically `sudo make install`), so the seeded files end up correctly
+  user-owned instead of root-owned.
+
+### Result
+- A fresh install works out of the box: `sudo make install` for the
+  binary and reference scripts, then just running `sxbar` populates
+  `~/.config/sxbar/scripts/` with editable copies on first launch.
+- `resolve_script()`'s existing preference order (user copy → installed
+  reference → no-op) is unchanged; this only affects how the user copy
+  gets there in the first place.
+
 ## Disk footprint

 - `sxbar` is lightweight: the compiled binary is about 36 KB without
diff --git a/src/parser.c b/src/parser.c
index d1ab803..08b4f55 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -1,9 +1,12 @@
 #define _POSIX_C_SOURCE 200809L
 #include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
 #include <limits.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/stat.h>
 #include <unistd.h>

 #include "defs.h"
@@ -121,6 +124,97 @@ static int grow_modules(Config *cfg)
 	return 0;
 }

+static int mkdir_p(const char *dir)
+{
+	char tmp[PATH_MAX];
+	snprintf(tmp, sizeof tmp, "%s", dir);
+	for (char *p = tmp + 1; *p; p++) {
+		if (*p != '/')
+			continue;
+		*p = '\0';
+		if (mkdir(tmp, 0755) != 0 && errno != EEXIST)
+			return -1;
+		*p = '/';
+	}
+	if (mkdir(tmp, 0755) != 0 && errno != EEXIST)
+		return -1;
+	return 0;
+}
+
+static void copy_script(const char *src_path, const char *dst_path)
+{
+	FILE *src = fopen(src_path, "rb");
+	if (!src)
+		return;
+	FILE *dst = fopen(dst_path, "wb");
+	if (!dst) {
+		fclose(src);
+		return;
+	}
+	char buf[4096];
+	size_t n;
+	while ((n = fread(buf, 1, sizeof buf, src)) > 0)
+		fwrite(buf, 1, n, dst);
+	fclose(src);
+	fclose(dst);
+	chmod(dst_path, 0755);
+}
+
+/* on a fresh install, $PREFIX/share/sxbar/scripts/ (installed by `make
+ * install`) has every built-in module's reference script, but
+ * ~/.config/sxbar/scripts/ -- the copy resolve_script() actually prefers,
+ * and the one users are told to edit -- starts out empty; nothing ever
+ * created it. Seed it once at startup: copy in any reference script whose
+ * name isn't already present under ~/.config/sxbar/scripts/, so built-in
+ * modules work (and are editable in place) without a manual copy step
+ * first. Never overwrites a file that's already there, so existing user
+ * edits are untouched; only fills in gaps (e.g. a script for a built-in
+ * module added in a later sxbar version). */
+static void seed_user_scripts(void)
+{
+	const char *home = getenv("HOME");
+	if (!home)
+		return;
+
+	char user_dir[PATH_MAX];
+	snprintf(user_dir, sizeof user_dir, "%s/.config/sxbar/scripts", home);
+
+	const char *sys_dir = "/usr/local/share/sxbar/scripts";
+	DIR *d = opendir(sys_dir);
+	if (!d)
+		return; /* not installed system-wide yet -- nothing to seed from */
+
+	int seeded = 0;
+	struct dirent *ent;
+	while ((ent = readdir(d))) {
+		size_t len = strlen(ent->d_name);
+		if (len < 4 || strcmp(ent->d_name + len - 3, ".sh"))
+			continue;
+
+		char dst_path[PATH_MAX];
+		if ((size_t)snprintf(dst_path, sizeof dst_path, "%s/%s", user_dir,
+		                      ent->d_name) >= sizeof dst_path)
+			continue; /* path too long to ever exist -- skip */
+		if (access(dst_path, F_OK) == 0)
+			continue; /* user already has (or edited) this one */
+
+		if (!seeded && mkdir_p(user_dir) != 0) {
+			closedir(d);
+			return;
+		}
+
+		char src_path[PATH_MAX];
+		snprintf(src_path, sizeof src_path, "%s/%s", sys_dir, ent->d_name);
+		copy_script(src_path, dst_path);
+		seeded++;
+	}
+	closedir(d);
+
+	if (seeded)
+		printf("sxbarc: seeded %d script%s into %s\n", seeded,
+		       seeded == 1 ? "" : "s", user_dir);
+}
+
 /* resolve a module name to its script: the user's own edited copy at
  * ~/.config/sxbar/scripts/<name>.sh wins over the installed reference copy
  * at $PREFIX/share/sxbar/scripts/<name>.sh (currently hardcoded to
@@ -661,6 +755,8 @@ void load_popup_from_script(Module *m, const char *script_path)

 int parse_config(Config *cfg)
 {
+	seed_user_scripts();
+
 	char path[PATH_MAX];
 	FILE *f = open_config(path, sizeof(path));
 	if (!f)