t3code: add module

Adds programs.t3code so T3 Code can be installed and configured from Home
Manager, including optional merge semantics when the app mutates its config
files. Includes a news entry and tests for empty, merged, and immutable setups.
This commit is contained in:
angel
2026-05-15 10:21:14 -05:00
committed by Austin Horstman
parent 1bedcc8740
commit ca77575d39
6 changed files with 555 additions and 0 deletions
@@ -0,0 +1,10 @@
{
time = "2026-05-10T20:18:04+00:00";
condition = true;
message = ''
A new module is available: 'programs.t3code'.
T3 Code is a minimal web GUI for coding agents; the module installs
the application and can manage its JSON settings in '~/.t3/userdata/'.
'';
}
+195
View File
@@ -0,0 +1,195 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
literalExpression
mkIf
mkMerge
mkOption
types
;
cfg = config.programs.t3code;
jsonFormat = pkgs.formats.json { };
json5 = pkgs.python3Packages.toPythonApplication pkgs.python3Packages.json5;
impureConfigMerger = empty: jqOperation: path: staticSettings: ''
mkdir -p "$(dirname -- ${lib.escapeShellArg path})"
if [ ! -e ${lib.escapeShellArg path} ]; then
echo ${lib.escapeShellArg empty} > ${lib.escapeShellArg path}
fi
dynamic="$(${lib.getExe json5} --as-json ${lib.escapeShellArg path})"
static="$(cat ${lib.escapeShellArg staticSettings})"
config="$(${lib.getExe pkgs.jq} -n ${lib.escapeShellArg jqOperation} --argjson dynamic "$dynamic" --argjson static "$static")"
printf '%s\n' "$config" > ${lib.escapeShellArg path}
unset config
'';
userDataDir = "${config.home.homeDirectory}/.t3/userdata";
in
{
meta.maintainers = [
lib.maintainers.iamanaws
];
options.programs.t3code = {
enable = lib.mkEnableOption "T3 Code, a minimal web GUI for coding agents";
package = mkOption {
type = types.nullOr types.package;
default = pkgs.t3code or null;
defaultText = literalExpression "pkgs.t3code";
description = ''
The t3code package to install.
'';
};
mutableUserSettings = mkOption {
type = types.bool;
default = true;
example = false;
description = ''
Whether user settings ({file}`settings.json`) can be updated by t3code.
'';
};
mutableKeybindings = mkOption {
type = types.bool;
default = true;
example = false;
description = ''
Whether user keybindings ({file}`keybindings.json`) can be updated by t3code.
'';
};
mutableClientSettings = mkOption {
type = types.bool;
default = true;
example = false;
description = ''
Whether client settings ({file}`client-settings.json`) can be updated by t3code.
'';
};
userSettings = mkOption {
inherit (jsonFormat) type;
default = { };
example = literalExpression ''
{
enableAssistantStreaming = true;
providerInstances = {
codex = {
driver = "codex";
enabled = true;
config = {
enabled = true;
binaryPath = "codex";
homePath = "";
shadowHomePath = "";
customModels = [ ];
};
};
};
}
'';
description = ''
Configuration written to t3code's {file}`settings.json`.
'';
};
keybindings = mkOption {
inherit (jsonFormat) type;
default = [ ];
example = literalExpression ''
[
{
key = "mod+j";
command = "terminal.toggle";
}
{
key = "mod+d";
command = "terminal.split";
when = "terminalFocus";
}
{
key = "mod+d";
command = "diff.toggle";
when = "!terminalFocus";
}
]
'';
description = ''
Configuration written to t3code's {file}`keybindings.json`.
'';
};
clientSettings = mkOption {
inherit (jsonFormat) type;
default = { };
example = literalExpression ''
{
settings = {
favorites = [
{
provider = "codex";
model = "gpt-5.5";
}
];
sidebarProjectGroupingMode = "repository";
timestampFormat = "locale";
};
}
'';
description = ''
Configuration written to t3code's {file}`client-settings.json`.
'';
};
};
config = mkIf cfg.enable {
home.packages = mkIf (cfg.package != null) [ cfg.package ];
home.activation = mkMerge [
(mkIf (cfg.mutableUserSettings && cfg.userSettings != { }) {
t3codeSettingsActivation = lib.hm.dag.entryAfter [ "linkGeneration" ] (
impureConfigMerger "{}" "$dynamic * $static" "${userDataDir}/settings.json" (
jsonFormat.generate "t3code-user-settings" cfg.userSettings
)
);
})
(mkIf (cfg.mutableKeybindings && cfg.keybindings != [ ]) {
t3codeKeybindingsActivation = lib.hm.dag.entryAfter [ "linkGeneration" ] (
impureConfigMerger "[]"
"$dynamic + $static | group_by([.key, .when]) | map(reduce .[] as $item ({}; . * $item))"
"${userDataDir}/keybindings.json"
(jsonFormat.generate "t3code-user-keybindings" cfg.keybindings)
);
})
(mkIf (cfg.mutableClientSettings && cfg.clientSettings != { }) {
t3codeClientSettingsActivation = lib.hm.dag.entryAfter [ "linkGeneration" ] (
impureConfigMerger "{}" "$dynamic * $static" "${userDataDir}/client-settings.json" (
jsonFormat.generate "t3code-client-settings" cfg.clientSettings
)
);
})
];
home.file = mkMerge [
(mkIf (!cfg.mutableUserSettings && cfg.userSettings != { }) {
".t3/userdata/settings.json".source = jsonFormat.generate "t3code-user-settings" cfg.userSettings;
})
(mkIf (!cfg.mutableKeybindings && cfg.keybindings != [ ]) {
".t3/userdata/keybindings.json".source =
jsonFormat.generate "t3code-user-keybindings" cfg.keybindings;
})
(mkIf (!cfg.mutableClientSettings && cfg.clientSettings != { }) {
".t3/userdata/client-settings.json".source =
jsonFormat.generate "t3code-client-settings" cfg.clientSettings;
})
];
};
}
@@ -0,0 +1,5 @@
{
t3code-settings = ./settings.nix;
t3code-settings-immutable = ./settings-immutable.nix;
t3code-settings-empty = ./settings-empty.nix;
}
@@ -0,0 +1,14 @@
{ config, ... }:
{
programs.t3code = {
enable = true;
package = config.lib.test.mkStubPackage { };
};
nmt.script = ''
assertPathNotExists "home-files/.t3/userdata/settings.json"
assertPathNotExists "home-files/.t3/userdata/keybindings.json"
assertPathNotExists "home-files/.t3/userdata/client-settings.json"
'';
}
@@ -0,0 +1,111 @@
{ config, ... }:
{
programs.t3code = {
enable = true;
package = config.lib.test.mkStubPackage { };
mutableUserSettings = false;
mutableKeybindings = false;
mutableClientSettings = false;
userSettings = {
enableAssistantStreaming = true;
providerInstances.codex = {
driver = "codex";
enabled = true;
config = {
enabled = true;
binaryPath = "codex";
homePath = "";
shadowHomePath = "";
customModels = [ ];
};
};
};
keybindings = [
{
key = "mod+d";
command = "terminal.split";
when = "terminalFocus";
}
{
key = "mod+d";
command = "diff.toggle";
when = "!terminalFocus";
}
];
clientSettings = {
settings = {
favorites = [
{
provider = "codex";
model = "gpt-5.5";
}
];
sidebarProjectGroupingMode = "repository";
timestampFormat = "locale";
};
};
};
nmt.script =
let
expectedSettings = builtins.toFile "expected-settings.json" ''
{
"enableAssistantStreaming": true,
"providerInstances": {
"codex": {
"config": {
"binaryPath": "codex",
"customModels": [],
"enabled": true,
"homePath": "",
"shadowHomePath": ""
},
"driver": "codex",
"enabled": true
}
}
}
'';
expectedKeybindings = builtins.toFile "expected-keybindings.json" ''
[
{
"command": "terminal.split",
"key": "mod+d",
"when": "terminalFocus"
},
{
"command": "diff.toggle",
"key": "mod+d",
"when": "!terminalFocus"
}
]
'';
expectedClientSettings = builtins.toFile "expected-client-settings.json" ''
{
"settings": {
"favorites": [
{
"model": "gpt-5.5",
"provider": "codex"
}
],
"sidebarProjectGroupingMode": "repository",
"timestampFormat": "locale"
}
}
'';
in
''
assertFileExists "home-files/.t3/userdata/settings.json"
assertFileContent "home-files/.t3/userdata/settings.json" "${expectedSettings}"
assertFileExists "home-files/.t3/userdata/keybindings.json"
assertFileContent "home-files/.t3/userdata/keybindings.json" "${expectedKeybindings}"
assertFileExists "home-files/.t3/userdata/client-settings.json"
assertFileContent "home-files/.t3/userdata/client-settings.json" "${expectedClientSettings}"
'';
}
+220
View File
@@ -0,0 +1,220 @@
{
config,
lib,
pkgs,
...
}:
{
programs.t3code = {
enable = true;
package = config.lib.test.mkStubPackage { };
userSettings = {
enableAssistantStreaming = true;
providerInstances.codex = {
driver = "codex";
enabled = true;
config = {
enabled = true;
binaryPath = "codex";
homePath = "";
shadowHomePath = "";
customModels = [ ];
};
};
};
keybindings = [
{
key = "mod+d";
command = "terminal.split";
when = "terminalFocus";
}
{
key = "mod+d";
command = "diff.toggle";
when = "!terminalFocus";
}
];
clientSettings = {
settings = {
favorites = [
{
provider = "codex";
model = "gpt-5.5";
}
];
sidebarProjectGroupingMode = "repository";
timestampFormat = "locale";
};
};
};
home.homeDirectory = lib.mkForce "/@TMPDIR@/hm-user";
nmt.script =
let
preexistingSettings = builtins.toFile "preexisting-settings.json" ''
{
"enableAssistantStreaming": false,
"providerInstances": {
"codex": {
"driver": "codex",
"enabled": false,
"config": {
"enabled": false,
"binaryPath": "old-codex",
"homePath": "",
"shadowHomePath": "",
"customModels": []
}
},
"opencode": {
"driver": "opencode",
"enabled": true,
"config": {
"enabled": true,
"binaryPath": "opencode",
"serverUrl": "",
"serverPassword": "",
"customModels": []
}
}
}
}
'';
preexistingKeybindings = builtins.toFile "preexisting-keybindings.json" ''
[
{
"key": "mod+d",
"command": "old.terminalSplit",
"when": "terminalFocus"
},
{
"key": "mod+d",
"command": "old.diffToggle",
"when": "!terminalFocus"
},
{
"key": "mod+j",
"command": "terminal.toggle"
}
]
'';
preexistingClientSettings = builtins.toFile "preexisting-client-settings.json" ''
{
"settings": {
"favorites": [],
"sidebarProjectGroupingMode": "workspace",
"timestampFormat": "relative",
"unmanagedSetting": true
}
}
'';
expectedSettings = builtins.toFile "expected-settings.json" ''
{
"enableAssistantStreaming": true,
"providerInstances": {
"codex": {
"driver": "codex",
"enabled": true,
"config": {
"enabled": true,
"binaryPath": "codex",
"homePath": "",
"shadowHomePath": "",
"customModels": []
}
},
"opencode": {
"driver": "opencode",
"enabled": true,
"config": {
"enabled": true,
"binaryPath": "opencode",
"serverUrl": "",
"serverPassword": "",
"customModels": []
}
}
}
}
'';
expectedKeybindings = builtins.toFile "expected-keybindings.json" ''
[
{
"key": "mod+d",
"command": "diff.toggle",
"when": "!terminalFocus"
},
{
"key": "mod+d",
"command": "terminal.split",
"when": "terminalFocus"
},
{
"key": "mod+j",
"command": "terminal.toggle"
}
]
'';
expectedClientSettings = builtins.toFile "expected-client-settings.json" ''
{
"settings": {
"favorites": [
{
"model": "gpt-5.5",
"provider": "codex"
}
],
"sidebarProjectGroupingMode": "repository",
"timestampFormat": "locale",
"unmanagedSetting": true
}
}
'';
settingsPath = ".t3/userdata/settings.json";
keybindingsPath = ".t3/userdata/keybindings.json";
clientSettingsPath = ".t3/userdata/client-settings.json";
settingsActivation = pkgs.writeScript "settings-activation" config.home.activation.t3codeSettingsActivation.data;
keybindingsActivation = pkgs.writeScript "keybindings-activation" config.home.activation.t3codeKeybindingsActivation.data;
clientSettingsActivation = pkgs.writeScript "client-settings-activation" config.home.activation.t3codeClientSettingsActivation.data;
in
''
export HOME=$TMPDIR/hm-user
mkdir -p $HOME/.t3/userdata
cat ${preexistingSettings} > $HOME/${settingsPath}
cat ${preexistingKeybindings} > $HOME/${keybindingsPath}
cat ${preexistingClientSettings} > $HOME/${clientSettingsPath}
substitute ${settingsActivation} $TMPDIR/settings-activate --subst-var TMPDIR
chmod +x $TMPDIR/settings-activate
$TMPDIR/settings-activate
substitute ${keybindingsActivation} $TMPDIR/keybindings-activate --subst-var TMPDIR
chmod +x $TMPDIR/keybindings-activate
$TMPDIR/keybindings-activate
substitute ${clientSettingsActivation} $TMPDIR/client-settings-activate --subst-var TMPDIR
chmod +x $TMPDIR/client-settings-activate
$TMPDIR/client-settings-activate
assertFileContent "$HOME/${settingsPath}" "${expectedSettings}"
assertFileContent "$HOME/${keybindingsPath}" "${expectedKeybindings}"
assertFileContent "$HOME/${clientSettingsPath}" "${expectedClientSettings}"
$TMPDIR/settings-activate
$TMPDIR/keybindings-activate
$TMPDIR/client-settings-activate
assertFileContent "$HOME/${settingsPath}" "${expectedSettings}"
assertFileContent "$HOME/${keybindingsPath}" "${expectedKeybindings}"
assertFileContent "$HOME/${clientSettingsPath}" "${expectedClientSettings}"
'';
}