HomeAboutPostsTagsProjectsRSS
┌─
ARTICLE
─┐

└─
─┘

I’ve been searching for a good solution to use Emacs29 with tree-sitter config in nix-darwin for quite some time.

Emacs29 comes with built-in support for Tree-Sitter, a parser generator tool and an incremental parsing library, the only requirement is the grammar files, detail in this masteringemacs article .

While there are some proposed solutions out there, such as the one found on the nixos discourse , they didn’t quite hit the mark for me. The issue was that I’m using an overridden package definition of pkgs.emacs29-macport, which complicated things a bit.

let
    myEmacs = (emacsPackagesFor emacs).emacsWithPackages (epkgs: with epkgs; [
        vterm
        treesit-grammars.with-all-grammars
    ];)
in

The above treesit-grammars.with-all-grammars definition only installs the dynamic libraries. However, Emacs was still unable to find the files. The crux of the problem was that I needed to link the directory to a location that Tree-Sitter could locate.

After many trials and tribulations, I finally managed to come up with a functional solution.

Solution

pkg-emacs29-macport.nix: package definition of emacs

{ pkgs ? (import <nixpkgs> { }) }:
let
  # https://github.com/railwaycat/homebrew-emacsmacport/blob/master/Formula/emacs-mac.rb
  emacs29-macport = (pkgs.emacs29-macport.overrideAttrs (prevAttrs: {
    patches = (prevAttrs.patches) ++ [
      # patch for multi-tty support, see the following links for details
      # https://bitbucket.org/mituharu/emacs-mac/pull-requests/2/add-multi-tty-support-to-be-on-par-with/diff
      # https://ylluminarious.github.io/2019/05/23/how-to-fix-the-emacs-mac-port-for-multi-tty-access/
      (pkgs.fetchpatch {
        url =
          "https://raw.githubusercontent.com/railwaycat/homebrew-emacsmacport/8b06f75ea28a68f9a490d9001ce33fd1b0d426aa/patches/emacs-mac-29-multi-tty.diff";
        sha256 = "sha256-OpSYG5ew8A1IL5rW/wPwmG2bzZa8iFF+xTYQGiWjzKg=";
      })
      # no-title-bars
      (pkgs.fetchpatch {
        url =
          "https://raw.githubusercontent.com/railwaycat/homebrew-emacsmacport/667f0efc08506facfc6963ac1fd1d5b9b777e094/patches/emacs-26.2-rc1-mac-7.5-no-title-bar.patch";
        sha256 = "sha256-f2DRcUZq8Y18n6MJ6vtChN5hLGERduMB8B1mrrds6Ns=";
      })
    ];
  })).override {
    # not necessary, but enforce these options to be true
    withNativeCompilation = true;
    withTreeSitter = true;
  };
  buildEmacs = (pkgs.emacsPackagesFor emacs29-macport).emacsWithPackages;
  treesitGrammars =
    (pkgs.emacsPackagesFor emacs29-macport).treesit-grammars.with-all-grammars;
in {
  emacs = buildEmacs (epkgs: with epkgs; [ vterm treesitGrammars ]);
  treesitGrammars = treesitGrammars;
}

config-emacs.nix: configuraiton of emacs

{ pkgs, config, lib, ... }:
let
  emacs29 = (pkgs.callPackage ./pkg-emacs29-macport.nix { });
in {
  home.packages = lib.mkBefore ([ emacs29.emacs ]);

  home.file = {
    # tree-sitter subdirectory of the directory specified by user-emacs-directory
    ".config/emacs/.local/cache/tree-sitter".source =
      "${emacs29.treesitGrammars}/lib";
  };
}

Tree-sitter language grammars are distributed as dynamic libraries. In order to use a language grammar in Emacs, you need to make sure that the dynamic library is installed on the system. Emacs looks for language grammars in several places, in the following order: first, in the list of directories specified by the variable treesit-extra-load-path; then, in the tree-sitter subdirectory of the directory specified by user-emacs-directory (see The Init File); and finally, in the system’s default locations for dynamic libraries.

Tree-sitter Language Grammar

Finally use this config-emacs.nix in your home.nix using home-manager

imports = [
  ./config-emacs.nix
]
┌─
ARTICLE
─┐

└─
─┘

I use Emacs --with-no-title-bar and use yabai to auto layout window, sketchybar to display current window title at the menu bar.

Due to the limited screen size on a laptop, it can be challenging to view the entire file path. Therefore, I created a function that shows a shortened version of the file path.

If you are editing a file, the function will display the file path. Otherwise, it will display the buffer name.

for example, ~/projects/blog/content/posts/emacs-frame-title-format.md will be display as ~/p/b/c/posts/emacs-frame-title-format.md


(defun shorten-path-for-title (path)
  "Shorten a file PATH to be displayed in the frame title.
Only the last directory's name is fully displayed; upper-layer directories are represented by their first letters."
  (let* ((components (split-string (or path "") "/" t))
         (filename (or (car (last components)) ""))
         (lastdir (if (> (length components) 1) (nth (- (length components) 2) components) ""))
         (dirs (butlast components 2))
         (shortened-dirs (mapcar (lambda (dir) (substring dir 0 1)) dirs)))
    (concat (string-join shortened-dirs "/")
            (if shortened-dirs "/")
            lastdir
            "/"
            filename)))

(setq frame-title-format
      '((:eval (if (buffer-file-name)
                   (shorten-path-for-title (abbreviate-file-name (buffer-file-name)))))
        (:eval (if (not (buffer-file-name)) (buffer-name)))))
┌─
ARTICLE
─┐

└─
─┘

Mac OS intercepts and handles certain key combinations (e.g., - for switching input languages). These will not be passed to Emacs. One can disable this interception by setting mac-pass-command-to-system or mac-pass-control-to-system to nil.

Mac Input - GNU Emacs Manual

┌─
ARTICLE
─┐

└─
─┘

Issue

When I decided to manage my sketchybar configuration as a git submodule within my Nix Flakes setup, I encountered an unexpected obstacle. Attempting to run darwin-rebuild was met with failure as the system couldn’t locate the source of the local directory. It’s a known issue as discussed in the Submodules of flakes are not working thread.

Overcoming the Submodule Challenge

I devised a workaround that resolved the problem and ensured proper file execution permissions. Before diving into the implementation details, I must emphasize the necessity of updating the flake.lock file. This is a critical step to ensure submodule is recognized by nix:

nix flake lock --update-input config-sketchybar-src

Solution

In flake.nix define the submodule directory as input

{
    inputs = {
      # submodule directory
      config-sketchybar-src = {
        url = "git+file:./config-sketchybar";
        flake = false;
      };
    };

    outputs = inputs@{ self, nixpkgs, darwin, home-manager, ... }:
    let username = "nohzafk";
    in {
      darwinConfigurations."MacBook-Pro" = darwin.lib.darwinSystem {
        system = "aarch64-darwin";
        specialArgs = { inherit inputs username; };
        modules = [
          ./configuration.nix
          ./system-preferences.nix
          {
            users.users."${username}" = {
              name = "${username}";
              home = "/Users/${username}";
            };
          }
          home-manager.darwinModules.home-manager
          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.users."${username}".imports = [ ./home.nix ];
            home-manager.extraSpecialArgs = { inherit inputs username; };
          }
        ];
      };
    };
}

in home.nix

{ inputs, username, config, pkgs, ... }: {
    imports = [
       ./config-window-manager.nix
    ];
}

in config-window-manager.nix

{ inputs, lib, pkgs, ... }: {
let
  sketchybar-config =
    pkgs.callPackage ./pkg-sketchybar-config.nix { inherit inputs pkgs; };
in {
  home.packages =
    lib.mkBefore = [ sketchybar-config ];

  home.file.".config/sketchybar".source = sketchybar-config;

}

finally in pkg-sketchybar-config.nix

{ inputs, pkgs, ... }:
pkgs.stdenv.mkDerivation {
  name = "sketchybar-config";

  dontConfigure = true;
  dontUnpack = true;
  src = inputs.config-sketchybar-src;

  installPhase = ''
    mkdir -p $out
    cp -r $src/config/sketchybar/* $out

    # Find all .sh files and substitute 'jq' with the full path to the jq binary
    find $out -type f -name "*.sh" | while read script; do
      substituteInPlace "$script" \
        --replace 'jq' '${pkgs.jq}/bin/jq'
    done

    chmod -R +x $out
  '';
}

In the end, the satisfaction of having your local directory seamlessly integrated as a git submodule and functioning perfectly within the Nix ecosystem is well worth the effort.

┌─
ARTICLE
─┐

└─
─┘

I’m quite fond of this saying—it captures the essence of Emacs perfectly.

The core idea of Emacs is to have an expressive digital material and an open-ended, user-extensible set of commands that manipulate that material and can be quickly invoked. Obviously, this model can be fruitfully applied to any digital material, not just plain text.

X

┌─
ARTICLE
─┐

└─
─┘

If you are experiencing issues with a Doom Emacs package that has been cloned from GitHub, and you want to force a re-download or re-clone of the package, you can use the following steps to address the issue:

  1. Delete the Problematic Package: Navigate to your .emacs.d/.local/straight/repos/ directory (or wherever your packages are stored; this path is the default for Doom Emacs). Locate the directory corresponding to the problematic package and delete it.
rm -rf ~/.emacs.d/.local/straight/repos/<package-name>

Replace with the actual name of the package directory you want to remove.

  1. Clear Build Cache: Clear the build cache that might contain failed or partial build information. The build cache is typically located in the .emacs.d/.local/straight/build/ directory.
rm -rf ~/.emacs.d/.local/straight/build/<package-name>
  1. Run Doom Sync: Run doom sync in your terminal to synchronize your configuration. This will re-download and rebuild any missing or deleted packages.
doom sync
  1. Restart Emacs: After running doom sync, restart Emacs to ensure that all changes take effect.

Remember that you should re-enable the package your config.el before running doom sync if you previously disabled it.

┌─
ARTICLE
─┐

└─
─┘

Packages defined with environment.systemPackages in nix-darwin are installed to /var/current-system/sw/bin. Those configured with home-manager go to the user-specific profile’s bin directory, typically found at ~/.nix-profile/bin.