Skip to content

Instantly share code, notes, and snippets.

@CipherLab
Last active May 12, 2025 18:51
Show Gist options
  • Save CipherLab/31c87b1f41e791d1f3f9 to your computer and use it in GitHub Desktop.
Save CipherLab/31c87b1f41e791d1f3f9 to your computer and use it in GitHub Desktop.
General useful functions
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
public class General
{
#region colorization code
protected Color HexStringToColor(string hex)
{
hex = hex.Replace("#", "");
if (hex.Length != 6)
throw new Exception(hex +
" is not a valid 6-place hexadecimal color code.");
string r, g, b;
r = hex.Substring(0, 2);
g = hex.Substring(2, 2);
b = hex.Substring(4, 2);
return System.Drawing.Color.FromArgb(HexStringToBase10Int(r),
HexStringToBase10Int(g),
HexStringToBase10Int(b));
}
protected int HexStringToBase10Int(string hex)
{
int base10value = 0;
try { base10value = System.Convert.ToInt32(hex, 16); }
catch { base10value = 0; }
return base10value;
}
static char[] hexDigits = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
public static string ColorToHexString(Color color)
{
byte[] bytes = new byte[3];
bytes[0] = color.R;
bytes[1] = color.G;
bytes[2] = color.B;
char[] chars = new char[bytes.Length * 2];
for (int i = 0; i < bytes.Length; i++)
{
int b = bytes[i];
chars[i * 2] = hexDigits[b >> 4];
chars[i * 2 + 1] = hexDigits[b & 0xF];
}
return new string(chars);
}
#endregion
private void DeleteFile(string tempFile)
{
try
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
catch { }
}
public static byte[] StringToByteArray(string str)
{
System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
return encoding.GetBytes(str);
}
public static string ImageToBase64(Image image, ImageFormat format)
{
using (MemoryStream ms = new MemoryStream())
{
// Convert Image to byte[]
image.Save(ms, format);
byte[] imageBytes = ms.ToArray();
// Convert byte[] to Base64 String
string base64String = Convert.ToBase64String(imageBytes);
return base64String;
}
}
public static string ByteArrayToString(byte[] input)
{
UTF8Encoding enc = new UTF8Encoding();
string str = enc.GetString(input);
return str;
}
public static Image getImageFromURL(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
url = "http://i.imgur.com/zxBgpGu.png";
}
var webClient = new WebClient();
byte[] imageBytes = webClient.DownloadData(url);
return byteArrayToImage(imageBytes);
}
public static Image byteArrayToImage(byte[] byteArrayIn)
{
try
{
MemoryStream ms = new MemoryStream(byteArrayIn, 0, byteArrayIn.Length);
ms.Write(byteArrayIn, 0, byteArrayIn.Length);
return Image.FromStream(ms, true);
}
catch
{
}
return null;
}
public static string CsvToJson(string value)
{
// Get lines.
if (value == null) return null;
string[] lines = value.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
if (lines.Length < 2) throw new InvalidDataException("Must have header line.");
// Get headers.
string[] headers = lines.First().Split(',');
// Build JSON array.
StringBuilder sb = new StringBuilder();
sb.AppendLine("[");
for (int i = 1; i < lines.Length; i++)
{
//string[] fields = lines[i].Split(',');
List<string> fields = new List<string>();
var regex = new Regex("(?<=^|,)(\"(?:[^\"]|\"\")*\"|[^,]*)");
foreach (Match m in regex.Matches(lines[i]))
{
fields.Add(m.Value.Replace("\"", ""));
}
if (fields.Count != headers.Length) throw new InvalidDataException("Field count must match header count.");
var jsonElements = headers.Zip(fields, (header, field) => string.Format("\"{0}\": \"{1}\"", header, field)).ToArray();
string jsonObject = "{" + string.Format("{0}", string.Join(",", jsonElements)) + "}";
if (i < lines.Length - 1)
jsonObject += ",";
sb.AppendLine(jsonObject);
}
sb.AppendLine("]");
return sb.ToString();
}
public static string ObjectToJson(object obj)
{
var js = new JavaScriptSerializer();
return js.Serialize(obj);
}
public static object JsonToObject(string datain, Type objectType)
{
var js = new JavaScriptSerializer();
return js.Deserialize(datain, objectType);
}
public static bool IsNumber(string s)
{
return s.All(char.IsDigit);
}
}
#!/bin/bash
# Default repository path (adjust if needed)
DEFAULT_REPO_PATH="/home/mnewport/repos/wyffels/Sales-App"
# On WSL, you might need the Linux path directly like above,
# or translate the Windows path if running Bash outside WSL directly:
# DEFAULT_REPO_PATH="/mnt/c/path/to/your/repo"
# --- Argument Parsing ---
FORCE_FETCH=false
SHOW_HELP=false
day_offset_param=""
author_index_param=""
# Process command line options
while getopts "fhr:d:a:" opt; do
case ${opt} in
f ) # Force fetch
FORCE_FETCH=true
;;
r ) # Custom repo path
custom_repo_path=$OPTARG
;;
h ) # Help
SHOW_HELP=true
;;
d ) # Day offset
day_offset_param=$OPTARG
;;
a ) # Author index
author_index_param=$OPTARG
;;
\? )
echo "Usage: $0 [-f] [-r repo_path] [-d day_offset] [-a author_index] [-h]"
exit 1
;;
esac
done
shift $((OPTIND -1))
# Remains of the debug function - now just a void where error messages go to die
# (No functions needed here now)
# --- Global Helper Function: Animation ---
show_animation() {
local pid=$1
local initial_message=$2
local frames=('' '' '' '' '' '' '' '')
# Generic messages suitable for various waiting periods
local messages=(
"Still waiting..."
"Processing data..."
"Fetching information..."
"Thinking really hard..."
"Almost there..."
"Checking the archives..."
"Consulting the git gods..."
"Wrangling bits and bytes..."
)
local count=0
local message="$initial_message" # Start with the initial message
echo # Newline before animation starts
while ps -p $pid > /dev/null; do
frame=${frames[count % ${#frames[@]}]}
# Change message every ~5 seconds (15 * 0.3s)
if [ $((count % 15)) -eq 0 ] && [ $count -ne 0 ]; then
message=${messages[$((RANDOM % ${#messages[@]}))]}
fi
printf "\r\033[K %s %s" "$frame" "$message"
sleep 0.3
count=$((count+1))
done
printf "\r\033[K" # Clear the animation line
echo # Newline after animation finishes
}
# First positional parameter as repo path if provided and -r not used
if [ -z "$custom_repo_path" ] && [ $# -gt 0 ]; then
custom_repo_path="$1"
fi
# Show help if requested
if [ "$SHOW_HELP" = true ]; then
cat << "EOF"
╔════════════════════════════════════════════════════════════╗
║ GIT ARCHAEOLOGY & ACCOUNTABILITY DISTRIBUTION ║
╚════════════════════════════════════════════════════════════╝
USAGE: ./git_commits.sh [-f] [-r repo_path] [-d day_offset] [-a author_index] [-h]
OPTIONS:
-f Force branch fetching (ignores cache)
For when you need to refresh your list of
things to never look at
-r repo_path Specify a repository path
Because pointing fingers at other directories
should be easier
-d day_offset Specify date offset (0 today, -1 yesterday, etc.)
Skip the date prompt
-a index Specify author index (1 for first author, etc.)
Skip the author selection prompt
-h Show this help
The closest you'll get to actual documentation
This script answers the age-old question: "What did I actually
do today that I can talk about in standup tomorrow?" by providing
a clean, timesheet-ready output.
It also calculates the total time you spent committing, which
bears no relation to the time you spent actually working.
Note: No git branches were harmed in the execution of this script,
though several were thoroughly judged.
EOF
exit 0
fi
# Repository path handling
repo_path="${custom_repo_path:-$DEFAULT_REPO_PATH}"
# --- Basic Validation ---
if [ ! -d "$repo_path" ]; then
echo "Error: Repository directory not found: $repo_path" >&2
exit 1
fi
# Change to the repository directory
cd "$repo_path" || { echo "Error: Could not change directory to $repo_path" >&2; exit 1; }
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
echo "Error: Not a valid Git repository: $repo_path" >&2
exit 1
fi
# --- Cache Remote Branches Analysis ---
cache_remote_branches() {
local cache_path="/tmp/git_remote_branches_cache_$(basename $(git rev-parse --show-toplevel))"
local cache_timeout=86400 # 24 hours in seconds
# Check if cache exists and is recent
if [ -f "$cache_path" ]; then
local cache_time=$(stat -c %Y "$cache_path" 2>/dev/null || stat -f %m "$cache_path" 2>/dev/null) # Added error suppression for stat -f
local current_time=$(date +%s)
local cache_age=$((current_time - cache_time))
if [ "$cache_age" -lt "$cache_timeout" ] && [ "$FORCE_FETCH" = false ]; then
echo "🧠 Using cached branch data (Cache is $((cache_age / 60)) minutes old). Use -f to force fetch."
return 0
elif [ "$FORCE_FETCH" = true ]; then
echo "🔥 Force fetch enabled! Ignoring cache."
rm -f "$cache_path"
else
echo "⏰ Cache expired. Fetching fresh data."
rm -f "$cache_path"
fi
fi
echo "🔄 Fetching ALL branches from ALL remotes..."
{ # Run fetch in background to show animation for it too
git fetch --all --prune
} &
local fetch_pid=$!
show_animation $fetch_pid "Fetching remote objects..."
wait $fetch_pid
echo "🌱 Creating local tracking branches (if needed)..."
# Run the branch creation process in the background and create cache
{
# Write remote branches to cache first
# Ensure color codes are stripped even if git config changes
git branch -r --no-color | grep -v '\->' > "$cache_path"
# Create the local tracking branches based on the cache file
while IFS= read -r remote; do
# Trim whitespace
remote=$(echo "$remote" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$remote" ]; then continue; fi
# Extract the base branch name (strip remote name like 'origin/')
# Assuming remote format is 'origin/branch-name'
if [[ "$remote" == *"/"* ]]; then
local_branch="${remote#*/}"
else
# Handle cases where remote might not have a slash (unlikely for standard setups)
local_branch="$remote"
fi
if [ -z "$local_branch" ]; then continue; fi # Skip empty lines if any
# Check if local branch *already exists* before trying to create it
if ! git show-ref --verify --quiet refs/heads/"$local_branch"; then
# Attempt to track; suppress stderr for "already exists" which can happen in races
git branch --track "$local_branch" "$remote" >/dev/null 2>&1
fi
done < "$cache_path"
# Update cache timestamp only after successfully processing
touch "$cache_path"
} & # End background process block
local branch_pid=$!
show_animation $branch_pid "Syncing local branches..." # Pass PID and initial message
wait $branch_pid # Wait for the background branch creation to finish
echo "✅ Branch collection complete."
echo "🧠 Branches cached for future script runs."
echo
}
# --- Fetch ALL Remote Branches ---
cache_remote_branches
# --- Date Input ---
if [ -n "$day_offset_param" ]; then
day_offset="$day_offset_param"
else
echo "Enter the date (0 today, -1 yesterday, etc.) to filter commits:"
read -r day_offset
# Default to 0 if empty
day_offset="${day_offset:-0}"
fi
# Validate numeric input
if ! [[ "$day_offset" =~ ^-?[0-9]+$ ]]; then
echo "Error: Invalid offset. Please enter an integer." >&2; exit 1
fi
# Calculate target date (YYYY-MM-DD) - Assumes GNU date
target_date=$(date -d "$day_offset days" '+%Y-%m-%d')
if [ $? -ne 0 ]; then
# Fallback for non-GNU date (macOS) - less reliable for negative offsets
target_date=$(date -v"${day_offset}d" '+%Y-%m-%d' 2>/dev/null)
if [ $? -ne 0 ]; then
echo "Error: Could not calculate date. Ensure 'date' command supports relative dates." >&2; exit 1
fi
fi
echo "Filtering for date: $target_date"
# Define start and end timestamps for the target date
start_date="${target_date}T00:00:00"
end_date="${target_date}T23:59:59"
# --- Author Filtering ---
echo -e "\nFetching authors..."
mapfile -t author_lines < <(git log --all --no-merges --format='%an' | sort | uniq -c | sort -nr) # Use --all instead of --branches
if [ ${#author_lines[@]} -eq 0 ]; then
echo "No authors found in the repository."
exit 0
fi
echo -e "\nUnique authors in the repository:"
authors=()
declare -A author_map # Map index to name
for i in "${!author_lines[@]}"; do
line="${author_lines[$i]}"
# Robust parsing for names potentially containing numbers
count=$(echo "$line" | awk '{print $1}')
name=$(echo "$line" | sed -e 's/^[[:space:]]*[0-9]*[[:space:]]*//') # Remove leading count and space
authors+=("$name")
author_map[$((i+1))]="$name"
printf "%d. %s - Commits: %d\n" "$((i+1))" "$name" "$count"
done
if [ -n "$author_index_param" ]; then
author_index="$author_index_param"
else
echo -e "\nEnter the number corresponding to the author (default 1):"
read -r author_index
author_index="${author_index:-1}"
fi
if ! [[ "$author_index" =~ ^[0-9]+$ ]] || [ "$author_index" -lt 1 ] || [ "$author_index" -gt ${#authors[@]} ]; then
echo "Error: Invalid author number." >&2; exit 1
fi
selected_author="${author_map[$author_index]}"
echo "Selected author: $selected_author"
# --- Commit Retrieval & Processing ---
echo -e "\nFetching commits for $selected_author on $target_date..."
# Use a character that should NEVER appear in git commit messages
# Record Separator (ASCII 30) is ideal for this purpose
SEP=$'\036'
commit_details=() # Initialize empty array
commit_output_file="/tmp/git_commits_output_$$" # Use $$ for better uniqueness
# Use EXIT trap for robust cleanup
trap 'rm -f "$commit_output_file"' EXIT
# Run git log in the background
{
# Use --all to search all refs (local branches, remote branches, tags)
# Protect against SEP in messages (though highly unlikely)
git log --all --no-merges --author="$selected_author" --after="$start_date" --before="$end_date" --date=iso --format="%at${SEP}%H${SEP}%ai${SEP}%s${SEP}%b" --reverse > "$commit_output_file"
git_status=$? # Capture exit status inside the subshell
exit $git_status # Exit subshell with git log's status
} &
commit_pid=$!
# Show animation while git log runs
show_animation $commit_pid "Digging through commit history..."
# Wait for git log to finish and capture its exit status
wait $commit_pid
git_log_exit_status=$?
# Check if the command failed
if [ $git_log_exit_status -ne 0 ]; then
echo "Error: git log command failed while fetching commits (Exit code: $git_log_exit_status)." >&2
# Optionally print stderr from git log if captured, but it wasn't here.
exit 1
fi
# Read the results from the temporary file if it has content
if [ -s "$commit_output_file" ]; then
# Use safe mapfile/read loop approach
while IFS= read -r line || [[ -n "$line" ]]; do # Process last line even if no trailing newline
if [ -n "$line" ]; then # Skip empty lines
commit_details+=("$line")
fi
done < "$commit_output_file"
else
# Handle case where git log ran successfully but produced no output
commit_details=()
fi
# --- Process Commits ---
if [ ${#commit_details[@]} -eq 0 ]; then
echo "No commits found for '$selected_author' on $target_date."
# Print the final clean footer even if no commits
echo -e "\n╔═════════════════════════════════════════════════════════╗"
echo -e "║ COMMITS FOR TIMESHEET: $target_date"
echo -e "╚═════════════════════════════════════════════════════════╝"
echo "* Branches: N/A"
printf "\n* Time logged: 00:00\n"
echo -e "╔═════════════════════════════════════════════════════════╗"
echo -e "║ END TIMESHEET DATA ║"
echo -e "╚═════════════════════════════════════════════════════════╝"
exit 0
fi
# Always print the clean header
echo -e "\n╔═════════════════════════════════════════════════════════╗"
echo -e "║ COMMITS FOR TIMESHEET: $target_date"
echo -e "╚═════════════════════════════════════════════════════════╝"
declare -A branch_commits # Associative array: branch -> list of commit details strings (newline separated)
declare -A branch_first_commit_time # Associative array: branch -> earliest timestamp
declare -A branch_last_segment_set # Use keys for uniqueness of last segments
first_commit_timestamp=""
last_commit_timestamp=""
echo "Associating commits with branches..." # Add progress indicator
# Process each commit detail line to find its branches
processed_count=0
total_commits=${#commit_details[@]}
for details in "${commit_details[@]}"; do
# Use a safer field splitting approach with read
IFS="$SEP" read -r timestamp sha datetime subject body_rest <<< "$details"
# Validate SHA exists - CRITICAL CHECK
if [ -z "$sha" ]; then
#echo "Skip: Missing SHA in line: $details" >&2 # More informative warning
#processed_count=$((processed_count + 1))
printf "\rProcessed %d / %d commits..." "$processed_count" "$total_commits"
continue # Skip to next commit
fi
# Validate timestamp is a number to avoid comparison errors
if [[ "$timestamp" =~ ^[0-9]+$ ]]; then
# Update overall first/last timestamps
if [ -z "$first_commit_timestamp" ] || [ "$timestamp" -lt "$first_commit_timestamp" ]; then
first_commit_timestamp="$timestamp"
fi
if [ -z "$last_commit_timestamp" ] || [ "$timestamp" -gt "$last_commit_timestamp" ]; then
last_commit_timestamp="$timestamp"
fi
else
echo "WARNING: Invalid timestamp for commit ${sha:0:8}: '$timestamp'" >&2
# Try to recover timestamp from datetime if possible
new_timestamp=$(date -d "$(echo "$datetime" | cut -d' ' -f1,2)" +%s 2>/dev/null)
if [[ "$new_timestamp" =~ ^[0-9]+$ ]]; then
timestamp="$new_timestamp"
# Re-run timestamp comparison logic with recovered value
if [ -z "$first_commit_timestamp" ] || [ "$timestamp" -lt "$first_commit_timestamp" ]; then first_commit_timestamp="$timestamp"; fi
if [ -z "$last_commit_timestamp" ] || [ "$timestamp" -gt "$last_commit_timestamp" ]; then last_commit_timestamp="$timestamp"; fi
else
# Fallback if recovery fails
if [ -n "$first_commit_timestamp" ]; then timestamp="$first_commit_timestamp"; else timestamp=$(date +%s); fi
fi
fi
# Safely get branches containing this commit (local and remote)
# Use --all to get local and remote branches directly
# Use --no-color to prevent issues
# Remove 'remotes/origin/' prefix commonly added by --all
mapfile -t branches < <(git branch --all --no-color --contains "$sha" --format='%(refname:short)' 2>/dev/null | sed 's|^remotes/origin/|origin/|' | sed 's|^origin/||') # Strip origin/ prefix
# Last resort fallback
if [ ${#branches[@]} -eq 0 ]; then
branches=("(no associated branch found)")
fi
# Store commit in each branch's commit list
for branch_name in "${branches[@]}"; do
# Trim potential whitespace from branch names
branch_name=$(echo "$branch_name" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
effective_branch_name="${branch_name:-"(unknown branch)"}"
# IMPORTANT: Use a newline separator for commit records within a branch
# Ensure the 'details' variable itself doesn't end with newline if IFS trimming removed it
# Safest to reconstruct the line if needed, or just append newline.
branch_commits["$effective_branch_name"]+="${details}"$'\n'
# Track earliest commit per branch for sorting
if [ -z "${branch_first_commit_time[$effective_branch_name]}" ] ||
[ "$timestamp" -lt "${branch_first_commit_time[$effective_branch_name]}" ]; then
branch_first_commit_time["$effective_branch_name"]="$timestamp"
fi
# Track branch name last segments for summary
last_segment=$(basename "$effective_branch_name" 2>/dev/null || echo "$effective_branch_name")
# Don't add empty or special names to segments
if [[ -n "$last_segment" && "$last_segment" != "(no associated branch found)" && "$last_segment" != "(unknown branch)" ]]; then
branch_last_segment_set["$last_segment"]=1
fi
done
processed_count=$((processed_count + 1))
# Simple progress indicator without animation
printf "\rProcessed %d / %d commits..." "$processed_count" "$total_commits"
done
printf "\r\033[K" # Clear progress line
echo "Sorting and formatting results..."
# --- Sort Branches by First Commit Time ---
if [ ${#branch_first_commit_time[@]} -gt 0 ]; then
mapfile -t sorted_branch_names < <(
for branch in "${!branch_first_commit_time[@]}"; do
printf "%s\t%s\n" "${branch_first_commit_time[$branch]}" "$branch"
done | sort -n | cut -f2-
)
else
# No branches found - rare but possible
sorted_branch_names=()
fi
# --- Output Commits Grouped by Branch (with De-duplication) ---
declare -A seen_commits # Track SHAs that have already been printed
for branch_name in "${sorted_branch_names[@]}"; do
# Get commit details for the branch, ensuring trailing newline for readarray
branch_content="${branch_commits[$branch_name]}"
if [ -z "$branch_content" ]; then
continue # Skip empty branch content
fi
# Create temporary array of sorted commits for this branch using process substitution
# Sort lines numerically based on the first field (timestamp)
mapfile -t commits_for_branch < <(echo -n "$branch_content" | sort -t"$SEP" -k1,1n)
branch_header_printed=0
for commit_line in "${commits_for_branch[@]}"; do
if [ -z "$commit_line" ]; then continue; fi
# Split fields safely
IFS="$SEP" read -r timestamp sha datetime subject body <<< "$commit_line"
# Validate SHA before proceeding
if [ -z "$sha" ]; then
echo "WARNING: Skipping display for commit with missing SHA in final output." >&2
continue
fi
# De-duplication check
if [[ -n "${seen_commits["$sha"]}" ]]; then
continue # Skip already printed commit
fi
# Parse time for display (handle ISO 8601 format from %ai)
# Example: 2025-04-02 09:40:15 -0500
time_str=$(echo "$datetime" | awk '{print $2}' | cut -d: -f1,2 2>/dev/null || echo "??:??")
# Skip invalid entries
if [ -z "$time_str" ] || [ "$time_str" = "??:??" ]; then
echo "Skipping commit with invalid time: ${sha:0:8} ($datetime)" >&2
continue
fi
# Ensure subject is not just whitespace
trimmed_subject=$(echo "$subject" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$trimmed_subject" ]; then
echo "Skipping commit with empty subject: ${sha:0:8}" >&2
continue
fi
# Print branch header if first valid commit in branch
if [ "$branch_header_printed" -eq 0 ]; then
echo -e "\n╭─────────────────────────────────────────────────────╮"
printf "│ %-51s │\n" "BRANCH: $branch_name"
echo -e "╰─────────────────────────────────────────────────────╯"
branch_header_printed=1
fi
# Mark commit as seen
seen_commits["$sha"]=1
# Print the commit
printf -- " - [%s] %s\n" "$time_str" "$subject" # Use original subject
# Print first line of body if it looks like a proper comment
if [ -n "$body" ]; then
# Read the first line without external commands
IFS= read -r first_line <<< "$body"
trimmed_first_line=$(echo "$first_line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
# Check if it's non-empty and doesn't look like commit metadata or separator line
if [[ -n "$trimmed_first_line" ]] && ! [[ "$trimmed_first_line" =~ ^(Signed-off-by:|Co-authored-by:|Change-Id:|Fixes:|See-also:|---|Merge:|\*\ ) ]]; then
printf " %s\n" "$first_line"
fi
fi
done
done
# --- Print Distinct Branch Last Segments (Clean Format) ---
if [ ${#branch_last_segment_set[@]} -gt 0 ]; then
mapfile -t sorted_last_segments < <(printf "%s\n" "${!branch_last_segment_set[@]}" | sort)
distinct_segments_str=$(printf "%s, " "${sorted_last_segments[@]}")
distinct_segments_str=${distinct_segments_str%, } # Remove trailing comma and space
else
distinct_segments_str="N/A"
fi
echo -e "\n* Branches: ${distinct_segments_str}" # Added newline for spacing
# --- Calculate and Print Total Time Span (Clean Format) ---
if [ -n "$first_commit_timestamp" ] && [ -n "$last_commit_timestamp" ] && [[ "$last_commit_timestamp" =~ ^[0-9]+$ ]] && [[ "$first_commit_timestamp" =~ ^[0-9]+$ ]] && [ "$last_commit_timestamp" -ge "$first_commit_timestamp" ]; then
total_seconds=$((last_commit_timestamp - first_commit_timestamp))
total_hours=$((total_seconds / 3600))
total_minutes=$(((total_seconds % 3600) / 60))
printf "\n* Time logged: %02d:%02d\n" "$total_hours" "$total_minutes"
else
# Handle cases where timestamps were invalid or missing
echo "Could not calculate time span due to missing or invalid timestamps." >&2
printf "\n* Time logged: 00:00\n"
fi
echo -e "╔═════════════════════════════════════════════════════════╗"
echo -e "║ END TIMESHEET DATA ║"
echo -e "╚═════════════════════════════════════════════════════════╝"
exit 0
#!/bin/bash
# terminal_ascension_v3.sh - The Sacred Terminal Transformation Ritual
# For Arch Linux & Manjaro users who've decided their terminal should work for them,
# not against them, unlike their corporate overlords
set -e # Exit immediately if a command exits with a non-zero status
# Because nothing says "I've given up" like letting errors silently accumulate
echo "================================================================"
echo " TERMINAL ASCENSION RITUAL v3 - PROCEED WITH REVERENCE "
echo " Your journey from terminal peasant to CLI royalty begins "
echo "================================================================"
# Create a log file to record our spiritual journey
LOG_FILE="$HOME/terminal_ascension_log.txt"
exec > >(tee -a "$LOG_FILE") 2>&1
echo "📝 Creating backup directory for your existing configs"
BACKUP_DIR="$HOME/.terminal_backup_$(date +%Y%m%d%H%M%S)"
mkdir -p "$BACKUP_DIR"
# Function to backup existing config files before we obliterate them with progress
backup_file() {
if [ -f "$1" ]; then
echo "📦 Backing up $1 to $BACKUP_DIR"
cp "$1" "$BACKUP_DIR/"
fi
}
# Backup existing configuration files
backup_file "$HOME/.zshrc"
backup_file "$HOME/.zpreztorc"
backup_file "$HOME/.config/kitty/kitty.conf"
backup_file "$HOME/.config/starship.toml"
echo "🔍 Checking for required packages..."
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Function to install a package if it doesn't exist
ensure_package() {
if ! command_exists "$1"; then
echo "📦 Installing $1..."
sudo pacman -S --noconfirm "$1" || { echo "❌ Failed to install $1"; exit 1; }
else
echo "$1 is already installed"
fi
}
# Install core packages
echo "🚀 Installing the digital cornerstones of your new terminal empire..."
PACKAGES=(
"kitty" # Because terminal emulators should be named after small felines
"zsh" # The shell that makes bash look like it's still using dial-up
"fzf" # For when you remember typing something, somewhere, sometime
"git" # The historian of your coding mistakes
"bat" # cat with wings, literally
"exa" # ls on steroids, because listing files should be an experience
"ripgrep" # grep but fast enough to match your diminishing patience
"fd" # find but without the arcane syntax from the 1970s
"gitui" # For when you're tired of memorizing git's 167 subcommands
)
for package in "${PACKAGES[@]}"; do
ensure_package "$package"
done
# Install fonts - because reading monospace without ligatures is like eating unseasoned food
echo "🔤 Installing fonts that won't make your eyes bleed..."
if ! pacman -Qi ttf-firacode-nerd > /dev/null 2>&1; then
sudo pacman -S --noconfirm ttf-firacode-nerd || echo "⚠️ Couldn't install Nerd Fonts. Your symbols may look like hieroglyphics."
else
echo "✅ Nerd Fonts already installed"
fi
# Install Starship - because prompts should be informative, not cryptic
echo "🚀 Installing Starship prompt - the GPS for your terminal journey..."
if ! command_exists starship; then
curl -sS https://starship.rs/install.sh | sh || echo "⚠️ Failed to install Starship. Your prompt will remain ordinary."
else
echo "✅ Starship is already installed"
fi
# Install Atuin - because your command history deserves better than a flat file
echo "🪄 Installing Atuin - the magical history manager..."
if ! command_exists atuin; then
sudo pacman -S --noconfirm atuin || echo "⚠️ Failed to install Atuin. Your history will remain in the dark ages."
else
echo "✅ Atuin is already installed"
fi
# Install Prezto - the Zsh framework that won't eat your CPU for breakfast
echo "🏗️ Setting up Prezto - your new Zsh framework..."
if [ ! -d "${ZDOTDIR:-$HOME}/.zprezto" ]; then
git clone --recursive https://github.com/sorin-ionescu/prezto.git "${ZDOTDIR:-$HOME}/.zprezto" || { echo "❌ Failed to clone Prezto"; exit 1; }
echo "📝 Creating Zsh config files..."
# Using bash-compatible commands instead of Zsh-specific globbing
for rcfile in $(find "${ZDOTDIR:-$HOME}"/.zprezto/runcoms -type f -not -name "README.md"); do
ln -sf "$rcfile" "${ZDOTDIR:-$HOME}/.$(basename $rcfile)"
done
else
echo "✅ Prezto is already installed"
fi
# Install per-directory-history - because context is everything
echo "🗂️ Installing per-directory-history plugin..."
if [ ! -d "$HOME/per-directory-history" ]; then
git clone https://github.com/jimhester/per-directory-history "$HOME/per-directory-history" || echo "⚠️ Failed to install per-directory-history. Your history will remain contextless."
else
echo "✅ per-directory-history is already installed"
fi
# Configure Kitty
echo "🐱 Configuring Kitty - your new terminal emulator..."
mkdir -p "$HOME/.config/kitty"
cat > "$HOME/.config/kitty/kitty.conf" << 'EOF'
# Kitty Configuration - Because terminals should be pretty AND functional
# Font settings - for when you stare at text all day
font_family FiraCode Nerd Font
bold_font auto
italic_font auto
bold_italic_font auto
font_size 12.0
# Visual tweaks - because aesthetics matter to the deeply jaded
background_opacity 0.95
window_padding_width 5
placement_strategy center
hide_window_decorations no
confirm_os_window_close 0
# Because bell sounds make people violent
enable_audio_bell no
visual_bell_duration 0.0
# Terminal performance settings - for when you cat a 10GB log file by accident
scrollback_lines 10000
input_delay 3
repaint_delay 10
sync_to_monitor yes
# URL handling - because clicking is easier than copying
url_style curly
open_url_with default
url_prefixes http https file ftp gemini irc gopher mailto news git
detect_urls yes
# Shortcuts that won't require finger gymnastics
map ctrl+shift+c copy_to_clipboard
map ctrl+shift+v paste_from_clipboard
map ctrl+shift+s paste_from_selection
map ctrl+shift+t new_tab
map ctrl+shift+w close_tab
map ctrl+shift+l next_tab
map ctrl+shift+h previous_tab
map ctrl+shift+n new_window
map ctrl+shift+q close_window
map ctrl+shift+equal increase_font_size
map ctrl+shift+minus decrease_font_size
map ctrl+shift+backspace restore_font_size
# Tab bar - because spatial awareness matters
tab_bar_edge top
tab_bar_style powerline
tab_powerline_style slanted
active_tab_foreground #000
active_tab_background #eee
active_tab_font_style bold
inactive_tab_foreground #444
inactive_tab_background #999
inactive_tab_font_style normal
# Colors - because black on white is for printed books
foreground #dddddd
background #121212
cursor #cccccc
EOF
# Configure Starship
echo "🌠 Configuring Starship - your new prompt..."
mkdir -p "$HOME/.config"
cat > "$HOME/.config/starship.toml" << 'EOF'
# Starship Configuration - For prompts that convey actual useful information
# General settings
add_newline = true
scan_timeout = 30
command_timeout = 500
# The iconic prompt character
[character]
success_symbol = "[λ](green)"
error_symbol = "[✗](red)"
vimcmd_symbol = "[](green)"
# Directory - where am I and how deep in the filesystem?
[directory]
truncation_length = 3
truncate_to_repo = true
style = "blue"
read_only = " 🔒"
read_only_style = "red"
# Git - what branch and are you behind/ahead?
[git_branch]
symbol = " "
style = "purple"
truncation_length = 18
truncation_symbol = "..."
[git_status]
format = '([\[$all_status$ahead_behind\]]($style) )'
style = "red"
conflicted = "⚔️ "
ahead = "⇡${count}"
behind = "⇣${count}"
diverged = "⇕⇡${ahead_count}⇣${behind_count}"
untracked = "?${count}"
stashed = "*${count}"
modified = "!${count}"
staged = "+${count}"
renamed = "»${count}"
deleted = "✘${count}"
# Language versions - what runtime are you actually using?
[nodejs]
format = "via [⬢ $version](green) "
[dotnet]
symbol = "󰪮 "
style = "blue"
heuristic = true
[python]
symbol = "🐍 "
pyenv_version_name = true
detect_files = ["requirements.txt", ".python-version", "pyproject.toml", "Pipfile", "tox.ini", "setup.py", "__init__.py"]
# Execution time - how slow was that command?
[cmd_duration]
min_time = 2000
format = "took [$duration](yellow) "
# Package version - because knowing is half the battle
[package]
format = "via [📦 $version](208) "
# Containers - because forgetting you're in a container leads to confusion
[container]
format = "[$symbol]($style) "
# Status code - when things go wrong and you need to know why
[status]
disabled = false
format = "[$symbol$status]($style) "
symbol = "✖ "
success_symbol = ""
not_executable_symbol = "🚫 "
not_found_symbol = "🔍 "
sigint_symbol = "🧱 "
signal_symbol = "⚡ "
style = "bold red"
# Hostname - for when you're SSH'd into 17 different servers
[hostname]
ssh_only = true
format = "on [$hostname](bold red) "
EOF
# Configure Zsh with Prezto, FZF, and other enhancements
echo "🛠️ Configuring Zsh - your new command shell..."
cat > "$HOME/.zshrc" << 'EOF'
# .zshrc - Where productivity dreams come true and muscle memory goes to die
# Source Prezto configuration
if [[ -s "${ZDOTDIR:-$HOME}/.zprezto/init.zsh" ]]; then
source "${ZDOTDIR:-$HOME}/.zprezto/init.zsh"
fi
# Set Prezto options - loaded modules
zstyle ':prezto:load' pmodule \
'environment' \
'terminal' \
'editor' \
'history' \
'directory' \
'spectrum' \
'utility' \
'completion' \
'git' \
'syntax-highlighting' \
'history-substring-search' \
'autosuggestions'
# Theme settings - because life's too short for ugly prompts
zstyle ':prezto:module:prompt' theme 'sorin'
# Loading per-directory-history - context-aware history
source ~/per-directory-history/per-directory-history.zsh
# FZF integration - for fuzzy finding everything
if [ -f /usr/share/fzf/key-bindings.zsh ]; then
source /usr/share/fzf/key-bindings.zsh
fi
if [ -f /usr/share/fzf/completion.zsh ]; then
source /usr/share/fzf/completion.zsh
fi
# Better tool alternatives - because life's too short for outdated tools
alias cat='bat --style=plain'
alias ls='exa --icons'
alias ll='exa -la --icons'
alias la='exa -a --icons'
alias lt='exa -T --icons'
alias grep='rg'
alias find='fd'
alias ps='procs'
alias du='dust'
# Atuin history management - because remembering is hard
eval "$(atuin init zsh)"
# Starship prompt - your CLI GPS
eval "$(starship init zsh)"
# Directory navigation - because cd is so 1970s
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
# Git aliases - because git commands are unnecessarily verbose
alias gs='git status'
alias ga='git add'
alias gc='git commit'
alias gco='git checkout'
alias gl='git log --oneline'
alias gp='git push'
alias gpull='git pull'
alias gd='git diff'
alias gb='git branch'
# npm/yarn shortcuts - for JavaScript development without the tears
alias ni='npm install'
alias nid='npm install --save-dev'
alias nig='npm install -g'
alias nr='npm run'
alias nt='npm test'
alias yi='yarn install'
alias ya='yarn add'
alias yad='yarn add --dev'
alias yr='yarn run'
alias yt='yarn test'
# dotnet shortcuts - for when C# calls
alias dn='dotnet'
alias dnr='dotnet run'
alias dnb='dotnet build'
alias dnt='dotnet test'
alias dnw='dotnet watch'
alias dnn='dotnet new'
alias dns='dotnet sln'
# Python shortcuts - for snake charmers
alias py='python'
alias pip='pip3'
alias pipup='pip install --upgrade pip'
alias venv='python -m venv venv'
alias activate='source venv/bin/activate'
# Custom functions - for the repetitive stuff
# Open a file in VSCode or the current directory if no args
vs() {
if [ $# -eq 0 ]; then
code .
else
code "$@"
fi
}
# Create a new directory and cd into it
mkcd() {
mkdir -p "$1" && cd "$1"
}
# Find files and open in VSCode
vsc() {
local files
files=($(fzf --preview 'bat --color=always --style=numbers --line-range=:500 {}' --multi))
[[ -n "$files" ]] && vs "${files[@]}"
}
# Find command in history and execute it
hex() {
local cmd
cmd=$(atuin search -i)
[[ -n "$cmd" ]] && eval "$cmd"
}
# The magical function that makes VSCode's integrated terminal
# inherit all this shell goodness
if [[ -n "$VSCODE_INJECTION" ]]; then
echo "VSCode Terminal Detected - All systems operational"
fi
# Node version manager - if you use fnm
if command -v fnm &> /dev/null; then
eval "$(fnm env --use-on-cd)"
fi
# Insightful welcome message - because terminal sessions should start with wisdom
cat << WELCOME
$(tput setaf 2)
Terminal Ascension Complete - Welcome to CLI Enlightenment
$(tput setaf 3)
"In the beginning was the command line." - Neal Stephenson
$(tput setaf 6)
• Use Ctrl+R to search history with fzf/atuin
• Toggle between directory/global history with Ctrl+G
• Type 'vsc' to fuzzy-find and open files in VSCode
• Type 'hex' to search and execute from your history
$(tput sgr0)
WELCOME
EOF
# Configure Zsh as the default shell
echo "🐚 Setting Zsh as your default shell..."
if [ "$SHELL" != "$(which zsh)" ]; then
chsh -s "$(which zsh)" || echo "⚠️ Failed to change default shell. Run 'chsh -s $(which zsh)' manually."
else
echo "✅ Zsh is already your default shell"
fi
# Initialize Atuin
echo "🧙 Initializing Atuin - importing your command history..."
command_exists atuin && {
# Import existing shell history
atuin import auto || echo "⚠️ Failed to import history. You can try 'atuin import auto' manually."
# Offer to set up sync
echo "🔄 Would you like to set up Atuin sync to share history across devices? (y/n)"
read -r sync_response
if [[ "$sync_response" =~ ^[Yy]$ ]]; then
echo "📝 Register an Atuin account (or login if you already have one):"
atuin register || echo "⚠️ Failed to register. You can try 'atuin register' manually."
atuin sync || echo "⚠️ Failed to sync. You can try 'atuin sync' manually."
fi
}
# Configure VSCode if it's installed
echo "🧩 Checking for VSCode integration..."
if command_exists code; then
echo "🔧 Configuring VSCode to use Zsh and FiraCode Nerd Font..."
mkdir -p "$HOME/.config/Code/User"
VSCODE_SETTINGS="$HOME/.config/Code/User/settings.json"
# Backup existing settings
if [ -f "$VSCODE_SETTINGS" ]; then
cp "$VSCODE_SETTINGS" "$BACKUP_DIR/settings.json"
fi
# Create or update VSCode settings
if [ -f "$VSCODE_SETTINGS" ]; then
# Ensure we don't mess up existing JSON - this is a simplified approach
# For a real script, consider using jq for proper JSON manipulation
grep -q "terminal.integrated.defaultProfile.linux" "$VSCODE_SETTINGS" || {
sed -i '$ s/}$/,\n "terminal.integrated.defaultProfile.linux": "zsh",\n "terminal.integrated.fontFamily": "FiraCode Nerd Font"\n}/' "$VSCODE_SETTINGS"
}
else
# Create a new settings file
cat > "$VSCODE_SETTINGS" << 'EOF'
{
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.fontFamily": "FiraCode Nerd Font",
"editor.fontFamily": "'FiraCode Nerd Font', 'Droid Sans Mono', 'monospace'",
"editor.fontLigatures": true
}
EOF
fi
echo "✅ VSCode configured to use Zsh and Nerd Fonts"
else
echo "⚠️ VSCode not detected. If you install it later, update settings manually."
fi
echo ""
echo "🎉 TERMINAL ASCENSION RITUAL COMPLETE 🎉"
echo "$(tput setaf 2)Your terminal has been transformed from a digital stone age relic"
echo "into a gleaming monument of developer efficiency.$(tput sgr0)"
echo ""
echo "👉 Next steps:"
echo " 1. Log out and log back in, or restart your shell with 'exec zsh'"
echo " 2. Run through the Atuin tutorial: 'atuin help'"
echo " 3. Customize your Starship prompt: edit ~/.config/starship.toml"
echo " 4. Explore your new aliases and functions in ~/.zshrc"
echo ""
echo "$(tput setaf 3)We've backed up your old configs to: $BACKUP_DIR$(tput sgr0)"
echo "Log file created at: $LOG_FILE"
echo ""
echo "$(tput setaf 6)May your keystrokes be few and your productivity plentiful.$(tput sgr0)"
# End of script
EOF
#!/bin/bash
# terminal_descension.sh - The Ritual of Terminal Reversion
# For those who tasted CLI enlightenment and decided "nah, I'm good"
set -e # Exit immediately if a command exits with a non-zero status
echo "================================================================"
echo " TERMINAL DESCENSION RITUAL - UNDOING THE ASCENSION "
echo " Returning your terminal to its humble origins... "
echo "================================================================"
# Create a log file for this reversal journey
LOG_FILE="$HOME/terminal_descension_log_$(date +%Y%m%d%H%M%S).txt"
exec > >(tee -a "$LOG_FILE") 2>&1
echo "Log file: $LOG_FILE"
# --- Configuration Restoration ---
echo ""
echo "🔎 Searching for backup directory created by terminal_ascension.sh..."
LATEST_BACKUP=$(find "$HOME" -maxdepth 1 -type d -name '.terminal_backup_*' -printf '%T@ %p\n' | sort -nr | head -n 1 | cut -d' ' -f2-)
RESTORED_CONFIG=false
if [ -n "$LATEST_BACKUP" ] && [ -d "$LATEST_BACKUP" ]; then
echo "✅ Found potential backup directory: $LATEST_BACKUP"
echo "🔄 Automatically restoring configuration files from this backup..."
# Force 'yes' path
echo "🛡️ Restoring configurations..."
# Function to restore a backed-up file
restore_file() {
BACKUP_FILE="$LATEST_BACKUP/$(basename "$1")"
if [ -f "$BACKUP_FILE" ]; then
echo "Restoring $1 from $BACKUP_FILE"
cp "$BACKUP_FILE" "$1" || echo "⚠️ Failed to restore $1"
else
echo "No backup found for $1 in $LATEST_BACKUP"
fi
}
restore_file "$HOME/.zshrc"
restore_file "$HOME/.zpreztorc" # Might not exist if Prezto wasn't fully set up
restore_file "$HOME/.config/kitty/kitty.conf"
restore_file "$HOME/.config/starship.toml"
restore_file "$HOME/.config/Code/User/settings.json" # Restore VSCode settings
# Remove Prezto symlinks if they exist, as the backup .zshrc might not use Prezto
echo "🧹 Removing potential Prezto symlinks..."
find "$HOME" -maxdepth 1 -name '.z*' -lname "${ZDOTDIR:-$HOME}/.zprezto/runcoms/*" -delete || echo "No Prezto symlinks found or error removing them."
RESTORED_CONFIG=true
echo "✅ Configuration files restored from backup."
# Removed else block
else
echo "⚠️ No backup directory found. Manual restoration might be needed."
fi
# --- Package Removal ---
echo ""
echo "🗑️ Preparing to uninstall packages installed by terminal_ascension.sh..."
PACKAGES_TO_REMOVE=(
"kitty"
"zsh"
"fzf"
# "git" # Usually essential, let's not remove git by default
"bat"
"exa"
"ripgrep"
"fd"
"gitui"
"ttf-firacode-nerd"
"atuin"
)
echo "The following packages might have been installed: ${PACKAGES_TO_REMOVE[*]}"
echo "❓ Automatically attempting to uninstall these packages..."
# Force 'yes' path
echo "Removing packages..."
sudo pacman -Rns --noconfirm "${PACKAGES_TO_REMOVE[@]}" || echo "⚠️ Some packages might not have been installed or failed to uninstall."
echo "✅ Package removal attempted."
# Removed else block
# --- Starship Removal ---
echo ""
echo "🚀 Checking for Starship installation..."
STARSHIP_PATH=$(which starship)
if [ -n "$STARSHIP_PATH" ]; then
echo "Starship binary found at: $STARSHIP_PATH"
echo "❓ Automatically removing the Starship binary..."
# Force 'yes' path
sudo rm -f "$STARSHIP_PATH" || echo "⚠️ Failed to remove Starship binary. Check permissions."
echo "✅ Starship binary removal attempted."
# Removed else block
else
echo "Starship binary not found in PATH."
fi
# --- Remove Cloned Repositories ---
echo ""
echo "🧹 Checking for cloned repositories..."
PREZTO_DIR="${ZDOTDIR:-$HOME}/.zprezto"
PDH_DIR="$HOME/per-directory-history"
REPOS_TO_REMOVE=()
if [ -d "$PREZTO_DIR" ]; then
REPOS_TO_REMOVE+=("$PREZTO_DIR")
fi
if [ -d "$PDH_DIR" ]; then
REPOS_TO_REMOVE+=("$PDH_DIR")
fi
if [ ${#REPOS_TO_REMOVE[@]} -gt 0 ]; then
echo "Found the following cloned repositories: ${REPOS_TO_REMOVE[*]}"
echo "❓ Automatically removing these directories..."
# Force 'yes' path
echo "Removing repositories..."
rm -rf "${REPOS_TO_REMOVE[@]}" || echo "⚠️ Failed to remove some repositories."
echo "✅ Repository removal attempted."
# Removed else block
else
echo "No Prezto or per-directory-history directories found."
fi
# --- Remove Configuration Files (if not restored from backup) ---
echo ""
if [ "$RESTORED_CONFIG" = false ]; then
echo "🗑️ Configuration files were not restored from backup."
CONFIG_FILES_TO_REMOVE=(
"$HOME/.config/kitty/kitty.conf"
"$HOME/.config/starship.toml"
"$HOME/.zshrc" # Remove the one created by the script
# "$HOME/.zpreztorc" # This is a symlink, should be gone if Prezto dir removed
)
echo "The following configuration files might have been created/overwritten: ${CONFIG_FILES_TO_REMOVE[*]}"
echo "❓ Automatically removing these specific configuration files..."
# Force 'yes' path
echo "Removing configuration files..."
for file in "${CONFIG_FILES_TO_REMOVE[@]}"; do
if [ -f "$file" ]; then
rm -f "$file" || echo "⚠️ Failed to remove $file"
fi
done
# Also remove Prezto symlinks again just in case
find "$HOME" -maxdepth 1 -name '.z*' -lname "${ZDOTDIR:-$HOME}/.zprezto/runcoms/*" -delete || echo "No Prezto symlinks found or error removing them."
echo "✅ Specific configuration file removal attempted."
# Removed else block
# Offer to revert VSCode settings manually if not restored
echo ""
echo "⚠️ VSCode settings were not restored from backup."
echo " You might need to manually edit $HOME/.config/Code/User/settings.json"
echo " to remove 'terminal.integrated.defaultProfile.linux': 'zsh'"
echo " and 'terminal.integrated.fontFamily': 'FiraCode Nerd Font'."
fi
# --- Change Default Shell ---
echo ""
echo "🐚 Checking default shell..."
CURRENT_SHELL=$(getent passwd "$USER" | cut -d: -f7)
ZSH_PATH=$(which zsh)
if [ "$CURRENT_SHELL" == "$ZSH_PATH" ]; then
echo "Your current default shell is Zsh."
echo "❓ Automatically changing the default shell back to /bin/bash..."
# Force 'yes' path
echo "Changing default shell to /bin/bash..."
sudo chsh -s /bin/bash "$USER" || echo "⚠️ Failed to change default shell. Run 'sudo chsh -s /bin/bash $USER' manually."
echo "✅ Default shell change attempted. You may need to log out and back in."
# Removed else block
else
echo "Your default shell is not Zsh ($CURRENT_SHELL)."
fi
# --- Atuin Data Removal (Optional and Dangerous) ---
echo ""
echo "⚠️ Atuin stores its database and configuration typically in ~/.local/share/atuin/ and ~/.config/atuin/"
echo " Removing this will permanently delete your synchronized command history if you used sync."
echo "❓ Automatically removing the Atuin data directories... (EXTREME CAUTION ADVISED)"
# Force 'yes' path
echo "Removing Atuin data directories..."
rm -rf "$HOME/.local/share/atuin" "$HOME/.config/atuin" || echo "⚠️ Failed to remove Atuin directories."
echo "✅ Atuin data removal attempted."
# Removed else block
# --- Final Messages ---
echo ""
echo "🎉 TERMINAL DESCENSION RITUAL COMPLETE 🎉"
echo "$(tput setaf 1)The terminal has been returned to a more... conventional state.$(tput sgr0)"
echo ""
echo "👉 Important Notes:"
echo " * You may need to log out and log back in for shell changes to take full effect."
echo " * If you skipped restoring backups, your original config files might still be in a backup directory like '$LATEST_BACKUP'."
echo " * Some packages might have dependencies that were not removed. You can run 'sudo pacman -Qdt' to list orphans and 'sudo pacman -Rns \$(pacman -Qdtq)' to remove them (use with care)."
echo " * Review the log file for details: $LOG_FILE"
echo ""
echo "$(tput setaf 6)May your default terminal serve you adequately.$(tput sgr0)"
# End of script
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment