Add CLAUDE.md and Readme.md, remove lots of cruft
This commit is contained in:
parent
16137efd33
commit
9e2234daa0
30 changed files with 280 additions and 41226 deletions
55
CLAUDE.md
Normal file
55
CLAUDE.md
Normal 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
47
Readme.md
Normal 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
|
||||
|
||||
28
colours.rb
28
colours.rb
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
pbpaste | ruby -e 'puts ARGF.read.length'
|
||||
|
|
@ -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__
|
||||
|
|
@ -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
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
docker run --rm --privileged alpine hwclock -s
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
69
flux
|
|
@ -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__
|
||||
24
flux-clouds
24
flux-clouds
|
|
@ -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__
|
||||
|
|
@ -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
|
||||
|
|
@ -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__
|
||||
|
|
@ -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}"
|
||||
|
|
@ -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
43
hist
|
|
@ -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
41
hist.rb
|
|
@ -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
294
jqed
|
|
@ -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)
|
||||
12
jsonugly
12
jsonugly
|
|
@ -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
79
lights
|
|
@ -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
23
roll
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
37902
supported_devices.csv
37902
supported_devices.csv
File diff suppressed because it is too large
Load diff
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in a new issue