From 4921973b9a069591c92d8bb7c69bdf993cf81b2b Mon Sep 17 00:00:00 2001 From: atagen Date: Mon, 27 Apr 2026 16:09:39 +1000 Subject: [PATCH] amaan can't into kernel do 400 pushups per cache miss idiot --- common/cli.nix | 90 +- common/debloat.nix | 30 +- common/docs.nix | 10 +- common/mime-types.nix | 164 ++ common/nix/gc.nix | 19 +- common/nix/plugins.nix | 7 +- common/nix/substituters.nix | 4 +- common/users.nix | 1 - flake.lock | 424 ++-- flake.nix | 45 +- graphical/boot.nix | 2 +- graphical/desktop/quickshell/rice/Colours.qml | 2 +- graphical/desktop/shell.nix | 23 +- graphical/desktop/wm.nix | 1 + graphical/desktop/wry.nix | 2 +- graphical/dev.nix | 1 + graphical/documents.nix | 14 +- graphical/foot-tabs.patch | 1902 +++++++++++++++++ graphical/integrations.nix | 3 + graphical/kernel.nix | 2 +- graphical/llm.nix | 177 -- graphical/llm/launcher.nu | 126 ++ graphical/llm/lift-claude-bun.py | 92 + graphical/llm/llm.nix | 152 ++ graphical/llm/patch-claude-src.py | 231 ++ graphical/llm/statusline-command.nu | 137 ++ graphical/media.nix | 12 +- graphical/password-manager.nix | 1 + graphical/platform-themes.nix | 12 +- graphical/terminal.nix | 115 +- hosts/quiver/kernel.nix | 8 +- hosts/quiver/ssh.nix | 15 + lib/create.nix | 1 + lib/wrappers.nix | 20 + 34 files changed, 3240 insertions(+), 605 deletions(-) create mode 100644 common/mime-types.nix create mode 100644 graphical/foot-tabs.patch delete mode 100644 graphical/llm.nix create mode 100644 graphical/llm/launcher.nu create mode 100644 graphical/llm/lift-claude-bun.py create mode 100644 graphical/llm/llm.nix create mode 100644 graphical/llm/patch-claude-src.py create mode 100644 graphical/llm/statusline-command.nu create mode 100644 hosts/quiver/ssh.nix create mode 100644 lib/wrappers.nix diff --git a/common/cli.nix b/common/cli.nix index f23fb96..6d76afa 100644 --- a/common/cli.nix +++ b/common/cli.nix @@ -3,24 +3,13 @@ lib, getPkgs, config, + mkWrappers, ... }: let pal = config.rice.palette.hex; ui = config.rice.roles pal; - - wrap = - name: pkg: args: - pkgs.symlinkJoin { - inherit name; - paths = [ pkg ]; - nativeBuildInputs = [ pkgs.makeWrapper ]; - postBuild = "wrapProgram $out/bin/${name} ${args}"; - }; - - wrapXdg = - name: pkg: configDir: - wrap name pkg ''--set XDG_CONFIG_HOME "${configDir}"''; + inherit (mkWrappers pkgs) wrap wrapXdg; lazygitConfig = pkgs.writeText "lazygit-config.yml" '' gui: @@ -68,55 +57,12 @@ let theme "nix-rice" ''; - btopConfig = pkgs.writeText "btop.conf" '' - color_theme = "nix-rice" - ''; - btopThemeDir = pkgs.runCommand "btop-themes" { } '' - mkdir -p $out - cp ${pkgs.writeText "nix-rice.theme" '' - theme[main_bg]="${ui.bg}" - theme[main_fg]="${ui.fg}" - theme[title]="${ui.fg}" - theme[hi_fg]="${ui.accent}" - theme[selected_bg]="${ui.surface}" - theme[selected_fg]="${ui.fg}" - theme[inactive_fg]="${ui.muted}" - theme[graph_text]="${ui.fg}" - theme[meter_bg]="${ui.overlay}" - theme[proc_misc]="${ui.primary}" - theme[cpu_box]="${ui.primary}" - theme[mem_box]="${ui.accent}" - theme[net_box]="${pal.normal.green}" - theme[proc_box]="${pal.normal.magenta}" - theme[div_line]="${ui.overlay}" - theme[temp_start]="${pal.normal.green}" - theme[temp_mid]="${ui.highlight}" - theme[temp_end]="${ui.error}" - theme[cpu_start]="${ui.primary}" - theme[cpu_mid]="${ui.accent}" - theme[cpu_end]="${ui.highlight}" - theme[free_start]="${pal.normal.green}" - theme[free_mid]="${ui.accent}" - theme[free_end]="${ui.primary}" - theme[cached_start]="${ui.primary}" - theme[cached_mid]="${ui.accent}" - theme[cached_end]="${pal.normal.green}" - theme[available_start]="${pal.normal.green}" - theme[available_mid]="${ui.accent}" - theme[available_end]="${ui.highlight}" - theme[used_start]="${ui.highlight}" - theme[used_mid]="${ui.error}" - theme[used_end]="${pal.bright.red}" - theme[download_start]="${pal.normal.green}" - theme[download_mid]="${ui.accent}" - theme[download_end]="${ui.primary}" - theme[upload_start]="${ui.highlight}" - theme[upload_mid]="${pal.bright.red}" - theme[upload_end]="${ui.error}" - theme[process_start]="${ui.primary}" - theme[process_mid]="${ui.accent}" - theme[process_end]="${pal.normal.green}" - ''} $out/nix-rice.theme + btopConfigDir = pkgs.runCommand "btop-xdg" { } '' + mkdir -p $out/btop + cp ${pkgs.writeText "btop.conf" '' + color_theme = "TTY" + update_ms = 100 + ''} $out/btop/btop.conf ''; in { @@ -124,16 +70,26 @@ in inherit (pkgs) curl eza - git + gitMinimal ripgrep fd ouch - btop bat ; - lazygit = wrap "lazygit" pkgs.lazygit ''--add-flags "--use-config-file=${lazygitConfig}"''; - zellij = wrap "zellij" pkgs.zellij ''--add-flags "--config-dir ${zellijConfig}"''; - # btop = wrap "btop" pkgs.btop ''--add-flags "--config ${btopConfig} --themes-dir ${btopThemeDir}"''; + lazygit = wrap { + name = "lazygit"; + pkg = pkgs.lazygit; + args = [ "--use-config-file=${lazygitConfig}" ]; + }; + zellij = wrap { + name = "zellij"; + pkg = pkgs.zellij; + args = [ "--config-dir ${zellijConfig}" ]; + envs = { + SHELL = "${config.users.defaultUserShell}/bin/nu"; + }; + }; + btop = wrapXdg "btop" pkgs.btop btopConfigDir; }; environment.variables.BAT_THEME = "ansi"; diff --git a/common/debloat.nix b/common/debloat.nix index 3063953..1d1369d 100644 --- a/common/debloat.nix +++ b/common/debloat.nix @@ -1,5 +1,7 @@ -{ lib, ... }: +{ inputs, lib, ... }: { + imports = [ inputs.nixos-core.nixosModules.default ]; + system.activationScripts.users = lib.mkForce ""; system.disableInstallerTools = true; programs.less.lessopen = null; boot.enableContainers = false; @@ -7,4 +9,30 @@ environment.defaultPackages = lib.mkDefault [ ]; documentation.info.enable = false; system.tools.nixos-option.enable = false; + system.tools.nixos-generate-config.enable = lib.mkDefault false; + system.nixos-core.enable = true; + # system.nixos-core.components.userGroupsActivation.enable = false; + nixpkgs.overlays = [ + (final: prev: { + xdg-utils = final.symlinkJoin { + name = "xdg-utils-handlr-shim-${prev.handlr-regex.version or "0"}"; + paths = [ + final.xdg-user-dirs + (final.writeShellScriptBin "xdg-open" ''exec ${final.handlr-regex}/bin/handlr open "$@"'') + (final.writeShellScriptBin "xdg-mime" ''exec ${final.handlr-regex}/bin/handlr mime "$@"'') + (final.writeShellScriptBin "xdg-settings" ''exec ${final.handlr-regex}/bin/handlr get "$@"'') + (final.writeShellScriptBin "xdg-email" ''exec ${final.handlr-regex}/bin/handlr open "mailto:$*"'') + + (final.writeShellScriptBin "xdg-desktop-menu" "exit 0") + (final.writeShellScriptBin "xdg-desktop-icon" "exit 0") + (final.writeShellScriptBin "xdg-icon-resource" "exit 0") + (final.writeShellScriptBin "xdg-screensaver" "exit 0") + ]; + meta = { + description = "xdg-utils shim backed by handlr-regex (perl-free)"; + mainProgram = "xdg-open"; + }; + }; + }) + ]; } diff --git a/common/docs.nix b/common/docs.nix index 1567bdc..1710d0d 100644 --- a/common/docs.nix +++ b/common/docs.nix @@ -6,8 +6,10 @@ man-pages-posix ; }; - - documentation.dev.enable = true; - documentation.man.enable = true; - documentation.enable = true; + documentation = { + dev.enable = false; + man.enable = true; + enable = true; + nixos.enable = false; + }; } diff --git a/common/mime-types.nix b/common/mime-types.nix new file mode 100644 index 0000000..88da706 --- /dev/null +++ b/common/mime-types.nix @@ -0,0 +1,164 @@ +{ + config, + lib, + ... +}: +let + # Most packages use pname as their desktop ID; override the exceptions + deskOf = pkg: "${pkg.pname or pkg.name}.desktop"; + desktopIds = { + pdfReader = "org.pwmt.zathura.desktop"; + musicPlayer = "org.strawberrymusicplayer.strawberry.desktop"; + ebookReader = "com.github.johnfactotum.Foliate.desktop"; + officeSuite = { + writer = "libreoffice-writer.desktop"; + calc = "libreoffice-calc.desktop"; + impress = "libreoffice-impress.desktop"; + draw = "libreoffice-draw.desktop"; + }; + }; + desk = appName: desktopIds.${appName} or (deskOf config.apps.${appName}); +in +{ + xdg.mime.defaultApplications = { + # web + "text/html" = desk "browser"; + "application/xhtml+xml" = desk "browser"; + "x-scheme-handler/http" = desk "browser"; + "x-scheme-handler/https" = desk "browser"; + "x-scheme-handler/ftp" = desk "browser"; + "x-scheme-handler/about" = desk "browser"; + "x-scheme-handler/unknown" = desk "browser"; + + # mail + "x-scheme-handler/mailto" = desk "mailClient"; + "message/rfc822" = desk "mailClient"; + "application/mbox" = desk "mailClient"; + + # text / code + "text/plain" = desk "editor"; + "text/markdown" = desk "editor"; + "application/json" = desk "editor"; + "application/xml" = desk "editor"; + "text/xml" = desk "editor"; + "application/javascript" = desk "editor"; + "text/javascript" = desk "editor"; + "text/x-python" = desk "editor"; + "text/x-script.python" = desk "editor"; + "text/x-shellscript" = desk "editor"; + "text/x-csrc" = desk "editor"; + "text/x-chdr" = desk "editor"; + "text/x-c++src" = desk "editor"; + "text/x-c++hdr" = desk "editor"; + "text/x-rust" = desk "editor"; + "text/x-go" = desk "editor"; + "text/x-java" = desk "editor"; + "text/x-haskell" = desk "editor"; + "text/x-nix" = desk "editor"; + "text/x-lua" = desk "editor"; + "text/x-toml" = desk "editor"; + "text/x-yaml" = desk "editor"; + "text/yaml" = desk "editor"; + + # directories + "inode/directory" = desk "fm"; + + # archives + "application/zip" = desk "archive"; + "application/x-zip-compressed" = desk "archive"; + "application/x-tar" = desk "archive"; + "application/gzip" = desk "archive"; + "application/x-gzip" = desk "archive"; + "application/x-bzip2" = desk "archive"; + "application/x-xz" = desk "archive"; + "application/x-7z-compressed" = desk "archive"; + "application/x-rar" = desk "archive"; + "application/x-rar-compressed" = desk "archive"; + "application/x-lzip" = desk "archive"; + "application/x-lzma" = desk "archive"; + "application/x-zstd" = desk "archive"; + "application/zstd" = desk "archive"; + + # pdf + "application/pdf" = desk "pdfReader"; + "application/x-pdf" = desk "pdfReader"; + + # images + "image/jpeg" = desk "imageViewer"; + "image/png" = desk "imageViewer"; + "image/gif" = desk "imageViewer"; + "image/webp" = desk "imageViewer"; + "image/avif" = desk "imageViewer"; + "image/jxl" = desk "imageViewer"; + "image/bmp" = desk "imageViewer"; + "image/x-bmp" = desk "imageViewer"; + "image/tiff" = desk "imageViewer"; + "image/x-tiff" = desk "imageViewer"; + "image/svg+xml" = desk "imageViewer"; + "image/x-portable-bitmap" = desk "imageViewer"; + "image/x-portable-pixmap" = desk "imageViewer"; + "image/vnd.microsoft.icon" = desk "imageViewer"; + "image/x-icon" = desk "imageViewer"; + + # video + "video/mp4" = desk "videoPlayer"; + "video/mpeg" = desk "videoPlayer"; + "video/x-matroska" = desk "videoPlayer"; + "video/webm" = desk "videoPlayer"; + "video/x-msvideo" = desk "videoPlayer"; + "video/vnd.avi" = desk "videoPlayer"; + "video/quicktime" = desk "videoPlayer"; + "video/x-flv" = desk "videoPlayer"; + "video/3gpp" = desk "videoPlayer"; + "video/ogg" = desk "videoPlayer"; + "video/x-ogm+ogg" = desk "videoPlayer"; + "video/x-ms-wmv" = desk "videoPlayer"; + + # audio + "audio/mpeg" = desk "musicPlayer"; + "audio/mp3" = desk "musicPlayer"; + "audio/ogg" = desk "musicPlayer"; + "audio/flac" = desk "musicPlayer"; + "audio/x-flac" = desk "musicPlayer"; + "audio/wav" = desk "musicPlayer"; + "audio/x-wav" = desk "musicPlayer"; + "audio/aac" = desk "musicPlayer"; + "audio/mp4" = desk "musicPlayer"; + "audio/x-m4a" = desk "musicPlayer"; + "audio/vorbis" = desk "musicPlayer"; + "audio/x-vorbis+ogg" = desk "musicPlayer"; + "audio/opus" = desk "musicPlayer"; + "audio/x-opus+ogg" = desk "musicPlayer"; + "audio/x-ape" = desk "musicPlayer"; + "audio/x-wavpack" = desk "musicPlayer"; + "audio/aiff" = desk "musicPlayer"; + "audio/x-aiff" = desk "musicPlayer"; + + # ebooks + "application/epub+zip" = desk "ebookReader"; + "application/x-mobipocket-ebook" = desk "ebookReader"; + "application/x-fictionbook+xml" = desk "ebookReader"; + "application/x-fictionbook" = desk "ebookReader"; + "application/x-cbz" = desk "ebookReader"; + "application/x-cbr" = desk "ebookReader"; + "image/vnd.djvu" = desk "ebookReader"; + "image/x-djvu" = desk "ebookReader"; + + # office — open document + "application/vnd.oasis.opendocument.text" = desktopIds.officeSuite.writer; + "application/vnd.oasis.opendocument.text-template" = desktopIds.officeSuite.writer; + "application/vnd.oasis.opendocument.spreadsheet" = desktopIds.officeSuite.calc; + "application/vnd.oasis.opendocument.spreadsheet-template" = desktopIds.officeSuite.calc; + "application/vnd.oasis.opendocument.presentation" = desktopIds.officeSuite.impress; + "application/vnd.oasis.opendocument.presentation-template" = desktopIds.officeSuite.impress; + "application/vnd.oasis.opendocument.graphics" = desktopIds.officeSuite.draw; + # office — microsoft + "application/msword" = desktopIds.officeSuite.writer; + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" = desktopIds.officeSuite.writer; + "application/vnd.ms-excel" = desktopIds.officeSuite.calc; + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" = desktopIds.officeSuite.calc; + "text/csv" = desktopIds.officeSuite.calc; + "application/vnd.ms-powerpoint" = desktopIds.officeSuite.impress; + "application/vnd.openxmlformats-officedocument.presentationml.presentation" = desktopIds.officeSuite.impress; + }; +} diff --git a/common/nix/gc.nix b/common/nix/gc.nix index 5a02bbe..41f90e2 100644 --- a/common/nix/gc.nix +++ b/common/nix/gc.nix @@ -1,4 +1,5 @@ { + pkgs, inputs, ... }: @@ -6,12 +7,20 @@ imports = [ inputs.angrr.nixosModules.angrr ]; + nix = { + package = pkgs.nixVersions.nix_2_31; + optimise = { + automatic = true; + dates = "weekly"; + persistent = true; - nix.gc = { - automatic = true; - dates = "weekly"; - persistent = true; - options = "--delete-older-than 14d"; + }; + gc = { + automatic = true; + dates = "weekly"; + persistent = true; + options = "--delete-older-than 14d"; + }; }; services.angrr = { diff --git a/common/nix/plugins.nix b/common/nix/plugins.nix index a98596a..ddf05cc 100644 --- a/common/nix/plugins.nix +++ b/common/nix/plugins.nix @@ -1,13 +1,14 @@ { inputs, getFlakePkg, + config, ... }: { - nix.settings.plugin-files = [ - "${getFlakePkg inputs.nix-scope-plugin}/lib/nix/plugins/libnix-scope-plugin.so" + imports = [ + inputs.nix-shorturl-plugin.nixosModules.default + inputs.nix-scope-plugin.nixosModules.default ]; - imports = [ inputs.nix-shorturl-plugin.nixosModules.default ]; nix.shorturls = { enable = true; schemes = { diff --git a/common/nix/substituters.nix b/common/nix/substituters.nix index 7dd92c5..f0d5d58 100644 --- a/common/nix/substituters.nix +++ b/common/nix/substituters.nix @@ -9,7 +9,7 @@ scope "nix.settings" { # "https://cache.atagen.co" # "https://cache.privatevoid.net" "https://cache.flox.dev" - # "https://cache.amaanq.com" + "https://cache.amaanq.com" "https://cache.nixos-cuda.org" "https://niri.cachix.org" ]; @@ -20,7 +20,7 @@ scope "nix.settings" { # "cache.atagen.co:SOUkNQxuu/eQ7FcI8nlUe7FpV27e7YjQlDQdn8HTUnw=" # "cache.privatevoid.net:SErQ8bvNWANeAvtsOESUwVYr2VJynfuc9JRwlzTTkVg=" "flox-cache-public-1:7F4OyH7ZCnFhcze3fJdfyXYLQw/aV7GEed86nQ7IsOs=" - # "cache.amaanq.com:H0iXsEEFsvUNtWb5v9V8Kss+L4F/tnXwDHXcY+xbmKk=" + "cache.amaanq.com:H0iXsEEFsvUNtWb5v9V8Kss+L4F/tnXwDHXcY+xbmKk=" "cache.nixos-cuda.org:74DUi4Ye579gUqzH4ziL9IyiJBlDpMRn9MBN8oNan9M=" "niri.cachix.org-1:Wv0OmO7PsuocRKzfDoJ3mulSl7Z6oezYhGhR+3W2964=" ]; diff --git a/common/users.nix b/common/users.nix index ad2b578..71d8b72 100644 --- a/common/users.nix +++ b/common/users.nix @@ -1,6 +1,5 @@ { lib, mainUser, ... }: { - services.userborn.enable = lib.mkDefault true; nix.settings.trusted-users = [ mainUser ]; users.users.${mainUser} = { isNormalUser = true; diff --git a/flake.lock b/flake.lock index 742ecf4..c06e08a 100644 --- a/flake.lock +++ b/flake.lock @@ -25,11 +25,11 @@ "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1774023259, - "narHash": "sha256-08QDVfScqZOrBhNdm8VUy1nIBnNgsnUhf7vHKKVdelw=", + "lastModified": 1776730547, + "narHash": "sha256-X0ZdetAsg4TaoKm6wGyzaZ/X2TlQFWQLHAUaMWDr+7A=", "owner": "linyinfeng", "repo": "angrr", - "rev": "9e327b2fa6e548ea9bebdabb667a09ce682aef0c", + "rev": "bb5cdadcce3e4406fbf79e7f3bcfea59794075cf", "type": "github" }, "original": { @@ -54,12 +54,15 @@ } }, "bunker": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, "locked": { - "lastModified": 1774155802, - "narHash": "sha256-W8QitI0AoI9CgRzNKoAwrGQTekhF45a0G9YeG3JLXck=", + "lastModified": 1777267575, + "narHash": "sha256-58VFXbVR76Lk96qIXU4zkyQgBUVm25kShazR5uSfQgQ=", "owner": "amaanq", "repo": "bunker-patches", - "rev": "1d6c5b0102797e6281e24311181781323b64c01d", + "rev": "27bf12d2e5e17d8932d9c043a605d8733a167b6c", "type": "github" }, "original": { @@ -70,11 +73,11 @@ }, "crane": { "locked": { - "lastModified": 1766194365, - "narHash": "sha256-4AFsUZ0kl6MXSm4BaQgItD0VGlEKR3iq7gIaL7TjBvc=", + "lastModified": 1775839657, + "narHash": "sha256-SPm9ck7jh3Un9nwPuMGbRU04UroFmOHjLP56T10MOeM=", "owner": "ipetkov", "repo": "crane", - "rev": "7d8ec2c71771937ab99790b45e6d9b93d15d9379", + "rev": "7cf72d978629469c4bd4206b95c402514c1f6000", "type": "github" }, "original": { @@ -83,25 +86,19 @@ "type": "github" } }, - "culr": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ], - "systems": "systems" - }, + "crane_2": { "locked": { - "lastModified": 1769663733, - "narHash": "sha256-wXko1Dptg1eof0MQMy/3oXt2QLjUVaQVSY5nIOcOiks=", - "ref": "refs/heads/master", - "rev": "bf1e3adeeba5534db64d8ba6b7aa3abb036c2c01", - "revCount": 71, - "type": "git", - "url": "https://git.lobotomise.me/atagen/culr" + "lastModified": 1776635034, + "narHash": "sha256-OEOJrT3ZfwbChzODfIH4GzlNTtOFuZFWPtW7jIeR8xU=", + "owner": "ipetkov", + "repo": "crane", + "rev": "dc7496d8ea6e526b1254b55d09b966e94673750f", + "type": "github" }, "original": { - "type": "git", - "url": "https://git.lobotomise.me/atagen/culr" + "owner": "ipetkov", + "repo": "crane", + "type": "github" } }, "fenix": { @@ -167,11 +164,11 @@ ] }, "locked": { - "lastModified": 1772408722, - "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=", + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", "type": "github" }, "original": { @@ -201,24 +198,6 @@ "type": "github" } }, - "flake-parts_3": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib_2" - }, - "locked": { - "lastModified": 1772408722, - "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, "git-hooks-nix": { "inputs": { "flake-compat": "flake-compat_2", @@ -266,14 +245,14 @@ }, "helium": { "inputs": { - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1774471299, - "narHash": "sha256-2c5vpl2WUUvXR4VPMjzKnGxdnVfkjblmd06Le0s/pMI=", + "lastModified": 1776646564, + "narHash": "sha256-/v9Hy6wnIu4RIEOkzL6Wy1WpBDkst9GMn+hXJLC2C7U=", "owner": "amaanq", "repo": "helium-flake", - "rev": "9dce150ececbf2996d356b96d90fe8468cca8871", + "rev": "a7c9452dbc7977c35409aeff4eb71c8730e1e466", "type": "github" }, "original": { @@ -285,7 +264,7 @@ "hudcore": { "inputs": { "nix-systems": "nix-systems", - "nixpkgs": "nixpkgs_3" + "nixpkgs": "nixpkgs_4" }, "locked": { "lastModified": 1774692528, @@ -304,6 +283,7 @@ "inshellah": { "inputs": { "nixpkgs": [ + "bunker", "nixpkgs" ] }, @@ -341,6 +321,7 @@ "inputs": { "nix-systems": "nix-systems_2", "nixpkgs": [ + "bunker", "nixpkgs" ], "unf": "unf" @@ -362,7 +343,7 @@ "naersk": { "inputs": { "fenix": "fenix", - "nixpkgs": "nixpkgs_7" + "nixpkgs": "nixpkgs_8" }, "locked": { "lastModified": 1768908532, @@ -380,7 +361,7 @@ }, "ndg": { "inputs": { - "nixpkgs": "nixpkgs_4" + "nixpkgs": "nixpkgs_5" }, "locked": { "lastModified": 1773478949, @@ -399,6 +380,7 @@ "nil": { "inputs": { "nixpkgs": [ + "bunker", "nixpkgs" ] }, @@ -420,17 +402,17 @@ "inputs": { "niri-stable": "niri-stable", "niri-unstable": "niri-unstable", - "nixpkgs": "nixpkgs_6", + "nixpkgs": "nixpkgs_7", "nixpkgs-stable": "nixpkgs-stable", "xwayland-satellite-stable": "xwayland-satellite-stable", "xwayland-satellite-unstable": "xwayland-satellite-unstable" }, "locked": { - "lastModified": 1774539534, - "narHash": "sha256-kRKybUNiXTivTZSUnqwXHCY4GRD3e+Nu+1Mb8jf2HCI=", + "lastModified": 1776879043, + "narHash": "sha256-M9RjuowtoqQbFRdQAm2P6GjFwgHjRcnWYcB7ChSjDms=", "owner": "sodiboo", "repo": "niri-flake", - "rev": "975e9c55001333df0ddab36d938372c14917998a", + "rev": "535ebbe038039215a5d1c6c0c67f833409a5be96", "type": "github" }, "original": { @@ -476,9 +458,10 @@ "naersk": "naersk", "niri": "niri_2", "nixpkgs": [ + "bunker", "nixpkgs" ], - "systems": "systems_2" + "systems": "systems" }, "locked": { "lastModified": 1772457471, @@ -497,11 +480,11 @@ "niri-unstable": { "flake": false, "locked": { - "lastModified": 1773130184, - "narHash": "sha256-3bwx4WqCB06yfQIGB+OgIckOkEDyKxiTD5pOo4Xz2rI=", + "lastModified": 1776853441, + "narHash": "sha256-mSxfoEs7DiDhMCBzprI/1K7UXzMISuGq0b7T06LVJXE=", "owner": "YaLTeR", "repo": "niri", - "rev": "b07bde3ee82dd73115e6b949e4f3f63695da35ea", + "rev": "74d2b18603366b98ec9045ecf4a632422f472365", "type": "github" }, "original": { @@ -534,11 +517,11 @@ ] }, "locked": { - "lastModified": 1773000227, - "narHash": "sha256-zm3ftUQw0MPumYi91HovoGhgyZBlM4o3Zy0LhPNwzXE=", + "lastModified": 1775037210, + "narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=", "owner": "nix-darwin", "repo": "nix-darwin", - "rev": "da529ac9e46f25ed5616fd634079a5f3c579135f", + "rev": "06648f4902343228ce2de79f291dd5a58ee12146", "type": "github" }, "original": { @@ -570,14 +553,14 @@ }, "nix-index-database": { "inputs": { - "nixpkgs": "nixpkgs_8" + "nixpkgs": "nixpkgs_9" }, "locked": { - "lastModified": 1774156144, - "narHash": "sha256-gdYe9wTPl4ignDyXUl1LlICWj41+S0GB5lG1fKP17+A=", + "lastModified": 1776829403, + "narHash": "sha256-oHVcvP2Ahhj1KUsEzp+2BQF55/r5VSa3QxdPdwE1p00=", "owner": "Mic92", "repo": "nix-index-database", - "rev": "55b588747fa3d7fc351a11831c4b874dab992862", + "rev": "c43246d4e9e506178b69baed075d797ec2d873e2", "type": "github" }, "original": { @@ -591,9 +574,9 @@ "flake-parts": "flake-parts_2", "git-hooks-nix": "git-hooks-nix", "kitty-themes-src": "kitty-themes-src", - "nixpkgs": "nixpkgs_9", + "nixpkgs": "nixpkgs_10", "nixpkgs-lib": "nixpkgs-lib", - "systems": "systems_3" + "systems": "systems_2" }, "locked": { "lastModified": 1768817933, @@ -612,15 +595,16 @@ "nix-scope-plugin": { "inputs": { "nixpkgs": [ + "bunker", "nixpkgs" ] }, "locked": { - "lastModified": 1772011628, - "narHash": "sha256-rhpsfs+/9zVr3HVqniwHyRSr92ga4J4mczFdHr1PN4s=", + "lastModified": 1776947301, + "narHash": "sha256-9jl+jsqh16I4hgjqV2YTuIQivjNBY9yW6Z2vhh9H1tk=", "ref": "refs/heads/main", - "rev": "0e5218513ec92ee751e7a0dc854082106b77473c", - "revCount": 1, + "rev": "70b537244df3f83523e82629b3c8cc1b0dea9c55", + "revCount": 4, "type": "git", "url": "https://git.lobotomise.me/atagen/nix-scope-plugin" }, @@ -632,15 +616,16 @@ "nix-shorturl-plugin": { "inputs": { "nixpkgs": [ + "bunker", "nixpkgs" ] }, "locked": { - "lastModified": 1771986085, - "narHash": "sha256-M7koECMnoi+7eI0yGj+Rxiu9gC/hdbJtwUR0AyTuFsY=", + "lastModified": 1776947322, + "narHash": "sha256-99Lh4XMng3ZbUFw2ISrS8ax/Q7E1fD8KGX4AA6odk48=", "ref": "refs/heads/main", - "rev": "807c3b0094963bb78198643b62b78e2a0c916230", - "revCount": 1, + "rev": "7fbcc8744d68f74a7439957fbc1d7eb8668c024d", + "revCount": 4, "type": "git", "url": "https://git.lobotomise.me/atagen/nix-shorturl-plugin" }, @@ -679,13 +664,34 @@ "type": "github" } }, + "nixos-core": { + "inputs": { + "nixpkgs": [ + "bunker", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1776982365, + "narHash": "sha256-3KuTDkbIZsv/luigCj4dHi+3a5dQRmDV8j1zYyE0A1Q=", + "owner": "feel-co", + "repo": "nixos-core", + "rev": "c8297e1d2acadc1e690b7875f408a60c04c6d01a", + "type": "github" + }, + "original": { + "owner": "feel-co", + "repo": "nixos-core", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1773821835, - "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", + "lastModified": 1776548001, + "narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", + "rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc", "type": "github" }, "original": { @@ -710,28 +716,13 @@ "type": "github" } }, - "nixpkgs-lib_2": { - "locked": { - "lastModified": 1772328832, - "narHash": "sha256-e+/T/pmEkLP6BHhYjx6GmwP5ivonQQn0bJdH9YrRB+Q=", - "owner": "nix-community", - "repo": "nixpkgs.lib", - "rev": "c185c7a5e5dd8f9add5b2f8ebeff00888b070742", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixpkgs.lib", - "type": "github" - } - }, "nixpkgs-stable": { "locked": { - "lastModified": 1774388614, - "narHash": "sha256-tFwzTI0DdDzovdE9+Ras6CUss0yn8P9XV4Ja6RjA+nU=", + "lastModified": 1776734388, + "narHash": "sha256-vl3dkhlE5gzsItuHoEMVe+DlonsK+0836LIRDnm6MXQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1073dad219cb244572b74da2b20c7fe39cb3fa9e", + "rev": "10e7ad5bbcb421fe07e3a4ad53a634b0cd57ffac", "type": "github" }, "original": { @@ -743,11 +734,11 @@ }, "nixpkgs-stable_2": { "locked": { - "lastModified": 1774388614, - "narHash": "sha256-tFwzTI0DdDzovdE9+Ras6CUss0yn8P9XV4Ja6RjA+nU=", + "lastModified": 1776734388, + "narHash": "sha256-vl3dkhlE5gzsItuHoEMVe+DlonsK+0836LIRDnm6MXQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1073dad219cb244572b74da2b20c7fe39cb3fa9e", + "rev": "10e7ad5bbcb421fe07e3a4ad53a634b0cd57ffac", "type": "github" }, "original": { @@ -759,26 +750,26 @@ }, "nixpkgs_10": { "locked": { - "lastModified": 1774597904, - "narHash": "sha256-3lqVgn9FTxmYypvNX7PL5m8U89jD2darWa+3RomXsok=", - "owner": "NixOS", + "lastModified": 1768810879, + "narHash": "sha256-6RrqzfHu3e4vvRaDRY0muyl/BkFzCEPfwXmYY8iRjSw=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "c2e2c66a1bc800d99cb85b54a9b7a0bdd06432c5", + "rev": "0ef1c5b62bec8b182affa76ff079d80a2ba026ba", "type": "github" }, "original": { - "owner": "NixOS", + "owner": "nixos", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_11": { "locked": { - "lastModified": 1766309749, - "narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=", + "lastModified": 1775710090, + "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816", + "rev": "4c1018dae018162ec878d42fec712642d214fdfa", "type": "github" }, "original": { @@ -789,22 +780,6 @@ } }, "nixpkgs_12": { - "locked": { - "lastModified": 1769018530, - "narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "88d3861acdd3d2f0e361767018218e51810df8a1", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_13": { "locked": { "lastModified": 1766651565, "narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=", @@ -820,7 +795,35 @@ "type": "github" } }, + "nixpkgs_13": { + "locked": { + "lastModified": 1775350496, + "narHash": "sha256-uuw97G2Qm6C7rdrkq4zBzwLo0oA35flYijyMy65w/8c=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9341c707a0f78d80c73e1b403d98d728eda607ac", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs_2": { + "locked": { + "lastModified": 1776877367, + "narHash": "sha256-wMN1gM00sUQ2KC9CNr/XEOGdfOrl67PabIRv9AYayTo=", + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre985613.0726a0ecb6d4/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" + } + }, + "nixpkgs_3": { "locked": { "lastModified": 1770562336, "narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=", @@ -836,7 +839,7 @@ "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_4": { "locked": { "lastModified": 1746397377, "narHash": "sha256-5oLdRa3vWSRbuqPIFFmQBGGUqaYZBxX+GGtN9f/n4lU=", @@ -852,7 +855,7 @@ "type": "github" } }, - "nixpkgs_4": { + "nixpkgs_5": { "locked": { "lastModified": 1773282481, "narHash": "sha256-oFe06TmOy8UUT1f7xMHqDpSYq2Fy1mkIsXZUvdnyfeY=", @@ -865,7 +868,7 @@ "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" } }, - "nixpkgs_5": { + "nixpkgs_6": { "locked": { "lastModified": 1774078191, "narHash": "sha256-nyxxxW1/2ouu9dU0I02ul5pHrmUrE1JVFhfFlmYe3Lw=", @@ -881,13 +884,13 @@ "type": "github" } }, - "nixpkgs_6": { + "nixpkgs_7": { "locked": { - "lastModified": 1774386573, - "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", + "lastModified": 1776548001, + "narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", + "rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc", "type": "github" }, "original": { @@ -897,7 +900,7 @@ "type": "github" } }, - "nixpkgs_7": { + "nixpkgs_8": { "locked": { "lastModified": 1752077645, "narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=", @@ -913,13 +916,13 @@ "type": "github" } }, - "nixpkgs_8": { + "nixpkgs_9": { "locked": { - "lastModified": 1773821835, - "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "type": "github" }, "original": { @@ -929,24 +932,10 @@ "type": "github" } }, - "nixpkgs_9": { - "locked": { - "lastModified": 1768810879, - "narHash": "sha256-6RrqzfHu3e4vvRaDRY0muyl/BkFzCEPfwXmYY8iRjSw=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "0ef1c5b62bec8b182affa76ff079d80a2ba026ba", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "type": "github" - } - }, "qstn": { "inputs": { "nixpkgs": [ + "bunker", "nixpkgs" ] }, @@ -967,6 +956,7 @@ "qtengine": { "inputs": { "nixpkgs": [ + "bunker", "nixpkgs" ] }, @@ -990,7 +980,6 @@ "angrr": "angrr", "arbys": "arbys", "bunker": "bunker", - "culr": "culr", "helium": "helium", "hudcore": "hudcore", "inshellah": "inshellah", @@ -1003,13 +992,16 @@ "nix-rice": "nix-rice", "nix-scope-plugin": "nix-scope-plugin", "nix-shorturl-plugin": "nix-shorturl-plugin", - "nixpkgs": "nixpkgs_10", + "nixos-core": "nixos-core", + "nixpkgs": [ + "bunker", + "nixpkgs" + ], "nixpkgs-stable": "nixpkgs-stable_2", "qstn": "qstn", "qtengine": "qtengine", "run0-shim": "run0-shim", "stash": "stash", - "stasis": "stasis", "tuigreet": "tuigreet", "wry": "wry", "yoke": "yoke" @@ -1019,16 +1011,17 @@ "inputs": { "nix-github-actions": "nix-github-actions", "nixpkgs": [ + "bunker", "nixpkgs" ], "treefmt-nix": "treefmt-nix_2" }, "locked": { - "lastModified": 1774550565, - "narHash": "sha256-5MLZTT9UDFJeOrtp+VKEkWSwLsx0ZMHkuPlcLPFoxlE=", + "lastModified": 1774702115, + "narHash": "sha256-iZ0HSQwjr9nYpVn10ZI4zQTdqvSggfxhXZ1c4oSZnuc=", "owner": "lordgrimmauld", "repo": "run0-sudo-shim", - "rev": "ef368ac5a90d0cc1977deae10f4cf7373c36786c", + "rev": "c9e06e2f220ab2fcf2228d4315c0a7fc2dc6e438", "type": "github" }, "original": { @@ -1054,17 +1047,38 @@ "type": "github" } }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "tuigreet", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1776654897, + "narHash": "sha256-Vqi4AiJVCcBGn/RmBtRCgyH5rCxqm/w0xV9diJWF1Ic=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "25d75be8139815a53560745fa060909777495105", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, "stash": { "inputs": { "crane": "crane", "nixpkgs": "nixpkgs_11" }, "locked": { - "lastModified": 1775243632, - "narHash": "sha256-aN31z1Pf0Aaz1r0PgtHFYBBM3QFDtnd7hiL7xGGpQLM=", + "lastModified": 1776783630, + "narHash": "sha256-FxNTnfZTH96ptne2wH5N4DzctdTHy75cPepZaVNQwck=", "owner": "notashelf", "repo": "stash", - "rev": "f139bda7b296e66998cc3077026cf18abfc9709d", + "rev": "4d3c99368fdb9373f0d7423de1a9642ae0c11745", "type": "github" }, "original": { @@ -1073,25 +1087,6 @@ "type": "github" } }, - "stasis": { - "inputs": { - "flake-parts": "flake-parts_3", - "nixpkgs": "nixpkgs_12" - }, - "locked": { - "lastModified": 1774290684, - "narHash": "sha256-1EsUpXEqL1bV3M1QiLFieynYqVpQY+vkqB98R6M0pIk=", - "owner": "saltnpepper97", - "repo": "stasis", - "rev": "1375c3afd6941fa874a5666974feecdf6644e55f", - "type": "github" - }, - "original": { - "owner": "saltnpepper97", - "repo": "stasis", - "type": "github" - } - }, "systems": { "locked": { "lastModified": 1689347949, @@ -1108,21 +1103,6 @@ } }, "systems_2": { - "locked": { - "lastModified": 1689347949, - "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", - "owner": "nix-systems", - "repo": "default-linux", - "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default-linux", - "type": "github" - } - }, - "systems_3": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -1137,7 +1117,7 @@ "type": "github" } }, - "systems_4": { + "systems_3": { "locked": { "lastModified": 1689347949, "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", @@ -1160,11 +1140,11 @@ ] }, "locked": { - "lastModified": 1773297127, - "narHash": "sha256-6E/yhXP7Oy/NbXtf1ktzmU8SdVqJQ09HC/48ebEGBpk=", + "lastModified": 1775636079, + "narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "71b125cd05fbfd78cab3e070b73544abe24c5016", + "rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba", "type": "github" }, "original": { @@ -1196,27 +1176,25 @@ }, "tuigreet": { "inputs": { - "nixpkgs": "nixpkgs_13" + "crane": "crane_2", + "nixpkgs": "nixpkgs_12", + "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1772641147, - "narHash": "sha256-ejqi9ujkralaPmvC3xY99NnhajNAyjPg3Nf72asKQno=", - "owner": "notashelf", - "repo": "tuigreet", - "rev": "fa44a85b65fb86984cb17de05208b46d1bd1b407", - "type": "github" + "lastModified": 1777107181, + "narHash": "sha256-zC0gTr/Rlz0lPH+LiH/UAeZgzLqr2Az270CB0xERGbI=", + "path": "/home/bolt/code/etc/tuigreet", + "type": "path" }, "original": { - "owner": "notashelf", - "repo": "tuigreet", - "rev": "fa44a85b65fb86984cb17de05208b46d1bd1b407", - "type": "github" + "path": "/home/bolt/code/etc/tuigreet", + "type": "path" } }, "unf": { "inputs": { "ndg": "ndg", - "nixpkgs": "nixpkgs_5" + "nixpkgs": "nixpkgs_6" }, "locked": { "lastModified": 1760178630, @@ -1234,22 +1212,17 @@ }, "wry": { "inputs": { - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs": "nixpkgs_13" }, "locked": { - "lastModified": 1775563304, - "narHash": "sha256-+2lylj1Aw6tFCH9w6Ua+jcYTAPU3Btx8giV0tySdIw4=", - "ref": "refs/heads/master", - "rev": "7aea0095e991a7b5b52e6109ee79a9deb8550d7b", - "revCount": 2084, - "type": "git", - "url": "ssh://git@git.kosslan.dev/wry/jay" + "lastModified": 1776171180, + "narHash": "sha256-hmf6U/73HgOgigs26zcpfVCQEZTDNWBi7whDNcYpBXo=", + "path": "/home/bolt/code/wry", + "type": "path" }, "original": { - "type": "git", - "url": "ssh://git@git.kosslan.dev/wry/jay" + "path": "/home/bolt/code/wry", + "type": "path" } }, "xwayland-satellite-stable": { @@ -1288,9 +1261,10 @@ "yoke": { "inputs": { "nixpkgs": [ + "bunker", "nixpkgs" ], - "systems": "systems_4" + "systems": "systems_3" }, "locked": { "lastModified": 1772106982, diff --git a/flake.nix b/flake.nix index a28f94d..f17ca9c 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,8 @@ outputs = _: { }; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs"; + # nixpkgs.url = "github:NixOS/nixpkgs"; + nixpkgs.follows = "bunker/nixpkgs"; nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.11"; nix-index-database.url = "github:Mic92/nix-index-database"; @@ -13,12 +14,7 @@ meat = { url = "atagen:meat"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - - culr = { - url = "atagen:culr"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; }; hudcore.url = "atagen:hudcore-plymouth"; @@ -26,7 +22,7 @@ niri-tag = { url = "atagen:niri-tag"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; }; angrr.url = "github:linyinfeng/angrr"; @@ -35,7 +31,7 @@ qstn = { url = "atagen:qstn"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; }; __flake-compat = { @@ -45,55 +41,58 @@ yoke = { url = "atagen:yoke"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; }; run0-shim = { url = "github:lordgrimmauld/run0-sudo-shim"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; }; bunker.url = "amaan:bunker-patches"; - stasis.url = "github:saltnpepper97/stasis"; - niri-s76.url = "atagen:niri-s76-bridge"; helium.url = "amaan:helium-flake"; nil = { url = "github:atagen/nil"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; }; nix-scope-plugin = { url = "atagen:nix-scope-plugin"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; }; nix-shorturl-plugin = { url = "atagen:nix-shorturl-plugin"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; }; stash.url = "github:notashelf/stash"; wry = { - url = "kosslan:wry/jay"; - # url = "path:/home/bolt/code/wry"; - inputs.nixpkgs.follows = "nixpkgs"; + # url = "kosslan:wry/jay"; + url = "path:/home/bolt/code/wry"; + # inputs.nixpkgs.follows = "bunker/nixpkgs"; }; - tuigreet.url = "github:notashelf/tuigreet/fa44a85b65fb86984cb17de05208b46d1bd1b407"; + # tuigreet.url = "github:notashelf/tuigreet"; + tuigreet.url = "path:/home/bolt/code/etc/tuigreet"; inshellah = { - # url = "path:/home/bolt/code/inshellah"; url = "atagen:inshellah"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; }; qtengine = { url = "github:kossLAN/qtengine"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; + }; + + nixos-core = { + url = "github:feel-co/nixos-core"; + inputs.nixpkgs.follows = "bunker/nixpkgs"; }; }; diff --git a/graphical/boot.nix b/graphical/boot.nix index 426f030..446cd6f 100644 --- a/graphical/boot.nix +++ b/graphical/boot.nix @@ -40,7 +40,7 @@ scope "boot" { maxGenerations = 5; }; plymouth = { - enable = true; + enable = false; inherit (config.rice.plymouth) theme themePackages font; }; diff --git a/graphical/desktop/quickshell/rice/Colours.qml b/graphical/desktop/quickshell/rice/Colours.qml index 22f5075..9e86f6a 120000 --- a/graphical/desktop/quickshell/rice/Colours.qml +++ b/graphical/desktop/quickshell/rice/Colours.qml @@ -1 +1 @@ -/nix/store/bc2v8k5620k5p57ggrxy6i5w6pay9kis-Colours.qml \ No newline at end of file +/nix/store/1ajn9faiksanmy64n7sg4xjqij1cc5qc-Colours.qml \ No newline at end of file diff --git a/graphical/desktop/shell.nix b/graphical/desktop/shell.nix index 1ce000d..bc1592b 100644 --- a/graphical/desktop/shell.nix +++ b/graphical/desktop/shell.nix @@ -5,6 +5,7 @@ mainUser, getPkgs, config, + getFlakePkg, ... }: let @@ -13,8 +14,8 @@ let let grim = lib.getExe pkgs.grim; slurp = lib.getExe pkgs.slurp; - wl-copy = lib.getExe' pkgs.wl-clipboard-rs "wl-copy"; - wl-paste = lib.getExe' pkgs.wl-clipboard-rs "wl-paste"; + wl-copy = lib.getExe' (getFlakePkg inputs.stash) "wl-copy"; + wl-paste = lib.getExe' (getFlakePkg inputs.stash) "wl-paste"; in pkgs.writeScriptBin "shotta" '' #! /usr/bin/env nu @@ -29,28 +30,12 @@ in environment.systemPackages = getPkgs { inherit shotta; inherit (pkgs.kdePackages) qtbase qtdeclarative; - inherit (pkgs) wl-clipboard quickshell; + inherit (pkgs) quickshell; }; imports = [ - inputs.stasis.nixosModules.default inputs.stash.nixosModules.default ]; - services.stasis = { - enable = false; - extraConfig = '' - default: - dpms_off: - timeout 300 - command "niri msg action power-off-monitors" - end - suspend: - timeout 600 - command "systemctl suspend" - end - end - ''; - }; services.stash-clipboard = { enable = true; diff --git a/graphical/desktop/wm.nix b/graphical/desktop/wm.nix index 108860a..0cd7de2 100644 --- a/graphical/desktop/wm.nix +++ b/graphical/desktop/wm.nix @@ -104,6 +104,7 @@ in config.programs.niri = { enable = true; package = niri; + useNautilus = false; }; config.services.niri-tag = { diff --git a/graphical/desktop/wry.nix b/graphical/desktop/wry.nix index 92bc4cc..d5831d2 100644 --- a/graphical/desktop/wry.nix +++ b/graphical/desktop/wry.nix @@ -26,7 +26,7 @@ in services.greetd = { enable = true; - settings.default_session.command = "${lib.getExe (getFlakePkg inputs.tuigreet)} --sessions /etc/greetd/wayland-sessions --remember-session"; + settings.default_session.command = "${lib.getExe (getFlakePkg inputs.tuigreet)} --sessions /etc/greetd/wayland-sessions --remember-session --animation doom"; }; environment.etc."greetd/wayland-sessions/wry.desktop".text = '' diff --git a/graphical/dev.nix b/graphical/dev.nix index 9ea45d2..9c47f75 100644 --- a/graphical/dev.nix +++ b/graphical/dev.nix @@ -7,6 +7,7 @@ programs.git = { enable = true; + package = pkgs.gitMinimal; config = { user = { name = "atagen"; diff --git a/graphical/documents.nix b/graphical/documents.nix index 2286e67..c07fe1a 100644 --- a/graphical/documents.nix +++ b/graphical/documents.nix @@ -1,9 +1,11 @@ { pkgs, config, + mkWrappers, ... }: let + inherit (mkWrappers pkgs) wrap; ui = config.rice.roles config.rice.palette.hex; zathuraConfigDir = pkgs.runCommand "zathura-config" { } '' mkdir -p $out @@ -44,14 +46,10 @@ let set index-active-bg "${ui.secondary}" ''} $out/zathurarc ''; - zathuraWrapped = pkgs.symlinkJoin { + zathuraWrapped = wrap { name = "zathura"; - paths = [ pkgs.zathura ]; - nativeBuildInputs = [ pkgs.makeWrapper ]; - postBuild = '' - wrapProgram $out/bin/zathura \ - --add-flags "--config-dir=${zathuraConfigDir}" - ''; + pkg = pkgs.zathura; + args = [ "--config-dir=${zathuraConfigDir}" ]; }; in with pkgs; @@ -59,7 +57,7 @@ scope "apps" { officeSuite = libreoffice; mailClient = thunderbird; noteTaking = obsidian; - ebookReader = foliate; + # ebookReader = foliate; # contains perl pdfReader = zathuraWrapped; calculator = mate-calc; } diff --git a/graphical/foot-tabs.patch b/graphical/foot-tabs.patch new file mode 100644 index 0000000..240f907 --- /dev/null +++ b/graphical/foot-tabs.patch @@ -0,0 +1,1902 @@ +diff --git a/config.c b/config.c +index 481d4c4f..7684b570 100644 +--- a/config.c ++++ b/config.c +@@ -148,6 +148,21 @@ static const char *const binding_action_map[] = { + [BIND_ACTION_THEME_SWITCH_LIGHT] = "color-theme-switch-light", + [BIND_ACTION_THEME_TOGGLE] = "color-theme-toggle", + ++ /* Tab actions */ ++ [BIND_ACTION_TAB_NEW] = "tab-new", ++ [BIND_ACTION_TAB_CLOSE] = "tab-close", ++ [BIND_ACTION_TAB_NEXT] = "tab-next", ++ [BIND_ACTION_TAB_PREV] = "tab-prev", ++ [BIND_ACTION_TAB_1] = "tab-1", ++ [BIND_ACTION_TAB_2] = "tab-2", ++ [BIND_ACTION_TAB_3] = "tab-3", ++ [BIND_ACTION_TAB_4] = "tab-4", ++ [BIND_ACTION_TAB_5] = "tab-5", ++ [BIND_ACTION_TAB_6] = "tab-6", ++ [BIND_ACTION_TAB_7] = "tab-7", ++ [BIND_ACTION_TAB_8] = "tab-8", ++ [BIND_ACTION_TAB_9] = "tab-9", ++ + /* Mouse-specific actions */ + [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", + [BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse", +@@ -1816,6 +1831,84 @@ parse_section_csd(struct context *ctx) + } + } + ++static bool ++parse_section_tabs(struct context *ctx) ++{ ++ struct config *conf = ctx->conf; ++ const char *key = ctx->key; ++ ++ if (streq(key, "enabled")) ++ return value_to_bool(ctx, &conf->tabs.enabled); ++ ++ else if (streq(key, "position")) { ++ _Static_assert(sizeof(conf->tabs.position) == sizeof(int), ++ "enum is not 32-bit"); ++ return value_to_enum( ++ ctx, ++ (const char *[]){"top", "bottom", NULL}, ++ (int *)&conf->tabs.position); ++ } ++ ++ else if (streq(key, "style")) { ++ _Static_assert(sizeof(conf->tabs.style) == sizeof(int), ++ "enum is not 32-bit"); ++ return value_to_enum( ++ ctx, ++ (const char *[]){"rounded", "square", "gradient", NULL}, ++ (int *)&conf->tabs.style); ++ } ++ ++ else if (streq(key, "layout")) { ++ _Static_assert(sizeof(conf->tabs.layout) == sizeof(int), ++ "enum is not 32-bit"); ++ return value_to_enum( ++ ctx, ++ (const char *[]){"span", "floating", NULL}, ++ (int *)&conf->tabs.layout); ++ } ++ ++ else if (streq(key, "height")) ++ return value_to_uint16(ctx, 10, &conf->tabs.height); ++ ++ else if (streq(key, "title-max-length")) ++ return value_to_uint16(ctx, 10, &conf->tabs.title_max_length); ++ ++ else if (streq(key, "tab-width")) ++ return value_to_uint16(ctx, 10, &conf->tabs.tab_width); ++ ++ else if (streq(key, "tab-padding")) ++ return value_to_uint16(ctx, 10, &conf->tabs.tab_padding); ++ ++ else if (streq(key, "label-padding")) ++ return value_to_uint16(ctx, 10, &conf->tabs.label_padding); ++ ++ else if (streq(key, "margin")) ++ return value_to_uint16(ctx, 10, &conf->tabs.margin); ++ ++ else if (streq(key, "corner-radius")) ++ return value_to_uint16(ctx, 10, &conf->tabs.corner_radius); ++ ++ else if (streq(key, "background")) ++ return value_to_color(ctx, &conf->tabs.colors.bg, false); ++ ++ else if (streq(key, "foreground")) ++ return value_to_color(ctx, &conf->tabs.colors.fg, false); ++ ++ else if (streq(key, "active-background")) ++ return value_to_color(ctx, &conf->tabs.colors.active_bg, false); ++ ++ else if (streq(key, "active-foreground")) ++ return value_to_color(ctx, &conf->tabs.colors.active_fg, false); ++ ++ else if (streq(key, "inherit-cwd")) ++ return value_to_bool(ctx, &conf->tabs.inherit_cwd); ++ ++ else { ++ LOG_CONTEXTUAL_ERR("not a valid tabs option: %s", key); ++ return false; ++ } ++} ++ + static void + free_binding_aux(struct binding_aux *aux) + { +@@ -3062,6 +3155,7 @@ enum section { + SECTION_ENVIRONMENT, + SECTION_TWEAK, + SECTION_TOUCH, ++ SECTION_TABS, + + /* Deprecated */ + SECTION_COLORS, +@@ -3098,6 +3192,7 @@ static const struct { + [SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"}, + [SECTION_TWEAK] = {&parse_section_tweak, "tweak"}, + [SECTION_TOUCH] = {&parse_section_touch, "touch"}, ++ [SECTION_TABS] = {&parse_section_tabs, "tabs"}, + + /* Deprecated */ + [SECTION_COLORS] = {&parse_section_colors, "colors"}, +@@ -3343,6 +3438,10 @@ add_default_key_bindings(struct config *conf) + {BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_0}}}, + {BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_0}}}, + {BIND_ACTION_SPAWN_TERMINAL, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_n}}}, ++ {BIND_ACTION_TAB_NEW, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_t}}}, ++ {BIND_ACTION_TAB_CLOSE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}}, ++ {BIND_ACTION_TAB_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Tab}}}, ++ {BIND_ACTION_TAB_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Tab}}}, + {BIND_ACTION_SHOW_URLS_LAUNCH, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_o}}}, + {BIND_ACTION_UNICODE_INPUT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_u}}}, + {BIND_ACTION_PROMPT_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_z}}}, +@@ -3630,6 +3729,27 @@ config_load(struct config *conf, const char *conf_path, + .long_press_delay = 400, + }, + ++ .tabs = { ++ .enabled = false, ++ .inherit_cwd = false, ++ .position = CONF_TABS_POSITION_TOP, ++ .style = CONF_TABS_STYLE_ROUNDED, ++ .layout = CONF_TABS_LAYOUT_SPAN, ++ .height = 26, ++ .tab_width = 200, ++ .tab_padding = 8, ++ .label_padding = 8, ++ .margin = 4, ++ .corner_radius = 6, ++ .title_max_length = 100, ++ .colors = { ++ .bg = 0x1c1c1c, ++ .fg = 0xb0b0b0, ++ .active_bg = 0x3a3a3a, ++ .active_fg = 0xffffff, ++ }, ++ }, ++ + .env_vars = tll_init(), + #if defined(UTMP_DEFAULT_HELPER_PATH) + .utmp_helper_path = ((strlen(UTMP_DEFAULT_HELPER_PATH) > 0 && +diff --git a/config.h b/config.h +index f8e99df3..905f7079 100644 +--- a/config.h ++++ b/config.h +@@ -466,6 +466,37 @@ struct config { + uint32_t long_press_delay; + } touch; + ++ struct { ++ bool enabled; ++ enum { ++ CONF_TABS_POSITION_TOP, ++ CONF_TABS_POSITION_BOTTOM, ++ } position; ++ enum { ++ CONF_TABS_STYLE_ROUNDED, ++ CONF_TABS_STYLE_SQUARE, ++ CONF_TABS_STYLE_GRADIENT, ++ } style; ++ enum { ++ CONF_TABS_LAYOUT_SPAN, ++ CONF_TABS_LAYOUT_FLOATING, ++ } layout; ++ uint16_t height; /* pill height; bar = height + margin in floating mode */ ++ uint16_t tab_width; /* max tab width in floating mode */ ++ uint16_t tab_padding; /* gap between tabs in floating mode */ ++ uint16_t label_padding; /* horizontal padding around the label inside each tab pill */ ++ uint16_t margin; /* edge gap in floating mode (added to bar height) */ ++ uint16_t corner_radius; ++ uint16_t title_max_length; /* max chars in composite window title before eliding with "..." */ ++ bool inherit_cwd; ++ struct { ++ uint32_t bg; ++ uint32_t fg; ++ uint32_t active_bg; ++ uint32_t active_fg; ++ } colors; ++ } tabs; ++ + user_notifications_t notifications; + }; + +diff --git a/cursor-shape.c b/cursor-shape.c +index c195a554..e68411c0 100644 +--- a/cursor-shape.c ++++ b/cursor-shape.c +@@ -16,6 +16,7 @@ cursor_shape_to_string(enum cursor_shape shape) + [CURSOR_SHAPE_NONE] = {NULL}, + [CURSOR_SHAPE_HIDDEN] = {"hidden", NULL}, + [CURSOR_SHAPE_LEFT_PTR] = {"default", "left_ptr", NULL}, ++ [CURSOR_SHAPE_POINTER] = {"pointer", "hand1", NULL}, + [CURSOR_SHAPE_TEXT] = {"text", "xterm", NULL}, + [CURSOR_SHAPE_TOP_LEFT_CORNER] = {"nw-resize", "top_left_corner", NULL}, + [CURSOR_SHAPE_TOP_RIGHT_CORNER] = {"ne-resize", "top_right_corner", NULL}, +@@ -37,6 +38,7 @@ cursor_shape_to_server_shape(enum cursor_shape shape) + { + static const enum wp_cursor_shape_device_v1_shape table[CURSOR_SHAPE_COUNT] = { + [CURSOR_SHAPE_LEFT_PTR] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, ++ [CURSOR_SHAPE_POINTER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER, + [CURSOR_SHAPE_TEXT] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, + [CURSOR_SHAPE_TOP_LEFT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE, + [CURSOR_SHAPE_TOP_RIGHT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE, +diff --git a/cursor-shape.h b/cursor-shape.h +index 13690588..51a411ed 100644 +--- a/cursor-shape.h ++++ b/cursor-shape.h +@@ -8,6 +8,7 @@ enum cursor_shape { + CURSOR_SHAPE_HIDDEN, + + CURSOR_SHAPE_LEFT_PTR, ++ CURSOR_SHAPE_POINTER, + CURSOR_SHAPE_TEXT, + CURSOR_SHAPE_TOP_LEFT_CORNER, + CURSOR_SHAPE_TOP_RIGHT_CORNER, +diff --git a/foot.ini b/foot.ini +index 81419d88..8b735fe1 100644 +--- a/foot.ini ++++ b/foot.ini +@@ -175,6 +175,24 @@ + # Same builtin defaults as [color], except for: + # dim-blend-towards=white + ++[tabs] ++enabled=yes ++position=bottom ++style=rounded # rounded | square | gradient ++layout=floating ++height=26 ++# tab-width=200 (max width per tab in floating mode) ++# tab-padding=8 (gap between tabs in floating mode) ++# label-padding=8 (horizontal padding around the label inside each tab pill) ++# margin=4 (edge gap; auto-added to bar height, does not squish pill) ++# corner-radius=6 (corner rounding in pixels) ++# title-max-length=100 (multi-tab window title elides at this many chars with "...") ++# background=1c1c1c ++# foreground=b0b0b0 ++# active-background=3a3a3a ++# active-foreground=ffffff ++# inherit-cwd=no (new tabs open in the active tab's cwd; requires OSC 7 shell support) ++ + [csd] + # preferred=server + # size=26 +diff --git a/input.c b/input.c +index 6a829a70..92e9fc1a 100644 +--- a/input.c ++++ b/input.c +@@ -457,6 +457,45 @@ execute_binding(struct seat *seat, struct terminal *term, + term_shutdown(term); + return true; + ++ case BIND_ACTION_TAB_NEW: ++ term_tab_new(term, 0, NULL, NULL, term->shutdown.cb, term->shutdown.cb_data); ++ return true; ++ ++ case BIND_ACTION_TAB_CLOSE: ++ term_tab_close(term); ++ return true; ++ ++ case BIND_ACTION_TAB_NEXT: { ++ struct wl_window *win = term->window; ++ if (win->tab_count > 1) ++ term_tab_switch(win, (win->active_tab + 1) % win->tab_count); ++ return true; ++ } ++ ++ case BIND_ACTION_TAB_PREV: { ++ struct wl_window *win = term->window; ++ if (win->tab_count > 1) ++ term_tab_switch(win, ++ win->active_tab == 0 ? win->tab_count - 1 : win->active_tab - 1); ++ return true; ++ } ++ ++ case BIND_ACTION_TAB_1: ++ case BIND_ACTION_TAB_2: ++ case BIND_ACTION_TAB_3: ++ case BIND_ACTION_TAB_4: ++ case BIND_ACTION_TAB_5: ++ case BIND_ACTION_TAB_6: ++ case BIND_ACTION_TAB_7: ++ case BIND_ACTION_TAB_8: ++ case BIND_ACTION_TAB_9: { ++ size_t idx = (size_t)(action - BIND_ACTION_TAB_1); ++ struct wl_window *win = term->window; ++ if (idx < win->tab_count) ++ term_tab_switch(win, idx); ++ return true; ++ } ++ + case BIND_ACTION_REGEX_LAUNCH: + case BIND_ACTION_REGEX_COPY: + if (binding->aux->type != BINDING_AUX_REGEX) +@@ -2510,6 +2549,7 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: ++ case TERM_SURF_TAB_BAR: + break; + + case TERM_SURF_BUTTON_MINIMIZE: +@@ -2582,7 +2622,7 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, + if (surface != NULL) { + /* Sway 1.4 sends this event with a NULL surface when we destroy the window */ + const struct wl_window UNUSED *win = wl_surface_get_user_data(surface); +- xassert(old_moused == win->term); ++ xassert(old_moused->window == win); + } + + enum term_surface active_surface = old_moused->active_surface; +@@ -2609,6 +2649,7 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: ++ case TERM_SURF_TAB_BAR: + break; + } + +@@ -2693,6 +2734,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: ++ case TERM_SURF_TAB_BAR: + break; + } + +@@ -2743,6 +2785,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: ++ case TERM_SURF_TAB_BAR: + break; + + case TERM_SURF_GRID: { +@@ -3294,6 +3337,21 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, + break; + } + ++ case TERM_SURF_TAB_BAR: { ++ if (state != WL_POINTER_BUTTON_STATE_PRESSED) ++ break; ++ if (button != BTN_LEFT) ++ break; ++ ++ size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y); ++ if (idx == SIZE_MAX || idx >= term->window->tab_count) ++ break; ++ if (idx == term->window->active_tab) ++ break; ++ term_tab_switch(term->window, idx); ++ break; ++ } ++ + case TERM_SURF_NONE: + BUG("Invalid surface type"); + break; +diff --git a/key-binding.h b/key-binding.h +index c4a04e99..acc11a7a 100644 +--- a/key-binding.h ++++ b/key-binding.h +@@ -49,6 +49,21 @@ enum bind_action_normal { + BIND_ACTION_THEME_SWITCH_LIGHT, + BIND_ACTION_THEME_TOGGLE, + ++ /* Tab actions */ ++ BIND_ACTION_TAB_NEW, ++ BIND_ACTION_TAB_CLOSE, ++ BIND_ACTION_TAB_NEXT, ++ BIND_ACTION_TAB_PREV, ++ BIND_ACTION_TAB_1, ++ BIND_ACTION_TAB_2, ++ BIND_ACTION_TAB_3, ++ BIND_ACTION_TAB_4, ++ BIND_ACTION_TAB_5, ++ BIND_ACTION_TAB_6, ++ BIND_ACTION_TAB_7, ++ BIND_ACTION_TAB_8, ++ BIND_ACTION_TAB_9, ++ + /* Mouse specific actions - i.e. they require a mouse coordinate */ + BIND_ACTION_SCROLLBACK_UP_MOUSE, + BIND_ACTION_SCROLLBACK_DOWN_MOUSE, +@@ -61,7 +76,7 @@ enum bind_action_normal { + BIND_ACTION_SELECT_QUOTE, + BIND_ACTION_SELECT_ROW, + +- BIND_ACTION_KEY_COUNT = BIND_ACTION_THEME_TOGGLE + 1, ++ BIND_ACTION_KEY_COUNT = BIND_ACTION_TAB_9 + 1, + BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, + }; + +diff --git a/osc.c b/osc.c +index 82793fb5..90704b70 100644 +--- a/osc.c ++++ b/osc.c +@@ -15,6 +15,7 @@ + #include "macros.h" + #include "notify.h" + #include "selection.h" ++#include "render.h" + #include "terminal.h" + #include "uri.h" + #include "util.h" +@@ -468,6 +469,7 @@ osc_set_pwd(struct terminal *term, char *string) + LOG_DBG("OSC7: pwd: %s", path); + free(term->cwd); + term->cwd = path; ++ render_refresh_tab_bar(term); + } else + free(path); + +diff --git a/pgo/pgo.c b/pgo/pgo.c +index 4ff4111c..96ddcce7 100644 +--- a/pgo/pgo.c ++++ b/pgo/pgo.c +@@ -71,6 +71,7 @@ void render_refresh_csd(struct terminal *term) {} + void render_refresh_title(struct terminal *term) {} + void render_refresh_app_id(struct terminal *term) {} + void render_refresh_icon(struct terminal *term) {} ++void render_refresh_tab_bar(struct terminal *term) {} + + void render_overlay(struct terminal *term) {} + +diff --git a/render.c b/render.c +index f74e9251..d7887d45 100644 +--- a/render.c ++++ b/render.c +@@ -60,6 +60,7 @@ static struct { + } presentation_statistics = {0}; + + static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data); ++static void render_tab_bar(struct terminal *term); + + struct renderer * + render_init(struct fdm *fdm, struct wayland *wayl) +@@ -4282,14 +4283,87 @@ render_urls(struct terminal *term) + } + } + ++/* Build "tab1 | tab2 | ... | tabN" composite. Result is xmalloc'd; caller frees. ++ * If the codepoint count exceeds max_chars, the tail is replaced with "...". */ ++static char * ++build_composite_title(const struct wl_window *win, size_t max_chars) ++{ ++ /* Sum the bytes needed for the full untruncated composite */ ++ size_t cap = 1; ++ for (size_t i = 0; i < win->tab_count; i++) { ++ const struct terminal *tab = win->tabs[i]; ++ const char *t = tab->window_title != NULL ? tab->window_title : "foot"; ++ cap += strlen(t) + (i > 0 ? 3 : 0); ++ } ++ ++ char *full = xmalloc(cap); ++ size_t pos = 0; ++ for (size_t i = 0; i < win->tab_count; i++) { ++ const struct terminal *tab = win->tabs[i]; ++ const char *t = tab->window_title != NULL ? tab->window_title : "foot"; ++ if (i > 0) { ++ memcpy(full + pos, " | ", 3); ++ pos += 3; ++ } ++ size_t tlen = strlen(t); ++ memcpy(full + pos, t, tlen); ++ pos += tlen; ++ } ++ full[pos] = '\0'; ++ ++ /* Count codepoints */ ++ size_t total = 0; ++ const unsigned char *p = (const unsigned char *)full; ++ while (*p != '\0') { ++ utf8proc_int32_t cp; ++ utf8proc_ssize_t consumed = utf8proc_iterate(p, -1, &cp); ++ if (consumed <= 0) break; ++ p += consumed; ++ total++; ++ } ++ ++ if (total <= max_chars) ++ return full; ++ ++ /* Walk the prefix to find the byte cut for (max_chars - 3) codepoints */ ++ const size_t target = max_chars > 3 ? max_chars - 3 : 0; ++ p = (const unsigned char *)full; ++ size_t k = 0; ++ size_t cut = 0; ++ while (*p != '\0' && k < target) { ++ utf8proc_int32_t cp; ++ utf8proc_ssize_t consumed = utf8proc_iterate(p, -1, &cp); ++ if (consumed <= 0) break; ++ p += consumed; ++ cut = (size_t)((const char *)p - full); ++ k++; ++ } ++ ++ char *result = xmalloc(cut + 4); ++ memcpy(result, full, cut); ++ memcpy(result + cut, "...", 3); ++ result[cut + 3] = '\0'; ++ free(full); ++ return result; ++} ++ + static void + render_update_title(struct terminal *term) + { + static const size_t max_len = 2048; + +- const char *title = term->window_title != NULL ? term->window_title : "foot"; +- char *copy = NULL; ++ struct wl_window *win = term->window; ++ char *composite = NULL; ++ const char *title; ++ ++ if (win != NULL && win->tab_count > 1) { ++ composite = build_composite_title(win, term->conf->tabs.title_max_length); ++ title = composite; ++ } else { ++ title = term->window_title != NULL ? term->window_title : "foot"; ++ } + ++ char *copy = NULL; + if (strlen(title) > max_len) { + copy = xstrndup(title, max_len); + title = copy; +@@ -4297,6 +4371,7 @@ render_update_title(struct terminal *term) + + xdg_toplevel_set_title(term->window->xdg_toplevel, title); + free(copy); ++ free(composite); + } + + static void +@@ -4614,10 +4689,28 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) + /* Padding */ + const int max_pad_x = (width - min_width) / 2; + const int max_pad_y = (height - min_height) / 2; ++ /* Total bar height = pill height + margin (margin only used in floating layout). ++ * Only reserve space for the bar when more than one tab is open. */ ++ const bool tab_bar_visible = ++ term->conf->tabs.enabled && term->window->tab_count > 1; ++ const uint16_t tabs_bar_logical = ++ tab_bar_visible ++ ? (term->conf->tabs.height + ++ (term->conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING ++ ? term->conf->tabs.margin : 0)) ++ : 0; ++ const int conf_pad_top = term->conf->pad_top + ++ (tab_bar_visible && ++ term->conf->tabs.position == CONF_TABS_POSITION_TOP ++ ? tabs_bar_logical : 0); ++ const int conf_pad_bottom = term->conf->pad_bottom + ++ (tab_bar_visible && ++ term->conf->tabs.position == CONF_TABS_POSITION_BOTTOM ++ ? tabs_bar_logical : 0); + const int pad_left = min(max_pad_x, scale * term->conf->pad_left); + const int pad_right = min(max_pad_x, scale * term->conf->pad_right); +- const int pad_top = min(max_pad_y, scale * term->conf->pad_top); +- const int pad_bottom= min(max_pad_y, scale * term->conf->pad_bottom); ++ const int pad_top = min(max_pad_y, scale * conf_pad_top); ++ const int pad_bottom= min(max_pad_y, scale * conf_pad_bottom); + + if (is_floating && + (opts & RESIZE_BY_CELLS) && +@@ -4709,8 +4802,8 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) + (center == CENTER_FULLSCREEN && term->window->is_fullscreen); + + if (centered_padding && !term->window->is_resizing) { +- term->margins.left = total_x_pad / 2; +- term->margins.top = total_y_pad / 2; ++ term->margins.left = max(pad_left, min(total_x_pad - pad_right, total_x_pad / 2)); ++ term->margins.top = max(pad_top, min(total_y_pad - pad_bottom, total_y_pad / 2)); + } else { + term->margins.left = pad_left; + term->margins.top = pad_top; +@@ -5133,6 +5226,10 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) + if (unlikely(term->shutdown.in_progress || !term->window->is_configured)) + continue; + ++ /* Skip inactive tabs - they process PTY data but don't render */ ++ if (term->window->term != term) ++ continue; ++ + bool grid = term->render.refresh.grid; + bool csd = term->render.refresh.csd; + bool search = term->is_searching && term->render.refresh.search; +@@ -5165,6 +5262,8 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) + render_search_box(term); + if (urls) + render_urls(term); ++ if (term->conf->tabs.enabled) ++ render_tab_bar(term); + if (grid | csd | search | urls) + grid_render(term); + +@@ -5180,6 +5279,15 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) + term->render.pending.csd |= csd; + term->render.pending.search |= search; + term->render.pending.urls |= urls; ++ ++ /* The tab bar is a desync subsurface — its commits apply ++ * independently of the parent surface, so we can repaint it ++ * now instead of deferring to the frame callback (which never ++ * calls render_tab_bar anyway). Without this, title changes ++ * while a frame is in flight leave the tab label stale until ++ * something else dirties the grid. */ ++ if (grid && term->conf->tabs.enabled) ++ render_tab_bar(term); + } + } + +@@ -5310,6 +5418,509 @@ render_refresh_urls(struct terminal *term) + term->render.refresh.urls = true; + } + ++void ++render_refresh_tab_bar(struct terminal *term) ++{ ++ if (term->conf->tabs.enabled) ++ term->render.refresh.grid = true; /* triggers full re-render which includes tab bar */ ++} ++ ++static void ++draw_rounded_corner_aa(pixman_image_t *pix, const pixman_color_t *color, ++ int dx, int dy, int r, ++ int cx_inside, int cy_inside) ++{ ++ if (r <= 0) ++ return; ++ ++ const int stride = (r + 3) & ~3; /* A8 stride aligned to 4 bytes */ ++ uint8_t *data = xcalloc(stride * r, sizeof(uint8_t)); ++ ++ /* Circle center in local (0..r, 0..r) coords of the corner tile. */ ++ const float cx = cx_inside > 0 ? (float)r : 0.f; ++ const float cy = cy_inside > 0 ? (float)r : 0.f; ++ const float r_f = (float)r; ++ ++ for (int py = 0; py < r; py++) { ++ for (int px = 0; px < r; px++) { ++ const float ex = (float)px + 0.5f - cx; ++ const float ey = (float)py + 0.5f - cy; ++ const float dist = sqrtf(ex * ex + ey * ey); ++ float cov = r_f - dist + 0.5f; ++ if (cov <= 0.f) ++ cov = 0.f; ++ else if (cov >= 1.f) ++ cov = 1.f; ++ data[py * stride + px] = (uint8_t)(cov * 255.f + 0.5f); ++ } ++ } ++ ++ pixman_image_t *mask = pixman_image_create_bits( ++ PIXMAN_a8, r, r, (uint32_t *)data, stride); ++ pixman_image_t *src = pixman_image_create_solid_fill(color); ++ pixman_image_composite32(PIXMAN_OP_OVER, ++ src, mask, pix, 0, 0, 0, 0, dx, dy, r, r); ++ pixman_image_unref(mask); ++ pixman_image_unref(src); ++ free(data); ++} ++ ++static void ++draw_rounded_rect(pixman_image_t *pix, const pixman_color_t *color, ++ int x, int y, int w, int h, int r, unsigned corners) ++{ ++ if (r <= 0 || w <= 0 || h <= 0) { ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, ++ &(pixman_rectangle16_t){x, y, w, h}); ++ return; ++ } ++ r = min(r, min(w / 2, h / 2)); ++ ++ const bool tl = corners & 1; ++ const bool tr = corners & 2; ++ const bool bl = corners & 4; ++ const bool br = corners & 8; ++ ++ /* Fill body regions. Corners handled separately if rounded. */ ++ /* Top band */ ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, ++ &(pixman_rectangle16_t){x + (tl ? r : 0), y, ++ w - (tl ? r : 0) - (tr ? r : 0), r}); ++ /* Middle band (full width) */ ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, ++ &(pixman_rectangle16_t){x, y + r, w, h - 2 * r}); ++ /* Bottom band */ ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, ++ &(pixman_rectangle16_t){x + (bl ? r : 0), y + h - r, ++ w - (bl ? r : 0) - (br ? r : 0), r}); ++ ++ /* Square corners: fill the r×r square. */ ++ if (!tl) ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, ++ &(pixman_rectangle16_t){x, y, r, r}); ++ if (!tr) ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, ++ &(pixman_rectangle16_t){x + w - r, y, r, r}); ++ if (!bl) ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, ++ &(pixman_rectangle16_t){x, y + h - r, r, r}); ++ if (!br) ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, ++ &(pixman_rectangle16_t){x + w - r, y + h - r, r, r}); ++ ++ /* Rounded corners: anti-aliased quarter-circles. */ ++ if (tl) draw_rounded_corner_aa(pix, color, x, y, r, 1, 1); ++ if (tr) draw_rounded_corner_aa(pix, color, x + w - r, y, r, -1, 1); ++ if (bl) draw_rounded_corner_aa(pix, color, x, y + h - r, r, 1, -1); ++ if (br) draw_rounded_corner_aa(pix, color, x + w - r, y + h - r, r, -1, -1); ++} ++ ++static int ++tab_label_build(char *buf, size_t bufsz, ++ const struct terminal *tab, size_t idx) ++{ ++ const char *title = NULL; ++ if (tab->window_title_has_been_set && tab->window_title != NULL) ++ title = tab->window_title; ++ else if (tab->cwd != NULL) { ++ const char *slash = strrchr(tab->cwd, '/'); ++ title = (slash != NULL && slash[1] != '\0') ? slash + 1 : tab->cwd; ++ } ++ int len = snprintf(buf, bufsz, "%zu: %s", idx + 1, ++ title != NULL ? title : ""); ++ if (len < 0) ++ return 0; ++ if ((size_t)len >= bufsz) ++ len = (int)bufsz - 1; ++ return len; ++} ++ ++static int ++tab_label_width(struct fcft_font *font, const char *buf, int len) ++{ ++ if (font == NULL || len <= 0) ++ return 0; ++ ++ int total = 0; ++ const unsigned char *p = (const unsigned char *)buf; ++ const unsigned char *end = p + len; ++ while (p < end) { ++ utf8proc_int32_t cp; ++ utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); ++ if (consumed <= 0 || cp < 0) { ++ p++; ++ continue; ++ } ++ p += consumed; ++ ++ const struct fcft_glyph *g = fcft_rasterize_char_utf32( ++ font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); ++ if (g != NULL) ++ total += g->advance.x; ++ } ++ return total; ++} ++ ++static void ++render_tab_label(pixman_image_t *pix, struct fcft_font *font, ++ const pixman_color_t *fg, const struct terminal *tab, ++ size_t idx, int x, int y, int max_x, float scale) ++{ ++ char label_buf[256]; ++ int label_len = tab_label_build(label_buf, sizeof(label_buf), tab, idx); ++ if (label_len <= 0 || font == NULL) ++ return; ++ ++ const int pad = (int)roundf(tab->conf->tabs.label_padding * scale); ++ int gx = x + pad; ++ const int clip_x = max_x - pad; ++ ++ const unsigned char *p = (const unsigned char *)label_buf; ++ const unsigned char *end = p + label_len; ++ while (p < end) { ++ utf8proc_int32_t cp; ++ utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); ++ if (consumed <= 0 || cp < 0) { ++ p++; ++ continue; ++ } ++ p += consumed; ++ ++ const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( ++ font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); ++ if (glyph == NULL) ++ continue; ++ ++ if (gx + glyph->advance.x > clip_x) ++ break; ++ ++ if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { ++ pixman_image_composite32(PIXMAN_OP_OVER, ++ glyph->pix, NULL, pix, 0, 0, 0, 0, ++ gx + glyph->x, y - glyph->y, ++ glyph->width, glyph->height); ++ } else { ++ pixman_image_t *src = pixman_image_create_solid_fill(fg); ++ pixman_image_composite32(PIXMAN_OP_OVER, ++ src, glyph->pix, pix, 0, 0, 0, 0, ++ gx + glyph->x, y - glyph->y, ++ glyph->width, glyph->height); ++ pixman_image_unref(src); ++ } ++ gx += glyph->advance.x; ++ } ++} ++ ++/* Greyscale ramp indices for the gradient tab style. ++ * The 256-color palette greyscale ramp lives at indices 232..255. */ ++#define GRADIENT_BAR_BG_IDX 234 ++#define GRADIENT_PILL_ACTIVE_IDX 250 ++#define GRADIENT_PILL_INACTIVE_IDX 240 ++#define GRADIENT_FG_ACTIVE_IDX 232 ++#define GRADIENT_FG_INACTIVE_IDX 250 ++ ++/* Hardcoded fade level masks (braille bitmasks); index = fade level (1..6). ++ * Mirrors the visual progression: ⠐ ⠡ ⡐ ⢔ ⣑ ⣪ */ ++static const uint8_t gradient_fade_masks[7] = { ++ 0x00, 0x10, 0x21, 0x50, 0x94, 0xD1, 0xEA, ++}; ++ ++/* Braille bit (0..7) → (col, row) within the 2-col × 4-row dot grid */ ++static const struct { uint8_t col, row; } gradient_dot_pos[8] = { ++ {0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {0, 3}, {1, 3}, ++}; ++ ++static void ++draw_gradient_pill(pixman_image_t *pix, const struct terminal *term, ++ uint8_t pill_idx, uint8_t bar_bg_idx, bool gamma_correct, ++ int x, int y, int w, int h, float scale) ++{ ++ const int n_levels = (int)ALEN(gradient_fade_masks) - 1; /* 6 */ ++ const int cell_w = max(2, (int)roundf(scale * 4)); ++ const int fade_w = n_levels * cell_w; ++ ++ const pixman_color_t pill = color_hex_to_pixman( ++ term->colors.table[pill_idx], gamma_correct); ++ ++ if (w <= 2 * fade_w) { ++ /* Tab too narrow for full fades — draw it solid */ ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &pill, ++ 1, &(pixman_rectangle16_t){x, y, w, h}); ++ return; ++ } ++ ++ /* Solid pill interior */ ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &pill, ++ 1, &(pixman_rectangle16_t){x + fade_w, y, w - 2 * fade_w, h}); ++ ++ const int dot_size = max(1, (int)roundf(scale)); ++ const float sub_w = (float)cell_w / 2.0f; ++ const float sub_h = (float)h / 4.0f; ++ ++ const int inner = (int)pill_idx - 1; ++ const int outer = (int)bar_bg_idx; ++ const int range = inner - outer; ++ const int t_den = max(1, n_levels - 1); ++ ++ for (int side = 0; side < 2; side++) { ++ const bool left = (side == 0); ++ for (int i = 1; i <= n_levels; i++) { ++ /* i == 1 is the outermost cell, i == n_levels the innermost */ ++ const int cell_x = left ++ ? x + (i - 1) * cell_w ++ : x + w - i * cell_w; ++ ++ const uint8_t band_idx = (uint8_t)( ++ outer + (range * (i - 1) + t_den / 2) / t_den); ++ const uint8_t dot_idx = band_idx > 233 ? band_idx - 2 : 232; ++ ++ const pixman_color_t band = color_hex_to_pixman( ++ term->colors.table[band_idx], gamma_correct); ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &band, ++ 1, &(pixman_rectangle16_t){cell_x, y, cell_w, h}); ++ ++ const uint8_t mask = gradient_fade_masks[i]; ++ const pixman_color_t dot = color_hex_to_pixman( ++ term->colors.table[dot_idx], gamma_correct); ++ ++ pixman_rectangle16_t dot_rects[8]; ++ int n_dots = 0; ++ for (int b = 0; b < 8; b++) { ++ if (!(mask & (1u << b))) ++ continue; ++ const int dx = (int)roundf((gradient_dot_pos[b].col + 0.5f) * sub_w) ++ - dot_size / 2; ++ const int dy = (int)roundf((gradient_dot_pos[b].row + 0.5f) * sub_h) ++ - dot_size / 2; ++ dot_rects[n_dots++] = (pixman_rectangle16_t){ ++ cell_x + dx, y + dy, dot_size, dot_size, ++ }; ++ } ++ if (n_dots > 0) ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &dot, ++ n_dots, dot_rects); ++ } ++ } ++} ++ ++static void ++render_tab_bar(struct terminal *term) ++{ ++ struct wl_window *win = term->window; ++ if (win->tab_bar.sub == NULL) ++ return; ++ ++ const struct config *conf = term->conf; ++ const float scale = term->scale; ++ const bool floating = (conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING); ++ const bool pos_top = (conf->tabs.position == CONF_TABS_POSITION_TOP); ++ ++ /* pill height is always conf->tabs.height; total bar = pill + margin in floating */ ++ const int margin_px = floating ? (int)roundf(scale * conf->tabs.margin) : 0; ++ const int tab_h_px = (int)roundf(scale * conf->tabs.height); ++ const int total_h = tab_h_px + margin_px; ++ const int width = term->width; ++ ++ if (width <= 0 || total_h <= 0) ++ return; ++ ++ /* Don't draw the tab bar unless more than one tab is open */ ++ if (win->tab_count <= 1) { ++ win->tab_layout.count = 0; ++ wl_surface_attach(win->tab_bar.surface.surf, NULL, 0, 0); ++ wl_surface_commit(win->tab_bar.surface.surf); ++ return; ++ } ++ ++ struct buffer_chain *chain = term->render.chains.tab_bar; ++ struct buffer *buf = shm_get_buffer(chain, width, total_h); ++ if (buf == NULL) ++ return; ++ ++ const bool gamma_correct = wayl_do_linear_blending(win->term->wl, conf); ++ const bool rounded = (conf->tabs.style == CONF_TABS_STYLE_ROUNDED); ++ const bool gradient = (conf->tabs.style == CONF_TABS_STYLE_GRADIENT); ++ const int r = rounded ? (int)roundf(scale * conf->tabs.corner_radius) : 0; ++ ++ /* Clear buffer: transparent for floating (shows terminal behind gaps), bg for span. ++ * Gradient style overrides the configured bar bg with a fixed greyscale ramp index. */ ++ const pixman_color_t transparent = {0, 0, 0, 0}; ++ const pixman_color_t bg_color = gradient ++ ? color_hex_to_pixman(term->colors.table[GRADIENT_BAR_BG_IDX], gamma_correct) ++ : color_hex_to_pixman(conf->tabs.colors.bg, gamma_correct); ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], ++ floating ? &transparent : &bg_color, ++ 1, &(pixman_rectangle16_t){0, 0, width, total_h}); ++ ++ const size_t tab_count = win->tab_count; ++ if (tab_count == 0) ++ goto commit; ++ ++ struct fcft_font *font = term->fonts[0]; ++ ++ /* Per-tab layout arrays */ ++ int tab_xs[TAB_MAX]; ++ int tab_ws[TAB_MAX]; ++ int tab_y, tab_h; ++ ++ const size_t n = min(tab_count, (size_t)TAB_MAX); ++ ++ if (floating) { ++ const int pad_px = (int)roundf(scale * conf->tabs.tab_padding); ++ const int max_tw = (int)roundf(scale * conf->tabs.tab_width); ++ const int label_pad = (int)roundf(scale * conf->tabs.label_padding); ++ const int min_tw = max(2 * label_pad, 1); ++ ++ /* Measure each tab's natural width (label + padding), capped at max_tw */ ++ int natural_w[TAB_MAX]; ++ int total_w = 0; ++ for (size_t i = 0; i < n; i++) { ++ char buf_s[256]; ++ int len = tab_label_build(buf_s, sizeof(buf_s), win->tabs[i], i); ++ int lw = tab_label_width(font, buf_s, len); ++ int w = lw + 2 * label_pad; ++ if (w > max_tw) w = max_tw; ++ if (w < min_tw) w = min_tw; ++ natural_w[i] = w; ++ total_w += w; ++ } ++ const int total_pads = pad_px * ((int)n - 1); ++ int bar_w = total_w + total_pads; ++ ++ /* Shrink uniformly if we overflow the bar width */ ++ if (bar_w > width && total_w > 0) { ++ const int avail = max(width - total_pads, (int)n); ++ int used = 0; ++ for (size_t i = 0; i < n; i++) { ++ int w = (int)((int64_t)natural_w[i] * avail / total_w); ++ if (w < 1) w = 1; ++ tab_ws[i] = w; ++ used += w; ++ } ++ /* Distribute rounding residue to the first few tabs */ ++ int residue = avail - used; ++ for (size_t i = 0; i < n && residue > 0; i++, residue--) ++ tab_ws[i]++; ++ bar_w = avail + total_pads; ++ } else { ++ for (size_t i = 0; i < n; i++) ++ tab_ws[i] = natural_w[i]; ++ } ++ ++ int x_cur = (width - bar_w) / 2; ++ for (size_t i = 0; i < n; i++) { ++ tab_xs[i] = x_cur; ++ x_cur += tab_ws[i] + pad_px; ++ } ++ ++ tab_y = pos_top ? margin_px : 0; ++ tab_h = tab_h_px; ++ } else { ++ const int base = width / (int)n; ++ int x_cur = 0; ++ for (size_t i = 0; i < n; i++) { ++ tab_xs[i] = x_cur; ++ tab_ws[i] = (i + 1 == n) ? (width - x_cur) : base; ++ x_cur += base; ++ } ++ tab_y = 0; ++ tab_h = tab_h_px; ++ } ++ ++ /* Text baseline centered within the pill */ ++ const int text_baseline = tab_y + (font ++ ? (tab_h - (int)(font->ascent + font->descent)) / 2 + (int)font->ascent ++ : tab_h / 2); ++ ++ for (size_t i = 0; i < n; i++) { ++ struct terminal *tab = win->tabs[i]; ++ const bool is_active = (i == win->active_tab); ++ const int x = tab_xs[i]; ++ const int w = tab_ws[i]; ++ ++ const pixman_color_t fg_color = gradient ++ ? color_hex_to_pixman( ++ term->colors.table[is_active ++ ? GRADIENT_FG_ACTIVE_IDX : GRADIENT_FG_INACTIVE_IDX], ++ gamma_correct) ++ : color_hex_to_pixman( ++ is_active ? conf->tabs.colors.active_fg : conf->tabs.colors.fg, ++ gamma_correct); ++ ++ if (gradient) { ++ const uint8_t pill_idx = is_active ++ ? GRADIENT_PILL_ACTIVE_IDX : GRADIENT_PILL_INACTIVE_IDX; ++ draw_gradient_pill(buf->pix[0], term, pill_idx, GRADIENT_BAR_BG_IDX, ++ gamma_correct, x, tab_y, w, tab_h, scale); ++ } else if (rounded) { ++ /* Floating: all 4 corners rounded. Span: only the open edge rounded. */ ++ const pixman_color_t tab_bg = color_hex_to_pixman( ++ is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg, ++ gamma_correct); ++ unsigned corners; ++ if (floating) { ++ corners = 0xf; /* all corners */ ++ } else if (pos_top) { ++ corners = 0x3; /* top-left + top-right */ ++ } else { ++ corners = 0xc; /* bottom-left + bottom-right */ ++ } ++ draw_rounded_rect(buf->pix[0], &tab_bg, x, tab_y, w, tab_h, r, corners); ++ } else { ++ const pixman_color_t tab_bg = color_hex_to_pixman( ++ is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg, ++ gamma_correct); ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &tab_bg, ++ 1, &(pixman_rectangle16_t){x, tab_y, w, tab_h}); ++ } ++ ++ /* Span: separator between inactive tabs (skipped for gradient — fades ++ * already provide visual separation) */ ++ if (!floating && !is_active && i + 1 < n && !gradient) { ++ const pixman_color_t sep = color_hex_to_pixman( ++ conf->tabs.colors.bg, gamma_correct); ++ pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &sep, ++ 1, &(pixman_rectangle16_t){x + w - 1, tab_y + 2, 1, tab_h - 4}); ++ } ++ ++ if (font != NULL) ++ render_tab_label(buf->pix[0], font, &fg_color, tab, i, ++ x, text_baseline, x + w, scale); ++ } ++ ++ /* Publish layout for hit-testing */ ++ for (size_t i = 0; i < n; i++) { ++ win->tab_layout.xs[i] = tab_xs[i]; ++ win->tab_layout.ws[i] = tab_ws[i]; ++ } ++ win->tab_layout.y = tab_y; ++ win->tab_layout.h = tab_h; ++ win->tab_layout.count = n; ++ ++commit: ; ++ if (tab_count == 0) ++ win->tab_layout.count = 0; ++ const int y_pos = pos_top ? 0 : term->height - total_h; ++ wl_subsurface_set_position(win->tab_bar.sub, 0, (int)roundf(y_pos / scale)); ++ ++ wayl_surface_scale(win, &win->tab_bar.surface, buf, scale); ++ wl_surface_attach(win->tab_bar.surface.surf, buf->wl_buf, 0, 0); ++ wl_surface_damage_buffer(win->tab_bar.surface.surf, 0, 0, width, total_h); ++ ++ if (!floating) { ++ struct wl_region *region = wl_compositor_create_region(term->wl->compositor); ++ if (region != NULL) { ++ wl_region_add(region, 0, 0, width, total_h); ++ wl_surface_set_opaque_region(win->tab_bar.surface.surf, region); ++ wl_region_destroy(region); ++ } ++ } else { ++ wl_surface_set_opaque_region(win->tab_bar.surface.surf, NULL); ++ } ++ ++ wl_surface_commit(win->tab_bar.surface.surf); ++} ++ + bool + render_xcursor_set(struct seat *seat, struct terminal *term, + enum cursor_shape shape) +diff --git a/render.h b/render.h +index e6674ab2..0277d2be 100644 +--- a/render.h ++++ b/render.h +@@ -27,6 +27,7 @@ void render_refresh_csd(struct terminal *term); + void render_refresh_search(struct terminal *term); + void render_refresh_title(struct terminal *term); + void render_refresh_urls(struct terminal *term); ++void render_refresh_tab_bar(struct terminal *term); + bool render_xcursor_set( + struct seat *seat, struct terminal *term, enum cursor_shape shape); + bool render_xcursor_is_valid(const struct seat *seat, const char *cursor); +diff --git a/terminal.c b/terminal.c +index 8eafbcbe..5446b1ea 100644 +--- a/terminal.c ++++ b/terminal.c +@@ -1371,6 +1371,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, + .url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), ++ .tab_bar = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + }, + .scrollback_lines = conf->scrollback.lines, + .app_sync_updates.timer_fd = app_sync_updates_fd, +@@ -1504,6 +1505,394 @@ close_fds: + return NULL; + } + ++struct terminal * ++term_tab_new(struct terminal *primary, ++ int argc, char *const *argv, const char *const *envp, ++ void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) ++{ ++ struct wl_window *win = primary->window; ++ ++ if (win->tab_count >= TAB_MAX) { ++ LOG_ERR("maximum number of tabs (%d) reached", TAB_MAX); ++ return NULL; ++ } ++ ++ const struct config *conf = primary->conf; ++ struct fdm *fdm = primary->fdm; ++ struct reaper *reaper = primary->reaper; ++ struct wayland *wayl = primary->wl; ++ ++ int ptmx = -1; ++ int flash_fd = -1; ++ int delay_lower_fd = -1; ++ int delay_upper_fd = -1; ++ int app_sync_updates_fd = -1; ++ int title_update_fd = -1; ++ int icon_update_fd = -1; ++ int app_id_update_fd = -1; ++ ++ struct terminal *term = malloc(sizeof(*term)); ++ if (unlikely(term == NULL)) { ++ LOG_ERRNO("malloc() failed"); ++ return NULL; ++ } ++ ++ ptmx = posix_openpt(PTY_OPEN_FLAGS); ++ if (ptmx < 0) { ++ LOG_ERRNO("failed to open PTY for new tab"); ++ goto close_fds; ++ } ++ if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || ++ (delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || ++ (delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || ++ (app_sync_updates_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || ++ (title_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || ++ (icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || ++ (app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) ++ { ++ LOG_ERRNO("failed to create timer FDs for new tab"); ++ goto close_fds; ++ } ++ ++ if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, ++ &(struct winsize){ ++ .ws_row = (unsigned short)primary->rows, ++ .ws_col = (unsigned short)primary->cols}) < 0) ++ { ++ LOG_ERRNO("failed to set TIOCSWINSZ for new tab"); ++ goto close_fds; ++ } ++ ++ key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); ++ ++ int ptmx_flags; ++ if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || ++ fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) ++ { ++ LOG_ERRNO("failed to configure ptmx as non-blocking"); ++ goto err; ++ } ++ ++ const enum shm_bit_depth desired_bit_depth = ++ conf->tweak.surface_bit_depth == SHM_BITS_AUTO ++ ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 ++ : conf->tweak.surface_bit_depth; ++ ++ const struct color_theme *theme = NULL; ++ switch (conf->initial_color_theme) { ++ case COLOR_THEME_DARK: theme = &conf->colors_dark; break; ++ case COLOR_THEME_LIGHT: theme = &conf->colors_light; break; ++ case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; ++ case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; ++ } ++ ++ *term = (struct terminal){ ++ .fdm = fdm, ++ .reaper = reaper, ++ .conf = conf, ++ .is_tab = true, ++ .slave = -1, ++ .ptmx = ptmx, ++ .ptmx_buffers = tll_init(), ++ .ptmx_paste_buffers = tll_init(), ++ .font_sizes = { ++ xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), ++ xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), ++ xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), ++ xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), ++ }, ++ .font_dpi = 0., ++ .font_dpi_before_unmap = -1., ++ .font_subpixel = (theme->alpha == 0xffff ++ ? FCFT_SUBPIXEL_DEFAULT ++ : FCFT_SUBPIXEL_NONE), ++ .cursor_keys_mode = CURSOR_KEYS_NORMAL, ++ .keypad_keys_mode = KEYPAD_NUMERICAL, ++ .reverse_wrap = true, ++ .auto_margin = true, ++ .window_title_stack = tll_init(), ++ .scale = primary->scale, ++ .scale_before_unmap = -1, ++ .flash = {.fd = flash_fd}, ++ .blink = {.fd = -1}, ++ .vt = {.state = 0}, ++ .colors = { ++ .fg = theme->fg, ++ .bg = theme->bg, ++ .alpha = theme->alpha, ++ .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text, ++ .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor, ++ .selection_fg = theme->selection_fg, ++ .selection_bg = theme->selection_bg, ++ .active_theme = conf->initial_color_theme, ++ }, ++ .color_stack = {.stack = NULL, .size = 0, .idx = 0}, ++ .origin = ORIGIN_ABSOLUTE, ++ .cursor_style = conf->cursor.style, ++ .cursor_blink = { ++ .decset = false, ++ .deccsusr = conf->cursor.blink.enabled, ++ .state = CURSOR_BLINK_ON, ++ .fd = -1, ++ }, ++ .selection = { ++ .coords = {.start = {-1, -1}, .end = {-1, -1}}, ++ .pivot = {.start = {-1, -1}, .end = {-1, -1}}, ++ .auto_scroll = {.fd = -1}, ++ }, ++ .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, ++ .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, ++ .grid = &term->normal, ++ .composed = NULL, ++ .alt_scrolling = conf->mouse.alternate_scroll_mode, ++ .meta = {.esc_prefix = true, .eight_bit = true}, ++ .num_lock_modifier = true, ++ .bell_action_enabled = true, ++ .tab_stops = tll_init(), ++ .wl = wayl, ++ .window = win, ++ .render = { ++ .chains = { ++ .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, ++ desired_bit_depth, &render_buffer_release_callback, term), ++ .search = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), ++ .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), ++ .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), ++ .url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), ++ .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), ++ .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), ++ .tab_bar = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), ++ }, ++ .scrollback_lines = conf->scrollback.lines, ++ .app_sync_updates.timer_fd = app_sync_updates_fd, ++ .title = {.timer_fd = title_update_fd}, ++ .icon = {.timer_fd = icon_update_fd}, ++ .app_id = {.timer_fd = app_id_update_fd}, ++ .workers = { ++ .count = conf->render_worker_count, ++ .queue = tll_init(), ++ }, ++ }, ++ .delayed_render_timer = { ++ .is_armed = false, ++ .lower_fd = delay_lower_fd, ++ .upper_fd = delay_upper_fd, ++ }, ++ .sixel = { ++ .scrolling = true, ++ .use_private_palette = true, ++ .palette_size = SIXEL_MAX_COLORS, ++ .max_width = SIXEL_MAX_WIDTH, ++ .max_height = SIXEL_MAX_HEIGHT, ++ }, ++ .shutdown = { ++ .terminate_timeout_fd = -1, ++ .cb = shutdown_cb, ++ .cb_data = shutdown_data, ++ }, ++ .foot_exe = xstrdup(primary->foot_exe), ++ .cwd = xstrdup( ++ conf->tabs.inherit_cwd ++ ? win->term->cwd ++ : (getenv("HOME") != NULL ? getenv("HOME") : "/")), ++ .grapheme_shaping = conf->tweak.grapheme_shaping, ++#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED ++ .ime_enabled = true, ++#endif ++ .active_notifications = tll_init(), ++ }; ++ ++ pixman_region32_init(&term->render.last_overlay_clip); ++ term_update_ascii_printer(term); ++ memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); ++ ++ /* Inherit font sizes from primary so the new tab matches its zoom level */ ++ for (size_t i = 0; i < 4; i++) { ++ const struct config_font_list *font_list = &conf->fonts[i]; ++ for (size_t j = 0; j < font_list->count; j++) ++ term->font_sizes[i][j] = primary->font_sizes[i][j]; ++ } ++ ++ for (size_t i = 0; i < ALEN(term->notification_icons); i++) ++ term->notification_icons[i].tmp_file_fd = -1; ++ ++ if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) || ++ !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || ++ !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || ++ !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || ++ !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || ++ !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || ++ !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) ++ { ++ goto err; ++ } ++ ++ add_utmp_record(conf, reaper, ptmx); ++ ++ if ((term->slave = slave_spawn( ++ term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, ++ conf->term, conf->shell, conf->login_shell, ++ &conf->notifications)) == -1) ++ { ++ goto err; ++ } ++ ++ reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); ++ ++ /* Load fonts (uses primary's scale) */ ++ if (!term_font_dpi_changed(term, 0.)) ++ goto err; ++ ++ term->font_subpixel = get_font_subpixel(term); ++ ++ /* Resize grid to match current window dimensions. cell_width and ++ * cell_height were already computed by term_set_fonts above, using the ++ * inherited font_sizes — inheriting primary's cell geometry would mask ++ * that, since primary may be mid-zoom or mid-reload. */ ++ term->width = primary->width; ++ term->height = primary->height; ++ ++ tll_push_back(wayl->terms, term); ++ ++ /* Register in window's tab list */ ++ win->tabs[win->tab_count++] = term; ++ ++ /* Enable ptmx I/O now (window is already configured) */ ++ fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); ++ ++ if (!initialize_render_workers(term)) ++ goto err; ++ ++ /* Switch to the new tab */ ++ term_tab_switch(win, win->tab_count - 1); ++ ++ return term; ++ ++err: ++ term->shutdown.in_progress = true; ++ term_destroy(term); ++ return NULL; ++ ++close_fds: ++ if (ptmx >= 0) close(ptmx); ++ fdm_del(fdm, flash_fd); ++ fdm_del(fdm, delay_lower_fd); ++ fdm_del(fdm, delay_upper_fd); ++ fdm_del(fdm, app_sync_updates_fd); ++ fdm_del(fdm, title_update_fd); ++ fdm_del(fdm, icon_update_fd); ++ fdm_del(fdm, app_id_update_fd); ++ free(term); ++ return NULL; ++} ++ ++void ++term_tab_switch(struct wl_window *win, size_t idx) ++{ ++ if (idx >= win->tab_count) ++ return; ++ ++ struct terminal *prev = win->term; ++ /* render_resize expects logical (unscaled) sizes; prev->width/height ++ * are physical pixels. Convert back to logical so render_resize's ++ * internal scale multiplication doesn't double-apply it. */ ++ int cur_width = (int)roundf(prev->width / prev->scale); ++ int cur_height = (int)roundf(prev->height / prev->scale); ++ ++ win->active_tab = idx; ++ win->term = win->tabs[idx]; ++ ++ struct terminal *term = win->term; ++ ++ /* Inherit the surface kind so cursor updates work without a pointer re-enter */ ++ term->active_surface = prev->active_surface; ++ ++ /* Update keyboard and mouse focus for all seats looking at this window. ++ * Do this before term_kbd_focus_out(prev) so its guard (checking whether ++ * any seat still points to it) sees that no seat does. */ ++ tll_foreach(term->wl->seats, it) { ++ struct seat *seat = &it->item; ++ if (seat->kbd_focus != NULL && seat->kbd_focus->window == win) ++ seat->kbd_focus = term; ++ if (seat->mouse_focus != NULL && seat->mouse_focus->window == win) ++ seat->mouse_focus = term; ++ } ++ ++ /* prev loses keyboard focus now that no seat points to it */ ++ term_kbd_focus_out(prev); ++ ++ bool has_kbd_focus = false; ++ tll_foreach(term->wl->seats, it) { ++ if (it->item.kbd_focus == term) { ++ has_kbd_focus = true; ++ break; ++ } ++ } ++ if (has_kbd_focus) ++ term_visual_focus_in(term); ++ else ++ term_visual_focus_out(term); ++ ++ /* Scale changes (fractional-scale-v1, preferred_buffer_scale, output ++ * enter/leave) only notify win->term — inactive tabs miss them. Catch ++ * the incoming tab up on scale, DPI, and font size before we resize. */ ++ const float old_scale = term->scale; ++ term_update_scale(term); ++ term_font_dpi_changed(term, old_scale); ++ term_font_subpixel_changed(term); ++ ++ /* Cancel the outgoing tab's in-flight frame_callback. Its listener data ++ * points at prev, so when it fires it services prev's empty pending bits ++ * and nothing gets drawn. Dropping it lets fdm_hook_refresh_pending_terminals ++ * take the immediate-render path for the new active tab. */ ++ if (win->frame_callback != NULL) { ++ wl_callback_destroy(win->frame_callback); ++ win->frame_callback = NULL; ++ } ++ ++ /* Use current window dimensions, not the inactive tab's stale ones. ++ * render_resize allocates the grid rows — term_kbd_focus_in must come ++ * after this because cursor_refresh dereferences cur_row. */ ++ render_resize(term, cur_width, cur_height, RESIZE_FORCE); ++ ++ if (has_kbd_focus) ++ term_kbd_focus_in(term); ++ ++ render_refresh_csd(term); ++ render_refresh_tab_bar(term); ++ render_refresh_title(term); ++ term_xcursor_update(term); ++} ++ ++void ++term_tab_close(struct terminal *term) ++{ ++ term_shutdown(term); ++} ++ ++size_t ++term_tab_bar_hit_test(const struct terminal *term, int x, int y) ++{ ++ const struct wl_window *win = term->window; ++ if (win->tab_bar.sub == NULL) ++ return SIZE_MAX; ++ ++ const size_t n = win->tab_layout.count; ++ if (n == 0) ++ return SIZE_MAX; ++ ++ if (y < win->tab_layout.y || y >= win->tab_layout.y + win->tab_layout.h) ++ return SIZE_MAX; ++ ++ for (size_t i = 0; i < n; i++) { ++ const int tx = win->tab_layout.xs[i]; ++ const int tw = win->tab_layout.ws[i]; ++ if (x >= tx && x < tx + tw) ++ return i; ++ } ++ return SIZE_MAX; ++} ++ + void + term_window_configured(struct terminal *term) + { +@@ -1585,10 +1974,10 @@ static void + shutdown_maybe_done(struct terminal *term) + { + bool shutdown_done = +- term->window == NULL && term->shutdown.client_has_terminated; ++ term->shutdown.fdm_done && term->shutdown.client_has_terminated; + +- LOG_DBG("window=%p, slave-has-been-reaped=%d --> %s", +- (void *)term->window, term->shutdown.client_has_terminated, ++ LOG_DBG("fdm_done=%d, slave-has-been-reaped=%d --> %s", ++ term->shutdown.fdm_done, term->shutdown.client_has_terminated, + (shutdown_done + ? "shutdown done, calling term_destroy()" + : "no action")); +@@ -1632,26 +2021,35 @@ fdm_shutdown(struct fdm *fdm, int fd, int events, void *data) + /* Kill the event FD */ + fdm_del(term->fdm, fd); + +- wayl_win_destroy(term->window); +- term->window = NULL; ++ struct wl_window *win = term->window; + +- struct wayland *wayl = term->wl; ++ if (win != NULL && win->tab_count <= 1) { ++ /* ++ * Last (or only) tab — destroy the whole window. ++ * ++ * Normally we'd get unmapped when we destroy the Wayland ++ * surface above. However, it appears that under certain ++ * conditions, those events are deferred (for example, when ++ * a screen locker is active), and thus we can get here ++ * without having been unmapped. ++ */ ++ wayl_win_destroy(win); ++ term->window = NULL; + ++ struct wayland *wayl = term->wl; ++ tll_foreach(wayl->seats, it) { ++ if (it->item.kbd_focus == term) ++ it->item.kbd_focus = NULL; ++ if (it->item.mouse_focus == term) ++ it->item.mouse_focus = NULL; ++ } ++ } + /* +- * Normally we'd get unmapped when we destroy the Wayland +- * above. +- * +- * However, it appears that under certain conditions, those events +- * are deferred (for example, when a screen locker is active), and +- * thus we can get here without having been unmapped. ++ * Multi-tab case: leave term->window intact so term_destroy() can ++ * use its existing tab-removal and tab-switch logic. + */ +- tll_foreach(wayl->seats, it) { +- if (it->item.kbd_focus == term) +- it->item.kbd_focus = NULL; +- if (it->item.mouse_focus == term) +- it->item.mouse_focus = NULL; +- } + ++ term->shutdown.fdm_done = true; + shutdown_maybe_done(term); + return true; + } +@@ -1840,7 +2238,99 @@ term_destroy(struct terminal *term) + fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); + + if (term->window != NULL) { +- wayl_win_destroy(term->window); ++ struct wl_window *win = term->window; ++ ++ /* Remove ourselves from the window's tab list */ ++ bool was_active = (win->term == term); ++ size_t our_idx = win->tab_count; /* invalid sentinel */ ++ for (size_t i = 0; i < win->tab_count; i++) { ++ if (win->tabs[i] == term) { ++ our_idx = i; ++ memmove(&win->tabs[i], &win->tabs[i + 1], ++ (win->tab_count - i - 1) * sizeof(win->tabs[0])); ++ win->tab_count--; ++ if (win->active_tab > 0 && win->active_tab >= win->tab_count) ++ win->active_tab = win->tab_count - 1; ++ else if (our_idx < win->active_tab) ++ win->active_tab--; ++ break; ++ } ++ } ++ ++ if (win->tab_count == 0) { ++ /* Last tab - destroy the window normally */ ++ wayl_win_destroy(win); ++ } else { ++ /* Other tabs remain - just purge our render chains */ ++ ++ /* Cancel any pending frame callback that still holds a pointer ++ * to this terminal, preventing a use-after-free when the ++ * compositor fires it after term is freed. */ ++ if (win->frame_callback != NULL) { ++ wl_callback_destroy(win->frame_callback); ++ win->frame_callback = NULL; ++ } ++ ++ render_wait_for_preapply_damage(term); ++ shm_purge(term->render.chains.search); ++ shm_purge(term->render.chains.scrollback_indicator); ++ shm_purge(term->render.chains.render_timer); ++ shm_purge(term->render.chains.grid); ++ shm_purge(term->render.chains.url); ++ shm_purge(term->render.chains.csd); ++ shm_purge(term->render.chains.tab_bar); ++ shm_purge(term->render.chains.overlay); ++ ++ /* Switch to the new active tab if needed */ ++ if (was_active) { ++ struct terminal *next = win->tabs[win->active_tab]; ++ win->term = next; ++ next->active_surface = term->active_surface; ++ ++ /* Update keyboard and mouse focus */ ++ bool has_kbd_focus = false; ++ tll_foreach(next->wl->seats, it) { ++ struct seat *seat = &it->item; ++ if (seat->kbd_focus == term) ++ seat->kbd_focus = next; ++ if (seat->mouse_focus == term) ++ seat->mouse_focus = next; ++ if (seat->kbd_focus == next) ++ has_kbd_focus = true; ++ } ++ ++ if (has_kbd_focus) { ++ term_kbd_focus_in(next); ++ term_visual_focus_in(next); ++ } else { ++ term_visual_focus_out(next); ++ } ++ ++ render_resize(next, ++ (int)roundf(term->width / term->scale), ++ (int)roundf(term->height / term->scale), ++ RESIZE_FORCE); ++ render_refresh_csd(next); ++ render_refresh_tab_bar(next); ++ render_refresh_title(next); ++ term_xcursor_update(next); ++ } else { ++ /* If we just dropped to a single tab, the surviving ++ * active tab needs to resize to reclaim the space the ++ * tab bar was occupying. */ ++ if (win->tab_count == 1) { ++ struct terminal *active = win->term; ++ render_resize(active, ++ (int)roundf(active->width / active->scale), ++ (int)roundf(active->height / active->scale), ++ RESIZE_FORCE); ++ } ++ /* Just update the tab bar to reflect removed tab */ ++ render_refresh_tab_bar(win->term); ++ render_refresh_title(win->term); ++ } ++ } ++ + term->window = NULL; + } + +@@ -1917,6 +2407,7 @@ term_destroy(struct terminal *term) + shm_chain_free(term->render.chains.url); + shm_chain_free(term->render.chains.csd); + shm_chain_free(term->render.chains.overlay); ++ shm_chain_free(term->render.chains.tab_bar); + pixman_region32_fini(&term->render.last_overlay_clip); + + tll_free(term->tab_stops); +@@ -3637,6 +4128,12 @@ term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) + shape = CURSOR_SHAPE_LEFT_PTR; + break; + ++ case TERM_SURF_TAB_BAR: { ++ size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y); ++ shape = (idx != SIZE_MAX) ? CURSOR_SHAPE_POINTER : CURSOR_SHAPE_LEFT_PTR; ++ break; ++ } ++ + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: +@@ -3680,6 +4177,7 @@ term_set_window_title(struct terminal *term, const char *title) + free(term->window_title); + term->window_title = xstrdup(title); + render_refresh_title(term); ++ render_refresh_tab_bar(term); + term->window_title_has_been_set = true; + } + +@@ -4448,6 +4946,8 @@ term_surface_kind(const struct terminal *term, const struct wl_surface *surface) + return TERM_SURF_BUTTON_MAXIMIZE; + else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf) + return TERM_SURF_BUTTON_CLOSE; ++ else if (surface == term->window->tab_bar.surface.surf) ++ return TERM_SURF_TAB_BAR; + else + return TERM_SURF_NONE; + } +diff --git a/terminal.h b/terminal.h +index 446d5f23..876db0d7 100644 +--- a/terminal.h ++++ b/terminal.h +@@ -361,6 +361,7 @@ enum term_surface { + TERM_SURF_BUTTON_MINIMIZE, + TERM_SURF_BUTTON_MAXIMIZE, + TERM_SURF_BUTTON_CLOSE, ++ TERM_SURF_TAB_BAR, + }; + + enum overlay_style { +@@ -402,6 +403,7 @@ struct terminal { + struct fdm *fdm; + struct reaper *reaper; + const struct config *conf; ++ bool is_tab; /* Secondary tab terminal (shares window with primary) */ + + void (*ascii_printer)(struct terminal *term, char32_t c); + union { +@@ -645,6 +647,7 @@ struct terminal { + struct buffer_chain *url; + struct buffer_chain *csd; + struct buffer_chain *overlay; ++ struct buffer_chain *tab_bar; + } chains; + + /* Scheduled for rendering, as soon-as-possible */ +@@ -811,6 +814,7 @@ struct terminal { + struct { + bool in_progress; + bool client_has_terminated; ++ bool fdm_done; + int terminate_timeout_fd; + int exit_status; + int next_signal; +@@ -842,6 +846,15 @@ struct terminal *term_init( + int argc, char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data); + ++struct terminal *term_tab_new( ++ struct terminal *primary, ++ int argc, char *const *argv, const char *const *envp, ++ void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data); ++ ++void term_tab_switch(struct wl_window *win, size_t idx); ++void term_tab_close(struct terminal *term); ++size_t term_tab_bar_hit_test(const struct terminal *term, int x, int y); ++ + bool term_shutdown(struct terminal *term); + int term_destroy(struct terminal *term); + +diff --git a/wayland.c b/wayland.c +index f5737c1e..ed61d2e7 100644 +--- a/wayland.c ++++ b/wayland.c +@@ -2163,6 +2163,19 @@ wayl_win_init(struct terminal *term, const char *token) + goto out; + } + ++ if (conf->tabs.enabled) { ++ if (!wayl_win_subsurface_new(win, &win->tab_bar, true)) { ++ LOG_ERR("failed to create tab bar surface"); ++ goto out; ++ } ++ wl_subsurface_set_desync(win->tab_bar.sub); ++ } ++ ++ /* Initialize tab list with primary terminal */ ++ win->tabs[0] = term; ++ win->tab_count = 1; ++ win->active_tab = 0; ++ + switch (conf->tweak.render_timer) { + case RENDER_TIMER_OSD: + case RENDER_TIMER_BOTH: +@@ -2221,6 +2234,12 @@ wayl_win_destroy(struct wl_window *win) + wl_surface_commit(win->search.surface.surf); + } + ++ /* Tab bar */ ++ if (win->tab_bar.surface.surf != NULL) { ++ wl_surface_attach(win->tab_bar.surface.surf, NULL, 0, 0); ++ wl_surface_commit(win->tab_bar.surface.surf); ++ } ++ + /* URLs */ + tll_foreach(win->urls, it) { + wl_surface_attach(it->item.surf.surface.surf, NULL, 0, 0); +@@ -2257,6 +2276,7 @@ wayl_win_destroy(struct wl_window *win) + wayl_win_subsurface_destroy(&win->scrollback_indicator); + wayl_win_subsurface_destroy(&win->render_timer); + wayl_win_subsurface_destroy(&win->overlay); ++ wayl_win_subsurface_destroy(&win->tab_bar); + + shm_purge(term->render.chains.search); + shm_purge(term->render.chains.scrollback_indicator); +@@ -2264,6 +2284,7 @@ wayl_win_destroy(struct wl_window *win) + shm_purge(term->render.chains.grid); + shm_purge(term->render.chains.url); + shm_purge(term->render.chains.csd); ++ shm_purge(term->render.chains.tab_bar); + + tll_foreach(win->xdg_tokens, it) { + xdg_activation_token_v1_destroy(it->item->xdg_token); +diff --git a/wayland.h b/wayland.h +index 9cbd1023..9e1a3a6d 100644 +--- a/wayland.h ++++ b/wayland.h +@@ -401,6 +401,22 @@ struct wl_window { + struct wayl_sub_surface scrollback_indicator; + struct wayl_sub_surface render_timer; + struct wayl_sub_surface overlay; ++ struct wayl_sub_surface tab_bar; ++ ++ /* Tab management */ ++#define TAB_MAX 32 ++ struct terminal *tabs[TAB_MAX]; ++ size_t tab_count; ++ size_t active_tab; ++ ++ /* Tab bar geometry cache, filled by render_tab_bar, read by hit-testing */ ++ struct { ++ int xs[TAB_MAX]; ++ int ws[TAB_MAX]; ++ int y; ++ int h; ++ size_t count; ++ } tab_layout; + + struct wl_callback *frame_callback; + diff --git a/graphical/integrations.nix b/graphical/integrations.nix index c49a283..01627f5 100644 --- a/graphical/integrations.nix +++ b/graphical/integrations.nix @@ -1,10 +1,13 @@ { lib, + pkgs, ... }: { xdg.autostart.enable = lib.mkForce false; + xdg.portal.extraPortals = lib.mkForce [ pkgs.xdg-desktop-portal-gtk ]; + environment.pathsToLink = [ "/share/xdg-desktop-portal" "/share/applications" diff --git a/graphical/kernel.nix b/graphical/kernel.nix index 28ffb91..22be143 100644 --- a/graphical/kernel.nix +++ b/graphical/kernel.nix @@ -4,6 +4,6 @@ services.scx = { enable = true; - scheduler = "scx_bpfland"; + scheduler = "scx_lavd"; }; } diff --git a/graphical/llm.nix b/graphical/llm.nix deleted file mode 100644 index 7e678a0..0000000 --- a/graphical/llm.nix +++ /dev/null @@ -1,177 +0,0 @@ -{ - pkgs, - lib, - mainUser, - ... -}: -let - clodTools = with pkgs; [ - bash - procps - ripgrep - socat - bubblewrap - ]; - mkClod = - { - confDir ? null, - suffix ? null, - }: - let - version = "2.1.81"; - runtimeDeps = lib.makeBinPath clodTools; - - patchScript = pkgs.writeScript "patch-claude-src" '' - #!${pkgs.python3}/bin/python3 - import re, sys - W = rb"[\w$]+" - data = open(sys.argv[1], "rb").read() - - pat = (rb"let (" + W + rb")=(" + W + rb")\((" + W + rb'),"CLAUDE\.md"\);' - rb"(" + W + rb")\.push\(\.\.\.(" + W + rb')\(\1,"Project",([^)]+)\)\)') - def agents(m): - v, pj, d, a, lf, rest = [m.group(i) for i in range(1, 7)] - return (b'for(let _f of["CLAUDE.md","AGENTS.md"]){let ' + v + b"=" + pj - + b"(" + d + b",_f);" + a + b".push(..." + lf + b"(" + v - + b',"Project",' + rest + b"))}") - data, n = re.subn(pat, agents, data) - sys.stderr.write(f"AGENTS.md: {n} site(s)\n") - - data = data.replace( - b'case"macos":return"/Library/Application Support/ClaudeCode"', - b'case"macos":return"/etc/claude-code"', - ) - - # Enable hard-disabled slash commands: /btw, /files, /tag - for anchor, label in [ - (b'name:"btw",description:"Ask a quick side question', b"/btw"), - (b'name:"files",description:"List all files currently in context"', b"/files"), - (b'name:"tag",userFacingName', b"/tag"), - ]: - pos = data.find(anchor) - if pos < 0: - sys.stderr.write(f"{label.decode()}: NOT FOUND\n"); continue - window = data[pos:pos+500] - patched = window.replace(b"isEnabled:()=>!1", b"isEnabled:()=>!0", 1) - data = data[:pos] + patched + data[pos+500:] - sys.stderr.write(f"{label.decode()}: enabled\n") - - # Bypass e2() for thinkback (e2 returns false when DISABLE_TELEMETRY is set) - data = data.replace( - b'e2("tengu_thinkback")', - b'!0||"tengu_thinkback"', - ) - sys.stderr.write("thinkback: force-enabled\n") - - # Enable custom keybindings (qA default is false, flip to true) - data = data.replace( - b'qA("tengu_keybinding_customization_release",!1)', - b'qA("tengu_keybinding_customization_release",!0)', - ) - sys.stderr.write("keybindings: force-enabled\n") - - # Force-enable remote control / bridge feature gate - data = data.replace( - b'function ek(){return qA("tengu_ccr_bridge",!1)}', - b'function ek(){return!0} ', - ) - sys.stderr.write("remote-control: force-enabled\n") - - # Fix Deno-compile bridge spawn: Deno compiled binaries intercept --flags - # as V8 flags. Rewrite spawn to go through env(1) which breaks the Deno - # runtime's flag parsing. - data = data.replace( - b'let O=iHz(A.execPath,$,', - b'let O=iHz("env",["--",A.execPath,...$],', - ) - sys.stderr.write("bridge-spawn: patched via env(1)\n") - - # Kill claude-developer-platform bundled skill (~400 tokens/turn dead weight) - data = data.replace( - b'name:"claude-developer-platform",description:`', - b'name:"claude-developer-platform",isEnabled:()=>!1,description:`', - ) - sys.stderr.write("claude-developer-platform: killed\n") - - pat = (rb"context_window:\{total_input_tokens:(" + W + rb"\(\))," - rb"total_output_tokens:(" + W + rb"\(\))," - rb"context_window_size:(" + W + rb")," - rb"current_usage:(" + W + rb")," - rb"used_percentage:(" + W + rb")\.used," - rb"remaining_percentage:\5\.remaining\}") - rl = re.search(rb"(" + W + rb')=\{status:"allowed",unifiedRateLimitFallbackAvailable:!1,isUsingOverage:!1\}', data) - m = re.search(pat, data) - if m and rl: - ci, co, sz, u, p, r = *[m.group(i) for i in range(1, 6)], rl.group(1) - data = data.replace(m[0], - b"context_window:{...(" + u + b"||{})," - b"context_window_size:" + sz + b",current_usage:" + u + b"," - b"used_percentage:" + p + b".used,remaining_percentage:" + p + b".remaining," - b"rate_limit:" + r + b",s_in:" + ci + b",s_out:" + co + b"}") - - open(sys.argv[1], "wb").write(data) - ''; - in - pkgs.writeShellScriptBin "claude${lib.optionalString (suffix != null) "-${suffix}"}" '' - set -euo pipefail - export DISABLE_AUTOUPDATER=1 - export DISABLE_INSTALLATION_CHECKS=1 - export USE_BUILTIN_RIPGREP=0 - export PATH="${runtimeDeps}:${pkgs.deno}/bin:$PATH" - - CACHE="''${XDG_CACHE_HOME:-$HOME/.cache}/claude-code" - BIN="$CACHE/claude-${version}" - ${lib.optionalString (confDir != null) "export CLAUDE_CONFIG_DIR=\"$HOME/${confDir}\""} - - if [ ! -x "$BIN" ]; then - mkdir -p "$CACHE" - DENO_DIR="$CACHE/.deno" - export DENO_DIR - deno cache "npm:@anthropic-ai/claude-code@${version}" - ${patchScript} "$DENO_DIR/npm/registry.npmjs.org/@anthropic-ai/claude-code/${version}/cli.js" - deno compile --allow-all --output "$BIN" "npm:@anthropic-ai/claude-code@${version}" 2>&1 - rm -rf "$DENO_DIR" - fi - - exec "$BIN" "$@" - ''; - claude-code = mkClod { }; -in -(scope "apps" { - "slop" = claude-code; -}) -// { - # required for loader - programs.nix-ld = { - enable = true; - libraries = [ pkgs.stdenv.cc.cc.lib ]; - }; - # experiment with our own sandboxing - # security.yoke.wrappers = - # let - # basePaths = [ - # "wrx=/home/${mainUser}/.claude.json:/home/${mainUser}/.claude-code:/home/${mainUser}/.cache/claude-code:$PWD/.claude" - # "rx=/" - # ]; - # base = { - # package = claude-code; - # executable = "claude"; - # retainEnv = true; - # unrestrictTcp = true; - # extraPackages = clodTools; - # }; - # in - # { - # clod-cuck = base // { - # pathRules = basePaths + [ "rx=$PWD" ]; - # }; - # clod = base // { - # pathRules = basePaths ++ [ - # "wrx=/home/${mainUser}" - # ]; - # addPwd = true; - # unrestrictSockets = true; - # unrestrictSignals = true; - # }; - # }; -} diff --git a/graphical/llm/launcher.nu b/graphical/llm/launcher.nu new file mode 100644 index 0000000..6202b67 --- /dev/null +++ b/graphical/llm/launcher.nu @@ -0,0 +1,126 @@ +def detect-platform []: nothing -> string { + let os = $nu.os-info.name | str downcase + let arch = $nu.os-info.arch | str downcase + + let norm_arch = match $arch { + "x86_64" | "x64" | "amd64" => "x64" + "aarch64" | "arm64" => "arm64" + _ => { error make { msg: $"unsupported arch: ($arch)" } } + } + + match $os { + "linux" => { + let musl_ld = $"/lib/ld-musl-($arch).so.1" + let suffix = if ($musl_ld | path exists) { "-musl" } else { "" } + $"linux-($norm_arch)($suffix)" + } + "macos" | "darwin" => $"darwin-($norm_arch)" + "windows" => { error make { msg: "windows unsupported by this launcher" } } + _ => { error make { msg: $"unsupported os: ($os)" } } + } +} + +def build-binary [version: string, binary_path: string, cache: string] { + let platform = detect-platform + let pkg = $"@anthropic-ai/claude-code-($platform)" + let tarball_url = $"https://registry.npmjs.org/($pkg)/-/claude-code-($platform)-($version).tgz" + + let tgz_dir = $cache | path join "tgz" + mkdir $tgz_dir + let tgz = $tgz_dir | path join $"claude-code-($platform)-($version).tgz" + + if not ($tgz | path exists) { + print --stderr $"(ansi cyan)fetch:(ansi reset) ($tarball_url)" + http get --raw $tarball_url | save --force --raw $tgz + } + + let workdir = $cache | path join $"build-($version)" + rm -rf $workdir + mkdir $workdir + + run-external $env._TAR "-xzf" $tgz "-C" $workdir + let native_bin = $workdir | path join "package" "claude" + if not ($native_bin | path exists) { + error make { msg: $"lift: ($native_bin) missing after tar extract" } + } + + let cli = $workdir | path join "cli.cjs" + run-external $env._LIFT_SCRIPT $native_bin $cli + run-external $env._PATCH_SCRIPT $cli + + # Bun's bundler keeps a handful of http/ws/schema libs as runtime-external. + # Deno has no equivalent provision — drop a package.json next to cli.cjs, + # resolve deps into a local node_modules/, and bundle that tree into the + # executable via --include. + cp --force $env._EXTERNAL_PACKAGE_JSON ($workdir | path join "package.json") + + cd $workdir + $env.DENO_DIR = ($workdir | path join ".deno") + run-external $env._DENO "install" "--node-modules-dir=auto" + run-external $env._DENO "compile" "--allow-all" "--no-check" "--node-modules-dir=auto" "--include=node_modules" "--output" $binary_path "cli.cjs" + + # nushell refuses to delete a directory you're currently inside + cd $cache + rm -rf $workdir +} + +def main --wrapped [...args] { + let cache = $env + | get --optional "XDG_CACHE_HOME" + | default ($env.HOME | path join ".cache") + | path join "claude-code" + mkdir $cache + + let config_dir = $env | get --optional "CLAUDE_CONFIG_DIR" | default ( + $env + | get --optional "XDG_CONFIG_HOME" + | default ($env.HOME | path join ".config") + | path join "claude" + ) + mkdir $config_dir + + # Sync declarative settings into writable config dir + cp --force $env._SETTINGS_JSON ($config_dir | path join "settings.json") + + let version = do { + let version_file = $cache | path join "latest-version" + let stale = try { (date now) - (ls $version_file | get 0.modified) > 6hr } catch { true } + + if not $stale { return (try { open $version_file } catch { "" }) } + + let version = try { + http get --max-time 5sec https://registry.npmjs.org/@anthropic-ai/claude-code/latest + | get version + } catch { + print --stderr $"(ansi yellow_bold)warn:(ansi reset) version cache stale, can't re-fetch" + return "" + } + + try { + $version | save --force $version_file + } + $version + } + + let binary_path = if ($version | is-empty) { + print --stderr $"(ansi yellow_bold)warn:(ansi reset) falling back to latest binary" + + try { + glob ($cache | path join "claude-*") | sort | last + } catch { + print --stderr $"(ansi red_bold)error:(ansi reset) no binary found" + exit 67 + } + } else { + $cache | path join $"claude-($version)" + } + + if not ($binary_path | path exists) { + build-binary $version $binary_path $cache + } + + $env.PATH = ($env.PATH | prepend ($env._RUNTIME_DEPS | split row ":")) + $env._ENV_JSON | load-env + + exec $binary_path ...$args +} diff --git a/graphical/llm/lift-claude-bun.py b/graphical/llm/lift-claude-bun.py new file mode 100644 index 0000000..3152595 --- /dev/null +++ b/graphical/llm/lift-claude-bun.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +# Extract the cli.js bundle from a bun --compile --bytecode executable. +# +# Starting with @anthropic-ai/claude-code 2.1.113 the npm package stopped +# shipping cli.js and instead publishes platform-specific tarballs that contain +# a bun-compiled ELF (~226 MB). The JavaScript is still fully embedded in the +# binary as plaintext — the @bytecode marker just means a V8 parse-cache lives +# alongside it, not instead of it. +# +# Layout of each CJS module inside the bun SEA payload: +# // @bun[ @bytecode] @bun-cjs\n +# (function(exports, require, module, __filename, __dirname) {})\n +# \x00/$bunfs/root/\x00... +# +# Claude Code ships three real modules in the tail region (past 0x6000000): +# the main cli (~12 MB), then two tiny native-loader stubs for the optional +# image-processor.node and audio-capture.node. Only the first is interesting. + +import sys +from pathlib import Path + +# Skip over .rodata / .text — those contain `// @bun` string literals (error +# messages, help text) that would confuse the scanner. The first real module +# sat at ~0xd333ec8 in 2.1.113; staying well below that survives future growth. +SCAN_FROM: int = 0x6000000 + +HEADERS: list[bytes] = [ + b"// @bun @bytecode @bun-cjs\n(function(exports, require, module, __filename, __dirname) {", + b"// @bun @bun-cjs\n(function(exports, require, module, __filename, __dirname) {", +] + +CJS_OPEN: bytes = b"(function(exports, require, module, __filename, __dirname) {" +CJS_END: bytes = b"})\n\x00" + + +def find_main_module(data: bytes) -> tuple[int, int]: + for header in HEADERS: + start = data.find(header, SCAN_FROM) + if start >= 0: + break + else: + sys.exit("lift: no bun CJS module header found past 0x6000000") + + end = data.find(CJS_END, start) + if end < 0: + sys.exit("lift: could not find module terminator (})\\n\\x00)") + return start, end + 3 # include })\n, exclude trailing NUL + + +def unwrap(mod: bytes) -> bytes: + nl = mod.find(b"\n") + if nl < 0: + sys.exit("lift: module has no header newline") + body = mod[nl + 1 :] + if not body.startswith(CJS_OPEN): + sys.exit("lift: module does not open with expected CJS wrapper") + body = body[len(CJS_OPEN) :] + # tail is either `})\n` or `})` + if body.endswith(b"})\n"): + body = body[:-3] + elif body.endswith(b"})"): + body = body[:-2] + else: + sys.exit("lift: module does not end with `})` wrapper close") + return body + + +def main() -> None: + if len(sys.argv) != 3: + sys.exit("usage: lift-claude-bun ") + + binary = Path(sys.argv[1]) + output = Path(sys.argv[2]) + + data = binary.read_bytes() + start, end = find_main_module(data) + body = unwrap(data[start:end]) + + # Sanity: the real claude-code cli.js always contains this legal banner. + if b"Anthropic" not in body[:4096]: + sys.exit("lift: extracted body is missing Anthropic banner — layout changed?") + + output.write_bytes(body) + sys.stderr.write( + f"lifted {len(body):,} bytes from {binary.name} " + f"(module @ {start:#x}..{end:#x}) -> {output}\n" + ) + + +if __name__ == "__main__": + main() diff --git a/graphical/llm/llm.nix b/graphical/llm/llm.nix new file mode 100644 index 0000000..8aa3e0d --- /dev/null +++ b/graphical/llm/llm.nix @@ -0,0 +1,152 @@ +{ + + lib, + pkgs, + ... +}: +let + inherit (lib) optionals; + settings = { + "$schema" = "https://json.schemastore.org/claude-code-settings.json"; + + env = { + CLAUDE_BASH_NO_LOGIN = "1"; + CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING = "1"; + CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY = "1"; + CLAUDE_CODE_DISABLE_TERMINAL_TITLE = "1"; + CLAUDE_CODE_EAGER_FLUSH = "1"; + CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"; + CLAUDE_CODE_FORCE_GLOBAL_CACHE = "1"; + CLAUDE_CODE_HIDE_ACCOUNT_INFO = "1"; + CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY = "20"; + CLAUDE_CODE_PLAN_V2_AGENT_COUNT = "5"; + CLAUDE_CODE_PLAN_V2_EXPLORE_AGENT_COUNT = "5"; + DISABLE_AUTO_COMPACT = "1"; + DISABLE_AUTOUPDATER = "1"; + DISABLE_COST_WARNINGS = "1"; + DISABLE_ERROR_REPORTING = "1"; + DISABLE_INSTALLATION_CHECKS = "1"; + DISABLE_TELEMETRY = "1"; + ENABLE_MCP_LARGE_OUTPUT_FILES = "1"; + ENABLE_TOOL_SEARCH = "auto:5"; + MAX_THINKING_TOKENS = "31999"; + MCP_CONNECTION_NONBLOCKING = "1"; + UV_THREADPOOL_SIZE = "16"; + }; + + attribution = { + commit = ""; + pr = ""; + }; + + permissions = { + allow = [ + "Read" + ]; + defaultMode = "bypassPermissions"; + }; + + statusLine = { + type = "command"; + command = "/etc/claude/statusline-command.nu"; + }; + + enabledPlugins = { + "clangd-lsp@claude-plugins-official" = true; + "rust-analyzer-lsp@claude-plugins-official" = true; + "context7@claude-plugins-official" = true; + "code-review@claude-plugins-official" = true; + "linear@claude-plugins-official" = true; + }; + + skipWebFetchPreflight = true; + + spinnerVerbs = { + mode = "replace"; + verbs = [ + "Redeeming" + "Clodding" + "Tokenmaxxing" + "Slopping" + "Clanking" + "Churning" + "Forgetting" + "Splurging" + "Ignoring GPL" + "Increasing ram prices" + ]; + }; + + cleanupPeriodDays = 90; + alwaysThinkingEnabled = true; + remoteControlAtStartup = true; + showClearContextOnPlanAccept = true; + skipDangerousModePermissionPrompt = true; + }; + + settingsJson = pkgs.writeText "claude-settings.json" (builtins.toJSON settings); + + runtimeDeps = lib.makeBinPath ([ + pkgs.bash + pkgs.procps + pkgs.ripgrep + pkgs.bubblewrap + pkgs.socat + ]); + + patchScript = pkgs.writeScript "patch-claude-src" '' + #!${pkgs.python3}/bin/python3 + ${builtins.readFile ./patch-claude-src.py} + ''; + + liftScript = pkgs.writeScript "lift-claude-bun" '' + #!${pkgs.python3}/bin/python3 + ${builtins.readFile ./lift-claude-bun.py} + ''; + + externalPackageJson = pkgs.writeText "claude-code-external-package.json" ( + builtins.toJSON { + name = "claude-code-lifted"; + type = "commonjs"; + dependencies = { + ws = "^8"; + undici = "^6"; + node-fetch = "^3"; + ajv = "^8"; + ajv-formats = "^3"; + yaml = "^2"; + }; + } + ); + + mkClod = + suffix: + let + finalSuffix = "-${suffix}"; + in + pkgs.writeScriptBin "claude${finalSuffix}" '' + #!${pkgs.nushell}/bin/nu --no-config-file + + $env._SETTINGS_JSON = "${settingsJson}" + $env._DENO = "${pkgs.deno}/bin/deno" + $env._PATCH_SCRIPT = "${patchScript}" + $env._LIFT_SCRIPT = "${liftScript}" + $env._EXTERNAL_PACKAGE_JSON = "${externalPackageJson}" + $env._TAR = "${pkgs.gnutar}/bin/tar" + $env._RUNTIME_DEPS = "${runtimeDeps}" + $env._ENV_JSON = ${lib.strings.toJSON settings.env} + $env.CLAUDE_CONFIG_DIR = ("~/.claude${finalSuffix}" | path expand) + + ${builtins.readFile ./launcher.nu} + ''; +in +{ + environment.systemPackages = [ + (mkClod "lillis") + (mkClod "amaan") + ]; + + environment.etc."claude/statusline-command.nu".source = ./statusline-command.nu; + + programs.nix-ld.enable = true; +} diff --git a/graphical/llm/patch-claude-src.py b/graphical/llm/patch-claude-src.py new file mode 100644 index 0000000..7036654 --- /dev/null +++ b/graphical/llm/patch-claude-src.py @@ -0,0 +1,231 @@ +from __future__ import annotations + +import re +import sys +from collections.abc import Callable +from pathlib import Path +from typing import Union + +type Replacement = Union[bytes, Callable[[re.Match[bytes]], bytes]] + +W: bytes = rb"[\w$]+" +# Qualified name: matches `FN` and also `NS.FN` (e.g. `Lf.join`, `Oc7.spawn`). +# Since 2.1.113 bun's bundler emits more member-style calls for path/spawn helpers. +Q: bytes = rb"[\w$]+(?:\.[\w$]+)*" +data: bytes = Path(sys.argv[1]).read_bytes() + +SEARCH_WINDOW: int = 500 + + +def log(msg: str) -> None: + sys.stderr.write(msg + "\n") + + +def patch(label: str, pattern: bytes, replacement: Replacement) -> None: + global data + data, n = re.subn(pattern, replacement, data) + log(f"{label} ({n})") + + +def replace(label: str, old: bytes, new: bytes) -> None: + global data + n: int = data.count(old) + if n == 0: + log(f"{label}: NOT FOUND") + return + data = data.replace(old, new) + log(f"{label} ({n})") + + +def flip_gates(gates: list[tuple[bytes, str]]) -> None: + """Flip all gate defaults from false to true in a single regex pass.""" + global data + gate_keys: list[bytes] = [g for g, _ in gates] + labels: dict[bytes, str] = dict(gates) + alternation: bytes = b"|".join(re.escape(g) for g in gate_keys) + pat: bytes = W + rb'\("(' + alternation + rb')",!1\)' + flipped: set[bytes] = set() + + def replacer(m: re.Match[bytes]) -> bytes: + flipped.add(m.group(1)) + return m[0].replace(b",!1)", b",!0)") + + data, n = re.subn(pat, replacer, data) + log(f"feature gates: {n} flipped across {len(flipped)} gates") + for key in gate_keys: + status = "ok" if key in flipped else "MISSED" + log(f" {labels[key]} [{status}]") + + +# --- AGENTS.md support --- +# The CLAUDE.md loader only reads CLAUDE.md. Patch it to also load AGENTS.md +# from the same directories. Pattern: let VAR=ME(DIR,"CLAUDE.md");ARR.push(...await XE(VAR,"Project",ARG,BOOL)) + +agents_pat: bytes = ( + rb"let (" + W + rb")=(" + Q + rb")\((" + W + rb'),"CLAUDE\.md"\);' + rb"(" + W + rb")\.push\(\.\.\.await (" + W + rb")\(\1,\"Project\",(" + W + rb"),(" + W + rb")\)\)" +) + + +def agents_repl(m: re.Match[bytes]) -> bytes: + var, join_fn, dir_, arr, load_fn, arg, flag = [m.group(i) for i in range(1, 8)] + return ( + b'for(let _f of["CLAUDE.md","AGENTS.md"]){let ' + + var + b"=" + join_fn + b"(" + dir_ + b",_f);" + + arr + b".push(...await " + load_fn + b"(" + var + b',"Project",' + arg + b"," + flag + b"))}" + ) + + +patch("agents.md loader", agents_pat, agents_repl) + +# --- macOS config path --- + +replace( + "macOS config path", + b'case"macos":return"/Library/Application Support/ClaudeCode"', + b'case"macos":return"/etc/claude-code"', +) + +# --- Enable hard-disabled slash commands --- + +slash_commands: list[tuple[bytes, str]] = [ + (b'name:"btw",description:"Ask a quick side question', "/btw"), + (b'name:"bridge-kick",description:"Inject bridge failure states', "/bridge-kick"), + (b'name:"files",description:"List all files currently in context"', "/files"), +] + +for anchor, label in slash_commands: + pos: int = data.find(anchor) + if pos < 0: + log(f"slash command {label}: NOT FOUND") + continue + window: bytes = data[pos : pos + SEARCH_WINDOW] + patched: bytes = window.replace(b"isEnabled:()=>!1", b"isEnabled:()=>!0", 1) + if patched == window: + log(f"slash command {label}: isEnabled not found in window") + continue + data = data[:pos] + patched + data[pos + SEARCH_WINDOW :] + log(f"slash command {label}: enabled") + +# --- Bypass telemetry gate in feature flag checker --- +# The chain is: h8(featureGate) bails to default if !Qo(); Qo()=Ew6(); +# Ew6()=!Cq6(); Cq6() returns true when on bedrock/vertex/foundry OR when +# user-facing telemetry is disabled (s_1()/equivalent). Drop the trailing +# telemetry-disabled check so feature gates still resolve with +# DISABLE_TELEMETRY=1 while preserving the bedrock/vertex/foundry detection. +# Anchor on the stable env-var literal CLAUDE_CODE_USE_BEDROCK; the obfuscated +# function name (Cq6) and the trailing wrapper name (s_1) both rotate. + +patch( + "telemetry gate (drop telemetry-disabled check)", + ( + rb"function (" + W + rb")\(\)\{return (" + W + rb")\(process\.env\.CLAUDE_CODE_USE_BEDROCK\)" + rb"\|\|\2\(process\.env\.CLAUDE_CODE_USE_VERTEX\)" + rb"\|\|\2\(process\.env\.CLAUDE_CODE_USE_FOUNDRY\)" + rb"\|\|" + W + rb"\(\)\}" + ), + lambda m: re.sub(rb"\|\|" + W + rb"\(\)\}$", b"||!1}", m[0]), +) + +# --- Restore 1h prompt cache TTL when telemetry is off --- +# https://github.com/anthropics/claude-code/issues/45381 +# The GrowthBook allowlist for "ttl":"1h" cache_control falls back to the +# default object when telemetry is off. Anthropic now ships +# {allowlist:["repl_main_thread*","sdk","auto_mode"]} as the default (up +# from the broken {} in earlier versions), so the TUI and SDK already get +# 1h TTL — but batch agents and less-common query sources still miss. +# Widen the default to ["*"] so everything matches. + +patch( + "1h prompt cache TTL fallback", + rb'(' + W + rb')\("tengu_prompt_cache_1h_config",\{allowlist:\[[^\]]+\]\}\)\.allowlist\?\?\[\]', + lambda m: m[1] + b'("tengu_prompt_cache_1h_config",{allowlist:["*"]}).allowlist??[]', +) + +# --- Fix Deno-compile bridge spawn --- +# Deno-compiled binaries eat --flags as V8 args, so we route spawns through +# env(1) to pass them as normal CLI flags instead. + +patch( + "deno bridge spawn fix", + rb"let (" + W + rb")=(" + Q + rb")\((" + W + rb")\.execPath,(" + W + rb"),", + lambda m: ( + b"let " + + m[1] + + b"=" + + m[2] + + b'("env",["--",' + + m[3] + + b".execPath,..." + + m[4] + + b"]," + ), +) + +# --- Flip feature gates --- +# DISABLE_TELEMETRY=1 prevents GrowthBook feature flag resolution, so all gates +# fall back to their hardcoded defaults (false). Flip them to true. + +Gate = tuple[bytes, str] + +core_gates: list[Gate] = [ + (b"tengu_ccr_bridge", "remote control"), + (b"tengu_bridge_system_init", "bridge SDK init on connect"), + (b"tengu_bridge_client_presence_enabled", "bridge presence heartbeats"), + (b"tengu_bridge_requires_action_details", "bridge rich tool-use payloads"), + (b"tengu_remote_backend", "remote backend"), + (b"tengu_immediate_model_command", "instant /model switching"), + (b"tengu_fgts", "fine-grained tool streaming"), + (b"tengu_auto_background_agents", "background agent timeout"), + (b"tengu_plan_mode_interview_phase", "plan mode interview"), + (b"tengu_surreal_dali", "scheduled agents/cron"), +] + +memory_gates: list[Gate] = [ + # (b"tengu_session_memory", "session memory"), # auto-memory; pollutes unrelated convos + (b"tengu_pebble_leaf_prune", "message pruning"), + (b"tengu_herring_clock", "team memory directory"), + (b"tengu_passport_quail", "typed combined memory prompts"), + (b"tengu_paper_halyard", "memory dedup in nested dirs"), +] + +ux_gates: list[Gate] = [ + (b"tengu_coral_fern", "grep hints in prompt"), + (b"tengu_kairos_brief", "brief output mode"), + (b"tengu_destructive_command_warning", "destructive command warnings"), + (b"tengu_amber_prism", "permission denial context"), + (b"tengu_hawthorn_steeple", "context windowing"), + (b"tengu_loud_sugary_rock", "Opus 4.7 terse output guidance"), + (b"tengu_verified_vs_assumed", "verified-vs-assumed reporting"), + (b"tengu_pewter_brook", "fullscreen TUI default"), +] + +tool_gates: list[Gate] = [ + (b"tengu_chrome_auto_enable", "auto-enable chrome devtools"), + (b"tengu_plum_vx3", "web search reranking"), + # (b"tengu_moth_copse", "relevant memory recall"), # auto-recall; pollutes unrelated convos + (b"tengu_cork_m4q", "batch command processing"), + (b"tengu_harbor", "plugin marketplace"), + (b"tengu_harbor_permissions", "plugin permissions"), + (b"tengu_relay_chain_v1", "parallel command chaining guidance"), + (b"tengu_edit_minimalanchor_jrn", "Edit tool minimal-anchor instructions"), + (b"tengu_slate_reef", "Read tool clearer offset/limit docs"), + (b"tengu_otk_slot_v1", "output-token escalation for complex tasks"), + (b"tengu_onyx_basin_m1k", "subagent tool-result truncation"), + (b"tengu_sub_nomdrep_q7k", "block subagent report .md writes"), + (b"tengu_amber_sentinel", "Monitor tool for streaming bg scripts"), + (b"tengu_miraculo_the_bard", "skip penguin-mode startup prefetch"), + (b"tengu_noreread_q7m_velvet", "sharper 'wasted read' feedback"), +] + +flip_gates(core_gates + memory_gates + ux_gates + tool_gates) + +# --- Bump background agent timeout from 120s to 240s --- + +patch( + "background agent timeout", + rb'"tengu_auto_background_agents",![01]\)\)return 120000', + lambda m: m[0].replace(b"120000", b"240000"), +) + +Path(sys.argv[1]).write_bytes(data) diff --git a/graphical/llm/statusline-command.nu b/graphical/llm/statusline-command.nu new file mode 100644 index 0000000..47b702f --- /dev/null +++ b/graphical/llm/statusline-command.nu @@ -0,0 +1,137 @@ +#!/usr/bin/env -S nu --no-config-file + +def format-duration [ms: int] { + let total_s = $ms // 1000 + let h = $total_s // 3600 + let m = ($total_s mod 3600) // 60 + let s = $total_s mod 60 + if $h > 0 { + $"($h)h($m | fill -a r -w 2 -c '0')m($s | fill -a r -w 2 -c '0')s" + } else if $m > 0 { + $"($m)m($s | fill -a r -w 2 -c '0')s" + } else { + $"($s)s" + } +} + +def color-for-pct [pct: number] { + let pct_int = $pct | math floor | into int + if $pct_int >= 80 { + "\e[31m" + } else if $pct_int >= 50 { + "\e[33m" + } else { + "\e[32m" + } +} + +def format-rate-limits [input: record] { + let session_pct = try { $input | get rate_limits.five_hour.used_percentage } catch { null } + let week_pct = try { $input | get rate_limits.seven_day.used_percentage } catch { null } + + let session_part = if $session_pct != null { + let c = color-for-pct $session_pct + let v = $session_pct | math round --precision 0 | into int + $"session: ($c)($v)%\e[0m" + } else { "" } + let week_part = if $week_pct != null { + let c = color-for-pct $week_pct + let v = $week_pct | math round --precision 0 | into int + $"week: ($c)($v)%\e[0m" + } else { "" } + + [$session_part $week_part] | where {|x| $x | is-not-empty} | str join " " +} + + +# --- Main --- +let input = (^cat | from json) + +let usage_info = format-rate-limits $input + +let model_name = ($input | get model?.display_name? | default ($input | get model?.id? | default "unknown")) +let used_pct = ($input | get context_window?.used_percentage? | default null) +let total_cost = ($input | get cost?.total_cost_usd? | default 0) +let total_input = ($input | get context_window?.s_in? | default ($input | get context_window?.total_input_tokens? | default 0)) +let total_output = ($input | get context_window?.s_out? | default ($input | get context_window?.total_output_tokens? | default 0)) +let duration_ms = ($input | get cost?.total_duration_ms? | default 0) +let api_duration_ms = ($input | get cost?.total_api_duration_ms? | default 0) +let lines_added = ($input | get cost?.total_lines_added? | default 0) +let lines_removed = ($input | get cost?.total_lines_removed? | default 0) +let exceeds_200k = ($input | get exceeds_200k_tokens? | default false) + +let cache_read = ($input | get context_window?.cache_read_tokens? | default 0) +let cache_create = ($input | get context_window?.cache_creation_tokens? | default 0) + +let total_tokens = $total_input + $total_output + +def format-tokens [n: int] { + if $n >= 1_000_000 { + $"($n / 1_000_000.0 | math round --precision 1)M" + } else if $n >= 1_000 { + $"($n / 1_000.0 | math round --precision 1)k" + } else { + $"($n)" + } +} + +let in_display = (format-tokens ($total_input | into int)) +let out_display = (format-tokens ($total_output | into int)) +let tok_display = $"($in_display)/($out_display)" + +let cache_total = $cache_read + $cache_create +let cache_display = if $cache_total > 0 { + let cache_pct = ($cache_read * 100 / $cache_total | math round --precision 0 | into int) + let cache_color = if $cache_pct >= 70 { + "\e[32m" + } else if $cache_pct >= 40 { + "\e[33m" + } else { + "\e[31m" + } + $" cache:($cache_color)($cache_pct)%\e[0m" +} else { "" } + +let context_display = if $used_pct != null { + let color = color-for-pct $used_pct + let pct_str = $used_pct | math round --precision 1 + $"($color)($pct_str)%\e[0m" +} else { "--" } + +let cost_cents = ($total_cost * 100 | math round | into int) +let cost_dollars = $cost_cents // 100 +let cost_frac = ($cost_cents mod 100 | math abs | into string | fill -a r -w 2 -c '0') +let cost_display = $"$($cost_dollars).($cost_frac)" +let elapsed_display = (format-duration ($duration_ms | into int)) +let wait_display = (format-duration ($api_duration_ms | into int)) +let churn_display = $"\e[32m+($lines_added)\e[0m/\e[31m-($lines_removed)\e[0m" +let marker_200k = if $exceeds_200k { " | \e[31m!200k\e[0m" } else { "" } +def format-cwd [dir: string] { + if ($dir | is-empty) { return "" } + let home = ($env.HOME? | default "") + let display = if ($home | is-not-empty) and ($dir | str starts-with $home) { + let rel = ($dir | str replace $home "" | str trim -l -c '/') + $"~/($rel)" + } else { + $dir + } + let parts = ($display | split row "/") + let shortened = if ($parts | length) <= 5 { + $display + } else { + let tail = ($parts | last 5 | str join "/") + $"…/($tail)" + } + $shortened +} + +let cwd_raw = ($input | get workspace?.current_dir? | default "") +let cwd_display = if ($cwd_raw | is-not-empty) { + let formatted = (format-cwd $cwd_raw) + $" | ($formatted)" +} else { "" } +let quota_section = if ($usage_info | is-not-empty) { + " | (usage) " + $usage_info +} else { "" } + +print -n $"($model_name) | Ctx: ($context_display) | ($tok_display)($cache_display) | ($cost_display) | t:($elapsed_display) w:($wait_display) | ($churn_display)($marker_200k)($quota_section)($cwd_display)" diff --git a/graphical/media.nix b/graphical/media.nix index 1a4a545..334a9b0 100644 --- a/graphical/media.nix +++ b/graphical/media.nix @@ -1,9 +1,11 @@ { config, pkgs, + mkWrappers, ... }: let + inherit (mkWrappers pkgs) wrap; ui = config.rice.roles config.rice.palette.hex; imvConfig = pkgs.writeText "imv-config" '' [options] @@ -12,14 +14,10 @@ let overlay_text_color=${ui.fg} overlay_background_color=${ui.surface} ''; - imvWrapped = pkgs.symlinkJoin { + imvWrapped = wrap { name = "imv"; - paths = [ pkgs.imv ]; - nativeBuildInputs = [ pkgs.makeWrapper ]; - postBuild = '' - wrapProgram $out/bin/imv \ - --set imv_config "${imvConfig}" - ''; + pkg = pkgs.imv; + envs = { imv_config = "${imvConfig}"; }; }; in with pkgs; diff --git a/graphical/password-manager.nix b/graphical/password-manager.nix index 553e7b0..666bc0d 100644 --- a/graphical/password-manager.nix +++ b/graphical/password-manager.nix @@ -20,6 +20,7 @@ let --set XDG_CONFIG_HOME "${rbwConfigDir}" done ''; + meta.mainProgram = "rbw"; }; in { diff --git a/graphical/platform-themes.nix b/graphical/platform-themes.nix index f2a3b06..29aaa99 100644 --- a/graphical/platform-themes.nix +++ b/graphical/platform-themes.nix @@ -10,12 +10,8 @@ let inherit (config) rice; in - (with pkgs.kdePackages; [ + [ pkgs.gtk-engine-murrine - breeze - breeze.qt5 - ]) - ++ [ rice.gtk-theme.package rice.fonts.sans.package rice.icons.package @@ -27,7 +23,9 @@ config = { theme = { colorScheme = - let ui = config.rice.roles config.rice.palette.hex; in + let + ui = config.rice.roles config.rice.palette.hex; + in pkgs.runCommand "theme.colors" { } '' sed \ -e "s|@bg@|${ui.bg}|g" \ @@ -43,7 +41,7 @@ > $out ''; iconTheme = config.rice.icons.name; - style = "breeze"; + style = "fusion"; font = with config.rice.fonts.sans; { family = name; size = size; diff --git a/graphical/terminal.nix b/graphical/terminal.nix index eefd6ce..52fd06e 100644 --- a/graphical/terminal.nix +++ b/graphical/terminal.nix @@ -2,55 +2,74 @@ pkgs, lib, config, + mkWrappers, ... }: -(scope "apps.terminal" <| pkgs.foot) -// scope "programs.foot" { - enable = true; - enableFishIntegration = true; - settings = { - main = - let - font = - config.rice.fonts.monospace.name + ":size=" + (builtins.toString config.rice.fonts.monospace.size); - in - { - inherit font; - font-bold = font; - font-italic = font; - }; - bell = { - system = true; - urgent = true; - notify = true; - visual = true; - }; - colors-dark = - let - pal = config.rice.palette.shortHex; - in - { - background = pal.util.bg; - foreground = pal.util.fg; - regular0 = pal.normal.black; - regular1 = pal.normal.red; - regular2 = pal.normal.green; - regular3 = pal.normal.yellow; - regular4 = pal.normal.blue; - regular5 = pal.normal.magenta; - regular6 = pal.normal.cyan; - regular7 = pal.normal.white; - bright0 = pal.bright.black; - bright1 = pal.bright.red; - bright2 = pal.bright.green; - bright3 = pal.bright.yellow; - bright4 = pal.bright.blue; - bright5 = pal.bright.magenta; - bright6 = pal.bright.cyan; - bright7 = pal.bright.white; +let + inherit (mkWrappers pkgs) wrap; + font = + config.rice.fonts.monospace.name + ":size=" + (builtins.toString config.rice.fonts.monospace.size); + pal = config.rice.palette.shortHex; - selection-foreground = pal.util.fg_sel; - selection-background = pal.util.bg_sel; - }; + footConfig = pkgs.writeText "foot.ini" '' + [main] + font=${font} + font-bold=${font} + font-italic=${font} + + [bell] + system=yes + urgent=yes + notify=yes + visual=yes + + [colors-dark] + background=${pal.util.bg} + foreground=${pal.util.fg} + regular0=${pal.normal.black} + regular1=${pal.normal.red} + regular2=${pal.normal.green} + regular3=${pal.normal.yellow} + regular4=${pal.normal.blue} + regular5=${pal.normal.magenta} + regular6=${pal.normal.cyan} + regular7=${pal.normal.white} + bright0=${pal.bright.black} + bright1=${pal.bright.red} + bright2=${pal.bright.green} + bright3=${pal.bright.yellow} + bright4=${pal.bright.blue} + bright5=${pal.bright.magenta} + bright6=${pal.bright.cyan} + bright7=${pal.bright.white} + selection-foreground=${pal.util.fg_sel} + selection-background=${pal.util.bg_sel} + + [tabs] + enabled=yes + position=bottom + inherit-cwd=yes + style=gradient + label-padding=15 + height=26 + background=${pal.bright.black} + foreground=${pal.util.fg} + active-background=${pal.util.bg_sel} + active-foreground=${pal.util.fg_sel} + layout=floating + tab-width=180 + tab-padding=10 + margin=8 + ''; + + footPatched = pkgs.foot.overrideAttrs (old: { + patches = (old.patches or [ ]) ++ [ ./foot-tabs.patch ]; + }); + + foot = wrap { + name = "foot"; + pkg = footPatched; + args = [ "--config=${footConfig}" ]; }; -} +in +scope "apps.terminal" foot diff --git a/hosts/quiver/kernel.nix b/hosts/quiver/kernel.nix index 58554e8..f9ea386 100644 --- a/hosts/quiver/kernel.nix +++ b/hosts/quiver/kernel.nix @@ -6,9 +6,9 @@ bunker.kernel = { enable = true; - cpuArch = "MZEN3"; - version = "6.19"; - hardened = false; - lto = "none"; + version = "7.0"; + # cpuArch = "MZEN3"; + # hardened = false; + # lto = "none"; }; } diff --git a/hosts/quiver/ssh.nix b/hosts/quiver/ssh.nix new file mode 100644 index 0000000..57f508b --- /dev/null +++ b/hosts/quiver/ssh.nix @@ -0,0 +1,15 @@ +_: { + services.openssh = { + enable = true; + listenAddresses = [ + { + addr = "100.64.0.3"; + port = 22; + } + { + addr = "192.168.20.7"; + port = 22; + } + ]; + }; +} diff --git a/lib/create.nix b/lib/create.nix index eae2f9c..47cad32 100644 --- a/lib/create.nix +++ b/lib/create.nix @@ -20,6 +20,7 @@ in mainUser = info.username; machineName = name; getPkgs = builtins.attrValues; + mkWrappers = import ./wrappers.nix lib; getFlakePkg = p: p.packages.${info.system}.default; getFlakePkg' = p: n: p.packages.${info.system}.${n}; stdenv.hostPlatform = info.system; diff --git a/lib/wrappers.nix b/lib/wrappers.nix new file mode 100644 index 0000000..efd3194 --- /dev/null +++ b/lib/wrappers.nix @@ -0,0 +1,20 @@ +lib: pkgs: +let + wrap = + { name, pkg, args ? [ ], envs ? { } }: + let + argsStr = lib.concatStringsSep " " (map (a: ''--add-flags "${a}"'') args); + envsStr = lib.concatStringsSep " " (lib.mapAttrsToList (k: v: ''--set ${k} "${v}"'') envs); + in + pkgs.symlinkJoin { + inherit name; + paths = [ pkg ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postBuild = "wrapProgram $out/bin/${name} ${argsStr} ${envsStr}"; + }; + + wrapXdg = + name: pkg: configDir: + wrap { inherit name pkg; envs = { XDG_CONFIG_HOME = configDir; }; }; +in +{ inherit wrap wrapXdg; }