sibling 3.0.0

get next/previous sibling directory name.
Documentation
# The functions for traversing the sibling directories.
# Elvish loads them as a module; save this script into the lib directory,
# which is usually ~/.config/elvish/lib.
#
#     sibling --init elvish > ~/.config/elvish/lib/sibling.elv
#
# Then, put the following line into your rc.elv.
#
#     use sibling
#
# The commands are available with the module name, such as "sibling:cdnext".
# To call them by the bare names, bind their function values in your rc.elv.
#
#     var cdnext~ = $sibling:cdnext~
#     var cdprev~ = $sibling:cdprev~
#
# Note that "eval (sibling --init elvish | slurp)" defines nothing; Elvish
# evaluates the code in a temporary namespace, and discards it.
#
# Each function receives the optional count of the traversing, such as
# "cdnext 3". A negative count traverses in the opposite direction.
# The count is ignored by cdfirst, cdlast, cdrand, lsfirst, lslast, and lsrand.
#
# They also receive "-f FILE", which traverses the directories listed in the
# file, instead of the siblings of the working directory, such as
# "cdnext -f ~/projects.txt". Give the file in an absolute path, since the
# working directory changes.

use re

# Put the count of the traversing and the file of the given arguments;
# the empty file means the working directory.
fn -parse {|@rest|
    var count = 1
    var file = ''
    while (> (count $rest) 0) {
        if (or (eq $rest[0] -f) (eq $rest[0] --file)) {
            if (< (count $rest) 2) {
                echo "sibling: "$rest[0]": no file is given" >&2
                fail 'no file is given'
            }
            set file = $rest[1]
            set rest = $rest[2..]
        } else {
            set count = $rest[0]
            set rest = $rest[1..]
        }
    }
    put $count $file
}

# Put the target of the traversing; the given file, or the working directory.
fn -target {|file|
    if (eq $file '') { put $pwd } else { put $file }
}

# Print the working directory with its position, such as "/path/to/c (3/26)".
fn -position {|file|
    sibling --progress --type keep -- (-target $file)
}

# Tell the user why no directory was found. The sibling command itself prints
# the reason of an error, hence, this reports the exhausted list only.
fn -report {|code|
    if (== $code 1) {
        echo "sibling: no more sibling directory" >&2
    }
}

# Change the working directory to the found sibling directory.
fn -cd {|type @rest|
    var count file = (-parse $@rest)
    var found = []
    # A non-zero exit status of an external command raises an exception in
    # Elvish; capture it by ?(...) to tell the result from its exit status.
    var err = ?(set found = [(sibling --type $type --step $count -- (-target $file))])
    if (not (is $err $ok)) {
        -report $err[reason][exit-status]
        return
    }
    cd $found[0]
    -position $file
}

# List the entries of the found sibling directory, without changing the
# working directory.
fn -ls {|type @rest|
    var count file = (-parse $@rest)
    var found = []
    var err = ?(set found = [(sibling --type $type --step $count -- (-target $file))])
    if (not (is $err $ok)) {
        -report $err[reason][exit-status]
        return
    }
    echo $found[0]
    ls -- $found[0]
}

# Choose a sibling directory with the filter command, such as peco and fzf,
# and change the working directory to it.
fn -cd-with-filter {|filter @rest|
    var _ file = (-parse $@rest)
    var selected = []
    var err = ?(set selected = [(sibling --format list --type keep -- (-target $file) | (external $filter))])
    if (not (is $err $ok)) {
        return
    }
    if (== (count $selected) 0) {
        return
    }
    # Each line of the list format consists of the index, the marker of the
    # current and the next directories, and the path; drop all but the path.
    cd (re:replace '^ *[0-9]+ (\* |> |  )' '' $selected[0])
    -position $file
}

# Print the found sibling directory, without changing the working directory.
# It tells the result by the exception only, since the caller usually reads it
# by the output capture, such as `cp file (nextdir)`.
fn -print {|type @rest|
    var count file = (-parse $@rest)
    var found = []
    var err = ?(set found = [(sibling --type $type --step $count -- (-target $file))])
    if (not (is $err $ok)) {
        return
    }
    echo $found[0]
}

# Whether the hook is registered; the callback of $after-chdir cannot be
# removed once it is added, hence, it asks this variable every time.
var hook-enabled = $false

# Set NEXTDIR and PREVDIR to the siblings of the working directory; they become
# empty when no such directory is found.
fn -hook {
    if (not $hook-enabled) {
        return
    }
    var next = []
    var err = ?(set next = [(sibling --type next -- $pwd 2>/dev/null)])
    var prev = []
    set err = ?(set prev = [(sibling --type previous -- $pwd 2>/dev/null)])
    set-env NEXTDIR (if (> (count $next) 0) { put $next[0] } else { put '' })
    set-env PREVDIR (if (> (count $prev) 0) { put $prev[0] } else { put '' })
}

# Run the hook on every change of the working directory. It is not registered
# by default, since it runs the sibling command twice on every change; reading
# a directory of ten thousand entries costs about 30 milliseconds.
fn sibling_hook_enable {
    if (not $hook-enabled) {
        set after-chdir = [$@after-chdir {|_| -hook }]
    }
    set hook-enabled = $true
    -hook
}

fn sibling_hook_disable {
    set hook-enabled = $false
    unset-env NEXTDIR
    unset-env PREVDIR
}

fn nextdir {|@rest| -print next $@rest }

fn prevdir {|@rest| -print previous $@rest }

fn cdnext {|@rest| -cd next $@rest }

fn cdprev {|@rest| -cd previous $@rest }

fn cdfirst {|@rest| -cd first $@rest }

fn cdlast {|@rest| -cd last $@rest }

fn cdrand {|@rest| -cd random $@rest }

fn lsnext {|@rest| -ls next $@rest }

fn lsprev {|@rest| -ls previous $@rest }

fn lsfirst {|@rest| -ls first $@rest }

fn lslast {|@rest| -ls last $@rest }

fn lsrand {|@rest| -ls random $@rest }

fn sibling_peco {|@rest| -cd-with-filter peco $@rest }

fn sibling_fzf {|@rest| -cd-with-filter fzf $@rest }