diff --git a/dots/copilot.sh b/dots/copilot.sh deleted file mode 100755 index 4149a28..0000000 --- a/dots/copilot.sh +++ /dev/null @@ -1,272 +0,0 @@ -#!/bin/sh - -# GitHub Copilot client ID -CLIENT_ID="Iv1.b507a08c87ecfe98" - -# Token cache file location -CACHE_DIR="${HOME}/.config/crush/github-copilot" -CACHE_FILE="${CACHE_DIR}/bearer_token" -GITHUB_TOKEN_FILE="${CACHE_DIR}/github_token" -GITHUB_COPILOT_APPS_FILE="${HOME}/.config/github-copilot/apps.json" - -# Function to extract OAuth token from GitHub Copilot apps.json -extract_oauth_token() { - local client_id="$1" - if [ -f "$GITHUB_COPILOT_APPS_FILE" ]; then - # Extract the oauth_token for our client ID (key format is "github.com:CLIENT_ID") - local oauth_token=$(grep -o "\"github.com:${client_id}\":{[^}]*\"oauth_token\":\"[^\"]*" "$GITHUB_COPILOT_APPS_FILE" | grep -o '"oauth_token":"[^"]*' | cut -d'"' -f4) - echo "$oauth_token" - fi -} - -# Function to extract expiration from token -extract_expiration() { - local token="$1" - echo "$token" | grep -o 'exp=[0-9]*' | cut -d'=' -f2 -} - -# Function to check if token is valid -is_token_valid() { - local token="$1" - local exp=$(extract_expiration "$token") - - if [ -z "$exp" ]; then - return 1 - fi - - local current_time=$(date +%s) - # Add 60 second buffer before expiration - local buffer_time=$((exp - 60)) - - if [ "$current_time" -lt "$buffer_time" ]; then - return 0 - else - return 1 - fi -} - -# Function to exchange GitHub token for bearer token -exchange_github_token() { - local github_token="$1" - local bearer_response=$(curl -s -X GET "https://api.github.com/copilot_internal/v2/token" \ - -H "Authorization: Token ${github_token}" \ - -H "User-Agent: CRUSH/1.0") - - local bearer_token=$(echo "$bearer_response" | grep -o '"token":"[^"]*' | cut -d'"' -f4) - echo "$bearer_token" -} - -# Check for cached bearer token -if [ -f "$CACHE_FILE" ]; then - CACHED_TOKEN=$(cat "$CACHE_FILE") - if is_token_valid "$CACHED_TOKEN"; then - # Token is still valid, output and exit - echo "$CACHED_TOKEN" - exit 0 - fi -fi - -# Bearer token is expired/missing, try to use cached GitHub token -if [ -f "$GITHUB_TOKEN_FILE" ]; then - GITHUB_TOKEN=$(cat "$GITHUB_TOKEN_FILE") - if [ -n "$GITHUB_TOKEN" ]; then - # Try to exchange GitHub token for new bearer token - BEARER_TOKEN=$(exchange_github_token "$GITHUB_TOKEN") - if [ -n "$BEARER_TOKEN" ]; then - # Successfully got new bearer token, cache it - echo "$BEARER_TOKEN" > "$CACHE_FILE" - chmod 600 "$CACHE_FILE" - echo "$BEARER_TOKEN" - exit 0 - fi - fi -fi - -# Try to get OAuth token from GitHub Copilot apps.json -OAUTH_TOKEN=$(extract_oauth_token "$CLIENT_ID") -if [ -n "$OAUTH_TOKEN" ]; then - # Try to exchange OAuth token for new bearer token - BEARER_TOKEN=$(exchange_github_token "$OAUTH_TOKEN") - if [ -n "$BEARER_TOKEN" ]; then - # Successfully got new bearer token, cache it - mkdir -p "$CACHE_DIR" - echo "$BEARER_TOKEN" > "$CACHE_FILE" - chmod 600 "$CACHE_FILE" - echo "$OAUTH_TOKEN" > "$GITHUB_TOKEN_FILE" - chmod 600 "$GITHUB_TOKEN_FILE" - echo "$BEARER_TOKEN" - exit 0 - fi -fi - -# Step 1: Get device code -DEVICE_RESPONSE=$(curl -s -X POST "https://github.com/login/device/code" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -H "User-Agent: CRUSH/1.0" \ - -H "Accept: application/json" \ - -d "client_id=${CLIENT_ID}&scope=user:email read:user copilot") - -# Extract values from response -DEVICE_CODE=$(echo "$DEVICE_RESPONSE" | grep -o '"device_code":"[^"]*' | cut -d'"' -f4) -USER_CODE=$(echo "$DEVICE_RESPONSE" | grep -o '"user_code":"[^"]*' | cut -d'"' -f4) -VERIFICATION_URI=$(echo "$DEVICE_RESPONSE" | grep -o '"verification_uri":"[^"]*' | cut -d'"' -f4) -INTERVAL=$(echo "$DEVICE_RESPONSE" | grep -o '"interval":[0-9]*' | cut -d':' -f2) -EXPIRES_IN=$(echo "$DEVICE_RESPONSE" | grep -o '"expires_in":[0-9]*' | cut -d':' -f2) - -# Ensure minimum interval -if [ "$INTERVAL" -lt 5 ]; then - INTERVAL=5 -fi - -# Step 2: Create a temporary HTML file with the code -TEMP_HTML="/tmp/copilot_auth_$$.html" -cat > "$TEMP_HTML" << EOF - - - - GitHub Copilot Authentication - - - -
-

GitHub Copilot Authentication

-

Copy this code:

-
$USER_CODE
- Copy Code & Continue to GitHub -

Click the button to copy the code and go to GitHub

-
- - - -EOF - -# Open the HTML file (suppress all output to avoid issues in POSIX shell) -if command -v xdg-open >/dev/null 2>&1; then - xdg-open "$TEMP_HTML" >/dev/null 2>&1 & -elif command -v open >/dev/null 2>&1; then - open "$TEMP_HTML" >/dev/null 2>&1 & -elif command -v start >/dev/null 2>&1; then - start "$TEMP_HTML" >/dev/null 2>&1 & -fi - -# Step 3: Poll for token -POLL_COUNT=0 -MAX_POLLS=60 -GITHUB_TOKEN="" - -# Initial delay -sleep 2 - -while [ $POLL_COUNT -lt $MAX_POLLS ] && [ -z "$GITHUB_TOKEN" ]; do - TOKEN_RESPONSE=$(curl -s -X POST "https://github.com/login/oauth/access_token" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -H "User-Agent: CRUSH/1.0" \ - -H "Accept: application/json" \ - -d "client_id=${CLIENT_ID}&device_code=${DEVICE_CODE}&grant_type=urn:ietf:params:oauth:grant-type:device_code") - - # Check for access token - ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*' | cut -d'"' -f4) - - if [ -n "$ACCESS_TOKEN" ]; then - GITHUB_TOKEN="$ACCESS_TOKEN" - break - fi - - # Check for errors - ERROR=$(echo "$TOKEN_RESPONSE" | grep -o '"error":"[^"]*' | cut -d'"' -f4) - - case "$ERROR" in - "authorization_pending") - # Still waiting, continue - ;; - "slow_down") - # Rate limiting - exit 1 - ;; - "expired_token") - # Token expired - exit 1 - ;; - *) - if [ -n "$ERROR" ]; then - exit 1 - fi - ;; - esac - - POLL_COUNT=$((POLL_COUNT + 1)) - sleep $INTERVAL -done - -if [ -z "$GITHUB_TOKEN" ]; then - exit 1 -fi - -# Step 4: Exchange GitHub token for Copilot bearer token -BEARER_TOKEN=$(exchange_github_token "$GITHUB_TOKEN") - -if [ -z "$BEARER_TOKEN" ]; then - exit 1 -fi - -# Create cache directory if it doesn't exist -mkdir -p "$CACHE_DIR" - -# Cache both tokens -echo "$BEARER_TOKEN" > "$CACHE_FILE" -chmod 600 "$CACHE_FILE" -echo "$GITHUB_TOKEN" > "$GITHUB_TOKEN_FILE" -chmod 600 "$GITHUB_TOKEN_FILE" - -# Output only the bearer token to stdout -echo "$BEARER_TOKEN" diff --git a/dots/dump-keybinds-mac.sh b/dots/dump-keybinds-mac.sh deleted file mode 100755 index 5f28aee..0000000 --- a/dots/dump-keybinds-mac.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash - -defaults export com.apple.symbolichotkeys - > /tmp/symbolichotkeys.plist -plutil -convert json -o /tmp/symbolichotkeys.json /tmp/symbolichotkeys.plist -cat /tmp/symbolichotkeys.json | \ -jq -r ' - .AppleSymbolicHotKeys - | to_entries - | map( - " \"" + .key + "\" = {\n" + - " enabled = " + ( - if (.value.enabled == 1 or .value.enabled == true) then - "true" - else - "false" - end - ) + ";\n" + - ( - if (.value.value? and .value.value.parameters? and .value.value.type?) then - " value = {\n" + - " parameters = [ " + ( - .value.value.parameters - | map(tostring) - | join(" ") - ) + " ];\n" + - " type = \"" + .value.value.type + "\";\n" + - " };\n" - else - "" - end - ) + - " };\n" - ) - | " AppleSymbolicHotKeys = {\n" + ( join("") ) + " };\n" -' - diff --git a/dots/hyprrec.sh b/dots/hyprrec.sh index aba2642..bfab7a7 100755 --- a/dots/hyprrec.sh +++ b/dots/hyprrec.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash +## ## Requirements: ## - `slurp`: to select an area ## - `notify-send`: to show notifications (provided by libnotify) @@ -21,24 +23,24 @@ if [ -n "$DEFAULT_DEVICE" ]; then MONITOR_DEVICE="${DEFAULT_DEVICE}.monitor" # Check if the device exists by attempting to get its properties - wpctl inspect $(wpctl status | grep -A 1 "Built-in Audio Analog Stereo" | grep -o '[0-9]\+\.' | head -1 | tr -d '.') > /dev/null 2>&1 - if [ $? -eq 0 ]; then + DEVICE_ID=$(wpctl status | grep -A 1 "Built-in Audio Analog Stereo" | grep -o '[0-9]\+\.' | head -1 | tr -d '.') + if [ -n "$DEVICE_ID" ] && wpctl inspect "$DEVICE_ID" > /dev/null 2>&1; then echo "Found audio device, will use monitor: $MONITOR_DEVICE" - AUDIO_ARGS="--audio --audio-device \"$MONITOR_DEVICE\"" + AUDIO_ARGS=(--audio --audio-device "$MONITOR_DEVICE") else echo "Couldn't confirm monitor device, falling back to default audio" - AUDIO_ARGS="--audio" + AUDIO_ARGS=(--audio) fi else echo "No default audio device found, falling back to default audio capture" - AUDIO_ARGS="--audio" + AUDIO_ARGS=(--audio) fi # Process arguments to determine if full screen or area selection if [ "$1" = "fullscreen" ]; then # Full screen recording notify-send -t 1000 -a "wl-screenrec" "Starting full screen recording" - ARGS="" + ARGS=() else # Area selection notify-send -t 1000 -a "wl-screenrec" "Select area or window to record" @@ -52,21 +54,23 @@ else exit 1 fi notify-send -t 1000 -a "wl-screenrec" "Starting area recording" - ARGS="-g \"$GEOMETRY\"" + ARGS=(-g "$GEOMETRY") fi -# Start recording with the selected parameters -touch /tmp/notify_result.txt -eval "wl-screenrec $ARGS $AUDIO_ARGS -f \"$FILE\"" && \ +# Start recording. Arrays rather than eval: a path or a geometry carrying a +# space should be one argument, not a new command. +NOTIFY_RESULT=$(mktemp -t hyprrec.XXXXXX) +trap 'rm -f "$NOTIFY_RESULT"' EXIT +wl-screenrec "${ARGS[@]}" "${AUDIO_ARGS[@]}" -f "$FILE" && \ # Create a thumbnail from the recording ffmpeg -i "$FILE" -ss 00:00:00 -vframes 1 -update 1 -frames:v 1 /tmp/screenrec_thumbnail.png -y && \ # Notify that recording is saved with clickable action notify-send -a "wl-screenrec" "Recording saved to $FILE" \ -i "/tmp/screenrec_thumbnail.png" \ - -A "default=Open" > /tmp/notify_result.txt + -A "default=Open" > "$NOTIFY_RESULT" # Check if notification was clicked -if [ -f /tmp/notify_result.txt ] && grep -q "default" /tmp/notify_result.txt; then +if grep -q "default" "$NOTIFY_RESULT"; then if command -v xdg-open > /dev/null; then xdg-open "$FILE" elif command -v gdbus > /dev/null; then diff --git a/dots/prettify-ss.sh b/dots/prettify-ss.sh index ec22785..261afa9 100644 --- a/dots/prettify-ss.sh +++ b/dots/prettify-ss.sh @@ -1,4 +1,6 @@ -#!/bin/bash +#!/usr/bin/env bash + +set -euo pipefail # Define temporary files TEMP_IMAGE=$(mktemp --suffix=.png) @@ -7,14 +9,16 @@ MASK_IMAGE=$(mktemp --suffix=.png) GRADIENT_IMAGE=$(mktemp --suffix=.png) FINAL_IMAGE=$(mktemp --suffix=.png) +# Clean up whatever we got to, not just the happy path. +trap 'rm -f "$TEMP_IMAGE" "$MASK_IMAGE" "$SHADOWED_IMAGE" "$GRADIENT_IMAGE" "$FINAL_IMAGE"' EXIT + # Grab the clipboard image wl-paste --type image/png > "$TEMP_IMAGE" -# Extract standout colors (3 dominant colors) +# Extract standout colors, and gradient between the two furthest apart COLORS=$(magick "$TEMP_IMAGE" -colors 3 -unique-colors txt: | grep -oP '#[0-9A-Fa-f]{6}') COLOR1=$(echo "$COLORS" | head -n 1) -COLOR2=$(echo "$COLORS" | tail -n 1 | head -n 1) -COLOR3=$(echo "$COLORS" | tail -n 1) +COLOR2=$(echo "$COLORS" | tail -n 1) # Get original image dimensions WIDTH=$(magick identify -format "%w" "$TEMP_IMAGE") @@ -41,7 +45,4 @@ magick "$GRADIENT_IMAGE" "$SHADOWED_IMAGE" -gravity center -composite "$FINAL_IM # Copy the final image back to the clipboard wl-copy < "$FINAL_IMAGE" -# Clean up temporary files -rm "$TEMP_IMAGE" "$MASK_IMAGE" "$SHADOWED_IMAGE" "$GRADIENT_IMAGE" "$FINAL_IMAGE" - diff --git a/dots/update-crush-models.sh b/dots/update-crush-models.sh deleted file mode 100755 index b6dd1f6..0000000 --- a/dots/update-crush-models.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Configuration -COPILOT_TOKEN=$(bash ~/.config/crush/copilot.sh) -ANTHROPIC_TOKEN=$(bunx anthropic-api-key) - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo -e "${GREEN}Fetching latest models from APIs...${NC}" - -# Fetch Copilot models -echo " → Fetching Copilot models..." -COPILOT_MODELS=$(curl -s https://api.githubcopilot.com/models \ - -H "Authorization: Bearer $COPILOT_TOKEN" \ - -H "Editor-Version: CRUSH/1.0" \ - -H "Editor-Plugin-Version: CRUSH/1.0" \ - -H "Copilot-Integration-Id: vscode-chat") - -# Fetch Anthropic models -echo " → Fetching Anthropic models..." -ANTHROPIC_MODELS=$(curl -s https://api.anthropic.com/v1/models \ - -H "Authorization: Bearer $ANTHROPIC_TOKEN" \ - -H "anthropic-version: 2023-06-01" \ - -H "anthropic-beta: oauth-2025-04-20") - -# Extract model-picker enabled Copilot models -echo -e "\n${GREEN}Copilot models (model_picker_enabled):${NC}" -echo "$COPILOT_MODELS" | jq -r '.data[] | select(.model_picker_enabled == true) | " ✓ \(.id) - \(.name)"' - -# Extract all Anthropic models -echo -e "\n${GREEN}Anthropic models:${NC}" -echo "$ANTHROPIC_MODELS" | jq -r '.data[] | " ✓ \(.id) - \(.display_name)"' - -# Generate Copilot models Nix config -generate_copilot_models() { - echo "$COPILOT_MODELS" | jq -r '.data[] | select(.model_picker_enabled == true) | - (if .capabilities.supports.tool_calls then true else false end) as $can_reason | - (if .capabilities.supports.vision then true else false end) as $supports_attachments | - " {\n" + - " id = \"\(.id)\";\n" + - " name = \"Copilot: \(.name)\";\n" + - " cost_per_1m_in = 0;\n" + - " cost_per_1m_out = 0;\n" + - " cost_per_1m_in_cached = 0;\n" + - " cost_per_1m_out_cached = 0;\n" + - " context_window = \(.capabilities.limits.max_context_window_tokens);\n" + - " default_max_tokens = \(.capabilities.limits.max_output_tokens);\n" + - " can_reason = \($can_reason);\n" + - " has_reasoning_efforts = false;\n" + - " supports_attachments = \($supports_attachments);\n" + - " }"' -} - -# Generate Anthropic models Nix config with pricing -generate_anthropic_models() { - echo "$ANTHROPIC_MODELS" | jq -r '.data[] | - # Determine pricing based on model family - (if (.id | contains("opus-4")) then - {in: 15.0, out: 75.0, in_cached: 1.5, out_cached: 75.0} - elif (.id | contains("sonnet-4")) then - {in: 3.0, out: 15.0, in_cached: 0.225, out_cached: 15.0} - elif (.id | contains("3-7-sonnet")) then - {in: 2.5, out: 12.0, in_cached: 0.187, out_cached: 12.0} - elif (.id | contains("3-5-sonnet")) then - {in: 3.0, out: 15.0, in_cached: 0.225, out_cached: 15.0} - elif (.id | contains("3-5-haiku")) then - {in: 0.8, out: 4.0, in_cached: 0.06, out_cached: 4.0} - elif (.id | contains("3-haiku")) then - {in: 0.25, out: 1.25, in_cached: 0.03, out_cached: 1.25} - elif (.id | contains("3-opus")) then - {in: 15.0, out: 75.0, in_cached: 1.5, out_cached: 75.0} - else - {in: 3.0, out: 15.0, in_cached: 0.225, out_cached: 15.0} - end) as $pricing | - - # Determine context window and max tokens - (if (.id | test("claude-sonnet-4-5")) - then {context: 200000, max_tokens: 64000} - elif (.id | test("claude-sonnet-4-")) - then {context: 200000, max_tokens: 64000} - elif (.id | test("claude-3-7-sonnet")) - then {context: 200000, max_tokens: 64000} - elif (.id | test("claude-opus-4-1")) - then {context: 200000, max_tokens: 32000} - elif (.id | test("claude-opus-4-")) - then {context: 200000, max_tokens: 32000} - elif (.id | test("claude-3-5-haiku")) - then {context: 200000, max_tokens: 8192} - elif (.id | test("claude-3-haiku")) - then {context: 200000, max_tokens: 4096} - else - {context: 200000, max_tokens: 8192} - end) as $limits | - - # Determine has_reasoning_efforts - (if (.id | contains("opus-4")) then true else false end) as $has_reasoning | - - " {\n" + - " id = \"\(.id)\";\n" + - " name = \"\(.display_name)\";\n" + - " cost_per_1m_in = \($pricing.in);\n" + - " cost_per_1m_out = \($pricing.out);\n" + - " cost_per_1m_in_cached = \($pricing.in_cached);\n" + - " cost_per_1m_out_cached = \($pricing.out_cached);\n" + - " context_window = \($limits.context);\n" + - " default_max_tokens = \($limits.max_tokens);\n" + - " can_reason = true;\n" + - " has_reasoning_efforts = \($has_reasoning);\n" + - " supports_attachments = true;\n" + - " }"' -} - -# Generate formatted model arrays -COPILOT_MODELS_NIX=$(generate_copilot_models) -ANTHROPIC_MODELS_NIX=$(generate_anthropic_models) - -# Output results -echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}" -echo -e "${YELLOW}COPILOT MODELS${NC}" -echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" -echo -e "\nPaste this into your copilot.models array:\n" -echo " models = [" -echo "$COPILOT_MODELS_NIX" -echo " ];" - -echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}" -echo -e "${YELLOW}ANTHROPIC MODELS${NC}" -echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" -echo -e "\nPaste this into your claude-pro.models array:\n" -echo " models = [" -echo "$ANTHROPIC_MODELS_NIX" -echo " ];" - -echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}" -echo -e "\n${GREEN}Summary:${NC}" -echo " • Copilot models: $(echo "$COPILOT_MODELS" | jq '[.data[] | select(.model_picker_enabled == true)] | length')" -echo " • Anthropic models: $(echo "$ANTHROPIC_MODELS" | jq '.data | length')" diff --git a/modules/home/apps/halloy.nix b/modules/home/apps/halloy.nix index 0a04efc..a35fcb5 100644 --- a/modules/home/apps/halloy.nix +++ b/modules/home/apps/halloy.nix @@ -42,7 +42,7 @@ "#meta" "#fraud-land" ]; - channel-keys = { + channel_keys = { fraud-land = "fraudpheus"; }; }; diff --git a/modules/home/system/shell.nix b/modules/home/system/shell.nix index 82183e4..030be03 100644 --- a/modules/home/system/shell.nix +++ b/modules/home/system/shell.nix @@ -149,12 +149,16 @@ let host="hackatime.hackclub.com" fi + # A predictable name in a shared directory is somebody else's to write. + summary=$(mktemp -t hackatime.XXXXXX) + trap 'rm -f "$summary"' EXIT + ${pkgs.gum}/bin/gum spin --spinner dot --title "Fetching summary from $host for $user_id..." -- \ ${pkgs.curl}/bin/curl -s -X 'GET' \ "https://$host/api/summary?user=''${user_id}&interval=month" \ -H 'accept: application/json' \ -H 'Authorization: Bearer 2ce9e698-8a16-46f0-b49a-ac121bcfd608' \ - > /tmp/hackatime-$$.json + > "$summary" ${pkgs.gum}/bin/gum style --bold --foreground 212 "Summary for $user_id" echo @@ -168,7 +172,7 @@ let else 0 end - ' /tmp/hackatime-$$.json) + ' "$summary") if [[ "$total_seconds" -gt 0 ]]; then hours=$((total_seconds / 3600)) @@ -190,7 +194,7 @@ let else " No projects" end - ' /tmp/hackatime-$$.json + ' "$summary" echo @@ -203,9 +207,7 @@ let else " No languages" end - ' /tmp/hackatime-$$.json - - rm -f /tmp/hackatime-$$.json + ' "$summary" ''; now = pkgs.writeShellScriptBin "now" '' @@ -228,9 +230,13 @@ let esac done - # Load account information from agenix secrets + # Load account information from agenix secrets. Exported rather than + # spliced into the script below: a password with a quote in it would + # otherwise end the string it was sitting in. if [[ -f "/run/agenix/bluesky" ]]; then + set -a source "/run/agenix/bluesky" + set +a else ${pkgs.gum}/bin/gum style --foreground 196 "Error: Bluesky credentials file not found at /run/agenix/bluesky" exit 1 @@ -245,6 +251,8 @@ let fi fi + export message + ${pkgs.gum}/bin/gum spin --spinner dot --title "Posting to Bluesky..." -- /bin/bash <