Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

nushell/modules/prompt.nu

Raw
# Fast prompt. Git is read from files; jj uses --ignore-working-copy and never snapshots.

const ICON_NOTES = (char -u "03C0")
const ICON_WORKSPACE = (char -u "03BB")
const ICON_HOME = (char home)
const ICON_GIT = (char nf_git)
const ICON_BRANCH = (char nf_branch)
const ICON_CHECK = (char -u "2713")
const ICON_FAILED = (char failed)
const ICON_PIPE = (char pipe)
const ICON_PROMPT = (char prompt)
const ICON_JJ = (char -u "f329") # nf-linux-parrot
const ICON_JJ_AT = "@"
const ICON_UNKNOWN = "?"
const FLAG_EMPTY = "∅"
const FLAG_CONFLICT = "×"
const FLAG_DIVERGENT = "≈"

const ANSI_DARK_GRAY = (ansi dark_gray)
const ANSI_DARK_GRAY_DIMMED = (ansi dark_gray_dimmed)
const ANSI_GREEN = (ansi green)
const ANSI_MAGENTA = (ansi magenta)
const ANSI_RED = (ansi red)
const ANSI_RESET = (ansi reset)

const PATH_SEP = (char path_sep)
const TRUNCATE_DEPTH = 5
const TRUNCATE_SYMBOL = $"…($PATH_SEP)"
const NOTES_DIR = "Notes"
const WORKSPACE_DIR = "Workspace"

const VCS_GIT = "git"
const VCS_JJ = "jj"
const DOT_GIT = ".git"
const DOT_JJ = ".jj"
const HEAD_FILE = "HEAD"
const OBJECTS_DIR = "objects"
const REFS_DIR = "refs"
const GITDIR_PREFIX = "gitdir: "
const REF_HEADS_PREFIX = "ref: refs/heads/"
const DETACHED_LABEL = "HEAD"

const JJ_TEMPLATE = '"{\"change\":" ++ change_id.short().escape_json() ++ ",\"bookmarks\":" ++ stringify(local_bookmarks.join(",")).escape_json() ++ ",\"empty\":" ++ json(empty) ++ ",\"conflict\":" ++ json(conflict) ++ ",\"divergent\":" ++ json(divergent) ++ "}\n"'

const PROMPT_INDICATOR_TEXT = $"($ANSI_GREEN) ⚈  ($ANSI_RESET)"
const PROMPT_INSERT_TEXT = $"($ANSI_MAGENTA) ($ICON_PIPE) ($ANSI_RESET)"
const PROMPT_NORMAL_TEXT = $"($ANSI_MAGENTA) ($ICON_PROMPT) ($ANSI_RESET)"
const PROMPT_MULTILINE_TEXT = $"($ANSI_MAGENTA) ($ICON_PROMPT)($ICON_PROMPT) ($ANSI_RESET)"

# Expand `~`; leave absolute/relative paths to `path join`/`path expand`.
def expand-home [path: string] {
    if ($path | str starts-with "~") { $path | path expand } else { $path }
}

# Resolve gitdir paths from `.git` files and `$GIT_DIR`.
def resolve-git-path [path: string, base: path] {
    $base | path join (expand-home $path) | path expand
}

def git-dir-from-file [dotgit: path, base: path] {
    let line = (try { open --raw $dotgit | str trim } catch { return null })
    if ($line | str starts-with $GITDIR_PREFIX) {
        resolve-git-path ($line | str replace $GITDIR_PREFIX "") $base
    }
}

def safe-path-type [value: any] {
    if $value == null { null } else { try { $value | path type } catch { null } }
}

def is-conventional-bare-git-dir [dir: path] {
    ($dir | path basename) == $DOT_GIT and (safe-path-type ($dir | path join $HEAD_FILE)) == file and (($dir | path join $OBJECTS_DIR) | path exists) and (($dir | path join $REFS_DIR) | path exists)
}

def env-git-vcs [] {
    let git_dir = $env.GIT_DIR?
    if ($git_dir == null) or ($git_dir | is-empty) { return null }

    {kind: $VCS_GIT, git_dir: (resolve-git-path $git_dir ($env.PWD? | default "."))}
}

def find-git-or-colocated-jj [] {
    let pwd = $env.PWD?
    if $pwd == null { return null }

    mut dir = ($pwd | path expand)

    loop {
        let dotgit = ($dir | path join $DOT_GIT)
        let type = (safe-path-type $dotgit)

        if $type == dir {
            if (safe-path-type ($dir | path join $DOT_JJ)) == dir { return {kind: $VCS_JJ, root: $dir} }
            return {kind: $VCS_GIT, git_dir: $dotgit}
        }

        if $type == file {
            let git_dir = (git-dir-from-file $dotgit $dir)
            if $git_dir != null {
                if (safe-path-type ($dir | path join $DOT_JJ)) == dir { return {kind: $VCS_JJ, root: $dir} }
                return {kind: $VCS_GIT, git_dir: $git_dir}
            }
        }

        if (is-conventional-bare-git-dir $dir) { return {kind: $VCS_GIT, git_dir: $dir} }

        let parent = ($dir | path dirname)
        if $parent == $dir { return (env-git-vcs) }
        $dir = $parent
    }
}

def find-jj-only [] {
    let pwd = $env.PWD?
    if $pwd == null { return null }

    mut dir = ($pwd | path expand)

    loop {
        if (safe-path-type ($dir | path join $DOT_JJ)) == dir { return {kind: $VCS_JJ, root: $dir} }

        let parent = ($dir | path dirname)
        if $parent == $dir { return null }
        $dir = $parent
    }
}

def find-vcs [] {
    let vcs = (find-git-or-colocated-jj)
    if $vcs != null { return $vcs }

    find-jj-only
}

def branch-from-head [head: string] {
    if ($head | str starts-with $REF_HEADS_PREFIX) {
        $head | str replace $REF_HEADS_PREFIX ""
    } else {
        $DETACHED_LABEL
    }
}

def git-segment [git_dir: path] {
    let head = (try { open --raw ($git_dir | path join $HEAD_FILE) | str trim } catch { return null })
    let branch = (branch-from-head $head)
    $" ($ICON_GIT) ($ICON_BRANCH) ($branch)"
}

def jj-installed [] {
    which jj | is-not-empty
}

# Query jj in prompt-safe mode: no snapshot, no pager, no color, JSON template.
def jj-info [root: path] {
    if not (jj-installed) { return {available: false} }

    let res = (try { ^jj -R $root --ignore-working-copy --no-pager --color never log --no-graph -r @ -T $JJ_TEMPLATE | complete } catch { return {available: false} })
    if $res.exit_code != 0 { return null }

    try { $res.stdout | from json | upsert available true } catch { null }
}

def jj-flags [info: record] {
    [
        (if $info.empty { $FLAG_EMPTY })
        (if $info.conflict { $FLAG_CONFLICT })
        (if $info.divergent { $FLAG_DIVERGENT })
    ] | str join
}

def suffix [value: string] {
    if ($value | is-empty) { "" } else { $" ($value)" }
}

def jj-segment [root: path] {
    let info = (jj-info $root)
    if $info == null { return null }
    if not $info.available { return $" ($ICON_JJ) ($ICON_UNKNOWN)" }

    $" ($ICON_JJ) ($ICON_JJ_AT) ($info.change)(suffix $info.bookmarks)(suffix (jj-flags $info))"
}

def get_vcs_path [] {
    let vcs = (find-vcs)
    if $vcs == null { return null }

    if $vcs.kind == $VCS_JJ { jj-segment $vcs.root } else { git-segment $vcs.git_dir }
}

def path-segment [] {
    let home = if $env.HOME? != null { $env.HOME? } else { $env.USERPROFILE? }
    let notes = if $env.NOTES? != null { $env.NOTES? } else if $home != null { $home | path join $NOTES_DIR } else { null }
    let workspace = if $env.WORKSPACE? != null { $env.WORKSPACE? } else if $home != null { $home | path join $WORKSPACE_DIR } else { null }

    let pwd = $env.PWD?
    if $pwd == null { return "" }

    mut path = ($pwd | path expand)
    if $notes != null { $path = ($path | str replace $notes $ICON_NOTES) }
    if $workspace != null { $path = ($path | str replace $workspace $ICON_WORKSPACE) }
    if $home != null { $path = ($path | str replace $home $ICON_HOME) }

    if ($path | path split | length) >= $TRUNCATE_DEPTH {
        $path | path dirname --num-levels 2 --replace $TRUNCATE_SYMBOL
    } else {
        $path
    }
}

def create_left_prompt [] {
    [
        $ANSI_DARK_GRAY
        (path-segment)
        $ANSI_RESET
        $ANSI_GREEN
        (get_vcs_path)
        $ANSI_RESET
    ] | str join
}

def create_right_prompt [] {
    let status = if (($env.LAST_EXIT_CODE? | default 0) == 0) { $" ($ICON_CHECK) " } else { $"($ANSI_RED) ($ICON_FAILED) ($ANSI_RESET)" }

    [
        $ANSI_DARK_GRAY_DIMMED
        $status
        $ANSI_RESET
    ] | str join
}

export-env {
    $env.PROMPT_COMMAND = { || create_left_prompt }
    $env.PROMPT_COMMAND_RIGHT = { || create_right_prompt }
    $env.PROMPT_INDICATOR = $PROMPT_INDICATOR_TEXT
    $env.PROMPT_INDICATOR_VI_INSERT = $PROMPT_INSERT_TEXT
    $env.PROMPT_INDICATOR_VI_NORMAL = $PROMPT_NORMAL_TEXT
    $env.PROMPT_MULTILINE_INDICATOR = $PROMPT_MULTILINE_TEXT
}