let
  shellBase =
    {
      pkgs,
      config,
      lib,
      ...
    }:
    let
      inherit (config) theme;
      inherit (lib.strings) readFile;
      inherit (lib.meta) getExe;
      inherit (lib.lists) singleton;

      homeDir = "($env.HOME)";

      aliases = {
        mp = "mprocs";

        todo = "hx ${homeDir}/notes/todo.md";
        notes = "hx ${homeDir}/notes";
        random = "hx ${homeDir}/notes/random.md";

        rm = "rm --recursive --verbose";
        cp = "cp --recursive --verbose --progress";
        mv = "mv --verbose";
        mk = "mkdir";

        ls = "eza";
        sl = "eza";
        ll = "eza -la";
        la = "eza -a";
        lsa = "eza -a";
        lsl = "eza -l -a";

        tree = "eza --tree --git-ignore --group-directories-first";

        git-graph = ''git log --graph --full-history --pretty=format:"%h%  %d%x20%s"'';

        fj = "fj --host https://git.plumj.am";

        oops = "nix run nixpkgs#sqlite -- ${homeDir}/.config/nushell/history.sqlite3 'DELETE FROM history WHERE rowid IN (SELECT rowid FROM history ORDER BY rowid DESC LIMIT 5);'";

        cat = "${getExe pkgs.bat} --theme ${theme.bat}";
        less = "${getExe pkgs.bat} --plain";

        mosh = "mosh --no-init";

        dc = "discordo";

        ns = "niri-session";

        h = "hx";
        e = "hx"; # editor

        j = "jj";
        lj = "lazyjj";
        ju = "jjui";

        codex = "codex resume --ask-for-approval untrusted";
        oc = "opencode --continue";

        # This absolute path is fine. Rebuilds always happen from the jam user.
        rebuild = "/home/jam/nixos/rebuild.nu";

        nu-config-reference = "nu -c 'config nu --doc | nu-highlight | bat'";
      };

      zoxideNushellIntegration = # nu
        ''
          source ${
            pkgs.runCommand "zoxide-init-nu" { } ''${getExe pkgs.zoxide} init nushell --cmd=cd >> "$out"''
          }
        '';
    in
    {
      hjem.extraModules = singleton {
        packages = [
          pkgs.bash
          pkgs.carapace
          pkgs.direnv
          pkgs.fish
          pkgs.inshellisense
          pkgs.nushell
          pkgs.zoxide
          pkgs.zsh
        ];

        xdg.config.files."direnv/lib/nix-direnv.sh".source = "${pkgs.nix-direnv}/share/nix-direnv/direnvrc";

        xdg.config.files."nushell/config.nu".text =
          # nu
          ''
            ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: val: "alias ${name} = ${val}") aliases)}
            $env.config.edit_mode = "vi"
            $env.config.buffer_editor = "${config.environment.variables.EDITOR}"
            $env.config.show_banner = false
            $env.config.footer_mode = "auto"
            $env.config.use_kitty_protocol = true

            $env.config.recursion_limit = 100
            $env.config.error_style = "nested"

            $env.config.completions.algorithm = "substring"
            $env.config.completions.sort = "smart"
            $env.config.completions.case_sensitive = false
            $env.config.completions.quick = true
            $env.config.completions.partial = true
            $env.config.completions.use_ls_colors = true

            $env.config.ls.use_ls_colors = true
            $env.config.ls.clickable_links = true

            $env.config.rm.always_trash = false

            $env.config.table.mode = "compact"
            $env.config.table.index_mode = "always"
            $env.config.table.show_empty = true

            $env.config.table.trim.methodology = "wrapping"
            $env.config.table.trim.wrapping_try_keep_words = true
            $env.config.table.trim.truncating_suffix = "..."

            $env.config.history.file_format = "sqlite"
            $env.config.history.max_size = 10000000
            $env.config.history.sync_on_enter = true

            $env.config.cursor_shape.emacs = "block"
            $env.config.cursor_shape.vi_insert = "line"
            $env.config.cursor_shape.vi_normal = "block"

            $env.config.float_precision = 2
            $env.config.use_ansi_coloring = "auto"

            $env.config.explore.help_banner = true
            $env.config.explore.exit_esc = true
            $env.config.explore.command_bar_text = "#C4C9C6"
            $env.config.explore.highlight.bg = "yellow"
            $env.config.explore.highlight.fg = "black"

            $env.config.explore.table.split_line = "#404040"
            $env.config.explore.table.cursor = true
            $env.config.explore.table.line_index = true
            $env.config.explore.table.line_shift = true
            $env.config.explore.table.line_head_top = true
            $env.config.explore.table.line_head_bottom = true
            $env.config.explore.table.show_head = true
            $env.config.explore.table.show_index = true

            $env.config.explore.config.cursor_color.bg = "yellow"
            $env.config.explore.config.cursor_color.fg = "black"

            $env.config.keybindings = [
              {
                name: quit_shell
                modifier: control
                keycode: char_d
                mode: emacs
                event: null
              }
              {
                name: quit_shell
                modifier: control
                keycode: char_d
                mode: vi_insert
                event: null
              }
              {
                name: quit_shell
                modifier: control
                keycode: char_d
                mode: vi_normal
                event: null
              }
            ]

            $env.config.hooks.env_change.PWD = [
              { |before, after| zellij-update-tabname }
              {||
                if (which direnv | is-empty) { return }

                direnv export json | from json | default {} | load-env
              }
            ]

            $env.config.hooks.display_output = {
              if (term size).columns >= 100  { tee { table --expand | print } } | try { if $in != null { $env.last = $in } }
            }

            $env.config.hooks.pre_prompt = [
              {
                if not (which direnv | is-empty) {
                  direnv export json | from json | default {} | load-env
                  $env.PATH = ($env.PATH | split row (char env_sep))
                }
              }
            ]

            ${readFile ./nushell.menus.nu}
            ${readFile ./nushell.functions.nu}

            ${zoxideNushellIntegration}

            # TODO: $env.XDG_CONFIG_HOME via hjem
            let topiary_dir = $env.HOME | path join .config topiary
            if not ($topiary_dir | path exists) {
              mkdir $topiary_dir
              mkdir ($topiary_dir | path join queries)
              http get https://raw.githubusercontent.com/blindFS/topiary-nushell/main/languages.ncl
              | save ($topiary_dir | path join languages.ncl)
              http get https://raw.githubusercontent.com/blindFS/topiary-nushell/main/queries/nu.scm
              | save ($topiary_dir | path join queries nu.scm)
            }
            $env.TOPIARY_CONFIG_FILE = ($topiary_dir | path join languages.ncl)
            $env.TOPIARY_LANGUAGE_DIR = ($topiary_dir | path join queries)

            source $"($nu.cache-dir)/carapace.nu"
            source $"($nu.cache-dir)/jj.nu"
          '';

        xdg.config.files."nushell/env.nu".text =
          with theme.withHash;
          #nu
          ''
            			use std/config ${theme.nushell}
            			$env.config.color_config = (${theme.nushell})

            			$env.CARAPACE_BRIDGES = "zsh,fish,bash,inshellisense,clap,jj,nu"
            			mkdir $"($nu.cache-dir)"
            			carapace _carapace nushell | save --force $"($nu.cache-dir)/carapace.nu"

            			jj util completion nushell | save --force $"($nu.cache-dir)/jj.nu"

            			# let carapace_completer = {|spans: list<string>|
              	# 		# If the current command is an alias, get it's expansion.
               #      let expanded_alias = (scope aliases | where name == $spans.0 | get -i 0 | get -i expansion)

               #      # Overwrite.
               #      let spans = (if $expanded_alias != null  {
               #          # put the first word of the expanded alias first in the span
               #          $spans | skip 1 | prepend ($expanded_alias | split row " " | take 1)
               #      } else { $spans })

               #      carapace $spans.0 nushell ...$spans
               #      | from json
               #      | if ($in | default [] | any {|| $in.display | str starts-with "ERR"}) { null } else { $in }
               #    }

            			$env.LS_COLORS = (${pkgs.vivid}/bin/vivid generate ${theme.vivid})

            			let theme_json = $"($env.HOME)/nixos/modules/theme.json"
            			if ($theme_json | path exists) {
            				let theme = (open $theme_json)
            				$env.THEME_MODE = $theme.mode
            				$env.THEME_SCHEME = $theme.scheme
            			} else {
            				$env.THEME_MODE = "${theme.variant}"
            				$env.THEME_SCHEME = "${theme.colorScheme}"
            			}

            			# Custom Nushell prompt.

            			def prompt [--transient]: nothing -> string {
            				let exit_code = $env.LAST_EXIT_CODE

            				let status = if not ($exit_code == 0) or $transient {
            					$"(ansi '${base0D}')┫(ansi rst)(if $exit_code == 0 { ansi '${base0D}' } else { ansi '${base08}' })($exit_code)(ansi rst)(ansi '${base0D}')┣(ansi rst)"
            				} else {
            					$"(ansi '${base0D}')━(ansi rst)"
            				}

            				let host = if ($env.SSH_CONNECTION? | is-not-empty) {
            					$" (ansi '${base0B}')(hostname)(ansi rst)"
            				} else { "" }

            				let jj_root = try {
            		      jj workspace root err> /dev/null
            		    } catch { "" }

            		    let pwd = pwd | path expand

            		    let dir = if ($jj_root | is-not-empty) {
            		      let subpath = $pwd | path relative-to $jj_root
            		      let subpath = if ($subpath | is-not-empty) {
            		        $"(ansi '${base0E}') ⟶ (ansi rst)(ansi '${base0B}')($subpath)(ansi rst)"
            		      }
            			      $"($jj_root | path basename)($subpath)"
            			    } else {
            			      let pwd = if ($pwd | str starts-with $env.HOME) {
            			        "~" | path join ($pwd | path relative-to $env.HOME)
            	      } else { $pwd }

            	      $pwd
            	    }

            				let directory = $"(ansi '${base0A}')($dir)(ansi rst)"

            				let jj_info = if (which jj | is-not-empty) {
            					try {
            						let jj_output = (jj --quiet --color always --ignore-working-copy log --no-graph --revisions @ --template '
            							separate(
            								" ",
            								bookmarks.join(", "),
            								if(empty, label("empty", "(empty)")),
            								coalesce(
            									surround("\"", "\"",
            										if(
            											description.first_line().substr(0, 26).starts_with(description.first_line()),
            											description.first_line().substr(0, 26),
            											description.first_line().substr(0, 25) ++ "…"
            								  	)
            								  ),
            									label(if(empty, "empty"), "")
            								),
            								change_id.shortest(),
            								commit_id.shortest(),
            								if(conflict, label("conflict", "(conflict)")),
            								if(divergent, label("divergent prefix", "(divergent)")),
            								if(hidden, label("hidden prefix", "(hidden)")),
            								if(immutable, label("immutable", "(immutable)")),
            							)
            						' err> /dev/null | str trim)

            						# Only show parent bookmark if current change has no bookmarks.
            						let jj_has_bookmark = (jj --quiet --color always log --no-graph --revisions @ --template 'bookmarks.len() > 0' err> /dev/null | str trim) == "true"
            						let jj_parent = if not $jj_has_bookmark {
            							(jj --quiet --color always --ignore-working-copy log --no-graph --revisions 'heads(::@ & bookmarks())' --template 'bookmarks ++ "\n"' err> /dev/null | lines | str join ",")
            						} else { "" }

            						let jj_parent_display = if ($jj_parent | is-not-empty) {
            							$"tug ← ($jj_parent)"
            						} else { "" }

            						let combined = if ($jj_parent_display | is-not-empty) and ($jj_output | is-not-empty) {
            							$" ($jj_parent_display) ($jj_output)"
            						} else if ($jj_parent_display | is-not-empty) {
            							$" ($jj_parent_display)"
            						} else if ($jj_output | is-not-empty) {
            							$" ($jj_output)"
            						} else { "" }
            						$combined
            					} catch { "" }
            				} else { "" }

            				let ms = ($env.CMD_DURATION_MS | into int)
            				let duration = if $transient or $ms > 1000 {
            					let secs = $ms / 1000 | math floor
            					if $transient and $ms < 1000 {
            						$" (ansi '${base0A}')($ms)ms"
            					} else {
            						$" (ansi '${base0A}')($secs)s"
            					}
            				} else { "" }

            				let bar = $"(ansi '${base0D}')(ansi attr_bold)━(ansi rst)"

            				let prompt_line = [
            					(char nl)
            					$bar
            					$status
            					$bar
            					$host
            					" "
            					$directory
            					" "
            					(if ($jj_info | is-not-empty) {
            						[
            							$"(ansi '${base0D}')━┫(ansi rst)"
            							$jj_info
            							$" (ansi '${base0D}')┣━(ansi rst)"
            						] | str join
            					} else {
            						[
            							$bar
            							$bar
            							$bar
            						] | str join
            					})
            					$duration
            					(char nl)
            				] | str join

            				$prompt_line
            			}

            			$env.PROMPT_COMMAND                 = { || prompt }
            			$env.PROMPT_COMMAND_RIGHT           = ""
            			$env.TRANSIENT_PROMPT_COMMAND       = { || prompt --transient }
            			$env.TRANSIENT_PROMPT_COMMAND_RIGHT = ""

            			$env.PROMPT_INDICATOR                     = " "
            			$env.PROMPT_INDICATOR_VI_NORMAL           = $env.PROMPT_INDICATOR
            			$env.PROMPT_INDICATOR_VI_INSERT           = $env.PROMPT_INDICATOR
            			$env.PROMPT_MULTILINE_INDICATOR           = $env.PROMPT_INDICATOR
            			$env.TRANSIENT_PROMPT_INDICATOR           = $env.PROMPT_INDICATOR
            			$env.TRANSIENT_PROMPT_INDICATOR_VI_NORMAL = $env.PROMPT_INDICATOR
            			$env.TRANSIENT_PROMPT_INDICATOR_VI_INSERT = $env.PROMPT_INDICATOR
            			$env.TRANSIENT_PROMPT_MULTILINE_INDICATOR = $env.PROMPT_INDICATOR
          '';
      };
    };

in
{
  flake.modules.nixos.shell = shellBase;
  flake.modules.darwin.shell = shellBase;
}