Configuration,
documented by itself.

Generated from the repository source.

Clipboard Picker

default.nix

clipboard-picker/default.nix · 46 lines

{ pkgs ? import <nixpkgs> { } }:

pkgs.stdenv.mkDerivation {
  pname = "clipboard-picker";
  version = "0.1.0";
  src = ./.;

  nativeBuildInputs = with pkgs; [
    pkg-config
    wrapGAppsHook4
    makeWrapper
  ];

  buildInputs = with pkgs; [
    gtk4
    glib
    json-glib
  ];

  buildPhase = ''
    runHook preBuild
    cc -O3 -Wall -Wextra -o clipboard-picker main.c \
      $(pkg-config --cflags --libs gtk4 gio-2.0 json-glib-1.0)
    runHook postBuild
  '';

  installPhase = ''
    runHook preInstall
    install -Dm755 clipboard-picker $out/bin/clipboard-picker

    wrapProgram $out/bin/clipboard-picker \
      --prefix PATH : ${pkgs.lib.makeBinPath [
        pkgs.cliphist
        pkgs.wl-clipboard
        pkgs.sway
        pkgs.wtype
      ]}
    runHook postInstall
  '';

  meta = with pkgs.lib; {
    description = "GTK4 C clipboard history picker for Sway";
    mainProgram = "clipboard-picker";
    platforms = platforms.linux;
  };
}

main.c

clipboard-picker/main.c · 1,298 lines

#include <errno.h>
#include <gio/gio.h>
#include <gtk/gtk.h>
#include <json-glib/json-glib.h>
#include <stddef.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/un.h>
#include <unistd.h>

#define APP_ID "com.erick.ClipboardPicker"
#define PREVIEW_DEBOUNCE_MS 100
#define SEARCH_PREVIEW_DEBOUNCE_MS 200
#define PREVIEW_CACHE_MAX_ITEMS 12
#define PREVIEW_CACHE_MAX_BYTES (16 * 1024 * 1024)
#define SEARCH_CACHE_MAX_ENTRIES 32

typedef struct {
  GObject parent_instance;
  gchar *raw;
  gchar *display_single_line;
  gchar *search_text;
  gboolean binary;
} ClipboardItem;

typedef struct {
  GObjectClass parent_class;
} ClipboardItemClass;

typedef struct {
  gchar *key;
  GBytes *bytes;
  gsize size;
} PreviewCacheEntry;

typedef struct {
  int fd;
  guint generation;
} FocusedAppRequest;

typedef struct {
  GtkApplication parent_instance;
  GListStore *results;
  GPtrArray *search_items;
  GHashTable *search_cache;
  GtkSingleSelection *selection;
  GtkSearchEntry *search;
  GtkBox *preview_box;
  GtkStack *preview_stack;
  GtkLabel *empty_label;
  GtkPicture *picture;
  GtkLabel *text_preview;
  GtkLabel *mime_value;
  GtkLabel *size_value;
  GtkLabel *details_value;
  GtkListView *list_view;
  GtkWindow *window;
  gchar *focused_app;
  gchar *pending_query;
  guint preview_timeout_id;
  guint paste_timeout_id;
  guint secondary_setup_id;
  guint history_refresh_id;
  ClipboardItem *pending_preview_item;
  ClipboardItem *displayed_preview_item;
  GCancellable *focused_app_cancellable;
  GCancellable *history_cancellable;
  GSubprocess *history_process;
  GCancellable *preview_cancellable;
  GSubprocess *preview_process;
  GCancellable *paste_cancellable;
  GSubprocess *paste_process;
  GHashTable *preview_cache;
  GQueue preview_cache_lru;
  gsize preview_cache_bytes;
  guint focused_app_generation;
  guint preview_generation;
  gboolean history_loading;
  gboolean updating_results;
  gboolean shutting_down;
} ClipboardPicker;

typedef struct {
  GtkApplicationClass parent_class;
} ClipboardPickerClass;

G_DEFINE_TYPE(ClipboardItem, clipboard_item, G_TYPE_OBJECT)
G_DEFINE_TYPE(ClipboardPicker, clipboard_picker, GTK_TYPE_APPLICATION)

static void on_search_activate(GtkSearchEntry *search, gpointer user_data);
static void update_search_results(ClipboardPicker *app);

static gboolean write_all(int fd, const void *buffer, size_t size) {
  const guint8 *data = buffer;
  while (size > 0) {
    ssize_t written = write(fd, data, size);
    if (written < 0 && errno == EINTR)
      continue;
    if (written <= 0)
      return FALSE;
    data += written;
    size -= written;
  }
  return TRUE;
}

static gboolean read_all(int fd, void *buffer, size_t size) {
  guint8 *data = buffer;
  while (size > 0) {
    ssize_t received = read(fd, data, size);
    if (received < 0 && errno == EINTR)
      continue;
    if (received <= 0)
      return FALSE;
    data += received;
    size -= received;
  }
  return TRUE;
}

static gchar *focused_app_from_json(JsonNode *node) {
  if (!JSON_NODE_HOLDS_OBJECT(node))
    return NULL;

  JsonObject *object = json_node_get_object(node);
  if (json_object_has_member(object, "focused") &&
      json_object_get_boolean_member(object, "focused")) {
    const gchar *app_id = json_object_has_member(object, "app_id")
                              ? json_object_get_string_member(object, "app_id")
                              : NULL;
    if (app_id && *app_id)
      return g_strdup(app_id);

    JsonObject *properties =
        json_object_get_object_member(object, "window_properties");
    if (properties) {
      const gchar *class_name =
          json_object_has_member(properties, "class")
              ? json_object_get_string_member(properties, "class")
              : NULL;
      if (class_name && *class_name)
        return g_strdup(class_name);
    }
  }

  const gchar *child_names[] = {"nodes", "floating_nodes", NULL};
  for (const gchar **name = child_names; *name; name++) {
    JsonArray *children = json_object_get_array_member(object, *name);
    if (!children)
      continue;
    for (guint i = 0; i < json_array_get_length(children); i++) {
      gchar *result =
          focused_app_from_json(json_array_get_element(children, i));
      if (result)
        return result;
    }
  }
  return NULL;
}

static int begin_focused_app_request(void) {
  const gchar *socket_path = g_getenv("SWAYSOCK");
  if (!socket_path || !*socket_path)
    return -1;

  int fd = socket(AF_UNIX, SOCK_STREAM, 0);
  if (fd < 0)
    return -1;

  struct timeval timeout = {.tv_sec = 2};
  setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
  setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));

  struct sockaddr_un address = {.sun_family = AF_UNIX};
  if (strlen(socket_path) >= sizeof(address.sun_path)) {
    close(fd);
    return -1;
  }
  g_strlcpy(address.sun_path, socket_path, sizeof(address.sun_path));
  if (connect(fd, (struct sockaddr *)&address,
              G_STRUCT_OFFSET(struct sockaddr_un, sun_path) +
                  strlen(socket_path) + 1) < 0) {
    close(fd);
    return -1;
  }

  const guint8 magic[] = {'i', '3', '-', 'i', 'p', 'c'};
  const guint32 payload_length = GUINT32_TO_LE(0);
  const guint32 message_type = GUINT32_TO_LE(4); /* GET_TREE */
  guint8 header[14];
  memcpy(header, magic, sizeof(magic));
  memcpy(header + 6, &payload_length, sizeof(payload_length));
  memcpy(header + 10, &message_type, sizeof(message_type));
  if (!write_all(fd, header, sizeof(header))) {
    close(fd);
    return -1;
  }
  return fd;
}

static gchar *read_focused_app_response(int fd) {
  const guint8 magic[] = {'i', '3', '-', 'i', 'p', 'c'};
  guint8 response_header[14];
  if (!read_all(fd, response_header, sizeof(response_header)) ||
      memcmp(response_header, magic, sizeof(magic)) != 0)
    return NULL;

  guint32 response_length = 0;
  memcpy(&response_length, response_header + 6, sizeof(response_length));
  response_length = GUINT32_FROM_LE(response_length);
  gchar *payload = g_malloc(response_length + 1);
  gchar *result = NULL;
  if (read_all(fd, payload, response_length)) {
    payload[response_length] = '\0';
    JsonParser *parser = json_parser_new();
    GError *error = NULL;
    if (json_parser_load_from_data(parser, payload, response_length, &error))
      result = focused_app_from_json(json_parser_get_root(parser));
    g_clear_error(&error);
    g_object_unref(parser);
  }
  g_free(payload);
  return result;
}

static void focused_app_request_free(gpointer data) {
  FocusedAppRequest *request = data;
  close(request->fd);
  g_free(request);
}

static void focused_app_task(GTask *task, gpointer source_object,
                             gpointer task_data, GCancellable *cancellable) {
  (void)source_object;
  (void)cancellable;
  FocusedAppRequest *request = task_data;
  if (g_task_return_error_if_cancelled(task))
    return;

  gchar *result = read_focused_app_response(request->fd);
  if (g_task_return_error_if_cancelled(task)) {
    g_free(result);
    return;
  }
  if (result) {
    gchar *normalized = g_ascii_strdown(result, -1);
    g_free(result);
    result = normalized;
  }
  g_task_return_pointer(task, result, g_free);
}

static void focused_app_loaded(GObject *source, GAsyncResult *result,
                               gpointer user_data) {
  (void)user_data;
  ClipboardPicker *app = (ClipboardPicker *)source;
  GTask *task = G_TASK(result);
  FocusedAppRequest *request = g_task_get_task_data(task);
  GError *error = NULL;
  gchar *focused_app = g_task_propagate_pointer(task, &error);

  if (request->generation == app->focused_app_generation) {
    g_clear_object(&app->focused_app_cancellable);
    if (!error && !app->shutting_down) {
      g_free(app->focused_app);
      app->focused_app = g_steal_pointer(&focused_app);
    }
  }
  g_free(focused_app);
  g_clear_error(&error);
}

static void load_focused_app_async(ClipboardPicker *app) {
  app->focused_app_generation++;
  if (app->focused_app_cancellable) {
    g_cancellable_cancel(app->focused_app_cancellable);
    g_clear_object(&app->focused_app_cancellable);
  }
  g_clear_pointer(&app->focused_app, g_free);

  int fd = begin_focused_app_request();
  if (fd < 0)
    return;

  FocusedAppRequest *request = g_new0(FocusedAppRequest, 1);
  request->fd = fd;
  request->generation = app->focused_app_generation;
  app->focused_app_cancellable = g_cancellable_new();
  GTask *task =
      g_task_new(app, app->focused_app_cancellable, focused_app_loaded, NULL);
  g_task_set_task_data(task, request, focused_app_request_free);
  g_task_run_in_thread(task, focused_app_task);
  g_object_unref(task);
}

static void clipboard_item_finalize(GObject *object) {
  ClipboardItem *item = (ClipboardItem *)object;
  g_free(item->raw);
  g_free(item->display_single_line);
  g_free(item->search_text);
  G_OBJECT_CLASS(clipboard_item_parent_class)->finalize(object);
}

static void clipboard_item_class_init(ClipboardItemClass *class) {
  G_OBJECT_CLASS(class)->finalize = clipboard_item_finalize;
}

static void clipboard_item_init(ClipboardItem *item) { (void)item; }

static void search_cache_reset(ClipboardPicker *app) {
  g_hash_table_remove_all(app->search_cache);
  g_hash_table_insert(app->search_cache, g_strdup(""),
                      g_ptr_array_ref(app->search_items));
}

static void search_cache_prune(ClipboardPicker *app, const gchar *query) {
  GHashTableIter iter;
  gpointer key;
  g_hash_table_iter_init(&iter, app->search_cache);
  while (g_hash_table_iter_next(&iter, &key, NULL)) {
    const gchar *cached_query = key;
    if (!g_str_has_prefix(query, cached_query))
      g_hash_table_iter_remove(&iter);
  }
}

static GPtrArray *search_cache_find_candidates(ClipboardPicker *app) {
  GPtrArray *best = app->search_items;
  gsize best_length = 0;
  GHashTableIter iter;
  gpointer key;
  gpointer value;
  g_hash_table_iter_init(&iter, app->search_cache);
  while (g_hash_table_iter_next(&iter, &key, &value)) {
    const gchar *cached_query = key;
    gsize cached_length = strlen(cached_query);
    if (cached_length >= best_length) {
      best = value;
      best_length = cached_length;
    }
  }
  return best;
}

static void search_cache_insert(ClipboardPicker *app, const gchar *query,
                                GPtrArray *matches) {
  g_hash_table_replace(app->search_cache, g_strdup(query),
                       g_ptr_array_ref(matches));

  while (g_hash_table_size(app->search_cache) > SEARCH_CACHE_MAX_ENTRIES) {
    const gchar *shortest_query = NULL;
    gsize shortest_length = G_MAXSIZE;
    GHashTableIter iter;
    gpointer key;
    g_hash_table_iter_init(&iter, app->search_cache);
    while (g_hash_table_iter_next(&iter, &key, NULL)) {
      const gchar *cached_query = key;
      gsize cached_length = strlen(cached_query);
      if (cached_length > 0 && cached_length < shortest_length) {
        shortest_query = cached_query;
        shortest_length = cached_length;
      }
    }
    g_hash_table_remove(app->search_cache, shortest_query);
  }
}

static void preview_cache_entry_free(PreviewCacheEntry *entry) {
  g_free(entry->key);
  g_bytes_unref(entry->bytes);
  g_free(entry);
}

static GBytes *preview_cache_lookup(ClipboardPicker *app, const gchar *key) {
  PreviewCacheEntry *entry = g_hash_table_lookup(app->preview_cache, key);
  if (!entry)
    return NULL;

  g_queue_remove(&app->preview_cache_lru, entry);
  g_queue_push_head(&app->preview_cache_lru, entry);
  return g_bytes_ref(entry->bytes);
}

static void preview_cache_insert(ClipboardPicker *app, const gchar *key,
                                 GBytes *bytes) {
  gsize size = g_bytes_get_size(bytes);
  if (size > PREVIEW_CACHE_MAX_BYTES)
    return;

  PreviewCacheEntry *entry = g_hash_table_lookup(app->preview_cache, key);
  if (entry) {
    GBytes *replacement = g_bytes_ref(bytes);
    app->preview_cache_bytes -= entry->size;
    g_clear_pointer(&entry->bytes, g_bytes_unref);
    entry->bytes = replacement;
    entry->size = size;
    app->preview_cache_bytes += size;
    g_queue_remove(&app->preview_cache_lru, entry);
    g_queue_push_head(&app->preview_cache_lru, entry);
  } else {
    entry = g_new0(PreviewCacheEntry, 1);
    entry->key = g_strdup(key);
    entry->bytes = g_bytes_ref(bytes);
    entry->size = size;
    app->preview_cache_bytes += size;
    g_hash_table_insert(app->preview_cache, entry->key, entry);
    g_queue_push_head(&app->preview_cache_lru, entry);
  }

  while (g_queue_get_length(&app->preview_cache_lru) >
             PREVIEW_CACHE_MAX_ITEMS ||
         app->preview_cache_bytes > PREVIEW_CACHE_MAX_BYTES) {
    entry = g_queue_pop_tail(&app->preview_cache_lru);
    app->preview_cache_bytes -= entry->size;
    g_hash_table_remove(app->preview_cache, entry->key);
    preview_cache_entry_free(entry);
  }
}

static void preview_cache_clear(ClipboardPicker *app) {
  PreviewCacheEntry *entry;
  while ((entry = g_queue_pop_head(&app->preview_cache_lru))) {
    g_hash_table_remove(app->preview_cache, entry->key);
    preview_cache_entry_free(entry);
  }
  app->preview_cache_bytes = 0;
}

static GBytes *decode_item(ClipboardItem *item) {
  const gchar *argv[] = {"cliphist", "decode", NULL};
  GSubprocessFlags flags = G_SUBPROCESS_FLAGS_STDIN_PIPE |
                           G_SUBPROCESS_FLAGS_STDOUT_PIPE |
                           G_SUBPROCESS_FLAGS_STDERR_PIPE;
  GSubprocess *process = g_subprocess_newv(argv, flags, NULL);
  if (!process)
    return NULL;

  GBytes *input = g_bytes_new(item->raw, strlen(item->raw));
  GBytes *stdout_data = NULL;
  GBytes *stderr_data = NULL;
  gboolean ok = g_subprocess_communicate(process, input, NULL, &stdout_data,
                                         &stderr_data, NULL);
  g_bytes_unref(input);
  if (!ok)
    g_clear_pointer(&stdout_data, g_bytes_unref);
  if (stderr_data) {
    gsize size = 0;
    const gchar *data = g_bytes_get_data(stderr_data, &size);
    if (size)
      g_debug("command: %.*s", (int)size, data);
    g_bytes_unref(stderr_data);
  }
  g_object_unref(process);
  return stdout_data;
}

static void parse_history_task(GTask *task, gpointer source_object,
                               gpointer task_data, GCancellable *cancellable) {
  (void)source_object;
  (void)cancellable;
  GBytes *output = task_data;
  gsize output_size = 0;
  const gchar *output_data = g_bytes_get_data(output, &output_size);
  gchar *text = g_malloc(output_size + 1);
  if (output_size)
    memcpy(text, output_data, output_size);
  text[output_size] = '\0';

  GPtrArray *items = g_ptr_array_new_with_free_func(g_object_unref);
  gchar *line_start = text;
  gchar *text_end = text + output_size;
  for (gchar *cursor = text;; cursor++) {
    if (cursor < text_end && *cursor != '\n')
      continue;

    *cursor = '\0';
    gchar *tab = memchr(line_start, '\t', cursor - line_start);
    if (tab && tab != line_start) {
      *tab = '\0';
      const gchar *display = tab + 1;
      ClipboardItem *item = g_object_new(clipboard_item_get_type(), NULL);
      item->raw = g_strdup(line_start);
      item->display_single_line = g_strdup(display);
      item->search_text = g_utf8_casefold(display, -1);
      item->binary = g_str_has_prefix(display, "[[ binary data");
      g_ptr_array_add(items, item);
    }
    if (cursor == text_end)
      break;
    line_start = cursor + 1;
  }

  g_free(text);
  g_task_return_pointer(task, items, (GDestroyNotify)g_ptr_array_unref);
}

static void history_parsed(GObject *source, GAsyncResult *result,
                           gpointer user_data) {
  (void)user_data;
  ClipboardPicker *app = (ClipboardPicker *)source;
  app->history_loading = FALSE;
  GPtrArray *items = g_task_propagate_pointer(G_TASK(result), NULL);
  if (app->shutting_down) {
    g_ptr_array_unref(items);
    return;
  }

  g_clear_pointer(&app->search_items, g_ptr_array_unref);
  app->search_items = items;
  search_cache_reset(app);

  if (app->search_items->len == 0)
    gtk_label_set_text(app->empty_label, "Clipboard history is empty");
  else
    gtk_label_set_text(app->empty_label, "Select an entry to preview");
  update_search_results(app);
}

static void history_loaded(GObject *source, GAsyncResult *result,
                           gpointer user_data) {
  ClipboardPicker *app = user_data;
  GSubprocess *process = G_SUBPROCESS(source);
  GBytes *output = NULL;
  GError *error = NULL;
  gboolean success =
      g_subprocess_communicate_finish(process, result, &output, NULL, &error);

  g_clear_object(&app->history_process);
  g_clear_object(&app->history_cancellable);
  g_object_unref(process);

  if (!success) {
    app->history_loading = FALSE;
    if (!app->shutting_down)
      gtk_label_set_text(app->empty_label, error->message);
    g_clear_error(&error);
    g_clear_pointer(&output, g_bytes_unref);
    g_object_unref(app);
    return;
  }
  if (app->shutting_down) {
    g_bytes_unref(output);
    g_object_unref(app);
    return;
  }

  GTask *task = g_task_new(app, NULL, history_parsed, NULL);
  g_task_set_task_data(task, output, (GDestroyNotify)g_bytes_unref);
  g_task_run_in_thread(task, parse_history_task);
  g_object_unref(task);
  g_object_unref(app);
}

static void load_history_async(ClipboardPicker *app) {
  if (app->history_loading)
    return;
  app->history_loading = TRUE;

  const gchar *argv[] = {"cliphist", "list", NULL};
  GError *error = NULL;
  GSubprocess *process =
      g_subprocess_newv(argv, G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error);
  if (!process) {
    app->history_loading = FALSE;
    gtk_label_set_text(app->empty_label, error->message);
    g_clear_error(&error);
    return;
  }
  app->history_cancellable = g_cancellable_new();
  app->history_process = g_object_ref(process);
  g_subprocess_communicate_async(process, NULL, app->history_cancellable,
                                 history_loaded, g_object_ref(app));
}

static gchar *format_size(gsize size) {
  if (size < 1024)
    return g_strdup_printf("%zu B", size);
  if (size < 1024 * 1024)
    return g_strdup_printf("%.1f KiB", size / 1024.0);
  return g_strdup_printf("%.1f MiB", size / (1024.0 * 1024.0));
}

static void show_preview(ClipboardPicker *app, ClipboardItem *item,
                         GBytes *bytes) {
  g_set_object(&app->displayed_preview_item, item);
  gsize size = 0;
  const gchar *data = g_bytes_get_data(bytes, &size);
  gchar *size_text = format_size(size);
  gtk_label_set_text(app->size_value, size_text);
  g_free(size_text);

  GError *error = NULL;
  gchar *content_type = g_content_type_guess(NULL, (guchar *)data, size, NULL);
  gchar *mime = g_content_type_get_mime_type(content_type);
  gtk_label_set_text(app->mime_value, mime ? mime : content_type);
  g_free(mime);
  g_free(content_type);

  if (item->binary) {
    GdkTexture *texture = gdk_texture_new_from_bytes(bytes, &error);
    if (texture) {
      gtk_picture_set_paintable(app->picture, GDK_PAINTABLE(texture));
      gchar *details =
          g_strdup_printf("%d × %d pixels", gdk_texture_get_width(texture),
                          gdk_texture_get_height(texture));
      gtk_label_set_text(app->details_value, details);
      g_free(details);
      gtk_stack_set_visible_child_name(app->preview_stack, "image");
      g_object_unref(texture);
      g_bytes_unref(bytes);
      return;
    }
  }

  gchar *text = g_strndup(data, size);
  gsize characters = g_utf8_strlen(text, -1);
  gsize lines = *text ? 1 : 0;
  for (gchar *p = text; *p; p++)
    if (*p == '\n')
      lines++;
  gchar *details =
      g_strdup_printf("%zu characters · %zu lines", characters, lines);
  gtk_label_set_text(app->details_value, details);
  g_free(details);
  gtk_label_set_text(app->text_preview, text);
  gtk_stack_set_visible_child_name(app->preview_stack, "text");
  g_clear_error(&error);
  g_free(text);
  g_bytes_unref(bytes);
}

typedef struct {
  ClipboardPicker *app;
  ClipboardItem *item;
  GBytes *input;
  guint generation;
  GCancellable *cancellable;
} PreviewRequest;

static void preview_request_free(PreviewRequest *request) {
  g_clear_pointer(&request->input, g_bytes_unref);
  g_clear_object(&request->item);
  g_clear_object(&request->cancellable);
  g_clear_object(&request->app);
  g_free(request);
}

static void preview_loaded(GObject *source, GAsyncResult *result,
                           gpointer user_data) {
  PreviewRequest *request = user_data;
  ClipboardPicker *app = request->app;
  GSubprocess *process = G_SUBPROCESS(source);
  GBytes *bytes = NULL;
  GError *error = NULL;
  gboolean success =
      g_subprocess_communicate_finish(process, result, &bytes, NULL, &error);
  if (app->preview_process == process) {
    g_clear_object(&app->preview_process);
    g_clear_object(&app->preview_cancellable);
  }
  g_object_unref(process);

  if (success && bytes && !app->shutting_down)
    preview_cache_insert(app, request->item->raw, bytes);

  if (!app->shutting_down && request->generation == app->preview_generation) {
    if (success && bytes) {
      show_preview(app, request->item, bytes);
      bytes = NULL;
    } else {
      gtk_label_set_text(app->empty_label,
                         error ? error->message
                               : "Unable to decode clipboard entry");
      gtk_stack_set_visible_child_name(app->preview_stack, "empty");
    }
  }
  g_clear_error(&error);
  g_clear_pointer(&bytes, g_bytes_unref);
  preview_request_free(request);
}

static void load_preview_async(ClipboardPicker *app, ClipboardItem *item) {
  PreviewRequest *request = g_new0(PreviewRequest, 1);
  request->app = g_object_ref(app);
  request->item = g_object_ref(item);
  request->generation = app->preview_generation;
  request->input = g_bytes_new(item->raw, strlen(item->raw));
  request->cancellable = g_cancellable_new();
  app->preview_cancellable = g_object_ref(request->cancellable);

  const gchar *argv[] = {"cliphist", "decode", NULL};
  GError *error = NULL;
  GSubprocess *process = g_subprocess_newv(
      argv, G_SUBPROCESS_FLAGS_STDIN_PIPE | G_SUBPROCESS_FLAGS_STDOUT_PIPE,
      &error);
  if (!process) {
    gtk_label_set_text(app->empty_label, error->message);
    g_clear_error(&error);
    g_clear_object(&app->preview_cancellable);
    preview_request_free(request);
    return;
  }
  app->preview_process = g_object_ref(process);
  g_subprocess_communicate_async(process, request->input, request->cancellable,
                                 preview_loaded, request);
}

static void cancel_preview_load(ClipboardPicker *app) {
  g_clear_handle_id(&app->preview_timeout_id, g_source_remove);
  g_clear_object(&app->pending_preview_item);
  if (app->preview_cancellable) {
    g_cancellable_cancel(app->preview_cancellable);
    g_clear_object(&app->preview_cancellable);
  }
  if (app->preview_process) {
    g_subprocess_force_exit(app->preview_process);
    g_clear_object(&app->preview_process);
  }
}

static gboolean preview_debounce_elapsed(gpointer user_data) {
  ClipboardPicker *app = user_data;
  app->preview_timeout_id = 0;
  ClipboardItem *item = g_steal_pointer(&app->pending_preview_item);
  if (!app->shutting_down && item) {
    GBytes *cached = preview_cache_lookup(app, item->raw);
    if (cached)
      show_preview(app, item, cached);
    else
      load_preview_async(app, item);
  }
  g_clear_object(&item);
  return G_SOURCE_REMOVE;
}

static void schedule_preview(ClipboardPicker *app, ClipboardItem *item,
                             guint delay_ms) {
  if (item && item == app->displayed_preview_item)
    return;

  app->preview_generation++;
  cancel_preview_load(app);
  g_clear_object(&app->displayed_preview_item);

  if (!item) {
    guint count = app->search_items->len;
    const gchar *message = app->history_loading ? "Loading clipboard history…"
                           : count == 0         ? "Clipboard history is empty"
                           : app->pending_query[0]
                               ? "No matching clipboard entries"
                               : "Select an entry to preview";
    gtk_label_set_text(app->empty_label, message);
    gtk_stack_set_visible_child_name(app->preview_stack, "empty");
    return;
  }

  gtk_label_set_text(app->empty_label, "Loading preview…");
  gtk_stack_set_visible_child_name(app->preview_stack, "empty");
  app->pending_preview_item = g_object_ref(item);
  app->preview_timeout_id =
      g_timeout_add_full(G_PRIORITY_DEFAULT, delay_ms, preview_debounce_elapsed,
                         g_object_ref(app), g_object_unref);
}

static void selection_changed(GObject *object, GParamSpec *pspec,
                              gpointer user_data) {
  (void)pspec;
  ClipboardPicker *app = user_data;
  if (app->updating_results)
    return;

  ClipboardItem *item =
      gtk_single_selection_get_selected_item(GTK_SINGLE_SELECTION(object));
  schedule_preview(app, item, PREVIEW_DEBOUNCE_MS);
}

static void factory_setup(GtkSignalListItemFactory *factory, GtkListItem *item,
                          gpointer user_data) {
  (void)factory;
  (void)user_data;
  GtkLabel *label = GTK_LABEL(gtk_label_new(NULL));
  gtk_widget_add_css_class(GTK_WIDGET(label), "history-item");
  gtk_label_set_xalign(label, 0);
  gtk_label_set_ellipsize(label, PANGO_ELLIPSIZE_END);
  gtk_label_set_single_line_mode(label, TRUE);
  gtk_list_item_set_child(item, GTK_WIDGET(label));
}

static void factory_bind(GtkSignalListItemFactory *factory, GtkListItem *item,
                         gpointer user_data) {
  (void)factory;
  (void)user_data;
  ClipboardItem *value = gtk_list_item_get_item(item);
  GtkLabel *label = GTK_LABEL(gtk_list_item_get_child(item));
  gtk_label_set_text(label, value->display_single_line);
}

static gboolean is_resident(ClipboardPicker *app) {
  return (g_application_get_flags(G_APPLICATION(app)) &
          G_APPLICATION_IS_SERVICE) != 0;
}

static void dismiss_picker(ClipboardPicker *app) {
  if (is_resident(app) && app->window)
    gtk_widget_set_visible(GTK_WIDGET(app->window), FALSE);
  else
    g_application_quit(G_APPLICATION(app));
}

static gboolean on_window_close_requested(GtkWindow *window,
                                          gpointer user_data) {
  ClipboardPicker *app = user_data;
  if (!is_resident(app))
    return FALSE;
  gtk_widget_set_visible(GTK_WIDGET(window), FALSE);
  return TRUE;
}

static gboolean inject_paste(gpointer user_data) {
  ClipboardPicker *app = user_data;
  app->paste_timeout_id = 0;
  if (app->shutting_down)
    return G_SOURCE_REMOVE;

  const gchar *terminal_names[] = {
      "alacritty", "foot",    "kitty", "wezterm", "ghostty",
      "terminal",  "konsole", "xterm", "urxvt",   NULL};
  gboolean terminal = FALSE;
  for (const gchar **name = terminal_names; *name; name++)
    if (app->focused_app && g_strrstr(app->focused_app, *name))
      terminal = TRUE;

  const gchar *argv_emacs[] = {"wtype", "-M", "ctrl", "-P",   "y",
                               "-p",    "y",  "-m",   "ctrl", NULL};
  const gchar *argv_terminal[] = {"wtype", "-M", "ctrl", "-M", "shift",
                                  "-P",    "v",  "-p",   "v",  "-m",
                                  "shift", "-m", "ctrl", NULL};
  const gchar *argv_regular[] = {"wtype", "-M", "ctrl", "-P",   "v",
                                 "-p",    "v",  "-m",   "ctrl", NULL};
  const gchar *const *argv =
      app->focused_app && g_strrstr(app->focused_app, "emacs") ? argv_emacs
      : terminal                                               ? argv_terminal
                                                               : argv_regular;
  GError *error = NULL;
  if (!g_spawn_async(NULL, (gchar **)argv, NULL, G_SPAWN_SEARCH_PATH, NULL,
                     NULL, NULL, &error)) {
    g_warning("clipboard-picker: wtype failed: %s", error->message);
    g_clear_error(&error);
  }
  g_free(app->focused_app);
  app->focused_app = NULL;
  dismiss_picker(app);
  return G_SOURCE_REMOVE;
}

static void paste_finished(GObject *source, GAsyncResult *result,
                           gpointer user_data) {
  ClipboardPicker *app = user_data;
  GSubprocess *process = G_SUBPROCESS(source);
  GError *error = NULL;
  gboolean success =
      g_subprocess_communicate_finish(process, result, NULL, NULL, &error);
  if (app->paste_process == process) {
    g_clear_object(&app->paste_process);
    g_clear_object(&app->paste_cancellable);
  }
  g_object_unref(process);

  if (!success) {
    if (!app->shutting_down)
      g_warning("clipboard-picker: wl-copy failed: %s", error->message);
    g_clear_error(&error);
    g_object_unref(app);
    return;
  }

  if (!app->shutting_down) {
    GtkWindow *window = gtk_application_get_active_window(GTK_APPLICATION(app));
    if (window)
      gtk_widget_set_visible(GTK_WIDGET(window), FALSE);
    app->paste_timeout_id =
        g_timeout_add_full(G_PRIORITY_DEFAULT, 120, inject_paste,
                           g_object_ref(app), g_object_unref);
  }
  g_object_unref(app);
}

static void activate_item(GtkListView *view, guint position,
                          gpointer user_data) {
  (void)view;
  ClipboardPicker *app = user_data;
  ClipboardItem *item =
      g_list_model_get_item(G_LIST_MODEL(app->results), position);
  if (!item)
    return;
  GBytes *data = decode_item(item);
  if (!data) {
    g_object_unref(item);
    return;
  }

  if (app->paste_cancellable) {
    g_cancellable_cancel(app->paste_cancellable);
    g_clear_object(&app->paste_cancellable);
  }
  if (app->paste_process) {
    g_subprocess_force_exit(app->paste_process);
    g_clear_object(&app->paste_process);
  }

  const gchar *argv[] = {"wl-copy", NULL};
  GError *error = NULL;
  GSubprocess *process =
      g_subprocess_newv(argv, G_SUBPROCESS_FLAGS_STDIN_PIPE, &error);
  if (!process) {
    g_warning("clipboard-picker: unable to start wl-copy: %s", error->message);
    g_clear_error(&error);
    g_bytes_unref(data);
    g_object_unref(item);
    return;
  }
  app->paste_cancellable = g_cancellable_new();
  app->paste_process = g_object_ref(process);
  g_subprocess_communicate_async(process, data, app->paste_cancellable,
                                 paste_finished, g_object_ref(app));
  g_bytes_unref(data);
  g_object_unref(item);
}

static void on_search_activate(GtkSearchEntry *search, gpointer user_data) {
  (void)search;
  ClipboardPicker *app = user_data;
  guint position = gtk_single_selection_get_selected(app->selection);
  if (position != GTK_INVALID_LIST_POSITION)
    activate_item(app->list_view, position, app);
}

static void apply_search_results(ClipboardPicker *app, GPtrArray *matches) {
  ClipboardItem *selected =
      gtk_single_selection_get_selected_item(app->selection);
  if (selected)
    g_object_ref(selected);

  guint selected_position = GTK_INVALID_LIST_POSITION;
  if (selected) {
    for (guint i = 0; i < matches->len; i++) {
      if (g_ptr_array_index(matches, i) == selected) {
        selected_position = i;
        break;
      }
    }
  }

  app->updating_results = TRUE;
  guint count = g_list_model_get_n_items(G_LIST_MODEL(app->results));
  g_list_store_splice(app->results, 0, count, matches->pdata, matches->len);
  if (matches->len) {
    guint position =
        selected_position == GTK_INVALID_LIST_POSITION ? 0 : selected_position;
    gtk_single_selection_set_selected(app->selection, position);
  }
  app->updating_results = FALSE;

  ClipboardItem *current =
      gtk_single_selection_get_selected_item(app->selection);
  schedule_preview(app, current, SEARCH_PREVIEW_DEBOUNCE_MS);
  g_clear_object(&selected);
}

static void update_search_results(ClipboardPicker *app) {
  if (app->shutting_down)
    return;

  search_cache_prune(app, app->pending_query);
  GPtrArray *cached =
      g_hash_table_lookup(app->search_cache, app->pending_query);
  if (cached) {
    apply_search_results(app, cached);
    return;
  }

  GPtrArray *candidates = search_cache_find_candidates(app);
  GPtrArray *matches = g_ptr_array_new_with_free_func(g_object_unref);
  for (guint i = 0; i < candidates->len; i++) {
    ClipboardItem *item = g_ptr_array_index(candidates, i);
    if (g_strstr_len(item->search_text, -1, app->pending_query))
      g_ptr_array_add(matches, g_object_ref(item));
  }

  search_cache_insert(app, app->pending_query, matches);
  apply_search_results(app, matches);
  g_ptr_array_unref(matches);
}

static void on_search_changed(GtkEditable *editable, gpointer user_data) {
  ClipboardPicker *app = user_data;
  g_free(app->pending_query);
  app->pending_query = g_utf8_casefold(gtk_editable_get_text(editable), -1);
  app->preview_generation++;
  cancel_preview_load(app);
  update_search_results(app);
}

static gboolean on_key_pressed(GtkEventControllerKey *controller, guint keyval,
                               guint keycode, GdkModifierType state,
                               gpointer user_data) {
  (void)controller;
  (void)keycode;
  ClipboardPicker *app = user_data;
  if (keyval == GDK_KEY_Escape) {
    dismiss_picker(app);
    return GDK_EVENT_STOP;
  }

  gboolean ctrl = (state & GDK_CONTROL_MASK) != 0;
  if (ctrl && (keyval == GDK_KEY_n || keyval == GDK_KEY_p)) {
    guint count = g_list_model_get_n_items(G_LIST_MODEL(app->results));
    if (count == 0)
      return GDK_EVENT_STOP;
    guint current = gtk_single_selection_get_selected(app->selection);
    gint direction = keyval == GDK_KEY_n ? 1 : -1;
    gint target = current == GTK_INVALID_LIST_POSITION
                      ? (direction > 0 ? 0 : (gint)count - 1)
                      : (gint)current + direction;
    target = CLAMP(target, 0, (gint)count - 1);
    gtk_single_selection_set_selected(app->selection, target);
    gtk_list_view_scroll_to(app->list_view, target, GTK_LIST_SCROLL_FOCUS,
                            NULL);
    return GDK_EVENT_STOP;
  }
  return GDK_EVENT_PROPAGATE;
}

static gboolean finish_secondary_setup(gpointer user_data) {
  ClipboardPicker *app = user_data;
  app->secondary_setup_id = 0;
  if (app->shutting_down)
    return G_SOURCE_REMOVE;

  app->picture = GTK_PICTURE(gtk_picture_new());
  gtk_picture_set_content_fit(app->picture, GTK_CONTENT_FIT_CONTAIN);
  gtk_stack_add_named(app->preview_stack, GTK_WIDGET(app->picture), "image");

  GtkScrolledWindow *text_scroll =
      GTK_SCROLLED_WINDOW(gtk_scrolled_window_new());
  app->text_preview = GTK_LABEL(gtk_label_new(NULL));
  gtk_widget_add_css_class(GTK_WIDGET(app->text_preview), "text-preview");
  gtk_label_set_selectable(app->text_preview, TRUE);
  gtk_label_set_wrap(app->text_preview, TRUE);
  gtk_label_set_wrap_mode(app->text_preview, PANGO_WRAP_WORD_CHAR);
  gtk_label_set_xalign(app->text_preview, 0.0);
  gtk_label_set_yalign(app->text_preview, 0.0);
  gtk_widget_set_margin_top(GTK_WIDGET(app->text_preview), 16);
  gtk_widget_set_margin_bottom(GTK_WIDGET(app->text_preview), 16);
  gtk_widget_set_margin_start(GTK_WIDGET(app->text_preview), 16);
  gtk_widget_set_margin_end(GTK_WIDGET(app->text_preview), 16);
  gtk_scrolled_window_set_child(text_scroll, GTK_WIDGET(app->text_preview));
  gtk_stack_add_named(app->preview_stack, GTK_WIDGET(text_scroll), "text");

  GtkGrid *metadata = GTK_GRID(gtk_grid_new());
  gtk_widget_add_css_class(GTK_WIDGET(metadata), "metadata");
  gtk_grid_set_column_spacing(metadata, 16);
  gtk_grid_set_row_spacing(metadata, 6);
  const gchar *names[] = {"MIME", "Size", "Details"};
  GtkLabel **values[] = {&app->mime_value, &app->size_value,
                         &app->details_value};
  for (guint i = 0; i < 3; i++) {
    GtkLabel *key = GTK_LABEL(gtk_label_new(names[i]));
    gtk_label_set_xalign(key, 0.0);
    gtk_widget_add_css_class(GTK_WIDGET(key), "metadata-key");
    gtk_grid_attach(metadata, GTK_WIDGET(key), 0, i, 1, 1);
    *values[i] = GTK_LABEL(gtk_label_new(""));
    gtk_label_set_xalign(*values[i], 0.0);
    gtk_grid_attach(metadata, GTK_WIDGET(*values[i]), 1, i, 1, 1);
  }
  gtk_box_append(app->preview_box,
                 GTK_WIDGET(gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)));
  gtk_box_append(app->preview_box, GTK_WIDGET(metadata));

  g_signal_connect(app->selection, "notify::selected-item",
                   G_CALLBACK(selection_changed), app);
  load_history_async(app);
  gtk_stack_set_visible_child_name(app->preview_stack, "empty");
  return G_SOURCE_REMOVE;
}

static void refresh_history(ClipboardPicker *app) {
  if (app->history_loading || !app->results)
    return;
  app->preview_generation++;
  cancel_preview_load(app);
  g_clear_object(&app->displayed_preview_item);
  app->updating_results = TRUE;
  g_list_store_remove_all(app->results);
  app->updating_results = FALSE;
  g_clear_pointer(&app->search_items, g_ptr_array_unref);
  app->search_items = g_ptr_array_new_with_free_func(g_object_unref);
  search_cache_reset(app);
  update_search_results(app);
  gtk_label_set_text(app->empty_label, "Loading clipboard history…");
  gtk_stack_set_visible_child_name(app->preview_stack, "empty");
  load_history_async(app);
}

static gboolean refresh_history_idle(gpointer user_data) {
  ClipboardPicker *app = user_data;
  app->history_refresh_id = 0;
  if (!app->shutting_down)
    refresh_history(app);
  return G_SOURCE_REMOVE;
}

static void clipboard_picker_activate(GApplication *application) {
  ClipboardPicker *app = (ClipboardPicker *)application;
  load_focused_app_async(app);
  if (app->window) {
    gtk_window_present(app->window);
    gtk_editable_set_text(GTK_EDITABLE(app->search), "");
    gtk_widget_grab_focus(GTK_WIDGET(app->search));
    if (!app->history_refresh_id && !app->secondary_setup_id) {
      app->history_refresh_id =
          g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, refresh_history_idle,
                          g_object_ref(app), g_object_unref);
    }
    return;
  }

  GtkApplicationWindow *window = GTK_APPLICATION_WINDOW(
      gtk_application_window_new(GTK_APPLICATION(application)));
  app->window = GTK_WINDOW(window);
  gtk_window_set_title(app->window, "Clipboard History (C)");
  gtk_window_set_decorated(app->window, FALSE);
  g_signal_connect(app->window, "close-request",
                   G_CALLBACK(on_window_close_requested), app);
  gtk_widget_add_css_class(GTK_WIDGET(window), "clipboard-picker");
  gtk_window_set_default_size(GTK_WINDOW(window), 900, 520);

  GtkBox *root = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 12));
  gtk_widget_set_margin_top(GTK_WIDGET(root), 12);
  gtk_widget_set_margin_bottom(GTK_WIDGET(root), 12);
  gtk_widget_set_margin_start(GTK_WIDGET(root), 12);
  gtk_widget_set_margin_end(GTK_WIDGET(root), 12);
  gtk_window_set_child(GTK_WINDOW(window), GTK_WIDGET(root));

  app->search = GTK_SEARCH_ENTRY(gtk_search_entry_new());
  gtk_search_entry_set_placeholder_text(app->search,
                                        "Search clipboard history…");
  g_signal_connect(app->search, "changed", G_CALLBACK(on_search_changed), app);
  g_signal_connect(app->search, "activate", G_CALLBACK(on_search_activate),
                   app);
  gtk_box_append(root, GTK_WIDGET(app->search));

  GtkPaned *panes = GTK_PANED(gtk_paned_new(GTK_ORIENTATION_HORIZONTAL));
  gtk_widget_add_css_class(GTK_WIDGET(panes), "content-panes");
  gtk_paned_set_position(panes, 420);
  gtk_widget_set_vexpand(GTK_WIDGET(panes), TRUE);
  gtk_box_append(root, GTK_WIDGET(panes));

  GtkScrolledWindow *history = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new());
  gtk_widget_add_css_class(GTK_WIDGET(history), "history");
  GtkListItemFactory *factory = gtk_signal_list_item_factory_new();
  g_signal_connect(factory, "setup", G_CALLBACK(factory_setup), app);
  g_signal_connect(factory, "bind", G_CALLBACK(factory_bind), app);

  app->results = g_list_store_new(clipboard_item_get_type());
  app->selection =
      gtk_single_selection_new(G_LIST_MODEL(g_object_ref(app->results)));
  GtkListView *list = GTK_LIST_VIEW(
      gtk_list_view_new(GTK_SELECTION_MODEL(app->selection), factory));
  app->list_view = list;
  g_signal_connect(list, "activate", G_CALLBACK(activate_item), app);
  gtk_scrolled_window_set_child(history, GTK_WIDGET(list));
  gtk_paned_set_start_child(panes, GTK_WIDGET(history));

  app->preview_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
  gtk_widget_add_css_class(GTK_WIDGET(app->preview_box), "preview");
  app->preview_stack = GTK_STACK(gtk_stack_new());
  gtk_widget_set_hexpand(GTK_WIDGET(app->preview_stack), TRUE);
  gtk_widget_set_vexpand(GTK_WIDGET(app->preview_stack), TRUE);
  gtk_box_append(app->preview_box, GTK_WIDGET(app->preview_stack));
  app->empty_label = GTK_LABEL(gtk_label_new("Loading clipboard history…"));
  gtk_stack_add_named(app->preview_stack, GTK_WIDGET(app->empty_label),
                      "empty");
  gtk_paned_set_end_child(panes, GTK_WIDGET(app->preview_box));

  GtkCssProvider *css = gtk_css_provider_new();
  gtk_css_provider_load_from_string(
      css,
      "window.clipboard-picker { background-color: #0f0f0f; border-radius: "
      "10px; }\n"
      ".content-panes > separator { min-width: 12px; background: transparent; "
      "}\n"
      ".history, .preview { background-color: #1b1b1b; "
      "border: 1px solid rgba(255, 255, 255, 0.10); border-radius: 8px; }\n"
      ".preview stack, .preview scrolledwindow, .preview picture, "
      ".preview scrolledwindow viewport, .history scrolledwindow viewport { "
      "background-color: #1b1b1b; }\n"
      ".history-item { padding: 9px 10px; }\n"
      ".metadata { padding: 10px 12px; }\n"
      ".metadata-key { color: rgba(255, 255, 255, 0.55); }\n"
      ".text-preview { background-color: #0f0f0f; }");
  gtk_style_context_add_provider_for_display(
      gdk_display_get_default(), GTK_STYLE_PROVIDER(css),
      GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
  g_object_unref(css);

  GtkEventController *keys = gtk_event_controller_key_new();
  gtk_event_controller_set_propagation_phase(keys, GTK_PHASE_CAPTURE);
  g_signal_connect(keys, "key-pressed", G_CALLBACK(on_key_pressed), app);
  gtk_widget_add_controller(GTK_WIDGET(window), keys);

  gtk_window_present(GTK_WINDOW(window));
  gtk_widget_grab_focus(GTK_WIDGET(app->search));
  app->secondary_setup_id =
      g_timeout_add_full(G_PRIORITY_LOW, 16, finish_secondary_setup,
                         g_object_ref(app), g_object_unref);
}

static void clipboard_picker_init(ClipboardPicker *app) {
  app->pending_query = g_strdup("");
  app->search_items = g_ptr_array_new_with_free_func(g_object_unref);
  app->search_cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
                                            (GDestroyNotify)g_ptr_array_unref);
  search_cache_reset(app);
  app->preview_cache = g_hash_table_new(g_str_hash, g_str_equal);
  g_queue_init(&app->preview_cache_lru);
}

static void clipboard_picker_shutdown(GApplication *application) {
  ClipboardPicker *app = (ClipboardPicker *)application;
  app->shutting_down = TRUE;

  cancel_preview_load(app);
  g_clear_handle_id(&app->paste_timeout_id, g_source_remove);
  g_clear_handle_id(&app->secondary_setup_id, g_source_remove);
  g_clear_handle_id(&app->history_refresh_id, g_source_remove);
  if (app->focused_app_cancellable)
    g_cancellable_cancel(app->focused_app_cancellable);
  if (app->history_cancellable)
    g_cancellable_cancel(app->history_cancellable);
  if (app->history_process)
    g_subprocess_force_exit(app->history_process);
  if (app->paste_cancellable)
    g_cancellable_cancel(app->paste_cancellable);
  if (app->paste_process)
    g_subprocess_force_exit(app->paste_process);

  G_APPLICATION_CLASS(clipboard_picker_parent_class)->shutdown(application);
}

static void clipboard_picker_finalize(GObject *object) {
  ClipboardPicker *app = (ClipboardPicker *)object;
  g_free(app->focused_app);
  g_free(app->pending_query);
  g_clear_object(&app->pending_preview_item);
  g_clear_object(&app->displayed_preview_item);
  g_clear_object(&app->focused_app_cancellable);
  g_clear_object(&app->history_cancellable);
  g_clear_object(&app->history_process);
  g_clear_object(&app->preview_cancellable);
  g_clear_object(&app->preview_process);
  g_clear_object(&app->paste_cancellable);
  g_clear_object(&app->paste_process);
  g_clear_object(&app->results);
  g_clear_pointer(&app->search_items, g_ptr_array_unref);
  g_clear_pointer(&app->search_cache, g_hash_table_unref);
  preview_cache_clear(app);
  g_clear_pointer(&app->preview_cache, g_hash_table_unref);
  G_OBJECT_CLASS(clipboard_picker_parent_class)->finalize(object);
}

static void clipboard_picker_startup(GApplication *application) {
  G_APPLICATION_CLASS(clipboard_picker_parent_class)->startup(application);
  if ((g_application_get_flags(application) & G_APPLICATION_IS_SERVICE) != 0)
    g_application_hold(application);
}

static void clipboard_picker_class_init(ClipboardPickerClass *class) {
  GObjectClass *object_class = G_OBJECT_CLASS(class);
  object_class->finalize = clipboard_picker_finalize;
  GApplicationClass *application_class = G_APPLICATION_CLASS(class);
  application_class->startup = clipboard_picker_startup;
  application_class->activate = clipboard_picker_activate;
  application_class->shutdown = clipboard_picker_shutdown;
}

int main(int argc, char **argv) {
  /* Work around GTK partial-redraw artifacts across renderers:
   * https://gitlab.gnome.org/GNOME/gtk/-/work_items/8339 */
  g_setenv("GSK_DEBUG", "full-redraw", TRUE);
  ClipboardPicker *app =
      g_object_new(clipboard_picker_get_type(), "application-id", APP_ID,
                   "flags", G_APPLICATION_DEFAULT_FLAGS, NULL);
  int status = g_application_run(G_APPLICATION(app), argc, argv);
  g_object_unref(app);
  return status;
}

README.md

clipboard-picker/README.md · 83 lines

# Clipboard Picker

This is the active C/GTK4 clipboard picker. It compiles with GCC, GTK4, GLib, and JSON-GLib
development files.

It provides:

- Cliphist history in a virtualized `Gtk.ListView`.
- Cancellable worker-thread search with generation-safe result updates.
- Text and image previews.
- MIME, size, and content details.
- `Ctrl+N`/`Ctrl+P` and arrow-key list navigation.
- `Escape` to quit and activation to copy with `wl-copy`.
- Terminal-aware paste injection through `wtype`.
- The `com.erick.ClipboardPicker` app ID for Sway window matching.
- Full redraws forced to work around partial-redraw artifacts across renderers
  ([GTK issue #8339](https://gitlab.gnome.org/GNOME/gtk/-/work_items/8339)).
- Binary-safe decoding for image previews and clipboard writes.
- A 12px transparent gap between the history and preview panels.

## Build locally

From this directory, with GTK4 and JSON-GLib development files installed:

```bash
cc -O3 -march=native -Wall -Wextra -o clipboard-picker main.c \
  $(pkg-config --cflags --libs gtk4 gio-2.0 json-glib-1.0)
./clipboard-picker
```

On NixOS, enter a temporary development shell first:

```bash
nix-shell -p gcc pkg-config gtk4 glib.dev json-glib --run bash
```

Then compile and run it:

```bash
cc -O3 -march=native -Wall -Wextra -o clipboard-picker main.c \
  $(pkg-config --cflags --libs gtk4 gio-2.0 json-glib-1.0)
./clipboard-picker
```

History loading, parsing, and preview decoding are asynchronous. Preview loading is debounced, and
decoded entries are retained in a bounded LRU cache for faster navigation.

Manual local builds use `-O3 -march=native`, which optimizes for the local CPU and is not portable
between machines. The Nix package uses `-O3` because Nix deliberately rejects native CPU flags for
reproducible builds.

## Install with Nix

Build the package from this directory:

```bash
nix-build default.nix
./result/bin/clipboard-picker
```

Install it into the current Nix profile with:

```bash
nix-env -if default.nix
```

## Resident user service

The NixOS configuration starts the picker with `--gapplication-service`. This preloads GTK without
opening a window. Running the normal executable then activates the resident process over D-Bus.
Escape and successful paste actions hide the resident window instead of stopping the service.

Build the package before starting the service, then rebuild NixOS:

```bash
nix-build default.nix
sudo nixos-rebuild switch --flake ../nixos
systemctl --user restart clipboard-picker.service
```

The package includes GTK4, GLib, and JSON-GLib build dependencies plus runtime paths for Cliphist,
`wl-copy`, Sway, and `wtype`. Focused-application detection uses Sway's Unix IPC directly, without
`jq` or `swaymsg`.

Emacs Open documented config →

early-init.el

.emacs.d/early-init.el · 43 lines

;;; early-init.el --- early init code -*- lexical-binding: t -*-
;;; Commentary:

;; only show menu bar when running Emacs with GUI on macOS
(unless (and (string-equal system-type "darwin") (display-graphic-p))
  (menu-bar-mode -1))

;;; Code:
;; Hide vertical scroll bar
(scroll-bar-mode -1)

;; Disable package.el in favor of elpaca.el
(setq package-enable-at-startup nil)

;; Hide toolbar
(tool-bar-mode -1)

;; Set transparency
(set-frame-parameter nil 'alpha 95)

;; Set fullscreen via default-frame-alist to avoid a post-init frame
;; resize.
(add-to-list 'default-frame-alist '(fullscreen . maximized))

;; Bump eval depth — yaml.el's giant pcase grammar exceeds default
;; 1600 during eager macro expansion
(setopt max-lisp-eval-depth 5000)

;; Change GC config to speed up startup time
;; Copied from https://github.com/hlissner/doom-emacs/issues/310#issuecomment-354424413
(defvar last-file-name-handler-alist file-name-handler-alist)
(setq gc-cons-threshold most-positive-fixnum
      gc-cons-percentage 0.6
      file-name-handler-alist nil)

;; Restore GC previous config
(add-hook 'emacs-startup-hook (lambda ()
                                (setq gc-cons-threshold (* 100 1024 1024)
                                      gc-cons-percentage 0.1
                                      file-name-handler-alist last-file-name-handler-alist)))

(provide 'early-init)
;;; early-init.el ends here

elpaca-lockfile.eld

.emacs.d/elpaca-lockfile.eld · 1,673 lines

((annalist :source "elpaca-menu-lock-file" :recipe
	   (:package "annalist" :fetcher github :repo
		     "noctuid/annalist.el" :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "e1ef5dad75fa502d761f70d9ddf1aeb1c423f41d" :id
		     annalist :type git))
 (cider :source "elpaca-menu-lock-file" :recipe
	(:package "cider" :fetcher github :repo "clojure-emacs/cider"
		  :files
		  ("lisp/*.el" "bin/*.sh" "*.el" "clojure.sh"
		   "lein.sh" (:exclude ".dir-locals.el"))
		  :old-names (nrepl) :source "elpaca-menu-lock-file"
		  :protocol https :inherit t :depth treeless :ref
		  "eb78711b7fb0cb89a9eb1b854aabb174b705f7b2" :id cider
		  :type git))
 (clj-refactor :source "elpaca-menu-lock-file" :recipe
	       (:package "clj-refactor" :fetcher github :repo
			 "clojure-emacs/clj-refactor.el" :files
			 (:defaults "CHANGELOG.md") :source
			 "elpaca-menu-lock-file" :protocol https
			 :inherit t :depth treeless :ref
			 "dc1bbc8cdaa723bdbb6669ea7d280625c370755d"
			 :id clj-refactor :type git))
 (cloak-mode :source "elpaca-menu-lock-file" :recipe
	     (:package "cloak-mode" :fetcher github :repo
		       "erickgnavar/cloak-mode" :files
		       ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			"*.texinfo" "doc/dir" "doc/*.info"
			"doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			"docs/dir" "docs/*.info" "docs/*.texi"
			"docs/*.texinfo"
			(:exclude ".dir-locals.el" "test.el"
				  "tests.el" "*-test.el" "*-tests.el"
				  "LICENSE" "README*" "*-pkg.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "8a8d06a280c340a7e1639f60c8c44cd4b4127e6b" :id
		       cloak-mode :type git))
 (clojure-mode :source "elpaca-menu-lock-file" :recipe
	       (:package "clojure-mode" :repo
			 "clojure-emacs/clojure-mode" :fetcher github
			 :files ("clojure-mode.el") :source
			 "elpaca-menu-lock-file" :protocol https
			 :inherit t :depth treeless :ref
			 "eabe29b076fff19db552d36d4babaa36ecf0b704"
			 :id clojure-mode :type git))
 (color-theme-sanityinc-tomorrow :source "elpaca-menu-lock-file"
				 :recipe
				 (:package
				  "color-theme-sanityinc-tomorrow"
				  :repo
				  "purcell/color-theme-sanityinc-tomorrow"
				  :fetcher github :files
				  ("*.el" "*.el.in" "dir" "*.info"
				   "*.texi" "*.texinfo" "doc/dir"
				   "doc/*.info" "doc/*.texi"
				   "doc/*.texinfo" "lisp/*.el"
				   "docs/dir" "docs/*.info"
				   "docs/*.texi" "docs/*.texinfo"
				   (:exclude ".dir-locals.el"
					     "test.el" "tests.el"
					     "*-test.el" "*-tests.el"
					     "LICENSE" "README*"
					     "*-pkg.el"))
				  :source "elpaca-menu-lock-file"
				  :protocol https :inherit t :depth
				  treeless :ref
				  "9ce4bc783acc4ebebed1b13427873cdffc1b7960"
				  :id color-theme-sanityinc-tomorrow
				  :type git))
 (company :source "elpaca-menu-lock-file" :recipe
	  (:package "company" :fetcher github :repo
		    "company-mode/company-mode" :files
		    (:defaults "icons"
			       ("images/small"
				"doc/images/small/*.png"))
		    :source "elpaca-menu-lock-file" :protocol https
		    :inherit t :depth treeless :ref
		    "1bf7dcd3ce1c8e192a402360930ca18790fec5d4" :id
		    company :type git))
 (compat :source "elpaca-menu-lock-file" :recipe
	 (:package "compat" :fetcher github :repo
		   "emacs-compat/compat" :files
		   ("compat.el" "compat-*.el" "compat.texi") :source
		   "elpaca-menu-lock-file" :protocol https :inherit t
		   :depth treeless :ref
		   "ad1806f7f677c4e295ed1e603ae6a6a5d9f9d399" :id
		   compat :type git))
 (cond-let
   :source "elpaca-menu-lock-file" :recipe
   (:package "cond-let" :fetcher github :repo "tarsius/cond-let"
	     :files
	     ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
	      "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
	      "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
	      "docs/*.texinfo"
	      (:exclude ".dir-locals.el" "test.el" "tests.el"
			"*-test.el" "*-tests.el" "LICENSE" "README*"
			"*-pkg.el"))
	     :source "elpaca-menu-lock-file" :id cond-let :type git
	     :protocol https :inherit t :depth treeless :ref
	     "21b9e9835756ff5cd1acb971cf9eb56fff671c8b"))
 (consult :source "elpaca-menu-lock-file" :recipe
	  (:package "consult" :repo "minad/consult" :fetcher github
		    :files
		    ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		     "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		     "doc/*.texinfo" "lisp/*.el" "docs/dir"
		     "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		     (:exclude ".dir-locals.el" "test.el" "tests.el"
			       "*-test.el" "*-tests.el" "LICENSE"
			       "README*" "*-pkg.el"))
		    :source "elpaca-menu-lock-file" :protocol https
		    :inherit t :depth treeless :ref
		    "74e6cc77609cb36cd61aa2a23848659fbb66f05a" :id
		    consult :type git))
 (consult-yasnippet :source "elpaca-menu-lock-file" :recipe
		    (:package "consult-yasnippet" :fetcher github
			      :repo "mohkale/consult-yasnippet" :files
			      ("*.el" "*.el.in" "dir" "*.info"
			       "*.texi" "*.texinfo" "doc/dir"
			       "doc/*.info" "doc/*.texi"
			       "doc/*.texinfo" "lisp/*.el" "docs/dir"
			       "docs/*.info" "docs/*.texi"
			       "docs/*.texinfo"
			       (:exclude ".dir-locals.el" "test.el"
					 "tests.el" "*-test.el"
					 "*-tests.el" "LICENSE"
					 "README*" "*-pkg.el"))
			      :source "elpaca-menu-lock-file"
			      :protocol https :inherit t :depth
			      treeless :ref
			      "834d39acfe8a7d2c304afbe4d649b9372118c756"
			      :id consult-yasnippet :type git))
 (csv-mode :source "elpaca-menu-lock-file" :recipe
	   (:package "csv-mode" :repo
		     ("https://git.savannah.gnu.org/git/emacs/elpa.git"
		      . "csv-mode")
		     :branch "externals/csv-mode" :files
		     ("*" (:exclude ".git")) :source
		     "elpaca-menu-lock-file" :protocol https :inherit
		     t :depth treeless :ref
		     "849ce3e754f291c3643bc36ed802226606955c3f" :id
		     csv-mode :type git))
 (d2-mode :source "elpaca-menu-lock-file" :recipe
	  (:package "d2-mode" :repo "andorsk/d2-mode" :fetcher github
		    :files
		    ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		     "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		     "doc/*.texinfo" "lisp/*.el" "docs/dir"
		     "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		     (:exclude ".dir-locals.el" "test.el" "tests.el"
			       "*-test.el" "*-tests.el" "LICENSE"
			       "README*" "*-pkg.el"))
		    :source "elpaca-menu-lock-file" :protocol https
		    :inherit t :depth treeless :ref
		    "e1fc7d6c1915acaf476060c0f79b8bdef6bd1952" :id
		    d2-mode :type git))
 (dart-mode :source "elpaca-menu-lock-file" :recipe
	    (:package "dart-mode" :fetcher github :repo
		      "emacsorphanage/dart-mode" :files
		      ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		       "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		       "doc/*.texinfo" "lisp/*.el" "docs/dir"
		       "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		       (:exclude ".dir-locals.el" "test.el" "tests.el"
				 "*-test.el" "*-tests.el" "LICENSE"
				 "README*" "*-pkg.el"))
		      :source "elpaca-menu-lock-file" :protocol https
		      :inherit t :depth treeless :ref
		      "02e919c1cf200b4938139c18068577faff1fb364" :id
		      dart-mode :type git))
 (dash :source "elpaca-menu-lock-file" :recipe
       (:package "dash" :fetcher github :repo "magnars/dash.el" :files
		 ("dash.el" "dash.texi") :source
		 "elpaca-menu-lock-file" :protocol https :inherit t
		 :depth treeless :ref
		 "1de9dcb83eacfb162b6d9a118a4770b1281bcd84" :id dash
		 :type git))
 (denote :source "elpaca-menu-lock-file" :recipe
	 (:package "denote" :repo
		   ("https://github.com/protesilaos/denote" . "denote")
		   :files
		   ("*" (:exclude ".git" "COPYING" "doclicense.texi"))
		   :source "elpaca-menu-lock-file" :protocol https
		   :inherit t :depth treeless :ref
		   "212a1b3c17ad99986e593901b3385c9c581b70e4" :id
		   denote :type git))
 (diff-hl :source "elpaca-menu-lock-file" :recipe
	  (:package "diff-hl" :fetcher github :repo "dgutov/diff-hl"
		    :files
		    ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		     "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		     "doc/*.texinfo" "lisp/*.el" "docs/dir"
		     "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		     (:exclude ".dir-locals.el" "test.el" "tests.el"
			       "*-test.el" "*-tests.el" "LICENSE"
			       "README*" "*-pkg.el"))
		    :source "elpaca-menu-lock-file" :protocol https
		    :inherit t :depth treeless :ref
		    "685ea2d50607a9902f1c6c141ee6268d759b9b25" :id
		    diff-hl :type git))
 (dired-hacks-utils :source "elpaca-menu-lock-file" :recipe
		    (:package "dired-hacks-utils" :fetcher github
			      :repo "Fuco1/dired-hacks" :files
			      ("dired-hacks-utils.el") :source
			      "elpaca-menu-lock-file" :protocol https
			      :inherit t :depth treeless :ref
			      "e9e408e8571aee5574ca0a431ef15cac5a3585d4"
			      :id dired-hacks-utils :type git))
 (dired-preview :source "elpaca-menu-lock-file" :recipe
		(:package "dired-preview" :repo
			  ("https://github.com/protesilaos/dired-preview"
			   . "dired-preview")
			  :files
			  ("*"
			   (:exclude ".git" "COPYING"
				     "doclicense.texi"))
			  :source "elpaca-menu-lock-file" :protocol
			  https :inherit t :depth treeless :ref
			  "76e2d2a6675807f41c18c2f000dfaec35ed59100"
			  :id dired-preview :type git))
 (dired-subtree :source "elpaca-menu-lock-file" :recipe
		(:package "dired-subtree" :fetcher github :repo
			  "Fuco1/dired-hacks" :files
			  ("dired-subtree.el") :source
			  "elpaca-menu-lock-file" :protocol https
			  :inherit t :depth treeless :ref
			  "e9e408e8571aee5574ca0a431ef15cac5a3585d4"
			  :id dired-subtree :type git))
 (doom-modeline :source "elpaca-menu-lock-file" :recipe
		(:package "doom-modeline" :repo
			  "seagle0128/doom-modeline" :fetcher github
			  :files
			  ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			   "*.texinfo" "doc/dir" "doc/*.info"
			   "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			   "docs/dir" "docs/*.info" "docs/*.texi"
			   "docs/*.texinfo"
			   (:exclude ".dir-locals.el" "test.el"
				     "tests.el" "*-test.el"
				     "*-tests.el" "LICENSE" "README*"
				     "*-pkg.el"))
			  :source "elpaca-menu-lock-file" :protocol
			  https :inherit t :depth treeless :ref
			  "33edc978783a311479ca8edf3f7437d7e930eb10"
			  :id doom-modeline :type git))
 (dumb-jump :source "elpaca-menu-lock-file" :recipe
	    (:package "dumb-jump" :repo "jacktasia/dumb-jump" :fetcher
		      github :files
		      ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		       "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		       "doc/*.texinfo" "lisp/*.el" "docs/dir"
		       "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		       (:exclude ".dir-locals.el" "test.el" "tests.el"
				 "*-test.el" "*-tests.el" "LICENSE"
				 "README*" "*-pkg.el"))
		      :source "elpaca-menu-lock-file" :protocol https
		      :inherit t :depth treeless :ref
		      "9f3967b0bea038c37d4a6c6bb96144574bb706ee" :id
		      dumb-jump :type git))
 (edit-indirect :source "elpaca-menu-lock-file" :recipe
		(:package "edit-indirect" :fetcher github :repo
			  "Fanael/edit-indirect" :files
			  ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			   "*.texinfo" "doc/dir" "doc/*.info"
			   "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			   "docs/dir" "docs/*.info" "docs/*.texi"
			   "docs/*.texinfo"
			   (:exclude ".dir-locals.el" "test.el"
				     "tests.el" "*-test.el"
				     "*-tests.el" "LICENSE" "README*"
				     "*-pkg.el"))
			  :source "elpaca-menu-lock-file" :protocol
			  https :inherit t :depth treeless :ref
			  "82a28d8a85277cfe453af464603ea330eae41c05"
			  :id edit-indirect :type git))
 (eglot-java :source "elpaca-menu-lock-file" :recipe
	     (:package "eglot-java" :repo "yveszoundi/eglot-java"
		       :fetcher github :files
		       ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			"*.texinfo" "doc/dir" "doc/*.info"
			"doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			"docs/dir" "docs/*.info" "docs/*.texi"
			"docs/*.texinfo"
			(:exclude ".dir-locals.el" "test.el"
				  "tests.el" "*-test.el" "*-tests.el"
				  "LICENSE" "README*" "*-pkg.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "b42b5190f3f59976d330fcec5fd27fc8e2701336" :id
		       eglot-java :type git))
 (elisp-refs :source "elpaca-menu-lock-file" :recipe
	     (:package "elisp-refs" :repo "Wilfred/elisp-refs"
		       :fetcher github :files
		       (:defaults (:exclude "elisp-refs-bench.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "541a064c3ce27867872cf708354a65d83baf2a6d" :id
		       elisp-refs :type git))
 (elm-mode :source "elpaca-menu-lock-file" :recipe
	   (:package "elm-mode" :fetcher github :repo
		     "jcollard/elm-mode" :files (:defaults "elm.tags")
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "699841865e1bd5b7f2077baa7121510b6bcad3c7" :id
		     elm-mode :type git))
 (elpaca :source
   "elpaca-menu-lock-file" :recipe
   (:source nil :package "elpaca" :id elpaca :repo
	    "https://github.com/progfolio/elpaca.git" :ref
	    "e974e6f4b5ea2e974f4d7686c5d956aff455594d" :depth nil
	    :inherit ignore :files
	    (:defaults "elpaca-test.el" (:exclude "extensions"))
	    :build (:not elpaca-activate) :type git :protocol https))
 (elpaca-use-package :source "elpaca-menu-lock-file" :recipe
		     (:package "elpaca-use-package" :wait t :repo
			       "https://github.com/progfolio/elpaca.git"
			       :files
			       ("extensions/elpaca-use-package.el")
			       :main
			       "extensions/elpaca-use-package.el"
			       :build (:not elpaca--compile-info)
			       :source "elpaca-menu-lock-file"
			       :protocol https :inherit t :depth
			       treeless :ref
			       "e974e6f4b5ea2e974f4d7686c5d956aff455594d"
			       :id elpaca-use-package :type git))
 (embark :source "elpaca-menu-lock-file" :recipe
	 (:package "embark" :repo "oantolin/embark" :fetcher github
		   :files ("embark.el" "embark-org.el" "embark.texi")
		   :source "elpaca-menu-lock-file" :protocol https
		   :inherit t :depth treeless :ref
		   "195add1f1ccd1059472c9df7334c97c4d155425e" :id
		   embark :type git))
 (embark-consult :source "elpaca-menu-lock-file" :recipe
		 (:package "embark-consult" :repo "oantolin/embark"
			   :fetcher github :files
			   ("embark-consult.el") :source
			   "elpaca-menu-lock-file" :protocol https
			   :inherit t :depth treeless :ref
			   "195add1f1ccd1059472c9df7334c97c4d155425e"
			   :id embark-consult :type git))
 (emmet-mode :source "elpaca-menu-lock-file" :recipe
	     (:package "emmet-mode" :fetcher github :repo
		       "smihica/emmet-mode" :files
		       ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			"*.texinfo" "doc/dir" "doc/*.info"
			"doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			"docs/dir" "docs/*.info" "docs/*.texi"
			"docs/*.texinfo"
			(:exclude ".dir-locals.el" "test.el"
				  "tests.el" "*-test.el" "*-tests.el"
				  "LICENSE" "README*" "*-pkg.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "322d3bb112fced57d63b44863357f7a0b7eee1e3" :id
		       emmet-mode :type git))
 (erlang-ts-mode :source "elpaca-menu-lock-file" :recipe
		 (:source "elpaca-menu-lock-file" :protocol https
			  :inherit t :depth treeless :host github
			  :repo "sebastiw/erlang-ts-mode" :package
			  "erlang-ts-mode" :ref
			  "ba5a01a2d7731a6fbf6129c6c8556fbceeb7d8a9"
			  :id erlang-ts-mode :type git))
 (evil :source "elpaca-menu-lock-file" :recipe
       (:package "evil" :repo "emacs-evil/evil" :fetcher github :files
		 (:defaults "doc/build/texinfo/evil.texi"
			    (:exclude "evil-test-helpers.el"))
		 :source "elpaca-menu-lock-file" :protocol https
		 :inherit t :depth treeless :ref
		 "729d9a58b387704011a115c9200614e32da3cefc" :id evil
		 :type git))
 (evil-collection :source "elpaca-menu-lock-file" :recipe
		  (:package "evil-collection" :fetcher github :repo
			    "emacs-evil/evil-collection" :files
			    (:defaults "modes") :source
			    "elpaca-menu-lock-file" :protocol https
			    :inherit t :depth treeless :ref
			    "7ab9179591bc2f9474f480f52ec8dc44cf0a05f1"
			    :id evil-collection :type git))
 (evil-commentary :source "elpaca-menu-lock-file" :recipe
		  (:package "evil-commentary" :repo
			    "linktohack/evil-commentary" :fetcher
			    github :files
			    ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			     "*.texinfo" "doc/dir" "doc/*.info"
			     "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			     "docs/dir" "docs/*.info" "docs/*.texi"
			     "docs/*.texinfo"
			     (:exclude ".dir-locals.el" "test.el"
				       "tests.el" "*-test.el"
				       "*-tests.el" "LICENSE"
				       "README*" "*-pkg.el"))
			    :source "elpaca-menu-lock-file" :protocol
			    https :inherit t :depth treeless :ref
			    "c5945f28ce47644c828aac1f5f6ec335478d17fb"
			    :id evil-commentary :type git))
 (evil-matchit :source "elpaca-menu-lock-file" :recipe
	       (:package "evil-matchit" :fetcher github :repo
			 "redguardtoo/evil-matchit" :files
			 ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			  "*.texinfo" "doc/dir" "doc/*.info"
			  "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			  "docs/dir" "docs/*.info" "docs/*.texi"
			  "docs/*.texinfo"
			  (:exclude ".dir-locals.el" "test.el"
				    "tests.el" "*-test.el"
				    "*-tests.el" "LICENSE" "README*"
				    "*-pkg.el"))
			 :source "elpaca-menu-lock-file" :protocol
			 https :inherit t :depth treeless :ref
			 "8a94e56bd2987c6d0c1a85fbf189f9a86e623cde"
			 :id evil-matchit :type git))
 (evil-surround :source "elpaca-menu-lock-file" :recipe
		(:package "evil-surround" :repo
			  "emacs-evil/evil-surround" :fetcher github
			  :old-names (surround) :files
			  ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			   "*.texinfo" "doc/dir" "doc/*.info"
			   "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			   "docs/dir" "docs/*.info" "docs/*.texi"
			   "docs/*.texinfo"
			   (:exclude ".dir-locals.el" "test.el"
				     "tests.el" "*-test.el"
				     "*-tests.el" "LICENSE" "README*"
				     "*-pkg.el"))
			  :source "elpaca-menu-lock-file" :protocol
			  https :inherit t :depth treeless :ref
			  "da05c60b0621cf33161bb4335153f75ff5c29d91"
			  :id evil-surround :type git))
 (exec-path-from-shell :source "elpaca-menu-lock-file" :recipe
		       (:package "exec-path-from-shell" :fetcher
				 github :repo
				 "purcell/exec-path-from-shell" :files
				 ("*.el" "*.el.in" "dir" "*.info"
				  "*.texi" "*.texinfo" "doc/dir"
				  "doc/*.info" "doc/*.texi"
				  "doc/*.texinfo" "lisp/*.el"
				  "docs/dir" "docs/*.info"
				  "docs/*.texi" "docs/*.texinfo"
				  (:exclude ".dir-locals.el" "test.el"
					    "tests.el" "*-test.el"
					    "*-tests.el" "LICENSE"
					    "README*" "*-pkg.el"))
				 :source "elpaca-menu-lock-file"
				 :protocol https :inherit t :depth
				 treeless :ref
				 "4896a797252fbfdac32fb77508500ac7d220f717"
				 :id exec-path-from-shell :type git))
 (f :source "elpaca-menu-lock-file" :recipe
    (:package "f" :fetcher github :repo "rejeep/f.el" :files
	      ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
	       "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
	       "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
	       "docs/*.texinfo"
	       (:exclude ".dir-locals.el" "test.el" "tests.el"
			 "*-test.el" "*-tests.el" "LICENSE" "README*"
			 "*-pkg.el"))
	      :source "elpaca-menu-lock-file" :protocol https :inherit
	      t :depth treeless :ref
	      "931b6d0667fe03e7bf1c6c282d6d8d7006143c52" :id f :type
	      git))
 (flutter :source "elpaca-menu-lock-file" :recipe
	  (:package "flutter" :repo "amake/flutter.el" :fetcher github
		    :files
		    ("flutter.el" "flutter-project.el"
		     "flutter-l10n.el")
		    :source "elpaca-menu-lock-file" :protocol https
		    :inherit t :depth treeless :ref
		    "e71235d400787d977da7ed792709437899c2a03c" :id
		    flutter :type git))
 (flymake-biome :source "elpaca-menu-lock-file" :recipe
		(:package "flymake-biome" :fetcher github :repo
			  "erickgnavar/flymake-biome" :files
			  ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			   "*.texinfo" "doc/dir" "doc/*.info"
			   "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			   "docs/dir" "docs/*.info" "docs/*.texi"
			   "docs/*.texinfo"
			   (:exclude ".dir-locals.el" "test.el"
				     "tests.el" "*-test.el"
				     "*-tests.el" "LICENSE" "README*"
				     "*-pkg.el"))
			  :source "elpaca-menu-lock-file" :protocol
			  https :inherit t :depth treeless :ref
			  "03fa55d23fdc80fb4bc963cd144da460e7da0220"
			  :id flymake-biome :type git))
 (flymake-markdownlint :source "elpaca-menu-lock-file" :recipe
		       (:package "flymake-markdownlint" :fetcher
				 codeberg :repo
				 "erickgnavar/flymake-markdownlint"
				 :files
				 ("*.el" "*.el.in" "dir" "*.info"
				  "*.texi" "*.texinfo" "doc/dir"
				  "doc/*.info" "doc/*.texi"
				  "doc/*.texinfo" "lisp/*.el"
				  "docs/dir" "docs/*.info"
				  "docs/*.texi" "docs/*.texinfo"
				  (:exclude ".dir-locals.el" "test.el"
					    "tests.el" "*-test.el"
					    "*-tests.el" "LICENSE"
					    "README*" "*-pkg.el"))
				 :source "elpaca-menu-lock-file"
				 :protocol https :inherit t :depth
				 treeless :host github :ref
				 "5167625b9ab958992ca68e225a0763c5c26b4eeb"
				 :id flymake-markdownlint :type git))
 (flymake-ruff :source "elpaca-menu-lock-file" :recipe
	       (:package "flymake-ruff" :fetcher github :repo
			 "erickgnavar/flymake-ruff" :files
			 ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			  "*.texinfo" "doc/dir" "doc/*.info"
			  "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			  "docs/dir" "docs/*.info" "docs/*.texi"
			  "docs/*.texinfo"
			  (:exclude ".dir-locals.el" "test.el"
				    "tests.el" "*-test.el"
				    "*-tests.el" "LICENSE" "README*"
				    "*-pkg.el"))
			 :source "elpaca-menu-lock-file" :protocol
			 https :inherit t :depth treeless :ref
			 "970acdac865d5b6ee8541b82a413f3a107b9332e"
			 :id flymake-ruff :type git))
 (flymake-sqlfluff :source "elpaca-menu-lock-file" :recipe
		   (:package "flymake-sqlfluff" :fetcher github :repo
			     "erickgnavar/flymake-sqlfluff" :files
			     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			      "*.texinfo" "doc/dir" "doc/*.info"
			      "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			      "docs/dir" "docs/*.info" "docs/*.texi"
			      "docs/*.texinfo"
			      (:exclude ".dir-locals.el" "test.el"
					"tests.el" "*-test.el"
					"*-tests.el" "LICENSE"
					"README*" "*-pkg.el"))
			     :source "elpaca-menu-lock-file" :protocol
			     https :inherit t :depth treeless :ref
			     "0a836d7a919723ae5897fce01c3c7d651a30e8c6"
			     :id flymake-sqlfluff :type git))
 (git-backup :source "elpaca-menu-lock-file" :recipe
	     (:package "git-backup" :fetcher github :repo
		       "antham/git-backup" :files
		       ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			"*.texinfo" "doc/dir" "doc/*.info"
			"doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			"docs/dir" "docs/*.info" "docs/*.texi"
			"docs/*.texinfo"
			(:exclude ".dir-locals.el" "test.el"
				  "tests.el" "*-test.el" "*-tests.el"
				  "LICENSE" "README*" "*-pkg.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "67e38c659c918e98642171ba3f385a15182347f4" :id
		       git-backup :type git))
 (git-link :source "elpaca-menu-lock-file" :recipe
	   (:package "git-link" :fetcher github :repo "sshaw/git-link"
		     :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "40f7b1674d2c703199ff2f82b464f17aa6f61b4b" :id
		     git-link :type git))
 (git-modes :source "elpaca-menu-lock-file" :recipe
	    (:package "git-modes" :fetcher github :repo
		      "magit/git-modes" :old-names
		      (gitattributes-mode gitconfig-mode
					  gitignore-mode)
		      :files
		      ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		       "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		       "doc/*.texinfo" "lisp/*.el" "docs/dir"
		       "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		       (:exclude ".dir-locals.el" "test.el" "tests.el"
				 "*-test.el" "*-tests.el" "LICENSE"
				 "README*" "*-pkg.el"))
		      :source "elpaca-menu-lock-file" :protocol https
		      :inherit t :depth treeless :ref
		      "f99010bbeb8b6d8a0819fac0195a2ef0159d08f0" :id
		      git-modes :type git))
 (git-timemachine :source "elpaca-menu-lock-file" :recipe
		  (:package "git-timemachine" :fetcher codeberg :repo
			    "pidu/git-timemachine" :files
			    ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			     "*.texinfo" "doc/dir" "doc/*.info"
			     "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			     "docs/dir" "docs/*.info" "docs/*.texi"
			     "docs/*.texinfo"
			     (:exclude ".dir-locals.el" "test.el"
				       "tests.el" "*-test.el"
				       "*-tests.el" "LICENSE"
				       "README*" "*-pkg.el"))
			    :source "elpaca-menu-lock-file" :protocol
			    https :inherit t :depth treeless :ref
			    "d1346a76122595aeeb7ebb292765841c6cfd417b"
			    :id git-timemachine :type git))
 (goto-chg :source "elpaca-menu-lock-file" :recipe
	   (:package "goto-chg" :repo "emacs-evil/goto-chg" :fetcher
		     github :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "72f556524b88e9d30dc7fc5b0dc32078c166fda7" :id
		     goto-chg :type git))
 (graphql-mode :source "elpaca-menu-lock-file" :recipe
	       (:package "graphql-mode" :repo "davazp/graphql-mode"
			 :fetcher github :files
			 ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			  "*.texinfo" "doc/dir" "doc/*.info"
			  "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			  "docs/dir" "docs/*.info" "docs/*.texi"
			  "docs/*.texinfo"
			  (:exclude ".dir-locals.el" "test.el"
				    "tests.el" "*-test.el"
				    "*-tests.el" "LICENSE" "README*"
				    "*-pkg.el"))
			 :source "elpaca-menu-lock-file" :protocol
			 https :inherit t :depth treeless :ref
			 "ee49531935ede7a2c9597713e13a4c9d33ef2220"
			 :id graphql-mode :type git))
 (haskell-mode :source "elpaca-menu-lock-file" :recipe
	       (:package "haskell-mode" :repo "haskell/haskell-mode"
			 :fetcher github :files
			 (:defaults "NEWS" "logo.svg") :source
			 "elpaca-menu-lock-file" :protocol https
			 :inherit t :depth treeless :ref
			 "495fb9688e12ed1bf945b4876f124ee38b84c039"
			 :id haskell-mode :type git))
 (hcl-mode :source "elpaca-menu-lock-file" :recipe
	   (:package "hcl-mode" :repo "hcl-emacs/hcl-mode" :fetcher
		     github :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "1da895ed75d28d9f87cbf9b74f075d90ba31c0ed" :id
		     hcl-mode :type git))
 (helpful :source "elpaca-menu-lock-file" :recipe
	  (:package "helpful" :repo "Wilfred/helpful" :fetcher github
		    :files
		    ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		     "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		     "doc/*.texinfo" "lisp/*.el" "docs/dir"
		     "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		     (:exclude ".dir-locals.el" "test.el" "tests.el"
			       "*-test.el" "*-tests.el" "LICENSE"
			       "README*" "*-pkg.el"))
		    :source "elpaca-menu-lock-file" :protocol https
		    :inherit t :depth treeless :ref
		    "34328c639ed7aad371a3f57209acad2a5bb66401" :id
		    helpful :type git))
 (hide-mode-line :source "elpaca-menu-lock-file" :recipe
		 (:package "hide-mode-line" :repo
			   "hlissner/emacs-hide-mode-line" :fetcher
			   github :files
			   ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			    "*.texinfo" "doc/dir" "doc/*.info"
			    "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			    "docs/dir" "docs/*.info" "docs/*.texi"
			    "docs/*.texinfo"
			    (:exclude ".dir-locals.el" "test.el"
				      "tests.el" "*-test.el"
				      "*-tests.el" "LICENSE" "README*"
				      "*-pkg.el"))
			   :source "elpaca-menu-lock-file" :protocol
			   https :inherit t :depth treeless :ref
			   "ddd154f1e04d666cd004bf8212ead8684429350d"
			   :id hide-mode-line :type git))
 (highlight-thing :source "elpaca-menu-lock-file" :recipe
		  (:package "highlight-thing" :fetcher github :repo
			    "fgeller/highlight-thing.el" :files
			    ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			     "*.texinfo" "doc/dir" "doc/*.info"
			     "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			     "docs/dir" "docs/*.info" "docs/*.texi"
			     "docs/*.texinfo"
			     (:exclude ".dir-locals.el" "test.el"
				       "tests.el" "*-test.el"
				       "*-tests.el" "LICENSE"
				       "README*" "*-pkg.el"))
			    :source "elpaca-menu-lock-file" :protocol
			    https :inherit t :depth treeless :ref
			    "ad788d7a7ee9eb287a8cca3adb21510b89270dca"
			    :id highlight-thing :type git))
 (htmlize :source "elpaca-menu-lock-file" :recipe
	  (:package "htmlize" :fetcher github :repo
		    "hniksic/emacs-htmlize" :files
		    ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		     "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		     "doc/*.texinfo" "lisp/*.el" "docs/dir"
		     "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		     (:exclude ".dir-locals.el" "test.el" "tests.el"
			       "*-test.el" "*-tests.el" "LICENSE"
			       "README*" "*-pkg.el"))
		    :source "elpaca-menu-lock-file" :protocol https
		    :inherit t :depth treeless :ref
		    "8e3841c837b4b78bd72ad7f0436e919f39315a46" :id
		    htmlize :type git))
 (hydra :source "elpaca-menu-lock-file" :recipe
	(:package "hydra" :repo "abo-abo/hydra" :fetcher github :files
		  (:defaults (:exclude "lv.el")) :source
		  "elpaca-menu-lock-file" :protocol https :inherit t
		  :depth treeless :ref
		  "317e1de33086637579a7aeb60f77ed0405bf359b" :id hydra
		  :type git))
 (hydra-posframe :source "elpaca-menu-lock-file" :recipe
		 (:source "elpaca-menu-lock-file" :protocol https
			  :inherit t :depth treeless :host github
			  :repo "Ladicle/hydra-posframe" :package
			  "hydra-posframe" :ref
			  "142a04dd588af6c725e331863c3ca7bd5dda13ec"
			  :id hydra-posframe :type git))
 (inflections :source "elpaca-menu-lock-file" :recipe
	      (:package "inflections" :repo "eschulte/jump.el"
			:fetcher github :files ("inflections.el")
			:source "elpaca-menu-lock-file" :protocol
			https :inherit t :depth treeless :ref
			"55caa66a7cc6e0b1a76143fd40eff38416928941" :id
			inflections :type git))
 (inheritenv :source "elpaca-menu-lock-file" :recipe
	     (:package "inheritenv" :fetcher github :repo
		       "purcell/inheritenv" :files
		       ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			"*.texinfo" "doc/dir" "doc/*.info"
			"doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			"docs/dir" "docs/*.info" "docs/*.texi"
			"docs/*.texinfo"
			(:exclude ".dir-locals.el" "test.el"
				  "tests.el" "*-test.el" "*-tests.el"
				  "LICENSE" "README*" "*-pkg.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "b9e67cc20c069539698a9ac54d0e6cc11e616c6f" :id
		       inheritenv :type git))
 (jit-spell :source "elpaca-menu-lock-file" :recipe
	    (:package "jit-spell" :repo
		      ("https://github.com/astoff/jit-spell"
		       . "jit-spell")
		      :files ("*" (:exclude ".git")) :source
		      "elpaca-menu-lock-file" :protocol https :inherit
		      t :depth treeless :ref
		      "54f6f69401cbaf3b103e5a2ff4bec34498098ef6" :id
		      jit-spell :type git))
 (jj-mode :source "elpaca-menu-lock-file" :recipe
	  (:source "elpaca-menu-lock-file" :package "jj-mode" :id
		   jj-mode :host github :repo "bolivier/jj-mode.el"
		   :type git :protocol https :inherit t :depth
		   treeless :ref
		   "64a59eda9022080dcd7e86c128bb1bc3c92964df"))
 (just-ts-mode :source "elpaca-menu-lock-file" :recipe
	       (:package "just-ts-mode" :fetcher github :repo
			 "leon-barrett/just-ts-mode.el" :files
			 ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			  "*.texinfo" "doc/dir" "doc/*.info"
			  "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			  "docs/dir" "docs/*.info" "docs/*.texi"
			  "docs/*.texinfo"
			  (:exclude ".dir-locals.el" "test.el"
				    "tests.el" "*-test.el"
				    "*-tests.el" "LICENSE" "README*"
				    "*-pkg.el"))
			 :source "elpaca-menu-lock-file" :protocol
			 https :inherit t :depth treeless :ref
			 "f4fb841f93222fcefccabd641281553b4c8d9d6e"
			 :id just-ts-mode :type git))
 (lfe-mode :source "elpaca-menu-lock-file" :recipe
	   (:package "lfe-mode" :fetcher github :repo "rvirding/lfe"
		     :files
		     ("emacs/inferior-lfe.el" "emacs/lfe-mode.el"
		      "emacs/lfe-indent.el")
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "2fb4de84f81ac822923ddc60eaccc4a938c9dfdc" :id
		     lfe-mode :type git))
 (ligature :source "elpaca-menu-lock-file" :recipe
	   (:package "ligature" :fetcher github :repo
		     "mickeynp/ligature.el" :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "6ac1634612dbd42f7eb81ecaf022bd239aabb954" :id
		     ligature :type git))
 (linkode :source "elpaca-menu-lock-file" :recipe
	  (:package "linkode" :fetcher github :repo
		    "erickgnavar/linkode.el" :files
		    ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		     "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		     "doc/*.texinfo" "lisp/*.el" "docs/dir"
		     "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		     (:exclude ".dir-locals.el" "test.el" "tests.el"
			       "*-test.el" "*-tests.el" "LICENSE"
			       "README*" "*-pkg.el"))
		    :source "elpaca-menu-lock-file" :protocol https
		    :inherit t :depth treeless :ref
		    "8bfb80ca046bc0c942a14effef354722ad8a8a6c" :id
		    linkode :type git))
 (llama :source "elpaca-menu-lock-file" :recipe
	(:package "llama" :fetcher github :repo "tarsius/llama" :files
		  ("llama.el" ".dir-locals.el") :source
		  "elpaca-menu-lock-file" :protocol https :inherit t
		  :depth treeless :ref
		  "d430d48e0b5afd2a34b5531f103dcb110c3539c4" :id llama
		  :type git))
 (lv :source "elpaca-menu-lock-file" :recipe
     (:package "lv" :repo "abo-abo/hydra" :fetcher github :files
	       ("lv.el") :source "elpaca-menu-lock-file" :protocol
	       https :inherit t :depth treeless :ref
	       "317e1de33086637579a7aeb60f77ed0405bf359b" :id lv :type
	       git))
 (magit :source "elpaca-menu-lock-file" :recipe
	(:package "magit" :fetcher github :repo "magit/magit" :files
		  ("lisp/magit*.el" "lisp/git-*.el" "docs/magit.texi"
		   "docs/AUTHORS.md" "LICENSE" ".dir-locals.el"
		   (:exclude "lisp/magit-section.el"))
		  :source "elpaca-menu-lock-file" :protocol https
		  :inherit t :depth treeless :ref
		  "c800f79c2061621fde847f6a53129eca0e8da728" :id magit
		  :type git))
 (magit-delta :source "elpaca-menu-lock-file" :recipe
	      (:package "magit-delta" :fetcher github :repo
			"dandavison/magit-delta" :files
			("*.el" "*.el.in" "dir" "*.info" "*.texi"
			 "*.texinfo" "doc/dir" "doc/*.info"
			 "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			 "docs/dir" "docs/*.info" "docs/*.texi"
			 "docs/*.texinfo"
			 (:exclude ".dir-locals.el" "test.el"
				   "tests.el" "*-test.el" "*-tests.el"
				   "LICENSE" "README*" "*-pkg.el"))
			:source "elpaca-menu-lock-file" :protocol
			https :inherit t :depth treeless :ref
			"5fc7dbddcfacfe46d3fd876172ad02a9ab6ac616" :id
			magit-delta :type git))
 (magit-section :source "elpaca-menu-lock-file" :recipe
		(:package "magit-section" :fetcher github :repo
			  "magit/magit" :files
			  ("lisp/magit-section.el"
			   "docs/magit-section.texi"
			   "magit-section-pkg.el")
			  :source "elpaca-menu-lock-file" :protocol
			  https :inherit t :depth treeless :ref
			  "c800f79c2061621fde847f6a53129eca0e8da728"
			  :id magit-section :type git))
 (major-mode-hydra :source "elpaca-menu-lock-file" :recipe
		   (:package "major-mode-hydra" :repo
			     "jerrypnz/major-mode-hydra.el" :fetcher
			     github :files ("major-mode-hydra.el")
			     :source "elpaca-menu-lock-file" :protocol
			     https :inherit t :depth treeless :wait t
			     :ref
			     "d0a5dadee97c3752fcdef113cf2ba1923972a480"
			     :id major-mode-hydra :type git))
 (marginalia :source "elpaca-menu-lock-file" :recipe
	     (:package "marginalia" :repo "minad/marginalia" :fetcher
		       github :files
		       ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			"*.texinfo" "doc/dir" "doc/*.info"
			"doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			"docs/dir" "docs/*.info" "docs/*.texi"
			"docs/*.texinfo"
			(:exclude ".dir-locals.el" "test.el"
				  "tests.el" "*-test.el" "*-tests.el"
				  "LICENSE" "README*" "*-pkg.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "a527fb03b76a2bce1e360c6e73a095e06922c3f3" :id
		       marginalia :type git))
 (mermaid-mode :source "elpaca-menu-lock-file" :recipe
	       (:package "mermaid-mode" :repo "abrochard/mermaid-mode"
			 :fetcher github :files
			 ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			  "*.texinfo" "doc/dir" "doc/*.info"
			  "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			  "docs/dir" "docs/*.info" "docs/*.texi"
			  "docs/*.texinfo"
			  (:exclude ".dir-locals.el" "test.el"
				    "tests.el" "*-test.el"
				    "*-tests.el" "LICENSE" "README*"
				    "*-pkg.el"))
			 :source "elpaca-menu-lock-file" :protocol
			 https :inherit t :depth treeless :ref
			 "e74d4da7612c7a88e07f9dd3369e3b9fd36f396c"
			 :id mermaid-mode :type git))
 (mini-meme :source "elpaca-menu-lock-file" :recipe
	    (:source "elpaca-menu-lock-file" :package "mini-meme" :id
		     mini-meme :host github :repo
		     "erickgnavar/mini-meme.el" :type git :protocol
		     https :inherit t :depth treeless :ref
		     "079e82c4c85b0fd3dfafb69b6778d4e2a158acad"))
 (mise :source "elpaca-menu-lock-file" :recipe
       (:package "mise" :fetcher github :repo "eki3z/mise.el" :files
		 ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
		  "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
		  "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
		  "docs/*.texinfo"
		  (:exclude ".dir-locals.el" "test.el" "tests.el"
			    "*-test.el" "*-tests.el" "LICENSE"
			    "README*" "*-pkg.el"))
		 :source "elpaca-menu-lock-file" :protocol https
		 :inherit t :depth treeless :ref
		 "60ef63466d07417a9ef956af363baae39c9ddf34" :id mise
		 :type git))
 (multiple-cursors :source "elpaca-menu-lock-file" :recipe
		   (:package "multiple-cursors" :fetcher github :repo
			     "magnars/multiple-cursors.el" :files
			     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			      "*.texinfo" "doc/dir" "doc/*.info"
			      "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			      "docs/dir" "docs/*.info" "docs/*.texi"
			      "docs/*.texinfo"
			      (:exclude ".dir-locals.el" "test.el"
					"tests.el" "*-test.el"
					"*-tests.el" "LICENSE"
					"README*" "*-pkg.el"))
			     :source "elpaca-menu-lock-file" :protocol
			     https :inherit t :depth treeless :ref
			     "dd10cf2334333838e4550f091a75695448e26765"
			     :id multiple-cursors :type git))
 (nerd-icons :source "elpaca-menu-lock-file" :recipe
	     (:package "nerd-icons" :repo
		       "rainstormstudio/nerd-icons.el" :fetcher github
		       :files (:defaults "data") :source
		       "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "f3e7ba37642455e5627968b1031faeefbcac1245" :id
		       nerd-icons :type git))
 (nerd-icons-completion :source "elpaca-menu-lock-file" :recipe
			(:package "nerd-icons-completion" :repo
				  "rainstormstudio/nerd-icons-completion"
				  :fetcher github :files
				  ("*.el" "*.el.in" "dir" "*.info"
				   "*.texi" "*.texinfo" "doc/dir"
				   "doc/*.info" "doc/*.texi"
				   "doc/*.texinfo" "lisp/*.el"
				   "docs/dir" "docs/*.info"
				   "docs/*.texi" "docs/*.texinfo"
				   (:exclude ".dir-locals.el"
					     "test.el" "tests.el"
					     "*-test.el" "*-tests.el"
					     "LICENSE" "README*"
					     "*-pkg.el"))
				  :source "elpaca-menu-lock-file"
				  :protocol https :inherit t :depth
				  treeless :ref
				  "8e5b995eb2439850ab21ba6062d9e6942c82ab9c"
				  :id nerd-icons-completion :type git))
 (nerd-icons-dired :source "elpaca-menu-lock-file" :recipe
		   (:package "nerd-icons-dired" :repo
			     "rainstormstudio/nerd-icons-dired"
			     :fetcher github :files
			     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			      "*.texinfo" "doc/dir" "doc/*.info"
			      "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			      "docs/dir" "docs/*.info" "docs/*.texi"
			      "docs/*.texinfo"
			      (:exclude ".dir-locals.el" "test.el"
					"tests.el" "*-test.el"
					"*-tests.el" "LICENSE"
					"README*" "*-pkg.el"))
			     :source "elpaca-menu-lock-file" :protocol
			     https :inherit t :depth treeless :ref
			     "c0b0cda2b92f831d0f764a7e8c0c6728d6a27774"
			     :id nerd-icons-dired :type git))
 (nix-ts-mode :source "elpaca-menu-lock-file" :recipe
	      (:package "nix-ts-mode" :fetcher github :repo
			"nix-community/nix-ts-mode" :files
			("*.el" "*.el.in" "dir" "*.info" "*.texi"
			 "*.texinfo" "doc/dir" "doc/*.info"
			 "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			 "docs/dir" "docs/*.info" "docs/*.texi"
			 "docs/*.texinfo"
			 (:exclude ".dir-locals.el" "test.el"
				   "tests.el" "*-test.el" "*-tests.el"
				   "LICENSE" "README*" "*-pkg.el"))
			:source "elpaca-menu-lock-file" :protocol
			https :inherit t :depth treeless :ref
			"706bbfaf7f344d7c5914bf7aa5beb2d904eb53b3" :id
			nix-ts-mode :type git))
 (ob-d2 :source "elpaca-menu-lock-file" :recipe
	(:package "ob-d2" :repo "xcapaldi/ob-d2" :fetcher github
		  :files
		  ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		   "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		   "doc/*.texinfo" "lisp/*.el" "docs/dir"
		   "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		   (:exclude ".dir-locals.el" "test.el" "tests.el"
			     "*-test.el" "*-tests.el" "LICENSE"
			     "README*" "*-pkg.el"))
		  :source "elpaca-menu-lock-file" :protocol https
		  :inherit t :depth treeless :ref
		  "5d197f8225a9fb4da997235b231abe30049c6825" :id ob-d2
		  :type git))
 (odin-ts-mode :source "elpaca-menu-lock-file" :recipe
	       (:source "elpaca-menu-lock-file" :protocol https
			:inherit t :depth treeless :host github :repo
			"Sampie159/odin-ts-mode" :package
			"odin-ts-mode" :ref
			"800134c4f104ab48b28ed33c8ebce1c8b8707add" :id
			odin-ts-mode :type git))
 (orderless :source "elpaca-menu-lock-file" :recipe
	    (:package "orderless" :repo "oantolin/orderless" :fetcher
		      github :files
		      ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		       "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		       "doc/*.texinfo" "lisp/*.el" "docs/dir"
		       "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		       (:exclude ".dir-locals.el" "test.el" "tests.el"
				 "*-test.el" "*-tests.el" "LICENSE"
				 "README*" "*-pkg.el"))
		      :source "elpaca-menu-lock-file" :protocol https
		      :inherit t :depth treeless :ref
		      "c7cb04499d94ee1c17affb29b1cfcd2a45116c97" :id
		      orderless :type git))
 (org-modern :source "elpaca-menu-lock-file" :recipe
	     (:package "org-modern" :repo "minad/org-modern" :fetcher
		       github :files
		       ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			"*.texinfo" "doc/dir" "doc/*.info"
			"doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			"docs/dir" "docs/*.info" "docs/*.texi"
			"docs/*.texinfo"
			(:exclude ".dir-locals.el" "test.el"
				  "tests.el" "*-test.el" "*-tests.el"
				  "LICENSE" "README*" "*-pkg.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "f514a2570da0f7a8ff0d72641458dbcf96ccf702" :id
		       org-modern :type git))
 (org-tree-slide :source "elpaca-menu-lock-file" :recipe
		 (:package "org-tree-slide" :fetcher github :repo
			   "takaxp/org-tree-slide" :files
			   ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			    "*.texinfo" "doc/dir" "doc/*.info"
			    "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			    "docs/dir" "docs/*.info" "docs/*.texi"
			    "docs/*.texinfo"
			    (:exclude ".dir-locals.el" "test.el"
				      "tests.el" "*-test.el"
				      "*-tests.el" "LICENSE" "README*"
				      "*-pkg.el"))
			   :source "elpaca-menu-lock-file" :protocol
			   https :inherit t :depth treeless :ref
			   "e2599a106a26ce5511095e23df4ea04be6687a8a"
			   :id org-tree-slide :type git))
 (ox-typst :source "elpaca-menu-lock-file" :recipe
	   (:package "ox-typst" :fetcher github :repo
		     "jmpunkt/ox-typst" :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "cc7e9665d4eabe0a8e0251408392f9578c7b1dac" :id
		     ox-typst :type git))
 (package-lint :source "elpaca-menu-lock-file" :recipe
	       (:package "package-lint" :fetcher github :repo
			 "purcell/package-lint" :files
			 (:defaults "data" (:exclude "*flymake.el"))
			 :source "elpaca-menu-lock-file" :protocol
			 https :inherit t :depth treeless :ref
			 "6f05a369e0718e93c5dce0951cad5e6646296612"
			 :id package-lint :type git))
 (paredit :source "elpaca-menu-lock-file" :recipe
	  (:package "paredit" :fetcher git :url
		    "https://mumble.net/~campbell/git/paredit.git"
		    :files
		    ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		     "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		     "doc/*.texinfo" "lisp/*.el" "docs/dir"
		     "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		     (:exclude ".dir-locals.el" "test.el" "tests.el"
			       "*-test.el" "*-tests.el" "LICENSE"
			       "README*" "*-pkg.el"))
		    :source "elpaca-menu-lock-file" :protocol https
		    :inherit t :depth treeless :ref
		    "af075775af91f2dbc63b915d762b4aec092946c4" :id
		    paredit :type git))
 (parrot :source "elpaca-menu-lock-file" :recipe
	 (:package "parrot" :fetcher github :repo "dp12/parrot" :files
		   (:defaults "img") :source "elpaca-menu-lock-file"
		   :protocol https :inherit t :depth treeless :ref
		   "1d381f24d74242018e306d1a0c891bed9a465ac3" :id
		   parrot :type git))
 (parseclj :source "elpaca-menu-lock-file" :recipe
	   (:package "parseclj" :repo "clojure-emacs/parseclj"
		     :fetcher github :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "6af22372e0fe14df882dd300b22b12ba2d7e00b0" :id
		     parseclj :type git))
 (parseedn :source "elpaca-menu-lock-file" :recipe
	   (:package "parseedn" :repo "clojure-emacs/parseedn"
		     :fetcher github :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "3407e4530a367b6c2b857dae261cdbb67a440aaa" :id
		     parseedn :type git))
 (perspective :source "elpaca-menu-lock-file" :recipe
	      (:package "perspective" :fetcher github :repo
			"nex3/perspective-el" :files
			("*.el" "*.el.in" "dir" "*.info" "*.texi"
			 "*.texinfo" "doc/dir" "doc/*.info"
			 "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			 "docs/dir" "docs/*.info" "docs/*.texi"
			 "docs/*.texinfo"
			 (:exclude ".dir-locals.el" "test.el"
				   "tests.el" "*-test.el" "*-tests.el"
				   "LICENSE" "README*" "*-pkg.el"))
			:source "elpaca-menu-lock-file" :protocol
			https :inherit t :depth treeless :ref
			"e32d3ea731f6bc551ce196527b3cb0dc19d71151" :id
			perspective :type git))
 (popper :source "elpaca-menu-lock-file" :recipe
	 (:package "popper" :fetcher github :repo "karthink/popper"
		   :files
		   ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		    "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		    "doc/*.texinfo" "lisp/*.el" "docs/dir"
		    "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		    (:exclude ".dir-locals.el" "test.el" "tests.el"
			      "*-test.el" "*-tests.el" "LICENSE"
			      "README*" "*-pkg.el"))
		   :source "elpaca-menu-lock-file" :protocol https
		   :inherit t :depth treeless :ref
		   "faf155059e519fb036324af579c342365795dbbb" :id
		   popper :type git))
 (popup :source "elpaca-menu-lock-file" :recipe
	(:package "popup" :fetcher github :repo
		  "auto-complete/popup-el" :files
		  ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		   "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		   "doc/*.texinfo" "lisp/*.el" "docs/dir"
		   "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		   (:exclude ".dir-locals.el" "test.el" "tests.el"
			     "*-test.el" "*-tests.el" "LICENSE"
			     "README*" "*-pkg.el"))
		  :source "elpaca-menu-lock-file" :protocol https
		  :inherit t :depth treeless :ref
		  "7a05700a37aae66d2b24f0cd8851f65383a5cf96" :id popup
		  :type git))
 (posframe :source "elpaca-menu-lock-file" :recipe
	   (:package "posframe" :fetcher github :repo
		     "tumashu/posframe" :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "fcf1757baee481f617fbf2dc39f8c561207df263" :id
		     posframe :type git))
 (pretty-hydra :source "elpaca-menu-lock-file" :recipe
	       (:package "pretty-hydra" :repo
			 "jerrypnz/major-mode-hydra.el" :fetcher
			 github :files ("pretty-hydra.el") :source
			 "elpaca-menu-lock-file" :protocol https
			 :inherit t :depth treeless :ref
			 "d0a5dadee97c3752fcdef113cf2ba1923972a480"
			 :id pretty-hydra :type git))
 (pythontest :source "elpaca-menu-lock-file" :recipe
	     (:package "pythontest" :fetcher github :repo
		       "erickgnavar/pythontest.el" :files
		       ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			"*.texinfo" "doc/dir" "doc/*.info"
			"doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			"docs/dir" "docs/*.info" "docs/*.texi"
			"docs/*.texinfo"
			(:exclude ".dir-locals.el" "test.el"
				  "tests.el" "*-test.el" "*-tests.el"
				  "LICENSE" "README*" "*-pkg.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "4bb4f330c13ef82bb6e4a4b15c47cb3fede83523" :id
		       pythontest :type git))
 (queue :source "elpaca-menu-lock-file" :recipe
	(:package "queue" :repo
		  ("https://git.savannah.gnu.org/git/emacs/elpa.git"
		   . "queue")
		  :branch "externals/queue" :files
		  ("*" (:exclude ".git")) :source
		  "elpaca-menu-lock-file" :protocol https :inherit t
		  :depth treeless :ref
		  "f986fb68e75bdae951efb9e11a3012ab6bd408ee" :id queue
		  :type git))
 (rainbow-delimiters :source "elpaca-menu-lock-file" :recipe
		     (:package "rainbow-delimiters" :fetcher github
			       :repo "Fanael/rainbow-delimiters"
			       :files
			       ("*.el" "*.el.in" "dir" "*.info"
				"*.texi" "*.texinfo" "doc/dir"
				"doc/*.info" "doc/*.texi"
				"doc/*.texinfo" "lisp/*.el" "docs/dir"
				"docs/*.info" "docs/*.texi"
				"docs/*.texinfo"
				(:exclude ".dir-locals.el" "test.el"
					  "tests.el" "*-test.el"
					  "*-tests.el" "LICENSE"
					  "README*" "*-pkg.el"))
			       :source "elpaca-menu-lock-file"
			       :protocol https :inherit t :depth
			       treeless :ref
			       "f40ece58df8b2f0fb6c8576b527755a552a5e763"
			       :id rainbow-delimiters :type git))
 (rainbow-mode :source "elpaca-menu-lock-file" :recipe
	       (:package "rainbow-mode" :repo
			 ("https://git.savannah.gnu.org/git/emacs/elpa.git"
			  . "rainbow-mode")
			 :branch "externals/rainbow-mode" :files
			 ("*" (:exclude ".git")) :source
			 "elpaca-menu-lock-file" :protocol https
			 :inherit t :depth treeless :ref
			 "f7db3b5919f70420a91eb199f8663468de3033f3"
			 :id rainbow-mode :type git))
 (reformatter :source "elpaca-menu-lock-file" :recipe
	      (:package "reformatter" :repo
			"purcell/emacs-reformatter" :fetcher github
			:files
			("*.el" "*.el.in" "dir" "*.info" "*.texi"
			 "*.texinfo" "doc/dir" "doc/*.info"
			 "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			 "docs/dir" "docs/*.info" "docs/*.texi"
			 "docs/*.texinfo"
			 (:exclude ".dir-locals.el" "test.el"
				   "tests.el" "*-test.el" "*-tests.el"
				   "LICENSE" "README*" "*-pkg.el"))
			:source "elpaca-menu-lock-file" :protocol
			https :inherit t :depth treeless :ref
			"f2cb59466b1c3f85a8c960f7d4b7b7ead015bedc" :id
			reformatter :type git))
 (request :source "elpaca-menu-lock-file"
   :recipe
   (:package "request" :repo "tkf/emacs-request" :fetcher github
	     :files ("request.el") :source "elpaca-menu-lock-file"
	     :protocol https :inherit t :depth treeless :ref
	     "01e338c335c07e4407239619e57361944a82cb8a" :id request
	     :type git))
 (restclient :source "elpaca-menu-lock-file" :recipe
	     (:package "restclient" :repo "emacsorphanage/restclient"
		       :fetcher github :files ("restclient.el")
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "757d82eacb997f82c84c4e6eea08a338b0b6afa8" :id
		       restclient :type git))
 (s :source "elpaca-menu-lock-file" :recipe
    (:package "s" :fetcher github :repo "magnars/s.el" :files
	      ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
	       "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
	       "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
	       "docs/*.texinfo"
	       (:exclude ".dir-locals.el" "test.el" "tests.el"
			 "*-test.el" "*-tests.el" "LICENSE" "README*"
			 "*-pkg.el"))
	      :source "elpaca-menu-lock-file" :protocol https :inherit
	      t :depth treeless :ref
	      "dda84d38fffdaf0c9b12837b504b402af910d01d" :id s :type
	      git))
 (sesman :source "elpaca-menu-lock-file" :recipe
	 (:package "sesman" :repo "vspinu/sesman" :fetcher github
		   :files
		   ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		    "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		    "doc/*.texinfo" "lisp/*.el" "docs/dir"
		    "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		    (:exclude ".dir-locals.el" "test.el" "tests.el"
			      "*-test.el" "*-tests.el" "LICENSE"
			      "README*" "*-pkg.el"))
		   :source "elpaca-menu-lock-file" :protocol https
		   :inherit t :depth treeless :ref
		   "7bca68dbbab0af26a6a23be1ff5fa97f9a18e022" :id
		   sesman :type git))
 (shrink-path :source "elpaca-menu-lock-file" :recipe
	      (:package "shrink-path" :fetcher gitlab :repo
			"bennya/shrink-path.el" :files
			("*.el" "*.el.in" "dir" "*.info" "*.texi"
			 "*.texinfo" "doc/dir" "doc/*.info"
			 "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			 "docs/dir" "docs/*.info" "docs/*.texi"
			 "docs/*.texinfo"
			 (:exclude ".dir-locals.el" "test.el"
				   "tests.el" "*-test.el" "*-tests.el"
				   "LICENSE" "README*" "*-pkg.el"))
			:source "elpaca-menu-lock-file" :protocol
			https :inherit t :depth treeless :ref
			"c14882c8599aec79a6e8ef2d06454254bb3e1e41" :id
			shrink-path :type git))
 (slint-mode :source "elpaca-menu-lock-file" :recipe
	     (:package "slint-mode" :fetcher github :repo
		       "nilclass/slint-mode" :files
		       ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			"*.texinfo" "doc/dir" "doc/*.info"
			"doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			"docs/dir" "docs/*.info" "docs/*.texi"
			"docs/*.texinfo"
			(:exclude ".dir-locals.el" "test.el"
				  "tests.el" "*-test.el" "*-tests.el"
				  "LICENSE" "README*" "*-pkg.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "168a6cfb90b5e36360074c83f80d5bbac2f0287e" :id
		       slint-mode :type git))
 (sly :source "elpaca-menu-lock-file" :recipe
      (:package "sly" :repo "joaotavora/sly" :fetcher github :files
		(:defaults "lib" "slynk" "contrib" "doc/images"
			   (:exclude "sly-autoloads.el"))
		:version-regexp "%v" :source "elpaca-menu-lock-file"
		:protocol https :inherit t :depth treeless :ref
		"c48defcf58596e035d473f3a125fdd1485593146" :id sly
		:type git))
 (spinner :source "elpaca-menu-lock-file" :recipe
	  (:package "spinner" :repo
		    ("https://github.com/Malabarba/spinner.el"
		     . "spinner")
		    :files ("*" (:exclude ".git")) :source
		    "elpaca-menu-lock-file" :protocol https :inherit t
		    :depth treeless :ref
		    "d4647ae87fb0cd24bc9081a3d287c860ff061c21" :id
		    spinner :type git))
 (svelte-ts-mode :source "elpaca-menu-lock-file" :recipe
		 (:source "elpaca-menu-lock-file" :package
			  "svelte-ts-mode" :id svelte-ts-mode :host
			  github :repo "leafOfTree/svelte-ts-mode"
			  :type git :protocol https :inherit t :depth
			  treeless :ref
			  "d079050fc1ba70f8fba9e596638daa2ca96e0fdd"))
 (swift-mode :source "elpaca-menu-lock-file" :recipe
	     (:package "swift-mode" :repo "swift-emacs/swift-mode"
		       :fetcher github :files
		       ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			"*.texinfo" "doc/dir" "doc/*.info"
			"doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			"docs/dir" "docs/*.info" "docs/*.texi"
			"docs/*.texinfo"
			(:exclude ".dir-locals.el" "test.el"
				  "tests.el" "*-test.el" "*-tests.el"
				  "LICENSE" "README*" "*-pkg.el"))
		       :source "elpaca-menu-lock-file" :protocol https
		       :inherit t :depth treeless :ref
		       "bf56866b98de2fd73029da2bca1035ca02f9e879" :id
		       swift-mode :type git))
 (telega :source "elpaca-menu-lock-file" :recipe
	 (:package "telega" :fetcher github :repo "zevlg/telega.el"
		   :files
		   (:defaults "etc" "server" "contrib" "Makefile")
		   :source "elpaca-menu-lock-file" :protocol https
		   :inherit t :depth treeless :ref
		   "85ad1717de4a47ebeb293e836e5a89e901b55dbd" :id
		   telega :type git))
 (terraform-mode :source "elpaca-menu-lock-file" :recipe
		 (:package "terraform-mode" :repo
			   "hcl-emacs/terraform-mode" :fetcher github
			   :files
			   ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			    "*.texinfo" "doc/dir" "doc/*.info"
			    "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			    "docs/dir" "docs/*.info" "docs/*.texi"
			    "docs/*.texinfo"
			    (:exclude ".dir-locals.el" "test.el"
				      "tests.el" "*-test.el"
				      "*-tests.el" "LICENSE" "README*"
				      "*-pkg.el"))
			   :source "elpaca-menu-lock-file" :protocol
			   https :inherit t :depth treeless :ref
			   "5bdd734a87f67f6574664f63eb4d02a0bc74c0d0"
			   :id terraform-mode :type git))
 (treesit-auto :source "elpaca-menu-lock-file" :recipe
	       (:package "treesit-auto" :fetcher github :repo
			 "renzmann/treesit-auto" :files
			 ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			  "*.texinfo" "doc/dir" "doc/*.info"
			  "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			  "docs/dir" "docs/*.info" "docs/*.texi"
			  "docs/*.texinfo"
			  (:exclude ".dir-locals.el" "test.el"
				    "tests.el" "*-test.el"
				    "*-tests.el" "LICENSE" "README*"
				    "*-pkg.el"))
			 :source "elpaca-menu-lock-file" :protocol
			 https :inherit t :depth treeless :ref
			 "433dac791c3a5c6f2aebddc70066f72940a12489"
			 :id treesit-auto :type git))
 (treesit-ispell :source "elpaca-menu-lock-file" :recipe
		 (:package "treesit-ispell" :fetcher github :repo
			   "erickgnavar/treesit-ispell.el" :files
			   ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			    "*.texinfo" "doc/dir" "doc/*.info"
			    "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			    "docs/dir" "docs/*.info" "docs/*.texi"
			    "docs/*.texinfo"
			    (:exclude ".dir-locals.el" "test.el"
				      "tests.el" "*-test.el"
				      "*-tests.el" "LICENSE" "README*"
				      "*-pkg.el"))
			   :source "elpaca-menu-lock-file" :protocol
			   https :inherit t :depth treeless :ref
			   "fd1598fb16fe99dc8d8245974641c3e474dc31d1"
			   :id treesit-ispell :type git))
 (typst-ts-mode :source "elpaca-menu-lock-file" :recipe
		(:package "typst-ts-mode" :repo
			  "meow_king/typst-ts-mode" :files
			  ("*" (:exclude ".git")) :source
			  "elpaca-menu-lock-file" :protocol https
			  :inherit t :depth treeless :type git :host
			  codeberg :ref
			  "972dc69d6b8a3f8983f6b8000654f59c8a8d05ba"
			  :id typst-ts-mode))
 (vertico :source "elpaca-menu-lock-file" :recipe
	  (:package "vertico" :repo "minad/vertico" :files
		    (:defaults "extensions/vertico-*.el") :fetcher
		    github :source "elpaca-menu-lock-file" :protocol
		    https :inherit t :depth treeless :ref
		    "17c629087ea23466b10fd0dd4ecce53e17a810e3" :id
		    vertico :type git))
 (vertico-posframe :source "elpaca-menu-lock-file" :recipe
		   (:package "vertico-posframe" :repo
			     ("https://github.com/tumashu/vertico-posframe"
			      . "vertico-posframe")
			     :files ("*" (:exclude ".git")) :source
			     "elpaca-menu-lock-file" :protocol https
			     :inherit t :depth treeless :ref
			     "d89a70743cfd95b7fcda621679b7555bbef51acb"
			     :id vertico-posframe :type git))
 (visual-fill-column :source "elpaca-menu-lock-file" :recipe
		     (:package "visual-fill-column" :fetcher codeberg
			       :repo "joostkremers/visual-fill-column"
			       :files
			       ("*.el" "*.el.in" "dir" "*.info"
				"*.texi" "*.texinfo" "doc/dir"
				"doc/*.info" "doc/*.texi"
				"doc/*.texinfo" "lisp/*.el" "docs/dir"
				"docs/*.info" "docs/*.texi"
				"docs/*.texinfo"
				(:exclude ".dir-locals.el" "test.el"
					  "tests.el" "*-test.el"
					  "*-tests.el" "LICENSE"
					  "README*" "*-pkg.el"))
			       :source "elpaca-menu-lock-file"
			       :protocol https :inherit t :depth
			       treeless :ref
			       "11575df4e042f3263db62de0e8ee4f2331fbb1b4"
			       :id visual-fill-column :type git))
 (vterm :source "elpaca-menu-lock-file" :recipe
	(:package "vterm" :fetcher github :repo
		  "akermu/emacs-libvterm" :files
		  ("CMakeLists.txt" "elisp.c" "elisp.h"
		   "emacs-module.h" "etc" "utf8.c" "utf8.h" "vterm.el"
		   "vterm-module.c" "vterm-module.h")
		  :source "elpaca-menu-lock-file" :protocol https
		  :inherit t :depth treeless :ref
		  "056ad74653704bc353d8ec8ab52ac75267b7d373" :id vterm
		  :type git))
 (vterm-toggle :source "elpaca-menu-lock-file" :recipe
	       (:package "vterm-toggle" :fetcher github :repo
			 "jixiuf/vterm-toggle" :files
			 ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			  "*.texinfo" "doc/dir" "doc/*.info"
			  "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			  "docs/dir" "docs/*.info" "docs/*.texi"
			  "docs/*.texinfo"
			  (:exclude ".dir-locals.el" "test.el"
				    "tests.el" "*-test.el"
				    "*-tests.el" "LICENSE" "README*"
				    "*-pkg.el"))
			 :source "elpaca-menu-lock-file" :protocol
			 https :inherit t :depth treeless :ref
			 "a0051a8b8eaa85f8df54ddb032f5710c1f62779d"
			 :id vterm-toggle :type git))
 (wakatime-mode :source "elpaca-menu-lock-file" :recipe
		(:package "wakatime-mode" :fetcher github :repo
			  "wakatime/wakatime-mode" :files
			  ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			   "*.texinfo" "doc/dir" "doc/*.info"
			   "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			   "docs/dir" "docs/*.info" "docs/*.texi"
			   "docs/*.texinfo"
			   (:exclude ".dir-locals.el" "test.el"
				     "tests.el" "*-test.el"
				     "*-tests.el" "LICENSE" "README*"
				     "*-pkg.el"))
			  :source "elpaca-menu-lock-file" :protocol
			  https :inherit t :depth treeless :ref
			  "1c5b2254dd72f2ff504d6a6189a8c10be03a98d1"
			  :id wakatime-mode :type git))
 (web-mode :source "elpaca-menu-lock-file" :recipe
	   (:package "web-mode" :repo "fxbois/web-mode" :fetcher
		     github :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "be2d59c8fa02b1a45ae54ce4079e502e659cefe6" :id
		     web-mode :type git))
 (websocket :source "elpaca-menu-lock-file" :recipe
	    (:package "websocket" :repo "ahyatt/emacs-websocket"
		      :fetcher github :files
		      ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		       "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		       "doc/*.texinfo" "lisp/*.el" "docs/dir"
		       "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		       (:exclude ".dir-locals.el" "test.el" "tests.el"
				 "*-test.el" "*-tests.el" "LICENSE"
				 "README*" "*-pkg.el"))
		      :source "elpaca-menu-lock-file" :protocol https
		      :inherit t :depth treeless :ref
		      "40c208eaab99999d7c1e4bea883648da24c03be3" :id
		      websocket :type git))
 (wgrep :source "elpaca-menu-lock-file" :recipe
	(:package "wgrep" :fetcher github :repo
		  "mhayashi1120/Emacs-wgrep" :files ("wgrep.el")
		  :source "elpaca-menu-lock-file" :protocol https
		  :inherit t :depth treeless :ref
		  "49f09ab9b706d2312cab1199e1eeb1bcd3f27f6f" :id wgrep
		  :type git))
 (with-editor :source "elpaca-menu-lock-file"
   :recipe
   (:package "with-editor" :fetcher github :repo "magit/with-editor"
	     :files
	     ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
	      "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
	      "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
	      "docs/*.texinfo"
	      (:exclude ".dir-locals.el" "test.el" "tests.el"
			"*-test.el" "*-tests.el" "LICENSE" "README*"
			"*-pkg.el"))
	     :source "elpaca-menu-lock-file" :protocol https :inherit
	     t :depth treeless :ref
	     "64211dcb815f2533ac3d2a7e56ff36ae804d8338" :id
	     with-editor :type git))
 (writeroom-mode :source "elpaca-menu-lock-file" :recipe
		 (:package "writeroom-mode" :fetcher github :repo
			   "joostkremers/writeroom-mode" :files
			   ("*.el" "*.el.in" "dir" "*.info" "*.texi"
			    "*.texinfo" "doc/dir" "doc/*.info"
			    "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			    "docs/dir" "docs/*.info" "docs/*.texi"
			    "docs/*.texinfo"
			    (:exclude ".dir-locals.el" "test.el"
				      "tests.el" "*-test.el"
				      "*-tests.el" "LICENSE" "README*"
				      "*-pkg.el"))
			   :source "elpaca-menu-lock-file" :protocol
			   https :inherit t :depth treeless :ref
			   "86965a56ed712ab70eb76577dc6fc5b01fd0eda6"
			   :id writeroom-mode :type git))
 (xterm-color :source "elpaca-menu-lock-file" :recipe
	      (:package "xterm-color" :repo "atomontage/xterm-color"
			:fetcher github :files
			("*.el" "*.el.in" "dir" "*.info" "*.texi"
			 "*.texinfo" "doc/dir" "doc/*.info"
			 "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
			 "docs/dir" "docs/*.info" "docs/*.texi"
			 "docs/*.texinfo"
			 (:exclude ".dir-locals.el" "test.el"
				   "tests.el" "*-test.el" "*-tests.el"
				   "LICENSE" "README*" "*-pkg.el"))
			:source "elpaca-menu-lock-file" :protocol
			https :inherit t :depth treeless :ref
			"7543ed80d0ad07f1044b4d55c9ee7893cd45362c" :id
			xterm-color :type git))
 (yaml :source "elpaca-menu-lock-file" :recipe
       (:package "yaml" :repo "zkry/yaml.el" :fetcher github :files
		 ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
		  "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
		  "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
		  "docs/*.texinfo"
		  (:exclude ".dir-locals.el" "test.el" "tests.el"
			    "*-test.el" "*-tests.el" "LICENSE"
			    "README*" "*-pkg.el"))
		 :source "elpaca-menu-lock-file" :protocol https
		 :inherit t :depth treeless :ref
		 "73a9f5356284716a123952ce194c14ad9d183444" :id yaml
		 :type git))
 (yaml-pro :source "elpaca-menu-lock-file" :recipe
	   (:package "yaml-pro" :repo "zkry/yaml-pro" :fetcher github
		     :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "f7706ea170de98d29b7cdd5a4f189bd038b1e27b" :id
		     yaml-pro :type git))
 (yasnippet :source "elpaca-menu-lock-file" :recipe
	    (:package "yasnippet" :repo "joaotavora/yasnippet"
		      :fetcher github :files
		      ("yasnippet.el" "snippets") :source
		      "elpaca-menu-lock-file" :protocol https :inherit
		      t :depth treeless :ref
		      "5b315f1753480ebe669b157b8242448b5eafcfea" :id
		      yasnippet :type git))
 (yasnippet-snippets :source "elpaca-menu-lock-file" :recipe
		     (:package "yasnippet-snippets" :repo
			       "AndreaCrotti/yasnippet-snippets"
			       :fetcher github :files
			       ("*.el" "snippets" ".nosearch") :source
			       "elpaca-menu-lock-file" :protocol https
			       :inherit t :depth treeless :ref
			       "f1907ed38acc479e78d5c113810465e4d77d8604"
			       :id yasnippet-snippets :type git))
 (zig-mode :source "elpaca-menu-lock-file" :recipe
	   (:package "zig-mode" :repo "ziglang/zig-mode" :fetcher
		     github :files
		     ("*.el" "*.el.in" "dir" "*.info" "*.texi"
		      "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
		      "doc/*.texinfo" "lisp/*.el" "docs/dir"
		      "docs/*.info" "docs/*.texi" "docs/*.texinfo"
		      (:exclude ".dir-locals.el" "test.el" "tests.el"
				"*-test.el" "*-tests.el" "LICENSE"
				"README*" "*-pkg.el"))
		     :source "elpaca-menu-lock-file" :protocol https
		     :inherit t :depth treeless :ref
		     "f0b4a487530146f99230f4a5ff67e8d56c8f3f80" :id
		     zig-mode :type git)))

init.el

.emacs.d/init.el · 80 lines

;;; Package --- init file -*- lexical-binding: t -*-
;;; init.el --- bootstrap file to load config from bootstrap.org
;;; Commentary:
;;; Code:

(defvar elpaca-lock-file (expand-file-name "elpaca-lockfile.eld" user-emacs-directory))

(defun my/elpaca-write-lock-file ()
  "Write lock file into a fixed path using `elpaca-lock-file'."
  (interactive)
  (minibuffer-with-setup-hook #'(lambda ()
                                  (insert elpaca-lock-file))
    (call-interactively 'elpaca-write-lock-file)))

;; Avoid to show a message about deprecation of cl package
(setq byte-compile-warnings '(cl-functions))

(defvar elpaca-installer-version 0.12)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-sources-directory (expand-file-name "sources/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
                              :ref nil :depth nil :inherit ignore
                              :files (:defaults "elpaca-test.el" (:exclude "extensions"))
                              :build (:not elpaca-activate)))
(let* ((repo  (expand-file-name "elpaca/" elpaca-sources-directory))
       (build (expand-file-name "elpaca/" elpaca-builds-directory))
       (order (cdr elpaca-order))
       (default-directory repo))
  (add-to-list 'load-path (if (file-exists-p build) build repo))
  (unless (file-exists-p repo)
    (make-directory repo t)
    (when (<= emacs-major-version 28) (require 'subr-x))
    (condition-case-unless-debug err
        (if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
                  ((zerop (apply #'call-process `("git" nil ,buffer t "clone"
                                                  ,@(when-let* ((depth (plist-get order :depth)))
                                                      (list (format "--depth=%d" depth) "--no-single-branch"))
                                                  ,(plist-get order :repo) ,repo))))
                  ((zerop (call-process "git" nil buffer t "checkout"
                                        (or (plist-get order :ref) "--"))))
                  (emacs (concat invocation-directory invocation-name))
                  ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
                                        "--eval" "(byte-recompile-directory \".\" 0 'force)")))
                  ((require 'elpaca))
                  ((elpaca-generate-autoloads "elpaca" repo)))
            (progn (message "%s" (buffer-string)) (kill-buffer buffer))
          (error "%s" (with-current-buffer buffer (buffer-string))))
      ((error) (warn "%s" err) (delete-directory repo 'recursive))))
  (unless (require 'elpaca-autoloads nil t)
    (require 'elpaca)
    (elpaca-generate-autoloads "elpaca" repo)
    (let ((load-source-file-function nil)) (load "./elpaca-autoloads"))))

(add-hook 'after-init-hook #'elpaca-process-queues)

(elpaca `(,@elpaca-order))

;; Install use-package support
(elpaca elpaca-use-package
  ;; Enable use-package :ensure support for Elpaca.
  (elpaca-use-package-mode))

;; setup zsh as a default shell when is available
(let ((zsh-path (executable-find "zsh")))
  (when zsh-path
    (setq shell-file-name zsh-path)))

;; Always follow symlinks, used to avoid emacs ask when open org conf file
(setq vc-follow-symlinks t)

;; read bootstrap.org and load emacs-lisp code
(org-babel-load-file (expand-file-name "bootstrap.org" user-emacs-directory))

;; custom code that will be unique per machine
(defconst local-config-file (expand-file-name "local.el" user-emacs-directory))
(load local-config-file :noerror)

(provide 'init.el)
;;; init.el ends here

README.org

.emacs.d/README.org · 3,302 lines

#+TITLE: Emacs config
#+AUTHOR: Erick Navarro
#+OPTIONS: num:nil

* Vanilla Emacs setup

This is required to enable lexical binding in the resulting ~.el~ file, which will be used by emacs at startup.

#+begin_src emacs-lisp
;; -*- lexical-binding: t -*-
#+end_src

#+BEGIN_SRC emacs-lisp
(setq inhibit-startup-message t)
(setq default-directory (expand-file-name "~/"))

;; Default font-family, this will used in all the function that manipulates font settings
(defvar my/font-family "JetBrainsMono Nerd Font")

;; Change backups directory to emacs folder
;; this avoid to save backup files in the same directory of the original files
(setq backup-directory-alist `(("." . ,(concat user-emacs-directory
                                               "backups"))))

;; Revert dired and other buffers when there are changes on disk
(setq global-auto-revert-non-file-buffers t)

;; Hide the bell in the center of screen
(setq ring-bell-function 'ignore)
(column-number-mode t)
(global-hl-line-mode 1)

;; in case the font doesn't exit this will raise an error saying that
(set-face-attribute 'default nil :font (format "%s 13" my/font-family))

(global-set-key (kbd "C-x -") 'my/decrease-font-height)
(global-set-key (kbd "C-x =") 'my/increase-font-height)
(global-set-key (kbd "C-x +") 'my/increase-font-height)

;; Navigate through buffers
(global-set-key (kbd "M-[") 'previous-buffer)
(global-set-key (kbd "M-]") 'next-buffer)

;; Fix unicode errors
(setenv "LANG" "en_US.UTF-8")
(setenv "LC_ALL" "en_US.UTF-8")
(setenv "LC_CTYPE" "en_US.UTF-8")

;; Fix size of scroll
(setq scroll-step 1
      scroll-conservatively  10000)

;; Show a marker when the line has empty characters at the end
(setq-default show-trailing-whitespace t)

;; disable `show-trailing-whitespace' for modes based in comint-mode
;; make prompt readonly
(add-hook 'comint-mode-hook #'(lambda ()
                                (setq-local show-trailing-whitespace nil)
                                (setq-local comint-prompt-read-only t)))

;; Avoid close emacs by mistake
(global-unset-key (kbd "C-x C-c"))

(defalias 'yes-or-no-p 'y-or-n-p)
(defalias 'run-elisp 'ielm)

;; place custom code generated for emacs in a separate file
(defconst custom-file (expand-file-name ".customize.el" user-emacs-directory))
(load custom-file :noerror)

;; disable mouse gestures to avoid scaling text by mistake
(global-set-key (kbd "<pinch>") 'ignore)
(global-set-key (kbd "<C-wheel-up>") 'ignore)
(global-set-key (kbd "<C-wheel-down>") 'ignore)
#+END_SRC

Show a message with emacs startup time

#+begin_src emacs-lisp
(defun my/display-startup-time ()
  (message "🚀 Emacs loaded in %s with %d garbage collections."
           (float-time (time-subtract after-init-time before-init-time))
           gcs-done))

(add-hook 'emacs-startup-hook #'my/display-startup-time)
#+end_src

Setup ligatures, by default it has support for ~JetBrainsMono~.

#+BEGIN_SRC emacs-lisp
(use-package ligature
  :ensure t
  :config
  ;; Enable ligatures in programming modes
  (ligature-set-ligatures 'prog-mode '("www" "**" "***" "**/" "*>" "*/" "\\\\" "\\\\\\" "{-" "::"
                                       ":::" ":=" "!!" "!=" "!==" "-}" "----" "-->" "->" "->>"
                                       "-<" "-<<" "-~" "#{" "#[" "##" "###" "####" "#(" "#?" "#_"
                                       "#_(" ".-" ".=" ".." "..<" "..." "?=" "??" ";;" "/*" "/**"
                                       "/=" "/==" "/>" "//" "///" "&&" "||" "||=" "|=" "|>" "^=" "$>"
                                       "++" "+++" "+>" "=:=" "==" "===" "==>" "=>" "=>>" "<="
                                       "=<<" "=/=" ">-" ">=" ">=>" ">>" ">>-" ">>=" ">>>" "<*"
                                       "<*>" "<|" "<|>" "<$" "<$>" "<!--" "<-" "<--" "<->" "<+"
                                       "<+>" "<=" "<==" "<=>" "<=<" "<>" "<<" "<<-" "<<=" "<<<"
                                       "<~" "<~~" "</" "</>" "~@" "~-" "~>" "~~" "~~>" "%%"))
  (global-ligature-mode 't))
#+END_SRC

Adjust font size to screen resolution, increase font size for 4K screens

#+BEGIN_SRC emacs-lisp
(when (and (display-graphic-p) (>= (x-display-pixel-width) 3840))
  (set-face-attribute 'default nil :font (format "%s 14" my/font-family)))
#+END_SRC

#+begin_src emacs-lisp
(define-minor-mode big-font-mode
  "Switch between a regular font size and a presentation font size."
  :init-value nil
  :global t
  (if big-font-mode
      (progn
        ;; save current font size in a temp variable to be able to restore it
        ;; after this minor mode is disabled
        (setq big-font-mode--tmp (/ (face-attribute 'default :height) 10))
        (set-face-attribute 'default nil :font (format "%s 24" my/font-family)))
    (set-face-attribute 'default nil :font (format "%s %d" my/font-family big-font-mode--tmp))))
#+end_src

Parrot mode FTW!

#+BEGIN_SRC emacs-lisp
(defun my/parrot-animate-when-compile-success (buffer result)
  (if (string-match "^finished" result)
      (parrot-start-animation)))

(use-package parrot
  :ensure t
  :hook
  (elpaca-after-init . parrot-mode)
  (before-save . parrot-start-animation)
  :config
  (add-to-list 'compilation-finish-functions 'my/parrot-animate-when-compile-success))
#+END_SRC

Open scratch buffer

#+BEGIN_SRC emacs-lisp
(defun my/scratch-buffer()
  "Switch to scratch buffer."
  (interactive)
  (let ((buffer (get-buffer "*scratch*")))
    (if buffer
        (switch-to-buffer buffer)
      (switch-to-buffer (get-buffer-create "*scratch*"))
      (insert (substitute-command-keys initial-scratch-message)))
    (lisp-interaction-mode)))
#+END_SRC

** Ansi term

For some reason ansi-term doesn't respect the global keybinding for =M-]= so this have to be setup in =term-raw-map= as well.

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'term
  (define-key term-raw-map (kbd "M-]") 'next-buffer))
#+END_SRC

** xref

Enable evil emacs state when entering a xref buffer

#+begin_src emacs-lisp
(with-eval-after-load 'xref
  (add-hook 'xref-after-update-hook #'(lambda () (evil-emacs-state))))
#+end_src

** Occur

#+begin_src emacs-lisp
;; occur is part of replace.el file
(with-eval-after-load 'replace
  (define-key occur-mode-map (kbd "C-c C-e") 'occur-edit-mode)
  ;; this will run every time search results are shown
  (setq occur-hook #'(lambda ()
                       (window-buffer)
                       (select-window (get-buffer-window "*Occur*"))
                       (evil-normal-state))))
#+end_src

** Popper.el

Display some special buffers in a little window always at bottom

#+begin_src emacs-lisp
(use-package popper
  :ensure t
  :init
  (setq popper-reference-buffers
        '("\\*eldoc\\*"
          help-mode))
  (popper-mode +1))
#+end_src

** Compilation

Disable =h= key-binding, this has a conflict with evil-mode left navigation key-binding.

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'compile
  ;; set cursor to follow compilation output
  (setq compilation-scroll-output t)
  ;; for elixir testing output test filename use black color which makes it ineligible
  (set-face-foreground 'ansi-color-bold "magenta")
  (define-key compilation-mode-map (kbd "C-n") 'compilation-next-error)
  (define-key compilation-mode-map (kbd "C-p") 'compilation-previous-error)
  (define-key compilation-mode-map (kbd "g") nil)
  (define-key compilation-mode-map (kbd "r") 'recompile)
  (define-key compilation-mode-map (kbd "h") nil))
#+END_SRC

Translate ANSI escape sequences into faces, for example to show colors.

#+BEGIN_SRC emacs-lisp
(require 'ansi-color)

(add-hook 'compilation-filter-hook 'ansi-color-compilation-filter)
#+END_SRC

Custom function to select a base directory before running compilation. ~M-x compile~ always use the base directory of the buffer from where it was called as ~default-directory~, this is awful when you want to run a project compilation command from a nested file buffer.

#+begin_src emacs-lisp
(defun my/compile ()
  "Run compilation process but ask for a `default-directory' before."
  (interactive)
  (let ((default-directory (read-directory-name "Base directory: " (my/project-root)))
        (cmd (read-string "Compile command: ")))
    ;; we need to "export" this variable to be able to re-run `compile' command
    (setq compile-command cmd)
    (compile compile-command)))
#+end_src

** Async shell commands

Helper function to run ~async-shell-command~ with some tweaks.

#+begin_src emacs-lisp
(defun my/async-shell-command ()
  "Run `async-shell-command' but at any location.
By default it will use project root but this can be changed"
  (interactive)
  (let* ((default-directory (read-directory-name "Base directory: " (my/project-root)))
         (action-name (read-string "Action name: "))
         (command (read-string "shell command: "))
         (buffer-name (format "(%s)*%s*" (my/project-name) action-name)))
    (async-shell-command command (get-buffer-create buffer-name))))
#+end_src

** Narrowing

#+begin_src emacs-lisp
(defun my/toggle-narrowing ()
  "Toggle narrow on the selected region."
  (interactive)
  (if (buffer-narrowed-p)
      (widen)
    (if (region-active-p)
        (narrow-to-region (region-beginning) (region-end))
      (user-error "No active selection"))))
#+end_src

** Ediff

#+begin_src emacs-lisp
(with-eval-after-load 'ediff
  (setq ediff-split-window-function 'split-window-horizontally)
  ;; put ediff buffer in a buffer at the bottom instead of in a new frame
  (setq ediff-window-setup-function 'ediff-setup-windows-plain))
#+end_src

* Theme and styles

** Spaceink theme

#+begin_src emacs-lisp
(use-package color-theme-sanityinc-tomorrow
  :ensure t
  :config
  (load-theme 'sanityinc-tomorrow-bright t))
#+end_src

** Doom modeline

Enable =display-battery-mode= only if a battery is present. =(battery)= reports "battery N/A" on
desktops, which we use to detect the absence of a battery.

#+BEGIN_SRC emacs-lisp
(use-package doom-modeline
  :ensure t
  :defer t
  :custom
  (doom-modeline-modal-icon nil)
  (doom-modeline-buffer-file-name-style 'relative-from-project)
  :hook
  (elpaca-after-init . doom-modeline-mode)
  (doom-modeline-mode . (lambda ()
                          ;; prevent show (battery) message in echo
                          ;; are and *Messages* buffer
                          (let* ((inhibit-message t)
                                 (message-log-max nil)
                                 (battery-p (not (string-match-p "battery N/A" (battery)))))
                            (display-battery-mode (if battery-p 1 -1))))))
#+END_SRC

* Dired

#+BEGIN_SRC emacs-lisp
(with-eval-after-load "dired"
  (add-hook 'dired-mode-hook 'evil-emacs-state)
  (define-key dired-mode-map (kbd "C-c C-e") 'wdired-change-to-wdired-mode))
#+END_SRC

** Dired preview

#+begin_src emacs-lisp
(use-package dired-preview
  :ensure t
  :after dired
  :config
  (define-key dired-mode-map (kbd "P") #'(lambda ()
                                           (interactive)
                                           (dired-preview-display-file (dired-file-name-at-point)))))
#+end_src

** Nerd icons dired

#+begin_src emacs-lisp
(use-package nerd-icons-dired
  :ensure t
  :defer t
  :hook
  (dired-mode . nerd-icons-dired-mode))
#+end_src

** Dired subtree

#+BEGIN_SRC emacs-lisp
(use-package dired-subtree
  :ensure t
  :after dired
  :config
  (define-key dired-mode-map (kbd "<tab>") 'dired-subtree-toggle))
#+END_SRC

* Editor enhancements

** Whitespace

Show special markers for tab and endline characters in ~prog-mode~

#+begin_src emacs-lisp
(use-package whitespace-mode
  :custom
  (whitespace-style '(tab-mark newline-mark))
  (whitespace-display-mappings '((newline-mark ?\n    [?¬ ?\n]  [?$ ?\n])
                                 (tab-mark     ?\t    [?» ?\t] [?\\ ?\t])))
  :hook
  (prog-mode . whitespace-mode))
#+end_src

** Deactivate extended region in visual mode

Allow to visual mode work more like vim visual highlighting.

#+begin_src emacs-lisp
(set-face-attribute 'region nil :extend nil)
#+end_src

** Dark and transparent title bar in macOS

#+BEGIN_SRC emacs-lisp
(when (memq window-system '(mac ns))
  (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
  (add-to-list 'default-frame-alist '(ns-appearance . dark)))
#+END_SRC

** Highlight TODO, FIXME, etc

#+BEGIN_SRC emacs-lisp
(defun my/highlight-todo-like-words ()
  "Highlight TODO like keywords."
  (font-lock-add-keywords
   nil `(("\\<\\(FIXME\\|TODO\\|BUG\\)" 1 font-lock-warning-face t)
         ("\\<\\(NOTE\\|HACK\\)" 1 font-lock-doc-face t))))

(add-hook 'prog-mode-hook 'my/highlight-todo-like-words)
#+END_SRC

** Auto fill mode

Use ~auto-fill-mode~ only for comments and only with programming buffers

#+begin_src emacs-lisp
(add-hook 'prog-mode-hook #'(lambda ()
                              (setq-local comment-auto-fill-only-comments t)
                              (auto-fill-mode 1)))
#+end_src

** Load PATH environment

=exec-path-from-shell= by default uses =("-l" "-i")= when starts a new shell to get the =PATH=, =-i= option was removed to open a non interactive shell so it can be faster at startup.

#+BEGIN_SRC emacs-lisp
(use-package exec-path-from-shell
  :ensure t
  :custom
  (exec-path-from-shell-arguments '("-l"))
  (exec-path-from-shell-check-startup-files nil)
  :config
  (when (memq window-system '(mac ns))
    (exec-path-from-shell-initialize)))
#+END_SRC

** Editorconfig

#+BEGIN_SRC emacs-lisp
(use-package editorconfig
  :ensure nil
  :hook
  (elpaca-after-init . editorconfig-mode))
#+END_SRC

** Snippets

#+BEGIN_SRC emacs-lisp
(use-package yasnippet
  :ensure t
  :hook ((prog-mode . yas-minor-mode)
         (conf-mode . yas-minor-mode)
         (text-mode . yas-minor-mode)
         (snippet-mode . yas-minor-mode)))

(use-package yasnippet-snippets
  :ensure t
  :after (yasnippet))
#+END_SRC

** Wakatime

#+begin_src emacs-lisp
(use-package wakatime-mode
  :ensure t
  :if (executable-find "wakatime-cli")
  :init
  (setq wakatime-cli-path (executable-find "wakatime-cli"))
  :config
  (global-wakatime-mode))
#+end_src

** Highlight thing

#+BEGIN_SRC emacs-lisp
(use-package highlight-thing
  :ensure t
  :hook
  (prog-mode . highlight-thing-mode))
#+END_SRC

** Various changes

Disable lock files

#+BEGIN_SRC emacs-lisp
(setq create-lockfiles nil)
#+END_SRC

** Reformatter

#+BEGIN_SRC emacs-lisp
(use-package reformatter
  :ensure t
  :defer t)
#+END_SRC

** Hydra for major modes

#+begin_src emacs-lisp
(use-package major-mode-hydra
  ;; we need to this to be installed before continuing because we need
  ;; it to configure other packages with :hydra option
  :ensure (:wait t)
  :config
  (with-eval-after-load 'evil
    (evil-define-key nil 'global (kbd "<leader>\\") #'(lambda ()
                                                        (interactive)
                                                        (major-mode-hydra)))))

(use-package hydra-posframe
  :ensure (:host github :repo "Ladicle/hydra-posframe")
  :if (display-graphic-p)
  :hook (elpaca-after-init . hydra-posframe-mode))
#+end_src

** Vterm

Function to search into zsh history

#+begin_src emacs-lisp
(defun my/select-from-zsh-history ()
  "Selectt one option from ~/.zsh_history file."
  (with-temp-buffer
    (insert-file-contents (expand-file-name "~/.zsh_history"))
    (let* ((raw-content (buffer-substring-no-properties (point-min) (point-max)))
           (lines (string-split raw-content "\n"))
           (choices (mapcar (lambda (line) (cl-second (string-split line ";"))) lines)))
      (completing-read "Select command: " choices))))

(defun my/insert-from-zsh-history ()
  "Search into zsh history and insert the selected choice into buffer."
  (interactive)
  (when-let* ((selected-choice (my/select-from-zsh-history)))
    (vterm-insert selected-choice)))
#+end_src

#+begin_src emacs-lisp
(use-package vterm
  :ensure t
  :defer t
  :hook
  (vterm-mode . (lambda ()
                  (setq-local show-trailing-whitespace nil)))
  :mode-hydra
  (vterm-mode
   (:title (concat (nerd-icons-icon-for-buffer) " Vterm commands"))
   ("History"
    (("i" my/insert-from-zsh-history "Insert command from history"))))
  :custom
  (vterm-module-cmake-args "-DUSE_SYSTEM_LIBVTERM=yes")
  (vterm-always-compile-module t))
#+end_src

** Toggle terminal

='project= use always the same terminal per project, this way we avoid to create a new terminal for each call to =vterm-toggle=.
='reset-window-configration= yes, it's suppose to be =configration=, for some reason it was defined like this instead of =configuration=

Also for easy access insert mode is activated right away after vterm is shown

#+begin_src emacs-lisp
(use-package vterm-toggle
  :ensure t
  :custom
  (vterm-toggle-scope 'project)
  (vterm-toggle-hide-method 'reset-window-configration)
  :hook
  (vterm-toggle-show . evil-insert-state))
#+end_src

** Spelling

#+begin_src emacs-lisp
(use-package jit-spell
  :ensure t
  :hook ((text-mode . jit-spell-mode)
         (prog-mode . jit-spell-mode))
  :config
  (define-key jit-spell-mode-map (kbd "M-\\") 'jit-spell-correct-word))
#+end_src

** iSpell

Avoid check spelling in markdown code blocks

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'ispell
  (setq ispell-program-name "aspell")
  (add-to-list 'ispell-skip-region-alist
               '("^```" . "^```")))
#+END_SRC

When editing a commit message =ispell= should ignore lines that start with =#=, these lines are diff details about the commit.

#+begin_src emacs-lisp
(defun my/setup-ispell-for-commit-message ()
  "Setup `ispell-skip-region-alist' to avoid lines starting with #.
  This way diff code will be ignored when ispell run."
  (setq-local ispell-skip-region-alist (cons '("^#" . "$") ispell-skip-region-alist)))
#+end_src

** Tree sitter

Incremental code parsing for better syntax highlighting

#+begin_src emacs-lisp
(use-package treesit-auto
  :ensure t
  :custom (treesit-auto-install t))
  ;; current version of treesit-auto-mode has a bug which causes a lot
  ;; of calls which makes Emacs slow, this will disabled until a fix
  ;; is made
  ;; :config
  ;; (global-treesit-auto-mode))
#+end_src

Run ispell in text nodes

#+begin_src emacs-lisp
(use-package treesit-ispell
  :ensure t
  :defer t
  :bind (("C-x C-s" . treesit-ispell-run-at-point)))
#+end_src

Set maximum value for ~font-locking~ in ~treesit~ native syntax highlighting

#+begin_src emacs-lisp
(with-eval-after-load 'treesit
  (setq treesit-font-lock-level 4))
#+end_src

* Evil

#+begin_src emacs-lisp
(defun my/find-file-under-cursor ()
  "Check it the filepath under cursor is an absolute path otherwise open `project-find-file' and insert the filepath."
  (interactive)
  (let ((file-path (thing-at-point 'filename t)))
    (if (file-name-absolute-p file-path)
        (find-file-at-point file-path)
      (minibuffer-with-setup-hook #'(lambda () (insert file-path))
        (project-find-file)))))
#+end_src

#+BEGIN_SRC emacs-lisp
(use-package evil
  :ensure t
  :defer t
  :init
  (setq evil-emacs-state-cursor '("white" box)
        evil-normal-state-cursor '("green" box)
        evil-visual-state-cursor '("orange" box)
        evil-insert-state-cursor '("red" bar)
        evil-want-keybinding nil
        ;; use emacs-28 undo system
        evil-undo-system 'undo-redo)
  :config
  (evil-mode 1)
  (modify-syntax-entry ?_ "w")
  (define-key evil-normal-state-map (kbd "C-p") 'diff-hl-previous-hunk)
  (define-key evil-normal-state-map (kbd "C-n") 'diff-hl-next-hunk)
  (define-key evil-normal-state-map "gf" 'my/find-file-under-cursor)
  (define-key evil-motion-state-map "gd" 'my/goto-definition-dumb-jump-fallback)
  (define-key evil-motion-state-map "gr" 'xref-find-references)
  (define-key evil-motion-state-map "K" 'eldoc-doc-buffer)
  (add-hook 'prog-mode-hook #'(lambda ()
                                (modify-syntax-entry ?_ "w")))

  ;; Setup leader key only for `normal', `visual' and `motion' modes
  (evil-set-leader '(normal visual motion) (kbd "\\"))

  (evil-define-key nil 'global (kbd "<leader>SPC") #'(lambda ()
                                                       (interactive)
                                                       (call-interactively #'execute-extended-command)))
  (evil-define-key nil 'global (kbd "<leader>a") #'(lambda ()
                                                     (interactive)
                                                     (if (region-active-p)
                                                         (my/grep-in-project (buffer-substring-no-properties (region-beginning) (region-end)))
                                                       (my/grep-in-project (thing-at-point 'symbol)))))
  (evil-define-key nil 'global (kbd "<leader>A") 'my/grep-in-project)
  (evil-define-key nil 'global (kbd "<leader>ba") 'my/add-bookmark)
  (evil-define-key nil 'global (kbd "<leader>bb") 'my/bookmark-switch)
  (evil-define-key nil 'global (kbd "<leader>B") #'(lambda ()
                                                     (interactive)
                                                     (call-interactively #'switch-to-buffer)))
  (evil-define-key nil 'global (kbd "<leader>c") 'vterm-toggle)
  (evil-define-key nil 'global (kbd "<leader>e") 'my/find-file-in-project)
  (evil-define-key nil 'global (kbd "<leader>E") 'my/find-file-in-project-local)
  (evil-define-key nil 'global (kbd "<leader>f") 'find-file)
  (evil-define-key nil 'global (kbd "<leader>g") 'magit-status)
  (evil-define-key nil 'global (kbd "<leader>G") 'magit-file-dispatch)
  (evil-define-key nil 'global (kbd "<leader>i") 'consult-imenu)
  (evil-define-key nil 'global (kbd "<leader>hs") 'diff-hl-stage-current-hunk)
  (evil-define-key nil 'global (kbd "<leader>hk") 'diff-hl-revert-hunk)
  (evil-define-key nil 'global (kbd "<leader>j") 'jj-log)
  (evil-define-key nil 'global (kbd "<leader>k") 'kill-buffer)
  (evil-define-key nil 'global (kbd "<leader>l") 'display-line-numbers-mode)
  (evil-define-key nil 'global (kbd "<leader>n") 'evil-buffer-new)
  (evil-define-key nil 'global (kbd "<leader>N") 'my/toggle-narrowing)
  (evil-define-key nil 'global (kbd "<leader>pa") 'my/copy-abs-path)
  (evil-define-key nil 'global (kbd "<leader>pr") 'my/copy-relative-path)
  (evil-define-key nil 'global (kbd "<leader>q") 'my/consult-line)
  (evil-define-key nil 'global (kbd "<leader>r") 'my/replace-at-point-or-region)
  (evil-define-key nil 'global (kbd "<leader>R") '(lambda ()
                                                    (interactive)
                                                    (save-excursion)
                                                    (with-current-buffer "*compilation*"
                                                      (recompile))))
  (evil-define-key nil 'global (kbd "<leader>s") 'my/toggle-spanish-characters)
  (evil-define-key nil 'global (kbd "<leader>t") 'persp-switch)
  (evil-define-key nil 'global (kbd "<leader>w") 'my/toggle-maximize)
  (evil-define-key nil 'global (kbd "<leader>x") 'my/resize-window)
  (evil-define-key nil 'global (kbd "<leader>y") 'consult-yank-from-kill-ring)

  (face-spec-set
   'evil-ex-substitute-matches
   '((t :foreground "red"
        :strike-through t
        :weight bold)))

  (face-spec-set
   'evil-ex-substitute-replacement
   '((t
      :foreground "green"
      :weight bold))))

(use-package evil-commentary
  :ensure t
  :after (evil)
  :config
  (evil-commentary-mode))

(use-package evil-surround
  :ensure t
  :after (evil)
  :config
  (global-evil-surround-mode 1))

(defun my/replace-at-point-or-region ()
  "Setup buffer replace string for word at point or active region using evil ex mode."
  (interactive)
  (let ((text (if (region-active-p)
                  (buffer-substring-no-properties (region-beginning) (region-end))
                (word-at-point))))
    (evil-ex (concat "%s/" text "/"))))

(use-package evil-matchit
  :ensure t
  :config (global-evil-matchit-mode 1))
#+END_SRC

** Evil collection

Allow to use default terminal keybinding in ~vterm~ without losing evil features

#+begin_src emacs-lisp
(use-package evil-collection
  :ensure t
  :config
  (evil-collection-init '(vterm)))
#+end_src

* Utils

** Which-key

#+BEGIN_SRC emacs-lisp
(use-package which-key
  :ensure nil
  :config
  (which-key-mode)
  (which-key-setup-minibuffer))
#+END_SRC

** Auto pair

Complete parenthesis, square brackets, etc

#+BEGIN_SRC emacs-lisp
(use-package elec-pair
  :ensure nil
  :config
  (electric-pair-mode))
#+END_SRC

** Restclient

#+BEGIN_SRC emacs-lisp
(defun my/toggle-restclient-follow-redirect ()
  "Toggle follow redirect for the current session."
  (interactive)
  (setopt restclient-follow-redirects (not restclient-follow-redirects))
  (if restclient-follow-redirects
      (message "Follow redirect enabled")
    (message "Follow redirect disabled")))

(use-package restclient
  :ensure t
  :defer t
  :mode (("\\.http\\'" . restclient-mode))
  :mode-hydra
  (restclient-mode
   (:title (concat (nerd-icons-icon-for-buffer) " restclient commands"))
   ("Format"
    ;; TODO: change to only apply json formatting when the content-type is application/json
    (("f" json-format-region))
    "Customization"
    (("SPC" my/toggle-restclient-follow-redirect "Toggle follow redirect"))
    "Secrets"
    (("h" cloak-mode "Toggle cloak" :toggle t))
    "Request"
    (("r" restclient-http-send-current-stay-in-window "Execute request")
     ("c" restclient-copy-curl-command "Copy CURL command")))))
#+END_SRC

** Rainbow delimiters

#+BEGIN_SRC emacs-lisp
(use-package rainbow-delimiters
  :ensure t
  :hook
  (prog-mode . rainbow-delimiters-mode))
#+END_SRC

** XML formatter

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define xml-format
    :program "xmlformat"
    :group 'xml))

(use-package nxml-mode
  :ensure nil
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " XML Commands"))
   ("Format"
    (("f" xml-format-buffer)))))
#+END_SRC

** SQL formatter

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define sql-format
    :program "sqlfluff"
    :args '("format" "--dialect" "postgres" "--nocolor" "-")))

(defun my/format-sql ()
  "Format active region otherwise format the entire buffer."
  (interactive)
  (if (region-active-p)
      (sql-format-region (region-beginning) (region-end))
    (sql-format-buffer)))

(use-package sql
  :ensure nil
  :hook ((sql-mode . flymake-sqlfluff-load)
         (sql-mode . flymake-mode))
  :mode-hydra
  (sql-mode
   (:title (concat (nerd-icons-icon-for-buffer) " SQL Commands"))
   ("Format"
    (("f" my/format-sql)))))
#+END_SRC

** SQL linter using [[https://www.sqlfluff.com][sqlfluff]]

#+begin_src emacs-lisp
(use-package flymake-sqlfluff
  :ensure t)
#+end_src

* Common packages

Used in every major mode

** Company

#+BEGIN_SRC emacs-lisp
(use-package company
  :ensure t
  :init
  (setq company-idle-delay 0.1
        company-tooltip-limit 10
        company-minimum-prefix-length 3)
  :hook (elpaca-after-init . global-company-mode)
  :config
  (define-key company-active-map (kbd "C-n") 'company-select-next)
  (define-key company-active-map (kbd "C-p") 'company-select-previous))
#+END_SRC

** Flymake

Only activate =flymake= for actual projects and for =prog-mode=

#+begin_src emacs-lisp
(defun my/setup-flymake ()
  "Activate flymake only if we are inside a project."
  (when (functionp 'my/project-p)
    (flymake-mode 1)))

(add-hook 'prog-mode-hook 'my/setup-flymake)

(with-eval-after-load "flymake"
  (setq flymake-show-diagnostics-at-end-of-line nil)
  (define-key flymake-mode-map (kbd "C-S-n") 'flymake-goto-next-error)
  (define-key flymake-mode-map (kbd "C-S-p") 'flymake-goto-prev-error))
#+end_src

** Mise

Handle environment variables per buffer using a ~mise.toml~ file.

#+BEGIN_SRC emacs-lisp
(use-package mise
  :ensure t
  :hook
  (elpaca-after-init . global-mise-mode))
#+END_SRC

Hydra commands, this have to be defined this way instead of using ~:mode-hydra~ inside ~use-package~ because
for some reason it mess up with environment variables loading

#+begin_src emacs-lisp
;TODO: migrate to use a specific major mode for mise.toml files
;; (with-eval-after-load 'major-mode-hydra
;;   (major-mode-hydra-define envrc-file-mode
;;     (:title (concat (nerd-icons-icon-for-buffer) " envrc commands"))
;;     ("Secrets"
;;      (("h" cloak-mode "Toggle cloak" :toggle t)))))
#+end_src

** Cloak mode

Hide values that match regex patterns for ~restclient~ files

#+begin_src emacs-lisp
(use-package cloak-mode
  :ensure t
  :custom
  (cloak-mode-patterns '((restclient-mode . "^:[^: ]+[ \t]*=[ \t]*\\(.+?\\)$")
                         ("mise.local.toml" . "[a-zA-Z_-]+[ \t]*=[ \t]*\"\\([^\"]+\\)\"")))
  (cloak-mode-mask "🙈🙈🙈")
  :hook
  (elpaca-after-init . global-cloak-mode))
#+end_src

** perspective.el

#+begin_src emacs-lisp
(use-package perspective
  :ensure t
  :custom
  ;; disable warnings
  (persp-suppress-no-prefix-key-warning t)
  (persp-modestring-short t)
  :config
  (persp-mode)
  ;; setup vim tab like key-bindings
  (define-key evil-normal-state-map (kbd "gt") 'persp-next)
  (define-key evil-normal-state-map (kbd "gT") 'persp-prev))
#+end_src

** Project.el

~project.el~ default prefix is ~C-x~

#+begin_src emacs-lisp
(defun my/project-edit-dir-locals ()
  "Edit .dir-locals.el file in project root."
  (interactive)
  (find-file (expand-file-name ".dir-locals.el" (my/project-root))))

(defun my/project-edit-mise ()
  "Edit mise.toml file in project root."
  (interactive)
  (find-file (expand-file-name "mise.toml" (my/project-root))))

(use-package project
  :ensure nil
  :bind (:map project-prefix-map
              ("m" . 'my/project-edit-mise)
              ("d" . 'project-dired)
              ("e" . 'my/project-edit-dir-locals)
              ("k" . 'my/project-kill-buffers)
              ("n" . 'my/project-open-new-project)
              ("p" . 'my/project-switch)))
#+end_src

Define helper functions to be used by other packages

#+begin_src emacs-lisp
(defun my/project-root ()
  "Return project root path."
  (project-root (project-current)))

(defun my/project-p ()
  (project-current))

(defun my/project-name ()
  "Get project name extracting latest part of project path."
  (if (my/project-p)
      (cl-second (reverse (split-string (my/project-root) "/")))
    nil))
#+end_src

~perspective.el~ integration, a new perspective should be "attached" to a project so it's easy to switch between them.

#+BEGIN_SRC emacs-lisp
(defun my/project-switch ()
  "Switch to a project and trigger switch action."
  (interactive)
  ;; make sure all the projects list is available to be used
  (project--ensure-read-project-list)
  (let* ((projects (mapcar 'car project--list))
         (choice (completing-read "Switch to project: " projects))
         (default-directory choice))
    ;; `default-directory' must be defined so `project.el' can know is in a new project
    (my/project-switch-action)))

(defun my/project-switch-action ()
  "Switch to a new perspective which name is project's name and open `project-find-file'."
  (interactive)
  (persp-switch (my/project-name))
  (project-find-file))

(defun my/project-kill-buffers ()
  "Kill all the related buffers to the current project and delete its perspective as well."
  (interactive)
  (let* ((project-name (my/project-name))
         (project (project-current))
         (buffers-to-kill (project--buffers-to-kill project)))
    (when (yes-or-no-p (format "Kill %d buffers in %s?" (length buffers-to-kill) (my/project-root)))
      ;; in case we're using eglot we shutdown its server
      (if (and (featurep 'eglot) (eglot-managed-p))
          (eglot-shutdown (eglot-current-server)))
      (mapc #'kill-buffer buffers-to-kill)
      (persp-kill project-name))))

(defun my/project-open-new-project ()
  "Open a project for the first time and add it to `project.el' projects list."
  (interactive)
  (let* ((project-path-abs (read-directory-name "Enter project root: "))
         ;; we need to define `default-directory' to be able to get the new project when `project-current' is called
         (default-directory (replace-regexp-in-string (expand-file-name "~") "~" project-path-abs)))
    (project-remember-project (project-current))
    (my/project-switch-action)))
#+END_SRC

** Completion

UI for completion

#+begin_src emacs-lisp
(use-package vertico
  :ensure t
  :hook
  (elpaca-after-init . vertico-mode)
  :custom
  ;; fixed height
  (vertico-resize nil)
  ;; show max 15 elements
  (vertico-count 15)
  :config
  ;; delete word like a regular buffer in emacs state
  (define-key vertico-map (kbd "C-w") #'vertico-directory-delete-word)
  ;; `C-;' will open embark and `o' with execute `find-file-other-window'
  (define-key vertico-map (kbd "C-<return>") (kbd "C-; o")))
#+end_src

Vertico posframe, show all the candidates in a child-frame, it will activated only for GUI version.

#+begin_src emacs-lisp
(use-package vertico-posframe
  :ensure t
  :if (display-graphic-p)
  :custom
  (vertico-posframe-width 100)
  (vertico-posframe-poshandler (lambda (info)
                                 (cons (/ (- (plist-get info :parent-frame-width)
                                             (plist-get info :posframe-width))
                                          2)
                                       40)))
  :hook
  (vertico-mode . vertico-posframe-mode))
#+end_src

Add annotations to results shown by vertico

#+begin_src emacs-lisp
(use-package marginalia
  :ensure
  :custom
  (marginalia-align 'right)
  :hook
  (elpaca-after-init . marginalia-mode))
#+end_src

Icons support

#+begin_src emacs-lisp
(use-package nerd-icons-completion
  :ensure t
  :after marginalia
  :config
  (nerd-icons-completion-mode)
  (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup))
#+end_src

Enable better completion styles

#+begin_src emacs-lisp
(use-package orderless
  :ensure t
  :config
  (setq completion-styles '(orderless basic)
        completion-category-overrides '((file (styles basic partial-completion)))))
#+end_src

Disable orderless completion style in company to keep previous behaviour which I like, this was copied from ~orderless~ documentation.

#+begin_src emacs-lisp
;; We follow a suggestion by company maintainer u/hvis:
;; https://www.reddit.com/r/emacs/comments/nichkl/comment/gz1jr3s/
(defun company-completion-styles (capf-fn &rest args)
  (let ((completion-styles '(basic partial-completion)))
    (apply capf-fn args)))

(advice-add 'company-capf :around #'company-completion-styles)
#+end_src

Search into project source

#+begin_src emacs-lisp
(use-package consult
  :ensure t
  :config
  ;; without --hidden ripgrep avoid hidden files like yaml files
  ;; inside a .github folder or this file inside .emacs.d
  (setopt consult-ripgrep-args (concat consult-ripgrep-args " --hidden "))
  ;; Use consult for completion inside minibuffer, for example when
  ;; searching for a file.
  (setq completion-in-region-function #'consult-completion-in-region))
#+end_src

Custom ~consult-line~

#+begin_src emacs-lisp
(defun my/consult-line ()
  "Call `consult-line' with current selection if possible."
  (interactive)
  (if (region-active-p)
      (let ((text (buffer-substring-no-properties (region-beginning) (region-end))))
        (minibuffer-with-setup-hook #'(lambda () (insert text)) (consult-line)))
    (consult-line)))
#+end_src

Integration with yasnippets

#+begin_src emacs-lisp
(use-package consult-yasnippet
  :ensure t
  :defer t)
#+end_src

Helpers to search term at point and general search into project

#+begin_src emacs-lisp
(defun my/grep-in-project (&optional term)
  "Run grep in current project.
If TERM is not nil it will be used as initial value."
  (interactive)
  (let* ((pattern (read-string "Pattern: " (or term "")))
         ;; add an extra space to be able to start typing more filters
         (pattern (concat pattern " ")))
    (call-interactively #'(lambda ()
                            (interactive)
                            (consult-ripgrep (my/project-root) pattern)))))
#+end_src

Switch fonts like ~consult-theme~

#+begin_src emacs-lisp
(defun my/consult-font (font)
  "Replace current font with FONT from `font-family-list'."
  (interactive
   (list
    (let ((saved-font (symbol-name (font-get (face-attribute 'default :font) :family))))
      (consult--read
       (font-family-list)
       :prompt "Font: "
       :require-match t
       :state (lambda (action font)
                (pcase action
                  ('return (my/consult-font (or font saved-font)))
                  ((and 'preview (guard font)) (my/consult-font font))))
       ))))
  (when font
    ;; size doesn't change during scrolling so we can reuse that to
    ;; configure new selected font
    (set-face-attribute 'default nil :font (format "%s %d" font (font-get (face-attribute 'default :font) :size)))))
#+end_src

Integration with ~embark~

#+begin_src emacs-lisp
(use-package embark
  :ensure t
  :bind
  ("C-;" . embark-act)
  :config
  ;; grep exported data can have a lot of white spaces so we don't want
  ;; them to be shown while editing their content
  (setq-mode-local embark-collect-mode show-trailing-whitespace nil))

(use-package embark-consult
  :ensure t
  :defer t)

(defun my/edit-completing-results ()
  "Use results origin to execute an action after export them with `embark-export'."
  (interactive)
  ;; call of `project-find-file'
  (when (cl-search "Find file in" (buffer-string))
    (run-at-time 0 nil #'embark-export)
    (run-at-time 0 nil #'wdired-change-to-wdired-mode)
    (run-at-time 0 nil #'evil-normal-state))
  ;; call of `consult-ripgrep'
  (when (cl-search "Ripgrep" (buffer-string))
    ;; we use `run-at-time' to ensure all of these steps
    ;; will be executed in order
    (run-at-time 0 nil #'embark-export)
    (run-at-time 0 nil #'wgrep-change-to-wgrep-mode)
    (run-at-time 0 nil #'evil-normal-state)))

(define-key minibuffer-mode-map (kbd "C-c C-e") #'my/edit-completing-results)
#+end_src

Edit grep buffer

#+begin_src emacs-lisp
(use-package wgrep
  :ensure t
  :custom
  (wgrep-auto-save-buffer t)
  :bind
  ("C-c C-c" . 'wgrep-finish-edit)
  ("C-c C-k" . 'wgrep-abort-changes))
#+end_src


* Emacs process list

#+begin_src emacs-lisp
(defun my/kill-emacs-process ()
  "Show a list of current Emacs processes and kill the selected one."
  (interactive)
  (let* ((names (mapcar #'process-name (process-list)))
         (process-name (completing-read "Choose process: " names)))
    (delete-process (get-process process-name))
    (message "%s process killed" (propertize process-name 'face '(:foreground "magenta")))))

(global-set-key (kbd "C-x c p") 'my/kill-emacs-process)
#+end_src

* Git backup

Save a backup on every save, also allow to recover any version of a file

#+begin_src emacs-lisp :lexical t
(defvar my/backup-dir (expand-file-name "~/.git-backup"))

(defun my/git-backup-versioning ()
  "Save a version of the current file."
  (unless (featurep 'git-backup)
    (require 'git-backup))
  (git-backup-version-file (executable-find "git") my/backup-dir '() (buffer-file-name)))

(defun my/git-backup-run-action (command commit-hash)
  "Execute COMMAND with COMMIT-HASH using another defaults arguments."
  (apply command `(,(executable-find "git") ,my/backup-dir ,commit-hash ,(buffer-file-name))))

(defun my/git-backup-sort (completions)
  "Given COMPLETIONS define a custom sort function."
  (lambda (string pred action)
    (if (eq action 'metadata)
        '(metadata (display-sort-function . identity))
      (complete-with-action action completions string pred))))

(defun my/git-backup ()
  "Navigate in versions of the current file."
  (interactive)
  (unless (featurep 'git-backup)
    (require 'git-backup))
  ;; for some reason an extra space after `%h|' is required to avoid an error when
  ;; the shell command is executed
  (let* ((candidates (git-backup-list-file-change-time (executable-find "git") my/backup-dir "%cI|%h| %ar" (buffer-file-name)))
         (selection (completing-read "Pick revision: " (my/git-backup-sort candidates)))
         (commit-hash (nth 1 (string-split selection "|")))
         (action (completing-read "Choose action: " '("diff" "new buffer" "replace current buffer"))))
    (cond ((string-equal action "diff") (my/git-backup-run-action 'git-backup-create-ediff commit-hash))
          ((string-equal action "new buffer") (my/git-backup-run-action 'git-backup-open-in-new-buffer commit-hash))
          ((string-equal action "replace current buffer") (my/git-backup-run-action 'git-backup-replace-current-buffer commit-hash))
          (t (message "Not valid option")))))

(use-package git-backup
  :ensure t
  :hook (after-save . my/git-backup-versioning))
#+end_src

* Meme

Create memes from a file or the macOS clipboard in a graphical editor with a live
preview. ImageMagick is only required when saving the result to a format other than SVG.

#+begin_src emacs-lisp
(use-package mini-meme
  :ensure (:host github :repo "erickgnavar/mini-meme.el")
  :commands mini-meme)

(defun my/meme-from-clipboard ()
  "Create a meme using an image from clipboard."
  (interactive)
  (unless (executable-find "pngpaste")
    (user-error "please install pngpaste"))

  (let* ((filepath (make-temp-file "clipboard" nil ".png"))
         (command (format "pngpaste %s" filepath))
         (command-stdout (shell-command-to-string command)))
    ;; pngpaste returns "" when found a valid image in the clipboard
    (unless (string-equal command-stdout "")
      (user-error (string-trim command-stdout)))

    (mini-meme filepath)
    ;; Mini Meme embeds the image when rendering, so keep the temporary
    ;; file until its editor buffer is closed.
    (add-hook 'kill-buffer-hook
              (lambda ()
                (when (file-exists-p filepath)
                  (delete-file filepath)))
              nil t)))
#+end_src

* Orgmode

Configured variables:

- =org-latex-caption-above= puts table captions at the bottom
- =org-src-fontify-natively= enables syntax highlighting for code blocks
- =org-log-done= saves the timestamp when a task is done
- =org-src-preserve-indentation= when is =t= avoid to insert a left indentation in source blocks

#+begin_src emacs-lisp

(defun my/org-insert-image-from-clipboard ()
  "Insert image from clipboard using an org tag"
  (interactive)
  (let* ((image-name (read-string "Filename: " "image.png"))
         (images-folder "./images")
         (image-path (format "%s/%s" images-folder image-name)))
    (unless (file-directory-p images-folder)
      (shell-command (format "mkdir -p %s" images-folder)))
    (shell-command (format "pngpaste %s" image-path))
    (insert (format "[[file:%s]]" image-path))))

(with-eval-after-load 'evil
  (evil-define-key nil org-mode-map (kbd "<leader>mii") 'my/org-insert-image-from-clipboard))

#+end_src

When I read books on Apple Books and I want to insert some quote Apple Books insert some text I don't want in my notes, this function delete that and just insert the meaning part using org quote syntax.

#+begin_src emacs-lisp
(defun my/org-insert-quote-from-apple-books ()
  "Take quote from clipboard and remove all the unnecesary text and insert
    an org quote in the current position"
  (interactive)
  (let* ((raw-value (current-kill 0 t))
         (tmp (cl-second (split-string raw-value "“")))
         (quote-value (car (split-string tmp "”"))))
    (insert "#+begin_quote\n")
    (insert (concat quote-value "\n"))
    (insert "#+end_quote\n")))
#+end_src

#+BEGIN_SRC emacs-lisp
(defvar my/org-src-block-tmp-window-configuration nil)

(defun my/org-edit-special (&optional arg)
  "Save current window configuration before a org-edit buffer is open."
  (setq my/org-src-block-tmp-window-configuration (current-window-configuration)))

(defun my/org-edit-src-exit ()
  "Restore the window configuration that was saved before org-edit-special was called."
  (set-window-configuration my/org-src-block-tmp-window-configuration))

(with-eval-after-load 'org
  ;; do not show trailing spaces in calendar buffer
  (add-hook 'calendar-mode-hook #'(lambda ()
                                    (setq-local show-trailing-whitespace nil)))

  (setq org-latex-caption-above nil
        org-src-fontify-natively t
        org-src-preserve-indentation t
        ;; use tectonic for export to PDF
        org-latex-pdf-process '("tectonic -X compile --outdir=%o -Z shell-escape %f")
        org-log-done t)
  (org-clock-persistence-insinuate)

  (add-hook 'org-mode-hook #'(lambda ()
                               (setq-local fill-column 100)
                               (auto-fill-mode 1)
                               ;; Since emacs 27 this is needed to use shortcuts like <s to create source blocks
                               (unless (featurep 'org-tempo)
                                 (require 'org-tempo))
                               (org-indent-mode -1)
                               ;; disable electric-pair to avoid conflict with ~<s~
                               (electric-pair-local-mode -1)))

  (advice-add 'org-edit-special :before 'my/org-edit-special)
  (advice-add 'org-edit-src-exit :after 'my/org-edit-src-exit)

  ;; setup python executable, it can be installed from homebrew, nix
  ;; or in another way
  (setq org-babel-python-command (executable-find "python3"))

  (org-babel-do-load-languages 'org-babel-load-languages
                               '((python . t)
                                 (shell . t)
                                 (lisp . t)
                                 (sql . t)
                                 (dot . t)
                                 (plantuml . t)
                                 (emacs-lisp . t))))

(use-package htmlize
  :ensure t
  :after (org))
#+END_SRC

Fix error with ~TAB~ in ~evil-mode~ in ~org-mode~ with org elements.

#+begin_src emacs-lisp
(defun my/org-tab ()
  "Run `org-cycle' only at point of an org element."
  (interactive)
  (if (org-element-at-point)
      (org-cycle)
    (evil-jump-forward)))

(with-eval-after-load 'org
  (define-key org-mode-map (kbd "<tab>") 'my/org-tab))
#+end_src

** Org typst

Allow to export to PDF using ~typst~ instead of ~latex~

#+begin_src emacs-lisp
(use-package ox-typst
  :ensure t
  :after org)
#+end_src

** Org modern

#+begin_src emacs-lisp
(use-package org-modern
  :ensure t
  :custom
  (org-modern-fold-stars '(("▶" . "▼") ("▷" . "▽") ("▹" . "▿") ("▸" . "▾")))
  :hook
  (org-mode . org-modern-mode))
#+end_src

** Org tree slide

A tool to show org file as an slideshow

=hide-mode-line= hide the modeline to allow to have a clean screen while using =org-tree-slide-mode=

#+begin_src emacs-lisp
(use-package hide-mode-line
  :ensure t
  :defer t)
#+end_src

Some tweaks to have a better looking while presenting slides

#+begin_src emacs-lisp
(defun my/org-tree-slide-setup ()
  (org-display-inline-images)
  (hide-mode-line-mode 1))

(defun my/org-tree-slide-end ()
  (org-display-inline-images)
  (hide-mode-line-mode 0))

(use-package org-tree-slide
  :ensure t
  :defer t
  :custom
  (org-image-actual-width nil)
  (org-tree-slide-activate-message "Presentation started!")
  (org-tree-slide-deactivate-message "Presentation finished!")
  :hook ((org-tree-slide-play . my/org-tree-slide-setup)
         (org-tree-slide-stop . my/org-tree-slide-end))
  :bind (:map org-tree-slide-mode-map
              ("C-<" . org-tree-slide-move-previous-tree)
              ("C->" . org-tree-slide-move-next-tree)))
#+end_src

* Denote

Note taking using ~denote~

#+begin_src emacs-lisp
(use-package denote
  :ensure t
  :custom (denote-directory "~/Documents/wiki")
  :hook ((dired-mode . denote-dired-mode)))

(defun my/wiki ()
  "Open personal wiki and launch Dired."
  (interactive)
  (dired (expand-file-name "~/Documents/wiki"))
  (dired-hide-details-mode t))
#+end_src

* Typst

Define formatter helpers using ~typstyle~

#+begin_src emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define typst-format
    :program "typstyle"))
#+end_src

#+begin_src emacs-lisp
(defun my/typst-compile-and-preview ()
  "Compile and preview PDF for the current buffer."
  (interactive)
  (typst-ts-compile t))

(use-package typst-ts-mode
  :ensure (:type git :host codeberg :repo "meow_king/typst-ts-mode")
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Typst Commands"))
   ("Build"
    (("b" typst-ts-compile "Compile")
     ("p" my/typst-compile-and-preview "Compile and preview"))
    "Format"
    (("f" typst-format-buffer "Format")))))
#+end_src

Set up LSP

#+begin_src emacs-lisp
(with-eval-after-load 'eglot
  (with-eval-after-load 'typst-ts-mode
    (add-to-list 'eglot-server-programs
                 `((typst-ts-mode) .
                   ,(eglot-alternatives '("tinymist"
                                          "typst-lsp"))))))
#+end_src

* Git

** Git-link

Open selected region in remote repo page

#+BEGIN_SRC emacs-lisp
(use-package git-link
  :ensure t
  :defer t
  :commands (git-link))
#+END_SRC

** Git modes

This pacakge includes ~gitignore-mode~, ~gitconfig-mode~ and ~gitattributes-mode~

#+BEGIN_SRC emacs-lisp
(use-package git-modes
  :defer t
  :ensure t)
#+END_SRC

** Transient

#+begin_src emacs-lisp
(use-package transient
  :ensure nil
  :custom
  (transient-save-history nil))
#+end_src

** JJ

#+begin_src emacs-lisp
(use-package jj-mode
  :ensure (:host github :repo "bolivier/jj-mode.el")
  :defer t
  :config
  (with-eval-after-load 'evil
    (evil-make-overriding-map jj-mode-map 'normal)))
#+end_src

** Magit

#+BEGIN_SRC emacs-lisp
(defun my/magit-blame-quit ()
  "Restore evil state after magit blame mode is closed."
  (evil-exit-emacs-state))

(use-package magit
  :ensure t
  :defer t
  :custom
  ;; restore previous window configuration after a buffer is closed
  (magit-bury-buffer-function 'magit-restore-window-configuration)
  ;; open magit status buffer in the whole frame
  (magit-display-buffer-function 'magit-display-buffer-fullframe-status-v1)
  :config
  (advice-add 'magit-blame-quit :after 'my/magit-blame-quit)
  (add-hook 'git-commit-mode-hook 'my/setup-ispell-for-commit-message)
  (add-hook 'magit-blame-mode-hook #'(lambda () (evil-emacs-state))))
#+END_SRC

** Magit delta

Use ~delta~ tool to show diffs in magit

#+begin_src emacs-lisp
(use-package magit-delta
  :ensure t
  :after (magit))

(defun my/toggle-magit-delta ()
  "Toggle magit-delta mode."
  (interactive)
  (magit-delta-mode
   (if magit-delta-mode -1 1))
  (magit-refresh))

(with-eval-after-load 'magit
  (transient-append-suffix 'magit-diff '(-1 -1 -1)
    '("D" "Toggle magit-delta" my/toggle-magit-delta)))
#+end_src

** Git diff-hl

#+BEGIN_SRC emacs-lisp
(use-package diff-hl
  :ensure t
  :custom
  (diff-hl-show-staged-changes nil)
  ;; disable highlight reference to avoid confusion with status
  ;; indicators in fringe area
  (diff-hl-highlight-reference-function nil)
  ;; for some reason the :hook form doesn't work so we have to use :init
  :init
  (add-hook 'magit-pre-refresh-hook 'diff-hl-magit-pre-refresh)
  (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh)
  (add-hook 'dired-mode-hook 'diff-hl-dired-mode)
  :config
  (global-diff-hl-mode))
#+END_SRC

** Timemachine

#+BEGIN_SRC emacs-lisp
(use-package git-timemachine
  :ensure t
  :config
  (add-hook 'git-timemachine-mode-hook #'(lambda () (evil-emacs-state))))
#+END_SRC

** Gist

#+BEGIN_SRC emacs-lisp
(defun my/gist ()
  "Create a public gist."
  (interactive)
  (my/create-gist t))

(defun my/private-gist ()
  "Create a private gist."
  (interactive)
  (my/create-gist :false))

(defun my/create-gist (public)
  "Create a gist, given PUBLIC value it will set as private or public."
  (let* ((oauth-token (my/gist-get-oauth-token))
         (content (my/gist-get-content))
         (payload (make-hash-table))
         (filename (or (and (buffer-file-name) (file-name-nondirectory (buffer-file-name))) (buffer-name)))
         (files (make-hash-table))
         (file (make-hash-table)))
    (puthash "description" "" payload)
    (puthash "public" public payload)
    (puthash "content" content file)
    (puthash filename file files)
    (puthash "files" files payload)
    (let ((url-request-method "POST")
          (url-request-extra-headers `(("Content-Type" . "application/json") ("User-Agent" . "gist.el") ("Authorization" . ,(concat "Bearer " oauth-token))))
          (url-request-data (json-encode payload)))
      (url-retrieve "https://api.github.com/gists" 'my/gist-handle-response))))

(defun my/gist-handle-response (response)
  "Process content of RESPONSE and extract link."
  (let* ((status-line (buffer-substring-no-properties (line-beginning-position) (line-end-position)))
         (status-code (nth 1 (split-string status-line " "))))

    (unless (string-equal status-code "201")
      (user-error "There was a problem with GitHub API, try again later"))

    (search-forward "\n\n")
    (let* ((raw-response (buffer-substring-no-properties (point) (point-max)))
           (data (json-parse-string raw-response))
           (gist-link (gethash "html_url" data)))
      (kill-new gist-link)
      (message "Paste created: %s" gist-link))))

(defun my/gist-get-oauth-token ()
  "Fetch oauth token from ~/.authinfo."
  (let ((entry (car (auth-source-search :host "gist"))))
    (unless entry
      (user-error "GitHub oauth token must be in ~/.authinfo using 'gist' as host"))
    (funcall (plist-get entry :secret))))

(defun my/gist-get-content ()
  "Create payload using current region or the whole buffer."
  (if (region-active-p)
      (buffer-substring-no-properties (region-beginning) (region-end))
    (buffer-substring-no-properties (point-min) (point-max))))
#+END_SRC

** Linkode

#+BEGIN_SRC emacs-lisp
(use-package linkode
  :ensure t
  :defer t)
#+END_SRC

* Web

** Web mode

#+BEGIN_SRC emacs-lisp
(defun my/web-mode-hook ()
  (emmet-mode)
  (rainbow-delimiters-mode-disable))

(use-package web-mode
  :ensure t
  :custom
  (web-mode-enable-current-element-highlight t)
  (web-mode-enable-current-column-highlight t)
  :mode (("\\.html\\'" . web-mode)
         ("\\.html.eex\\'" . web-mode)
         ("\\.html.leex\\'" . web-mode)
         ("\\.hbs\\'" . web-mode))
  :config
  (add-hook 'web-mode-hook 'my/web-mode-hook))
#+END_SRC

** Emmet

#+BEGIN_SRC emacs-lisp
(use-package emmet-mode
  :ensure t
  :defer t)
#+END_SRC

** CSS mode

#+BEGIN_SRC emacs-lisp
(use-package css-ts-mode
  :ensure nil
  :mode "\\.css\\'"
  :defer t
  :hook (css-ts-mode . flymake-biome-load)
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " CSS Commands"))
   ("Format"
    (("f" css-format-buffer)))))
#+END_SRC

#+begin_src emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define css-format
    :program "biome"
    :args '("format" "--stdin-file-path" "a.css" "--css-formatter-indent-style" "space")))
#+end_src

** Rainbow

#+BEGIN_SRC emacs-lisp
(use-package rainbow-mode
  :ensure t
  :hook
  ((css-ts-mode . rainbow-mode)
   (sass-mode . rainbow-mode)
   (heex-ts-mode . rainbow-mode)
   (scss-mode . rainbow-mode)))
#+END_SRC

* Telegram

In case on error when compiling ~tdlib~ the first time we need to specify where it's installed, for ~macOS~ we can specify:

~(telega-server-libs-prefix "/opt/homebrew/Cellar/tdlib/HEAD-fd3154b")~

The last part can change depending of the version installed

#+begin_src emacs-lisp
(use-package telega
  :ensure t
  :defer t
  :custom
  ;; enable markdown for code snippets
  (telega-chat-input-markups '("markdown2" "org" nil))
  ;; use vertico for completion
  (telega-completing-read-function 'completing-read)
  :config
  ;; use shift enter to make multi line messages and enter to send it
  (define-key telega-chat-mode-map (kbd "S-<return>") #'newline)
  ;; disable copy message link when moving over text with evil-mode
  (define-key telega-msg-button-map (kbd "l") nil)
  ;; avoid showing blank spaces highlighted
  (add-hook 'telega-chat-mode-hook #'(lambda ()
                                       (setq-local show-trailing-whitespace nil))))

(defun my/start-telega ()
  "Start `telega' inside a new perspective and activate 'telega-mode-line-mode'"
  (interactive)
  (persp-switch "*telega*")
  (telega)
  (telega-mode-line-mode))
#+end_src

* Miscellaneous

#+BEGIN_SRC emacs-lisp
(use-package writeroom-mode
  :ensure t
  :defer t)

(use-package csv-mode
  :ensure t
  :defer t)

(with-eval-after-load 'reformatter
  (reformatter-define json-format
    :program "jq"
    :args '(".")
    :group 'json))

(use-package json-ts-mode
  :ensure nil
  :mode "\\.hujson\\'"
  :mode "\\.json[cl]?\\'"
  :mode-hydra
  (json-ts-mode
   (:title (concat (nerd-icons-icon-for-buffer) " JSON commands"))
   ("Format"
    (("f" json-format-buffer)))))

(use-package request
  :ensure t
  :defer t)

(use-package graphql-mode
  :ensure t
  :defer t)

(with-eval-after-load 'reformatter
  (reformatter-define terraform-format
    :program "terraform"
    :args '("fmt" "-")
    :group 'terraform))

(use-package terraform-mode
  :ensure t
  :defer t
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Terraform Commands"))
   ("Format"
    (("f" terraform-format-buffer)))))

(defun my/k8s-apply ()
  "Apply current yaml file to the current kubernetes context."
  (interactive)
  (let ((default-directory (file-name-directory buffer-file-name)))
    (compile (format "kubectl apply -f %s" buffer-file-name))))

(defun my/k8s-delete ()
  "Delete current yaml file to the current kubernetes context."
  (interactive)
  (let ((default-directory (file-name-directory buffer-file-name)))
    (compile (format "kubectl delete -f %s" buffer-file-name))))

(use-package yaml-ts-mode
  :ensure nil
  :mode "\\.ya?ml\\'"
  :hook (yaml-ts-mode . flymake-mode)
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " YAML Commands"))
   ("Format"
    (("f" yaml-pro-format))
    "Kubernetes"
    (("a" my/k8s-apply)
     ("k" my/k8s-delete)))))

(use-package yaml-pro
  :ensure t
  :defer t
  :hook
  (yaml-ts-mode . yaml-pro-ts-mode)
  :config
  ;; this binding conflicts with org indirect mode
  (define-key yaml-pro-ts-mode-map (kbd "C-c '") nil))

(use-package hcl-mode
  :ensure t
  :defer t)

(use-package just-ts-mode
  :ensure t
  :defer t
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Justfile Commands"))
   ("Format"
    (("f" just-ts-format-buffer)))))

(use-package toml-ts-mode
  :ensure nil
  :mode "\\.toml\\'"
  :defer t)

(use-package slint-mode
  :ensure t
  :hook (slint-mode . eglot-ensure)
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Slint Commands"))
   ("Format"
    (("f" eglot-format-buffer)))))

(with-eval-after-load 'reformatter
  (reformatter-define nix-format
    :program "nixpkgs-fmt"
    :group 'nix))

(use-package nix-ts-mode
  :ensure t
  :defer t
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Nix commands"))
   ("Format"
    (("f" nix-format-buffer))))
  :mode "\\.nix\\'")

(use-package d2-mode
  :ensure t
  :defer t
  :mode "\\.d2\\'")

(use-package ob-d2
  :defer t
  :ensure t
  :config
  (with-eval-after-load 'org
    (org-babel-do-load-languages
     'org-babel-load-languages
     '((d2 . t)))))

(use-package mermaid-mode
  :ensure t
  :defer t)

(defun my/preview-mermaid ()
  "Render region inside a webit embebed browser."
  (interactive)
  (unless (region-active-p)
    (user-error "Select a region first"))
  (let* ((path (concat (make-temp-file (temporary-file-directory)) ".html"))
         (mermaid-code (buffer-substring-no-properties (region-beginning) (region-end))))
    (save-excursion
      (with-temp-buffer
        (insert "<body>
  <pre class=\"mermaid\">")
        (insert mermaid-code)
        ;; js script copied from mermaid documentation
        (insert "</pre>
  <script type=\"module\">
    import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
    mermaid.initialize({ startOnLoad: true });
  </script>
</body>")
        (write-file path)))
    (xwidget-webkit-browse-url (format "file://%s" path))))

(defun my/markdown-ts-edit-code-block ()
  "Edit the fenced code block at point in an indirect buffer via `edit-indirect'."
  (interactive)
  (let* ((beg (get-char-property (point) 'markdown-ts-code-beg-marker))
         (end (get-char-property (point) 'markdown-ts-code-end-marker))
         (mode (markdown-ts-code-block-mode-at))
         (indirect-buf (edit-indirect-region (marker-position beg)
                                             (marker-position end)
                                             'display-buffer)))
    (unless (and beg end)
      (user-error "Not in a fenced code block"))
    (with-current-buffer indirect-buf
      (funcall mode))))

(use-package markdown-ts-mode
  :ensure nil
  :mode "\\.md\\'"
  :bind (:map markdown-ts-mode-map
              ("C-c '" . 'my/markdown-ts-edit-code-block))
  :custom
  (markdown-ts-fontify-code-blocks-natively t)
  :config
  (add-hook 'markdown-ts-mode-hook #'(lambda ()
                                       (setq-local fill-column 100)
                                       (auto-fill-mode 1))))

(use-package flymake-markdownlint
  :ensure (:host github :repo "erickgnavar/flymake-markdownlint")
  :hook ((markdown-ts-mode . flymake-markdownlint-load)
         (markdown-ts-mode . flymake-mode)))

(use-package edit-indirect
  :ensure t
  :defer t)

(use-package dockerfile-ts-mode
  :ensure nil
  :defer t
  :mode (("\\Dockerfile\\'" . dockerfile-ts-mode)
         ("\\.dockerignore\\'" . dockerfile-ts-mode)))

(use-package dumb-jump
  :ensure t
  :defer t
  :custom
  (dumb-jump-force-searcher 'rg)
  (dumb-jump-selector 'completing-read))
#+END_SRC

helpful, enhance help functions

#+begin_src emacs-lisp
(use-package helpful
  :ensure t
  :defer t
  :bind (("C-h f" . helpful-callable)
         ("C-h v" . helpful-variable)
         ("C-h k" . helpful-key)))
#+end_src

Use ESC key instead C-g to close and abort

Copied from somewhere

#+BEGIN_SRC emacs-lisp
(defun minibuffer-keyboard-quit ()
  "Abort recursive edit.
  In Delete Selection mode, if the mark is active, just deactivate it;
  then it takes a second \\[keyboard-quit] to abort the minibuffer."
  (interactive)
  (if (and delete-selection-mode transient-mark-mode mark-active)
      (setq deactivate-mark  t)
    (when (get-buffer "*Completions*") (delete-windows-on "*Completions*"))
    (abort-recursive-edit)))

(with-eval-after-load 'evil
  (define-key evil-normal-state-map [escape] 'keyboard-quit)
  (define-key evil-visual-state-map [escape] 'keyboard-quit))

(define-key minibuffer-local-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-ns-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-completion-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-must-match-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-isearch-map [escape] 'minibuffer-keyboard-quit)
(global-set-key [escape] 'evil-exit-emacs-state)
#+END_SRC

websockets client

#+begin_src emacs-lisp
(use-package websocket
  :ensure t
  :defer t)
#+end_src

* LSP

#+begin_src emacs-lisp
(use-package eglot
  :defer t
  :ensure nil
  :bind (:map eglot-mode-map
              ("<M-return>" . 'eglot-code-actions)
              ("C-c C-s" . 'xref-find-references))
  :custom
  (eglot-ignored-server-capabilities '(:inlayHintProvider))
  :config
  ;; only for new(emacs 31 or above) versions of eglot, avoid an error
  ;; where when cursor is at an error it increase its line height a
  ;; little bit
  (setq eglot-code-action-indicator "")
  ;; tailwind LSP support
  (add-to-list 'eglot-server-programs
               '(((heex-ts-mode :language-id "html")
                  (web-mode :language-id "html")) . ("tailwindcss-language-server" "--stdio")))
  ;; elixir LSP server
  (setf (alist-get '(elixir-ts-mode heex-ts-mode) eglot-server-programs) '("expert")))
#+end_src

In case we don't have eglot running we can relay on dumb-jump

#+begin_src emacs-lisp
(defun my/goto-definition-dumb-jump-fallback ()
  "Go to definition using eglot when is active otherwise use dumb-jump."
  (interactive)
  (if (and (featurep 'eglot) (eglot-managed-p))
      (xref-find-definitions (thing-at-point 'symbol))
    (if (member major-mode '(emacs-lisp-mode lisp-interaction-mode))
        (xref-find-definitions (thing-at-point 'symbol))
      (dumb-jump-go))))
#+end_src

Start a ~expert~ process and listen on a random port

#+begin_src emacs-lisp
(defun my/start-elixir-expert-server ()
  "Start expert LSP server."
  (interactive)
  (let* ((port (+ 1024 (random (- 65536 1024))))
         (default-directory (my/project-root))
         (command (format "expert --port %s" port))
         (buffer-name (format "(%s) expert server" (my/project-root))))
    (async-shell-command command (get-buffer-create buffer-name))
    (require 'eglot)
    (setf (alist-get 'elixir-ts-mode eglot-server-programs nil nil #'equal)
          `("localhost" ,port))
    (message "Server running on %d" port)))
#+end_src

* Programming languages

** Shell scripts

Bash formatter using ~shfmt~

#+begin_src emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define sh-format
    :program "shfmt"
    :args '("-i" "2" "-")
    :group 'sh))

(use-package bash-ts-mode
  :ensure nil
  :hook (sh-mode . bash-ts-mode)
  :mode-hydra
  ((:title (concat (nerd-icons--icon-info-for-buffer) " Bash commands"))
   ("Format"
    (("f" sh-format-buffer)))))
#+end_src

** C

=clang-format= is required for this, we can install it with =brew install clang-format=

#+begin_src emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define c-format
    :program "clang-format"))

(use-package c-ts-mode
  :ensure nil
  :mode (("\\.c\\'" . c-ts-mode)
         ("\\.i\\'" . c-ts-mode)
         ("\\.h\\'" . c-ts-mode))
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " C commands"))
   ("Format"
    (("f" c-format-buffer)))))

(use-package c++-ts-mode
  :ensure nil
  :mode (("\\.cc\\'" . c++-ts-mode)
         ("\\.cpp\\'" . c++-ts-mode)
         ("\\.cxx\\'" . c++-ts-mode)
         ("\\.hh\\'" . c++-ts-mode)
         ("\\.hpp\\'" . c++-ts-mode)
         ("\\.hxx\\'" . c++-ts-mode))
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " C++ commands"))
   ("Format"
    (("f" c-format-buffer)))))
#+end_src

** Python

For LSP support one of ~ty~, ~pyrefly~ or ~pyright~ is required

Install ~flymake-ruff~

#+begin_src emacs-lisp
(use-package flymake-ruff
  :ensure t)
#+end_src

Generate a ~pyrightconfig.json~ file

#+begin_src emacs-lisp
(defun my/gen-pyright-config-file ()
  "Generate a configuration file for PyRight."
  (interactive)
  (let* ((input-dir (read-directory-name "Virtualenv directory: "))
         (venv-path (file-name-parent-directory input-dir))
         (venv (file-name-base (replace-regexp-in-string "/$" "" input-dir)))
         (data (make-hash-table)))
    (puthash "venvPath" venv-path data)
    (puthash "venv" venv data)
    (with-temp-buffer
      (insert (json-encode data))
      (write-file (file-name-concat venv-path "pyrightconfig.json")))))
#+end_src

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define python-ruff-format
    :program "ruff"
    :args '("format" "-")
    :group 'python)

  (reformatter-define python-sort-imports
    :program "ruff"
    :args '("check" "--fix" "--select" "I001" "-")
    :group 'python))

(setq python-shell-completion-native-enable nil)

(use-package python-ts-mode
  :ensure nil
  :defer t
  :mode "\\.py\\'"
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Python commands"))
   ("Format"
    (("f" python-ruff-format-buffer)
     ("si" python-sort-imports-region))
    "Testing"
    (("tt" pythontest-test-at-point)
     ("tf" pythontest-test-file)
     ("ta" pythontest-test-all))))
  :hook (python-ts-mode . flymake-ruff-load))
#+END_SRC

Testing package

#+begin_src emacs-lisp
(use-package pythontest
  :ensure t
  :defer t
  :custom
  (pythontest-pytest-command "pytest -s -vv"))
#+end_src

Show a list of the available django commands and run the selected one using a compilation buffer.

#+BEGIN_SRC emacs-lisp
(defun my/run-django-command ()
  "Run a django command."
  (interactive)
  (let* ((python-bin (concat (getenv "VIRTUAL_ENV") "/bin/python"))
         (manage-py-file (concat (my/project-root) "manage.py"))
         (default-directory (my/project-root))
         (raw-help (shell-command-to-string (concat python-bin " " manage-py-file " help")))
         (splited-lines (split-string raw-help "\n"))
         (options (seq-filter #'(lambda (line) (cl-search "    " line)) splited-lines))
         (selection (completing-read "Pick django command: " (mapcar 'string-trim options)))
         (command (concat python-bin " " manage-py-file " " selection)))
    (compile command)))
#+END_SRC

** Erlang

#+begin_src emacs-lisp
(use-package erlang-ts-mode
  :ensure (:host github :repo "sebastiw/erlang-ts-mode")
  :defer t)
#+end_src

** Elixir

*** LSP server building

To have support for LSP we need to compile [[https://github.com/elixir-lang/expert][expert]] and setup the generated release into ~eglot-server-programs~.

Recommended versions(using ~mise~) for compiling ~expert~:

#+begin_src toml :tangle no
[tools]
elixir = "1.17.3-otp-27"
erlang = "27.3.4.2"
zig = "0.14.1"
#+end_src

Install with ~just install~

*** Compilation integration

Add regex to match ~mix test~ execution output and be able to navigate between errors.

#+begin_src emacs-lisp
(with-eval-after-load 'compile
  (push 'mix compilation-error-regexp-alist)
  (push '(mix "^\\ \\ \\ \\ \\ \\([a-zA-Z/_/.]+\\):\\([0-9]+\\)$" 1 2) compilation-error-regexp-alist-alist))
#+end_src

*** Config

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define elixir-format
    :program "mix"
    :args '("format" "-")
    :group 'elixir))

(defun my/elixir-format-buffer ()
  "Format elixir buffers using eglot when is active otherwise use reformatter function."
  (interactive)
  ;; eglot formatter is preferred because it will use project .formatter.exs file
  ;; regular formatter generated by reformatted will ignore that file
  (if (and (featurep 'eglot) (eglot-managed-p))
      (eglot-format-buffer)
    (elixir-format-buffer)))

(defun my/elixir-format-project ()
  "Format the whole project."
  (interactive)
  (let* ((mix-file (expand-file-name (concat (locate-dominating-file (buffer-file-name) "mix.exs") "mix.exs")))
         (default-directory (file-name-directory mix-file)))
    (compile "mix format --migrate")))

(use-package elixir-ts-mode
  :ensure nil
  :mode (("\\.ex\\'" . elixir-ts-mode)
         ("\\.exs\\'" . elixir-ts-mode)
         ("\\mix.lock\\'" . elixir-ts-mode))
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Elixir Commands"))
   ("Format"
    (("ff" my/elixir-format-buffer)
     ("fa" my/elixir-format-project))
    "Testing"
    (("tt" my/mix-run-test-at-point)
     ("tf" my/mix-run-test-file)
     ("ta" my/mix-run-test-all)))))
#+END_SRC

~heex~ templates support

#+begin_src emacs-lisp
(defun my/heex-wrap-text-in-get-text ()
  "Convert region content into a tag using gettext."
  (interactive)
  (unless (region-active-p)
    (user-error "Must select some text first"))
  (save-excursion
    (let* ((text (buffer-substring-no-properties (region-beginning) (region-end)))
           (new-text (concat "<%= " "gettext(\"" text "\") %>")))
      (kill-region (region-beginning) (region-end))
      (insert new-text))))
#+end_src

#+begin_src emacs-lisp
(use-package heex-ts-mode
  :ensure nil
  :hook (heex-ts-mode . emmet-mode)
  :mode "\\.html.heex\\'"
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Heex Commands"))
   ("Format"
    (("f" my/heex-format-file))
    "Text"
    (("ii" my/phx-insert-hero-icon)
     ("wg" my/heex-wrap-text-in-get-text)))))
#+end_src

Set up hydra commands

#+begin_src emacs-lisp
(defun my/heex-format-file ()
  "Format heex file using mix command."
  (interactive)
  ;; avoid to show *Async-Shell-Command* buffer
  (let* ((display-buffer-alist (list (cons "\\*Async Shell Command\\*.*" (cons #'display-buffer-no-window nil))))
         (mix-file (expand-file-name (concat (locate-dominating-file (buffer-file-name) "mix.exs") "mix.exs")))
         (default-directory (file-name-directory mix-file)))
    (async-shell-command (format "mix format %s" (buffer-file-name)) nil)
    (revert-buffer (current-buffer) :ignore-auto :no-confirm)))
#+end_src

Custom functions to run elixir tests.

=elixir-extra-test-env= can be set up on =.dir-locals.el=

#+BEGIN_SRC emacs-lisp
(defun my/mix-run-test (&optional scope)
  "Run elixir test for the given SCOPE."
  (interactive)
  ;; we need to get absolute path of mix binary because when running
  ;; on nix this path will be different per project, version, etc
  (let* ((mix-binary (executable-find "mix"))
         (current-file (buffer-file-name))
         (current-line (line-number-at-pos))
         (mix-file (expand-file-name (concat (locate-dominating-file (buffer-file-name) "mix.exs") "mix.exs")))
         (default-directory (file-name-directory mix-file))
         (extra-env (if (boundp 'elixir-extra-test-env) elixir-extra-test-env ""))
         (mix-env (concat "MIX_ENV=test " extra-env)))

    (cond
     ((string-equal scope "file")
      (compile (format "%s %s test %s" mix-env mix-binary current-file)))

     ((string-equal scope "at-point")
      (compile (format "%s %s test %s:%s" mix-env mix-binary current-file current-line)))

     (t
      (compile (format "%s %s test" mix-env mix-binary))))))

(defun my/mix-run-test-file ()
  "Run mix test over the current file."
  (interactive)
  (my/mix-run-test "file"))

(defun my/mix-run-test-at-point ()
  "Run mix test at point."
  (interactive)
  (my/mix-run-test "at-point"))

(defun my/mix-run-test-all ()
  "Run mix test at point."
  (interactive)
  (my/mix-run-test))
#+END_SRC

#+begin_src emacs-lisp
(defun my/phx-insert-hero-icon ()
  "Insert icon component in a Elixir Phoenix application."
  (interactive)
  (let* ((base-dir (expand-file-name (locate-dominating-file (buffer-file-name) "mix.exs")))
         (icons-dir (format "%s/deps/heroicons/optimized/20/solid/" base-dir))
         (option (completing-read "Select icon: " (directory-files icons-dir nil "svg")))
         (no-extension (string-replace ".svg" "" (string-trim option) )))
    (insert (format "<.icon name=\"hero-%s\" />" no-extension))))
#+end_src

** LFE

#+BEGIN_SRC emacs-lisp
(use-package lfe-mode
  :ensure t
  :if (executable-find "lfe")
  :bind (:map lfe-mode-map
              ("C-c C-c" . lfe-eval-buffer))
  :init
  (defun lfe-eval-buffer ()
    "Send current buffer to inferior LFE process."
    (interactive)
    (if (eq (get-buffer-window "*inferior-lfe*") nil)
        (run-lfe nil))
    (lfe-eval-region (point-min) (point-max) nil)))
#+END_SRC

** Elm

Install Elm

#+BEGIN_SRC sh
npm -g install elm elm-format elm-oracle
#+END_SRC

#+BEGIN_SRC emacs-lisp

(use-package elm-mode
  :ensure t
  :if (executable-find "elm")
  :bind (:map elm-mode-map
              ("C-c C-d" . elm-oracle-doc-at-point))
  :config
  (add-hook 'elm-mode-hook #'elm-oracle-setup-completion)
  (add-to-list 'company-backends 'company-elm))

#+END_SRC

** Haskell

Install haskell binaries =hlint= and =hindent= and make sure =~/.local/bin/= is loaded in =PATH=.

#+BEGIN_SRC shell
stack install hlint
stack install hindent
#+END_SRC

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define haskell-format
    :program "hindent"
    :group 'haskell))

(use-package haskell-mode
  :ensure t
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Haskell Commands"))
   ("Format"
    (("f" haskell-format-buffer))
    "Loading"
    (("l" haskell-process-load-file)))))

(defun my/run-hlint ()
  "Run  hlint over the current project."
  (interactive)
  (let ((default-directory (my/project-root)))
    (compile "hlint .")))

(defun my/run-hlint-buffer ()
  "Run  hlint over the current buffer."
  (interactive)
  (let* ((current-file (buffer-file-name))
         (default-directory (my/project-root)))
    (compile (concat "hlint " current-file))))
#+END_SRC

** Lua

#+BEGIN_SRC emacs-lisp
(use-package lua-ts-mode
  :ensure nil
  :defer t
  :mode "\\.lua\\'"
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Lua Commands"))
   ("Format"
    (("f" lua-format-buffer))
    "Compilation"
    (("b" compile)))))
#+END_SRC

Define formatter using [[https://github.com/JohnnyMorganz/StyLua][StyLua]]

#+begin_src emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define lua-format
    :program "stylua"
    :args '("-")
    :group 'lua))
#+end_src

** Java

#+begin_src emacs-lisp
(use-package java-ts-mode
  :ensure nil
  :defer t
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Java commands"))
   ("Format"
    (("f" eglot-format-buffer))
    "Testing"
    (("t" eglot-java-run-test))))
  :config
  (add-hook 'java-ts-mode-hook #'(lambda ()
                                   (setq-local tab-width 4))))
#+end_src

#+begin_src emacs-lisp
(use-package eglot-java
  :ensure t
  :defer t
  :hook (java-ts-mode . eglot-java-mode))
#+end_src

** Javascript

Linter using [[https://biomejs.dev][biome]]

#+begin_src emacs-lisp
(use-package flymake-biome
  :ensure t)
#+end_src

#+begin_src emacs-lisp
(use-package js-ts-mode
  :ensure nil
  :defer t
  :mode "\\.m?jsx?\\'"
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Javascript Commands"))
   ("Format"
    (("f" js-format-buffer))))
  :hook (js-ts-mode . flymake-biome-load))
#+end_src

*** Formattter

We need to use ~--stdin-file-path a.js~ to tell ~biome~ to use ~stdin~.

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define js-format
    :program "biome"
    :args '("format" "--stdin-file-path" "a.js" "--javascript-formatter-indent-style" "space")))

(with-eval-after-load 'reformatter
  (reformatter-define ts-format
    :program "biome"
    :args '("format" "--stdin-file-path" "a.ts" "--indent-style" "space")))
#+END_SRC

** Typescript

#+begin_src emacs-lisp
(use-package typescript-ts-mode
  :ensure nil
  :defer t
  :mode "\\.tsx?\\'"
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Typescript Commands"))
   ("Format"
    (("f" ts-format-buffer))))
  :hook (typescript-ts-mode . flymake-biome-load))
#+end_src

** Svelte

#+begin_src emacs-lisp
(use-package svelte-ts-mode
  :ensure (:host github :repo "leafOfTree/svelte-ts-mode")
  :config
  (add-to-list 'treesit-language-source-alist
               '(svelte "https://github.com/tree-sitter-grammars/tree-sitter-svelte")))
#+end_src

after installation run ~M-x treesit-install-language-grammer~ to compile ~svelte~ grammar

** Swift

#+begin_src emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define swift-format
    :program "swift-format"
    :args '("/dev/stdin")))

(use-package swift-mode
  :ensure t
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Swift Commands"))
   ("Format"
    (("f" swift-format-buffer)))))
#+end_src

** Zig

#+begin_src emacs-lisp
(use-package zig-mode
  :ensure t
  :defer t
  :custom
  (zig-format-on-save nil))
#+end_src

** Odin

To compile grammar we need to run ~M-x treesit-install-language-grammar~

#+begin_src emacs-lisp
(defun my/run-odin ()
  "Run odin program."
  (interactive)
  (compile "odin run ."))

(use-package odin-ts-mode
  :ensure (:host github :repo "Sampie159/odin-ts-mode")
  :mode "\\.odin\\'"
  :init
  (add-to-list 'treesit-language-source-alist '(odin "https://github.com/tree-sitter-grammars/tree-sitter-odin"))

  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Odin Commands"))
   ("Format"
    (("f" eglot-format-buffer))
    "Run"
    (("r" my/run-odin))))
  :config
  ;; by default tab width is 8, that's too much space so we define 4
  ;; only for odin buffers
  (add-hook 'odin-ts-mode-hook #'(lambda ()
                                   (setq-local tab-width 4))))
#+end_src

** Rust

Install rust analyzer, this should be installed when ~rustup-init~ is executed but in case is not we can execute:

#+BEGIN_SRC sh
rustup component add rust-analyzer
#+END_SRC

Install rust source code, it is required by ~rust-analyzer~, in case it's not installed automatically

#+begin_src shell
rustup component add rust-src
#+end_src

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define rust-format
    :program "rustfmt"))

(defun my/rust-format-buffer ()
  "Format rust buffer with LSP or fallback with rust fmt."
  (interactive)
  (if (and (featurep 'eglot) (eglot-managed-p))
      (eglot-format-buffer)
    (rust-format-buffer)))

(defun my/rust-cargo-build ()
  "Build current project."
  (interactive)
  (let ((default-directory (my/project-root)))
    (compile "cargo build")))

(defun my/rust-cargo-run ()
  "Run current project."
  (interactive)
  (let ((default-directory (my/project-root)))
    (compile "cargo run")))

(defun my/rust-cargo-test-at-point ()
  "Run the test function at or above point."
  (interactive)
  (let* ((default-directory (locate-dominating-file (buffer-file-name) "Cargo.toml"))
         (test-name (save-excursion
                      (re-search-backward "#\\[test\\]" nil t)
                      (forward-line 1)
                      (when (re-search-forward "fn\\s-+\\([a-zA-Z0-9_]+\\)" (line-end-position) t)
                        (match-string-no-properties 1)))))
    (if test-name
        (compile (format "cargo test %s" test-name))
      (user-error "No #[test] found above point"))))

(defun my/rust-cargo-test-all ()
  "Run current project."
  (interactive)
  (let ((default-directory (locate-dominating-file (buffer-file-name) "Cargo.toml")))
    (compile "cargo test")))

(use-package rust-ts-mode
  :ensure nil
  :mode "\\.rs\\'"
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Rust Commands"))
   ("Format"
    (("f" my/rust-format-buffer))
    "Cargo"
    (("b" my/rust-cargo-build)
     ("r" my/rust-cargo-run))
    "Testing"
    (("ta" my/rust-cargo-test-all)
     ("tt" my/rust-cargo-test-at-point)))))
#+END_SRC

#+begin_src emacs-lisp
(defun my/rust-run-file ()
  "Compile and rust current file."
  (interactive)
  (unless (buffer-file-name)
    (user-error "Save file before"))
  (let* ((path (buffer-file-name))
         (default-directory (file-name-directory path))
         (filename (buffer-name))
         (command (format "rustc %s && ./%s" filename (string-replace ".rs" "" filename))))
    (compile command)))
#+end_src

** Golang

Install dependencies: goimports

#+BEGIN_SRC sh
go install golang.org/x/tools/cmd/goimports@latest
#+END_SRC

Install ~gopls~ to have LSP support using eglot

#+begin_src shell
brew install gopls
#+end_src

#+begin_src emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define go-format
    :program "goimports"
    :group 'go-ts-mode))
#+end_src

#+BEGIN_SRC emacs-lisp
(use-package go-ts-mode
  :ensure nil
  :hook (go-ts-mode . eglot-ensure)
  :mode (("\\.go\\'" . go-ts-mode))
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Go Commands"))
   ("Format"
    (("f" go-format-buffer))
    "Execute"
    (("r" go-run))
    "Testing"
    (("tt" go-ts-mode-test-function-at-point)
     ("tf" go-ts-mode-test-this-file)
     ("ta" go-ts-mode-test-this-package))))
  :config
  ;; by default tab width is 8, that's too much space so we define 4
  ;; only for go buffers
  (add-hook 'go-ts-mode-hook #'(lambda ()
                                 (setq-local tab-width 4))))
#+END_SRC

** Common lisp

#+BEGIN_SRC emacs-lisp
(defconst inferior-lisp-program (executable-find "sbcl"))

(use-package sly
  :ensure t
  :defer t)
#+END_SRC

** Clojure

#+BEGIN_SRC emacs-lisp
(defun my/clj-format-code ()
  "Format clojure code using cider commands."
  (interactive)
  (if (region-active-p)
      (cider-format-region (region-beginning) (region-end))
    (cider-format-buffer)))

(defun my/cider-repl-reset ()
  "Call (reset) in the active repl and return to the position where was called."
  (interactive)
  (save-window-excursion
    (cider-insert-in-repl "(reset)" t)))

(use-package cider
  :ensure t
  :bind (:map cider-mode-map
              ("C-c C-f" . my/clj-format-code)
              ("C-c C-r" . my/cider-repl-reset)))

(with-eval-after-load 'evil
  (evil-set-initial-state 'cider-stacktrace-mode 'emacs))
#+END_SRC

#+begin_src emacs-lisp
(use-package clj-refactor
  :ensure t
  :after cider
  :bind (:map clojure-mode-map
              ("C-c C-a" . cljr-add-project-dependency))
  :hook (clojure . clj-refactor))
#+end_src

** Emacs lisp

Disable indentation with tabs for =emacs-lisp-mode=

#+begin_src emacs-lisp
(defun my/emacs-lisp-hook-setup ()
  (setq indent-tabs-mode nil))

(add-hook 'emacs-lisp-mode-hook 'my/emacs-lisp-hook-setup)
#+end_src

Enable flymake

#+begin_src emacs-lisp
(add-hook 'emacs-lisp-mode-hook #'(lambda () (flymake-mode 1)))
#+end_src

~package-lint~, used for packages development

#+begin_src emacs-lisp
(use-package package-lint
  :ensure t
  :defer t)
#+end_src

** Dart

#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'reformatter
  (reformatter-define dart-format
    :program "dart"
    :args '("format")
    :group 'dart))

(defun my/dart-run-file ()
  "Execute the code of the current file."
  (interactive)
  (compile (format "dart %s" (buffer-file-name))))

(use-package dart-mode
  :ensure t
  :if (or (executable-find "dart") (executable-find "flutter"))
  :mode-hydra
  ((:title (concat (nerd-icons-icon-for-buffer) " Dart Commands"))
   ("Format"
    (("f" dart-format-buffer))
    "Execution"
    (("r" my/dart-run-file)))))
#+END_SRC

*** Flutter

#+begin_src emacs-lisp
(defun my/flutter-goto-logs-buffer()
  "Go to buffer logs buffer."
  (interactive)
  (let ((buffer (get-buffer flutter-buffer-name)))
    (unless buffer
      (user-error "flutter is not running."))
    (switch-to-buffer buffer)
    (goto-line (point-max))))

(use-package flutter
  :ensure t
  :after dart-mode
  :bind (:map dart-mode-map
              ("C-c C-r" . #'flutter-run-or-hot-reload)
              ("C-c C-l" . #'my/flutter-goto-logs-buffer))
  :hook (dart-mode . flutter-test-mode)
  :custom
  ;; sdk path will be the parent-parent directory of flutter cli
  (flutter-sdk-path (directory-file-name
                     (file-name-directory
                      (directory-file-name
                       (file-name-directory (executable-find "flutter")))))))
#+end_src

* Writing

Custom functions to speed up writing process

** Hugo

Insert =org-link= image using clipboard value, if the current file is =blog/demo.org= it will place the resulting image into =static/images/blog/demo/image.png=.

#+begin_src emacs-lisp
(defun my/hugo-insert-image-from-clipboard ()
  "Use clipoard image and put it in a generated images folder for the current file."
  (interactive)
  (let* ((absolute-path (buffer-file-name))
         (splitted (reverse (split-string absolute-path "/")))
         (filename (replace-regexp-in-string ".org" "" (car splitted)))
         (dir (nth 1 splitted))
         (base-image-path (concat (my/project-root) "static/images"))
         (result-image-dir (format "%s/%s/%s" base-image-path dir filename))
         (result-image-name (read-string "Filename: " "image.png"))
         (full-path-result-image (format "%s/%s" result-image-dir result-image-name)))

    (shell-command (format "mkdir -p %s" result-image-dir))
    (shell-command (format "pngpaste %s" full-path-result-image))
    (insert (format "[[file:%s]]" (car (cdr (split-string full-path-result-image "static")))))))
#+end_src

Insert docsy link

#+begin_src emacs-lisp
(defun my/docsy-insert-ref ()
  "Insert a link using docsy ref helper."
  (interactive)
  (let* ((filename (read-file-name "Select file: " (my/project-root)))
         (prefix-to-remove (concat (my/project-root) "content/"))
         (relative-path (string-replace prefix-to-remove "" filename)))
    (insert (format "{{< ref \"%s\" >}}" relative-path))))
#+end_src

* Custom functions

Simple bookmarks management

#+begin_src emacs-lisp
(defvar my/bookmarks (make-hash-table :test 'equal))

(defun my/bookmark-switch ()
  "Switch to selected bookmark."
  (interactive)
  (let* ((key (my/project-name))
         (items (gethash key my/bookmarks (make-hash-table :test 'equal)))
         (options (hash-table-keys items))
         (selected (completing-read "Pick buffer: " options))
         (selected-buffer (gethash selected items)))
    (when selected-buffer

      (switch-to-buffer selected-buffer))))

(defun my/add-bookmark ()
  "Add current buffer to bookmark list."
  (interactive)
  (let* ((project-key (my/project-name))
         (buffer-key (buffer-name))
         (items (gethash project-key my/bookmarks (make-hash-table :test 'equal))))
    (puthash buffer-key (current-buffer) items)
    (puthash project-key items my/bookmarks)
    (message "%s added to bookmarks" buffer-key)))
#+end_src

Manage window configurations, allows to save a "snapshot" of the current windows configuration. Also allows to restore a saved "snapshot".

#+BEGIN_SRC emacs-lisp
(defvar my/window-snapshots '())

(defun my/save-window-snapshot ()
  "Save the current window configuration into `window-snapshots` alist."
  (interactive)
  (let ((key (read-string "Enter a name for the snapshot: ")))
    (setf (alist-get key my/window-snapshots) (current-window-configuration))
    (message "%s window snapshot saved!" key)))

(defun my/get-window-snapshot (key)
  "Given a KEY return the saved value in `window-snapshots` alist."
  (let ((value (assoc key my/window-snapshots)))
    (cdr value)))

(defun my/restore-window-snapshot ()
  "Restore a window snapshot from the window-snapshots alist."
  (interactive)
  (let* ((snapshot-name (completing-read "Choose snapshot: " (mapcar #'car my/window-snapshots)))
         (snapshot (my/get-window-snapshot snapshot-name)))
    (if snapshot
        (set-window-configuration snapshot)
      (message "Snapshot %s not found" snapshot-name))))
#+END_SRC

Manipulate frame font height.

#+BEGIN_SRC emacs-lisp
(defun my/change-font-height (delta)
  "Use DELTA to increase/decrease the frame font height."
  (let* ((current-height (face-attribute 'default :height))
         (new-height (+ current-height delta)))
    (set-face-attribute 'default nil :height new-height)))

(defun my/decrease-font-height ()
  "Decrease font height by 10."
  (interactive)
  (my/change-font-height -10))

(defun my/increase-font-height ()
  "Increase font height by 10."
  (interactive)
  (my/change-font-height +10))
#+END_SRC

#+BEGIN_SRC emacs-lisp
(defun my/find-file-in-project ()
  "Custom find file function."
  (interactive)
  (let ((completion-ignore-case t)
        (read-file-name-completion-ignore-case t))
    (if (my/project-p)
        (project-find-file)
      (call-interactively #'find-file))))

(defun my/find-file-in-project-local ()
  "Find inside a relative path."
  (interactive)
  ;; mix.exs for elixir monorepos and .env for terraform repositories
  ;; with many levels
  (when-let* ((base-dir (or (locate-dominating-file (buffer-file-name) "mix.exs")
                            (locate-dominating-file (buffer-file-name) ".env"))))
    (project-find-file-in "" (list base-dir) (project-current))))

(defun my/fold-buffer-when-is-too-big (max-lines)
  "Fold buffer is max lines if grater than as MAX-LINES."
  (if (> (count-lines (point-min) (point-max)) max-lines)
      (hs-hide-all)))

;; used to store all the configuration per perspective
(defvar my/toggle-window-configurations (make-hash-table))

(defun my/toggle-maximize ()
  "Toggle maximization of current window."
  (interactive)
  (let* ((key (persp-name (persp-curr)))
         (value (gethash key my/toggle-window-configurations)))
    (if (eq value nil)
        (progn
          (puthash key (current-window-configuration) my/toggle-window-configurations)
          (delete-other-windows))
      (progn
        (set-window-configuration value)
        (puthash key nil my/toggle-window-configurations)))))

(defun my/config-file ()
  "Open config file."
  (interactive)
  (find-file (expand-file-name (concat user-emacs-directory "bootstrap.org"))))

(defun my/toggle-spanish-characters ()
  "Enable/disable alt key to allow insert spanish characters."
  (interactive)
  (if (eq ns-alternate-modifier 'meta)
      (setq ns-alternate-modifier nil)
    (setq ns-alternate-modifier 'meta)))

(defun my/change-font-size()
  "Change frame font size."
  (interactive)
  (let* ((size (read-number "New size: ")))
    (set-face-attribute 'default nil :font (format "%s %d" my/font-family size))))
#+END_SRC

Function to extract clocks from org buffer and filter them by month

#+BEGIN_SRC emacs-lisp
(defun my/collect-clocks ()
  "Collect all the clocks of current buffer."
  (org-element-map (org-element-parse-buffer) 'clock #'(lambda (clock) clock)))

(defun my/filter-clocks-by-month (clocks month)
  "Filter CLOCKS using MONTH value."
  (seq-filter #'(lambda (clock)
                  (eq (org-element-property :month-end (org-element-property :value clock)) month)) clocks))

(defun my/org-filter-clocks-report ()
  "Create a buffer with the tasks filtered by month."
  (interactive)
  (let* ((month (read-number "Insert month: "))
         (clocks (my/collect-clocks))
         (filtered-clocks (my/filter-clocks-by-month clocks month))
         (buffer (get-buffer-create "*clocks report*")))
    (switch-to-buffer buffer)
    (org-mode)
    (insert "* Report\n")
    (seq-map #'(lambda (clock)
                 (insert (format "CLOCK: %s\n" (org-element-property :raw-value (org-element-property :value clock))))) filtered-clocks)
    (org-clock-display)))
#+END_SRC

Copy absolute and relative path to clipboard

#+BEGIN_SRC emacs-lisp
(defun my/copy-abs-path ()
  "Copy absolute path of the buffer to clipboard"
  (interactive)
  (if buffer-file-name
      (progn
        (kill-new buffer-file-name)
        (message (format "%s copied to clipboard" buffer-file-name)))
    (message "File not saved yet")))

(defun my/copy-relative-path ()
  "Copy relative path of the buffer to clipboard"
  (interactive)
  (if (and (my/project-p) buffer-file-name)
      (let ((path (file-relative-name buffer-file-name (my/project-root))))
        (kill-new path)
        (message (format "%s copied to clipboard" path)))
    (message "File not saved yet or not inside project")))
#+END_SRC

Create a temp file with the current buffer content and render it with =eww=.

#+BEGIN_SRC emacs-lisp
(defun my/preview-buffer-in-eww ()
  "Preview buffer content in EWW."
  (interactive)
  (let* ((temp-file (make-temp-name (temporary-file-directory)))
         (path (concat temp-file ".html")))
    (write-file path)
    (kill-buffer)
    (eww-open-file path)))

(defun my/preview-buffer-in-xwidget-browser ()
  "Preview buffer content in EWW."
  (interactive)
  (let* ((temp-file (make-temp-name (temporary-file-directory)))
         (path (concat temp-file ".html")))
    (write-file path)
    (kill-buffer)
    (xwidget-webkit-browse-url (format "file://%s" path))))
#+END_SRC

Resize window: allow create a "resize mode" and use hjkl to increase/decrease width/height of the current window

#+BEGIN_SRC emacs-lisp
(defun my/resize-window ()
  "Resize window using j k h l keys."
  (interactive)
  (let ((keys-map '((?h . evil-window-decrease-width)
                    (?j . evil-window-decrease-height)
                    (?k . evil-window-increase-height)
                    (?l . evil-window-increase-width)))
        (overlay (make-overlay (point-min) (point-max) (window-buffer))))
    (let ((is-reading t))
      (overlay-put overlay 'face '((t (:foreground "gray40"))))
      (while is-reading
        (let ((action (alist-get (read-key) keys-map)))
          (if action
              (apply action '(1))
            (setq is-reading nil)
            (delete-overlay overlay)))))))
#+END_SRC

Kill the current buffer and delete the related file

#+BEGIN_SRC emacs-lisp
(defun my/delete-close-file ()
  "Delete the current file and kill its buffer."
  (interactive)
  (when buffer-file-name
    (delete-file buffer-file-name)
    (kill-buffer)))
#+END_SRC

Copy json text from clipboard in a new buffer and format it

#+begin_src emacs-lisp
(defun my/copy-and-format-json-from-clipboard ()
  "Copy content from clipboard and format it in a new buffer."
  (interactive)
  (let ((buffer (generate-new-buffer "tmp.json")))
    (with-current-buffer buffer
      (yank)
      (json-ts-mode)
      (json-format-region (point-min) (point-max)))
    (set-window-buffer nil buffer)))
#+end_src

** MacOS

Functions to open Finder using current file or current project.

#+BEGIN_SRC emacs-lisp
(defun my/open-finder-at (path)
  "Open Finder app with the given PATH."
  (let* ((finder (executable-find "open"))
	 (command (format "%s %s" finder path)))
    (shell-command command)))

(defun my/open-project-in-finder ()
  "Open current project in Finder app."
  (interactive)
  (if (my/project-p)
      (my/open-finder-at (my/project-root))
    (message "There is no active project.")))

(defun my/open-current-file-in-finder ()
  "Open current file in Finder."
  (interactive)
  (let ((file (buffer-file-name)))
    (if file
        (my/open-finder-at (file-name-directory file))
      (message "Buffer has not been saved yet!"))))
#+END_SRC

Open current file with an macOS app. Installed macOS apps will be listed using ~completing-read~

#+BEGIN_SRC emacs-lisp
(defun my/macos-open-file-with ()
  "Open current file with and macOS installed app."
  (interactive)
  (let* ((apps-list (directory-files "/Applications" nil "\\.app$"))
         (selected-app (completing-read "Choose an application: " apps-list)))
    (shell-command (format "open %s -a '%s'" (buffer-file-name) selected-app))))
#+END_SRC

Open the current file with macOS =open= command. This will open the file with the default app configured for the type of file.

#+BEGIN_SRC emacs-lisp
(defun my/macos-open-current-file ()
  (interactive)
  (shell-command (format "open %s" (buffer-file-name))))
#+END_SRC

Save image from clipboard to path.

#+begin_src emacs-lisp
(defun my/save-image-from-clipboard ()
  "Save image from clipboard to the given path."
  (interactive)
  (unless (executable-find "pngpaste")
    (user-error "Install pngpaste to continue"))
  (let* ((path (read-file-name ""))
         (command (format "pngpaste %s" path)))
    (shell-command command)
    (kill-new path)))
#+end_src

af

.emacs.d/snippets/dart-mode/af · 5 lines

# -*- mode: snippet -*-
# name: Arrow function
# key: af
# --
($1) => $2

fu

.emacs.d/snippets/dart-mode/fu · 7 lines

# -*- mode: snippet -*-
# name: Inline function
# key: fu
# --
($1) {
  $2
}

iclass

.emacs.d/snippets/dart-mode/iclass · 7 lines

# -*- mode: snippet -*-
# name: Inherit class
# key: icls
# --
class $1 extends $2 {
  $0
}

print

.emacs.d/snippets/dart-mode/print · 5 lines

# -*- mode: snippet -*-
# name: print
# key: p
# --
print($1);$2

stanim

.emacs.d/snippets/dart-mode/stanim · 33 lines

# -*- mode: snippet -*-
# name: Flutter widget with AnimationController
# key: stanim
# group: flutter
# --
class $1 extends StatefulWidget {
  @override
  $1State createState() => $1State();
}

class $1State extends State<$1>
with SingleTickerProviderStateMixin {
  AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
  }
  
  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Container(
      $2
    );
  }
}

stful

.emacs.d/snippets/dart-mode/stful · 18 lines

# -*- mode: snippet -*-
# name: Flutter stateful widget
# key: stful
# group: flutter
# --
class $1 extends StatefulWidget {
  @override
  _$1State createState() => _$1State();
}

class _$1State extends State<$1> {
  @override
  Widget build(BuildContext context) {
    return Container(
      $2
    );
  }
}

stless

.emacs.d/snippets/dart-mode/stless · 13 lines

# -*- mode: snippet -*-
# name: Flutter stateless widget
# key: stless
# group: flutter
# --
class $1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
    $2
  );
  }
}

inspect-ecto-query

.emacs.d/snippets/elixir-mode/inspect-ecto-query · 5 lines

# -*- mode: snippet -*-
# name: ecto inspect query
# key: iq
# --
IO.inspect(Ecto.Adapters.SQL.to_sql(:all, ${1:Repo}, ${0:query}))

mixtask

.emacs.d/snippets/elixir-mode/mixtask · 14 lines

# -*- mode: snippet -*-
# name: mix task
# key: mixtask
# --
defmodule Mix.Tasks.$1 do
  @moduledoc false
  use Mix.Task

  def run(_) do
    Application.ensure_all_started(:$2)
    IO.puts("runnning...")
    $0
  end
end

pipe-inspect

.emacs.d/snippets/elixir-mode/pipe-inspect · 5 lines

# -*- mode: snippet -*-
# name: Pipe inspect
# key: pi
# --
|> IO.inspect(label: "$1")$0

if-error

.emacs.d/snippets/go-mode/if-error · 7 lines

# -*- mode: snippet -*-
# name: if error
# key: ie
# --
if err != nil {
   $0
}

inspect

.emacs.d/snippets/go-mode/inspect · 5 lines

# -*- mode: snippet -*-
# name: inspect
# key: i
# --
fmt.Printf("Type %T for %[1]v\n", $1)$0

print

.emacs.d/snippets/go-mode/print · 5 lines

# -*- mode: snippet -*-
# name: print
# key: p
# --
fmt.Println($0)

inspect

.emacs.d/snippets/js-mode/inspect · 5 lines

# -*- mode: snippet -*-
# name: inspect variable
# key: i
# --
console.log("$1", $1)$0

inspect

.emacs.d/snippets/python-mode/inspect · 5 lines

# -*- mode: snippet -*-
# name: inspect variable
# key: i
# --
print("$1", $1)$0

logger

.emacs.d/snippets/python-mode/logger · 19 lines

# -*- mode: snippet -*-
# name: logger
# key: logger
# --

import logging
import sys


logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

logger.addHandler(handler)
$0

inspect

.emacs.d/snippets/rust-mode/inspect · 5 lines

# -*- mode: snippet -*-
# name: inspect value
# key: i
# --
println!("{:?}", $1);$0

Espanso

default.yml

espanso/config/default.yml · 2 lines

search_shortcut: off
show_icon: false

base.yml

espanso/match/base.yml · 62 lines

---
# espanso match file

# For a complete introduction, visit the official docs at: https://espanso.org/docs/

# You can use this file to define the base matches (aka snippets)
# that will be available in every application when using espanso.

# Matches are substitution rules: when you type the "trigger" string
# it gets replaced by the "replace" string.
matches:
  - trigger: "::date"
    replace: "{{date}}"
    vars:
      - name: date
        type: date
        params:
          format: "%m/%d/%Y"

  - trigger: "::isodate"
    replace: "{{date}}"
    vars:
      - name: date
        type: date
        params:
          format: "%Y-%m-%d"

  - trigger: "::time"
    replace: "{{time}}"
    vars:
      - name: time
        type: date
        params:
          format: "%X"

  - trigger: "::fliptable"
    replace: "(ノ°Д°)ノ︵ ┻━┻"

  - trigger: "::well"
    replace: "¯\\_(ツ)_/¯"

  - trigger: "::ip"
    replace: "{{ip}}"
    vars:
      - name: ip
        type: shell
        params:
          cmd: "curl ifconfig.me"

  - trigger: "::arigato"
    replace: "ありがとう"

  - trigger: "::kudasai"
    replace: "ください"

  - trigger: "::lorem"
    replace: "{{lorem}}"
    vars:
      - name: lorem
        type: shell
        params:
          cmd: "lorem -n 50"

Eww

eww.scss

eww/eww.scss · 247 lines

.background {
  background-color: transparent;
}

window {
  background-color: transparent;
}

.panel {
  background-color: rgba(15, 15, 15, 0.45);
  padding: 12px 14px;
  border-radius: 8px;
}

.spotify-card {
  background-color: rgba(15, 15, 15, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  padding: 16px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}

.spotify-card-cover {
  border-radius: 8px;
}

.spotify-card-details {
  min-width: 240px;
}

.spotify-card-status {
  color: rgba(255, 255, 255, 0.45);
  font-size: 11px;
  letter-spacing: 1px;
}

.spotify-card-title {
  color: #eee;
  font-size: 18px;
  font-weight: 600;
}

.spotify-card-artist {
  color: rgba(255, 255, 255, 0.75);
  font-size: 14px;
}

.spotify-card-album {
  color: rgba(255, 255, 255, 0.45);
  font-size: 12px;
}

.spotify-card-controls {
  margin-top: 4px;
}

.spotify-card-control {
  min-width: 30px;
  min-height: 26px;
  padding: 0;
  border: none;
  border-radius: 6px;
  background-color: rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.75);
  box-shadow: none;
}

.spotify-card-control:hover {
  background-color: rgba(255, 255, 255, 0.16);
  color: #eee;
}

.spotify-card-control.play-pause {
  color: #eee;
}

.calendar-panel {
  background-color: rgba(15, 15, 15, 0.45);
  padding: 12px 14px;
  border-radius: 8px;
}

.desktop-calendar,
.desktop-calendar header,
.desktop-calendar button,
.desktop-calendar label,
.desktop-calendar grid {
  background-color: transparent;
  background-image: none;
  color: #eee;
  border: none;
  box-shadow: none;
}

.desktop-calendar button:hover {
  background-color: rgba(255, 255, 255, 0.1);
}

.desktop-calendar:selected,
.desktop-calendar label:selected,
.desktop-calendar .today {
  background-color: #eee;
  color: #0f0f0f;
  border-radius: 4px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.6);
}

.weather-glyph {
  font-size: 32px;
  color: #eee;
}

.weather-temp {
  font-size: 20px;
  font-weight: 500;
  color: #eee;
  text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
}

.weather-desc {
  font-size: 12px;
  color: rgba(255, 255, 255, 0.6);
}

.weather-sub {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.5);
}

.weather-city {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.6);
  letter-spacing: 0.5px;
}


.net-header {
  color: rgba(255, 255, 255, 0.5);
  font-size: 11px;
  letter-spacing: 1px;
}

.net-arrow {
  color: rgba(255, 255, 255, 0.6);
  font-size: 13px;
}

.net-value {
  color: #eee;
  font-size: 13px;
  font-weight: 500;
  text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
}

.net-usage-row {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.5);
  margin-top: 2px;
}

.ram-header {
  color: rgba(255, 255, 255, 0.5);
  font-size: 11px;
  letter-spacing: 1px;
}

.usage-gauge {
  color: #eee;
  background-color: rgba(255, 255, 255, 0.15);
}

.usage-gauge-label {
  min-width: 56px;
  min-height: 56px;
  background-color: #161616;
  border-radius: 50%;
  color: #eee;
  font-size: 20px;
  font-weight: 500;
}

.usage-gauge.critical,
.usage-gauge.critical .usage-gauge-label {
  color: #f53c3c;
}

.ram-sub {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.5);
  margin-top: 2px;
}

.cpu-header,
.gpu-header {
  color: rgba(255, 255, 255, 0.5);
  font-size: 11px;
  letter-spacing: 1px;
}

.cpu-sub,
.gpu-sub {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.5);
  margin-top: 2px;
}

.disk-header {
  color: rgba(255, 255, 255, 0.5);
  font-size: 11px;
  letter-spacing: 1px;
}

.disk-bar {
  min-height: 8px;
  margin: 6px 0;
}

.disk-bar trough {
  min-height: 8px;
  background-color: rgba(255, 255, 255, 0.15);
  border-radius: 4px;
}

.disk-bar trough progress {
  background-color: #eee;
  border-radius: 4px;
}

.disk-bar.critical trough progress {
  background-color: #f53c3c;
}

.disk-percent {
  min-width: 30px;
  color: #eee;
  font-size: 11px;
}

.disk-percent.critical {
  color: #f53c3c;
}

.disk-sub {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.5);
  margin-top: 2px;
}

eww.yuck

eww/eww.yuck · 194 lines

(defwidget calendar-panel []
  (box :class "calendar-panel"
    (calendar :class "desktop-calendar"
              :show-heading true
              :show-day-names true
              :show-week-numbers false)))

(defwindow calendar-window
  :monitor 0
  :windowtype "dock"
  :stacking "bottom"
  :exclusive false
  :focusable false
  :geometry (geometry
              :x "24px"
              :y "24px"
              :width "280px"
              :height "220px"
              :anchor "top left")
  (calendar-panel))

(deflisten spotify-data
  :initial "{\"status\":\"Stopped\",\"artist\":\"\",\"title\":\"Nothing playing\",\"album\":\"\",\"cover\":\"\"}"
  `scripts/spotify.sh`)

(defwidget spotify-card-panel []
  (box :class "spotify-card"
       :visible {spotify-data.status == "Playing" || spotify-data.status == "Paused"}
       :orientation "h" :space-evenly false :spacing 16
    (image :class "spotify-card-cover"
           :path {spotify-data.cover}
           :image-width 120
           :image-height 120
           :preserve-aspect-ratio true)
    (box :class "spotify-card-details"
         :orientation "v" :space-evenly false :spacing 6 :valign "center"
      (label :class "spotify-card-status" :text {spotify-data.status} :halign "start")
      (label :class "spotify-card-title" :text {spotify-data.title}
             :limit-width 26 :truncate true :halign "start")
      (label :class "spotify-card-artist" :text {spotify-data.artist}
             :limit-width 30 :truncate true :halign "start")
      (label :class "spotify-card-album" :text {spotify-data.album}
             :limit-width 30 :truncate true :halign "start")
      (box :class "spotify-card-controls"
           :orientation "h" :space-evenly false :spacing 8 :halign "start"
        (button :class "spotify-card-control" :tooltip "Previous"
                :onclick "playerctl -p spotify previous" "")
        (button :class "spotify-card-control play-pause" :tooltip "Play or pause"
                :onclick "playerctl -p spotify play-pause"
          (label :text {spotify-data.status == "Playing" ? "" : ""}))
        (button :class "spotify-card-control" :tooltip "Next"
                :onclick "playerctl -p spotify next" "")))))

(defwindow spotify-card
  :monitor 0
  :windowtype "dock"
  :stacking "overlay"
  :exclusive false
  :focusable false
  :geometry (geometry
              :x "0px"
              :y "28px"
              :width "420px"
              :height "160px"
              :anchor "top center")
  (spotify-card-panel))

(defpoll weather-data
  :interval "15m"
  :initial "{\"icon\": \"\", \"temp\": \"--\", \"feels\": \"--\", \"desc\": \"loading\", \"humidity\": \"--\", \"wind\": \"--\", \"city\": \"--\"}"
  `scripts/weather.sh`)

(defwidget weather-panel []
  (box :class "panel weather-panel" :orientation "v" :space-evenly false :spacing 4 :height 102
    (label :class "weather-city" :text {weather-data.city} :halign "start")
    (box :orientation "h" :space-evenly false :spacing 10
      (label :class "weather-glyph" :text {weather-data.icon})
      (box :orientation "v" :space-evenly false
        (label :class "weather-temp" :text "${weather-data.temp}°" :halign "start")
        (label :class "weather-desc" :text {weather-data.desc} :halign "start")))
    (box :class "weather-sub" :orientation "h" :space-evenly false :spacing 10
      (label :text "feels ${weather-data.feels}°")
      (label :text "${weather-data.humidity}% hum")
      (label :text "${weather-data.wind} km/h"))))

(defpoll net-speed
  :interval "1s"
  `scripts/net.sh speed`)

(defpoll net-usage
  :interval "30s"
  `scripts/net.sh usage`)

(defwidget net-panel []
  (box :class "panel net-panel" :orientation "v" :space-evenly false :spacing 6 :height 102
    (label :class "net-header" :text "network" :halign "start")
    (box :orientation "h" :space-evenly false :spacing 16
      (box :orientation "h" :space-evenly false :spacing 4
        (label :class "net-arrow" :text "↓")
        (label :class "net-value" :text {net-speed.down}))
      (box :orientation "h" :space-evenly false :spacing 4
        (label :class "net-arrow" :text "↑")
        (label :class "net-value" :text {net-speed.up})))
    (box :class "net-usage-row" :orientation "h" :space-evenly false :spacing 12
      (label :text "total ↓ ${net-usage.down_total}")
      (label :text "↑ ${net-usage.up_total}"))))

(defpoll ram-data
  :interval "5s"
  :initial "{\"used\": \"--\", \"total\": \"--\", \"percent\": \"0\"}"
  `scripts/ram.sh`)

(defwidget ram-panel []
  (box :class "panel ram-panel" :orientation "v" :space-evenly false :spacing 4 :height 138
    (label :class "ram-header" :text "memory" :halign "start")
    (circular-progress :class "usage-gauge ${ram-data.percent > 85 ? "critical" : ""}"
                       :value {ram-data.percent}
                       :thickness 8
                       :width 72
                       :height 72
      (label :class "usage-gauge-label" :text "${ram-data.percent}%" :width 56 :height 56 :halign "center"))
    (label :class "ram-sub" :text "${ram-data.used} / ${ram-data.total}" :halign "start")))

(defpoll cpu-data
  :interval "2s"
  :initial "{\"percent\": \"0\", \"load\": \"--\", \"cores\": \"--\"}"
  `scripts/cpu.sh`)

(defwidget cpu-panel []
  (box :class "panel cpu-panel" :orientation "v" :space-evenly false :spacing 4 :height 138
    (label :class "cpu-header" :text "cpu" :halign "start")
    (circular-progress :class "usage-gauge ${cpu-data.percent > 85 ? "critical" : ""}"
                       :value {cpu-data.percent}
                       :thickness 8
                       :width 72
                       :height 72
      (label :class "usage-gauge-label" :text "${cpu-data.percent}%" :width 56 :height 56 :halign "center"))
    (label :class "cpu-sub" :text "load ${cpu-data.load} / ${cpu-data.cores} cores" :halign "start")))

(defpoll gpu-data
  :interval "2s"
  :initial "{\"percent\": \"0\", \"vram_used\": \"--\", \"vram_total\": \"--\"}"
  `scripts/gpu.sh`)

(defwidget gpu-panel []
  (box :class "panel gpu-panel" :orientation "v" :space-evenly false :spacing 4 :height 138
    (label :class "gpu-header" :text "gpu" :halign "start")
    (circular-progress :class "usage-gauge ${gpu-data.percent > 85 ? "critical" : ""}"
                       :value {gpu-data.percent}
                       :thickness 8
                       :width 72
                       :height 72
      (label :class "usage-gauge-label" :text "${gpu-data.percent}%" :width 56 :height 56 :halign "center"))
    (label :class "gpu-sub" :text "${gpu-data.vram_used}M / ${gpu-data.vram_total}M vram" :halign "start")))

(defpoll disk-data
  :interval "30s"
  :initial "{\"percent\": \"0\", \"used\": \"--\", \"total\": \"--\"}"
  `scripts/disk.sh`)

(defwidget disk-panel []
  (box :class "panel disk-panel" :orientation "v" :space-evenly false :spacing 4 :height 102
    (label :class "disk-header" :text "disk" :halign "start")
    (progress :class "disk-bar ${disk-data.percent > 85 ? "critical" : ""}"
              :value {disk-data.percent}
              :hexpand true)
    (box :class "disk-sub" :orientation "h" :space-evenly false
      (label :text "${disk-data.used} / ${disk-data.total}" :halign "start" :hexpand true)
      (label :class "disk-percent ${disk-data.percent > 85 ? "critical" : ""}"
             :text "${disk-data.percent}%"
             :halign "end"))))

(defwidget widget-stack []
  (box :orientation "v" :space-evenly false :spacing 12
    (weather-panel)
    (net-panel)
    (ram-panel)
    (cpu-panel)
    (gpu-panel)
    (disk-panel)))

(defwindow widgets
  :monitor 0
  :windowtype "dock"
  :stacking "bottom"
  :exclusive false
  :focusable false
  :geometry (geometry
              :x "24px"
              :y "24px"
              :width "200px"
              :height "780px"
              :anchor "top right")
  (widget-stack))

cpu.sh

eww/scripts/cpu.sh · 22 lines

#!/usr/bin/env bash
# Usage: cpu.sh

read -r _ user nice system idle iowait irq softirq steal _ _ </proc/stat
prev_idle=$((idle + iowait))
prev_total=$((user + nice + system + idle + iowait + irq + softirq + steal))

sleep 0.5

read -r _ user nice system idle iowait irq softirq steal _ _ </proc/stat
idle_now=$((idle + iowait))
total_now=$((user + nice + system + idle + iowait + irq + softirq + steal))

diff_idle=$((idle_now - prev_idle))
diff_total=$((total_now - prev_total))
percent=$(((1000 * (diff_total - diff_idle) / diff_total + 5) / 10))

load=$(awk '{print $1}' /proc/loadavg)
cores=$(nproc)

jq -n --arg percent "$percent" --arg load "$load" --arg cores "$cores" \
  '{percent: $percent, load: $load, cores: $cores}'

disk.sh

eww/scripts/disk.sh · 7 lines

#!/usr/bin/env bash
# Usage: disk.sh

read -r percent used total <<<"$(df -h / | awk 'NR==2 {gsub("%","",$5); print $5, $3, $2}')"

jq -n --arg percent "$percent" --arg used "$used" --arg total "$total" \
  '{percent: $percent, used: $used, total: $total}'

gpu.sh

eww/scripts/gpu.sh · 34 lines

#!/usr/bin/env bash
# Usage: gpu.sh

percent=0
vram_used=0
vram_total=0

# Prefer NVIDIA's own tool when available; DRM card numbering is not stable.
if command -v nvidia-smi >/dev/null; then
  gpu_info=$(nvidia-smi \
    --query-gpu=utilization.gpu,memory.used,memory.total \
    --format=csv,noheader,nounits 2>/dev/null | head -n 1 | tr -d ',')
  if [ -n "$gpu_info" ]; then
    read -r percent vram_used vram_total <<<"$gpu_info"
  fi
fi

# Fall back to the first AMD GPU exposed through sysfs.
if [ "$percent" -eq 0 ] && [ "$vram_used" -eq 0 ] && [ "$vram_total" -eq 0 ]; then
  for busy_path in /sys/class/drm/card[0-9]*/device/gpu_busy_percent; do
    [ -r "$busy_path" ] || continue
    device_path=${busy_path%/gpu_busy_percent}
    percent=$(<"$busy_path")
    vram_used_bytes=$(cat "$device_path/mem_info_vram_used" 2>/dev/null || echo 0)
    vram_total_bytes=$(cat "$device_path/mem_info_vram_total" 2>/dev/null || echo 0)
    vram_used=$((vram_used_bytes / 1048576))
    vram_total=$((vram_total_bytes / 1048576))
    break
  done
fi

jq -n --argjson percent "$percent" --argjson vram_used "$vram_used" \
  --argjson vram_total "$vram_total" \
  '{percent: $percent, vram_used: $vram_used, vram_total: $vram_total}'

net.sh

eww/scripts/net.sh · 79 lines

#!/usr/bin/env bash
# Usage: net.sh speed | net.sh usage

mode="$1"

# Detect first active (UP, non-loopback) interface
iface=$(ip -brief link | grep "  UP " | awk '{print $1}' | xargs | awk '$1 != "lo" {print $1; exit}')

if [ -z "$iface" ]; then
  if [ "$mode" = "speed" ]; then
    echo '{"down": "--", "up": "--"}'
  else
    echo '{"down_total": "--", "up_total": "--"}'
  fi
  exit 0
fi

rx_path="/sys/class/net/$iface/statistics/rx_bytes"
tx_path="/sys/class/net/$iface/statistics/tx_bytes"

if [ ! -f "$rx_path" ]; then
  if [ "$mode" = "speed" ]; then
    echo '{"down": "--", "up": "--"}'
  else
    echo '{"down_total": "--", "up_total": "--"}'
  fi
  exit 0
fi

human_rate() {
  local kb=$1
  if [ "$kb" -gt 1000 ]; then
    echo "$(echo "scale=1; $kb/1024" | bc) MB/s"
  else
    echo "${kb} KB/s"
  fi
}

human_total() {
  local bytes=$1
  if [ "$bytes" -gt 1073741824 ]; then
    echo "$(echo "scale=2; $bytes/1073741824" | bc) GB"
  else
    echo "$((bytes / 1048576)) MB"
  fi
}

case "$mode" in
speed)
  rx1=$(cat "$rx_path")
  tx1=$(cat "$tx_path")
  sleep 1
  rx2=$(cat "$rx_path")
  tx2=$(cat "$tx_path")

  down_kb=$(((rx2 - rx1) / 1024))
  up_kb=$(((tx2 - tx1) / 1024))

  down=$(human_rate "$down_kb")
  up=$(human_rate "$up_kb")

  jq -n --arg down "$down" --arg up "$up" '{down: $down, up: $up}'
  ;;

usage)
  rx=$(cat "$rx_path")
  tx=$(cat "$tx_path")

  down_total=$(human_total "$rx")
  up_total=$(human_total "$tx")

  jq -n --arg down "$down_total" --arg up "$up_total" '{down_total: $down, up_total: $up}'
  ;;

*)
  echo '{"error": "usage: net.sh speed|usage"}'
  exit 1
  ;;
esac

ram.sh

eww/scripts/ram.sh · 22 lines

#!/usr/bin/env bash
# Usage: ram.sh

mem=$(free -m | awk '/Mem:/ {printf "%d %d %d", $3, $2, ($3/$2)*100}')
used=$(echo "$mem" | awk '{print $1}')
total=$(echo "$mem" | awk '{print $2}')
percent=$(echo "$mem" | awk '{print $3}')

human() {
  local mb=$1
  if [ "$mb" -gt 1024 ]; then
    echo "$(echo "scale=1; $mb/1024" | bc)G"
  else
    echo "${mb}M"
  fi
}

used_h=$(human "$used")
total_h=$(human "$total")

jq -n --arg used "$used_h" --arg total "$total_h" --arg percent "$percent" \
  '{used: $used, total: $total, percent: $percent}'

spotify.sh

eww/scripts/spotify.sh · 59 lines

#!/usr/bin/env bash
# Stream Spotify metadata for Eww.

cover_dir=${XDG_CACHE_HOME:-$HOME/.cache}/eww
cover_path=$cover_dir/spotify-cover
cover_url_path=$cover_dir/spotify-cover-url

resolve_cover() {
  local art_url=$1 cached_url temp_path

  case "$art_url" in
  file://*)
    printf '%s\n' "${art_url#file://}"
    return
    ;;
  http://* | https://*)
    mkdir -p "$cover_dir"
    cached_url=$(cat "$cover_url_path" 2>/dev/null || true)
    if [ "$art_url" != "$cached_url" ] || [ ! -s "$cover_path" ]; then
      temp_path=$cover_path.tmp
      if wget --quiet --output-document="$temp_path" "$art_url"; then
        mv "$temp_path" "$cover_path"
        printf '%s' "$art_url" >"$cover_url_path"
      else
        rm -f "$temp_path"
      fi
    fi
    [ -s "$cover_path" ] && printf '%s\n' "$cover_path"
    ;;
  esac
}

emit_metadata() {
  jq -cn \
    --arg status "$1" \
    --arg artist "$2" \
    --arg title "$3" \
    --arg album "$4" \
    --arg cover "$5" \
    '{status: $status, artist: $artist, title: $title, album: $album, cover: $cover}'
}

if ! command -v playerctl >/dev/null; then
  emit_metadata "Unavailable" "" "Spotify unavailable" "" ""
  while sleep 60; do :; done
fi

while true; do
  playerctl -p spotify metadata --follow \
    --format $'{{status}}\x1f{{artist}}\x1f{{title}}\x1f{{album}}\x1f{{mpris:artUrl}}' \
    2>/dev/null |
    while IFS=$'\x1f' read -r status artist title album art_url; do
      cover=$(resolve_cover "$art_url")
      emit_metadata "$status" "${artist:---}" "${title:---}" "${album:---}" "$cover"
    done

  emit_metadata "Stopped" "" "Nothing playing" "" ""
  sleep 2
done

weather.sh

eww/scripts/weather.sh · 39 lines

#!/usr/bin/env bash
data=$(curl -s "wttr.in/?format=j1" --max-time 5)
location=$(curl -s "wttr.in/?format=%l" --max-time 5)

if [ -z "$data" ]; then
  echo '{"icon": "\uf071", "temp": "--", "feels": "--", "desc": "offline", "humidity": "--", "wind": "--", "city": "--"}'
  exit 0
fi

temp=$(echo "$data" | jq -r '.current_condition[0].temp_C')
feels=$(echo "$data" | jq -r '.current_condition[0].FeelsLikeC')
desc=$(echo "$data" | jq -r '.current_condition[0].weatherDesc[0].value')
humidity=$(echo "$data" | jq -r '.current_condition[0].humidity')
wind=$(echo "$data" | jq -r '.current_condition[0].windspeedKmph')
code=$(echo "$data" | jq -r '.current_condition[0].weatherCode')
# The penultimate component is the city; single-component responses use the full value.
city=$(printf '%s' "$location" | awk -F',' '{ city = $(NF - 1); gsub(/^[[:space:]]+|[[:space:]]+$/, "", city); print toupper(city) }')
[ -n "$city" ] || city="--"

case "$code" in
113) icon="\ue30d" ;;
116) icon="\ue302" ;;
119 | 122) icon="\ue312" ;;
143 | 248 | 260) icon="\ue313" ;;
176 | 263 | 266 | 281 | 284 | 293 | 296 | 299 | 302 | 305 | 308 | 311 | 314 | 353 | 356 | 359) icon="\ue318" ;;
179 | 182 | 185 | 227 | 230 | 317 | 320 | 323 | 326 | 329 | 332 | 335 | 338 | 350 | 362 | 365 | 368 | 371 | 374 | 377) icon="\ue31a" ;;
200 | 386 | 389 | 392 | 395) icon="\ue31d" ;;
*) icon="\ue312" ;;
esac

jq -n \
  --arg icon "$icon" \
  --arg temp "$temp" \
  --arg feels "$feels" \
  --arg desc "$desc" \
  --arg humidity "$humidity" \
  --arg wind "$wind" \
  --arg city "$city" \
  '{icon: $icon, temp: $temp, feels: $feels, desc: $desc, humidity: $humidity, wind: $wind, city: $city}'

General

.aerospace.toml

.aerospace.toml · 174 lines

# Place a copy of this config to ~/.aerospace.toml
# After that, you can edit ~/.aerospace.toml to your liking

# You can use it to add commands that run after login to macOS user session.
# 'start-at-login' needs to be 'true' for 'after-login-command' to work
# Available commands: https://nikitabobko.github.io/AeroSpace/commands
after-login-command = []

# You can use it to add commands that run after AeroSpace startup.
# 'after-startup-command' is run after 'after-login-command'
# Available commands : https://nikitabobko.github.io/AeroSpace/commands
after-startup-command = []

# Start AeroSpace at login
start-at-login = false

# Normalizations. See: https://nikitabobko.github.io/AeroSpace/guide#normalization
enable-normalization-flatten-containers = true
enable-normalization-opposite-orientation-for-nested-containers = true

# See: https://nikitabobko.github.io/AeroSpace/guide#layouts
# The 'accordion-padding' specifies the size of accordion padding
# You can set 0 to disable the padding feature
accordion-padding = 30

# Possible values: tiles|accordion
default-root-container-layout = 'tiles'

# Possible values: horizontal|vertical|auto
# 'auto' means: wide monitor (anything wider than high) gets horizontal orientation,
#               tall monitor (anything higher than wide) gets vertical orientation
default-root-container-orientation = 'auto'

# Mouse follows focus when focused monitor changes
# Drop it from your config, if you don't like this behavior
# See https://nikitabobko.github.io/AeroSpace/guide#on-focus-changed-callbacks
# See https://nikitabobko.github.io/AeroSpace/commands#move-mouse
# Fallback value (if you omit the key): on-focused-monitor-changed = []
on-focused-monitor-changed = ['move-mouse monitor-lazy-center']

# You can effectively turn off macOS "Hide application" (cmd-h) feature by toggling this flag
# Useful if you don't use this macOS feature, but accidentally hit cmd-h or cmd-alt-h key
# Also see: https://nikitabobko.github.io/AeroSpace/goodies#disable-hide-app
automatically-unhide-macos-hidden-apps = false

# Possible values: (qwerty|dvorak)
# See https://nikitabobko.github.io/AeroSpace/guide#key-mapping
[key-mapping]
preset = 'qwerty'

# Gaps between windows (inner-*) and between monitor edges (outer-*).
# Possible values:
# - Constant:     gaps.outer.top = 8
# - Per monitor:  gaps.outer.top = [{ monitor.main = 16 }, { monitor."some-pattern" = 32 }, 24]
#                 In this example, 24 is a default value when there is no match.
#                 Monitor pattern is the same as for 'workspace-to-monitor-force-assignment'.
#                 See: https://nikitabobko.github.io/AeroSpace/guide#assign-workspaces-to-monitors
[gaps]
inner.horizontal = 0
inner.vertical =   0
outer.left =       0
outer.bottom =     0
outer.top =        0
outer.right =      0

# 'main' binding mode declaration
# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes
# 'main' binding mode must be always presented
# Fallback value (if you omit the key): mode.main.binding = {}
[mode.main.binding]

# All possible keys:
# - Letters.        a, b, c, ..., z
# - Numbers.        0, 1, 2, ..., 9
# - Keypad numbers. keypad0, keypad1, keypad2, ..., keypad9
# - F-keys.         f1, f2, ..., f20
# - Special keys.   minus, equal, period, comma, slash, backslash, quote, semicolon, backtick,
#                   leftSquareBracket, rightSquareBracket, space, enter, esc, backspace, tab
# - Keypad special. keypadClear, keypadDecimalMark, keypadDivide, keypadEnter, keypadEqual,
#                   keypadMinus, keypadMultiply, keypadPlus
# - Arrows.         left, down, up, right

# All possible modifiers: cmd, alt, ctrl, shift

# All possible commands: https://nikitabobko.github.io/AeroSpace/commands

# See: https://nikitabobko.github.io/AeroSpace/commands#exec-and-forget
# You can uncomment the following lines to open up terminal with alt + enter shortcut (like in i3)
# alt-enter = '''exec-and-forget osascript -e '
# tell application "Terminal"
#     do script
#     activate
# end tell'
# '''

# See: https://nikitabobko.github.io/AeroSpace/commands#layout
alt-slash = 'layout tiles horizontal vertical'
alt-comma = 'layout accordion horizontal vertical'

# See: https://nikitabobko.github.io/AeroSpace/commands#focus
alt-h = 'focus left'
alt-j = 'focus down'
alt-k = 'focus up'
alt-l = 'focus right'

# See: https://nikitabobko.github.io/AeroSpace/commands#move
alt-shift-h = 'move left'
alt-shift-j = 'move down'
alt-shift-k = 'move up'
alt-shift-l = 'move right'

# See: https://nikitabobko.github.io/AeroSpace/commands#resize
alt-shift-minus = 'resize smart -50'
alt-shift-equal = 'resize smart +50'

# See: https://nikitabobko.github.io/AeroSpace/commands#workspace
alt-1 = 'workspace 1'
alt-2 = 'workspace 2'
alt-3 = 'workspace 3'
alt-4 = 'workspace 4'
alt-5 = 'workspace 5'
alt-6 = 'workspace 6'
alt-7 = 'workspace 7'
alt-8 = 'workspace 8'
alt-9 = 'workspace 9'
alt-0 = 'workspace 0'

# See: https://nikitabobko.github.io/AeroSpace/commands#move-node-to-workspace
alt-shift-1 = 'move-node-to-workspace 1'
alt-shift-2 = 'move-node-to-workspace 2'
alt-shift-3 = 'move-node-to-workspace 3'
alt-shift-4 = 'move-node-to-workspace 4'
alt-shift-5 = 'move-node-to-workspace 5'
alt-shift-6 = 'move-node-to-workspace 6'
alt-shift-7 = 'move-node-to-workspace 7'
alt-shift-8 = 'move-node-to-workspace 8'
alt-shift-9 = 'move-node-to-workspace 9'
alt-shift-0 = 'move-node-to-workspace 0'

# See: https://nikitabobko.github.io/AeroSpace/commands#workspace-back-and-forth
alt-tab = 'workspace-back-and-forth'
# See: https://nikitabobko.github.io/AeroSpace/commands#move-workspace-to-monitor
alt-shift-tab = 'move-workspace-to-monitor --wrap-around next'

# See: https://nikitabobko.github.io/AeroSpace/commands#mode
alt-shift-semicolon = 'mode service'

# 'service' binding mode declaration.
# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes
[mode.service.binding]
esc = ['reload-config', 'mode main']
r = ['flatten-workspace-tree', 'mode main'] # reset layout
f = ['layout floating tiling', 'mode main'] # Toggle between floating and tiling layout
backspace = ['close-all-windows-but-current', 'mode main']

# sticky is not yet supported https://github.com/nikitabobko/AeroSpace/issues/2
#s = ['layout sticky tiling', 'mode main']

alt-shift-h = ['join-with left', 'mode main']
alt-shift-j = ['join-with down', 'mode main']
alt-shift-k = ['join-with up', 'mode main']
alt-shift-l = ['join-with right', 'mode main']

down = 'volume down'
up = 'volume up'

shift-down = ['volume set 0', 'mode main']

# custom rules
# https://nikitabobko.github.io/AeroSpace/guide#on-window-detected-callback
[[on-window-detected]]
if.app-id = 'com.apple.finder'
check-further-callbacks = true
run = ['layout floating']

.alacritty.toml

.alacritty.toml · 627 lines

[general]
live_config_reload = true

[bell]
animation = "EaseOutExpo"
duration = 0

[colors]
draw_bold_text_with_bright_colors = true
indexed_colors = []

[colors.bright]
black = "0x4d4d4d"
blue = "0xcaa9fa"
cyan = "0x9aedfe"
green = "0x5af78e"
magenta = "0xff92d0"
red = "0xff6e67"
white = "0xe6e6e6"
yellow = "0xf4f99d"

[colors.cursor]
cursor = "0xf8f8f2"
text = "0x44475a"

[colors.dim]
black = "0x14151b"
blue = "0x4d5b86"
cyan = "0x59dffc"
green = "0x1ef956"
magenta = "0xff46b0"
red = "0xff2222"
white = "0xe6e6d1"
yellow = "0xebf85b"

[colors.normal]
black = "0x000000"
blue = "0xbd93f9"
cyan = "0x8be9fd"
green = "0x50fa7b"
magenta = "0xff79c6"
red = "0xff5555"
white = "0xbfbfbf"
yellow = "0xf1fa8c"

[colors.primary]
background = "0x282a36"
foreground = "0xf8f8f2"

[cursor]
style = "Block"
unfocused_hollow = true

[env]
TERM = "xterm-256color"

[font]
size = 12.0

[font.normal]
family = "JetBrainsMono Nerd Font"

[font.glyph_offset]
x = 0
y = 0

[font.offset]
x = 0
y = 0

[[keyboard.bindings]]
action = "Paste"
key = "V"
mods = "Command"

[[keyboard.bindings]]
action = "Copy"
key = "C"
mods = "Command"

[[keyboard.bindings]]
action = "Paste"
key = "Paste"

[[keyboard.bindings]]
action = "Copy"
key = "Copy"

[[keyboard.bindings]]
action = "Hide"
key = "H"
mods = "Command"

[[keyboard.bindings]]
action = "Quit"
key = "Q"
mods = "Command"

[[keyboard.bindings]]
action = "Quit"
key = "W"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001BOH"
key = "Home"
mode = "AppCursor"

[[keyboard.bindings]]
chars = "\u001B[H"
key = "Home"
mode = "~AppCursor"

[[keyboard.bindings]]
chars = "\u001BOF"
key = "End"
mode = "AppCursor"

[[keyboard.bindings]]
chars = "\u001B[F"
key = "End"
mode = "~AppCursor"

[[keyboard.bindings]]
action = "ResetFontSize"
key = "Key0"
mods = "Command"

[[keyboard.bindings]]
action = "IncreaseFontSize"
key = "Equals"
mods = "Command"

[[keyboard.bindings]]
action = "DecreaseFontSize"
key = "Minus"
mods = "Command"

[[keyboard.bindings]]
action = "ClearHistory"
key = "K"
mods = "Command"

[[keyboard.bindings]]
chars = "\f"
key = "K"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[5;2~"
key = "PageUp"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[5;5~"
key = "PageUp"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[5~"
key = "PageUp"

[[keyboard.bindings]]
chars = "\u001B[6;2~"
key = "PageDown"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[6;5~"
key = "PageDown"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[6~"
key = "PageDown"

[[keyboard.bindings]]
chars = "\u001B[Z"
key = "Tab"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u007F"
key = "Back"

[[keyboard.bindings]]
chars = "\u001B\u007F"
key = "Back"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[2~"
key = "Insert"

[[keyboard.bindings]]
chars = "\u001B[3~"
key = "Delete"

[[keyboard.bindings]]
chars = "\u001B[1;2D"
key = "Left"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[1;5D"
key = "Left"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001Bb"
key = "Left"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[D"
key = "Left"
mode = "~AppCursor"

[[keyboard.bindings]]
chars = "\u001BOD"
key = "Left"
mode = "AppCursor"

[[keyboard.bindings]]
chars = "\u001B[1;2C"
key = "Right"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[1;5C"
key = "Right"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001Bf"
key = "Right"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[C"
key = "Right"
mode = "~AppCursor"

[[keyboard.bindings]]
chars = "\u001BOC"
key = "Right"
mode = "AppCursor"

[[keyboard.bindings]]
chars = "\u001B[1;2A"
key = "Up"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[1;5A"
key = "Up"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[1;3A"
key = "Up"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[A"
key = "Up"
mode = "~AppCursor"

[[keyboard.bindings]]
chars = "\u001BOA"
key = "Up"
mode = "AppCursor"

[[keyboard.bindings]]
chars = "\u001B[1;2B"
key = "Down"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[1;5B"
key = "Down"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[1;3B"
key = "Down"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[B"
key = "Down"
mode = "~AppCursor"

[[keyboard.bindings]]
chars = "\u001BOB"
key = "Down"
mode = "AppCursor"

[[keyboard.bindings]]
chars = "\u001BOP"
key = "F1"

[[keyboard.bindings]]
chars = "\u001BOQ"
key = "F2"

[[keyboard.bindings]]
chars = "\u001BOR"
key = "F3"

[[keyboard.bindings]]
chars = "\u001BOS"
key = "F4"

[[keyboard.bindings]]
chars = "\u001B[15~"
key = "F5"

[[keyboard.bindings]]
chars = "\u001B[17~"
key = "F6"

[[keyboard.bindings]]
chars = "\u001B[18~"
key = "F7"

[[keyboard.bindings]]
chars = "\u001B[19~"
key = "F8"

[[keyboard.bindings]]
chars = "\u001B[20~"
key = "F9"

[[keyboard.bindings]]
chars = "\u001B[21~"
key = "F10"

[[keyboard.bindings]]
chars = "\u001B[23~"
key = "F11"

[[keyboard.bindings]]
chars = "\u001B[24~"
key = "F12"

[[keyboard.bindings]]
chars = "\u001B[1;2P"
key = "F1"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[1;2Q"
key = "F2"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[1;2R"
key = "F3"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[1;2S"
key = "F4"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[15;2~"
key = "F5"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[17;2~"
key = "F6"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[18;2~"
key = "F7"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[19;2~"
key = "F8"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[20;2~"
key = "F9"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[21;2~"
key = "F10"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[23;2~"
key = "F11"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[24;2~"
key = "F12"
mods = "Shift"

[[keyboard.bindings]]
chars = "\u001B[1;5P"
key = "F1"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[1;5Q"
key = "F2"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[1;5R"
key = "F3"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[1;5S"
key = "F4"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[15;5~"
key = "F5"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[17;5~"
key = "F6"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[18;5~"
key = "F7"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[19;5~"
key = "F8"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[20;5~"
key = "F9"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[21;5~"
key = "F10"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[23;5~"
key = "F11"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[24;5~"
key = "F12"
mods = "Control"

[[keyboard.bindings]]
chars = "\u001B[1;6P"
key = "F1"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[1;6Q"
key = "F2"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[1;6R"
key = "F3"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[1;6S"
key = "F4"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[15;6~"
key = "F5"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[17;6~"
key = "F6"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[18;6~"
key = "F7"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[19;6~"
key = "F8"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[20;6~"
key = "F9"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[21;6~"
key = "F10"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[23;6~"
key = "F11"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[24;6~"
key = "F12"
mods = "Alt"

[[keyboard.bindings]]
chars = "\u001B[1;3P"
key = "F1"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[1;3Q"
key = "F2"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[1;3R"
key = "F3"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[1;3S"
key = "F4"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[15;3~"
key = "F5"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[17;3~"
key = "F6"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[18;3~"
key = "F7"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[19;3~"
key = "F8"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[20;3~"
key = "F9"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[21;3~"
key = "F10"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[23;3~"
key = "F11"
mods = "Command"

[[keyboard.bindings]]
chars = "\u001B[24;3~"
key = "F12"
mods = "Command"

[[keyboard.bindings]]
chars = """

"""
key = "NumpadEnter"

# Shift+Enter for multi-line input in pi
[[keyboard.bindings]]
chars = "\u001B[13;2u"
key = "Return"
mods = "Shift"

[mouse]
hide_when_typing = false

[[mouse.bindings]]
action = "PasteSelection"
mouse = "Middle"

[scrolling]
history = 10000
multiplier = 3

[selection]
save_to_clipboard = false
semantic_escape_chars = ",│`|:\"' ()[]{}<>"

[window]
decorations = "full"
dynamic_title = true
opacity = 0.9
blur = true
option_as_alt = "both"

[window.dimensions]
columns = 200
lines = 50

[window.padding]
x = 2
y = 2

.editorconfig

.editorconfig · 22 lines

# http://editorconfig.org

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{ini,yml,json,jsonc,lua,sh,css,scss}]
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

[.gitconfig]
indent_style = tab

.git_commit_message.txt

.git_commit_message.txt · 5 lines

# If applied, this commit will...

# Explain why this change is being made

# Provide links to any relevant tickets, articles or other resources

.gitconfig

.gitconfig · 35 lines

[user]
	name = Erick Navarro
	email = erick@navarro.io
	signingKey = ~/.ssh/id_ed25519.pub
[alias]
	pl = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative
	cm = commit -v
	co = checkout
	br = branch
	json = log --pretty=format:'{%n  "commit": "%H",%n  "abbreviated_commit": "%h",%n  "tree": "%T",%n  "abbreviated_tree": "%t",%n  "parent": "%P",%n  "abbreviated_parent": "%p",%n  "refs": "%D",%n  "encoding": "%e",%n  "subject": "%s",%n  "sanitized_subject_line": "%f",%n  "body": "%b",%n  "commit_notes": "%N",%n  "verification_flag": "%G?",%n  "signer": "%GS",%n  "signer_key": "%GK",%n  "author": {%n    "name": "%aN",%n    "email": "%aE",%n    "date": "%aD"%n  },%n  "commiter": {%n    "name": "%cN",%n    "email": "%cE",%n    "date": "%cD"%n  }%n},'
[core]
	editor = vim
	autocrlf = input
	quotepath = false
	excludesfile = ~/.gitignore
[github]
	user = erickgnavar
[diff]
	compactionHeuristic = true
[color "diff"]
	old = red strike
	new = green italic
[commit]
	template = ~/.git_commit_message.txt
	gpgsign = true
[gpg]
	format = ssh
[status]
	showUntrackedFiles = all
[gitlab]
	user = erickgnavar
[pull]
	rebase = true
[init]
	defaultBranch = master

.gitignore

.gitignore · 7 lines

.DS_Store
.emacs.d/elpa
.emacs.d/backups
.emacs.d/quelpa
*.elc
wallpaper-picker/result
clipboard-picker/result

.ideavimrc

.ideavimrc · 2 lines

nnoremap gcc :action CommentByLineComment<CR>
vnoremap gc :action CommentByLineComment<CR>

.shellcheckrc

.shellcheckrc · 3 lines

disable=SC1090 # load files using source or .
disable=SC1091 # cannot reach files outside project
disable=SC2012 # temporary until replace ls with find

.tmux.conf

.tmux.conf · 125 lines

# Enable extended keys for Shift+Enter, Ctrl+Enter etc.
set -g extended-keys on
set -g extended-keys-format csi-u

# Terminal capabilities
set -g default-terminal "tmux-256color"
set -ag terminal-overrides ",xterm-256color:RGB"

# Change prefix key
unbind C-b
set -g prefix C-Space

# reload conf
bind r source-file ~/.tmux.conf \; display "tmux config reloaded!"

# move through panes
unbind h
bind -r h select-pane -L

unbind j
bind -r j select-pane -D

unbind k
bind -r k select-pane -U

unbind l
bind -r l select-pane -R

unbind n
bind -r n next-window

unbind p
bind -r p previous-window

# Resizing
bind -r C-h resize-pane -L 5
bind -r C-j resize-pane -D 5
bind -r C-k resize-pane -U 5
bind -r C-l resize-pane -R 5

# Move current pane to a separate window
bind b break-pane -d

# More comfortable split shortcuts
bind '\' split-window -h -c '#{pane_current_path}'  # Split panes horizontal
bind - split-window -v -c '#{pane_current_path}'  # Split panes vertically
# Open new windows at the session's base path
bind c new-window -c '#{session_path}'

# fzf session switcher on prefix + space
bind Space display-popup -E 'tmux list-sessions -F "#{session_name}" | fzf --prompt="session> " | xargs -r tmux switch-client -t'

# allow to select links in buffer using fzf
bind u display-popup -E 'url=$(tmux capture-pane -J -p | grep -oE "https?://[^[:space:]]+" | sort -u | fzf --reverse); [ -z "$url" ] || if [ "$(uname -s)" = Darwin ]; then open "$url"; else xdg-open "$url"; fi'

# window list
set-window-option -g mode-keys vi
set-window-option -g automatic-rename off

# enable mouse
set -g mouse on

# Keep enough scrollback for reviewing long-running agent output.
set -g history-limit 20000

# Status update interval
set -g status-interval 1

# UI improvements copied originally from https://github.com/jimeh/tmux-themepack

# Basic status bar colors
set -g status-style fg=cyan,bg=black

# Left side of status bar
set -g status-left-style fg=green,bg=black
set -g status-left-length 40
set -g status-left '#{?client_prefix,#[fg=black]#[bg=cyan]#[bold] PREFIX #[default] ,}#[fg=green]#[bg=black]#S'

# Right side of status bar
set -g status-right-style fg=cyan,bg=black
set -g status-right-length 70
set -g status-right '#{?pane_in_mode,#[fg=black]#[bg=yellow]#[bold] COPY #[default] ,}#{?window_zoomed_flag,#[fg=black]#[bg=magenta]#[bold] ZOOM #[default] ,}#[fg=cyan]#H #[fg=white]« #[fg=yellow]%H:%M:%S #[fg=green]%d-%b-%y'

# Window status
set -g window-status-format " #I:#W#F "
set -g window-status-current-format " #I:#W#F "

# Current window status
set -g window-status-current-style fg=black,bg=red

# Window with activity status
set -g window-status-activity-style fg=black,bg=yellow

# Window separator
set -g window-status-separator ""

# Window status alignment
set -g status-justify centre

# show border indicator and arrows
set -g pane-border-indicators both

# Pane border
set -g pane-border-style fg=colour240,bg=default
set -g pane-border-lines heavy

# Active pane border
set -g pane-active-border-style fg=magenta,bg=default,bold

# Pane number indicator
set -g display-panes-colour default
set -g display-panes-active-colour default

# Clock mode
set -g clock-mode-colour red
set -g clock-mode-style 24

# Message
set -g message-style fg=default,bg=default

# Command message
set -g message-command-style fg=default,bg=default

# Mode
set -g mode-style fg=default,bg=red

.vimrc

.vimrc · 164 lines

set nocompatible

" --- vim-plug bootstrap -------------------------------------------------
" Downloads plug.vim to ~/.vim/autoload/ on first run so plug#begin is
" available immediately, then runs :PlugInstall to fetch the plugins.
let s:plug_install = empty(glob('~/.vim/autoload/plug.vim'))
if s:plug_install
    echo "Installing vim-plug..."
    silent !mkdir -p ~/.vim/autoload
    silent !curl -fLo ~/.vim/autoload/plug.vim
        \ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
endif
call plug#begin('~/.vim/plugged')

" Plugins
Plug 'tomtom/tcomment_vim'
" Zen coding
Plug 'mattn/emmet-vim'
" Airline
Plug 'vim-airline/vim-airline'
Plug 'vim-airline/vim-airline-themes'
Plug 'tpope/vim-surround'
Plug 'Townk/vim-autoclose' " Allow show diff sign of opened files
Plug 'lilydjwg/colorizer'
Plug 'editorconfig/editorconfig-vim'
Plug 'sheerun/vim-polyglot'  " Plugins for most of the programming languages
Plug 'elzr/vim-json'
Plug 'mattn/webapi-vim' "gist-vim requirement
Plug 'mattn/gist-vim'
Plug 'tpope/vim-fugitive'
Plug 'airblade/vim-gitgutter'
Plug 'henrik/vim-indexed-search'
Plug 'airblade/vim-rooter'
Plug 'terryma/vim-expand-region'
Plug 'chriskempson/tomorrow-theme', { 'rtp': 'vim' }
" Provides the fzf#run autoload glue used by fzf.vim; the binary comes from the system.
Plug 'junegunn/fzf'
Plug 'junegunn/fzf.vim'
Plug 'andymass/vim-matchup'

call plug#end()

let g:loaded_matchit = 1

if s:plug_install
    echo "Installing plugins, please ignore key map error messages"
    echo ""
    :PlugInstall
endif

" ============================================================================
" Vim settings and mappings
" You can edit them as you wish

" allow plugins by file type (required for plugins!)
filetype plugin on
filetype indent on

" tabs and spaces handling
set expandtab
set tabstop=4
set softtabstop=4
set shiftwidth=4

set ls=2 "always show status bar

set incsearch "incremental search
set hlsearch "highlighted search results

syntax enable "enable syntax highlight
set path+=**

" avoid to show an error with long file paths
set shortmess=F

set number "show line numbers
set showcmd "show last command
set wildmenu "visual autocomplete for command menu
set showmatch "highlight match [{}]
set encoding=utf-8
nnoremap <space> za

" Gist commands
let g:gist_clip_command = 'pbcopy'
let g:gist_detect_filetype = 1

set completeopt-=preview

" save as sudo
ca w!! w !sudo tee "%"

" colors
if has('gui_running')
    set guifont=JetBrainsMono\ Nerd\ Font:h12
endif

silent! colorscheme Tomorrow-Night-Bright

set scrolloff=3 " when scrolling, keep cursor 3 lines away from screen border

" better backup, swap and undos storage
set directory=~/.vim/dirs/tmp     " directory to place swap files in
set backup                        " make backup files
set backupdir=~/.vim/dirs/backups " where to put backup files
set undofile                      " persistent undos - undo after you re-open the file
set undodir=~/.vim/dirs/undos
set viminfo+=n~/.vim/dirs/viminfo

" create needed directories if they don't exist
if !isdirectory(&backupdir)
    call mkdir(&backupdir, "p")
endif
if !isdirectory(&directory)
    call mkdir(&directory, "p")
endif
if !isdirectory(&undodir)
    call mkdir(&undodir, "p")
endif

" Setup clipboard
set clipboard=unnamed

" Setup leader functions
nmap <Leader>a :Rg<space>
nmap <Leader>b :Buffers<ENTER>
nmap <Leader>c :ClearCtrlPCache<ENTER>
nmap <leader>e :call MyFindFiles()<ENTER>
nmap <Leader>g :Git<ENTER>
nmap <Leader>k :bdelete<space>
nmap <Leader>n :enew<ENTER>
nmap <Leader>q :BLines<ENTER>

function MyFindFiles()
  if system('git rev-parse --is-inside-work-tree 2>/dev/null') =~ 'true'
    :GFiles --exclude-standard --others --cached
  else
    :Files
  endif
endfunction

" " Autoclose ------------------------------
" " Fix to let ESC work as espected with Autoclose plugin
let g:AutoClosePumvisible = {"ENTER": "\<C-Y>", "ESC": "\<ESC>"}

set backspace=2

let g:airline_powerline_fonts = 0
let g:airline#extensions#tabline#enabled = 1
let g:airline#extensions#whitespace#enabled = 0

set lazyredraw
set ttyfast

" GitGutter config
set updatetime=100

" Navigate through hunks
nmap <c-n> <Nop>
nmap <c-p> <Nop>
nmap <c-n> :GitGutterNextHunk<ENTER>
nmap <c-p> :GitGutterPrevHunk<ENTER>

" automatically rebalance windows on vim resize
autocmd VimResized * :wincmd = "

.Xmodmap

.Xmodmap · 5 lines

!switch caps lock with escape
remove Lock = Caps_Lock
keysym Caps_Lock = Escape
keysym Escape = Caps_Lock
add Lock = Caps_Lock

Brewfile

Brewfile · 178 lines

tap "grafana/grafana"
tap "homebrew/bundle"
tap "homebrew/services"
tap "nikitabobko/tap"
tap "twitchdev/twitch"
brew "libtiff"
brew "little-cms2"
brew "webp"
brew "arp-scan"
brew "aspell"
brew "autoconf"
brew "automake"
brew "openssl@3"
brew "bat"
brew "biome"
brew "btop"
brew "caddy", restart_service: :changed
brew "clang-format"
brew "openjdk"
brew "clojure"
brew "cmake"
brew "cocoapods"
brew "colima"
brew "cookiecutter"
brew "curl"
brew "libusb"
brew "cyme"
brew "d2"
brew "defaultbrowser"
brew "docker"
brew "docker-compose"
brew "duf"
brew "exercism"
brew "exiftool"
brew "expat"
brew "eza"
brew "fastfetch"
brew "gnutls"
brew "tesseract"
brew "ffmpeg"
brew "flyctl"
brew "fzf"
brew "gh"
brew "git"
brew "git-delta"
brew "gnu-sed"
brew "gnupg"
brew "go"
brew "gopls"
brew "grafana", restart_service: :changed
brew "librsvg"
brew "graphviz"
brew "helm"
brew "ncurses"
brew "htop"
brew "httpie"
brew "hugo"
brew "imagemagick"
brew "jdtls"
brew "jpeg"
brew "jq"
brew "just"
brew "kondo"
brew "kubernetes-cli"
brew "leiningen"
brew "libgccjit"
brew "libiconv"
brew "libpq"
brew "libvterm"
brew "libxml2"
brew "lorem"
brew "lsusb"
brew "lua-language-server"
brew "mailutils"
brew "markdownlint-cli"
brew "minikube"
brew "minio", restart_service: :changed
brew "mise"
brew "nerdfetch"
brew "nmap"
brew "ocaml"
brew "raylib"
brew "odin"
brew "opam"
brew "pandoc"
brew "pkgconf"
brew "plantuml"
brew "pngpaste"
brew "poppler"
brew "postgresql@13"
brew "prometheus"
brew "pyright"
brew "ripgrep"
brew "ruff"
brew "sbcl"
brew "sevenzip"
brew "shellcheck"
brew "shfmt"
brew "sl"
brew "sqlfluff"
brew "stripe-cli"
brew "stylua"
brew "tdlib", args: ["HEAD"]
brew "tealdeer"
brew "tectonic"
brew "terraform"
brew "texinfo"
brew "texlab"
brew "tinymist"
brew "tmux"
brew "tokei"
brew "tree"
brew "tree-sitter"
brew "typst"
brew "typstyle"
brew "uv"
brew "vim"
brew "wakatime-cli"
brew "watch"
brew "wget"
brew "wrk"
brew "wxwidgets"
brew "xmlformat"
brew "yamllint"
brew "youtube-dl"
brew "zlib"
brew "twitchdev/twitch/twitch-cli"
cask "1password"
cask "aerospace"
cask "alacritty"
cask "alfred"
cask "audacity"
cask "balenaetcher"
cask "blender@lts"
cask "brave-browser"
cask "bruno"
cask "claude"
cask "cleanshot"
cask "cyberduck"
cask "deckset"
cask "discord"
cask "espanso"
cask "firefox"
cask "font-jetbrains-mono-nerd-font"
cask "font-monocraft"
cask "font-open-dyslexic"
cask "font-pacifico"
cask "freecad"
cask "gcloud-cli"
cask "google-chrome"
cask "hammerspoon"
cask "homerow"
cask "imageoptim"
cask "keycastr"
cask "krisp"
cask "loopback"
cask "minecraft"
cask "obs"
cask "openemu"
cask "orcaslicer"
cask "pencil"
cask "pixieditor"
cask "qflipper"
cask "raspberry-pi-imager"
cask "soundsource"
cask "spotify"
cask "steam"
cask "telegram"
cask "textual"
cask "tor-browser"
cask "transmission"
cask "tunnelblick"
cask "vitalsource-bookshelf"
cask "vlc"
cask "wireshark-app"
cask "yubico-authenticator"
cask "zen"
cask "zotero"

global.gitignore

global.gitignore · 11 lines

# macOS files
.DS_Store
# direnv file
.envrc
# mise local files
mise.local.toml
# emacs per project configuration file
.dir-locals.el
# mutagen config files
mutagen.yml
mutagen.yml.lock

README.md

README.md · 25 lines

# Dotfiles

These are my config files for the tools I use:

## Emacs

Requires emacs 26 because it uses native line numbers. Go to `.emacs.d` folder to check the config.

Tested on MacOS Mojave

## Vim

I use vim for quick edits, check out the cconf [here](.vimrc)

## Alacrity

A multiplatform terminal emulator. It's simple and configurable by a yaml [config](.alacritty.yml) file.

## Tmux

A terminal multiplexer, easy to handle panes, splitted windows, sessions, etc. 

## ZSH

Go to `zsh` folder to check the config.

setup.sh

setup.sh · 40 lines

#!/usr/bin/env bash
set -euo pipefail

echo "Setting up your system!"

platform="${1:-$(uname -s)}"

case "$platform" in
Darwin | darwin | macOS | macos) platform_file="placements/macos.txt" ;;
Linux | linux) platform_file="placements/linux.txt" ;;
*)
  echo "Unsupported platform: $platform" >&2
  exit 1
  ;;
esac

while read -r line; do
  # Skip empty lines and comments
  [[ -z "$line" || "$line" == \#* ]] && continue

  source_path="${line%% *}"
  path="${line#* }"
  config_file="$(pwd)/$source_path"
  # Expand ~ to $HOME (avoids unsafe eval)
  path="${path/#\~/$HOME}"
  parent_dir=$(dirname "$path")

  # ensure the parent directory exists
  if [ ! -d "$parent_dir" ]; then
    echo "creating $parent_dir"
    mkdir -p "$parent_dir"
  fi

  if [ -e "$path" ]; then
    echo "$path already exists"
  else
    ln -s "$config_file" "$path"
    echo "symlink created: $config_file -> $path"
  fi
done < <(cat placements/common.txt "$platform_file")

Ghostty

config

ghostty/config · 6 lines

theme = dracula
font-size = 12
font-family = JetBrainsMono Nerd Font
background-opacity = 0.9
background-blur-radius = 20
confirm-close-surface = false

GitHub

bash-ci.yaml

.github/workflows/bash-ci.yaml · 22 lines

---
name: Bash CI

on:
  push:
    branches: [ master ]

jobs:
  lint-and-formatter:
    runs-on: ubuntu-24.04
    steps:
      - name: Check out the repo
        uses: actions/checkout@v7

      - name: Install shellcheck
        run: sudo apt update && sudo apt install shellcheck shfmt --yes

      - name: Run formatter using shfmt
        run: shfmt -l -i 2 .

      - name: Run shellcheck
        run: find . -name "*.sh" -type f -exec shellcheck {} \;

c-ci.yaml

.github/workflows/c-ci.yaml · 20 lines

---
name: C CI

on:
  push:
    branches: [ master ]
  pull_request:

jobs:
  format:
    runs-on: ubuntu-24.04
    steps:
      - name: Check out the repo
        uses: actions/checkout@v7

      - name: Install clang-format
        run: sudo apt update && sudo apt install clang-format --yes

      - name: Check C formatting
        run: find . -type f -name "*.c" -print0 | xargs -0 clang-format --dry-run --Werror

lua-ci.yaml

.github/workflows/lua-ci.yaml · 19 lines

---
name: Lua CI

on:
  push:
    branches: [ master ]

jobs:
  formatter:
    runs-on: ubuntu-24.04
    steps:
      - name: Check out the repo
        uses: actions/checkout@v7

      - uses: JohnnyMorganz/stylua-action@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          version: "0.20.0"
          args: --check --config-path hammerspoon/.stylua.toml .

publish-config-site.yml

.github/workflows/publish-config-site.yml · 53 lines

---
name: Publish config site

on:
  push:
    branches: [master]

permissions:
  contents: write

jobs:
  publish:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - name: Install Emacs
        run: sudo apt-get update && sudo apt-get install --yes emacs-nox
      - name: Generate site
        run: ./scripts/generate-config-site.py
      - name: Publish gh-pages branch
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh auth setup-git
          publish_dir="${RUNNER_TEMP}/config-site-pages"
          repository="https://github.com/${GITHUB_REPOSITORY}.git"

          if git ls-remote --exit-code --heads "$repository" gh-pages >/dev/null; then
            git clone --depth=1 --branch=gh-pages "$repository" "$publish_dir"
          else
            branch_status=$?
            if ((branch_status != 2)); then
              echo "Failed to inspect the gh-pages branch" >&2
              exit "$branch_status"
            fi
            git init --initial-branch=gh-pages "$publish_dir"
            git -C "$publish_dir" remote add origin "$repository"
          fi

          git -C "$publish_dir" rm -r --ignore-unmatch .
          cp -a site/. "$publish_dir"/
          git -C "$publish_dir" add --all
          git -C "$publish_dir" config user.name "$GITHUB_ACTOR"
          git -C "$publish_dir" config user.email "${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com"

          if git -C "$publish_dir" diff --cached --quiet; then
            echo "No site changes to publish"
          else
            git -C "$publish_dir" commit \
              --message="deploy: publish config site" \
              --message="Generated from ${GITHUB_SHA}"
            git -C "$publish_dir" push origin gh-pages
          fi

Gtk

settings.ini

gtk/gtk-3.0/settings.ini · 5 lines

[Settings]
gtk-theme-name=adw-gtk3-dark
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Sans 10
gtk-application-prefer-dark-theme=1

settings.ini

gtk/gtk-4.0/settings.ini · 5 lines

[Settings]
gtk-theme-name=Adwaita-dark
gtk-icon-theme-name=Papirus-Dark
gtk-font-name=Sans 10
gtk-application-prefer-dark-theme=1

Hammerspoon

.stylua.toml

hammerspoon/.stylua.toml · 4 lines

line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
call_parentheses = "None"

init.lua

hammerspoon/init.lua · 48 lines

-- prefix used to call all the hot keys
leader = { "cmd", "alt", "ctrl" }

hs.hotkey.bind(leader, "R", function()
  hs.reload()
end)

hs.hotkey.bind(leader, "T", function()
  hs.application.open "Terminal.app"
end)

-- windows management config
require "windows"

-- shortcuts to open more common application
hs.hotkey.bind("alt", "1", function()
  hs.application.open "Alacritty.app"
end)

hs.hotkey.bind("alt", "2", function()
  local defaultBrowser = hs.urlevent.getDefaultHandler "http"
  hs.application.launchOrFocusByBundleID(defaultBrowser)
end)

hs.hotkey.bind("alt", "3", function()
  -- nix-launched Emacs registers only as name=emacs (no bundle ID or path),
  -- and hs.application.find can return an hs.window instead, so scan by name.
  local app
  for _, running in ipairs(hs.application.runningApplications()) do
    if running:name() == "emacs" then
      app = running
      break
    end
  end

  if app then
    app:activate()
  else
    -- we need to execute Emacs this way to be able to use mise
    -- properly, otherwise we can't execute stuff like, mix, elixir
    -- and so on
    hs.task.new("/bin/zsh", nil, { "-l", "-c", "emacs > /dev/null 2>&1 & disown" }):start()
  end
end)

hs.hotkey.bind("alt", "4", function()
  hs.application.open "Spotify.app"
end)

windows.lua

hammerspoon/windows.lua · 71 lines

-- coordinates X from left to right and Y from top to bottom

-- remove window movement animation
hs.window.animationDuration = 0

local function getWindowContext()
  local win = hs.window.focusedWindow()
  return win, win:frame(), win:screen(), win:screen():frame()
end

-- put to left
hs.hotkey.bind(leader, "H", function()
  local win, frame, screen, max = getWindowContext()
  frame.x = max.x
  frame.y = max.y
  frame.w = max.w / 2
  frame.h = max.h
  win:setFrame(frame)
end)

-- put to right
hs.hotkey.bind(leader, "L", function()
  local win, frame, screen, max = getWindowContext()
  frame.x = max.x + (max.w / 2)
  frame.y = max.y
  frame.w = max.w / 2
  frame.h = max.h
  win:setFrame(frame)
end)

-- put to top
hs.hotkey.bind(leader, "K", function()
  local win, frame, screen, max = getWindowContext()
  frame.x = max.x
  frame.y = max.y
  frame.w = max.w
  frame.h = max.h / 2
  win:setFrame(frame)
end)

-- put to bottom
hs.hotkey.bind(leader, "J", function()
  local win, frame, screen, max = getWindowContext()
  frame.x = max.x
  frame.y = max.y + (max.h / 2)
  frame.w = max.w
  frame.h = max.h / 2
  win:setFrame(frame)
end)

-- maximize frame
hs.hotkey.bind(leader, "RETURN", function()
  hs.window.focusedWindow():maximize()
end)

-- center frame
hs.hotkey.bind(leader, "\\", function()
  local win, frame, screen, max = getWindowContext()
  -- base grid layout will be 6 x 5 and center frame should be 4 x 3
  frame.x = max.x + (max.w / 6) * 1
  frame.y = max.y + (max.h / 5) * 1
  frame.w = (max.w / 6) * 4
  frame.h = (max.h / 5) * 3
  win:setFrame(frame)
end)

-- move application between screens
hs.hotkey.bind(leader, "M", function()
  local app = hs.window.focusedWindow()
  app:moveToScreen(app:screen():next())
end)

I3

config

i3/config · 128 lines

# use cmd/win key
set $mod Mod4

# TODO: replace font with jetbrains mono
font pango:Cantarell 10

gaps outer 5px
gaps inner 0px

# Use Mouse+$mod to drag floating windows to their wanted position
floating_modifier $mod

# remove application title
default_border pixel 1
default_floating_border pixel 1

# start a terminal
bindsym $mod+Return exec alacritty

# kill focused window
bindsym $mod+q kill

# start rofi (a program launcher)
bindsym $mod+space exec rofi -show drun -show-icons

# change focus
bindsym $mod+h focus left
bindsym $mod+j focus down
bindsym $mod+k focus up
bindsym $mod+l focus right

# move focused window
bindsym $mod+Shift+h move left
bindsym $mod+Shift+j move down
bindsym $mod+Shift+k move up
bindsym $mod+Shift+l move right

# enter fullscreen mode for the focused container
bindsym $mod+f fullscreen toggle

# change container layout (stacked, tabbed, toggle split)
bindsym $mod+s layout stacking
bindsym $mod+w layout tabbed
bindsym $mod+e layout toggle split

# toggle tiling / floating
bindsym $mod+Shift+d floating toggle

# change focus between tiling / floating windows
bindsym $mod+d focus mode_toggle

# Define names for default workspaces for which we configure key bindings later on.
# We use variables to avoid repeating the names in multiple places.
set $ws1 "1"
set $ws2 "2"
set $ws3 "3"
set $ws4 "4"
set $ws5 "5"
set $ws6 "6"
set $ws7 "7"
set $ws8 "8"
set $ws9 "9"
set $ws10 "10"

# switch to workspace
bindsym $mod+1 workspace $ws1
bindsym $mod+2 workspace $ws2
bindsym $mod+3 workspace $ws3
bindsym $mod+4 workspace $ws4
bindsym $mod+5 workspace $ws5
bindsym $mod+6 workspace $ws6
bindsym $mod+7 workspace $ws7
bindsym $mod+8 workspace $ws8
bindsym $mod+9 workspace $ws9
bindsym $mod+0 workspace $ws10

# move focused container to workspace
bindsym $mod+Shift+1 move container to workspace $ws1
bindsym $mod+Shift+2 move container to workspace $ws2
bindsym $mod+Shift+3 move container to workspace $ws3
bindsym $mod+Shift+4 move container to workspace $ws4
bindsym $mod+Shift+5 move container to workspace $ws5
bindsym $mod+Shift+6 move container to workspace $ws6
bindsym $mod+Shift+7 move container to workspace $ws7
bindsym $mod+Shift+8 move container to workspace $ws8
bindsym $mod+Shift+9 move container to workspace $ws9
bindsym $mod+Shift+0 move container to workspace $ws10

# volume
bindsym XF86AudioMute exec pamixer --toggle-mute
bindsym XF86AudioRaiseVolume exec pamixer -i 5
bindsym XF86AudioLowerVolume exec pamixer -d 5

# reload the configuration file
bindsym $mod+Shift+c reload

# lock screen using a blur background
bindsym $mod+Ctrl+q exec i3lock-fancy

# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
bindsym $mod+Shift+r restart

# exit i3 (logs you out of your X session)
bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'Do you really want to exit i3? This will end your X session.' -B 'Yes, exit i3' 'i3-msg exit'"

# resize window (you can also use the mouse for that)
bindsym $mod+r mode "resize"

mode "resize" {
  bindsym h resize shrink width 10 px or 10 ppt
  bindsym j resize grow height 10 px or 10 ppt
  bindsym k resize shrink height 10 px or 10 ppt
  bindsym l resize grow width 10 px or 10 ppt

  # back to normal: Enter or Escape or $mod+r
  bindsym Return mode "default"
  bindsym Escape mode "default"
  bindsym $mod+r mode "default"
}

# start polybar
exec_always --no-startup-id $HOME/.config/polybar/launch.sh

# set up key delay and rate
exec_always xset r rate 300 60

# change styles of app window
client.focused          #2e3436 #2e3436 #ffffff #2e3436   #2e3436

Jj

config.toml

jj/config.toml · 13 lines

#:schema https://docs.jj-vcs.dev/latest/config-schema.json

[user]
name = "Erick Navarro"
email = "erick@navarro.io"

[ui]
default-command = "log"

[signing]
behavior = "own"
backend = "ssh"
key = "~/.ssh/id_ed25519.pub"

Nautilus

send-to-localsend.py

nautilus/scripts/send-to-localsend.py · 46 lines

#!/usr/bin/env python3
import os
import subprocess
from urllib.parse import unquote_to_bytes, urlsplit


def fail(message: str) -> None:
    subprocess.run(
        ["notify-send", "LocalSend", message],
        check=False,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    raise SystemExit(1)


selected_uris = os.environ.get("NAUTILUS_SCRIPT_SELECTED_URIS", "")
if not selected_uris:
    fail("No files or folders were selected.")

paths: list[str] = []
for uri in selected_uris.splitlines():
    parsed = urlsplit(uri)
    if parsed.scheme != "file" or parsed.netloc not in ("", "localhost"):
        fail("Only local files and folders can be sent.")
    if parsed.query or parsed.fragment:
        fail("A selected file has an unsupported URI.")

    path = os.fsdecode(unquote_to_bytes(parsed.path))
    if not os.path.exists(path):
        fail(f"The selected path no longer exists: {path}")
    paths.append(path)

if not paths:
    fail("No local files or folders were selected.")

try:
    subprocess.Popen(
        ["localsend_app", *paths],
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        start_new_session=True,
    )
except OSError as error:
    fail(f"Could not open LocalSend: {error}")

Nix Darwin

common.nix

nix-darwin/common.nix · 244 lines

{ pkgs, config, emacs-src, ... }: {
  # Allow unfree packages
  nixpkgs.config.allowUnfree = true;

  fonts.packages = with pkgs; [
    jetbrains-mono
    # this is required to show symbols properly
    nerd-fonts.symbols-only
    nerd-fonts.jetbrains-mono
    open-dyslexic
  ];
  # List packages installed in system profile. To search by name, run:
  # $ nix-env -qaP | grep wget
  environment.systemPackages = with pkgs; [
    mkalias
    btop
    htop
    tokei
    vim
    git
    jujutsu
    gh
    delta
    # cmake and glibtool are required to compile libvterm in emacs
    cmake
    just
    glibtool
    alacritty
    eza
    tree
    go
    gopls
    gotools
    air
    biome
    bat
    mise
    xz
    typst
    typstyle
    # LSP for typst
    tinymist
    tealdeer
    nerdfetch
    fastfetch
    fzf
    tmux
    ((pkgs.emacs.override {
      srcRepo = true;
      # drop libgccjit dep, no JIT at runtime
      withNativeCompilation = false;
    }).overrideAttrs (old: {
      name = "emacs-31";
      src = emacs-src;
      enableParallelBuilding = true; # parallel make
      NIX_BUILD_CORES = 0; # use all cores
      CFLAGS = "-O2 -mcpu=native"; # tune for this CPU
      # nixpkgs patches don't apply to this emacs-31 commit (already upstream)
      patches = [ ];
      postPatch = (old.postPatch or "") + ''
        # skip byte compilation of thousands of .el files (saves ~80% of build time)
        substituteInPlace lisp/Makefile.in \
          --replace-fail 'all: compile-main' 'all:'
      '';
    }))
    nixpkgs-fmt
    nixd
    shfmt
    (google-cloud-sdk.withExtraComponents [ google-cloud-sdk.components.gke-gcloud-auth-plugin ])
    ripgrep
    docker-client
    colima
    wakatime-cli
    uv
    kubectl
    kubernetes-helm
    sqlfluff
    stylua
    defaultbrowser
    xmlformat
    yamllint
    jq
    ruff
    pyright
    ty
    pyrefly
    exiftool
    gnused
    imagemagick
    librsvg
    pngpaste
    watch
    gnupg
    (aspellWithDicts (dicts: with dicts; [ en en-computers es ]))
  ];

  homebrew = {
    enable = true;
    # when removing a brew application from this file it should
    # deleted from system as well
    onActivation = {
      cleanup = "zap";
      # Homebrew 4.5+ (merged 2026-05-29) requires --force-cleanup for
      # non-interactive `brew bundle install --cleanup`.
      # https://github.com/Homebrew/brew/pull/22453
      extraFlags = [ "--force-cleanup" ];
    };
    brews = [
      # libvterm is not available in nix for aarch64 so we
      # install it from homebrew
      "libvterm"
      # this is required for mise be able to install erlang,
      # mise cannot identify openssl version installed in nix so
      # we need to use homebrew version to be able to use erlang
      # with no issues
      "openssl@3"
      "lorem"
    ];
    casks = [
      "macvim-app"
      "hammerspoon"
      "espanso"
      "homerow"
      "localsend"
      "postgres-app"
      "tailscale-app"
      "the-unarchiver"
      "keepingyouawake"
      "reflex-app"
      "handy"
    ];
  };

  # we need to specify primary user since nix-darwin 25.05
  system.primaryUser = "erick";

  # all available options are defined here https://daiderd.com/nix-darwin/manual/index.html
  system = {
    defaults = {
      dock.autohide = true;
      # Don't arrange spaces based on most recent use
      dock.mru-spaces = false;
      dock.orientation = "left";
      dock.showAppExposeGestureEnabled = true;
      finder.AppleShowAllExtensions = true;
      NSGlobalDomain.InitialKeyRepeat = 14;
      NSGlobalDomain.KeyRepeat = 2;
      NSGlobalDomain.AppleInterfaceStyle = "Dark";
      # use fn to access to media keys
      NSGlobalDomain."com.apple.keyboard.fnState" = true;
      # disable natural scrolling
      NSGlobalDomain."com.apple.swipescrolldirection" = false;
      # Use scroll gesture with the Ctrl (^) modifier key to
      # zoom, this requires to have "Full disk access" in the
      # program which run nix-darwin command
      universalaccess.closeViewScrollWheelToggle = true;
      trackpad = {
        Clicking = true;
        TrackpadRightClick = true;
        TrackpadThreeFingerVertSwipeGesture = 2;
      };
      controlcenter.Sound = true;
      controlcenter.Bluetooth = true;
      controlcenter.NowPlaying = false;
    };
    keyboard = {
      enableKeyMapping = true;
      remapCapsLockToEscape = true;
    };
  };

  system.defaults.CustomUserPreferences = {
    "com.apple.finder" = {
      ShowExternalHardDrivesOnDesktop = true;
      ShowHardDrivesOnDesktop = true;
      ShowMountedServersOnDesktop = true;
      ShowRemovableMediaOnDesktop = true;
      # When performing a search, search the current folder by default
      FXDefaultSearchScope = "SCcf";
      DisableAllAnimations = true;
      NewWindowTarget = "PfHm";
      NewWindowTargetPath = "file://$\{HOME\}/";
      AppleShowAllExtensions = true;
      FXEnableExtensionChangeWarning = false;
      ShowStatusBar = true;
      ShowPathbar = true;
      WarnOnEmptyTrash = false;
    };
    "com.apple.Safari" = {
      # Privacy: don’t send search queries to Apple
      UniversalSearchEnabled = false;
      SuppressSearchSuggestions = true;
      # Prevent Safari from opening ‘safe’ files automatically after downloading
      AutoOpenSafeDownloads = false;
      AutoFillFromAddressBook = false;
      AutoFillCreditCardData = false;
    };
    "com.apple.print.PrintingPrefs" = {
      # Automatically quit printer app once the print jobs complete
      "Quit When Finished" = true;
    };
  };

  # link nix-apps into /Applications to be indexed by spotlight
  system.activationScripts.applications.text =
    let
      env = pkgs.buildEnv {
        name = "system-applications";
        paths = config.environment.systemPackages;
        pathsToLink = [ "/Applications" ];
      };

    in
    pkgs.lib.mkForce ''
      rm -fr /Applications/Nix\ Apps
      mkdir -p /Applications/Nix\ Apps
      find ${env}/Applications -maxdepth 1 -type l -exec readlink '{}' + |
      while read -r src; do
        app_name=$(basename "$src")
        echo "copying $src" >&2
        ${pkgs.mkalias}/bin/mkalias "$src" "/Applications/Nix Apps/$app_name"
      done
    '';

  # Auto upgrade nix package and the daemon service.
  # nix.package = pkgs.nix;

  # Necessary for using flakes on this system.
  nix.settings.experimental-features = "nix-command flakes";

  # Create /etc/zshrc that loads the nix-darwin environment.
  programs.zsh.enable = true; # default shell on catalina
  # programs.fish.enable = true;

  # # Set Git commit hash for darwin-version.
  # system.configurationRevision = self.rev or self.dirtyRev or null;

  # Used for backwards compatibility, please read the changelog before changing.
  # $ darwin-rebuild changelog
  system.stateVersion = 5;

  # The platform the configuration will be used on.
  nixpkgs.hostPlatform = "aarch64-darwin";
}

flake.nix

nix-darwin/flake.nix · 35 lines

{
  description = "Darwin system flake";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";

    nix-darwin.url = "github:nix-darwin/nix-darwin/master";
    nix-darwin.inputs.nixpkgs.follows = "nixpkgs";

    emacs-src = {
      url = "github:emacs-mirror/emacs/1a49c3a02d0dc62976d918da644a562d2cc0e981";
      flake = false;
    };
  };

  outputs = inputs@{ self, nix-darwin, nixpkgs, emacs-src }:
    {
      # Build darwin flake using:
      # $ darwin-rebuild build --flake .#simple
      darwinConfigurations."simple" = nix-darwin.lib.darwinSystem {
        modules = [
          ./common.nix
          ./personal.nix
          {
            # Set Git commit hash for darwin-version.
            system.configurationRevision = self.rev or self.dirtyRev or null;
          }
        ];
        specialArgs = { inherit emacs-src; };
      };

      # Expose the package set, including overlays, for convenience.
      darwinPackages = self.darwinConfigurations."simple".pkgs;
    };
}

personal.nix

nix-darwin/personal.nix · 71 lines

{ pkgs, config, ... }: {
  environment.systemPackages = with pkgs; [
    xcodegen
    swift-format
    mutagen
    ffmpeg
  ];

  homebrew = {
    taps = [
      { name = "ruaylabs/tap"; trusted = true; }
    ];
    brews = [
      "llama.cpp"
      "odin"
      "ols"
    ];
    casks = [
      "helium-browser"
      "krisp"
      "spotify"
      "alfred"
      "1password@7"
      "obs"
      "telegram"
      "firefox"
      "vlc"
      "utm"
      "brave-browser"
      "protonvpn"
      "signal"
      "freecad"
      "orcaslicer"
      "blender@lts"
      "imageoptim"
      "discord"
      "tigervnc"
      "calibre"
      "loopback"
      "keepassxc"
      { "name" = "guanchzhou/tap/threemf"; trusted = true; }
      "ruaylabs/tap/melocoton"
      "ruaylabs/tap/hitsu"
      "ruaylabs/tap/miru"
      "yubico-authenticator"
      "steam"
    ];
    masApps = {
      "Keynote" = 409183694;
      "Numbers" = 409203825;
      "Pages" = 409201541;
      "Pixelmator Pro" = 1289583905;
      "Things 3" = 904280696;
      "Xcode" = 497799835;
    };
  };

  # all available options are defined here https://daiderd.com/nix-darwin/manual/index.html
  system = {
    defaults = {
      dock.persistent-apps = [
        "/System/Applications/Mail.app"
        "/System/Cryptexes/App/System/Applications/Safari.app"
        "/System/Applications/Messages.app"
        "/System/Applications/Notes.app"
        "/Applications/MacVim.app"
        "${pkgs.emacs}/Applications/Emacs.app"
      ];
    };
  };
}

README.md

nix-darwin/README.md · 22 lines

1. Install upstream `nix` from `DeterminateSystems`:

```shell
curl -fsSL https://install.determinate.systems/nix | sh -s -- install
```

2. Make build build, inside `nix-darwin` folder:

> Make sure terminal.app have permissions to access full disk, this is required because we need to modify some system
> preferences

```shell
nix --extra-experimental-features 'nix-command flakes' build ".#darwinConfigurations.simple.system"
```

3. Install build

```shell
sudo ./result/sw/bin/darwin-rebuild switch --flake ".#simple"
```

4. After this we can rebuild system by using this helper function `nixdarwnin_run_install`

work.nix

nix-darwin/work.nix · 41 lines

{ pkgs, config, ... }: {
  environment.systemPackages = with pkgs; [
    lima
    mutagen
    (azure-cli.withExtensions [ azure-cli.extensions.aks-preview ])
    google-cloud-sql-proxy
  ];

  homebrew = {
    taps = [
      "Azure/kubelogin"
      { name = "abue-ammar/tinycast"; trusted = true; }
    ];
    brews = [
      "Azure/kubelogin/kubelogin"
    ];
    casks = [
      "1password"
      "brave-browser"
      "claude"
      "slack"
      "notion"
      "1password-cli"
      "maccy"
      "tinycast"
    ];
  };

  # all available options are defined here https://daiderd.com/nix-darwin/manual/index.html
  system = {
    defaults = {
      dock.persistent-apps = [
        "/System/Applications/Mail.app"
        "/Applications/Brave Browser.app/"
        "/System/Applications/Notes.app"
        "/Applications/MacVim.app"
        "${pkgs.emacs}/Applications/Emacs.app"
      ];
    };
  };
}

NixOS

configuration.nix

nixos/configuration.nix · 445 lines

# Edit this configuration file to define what should be installed on # your system.  Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).

{ config, pkgs, ... }:

{
  imports =
    [
      # Include the results of the hardware scan.
      /etc/nixos/hardware-configuration.nix
    ];

  nix.gc = {
    # Enable the automatic garbage collector
    automatic = true;
    # Remove generations older than two weeks once a week.
    dates = "weekly";
    options = "--delete-older-than 14d";
  };

  nix.settings.experimental-features = [ "nix-command" "flakes" ];

  # Bootloader.
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
  # enable sound card
  boot.kernelParams = [ "snd-intel-dspcfg.dsp_driver=1" ];
  # clean tmp dir on restart
  boot.tmp.cleanOnBoot = true;

  networking.hostName = "nixos"; # Define your hostname.
  # networking.wireless.enable = true;  # Enables wireless support via wpa_supplicant.

  # Configure network proxy if necessary
  # networking.proxy.default = "http://user:password@proxy:port/";
  # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";

  # Enable networking
  networking.networkmanager.enable = true;

  # Enable Bluetooth and the Blueman applet service.
  hardware.bluetooth.enable = true;
  services.blueman.enable = true;

  # Set your time zone.
  time.timeZone = "America/Mexico_City";

  # Select internationalisation properties.
  i18n.defaultLocale = "en_US.UTF-8";

  # Configure keymap in X11
  services.xserver = {
    xkb = {
      variant = "mac";
      layout = "us";
      # Use Left Alt for macOS-style accent sequences.
      options = "lv3:lalt_switch";
    };
  };

  # Remap caps lock to esc
  services.keyd = {
    enable = true;
    keyboards = {
      default = {
        ids = [ "*" ];
        settings = {
          main = {
            capslock = "esc";
            # Swap Meta and Alt for laptop keyboard layouts. This is not
            # required on desktop computers with a custom keyboard.
            # leftmeta = "layer(alt)";
            # leftalt = "layer(meta)";
          };
        };
      };
    };
  };

  services.displayManager.sddm.wayland.enable = true;
  services.displayManager.sddm.enable = true;
  services.displayManager.sddm.package = pkgs.kdePackages.sddm;
  services.displayManager.sddm.theme = "sddm-astronaut-theme";
  services.displayManager.sddm.extraPackages = with pkgs.kdePackages; [
    qtmultimedia
  ];

  # enable keyring on unlock
  security.pam.services.sddm.enableGnomeKeyring = true;
  security.pam.services."swaylock-plugin" = { };
  security.pam.services.wayvnc = { };
  # Give PipeWire realtime priority for low-latency, stable audio.
  security.rtkit.enable = true;

  services.pulseaudio.enable = false;
  services.pipewire = {
    enable = true;
    alsa.enable = true;
    alsa.support32Bit = true;
    pulse.enable = true;
    # Manage PipeWire devices, routing, and audio policy.
    wireplumber.enable = true;
  };

  # Define a user account. Don't forget to set a password with ‘passwd’.
  users.users.erick = {
    isNormalUser = true;
    description = "Erick Navarro";
    extraGroups = [ "networkmanager" "wheel" "docker" "libvirtd" ];
    packages = with pkgs; [ ];
  };

  programs.zsh.enable = true;
  programs.ssh.startAgent = true;

  services.espanso = {
    enable = true;
    package = pkgs.espanso-wayland;
  };
  services.gnome.gcr-ssh-agent.enable = false;

  users.users.erick.shell = pkgs.zsh;

  fonts.packages = with pkgs; [
    jetbrains-mono
    nerd-fonts.jetbrains-mono
    nerd-fonts.symbols-only
    noto-fonts-color-emoji
  ];

  # Allow unfree packages
  nixpkgs.config.allowUnfree = true;

  programs.sway = {
    enable = true;
    wrapperFeatures.gtk = true;
    extraPackages = with pkgs; [
      swayidle
      waybar
    ];
  };

  xdg.portal = {
    enable = true;
    wlr = {
      enable = true;
      # OBS requires an explicit output chooser for PipeWire screen capture.
      settings.screencast = {
        chooser_type = "simple";
        chooser_cmd = "${pkgs.slurp}/bin/slurp -f 'Monitor: %o' -or";
      };
    };
    extraPortals = [ pkgs.xdg-desktop-portal-wlr ];
  };

  services.dbus.enable = true;

  # Let Gammastep determine sunrise and sunset from the current location.
  services.geoclue2 = {
    enable = true;
    appConfig.gammastep = {
      isAllowed = true;
      isSystem = false;
    };
  };

  # Enable Flatpak applications and configure Flathub system-wide.
  services.flatpak.enable = true;
  systemd.services.flatpak-setup = {
    description = "Configure the Flathub Flatpak remote";
    wantedBy = [ "multi-user.target" ];
    after = [ "network-online.target" ];
    wants = [ "network-online.target" ];
    path = [ pkgs.flatpak ];
    serviceConfig.Type = "oneshot";
    script = ''
      flatpak remote-add --system --if-not-exists \
        flathub https://flathub.org/repo/flathub.flatpakrepo
    '';
  };

  # Enable the gnome-keyring secrets vault.
  # Will be exposed through DBus to programs willing to store secrets.
  services.gnome.gnome-keyring.enable = true;

  programs.dconf = {
    enable = true;
    profiles.user.databases = [
      {
        settings."org/gnome/desktop/interface" = {
          color-scheme = "prefer-dark";
          gtk-theme = "adw-gtk3-dark";
          icon-theme = "Papirus-Dark";
          font-name = "Sans 10";
        };
      }
    ];
  };

  virtualisation.docker.enable = true;
  virtualisation.libvirtd.enable = true;
  programs.virt-manager.enable = true;

  # Keep the clipboard picker resident so hotkey activation only presents its existing process.
  systemd.user.services.clipboard-picker = {
    description = "Resident GTK clipboard picker";
    wantedBy = [ "graphical-session.target" ];
    after = [ "graphical-session.target" ];
    partOf = [ "graphical-session.target" ];
    serviceConfig = {
      ExecStart = "%h/dotfiles/clipboard-picker/result/bin/clipboard-picker --gapplication-service";
      Restart = "on-failure";
      RestartSec = 2;
    };
  };

  # Keep Handy ready for Sway's on-demand transcription shortcut.
  systemd.user.services.handy = {
    description = "Handy offline speech-to-text";
    after = [ "graphical-session.target" ];
    partOf = [ "graphical-session.target" ];
    path = [ pkgs.wtype ];
    serviceConfig = {
      ExecStart = "${pkgs.handy}/bin/handy --start-hidden";
      Restart = "on-failure";
      RestartSec = 2;
    };
  };

  # Rotate wallpapers while the graphical session is active.
  systemd.user.services.wallpaper-rotate = {
    description = "Select the next desktop wallpaper";
    after = [ "graphical-session.target" ];
    path = with pkgs; [
      coreutils
      file
      findutils
      procps
      sway
      systemd
    ];
    serviceConfig = {
      Type = "oneshot";
      ExecStart = "${pkgs.bash}/bin/bash %h/.config/sway/scripts/wallpaper.sh";
    };
  };

  systemd.user.timers.wallpaper-rotate = {
    description = "Rotate the desktop wallpaper every 30 minutes";
    wantedBy = [ "graphical-session.target" ];
    partOf = [ "graphical-session.target" ];
    timerConfig = {
      OnActiveSec = "1s";
      OnUnitActiveSec = "30m";
      Unit = "wallpaper-rotate.service";
    };
  };

  # Keep video wallpaper playback under systemd supervision.
  systemd.user.services.wallpaper-video = {
    description = "Video desktop wallpaper";
    after = [ "graphical-session.target" ];
    partOf = [ "graphical-session.target" ];
    serviceConfig = {
      ExecStart = ''${pkgs.mpvpaper}/bin/mpvpaper -o "hwdec=vaapi no-audio loop" "*" %h/.local/state/wallpaper/current-video'';
      Restart = "on-failure";
      RestartSec = 2;
    };
  };

  environment.etc."wayvnc/config".text = ''
    enable_auth=true
    enable_pam=true
  '';

  # Waybar starts and stops this service on demand.
  systemd.user.services.wayvnc = {
    description = "WayVNC remote desktop server";
    after = [ "graphical-session.target" ];
    partOf = [ "graphical-session.target" ];
    environment.WAYLAND_DISPLAY = "wayland-1";
    serviceConfig = {
      # Port 5900 is reserved by libvirt/QEMU's local VNC display.
      ExecStart = "${pkgs.wayvnc}/bin/wayvnc -C /etc/wayvnc/config -Ldebug 0.0.0.0 5901";
      Restart = "on-failure";
      RestartSec = 2;
    };
  };

  environment.systemPackages = with pkgs; [
    pamixer
    i3lock-fancy
    polybar
    # available theme config file are inside
    # /run/current-system/sw/share/sddm/themes/sddm-astronaut-theme/Themes
    (sddm-astronaut.overrideAttrs (oldAttrs: {
      postInstall = (oldAttrs.postInstall or "") + ''
        substituteInPlace $out/share/sddm/themes/sddm-astronaut-theme/metadata.desktop \
          --replace "ConfigFile=Themes/astronaut.conf" "ConfigFile=Themes/pixel_sakura.conf"
      '';
    }))
    jq
    file # detect image and video wallpaper types
    ffmpeg # re-encode videos
    bc # required for eww/net.sh calculations
    nixpkgs-fmt
    kdePackages.qtdeclarative # qmlformat for Quickshell QML files
    wget
    vim
    rofi
    rofimoji
    wayvnc # enable remote control
    pavucontrol
    killall
    ripgrep
    mutagen
    aspell
    aspellDicts.en
    aspellDicts.es
    hunspell
    hunspellDicts.en_US
    tree-sitter
    fzf
    shfmt
    shellcheck
    nautilus
    sushi # allow show previews in nautilus when pressing space
    tectonic
    wayland
    # Quickshell needs Qt Multimedia's QML module and plugins for video previews;
    # SDDM's extraPackages do not expose them to the Quickshell process.
    (symlinkJoin {
      name = "quickshell-with-multimedia";
      paths = [ quickshell ];
      nativeBuildInputs = [ makeWrapper ];
      postBuild = ''
        wrapProgram $out/bin/qs \
          --prefix QML_IMPORT_PATH : ${kdePackages.qtmultimedia}/lib/qt-6/qml \
          --prefix QML2_IMPORT_PATH : ${kdePackages.qtmultimedia}/lib/qt-6/qml \
          --prefix QT_PLUGIN_PATH : ${kdePackages.qtmultimedia}/lib/qt-6/plugins
      '';
    })
    wlogout
    wl-clipboard
    xdg-utils # open URLs with their default Linux application
    wtype # sends paste shortcuts for the Sway clipboard menu
    handy # offline speech-to-text input
    glib
    adw-gtk3
    papirus-icon-theme
    nwg-look
    networkmanagerapplet
    qrencode # generate scannable Wi-Fi credentials
    swaylock-effects
    swaylock-plugin
    eww
    mpvpaper # for live wallpapers
    nerdfetch
    fastfetch
    # image annotation tool
    satty
    woomer # screen magnification tool
    wlrctl # scroll and click from Sway's keyboard pointer mode
    playerctl # to know what song is being played
    swappy
    grim
    slurp
    cliphist
    telegram-desktop
    firefox
    brave
    alacritty
    tmux
    emacs31-gtk3
    localsend
    python3 # run the Nautilus LocalSend action
    mise
    bat
    eza
    unzip
    git
    delta
    gtk3
    zathura
    gnumake
    gcc
    cmake
    libtool
    libvterm
    clang-tools
    ruff
    pyright
    zig
    zls
    nixd
    biome
    brightnessctl
    gammastep # adjust screen color temperature based on sunrise and sunset
    libnotify
    swaynotificationcenter
    spotify
    tree
    btop
    htop
    obs-studio
    go
    gopls # LSP server
    gotools # includes goimports
    autotiling
  ];

  # Some programs need SUID wrappers, can be configured further or are
  # started in user sessions.
  # programs.mtr.enable = true;
  # programs.gnupg.agent = { #   enable = true; #   enableSSHSupport = true;
  # };

  # List services that you want to enable:

  # Enable the OpenSSH daemon.
  services.openssh.enable = true;

  # Enable tailscale
  services.tailscale.enable = true;

  networking.firewall = {
    enable = true;
    # Keep Tailscale exit nodes working with reverse-path filtering enabled.
    checkReversePath = "loose";
    # LocalSend uses TCP for transfers and UDP for device discovery.
    allowedTCPPorts = [ 53317 ];
    # Allow direct Tailscale peer connections and LocalSend discovery.
    allowedUDPPorts = [ config.services.tailscale.port 53317 ];
    # WayVNC listens globally but is reachable only through Tailscale.
    interfaces.${config.services.tailscale.interfaceName}.allowedTCPPorts = [ 5901 ];
  };

  # This value determines the NixOS release from which the default
  # settings for stateful data, like file locations and database versions
  # on your system were taken. It‘s perfectly fine and recommended to leave
  # this value at the release version of the first install of this system.
  # Before changing this value read the documentation for this option
  # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
  system.stateVersion = "24.05"; # Did you read the comment?
}

flake.nix

nixos/flake.nix · 14 lines

{
  description = "NixOS flake";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  };

  outputs = { self, nixpkgs }: {
    nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [ ./configuration.nix ];
    };
  };
}

Pi

AGENTS.md

.pi/agent/AGENTS.md · 55 lines

## Aliases

When the user types an alias, interpret it as its full meaning. The agent only generates
the commit message text — it never runs `git commit`:

- `gmcm` → "give me a commit message for the current change based on our conversation context (text only, do not run `git commit`; only inspect the repo if we haven't already discussed the changes)"
- `cad` → "check your previous response against the official sources for the current context (docs, library repos, etc.); correct anything wrong or outdated; cite the sources"

## Interactions

- If the user asks a question, do not apply any changes — just answer.
- Always minimize diffs; avoid unnecessary edits, reformatting, or unrelated changes.
- Keep reports, including plans, bug lists, and feature lists, to a maximum line width of 100
  characters.
- Before editing files, check for an applicable `.editorconfig` and follow its guidelines.
- For existing files, check whether they end with blank lines and preserve their end-of-file
  convention to avoid unnecessary diffs. Always end new files with a newline.
- After every batch of edits, show a colorized diff-stat before any explanation:
  - Use a fenced `diff` block so additions render green and deletions render red.
  - For each changed file, show a `+` line for additions and a `-` line for deletions, omitting
    either line when its count is zero.
  - Use the format: marker, relative path, pipe, count, and a visual bar proportional to the changed
    lines (roughly 1 bar character per changed line).
  - Include new files and finish with the total files changed, insertions, and deletions.
  - Example:
    ```diff
    + src/lib/foo.ts     | 7 +++++++
    - src/lib/foo.ts     | 5 -----
    + src/lib/bar.svelte | 36 ++++++++++++++++++++++++++++++++++++
    - src/lib/baz.svelte | 12 ------------
    3 files changed, 43 insertions(+), 17 deletions(-)
    ```
- Conventional commits with scope, unless the project defines its own convention.
  Add a body only when the summary alone is unclear.

## Session patterns that worked well

- **Work in improvement lists.** After a feature is done, propose a numbered list of follow-up
  improvements. Let the user pick which to implement and in what order.
- **Don't abstract prematurely.** Only extract shared code/styles when there are 3+ consumers. If
  there's just one, leave it in place — it can always be extracted later.
- **Prefer the standard library.** Prioritize standard-library functionality over third-party
  libraries whenever it meets the requirements.
- **Always verify after changes.** Run the project's type-check/lint command after every edit batch
  without waiting to be asked.
- **Keep commits atomic.** One change = one commit. If the user asks for a commit
  message, keep the summary short.
- **Revert fast.** If an extraction turns out to be wrong ("this is only used in one component"),
revert it immediately rather than defending it.

## Language / framework conventions

Language-specific conventions live in skill files under `~/.pi/agent/skills/`.
A `lang-directives` extension auto-detects relevant skills from the prompt and
reminds you to load them.

alias-expand.ts

.pi/agent/extensions/alias-expand.ts · 37 lines

/** Expands single-word aliases from "## Aliases" sections in AGENTS.md files. */

import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

function aliasesFrom(content: string): Record<string, string> {
  const aliases: Record<string, string> = {};
  const section = content.split(/^##\s+Aliases\s*$/im)[1];
  if (!section) return aliases;
  for (const [, alias, expansion] of section
    .split(/^##\s/m)[0]
    .matchAll(/^-\s+`([^`\n]+)`\s*→\s*"(.+)"\s*$/gm))
    aliases[alias.trim()] = expansion.trim();
  return aliases;
}

const read = (path: string) => {
  try {
    return readFileSync(path, "utf-8");
  } catch {
    return "";
  }
};

export default function (pi: ExtensionAPI) {
  pi.on("input", async (event, ctx) => {
    const text = event.text.trim();
    if (event.source !== "interactive" || /\s/.test(text)) return;
    const aliases = {
      ...aliasesFrom(read(join(homedir(), ".pi", "agent", "AGENTS.md"))),
      ...aliasesFrom(read(join(ctx.cwd, "AGENTS.md"))),
    };
    if (aliases[text]) return { action: "transform", text: aliases[text] };
  });
}

lang-directives.ts

.pi/agent/extensions/lang-directives.ts · 162 lines

/**
 * Lang Directives Extension
 *
 * Reads lang-directives.json which maps path and word triggers to skill names.
 * At before_agent_start, scans the user's prompt for matching triggers and
 * reminds the model to read the relevant skill files.
 *
 * The skill files and their paths come from pi's own skill loader
 * (event.systemPromptOptions.skills); this extension only owns the
 * trigger->name mapping. Keep directive content in skill files under
 * ~/.pi/agent/skills/<name>/SKILL.md, not here.
 */

import {
  CONFIG_DIR_NAME,
  type ExtensionAPI,
} from "@earendil-works/pi-coding-agent";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";

interface SkillEntry {
  name: string;
  paths: string[];
  words: string[];
}

interface Config {
  skills: SkillEntry[];
}

const HOME = homedir();
const GLOBAL_CONFIG = join(HOME, ".pi/agent/lang-directives.json");

/** Path triggers match literal substrings; word triggers require word boundaries. */
type MatchMode = "path" | "word";
type Matcher = (promptLower: string) => boolean;

function buildMatcher(value: string, mode: MatchMode): Matcher {
  const key = value.toLowerCase();
  if (mode === "path") return (prompt) => prompt.includes(key);

  const re = new RegExp(
    `(?<![\\p{L}\\p{N}_])${escapeRegex(key)}(?![\\p{L}\\p{N}_])`,
    "u",
  );
  return (prompt) => re.test(prompt);
}

function escapeRegex(s: string): string {
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

class DirectiveIndex {
  /** keyword -> matcher */
  private matchers = new Map<string, Matcher>();
  /** keyword -> skill names */
  private names = new Map<string, Set<string>>();

  isEmpty(): boolean {
    return this.matchers.size === 0;
  }

  /** Rebuild from one or more configs (later configs add to, not replace). */
  rebuild(configs: Config[]): void {
    this.matchers.clear();
    this.names.clear();
    for (const config of configs) {
      for (const skill of config.skills) {
        if (!skill.name) continue;
        const groups: Array<[MatchMode, string[]]> = [
          ["path", skill.paths],
          ["word", skill.words],
        ];
        for (const [mode, values] of groups) {
          if (!Array.isArray(values)) continue;
          for (const value of values) {
            if (!value) continue;
            const key = `${mode}:${value.toLowerCase()}`;
            if (!this.matchers.has(key)) {
              this.matchers.set(key, buildMatcher(value, mode));
            }
            const set = this.names.get(key) ?? new Set<string>();
            set.add(skill.name);
            this.names.set(key, set);
          }
        }
      }
    }
  }

  /** Return de-duplicated skill names whose keywords appear in the prompt. */
  matchedSkillNames(prompt: string): string[] {
    const lower = prompt.toLowerCase();
    const result = new Set<string>();
    for (const [kw, matcher] of this.matchers) {
      if (matcher(lower)) {
        for (const name of this.names.get(kw) ?? []) result.add(name);
      }
    }
    return [...result];
  }
}

function loadConfig(filePath: string): Config | null {
  try {
    if (!existsSync(filePath)) return null;
    const config = JSON.parse(readFileSync(filePath, "utf-8")) as Config;
    return config.skills?.length ? config : null;
  } catch {
    return null;
  }
}

export default function (pi: ExtensionAPI) {
  const index = new DirectiveIndex();
  let lastCwd = "";

  function rebuild(cwd: string) {
    if (cwd === lastCwd && !index.isEmpty()) return; // cheap no-op re-entry
    const configs: Config[] = [];
    const global = loadConfig(GLOBAL_CONFIG);
    if (global) configs.push(global);
    const project = loadConfig(
      join(cwd, CONFIG_DIR_NAME, "lang-directives.json"),
    );
    if (project) configs.push(project);
    index.rebuild(configs);
    lastCwd = cwd;
  }

  pi.on("session_start", async (_event, ctx) => rebuild(ctx.cwd));
  pi.on("resources_discover", async (_event, ctx) => rebuild(ctx.cwd));

  pi.on("before_agent_start", async (event) => {
    if (index.isEmpty()) return;
    const wanted = index.matchedSkillNames(event.prompt);
    if (wanted.length === 0) return;

    // Resolve against pi's already-loaded skills so paths/discovery stay in
    // sync with the rest of the system. Skills flagged
    // disableModelInvocation are hidden from the model by pi itself; skip
    // them here too so we never remind the model of an un-invokable skill.
    const loaded = event.systemPromptOptions.skills ?? [];
    const byName = new Map(loaded.map((s) => [s.name, s]));
    const out = new Set<string>();
    for (const name of wanted) {
      const skill = byName.get(name);
      if (skill && !skill.disableModelInvocation) {
        out.add(`- \`${skill.filePath}\` — \`read\` for ${name} conventions`);
      }
    }
    if (out.size === 0) return;

    const reminder =
      `\n\n## Relevant conventions\n` +
      `Read these before writing or reviewing code:\n` +
      [...out].join("\n");

    return { systemPrompt: event.systemPrompt + reminder };
  });
}

prompt-prefix.ts

.pi/agent/extensions/prompt-prefix.ts · 36 lines

/**
 * Prompt Prefix Extension - Shows ❯ before text in the editor, like a shell prompt.
 *
 * Place in ~/.pi/agent/extensions/ for auto-discovery.
 */

import {
  CustomEditor,
  type ExtensionAPI,
} from "@earendil-works/pi-coding-agent";
import { truncateToWidth } from "@earendil-works/pi-tui";

class PromptEditor extends CustomEditor {
  render(width: number): string[] {
    const lines = super.render(width);
    if (lines.length < 3) return lines;

    // Only show ❯ when the actual first line of text is visible (not scrolled)
    if (this.scrollOffset > 0) return lines;

    // First content line is at index 1 (after the top border)
    const prompt = this.borderColor("❯ ");
    // Consume 2 columns from the right (padding) so total width stays within limits
    lines[1] = prompt + truncateToWidth(lines[1]!, Math.max(0, width - 2), "");

    return lines;
  }
}

export default function (pi: ExtensionAPI) {
  pi.on("session_start", (_event, ctx) => {
    ctx.ui.setEditorComponent(
      (tui, theme, kb) => new PromptEditor(tui, theme, kb),
    );
  });
}

lang-directives.json

.pi/agent/lang-directives.json · 24 lines

{
  "skills": [
    {
      "name": "rust",
      "paths": [
        ".rs",
        "Cargo.toml",
        "Cargo.lock",
        "build.rs",
        "rust-toolchain.toml",
        "rustfmt.toml",
        "clippy.toml",
        ".cargo/"
      ],
      "words": [
        "rust",
        "rustc",
        "rustup",
        "rust-analyzer",
        "cargo"
      ]
    }
  ]
}

SKILL.md

.pi/agent/skills/rust/SKILL.md · 13 lines

---
name: rust
description: General Rust conventions and best practices. Use when writing or reviewing Rust code.
---

# Rust Conventions

- Run `cargo fmt --check`, `cargo check`, and `cargo clippy` after edits.
- Run `cargo test` after logic changes.
- Prefer clear ownership and borrowing over unnecessary cloning.
- Use `Result` and `?` for recoverable errors.
- Avoid `unwrap()` and `expect()` outside tests or proven invariants.
- Use `anyhow` for application errors and `thiserror` for typed library errors.

tomorrow.json

.pi/agent/themes/tomorrow.json · 78 lines

{
  "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
  "name": "tomorrow",
  "vars": {
    "bg": "#000000",
    "currentLine": "#2a2a2a",
    "selection": "#424242",
    "fg": "#d6d6d6",
    "comment": "#969896",
    "red": "#d54e53",
    "orange": "#e78c45",
    "yellow": "#e7c547",
    "green": "#b9ca4a",
    "aqua": "#70c0b1",
    "blue": "#7aa6da",
    "purple": "#c397d8"
  },
  "colors": {
    "accent": "blue",
    "border": "#4a4a4a",
    "borderAccent": "blue",
    "borderMuted": "#424242",
    "success": "green",
    "error": "red",
    "warning": "orange",
    "muted": "comment",
    "dim": "#707070",
    "text": "fg",
    "thinkingText": "comment",
    "selectedBg": "selection",
    "scrollbarThumb": "#4a4a4a",
    "userMessageBg": "currentLine",
    "userMessageText": "fg",
    "customMessageBg": "currentLine",
    "customMessageText": "fg",
    "customMessageLabel": "blue",
    "toolPendingBg": "#1a1a1a",
    "toolSuccessBg": "#1a2a1a",
    "toolErrorBg": "#2a1a1a",
    "toolTitle": "fg",
    "toolOutput": "fg",
    "mdHeading": "purple",
    "mdLink": "blue",
    "mdLinkUrl": "comment",
    "mdCode": "orange",
    "mdCodeBlock": "fg",
    "mdCodeBlockBorder": "#4a4a4a",
    "mdQuote": "comment",
    "mdQuoteBorder": "#424242",
    "mdHr": "#424242",
    "mdListBullet": "aqua",
    "toolDiffAdded": "green",
    "toolDiffRemoved": "red",
    "toolDiffContext": "fg",
    "syntaxComment": "comment",
    "syntaxKeyword": "purple",
    "syntaxFunction": "blue",
    "syntaxVariable": "red",
    "syntaxString": "green",
    "syntaxNumber": "orange",
    "syntaxType": "yellow",
    "syntaxOperator": "fg",
    "syntaxPunctuation": "comment",
    "thinkingOff": "#4a4a4a",
    "thinkingMinimal": "blue",
    "thinkingLow": "purple",
    "thinkingMedium": "aqua",
    "thinkingHigh": "yellow",
    "thinkingXhigh": "red",
    "thinkingMax": "red",
    "bashMode": "yellow"
  },
  "export": {
    "pageBg": "#000000",
    "cardBg": "#2a2a2a",
    "infoBg": "#424242"
  }
}

Placements

common.txt

placements/common.txt · 24 lines

.alacritty.toml ~/.alacritty.toml
.emacs.d/bootstrap.org ~/.config/emacs/bootstrap.org
.emacs.d/early-init.el ~/.config/emacs/early-init.el
.emacs.d/init.el ~/.config/emacs/init.el
.emacs.d/snippets ~/.config/emacs/snippets
.emacs.d/elpaca-lockfile.eld ~/.config/emacs/elpaca-lockfile.eld
.git_commit_message.txt ~/.git_commit_message.txt
.gitconfig ~/.gitconfig
.tmux.conf ~/.tmux.conf
.vimrc ~/.vimrc
.ideavimrc ~/.ideavimrc
global.gitignore ~/.gitignore
zsh/zshenv.sh ~/.zshenv
zsh/zshrc.sh ~/.zshrc
zsh/main.sh ~/.config/zsh/main.sh
ghostty/config ~/.config/ghostty/config
jj/config.toml ~/.config/jj/config.toml
.pi/agent/extensions/prompt-prefix.ts ~/.pi/agent/extensions/prompt-prefix.ts
.pi/agent/themes/tomorrow.json ~/.pi/agent/themes/tomorrow.json
.pi/agent/AGENTS.md ~/.pi/agent/AGENTS.md
.pi/agent/extensions/lang-directives.ts ~/.pi/agent/extensions/lang-directives.ts
.pi/agent/extensions/alias-expand.ts ~/.pi/agent/extensions/alias-expand.ts
.pi/agent/lang-directives.json ~/.pi/agent/lang-directives.json
.pi/agent/skills ~/.pi/agent/skills

linux.txt

placements/linux.txt · 41 lines

espanso ~/.config/espanso
zsh/linux.sh ~/.config/zsh/linux.sh
.Xmodmap ~/.Xmodmap
polybar/config.ini ~/.config/polybar/config.ini
polybar/launch.sh ~/.config/polybar/launch.sh
i3/config ~/.config/i3/config
sway/config ~/.config/sway/config
sway/scripts/wallpaper.sh ~/.config/sway/scripts/wallpaper.sh
sway/scripts/pick-wallpaper.sh ~/.config/sway/scripts/pick-wallpaper.sh
sway/scripts/wifi-qr.sh ~/.config/sway/scripts/wifi-qr.sh
sway/scripts/lockscreen.sh ~/.config/sway/scripts/lockscreen.sh
sway/scripts/quick-actions.sh ~/.config/sway/scripts/quick-actions.sh
sway/scripts/screenshot.sh ~/.config/sway/scripts/screenshot.sh
sway/scripts/clipboard-menu.sh ~/.config/sway/scripts/clipboard-menu.sh
sway/scripts/wlogout-menu.sh ~/.config/sway/scripts/wlogout-menu.sh
waybar/config.jsonc ~/.config/waybar/config.jsonc
waybar/styles.css ~/.config/waybar/styles.css
quickshell ~/.config/quickshell
waybar/scripts/gpu.sh ~/.config/waybar/scripts/gpu.sh
waybar/scripts/vpn.sh ~/.config/waybar/scripts/vpn.sh
waybar/scripts/night-light.sh ~/.config/waybar/scripts/night-light.sh
waybar/scripts/vnc-session.sh ~/.config/waybar/scripts/vnc-session.sh
waybar/scripts/vnc-toggle.sh ~/.config/waybar/scripts/vnc-toggle.sh
eww/eww.yuck ~/.config/eww/eww.yuck
eww/eww.scss ~/.config/eww/eww.scss
eww/scripts/weather.sh ~/.config/eww/scripts/weather.sh
eww/scripts/net.sh ~/.config/eww/scripts/net.sh
eww/scripts/ram.sh ~/.config/eww/scripts/ram.sh
eww/scripts/cpu.sh ~/.config/eww/scripts/cpu.sh
eww/scripts/gpu.sh ~/.config/eww/scripts/gpu.sh
eww/scripts/disk.sh ~/.config/eww/scripts/disk.sh
eww/scripts/spotify.sh ~/.config/eww/scripts/spotify.sh
wlogout/layout ~/.config/wlogout/layout
wlogout/style.css ~/.config/wlogout/style.css
swaync/config.json ~/.config/swaync/config.json
swaync/style.css ~/.config/swaync/style.css
nautilus/scripts/send-to-localsend.py ~/.local/share/nautilus/scripts/Send-to-LocalSend
rofi/config.rasi ~/.config/rofi/config.rasi
swappy/config ~/.config/swappy/config
gtk/gtk-3.0/settings.ini ~/.config/gtk-3.0/settings.ini
gtk/gtk-4.0/settings.ini ~/.config/gtk-4.0/settings.ini

macos.txt

placements/macos.txt · 5 lines

espanso ~/Library/Application Support/espanso
zsh/mac.sh ~/.config/zsh/mac.sh
hammerspoon/init.lua ~/.hammerspoon/init.lua
hammerspoon/windows.lua ~/.hammerspoon/windows.lua
.aerospace.toml ~/.aerospace.toml

Polybar

config.ini

polybar/config.ini · 116 lines

[colors]
background = #282A2E
background-alt = #373B41
foreground = #C5C8C6
primary = #F0C674
secondary = #8ABEB7
alert = #A54242
disabled = #707880

[bar/home]
width = 100%
height = 24pt
radius = 3
background = ${colors.background}
foreground = ${colors.foreground}
line-size = 5pt
border-size = 5px
border-color = #00000000
padding-left = 0
padding-right = 1
module-margin = 1
separator = |
separator-foreground = ${colors.disabled}
font-0 = monospace;2
modules-left = xworkspaces xwindow
modules-right = filesystem pulseaudio memory cpu wlan eth battery date
cursor-click = pointer
cursor-scroll = ns-resize
enable-ipc = true

[module/xworkspaces]
type = internal/xworkspaces
label-active = %name%
label-active-background = ${colors.background-alt}
label-active-underline= ${colors.primary}
label-active-padding = 1
label-occupied = %name%
label-occupied-padding = 1
label-urgent = %name%
label-urgent-background = ${colors.alert}
label-urgent-padding = 1
label-empty = %name%
label-empty-foreground = ${colors.disabled}
label-empty-padding = 1

[module/xwindow]
type = internal/xwindow
label = %title:0:60:...%

[module/filesystem]
type = internal/fs
interval = 25
mount-0 = /
label-mounted = %{F#F0C674}%mountpoint%%{F-} %percentage_used%%
label-unmounted = %mountpoint% not mounted
label-unmounted-foreground = ${colors.disabled}

[module/pulseaudio]
type = internal/pulseaudio
format-volume-prefix = "VOL "
format-volume-prefix-foreground = ${colors.primary}
format-volume = <label-volume>
label-volume = %percentage%%
label-muted = muted
label-muted-foreground = ${colors.disabled}

[module/memory]
type = internal/memory
interval = 2
format-prefix = "RAM "
format-prefix-foreground = ${colors.primary}
label = %percentage_used:2%%

[module/cpu]
type = internal/cpu
interval = 2
format-prefix = "CPU "
format-prefix-foreground = ${colors.primary}
label = %percentage:2%%

[network-base]
type = internal/network
interval = 5
format-connected = <label-connected>
format-disconnected = <label-disconnected>
label-disconnected = %{F#F0C674}%ifname%%{F#707880} disconnected

[module/wlan]
inherit = network-base
interface-type = wireless
label-connected = %{F#F0C674}%ifname%%{F-} %essid% %local_ip%

[module/eth]
inherit = network-base
interface-type = wired
label-connected = %{F#F0C674}%ifname%%{F-} %local_ip%

[module/date]
type = internal/date
interval = 1
date= %Y-%m-%d %H:%M:%S
label = %date%
label-foreground = ${colors.primary}

[module/battery]
type = internal/battery
# in case for some reason battery never gets 100%
full-at = 99
low-at = 10
battery = BAT10
adapter = ADP1
poll-interval = 5

[settings]
screenchange-reload = true
pseudo-transparency = true

launch.sh

polybar/launch.sh · 5 lines

#!/usr/bin/env bash
set -euo pipefail

killall -q polybar
polybar -c ~/.config/polybar/config.ini

Quickshell

apply.sh

quickshell/wallpaper-picker/apply.sh · 34 lines

#!/usr/bin/env bash
set -euo pipefail

selected=${1:?Usage: apply.sh WALLPAPER}
[[ -f "$selected" ]] || exit 1

STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/wallpaper"
VIDEO_WALLPAPER_LINK="$STATE_DIR/current-video"
mkdir -p "$STATE_DIR"
printf '%s\n' "$selected" >"$STATE_DIR/last-wallpaper"
mime_type=$(file --brief --mime-type -- "$selected")

case "$mime_type" in
image/*)
  systemctl --user stop wallpaper-video.service
  swaymsg output '*' bg "$selected" fill
  ;;
video/*)
  mapfile -t swaybg_pids < <(pgrep -f '[s]waybg' || true)
  if ((${#swaybg_pids[@]})); then
    kill "${swaybg_pids[@]}" 2>/dev/null || true
    for _ in {1..10}; do
      swaybg_running=false
      for pid in "${swaybg_pids[@]}"; do
        kill -0 "$pid" 2>/dev/null && swaybg_running=true
      done
      "$swaybg_running" || break
      sleep 0.05
    done
  fi
  ln -sfn -- "$selected" "$VIDEO_WALLPAPER_LINK"
  systemctl --user restart wallpaper-video.service
  ;;
esac

scan.py

quickshell/wallpaper-picker/scan.py · 169 lines

#!/usr/bin/env python3
"""Stream wallpaper entries and cached thumbnails to Quickshell."""

import hashlib
import json
import os
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

IMAGES = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff", ".avif"}
VIDEOS = {".mp4", ".mkv", ".webm", ".mov", ".avi"}


def emit(entry):
    print(json.dumps(entry), flush=True)


def small_thumbnail_path(path, cache):
    key = hashlib.sha256(
        (str(path) + str(path.stat().st_mtime_ns) + ":small-v1").encode()
    )
    return cache / (key.hexdigest() + ".png")


def thumbnail(path, target, video, source):
    if not target.is_file():
        temporary = target.with_suffix(".tmp.png")
        command = ["ffmpeg", "-y", "-hide_banner", "-loglevel", "error"]
        if video and source is None:
            command += ["-ss", "1"]
        command += [
            "-i",
            str(source or path),
            "-vf",
            "scale=200:120:force_original_aspect_ratio=decrease",
            "-frames:v",
            "1",
            str(temporary),
        ]
        result = subprocess.run(
            command,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            check=False,
        )
        if result.returncode != 0:
            temporary.unlink(missing_ok=True)
            return None
        temporary.replace(target)
    return str(target)


def video_preview(path, target):
    temporary = target.with_suffix(".tmp.png")
    result = subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-hide_banner",
            "-loglevel",
            "error",
            "-ss",
            "1",
            "-i",
            str(path),
            "-frames:v",
            "1",
            str(temporary),
        ],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        check=False,
    )
    if result.returncode != 0:
        temporary.unlink(missing_ok=True)
        return None
    temporary.replace(target)
    return str(target)


def main(directory):
    cache = (
        Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
        / "wallpaper-picker"
    )
    pending = []
    previews = []
    try:
        entries = sorted(
            Path(directory).iterdir(), key=lambda path: path.name.casefold()
        )
    except OSError as error:
        emit({"error": str(error)})
        return

    for path in entries:
        if not path.is_file():
            continue
        path = path.resolve()
        suffix = path.suffix.lower()
        if suffix in IMAGES:
            cached = small_thumbnail_path(path, cache)
            ready = cached.is_file()
            emit(
                {
                    "path": str(path),
                    "thumbnail": str(cached) if ready else str(path),
                    "video": False,
                    "small": ready,
                    "preview": str(path),
                }
            )
            if not ready:
                pending.append((path, cached, False, None))
        elif suffix in VIDEOS:
            cached = small_thumbnail_path(path, cache)
            ready = cached.is_file()
            key = hashlib.sha256(
                (str(path) + ":full-resolution-v1").encode()
            ).hexdigest()
            old = cache / (key + ".png")
            old_ready = (
                old.is_file() and old.stat().st_mtime_ns >= path.stat().st_mtime_ns
            )
            emit(
                {
                    "path": str(path),
                    "thumbnail": str(cached)
                    if ready
                    else str(old)
                    if old_ready
                    else "",
                    "preview": str(old) if old_ready else str(cached) if ready else "",
                    "video": True,
                    "small": ready,
                }
            )
            if not ready:
                pending.append((path, cached, True, old if old_ready else None))
            if not old_ready:
                previews.append((path, old))

    if pending or previews:
        cache.mkdir(parents=True, exist_ok=True)
        with ThreadPoolExecutor(max_workers=2) as pool:
            futures = {
                pool.submit(thumbnail, path, target, video, source): (path, "thumbnail")
                for path, target, video, source in pending
            }
            futures.update(
                {
                    pool.submit(video_preview, path, target): (path, "preview")
                    for path, target in previews
                }
            )
            for future in as_completed(futures):
                result = future.result()
                if result:
                    path, role = futures[future]
                    update = {"path": str(path), role: result}
                    if role == "thumbnail":
                        update["small"] = True
                    emit(update)


if __name__ == "__main__":
    main(sys.argv[1])

shell.qml

quickshell/wallpaper-picker/shell.qml · 315 lines

import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtMultimedia

ShellRoot {
    id: shell

    property string directory: Quickshell.env("HOME") + "/Wallpapers"
    property string selectedPath: ""
    property bool selectedVideo: false
    property string selectedThumbnail: ""
    property string selectedPreview: ""
    property string scanError: ""

    function fileUrl(path) {
        return path ? "file://" + encodeURIComponent(path).replace(/%2F/gi, "/") : "";
    }

    function open() {
        if (panel.visible) {
            list.forceActiveFocus();
            return;
        }
        wallpapers.clear();
        shell.selectedPath = "";
        shell.selectedThumbnail = "";
        shell.selectedPreview = "";
        shell.scanError = "";
        scanner.running = true;
        panel.visible = true;
        list.forceActiveFocus();
    }

    function close() {
        panel.visible = false;
        videoDelay.stop();
        player.stop();
        player.source = "";
        scanner.running = false;
    }

    function select(index) {
        if (wallpapers.count === 0 || index < 0 || index >= wallpapers.count)
            return;
        list.currentIndex = index;
        const entry = wallpapers.get(index);
        shell.selectedPath = entry.path;
        shell.selectedVideo = entry.video;
        shell.selectedThumbnail = entry.thumbnail;
        shell.selectedPreview = entry.preview || entry.thumbnail;
        videoDelay.stop();
        if (player.source) {
            player.stop();
            player.source = "";
        }
        if (entry.video)
            videoDelay.start();
    }

    IpcHandler {
        target: "picker"
        function toggle(): void {
            if (panel.visible)
                shell.close();
            else
                shell.open();
        }
    }

    ListModel {
        id: wallpapers
    }

    Process {
        id: scanner
        command: ["python3", Qt.resolvedUrl("scan.py").toString().replace("file://", ""), shell.directory]
        stdout: SplitParser {
            onRead: function (line) {
                if (!panel.visible)
                    return;
                try {
                    const entry = JSON.parse(line);
                    if (entry.error) {
                        shell.scanError = entry.error;
                    } else if (entry.video !== undefined) {
                        wallpapers.append(entry);
                        if (wallpapers.count === 1)
                            shell.select(0);
                    } else {
                        for (let i = 0; i < wallpapers.count; i++) {
                            if (wallpapers.get(i).path === entry.path) {
                                if (entry.thumbnail !== undefined) {
                                    wallpapers.setProperty(i, "thumbnail", entry.thumbnail);
                                    wallpapers.setProperty(i, "small", entry.small);
                                    if (shell.selectedPath === entry.path)
                                        shell.selectedThumbnail = entry.thumbnail;
                                    if (!wallpapers.get(i).preview) {
                                        wallpapers.setProperty(i, "preview", entry.thumbnail);
                                        if (shell.selectedPath === entry.path)
                                            shell.selectedPreview = entry.thumbnail;
                                    }
                                }
                                if (entry.preview !== undefined) {
                                    wallpapers.setProperty(i, "preview", entry.preview);
                                    if (shell.selectedPath === entry.path)
                                        shell.selectedPreview = entry.preview;
                                }
                                break;
                            }
                        }
                    }
                } catch (error) {
                    console.warn("Wallpaper scan:", error);
                }
            }
        }
    }

    Process {
        id: apply
        onExited: function (code) {
            if (code !== 0)
                console.warn("Could not apply wallpaper (exit " + code + ")");
        }
    }

    Timer {
        id: videoDelay
        interval: 600
        onTriggered: {
            if (panel.visible && shell.selectedVideo) {
                player.source = shell.fileUrl(shell.selectedPath);
                player.play();
            }
        }
    }

    MediaPlayer {
        id: player
        videoOutput: videoOutput
        loops: MediaPlayer.Infinite
    }

    PanelWindow {
        id: panel
        anchors {
            top: true
            bottom: true
            left: true
            right: true
        }
        exclusiveZone: 0
        WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
        color: "transparent"
        visible: false
        onVisibleChanged: {
            if (visible)
                Qt.callLater(function () {
                    list.forceActiveFocus();
                });
        }

        Shortcut {
            sequence: "Ctrl+N"
            enabled: panel.visible
            onActivated: shell.select((list.currentIndex + 1) % wallpapers.count)
        }
        Shortcut {
            sequence: "Ctrl+P"
            enabled: panel.visible
            onActivated: shell.select((list.currentIndex - 1 + wallpapers.count) % wallpapers.count)
        }

        FocusScope {
            id: keyboard
            anchors.fill: parent
            focus: true
            Keys.onPressed: function (event) {
                if (event.key === Qt.Key_Escape)
                    shell.close();
                else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
                    if (shell.selectedPath && !apply.running) {
                        apply.command = ["bash", Qt.resolvedUrl("apply.sh").toString().replace("file://", ""), shell.selectedPath];
                        apply.running = true;
                        shell.close();
                    }
                } else if (event.key === Qt.Key_Down || event.key === Qt.Key_Right || (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_N)) {
                    shell.select((list.currentIndex + 1) % wallpapers.count);
                } else if (event.key === Qt.Key_Up || event.key === Qt.Key_Left || (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_P)) {
                    shell.select((list.currentIndex - 1 + wallpapers.count) % wallpapers.count);
                } else
                    return;
                event.accepted = true;
            }

            Rectangle {
                anchors.fill: parent
                color: "#99000000"
                MouseArea {
                    anchors.fill: parent
                    onClicked: shell.close()
                }
            }

            Rectangle {
                width: Math.min(1200, parent.width - 48)
                height: Math.min(760, parent.height - 48)
                anchors.centerIn: parent
                radius: 12
                color: "#f20f0f0f"
                border.color: "#1fffffff"

                ColumnLayout {
                    anchors.fill: parent
                    anchors.margins: 16
                    spacing: 12

                    RowLayout {
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                        spacing: 16

                        ListView {
                            id: list
                            focus: true
                            onCurrentIndexChanged: {
                                if (currentIndex >= 0 && currentIndex < wallpapers.count && shell.selectedPath !== wallpapers.get(currentIndex).path)
                                    shell.select(currentIndex);
                            }
                            Layout.preferredWidth: 120
                            Layout.fillHeight: true
                            clip: true
                            spacing: 12
                            model: wallpapers
                            highlightMoveDuration: 0
                            delegate: Rectangle {
                                required property string path
                                required property string thumbnail
                                required property bool small
                                required property int index
                                width: list.width
                                height: 90
                                radius: 6
                                color: index === list.currentIndex ? "#64727d" : "#26ffffff"

                                Image {
                                    anchors.fill: parent
                                    anchors.margins: 4
                                    source: shell.fileUrl(thumbnail)
                                    sourceSize.width: 200
                                    sourceSize.height: 120
                                    fillMode: Image.PreserveAspectFit
                                    asynchronous: !small
                                }
                                MouseArea {
                                    anchors.fill: parent
                                    onClicked: {
                                        shell.select(index);
                                        list.forceActiveFocus();
                                    }
                                    onDoubleClicked: {
                                        shell.select(index);
                                        if (!apply.running) {
                                            apply.command = ["bash", Qt.resolvedUrl("apply.sh").toString().replace("file://", ""), path];
                                            apply.running = true;
                                            shell.close();
                                        }
                                    }
                                }
                            }
                        }

                        Item {
                            Layout.fillWidth: true
                            Layout.fillHeight: true
                            Image {
                                anchors.fill: parent
                                source: shell.fileUrl(shell.selectedPreview)
                                sourceSize.width: 1200
                                sourceSize.height: 760
                                cache: false
                                fillMode: Image.PreserveAspectFit
                                asynchronous: true
                                visible: player.playbackState !== MediaPlayer.PlayingState
                            }
                            VideoOutput {
                                id: videoOutput
                                anchors.fill: parent
                                fillMode: VideoOutput.PreserveAspectFit
                                visible: player.playbackState === MediaPlayer.PlayingState
                            }
                            Label {
                                id: message
                                anchors.centerIn: parent
                                visible: !shell.selectedPath
                                text: shell.scanError || "No wallpapers found"
                                color: "#eeeeee"
                            }
                        }
                    }

                    Label {
                        Layout.alignment: Qt.AlignHCenter
                        text: "Arrow keys to navigate · Enter to select · Escape to cancel"
                        color: "#aaaaaa"
                    }
                }
            }
        }
    }
}

Rofi

config.rasi

rofi/config.rasi · 91 lines

configuration {
  modi: "drun,run,window";
  show-icons: true;
  display-drun: "Applications";
  display-run: "Run";
  display-window: "Windows";
}

* {
  background: rgba(15, 15, 15, 92%);
  surface: rgba(255, 255, 255, 6%);
  surface-hover: rgba(126, 186, 228, 12%);
  border-color: rgba(255, 255, 255, 12%);
  foreground: #eeeeee;
  muted: rgba(255, 255, 255, 55%);
  accent: #7ebae4;
  critical: #f53c3c;
  background-color: transparent;
  text-color: @foreground;
  font: "JetBrainsMono Nerd Font 11";
}

window {
  width: 600px;
  padding: 12px;
  border: 1px;
  border-color: @border-color;
  border-radius: 10px;
  background-color: @background;
}

mainbox {
  spacing: 10px;
}

inputbar {
  padding: 10px 12px;
  spacing: 10px;
  border-radius: 8px;
  background-color: @surface;
}

prompt {
  text-color: @accent;
}

entry {
  placeholder: "Search…";
  placeholder-color: @muted;
}

listview {
  lines: 8;
  columns: 1;
  spacing: 4px;
  scrollbar: false;
  fixed-height: false;
}

element {
  padding: 8px 10px;
  spacing: 10px;
  border-radius: 8px;
  background-color: transparent;
}

element normal normal,
element alternate normal {
  background-color: transparent;
  text-color: @foreground;
}

element selected normal {
  background-color: @surface-hover;
  text-color: @accent;
}

element urgent normal {
  text-color: @critical;
}

element-icon {
  size: 24px;
  background-color: transparent;
}

element-text {
  vertical-align: 0.5;
  background-color: transparent;
  text-color: inherit;
}

Scripts

build_mac.sh

scripts/build_mac.sh · 63 lines

#!/usr/bin/env bash
set -euo pipefail

CWD=$(pwd)

if [ -z "$1" ]; then
  echo "Please provide the directory where emacs source is located"
  exit 1
fi

EMACS_REPO_DIR=$1

if [ ! -d "$EMACS_REPO_DIR" ]; then
  echo "$EMACS_REPO_DIR is not a valid directory"
  exit 1
fi

cd "$EMACS_REPO_DIR" || exit

# clean past build data
git clean -fdx

make clean

echo "Making sure all requirements are installed..."

# harfbuzz adds support for ligatures, this is required by ligature.el package
brew install autoconf automake libxml2 gnutls cmake librsvg texinfo libgccjit

echo "Seting up variables..."

make configure
./configure --with-ns --with-xwidgets --with-tree-sitter
make -j install

echo "Compilation done!"

echo "Move compiled application into Applications folder?"
read -r answer

# we ask for confirmation so we can check compilation stdout before replacing a working version of emacs
if [ "$answer" != "${answer#[Yy]}" ]; then
  echo "Compiled application was moved into Applications folder"
  # Copy app in Applications folder
  cp -rf nextstep/Emacs.app /Applications/

  # Set custom icon that fits Bug Sur design
  # copied from https://github.com/mklement0/fileicon/blob/master/bin/fileicon
  osascript <<EOF
    use framework "Cocoa"

    set sourcePath to "$CWD/Emacs.icns"
    set destPath to "/Applications/Emacs.app"

    set imageData to (current application's NSImage's alloc()'s initWithContentsOfFile:sourcePath)
    (current application's NSWorkspace's sharedWorkspace()'s setIcon:imageData forFile:destPath options:2)
EOF
else
  echo "Compiled application wasn't moved"
  exit 1
fi

cd - || exit

config-site.css

scripts/config-site.css · 91 lines

:root { color-scheme: dark; --bg: #111318; --panel: #191c23; --sidebar: #15171d; --muted: #969dab; --text: #e6e9ef; --accent: #8aadf4; --border: #2c313c; }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.6 system-ui, sans-serif; }
a { color: inherit; text-decoration: none; }

/* Dotfiles index */
aside { position: fixed; inset: 0 auto 0 0; width: 270px; padding: 28px 20px; overflow: auto; border-right: 1px solid var(--border); background: var(--sidebar); }
aside h1 { margin: 0; font-size: 1.4rem; }
aside > p { margin: 4px 0 20px; color: var(--muted); font-size: .85rem; }
nav a { display: flex; justify-content: space-between; padding: 7px 9px; border-radius: 6px; color: var(--muted); }
nav a:hover { color: var(--text); background: var(--panel); }
nav b { color: var(--accent); font-size: .75rem; }
.nav-toggle { display: none; width: 100%; margin-bottom: 10px; padding: 8px 10px; color: var(--text); border: 1px solid var(--border); border-radius: 6px; background: var(--panel); text-align: left; cursor: pointer; }
main { max-width: 1100px; margin-left: 270px; padding: 50px 5vw 100px; }
.intro h1 { margin: 0; font-size: clamp(2rem, 5vw, 4rem); letter-spacing: -.05em; }
.intro p { color: var(--muted); }
.group { padding-top: 28px; }
.group > h2 { margin: 0 0 14px; font-size: 1.5rem; }
.group > h2 small { margin-left: 8px; color: var(--accent); font-size: .75rem; font-weight: 500; }
.file { margin-bottom: 22px; overflow: hidden; border: 1px solid var(--border); border-radius: 10px; background: var(--panel); scroll-margin-top: 18px; }
.file-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border); }
.file-header h3, .file-header p { margin: 0; }
.file-header h3 { font: 600 .9rem ui-monospace, monospace; }
.file-header p { color: var(--muted); font-size: .75rem; }
.file-actions { display: flex; align-items: center; gap: 10px; }
.source-link { color: var(--accent); font-size: .75rem; white-space: nowrap; }
.source-link:hover { color: var(--text); }
.copy { padding: 5px 10px; color: var(--muted); border: 1px solid var(--border); border-radius: 6px; background: transparent; cursor: pointer; }
.copy:hover { color: var(--text); border-color: var(--accent); }
pre[class*="language-"] { max-height: 650px; margin: 0 !important; border-radius: 0; font-size: .8rem; }

/* Literate Emacs config */
#content { max-width: 1100px; margin-left: 270px; padding: 50px 5vw 100px; }
#content a { color: var(--accent); }
#content a:hover { color: var(--text); }
h1.title { margin: 0 0 32px; color: var(--text); font-size: clamp(2rem, 5vw, 4rem); letter-spacing: -.05em; text-align: left; }
.page-links { position: fixed; z-index: 2; top: 24px; left: 20px; display: flex; width: 230px; justify-content: space-between; font-size: .8rem; }
.page-links a { color: var(--accent); }
#table-of-contents { position: fixed; z-index: 1; inset: 0 auto 0 0; width: 270px; padding: 62px 20px 28px; overflow: auto; border-right: 1px solid var(--border); background: var(--sidebar); }
#table-of-contents h2 { margin: 0 0 14px; color: var(--text); font-size: 1.4rem; }
#text-table-of-contents ul { margin: 0; padding-left: 14px; list-style: none; }
#text-table-of-contents > ul { padding-left: 0; }
#text-table-of-contents a { display: block; padding: 4px 7px; border-radius: 6px; color: var(--muted); font-size: .82rem; line-height: 1.35; }
#text-table-of-contents a:hover { color: var(--text); background: var(--panel); }
.outline-2 { padding-top: 22px; }
h2 { margin: 0 0 14px; color: var(--text); font-size: 1.5rem; }
h3 { margin-top: 32px; color: var(--text); }
p, li { color: var(--text); }
.org-src-container { position: relative; }
.org-src-container > .copy { position: absolute; z-index: 1; top: 8px; right: 8px; background: var(--panel); }
pre.src, pre.example { max-height: 650px; margin: 16px 0; padding: 16px; overflow: auto; border: 1px solid var(--border); border-radius: 10px; background: var(--panel); color: var(--text); font: .8rem/1.5 ui-monospace, monospace; }
.org-comment, .org-comment-delimiter { color: #6c7086; font-style: italic; }
.org-keyword, .org-negation-char { color: #cba6f7; }
.org-string, .org-doc { color: #a6e3a1; }
.org-function-name { color: #89b4fa; }
.org-variable-name { color: #f5c2e7; }
.org-builtin, .org-constant, .org-type { color: #f9e2af; }
.org-regexp-grouping-backslash, .org-regexp-grouping-construct { color: #94e2d5; }
.org-warning { color: #f38ba8; font-weight: 600; }
code { padding: 2px 5px; border-radius: 4px; background: var(--panel); font-family: ui-monospace, monospace; }
pre code { padding: 0; background: transparent; }
table { border-collapse: collapse; }
th, td { padding: 8px 12px; border: 1px solid var(--border); }
#postamble { margin-left: 270px; padding: 20px 5vw; color: var(--muted); border-top: 1px solid var(--border); font-size: .75rem; }

@media (prefers-color-scheme: light) {
  :root { color-scheme: light; --bg: #f6f7f9; --panel: #ffffff; --sidebar: #eef1f5; --muted: #667085; --text: #1d2433; --accent: #315fa8; --border: #d8dee8; }
  .org-comment, .org-comment-delimiter { color: #6a737d; }
  .org-keyword, .org-negation-char { color: #8250df; }
  .org-string, .org-doc { color: #116329; }
  .org-function-name { color: #0550ae; }
  .org-variable-name { color: #953800; }
  .org-builtin, .org-constant, .org-type { color: #9a6700; }
  .org-regexp-grouping-backslash, .org-regexp-grouping-construct { color: #0a7c72; }
  .org-warning { color: #cf222e; }
}

@media (max-width: 760px) {
  aside { position: static; width: auto; border-right: 0; border-bottom: 1px solid var(--border); }
  .nav-toggle { display: block; }
  aside nav { display: none; }
  aside.nav-open nav { display: block; }
  main { margin: 0; padding: 30px 18px 70px; }
  .file-header { align-items: flex-start; gap: 8px; }
  .file-actions { flex-direction: column; align-items: flex-end; gap: 6px; }
  .page-links { position: static; width: auto; padding: 18px 18px 0; }
  #table-of-contents { position: static; width: auto; max-height: 45vh; padding: 18px; border-right: 0; border-bottom: 1px solid var(--border); }
  #content { margin: 0; padding: 30px 18px 70px; }
  #postamble { margin: 0; padding: 20px 18px; }
}

generate-config-site.py

scripts/generate-config-site.py · 378 lines

#!/usr/bin/env python3
"""Generate a single-page site from the repository's config files."""

import argparse
import fnmatch
import hashlib
import html
import os
import subprocess
import sys
import tempfile
from collections import defaultdict
from pathlib import Path
from urllib.parse import quote

ROOT = Path(__file__).resolve().parent.parent
EMACS_SOURCE = Path(".emacs.d/bootstrap.org")
EMACS_RENDERER = ROOT / "scripts" / "render-emacs-config-to-html.sh"
STYLESHEET = ROOT / "scripts" / "config-site.css"
MAX_FILE_BYTES = 1_000_000
DEFAULT_EXCLUDES = (
    ".DS_Store",
    "*.lock",
    "*.db",
    "*.elc",
    "*.png",
    "*.jpg",
    "*.jpeg",
    "*.gif",
    "*.webp",
    "*.icns",
    "*.woff*",
    "*.ttf",
    "*.pdf",
    ".env*",
    "*credentials*",
    "*secret*",
    "id_rsa*",
    "site/**",
    "scripts/output/**",
    "**/__pycache__/**",
)

LANGUAGES = {
    ".css": "css",
    ".el": "lisp",
    ".html": "markup",
    ".ini": "ini",
    ".json": "json",
    ".jsonc": "json",
    ".lua": "lua",
    ".md": "markdown",
    ".nix": "nix",
    ".org": "markup",
    ".rasi": "css",
    ".scss": "scss",
    ".sh": "bash",
    ".toml": "toml",
    ".xml": "markup",
    ".yaml": "yaml",
    ".yml": "yaml",
    ".yuck": "lisp",
}

SPECIAL_LANGUAGES = {
    "Brewfile": "ruby",
    ".gitconfig": "ini",
    ".ideavimrc": "vim",
    ".tmux.conf": "bash",
    ".vimrc": "vim",
    ".Xmodmap": "text",
}

GROUP_NAMES = {
    ".emacs.d": "Emacs",
    ".github": "GitHub",
    ".pi": "Pi",
    "nix-darwin": "Nix Darwin",
    "nixos": "NixOS",
    "zsh": "Zsh",
}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "-o",
        "--output",
        type=Path,
        default=ROOT / "site" / "index.html",
        help="output HTML path (default: site/index.html)",
    )
    parser.add_argument(
        "--exclude",
        action="append",
        default=[],
        metavar="GLOB",
        help="additional repository-relative glob to exclude; repeatable",
    )
    return parser.parse_args()


def git_output(*arguments: str) -> str:
    try:
        result = subprocess.run(
            ["git", *arguments],
            cwd=ROOT,
            check=True,
            capture_output=True,
            text=True,
        )
    except FileNotFoundError as error:
        raise RuntimeError("Git is required to generate the site") from error
    except subprocess.CalledProcessError as error:
        detail = error.stderr.strip()
        raise RuntimeError(detail or f"git {' '.join(arguments)} failed") from error
    return result.stdout.strip()


def github_source_base() -> str:
    repository = os.environ.get("GITHUB_REPOSITORY")
    if repository is None:
        remote = git_output("remote", "get-url", "origin")
        marker = "github.com"
        if marker not in remote:
            raise RuntimeError("the origin remote is not hosted on GitHub")
        repository = remote.split(marker, 1)[1].lstrip(":/").removesuffix(".git")

    revision = os.environ.get("GITHUB_SHA") or git_output("rev-parse", "HEAD")
    return f"https://github.com/{repository}/blob/{revision}/"


def repository_files() -> list[Path]:
    """Return tracked files, failing closed outside a Git checkout."""
    try:
        result = subprocess.run(
            ["git", "ls-files", "-z"],
            cwd=ROOT,
            check=True,
            capture_output=True,
        )
    except FileNotFoundError as error:
        raise RuntimeError("Git is required to discover publishable files") from error
    except subprocess.CalledProcessError as error:
        detail = error.stderr.decode(errors="replace").strip()
        message = "not inside a Git repository"
        raise RuntimeError(f"could not list tracked files: {detail or message}") from error

    return [ROOT / os.fsdecode(item) for item in result.stdout.split(b"\0") if item]


def is_excluded(relative: Path, patterns: tuple[str, ...]) -> bool:
    name = relative.as_posix()
    return any(
        fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(relative.name, pattern)
        for pattern in patterns
    )


def read_text(path: Path) -> str | None:
    try:
        data = path.read_bytes()
    except OSError as error:
        print(f"warning: skipping {path}: {error}", file=sys.stderr)
        return None
    if len(data) > MAX_FILE_BYTES:
        print(f"warning: skipping {path}: larger than {MAX_FILE_BYTES:,} bytes", file=sys.stderr)
        return None
    if b"\0" in data:
        return None
    try:
        return data.decode("utf-8")
    except UnicodeDecodeError:
        return None


def group_name(relative: Path) -> str:
    if len(relative.parts) == 1:
        return "General"
    first = relative.parts[0]
    return GROUP_NAMES.get(first, first.lstrip(".").replace("-", " ").title())


def language_for(path: Path, source: str) -> str:
    if path.name in SPECIAL_LANGUAGES:
        return SPECIAL_LANGUAGES[path.name]
    if path.suffix in LANGUAGES:
        return LANGUAGES[path.suffix]
    if source.startswith("#!") and "sh" in source.splitlines()[0]:
        return "bash"
    return "text"


def file_id(relative: Path) -> str:
    path = relative.as_posix()
    slug = "".join(char if char.isalnum() else "-" for char in path).strip("-")
    digest = hashlib.sha256(path.encode()).hexdigest()[:10]
    return f"file-{slug}-{digest}"


def render_file(relative: Path, source: str, source_base: str) -> str:
    path = relative.as_posix()
    source_url = source_base + quote(path, safe="/")
    line_count = len(source.splitlines())
    language = language_for(relative, source)
    return f"""
<article class="file" id="{file_id(relative)}">
  <header class="file-header">
    <div>
      <h3>{html.escape(relative.name)}</h3>
      <p>{html.escape(path)} · {line_count:,} lines</p>
    </div>
    <div class="file-actions">
      <a class="source-link" href="{html.escape(source_url, quote=True)}">View on GitHub</a>
      <button class="copy" type="button" aria-label="Copy {html.escape(path, quote=True)}">Copy</button>
    </div>
  </header>
  <pre><code class="language-{language}">{html.escape(source)}</code></pre>
</article>"""


def render_site(
    files: list[tuple[Path, str]], stylesheet: str, source_base: str
) -> str:
    grouped: dict[str, list[tuple[Path, str]]] = defaultdict(list)
    for relative, source in files:
        grouped[group_name(relative)].append((relative, source))

    navigation = []
    sections = []
    for group in sorted(grouped, key=str.casefold):
        entries = sorted(grouped[group], key=lambda item: item[0].as_posix().casefold())
        group_id = "group-" + group.lower().replace(" ", "-")
        if group == "Emacs":
            navigation.append(
                '<a href="emacs.html"><span>Emacs guide</span><b>↗</b></a>'
            )
            heading = '<a href="emacs.html">Emacs <small>Open documented config →</small></a>'
        else:
            navigation.append(
                f'<a href="#{group_id}"><span>{html.escape(group)}</span><b>{len(entries)}</b></a>'
            )
            heading = html.escape(group)
        cards = "\n".join(
            render_file(path, source, source_base) for path, source in entries
        )
        sections.append(
            f'<section class="group" id="{group_id}"><h2>{heading}</h2>{cards}</section>'
        )

    return TEMPLATE.replace("{{NAVIGATION}}", "\n".join(navigation)).replace(
        "{{SECTIONS}}", "\n".join(sections)
    ).replace("121", str(len(files))).replace(
        "27", str(len(grouped))
    ).replace(":root { color-scheme: dark; --bg: #111318; --panel: #191c23; --sidebar: #15171d; --muted: #969dab; --text: #e6e9ef; --accent: #8aadf4; --border: #2c313c; }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.6 system-ui, sans-serif; }
a { color: inherit; text-decoration: none; }

/* Dotfiles index */
aside { position: fixed; inset: 0 auto 0 0; width: 270px; padding: 28px 20px; overflow: auto; border-right: 1px solid var(--border); background: var(--sidebar); }
aside h1 { margin: 0; font-size: 1.4rem; }
aside > p { margin: 4px 0 20px; color: var(--muted); font-size: .85rem; }
nav a { display: flex; justify-content: space-between; padding: 7px 9px; border-radius: 6px; color: var(--muted); }
nav a:hover { color: var(--text); background: var(--panel); }
nav b { color: var(--accent); font-size: .75rem; }
.nav-toggle { display: none; width: 100%; margin-bottom: 10px; padding: 8px 10px; color: var(--text); border: 1px solid var(--border); border-radius: 6px; background: var(--panel); text-align: left; cursor: pointer; }
main { max-width: 1100px; margin-left: 270px; padding: 50px 5vw 100px; }
.intro h1 { margin: 0; font-size: clamp(2rem, 5vw, 4rem); letter-spacing: -.05em; }
.intro p { color: var(--muted); }
.group { padding-top: 28px; }
.group > h2 { margin: 0 0 14px; font-size: 1.5rem; }
.group > h2 small { margin-left: 8px; color: var(--accent); font-size: .75rem; font-weight: 500; }
.file { margin-bottom: 22px; overflow: hidden; border: 1px solid var(--border); border-radius: 10px; background: var(--panel); scroll-margin-top: 18px; }
.file-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border); }
.file-header h3, .file-header p { margin: 0; }
.file-header h3 { font: 600 .9rem ui-monospace, monospace; }
.file-header p { color: var(--muted); font-size: .75rem; }
.file-actions { display: flex; align-items: center; gap: 10px; }
.source-link { color: var(--accent); font-size: .75rem; white-space: nowrap; }
.source-link:hover { color: var(--text); }
.copy { padding: 5px 10px; color: var(--muted); border: 1px solid var(--border); border-radius: 6px; background: transparent; cursor: pointer; }
.copy:hover { color: var(--text); border-color: var(--accent); }
pre[class*="language-"] { max-height: 650px; margin: 0 !important; border-radius: 0; font-size: .8rem; }

/* Literate Emacs config */
#content { max-width: 1100px; margin-left: 270px; padding: 50px 5vw 100px; }
#content a { color: var(--accent); }
#content a:hover { color: var(--text); }
h1.title { margin: 0 0 32px; color: var(--text); font-size: clamp(2rem, 5vw, 4rem); letter-spacing: -.05em; text-align: left; }
.page-links { position: fixed; z-index: 2; top: 24px; left: 20px; display: flex; width: 230px; justify-content: space-between; font-size: .8rem; }
.page-links a { color: var(--accent); }
#table-of-contents { position: fixed; z-index: 1; inset: 0 auto 0 0; width: 270px; padding: 62px 20px 28px; overflow: auto; border-right: 1px solid var(--border); background: var(--sidebar); }
#table-of-contents h2 { margin: 0 0 14px; color: var(--text); font-size: 1.4rem; }
#text-table-of-contents ul { margin: 0; padding-left: 14px; list-style: none; }
#text-table-of-contents > ul { padding-left: 0; }
#text-table-of-contents a { display: block; padding: 4px 7px; border-radius: 6px; color: var(--muted); font-size: .82rem; line-height: 1.35; }
#text-table-of-contents a:hover { color: var(--text); background: var(--panel); }
.outline-2 { padding-top: 22px; }
h2 { margin: 0 0 14px; color: var(--text); font-size: 1.5rem; }
h3 { margin-top: 32px; color: var(--text); }
p, li { color: var(--text); }
.org-src-container { position: relative; }
.org-src-container > .copy { position: absolute; z-index: 1; top: 8px; right: 8px; background: var(--panel); }
pre.src, pre.example { max-height: 650px; margin: 16px 0; padding: 16px; overflow: auto; border: 1px solid var(--border); border-radius: 10px; background: var(--panel); color: var(--text); font: .8rem/1.5 ui-monospace, monospace; }
.org-comment, .org-comment-delimiter { color: #6c7086; font-style: italic; }
.org-keyword, .org-negation-char { color: #cba6f7; }
.org-string, .org-doc { color: #a6e3a1; }
.org-function-name { color: #89b4fa; }
.org-variable-name { color: #f5c2e7; }
.org-builtin, .org-constant, .org-type { color: #f9e2af; }
.org-regexp-grouping-backslash, .org-regexp-grouping-construct { color: #94e2d5; }
.org-warning { color: #f38ba8; font-weight: 600; }
code { padding: 2px 5px; border-radius: 4px; background: var(--panel); font-family: ui-monospace, monospace; }
pre code { padding: 0; background: transparent; }
table { border-collapse: collapse; }
th, td { padding: 8px 12px; border: 1px solid var(--border); }
#postamble { margin-left: 270px; padding: 20px 5vw; color: var(--muted); border-top: 1px solid var(--border); font-size: .75rem; }

@media (prefers-color-scheme: light) {
  :root { color-scheme: light; --bg: #f6f7f9; --panel: #ffffff; --sidebar: #eef1f5; --muted: #667085; --text: #1d2433; --accent: #315fa8; --border: #d8dee8; }
  .org-comment, .org-comment-delimiter { color: #6a737d; }
  .org-keyword, .org-negation-char { color: #8250df; }
  .org-string, .org-doc { color: #116329; }
  .org-function-name { color: #0550ae; }
  .org-variable-name { color: #953800; }
  .org-builtin, .org-constant, .org-type { color: #9a6700; }
  .org-regexp-grouping-backslash, .org-regexp-grouping-construct { color: #0a7c72; }
  .org-warning { color: #cf222e; }
}

@media (max-width: 760px) {
  aside { position: static; width: auto; border-right: 0; border-bottom: 1px solid var(--border); }
  .nav-toggle { display: block; }
  aside nav { display: none; }
  aside.nav-open nav { display: block; }
  main { margin: 0; padding: 30px 18px 70px; }
  .file-header { align-items: flex-start; gap: 8px; }
  .file-actions { flex-direction: column; align-items: flex-end; gap: 6px; }
  .page-links { position: static; width: auto; padding: 18px 18px 0; }
  #table-of-contents { position: static; width: auto; max-height: 45vh; padding: 18px; border-right: 0; border-bottom: 1px solid var(--border); }
  #content { margin: 0; padding: 30px 18px 70px; }
  #postamble { margin: 0; padding: 20px 18px; }
}
", stylesheet)


TEMPLATE = """<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Dotfiles</title>
  <link rel="preconnect" href="https://cdnjs.cloudflare.com">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" media="(prefers-color-scheme: light)">
  <style>
:root { color-scheme: dark; --bg: #111318; --panel: #191c23; --sidebar: #15171d; --muted: #969dab; --text: #e6e9ef; --accent: #8aadf4; --border: #2c313c; }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.6 system-ui, sans-serif; }
a { color: inherit; text-decoration: none; }

/* Dotfiles index */
aside { position: fixed; inset: 0 auto 0 0; width: 270px; padding: 28px 20px; overflow: auto; border-right: 1px solid var(--border); background: var(--sidebar); }
aside h1 { margin: 0; font-size: 1.4rem; }
aside > p { margin: 4px 0 20px; color: var(--muted); font-size: .85rem; }
nav a { display: flex; justify-content: space-between; padding: 7px 9px; border-radius: 6px; color: var(--muted); }
nav a:hover { color: var(--text); background: var(--panel); }
nav b { color: var(--accent); font-size: .75rem; }
.nav-toggle { display: none; width: 100%; margin-bottom: 10px; padding: 8px 10px; color: var(--text); border: 1px solid var(--border); border-radius: 6px; background: var(--panel); text-align: left; cursor: pointer; }
main { max-width: 1100px; margin-left: 270px; padding: 50px 5vw 100px; }
.intro h1 { margin: 0; font-size: clamp(2rem, 5vw, 4rem); letter-spacing: -.05em; }
.intro p { color: var(--muted); }
.group { padding-top: 28px; }
.group > h2 { margin: 0 0 14px; font-size: 1.5rem; }
.group > h2 small { margin-left: 8px; color: var(--accent); font-size: .75rem; font-weight: 500; }
.file { margin-bottom: 22px; overflow: hidden; border: 1px solid var(--border); border-radius: 10px; background: var(--panel); scroll-margin-top: 18px; }
.file-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border); }
.file-header h3, .file-header p { margin: 0; }
.file-header h3 { font: 600 .9rem ui-monospace, monospace; }
.file-header p { color: var(--muted); font-size: .75rem; }
.file-actions { display: flex; align-items: center; gap: 10px; }
.source-link { color: var(--accent); font-size: .75rem; white-space: nowrap; }
.source-link:hover { color: var(--text); }
.copy { padding: 5px 10px; color: var(--muted); border: 1px solid var(--border); border-radius: 6px; background: transparent; cursor: pointer; }
.copy:hover { color: var(--text); border-color: var(--accent); }
pre[class*="language-"] { max-height: 650px; margin: 0 !important; border-radius: 0; font-size: .8rem; }

/* Literate Emacs config */
#content { max-width: 1100px; margin-left: 270px; padding: 50px 5vw 100px; }
#content a { color: var(--accent); }
#content a:hover { color: var(--text); }
h1.title { margin: 0 0 32px; color: var(--text); font-size: clamp(2rem, 5vw, 4rem); letter-spacing: -.05em; text-align: left; }
.page-links { position: fixed; z-index: 2; top: 24px; left: 20px; display: flex; width: 230px; justify-content: space-between; font-size: .8rem; }
.page-links a { color: var(--accent); }
#table-of-contents { position: fixed; z-index: 1; inset: 0 auto 0 0; width: 270px; padding: 62px 20px 28px; overflow: auto; border-right: 1px solid var(--border); background: var(--sidebar); }
#table-of-contents h2 { margin: 0 0 14px; color: var(--text); font-size: 1.4rem; }
#text-table-of-contents ul { margin: 0; padding-left: 14px; list-style: none; }
#text-table-of-contents > ul { padding-left: 0; }
#text-table-of-contents a { display: block; padding: 4px 7px; border-radius: 6px; color: var(--muted); font-size: .82rem; line-height: 1.35; }
#text-table-of-contents a:hover { color: var(--text); background: var(--panel); }
.outline-2 { padding-top: 22px; }
h2 { margin: 0 0 14px; color: var(--text); font-size: 1.5rem; }
h3 { margin-top: 32px; color: var(--text); }
p, li { color: var(--text); }
.org-src-container { position: relative; }
.org-src-container > .copy { position: absolute; z-index: 1; top: 8px; right: 8px; background: var(--panel); }
pre.src, pre.example { max-height: 650px; margin: 16px 0; padding: 16px; overflow: auto; border: 1px solid var(--border); border-radius: 10px; background: var(--panel); color: var(--text); font: .8rem/1.5 ui-monospace, monospace; }
.org-comment, .org-comment-delimiter { color: #6c7086; font-style: italic; }
.org-keyword, .org-negation-char { color: #cba6f7; }
.org-string, .org-doc { color: #a6e3a1; }
.org-function-name { color: #89b4fa; }
.org-variable-name { color: #f5c2e7; }
.org-builtin, .org-constant, .org-type { color: #f9e2af; }
.org-regexp-grouping-backslash, .org-regexp-grouping-construct { color: #94e2d5; }
.org-warning { color: #f38ba8; font-weight: 600; }
code { padding: 2px 5px; border-radius: 4px; background: var(--panel); font-family: ui-monospace, monospace; }
pre code { padding: 0; background: transparent; }
table { border-collapse: collapse; }
th, td { padding: 8px 12px; border: 1px solid var(--border); }
#postamble { margin-left: 270px; padding: 20px 5vw; color: var(--muted); border-top: 1px solid var(--border); font-size: .75rem; }

@media (prefers-color-scheme: light) {
  :root { color-scheme: light; --bg: #f6f7f9; --panel: #ffffff; --sidebar: #eef1f5; --muted: #667085; --text: #1d2433; --accent: #315fa8; --border: #d8dee8; }
  .org-comment, .org-comment-delimiter { color: #6a737d; }
  .org-keyword, .org-negation-char { color: #8250df; }
  .org-string, .org-doc { color: #116329; }
  .org-function-name { color: #0550ae; }
  .org-variable-name { color: #953800; }
  .org-builtin, .org-constant, .org-type { color: #9a6700; }
  .org-regexp-grouping-backslash, .org-regexp-grouping-construct { color: #0a7c72; }
  .org-warning { color: #cf222e; }
}

@media (max-width: 760px) {
  aside { position: static; width: auto; border-right: 0; border-bottom: 1px solid var(--border); }
  .nav-toggle { display: block; }
  aside nav { display: none; }
  aside.nav-open nav { display: block; }
  main { margin: 0; padding: 30px 18px 70px; }
  .file-header { align-items: flex-start; gap: 8px; }
  .file-actions { flex-direction: column; align-items: flex-end; gap: 6px; }
  .page-links { position: static; width: auto; padding: 18px 18px 0; }
  #table-of-contents { position: static; width: auto; max-height: 45vh; padding: 18px; border-right: 0; border-bottom: 1px solid var(--border); }
  #content { margin: 0; padding: 30px 18px 70px; }
  #postamble { margin: 0; padding: 20px 18px; }
}

  </style>
</head>
<body>
  <aside>
    <h1>Dotfiles</h1>
    <p>121 tracked files across 27 groups</p>
    <button class="nav-toggle" type="button" aria-expanded="false" aria-controls="site-navigation">Browse sections</button>
    <nav id="site-navigation">{{NAVIGATION}}</nav>
  </aside>
  <main>
    <header class="intro">
      <h1>Configuration,<br>documented by itself.</h1>
      <p>Generated from the repository source.</p>
    </header>
    {{SECTIONS}}
  </main>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
  <script>
    const sidebar = document.querySelector('aside');
    const navToggle = document.querySelector('.nav-toggle');
    navToggle.addEventListener('click', () => {
      const open = sidebar.classList.toggle('nav-open');
      navToggle.setAttribute('aria-expanded', String(open));
    });
    document.querySelectorAll('.copy').forEach(button => button.addEventListener('click', async () => {
      await navigator.clipboard.writeText(button.closest('.file').querySelector('code').textContent);
      button.textContent = 'Copied';
      setTimeout(() => button.textContent = 'Copy', 1200);
    }));
  </script>
</body>
</html>
"""


def main() -> int:
    args = parse_args()
    patterns = DEFAULT_EXCLUDES + tuple(args.exclude)
    try:
        candidates = repository_files()
        source_base = github_source_base()
    except RuntimeError as error:
        print(f"error: {error}", file=sys.stderr)
        return 1

    files: list[tuple[Path, str]] = []

    for path in candidates:
        try:
            relative = path.relative_to(ROOT)
        except ValueError:
            continue
        if is_excluded(relative, patterns) or relative == EMACS_SOURCE:
            continue
        source = read_text(path)
        if source is not None:
            files.append((relative, source))

    if not files:
        print("error: no publishable files found", file=sys.stderr)
        return 1

    output = args.output.expanduser().resolve()
    emacs_output = output.with_name("emacs.html")
    if output == emacs_output:
        print("error: the main output cannot be named emacs.html", file=sys.stderr)
        return 1

    try:
        stylesheet = STYLESHEET.read_text(encoding="utf-8")
    except OSError as error:
        print(f"error: could not read {STYLESHEET}: {error}", file=sys.stderr)
        return 1

    output.parent.mkdir(parents=True, exist_ok=True)
    try:
        with tempfile.TemporaryDirectory(
            prefix=".config-site-", dir=output.parent
        ) as staging_directory:
            staging = Path(staging_directory)
            staged_index = staging / output.name
            staged_emacs = staging / emacs_output.name
            emacs_source_url = source_base + quote(EMACS_SOURCE.as_posix(), safe="/")
            subprocess.run(
                [
                    "bash",
                    str(EMACS_RENDERER),
                    str(staged_emacs),
                    emacs_source_url,
                ],
                cwd=ROOT,
                check=True,
            )
            staged_index.write_text(
                render_site(files, stylesheet, source_base), encoding="utf-8"
            )
            os.replace(staged_emacs, emacs_output)
            os.replace(staged_index, output)
    except (OSError, subprocess.CalledProcessError) as error:
        print(f"error: could not generate site: {error}", file=sys.stderr)
        return 1

    print(f"Generated {output} from {len(files)} files")
    print(f"Generated {emacs_output} from {EMACS_SOURCE}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

org-render-html-minimal.el

scripts/org-render-html-minimal.el · 55 lines

(require 'package)

(setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                         ("melpa" . "https://melpa.org/packages/")))

(package-initialize)

;; This is required to highlight code blocks properly.
(package-refresh-contents)
(package-install 'htmlize)

(require 'htmlize)
(require 'org)

(defconst dotfiles-html-head
  (let ((stylesheet
         (expand-file-name "config-site.css"
                           (file-name-directory load-file-name))))
    (with-temp-buffer
      (insert-file-contents stylesheet)
      (concat "<style>\n" (buffer-string) "</style>"))))

(defconst dotfiles-html-scripts
  "<script>
document.addEventListener('DOMContentLoaded', () => {
  document.querySelectorAll('pre.src').forEach(block => {
    const button = document.createElement('button');
    button.className = 'copy';
    button.type = 'button';
    button.textContent = 'Copy';
    button.setAttribute('aria-label', 'Copy code snippet');
    button.addEventListener('click', async () => {
      await navigator.clipboard.writeText(block.textContent);
      button.textContent = 'Copied';
      setTimeout(() => button.textContent = 'Copy', 1200);
    });
    block.parentElement.prepend(button);
  });
});
</script>")

(defconst dotfiles-html-preamble
  (format
   "<nav class=\"page-links\"><a href=\"index.html\">Dotfiles</a><a href=\"%s\">View on GitHub</a></nav>"
   (getenv "DOTFILES_SOURCE_URL")))

;; Keep source highlighting in CSS classes and use the same visual language as
;; the generated dotfiles index instead of an external Org theme.
(setq org-export-time-stamp-file nil
      org-html-htmlize-output-type 'css
      org-html-head dotfiles-html-head
      org-html-head-extra dotfiles-html-scripts
      org-html-preamble dotfiles-html-preamble
      org-html-head-include-default-style nil
      org-html-head-include-scripts nil)

render-emacs-config-to-html.sh

scripts/render-emacs-config-to-html.sh · 32 lines

#!/usr/bin/env bash
set -euo pipefail

script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
root_dir=$(cd -- "$script_dir/.." && pwd)
if (($# != 2)); then
  echo "Usage: $0 OUTPUT SOURCE_URL" >&2
  exit 64
fi
output=$1
source_url=$2
work_dir=$(mktemp -d)
trap 'rm -rf "$work_dir"' EXIT

# Isolate package.el and all Emacs state from the user's configuration. This
# makes the batch export reproducible and prevents installed packages or an
# existing init file from affecting it.
export HOME="$work_dir/home"
export XDG_CONFIG_HOME="$work_dir/config"
export DOTFILES_SOURCE_URL="$source_url"
mkdir -p "$HOME" "$XDG_CONFIG_HOME"

cp "$root_dir/.emacs.d/bootstrap.org" "$work_dir/index.org"

emacs --batch \
  -Q \
  --load "$script_dir/org-render-html-minimal.el" \
  "$work_dir/index.org" \
  -f org-html-export-to-html

mkdir -p "$(dirname -- "$output")"
mv "$work_dir/index.html" "$output"

Swappy

config

swappy/config · 10 lines

[Default]
save_dir=$HOME/Desktop
save_filename_format=swappy-%Y%m%d-%H%M%S.png
show_panel=true
line_size=5
text_size=20
text_font=sans-serif
paint_mode=brush
early_exit=false
fill_shape=false

Sway

config

sway/config · 269 lines

# Logo key. Use Mod1 for Alt.
set $mod Mod4
# Home row direction keys, like vim
set $left h
set $down j
set $up k
set $right l
# Your preferred terminal emulator
set $term alacritty
set $config_dir ~/.config/sway
set $scripts $config_dir/scripts

# Hide tiled window titles and show them on floating windows.
default_border none
default_floating_border normal 0

gaps inner 5

### Output configuration
#
# Default wallpaper (more resolutions are available in /run/current-system/sw/share/backgrounds/sway/)
# output * bg /run/current-system/sw/share/backgrounds/sway/Sway_Wallpaper_Blue_1920x1080.png fill
#
# Example configuration:
#
#   output HDMI-A-1 resolution 1920x1080 position 1920,0
#
# You can get the names of your outputs by running: swaymsg -t get_outputs

# Make the active Sway sockets available to graphical systemd user services.
exec_always systemctl --user import-environment WAYLAND_DISPLAY SWAYSOCK
# Restart resident graphical services once Sway's Wayland environment is available.
exec sh -c 'systemctl --user import-environment WAYLAND_DISPLAY SWAYSOCK; \
    systemctl --user restart handy.service clipboard-picker.service'

exec waybar -c ~/.config/waybar/config.jsonc -s ~/.config/waybar/styles.css
exec qs -c wallpaper-picker
exec swaync

# Shift from 6500K to 4000K using GeoClue-based sunrise and sunset times.
exec gammastep -m wayland -l geoclue2 -t 6500:4000

exec_always eww daemon
exec_always eww open widgets
exec_always eww open calendar-window

exec wl-paste --type text --watch cliphist store
exec wl-paste --type image --watch cliphist store

exec autotiling

### Idle configuration
#
# Example configuration:
#
# exec swayidle -w \
#          timeout 300 'swaylock -f -c 000000' \
#          timeout 600 'swaymsg "output * power off"' resume 'swaymsg "output * power on"' \
#          before-sleep 'swaylock -f -c 000000'
#
# This will lock your screen after 300 seconds of inactivity, then turn off
# your displays after another 300 seconds, and turn your screens back on when
# resumed. It will also lock your screen before your computer goes to sleep.

### Input configuration
#
# Example configuration:
#
#   input "2:14:SynPS/2_Synaptics_TouchPad" {
#       dwt enabled
#       tap enabled
#       natural_scroll enabled
#       middle_emulation enabled
#   }
#
# You can get the names of your inputs by running: swaymsg -t get_inputs
# Read `man 5 sway-input` for more information about this section.

input * {
    xkb_layout us
    xkb_variant mac
    # Use Left Alt for macOS-style accent sequences.
    xkb_options lv3:lalt_switch
    repeat_delay 250
    repeat_rate 50
}

### Key bindings
#
# Basics:
#
    # Start a terminal
    bindsym $mod+Return exec $term

    # Kill focused window
    bindsym $mod+q kill

    # lock screen using a video as wallpaper
    bindsym $mod+Ctrl+q exec $scripts/lockscreen.sh

    # Start rofi (a program launcher)
    bindsym $mod+space exec rofi -show drun -show-icons

    # Open frequently used desktop actions
    bindsym $mod+k exec ~/.config/sway/scripts/quick-actions.sh

    # Open the emoji picker with the macOS shortcut: Control+Command+Space
    bindsym Ctrl+$mod+space exec rofimoji --selector rofi --action type --typer wtype

    # Hold Left Alt+Space for offline push-to-talk transcription.
    bindsym --no-repeat Mod5+space exec handy --toggle-transcription
    bindsym --release Mod5+space exec handy --toggle-transcription

    # Switch tabs with macOS-style Command+Shift+[ and Command+Shift+].
    bindsym $mod+Shift+bracketleft exec wtype -M ctrl -P Page_Up -p Page_Up -m ctrl
    bindsym $mod+Shift+bracketright exec wtype -M ctrl -P Page_Down -p Page_Down -m ctrl

    # Magnify a frozen capture of the screen; scroll to zoom and Escape to exit.
    bindsym $mod+z exec woomer

    # Enter a keyboard pointer mode for scrolling and clicking.
    bindsym $mod+Shift+g mode "Mouse"
    mode "Mouse" {
        bindsym j exec wlrctl pointer scroll 10 0
        bindsym k exec wlrctl pointer scroll -10 0
        bindsym h exec wlrctl pointer scroll 0 -10
        bindsym l exec wlrctl pointer scroll 0 10
        bindsym --no-repeat g mode "Mouse Go"
        bindsym --no-repeat Shift+g exec wtype -P End -p End
        bindsym --no-repeat s exec wlrctl pointer click left
        bindsym --no-repeat d exec wlrctl pointer click middle
        bindsym --no-repeat f exec wlrctl pointer click right
        bindsym Escape mode default
    }

    mode "Mouse Go" {
        bindsym --no-repeat g exec wtype -P Home -p Home, mode "Mouse"
        bindsym Escape mode "Mouse"
    }

    # Open clipboard history with the compiled C picker.
    bindsym $mod+y exec "$HOME/dotfiles/clipboard-picker/result/bin/clipboard-picker"

    # Drag floating windows by holding down $mod and left mouse button.
    # Resize them with right mouse button + $mod.
    # Despite the name, also works for non-floating windows.
    # Change normal to inverse to use left mouse button for resizing and right
    # mouse button for dragging.
    floating_modifier $mod normal

    # Reload the configuration file
    bindsym $mod+Shift+c reload

    # Exit sway (logs you out of your Wayland session)
    bindsym $mod+Shift+e exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -B 'Yes, exit sway' 'swaymsg exit'
#
# Moving around:
#
    # Move your focus around
    bindsym $mod+$left focus left
    bindsym $mod+$down focus down
    # Mod+k opens quick actions, so keep upward focus movement on the arrow key.
    bindsym $mod+Up focus up
    bindsym $mod+$right focus right

    # Move the focused window with the same, but add Shift
    bindsym $mod+Shift+$left move left
    bindsym $mod+Shift+$down move down
    bindsym $mod+Shift+$up move up
    bindsym $mod+Shift+$right move right
#
# Workspaces:
#
    # Switch to workspace
    bindsym $mod+1 workspace number 1
    bindsym $mod+2 workspace number 2
    bindsym $mod+3 workspace number 3
    bindsym $mod+4 workspace number 4
    bindsym $mod+5 workspace number 5
    bindsym $mod+6 workspace number 6
    bindsym $mod+7 workspace number 7
    bindsym $mod+8 workspace number 8
    bindsym $mod+9 workspace number 9
    bindsym $mod+0 workspace number 10
    # Move focused container to workspace
    bindsym $mod+Shift+1 move container to workspace number 1
    bindsym $mod+Shift+2 move container to workspace number 2
    bindsym $mod+Shift+3 move container to workspace number 3
    bindsym $mod+Shift+4 move container to workspace number 4
    bindsym $mod+Shift+5 move container to workspace number 5
    bindsym $mod+Shift+6 move container to workspace number 6
    bindsym $mod+Shift+7 move container to workspace number 7
    bindsym $mod+Shift+8 move container to workspace number 8
    bindsym $mod+Shift+9 move container to workspace number 9
    bindsym $mod+Shift+0 move container to workspace number 10
    # Note: workspaces can have any name you want, not just numbers.
    # We just use 1-10 as the default.
#
# Layout stuff:
#
    # You can "split" the current object of your focus with
    # $mod+b or $mod+v, for horizontal and vertical splits
    # respectively.
    bindsym $mod+b splith
    bindsym $mod+v splitv

    # Switch the current container between different layout styles
    bindsym $mod+s layout stacking
    bindsym $mod+w layout tabbed
    bindsym $mod+e layout toggle split

    # Make the current focus fullscreen (macOS-style Control+Command+F)
    bindsym Ctrl+$mod+f fullscreen

    # Toggle the current focus between tiling and floating mode
    bindsym $mod+Shift+f floating toggle

    # Swap focus between the tiling area and the floating area
    # bindsym $mod+space focus mode_toggle

    # Move focus to the parent container
    bindsym $mod+a focus parent

# Resizing containers:
#
mode "resize" {
    bindsym $left resize shrink width 10px
    bindsym $down resize grow height 10px
    bindsym $up resize shrink height 10px
    bindsym $right resize grow width 10px

    # Return to default mode
    bindsym Return mode "default"
    bindsym Escape mode "default"
}
bindsym $mod+r mode "resize"
#
# Utilities:
#
    # Special keys to adjust volume via PulseAudio
    bindsym --locked XF86AudioMute exec pactl set-sink-mute \@DEFAULT_SINK@ toggle
    bindsym --locked XF86AudioLowerVolume exec pactl set-sink-volume \@DEFAULT_SINK@ -5%
    bindsym --locked XF86AudioRaiseVolume exec pactl set-sink-volume \@DEFAULT_SINK@ +5%
    bindsym --locked XF86AudioMicMute exec pactl set-source-mute \@DEFAULT_SOURCE@ toggle
    # Special keys to adjust brightness via brightnessctl
    bindsym --locked XF86MonBrightnessDown exec brightnessctl set 5%-
    bindsym --locked XF86MonBrightnessUp exec brightnessctl set 5%+
    # Capture screenshots and open them in Satty for annotation
    bindsym $mod+Shift+s exec $scripts/screenshot.sh

# Enable services so sway can run with no errors, this is required to be able to run waybar
# because we need to have xdg-portal service enabled
include /etc/sway/config.d/*

for_window [app_id="^com.erick.ClipboardPicker$"] floating enable, move position center
for_window [app_id="^com.erick.WallpaperPicker$"] floating enable, move position center
for_window [app_id="org.gnome.NautilusPreviewer"] floating enable, move position center, resize set 900 700
for_window [app_id="^blueman-manager$"] floating enable, move position center
for_window [class="^Blueman-manager$"] floating enable, move position center
for_window [app_id="^nm-connection-editor$"] floating enable, move position center
for_window [class="^Nm-connection-editor$"] floating enable, move position center
for_window [app_id="^org.pulseaudio.pavucontrol$"] floating enable, border normal 0, move position center
for_window [app_id="^pavucontrol$"] floating enable, border normal 0, move position center
for_window [class="^Pavucontrol$"] floating enable, border normal 0, move position center
for_window [app_id="^org.satty.satty$"] floating enable, border normal 0, resize set 1200 800, move position center

for_window [app_id="^firefox$"] move to workspace 2
for_window [class="^Emacs$"] move to workspace 3
for_window [class="^Spotify$"] move to workspace 4

clipboard-menu.sh

sway/scripts/clipboard-menu.sh · 41 lines

#!/usr/bin/env bash
set -euo pipefail

preview_dir=$(mktemp -d)
menu_file="$preview_dir/menu"
: >"$menu_file"
trap 'rm -rf "$preview_dir"' EXIT

while IFS= read -r entry; do
  display=${entry#*$'\t'}
  if [[ "$display" == "[[ binary data"* ]]; then
    id=${entry%%$'\t'*}
    preview="$preview_dir/$id"
    if printf '%s' "$entry" | cliphist decode >"$preview"; then
      details=${display#"[[ binary data "}
      details=${details%"]]"}
      printf '%s\0icon\x1f%s\x1fdisplay\x1fImage · %s\n' "$entry" "$preview" "$details" >>"$menu_file"
      continue
    fi
  fi
  printf '%s\0display\x1f%s\n' "$entry" "$display" >>"$menu_file"
done < <(cliphist list)

selection=$(rofi -dmenu -i -show-icons -p "Clipboard" \
  -theme-str 'element-icon { size: 80px; }' <"$menu_file") || exit 0
[[ -n "$selection" ]] || exit 0

printf '%s' "$selection" | cliphist decode | wl-copy
sleep 0.05

app_id=$(swaymsg -t get_tree | jq -r \
  'first(.. | objects | select(.focused? == true) | (.app_id // .window_properties.class // empty)) // empty')

case "${app_id,,}" in
*alacritty* | foot | footclient | *kitty* | *wezterm* | *ghostty* | *terminal* | konsole | xterm | urxvt)
  wtype -M ctrl -M shift v -m shift -m ctrl
  ;;
*)
  wtype -M ctrl v -m ctrl
  ;;
esac

lockscreen.sh

sway/scripts/lockscreen.sh · 21 lines

#!/usr/bin/env bash
set -euo pipefail

VIDEO_DIR="$HOME/Wallpapers/"

if [[ ! -d "$VIDEO_DIR" ]]; then
  echo "Error: '$VIDEO_DIR' is not a directory" >&2
  exit 1
fi

mapfile -d '' -t videos < <(find "$VIDEO_DIR" -type f -iname '*.mp4' -print0)

if [[ ${#videos[@]} -eq 0 ]]; then
  echo "No .mp4 files found in '$VIDEO_DIR'" >&2
  exit 1
fi

random_index=$((RANDOM % ${#videos[@]}))
selected="${videos[$random_index]}"

swaylock-plugin --command "mpvpaper -o 'hwdec=vaapi no-audio loop' '*' $selected"

optimize-wallpaper-videos.sh

sway/scripts/optimize-wallpaper-videos.sh · 69 lines

#!/usr/bin/env bash
set -uo pipefail

WALLPAPER_DIR="${1:-$HOME/Wallpapers}"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/wallpaper"
BACKUP_DIR="$STATE_DIR/original-videos/$(date +%Y%m%d-%H%M%S)-$$"
temporary=""

cleanup() {
  [[ -n "$temporary" ]] && rm -f -- "$temporary"
}
trap cleanup EXIT INT TERM

if [[ ! -d "$WALLPAPER_DIR" ]]; then
  echo "Error: '$WALLPAPER_DIR' is not a directory" >&2
  exit 1
fi

videos=()
while IFS= read -r -d '' candidate; do
  [[ $(file --brief --mime-type -- "$candidate") == video/* ]] && videos+=("$candidate")
done < <(find "$WALLPAPER_DIR" -type f ! -name '.wallpaper-*.mp4' -print0)

if ((${#videos[@]} == 0)); then
  echo "No video wallpapers found in '$WALLPAPER_DIR'"
  exit 0
fi

converted=0
failed=0
for source in "${videos[@]}"; do
  relative=${source#"$WALLPAPER_DIR"/}
  output="$WALLPAPER_DIR/${relative%.*}.mp4"
  backup="$BACKUP_DIR/$relative"

  if [[ "$output" != "$source" && -e "$output" ]]; then
    echo "Skipping '$source': '$output' already exists" >&2
    ((failed++))
    continue
  fi

  mkdir -p "$(dirname "$output")" "$(dirname "$backup")"
  temporary=$(mktemp --tmpdir="$(dirname "$output")" ".wallpaper-XXXXXX.mp4")

  echo "Encoding '$relative' at 30 FPS and at most 4096x4096..."
  if ffmpeg -y -nostdin -hide_banner -loglevel warning -stats \
    -i "$source" -map 0:v:0 -an \
    -vf 'scale=4096:4096:force_original_aspect_ratio=decrease:force_divisible_by=2,fps=30' \
    -c:v libx264 -preset slow -crf 23 -pix_fmt yuv420p \
    -movflags +faststart "$temporary" &&
    ffprobe -v error -select_streams v:0 -show_entries stream=index \
      -of csv=p=0 "$temporary" | grep -q .; then
    mv -- "$source" "$backup"
    mv -- "$temporary" "$output"
    temporary=""
    ((converted++))
  else
    echo "Failed to encode '$source'" >&2
    rm -f -- "$temporary"
    temporary=""
    ((failed++))
  fi
done

echo "Converted: $converted; failed or skipped: $failed"
if ((converted > 0)); then
  echo "Original videos: $BACKUP_DIR"
fi
((failed == 0))

pick-wallpaper.sh

sway/scripts/pick-wallpaper.sh · 37 lines

#!/usr/bin/env bash
set -euo pipefail

PICKER="$HOME/dotfiles/wallpaper-picker/result/bin/wallpaper-picker"
WALLPAPER_DIR="$HOME/Wallpapers"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/wallpaper"
VIDEO_WALLPAPER_LINK="$STATE_DIR/current-video"

selected=$("$PICKER" "$WALLPAPER_DIR") || exit 0
[[ -n "$selected" && -f "$selected" ]] || exit 0

mkdir -p "$STATE_DIR"
printf '%s\n' "$selected" >"$STATE_DIR/last-wallpaper"
mime_type=$(file --brief --mime-type -- "$selected")

case "$mime_type" in
image/*)
  systemctl --user stop wallpaper-video.service
  swaymsg output '*' bg "$selected" fill
  ;;
video/*)
  mapfile -t swaybg_pids < <(pgrep -f '[s]waybg' || true)
  if ((${#swaybg_pids[@]})); then
    kill "${swaybg_pids[@]}" 2>/dev/null || true
    for _ in {1..10}; do
      swaybg_running=false
      for pid in "${swaybg_pids[@]}"; do
        kill -0 "$pid" 2>/dev/null && swaybg_running=true
      done
      "$swaybg_running" || break
      sleep 0.05
    done
  fi
  ln -sfn -- "$selected" "$VIDEO_WALLPAPER_LINK"
  systemctl --user restart wallpaper-video.service
  ;;
esac

quick-actions.sh

sway/scripts/quick-actions.sh · 164 lines

#!/usr/bin/env bash
set -euo pipefail

selection=$(
  printf '%s\n' \
    "󰸉  Change wallpaper" \
    "󰑇  Pick wallpaper" \
    "󰄀  Take screenshot" \
    "󰖩  Share Wi-Fi" \
    "󰍹  Scale display" \
    "󰑐  Refresh rate" \
    "  Clipboard history" \
    "  Toggle night light" \
    "󰢹  Toggle remote desktop" \
    "󰆴  Kill process" \
    "󰩹  Empty trash" \
    "󰌾  Lock screen" \
    "󰐥  Power menu" \
    "󰑐  Reload Waybar" \
    "󰑐  Reload Eww" |
    rofi -dmenu -i -p "Quick Actions"
) || exit 0

case "$selection" in
"󰸉  Change wallpaper")
  systemctl --user start wallpaper-rotate.service
  ;;
"󰑇  Pick wallpaper")
  exec qs -c wallpaper-picker ipc call picker toggle
  ;;
"󰄀  Take screenshot")
  exec "$HOME/.config/sway/scripts/screenshot.sh"
  ;;
"󰖩  Share Wi-Fi")
  exec "$HOME/.config/sway/scripts/wifi-qr.sh"
  ;;
"󰍹  Scale display")
  outputs=$(swaymsg --type get_outputs --raw)
  output=$(jq -r \
    'map(select(.focused))[0].name // map(select(.active))[0].name // empty' \
    <<<"$outputs")
  [[ -n "$output" ]] || exit 1

  current_scale=$(jq -r --arg output "$output" \
    '.[] | select(.name == $output) | .scale' <<<"$outputs")
  scale_selection=$(
    printf '%s\n' \
      "75%   (0.75×)" \
      "80%   (0.80×)" \
      "90%   (0.90×)" \
      "100%  (1.00×)" \
      "125%  (1.25×)" \
      "150%  (1.50×)" \
      "175%  (1.75×)" \
      "200%  (2.00×)" |
      rofi -dmenu -p "$output · Current ${current_scale}×"
  ) || exit 0

  case "$scale_selection" in
  "75%   (0.75×)") scale=0.75 ;;
  "80%   (0.80×)") scale=0.8 ;;
  "90%   (0.90×)") scale=0.9 ;;
  "100%  (1.00×)") scale=1.0 ;;
  "125%  (1.25×)") scale=1.25 ;;
  "150%  (1.50×)") scale=1.5 ;;
  "175%  (1.75×)") scale=1.75 ;;
  "200%  (2.00×)") scale=2.0 ;;
  *) exit 0 ;;
  esac

  swaymsg output "$output" scale "$scale" >/dev/null
  ;;
"󰑐  Refresh rate")
  compact_mode_filter='
    def compact_mode:
      if (.current_mode | type) == "object" then
        "\(.current_mode.width)x\(.current_mode.height)@\(.current_mode.refresh / 1000)hz"
      else
        ((try (
          (.current_mode // "")
          | capture("(?<width>[0-9]+)x(?<height>[0-9]+).*@ ?(?<rate>[0-9.]+).*Hz")
          | "\(.width)x\(.height)@\(.rate)hz"
        ) catch null) // "unknown")
      end;
  '
  outputs=$(swaymsg --type get_outputs --raw)
  output=$(
    jq -r 'map(select(.focused))[0].name // map(select(.active))[0].name // empty' \
      <<<"$outputs"
  )
  [[ -n "$output" ]] || exit 1

  output_selection=$(
    jq -r "$compact_mode_filter .[] | \"\(.name)\\t\(compact_mode)\"" <<<"$outputs" |
      rofi -dmenu -p "Select display"
  ) || exit 0
  [[ -n "$output_selection" ]] || exit 0
  output=${output_selection%%$'\t'*}

  current_status=$(jq -r --arg output "$output" \
    "$compact_mode_filter .[] | select(.name == \$output) | compact_mode" \
    <<<"$outputs")
  mode_selection=$(
    jq -r --arg output "$output" \
      '.[] | select(.name == $output) | .modes
      | sort_by([.width, .height, .refresh])
      | group_by([.width, .height, .refresh]) | map(.[0])
      | map("\(.width)x\(.height)@\(.refresh / 1000)hz")[]' \
      <<<"$outputs" |
      rofi -dmenu -p "$output · Current: $current_status"
  ) || exit 0
  [[ -n "$mode_selection" ]] || exit 0

  if [[ "$mode_selection" =~ ^([0-9]+)x([0-9]+)@([0-9.]+)hz$ ]]; then
    width=${BASH_REMATCH[1]}
    height=${BASH_REMATCH[2]}
    refresh=${BASH_REMATCH[3]}
  else
    exit 1
  fi

  swaymsg output "$output" mode "${width}x${height}@${refresh}Hz" >/dev/null
  ;;
"  Clipboard history")
  exec "$HOME/dotfiles/clipboard-picker/result/bin/clipboard-picker"
  ;;
"  Toggle night light")
  exec "$HOME/.config/waybar/scripts/night-light.sh" toggle
  ;;
"󰢹  Toggle remote desktop")
  exec "$HOME/.config/waybar/scripts/vnc-toggle.sh"
  ;;
"󰆴  Kill process")
  process_selection=$(
    ps -eo pid=,user=,%cpu=,%mem=,args= --sort=-%cpu |
      awk '{command = $5; sub(".*/", "", command); $5 = command; print}' |
      rofi -dmenu -i -p "Kill Process"
  ) || exit 0
  [[ -n "$process_selection" ]] || exit 0

  pid=$(awk '{print $1}' <<<"$process_selection")
  [[ "$pid" =~ ^[0-9]+$ ]] || exit 1
  kill -TERM -- "$pid"
  ;;
"󰩹  Empty trash")
  trash_dir="${XDG_DATA_HOME:-$HOME/.local/share}/Trash"
  for directory in files info; do
    [[ -d "$trash_dir/$directory" ]] || continue
    find "$trash_dir/$directory" -mindepth 1 -delete
  done
  ;;
"󰌾  Lock screen")
  exec "$HOME/.config/sway/scripts/lockscreen.sh"
  ;;
"󰐥  Power menu")
  exec "$HOME/.config/sway/scripts/wlogout-menu.sh"
  ;;
"󰑐  Reload Waybar")
  kill -SIGUSR2 $(pgrep waybar)
  ;;
"󰑐  Reload Eww")
  eww reload
  ;;
esac

screenshot.sh

sway/scripts/screenshot.sh · 45 lines

#!/usr/bin/env bash
set -euo pipefail

mode=${1:-}
if [[ -z "$mode" ]]; then
  selection=$(printf '%s\n' "Full screen" "Select region" "Focused window" |
    rofi -dmenu -i -p "Screenshot") || exit 0
  case "$selection" in
  "Full screen") mode="full" ;;
  "Select region") mode="region" ;;
  "Focused window") mode="window" ;;
  *) exit 0 ;;
  esac
fi

screenshot_dir="$HOME/Pictures/Screenshots"
capture=$(mktemp "${XDG_RUNTIME_DIR:-/tmp}/screenshot.XXXXXX.png")
trap 'rm -f "$capture"' EXIT
mkdir -p "$screenshot_dir"

case "$mode" in
full)
  grim "$capture"
  ;;
region)
  geometry=$(slurp) || exit 0
  [[ -n "$geometry" ]] || exit 0
  grim -g "$geometry" "$capture"
  ;;
window)
  geometry=$(swaymsg -t get_tree | jq -r \
    'first(.. | objects | select(.focused? == true) | .rect | "\(.x),\(.y) \(.width)x\(.height)") // empty')
  [[ -n "$geometry" ]] || exit 1
  grim -g "$geometry" "$capture"
  ;;
*)
  printf 'Usage: %s {full|region|window}\n' "${0##*/}" >&2
  exit 2
  ;;
esac

satty --filename "$capture" \
  --app-id org.satty.satty \
  --copy-command wl-copy \
  --output-filename "$screenshot_dir/screenshot-%Y-%m-%d_%H-%M-%S.png"

wallpaper.sh

sway/scripts/wallpaper.sh · 65 lines

#!/usr/bin/env bash
set -euo pipefail

WALLPAPER_DIR="$HOME/Wallpapers"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/wallpaper"
LAST_WALLPAPER_FILE="$STATE_DIR/last-wallpaper"
VIDEO_WALLPAPER_LINK="$STATE_DIR/current-video"

if [[ ! -d "$WALLPAPER_DIR" ]]; then
  echo "Error: '$WALLPAPER_DIR' is not a directory" >&2
  exit 1
fi

media=()
while IFS= read -r -d '' candidate; do
  mime_type=$(file --brief --mime-type -- "$candidate")
  case "$mime_type" in
  image/* | video/*) media+=("$candidate") ;;
  esac
done < <(find "$WALLPAPER_DIR" -type f -print0)

if ((${#media[@]} == 0)); then
  echo "No image or video files found in '$WALLPAPER_DIR'" >&2
  exit 1
fi

last_wallpaper=""
[[ -r "$LAST_WALLPAPER_FILE" ]] && last_wallpaper=$(<"$LAST_WALLPAPER_FILE")

choices=("${media[@]}")
if ((${#media[@]} > 1)) && [[ -n "$last_wallpaper" ]]; then
  choices=()
  for candidate in "${media[@]}"; do
    [[ "$candidate" != "$last_wallpaper" ]] && choices+=("$candidate")
  done
fi

selected=${choices[RANDOM % ${#choices[@]}]}
mkdir -p "$STATE_DIR"
printf '%s\n' "$selected" >"$LAST_WALLPAPER_FILE"
mime_type=$(file --brief --mime-type -- "$selected")

case "$mime_type" in
image/*)
  systemctl --user stop wallpaper-video.service
  swaymsg output '*' bg "$selected" fill
  ;;
video/*)
  # Sway starts swaybg for static images; stop it before using mpvpaper.
  mapfile -t swaybg_pids < <(pgrep -f '[s]waybg' || true)
  if ((${#swaybg_pids[@]})); then
    kill "${swaybg_pids[@]}" 2>/dev/null || true
    for _ in {1..10}; do
      swaybg_running=false
      for pid in "${swaybg_pids[@]}"; do
        kill -0 "$pid" 2>/dev/null && swaybg_running=true
      done
      "$swaybg_running" || break
      sleep 0.05
    done
  fi
  ln -sfn -- "$selected" "$VIDEO_WALLPAPER_LINK"
  systemctl --user restart wallpaper-video.service
  ;;
esac

wifi-qr.sh

sway/scripts/wifi-qr.sh · 67 lines

#!/usr/bin/env bash
set -euo pipefail

umask 077
ulimit -c 0

show_error() {
  rofi -e "$1"
  exit 1
}

escape_wifi_field() {
  printf '%s' "$1" | sed 's/[\\;,:]/\\&/g'
}

[[ -n "${XDG_RUNTIME_DIR:-}" && -d "$XDG_RUNTIME_DIR" && -w "$XDG_RUNTIME_DIR" ]] ||
  show_error "A private runtime directory is not available"

device=$(nmcli -t --escape no -f DEVICE,TYPE,STATE device status |
  awk -F: '$2 == "wifi" && $3 == "connected" { print $1; exit }')
[[ -n "$device" ]] || show_error "No active Wi-Fi connection"

connection_uuid=$(nmcli --escape no -g GENERAL.CON-UUID device show "$device")
[[ -n "$connection_uuid" ]] || show_error "The active Wi-Fi connection has no UUID"
ssid=$(nmcli --escape no -g 802-11-wireless.ssid connection show "$connection_uuid")
key_mgmt=$(nmcli --escape no -g 802-11-wireless-security.key-mgmt connection show "$connection_uuid")
hidden=$(nmcli --escape no -g 802-11-wireless.hidden connection show "$connection_uuid")
[[ -n "$ssid" ]] || show_error "The active Wi-Fi network has no SSID"

password=""
case "$key_mgmt" in
"" | none)
  password=$(nmcli -s --escape no -g 802-11-wireless-security.wep-key0 \
    connection show "$connection_uuid" 2>/dev/null || true)
  if [[ -n "$password" ]]; then
    security=WEP
  else
    security=nopass
  fi
  ;;
wpa-psk | sae | wpa-none)
  security=WPA
  password=$(nmcli -s --escape no -g 802-11-wireless-security.psk connection show "$connection_uuid")
  [[ -n "$password" ]] || show_error "NetworkManager did not provide the Wi-Fi password"
  ;;
wpa-eap | ieee8021x)
  show_error "Enterprise Wi-Fi QR sharing is not supported"
  ;;
*)
  show_error "Unsupported Wi-Fi security type: $key_mgmt"
  ;;
esac

ssid_escaped=$(escape_wifi_field "$ssid")
password_escaped=$(escape_wifi_field "$password")
[[ "$hidden" == yes ]] && hidden=true || hidden=false
payload="WIFI:T:$security;S:$ssid_escaped;P:$password_escaped;H:$hidden;;"

qr_file=$(mktemp "$XDG_RUNTIME_DIR/wifi-qr.XXXXXX.png")
trap 'rm -f "$qr_file"' EXIT
printf '%s' "$payload" | qrencode -t PNG -s 8 -m 2 -o "$qr_file"
unset password password_escaped payload

printf 'Scan to join %s\0icon\x1f%s\n' "$ssid" "$qr_file" |
  rofi -dmenu -p "Wi-Fi QR" -show-icons -no-custom \
    -theme-str 'window { width: 360px; } listview { lines: 1; } element { orientation: vertical; } element-icon { size: 256px; } element-text { horizontal-align: 0.5; }' \
    >/dev/null || true

wlogout-menu.sh

sway/scripts/wlogout-menu.sh · 33 lines

#!/usr/bin/env bash
set -euo pipefail

read -r width height monitor < <(
  swaymsg --type get_outputs --raw |
    jq --raw-output '
      to_entries as $outputs
      | ($outputs | map(select(.value.focused))[0] // $outputs[0])
      | "\(.value.rect.width) \(.value.rect.height) \(.key)"
    '
)

buttons=6
spacing=12
button_width=140
menu_height=160
menu_width=$((buttons * button_width + (buttons - 1) * spacing))
horizontal_margin=$(((width - menu_width) / 2))
vertical_margin=$(((height - menu_height) / 2))

((horizontal_margin < 16)) && horizontal_margin=16
((vertical_margin < 16)) && vertical_margin=16

exec wlogout \
  --protocol layer-shell \
  --buttons-per-row "$buttons" \
  --column-spacing "$spacing" \
  --margin-left "$horizontal_margin" \
  --margin-right "$horizontal_margin" \
  --margin-top "$vertical_margin" \
  --margin-bottom "$vertical_margin" \
  --primary-monitor "$monitor" \
  --no-span

Swaync

config.json

swaync/config.json · 49 lines

{
  "positionX": "right",
  "positionY": "top",
  "layer": "overlay",
  "control-center-layer": "top",
  "layer-shell": true,
  "cssPriority": "user",
  "control-center-margin-top": 8,
  "control-center-margin-bottom": 8,
  "control-center-margin-right": 8,
  "control-center-margin-left": 8,
  "notification-2fa-action": true,
  "notification-inline-replies": false,
  "notification-body-image-height": 100,
  "notification-body-image-width": 200,
  "timeout": 8,
  "timeout-low": 5,
  "timeout-critical": 0,
  "fit-to-screen": true,
  "relative-timestamps": true,
  "control-center-width": 380,
  "control-center-height": 600,
  "notification-window-width": 380,
  "keyboard-shortcuts": true,
  "notification-grouping": true,
  "image-visibility": "when-available",
  "transition-time": 180,
  "hide-on-clear": false,
  "hide-on-action": true,
  "text-empty": "No notifications",
  "widgets": [
    "title",
    "dnd",
    "notifications"
  ],
  "widget-config": {
    "notifications": {
      "vexpand": true
    },
    "title": {
      "text": "Notifications",
      "clear-all-button": true,
      "button-text": "Clear all"
    },
    "dnd": {
      "text": "Do not disturb"
    }
  }
}

style.css

swaync/style.css · 155 lines

* {
  font-family: "JetBrainsMono Nerd Font";
  color: #eeeeee;
}

notificationwindow,
blankwindow,
.notification-window,
.blank-window,
.floating-notifications {
  background: transparent;
}

.notification-row {
  background: transparent;
  outline: none;
}

.notification-background {
  padding: 6px 10px;
}

.notification {
  border: 1px solid rgba(126, 186, 228, 0.48);
  border-radius: 10px;
  background-color: rgba(15, 15, 15, 0.94);
  box-shadow: none;
}

.notification.low {
  border-color: rgba(255, 255, 255, 0.1);
  background-color: rgba(15, 15, 15, 0.82);
}

.notification.low .summary,
.notification.low .body,
.notification.low .time {
  color: rgba(255, 255, 255, 0.5);
}

.notification.normal {
  border-color: rgba(126, 186, 228, 0.48);
}

.notification.critical {
  border-color: #f53c3c;
  background-color: rgba(45, 15, 15, 0.96);
}

.notification.critical .summary {
  color: #f53c3c;
}

.notification.critical .body {
  color: rgba(255, 255, 255, 0.86);
}

.notification-default-action {
  margin: 0;
  padding: 8px;
  border: none;
  border-radius: 10px;
  background: transparent;
  box-shadow: none;
}

.notification-default-action:hover,
.notification-action > button:hover {
  background-color: rgba(126, 186, 228, 0.12);
}

.notification-content {
  padding: 2px;
  background: transparent;
}

.summary {
  color: #7ebae4;
  font-size: 14px;
  font-weight: bold;
  text-shadow: none;
}

.time {
  color: rgba(255, 255, 255, 0.45);
  font-size: 11px;
  text-shadow: none;
}

.body {
  color: rgba(255, 255, 255, 0.72);
  font-size: 12px;
  text-shadow: none;
}

.close-button {
  min-width: 24px;
  min-height: 24px;
  margin: 8px;
  padding: 0;
  border: none;
  border-radius: 50%;
  color: #eeeeee;
  background-color: rgba(255, 255, 255, 0.1);
  box-shadow: none;
}

.close-button:hover {
  color: #ffffff;
  background-color: rgba(245, 60, 60, 0.8);
}

.notification-action > button {
  margin: 4px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  color: #eeeeee;
  background-color: rgba(255, 255, 255, 0.06);
}

.control-center {
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  color: #eeeeee;
  background-color: rgba(15, 15, 15, 0.94);
}

.control-center .notification {
  border-color: rgba(255, 255, 255, 0.12);
}

.widget {
  margin: 8px;
  padding: 8px;
  border-radius: 10px;
  background-color: rgba(255, 255, 255, 0.04);
}

.widget-title > label {
  color: #7ebae4;
  font-size: 16px;
  font-weight: bold;
}

.widget-title > button,
.widget-dnd > switch {
  border: 1px solid rgba(126, 186, 228, 0.35);
  border-radius: 8px;
  color: #eeeeee;
  background-color: rgba(126, 186, 228, 0.1);
}

.widget-title > button:hover {
  background-color: rgba(126, 186, 228, 0.2);
}

Wallpaper Picker

default.nix

wallpaper-picker/default.nix · 52 lines

{ pkgs ? import <nixpkgs> { } }:

pkgs.stdenv.mkDerivation {
  pname = "wallpaper-picker";
  version = "0.1.0";
  src = ./.;

  nativeBuildInputs = with pkgs; [
    pkg-config
    wrapGAppsHook4
    makeWrapper
  ];

  buildInputs = with pkgs; [
    gtk4
    gdk-pixbuf
    glib
    gst_all_1.gstreamer
    gst_all_1.gst-plugins-base
    gst_all_1.gst-plugins-good
    gst_all_1.gst-plugins-bad
    gst_all_1.gst-libav
  ];

  buildPhase = ''
    runHook preBuild
    cc -O3 -Wall -Wextra -o wallpaper-picker main.c \
      $(pkg-config --cflags --libs gtk4 gdk-pixbuf-2.0)
    runHook postBuild
  '';

  installPhase = ''
    runHook preInstall
    install -Dm755 wallpaper-picker $out/bin/wallpaper-picker

    wrapProgram $out/bin/wallpaper-picker \
      --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.ffmpeg ]} \
      --prefix GST_PLUGIN_SYSTEM_PATH_1_0 : ${pkgs.lib.makeSearchPath "lib/gstreamer-1.0" [
        pkgs.gst_all_1.gst-plugins-base
        pkgs.gst_all_1.gst-plugins-good
        pkgs.gst_all_1.gst-plugins-bad
        pkgs.gst_all_1.gst-libav
      ]}
    runHook postInstall
  '';

  meta = with pkgs.lib; {
    description = "GTK4 keyboard-driven image and video wallpaper picker";
    mainProgram = "wallpaper-picker";
    platforms = platforms.linux;
  };
}

main.c

wallpaper-picker/main.c · 713 lines

#include <gdk-pixbuf/gdk-pixbuf.h>
#include <glib/gstdio.h>
#include <gtk/gtk.h>
#include <string.h>

#define APP_ID "com.erick.WallpaperPicker"
#define THUMB_WIDTH 100

static const gchar *video_extensions[] = {"mp4", "mkv", "webm",
                                          "mov", "avi", NULL};

typedef struct {
  gchar *path;
  gchar *thumbnail;
  GdkTexture *small_texture;
  GdkTexture *preview_texture;
  gboolean preview_loading;
  GWeakRef picture;
} Wallpaper;

typedef struct {
  GtkApplication parent_instance;
  gchar *directory;
  GWeakRef window;
  guint window_generation;
  GPtrArray *wallpapers;
  GDir *wallpaper_dir;
  GQueue *pending_paths;
  GQueue *preview_cache;
  gboolean scan_complete;
  guint scan_source_id;
  GtkFlowBox *flow_box;
  GtkPicture *preview;
  GtkMediaStream *media_stream;
  guint video_preview_timeout_id;
  guint prefetch_timeout_id;
} WallpaperPicker;

typedef struct {
  GtkApplicationClass parent_class;
} WallpaperPickerClass;

G_DEFINE_TYPE(WallpaperPicker, wallpaper_picker, GTK_TYPE_APPLICATION)

static gboolean has_video_extension(const gchar *path) {
  gchar *suffix =
      g_ascii_strdown(strrchr(path, '.') ? strrchr(path, '.') + 1 : "", -1);
  gboolean result = FALSE;
  for (const gchar **extension = video_extensions; *extension; extension++) {
    if (g_strcmp0(suffix, *extension) == 0) {
      result = TRUE;
      break;
    }
  }
  g_free(suffix);
  return result;
}

static gchar *thumbnail_path(const gchar *path) {
  gchar *cache_key = g_strconcat(path, ":full-resolution-v1", NULL);
  gchar *key = g_compute_checksum_for_string(G_CHECKSUM_SHA256, cache_key, -1);
  g_free(cache_key);
  gchar *cache_dir =
      g_build_filename(g_get_user_cache_dir(), "wallpaper-picker", NULL);
  g_mkdir_with_parents(cache_dir, 0700);
  gchar *result = g_strdup_printf("%s/%s.png", cache_dir, key);
  g_free(cache_dir);
  g_free(key);
  return result;
}

static gchar *make_video_thumbnail(const gchar *path) {
  gchar *thumbnail = thumbnail_path(path);
  if (g_file_test(thumbnail, G_FILE_TEST_EXISTS))
    return thumbnail;

  gchar *argv[] = {(gchar *)"ffmpeg",
                   (gchar *)"-y",
                   (gchar *)"-hide_banner",
                   (gchar *)"-loglevel",
                   (gchar *)"error",
                   (gchar *)"-ss",
                   (gchar *)"1",
                   (gchar *)"-i",
                   (gchar *)path,
                   (gchar *)"-frames:v",
                   (gchar *)"1",
                   thumbnail,
                   NULL};
  gint exit_status = 0;
  GError *error = NULL;
  if (!g_spawn_sync(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL,
                    NULL, &exit_status, &error) ||
      !g_spawn_check_wait_status(exit_status, NULL)) {
    g_warning("Could not create thumbnail for %s: %s", path,
              error ? error->message : "ffmpeg failed");
    g_clear_error(&error);
    g_remove(thumbnail);
    g_free(thumbnail);
    return NULL;
  }
  return thumbnail;
}

static void wallpaper_free(gpointer data) {
  Wallpaper *wallpaper = data;
  g_free(wallpaper->path);
  g_free(wallpaper->thumbnail);
  g_clear_object(&wallpaper->small_texture);
  g_clear_object(&wallpaper->preview_texture);
  g_weak_ref_clear(&wallpaper->picture);
  g_free(wallpaper);
}

static gint compare_wallpaper_children(GtkFlowBoxChild *a, GtkFlowBoxChild *b,
                                       gpointer user_data) {
  (void)user_data;
  Wallpaper *left = g_object_get_data(G_OBJECT(a), "wallpaper");
  Wallpaper *right = g_object_get_data(G_OBJECT(b), "wallpaper");
  return g_ascii_strcasecmp(left->path, right->path);
}

static GtkWidget *wallpaper_widget(Wallpaper *wallpaper) {
  GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
  gtk_widget_set_size_request(box, 100, 90);

  GtkWidget *picture = gtk_picture_new();
  g_weak_ref_set(&wallpaper->picture, G_OBJECT(picture));
  gtk_picture_set_content_fit(GTK_PICTURE(picture), GTK_CONTENT_FIT_CONTAIN);
  gtk_widget_set_size_request(picture, THUMB_WIDTH, 60);
  gtk_widget_set_halign(picture, GTK_ALIGN_FILL);
  gtk_widget_set_valign(picture, GTK_ALIGN_FILL);
  gtk_widget_set_hexpand(picture, TRUE);
  gtk_widget_set_vexpand(picture, TRUE);
  gtk_box_append(GTK_BOX(box), picture);

  return box;
}

static gboolean on_key_pressed(GtkEventControllerKey *controller, guint keyval,
                               guint keycode, GdkModifierType state,
                               gpointer user_data) {
  (void)controller;
  (void)keycode;
  WallpaperPicker *picker = user_data;

  if (keyval == GDK_KEY_Escape) {
    g_application_quit(G_APPLICATION(picker));
    return TRUE;
  }
  if (!(state & GDK_CONTROL_MASK) ||
      (keyval != GDK_KEY_n && keyval != GDK_KEY_p))
    return FALSE;

  GList *selected = gtk_flow_box_get_selected_children(picker->flow_box);
  GtkFlowBoxChild *current = selected ? selected->data : NULL;
  gint index = current ? gtk_flow_box_child_get_index(current) : 0;
  gint next = index + (keyval == GDK_KEY_n ? 1 : -1);
  if (next < 0)
    next = picker->wallpapers->len - 1;
  else if (next >= (gint)picker->wallpapers->len)
    next = 0;

  GtkFlowBoxChild *child =
      gtk_flow_box_get_child_at_index(picker->flow_box, next);
  if (child) {
    gtk_flow_box_select_child(picker->flow_box, child);
    gtk_widget_grab_focus(GTK_WIDGET(child));
  }
  g_list_free(selected);
  return TRUE;
}

static gboolean play_video_preview(gpointer user_data) {
  WallpaperPicker *picker = user_data;
  picker->video_preview_timeout_id = 0;

  GList *selected = gtk_flow_box_get_selected_children(picker->flow_box);
  GtkFlowBoxChild *child = selected ? selected->data : NULL;
  Wallpaper *wallpaper =
      child ? g_object_get_data(G_OBJECT(child), "wallpaper") : NULL;
  if (wallpaper && has_video_extension(wallpaper->path)) {
    GFile *file = g_file_new_for_path(wallpaper->path);
    picker->media_stream = gtk_media_file_new_for_file(file);
    gtk_media_stream_set_muted(picker->media_stream, TRUE);
    gtk_media_stream_set_loop(picker->media_stream, TRUE);
    gtk_picture_set_paintable(picker->preview,
                              GDK_PAINTABLE(picker->media_stream));
    gtk_media_stream_play(picker->media_stream);
    g_object_unref(file);
  }
  g_list_free(selected);
  return G_SOURCE_REMOVE;
}

typedef struct {
  gchar *path;
  Wallpaper *wallpaper;
  guint window_generation;
} PreviewRequest;

static void preview_request_free(gpointer data) {
  PreviewRequest *request = data;
  g_free(request->path);
  g_free(request);
}

static void load_preview_async(GTask *task, gpointer source_object,
                               gpointer task_data, GCancellable *cancellable) {
  (void)source_object;
  (void)cancellable;
  PreviewRequest *request = task_data;
  GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file(request->path, NULL);
  g_task_return_pointer(task, pixbuf, g_object_unref);
}

static void load_preview_finished(GObject *source_object, GAsyncResult *result,
                                  gpointer user_data) {
  (void)user_data;
  WallpaperPicker *picker = (WallpaperPicker *)source_object;
  PreviewRequest *request = g_task_get_task_data(G_TASK(result));
  GdkPixbuf *pixbuf = g_task_propagate_pointer(G_TASK(result), NULL);
  GtkWindow *window = g_weak_ref_get(&picker->window);
  if (!window || request->window_generation != picker->window_generation) {
    g_clear_object(&window);
    g_clear_object(&pixbuf);
    return;
  }
  g_object_unref(window);
  request->wallpaper->preview_loading = FALSE;
  GList *selected = gtk_flow_box_get_selected_children(picker->flow_box);
  GtkFlowBoxChild *child = selected ? selected->data : NULL;
  gint index = child ? gtk_flow_box_child_get_index(child) : -1;
  gboolean current = child && g_object_get_data(G_OBJECT(child), "wallpaper") ==
                                  request->wallpaper;
  gboolean nearby = current;
  for (gint offset = -1; index >= 0 && offset <= 1; offset += 2) {
    GtkFlowBoxChild *neighbor =
        gtk_flow_box_get_child_at_index(picker->flow_box, index + offset);
    if (neighbor && g_object_get_data(G_OBJECT(neighbor), "wallpaper") ==
                        request->wallpaper)
      nearby = TRUE;
  }
  if (pixbuf && nearby) {
    GBytes *bytes = gdk_pixbuf_read_pixel_bytes(pixbuf);
    GdkMemoryFormat format = gdk_pixbuf_get_has_alpha(pixbuf)
                                 ? GDK_MEMORY_R8G8B8A8
                                 : GDK_MEMORY_R8G8B8;
    GdkTexture *texture = gdk_memory_texture_new(
        gdk_pixbuf_get_width(pixbuf), gdk_pixbuf_get_height(pixbuf), format,
        bytes, gdk_pixbuf_get_rowstride(pixbuf));
    g_bytes_unref(bytes);
    request->wallpaper->preview_texture = texture;
    g_queue_push_tail(picker->preview_cache, request->wallpaper);
    if (g_queue_get_length(picker->preview_cache) > 3) {
      Wallpaper *old = g_queue_pop_head(picker->preview_cache);
      g_clear_object(&old->preview_texture);
    }
    if (current && !picker->media_stream)
      gtk_picture_set_paintable(picker->preview, GDK_PAINTABLE(texture));
  }
  g_list_free(selected);
  if (pixbuf)
    g_object_unref(pixbuf);
}

static void start_preview_load(WallpaperPicker *picker, Wallpaper *wallpaper,
                               gint priority) {
  if (!wallpaper->thumbnail || wallpaper->preview_texture ||
      wallpaper->preview_loading)
    return;
  PreviewRequest *request = g_new0(PreviewRequest, 1);
  request->path = g_strdup(wallpaper->thumbnail);
  request->wallpaper = wallpaper;
  request->window_generation = picker->window_generation;
  wallpaper->preview_loading = TRUE;
  GTask *task = g_task_new(picker, NULL, load_preview_finished, NULL);
  g_task_set_task_data(task, request, preview_request_free);
  g_task_set_priority(task, priority);
  g_task_run_in_thread(task, load_preview_async);
  g_object_unref(task);
}

static void update_preview(WallpaperPicker *picker) {
  GList *selected = gtk_flow_box_get_selected_children(picker->flow_box);
  GtkFlowBoxChild *child = selected ? selected->data : NULL;
  if (!child) {
    g_list_free(selected);
    return;
  }

  Wallpaper *wallpaper = g_object_get_data(G_OBJECT(child), "wallpaper");
  gtk_picture_set_paintable(picker->preview,
                            wallpaper->preview_texture
                                ? GDK_PAINTABLE(wallpaper->preview_texture)
                                : NULL);
  if (wallpaper->preview_texture) {
    g_queue_remove(picker->preview_cache, wallpaper);
    g_queue_push_tail(picker->preview_cache, wallpaper);
  } else if (wallpaper->thumbnail) {
    start_preview_load(picker, wallpaper, G_PRIORITY_HIGH);
  }
  if (has_video_extension(wallpaper->path))
    picker->video_preview_timeout_id =
        g_timeout_add(600, play_video_preview, picker);
  g_list_free(selected);
}

typedef struct {
  gchar *thumbnail;
  GdkPixbuf *pixbuf;
} ThumbnailResult;

static void thumbnail_result_free(gpointer data) {
  ThumbnailResult *result = data;
  g_free(result->thumbnail);
  g_clear_object(&result->pixbuf);
  g_free(result);
}

static void load_thumbnail_async(GTask *task, gpointer source_object,
                                 gpointer task_data,
                                 GCancellable *cancellable) {
  (void)source_object;
  (void)cancellable;
  Wallpaper *wallpaper = task_data;
  ThumbnailResult *result = g_new0(ThumbnailResult, 1);
  result->thumbnail = wallpaper->thumbnail
                          ? g_strdup(wallpaper->thumbnail)
                          : make_video_thumbnail(wallpaper->path);
  if (result->thumbnail)
    result->pixbuf = gdk_pixbuf_new_from_file_at_scale(
        result->thumbnail, THUMB_WIDTH * 2, 120, TRUE, NULL);
  g_task_return_pointer(task, result, thumbnail_result_free);
}

static void load_thumbnail_finished(GObject *source_object,
                                    GAsyncResult *result, gpointer user_data) {
  (void)user_data;
  WallpaperPicker *picker = (WallpaperPicker *)source_object;
  Wallpaper *wallpaper = g_task_get_task_data(G_TASK(result));
  ThumbnailResult *thumbnail = g_task_propagate_pointer(G_TASK(result), NULL);
  GtkWindow *window = g_weak_ref_get(&picker->window);
  if (!window || GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(result),
                                                    "window-generation")) !=
                     picker->window_generation) {
    g_clear_object(&window);
    if (thumbnail)
      thumbnail_result_free(thumbnail);
    return;
  }
  g_object_unref(window);
  if (!thumbnail)
    return;

  if (thumbnail->thumbnail && !wallpaper->thumbnail) {
    wallpaper->thumbnail = thumbnail->thumbnail;
    thumbnail->thumbnail = NULL;
  }
  if (thumbnail->pixbuf) {
    GBytes *bytes = gdk_pixbuf_read_pixel_bytes(thumbnail->pixbuf);
    GdkMemoryFormat format = gdk_pixbuf_get_has_alpha(thumbnail->pixbuf)
                                 ? GDK_MEMORY_R8G8B8A8
                                 : GDK_MEMORY_R8G8B8;
    wallpaper->small_texture = gdk_memory_texture_new(
        gdk_pixbuf_get_width(thumbnail->pixbuf),
        gdk_pixbuf_get_height(thumbnail->pixbuf), format, bytes,
        gdk_pixbuf_get_rowstride(thumbnail->pixbuf));
    g_bytes_unref(bytes);
    GtkPicture *picture = g_weak_ref_get(&wallpaper->picture);
    if (picture) {
      gtk_picture_set_paintable(picture,
                                GDK_PAINTABLE(wallpaper->small_texture));
      g_object_unref(picture);
    }
  }

  GList *selected = gtk_flow_box_get_selected_children(picker->flow_box);
  GtkFlowBoxChild *child = selected ? selected->data : NULL;
  if (child && g_object_get_data(G_OBJECT(child), "wallpaper") == wallpaper &&
      !picker->media_stream) {
    if (wallpaper->thumbnail && has_video_extension(wallpaper->path) &&
        !wallpaper->preview_loading && !wallpaper->preview_texture)
      start_preview_load(picker, wallpaper, G_PRIORITY_HIGH);
  }
  g_list_free(selected);
  thumbnail_result_free(thumbnail);
}

static gboolean prefetch_adjacent(gpointer user_data) {
  WallpaperPicker *picker = user_data;
  picker->prefetch_timeout_id = 0;
  GList *selected = gtk_flow_box_get_selected_children(picker->flow_box);
  GtkFlowBoxChild *child = selected ? selected->data : NULL;
  if (child) {
    gint index = gtk_flow_box_child_get_index(child);
    for (gint offset = -1; offset <= 1; offset += 2) {
      GtkFlowBoxChild *neighbor =
          gtk_flow_box_get_child_at_index(picker->flow_box, index + offset);
      if (neighbor) {
        Wallpaper *wallpaper =
            g_object_get_data(G_OBJECT(neighbor), "wallpaper");
        start_preview_load(picker, wallpaper, G_PRIORITY_LOW);
      }
    }
  }
  g_list_free(selected);
  return G_SOURCE_REMOVE;
}

static void on_window_width_changed(GObject *object, GParamSpec *pspec,
                                    gpointer user_data) {
  (void)pspec;
  GtkWidget *window = GTK_WIDGET(object);
  GtkWidget *wallpaper_list = user_data;
  gint width = gtk_widget_get_width(window);
  if (width > 0)
    gtk_widget_set_size_request(wallpaper_list, width / 10, -1);
}

static void on_selection_changed(GtkFlowBox *flow_box, gpointer user_data) {
  (void)flow_box;
  WallpaperPicker *picker = user_data;
  if (!picker->flow_box)
    return;
  if (picker->video_preview_timeout_id) {
    g_source_remove(picker->video_preview_timeout_id);
    picker->video_preview_timeout_id = 0;
  }
  if (picker->prefetch_timeout_id)
    g_source_remove(picker->prefetch_timeout_id);
  g_clear_object(&picker->media_stream);
  update_preview(picker);
  picker->prefetch_timeout_id = g_timeout_add(150, prefetch_adjacent, picker);
}

static void on_wallpaper_activated(GtkFlowBox *flow_box, GtkFlowBoxChild *child,
                                   gpointer user_data) {
  (void)flow_box;
  Wallpaper *wallpaper = g_object_get_data(G_OBJECT(child), "wallpaper");
  g_print("%s\n", wallpaper->path);
  g_application_quit(G_APPLICATION(user_data));
}

static void add_wallpaper(WallpaperPicker *picker, gchar *path,
                          gchar *thumbnail) {
  Wallpaper *wallpaper = g_new0(Wallpaper, 1);
  wallpaper->path = path;
  wallpaper->thumbnail = thumbnail;
  g_weak_ref_init(&wallpaper->picture, NULL);
  g_ptr_array_add(picker->wallpapers, wallpaper);

  GtkWidget *child = gtk_flow_box_child_new();
  gtk_flow_box_child_set_child(GTK_FLOW_BOX_CHILD(child),
                               wallpaper_widget(wallpaper));
  g_object_set_data(G_OBJECT(child), "wallpaper", wallpaper);
  gtk_flow_box_append(picker->flow_box, child);
  if (picker->wallpapers->len == 1)
    gtk_flow_box_select_child(picker->flow_box, GTK_FLOW_BOX_CHILD(child));

  GTask *task = g_task_new(picker, NULL, load_thumbnail_finished, NULL);
  g_object_set_data(G_OBJECT(task), "window-generation",
                    GUINT_TO_POINTER(picker->window_generation));
  g_task_set_task_data(task, wallpaper, NULL);
  g_task_run_in_thread(task, load_thumbnail_async);
  g_object_unref(task);
}

static gboolean scan_wallpapers(gpointer user_data) {
  WallpaperPicker *picker = user_data;
  GtkWindow *window = g_weak_ref_get(&picker->window);
  if (!window) {
    picker->scan_source_id = 0;
    return G_SOURCE_REMOVE;
  }
  g_object_unref(window);
  if (!picker->scan_complete && !picker->wallpaper_dir) {
    picker->wallpaper_dir = g_dir_open(picker->directory, 0, NULL);
    if (!picker->wallpaper_dir) {
      picker->scan_source_id = 0;
      g_printerr("Could not open wallpaper directory '%s'\n",
                 picker->directory);
      g_application_quit(G_APPLICATION(picker));
      return G_SOURCE_REMOVE;
    }
  }

  if (!picker->scan_complete) {
    for (guint i = 0; i < 8; i++) {
      const gchar *name = g_dir_read_name(picker->wallpaper_dir);
      if (!name) {
        g_dir_close(picker->wallpaper_dir);
        picker->wallpaper_dir = NULL;
        picker->scan_complete = TRUE;
        break;
      }

      gchar *path = g_build_filename(picker->directory, name, NULL);
      if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) {
        g_free(path);
        continue;
      }

      if (has_video_extension(path)) {
        gchar *thumbnail = thumbnail_path(path);
        if (g_file_test(thumbnail, G_FILE_TEST_EXISTS)) {
          add_wallpaper(picker, path, thumbnail);
          continue;
        }
        g_free(thumbnail);
        g_queue_push_tail(picker->pending_paths, path);
      } else if (gdk_pixbuf_get_file_info(path, NULL, NULL)) {
        add_wallpaper(picker, path, g_strdup(path));
      } else {
        g_free(path);
      }
    }
    return G_SOURCE_CONTINUE;
  }

  for (guint i = 0; i < 8 && !g_queue_is_empty(picker->pending_paths); i++) {
    gchar *path = g_queue_pop_head(picker->pending_paths);
    add_wallpaper(picker, path, NULL);
  }

  if (g_queue_is_empty(picker->pending_paths)) {
    picker->scan_source_id = 0;
    if (picker->wallpapers->len == 0) {
      g_printerr("No supported images or videos found in '%s'\n",
                 picker->directory);
      g_application_quit(G_APPLICATION(picker));
    }
    return G_SOURCE_REMOVE;
  }
  return G_SOURCE_CONTINUE;
}

static gboolean on_window_close_request(GtkWindow *window, gpointer user_data) {
  WallpaperPicker *picker = user_data;
  GtkWindow *current = g_weak_ref_get(&picker->window);
  gboolean active = current == window;
  g_clear_object(&current);
  if (!active)
    return FALSE;

  g_weak_ref_set(&picker->window, NULL);
  picker->window_generation++;
  picker->flow_box = NULL;
  picker->preview = NULL;
  if (picker->scan_source_id) {
    g_source_remove(picker->scan_source_id);
    picker->scan_source_id = 0;
  }
  if (picker->wallpaper_dir) {
    g_dir_close(picker->wallpaper_dir);
    picker->wallpaper_dir = NULL;
  }
  if (picker->video_preview_timeout_id) {
    g_source_remove(picker->video_preview_timeout_id);
    picker->video_preview_timeout_id = 0;
  }
  if (picker->prefetch_timeout_id) {
    g_source_remove(picker->prefetch_timeout_id);
    picker->prefetch_timeout_id = 0;
  }
  g_clear_object(&picker->media_stream);
  return FALSE;
}

static void wallpaper_picker_activate(GApplication *application) {
  WallpaperPicker *picker = (WallpaperPicker *)application;
  GtkWindow *existing = g_weak_ref_get(&picker->window);
  if (existing) {
    gtk_window_present(existing);
    g_object_unref(existing);
    return;
  }
  if (!picker->directory) {
    g_printerr("Usage: wallpaper-picker DIRECTORY\n");
    g_application_quit(application);
    return;
  }

  GtkWidget *window = gtk_application_window_new(GTK_APPLICATION(application));
  g_weak_ref_set(&picker->window, window);
  picker->window_generation++;
  g_signal_connect(window, "close-request", G_CALLBACK(on_window_close_request),
                   picker);
  gtk_window_set_title(GTK_WINDOW(window), "Wallpaper Picker");
  gtk_window_set_default_size(GTK_WINDOW(window), 1200, 760);

  GtkWidget *main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
  gtk_widget_set_margin_start(main_box, 16);
  gtk_widget_set_margin_end(main_box, 16);
  gtk_widget_set_margin_top(main_box, 16);
  gtk_widget_set_margin_bottom(main_box, 16);
  gtk_window_set_child(GTK_WINDOW(window), main_box);

  GtkWidget *content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 16);
  gtk_widget_set_vexpand(content, TRUE);
  gtk_box_append(GTK_BOX(main_box), content);

  GtkEventController *keys = gtk_event_controller_key_new();
  g_signal_connect(keys, "key-pressed", G_CALLBACK(on_key_pressed), picker);
  gtk_widget_add_controller(window, keys);

  GtkWidget *scrolled = gtk_scrolled_window_new();
  gtk_widget_set_size_request(scrolled, 120, -1);
  gtk_widget_set_hexpand(scrolled, FALSE);
  gtk_widget_set_vexpand(scrolled, TRUE);
  gtk_box_append(GTK_BOX(content), scrolled);

  picker->flow_box = GTK_FLOW_BOX(gtk_flow_box_new());
  gtk_flow_box_set_selection_mode(picker->flow_box, GTK_SELECTION_SINGLE);
  gtk_flow_box_set_sort_func(picker->flow_box, compare_wallpaper_children, NULL,
                             NULL);
  gtk_flow_box_set_activate_on_single_click(picker->flow_box, FALSE);
  gtk_flow_box_set_max_children_per_line(picker->flow_box, 1);
  gtk_flow_box_set_min_children_per_line(picker->flow_box, 1);
  gtk_flow_box_set_column_spacing(picker->flow_box, 12);
  gtk_flow_box_set_row_spacing(picker->flow_box, 12);
  gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrolled),
                                GTK_WIDGET(picker->flow_box));

  GtkWidget *preview_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
  gtk_widget_set_size_request(preview_box, 420, -1);
  gtk_widget_set_hexpand(preview_box, FALSE);
  gtk_box_append(GTK_BOX(content), preview_box);

  picker->preview = GTK_PICTURE(gtk_picture_new());
  gtk_picture_set_content_fit(picker->preview, GTK_CONTENT_FIT_CONTAIN);
  gtk_picture_set_can_shrink(picker->preview, TRUE);
  gtk_widget_set_size_request(GTK_WIDGET(picker->preview), 420, -1);
  gtk_widget_set_hexpand(GTK_WIDGET(picker->preview), FALSE);
  gtk_widget_set_vexpand(GTK_WIDGET(picker->preview), TRUE);
  gtk_box_append(GTK_BOX(preview_box), GTK_WIDGET(picker->preview));

  GtkWidget *hint = gtk_label_new(
      "Arrow keys to navigate · Enter to select · Escape to cancel");
  gtk_widget_add_css_class(hint, "dim-label");
  gtk_box_append(GTK_BOX(main_box), hint);

  g_signal_connect(window, "notify::width", G_CALLBACK(on_window_width_changed),
                   scrolled);
  on_window_width_changed(G_OBJECT(window), NULL, scrolled);

  g_signal_connect(picker->flow_box, "selected-children-changed",
                   G_CALLBACK(on_selection_changed), picker);
  g_signal_connect(picker->flow_box, "child-activated",
                   G_CALLBACK(on_wallpaper_activated), picker);
  gtk_window_present(GTK_WINDOW(window));
  picker->scan_source_id = g_timeout_add(10, scan_wallpapers, picker);
}

static void wallpaper_picker_open(GApplication *application, GFile **files,
                                  gint n_files, const gchar *hint) {
  (void)hint;
  WallpaperPicker *picker = (WallpaperPicker *)application;
  GtkWindow *existing = g_weak_ref_get(&picker->window);
  if (existing) {
    gtk_window_present(existing);
    g_object_unref(existing);
    return;
  }
  if (n_files > 0) {
    g_free(picker->directory);
    picker->directory = g_file_get_path(files[0]);
  }
  wallpaper_picker_activate(application);
}

static void wallpaper_picker_finalize(GObject *object) {
  WallpaperPicker *picker = (WallpaperPicker *)object;
  g_weak_ref_clear(&picker->window);
  g_free(picker->directory);
  if (picker->scan_source_id)
    g_source_remove(picker->scan_source_id);
  if (picker->wallpaper_dir)
    g_dir_close(picker->wallpaper_dir);
  g_queue_free_full(picker->pending_paths, g_free);
  g_queue_free(picker->preview_cache);
  if (picker->video_preview_timeout_id)
    g_source_remove(picker->video_preview_timeout_id);
  if (picker->prefetch_timeout_id)
    g_source_remove(picker->prefetch_timeout_id);
  g_clear_object(&picker->media_stream);
  g_ptr_array_unref(picker->wallpapers);
  G_OBJECT_CLASS(wallpaper_picker_parent_class)->finalize(object);
}

static void wallpaper_picker_class_init(WallpaperPickerClass *class) {
  GObjectClass *object_class = G_OBJECT_CLASS(class);
  object_class->finalize = wallpaper_picker_finalize;
}

static void wallpaper_picker_init(WallpaperPicker *picker) {
  g_weak_ref_init(&picker->window, NULL);
  picker->wallpapers = g_ptr_array_new_with_free_func(wallpaper_free);
  picker->pending_paths = g_queue_new();
  picker->preview_cache = g_queue_new();
}

int main(int argc, char **argv) {
  WallpaperPicker *picker =
      g_object_new(wallpaper_picker_get_type(), "application-id", APP_ID,
                   "flags", G_APPLICATION_HANDLES_OPEN, NULL);
  g_signal_connect(picker, "activate", G_CALLBACK(wallpaper_picker_activate),
                   NULL);
  g_signal_connect(picker, "open", G_CALLBACK(wallpaper_picker_open), NULL);
  int status = g_application_run(G_APPLICATION(picker), argc, argv);
  g_object_unref(picker);
  return status;
}

README.md

wallpaper-picker/README.md · 56 lines

# Wallpaper Picker

A small GTK4 wallpaper picker for Sway. It accepts a wallpaper directory, displays image
and video thumbnails, plays the selected video in the preview pane, and lets you navigate with
the keyboard.

## Usage

```bash
wallpaper-picker ~/Wallpapers
```

- Arrow keys or `Ctrl+N`/`Ctrl+P`: navigate wallpapers
- Enter: print the selected wallpaper path and exit
- Escape: cancel

The selected path is written to stdout, so it can be composed with other commands:

```bash
selected=$(wallpaper-picker "$HOME/Wallpapers")
[[ -n "$selected" ]] && printf '%s\n' "$selected"
```

Video thumbnails are generated with FFmpeg and cached in:

```text
~/.cache/wallpaper-picker/
```

## Build with Nix

Build without installing:

```bash
nix-build -E 'with import <nixpkgs> {}; callPackage ./wallpaper-picker {}'
```

Or build the derivation directly from this directory:

```bash
nix-build default.nix
./result/bin/wallpaper-picker ~/Wallpapers
```

The package wraps FFmpeg so video thumbnails work at runtime.

## Local build

```bash
nix-shell -p gcc pkg-config gtk4 gdk-pixbuf glib ffmpeg \
  gst_all_1.gstreamer gst_all_1.gst-plugins-base gst_all_1.gst-plugins-good \
  gst_all_1.gst-plugins-bad gst_all_1.gst-libav --run bash
cc -O3 -Wall -Wextra -o wallpaper-picker main.c \
  $(pkg-config --cflags --libs gtk4 gdk-pixbuf-2.0)
./wallpaper-picker ~/Wallpapers
```

Waybar

config.jsonc

waybar/config.jsonc · 217 lines

{
  // "layer": "top", // Waybar at top layer
  "position": "top", // Waybar position (top|bottom|left|right)
  // Height is calculated automatically from the content and CSS.
  "spacing": 4, // Gaps between modules (4px)
  // Choose the order of the modules
  "modules-left": [
    "custom/power",
    "sway/workspaces",
    "sway/mode",
    "sway/window",
  ],
  "modules-center": ["mpris"],
  "modules-right": [
    "tray",
    "idle_inhibitor",
    "custom/night-light",
    "pulseaudio",
    "pulseaudio#microphone",
    "privacy",
    "custom/vnc-session",
    "bluetooth",
    "custom/vpn",
    "network",
    // "cpu",
    // "memory",
    // "custom/gpu",
    "temperature",
    "battery",
    "custom/wallpaper",
    "clock",
    "custom/notification"
  ],
  "sway/workspaces": {
    "format": "{name}",
    "all-outputs": true,
    "persistent-workspaces": {
      "1": [],
      "2": [],
      "3": [],
      "4": [],
      "5": []
    }
  },
  "idle_inhibitor": {
    "format": "{icon}",
    "format-icons": {
      "activated": "",
      "deactivated": ""
    }
  },
  "tray": {
    "icon-size": 13,
    "spacing": 10
  },
  "clock": {
    "format": "{:%a %d %b %I:%M %p}",
    "tooltip-format": "<tt><small>{calendar}</small></tt>",
    "calendar": {
      "format": {
        "today": "<span color='#75f1fa'><b>{}</b></span>"
      }
    },
  },
  "cpu": {
    "format": "{usage}% ",
    "tooltip": true
  },
  "memory": {
    "format": "{}% "
  },
  "custom/gpu": {
    "exec": "~/.config/waybar/scripts/gpu.sh",
    "return-type": "json",
    "interval": 2,
    "format": "🎮 {}"
  },
  "mpris": {
    "player": "spotify",
    "format": "{artist} - {title}",
    "format-paused": "{artist} - {title}",
    "format-stopped": "",
    "tooltip": false,
    "max-length": 40,
    "on-click": "eww open spotify-card --toggle"
  },
  "custom/power": {
    "format": "\uf313",
    "tooltip-format": "Power menu",
    "on-click": "~/.config/sway/scripts/wlogout-menu.sh"
  },
  "custom/wallpaper": {
    "format": "󰸉",
    "tooltip-format": "Change wallpaper",
    "on-click": "systemctl --user start wallpaper-rotate.service"
  },
  "custom/night-light": {
    "exec": "~/.config/waybar/scripts/night-light.sh status",
    "return-type": "json",
    "interval": 2,
    "format": "{}",
    "on-click": "~/.config/waybar/scripts/night-light.sh toggle"
  },
  "custom/notification": {
    "tooltip": true,
    "format": "{icon}",
    "format-icons": {
      "notification": "󱅫",
      "none": "󰂜",
      "dnd-notification": "󰂠",
      "dnd-none": "󰪓",
      "inhibited-notification": "󰂛",
      "inhibited-none": "󰪑",
      "dnd-inhibited-notification": "󰂛",
      "dnd-inhibited-none": "󰪑"
    },
    "return-type": "json",
    "exec": "swaync-client -swb",
    "on-click": "swaync-client -t -sw",
    "on-click-right": "swaync-client -d -sw",
    "escape": true
  },
  "temperature": {
    "critical-threshold": 80,
    "format": "{temperatureC}°C {icon}",
    "format-icons": ["", "", ""]
  },
  "battery": {
    "states": {
      "good": 95,
      "warning": 30,
      "critical": 15
    },
    "format": "{capacity}% {icon}",
    "format-full": "{capacity}% {icon}",
    "format-charging": "{capacity}% ",
    "format-plugged": "{capacity}% ",
    "format-alt": "{time} {icon}",
    "format-icons": ["", "", "", "", ""]
  },
  "bluetooth": {
    "format": "",
    "format-disabled": "",
    "format-off": "",
    "format-connected": " {num_connections}",
    "on-click": "blueman-manager",
    "tooltip-format": "{controller_alias}: {status}",
    "tooltip-format-connected": "{controller_alias}: {status}\n\n{device_enumerate}",
    "tooltip-format-enumerate-connected": "{device_alias}"
  },
  "custom/vpn": {
    "exec": "~/.config/waybar/scripts/vpn.sh",
    "return-type": "json",
    "interval": 5,
    "format": "{}"
  },
  "custom/vnc-session": {
    "exec": "~/.config/waybar/scripts/vnc-session.sh",
    "return-type": "json",
    "restart-interval": 2,
    "format": "{}",
    "on-click": "~/.config/waybar/scripts/vnc-toggle.sh"
  },
  "network": {
    // "interface": "wlp2*", // (Optional) To force the use of this interface
    "format-wifi": "",
    "format-ethernet": "󰈀",
    "tooltip-format": "{ifname}\nIP: {ipaddr}",
    "tooltip-format-wifi": "{ifname}: {essid}\nSignal: {signalStrength}%\nIP: {ipaddr}",
    "tooltip-format-ethernet": "{ifname}\nIP: {ipaddr}",
    "format-linked": "󰈀",
    "format-disconnected": "󰖪",
    "format-alt": "",
    "on-click": "nm-connection-editor"
  },
  "pulseaudio#microphone": {
    "format": "{format_source}",
    "format-source": "",
    "format-source-muted": "",
    "tooltip": false,
    "on-click": "pamixer --default-source --toggle-mute"
  },
  "privacy": {
    "icon-spacing": 4,
    "icon-size": 13,
    "transition-duration": 250,
    "modules": [
      {
        "type": "screenshare",
        "tooltip": true,
        "tooltip-icon-size": 24
      },
      {
        "type": "audio-in",
        "tooltip": true,
        "tooltip-icon-size": 24
      }
    ]
  },
  "pulseaudio": {
    // "scroll-step": 1, // %, can be a float
    "format": "{volume}% {icon}",
    "format-bluetooth": "{volume}% {icon}",
    "format-bluetooth-muted": "󰝟 {icon}",
    "format-muted": "󰝟",
    "format-icons": {
      "headphone": "",
      "hands-free": "󰋎",
      "headset": "󰋎",
      "phone": "",
      "portable": "",
      "car": "",
      "default": ["", "", ""]
    },
    "on-click": "pavucontrol"
  }
}

gpu.sh

waybar/scripts/gpu.sh · 9 lines

#!/usr/bin/env bash
if [ -f /sys/class/drm/card1/device/gpu_busy_percent ]; then
  u=$(cat /sys/class/drm/card1/device/gpu_busy_percent)
elif command -v nvidia-smi >/dev/null; then
  u=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits)
else
  u=0
fi
echo "{\"text\": \"${u}%\", \"tooltip\": \"GPU: ${u}%\"}"

night-light.sh

waybar/scripts/night-light.sh · 27 lines

#!/usr/bin/env bash
set -euo pipefail

is_running() {
  pgrep -f '(^|/)gammastep([[:space:]]|$)' >/dev/null
}

case "${1:-status}" in
status)
  if is_running; then
    printf '{"text":"","tooltip":"Automatic night light enabled","class":"enabled"}\n'
  else
    printf '{"text":"","tooltip":"Automatic night light disabled","class":"disabled"}\n'
  fi
  ;;
toggle)
  if is_running; then
    pkill -f '(^|/)gammastep([[:space:]]|$)'
  else
    swaymsg exec 'gammastep -m wayland -l geoclue2 -t 6500:4000' >/dev/null
  fi
  ;;
*)
  echo "Usage: $0 [status|toggle]" >&2
  exit 2
  ;;
esac

vnc-session.sh

waybar/scripts/vnc-session.sh · 38 lines

#!/usr/bin/env bash
set -euo pipefail

emit_status() {
  local clients count tooltip

  if ! clients=$(wayvncctl --json client-list 2>/dev/null); then
    jq -cn '{text: "󰢹", tooltip: "WayVNC stopped — click to start", class: "stopped"}'
    return
  fi
  if ! count=$(jq -r 'if type == "array" then length else 0 end' <<<"$clients" 2>/dev/null); then
    count=0
  fi

  if ((count > 0)); then
    tooltip=$(jq -r '
      .[]
      | if .username then
          "\(.address // "Unknown address") (\(.username))"
        else
          (.address // "Unknown client")
        end
    ' <<<"$clients")
    jq -cn --arg count "$count" --arg tooltip "$tooltip" \
      '{text: ("󰢹 " + $count), tooltip: ("Active VNC sessions\n" + $tooltip + "\nClick to stop"), class: "active"}'
  else
    jq -cn '{text: "󰢹", tooltip: "WayVNC running — no active sessions\nClick to stop", class: "running"}'
  fi
}

emit_status
while IFS= read -r event; do
  case $(jq -r '.method // empty' <<<"$event" 2>/dev/null) in
  client-connected | client-disconnected | wayvnc-startup | wayvnc-shutdown)
    emit_status
    ;;
  esac
done < <(wayvncctl --wait --reconnect --json event-receive 2>/dev/null)

vnc-toggle.sh

waybar/scripts/vnc-toggle.sh · 23 lines

#!/usr/bin/env bash
set -euo pipefail

if wayvncctl --json version >/dev/null 2>&1; then
  if systemctl --user is-active --quiet wayvnc.service; then
    systemctl --user stop wayvnc.service
  else
    # Also stop WayVNC instances started manually before the service existed.
    wayvncctl wayvnc-exit >/dev/null
  fi
  notify-send -a "WayVNC" -i preferences-desktop-remote-desktop -u low -t 3000 \
    "WayVNC stopped" "Remote desktop access is disabled."
  exit 0
fi

if ! systemctl --user start wayvnc.service; then
  notify-send -a "WayVNC" -i dialog-error -u critical \
    "WayVNC could not start" "Check the WayVNC user service logs."
  exit 1
fi

notify-send -a "WayVNC" -i preferences-desktop-remote-desktop -u normal -t 4000 \
  "WayVNC started" "Listening on 0.0.0.0:5901 using wayland-1."

vpn.sh

waybar/scripts/vpn.sh · 50 lines

#!/usr/bin/env bash
set -uo pipefail

STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/waybar"
STATE_FILE="$STATE_DIR/vpn-state"
connections=()

while IFS=: read -r type name; do
  case "$type" in
  vpn | wireguard)
    [[ -n "$name" ]] && connections+=("$name")
    ;;
  esac
done < <(nmcli -t --escape no -f TYPE,NAME connection show --active 2>/dev/null)

if command -v tailscale >/dev/null 2>&1; then
  tailscale_state=$(timeout 2s tailscale status --json 2>/dev/null | jq -r '.BackendState // "Stopped"' 2>/dev/null)
  if [[ "$tailscale_state" == "Running" ]]; then
    connections+=("Tailscale")
  fi
fi

if ((${#connections[@]})); then
  tooltip=$(printf '%s\n' "${connections[@]}")
  current_state="connected:$tooltip"
else
  tooltip=""
  current_state="disconnected"
fi

previous_state=""
[[ -r "$STATE_FILE" ]] && previous_state=$(<"$STATE_FILE")
if [[ -n "$previous_state" && "$previous_state" != "$current_state" ]]; then
  if ((${#connections[@]})); then
    notify-send -a "VPN" -i network-vpn -u normal -t 4000 \
      "VPN connected" "$tooltip"
  else
    notify-send -a "VPN" -i network-vpn-disconnected -u normal -t 4000 \
      "VPN disconnected" "No VPN connection is active."
  fi
fi
mkdir -p "$STATE_DIR"
printf '%s\n' "$current_state" >"$STATE_FILE"

if ((${#connections[@]})); then
  jq -cn --arg tooltip "$tooltip" \
    '{text: "", tooltip: ("VPN connected\n" + $tooltip), class: "connected"}'
else
  jq -cn '{text: "", tooltip: "VPN disconnected", class: "disconnected"}'
fi

styles.css

waybar/styles.css · 270 lines

* {
  /* `otf-font-awesome` is required to be installed for icons */
  font-family: "JetBrainsMono Nerd Font", FontAwesome, Roboto, Helvetica, Arial, sans-serif;
  font-size: 13px;
  padding: 1px;
  margin: 2px 0;
}

#workspaces button.active {
  color: peru;
  border-bottom: 3px solid peru;
}

#workspaces button.empty {
  color: lightgray;
}

window#waybar {
  background-color: rgba(43, 48, 59, 0.5);
  border-bottom: 3px solid rgba(100, 114, 125, 0.5);
  color: #ffffff;
  transition-property: background-color;
  transition-duration: .5s;
}

window#waybar.hidden {
  opacity: 0.2;
}

button {
  /* Use box-shadow instead of border so the text isn't offset */
  box-shadow: inset 0 -3px transparent;
  /* Avoid rounded borders under each button name */
  border: none;
  border-radius: 0;
}

/* https://github.com/Alexays/Waybar/wiki/FAQ#the-workspace-buttons-have-a-strange-hover-effect */
button:hover {
  background: inherit;
  box-shadow: inset 0 -3px #ffffff;
}

/* you can set a style on hover for any module like this */
#pulseaudio:hover {
  background-color: transparent;
}

#workspaces button {
  padding: 0 5px;
  background-color: transparent;
  color: #ffffff;
}

#workspaces button:hover {
  background: rgba(0, 0, 0, 0.2);
}

#workspaces button.focused {
  background-color: #64727d;
  box-shadow: inset 0 -3px #ffffff;
}

#workspaces button.urgent {
  background-color: #eb4d4b;
}

#mode {
  background-color: #64727d;
  box-shadow: inset 0 -3px #ffffff;
}

#clock,
#battery,
#temperature,
#bluetooth,
#network,
#pulseaudio,
#custom-vpn,
#tray,
#mode,
#idle_inhibitor {
  padding: 0 10px;
  color: #ffffff;
}

#window,
#workspaces {
  margin: 0 4px;
}

/* If workspaces is the leftmost module, omit left margin */
.modules-left > widget:first-child > #workspaces {
  margin-left: 0;
}

/* If workspaces is the rightmost module, omit right margin */
.modules-right > widget:last-child > #workspaces {
  margin-right: 0;
}

#clock {
  background-color: transparent;
  color: #ffffff;
}

#battery {
  background-color: transparent;
  color: #ffffff;
}

#battery.charging,
#battery.plugged {
  color: #ffffff;
  background-color: #26a65b;
}

@keyframes blink {
  to {
    background-color: #ffffff;
    color: #000000;
  }
}

/* Using steps() instead of linear as a timing function to limit cpu usage */
#battery.critical:not(.charging) {
  background-color: #f53c3c;
  color: #ffffff;
  animation-name: blink;
  animation-duration: 0.5s;
  animation-timing-function: steps(12);
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

label:focus {
  background-color: #000000;
}

#bluetooth {
  background-color: transparent;
  color: #ffffff;
}

#bluetooth.disabled,
#bluetooth.off {
  color: rgba(255, 255, 255, 0.35);
}

#bluetooth.connected {
  color: #75f1fa;
}

#network {
  background-color: transparent;
  color: #ffffff;
}

#network.disconnected {
  background-color: #f53c3c;
}

/* Session states: blue is active, muted is inactive, red needs attention. */
#custom-vpn,
#idle_inhibitor,
#custom-night-light,
#custom-notification {
  padding: 0 8px;
  background-color: transparent;
  color: rgba(255, 255, 255, 0.35);
}

#custom-vpn.connected,
#idle_inhibitor.activated,
#custom-night-light.enabled,
#custom-notification.notification,
#custom-notification.dnd-notification,
#custom-notification.dnd-none,
#custom-notification.inhibited-notification,
#custom-notification.inhibited-none,
#custom-notification.dnd-inhibited-notification,
#custom-notification.dnd-inhibited-none {
  color: #7ebae4;
}

#pulseaudio {
  background-color: transparent;
  color: #fff;
}

#pulseaudio.muted {
  background-color: #90b1b1;
  color: #2a5c45;
}

#pulseaudio.microphone.source-muted {
  background-color: transparent;
  color: #f53c3c;
}

#temperature {
  background-color: transparent;
  color: #ffffff;
}

#temperature.critical {
  background-color: #eb4d4b;
}

#tray {
  background-color: transparent;
  color: #ffffff;
}

#tray > .passive {
  -gtk-icon-effect: dim;
}

#tray > .needs-attention {
  -gtk-icon-effect: highlight;
  background-color: #eb4d4b;
}

#privacy {
  padding: 0;
}

#privacy-item {
  padding: 0 8px;
  color: #f53c3c;
  background-color: transparent;
}

#custom-vnc-session {
  padding: 0 8px;
  color: rgba(255, 255, 255, 0.35);
  background-color: transparent;
}

#custom-vnc-session.running {
  color: #7ebae4;
}

#custom-vnc-session.active {
  color: #2ecc71;
}

#mpris {
  color: #fff;
}
#mpris.paused {
  color: #fff;
}

#custom-power {
  font-family: "Symbols Nerd Font";
  padding: 0 10px;
  color: #7ebae4; /* NixOS blue */
}

#custom-wallpaper {
  font-family: "Symbols Nerd Font";
  padding: 0 8px;
  color: #7ebae4;
}

#custom-notification {
  font-family: "Symbols Nerd Font";
  padding: 0 8px;
}

Wlogout

layout

wlogout/layout · 36 lines

{
    "label" : "lock",
    "action" : "~/.config/sway/scripts/lockscreen.sh",
    "text" : "Lock",
    "keybind" : "l"
}
{
    "label" : "hibernate",
    "action" : "systemctl hibernate",
    "text" : "Hibernate",
    "keybind" : "h"
}
{
    "label" : "logout",
    "action" : "swaymsg exit",
    "text" : "Logout",
    "keybind" : "e"
}
{
    "label" : "shutdown",
    "action" : "systemctl poweroff",
    "text" : "Shutdown",
    "keybind" : "s"
}
{
    "label" : "suspend",
    "action" : "systemctl suspend",
    "text" : "Suspend",
    "keybind" : "u"
}
{
    "label" : "reboot",
    "action" : "systemctl reboot",
    "text" : "Reboot",
    "keybind" : "r"
}

style.css

wlogout/style.css · 58 lines

* {
  background-image: none;
  box-shadow: none;
  font-family: "JetBrainsMono Nerd Font";
}

window {
  background-color: rgba(10, 10, 10, 0.68);
}

button {
  margin: 6px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  color: #eeeeee;
  background-color: rgba(15, 15, 15, 0.9);
  background-repeat: no-repeat;
  background-position: center 38%;
  background-size: 42px;
  font-size: 13px;
  font-weight: 500;
}

button:hover,
button:focus {
  border-color: #7ebae4;
  color: #7ebae4;
  background-color: rgba(24, 38, 50, 0.96);
  outline-style: none;
}

button:active {
  background-color: rgba(36, 58, 76, 0.98);
}

#lock {
  background-image: -gtk-icontheme("system-lock-screen-symbolic");
}

#logout {
  background-image: -gtk-icontheme("system-log-out-symbolic");
}

#suspend {
  background-image: -gtk-icontheme("media-playback-pause-symbolic");
}

#hibernate {
  background-image: -gtk-icontheme("weather-clear-night-symbolic");
}

#shutdown {
  background-image: -gtk-icontheme("system-shutdown-symbolic");
}

#reboot {
  background-image: -gtk-icontheme("system-reboot-symbolic");
}

Zsh

linux.sh

zsh/linux.sh · 18 lines

#!/usr/bin/env bash

# Alias to use xclip like macOS
if [ "$XDG_SESSION_TYPE" = "wayland" ]; then
  alias pbcopy="wl-copy"
  alias pbpaste="wl-paste"
else
  alias pbcopy="xclip -selection clipboard"
  alias pbpaste="xclip -selection clipboard -o"
fi

nixos_run_install() {
  cd ~/dotfiles/nixos/ || exit 1
  # we need to use --impure because we're referencing absolute path
  # for hardware-configuration.nix file
  sudo nixos-rebuild --impure switch --flake .
  cd - || exit 1
}

mac.sh

zsh/mac.sh · 56 lines

#!/usr/bin/env bash
# postgres.app setup
export PATH=/usr/local/bin:$PATH:/Applications/Postgres.app/Contents/Versions/latest/bin

# Avoid auto update homwbrew packages
export HOMEBREW_NO_AUTO_UPDATE=1

# Setup gettext binaries
export PATH="/usr/local/opt/gettext/bin:$PATH"

# fix emoji and symbol pallete when it doesn't show up
fix_emoji_palette() {
  preferences_path="$HOME/Library/Preferences/com.apple.HIToolbox.plist"

  if [ -e "$preferences_path" ]; then
    rm "$preferences_path"
  fi
}

rae() {
  open "https://dle.rae.es/$1"
}

# use native macOS network check tool
alias speedtest=networkQuality

reset_cache() {
  sudo dscacheutil -flushcache
  sudo killall -HUP mDNSResponder
}

homebrew-dump() {
  cd ~/dotfiles/ || exit
  brew bundle dump --force
  git diff Brewfile
  cd - || exit
}

nixdarwin_run_install() {
  cd ~/dotfiles/nix-darwin/ || exit 1
  mkdir -p ~/.config/nix-darwin/
  cp *.nix ~/.config/nix-darwin/
  cp flake.lock ~/.config/nix-darwin/
  sudo darwin-rebuild switch --flake ~/.config/nix-darwin#simple
  # now we copy lock file back to dotfiles
  cp ~/.config/nix-darwin/flake.lock .
  cd - || exit 1
}

nixdarwin_free_space() {
  echo "collecting garbage..."
  nix-collect-garbage -d
  sudo nix-collect-garbage -d
  echo "optimizing storage, it might take a while..."
  nix-store --optimise
}

main.sh

zsh/main.sh · 254 lines

#!/usr/bin/env bash
# ZSH history file config
export HISTFILE="$HOME/.zsh_history"
export HISTSIZE=1000000
export SAVEHIST=1000000

# Remove older duplicates when a new duplicate is added
setopt HIST_IGNORE_ALL_DUPS
# Expire duplicates before unique entries when history is trimmed
setopt HIST_EXPIRE_DUPS_FIRST
# Avoid duplicate entries during history search
setopt HIST_FIND_NO_DUPS
# Omit older duplicates when writing the history file
setopt HIST_SAVE_NO_DUPS
# Ignore when prefixed with space
setopt HIST_IGNORE_SPACE
# Share history between sessions
setopt SHARE_HISTORY

# Load the default .profile
[[ -s "$HOME/.profile" ]] && source "$HOME/.profile"

# setup fzf
source <(fzf --zsh)
# Disable C-t which search into all files for the current folder to
# avoid press it by mistake
bindkey -r "^T"

# Use Emacs-style key bindings.
bindkey -e
# Load Zsh's edit-command-line widget.
autoload -Uz edit-command-line
# Register it as a ZLE widget.
zle -N edit-command-line
# Bind Ctrl-X Ctrl-E to edit the current command in $EDITOR.
bindkey '^X^E' edit-command-line

# helper functions

reload() {
  source "$HOME/.zshrc"
}

# Match directory completion without regard to letter case.
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

# Long listing alias.
alias ll="ls -lh"

# The `take` helpers below are adapted from Oh My Zsh's lib/functions.zsh:
# https://github.com/ohmyzsh/ohmyzsh/blob/master/lib/functions.zsh
takedir() {
  local target
  for target in "$@"; do
    :
  done
  mkdir -p -- "$@" && cd -- "$target"
}

takeurl() {
  local data thedir
  data="$(mktemp)"
  curl -L "$1" >"$data"
  tar xf "$data"
  thedir="$(tar tf "$data" | head -n 1)"
  rm "$data"
  cd -- "$thedir"
}

takezip() {
  local data thedir
  data="$(mktemp)"
  curl -L "$1" >"$data"
  unzip "$data" -d "./"
  thedir="$(unzip -l "$data" | awk 'NR==4 {print $4}' | sed 's|/.*||')"
  rm "$data"
  cd -- "$thedir"
}

takegit() {
  git clone "$1" && cd -- "$(basename -- "${1%.git}")"
}

take() {
  if [[ $1 =~ ^(https?|ftp).*\.(tar\.(gz|bz2|xz)|tgz)$ ]]; then
    takeurl "$1"
  elif [[ $1 =~ ^(https?|ftp).*\.(zip)$ ]]; then
    takezip "$1"
  elif [[ $1 =~ ^([A-Za-z0-9]+@|https?|git|ssh|ftps?|rsync).*\.git/?$ ]]; then
    takegit "$1"
  else
    takedir "$@"
  fi
}

t() {
  tree -I '.git|node_modules|bower_components|.DS_store' --dirsfirst -L "${1:-3}" -aC "$2"
}

docker_remove_containers() {
  docker rm "$(docker ps -a -q)"
}

random_password() {
  openssl rand -base64 "$1"
}

# Estimate the compressed size of a file (GZIP)
gzip_estimation() {
  gzip -9 -c "$1" | wc -c | awk '{$1=$1/1024; print "Estimated size:", $1, "Kb";}'
}

docker_remove_images() {
  docker rmi "$(docker images -q)"
}

git_search() {
  git grep "$1" "$(git rev-list --all)"
}

# Use ~/.ssh/config to prompt a list of host and ssh to the chosen one
ssh_connect() {
  local host
  host=$(grep "^Host " ~/.ssh/config | awk '{print $2}' | fzf --prompt="SSH Host: ")
  if [[ -n "$host" ]]; then
    echo "Connecting to $host..."
    ssh "$host"
  fi
}

# custom alias
alias got="ps aux | grep"
alias grep="grep --color=auto"
alias json="python -m json.tool"
alias pipgrep="pip freeze | grep"
alias py3="python3"
alias py="python"
alias vi="vim"
alias weather="curl http://wttr.in/Lima"
alias g="git"
alias wow="git status"
alias cat="bat"
alias ls="eza --icons"
alias k="kubectl"
# Use z zsh plugin as autojump
alias j="z"

# enable mise
eval "$(mise activate zsh)"

# load custom OS code
if [ "$(uname -s)" = "Darwin" ]; then
  source "$(dirname "$0")/mac.sh"
else
  source "$(dirname "$0")/linux.sh"
fi

gitpr() {
  git fetch upstream "refs/pull/$1/head:PR$1"
  git checkout "PR$1"
}

video_to_gif() {
  ffmpeg -i "$1" "$2" -hide_banner
}

# k8s helpers

pick_namespace() {
  kubectl get ns | sed 1d | awk '{print $1}' | fzf --header="Select namespace"
}

pick_pod() {
  kubectl get pods -n "$1" | sed 1d | awk '/Running/ {print $1}' | fzf --header="Select pod"
}

pod_shell() {
  local namespace
  namespace=$(pick_namespace)
  local pod
  pod=$(pick_pod "$namespace")
  echo "Connecting to $pod"

  if [ -z "${1:-}" ]; then
    kubectl -n "$namespace" exec -ti "$pod" -- bash 2>/dev/null || kubectl -n "$namespace" exec -ti "$pod" -- sh
  else
    kubectl -n "$namespace" exec -ti "$pod" -- "$1"
  fi
}

testpod() {
  kubectl run "testpod-$(date +%s)" --rm -it --image=debian:13 -- bash
}

pod_proxy() {
  local namespace
  namespace=$(pick_namespace)
  local pod
  pod=$(pick_pod "$namespace")
  local port_mapping
  echo "Enter port mapping using the form local_port:pod_port"
  read -r port_mapping
  echo "Setting up proxy to $pod on $port_mapping..."

  kubectl port-forward -n "$namespace" "$pod" "$port_mapping"
}

pod_logs() {
  local namespace
  namespace=$(pick_namespace)
  local pod
  pod=$(pick_pod "$namespace")
  echo "Showing logs for $pod"

  kubectl -n "$namespace" logs -f "$pod"
}

k8s_change_context() {
  local context
  context=$(kubectl config get-contexts --output='name' | fzf --header="Select context")
  echo "Changing to $context"
  kubectl config use-context "$context"
}

ck8s() {
  kubectl config current-context
}

# my jump, it's a similar concept of jump command but search into ~/Code folder
# and create a new tmux session with the selected project folder
mj() {
  local project_path
  project_path=$(tree ~/Code -L 2 --noreport -d -f -i | fzf)
  local org
  org=$(echo "$project_path" | awk '{split($0, a, "/"); print a[5]}')
  local project
  project=$(echo "$project_path" | awk '{split($0, a, "/"); print a[6]}')
  local session_name="$org/$project"

  # prevent tmux complain about being already inside a tmux session
  export TMUX=

  if [ "$session_name" = "" ]; then
    exit 1
  fi

  if ! tmux has-session -t "$session_name"; then
    # Set the session path so new windows start in the selected project.
    tmux new-session -s "$session_name" -c "$project_path" -d
  fi

  # switch to session
  tmux switch-client -t "$session_name"
}

zshenv.sh

zsh/zshenv.sh · 49 lines

#!/usr/bin/env sh
# This file is always executed, even for non interactive shells so it should only contains environment variables
# Setup lang
export LANG="en_US.UTF-8"
export LC_COLLATE="en_US.UTF-8"
export LC_CTYPE="en_US.UTF-8"
export LC_MESSAGES="en_US.UTF-8"
export LC_MONETARY="en_US.UTF-8"
export LC_NUMERIC="en_US.UTF-8"
export LC_TIME="en_US.UTF-8"
export LC_ALL=
export EDITOR=vim

# Add homebrew binaries PATH
export PATH="$PATH":/usr/local/bin/

# Go binaries
export PATH="$PATH":"$HOME/go/bin"

# Homebrew binaries
export PATH="/opt/homebrew/bin/:$PATH"

# Use binaries from ~/.local/bin
# haskell stack install some binaries here: hindent, hlint, etc
export PATH="$PATH:$HOME/.local/bin"

# --without-javac will disable java requirement for erlang compilation
# --enable-m64-build will pass a flag to gcc about how to build 64 bits binaries, fixes an error with make while compiling erlang using kerl
export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac --enable-m64-build"

# Tell kerl to build documentation when compiling erlang
export KERL_BUILD_DOCS="yes"

# Always save iex session history
export ERL_AFLAGS="-kernel shell_history enabled"

# enhance man pages using bat
export MANPAGER="sh -c 'col -bx | bat -l man -p'"

if [ -d "$HOME/.ghcup/" ]; then
  export PATH="$HOME/.cabal/bin:$HOME/.ghcup/bin:$PATH"
fi

# this will load credentials from a private file what won't be commited
test -r "$HOME/.zshenv.private" && . "$HOME/.zshenv.private"

# Disable telemetry in homebrew
export HOMEBREW_NO_ANALYTICS=1

zshrc.sh

zsh/zshrc.sh · 31 lines

#!/usr/bin/env bash
mkdir -p ~/.config/zsh

load_zsh_plugin() {
  url=$1
  loader=$2
  plugin_dir=$(echo "$url" | awk -F'[/.]' '{print $(NF-1)}')

  if [ ! -d "$HOME/.config/zsh/$plugin_dir" ]; then
    git clone --depth 1 "$url" "$HOME/.config/zsh/$plugin_dir" || return
  fi

  . "$HOME/.config/zsh/$plugin_dir/$loader"
}

load_zsh_plugin "https://github.com/zsh-users/zsh-syntax-highlighting.git" "zsh-syntax-highlighting.zsh"
load_zsh_plugin "https://github.com/mafredri/zsh-async.git" "async.zsh"
load_zsh_plugin "https://github.com/sindresorhus/pure.git" "pure.zsh"
load_zsh_plugin "https://github.com/agkozak/zsh-z.git" "zsh-z.plugin.zsh"
load_zsh_plugin "https://github.com/Aloxaf/fzf-tab.git" "fzf-tab.plugin.zsh"

fpath+=("$HOME/.config/zsh/pure")
# setup pure-prompt
autoload -U promptinit
promptinit
prompt pure

# load custom stuff per OS
source ~/.config/zsh/main.sh

nerdfetch