Add CLAUDE.md and Readme.md, remove lots of cruft

This commit is contained in:
Sami Samhuri 2025-06-06 10:17:36 -07:00
parent 16137efd33
commit 9e2234daa0
No known key found for this signature in database
30 changed files with 280 additions and 41226 deletions

55
CLAUDE.md Normal file
View file

@ -0,0 +1,55 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
This is a personal utility bin directory containing command-line tools and scripts for development, git operations, and system automation. The scripts are primarily written in bash, Ruby, and Perl, with some Python utilities.
## Script Categories and Architecture
### Git Utilities
- Git workflow enhancement scripts (git-update, git-remove-merged-branches, git-ai-message, etc.)
- Follow the pattern of accepting remote/branch parameters with sensible defaults
- Include error handling with `set -e` in bash scripts
- Support dry-run modes where applicable (use `-n` flag)
### Development Tools
- Web development utilities (serve, make-bookmarklet, sri-integrity)
- Image processing tools (scale-app-icons, generate-xcode-imageset, retina-scale)
- Data processing scripts (colours.rb for color conversion, progress for piped data)
### System Scripts
- Shell enhancement utilities (hist.rb for command analysis, enable-sudo-touch-id)
- File management tools (find-unused-images, count-chars)
## Common Patterns
### Script Structure
- All executable scripts start with appropriate shebang (`#!/bin/bash`, `#!/usr/bin/env ruby`, etc.)
- Bash scripts use `set -e` for error handling
- Ruby scripts often process STDIN/command line arguments with explicit argument parsing
- Support both piped input and file arguments where relevant
### Argument Handling
- Use `"${1:-default}"` pattern for optional arguments with defaults in bash
- Ruby scripts use ARGV.shift pattern for argument processing
- Include help/usage when arguments are malformed
### Git Script Conventions
- Accept remote name as first argument (default to origin or auto-detect)
- Accept branch name as second argument (default to current branch or master/main)
- Preserve original branch state (stash/unstash, checkout back to original branch)
- Use `git rev-parse --abbrev-ref HEAD` to get current branch
### Data Processing
- Ruby scripts that process data streams use proper buffering and error handling
- Support both byte and line counting modes where applicable
- Use STDERR for progress/status output, STDOUT for data
## Development Workflow
This repository has no build system, package.json, or formal testing framework. Scripts are standalone utilities meant to be:
- Executable from anywhere in the system PATH
- Self-contained with minimal dependencies
- Robust with proper error handling

47
Readme.md Normal file
View file

@ -0,0 +1,47 @@
# bin
My personal collection of command-line utilities. Mostly git workflow stuff, but there's a bunch of other random tools that have accumulated over the years.
## What's in here
These are shell scripts, Ruby utilities, and various other command-line tools that live in my PATH. I've been collecting them for ages - some date back to 2006 - and they handle the mundane tasks I got tired of doing manually.
The usual suspects:
- Git workflow shortcuts (because who remembers those flag combinations?)
- Image processing for app icons and retina displays
- JSON manipulation and data analysis tools
- System automation for macOS
- Media conversion utilities
Most follow the Unix philosophy of doing one thing well, though some have grown a bit over time. The ones I still use regularly work reliably, but there are definitely some in here that haven't been touched in years and may or may not still function properly.
## The collection
- [**colours.rb**](https://github.com/samsonjs/config/blob/main/bin/colours.rb) - Convert between color formats (hex, RGB, UIColor)
- [**convert-all-songs**](https://github.com/samsonjs/config/blob/main/bin/convert-all-songs) - Batch convert audio files to different formats
- [**convert-song**](https://github.com/samsonjs/config/blob/main/bin/convert-song) - Convert single audio file to different format
- [**diff-so-fancy**](https://github.com/samsonjs/config/blob/main/bin/diff-so-fancy) - Enhanced git diff output with better formatting ([source](https://github.com/so-fancy/diff-so-fancy))
- [**enable-sudo-touch-id**](https://github.com/samsonjs/config/blob/main/bin/enable-sudo-touch-id) - Enable Touch ID authentication for sudo commands
- [**finder-show-hidden-files**](https://github.com/samsonjs/config/blob/main/bin/finder-show-hidden-files) - Toggle visibility of hidden files in Finder
- [**generate-xcode-imageset**](https://github.com/samsonjs/config/blob/main/bin/generate-xcode-imageset) - Generate Xcode imageset from @2x and @3x images
- [**git-conflicts**](https://github.com/samsonjs/config/blob/main/bin/git-conflicts) - List files in merge conflict state
- [**git-diff-merge-conflict-resolution**](https://github.com/samsonjs/config/blob/main/bin/git-diff-merge-conflict-resolution) - Show diff for merge conflict resolution
- [**git-edit-conflicted-files**](https://github.com/samsonjs/config/blob/main/bin/git-edit-conflicted-files) - Open all conflicted files in editor
- [**git-large-files**](https://github.com/samsonjs/config/blob/main/bin/git-large-files) - Find largest objects in git repository pack files
- [**git-open-in-github**](https://github.com/samsonjs/config/blob/main/bin/git-open-in-github) - Open current repo/branch in GitHub web interface
- [**git-remove-merged-branches**](https://github.com/samsonjs/config/blob/main/bin/git-remove-merged-branches) - Delete merged branches from remote repository
- [**git-uncommit**](https://github.com/samsonjs/config/blob/main/bin/git-uncommit) - Undo the last commit (soft reset)
- [**git-update**](https://github.com/samsonjs/config/blob/main/bin/git-update) - Update and rebase current branch from remote with stash management
- [**jsonugly**](https://github.com/samsonjs/config/blob/main/bin/jsonugly) - Minify JSON by removing whitespace
- [**make-bookmarklet**](https://github.com/samsonjs/config/blob/main/bin/make-bookmarklet) - Convert JavaScript code to bookmarklet format
- [**progress**](https://github.com/samsonjs/config/blob/main/bin/progress) - Add progress indicators to piped data streams
- [**retina-scale**](https://github.com/samsonjs/config/blob/main/bin/retina-scale) - Scale images for 1x, 2x, and 3x display densities
- [**roll**](https://github.com/samsonjs/config/blob/main/bin/roll) - Random choice selector from command line arguments
- [**save-keyboard-shortcuts.sh**](https://github.com/samsonjs/config/blob/main/bin/save-keyboard-shortcuts.sh) - Export macOS keyboard shortcuts to file
- [**scale-app-icons**](https://github.com/samsonjs/config/blob/main/bin/scale-app-icons) - Generate iOS and macOS app icons at all required sizes
- [**screen-shell**](https://github.com/samsonjs/config/blob/main/bin/screen-shell) - Screen session management utility
- [**sd-before-copy**](https://github.com/samsonjs/config/blob/main/bin/sd-before-copy) - Pre-backup script for SuperDuper! to run Vortex backup system
- [**serve**](https://github.com/samsonjs/config/blob/main/bin/serve) - Start simple HTTP server (default port 8080)
- [**sri-integrity**](https://github.com/samsonjs/config/blob/main/bin/sri-integrity) - Generate Sub-Resource Integrity hashes for external resources
- [**youtube-snarf-audio**](https://github.com/samsonjs/config/blob/main/bin/youtube-snarf-audio) - Extract audio from YouTube videos

2624
ack

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,32 @@
#!/usr/bin/env ruby
#
# colours.rb - Convert between color formats (hex, RGB, UIColor)
#
# Converts between hex colors and RGB values, with output in multiple formats
# including CSS, UIColor (iOS/macOS), and normalized float values.
#
# Usage: colours.rb <hex-color>
# colours.rb <red> <green> <blue>
#
# Examples:
# colours.rb "#ff0000"
# colours.rb ff0000
# colours.rb 255 0 0
# colours.rb 1.0 0.0 0.0
if ARGV.empty? || ARGV.include?('-h') || ARGV.include?('--help')
puts "Usage: #{File.basename(__FILE__)} <hex-color>"
puts " #{File.basename(__FILE__)} <red> <green> <blue>"
puts ""
puts "Convert between color formats (hex, RGB, UIColor)"
puts ""
puts "Examples:"
puts " #{File.basename(__FILE__)} \"#ff0000\""
puts " #{File.basename(__FILE__)} ff0000"
puts " #{File.basename(__FILE__)} 255 0 0"
puts " #{File.basename(__FILE__)} 1.0 0.0 0.0"
exit 0
end
hex = ''
rgb = []

View file

@ -1,4 +1,13 @@
#!/bin/bash
#
# convert-all-songs - Batch convert all mp3 files in current directory to m4a format
#
# Converts all .mp3 files in the current directory to .m4a (AAC) format at 128 kbps.
# Removes any existing .m4a files before conversion to avoid conflicts.
#
# Usage: convert-all-songs
#
# Requirements: ffmpeg, convert-song script in same directory
set -euo pipefail

View file

@ -1,4 +1,13 @@
#!/bin/bash
#
# convert-song - Convert an mp3 file to m4a format with reduced bitrate
#
# Converts an mp3 to an m4a (AAC) and reduces the bitrate to 128 kbps.
# Drops video streams (useful for mp3s with embedded artwork).
#
# Usage: convert-song <input.mp3>
#
# Requirements: ffmpeg
set -euo pipefail

View file

@ -1,3 +0,0 @@
#!/bin/sh
pbpaste | ruby -e 'puts ARGF.read.length'

View file

@ -1,57 +0,0 @@
#!/usr/bin/env ruby -w
require 'csv'
HEADERS = %w[device_name sessions].freeze
DEVICES_PATH = File.join(__dir__, 'supported_devices.csv')
# Maps device model to name using Google's giant CSV that lives alongside this file.
DEVICE_MAP = CSV.foreach(DEVICES_PATH).each_with_object({}) do |row, map|
# skip the first header row
next if row[0] == 'Retail Branding'
# columns: Retail Branding (maker), Marketing Name, Device (unused), Model
maker = row[0]
name = row[1]
model = row[3]
map[model] = "#{maker} #{name}"
end
def main
in_csv = CSV.new(ARGF)
sessions_by_device = count_devices(in_csv)
render_csv(sessions_by_device)
end
def zero_hash
Hash.new { |_k, _v| 0 }
end
def count_devices(in_csv)
# skip the first header row
in_csv.drop(1).each_with_object(zero_hash) do |row, h|
# devices come in a raw model and we have to look up the marketing name for each one
# e.g. SM-S908N and SM-S908U are 2 of 9 models of the Galaxy S22 Ultra line
device_model = row[0]
device_name = DEVICE_MAP[device_model] || device_model
# Skip things that are obviously not Android
next if device_name =~ /iphone|ipad/i
h[device_name] += 1
end
end
def render_csv(sessions_by_device)
puts CSV.generate_line(HEADERS)
sessions_by_device
.sort_by { |_device, sessions| sessions }
.reverse
.each do |device_name, sessions|
out_row = [device_name, sessions]
puts CSV.generate_line(out_row)
end
end
main if $PROGRAM_NAME == __FILE__

View file

@ -1,28 +0,0 @@
#!/usr/bin/env ruby -w
require 'csv'
HEADERS = %w[version sessions].freeze
in_csv = CSV.new(ARGF)
is_header = true
zero_hash = Hash.new { |k, v| 0 }
sessions_by_version = in_csv.inject(zero_hash) do |h, row|
if is_header
is_header = false
next h
end
version = row[1]
sessions = row[2].to_i
major_version = version.split('.').first
h[major_version] += sessions
h
end
puts CSV.generate_line(HEADERS)
sessions_by_version.keys.map(&:to_i).sort.each do |version|
out_row = [version, sessions_by_version[version.to_s]]
puts CSV.generate_line(out_row)
end

View file

@ -1,3 +0,0 @@
#!/bin/sh
docker run --rm --privileged alpine hwclock -s

View file

@ -1,4 +1,18 @@
#!/bin/zsh
#
# enable-sudo-touch-id - Enable Touch ID authentication for sudo commands
#
# Configures PAM to allow Touch ID for sudo authentication on macOS.
# Creates /etc/pam.d/sudo_local from template and enables the auth module.
#
# Usage: enable-sudo-touch-id
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
echo "Usage: $(basename "$0")"
echo "Enable Touch ID authentication for sudo commands"
echo "Requires macOS with Touch ID support"
exit 0
fi
if [[ -e /etc/pam.d/sudo_local ]]; then
echo "TouchID unlock already in place"

View file

@ -1,9 +0,0 @@
#!/bin/bash
for i in `find . -name "*.png" -o -name "*.jpg"`; do
file=`basename -s .jpg "$i" | xargs basename -s .png | xargs basename -s @2x | xargs basename -s @3x`
result=`ack -i "$file"`
if [ -z "$result" ]; then
echo "$i"
fi
done

View file

@ -1,4 +1,22 @@
#!/bin/sh
#
# finder-show-hidden-files - Toggle visibility of hidden files in macOS Finder
#
# Sets the AppleShowAllFiles preference and restarts Finder to show/hide hidden files.
# Defaults to showing hidden files if no argument is provided.
#
# Usage: finder-show-hidden-files [true|false]
#
# Examples:
# finder-show-hidden-files # Show hidden files
# finder-show-hidden-files false # Hide hidden files
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
echo "Usage: $(basename "$0") [true|false]"
echo "Toggle visibility of hidden files in Finder"
echo "Defaults to 'true' (show hidden files) if no argument provided"
exit 0
fi
status=${1:-true}
defaults write com.apple.finder AppleShowAllFiles $status

69
flux
View file

@ -1,69 +0,0 @@
#!/usr/bin/env ruby
require 'date'
require 'json'
require 'optparse'
TIMES = JSON.parse(File.read(File.expand_path('../sun-yyj.json', __FILE__)))['times']
USAGE_TEXT = "Usage: flux [options]"
def main
options = parse_options
flux(options)
end
def parse_options
options = {
dry_run: false,
set_current: false,
}
OptionParser.new do |opts|
opts.banner = USAGE_TEXT
opts.on("-n", "--dry-run", "Don't actually change the lights") do |dry_run|
options[:dry_run] = dry_run
end
opts.on("-c", "--set-current", "Change the lights to the setting that should currently be active") do |current|
options[:set_current] = current
end
end.parse!
options
end
def flux(options)
date = Date.today
month = date.month.to_s
day = date.day.to_s
if times = TIMES[month][day]
times['midnight'] = '22:30'
puts "sunrise: #{times['sunrise']}"
puts "morning: #{times['morning']}"
puts "sunset: #{times['sunset']}"
puts "night: #{times['night']}"
puts "midnight: #{times['midnight']}"
time = Time.now
hour, min = time.hour, time.min
padded_min = min < 10 ? "0#{min}" : "#{min}"
now = "#{hour}:#{padded_min}"
puts "current time: #{now}"
found =
if options[:set_current]
now = now.sub(':', '').to_i
if k = times.keys.select { |k| now >= times[k].sub(':', '').to_i }.last
[k, times[k]]
end
else
times.detect { |k, v| now == v }
end
if found
setting = found[0]
puts "> exec lights #{setting} - 300"
exec "lights #{setting} - 300" unless options[:dry_run]
end
else
raise "Cannot find today's date (#{date}) in times: #{TIMES.inspect}"
end
end
main if $0 == __FILE__

View file

@ -1,24 +0,0 @@
#!/usr/bin/env ruby
require 'forecast_io'
require 'json'
ForecastIO.api_key = JSON.parse(File.read(File.expand_path('~/Dropbox/Personal/forecastio.json', __FILE__)))['apikey']
LATITUDE = 48.456642
LONGITUDE = -123.370325
def main
if forecast = ForecastIO.forecast(LATITUDE, LONGITUDE)
cloud_cover = forecast.currently.cloudCover
puts "Cloud cover: #{cloud_cover}"
setting = cloud_cover > 0.6 ? 'cloudy' : 'sunny'
# File.open('/Users/sjs/flux-clouds.log', 'a') { |f| f.puts "Cloud cover: #{cloud_cover}"; f.puts "> lights #{setting} - 100" }
puts "> lights #{setting} - 100"
exec "lights #{setting} - 100"
else
raise "Unable to check forecast"
end
end
main if $0 == __FILE__

View file

@ -1,7 +0,0 @@
#!/bin/zsh
BRANCH=$(git rev-parse --abbrev-ref HEAD)
DIFF=$(git diff --staged)
if [[ -n "$DIFF" ]]; then
llm "Write a code commit message for this diff on the branch $BRANCH and only output the message itself so it can be used directly to commit. Include a one-line summary, and optionally a description below that if there are lots of changes. Be concise and avoid adding fluff or filler. Wrap the description at 70 characters per line.\n\n\`\`\`\n$DIFF\n\`\`\`"
fi

View file

@ -1,41 +0,0 @@
#!/usr/bin/env ruby
def git_dir?
File.exists?('.git') || `git rev-parse --git-dir >/dev/null`
end
def authors
`git log | grep '^Author:' | sed -e 's/Author: //g' | sort -f | uniq`.split(/\n/)
end
def authors_by_field n
raise "invalid field #{n}, expected 0 or 1" unless n == 0 || n == 1
other = (n-1).abs
authors.inject({}) do |as, a|
parts = a.split(' <')
parts[1].sub!(/>$/, '')
(as[parts[n]] ||= []) << parts[other]
as
end
end
def authors_by_name; authors_by_field(0) end
def authors_by_email; authors_by_field(1) end
def main
exit 1 unless git_dir?
sort = :name
sort = ARGV.first.to_sym if ARGV.length > 0
if sort == :name
authors_by_name.each do |name, emails|
puts "#{name} <#{emails.join(', ')}>"
end
else
authors_by_email.each do |email, names|
puts "#{email}: #{names.first} (aka #{names[1..-1].join(' aka ')})"
end
end
end
main if $0 == __FILE__

View file

@ -1,3 +1,21 @@
#!/bin/zsh
#
# git-diff-merge-conflict-resolution - Show combined diff for merge conflict resolution
#
# Displays a combined diff showing how conflicts were resolved in a merge commit.
# Uses git diff-tree with --cc to show the changes from all parents.
#
# Usage: git-diff-merge-conflict-resolution [commit]
#
# Examples:
# git-diff-merge-conflict-resolution # Show resolution for HEAD
# git-diff-merge-conflict-resolution abc123 # Show resolution for specific commit
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
echo "Usage: $(basename "$0") [commit]"
echo "Show combined diff for merge conflict resolution"
echo "Defaults to HEAD if no commit specified"
exit 0
fi
git diff-tree --color --cc "${1:-HEAD}"

View file

@ -1,4 +1,22 @@
#!/bin/sh
#
# git-edit-conflicted-files - Open all conflicted files in your editor
#
# Opens all files with merge conflicts in your preferred editor.
# Uses git-conflicts to find conflicted files and opens them all at once.
#
# Usage: git-edit-conflicted-files [editor]
#
# Examples:
# git-edit-conflicted-files # Use $VISUAL or $EDITOR
# git-edit-conflicted-files vim # Use vim specifically
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
echo "Usage: $(basename "$0") [editor]"
echo "Open all conflicted files in your editor"
echo "Uses \$VISUAL or \$EDITOR if no editor specified"
exit 0
fi
EDIT="${1:-${VISUAL:-$EDITOR}}"
eval $EDIT $(git conflicts | ruby -e "puts ARGF.each_line.to_a.map{|l| \"'\"+l.strip+\"'\"}.join(' ')")

43
hist
View file

@ -1,43 +0,0 @@
#!/usr/bin/env ruby
# Project Name: None
# File / Folder: hist.rb
# File Language: ruby
# Copyright (C): 2006 heptadecagram
# First Author: heptadecagram
# First Created: 2006.03.13 20:14:58
# Last Modifier: sjs
# Last Modified: 2010.02.11
#
# now works w/ zsh and reports commands that account for at least 1% of the total
command = {}
execution = {}
$total = 0
IO.foreach('/Users/sjs/config/zsh/zhistory') do |line|
line.chomp! =~ /^:\s\d+:\d+;((\S+).*)$/
next if $1.nil? || $2.nil?
execution[$1] = 1 + execution[$1].to_i
command[$2] = 1 + command[$2].to_i
$total += 1
end
puts $total
execution = execution.select {|a,b| b.to_f / $total > 0.01}
command = command.select {|a,b| b.to_f / $total > 0.01}
Max_length = execution.sort_by {|a| a[0].length }.reverse[0][0].length
def print_hash(hash)
sorted = hash.sort {|a,b| b[1] <=> a[1] }
sorted.each do |cmd,value|
printf " %#{Max_length}s: %3d(%.2f%%)\n", cmd, value, value.to_f / $total
end
end
puts "Executions:\n"
print_hash(execution)
puts "Commands:\n"
print_hash(command)

41
hist.rb
View file

@ -1,41 +0,0 @@
#!/usr/bin/env ruby
# Project Name: None
# File / Folder: hist.rb
# File Language: ruby
# Copyright (C): 2006 heptadecagram
# First Author: heptadecagram
# First Created: 2006.03.13 20:14:58
# Last Modifier: heptadecagram
# Last Modified: 2008.05.02
command = {}
execution = {}
$total = 0
IO.foreach('/Users/sjs/config/zsh/zhistory') do |line|
line.chomp! =~ /^:\s\d+:\d+;((\S+).*)$/
next if $1.nil? || $2.nil?
execution[$1] = 1 + execution[$1].to_i
command[$2] = 1 + command[$2].to_i
$total += 1
end
puts $total
execution = execution.select {|a,b| b.to_f / $total > 0.01}
command = command.select {|a,b| b.to_f / $total > 0.01}
Max_length = execution.sort_by {|a| a[0].length }.reverse[0][0].length
def print_hash(hash)
sorted = hash.sort {|a,b| b[1] <=> a[1] }
sorted.each do |cmd,value|
printf " %#{Max_length}s: %3d(%.2f%%)\n", cmd, value, value.to_f / $total
end
end
puts "Executions:\n"
print_hash(execution)
puts "Commands:\n"
print_hash(command)

294
jqed
View file

@ -1,294 +0,0 @@
#!/usr/bin/env python3
import sys
import urwid
import urwid_readline
import os
import subprocess as sp
import shutil
import shlex
import select
import platform
import re
VERSION = 'v0.1.3 (2020-09-25)'
PROMPT = 'jq> '
PAUSED_PROMPT_A = '||'
PAUSED_PROMPT_B = '> '
IS_WSL = "Microsoft" in platform.platform()
palette = [
('prompt_ok', 'light green,bold', 'default'),
('prompt_paused', 'yellow,bold', 'default'),
('prompt_err', 'light red,bold', 'default'),
('inp_plain', 'bold', 'default'),
('body_plain', '', 'default'),
('err_bar', 'light red,bold', 'default'),
]
class JqManager:
def __init__(self, inp_file, loop):
self.inp_file = inp_file
self.loop = loop
self.loop.event_loop.watch_file(self.inp_file.fileno(), self._file_avail_cb)
self.inp_data = ''
self.last_out_data = ''
self.out_data = ''
self.out_err = ''
self.scroll_line = 0
self.paused = False
self.prompt_ok = True
self.is_inp_data_done = False
self._jq_path = shutil.which('jq')
if not self._jq_path:
try:
orig_stdout.write('jq does not seem to be installed\nPerhaps you want: sudo apt install jq\n'.encode())
except BrokenPipeError:
sys.stderr.write('jq does not seem to be installed\nPerhaps you want: sudo apt install jq\n')
exit(1)
self.jq_proc = None
self.respawn_jq(None, inp.get_edit_text())
urwid.connect_signal(inp, 'change', self.respawn_jq)
def _file_avail_cb(self):
chunk = os.read(orig_stdin.fileno(), 1024).decode()
if len(chunk) != 0:
self.inp_data += chunk
try:
self.jq_proc.stdin.write(chunk.encode())
except ValueError:
# if `self.jq_proc.stdin` was closed
pass
else:
self.loop.event_loop.remove_watch_file(orig_stdin.fileno())
self.is_inp_data_done = True
self.jq_proc.stdin.close()
def toggle_pause(self):
self.paused = not self.paused
if self.out_data != '':
self.last_out_data = self.out_data
if self.prompt_ok:
self.update_body()
if self.paused:
if self.prompt_ok:
inp.set_caption([('prompt_paused', PAUSED_PROMPT_A), ('prompt_ok', PAUSED_PROMPT_B)])
else:
inp.set_caption([('prompt_paused', PAUSED_PROMPT_A), ('prompt_err', PAUSED_PROMPT_B)])
self.loop.event_loop.remove_watch_file(self.inp_file.fileno())
else:
if self.prompt_ok:
inp.set_caption(('prompt_ok', PROMPT))
else:
inp.set_caption(('prompt_err', PROMPT))
self.loop.event_loop.watch_file(self.inp_file.fileno(), self._file_avail_cb)
def _jq_out_avail_cb(self):
if self.jq_proc.stdout not in select.select([self.jq_proc.stdout], [], [], 0)[0]:
# Ignore spurius calls
return
chunk = ''
while self.jq_proc.stdout in select.select([self.jq_proc.stdout], [], [], 0)[0]:
new_chunk = os.read(self.jq_proc.stdout.fileno(), 1024).decode()
if len(new_chunk) == 0:
break
chunk += new_chunk
if len(chunk) != 0:
self.out_data += chunk
if not self.paused:
self.update_body()
else:
if self.out_err == '':
if loop.screen_size is not None:
new_scroll_line = min(max(len(self.out_data.split('\n')) - int(loop.screen_size[1] / 2), 0), self.scroll_line)
if new_scroll_line != self.scroll_line:
self.scroll_line = new_scroll_line
self.update_body()
self.loop.event_loop.remove_watch_file(self.jq_proc.stdout.fileno())
self.jq_proc.stdout.close()
self.jq_proc.stdin.close()
self.jq_proc.wait()
def _jq_err_avail_cb(self):
if self.jq_proc.stderr not in select.select([self.jq_proc.stderr], [], [], 0)[0]:
# Ignore spurius calls
return
chunk = ''
while self.jq_proc.stderr in select.select([self.jq_proc.stderr], [], [], 0)[0]:
new_chunk = os.read(self.jq_proc.stderr.fileno(), 1024).decode()
if len(new_chunk) == 0:
break
chunk += new_chunk
if len(chunk) != 0:
self.out_err += chunk
err_bar.set_text(self.out_err.replace(' (Unix shell quoting issues?)', '').strip())
else:
self.loop.event_loop.remove_watch_file(self.jq_proc.stderr.fileno())
self.jq_proc.stderr.close()
if self.out_err != '':
self.prompt_ok = False
if self.paused:
inp.set_caption([('prompt_paused', PAUSED_PROMPT_A), ('prompt_err', PAUSED_PROMPT_B)])
else:
inp.set_caption(('prompt_err', PROMPT))
elif self.out_data == '':
self.last_out_data = ''
self.update_body()
def respawn_jq(self, _, query):
if self.jq_proc is not None:
if not self.jq_proc.stdout.closed:
self.loop.event_loop.remove_watch_file(self.jq_proc.stdout.fileno())
if not self.jq_proc.stderr.closed:
self.loop.event_loop.remove_watch_file(self.jq_proc.stderr.fileno())
self.jq_proc.stdin.close()
self.jq_proc.stdout.close()
self.jq_proc.stderr.close()
self.jq_proc.terminate()
self.jq_proc.wait()
err_bar.set_text('')
if self.out_data != '' and not self.paused:
self.last_out_data = self.out_data
self.out_data = ''
self.out_err = ''
self.prompt_ok = True
if self.paused:
inp.set_caption([('prompt_paused', PAUSED_PROMPT_A), ('prompt_ok', PAUSED_PROMPT_B)])
else:
inp.set_caption(('prompt_ok', PROMPT))
self.jq_proc = sp.Popen([self._jq_path, query], stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE, bufsize=0)
try:
encoded_inp_data = self.inp_data.encode()
try:
for offset in range(0, len(encoded_inp_data), 1024):
self.jq_proc.stdin.write(encoded_inp_data[offset:offset+1024])
self._jq_out_avail_cb()
self._jq_err_avail_cb()
if self.is_inp_data_done:
self.jq_proc.stdin.close()
self.loop.event_loop.watch_file(self.jq_proc.stdout.fileno(), self._jq_out_avail_cb)
self.loop.event_loop.watch_file(self.jq_proc.stderr.fileno(), self._jq_err_avail_cb)
except ValueError:
pass
except BrokenPipeError:
pass
def update_body(self):
height = 256
if self.loop.screen_size is not None:
height = self.loop.screen_size[1]
if self.out_data and not self.paused:
body.set_text('\n'.join(self.out_data.split('\n')[self.scroll_line:][:height]))
else:
body.set_text('\n'.join(self.last_out_data.split('\n')[self.scroll_line:][:height]))
class BetterEdit(urwid_readline.ReadlineEdit):
def keypress(self, size, key):
if key == 'ctrl left':
try:
self.edit_pos = self.edit_text[:self.edit_pos].rindex(' ')
except ValueError:
self.edit_pos = 0
elif key == 'ctrl right':
try:
self.edit_pos += self.edit_text[self.edit_pos:].index(' ') + 1
except ValueError:
self.edit_pos = len(self.edit_text)
elif key == 'ctrl p':
jq_man.toggle_pause()
elif key in ('up', 'down', 'page up', 'page down'):
if key == 'up':
jq_man.scroll_line = max(0, jq_man.scroll_line - 1)
elif key == 'down':
jq_man.scroll_line = min(max(len(jq_man.out_data.split('\n')) - int(loop.screen_size[1] / 2), 0), jq_man.scroll_line + 1)
elif key == 'page up':
jq_man.scroll_line = max(0, jq_man.scroll_line - int(loop.screen_size[1] / 2))
elif key == 'page down':
jq_man.scroll_line = min(max(len(jq_man.out_data.split('\n')) - int(loop.screen_size[1] / 2), 0), jq_man.scroll_line + int(loop.screen_size[1] / 2))
jq_man.update_body()
else:
return super().keypress(size, key)
class WSLScreen(urwid.raw_display.Screen):
"""
This class is used to fix issue #6, where urwid has artifacts under WSL
"""
def write(self, data):
# replace urwid's SI/SO, which produce artifacts under WSL.
# at some point we may figure out what they actually do.
data = re.sub("[\x0e\x0f]", "", data)
super().write(data)
if __name__ == '__main__':
if sys.stdin.isatty():
sys.stderr.write('error: jqed requires some data piped on standard input, for example try: `ip --json link | jqed`\n')
exit(1)
if len(sys.argv) > 2:
sys.stderr.write('usage: jqed [initial expression]\n')
exit(1)
# Preserve original stdio, and replace stdio with /dev/tty
orig_stdin = os.fdopen(os.dup(sys.stdin.fileno()))
orig_stdout = os.fdopen(os.dup(sys.stdout.fileno()), mode='wb', buffering=0)
os.close(0)
os.close(1)
sys.stdin = open('/dev/tty', 'rb')
sys.stdout = open('/dev/tty', 'wb')
# Apparently urwid has some artifacts with WSL, see issue #6
# Hopefully this won't break WSL2
if IS_WSL:
urwid_screen = WSLScreen()
else:
urwid_screen = urwid.raw_display.Screen()
# Create gui
inp = BetterEdit(('prompt_ok', PROMPT))
if len(sys.argv) == 2:
# If the user specified an argument, use it as an initial expression
inp.set_edit_text(sys.argv[1])
inp.set_edit_pos(len(sys.argv[1]))
body = urwid.Text('')
body_filler = urwid.AttrMap(urwid.Filler(body, 'top'), 'body_plain')
err_bar = urwid.Text(('inp_plain', 'HELP: ^C: Exit, ^P: Pause, jq manual: https://stedolan.github.io/jq/manual'))
frame = urwid.Frame(
body_filler,
header=urwid.AttrMap(inp, 'inp_plain'),
footer=urwid.AttrMap(err_bar, 'err_bar'),
focus_part='header'
)
loop = urwid.MainLoop(frame, palette, handle_mouse=False, screen=urwid_screen)
try:
jq_man = JqManager(orig_stdin, loop)
loop.run()
except KeyboardInterrupt:
line = shlex.quote(inp.edit_text.strip())
if line.startswith("''"):
line = line[2:]
if line.endswith("''"):
line = line[:-2]
try:
orig_stdout.write(
('{}\njqed: jq editor ' + VERSION + ' https://github.com/wazzaps/jqed\n' +
'jqed: | jq {}\n').format(jq_man.out_data, line).encode())
except BrokenPipeError:
sys.stderr.write('jq {}\n'.format(line))
exit(0)

View file

@ -1,4 +1,16 @@
#!/usr/bin/env ruby -w
#
# jsonugly - Minify JSON by removing whitespace
#
# Reads JSON from stdin or files and outputs compact/minified JSON.
# Removes all unnecessary whitespace to make JSON "ugly" but smaller.
#
# Usage: jsonugly [file ...]
# cat file.json | jsonugly
#
# Examples:
# jsonugly data.json
# echo '{"a": 1, "b": 2}' | jsonugly # outputs {"a":1,"b":2}
require 'json'

79
lights
View file

@ -1,79 +0,0 @@
#!/usr/bin/env ruby
require 'huemote'
LIGHT_NAMES = ['Office 1', 'Office 2']
TEMPS = {
'bright' => {
brightness: 254,
temp: 4200,
},
'cloudy' => {
brightness: 220,
temp: 3500,
},
'sunrise' => {
brightness: 180,
temp: 2200,
},
'morning' => {
brightness: 220,
temp: 2600,
},
'noon' => {
brightness: 254,
temp: 4200,
},
'sunset' => {
brightness: 210,
temp: 2600,
},
'night' => {
brightness: 160,
temp: 2300,
},
'midnight' => {
brightness: 120,
temp: 2100,
},
}
TEMPS['sunny'] = TEMPS['bright']
TEMPS['overcast'] = TEMPS['cloudy']
setting = ARGV[0]
brightness = ARGV[1] || 220
transition = ARGV[2]
Huemote.set_ip '192.168.0.2'
lights = LIGHT_NAMES.map { |name| Huemote::Light.find(name) }
def kelvin_to_mireds(temp)
[[1_000_000 / temp, 154].max, 500].min
end
case setting
when 'off'
lights.each(&:off!)
when 'on'
lights.each(&:on!)
else
lights.each(&:on!)
attrs = TEMPS[setting] || {
temp: setting.to_i,
brightness: brightness.to_i,
}
if attrs
attrs[:bri] = attrs.delete(:brightness)
if attrs[:temp]
attrs[:ct] = kelvin_to_mireds(attrs.delete(:temp))
end
if transition
# tenths of a second
attrs[:transitiontime] = transition.to_i
end
lights.each do |l|
l.send(:set!, attrs)
end
else
puts "Unknown setting: #{setting}"
end
end

23
roll
View file

@ -1,4 +1,27 @@
#!/usr/bin/env ruby
#
# roll - Random choice selector from command line arguments
#
# Randomly selects one item from the provided command line arguments.
# Useful for making decisions or selecting random items from a list.
#
# Usage: roll <choice1> [<choice2> ...]
#
# Examples:
# roll heads tails
# roll red blue green yellow
# roll "option 1" "option 2" "option 3"
if ARGV.empty? || ARGV.include?('-h') || ARGV.include?('--help')
puts "Usage: #{File.basename(__FILE__)} <choice1> [<choice2> ...]"
puts "Randomly select one item from the provided arguments"
puts ""
puts "Examples:"
puts " #{File.basename(__FILE__)} heads tails"
puts " #{File.basename(__FILE__)} red blue green yellow"
puts " #{File.basename(__FILE__)} \"option 1\" \"option 2\" \"option 3\""
exit 0
end
choices = ARGV
puts choices.sample

View file

@ -1,5 +1,13 @@
#!/bin/sh
# save-keyboard-shortcuts.sh
#
# save-keyboard-shortcuts.sh - Export macOS keyboard shortcuts to restore script
#
# Reads all NSUserKeyEquivalents from macOS preferences and generates a script
# to restore them. Useful for backing up custom keyboard shortcuts.
#
# Usage: save-keyboard-shortcuts.sh
#
# Output: Creates ~/bin/restore-keyboard-shortcuts.sh
DESTFILE=~/bin/restore-keyboard-shortcuts.sh
echo '#!/bin/bash' > $DESTFILE
@ -7,3 +15,5 @@ echo '#!/bin/bash' > $DESTFILE
defaults find NSUserKeyEquivalents | sed -e "s/Found [0-9]* keys in domain '\\([^']*\\)':/defaults write \\1 NSUserKeyEquivalents '/" -e "s/ NSUserKeyEquivalents = {//" -e "s/};//" -e "s/}/}'/" >> $DESTFILE
echo killall cfprefsd >> $DESTFILE
chmod a+x $DESTFILE
echo "Keyboard shortcuts saved to $DESTFILE"

View file

@ -1,4 +1,8 @@
#!/bin/zsh
#
# sd-before-copy - Pre-backup script for SuperDuper! to run the backup vortex
#
# Usage: sd-before-copy (called automatically by SuperDuper!)
# Redirect stderr to stdout because SuperDuper! interprets any output to stderr as a failure.
# Mystifying decision but whatever. Easy to work around it.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,18 @@
#!/usr/bin/env ruby
#
# youtube-snarf-audio - Extract audio from YouTube videos
#
# Downloads YouTube videos and extracts audio to m4a format.
# Accepts YouTube URLs or just video IDs as arguments.
# Skips audio extraction if output file already exists.
#
# Usage: youtube-snarf-audio <youtube-url> [<youtube-url> ...]
#
# Examples:
# youtube-snarf-audio https://youtube.com/watch?v=abc123
# youtube-snarf-audio abc123
#
# Requires: youtube-dl, ffmpeg
require 'cgi'
require 'faraday'