Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ want real colors). Resolved in this order:
The config file's own location can be changed at build time too, with
`meson configure -Dconfig_subpath=...`.

### Custom themes

Any `--theme=NAME` (or `DAF_THEME`, or config `theme=`) that isn't
`dark`/`light`/`mono` is looked up as a file at
`$XDG_CONFIG_HOME/daf/themes/NAME.conf` (or
`~/.config/daf/themes/NAME.conf` — directory changeable at build time via
`meson configure -Dtheme_dir_subpath=...`). It's `key=value` lines, each
value a 256-color palette index as `fg` or `fg,bg`:

```
# ~/.config/daf/themes/mytheme.conf
base=dark # start from a built-in theme; defaults to dark
selection=196,231 # override just the roles you want
```

Unset roles are inherited from `base`. Available roles: `normal`, `gutter`,
`gutter_current` (the current line's line-number digit), `selection`,
`search_match`, `status_bar`, `tilde`.

## Building

Built with [Meson](https://mesonbuild.com/):
Expand Down
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ add_project_arguments('-D_POSIX_C_SOURCE=200809L', language: 'c')
add_project_arguments(
'-DDAF_DEFAULT_THEME="' + get_option('default_theme') + '"',
'-DDAF_CONFIG_SUBPATH="' + get_option('config_subpath') + '"',
'-DDAF_THEME_DIR_SUBPATH="' + get_option('theme_dir_subpath') + '"',
language: 'c',
)

Expand Down
7 changes: 7 additions & 0 deletions meson_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ option(
value: 'daf/config',
description: 'Path to the user config file, relative to $XDG_CONFIG_HOME (or $HOME/.config)',
)

option(
'theme_dir_subpath',
type: 'string',
value: 'daf/themes',
description: 'Directory of custom theme files (<name>.conf), relative to $XDG_CONFIG_HOME (or $HOME/.config)',
)
2 changes: 1 addition & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ static const theme_t *resolve_theme(const char *cli_theme_name)
name = theme_build_default_name();
}

const theme_t *theme = theme_find(name);
const theme_t *theme = theme_load(name);
if (theme == NULL) {
fprintf(stderr, "daf: unknown theme \"%s\", falling back to \"dark\"\n", name);
theme = theme_find("dark");
Expand Down
153 changes: 153 additions & 0 deletions src/theme.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@

#include "theme.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "common.h"

#ifndef DAF_DEFAULT_THEME
#define DAF_DEFAULT_THEME "dark"
#endif

#ifndef DAF_THEME_DIR_SUBPATH
#define DAF_THEME_DIR_SUBPATH "daf/themes"
#endif

static const theme_t k_theme_dark = {
.name = "dark",
.normal = "\x1b[38;5;253;48;5;234m",
Expand Down Expand Up @@ -64,3 +72,148 @@ const char *theme_build_default_name(void)
{
return DAF_DEFAULT_THEME;
}

const char *theme_dir_subpath(void)
{
return DAF_THEME_DIR_SUBPATH;
}

static void build_theme_path(char *out, size_t out_size, const char *name)
{
const char *xdg = getenv("XDG_CONFIG_HOME");
if (xdg != NULL && xdg[0] != '\0') {
snprintf(out, out_size, "%s/%s/%s.conf", xdg, DAF_THEME_DIR_SUBPATH, name);
return;
}

const char *home = getenv("HOME");
if (home != NULL && home[0] != '\0') {
snprintf(out, out_size, "%s/.config/%s/%s.conf", home, DAF_THEME_DIR_SUBPATH, name);
return;
}

out[0] = '\0';
}

static char *trim(char *s)
{
while (*s == ' ' || *s == '\t') {
s++;
}
size_t len = strlen(s);
while (len > 0 &&
(s[len - 1] == ' ' || s[len - 1] == '\t' || s[len - 1] == '\n' || s[len - 1] == '\r')) {
s[--len] = '\0';
}
return s;
}

static char *build_sgr(const char *spec)
{
int fg = -1;
int bg = -1;
sscanf(spec, "%d,%d", &fg, &bg);
if (fg < 0) {
return NULL;
}

char buf[64];
if (bg >= 0) {
snprintf(buf, sizeof(buf), "\x1b[38;5;%d;48;5;%dm", fg, bg);
} else {
snprintf(buf, sizeof(buf), "\x1b[38;5;%dm", fg);
}
return xstrdup(buf);
}

static void apply_role(theme_t *custom, const char *key, char *sgr)
{
if (strcmp(key, "normal") == 0) {
custom->normal = sgr;
} else if (strcmp(key, "gutter") == 0) {
custom->gutter = sgr;
} else if (strcmp(key, "gutter_current") == 0) {
custom->gutter_current = sgr;
} else if (strcmp(key, "selection") == 0) {
custom->selection = sgr;
} else if (strcmp(key, "search_match") == 0) {
custom->search_match = sgr;
} else if (strcmp(key, "status_bar") == 0) {
custom->status_bar = sgr;
} else if (strcmp(key, "tilde") == 0) {
custom->tilde = sgr;
} else {
free(sgr);
}
}

const theme_t *theme_load(const char *name)
{
const theme_t *builtin = theme_find(name);
if (builtin != NULL) {
return builtin;
}

char path[512];
build_theme_path(path, sizeof(path), name);
if (path[0] == '\0') {
return NULL;
}

FILE *fp = fopen(path, "r");
if (fp == NULL) {
return NULL;
}

const theme_t *base = &k_theme_dark;
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
char *s = trim(line);
if (s[0] == '\0' || s[0] == '#') {
continue;
}
char *eq = strchr(s, '=');
if (eq == NULL) {
continue;
}
*eq = '\0';
char *key = trim(s);
char *value = trim(eq + 1);
if (strcmp(key, "base") == 0) {
const theme_t *found = theme_find(value);
if (found != NULL) {
base = found;
}
}
}

theme_t *custom = xmalloc(sizeof(theme_t));
*custom = *base;
custom->name = xstrdup(name);

rewind(fp);
while (fgets(line, sizeof(line), fp) != NULL) {
char *s = trim(line);
if (s[0] == '\0' || s[0] == '#') {
continue;
}
char *eq = strchr(s, '=');
if (eq == NULL) {
continue;
}
*eq = '\0';
char *key = trim(s);
char *value = trim(eq + 1);
if (strcmp(key, "base") == 0) {
continue;
}

char *sgr = build_sgr(value);
if (sgr != NULL) {
apply_role(custom, key, sgr);
}
}

fclose(fp);
return custom;
}
4 changes: 4 additions & 0 deletions src/theme.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ typedef struct {

const theme_t *theme_find(const char *name);

const theme_t *theme_load(const char *name);

const char *theme_build_default_name(void);

const char *theme_dir_subpath(void);

#endif /* DAF_THEME_H */
88 changes: 87 additions & 1 deletion tests/test_theme.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@
*/

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include "theme.h"

int main(void)
static void write_file(const char *path, const char *content)
{
FILE *fp = fopen(path, "w");
assert(fp != NULL);
fputs(content, fp);
fclose(fp);
}

static void test_builtin_themes(void)
{
const theme_t *dark = theme_find("dark");
assert(dark != NULL);
Expand All @@ -28,6 +40,80 @@ int main(void)

const char *default_name = theme_build_default_name();
assert(theme_find(default_name) != NULL);
}

static void test_load_prefers_builtin(void)
{
assert(theme_load("dark") == theme_find("dark"));
}

static void test_load_missing_returns_null(void)
{
setenv("XDG_CONFIG_HOME", "/tmp/daf_theme_test_does_not_exist", 1);
assert(theme_load("no-such-custom-theme") == NULL);
}

static const char *setup_theme_dir(void)
{
static char base[256];
snprintf(base, sizeof(base), "/tmp/daf_theme_test_%d", (int)getpid());
mkdir(base, 0700);

char subpath_copy[256];
snprintf(subpath_copy, sizeof(subpath_copy), "%s", theme_dir_subpath());

char path[600];
snprintf(path, sizeof(path), "%s", base);

char *segment = strtok(subpath_copy, "/");
while (segment != NULL) {
size_t len = strlen(path);
snprintf(path + len, sizeof(path) - len, "/%s", segment);
mkdir(path, 0700);
segment = strtok(NULL, "/");
}

setenv("XDG_CONFIG_HOME", base, 1);
return base;
}

static void test_load_custom_theme_with_base(void)
{
const char *base = setup_theme_dir();
char path[700];
snprintf(path, sizeof(path), "%s/%s/mytheme.conf", base, theme_dir_subpath());
write_file(path, "# custom theme\nbase=light\nselection=196,231\n");

const theme_t *custom = theme_load("mytheme");
assert(custom != NULL);
assert(strcmp(custom->name, "mytheme") == 0);
assert(strcmp(custom->selection, "\x1b[38;5;196;48;5;231m") == 0);
/* Roles not overridden inherit from the requested base (light). */
const theme_t *light = theme_find("light");
assert(strcmp(custom->normal, light->normal) == 0);
assert(strcmp(custom->status_bar, light->status_bar) == 0);
}

static void test_load_custom_theme_defaults_to_dark_base(void)
{
const char *base = setup_theme_dir();
char path[700];
snprintf(path, sizeof(path), "%s/%s/nobase.conf", base, theme_dir_subpath());
write_file(path, "tilde=250\n");

const theme_t *custom = theme_load("nobase");
assert(custom != NULL);
assert(strcmp(custom->tilde, "\x1b[38;5;250m") == 0);
const theme_t *dark = theme_find("dark");
assert(strcmp(custom->normal, dark->normal) == 0);
}

int main(void)
{
test_builtin_themes();
test_load_prefers_builtin();
test_load_missing_returns_null();
test_load_custom_theme_with_base();
test_load_custom_theme_defaults_to_dark_base();
return 0;
}
Loading