diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-07 23:46:42 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-07 23:46:42 +0200 |
| commit | f408ae894e4e10bbf4358c7c07bb9618ef1353e4 (patch) | |
| tree | 0722f09473a58f1b9027e271cd6bd0e96e7a390d /scripts | |
| parent | bf56ef64278a90acab9116311e649e762d39cd34 (diff) | |
rename
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/README.md | 3 | ||||
| -rwxr-xr-x | scripts/ai | 7 | ||||
| -rw-r--r-- | scripts/brokenlinkfinder | 73 | ||||
| -rwxr-xr-x | scripts/gvim | 7 | ||||
| -rwxr-xr-x | scripts/hx.aichat-prompt | 9 | ||||
| -rwxr-xr-x | scripts/hx.chatgpt-prompt | 3 | ||||
| -rwxr-xr-x | scripts/hx.goformatter | 3 | ||||
| -rwxr-xr-x | scripts/hx.hexai-prompt | 9 | ||||
| -rwxr-xr-x | scripts/hx.nvim-copilot-prompt | 32 | ||||
| -rwxr-xr-x | scripts/hx.prompt | 14 | ||||
| -rw-r--r-- | scripts/randomnote.rb | 30 | ||||
| -rw-r--r-- | scripts/taskwarriorfeeder.rb | 220 | ||||
| -rwxr-xr-x | scripts/tmux-edit-send | 246 | ||||
| -rwxr-xr-x | scripts/wol-f3s | 86 |
14 files changed, 742 insertions, 0 deletions
diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..ecbc8ec --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,3 @@ +# Scripts installed to my ~/scripts + +Mostly quick-n-dirty ones! diff --git a/scripts/ai b/scripts/ai new file mode 100755 index 0000000..abcf490 --- /dev/null +++ b/scripts/ai @@ -0,0 +1,7 @@ +#!/usr/bin/env zsh + +if [ $(uname) = Darwin ]; then + exec hx.nvim-copilot-prompt "$@" +else + exec hx.hexai-prompt "$@" +fi diff --git a/scripts/brokenlinkfinder b/scripts/brokenlinkfinder new file mode 100644 index 0000000..7fe1576 --- /dev/null +++ b/scripts/brokenlinkfinder @@ -0,0 +1,73 @@ +#!/usr/bin/env ruby + +require 'net/http' +require 'uri' +require 'nokogiri' +require 'set' + +# Method to fetch and parse HTML from a URL +def fetch_html(url) + response = Net::HTTP.get_response(URI(url)) + response.body if response.is_a?(Net::HTTPSuccess) +rescue StandardError => e + puts "Error fetching #{url}: #{e.message}" + nil +end + +# Method to find and check links on a page +def check_links(url, domain) + html = fetch_html(url) + return unless html + + checked = Set.new + broken = Set.new + + document = Nokogiri::HTML(html) + links = document.css('a').map { |link| link['href'] }.compact + + internal_links = links.select do |link| + link.start_with?('/') || link.start_with?('./') || URI(link).host == domain + end + puts "Internal links: #{internal_links}" + + internal_links.uniq.each do |link| + full_url = link.start_with?('/') || link.start_with?('./') ? "#{url}#{link}" : link + full_url.sub!('./', '/') + next if checked.include?(full_url) + + broken << full_url unless check_link(full_url) + checked << full_url + end + + broken +end + +# Method to check if a link is broken +def check_link(url) + uri = URI(url) + response = Net::HTTP.get_response(uri) + + if response.is_a?(Net::HTTPSuccess) + puts "Working link: #{url}" + true + else + puts "Broken link: #{url} (HTTP #{response.code})" + false + end +rescue StandardError => e + puts "Error checking #{url}: #{e.message}" + false +end + +# Main program +if ARGV.length != 1 + puts 'Usage: ruby brokenlinkfinder.rb <URL>' + exit +end + +start_url = ARGV.first +domain = URI(start_url).host + +check_links(start_url, domain).each do |broken| + puts "Broken: #{broken}" +end diff --git a/scripts/gvim b/scripts/gvim new file mode 100755 index 0000000..5777a7c --- /dev/null +++ b/scripts/gvim @@ -0,0 +1,7 @@ +#!/bin/bash +# Hack so qutebrowser starts an editor (Helix) in a new ghostty terminal. + +declare -r FILE_PATH="$2" +#echo "$@" > /tmp/params.txt + +ghostty -e "hx $FILE_PATH" diff --git a/scripts/hx.aichat-prompt b/scripts/hx.aichat-prompt new file mode 100755 index 0000000..4cafcf5 --- /dev/null +++ b/scripts/hx.aichat-prompt @@ -0,0 +1,9 @@ +#!/usr/bin/env zsh + +declare -xr INSTRUCTIONS='Answer only. If it is code, code only without code-block at the beginning and the end.' + +if [[ $# -eq 0 ]]; then + aichat "$(hx.prompt). $INSTRUCTIONS" +else + aichat "$@. $INSTRUCTIONS" +fi diff --git a/scripts/hx.chatgpt-prompt b/scripts/hx.chatgpt-prompt new file mode 100755 index 0000000..e4b6047 --- /dev/null +++ b/scripts/hx.chatgpt-prompt @@ -0,0 +1,3 @@ +#!/usr/bin/env zsh + +chatgpt "$(hx.prompt). Answer only. If it is code, code only without code-block at the beginning and the end." diff --git a/scripts/hx.goformatter b/scripts/hx.goformatter new file mode 100755 index 0000000..028fbb2 --- /dev/null +++ b/scripts/hx.goformatter @@ -0,0 +1,3 @@ +#!/bin/sh + +goimports | gofumpt diff --git a/scripts/hx.hexai-prompt b/scripts/hx.hexai-prompt new file mode 100755 index 0000000..ef413c0 --- /dev/null +++ b/scripts/hx.hexai-prompt @@ -0,0 +1,9 @@ +#!/usr/bin/env zsh + +declare -xr INSTRUCTIONS='Answer only. If it is code, code only without code-block at the beginning and the end.' + +if [[ $# -eq 0 ]]; then + hexai "$(hx.prompt). $INSTRUCTIONS" 2>/dev/null +else + hexai "$@. $INSTRUCTIONS" 2>/dev/null +fi diff --git a/scripts/hx.nvim-copilot-prompt b/scripts/hx.nvim-copilot-prompt new file mode 100755 index 0000000..dcb2837 --- /dev/null +++ b/scripts/hx.nvim-copilot-prompt @@ -0,0 +1,32 @@ +#!/usr/bin/env zsh + +declare -r STDIN_FILE=~/.copilot_prompt_stdin.txt +declare -r INPUT_FILE=~/.copilot_chat_input.txt +declare -r OUTPUT_FILE=~/.copilot_chat_output.txt +declare INPUT_PROMPT + +if [ -f $OUTPUT_FILE.done ]; then + rm $OUTPUT_FILE.done +fi +cat > $STDIN_FILE &>/dev/null + +if [ $# -eq 0 ]; then + INPUT_PROMPT="$(hx.prompt)" +else + INPUT_PROMPT="$@" +fi + +cat <<INPUT_FILE > $INPUT_FILE +$INPUT_PROMPT for the following: + +$(cat $STDIN_FILE) + +If the result is code, print out the code only, don't print the \`\`\`-markers around the code block. +INPUT_FILE + +tmux split-window -v "nvim +':CopilotAsk'; mv $OUTPUT_FILE $OUTPUT_FILE.done" + +while [ ! -f "$OUTPUT_FILE.done" ]; do + sleep 0.2 +done +sed -n '/^## Copilot/,/^## User/ { /^## Copilot/d; /\[file:/d; /^## User/d; p; }' $OUTPUT_FILE.done diff --git a/scripts/hx.prompt b/scripts/hx.prompt new file mode 100755 index 0000000..8dd14dd --- /dev/null +++ b/scripts/hx.prompt @@ -0,0 +1,14 @@ +#!/usr/bin/env zsh + +declare -r REPLY_FILE=~/.hx-prompt-reply +if [ -f "$REPLY_FILE" ]; then + rm "$REPLY_FILE" +fi + +tmux split-window -v "touch $REPLY_FILE.tmp; hx $REPLY_FILE.tmp; mv $REPLY_FILE.tmp $REPLY_FILE" + +while [ ! -f "$REPLY_FILE" ]; do + sleep 0.2 +done + +cat "$REPLY_FILE" diff --git a/scripts/randomnote.rb b/scripts/randomnote.rb new file mode 100644 index 0000000..b0c1b49 --- /dev/null +++ b/scripts/randomnote.rb @@ -0,0 +1,30 @@ +#!/usr/bin/env ruby + +NOTES_DIR = "#{ENV['HOME']}/git/foo.zone-content/gemtext/notes" +BOOK_PATH = "#{ENV['HOME']}/Buecher/Diverse/Search-Inside-Yourself.txt" +MIN_PERCENTAGE = 80 +MIN_LENGTH = 10 + +class String + CLEAN_PATTERN = [ + /\d\d\d-\d\d-\d\d/, /[^A-Za-z0-9!.;,?'" @]/, + /http.?:\/\/\S+/, /\S+\.gmi/, /^\./, /^\d/, + ] + def clean + CLEAN_PATTERN.each {|p| gsub! p, '' } + gsub(/\s+/, ' ').strip + end + def letter_percentage?(threshold) = threshold <= (100 * count("A-Za-z")) / length +end + +begin + srand Random.new_seed + puts File.read((Dir["#{NOTES_DIR}/*.gmi"] + [BOOK_PATH]).shuffle.sample) + .split("\n") + .map(&:clean) + .select{ |l| l.length >= MIN_LENGTH } + .reject{ |l| l.match?(/(Published at|EMail your comments)/) } + .reject{ |l| l.match?(/'|book notes/) } + .select{ |l| l.letter_percentage?(MIN_PERCENTAGE) } + .shuffle.sample +end diff --git a/scripts/taskwarriorfeeder.rb b/scripts/taskwarriorfeeder.rb new file mode 100644 index 0000000..117ca69 --- /dev/null +++ b/scripts/taskwarriorfeeder.rb @@ -0,0 +1,220 @@ +#!/usr/bin/env ruby + +require 'optparse' +require 'digest' +require 'json' +require 'set' + +PERSONAL_TIMESPAN_D = 30 +WORK_TIMESPAN_D = 14 +WORKTIME_DIR = "#{ENV['HOME']}/git/worktime".freeze +GOS_DIR = "#{ENV['HOME']}/.gosdir".freeze +MAX_PENDING_RANDOM_TASKS = 42 + +def maybe? + [true, false].sample +end + +def run_from_personal_device? + `uname`.chomp == 'Linux' +end + +def random_count + MAX_PENDING_RANDOM_TASKS - `task status:pending +random -work count`.to_i +end + +def notes(notes_dirs, prefix, dry) + notes_dirs.each do |notes_dir| + Dir["#{notes_dir}/#{prefix}-*"].each do |notes_file| + match = File.read(notes_file).strip.match(/(?<due>\d+)? *(?<tag>[A-Z]?[a-z,-:]+) *(?<body>.*)/m) + next unless match + + tags = match[:tag].split(',') + [prefix] + due = if match[:due].nil? + tags.include?('track') ? 'eow' : "#{rand(0..PERSONAL_TIMESPAN_D)}d" + else + "#{match[:due]}d" + end + yield tags, match[:body], due + File.delete(notes_file) unless dry + end + end +end + +def random_quote(md_file) + tag = File.basename(md_file, '.md').downcase + lines = File.readlines(md_file) + + match = lines.first.match(/\((\d+)\)/) + timespan = run_from_personal_device? ? PERSONAL_TIMESPAN_D : WORK_TIMESPAN_D + timespan = match ? match[1].to_i : timespan + + quote = lines.select { |l| l.start_with? '*' }.map { |l| l.sub(/\* +/, '') }.sample + tags = [tag, 'random'] + tags << 'work' if maybe? and maybe? + yield tags, quote.chomp, "#{rand(0..timespan)}d" +end + +def run!(cmd, dry) + puts cmd + return if dry + + puts `#{cmd}` + raise "Command '#{cmd}' failed with #{$?.exitstatus}" if $?.exitstatus != 0 +rescue StandardError => e + puts "Error running command '#{cmd}': #{e.message}" + exit 1 +end + +def skill_add!(skills_str, dry) + skills_file = "#{WORKTIME_DIR}/skills.txt" + skills_str.split(',').map(&:strip).each { skills[_1.to_s.downcase] = _1 } + + File.foreach(skills_file) do |line| + line.chomp! + skills[line.downcase] = line + end + File.open("#{skills_file}.tmp", 'w') do |file| + skills.each_value { |skill| file.puts(skill) } + end + return if dry + + File.rename("#{skills_file}.tmp", skills_file) +end + +def worklog_add!(tag, quote, due, dry) + file = "#{WORKTIME_DIR}/wl-#{Time.now.to_i}n.txt" + content = "#{due.chomp 'd'} #{tag} #{quote}" + + puts "#{file}: #{content}" + File.write(file, content) unless dry +end + +# Queue to Gos https://codeberg.org/snonux/gos +def gos_queue!(tags, message, dry) + tags.delete('share') + platforms = [] + %w[linkedin li mastodon ma noop no].select { tags.include?(_1) }.each do |platform| + platforms << platform + tags.delete(platform) + end + unless platforms.empty? + platforms = %w[share] + platforms + tags = ["#{platforms.join(':')}"] + tags + end + tags = %w[share] + tags if tags.size == 1 && !tags.first.start_with?('share') + tags_str = tags.join(',') + + message = "#{tags_str.empty? ? '' : "#{tags_str} "}#{message}" + file = "#{GOS_DIR}/#{Digest::MD5.hexdigest(message)}.txt" + puts "Writing #{file} with #{message}" + File.write(file, message) unless dry +end + +def task_add!(tags, quote, due, dry) + if quote.empty? + puts 'Not adding task with empty quote' + return + end + if tags.include?('tr') + tags << 'track' + tags.delete('tr') + end + tags << 'work' if tags.include?('mentoring') || tags.include?('productivity') + tags.uniq! + + if tags.include?('task') + run! "task #{quote}", dry + else + project = tags.find { |t| t =~ /^[A-Z]/ } + project = if project.nil? + '' + else + tags.delete(project) + " project:#{project.downcase}" + end + priority = tags.include?('high') ? 'H' : '' + run! "task add due:#{due} priority:#{priority}#{project} +#{tags.join(' +')} '#{quote.gsub("'", '"')}'", dry + end +end + +def task_schedule!(id, due, dry) + run! "timeout 5s task modify #{id} due:#{due}", dry +end + +def filter_tasks(filter) + lines = `task #{filter} 2>/dev/null`.split("\n").drop(1) + lines.pop + lines.map { |foo| foo.split.first }.each do |id| + yield id if id.to_i.positive? + end +end + +begin + opts = { + random_dir: "#{ENV['HOME']}/Notes/random", + notes_dirs: "#{ENV['HOME']}/Notes,#{ENV['HOME']}/Notes/Quicklogger,#{ENV['HOME']}/git/worktime", + dry_run: false, + no_random: false + } + + opt_parser = OptionParser.new do |o| + o.banner = 'Usage: ruby taskwarriorfeeder.rb [options]' + o.on('-d', '--random-dir DIR', 'The random quotes directory') { |v| opts[:random_dir] = v } + o.on('-n', '--notes-dirs DIR1,DIR2,...', 'The notes directories') { |v| opts[:notes_dirs] = v } + o.on('-D', '--dry-run', 'Dry run mode') { opts[:dry_run] = true } + o.on('-R', '--no-randoms', 'No random entries') { opts[:no_random] = true } + o.on_tail('-h', '--help', 'Show this help message and exit') { puts o and exit } + end + + opt_parser.parse!(ARGV) + + (run_from_personal_device? ? %w[ql pl] : %w[wl]).each do |prefix| + notes(opts[:notes_dirs].split(','), prefix, opts[:dry_run]) do |tags, note, due| + if tags.include?('skill') || tags.include?('skills') + skill_add!(note, opts[:dry_run]) + elsif tags.include? 'work' + worklog_add!(:log, note, due, opts[:dry_run]) + elsif tags.any? { |tag| tag.start_with?('share') } + gos_queue!(tags, note, opts[:dry_run]) + else + task_add!(tags, note, due, opts[:dry_run]) + end + end + end + + unless opts[:no_random] + count = random_count + + Dir["#{opts[:random_dir]}/*.md"].shuffle.each do |md_file| + next unless maybe? + break if count <= 0 + + random_quote(md_file) do |tags, quote, due| + task_add!(tags, quote, due, opts[:dry_run]) + count -= 1 + end + end + end + + if Dir.exist?(GOS_DIR) && !opts[:dry_run] + Dir["#{WORKTIME_DIR}/tw-gos-*.json"].each do |tw_gos| + JSON.parse(File.read(tw_gos)).each do |entry| + gos_queue!(entry['tags'], entry['description'], opts[:dry_run]) + end + File.delete(tw_gos) + rescue StandardError => e + puts e + end + end + + # Schedule track tasks to end of week + filter_tasks('+track due:') do |id| + task_schedule!(id, 'eow', opts[:dry_run]) + end + + # Randomly schedule other unscheduled tasks + filter_tasks('-unsched -nosched -meeting -track due:') do |id| + task_schedule!(id, "#{rand(0..PERSONAL_TIMESPAN_D)}d", opts[:dry_run]) + end +end diff --git a/scripts/tmux-edit-send b/scripts/tmux-edit-send new file mode 100755 index 0000000..05fe62d --- /dev/null +++ b/scripts/tmux-edit-send @@ -0,0 +1,246 @@ +#!/usr/bin/env bash +set -u -o pipefail + +LOG_ENABLED=0 +log_file="${TMPDIR:-/tmp}/tmux-edit-send.log" +log() { + if [ "$LOG_ENABLED" -eq 1 ]; then + printf '%s\n' "$*" >> "$log_file" + fi +} + +# Read the target pane id from a temp file created by tmux binding. +read_target_from_file() { + local file_path="$1" + local pane_id + if [ -n "$file_path" ] && [ -f "$file_path" ]; then + pane_id="$(sed -n '1p' "$file_path" | tr -d '[:space:]')" + # Ensure pane ID has % prefix + if [ -n "$pane_id" ] && [[ "$pane_id" != %* ]]; then + pane_id="%${pane_id}" + fi + printf '%s' "$pane_id" + fi +} + +# Read the target pane id from tmux environment if present. +read_target_from_env() { + local env_line pane_id + env_line="$(tmux show-environment -g TMUX_EDIT_TARGET 2>/dev/null || true)" + case "$env_line" in + TMUX_EDIT_TARGET=*) + pane_id="${env_line#TMUX_EDIT_TARGET=}" + # Ensure pane ID has % prefix + if [ -n "$pane_id" ] && [[ "$pane_id" != %* ]] && [[ "$pane_id" =~ ^[0-9]+$ ]]; then + pane_id="%${pane_id}" + fi + printf '%s' "$pane_id" + ;; + esac +} + +# Resolve the target pane id, falling back to the last pane. +resolve_target_pane() { + local candidate="$1" + local current_pane last_pane + + current_pane="$(tmux display-message -p "#{pane_id}" 2>/dev/null || true)" + log "current pane=${current_pane:-<empty>}" + + # Ensure candidate has % prefix if it's a pane ID + if [ -n "$candidate" ] && [[ "$candidate" =~ ^[0-9]+$ ]]; then + candidate="%${candidate}" + log "normalized candidate to $candidate" + fi + + if [ -n "$candidate" ] && [[ "$candidate" == *"#{"* ]]; then + log "format target detected, clearing" + candidate="" + fi + if [ -z "$candidate" ]; then + candidate="$(tmux display-message -p "#{last_pane}" 2>/dev/null || true)" + log "using last pane as fallback: $candidate" + elif [ "$candidate" = "$current_pane" ]; then + last_pane="$(tmux display-message -p "#{last_pane}" 2>/dev/null || true)" + if [ -n "$last_pane" ]; then + candidate="$last_pane" + log "candidate was current, using last pane: $candidate" + fi + fi + printf '%s' "$candidate" +} + +# Capture the latest multi-line prompt content from the pane. +capture_prompt_text() { + local target="$1" + tmux capture-pane -p -t "$target" -S -2000 2>/dev/null | awk ' + function trim_box(line) { + sub(/^ *│ ?/, "", line) + sub(/ *│ *$/, "", line) + sub(/[[:space:]]+$/, "", line) + return line + } + /^ *│ *→/ && index($0,"INSERT")==0 && index($0,"Add a follow-up")==0 { + if (text != "") last = text + text = "" + capture = 1 + line = $0 + sub(/^.*→ ?/, "", line) + line = trim_box(line) + if (line != "") text = line + next + } + capture { + if ($0 ~ /^ *└/) { + capture = 0 + if (text != "") last = text + next + } + if ($0 ~ /^ *│/ && index($0,"INSERT")==0 && index($0,"Add a follow-up")==0) { + line = trim_box($0) + if (line != "") { + if (text != "") text = text " " line + else text = line + } + } + } + END { + if (text != "") last = text + if (last != "") print last + } + ' +} + +# Write captured prompt text into the temp file if available. +prefill_tmpfile() { + local tmpfile="$1" + local prompt_text="$2" + if [ -n "$prompt_text" ]; then + printf '%s\n' "$prompt_text" > "$tmpfile" + fi +} + +# Ensure the target pane exists before sending keys. +validate_target_pane() { + local target="$1" + local pane target_found + if [ -z "$target" ]; then + log "error: no target pane determined" + echo "Could not determine target pane." >&2 + return 1 + fi + target_found=0 + log "validate: looking for target='$target' in all panes:" + for pane in $(tmux list-panes -a -F "#{pane_id}" 2>/dev/null || true); do + log "validate: checking pane='$pane'" + if [ "$pane" = "$target" ]; then + target_found=1 + log "validate: MATCH FOUND!" + break + fi + done + if [ "$target_found" -ne 1 ]; then + log "error: target pane not found: $target" + echo "Target pane not found: $target" >&2 + return 1 + fi + log "validate: target pane validated successfully" +} + +# Send temp file contents to the target pane line by line. +send_content() { + local target="$1" + local tmpfile="$2" + local prompt_text="$3" + local first_line=1 + local line + log "send_content: target=$target, prompt_text='$prompt_text'" + while IFS= read -r line || [ -n "$line" ]; do + log "send_content: read line='$line'" + if [ "$first_line" -eq 1 ] && [ -n "$prompt_text" ]; then + if [[ "$line" == "$prompt_text"* ]]; then + local old_line="$line" + line="${line#"$prompt_text"}" + log "send_content: stripped prompt, was='$old_line' now='$line'" + fi + fi + first_line=0 + log "send_content: sending line='$line'" + tmux send-keys -t "$target" -l "$line" + tmux send-keys -t "$target" Enter + done < "$tmpfile" + log "sent content to $target" +} + +# Main entry point. +main() { + local target_file="${1:-}" + local target + local editor="${EDITOR:-vi}" + local tmpfile + local prompt_text + + log "=== tmux-edit-send starting ===" + log "target_file=$target_file" + log "EDITOR=$editor" + + target="$(read_target_from_file "$target_file" || true)" + if [ -n "$target" ]; then + log "file target=${target:-<empty>}" + rm -f "$target_file" + fi + if [ -z "$target" ]; then + target="${TMUX_EDIT_TARGET:-}" + fi + log "env target=${target:-<empty>}" + if [ -z "$target" ]; then + target="$(read_target_from_env || true)" + fi + log "tmux env target=${target:-<empty>}" + target="$(resolve_target_pane "$target")" + log "fallback target=${target:-<empty>}" + + tmpfile="$(mktemp)" + log "created tmpfile=$tmpfile" + if [ ! -f "$tmpfile" ]; then + log "ERROR: mktemp failed to create file" + echo "ERROR: mktemp failed" >&2 + exit 1 + fi + mv "$tmpfile" "${tmpfile}.md" 2>&1 | while read -r line; do log "mv output: $line"; done + tmpfile="${tmpfile}.md" + log "renamed to tmpfile=$tmpfile" + if [ ! -f "$tmpfile" ]; then + log "ERROR: tmpfile does not exist after rename" + echo "ERROR: tmpfile rename failed" >&2 + exit 1 + fi + trap 'rm -f "$tmpfile"' EXIT + + log "capturing prompt text from target=$target" + prompt_text="$(capture_prompt_text "$target")" + log "captured prompt_text='$prompt_text'" + prefill_tmpfile "$tmpfile" "$prompt_text" + log "prefilled tmpfile" + + log "launching editor: $editor $tmpfile" + "$editor" "$tmpfile" + local editor_exit=$? + log "editor exited with status $editor_exit" + + if [ ! -s "$tmpfile" ]; then + log "empty file, nothing sent" + exit 0 + fi + + log "tmpfile contents:" + log "$(cat "$tmpfile")" + + log "validating target pane" + validate_target_pane "$target" + log "sending content to target=$target" + send_content "$target" "$tmpfile" "$prompt_text" + log "=== tmux-edit-send completed ===" +} + +main "$@" diff --git a/scripts/wol-f3s b/scripts/wol-f3s new file mode 100755 index 0000000..263763b --- /dev/null +++ b/scripts/wol-f3s @@ -0,0 +1,86 @@ +#!/bin/bash +# Wake-on-LAN and shutdown script for f3s cluster (f0, f1, f2) +# +# Usage: +# wol-f3s # Wake all three Beelinks +# wol-f3s f0 # Wake only f0 +# wol-f3s f1 # Wake only f1 +# wol-f3s f2 # Wake only f2 +# wol-f3s shutdown # Shutdown all three Beelinks + +# MAC addresses +F0_MAC="e8:ff:1e:d7:1c:ac" # f0 (192.168.1.130) +F1_MAC="e8:ff:1e:d7:1e:44" # f1 (192.168.1.131) +F2_MAC="e8:ff:1e:d7:1c:a0" # f2 (192.168.1.132) + +# IP addresses +F0_IP="192.168.1.130" +F1_IP="192.168.1.131" +F2_IP="192.168.1.132" + +# SSH user +SSH_USER="paul" + +# Broadcast address for your LAN +BROADCAST="192.168.1.255" + +wake() { + local name=$1 + local mac=$2 + + if [[ "$mac" == "XX:XX:XX:XX:XX:XX" ]]; then + echo "⚠️ $name MAC address not configured yet" + return 1 + fi + + echo "Sending WoL packet to $name ($mac)..." + wol -i "$BROADCAST" "$mac" +} + +shutdown_host() { + local name=$1 + local ip=$2 + + echo "Shutting down $name ($ip)..." + ssh -o ConnectTimeout=5 "$SSH_USER@$ip" "doas poweroff" 2>/dev/null && \ + echo " ✓ Shutdown command sent to $name" || \ + echo " ✗ Failed to reach $name (already down?)" +} + +ACTION="${1:-all}" + +case "$ACTION" in + f0) + wake "f0" "$F0_MAC" + ;; + f1) + wake "f1" "$F1_MAC" + ;; + f2) + wake "f2" "$F2_MAC" + ;; + all|"") + wake "f0" "$F0_MAC" + wake "f1" "$F1_MAC" + wake "f2" "$F2_MAC" + ;; + shutdown|poweroff|down) + # This is to mute Gogios alerts for a day + ssh rex@blowfish.buetow.org touch /tmp/f3s_taken_down + ssh rex@fishfinger.buetow.org touch /tmp/f3s_taken_down + shutdown_host "f0" "$F0_IP" + shutdown_host "f1" "$F1_IP" + shutdown_host "f2" "$F2_IP" + echo "" + echo "✓ Shutdown commands sent to all machines." + exit 0 + ;; + *) + echo "Usage: $0 [f0|f1|f2|all|shutdown]" + exit 1 + ;; +esac + +echo "" +echo "✓ WoL packets sent. Machines should boot in a few seconds." +echo " Wait ~30 seconds, then try: ssh paul@192.168.1.130" |
