#!/usr/bin/env bash set -euo pipefail # Script Name: clip # Description: Smart clipboard manager with history, search, and categories # Usage: clip # Show history with fzf search # clip pin # Pin current clipboard item # clip cat 5 # Show 5th history item # clip search "192" # Search history # clip clear # Clear history VERSION="1.0.0" CLIP_DIR="$HOME/.clipboard" HISTORY_FILE="$CLIP_DIR/history.txt" PINS_FILE="$CLIP_DIR/pins.txt" MAX_HISTORY=100 # Colors readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly RED='\033[0;31m' readonly CYAN='\033[0;36m' readonly MAGENTA='\033[0;35m' readonly BOLD='\033[1m' readonly NC='\033[0m' # Initialize clip directory init_clip() { if [[ ! -d "$CLIP_DIR" ]]; then mkdir -p "$CLIP_DIR" touch "$HISTORY_FILE" touch "$PINS_FILE" fi } show_help() { echo -e "${BOLD}clip${NC} - Smart Clipboard Manager v${VERSION}" echo echo -e "${BOLD}USAGE:${NC}" echo " clip [COMMAND]" echo echo -e "${BOLD}COMMANDS:${NC}" echo -e " ${CYAN}(no args)${NC} Show history with fzf search" echo -e " ${CYAN}pin${NC} Pin current clipboard item" echo -e " ${CYAN}pins${NC} Show pinned items" echo -e " ${CYAN}cat N${NC} Show Nth history item" echo -e " ${CYAN}search TERM${NC} Search history" echo -e " ${CYAN}list${NC} List recent history (last 20)" echo -e " ${CYAN}save${NC} Save current clipboard to history" echo -e " ${CYAN}clear${NC} Clear history" echo -e " ${CYAN}delete N${NC} Delete Nth history item" echo -e " ${CYAN}unpin N${NC} Delete Nth pinned item" echo -e " ${CYAN}stats${NC} Show statistics" echo -e " ${CYAN}-h, --help${NC} Show this help" echo echo -e "${BOLD}EXAMPLES:${NC}" echo " clip # Interactive search" echo " clip pin # Pin important item" echo " clip search \"192.168\" # Find IP addresses" echo " clip cat 1 # Show most recent" echo " clip delete 5 # Delete 5th item from history" echo " clip unpin 1 # Delete 1st pinned item" echo echo -e "${BOLD}FEATURES:${NC}" echo " - Automatic history (monitors clipboard)" echo " - Pattern detection (URLs, IPs, hashes)" echo " - Security: Auto-expire sensitive data" echo " - Pin important items" echo " - fzf integration for search" echo echo -e "${BOLD}NOTES:${NC}" echo " History: $HISTORY_FILE" echo " Pins: $PINS_FILE" echo " Max history: $MAX_HISTORY items" } # Detect content type detect_type() { local content="$1" # URL if [[ "$content" =~ ^https?:// ]]; then echo "url" # IPv4 elif [[ "$content" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} ]]; then echo "ip" # Hash (32 or 40 or 64 hex chars) elif [[ "$content" =~ ^[a-f0-9]{32}$ ]] || [[ "$content" =~ ^[a-f0-9]{40}$ ]] || [[ "$content" =~ ^[a-f0-9]{64}$ ]]; then echo "hash" # Code (contains common code markers) elif [[ "$content" =~ (function|const|var|class|def|import|export) ]]; then echo "code" # Credential patterns (don't save these!) elif [[ "$content" =~ (password|secret|token|key|bearer|api[_-]?key) ]]; then echo "credential" else echo "text" fi } # Save item to history save_to_history() { local content="$1" local type=$(detect_type "$content") # Don't save credentials if [[ "$type" == "credential" ]]; then echo -e "${YELLOW}⚠${NC} Sensitive data detected - not saved to history" >&2 return 0 fi # Don't save empty if [[ -z "$content" ]]; then return 0 fi # Don't save if it's already the most recent if [[ -f "$HISTORY_FILE" ]]; then last_entry=$(tail -1 "$HISTORY_FILE" | cut -d'|' -f3-) if [[ "$last_entry" == "$content" ]]; then return 0 fi fi # Save: timestamp|type|content timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "$timestamp|$type|$content" >> "$HISTORY_FILE" # Trim history to max size if [[ $(wc -l < "$HISTORY_FILE") -gt $MAX_HISTORY ]]; then tail -n $MAX_HISTORY "$HISTORY_FILE" > "$HISTORY_FILE.tmp" mv "$HISTORY_FILE.tmp" "$HISTORY_FILE" fi } # Get current clipboard get_clipboard() { if command -v xsel &>/dev/null; then xsel --output --clipboard elif command -v xclip &>/dev/null; then xclip -selection clipboard -o else echo "Error: No clipboard tool found" >&2 return 1 fi } # Set clipboard set_clipboard() { if command -v xsel &>/dev/null; then echo -n "$1" | xsel --input --clipboard elif command -v xclip &>/dev/null; then echo -n "$1" | xclip -selection clipboard else echo "Error: No clipboard tool found" >&2 return 1 fi } # Interactive history browser with fzf show_history_fzf() { if ! command -v fzf &>/dev/null; then echo -e "${RED}Error:${NC} fzf not found" >&2 echo "Install it with: sudo apt install fzf" >&2 exit 1 fi if [[ ! -f "$HISTORY_FILE" ]] || [[ ! -s "$HISTORY_FILE" ]]; then echo -e "${YELLOW}No history yet${NC}" >&2 exit 0 fi # Format for fzf: index | time | type | preview selected=$(tac "$HISTORY_FILE" | awk -F'|' ' { idx = NR time = $1 type = $2 content = $3 for (i=4; i<=NF; i++) content = content "|" $i # Truncate preview preview = content if (length(preview) > 60) { preview = substr(preview, 1, 60) "..." } printf "%3d | %s | %-10s | %s\n", idx, time, "[" type "]", preview } ' | fzf --height=60% --layout=reverse \ --header="Select item to copy (ESC to cancel)" \ --preview='echo {}' \ --preview-window=up:3:wrap) if [[ -n "$selected" ]]; then # Extract the full content index=$(echo "$selected" | awk '{print $1}') full_content=$(tac "$HISTORY_FILE" | sed -n "${index}p" | cut -d'|' -f3-) # Copy to clipboard set_clipboard "$full_content" echo -e "${GREEN}✓${NC} Copied to clipboard" fi } # Pin current clipboard pin_item() { local content=$(get_clipboard) if [[ -z "$content" ]]; then echo -e "${RED}Error:${NC} Clipboard is empty" >&2 exit 1 fi type=$(detect_type "$content") timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "$timestamp|$type|$content" >> "$PINS_FILE" echo -e "${GREEN}✓${NC} Pinned item (type: $type)" } # Show pins show_pins() { if [[ ! -f "$PINS_FILE" ]] || [[ ! -s "$PINS_FILE" ]]; then echo -e "${YELLOW}No pinned items${NC}" exit 0 fi echo -e "${BOLD}${CYAN}Pinned Items:${NC}" echo if command -v fzf &>/dev/null; then # Interactive selection selected=$(cat "$PINS_FILE" | awk -F'|' ' { idx = NR time = $1 type = $2 content = $3 for (i=4; i<=NF; i++) content = content "|" $i preview = content if (length(preview) > 60) { preview = substr(preview, 1, 60) "..." } printf "%3d | %s | %-10s | %s\n", idx, time, "[" type "]", preview } ' | fzf --height=60% --layout=reverse \ --header="Select pinned item to copy (ESC to cancel)" \ --preview='echo {}') if [[ -n "$selected" ]]; then index=$(echo "$selected" | awk '{print $1}') full_content=$(sed -n "${index}p" "$PINS_FILE" | cut -d'|' -f3-) set_clipboard "$full_content" echo -e "${GREEN}✓${NC} Copied pinned item to clipboard" fi else # Just list them cat "$PINS_FILE" | awk -F'|' ' { idx = NR time = $1 type = $2 content = $3 for (i=4; i<=NF; i++) content = content "|" $i preview = content if (length(preview) > 60) { preview = substr(preview, 1, 60) "..." } printf "%3d | %s | %-10s | %s\n", idx, time, "[" type "]", preview } ' fi } # Show specific item show_item() { local index=$1 if [[ ! -f "$HISTORY_FILE" ]]; then echo -e "${RED}Error:${NC} No history" >&2 exit 1 fi content=$(tac "$HISTORY_FILE" | sed -n "${index}p" | cut -d'|' -f3-) if [[ -z "$content" ]]; then echo -e "${RED}Error:${NC} No item at index $index" >&2 exit 1 fi echo "$content" } # Search history search_history() { local query="$1" if [[ ! -f "$HISTORY_FILE" ]]; then echo -e "${RED}Error:${NC} No history" >&2 exit 1 fi echo -e "${BOLD}${CYAN}Search results for: ${query}${NC}" echo grep -i "$query" "$HISTORY_FILE" | awk -F'|' ' { time = $1 type = $2 content = $3 for (i=4; i<=NF; i++) content = content "|" $i preview = content if (length(preview) > 60) { preview = substr(preview, 1, 60) "..." } printf "%s | %-10s | %s\n", time, "[" type "]", preview } ' } # List recent items list_recent() { if [[ ! -f "$HISTORY_FILE" ]]; then echo -e "${YELLOW}No history${NC}" exit 0 fi echo -e "${BOLD}${CYAN}Recent Clipboard History (last 20):${NC}" echo tail -20 "$HISTORY_FILE" | tac | awk -F'|' ' { idx = NR time = $1 type = $2 content = $3 for (i=4; i<=NF; i++) content = content "|" $i preview = content if (length(preview) > 60) { preview = substr(preview, 1, 60) "..." } printf "%3d | %s | %-10s | %s\n", idx, time, "[" type "]", preview } ' } # Statistics show_stats() { if [[ ! -f "$HISTORY_FILE" ]]; then echo -e "${YELLOW}No history${NC}" exit 0 fi total=$(wc -l < "$HISTORY_FILE") pins=$(wc -l < "$PINS_FILE" 2>/dev/null || echo 0) echo -e "${BOLD}${CYAN}Clipboard Statistics:${NC}" echo echo " Total items: $total" echo " Pinned items: $pins" echo " Max history: $MAX_HISTORY" echo echo -e "${BOLD}${CYAN}By Type:${NC}" awk -F'|' '{print $2}' "$HISTORY_FILE" | sort | uniq -c | sort -rn | awk ' {printf " %-15s %d\n", $2, $1}' } # Delete specific item from history delete_item() { local index=$1 if [[ ! -f "$HISTORY_FILE" ]]; then echo -e "${RED}Error:${NC} No history" >&2 exit 1 fi total=$(wc -l < "$HISTORY_FILE") if [[ $index -lt 1 ]] || [[ $index -gt $total ]]; then echo -e "${RED}Error:${NC} Invalid index (1-$total)" >&2 exit 1 fi # Show what we're deleting content=$(tac "$HISTORY_FILE" | sed -n "${index}p" | cut -d'|' -f3-) preview="${content:0:60}" if [[ ${#content} -gt 60 ]]; then preview="${preview}..." fi echo -e "${YELLOW}Delete item $index:${NC} $preview" echo -n "Continue? (y/N) " read -r response if [[ "$response" =~ ^[Yy]$ ]]; then # Delete line (remember tac reverses order) actual_line=$((total - index + 1)) sed -i "${actual_line}d" "$HISTORY_FILE" echo -e "${GREEN}✓${NC} Deleted item $index" else echo "Cancelled" fi } # Delete pinned item delete_pin() { local index=$1 if [[ ! -f "$PINS_FILE" ]] || [[ ! -s "$PINS_FILE" ]]; then echo -e "${YELLOW}No pinned items${NC}" exit 0 fi total=$(wc -l < "$PINS_FILE") if [[ $index -lt 1 ]] || [[ $index -gt $total ]]; then echo -e "${RED}Error:${NC} Invalid index (1-$total)" >&2 exit 1 fi # Show what we're deleting content=$(sed -n "${index}p" "$PINS_FILE" | cut -d'|' -f3-) preview="${content:0:60}" if [[ ${#content} -gt 60 ]]; then preview="${preview}..." fi echo -e "${YELLOW}Unpin item $index:${NC} $preview" echo -n "Continue? (y/N) " read -r response if [[ "$response" =~ ^[Yy]$ ]]; then sed -i "${index}d" "$PINS_FILE" echo -e "${GREEN}✓${NC} Unpinned item $index" else echo "Cancelled" fi } # Clear history clear_history() { echo -n "Clear clipboard history? (y/N) " read -r response if [[ "$response" =~ ^[Yy]$ ]]; then > "$HISTORY_FILE" echo -e "${GREEN}✓${NC} History cleared" else echo "Cancelled" fi } # Initialize init_clip # Parse command if [[ $# -eq 0 ]]; then show_history_fzf exit 0 fi case $1 in -h|--help|help) show_help ;; save) content=$(get_clipboard) save_to_history "$content" echo -e "${GREEN}✓${NC} Saved to history" ;; pin) pin_item ;; pins) show_pins ;; cat) if [[ $# -lt 2 ]]; then echo -e "${RED}Error:${NC} Item index required" >&2 exit 1 fi show_item "$2" ;; search|s) if [[ $# -lt 2 ]]; then echo -e "${RED}Error:${NC} Search query required" >&2 exit 1 fi shift search_history "$*" ;; list|ls|l) list_recent ;; delete|del|rm) if [[ $# -lt 2 ]]; then echo -e "${RED}Error:${NC} Item index required" >&2 exit 1 fi delete_item "$2" ;; unpin) if [[ $# -lt 2 ]]; then echo -e "${RED}Error:${NC} Item index required" >&2 exit 1 fi delete_pin "$2" ;; clear) clear_history ;; stats) show_stats ;; *) echo -e "${RED}Error:${NC} Unknown command: $1" >&2 echo "Run 'clip --help' for usage" >&2 exit 1 ;; esac