#!/usr/bin/env bash
set -o pipefail
# ============================================================
# Hastebin / pste.us uploader
#
# Dependencies:
# curl jq gum fzf qrencode
#
# Optional:
# wl-copy / xclip / xsel / pbcopy
# xdg-open / open
#
# Examples:
# ./haste
# ./haste file.txt
# ./haste file.txt --open
# ./haste file.txt --no-qr
# ./haste file.txt --quiet
# ./haste --history
# ============================================================
# -----------------------------
# Configuration
# -----------------------------
UPLOAD_URL="https://pste.us/documents"
BASE_URL="https://pste.us"
HISTORY_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/haste"
HISTORY_FILE="$HISTORY_DIR/history"
# Colors
PINK="#f5c2e7"
BLUE="#89b4fa"
GREEN="#a6e3a1"
RED="#f38ba8"
YELLOW="#f9e2af"
CYAN="#89dceb"
TEXT="#cdd6f4"
MAUVE="#cba6f7"
# Temporary files
RESPONSE_FILE=""
PROGRESS_FILE=""
# -----------------------------
# Cleanup
# -----------------------------
cleanup() {
[[ -n "$RESPONSE_FILE" && -f "$RESPONSE_FILE" ]] && rm -f "$RESPONSE_FILE"
[[ -n "$PROGRESS_FILE" && -f "$PROGRESS_FILE" ]] && rm -f "$PROGRESS_FILE"
}
trap cleanup EXIT
trap 'exit 130' INT TERM
# -----------------------------
# Helpers
# -----------------------------
die() {
gum style \
--foreground "$RED" \
--border double \
--border-foreground "$RED" \
--padding "1 2" \
"✗ $1"
exit 1
}
info() {
gum style \
--foreground "$CYAN" \
--padding "0 1" \
"$1"
}
success() {
gum style \
--foreground "$GREEN" \
--padding "0 1" \
"✓ $1"
}
warning() {
gum style \
--foreground "$YELLOW" \
--padding "0 1" \
"⚠ $1"
}
# -----------------------------
# Dependency check
# -----------------------------
REQUIRED_COMMANDS=(
curl
jq
gum
fzf
)
for cmd in "${REQUIRED_COMMANDS[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
die "Required command not found: $cmd"
fi
done
# -----------------------------
# Arguments
# -----------------------------
SHOW_HISTORY=false
OPEN_AFTER=false
SHOW_QR=true
QUIET=false
FILE_PATH=""
while [[ $# -gt 0 ]]; do
case "$1" in
--history|-H)
SHOW_HISTORY=true
shift
;;
--open|-o)
OPEN_AFTER=true
shift
;;
--no-qr)
SHOW_QR=false
shift
;;
--quiet|-q)
QUIET=true
shift
;;
--help|-h)
cat <<EOF
Usage:
haste [FILE]
haste [OPTIONS] [FILE]
Options:
-H, --history Show upload history
-o, --open Open URL in browser
-q, --quiet Print only the URL
--no-qr Don't display QR code
-h, --help Show this help
Examples:
haste
haste file.txt
haste file.txt --open
haste file.txt --no-qr
haste file.txt --quiet
haste --history
EOF
exit 0
;;
-*)
die "Unknown option: $1"
;;
*)
FILE_PATH="$1"
shift
;;
esac
done
# -----------------------------
# History
# -----------------------------
show_history() {
if [[ ! -f "$HISTORY_FILE" || ! -s "$HISTORY_FILE" ]]; then
warning "No upload history yet."
exit 0
fi
echo
gum style \
--foreground "$MAUVE" \
--border double \
--border-foreground "$MAUVE" \
--align center \
--width 70 \
--padding "0 2" \
"UPLOAD HISTORY"
echo
# Format:
# timestamp|filename|url
HISTORY_SELECTION=$(
tac "$HISTORY_FILE" |
awk -F'|' '{
printf "%s │ %s │ %s\n", $2, $3, $1
}' |
gum choose \
--height 15 \
--cursor "➜ " \
--header "Select an upload"
)
if [[ -z "$HISTORY_SELECTION" ]]; then
exit 0
fi
SELECTED_URL=$(printf '%s\n' "$HISTORY_SELECTION" | awk -F' │ ' '{print $2}')
echo
echo -e "\033[36m$SELECTED_URL\033[0m"
echo
if gum confirm "Copy this URL to clipboard?"; then
if command -v wl-copy &>/dev/null; then
printf '%s' "$SELECTED_URL" | wl-copy
success "Copied to clipboard."
elif command -v xclip &>/dev/null; then
printf '%s' "$SELECTED_URL" | xclip -selection clipboard
success "Copied to clipboard."
elif command -v xsel &>/dev/null; then
printf '%s' "$SELECTED_URL" | xsel --clipboard --input
success "Copied to clipboard."
elif command -v pbcopy &>/dev/null; then
printf '%s' "$SELECTED_URL" | pbcopy
success "Copied to clipboard."
else
warning "No clipboard utility found."
fi
fi
if gum confirm "Open this URL in browser?"; then
if command -v xdg-open &>/dev/null; then
xdg-open "$SELECTED_URL" &>/dev/null &
elif command -v open &>/dev/null; then
open "$SELECTED_URL" &>/dev/null &
else
warning "No browser opener found."
fi
fi
exit 0
}
if [[ "$SHOW_HISTORY" == true ]]; then
show_history
fi
# -----------------------------
# File selection
# -----------------------------
if [[ -z "$FILE_PATH" ]]; then
FILE_PATH=$(
find . -maxdepth 1 -type f -printf '%f\n' |
fzf \
--prompt=" Select file to upload: " \
--border \
--reverse \
--header="Use ↑/↓ to select, <Enter> to confirm" \
--pointer="➤" \
--color="bg+:#1e1e2e,bg:#181825,spinner:#f5c2e7,hl:#f38ba8,fg:#cdd6f4,header:#89b4fa,info:#f9e2af,pointer:#a6e3a1,marker:#f5c2e7,fg+:#cdd6f4,prompt:#89b4fa,hl+:#f38a8a" \
--preview='bat --style=numbers --color=always {}'
)
if [[ -z "$FILE_PATH" ]]; then
warning "No file selected."
exit 1
fi
fi
# -----------------------------
# Validate file
# -----------------------------
if [[ ! -f "$FILE_PATH" ]]; then
die "File does not exist: $FILE_PATH"
fi
if [[ ! -r "$FILE_PATH" ]]; then
die "File is not readable: $FILE_PATH"
fi
# -----------------------------
# File information
# -----------------------------
FILE_NAME="$(basename "$FILE_PATH")"
if command -v numfmt &>/dev/null; then
FILE_BYTES=$(stat -c%s "$FILE_PATH" 2>/dev/null || stat -f%z "$FILE_PATH")
FILE_SIZE=$(numfmt --to=iec --suffix=B "$FILE_BYTES" 2>/dev/null || echo "${FILE_BYTES} B")
else
FILE_SIZE=$(du -h "$FILE_PATH" | cut -f1)
fi
FILE_TYPE=$(file -b "$FILE_PATH" 2>/dev/null || echo "Unknown")
if [[ -r "$FILE_PATH" ]]; then
LINE_COUNT=$(wc -l < "$FILE_PATH" 2>/dev/null || echo "N/A")
else
LINE_COUNT="N/A"
fi
# -----------------------------
# Quiet mode
# -----------------------------
if [[ "$QUIET" != true ]]; then
gum style \
--foreground "$BLUE" \
--border double \
--border-foreground "$BLUE" \
--align center \
--width 64 \
--padding "1 2" \
"HASTEBIN UPLOADER" \
"" \
"File: $FILE_NAME"
echo
gum style \
--foreground "$TEXT" \
--border rounded \
--border-foreground "$MAUVE" \
--padding "1 2" \
"File : $FILE_NAME" \
"Size : $FILE_SIZE" \
"Type : $FILE_TYPE" \
"Lines : $LINE_COUNT"
echo
fi
# -----------------------------
# Prepare temporary files
# -----------------------------
RESPONSE_FILE=$(mktemp)
PROGRESS_FILE=$(mktemp)
# -----------------------------
# Upload
# -----------------------------
if [[ "$QUIET" == true ]]; then
if ! curl -fsS \
-X POST \
-H "Content-Type: text/plain" \
--data-binary @"$FILE_PATH" \
"$UPLOAD_URL" \
>"$RESPONSE_FILE"; then
die "Failed to upload the document."
fi
else
echo
gum style \
--foreground "$CYAN" \
--padding "0 1" \
"↑ Uploading $FILE_NAME"
echo
# Start curl in the background.
# Its progress output is written to PROGRESS_FILE.
curl \
-fS \
--progress-bar \
-X POST \
-H "Content-Type: text/plain" \
--data-binary @"$FILE_PATH" \
"$UPLOAD_URL" \
>"$RESPONSE_FILE" \
2>"$PROGRESS_FILE" &
CURL_PID=$!
LAST_PERCENT=-1
while kill -0 "$CURL_PID" 2>/dev/null; do
# curl writes percentage information using carriage returns.
# Extract the most recent percentage we can find.
CURRENT_PERCENT=$(
tr '\r' '\n' < "$PROGRESS_FILE" 2>/dev/null |
grep -oE '[0-9]{1,3}(\.[0-9]+)?%' |
tail -1 |
tr -d '%' |
cut -d. -f1
)
if [[ -z "$CURRENT_PERCENT" ]]; then
CURRENT_PERCENT=0
fi
if (( CURRENT_PERCENT < 0 )); then
CURRENT_PERCENT=0
fi
if (( CURRENT_PERCENT > 100 )); then
CURRENT_PERCENT=100
fi
if [[ "$CURRENT_PERCENT" != "$LAST_PERCENT" ]]; then
BAR_WIDTH=40
FILLED=$(( CURRENT_PERCENT * BAR_WIDTH / 100 ))
EMPTY=$(( BAR_WIDTH - FILLED ))
FILLED_BAR=$(printf '%*s' "$FILLED" '' | tr ' ' '█')
EMPTY_BAR=$(printf '%*s' "$EMPTY" '' | tr ' ' '░')
PROGRESS_LINE="$FILLED_BAR$EMPTY_BAR"
# Move cursor to beginning of current line and redraw.
printf '\r'
gum style \
--foreground "$GREEN" \
--padding "0 1" \
"[$PROGRESS_LINE] ${CURRENT_PERCENT}%"
LAST_PERCENT="$CURRENT_PERCENT"
fi
sleep 0.1
done
wait "$CURL_PID"
CURL_STATUS=$?
echo
if [[ "$CURL_STATUS" -ne 0 ]]; then
echo
die "Upload failed."
fi
gum spin \
--spinner points \
--spinner.foreground "$MAUVE" \
--title "Processing server response..." \
-- sleep 0.4
fi
# -----------------------------
# Parse response
# -----------------------------
HASTE_KEY=$(jq -r '.key // empty' "$RESPONSE_FILE" 2>/dev/null)
if [[ -z "$HASTE_KEY" ]]; then
die "Server returned an invalid response."
fi
URL="$BASE_URL/$HASTE_KEY"
# -----------------------------
# Clipboard
# -----------------------------
COPIED=false
if command -v wl-copy &>/dev/null; then
printf '%s' "$URL" | wl-copy
COPIED=true
elif command -v xclip &>/dev/null; then
printf '%s' "$URL" | xclip -selection clipboard
COPIED=true
elif command -v xsel &>/dev/null; then
printf '%s' "$URL" | xsel --clipboard --input
COPIED=true
elif command -v pbcopy &>/dev/null; then
printf '%s' "$URL" | pbcopy
COPIED=true
fi
# -----------------------------
# Save history
# -----------------------------
mkdir -p "$HISTORY_DIR"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
printf '%s|%s|%s\n' \
"$TIMESTAMP" \
"$FILE_NAME" \
"$URL" >> "$HISTORY_FILE"
# -----------------------------
# Quiet output
# -----------------------------
if [[ "$QUIET" == true ]]; then
printf '%s\n' "$URL"
exit 0
fi
# -----------------------------
# QR code
# -----------------------------
QR_CODE=""
if [[ "$SHOW_QR" == true ]] && command -v qrencode &>/dev/null; then
QR_CODE=$(
qrencode \
-t ANSIUTF8 \
-m 1 \
"$URL"
)
fi
# -----------------------------
# Build result panel
# -----------------------------
STATUS_TEXT="✓ Copied to clipboard"
if [[ "$COPIED" != true ]]; then
STATUS_TEXT="⚠ Clipboard utility not found"
fi
RESULT_TEXT="File : $FILE_NAME
Size : $FILE_SIZE
Type : $FILE_TYPE
URL : $URL
"
if [[ "$COPIED" == true ]]; then
RESULT_TEXT+="
$STATUS_TEXT"
else
RESULT_TEXT+="
$STATUS_TEXT"
fi
# -----------------------------
# Result
# -----------------------------
echo
gum style \
--foreground "$GREEN" \
--border double \
--border-foreground "$GREEN" \
--padding "1 2" \
--width 64 \
"Upload Complete" \
"" \
"$RESULT_TEXT"
# -----------------------------
# QR
# -----------------------------
if [[ "$SHOW_QR" == true ]]; then
echo
if [[ -n "$QR_CODE" ]]; then
QR_PANEL=$(
gum style \
--foreground "$TEXT" \
--border rounded \
--border-foreground "$MAUVE" \
--padding "1 2" \
--align center \
"Scan QR Code" \
"" \
"$QR_CODE"
)
printf '%s\n' "$QR_PANEL"
else
warning "qrencode is not installed; QR code skipped."
gum style \
--foreground "$YELLOW" \
--padding "0 1" \
"Install it with: sudo pacman -S qrencode"
fi
fi
# -----------------------------
# Actions
# -----------------------------
echo
ACTION=$(
gum choose \
--cursor "➜ " \
--header "What would you like to do?" \
"Open in browser" \
"Copy URL again" \
"Show URL" \
"Done"
)
case "$ACTION" in
"Open in browser")
if command -v xdg-open &>/dev/null; then
xdg-open "$URL" &>/dev/null &
success "Opened in browser."
elif command -v open &>/dev/null; then
open "$URL" &>/dev/null &
success "Opened in browser."
else
warning "No browser opener found."
fi
;;
"Copy URL again")
if command -v wl-copy &>/dev/null; then
printf '%s' "$URL" | wl-copy
success "Copied to clipboard."
elif command -v xclip &>/dev/null; then
printf '%s' "$URL" | xclip -selection clipboard
success "Copied to clipboard."
elif command -v xsel &>/dev/null; then
printf '%s' "$URL" | xsel --clipboard --input
success "Copied to clipboard."
elif command -v pbcopy &>/dev/null; then
printf '%s' "$URL" | pbcopy
success "Copied to clipboard."
else
warning "No clipboard utility found."
fi
;;
"Show URL")
echo
gum style \
--foreground "$GREEN" \
--border rounded \
--border-foreground "$GREEN" \
--padding "1 2" \
"$URL"
;;
"Done"|"")
;;
esac
echo