added experimental mdcat

This commit is contained in:
fatalerrors
2026-05-20 13:55:28 +02:00
parent 9ec52aa49f
commit 28e4c112af
3 changed files with 475 additions and 1 deletions

View File

@@ -105,6 +105,7 @@ A bar-style prompt showing current time, execution time of the last command
| `ku` | processes | Kill all processes owned by the given user name or ID |
| `matrix` | rain | Console screensaver with Matrix-style digital rain (binary, kana, ascii charset) |
| `mcd` | filefct | Create a directory and immediately move into it |
| `mdcat` | disp | Render Markdown files in terminal with colors, inline formatting, and framed code blocks |
| `meteo` | info | Display weather forecast for the configured or given city |
| `myextip` | net | Get information about your public IP address |
| `get_pkgmgr` | packages | Detect the active package manager of the running distribution (`apt`, `dnf`, `yum`, `zypper`, `pacman`, `apk`, `portage`, `xbps`, `nix`) |

View File

@@ -236,6 +236,478 @@ export -f disp
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Render Markdown files with terminal formatting
# Usage: mdcat [file]
mdcat()
{
_mdcat_style_inline()
{
local text="$1"
local bold_on="" italic_on="" code_on="" style_off=""
local link_text_on="" link_url_on=""
if [[ -z $NO_COLOR ]]; then
bold_on=$'\e[1m'
italic_on=$'\e[3m'
code_on="${On_IBlack}${BIWhite}"
link_text_on=$'\e[4;96m'
link_url_on="$IBlack"
style_off="$RESETCOL"
fi
# Apply inline transforms in a safe order: code, links, then emphasis.
# This prevents emphasis parsing inside code spans or link URLs.
while [[ "$text" =~ \[([^][]+)\]\(([^()]+)\) ]]; do
local match="${BASH_REMATCH[0]}"
local label="${BASH_REMATCH[1]}"
local url="${BASH_REMATCH[2]}"
local before="${text%%"$match"*}"
local after="${text#*"$match"}"
if [[ "$label" == "$url" ]]; then
if [[ -z $NO_COLOR ]]; then
text="${before}${link_text_on}${label}${style_off}${after}"
else
text="${before}${label}${after}"
fi
elif [[ -z $NO_COLOR ]]; then
text="${before}${link_text_on}${label}${style_off} ${link_url_on}(${url})${style_off}${after}"
else
text="${before}${label} (${url})${after}"
fi
done
# Style bare URLs without re-matching the same URL forever.
# The prefix capture keeps progress monotonic and prevents freeze loops.
# Skip this transformation in NO_COLOR mode.
if [[ -z $NO_COLOR ]]; then
while [[ "$text" =~ (^|[[:space:]\(])(https?://[^[:space:]\)]+) ]]; do
local match="${BASH_REMATCH[0]}"
local pre="${BASH_REMATCH[1]}"
local url="${BASH_REMATCH[2]}"
local before="${text%%"$match"*}"
local after="${text#*"$match"}"
text="${before}${pre}${link_text_on}${url}${style_off}${after}"
done
fi
while [[ "$text" =~ \`([^\`]+)\` ]]; do
local match="${BASH_REMATCH[0]}"
local val="${BASH_REMATCH[1]}"
local before="${text%%"$match"*}"
local after="${text#*"$match"}"
text="${before}${code_on}${val}${style_off}${after}"
done
while [[ "$text" =~ \*\*([^*]+)\*\* ]]; do
local match="${BASH_REMATCH[0]}"
local val="${BASH_REMATCH[1]}"
local before="${text%%"$match"*}"
local after="${text#*"$match"}"
text="${before}${bold_on}${val}${style_off}${after}"
done
while [[ "$text" =~ __([^_]+)__ ]]; do
local match="${BASH_REMATCH[0]}"
local val="${BASH_REMATCH[1]}"
local before="${text%%"$match"*}"
local after="${text#*"$match"}"
text="${before}${bold_on}${val}${style_off}${after}"
done
while [[ "$text" =~ \*([^*]+)\* ]]; do
local match="${BASH_REMATCH[0]}"
local val="${BASH_REMATCH[1]}"
local before="${text%%"$match"*}"
local after="${text#*"$match"}"
text="${before}${italic_on}${val}${style_off}${after}"
done
# Match _italic_ only when underscores are outside word-like identifiers.
while [[ "$text" =~ (^|[[:space:][:punct:]])_([^[:space:]_][^_]*[^[:space:]_])_([[:space:][:punct:]]|$) ]]; do
local match="${BASH_REMATCH[0]}"
local pre="${BASH_REMATCH[1]}"
local val="${BASH_REMATCH[2]}"
local post="${BASH_REMATCH[3]}"
local before="${text%%"$match"*}"
local after="${text#*"$match"}"
text="${before}${pre}${italic_on}${val}${style_off}${post}${after}"
done
# Unescape Markdown punctuation escapes, such as \<, \>, \_, and \*.
while [[ "$text" =~ \\([[:punct:]]) ]]; do
local match="${BASH_REMATCH[0]}"
local val="${BASH_REMATCH[1]}"
local before="${text%%"$match"*}"
local after="${text#*"$match"}"
text="${before}${val}${after}"
done
printf "%s\n" "$text"
}
_mdcat_print_hr()
{
local cols="${COLUMNS:-}"
if [[ -z "$cols" || ! "$cols" =~ ^[0-9]+$ || "$cols" -lt 20 ]]; then
cols=$(tput cols 2>/dev/null)
fi
[[ -z "$cols" || ! "$cols" =~ ^[0-9]+$ || "$cols" -lt 20 ]] && cols=80
local hr
printf -v hr "%*s" "$cols" ""
hr="${hr// /-}"
if [[ -z $NO_COLOR ]]; then
printf "%b%s%b\n" "$IBlack" "$hr" "$RESETCOL"
else
printf "%s\n" "$hr"
fi
}
_mdcat_print_code_block()
{
local lang="$1"
shift
local -a lines=("$@")
local cols="${COLUMNS:-}"
if [[ -z "$cols" || ! "$cols" =~ ^[0-9]+$ || "$cols" -lt 20 ]]; then
cols=$(tput cols 2>/dev/null)
fi
[[ -z "$cols" || ! "$cols" =~ ^[0-9]+$ || "$cols" -lt 20 ]] && cols=80
local max_inner=$((cols - 4))
(( max_inner < 16 )) && max_inner=16
local width=16 line
for line in "${lines[@]}"; do
local line_len=${#line}
(( line_len > width )) && width=$line_len
done
(( width > max_inner )) && width=$max_inner
local border
printf -v border "+-%*s-+" "$width" ""
border="${border// /-}"
local frame_on="" code_on="" off=""
if [[ -z $NO_COLOR ]]; then
frame_on="$IBlack"
code_on="$BIWhite"
off="$RESETCOL"
fi
printf "%b%s%b\n" "$frame_on" "$border" "$off"
if [[ -n "$lang" ]]; then
local tag="language: $lang"
local wrapped_tag
wrapped_tag=$(printf "%s" "$tag" | fold -s -w "$width")
while IFS= read -r line || [[ -n "$line" ]]; do
printf "%b| %b%-*s%b |%b\n" "$frame_on" "$code_on" "$width" "$line" "$frame_on" "$off"
done <<< "$wrapped_tag"
printf "%b%s%b\n" "$frame_on" "$border" "$off"
fi
if [[ ${#lines[@]} -eq 0 ]]; then
printf "%b| %b%-*s%b |%b\n" "$frame_on" "$code_on" "$width" "" "$frame_on" "$off"
else
for line in "${lines[@]}"; do
local wrapped
wrapped=$(printf "%s" "$line" | fold -s -w "$width")
while IFS= read -r wline || [[ -n "$wline" ]]; do
printf "%b| %b%-*s%b |%b\n" "$frame_on" "$code_on" "$width" "$wline" "$frame_on" "$off"
done <<< "$wrapped"
done
fi
printf "%b%s%b\n" "$frame_on" "$border" "$off"
}
_mdcat_print_table()
{
local -a lines=("$@")
local -a table_rows=()
local -a col_widths=()
local i j ncols=0
local sep=$'\x1f'
_mdcat_parse_table_row()
{
local input="$1"
local line
line=$(printf '%s' "$input" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
line="${line#|}"
line="${line%|}"
# Parse Markdown cells using only '|' separators and preserve spaces inside cells.
local -a cells=()
local cell rest="$line"
while :; do
if [[ "$rest" == *'|'* ]]; then
cell="${rest%%|*}"
rest="${rest#*|}"
else
cell="$rest"
rest=""
fi
cell=$(printf '%s' "$cell" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
cells+=("$cell")
[[ -z "$rest" ]] && break
done
if [[ ${#cells[@]} -eq 0 ]]; then
printf '%s' ""
return
fi
local joined="${cells[0]}"
for ((j=1; j<${#cells[@]}; ++j)); do
joined+="$sep${cells[j]}"
done
printf '%s' "$joined"
}
# Parse header and data rows, skipping the Markdown separator row.
# Width is computed from visible text length with ANSI escapes stripped.
for ((i=0; i<${#lines[@]}; ++i)); do
(( i == 1 )) && continue
local parsed
parsed=$(_mdcat_parse_table_row "${lines[i]}")
table_rows+=("$parsed")
local -a row=()
IFS="$sep" read -r -a row <<< "$parsed"
(( ncols < ${#row[@]} )) && ncols=${#row[@]}
for ((j=0; j<${#row[@]}; ++j)); do
local vis
vis=$(_mdcat_style_inline "${row[j]}")
vis=$(printf '%b' "$vis" | sed -E 's/\x1B\[[0-9;]*[mK]//g')
local cell_len=${#vis}
[[ -z "${col_widths[j]}" ]] && col_widths[j]=0
(( col_widths[j] < cell_len )) && col_widths[j]=$cell_len
done
done
# Ensure all width slots are initialized before drawing borders.
for ((j=0; j<ncols; ++j)); do
[[ -z "${col_widths[j]}" ]] && col_widths[j]=0
done
# Draw top border
local border="+"
for ((j=0; j<ncols; ++j)); do
local w=$((col_widths[j]+2))
border+="$(printf '%*s' "$w" "" | tr ' ' '-')+"
done
printf "%b\n" "$IBlack$border$RESETCOL"
# Print header row
local -a header=()
IFS="$sep" read -r -a header <<< "${table_rows[0]}"
printf "%b|" "$IBlack"
for ((j=0; j<ncols; ++j)); do
local raw_cell="${header[j]:-}"
local styled_cell
styled_cell=$(_mdcat_style_inline "$raw_cell")
local visible
visible=$(printf '%b' "$styled_cell" | sed -E 's/\x1B\[[0-9;]*[mK]//g')
local pad=$((col_widths[j] - ${#visible}))
(( pad < 0 )) && pad=0
printf " %b%b%*s%b |" "$BBlue" "$styled_cell" "$pad" "" "$IBlack"
done
printf "%b\n" "$RESETCOL"
# Header separator
printf "%b|" "$IBlack"
for ((j=0; j<ncols; ++j)); do
printf " %s |" "$(printf '%*s' "${col_widths[j]}" "" | tr ' ' '-')"
done
printf "%b\n" "$RESETCOL"
# Print data rows
for ((i=1; i<${#table_rows[@]}; ++i)); do
local -a row=()
IFS="$sep" read -r -a row <<< "${table_rows[i]}"
printf "%b|" "$IBlack"
for ((j=0; j<ncols; ++j)); do
local raw_cell="${row[j]:-}"
local styled_cell
styled_cell=$(_mdcat_style_inline "$raw_cell")
local visible
visible=$(printf '%b' "$styled_cell" | sed -E 's/\x1B\[[0-9;]*[mK]//g')
local pad=$((col_widths[j] - ${#visible}))
(( pad < 0 )) && pad=0
printf " %b%b%*s%b |" "$RESETCOL" "$styled_cell" "$pad" "" "$IBlack"
done
printf "%b\n" "$RESETCOL"
done
# Draw bottom border
printf "%b\n" "$IBlack$border$RESETCOL"
}
local PARSED
PARSED=$(getopt -o h --long help -n 'mdcat' -- "$@")
# shellcheck disable=SC2181 # getopt return code is checked immediately after
if [[ $? -ne 0 ]]; then
disp E "Invalid options, use \"mdcat --help\" to display usage."
return 1
fi
eval set -- "$PARSED"
while true; do
case "$1" in
-h|--help)
printf "mdcat: Render a Markdown file with terminal formatting.\n"
printf "Usage: mdcat [file]\n"
printf "If no file is provided, mdcat reads from standard input.\n"
return 0
;;
--)
shift
break
;;
*)
disp E "Invalid option, use \"mdcat --help\" to display options list"
return 1
;;
esac
done
if [[ $# -gt 1 ]]; then
disp E "Too many arguments. Usage: mdcat [file]"
return 1
fi
local input_file=""
if [[ $# -eq 1 ]]; then
input_file="$1"
if [[ ! -f "$input_file" ]]; then
disp E "File not found: $input_file"
return 1
fi
if [[ ! -r "$input_file" ]]; then
disp E "File is not readable: $input_file"
return 1
fi
elif [[ -t 0 ]]; then
disp E "No input provided. Usage: mdcat [file] or: cat file.md | mdcat"
return 1
fi
local in_code=0 code_lang="" raw line
local -a code_lines=()
local in_table=0
local -a table_lines=()
while IFS= read -r raw || [[ -n "$raw" ]]; do
line="${raw%$'\r'}"
# Table detection: line with |, next line with | and ---
if [[ $in_table -eq 0 && "$line" =~ ^[[:space:]]*\|.*\|[[:space:]]*$ ]]; then
local next
IFS= read -r next || true
# Accept: | --- | --- | or |:---|---:| etc.
if [[ "$next" =~ ^[[:space:]]*\|[[:space:]]*:?[-]+:?([[:space:]]*\|[[:space:]]*:?[ -]+:?)*\|[[:space:]]*$ ]]; then
in_table=1
table_lines=("$line" "$next")
continue
fi
fi
if [[ $in_table -eq 1 ]]; then
# Accept table row if it starts and ends with |
if [[ "$line" =~ ^[[:space:]]*\|.*\|[[:space:]]*$ && ! "$line" =~ ^[[:space:]]*\|[[:space:]]*:?[-]+:?([[:space:]]*\|[[:space:]]*:?[ -]+:?)*\|[[:space:]]*$ ]]; then
table_lines+=("$line")
continue
else
_mdcat_print_table "${table_lines[@]}"
in_table=0
table_lines=()
fi
fi
if [[ $in_code -eq 1 ]]; then
if [[ "$line" =~ ^\`\`\` ]]; then
_mdcat_print_code_block "$code_lang" "${code_lines[@]}"
in_code=0
code_lang=""
code_lines=()
else
code_lines+=("$line")
fi
continue
fi
if [[ "$line" =~ ^\`\`\`[[:space:]]*([^[:space:]]*) ]]; then
in_code=1
code_lang="${BASH_REMATCH[1]}"
code_lines=()
continue
fi
if [[ "$line" =~ ^(#{1,6})[[:space:]]+(.*)$ ]]; then
local lvl=${#BASH_REMATCH[1]}
local title="${BASH_REMATCH[2]}"
local h_on=""
if [[ -z $NO_COLOR ]]; then
case "$lvl" in
1) h_on="$BBlue" ;;
2) h_on="$BCyan" ;;
3) h_on="$BGreen" ;;
*) h_on="$BIWhite" ;;
esac
fi
printf "%b%s%b\n" "$h_on" "$title" "$RESETCOL"
continue
fi
if [[ "$line" =~ ^[[:space:]]*([\-*_])[[:space:]]*\1[[:space:]]*\1[\-*_[:space:]]*$ ]]; then
_mdcat_print_hr
continue
fi
if [[ "$line" =~ ^([[:space:]]*)\>[[:space:]]?(.*)$ ]]; then
local quote="${BASH_REMATCH[2]}"
quote=$(_mdcat_style_inline "$quote")
if [[ -z $NO_COLOR ]]; then
printf "%s%b|%b %b\n" "${BASH_REMATCH[1]}" "$ICyan" "$RESETCOL" "$quote"
else
printf "%s| %b\n" "${BASH_REMATCH[1]}" "$quote"
fi
continue
fi
if [[ "$line" =~ ^([[:space:]]*)[-+*][[:space:]]+(.*)$ ]]; then
local item="${BASH_REMATCH[2]}"
item=$(_mdcat_style_inline "$item")
if [[ -z $NO_COLOR ]]; then
printf "%s%b*%b %b\n" "${BASH_REMATCH[1]}" "$IGreen" "$RESETCOL" "$item"
else
printf "%s* %b\n" "${BASH_REMATCH[1]}" "$item"
fi
continue
fi
if [[ "$line" =~ ^([[:space:]]*)([0-9]+)\.[[:space:]]+(.*)$ ]]; then
local nitem="${BASH_REMATCH[3]}"
nitem=$(_mdcat_style_inline "$nitem")
if [[ -z $NO_COLOR ]]; then
printf "%s%b%s.%b %b\n" "${BASH_REMATCH[1]}" "$IGreen" "${BASH_REMATCH[2]}" "$RESETCOL" "$nitem"
else
printf "%s%s. %b\n" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "$nitem"
fi
continue
fi
printf "%b\n" "$(_mdcat_style_inline "$line")"
done < "${input_file:-/dev/stdin}"
if [[ $in_code -eq 1 ]]; then
_mdcat_print_code_block "$code_lang" "${code_lines[@]}"
fi
}
export -f mdcat
# ------------------------------------------------------------------------------
# Load disp section variables
load_conf disp
set_colors

View File

@@ -79,6 +79,7 @@ help()
printf "ku\t\tKill all processes owned by the given user name or ID\n"
printf "matrix\t\tConsole screensaver with Matrix-style digital rain (binary, kana, ascii charset)\n"
printf "mcd\t\tCreate a directory and immediately move into it\n"
printf "mdcat\t\tRender Markdown files in terminal with colors and code frames\n"
printf "meteo\t\tDisplay weather forecast for the configured or given city\n"
printf "myextip\t\tGet information about your public IP address\n"
printf "pkgs\t\tSearch for a pattern in installed package names (dpkg/rpm, supports -i)\n"
@@ -102,7 +103,7 @@ help()
printf "utaz\t\tSmartly uncompress archives (zip, tar.gz/bz2/xz/lz, rar, arj, lha, ace, 7z, zst, cpio, cab, deb, rpm)\n"
printf "ver\t\tDisplay the installed profile version\n\n"
printf "\nPlease use <command> --help to obtain usage details.\n"
printf "\nPlease use <command> --help or help <command> to obtain usage details.\n"
}
export -f help
# ------------------------------------------------------------------------------