Files
home-manager/modules/programs/gram.nix
2026-09-05 18:29:19 -05:00

329 lines
8.7 KiB
Nix
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
concatStringsSep
escapeShellArgs
filterAttrs
getName
getVersion
hasAttr
hasSuffix
isString
literalExpression
maintainers
mapAttrs'
mkEnableOption
mkIf
mkOption
mkPackageOption
nameValuePair
optional
types
;
inherit (pkgs)
formats
runCommand
symlinkJoin
;
jsonFormat = formats.json { };
cfg = config.programs.gram;
extensionsDir = runCommand "gram-extensions" { } ''
set -euo pipefail
drvs=(${escapeShellArgs cfg.extensionPackages})
paths=(/share/gram/extensions /share/zed/extensions)
mkdir $out/
for drv in "''${drvs[@]}"; do
for path in "''${paths[@]}"; do
if [ -e "$drv/$path" ]; then
ln -s "$drv/$path"/* $out/
fi
done
done
'';
in
{
meta.maintainers = with maintainers; [ mikaeladev ];
options.programs.gram = {
enable = mkEnableOption "Gram";
# packages #
package = mkPackageOption pkgs "gram" { nullable = true; };
extraPackages = mkOption {
type = with types; listOf package;
default = [ ];
example = literalExpression "with pkgs; [ nil ]";
description = ''
List of extra packages available to Gram.
'';
};
extensionPackages = mkOption {
type = with types; listOf package;
default = [ ];
example = literalExpression "with gram-extensions; [ catppuccin ]";
description = ''
List of extension packages to install.
See also:
- <https://codeberg.org/niklaskorz/nix-gram-extensions>
- <https://github.com/DuskSystems/nix-zed-extensions>
'';
};
# single-file configs #
settings = mkOption {
type = with types; either (attrsOf jsonFormat.type) lines;
default = { };
example = {
buffer_font_family = "JetBrains Mono";
buffer_font_weight = 400;
buffer_font_size = 14;
};
description = ''
Set of [settings] to write to {file}`settings.jsonc`.
[settings]: https://gram-editor.com/docs/configuring-gram/
'';
};
debugger = mkOption {
type = with types; either (listOf jsonFormat.type) lines;
default = [ ];
example = [
{
label = "Example Start debugger config";
adapter = "Example adapter name";
request = "launch";
program = "path_to_program";
cwd = "$GRAM_WORKTREE_ROOT";
}
];
description = ''
List of [debug tasks] to write to {file}`debug.jsonc`.
[debug tasks]: https://gram-editor.com/docs/debugger/
'';
};
keymaps = mkOption {
type = with types; either (listOf jsonFormat.type) lines;
default = [ ];
example = [
{
bindings = {
ctrl-right = "editor::SelectLargerSyntaxNode";
ctrl-left = "editor::SelectSmallerSyntaxNode";
};
}
{
context = "ProjectPanel && not_editing";
bindings.o = "project_panel::Open";
}
];
description = ''
List of [key bindings] to write to {file}`keymap.jsonc`.
[key bindings]: https://gram-editor.com/docs/key-bindings/
'';
};
tasks = mkOption {
type = with types; either (listOf jsonFormat.type) lines;
default = [ ];
example = [
{
label = "Example task";
command = ''for i in {1..5}; do echo "Hello $i/5"; sleep 1; done'';
env.foo = "bar";
use_new_terminal = false;
allow_concurrent_runs = false;
reveal = "always";
hide = "never";
shell = "system";
show_summary = true;
show_command = true;
save = "none";
}
];
description = ''
List of [tasks] to write to {file}`tasks.jsonc`.
[tasks]: https://gram-editor.com/docs/tasks/
'';
};
# multi-file configs #
snippets = mkOption {
type = with types; attrsOf (either (attrsOf jsonFormat.type) lines);
default = { };
example = {
html = {
"Define doctype" = {
prefix = "doctype";
description = "Defines the document type";
body = [
"<!DOCTYPE>"
"$1"
];
};
};
javascript = {
"Log to console" = {
prefix = "log";
description = "Logs to console";
body = [
''console.info("Hello, ''${1:World}!")''
"$0"
];
};
};
};
description = ''
Set of [snippets] to write to {file}`snippets/name.json`.
[snippets]: https://gram-editor.com/docs/snippets/
'';
};
themes = mkOption {
type = with types; attrsOf (either (attrsOf jsonFormat.type) lines);
default = { };
example = literalExpression ''
{
my-cool-theme = {
name = "My Cool Theme";
author = "You!";
themes = [
{
name = "My Cool Dark Theme";
appearance = "dark";
style = {
"editor.background" = "#000";
# ...
};
}
{
name = "My Cool Light Theme";
appearance = "light";
style = {
"editor.background" = "#fff";
# ...
};
}
];
};
}
'';
description = ''
Set of [local themes] to write to {file}`themes/name.json`.
[local themes]: https://gram-editor.com/docs/themes/
'';
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.extraPackages != [ ] -> cfg.package != null;
message = ''
The `programs.gram.extraPackages` option requires that `programs.gram.package`
not be null.
'';
}
]
++ (
let
mkSnippetException = scope: substitute: {
assertion = !((hasAttr scope cfg.snippets) || (hasAttr (scope + ".json") cfg.snippets));
message = ''
The snippet scope defined at `programs.gram.snippets.${scope}` is incorrect,
use `programs.gram.snippets.${substitute}` instead.
(reasoning: <https://gram-editor.com/docs/snippets/>)
'';
};
in
[
(mkSnippetException "global" "snippets")
(mkSnippetException "jsx" "javascript")
(mkSnippetException "plain" "plaintext")
]
);
home.packages = optional (cfg.package != null) (
if cfg.extraPackages == [ ] && cfg.extensionPackages == [ ] then
cfg.package
else
(symlinkJoin {
pname = "${getName cfg.package}-wrapped";
version = getVersion cfg.package;
paths = [ cfg.package ];
preferLocalBuild = true;
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild =
let
optionalArgs =
(optional (cfg.extraPackages != [ ]) "--suffix PATH : ${lib.makeBinPath cfg.extraPackages}")
++ (optional (cfg.extensionPackages != [ ]) "--set GRAM_SYSTEM_EXTENSIONS_DIR ${extensionsDir}");
in
''
wrapProgram $out/bin/${cfg.package.meta.mainProgram or "gram"} \
${concatStringsSep " " optionalArgs}
'';
})
);
home.sessionVariables = mkIf (cfg.package == null && cfg.extensionPackages != [ ]) {
GRAM_SYSTEM_EXTENSIONS_DIR = extensionsDir;
};
xdg.configFile =
let
withSuffix = suffix: value: if (hasSuffix suffix value) then value else (value + suffix);
mkFileValue =
name: value:
if isString value then { text = value; } else { source = jsonFormat.generate name value; };
mapConfigAttrs =
prefix: suffix:
mapAttrs' (
name: value:
let
suffixedName = withSuffix suffix name;
in
nameValuePair (prefix + suffixedName) (mkFileValue suffixedName value)
);
singleFileConfigs = filterAttrs (_: value: value != null) {
settings = if (cfg.settings != { }) then cfg.settings else null;
debug = if (cfg.debugger != [ ]) then cfg.debugger else null;
keymap = if (cfg.keymaps != [ ]) then cfg.keymaps else null;
tasks = if (cfg.tasks != [ ]) then cfg.tasks else null;
};
in
(mapConfigAttrs "gram/" ".jsonc" singleFileConfigs)
// (mapConfigAttrs "gram/snippets/" ".json" cfg.snippets)
// (mapConfigAttrs "gram/themes/" ".json" cfg.themes);
};
}