diff --git a/modules/prompt/full-line.nu b/modules/prompt/full-line.nu index 1fb8d25..afe3bd5 100644 --- a/modules/prompt/full-line.nu +++ b/modules/prompt/full-line.nu @@ -1,49 +1,507 @@ -# Nushell "Full Line" Prompt -# Fills the entire line above the prompt with "useful" info, creates an easy-to-read separation between commands outputs. -# Just the thing for portrait format terminal windows. -# This one includes working directory in the middle and has option for timestamp info on right. -# (many) other permutations are possible. -# You can play with this by `source full-line.nu` in an active shell. -# To enable permanently, modify your `env.nu` file; add `create_center_prompt` and modify `let-env PROMPT_COMMAND` line as shown here. -# TODO: revisit the below comment with fill -# Life made complicated by `str lpad` counting all the ansi sequences as visible, so it doesn't pad enough. +# Build a full-line prompt with widgets for: +# current working directory; git status; and current position in remembered working directories (`std dirs`, a.k.a. `shells`) +# +# to use: +# 1. copy this file to `($nu.default-config-dir | path add 'scripts')` (Or someplace on your $env.NU_LIB_DIRS path, defined in env.nu) +# 2. cut `let-env PROMPT_COMMAND` and `PROMPT_OMMAND_RIGHT' from your env.nu. +# These will depend on `use full-line`, which can not be done in env.nu. +# You can leave the `PROMPT-*INDICATOR*` statements in env.nu or +# consolidate all prompt stuff in config.nu. +# 3. Add new prompt setup stuff somewhere in config.nu: +# ``` +# use prompt-builder.nu +# let-env PROMPT_COMMAND = {|| prompt-builder } +# let-env PROMPT_COMMAND_RIGHT = "" +# ``` +# +# credit panache-git for the git status widget. -def create_center_prompt [] { - let path_segment = if (is-admin) { - $" (ansi red_bold)($env.PWD)(ansi reset) " - } else { - $" (ansi green_bold)($env.PWD)(ansi reset) " - } - let path_segment_visible_length = ($path_segment | ansi strip | str length) - let path_segment_excess_length = ($path_segment | str length) - $path_segment_visible_length +use std dirs - # to disable the right hand segment, change line below to be simply `let time_segment = ''`. - let time_segment = $"(date now | date format ' %F %r')(ansi reset)" - let time_segment_excess_length = ($time_segment | str length) - ($time_segment | ansi strip | str length) +# build the prompt from segments, looks like: +# +# ^------------- ------ --- $ +# +# or, if no git repo current directory +# +# ^-------------------------- ------------- $ +export def main [ + --pad_char (-p) = '_' # character to fill with +] { + let left_segment = $" (dir_string) " + let left_segment_length = ($left_segment | ansi strip | str length -g) + + let dirs_segment = $"|(dirs show | each {|it| if $it.active {'V'} else {'.'}} | str join '')|" + mut git_segment = $" (repo-styled) " + if ($git_segment | str length -g) <= 2 {$git_segment = ''} + let right_segment = $"($git_segment)($pad_char * 3) ($dirs_segment)" + let right_segment_length = ($right_segment | ansi strip | str length -g) - let path_segment_pad = ((((term size).columns + $path_segment_visible_length) / 2) | into int) - let time_segment_pad = (term size).columns - $path_segment_pad - - let pad_char = '-' - - let segment = ([ - ( $path_segment | fill -w ($path_segment_pad + $path_segment_excess_length) -c $pad_char -a r), - ( $time_segment | fill -w ($time_segment_pad + $time_segment_excess_length) -c $pad_char -a r) - ] | str join) - - $segment + let term_width = ((term size) | get columns) + mut left_half_length = (($term_width + $left_segment_length) / 2 | into int) + if (($left_half_length + $right_segment_length) > $term_width) { + $left_half_length = $left_segment_length # adapt to narrow console + } + let right_half_length = ($term_width - $left_half_length) # guarantee sum === term_width + + [(ansi reset), + ($left_segment | fill --character $pad_char --width $left_half_length --alignment right), + ($right_segment | fill --character $pad_char --width $right_half_length --alignment right), + "\n" + ] | str join '' } -# For full width prompt we simply print the prompt line but throw away the value; -# Nushell displays PROMPT_INDICATOR in left hand column, leaving lots of room for long command lines. -let-env PROMPT_COMMAND = { print (create_center_prompt); } -# let-env PROMPT_COMMAND_RIGHT = { create_right_prompt } -let-env PROMPT_COMMAND_RIGHT = '' +# build current working directory segment +def dir_string [] { -# The prompt indicators are environmental variables that represent -# the state of the prompt -let-env PROMPT_INDICATOR = { "〉" } -let-env PROMPT_INDICATOR_VI_INSERT = { ": " } -let-env PROMPT_INDICATOR_VI_NORMAL = { "〉" } -let-env PROMPT_MULTILINE_INDICATOR = { "::: " } + mut home = "" + try { + if $nu.os-info.name == "windows" { + $home = $env.USERPROFILE + } else { + $home = $env.HOME + } + } + + let dir = ([ + ($env.PWD | str substring 0..($home | str length) | str replace --string $home "~"), + ($env.PWD | str substring ($home | str length)..) + ] | str join) + + let path_color = (if (is-admin) { ansi red_bold } else { ansi green_bold }) + let separator_color = (if (is-admin) { ansi light_red_bold } else { ansi light_green_bold }) + $"($path_color)($dir)(ansi reset)" | str replace --all --string (char path_sep) $"($separator_color)/($path_color)" +} + +# Following code cheerfully ~~stolen~~ adapted from: +# https://github.com/nushell/nu_scripts/blob/ab0d3aaad015ca8ac2c2004d728cc8bac32cda1b/modules/prompt/panache-git.nu + +# Get repository status as structured data +def repo-structured [] { + let in_git_repo = (do --ignore-errors { git rev-parse --abbrev-ref HEAD } | is-empty | nope) + + let status = (if $in_git_repo { + git --no-optional-locks status --porcelain=2 --branch | lines + } else { + [] + }) + + let on_named_branch = (if $in_git_repo { + $status + | where ($it | str starts-with '# branch.head') + | first + | str contains '(detached)' + | nope + } else { + false + }) + + let branch_name = (if $on_named_branch { + $status + | where ($it | str starts-with '# branch.head') + | split column ' ' col1 col2 branch + | get branch + | first + } else { + '' + }) + + let commit_hash = (if $in_git_repo { + $status + | where ($it | str starts-with '# branch.oid') + | split column ' ' col1 col2 full_hash + | get full_hash + | first + | str substring 0..7 + } else { + '' + }) + + let tracking_upstream_branch = (if $in_git_repo { + $status + | where ($it | str starts-with '# branch.upstream') + | str join + | is-empty + | nope + } else { + false + }) + + let upstream_exists_on_remote = (if $in_git_repo { + $status + | where ($it | str starts-with '# branch.ab') + | str join + | is-empty + | nope + } else { + false + }) + + let ahead_behind_table = (if $upstream_exists_on_remote { + $status + | where ($it | str starts-with '# branch.ab') + | split column ' ' col1 col2 ahead behind + } else { + [[]] + }) + + let commits_ahead = (if $upstream_exists_on_remote { + $ahead_behind_table + | get ahead + | first + | into int + } else { + 0 + }) + + let commits_behind = (if $upstream_exists_on_remote { + $ahead_behind_table + | get behind + | first + | into int + | math abs + } else { + 0 + }) + + let has_staging_or_worktree_changes = (if $in_git_repo { + $status + | where ($it | str starts-with '1') or ($it | str starts-with '2') + | str join + | is-empty + | nope + } else { + false + }) + + let has_untracked_files = (if $in_git_repo { + $status + | where ($it | str starts-with '?') + | str join + | is-empty + | nope + } else { + false + }) + + let has_unresolved_merge_conflicts = (if $in_git_repo { + $status + | where ($it | str starts-with 'u') + | str join + | is-empty + | nope + } else { + false + }) + + let staging_worktree_table = (if $has_staging_or_worktree_changes { + $status + | where ($it | str starts-with '1') or ($it | str starts-with '2') + | split column ' ' + | get column2 + | split column '' staging worktree --collapse-empty + } else { + [[]] + }) + + let staging_added_count = (if $has_staging_or_worktree_changes { + $staging_worktree_table + | where staging == 'A' + | length + } else { + 0 + }) + + let staging_modified_count = (if $has_staging_or_worktree_changes { + $staging_worktree_table + | where staging in ['M', 'R'] + | length + } else { + 0 + }) + + let staging_deleted_count = (if $has_staging_or_worktree_changes { + $staging_worktree_table + | where staging == 'D' + | length + } else { + 0 + }) + + let untracked_count = (if $has_untracked_files { + $status + | where ($it | str starts-with '?') + | length + } else { + 0 + }) + + let worktree_modified_count = (if $has_staging_or_worktree_changes { + $staging_worktree_table + | where worktree in ['M', 'R'] + | length + } else { + 0 + }) + + let worktree_deleted_count = (if $has_staging_or_worktree_changes { + $staging_worktree_table + | where worktree == 'D' + | length + } else { + 0 + }) + + let merge_conflict_count = (if $has_unresolved_merge_conflicts { + $status + | where ($it | str starts-with 'u') + | length + } else { + 0 + }) + + { + in_git_repo: $in_git_repo, + on_named_branch: $on_named_branch, + branch_name: $branch_name, + commit_hash: $commit_hash, + tracking_upstream_branch: $tracking_upstream_branch, + upstream_exists_on_remote: $upstream_exists_on_remote, + commits_ahead: $commits_ahead, + commits_behind: $commits_behind, + staging_added_count: $staging_added_count, + staging_modified_count: $staging_modified_count, + staging_deleted_count: $staging_deleted_count, + untracked_count: $untracked_count, + worktree_modified_count: $worktree_modified_count, + worktree_deleted_count: $worktree_deleted_count, + merge_conflict_count: $merge_conflict_count + } +} + +# Get repository status as a styled string +def repo-styled [] { + let status = (repo-structured) + + let is_local_only = ($status.tracking_upstream_branch != true) + + let upstream_deleted = ( + $status.tracking_upstream_branch and + $status.upstream_exists_on_remote != true + ) + + let is_up_to_date = ( + $status.upstream_exists_on_remote and + $status.commits_ahead == 0 and + $status.commits_behind == 0 + ) + + let is_ahead = ( + $status.upstream_exists_on_remote and + $status.commits_ahead > 0 and + $status.commits_behind == 0 + ) + + let is_behind = ( + $status.upstream_exists_on_remote and + $status.commits_ahead == 0 and + $status.commits_behind > 0 + ) + + let is_ahead_and_behind = ( + $status.upstream_exists_on_remote and + $status.commits_ahead > 0 and + $status.commits_behind > 0 + ) + + let branch_name = (if $status.in_git_repo { + (if $status.on_named_branch { + $status.branch_name + } else { + ['(' $status.commit_hash '...)'] | str join + }) + } else { + '' + }) + + let branch_styled = (if $status.in_git_repo { + (if $is_local_only { + (branch-local-only $branch_name) + } else if $is_up_to_date { + (branch-up-to-date $branch_name) + } else if $is_ahead { + (branch-ahead $branch_name $status.commits_ahead) + } else if $is_behind { + (branch-behind $branch_name $status.commits_behind) + } else if $is_ahead_and_behind { + (branch-ahead-and-behind $branch_name $status.commits_ahead $status.commits_behind) + } else if $upstream_deleted { + (branch-upstream-deleted $branch_name) + } else { + $branch_name + }) + } else { + '' + }) + + let has_staging_changes = ( + $status.staging_added_count > 0 or + $status.staging_modified_count > 0 or + $status.staging_deleted_count > 0 + ) + + let has_worktree_changes = ( + $status.untracked_count > 0 or + $status.worktree_modified_count > 0 or + $status.worktree_deleted_count > 0 or + $status.merge_conflict_count > 0 + ) + + let has_merge_conflicts = $status.merge_conflict_count > 0 + + let staging_summary = (if $has_staging_changes { + (staging-changes $status.staging_added_count $status.staging_modified_count $status.staging_deleted_count) + } else { + '' + }) + + let worktree_summary = (if $has_worktree_changes { + (worktree-changes $status.untracked_count $status.worktree_modified_count $status.worktree_deleted_count) + } else { + '' + }) + + let merge_conflict_summary = (if $has_merge_conflicts { + (unresolved-conflicts $status.merge_conflict_count) + } else { + '' + }) + + let delimiter = (if ($has_staging_changes and $has_worktree_changes) { + ('|' | bright-yellow) + } else { + '' + }) + + let local_summary = ( + $'($staging_summary) ($delimiter) ($worktree_summary) ($merge_conflict_summary)' | str trim + ) + + let local_indicator = (if $status.in_git_repo { + (if $has_worktree_changes { + ('!' | red) + } else if $has_staging_changes { + ('~' | bright-cyan) + } else { + '' + }) + } else { + '' + }) + + let repo_summary = ( + $'($branch_styled) ($local_summary) ($local_indicator)' | str trim + ) + + let left_bracket = ('[' | bright-yellow) + let right_bracket = (']' | bright-yellow) + + (if $status.in_git_repo { + $'($left_bracket)($repo_summary)($right_bracket)' + } else { + '' + }) +} + +# Helper commands to encapsulate style and make everything else more readable + +def nope [] { + each { |it| $it == false } +} + +def bright-cyan [] { + each { |it| $"(ansi -e '96m')($it)(ansi reset)" } +} + +def bright-green [] { + each { |it| $"(ansi -e '92m')($it)(ansi reset)" } +} + +def bright-red [] { + each { |it| $"(ansi -e '91m')($it)(ansi reset)" } +} + +def bright-yellow [] { + each { |it| $"(ansi -e '93m')($it)(ansi reset)" } +} + +def green [] { + each { |it| $"(ansi green)($it)(ansi reset)" } +} + +def red [] { + each { |it| $"(ansi red)($it)(ansi reset)" } +} + +def branch-local-only [ + branch: string +] { + $branch | bright-cyan +} + +def branch-upstream-deleted [ + branch: string +] { + $'($branch) (char failed)' | bright-cyan +} + +def branch-up-to-date [ + branch: string +] { + $'($branch) (char identical_to)' | bright-cyan +} + +def branch-ahead [ + branch: string + ahead: int +] { + $'($branch) (char branch_ahead)($ahead)' | bright-green +} + +def branch-behind [ + branch: string + behind: int +] { + $'($branch) (char branch_behind)($behind)' | bright-red +} + +def branch-ahead-and-behind [ + branch: string + ahead: int + behind: int +] { + $'($branch) (char branch_behind)($behind) (char branch_ahead)($ahead)' | bright-yellow +} + +def staging-changes [ + added: int + modified: int + deleted: int +] { + $'+($added) ~($modified) -($deleted)' | green +} + +def worktree-changes [ + added: int + modified: int + deleted: int +] { + $'+($added) ~($modified) -($deleted)' | red +} + +def unresolved-conflicts [ + conflicts: int +] { + $'!($conflicts)' | red +}