Tag: command-line

  • Rescuing Tampermonkey Scripts from a LevelDB Backup

    Rescuing Tampermonkey Scripts from a LevelDB Backup

    I have a confession to make. I didn’t back up my Tampermonkey scripts. I recently switched to a new computer, and thought I had copied all of my important information over from the old one.

    It turns out, I did not. Luckily I did have a full disk backup that I thought I could just pull the .user.js files off of. OH BOY WAS I WRONG.

    The Oops: No Script Backup

    I’d cobbled together a dozen userscripts or more over years, but never bothered to add them to a backup routine. When I opened Tampermonkey on the new machine, it greeted me with the emptiest dashboard imaginable.

    Digging Into Chrome’s LevelDB

    It turns out Chrome buries extension data in your profile directory. The Tampermonkey store lives in a LevelDB folder named after its extension ID:

    /Volumes/OldHDD/derrick/Library/Application Support/Google/Chrome/Profile 1/IndexedDB/\
    chrome-extension_dhdgffkkebhmkfjojejmpbldmpobfkfo_0.indexeddb.leveldb

    Dumping with leveldbutil

    I used leveldbutil, a C++ command-line tool for dumping LevelDB databases. I had to clone the repo and compile it from source before it would run on macOS.

    ./leveldbutil dump "/Volumes/OldHDD/derrick/Library/Application Support/Google/Chrome/Profile 1/IndexedDB/chrome-extension_dhdgffkkebhmkfjojejmpbldmpobfkfo_0.indexeddb.leveldb" > tampermonkey-dump.txtCode language: Bash (bash)

    This produced a massive text dump (tampermonkey-dump.txt) of JSON blobs—metadata, source code, state… you name it.

    Parsing the Dump: My One-Off Script

    Rather than manually eyeballing hundreds of lines, I whipped up a throwaway PHP script, let’s call it ScriptSalvager, to automate recovery. It:

    1. Loads the dump file.
    2. Scans for script entries via regex.
    3. json_decode()s each blob.
    4. Writes out individual script files.
    <?php
    /**
     * Exports Tampermonkey userscripts from a raw dump to per-script directories.
     *
     * This script reads the Tampermonkey IndexedDB dump, splits it into
     * individual userscript segments, parses each piece (meta, source,
     * rules, state, etc.), and writes each to its own file.
     */
    
    // Path to the raw Tampermonkey dump.
    $file = __DIR__ . '/tampermonkey-dump.txt';
    
    if ( ! is_readable( $file ) ) {
    	fwrite( STDERR, 'Cannot read file: ' . $file . "\n" );
    	exit( 1 );
    }
    
    // Load the file.
    $contents = file_get_contents( $file );
    
    // Split on lines like: "--- offset 123456; sequence 789".
    $segments = preg_split( '/^--- offset \d+; sequence \d+/m', $contents );
    
    // Remove any leading empty segment
    if ( isset( $segments[0] ) && trim( $segments[0] ) === '' ) {
    	array_shift( $segments );
    }
    
    // Collect only the userscript segments (those with both @meta and @source).
    $userscripts = [];
    foreach ( $segments as $segment ) {
    	if ( str_contains( $segment, "put '@meta#" ) && str_contains( $segment, "put '@source#" ) ) {
    		  // Normalize line endings and split into lines for readability.
    		  $normalized = str_replace( [ "\r\n", "\r" ], "\n", $segment );
    		  $lines = explode( "\n", $normalized );
    
    		  // Trim each line, and remove empty or external-resource lines.
    		  $filtered = [];
    		  foreach ( $lines as $line ) {
    			  $line = trim( $line );
    			  // Skip blank lines.
    			  if ( $line === '' ) {
    				  continue;
    			  }
    			  // Skip external-resource entries.
    			  if ( str_starts_with( $line, "put '@ext#" ) ) {
    				  continue;
    			  }
    			  $filtered[] = $line;
    		  }
    
    		  $userscripts[] = $filtered;
    	}
    }
    
    // Free memory we no longer need.
    unset( $contents, $segments );
    
    // Build an associative array of scripts by their UID values.
    $scripts = [];
    foreach ( $userscripts as $lines ) {
    	$items = [];
    	$script_uuid = null;
    	foreach ( $lines as $line ) {
    		  $trimmed = trim( $line );
    		  if ( ! str_starts_with( $trimmed, "put '" ) ) {
    			  echo "Error: Expected line to start with 'put '\n";
    			  die();
    		  }
    		  // Strip off leading "put '".
    		  $rest = substr( $trimmed, strlen( "put '" ) );
    		  // Split into type and remainder.
    		  [ $type, $after_hash ] = explode( '#', $rest, 2 );
    		  // Split remainder into UUID+closing-quote and JSON blob.
    		  [ $uid_quoted, $json_with_quote ] = explode( "' ", $after_hash, 2 );
    		  // Extract the UUID (no quotes).
    		  $uuid = trim( $uid_quoted, "'" );
    		  if ( $script_uuid === null ) {
    			  $script_uuid = $uuid;
    		  }
    
    		  // Trim single-quotes from JSON.
    		  $json_blob = trim( $json_with_quote, "'" );
    		  $value = $json_blob;
    		  $value = json_decode( $json_blob, true );
    		  if ( '@source' === $type ) {
    			  // Decode the source JSON.
    			  $value = json_decode( $json_blob, true );
    			  $value = $value['value'];
    		  } else {
    			$value = json_encode( $value['value'] ) ?? $json_blob;
    		}
    
    		  // Store, allowing duplicates to become arrays.
    		  if ( isset( $items[$type] ) ) {
    			  if ( ! is_array( $items[$type] ) ) {
    				  $items[$type] = [ $items[$type] ];
    			  }
    			  $items[$type][] = $value;
    		  } else {
    			  $items[$type] = $value;
    		  }
    	}
    	// Use the extracted UUID as the script key.
    	if ( $script_uuid === null ) {
    		echo "Error: Could not extract script UUID\n";
    		die();
    	}
    
    	if ( ! isset( $scripts[$script_uuid] ) ) {
    		$scripts[$script_uuid] = [];
    	}
    
    	$scripts[$script_uuid][] = $items;
    }
    
    // Add script names to the array.
    foreach ( $scripts as $uuid => $script ) {
    	foreach ( $script as $index => $version ) {
    		$name = $uuid;
    		// Check if the script has a name.
    		if ( isset( $version['@uid'] ) ) {
    			$uid = $version['@uid'];
    			$name = $uid;
    		} elseif ( isset( $version['@source'] ) ) {
    			$source = $version['@source'];
    			$normalized_source = str_replace( [ "\r\n", "\r" ], "\n", $source ?? '' );
    			$lines_source = explode( "\n", $normalized_source );
    			foreach( $lines_source as $line_source ) {
    				$line_source = trim( $line_source );
    				// Skip blank lines.
    				if ( $line_source === '' ) {
    					continue;
    				}
    
    				if ( str_contains( $line_source, "@name" ) ) {
    					$name_lines = explode( "@name", $line_source );
    					$name = trim( $name_lines[1] );
    					break;
    				}
    			}
    		}
    		if ( $name === $uuid ) {
    			echo "Error: Could not extract script name\n";
    			die();
    		}
    
    		// Remove leading and trailing quotes.
    		$name = trim( $name, '"' );
    
    		// Store the name in the script array.
    		$scripts[$uuid][$index]['@name'] = $name;
    	}
    }
    
    // If a script has multiple meta entries, split them into separate scripts.
    foreach ( $scripts as $uuid => $script ) {
    	foreach ( $script as $index => $version ) {
    		// Check if the script has multiple meta entries.
    		if ( isset( $version['@meta'] ) && is_array( $version['@meta'] ) ) {
    			foreach ( $version['@meta'] as $meta_index => $meta_value ) {
    				// Copy the script data to a new entry.
    				$new_script = $scripts[$uuid][$index];
    				// Remove the meta entry from the original script.
    				unset( $new_script['@meta'] );
    				// Copy the single meta entry to the new script.
    				$new_script['@meta'] = $meta_value;
    				// Unset the meta entry from the original script.
    				unset( $scripts[$uuid][$index]['@meta'] );
    				// Add the new script to the array.
    				$scripts[$uuid][] = $new_script;
    			}
    			// After splitting, remove the original script entry.
    			unset( $scripts[$uuid][$index] );
    		}
    	}
    }
    
    // Add lastModified to the array.
    foreach ( $scripts as $uuid => $script ) {
    	foreach ( $script as $index => $version ) {
    		// Check if the script has a lastModified.
    		if ( isset( $version['@meta'] ) ) {
    			$meta = $version['@meta'];
    			$meta = json_decode( $meta, true );
    			$meta = $meta['value'] ?? $meta;
    		   
    			if ( isset( $meta['lastModified'] ) ) {
    				$last_modified = $meta['lastModified'];
    				$scripts[$uuid][$index]['@lastModified'] = $last_modified;
    				$scripts[$uuid][$index]['@lastModifiedHuman'] = date( 'Y-m-d H:i:s', intval($last_modified/1000 ));
    			}
    
    			if ( isset( $meta['version'] ) ) {
    				$version_number = $meta['version'];
    				$scripts[$uuid][$index]['@version'] = $version_number;
    			}
    
    		}
    	}
    }
    
    // Export each script’s data into its own directory, with a unique name, timestamp, and version.
    foreach ( $scripts as $uuid => $script ) {
    	$latest_version = 0;
    	foreach ( $script as $index => $version ) {
    		// Determine directory name based on script name (fallback to UUID).
    		$name = $version['@name'];
    
    		$directory = $name . '/' . $version['@lastModifiedHuman'] . ' - v' . $version['@version'];
    
    		// Create or clear directory.
    		if ( ! is_dir( $directory ) ) {
    			mkdir( $directory, 0777, true );
    		}
    
    		// Write each part to its own JSON file.
    		foreach ( $version as $type => $value ) {
    			$base = trim( $type, '@' );
    
    			if ( 'source' === $base) {
    				// Export the userscript source unescaped so code remains intact.
    				$file_name = "{$directory}/{$base}.user.js";
    				file_put_contents( $file_name, $value );
    			} else {
    				// Single value, write normally.
    				if ( ! in_array( $base, [ 'name', 'lastModified', 'lastModifiedHuman', 'version' ] ) ) {
    					$value = json_decode( $value, true );
    				}
    				$file_name = "{$directory}/{$base}.json";
    				file_put_contents( $file_name, json_encode( $value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
    			}
    		}
    
    		// Find the most recent lastModified date and set it as the "latest" version.
    		if ( isset( $version['@lastModified'] ) && $version['@lastModified'] > $latest_version ) {
    			$latest_version = $version['@lastModified'];
    
    			// Are we at the end of the array?
    			if ( $index === count( $script ) - 1 ) {
    				// Create the latest version directory.
    				$latest_directory = $name . '/latest';
    				if ( ! is_dir( $latest_directory ) ) {
    					mkdir( $latest_directory, 0777, true );
    				}
    				// Write each part to its own JSON file.
    				foreach ( $version as $type => $value ) {
    					if ( $latest_version !== $version['@lastModified'] ) {
    						continue;
    					}
    					$base = trim( $type, '@' );
    
    					if ( 'source' === $base) {
    						// Export the userscript source unescaped so code remains intact.
    						$file_name = "{$latest_directory}/{$base}.user.js";
    						file_put_contents( $file_name, $value );
    					} else {
    						// Single value, write normally.
    						if ( ! in_array( $base, [ 'name', 'lastModified', 'lastModifiedHuman', 'version' ] ) ) {
    							$value = json_decode( $value, true );
    						}
    						$file_name = "{$latest_directory}/{$base}.json";
    						file_put_contents( $file_name, json_encode( $value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
    					}
    				}
    			}
    		}
    	}
    }Code language: PHP (php)

    Note: This was a one-and-done hack, don’t judge my variable names.

    The Recovery Steps

    1. Mount or locate your backup folder.
    2. Dump the LevelDB files: ./leveldbutil dump "/path/to/<profile>.leveldb" > tampermonkey-dump.txt
    3. Run ScriptSalvager: php scriptsalvager.php
    4. Cross your fingers and hope it dumps out all of your scripts.

    Lessons (That I Probably Won’t Learn)

    • Always export your userscripts from Tampermonkey–seriously.
    • Add them to your actual backup routine.
    • Or just never switch laptops again. 😬

    Happy salvaging!

  • Get OctoPrint Status via Bash

    Get OctoPrint Status via Bash

    I’m working on a module to add OctoPrint status to my zsh prompt, which I’ll probably write about in the future as a bigger post about my prompt customizations.

    To start with that though, I need to play around with accessing the API via curl.

    So here’s my super alpha version that will request the current status via HTTP and output it:

    #!/bin/bash
    
    OCTOPRINT_API="hunter2"
    BASE_URL="http://octoprint.example.com"
    JOB_ENDPOINT="${BASE_URL}/api/job"
    CURL_TIMEOUT="0.5"
    
    # Fetch the JSON from OctoPrint
    response="$(curl --max-time ${CURL_TIMEOUT} --silent --fail \
      --header "X-Api-Key: ${OCTOPRINT_API}" \
      "${JOB_ENDPOINT}")"
    
    # Extract fields with jq
    file_name=$(jq -r '.job.file.display' <<< "$response")
    completion=$(jq -r '.progress.completion' <<< "$response")
    state=$(jq -r '.state' <<< "$response")
    time_elapsed=$(jq -r '.progress.printTime' <<< "$response")
    time_left=$(jq -r '.progress.printTimeLeft' <<< "$response")
    
    # Round the completion percentage to two decimals
    completion_str=$(printf "%.2f" "$completion")
    
    # Convert seconds to H:MM:SS
    function fmt_time() {
      local total_seconds="$1"
      local hours=$((total_seconds / 3600))
      local minutes=$(((total_seconds % 3600) / 60))
      local seconds=$((total_seconds % 60))
      printf "%02d:%02d:%02d" "$hours" "$minutes" "$seconds"
    }
    
    # Convert the times
    time_elapsed_str=$(fmt_time "$time_elapsed")
    time_left_str=$(fmt_time "$time_left")
    
    # Print a readable summary
    echo "File: ${file_name}"
    echo "State: ${state}"
    echo "Completion: ${completion_str}%"
    echo "Time Elapsed: ${time_elapsed_str}"
    echo "Time Left: ${time_left_str}"
    Code language: Bash (bash)

    Do with this what you will.

  • Super Simple OpenAI PHP Class

    Super Simple OpenAI PHP Class

    I’ve been playing around with hooking up ChatGPT/Dall-E to WordPress and WP-CLI. To do this, I whipped up a super simple class to make this easier:

    <?php
    class OpenAI_API {
    	public const API_KEY = 'hunter2';  // Get your own darn key!
    
    	/**
    	 * Generates an image based on the provided prompt using the OpenAI API.
    	 *
    	 * @param string $prompt The text prompt to generate the image from. Default is an empty string.
    	 * @return string The response body from the OpenAI API, or a JSON-encoded error message if the request fails.
    	 */
    	public static function generate_image( string $prompt = '' ): string {
    		$data = array(
    			'model'   => 'dall-e-3',
    			'prompt'  => trim( $prompt ),
    			'quality' => 'hd',
    			'n'       => 1,
    			'size'    => '1024x1024',
    		);
    
    		$args = array(
    			'body'        => wp_json_encode( $data ),
    			'headers'     => array(
    				'Content-Type'  => 'application/json',
    				'Authorization' => 'Bearer ' . OpenAI_API::API_KEY,
    			),
    			'method'      => 'POST',
    			'data_format' => 'body',
    		);
    
    		$response = wp_remote_post( 'https://api.openai.com/v1/images/generations', $args );
    
    		if ( is_wp_error( $response ) ) {
    			return wp_json_encode( $response );
    		} else {
    			$body = wp_remote_retrieve_body( $response );
    			return $body;
    		}
    	}
    
    	/**
    	 * Creates a chat completion using the OpenAI GPT-3.5-turbo model.
    	 *
    	 * @param string $prompt The user prompt to be sent to the OpenAI API.
    	 * @param string $system_prompt Optional. The system prompt to be sent to the OpenAI API. Defaults to a predefined prompt.
    	 * 
    	 * @return string The response body from the OpenAI API, or a JSON-encoded error message if the request fails.
    	 */
    	public static function create_chat_completion( string $prompt = '', string $system_prompt = '' ): string {
    		if ( empty( $system_prompt ) ) {
    			$system_prompt = 'You are a virtual assistant designed to provide general support across a wide range of topics. Answer concisely and directly, focusing on essential information only. Maintain a friendly and approachable tone, adjusting response length based on the complexity of the question.';
    		}
    
    		// The data to send in the request body
    		$data = array(
    			'model'    => 'gpt-3.5-turbo',
    			'messages' => array(
    				array(
    					'role'    => 'system',
    					'content' => trim( $system_prompt ),
    				),
    				array(
    					'role'    => 'user',
    					'content' => trim( $prompt ),
    				),
    			),
    		);
    
    		$args = array(
    			'body'        => wp_json_encode( $data ),
    			'headers'     => array(
    				'Content-Type'  => 'application/json',
    				'Authorization' => 'Bearer ' . OpenAI_API::API_KEY,
    			),
    			'method'      => 'POST',
    			'data_format' => 'body',
    			'timeout'     => 15,
    		);
    
    		// Perform the POST request
    		$response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', $args );
    
    		// Error handling
    		if ( is_wp_error( $response ) ) {
    			return wp_json_encode( $response );
    		} else {
    			if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
    				return wp_json_encode( array( 'error' => 'API returned non-200 status code', 'response' => wp_remote_retrieve_body( $response ) ) );
    			}
    
    			// Assuming the request was successful, you can access the response body as follows:
    			$body = wp_remote_retrieve_body( $response );
    			return $body;
    		}
    	}
    }Code language: PHP (php)

    I can generate images and get back text from the LLM. Here’s some examples ChatGPT made to show how you can use these:


    Example 1: Generating an Image

    This example generates an image of a “cozy cabin in the snowy woods at sunset” using the generate_image method and displays it in an <img> tag.

    <?php
    $image_url = OpenAI_API::generate_image("A cozy cabin in the snowy woods at sunset");
    
    if ( ! empty( $image_url ) ) {
        echo '<img src="' . esc_url( $image_url ) . '" alt="Cozy cabin in winter">';
    } else {
        echo 'Image generation failed.';
    }
    ?>Code language: PHP (php)

    Example 2: Simple Chat Completion

    This example sends a question to the create_chat_completion method and prints the response directly.

    <?php
    $response = OpenAI_API::create_chat_completion("How does photosynthesis work?");
    echo $response;
    ?>Code language: PHP (php)

    Example 3: Chat Completion with Custom System Prompt

    This example sets a custom system prompt for a specific tone, here focusing on culinary advice, and asks a relevant question.

    <?php
    $system_prompt = "You are a culinary expert. Please provide advice on healthy meal planning.";
    $response = OpenAI_API::create_chat_completion("What are some good meals for weight loss?", $system_prompt);
    echo $response;
    ?>Code language: PHP (php)

    Here are some key limitations of this simple API implementation and why these are crucial considerations for production:

    • Lack of Robust Error Handling:
      • This API implementation has basic error handling that only checks if an error occurred during the request. It doesn’t provide specific error messages for different types of failures (like rate limits, invalid API keys, or network issues).
      • Importance: In production, detailed error handling allows for clearer diagnostics and faster troubleshooting when issues arise.
    • No Caching:
      • The current API makes a fresh request for each call, even if the response might be identical to a recent query.
      • Importance: Caching can reduce API usage costs, improve response times, and reduce server load, particularly for commonly repeated queries.
    • No API Rate Limiting:
      • This implementation doesn’t limit the number of requests sent within a certain time frame.
      • Importance: Rate limiting prevents hitting API request quotas and helps avoid unexpected costs or blocked access if API limits are exceeded.
    • No Logging for Debugging:
      • There’s no logging in place for tracking request errors or failed attempts.
      • Importance: Logs provide an audit trail that helps diagnose issues over time, which is crucial for maintaining a stable application in production.
    • Lack of Security for API Key Management:
      • The API key is currently hard coded into the class.
      • Importance: In production, it’s best to use environment variables or a secure key management system to protect sensitive information and prevent accidental exposure of the API key.
    • No Response Parsing or Validation:
      • The code assumes that the API response format is always correct, without validation.
      • Importance: Inconsistent or unexpected responses can cause failures. Validation ensures the app handles different cases gracefully.

    Why Not Use in Production?

    Due to these limitations, this API should be considered a prototype or learning tool rather than a production-ready solution. Adding robust error handling, caching, rate limiting, and logging would make it more resilient, secure, and efficient for a production environment.


    Alright, so listen to the LLM and don’t do anything stupid with this, like I am doing.

  • Capturing MacOS Settings Changes

    Capturing MacOS Settings Changes

    Let me get this right out. I upgraded to MacOS 15 beta and it totally borked my machine. I had to do a full, fresh reinstall.

    Totally my fault, and I should have prepared better.

    So now, I’m trying to remember to capture my personal settings so I can use a shell script to restore them in case of emergency with defaults

    With the help of ChatGPT and a lot of stupidity, I have created this:

    #!/bin/bash
    
    # Set the DEBUG flag (default is false)
    DEBUG=false
    
    # Define a function for logging debug information
    log() {
        if [ "$DEBUG" = true ]; then
            echo "$@"
        fi
    }
    
    # Define a function for running commands with error suppression unless DEBUG is true
    run_command() {
        if [ "$DEBUG" = true ]; then
            "$@"
        else
            "$@" 2>/dev/null
        fi
    }
    
    # Step 1: Define directories for temporary files
    before_dir="/tmp/before_defaults"
    after_dir="/tmp/after_defaults"
    mkdir -p "$before_dir" "$after_dir"
    
    # Step 2: Capture system-wide and global preferences
    
    echo "Reading user-specific global preferences..."
    run_command defaults read -g > "$before_dir/user_global_defaults.plist"
    
    echo "Reading system-wide global preferences..."
    run_command sudo defaults read -g > "$before_dir/system_global_defaults.plist"
    
    echo "Reading user-specific system preferences..."
    run_command defaults read > "$before_dir/user_system_defaults.plist"
    
    echo "Reading system-wide system preferences..."
    run_command sudo defaults read > "$before_dir/system_system_defaults.plist"
    
    # Step 3: Get all domains and capture their preferences with and without sudo
    echo "Reading defaults for all domains..."
    domains=$(run_command defaults domains)
    for domain in $domains; do
        log "Reading user-specific defaults for domain: $domain"
        run_command defaults read "$domain" > "$before_dir/${domain}_user_defaults.plist"
        
        log "Reading system-wide defaults for domain: $domain"
        run_command sudo defaults read "$domain" > "$before_dir/${domain}_system_defaults.plist"
    done
    
    # Step 4: Capture network settings (excluding Launch Services to reduce noise)
    echo "Capturing network settings..."
    run_command sudo cp /Library/Preferences/SystemConfiguration/preferences.plist "$before_dir/network_settings.plist"
    
    echo "Current preferences saved. Please make your changes now."
    echo "Press any key to continue once you've made the changes..."
    read -n 1 -s
    
    # Step 5: Capture updated defaults
    echo "Reading updated user-specific global preferences..."
    run_command defaults read -g > "$after_dir/user_global_defaults.plist"
    
    echo "Reading updated system-wide global preferences..."
    run_command sudo defaults read -g > "$after_dir/system_global_defaults.plist"
    
    echo "Reading updated user-specific system preferences..."
    run_command defaults read > "$after_dir/user_system_defaults.plist"
    
    echo "Reading updated system-wide system preferences..."
    run_command sudo defaults read > "$after_dir/system_system_defaults.plist"
    
    echo "Reading updated defaults for all domains..."
    for domain in $domains; do
        log "Reading updated user-specific defaults for domain: $domain"
        run_command defaults read "$domain" > "$after_dir/${domain}_user_defaults.plist"
        
        log "Reading updated system-wide defaults for domain: $domain"
        run_command sudo defaults read "$domain" > "$after_dir/${domain}_system_defaults.plist"
    done
    
    echo "Capturing updated network settings..."
    run_command sudo cp /Library/Preferences/SystemConfiguration/preferences.plist "$after_dir/network_settings.plist"
    
    # Step 6: Compare before and after to identify changes with unified diffs and output results directly
    
    echo "Comparing preferences and generating diffs..."
    
    # Global and system diffs
    echo "Comparing global and system preferences..."
    diff -u "$before_dir/user_global_defaults.plist" "$after_dir/user_global_defaults.plist" | sed '/^\s*$/d'
    diff -u "$before_dir/system_global_defaults.plist" "$after_dir/system_global_defaults.plist" | sed '/^\s*$/d'
    
    diff -u "$before_dir/user_system_defaults.plist" "$after_dir/user_system_defaults.plist" | sed '/^\s*$/d'
    diff -u "$before_dir/system_system_defaults.plist" "$after_dir/system_system_defaults.plist" | sed '/^\s*$/d'
    
    echo "Comparing Domain Specific"
    # Domain-specific diffs
    for domain in $domains; do
        # Only run diff if both user-specific files exist
        if [ -f "$before_dir/${domain}_user_defaults.plist" ] && [ -f "$after_dir/${domain}_user_defaults.plist" ]; then
            
            #echo "Comparing user-specific defaults for domain: $domain"
            diff -u "$before_dir/${domain}_user_defaults.plist" "$after_dir/${domain}_user_defaults.plist" | sed '/^\s*$/d'
        fi
    
        # Only run diff if both system-wide files exist
        if [ -f "$before_dir/${domain}_system_defaults.plist" ] && [ -f "$after_dir/${domain}_system_defaults.plist" ]; then
            #echo "Comparing system-wide defaults for domain: $domain"
            diff -u "$before_dir/${domain}_system_defaults.plist" "$after_dir/${domain}_system_defaults.plist" | sed '/^\s*$/d'
        fi
    done
    
    diff -u "$before_dir/network_settings.plist" "$after_dir/network_settings.plist" | sed '/^\s*$/d'
    
    # Step 7: Clean up temporary files (optional, currently commented out)
    #log "Cleaning up temporary files..."
    #run_command sudo rm -r "$before_dir" "$after_dir"
    
    echo "Comparison complete."
    Code language: Bash (bash)

    This will basically do a scan of all defaults settings it can find, wait for you to make changes, and then show you a diff of what has changed.

    For a bad example, I wanted to auto hide the dock:

    $ bash default-changes.sh
    Reading user-specific global preferences...
    Reading system-wide global preferences...
    Reading user-specific system preferences...
    Reading system-wide system preferences...
    Reading defaults for all domains...
    Capturing network settings...
    Current preferences saved. Please make your changes now.
    Press any key to continue once you've made the changes...
    Reading updated user-specific global preferences...
    Reading updated system-wide global preferences...
    Reading updated user-specific system preferences...
    Reading updated system-wide system preferences...
    Reading updated defaults for all domains...
    Capturing updated network settings...
    Comparing preferences and generating diffs...
    Comparing global and system preferences...
    --- /tmp/before_defaults/user_system_defaults.plist	2024-09-30 21:23:17.183140919 -0400
    +++ /tmp/after_defaults/user_system_defaults.plist	2024-09-30 21:23:45.721177080 -0400
    @@ -95,9 +95,9 @@
         };
         ContextStoreAgent =     {
             "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertEventsNotification:/app/mediaUsageActivityDate" = "2024-08-15 01:49:34 +0000";
    -        "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertEventsNotification:/app/usageActivityDate" = "2024-10-01 01:22:53 +0000";
    +        "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertEventsNotification:/app/usageActivityDate" = "2024-10-01 01:23:42 +0000";
             "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertLocalEventsNotification:/app/mediaUsageActivityDate" = "2024-08-15 01:49:34 +0000";
    -        "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertLocalEventsNotification:/app/usageActivityDate" = "2024-10-01 01:22:53 +0000";
    +        "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertLocalEventsNotification:/app/usageActivityDate" = "2024-10-01 01:23:42 +0000";
         };
         LighthouseBitacoraFramework =     {
             "lastAggregationDate_MLHost" = "2024-08-31 00:00:00 +0000";
    @@ -6998,6 +6998,7 @@
             );
         };
         "com.apple.dock" =     {
    +        autohide = 1;
             "last-analytics-stamp" =         (
                 "748840077.891269"
             );
    @@ -12104,7 +12105,7 @@
                 "com.apple.photolibraryd.curatedlibraryprocessing" = "2024-09-30 11:58:18 +0000";
                 "com.apple.photolibraryd.periodicmaintenance" = "2024-09-29 18:28:19 +0000";
                 "com.apple.proactive.PersonalIntelligence.PersonalIntelligenceMetrics" = "2024-09-29 14:29:35 +0000";
    -            "com.apple.proactive.PersonalizationPortrait.ClientLinkStatus" = "2024-09-22 01:54:07 +0000";
    +            "com.apple.proactive.PersonalizationPortrait.ClientLinkStatus" = "2024-10-04 09:16:11 +0000";
                 "com.apple.proactive.PersonalizationPortrait.ContactHandlesCache" = "2024-09-29 23:56:51 +0000";
                 "com.apple.proactive.PersonalizationPortrait.ContactsImport" = "2024-09-29 23:57:21 +0000";
                 "com.apple.proactive.PersonalizationPortrait.CoreRoutineImport" = "2024-09-29 23:56:50 +0000";
    @@ -12115,11 +12116,11 @@
                 "com.apple.proactive.PersonalizationPortrait.ExpiredLinkReview" = "2024-09-29 23:57:21 +0000";
                 "com.apple.proactive.PersonalizationPortrait.FeedbackProcessing" = "2024-09-29 23:56:50 +0000";
                 "com.apple.proactive.PersonalizationPortrait.FeedbackStreamReviewed" = "2024-09-29 23:56:51 +0000";
    -            "com.apple.proactive.PersonalizationPortrait.LinkStatusGeneration" = "2024-09-22 01:54:07 +0000";
    +            "com.apple.proactive.PersonalizationPortrait.LinkStatusGeneration" = "2024-10-04 09:16:11 +0000";
                 "com.apple.proactive.PersonalizationPortrait.LogLocationPerplexity" = "2024-09-29 23:54:27 +0000";
    -            "com.apple.proactive.PersonalizationPortrait.LogNamedEntityFirstSource" = "2024-09-22 01:54:07 +0000";
    +            "com.apple.proactive.PersonalizationPortrait.LogNamedEntityFirstSource" = "2024-10-04 09:16:11 +0000";
                 "com.apple.proactive.PersonalizationPortrait.LogNamedEntityPerplexity" = "2024-09-29 23:56:52 +0000";
    -            "com.apple.proactive.PersonalizationPortrait.LogTopicFirstSource" = "2024-09-22 01:54:07 +0000";
    +            "com.apple.proactive.PersonalizationPortrait.LogTopicFirstSource" = "2024-10-04 09:16:11 +0000";
                 "com.apple.proactive.PersonalizationPortrait.LogTopicPerplexity" = "2024-09-29 23:57:22 +0000";
                 "com.apple.proactive.PersonalizationPortrait.MapsImport" = "2024-09-29 23:57:22 +0000";
                 "com.apple.proactive.PersonalizationPortrait.NamedEntityFiltering" = "2024-09-29 23:57:22 +0000";
    @@ -12136,7 +12137,7 @@
                 "com.apple.proactive.PersonalizationPortrait.TopicImport" = "2024-09-26 17:42:30 +0000";
                 "com.apple.proactive.PersonalizationPortrait.TopicRepairAndExport" = "2024-09-26 17:45:06 +0000";
                 "com.apple.proactive.PersonalizationPortrait.UnsupportedClient" = "2024-09-29 23:57:21 +0000";
    -            "com.apple.proactive.PersonalizationPortrait.VacuumDatabase" = "2024-09-22 01:54:07 +0000";
    +            "com.apple.proactive.PersonalizationPortrait.VacuumDatabase" = "2024-10-04 09:16:11 +0000";
                 "com.apple.proactive.ProactiveHarvesting.Cleanup" = "2024-09-29 14:29:35 +0000";
                 "com.apple.proactive.ProactiveHarvesting.Harvest.PeriodicBackground" = "2024-09-29 21:54:28 +0000";
                 "com.apple.proactived.contextualactions.training" = "2024-09-28 03:57:02 +0000";
    --- /tmp/before_defaults/system_system_defaults.plist	2024-09-30 21:23:17.281165387 -0400
    +++ /tmp/after_defaults/system_system_defaults.plist	2024-09-30 21:23:45.810771555 -0400
    @@ -374,7 +374,7 @@
                 "501:com.apple.mlhost.telemetry.daily" = "2024-09-26 21:59:29 +0000";
                 "501:com.apple.mlhostd.daily" = "2024-09-28 08:35:02 +0000";
                 "501:com.apple.parsec.SafariBrowsingAssistant" = "2024-09-27 02:53:00 +0000";
    -            "501:com.apple.photoanalysisd.backgroundanalysis" = "2024-09-30 20:14:26 +0000";
    +            "501:com.apple.photoanalysisd.backgroundanalysis" = "2024-10-01 00:02:14 +0000";
                 "501:com.apple.photoanalysisd.graphNonIntensiveTasks" = "2024-09-30 20:20:03 +0000";
                 "501:com.apple.photoanalysisd.internal" = "2024-09-15 01:53:51 +0000";
                 "501:com.apple.photoanalysisd.music" = "2024-09-30 17:05:59 +0000";
    @@ -2312,7 +2312,7 @@
             LastOSLaunchVersion = 24A335;
             "baseDate.LocalBeaconingManager" = "2023-06-01 15:07:33 +0000";
             lastAttemptDate = "2024-08-15 01:48:08 +0000";
    -        lastFinderAttemptDate = "2024-10-01 01:23:05 +0000";
    +        lastFinderAttemptDate = "2024-10-01 01:23:36 +0000";
             lastFinderPublishDates =         {
                 batteryWiFi =             (
                     "2024-09-21 01:07:39 +0000",
    @@ -3859,7 +3859,7 @@
                 "com.apple.awdd.publication" = "2024-09-29 18:17:55 +0000";
                 "com.apple.awdd.trigger:0x7f004:86400:2" = "2024-09-28 13:31:28 +0000";
                 "com.apple.awdd.trigger:0x7f006:14400:2" = "2024-09-30 05:24:18 +0000";
    -            "com.apple.backupd-auto" = "2024-10-01 01:08:37 +0000";
    +            "com.apple.backupd-auto" = "2024-10-01 01:38:37 +0000";
                 "com.apple.backupd-auto.dryspell" = "2024-09-28 01:22:46 +0000";
                 "com.apple.backupd.analytics" = "2024-09-28 05:36:06 +0000";
                 "com.apple.biome.prune-expired-events" = "2024-09-28 03:56:23 +0000";
    Comparing Domain Specific
    Comparison complete.Code language: PHP (php)

    You can see that there’s a lot of background noise, but in there is what I was looking for: com.apple.dock and autohide = 1.

    Now I can head over to macos-defaults.com and I can search for com.apple.doc and find the command to set the dock to auto hide.

    defaults write com.apple.dock "autohide" -bool "true" && killall Dock

    Thinking about it, this was probably a dumb example as it was easy to just go to macos-defaults.com and search for “autohide” to find it.

    Well, any way do what you want with this stupid thing.

  • BUTTS

    BUTTS

    echo $(echo 66 85 84 84 83 | awk '{for(i=1;i<=NF;i++) printf "%c", $i}')

  • Quick Tip: Add Screen Name to Bash Prompt

    Quick Tip: Add Screen Name to Bash Prompt

    I often SSH into servers to get some work done, and one of the things I discovered recently is that I may not always know or remember if I’m in a screen session.

    So I had the bright idea to just add it to my shell prompt!

    Simply just add one of these to your RC file of choice:

    Bash

    # Add Screen name to PS1 if we're in a screen.
    if [ -n "$STY" ]; then
    	PS1="\[\e[1m\](Screen: $STY)\[\e[0m\]\n$PS1"
    fiCode language: PHP (php)

    ZSH

    # Add Screen name to PROMPT if we're in a screen.
    if [[ -n "$STY" ]]; then
    	PROMPT="%B(Screen: $STY)%b"$'\n'"$PROMPT"
    fiCode language: PHP (php)

    And remember, if you’re asking yourself if you should run something in a screen, you’re already too late!

  • Bash Script: Calculate before/after averages

    Bash Script: Calculate before/after averages

    I’ve been doing some performance testing, and wanted a quick way to test how well or poorly changes affect a site. Normally I’d whip out the ol’ calculator app and do this manually. That got tiring after a while, so instead with the help of ChatGPT, I made this little bash script that will do the work for you:

    #!/bin/bash
    
    # Function to calculate the average of a list of numbers
    average() {
    	local sum=0
    	local count=0
    
    	for num in "$@"; do
    		sum=$(echo "$sum + $num" | bc -l)
    		count=$((count+1))
    	done
    
    	echo "$sum / $count" | bc -l
    }
    
    # Parse arguments
    for i in "$@"; do
    	case $i in
    		--before=*)
    		BEFORE="${i#*=}"
    		shift
    		;;
    		--after=*)
    		AFTER="${i#*=}"
    		shift
    		;;
    		*)
    		# unknown option
    		;;
    	esac
    done
    
    # Check if both BEFORE and AFTER parameters are provided
    if [ -z "$BEFORE" ] || [ -z "$AFTER" ]; then
    	echo "Error: Missing required parameters."
    	echo "Usage: $0 --before=<comma-separated-values> --after=<comma-separated-values>"
    	exit 1
    fi
    
    IFS=',' read -ra BEFORE_LIST <<< "$BEFORE"
    IFS=',' read -ra AFTER_LIST <<< "$AFTER"
    
    # Calculate average for before and after lists
    BEFORE_AVG=$(printf "%.2f\n" $(average "${BEFORE_LIST[@]}"))
    AFTER_AVG=$(printf "%.2f\n" $(average "${AFTER_LIST[@]}"))
    
    echo "Before average: $BEFORE_AVG"
    echo "After average: $AFTER_AVG"
    
    # Calculate average percent increase, decrease or no change for the list
    if [ "$BEFORE_AVG" != "0.00" ]; then
    	PERCENT_CHANGE=$(echo "(($AFTER_AVG - $BEFORE_AVG) / $BEFORE_AVG) * 100" | bc -l)
    	if [ "$(echo "$PERCENT_CHANGE > 0" | bc -l)" -eq 1 ]; then
    		printf "Average percent increased: %.2f%%\n" "$PERCENT_CHANGE"
    	elif [ "$(echo "$PERCENT_CHANGE < 0" | bc -l)" -eq 1 ]; then
    		printf "Average percent decreased: %.2f%%\n" "$PERCENT_CHANGE" | tr -d '-'
    	else
    		echo "No change in average."
    	fi
    else
    	echo "Percent change from before to after: undefined (division by zero)"
    fi
    Code language: Bash (bash)

    It runs like this:

    $ average.sh --before=13.07,9.75,16.14,7.71,10.32 --after=1.22,1.28,1.13,1.19,1.26
    Before average: 11.40
    After average: 1.22
    Average percent decreased: 89.30%
    

    In this instance, it was calculating seconds–but you need to remember that it only goes to two decimal places, so if you need something finer you’ll need to adjust the code or your inputs.

    Happy Slacking!

  • Supercharge Your Clipboard with a Shell Function for iTerm2

    Supercharge Your Clipboard with a Shell Function for iTerm2

    Well, I’ve been playing with ChatGPT more, and I had ChatGPT (GPT-4) write this blog post for me. I told it that I was going to give it some of my blog posts to determine my writing style, and to have it write me a blog post about this bash function. It’s …. meh … not really “my style” but whatever.

    The idea of this shell function is to integrate with iTerm2’s proprietary escape sequences to allow you to pipe content right to your clipboard. I’m using the name pbcopy from MacOS so that I don’t need to think about it, and can use pbcopy anywhere. Now I can work on a remote server and cat stupid-stuff.log | pbcopy and get a copy of it locally to mess with, without having to click and drag or download the file. Super lazy ❇

    Enjoy the AI blog post:


    Hey folks, today we’re going to explore a neat shell function that can make your life a lot easier when you’re working in iTerm2 on macOS. The purpose of this function is to make the built-in pbcopy command work seamlessly in iTerm2, even when the command isn’t available in your shell environment.

    Without further ado, let’s dive into the code:

    # pbcopy via iTerm2
    function pbcopy() {
    	if which pbcopy >/dev/null 2>&1; then
    		pbcopy "$@"
    	else
    		# Replace ^[ with the ASCII escape character
    		local start="\e]1337;CopyToClipboard\a"
    		local end="\e]1337;EndCopy\a"
    		printf "${start}$(cat)${end}"
    	fi
    }
    Code language: PHP (php)

    You might be wondering how this magic works. Let’s break it down step by step:

    1. The function first checks if the pbcopy command is available in your shell environment using the which command. If it is, it simply calls the built-in pbcopy command with the provided arguments.
    2. If the pbcopy command isn’t available, the function creates two local variables, start and end, containing special escape sequences that iTerm2 understands.
    3. The printf command is then used to concatenate the start escape sequence, the contents of the input (read using cat), and the end escape sequence. This formatted string is what gets sent to iTerm2.

    When iTerm2 receives this specially formatted string, it recognizes the escape sequences and takes care of copying the text between them to the system clipboard.

    And that’s it! With this function in your shell environment, you can copy text to your clipboard using pbcopy without any hassle, whether the command is natively available or not. To use this function, simply add it to your shell startup file (like .bashrc or .zshrc), and you’re good to go.

    In a practical scenario, let’s say you’re working on a project and you want to share the contents of a log file with a colleague. Instead of manually opening the log file, selecting its content, and copying it to your clipboard, you can use this shell script to streamline the process. With the pbcopy function added to your shell startup file, simply open your terminal and run the command cat logfile.txt | pbcopy. This command will read the content of the logfile.txt file and pipe it directly to the pbcopy function, which in turn places the content into your clipboard. Now you can easily paste the log file content into an email, chat application, or any other medium you choose, all without leaving the comfort of your terminal. This not only saves time but also enhances your overall productivity when working with text-based data in a command-line environment.

    In conclusion, this shell function is a fantastic little tool to enhance your iTerm2 experience on macOS. I hope you find it as useful as I do. As always, if you have any questions or comments, feel free to drop them below. Happy coding!


    Happy coding, indeed 🤖

  • Gathering database performance with WP-CLI

    Gathering database performance with WP-CLI

    Recently at work, my team was asked to help gather data about database server performance before and after an upgrade. To help with this, we collected a number of heavy database pages on some WordPress sites, dumped every query running to generate the page, and grabbed them to profile.

    I whipped up this quick and dirty WP-CLI command that will run a list of SQL queries 1,000 times and give you the minimum time, maximum time, average time, and standard deviation for each query.

    With this data, you can re-run the same queries again after a server change to determine if there has been any major SQL performance differences:

    /**
     * Profiles DB performance by running SQL queries and returning timing statistics.
     */
    public function db_profile( $args, $assoc_args ) {
    	$format = WP_CLI\Utils\get_flag_value( $assoc_args, 'format', 'table' );
    
    // Put all of your queries here!
    $site_sql_lines = <<<END
    SHOW TABLES LIKE 'wp_posts';
    END;
    
    	$lines = explode( PHP_EOL, $site_sql_lines );
    
    	global $wpdb;
    
    	$stats         = array();
    	$total_time_us = 0;
    	$runs          = 100;
    	$progress      = \WP_CLI\Utils\make_progress_bar( sprintf( 'Running %s Queries', number_format( count( $lines ) * $runs ) ), count( $lines ) * $runs );
    
    	for ( $run = 1; $run <= $runs; $run++ ) {
    		foreach ( $lines as $index => $line ) {
    			if ( ! isset( $stats[ $index ] ) || ! is_array( $stats[ $index ] ) ) {
    				$stats[ $index ] = array(
    					'query' => $line,
    					'runs'  => array(),
    				);
    			}
    
    			$start_time = hrtime( true );
    			$results    = count( $wpdb->get_results( $line, ARRAY_N ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
    			$end_time   = hrtime( true );
    			$time_us    = ( $end_time - $start_time ) / 1000;
    
    			$total_time_us += $time_us;
    
    			$stats[ $index ]['runs'][ $run ] = $time_us;
    			$progress->tick();
    		}
    	}
    
    	$data = array();
    
    	foreach ( $stats as $index => $stat ) {
    		$stats[ $index ]['stats'] = array(
    			'min' => min( $stats[ $index ]['runs'] ),
    			'max' => max( $stats[ $index ]['runs'] ),
    			'avg' => array_sum( $stats[ $index ]['runs'] ) / count( $stats[ $index ]['runs'] ),
    		);
    
    		$data[ $index ] = array(
    			'Query'  => $stat['query'],
    			'Min'    => number_format( (float) $stats[ $index ]['stats']['min'] / 1000, 3 ),
    			'Max'    => number_format( (float) $stats[ $index ]['stats']['max'] / 1000, 3 ),
    			'Avg'    => number_format( (float) $stats[ $index ]['stats']['avg'] / 1000, 3 ),
    			'StdDev' => number_format( (float) $this->stats_standard_deviation( $stats[ $index ]['runs'] ) / 1000, 3 ),
    		);
    
    		if ( 'csv' === $format ) {
    			$data[ $index ]['Runs'] = wp_json_encode( $stats[ $index ]['runs'] );
    		}
    	}
    
    	$progress->finish();
    
    	// Output data.
    	WP_CLI\Utils\format_items( $format, $data, array_keys( $data[0] ) );
    
    	// Output totals if we're not piping somewhere.
    	if ( ! WP_CLI\Utils\isPiped() ) {
    		WP_CLI::success(
    			sprintf(
    				'Total queries ran: %s, DB Time Taken: %s',
    				WP_CLI::colorize( '%g' . number_format( count( $lines ) * $runs, 0 ) . '%n' ),
    				WP_CLI::colorize( '%g' . $this->convert_to_human_readable( $total_time_us ) . '%n' ),
    			)
    		);
    	}
    }
    
    Code language: PHP (php)

    You can then run it and gather the data as a table, or as a CSV file (--format=csv) which is much more likely to be useful.