├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── app-icon-extract ├── cask-analytics ├── cask-repair ├── gfv ├── linux-usb ├── lossless-compress ├── makeicns ├── manpages ├── _rebuild_man_pages ├── progressbar.1 └── progressbar.md ├── pingpong ├── progressbar └── seren /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | If you’re opening an issue to report `Error creating pull request: Unprocessable Entity (HTTP 422)` in `cask-repair`: 2 | 3 | 1. [Test your ssh connection to GitHub](https://help.github.com/articles/testing-your-ssh-connection/). 4 | 2. Add the following to your `~/.ssh/config` file (correct the last line with the actual path): 5 | 6 | ```git 7 | Host github.com 8 | Hostname github.com 9 | User git 10 | IdentityFile /path/to/ssh/key 11 | ``` 12 | 13 | --- 14 | 15 | If reporting any other issue, delete this template and go ahead. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny Scripts 2 | 3 | Tiny Scripts was a collection of scripts I created and maintained for over a decade. I have since changed that practive to publishing them to individual homes, so search my other repositories instead. 4 | 5 | Not all scripts were given a new home. These are no longer maintained but should nevertheless continue working indefinitely, so do use them for as long as you wish. 6 | 7 | | Script | Description | 8 | | ------------------- | -------------------------------------------------------------- | 9 | | `app-icon-extract` | Extract app bundle icon as png | 10 | | `cask-analytics` | Show analytics information for casks in the main taps | 11 | | `cask-repair` | Quickly repair outdated/broken Casks from homebrew-cask | 12 | | `gfv` | Make animated gifs from a video file | 13 | | `linux-usb` | Create bootable Linux USB sticks from ISOs on macOS | 14 | | `lossless-compress` | Losslessly compress files | 15 | | `pingpong` | Stitch a video with its reversed version, for continuous loops | 16 | | `progressbar` | Overlay a progress bar on videos or gifs | 17 | | `seren` | Rename files in a numerical sequence | 18 | -------------------------------------------------------------------------------- /app-icon-extract: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | readonly program="$(basename "${0}")" 4 | 5 | function get_output_path { 6 | local -r ext="${1}" 7 | local -r input_path="${2}" 8 | local -r init_output_path="${3}" 9 | 10 | if [[ -n "${init_output_path}" ]]; then 11 | [[ "${init_output_path##*.}" == "${ext##*.}" ]] && echo "${init_output_path}" || echo "${init_output_path}${ext}" 12 | else 13 | echo "$(pwd -P)/$(basename "${input_path%.*}${ext}")" 14 | fi 15 | } 16 | 17 | function try_overwrite { 18 | local -r force="${1}" 19 | local -r input_path="${2}" 20 | 21 | if [[ "${force}" == 'true' ]]; then 22 | mkdir -p "$(dirname "${input_path}")" 23 | return 0 24 | fi 25 | 26 | if [[ ! -d "$(dirname "${input_path}")" ]]; then 27 | echo "Cannot create '${input_path}'. Parent directory does not exist." >&2 28 | exit 1 29 | fi 30 | 31 | if [[ -e "${input_path}" ]]; then 32 | echo "Cannot write to '${input_path}'. Already exists." >&2 33 | exit 1 34 | fi 35 | } 36 | 37 | function usage { 38 | echo " 39 | Extract app bundle icon as png. 40 | 41 | Usage: 42 | ${program} [options] 43 | 44 | Options: 45 | -o, --output-file File to output to. Default is with same name on current directory. 46 | -O, --overwrite Create intermediary directories and overwrite output. 47 | -h, --help Show this message. 48 | " | sed -E 's/^ {4}//' 49 | } 50 | 51 | function get_app_key { 52 | local -r key="${1}" 53 | local -r app="${2}" 54 | local -r plist="${app}/Contents/Info.plist" 55 | 56 | if ! defaults read "${plist}" "${key}" 2> /dev/null; then 57 | return 1 58 | fi 59 | } 60 | 61 | # Options 62 | args=() 63 | while [[ "${1}" ]]; do 64 | case "${1}" in 65 | -h | --help) 66 | usage 67 | exit 0 68 | ;; 69 | -o | --output-file) 70 | readonly given_output_path="${2}" 71 | shift 72 | ;; 73 | -O | --overwrite) 74 | readonly overwrite='true' 75 | ;; 76 | --) 77 | shift 78 | args+=("${@}") 79 | break 80 | ;; 81 | -*) 82 | echo "Unrecognised option: ${1}" 83 | exit 1 84 | ;; 85 | *) 86 | args+=("${1}") 87 | ;; 88 | esac 89 | shift 90 | done 91 | set -- "${args[@]}" 92 | 93 | readonly input_app="${1}" 94 | readonly app_icon_name="$(get_app_key 'CFBundleIconFile' "${input_app}" | sed 's/\.icns$//')" 95 | readonly resources_dir="${input_app}/Contents/Resources" 96 | readonly icns="${resources_dir}/${app_icon_name}.icns" 97 | readonly output_file="$(get_output_path '.png' "${input_app}" "${given_output_path}")" 98 | try_overwrite "${overwrite:-false}" "${output_file}" 99 | 100 | if [[ "${#}" -ne 1 || ! -d "${input_app}" || "${input_app}" != *'.app' ]]; then 101 | echo 'An app bundle needs to be given as input' >&2 102 | exit 1 103 | fi 104 | 105 | if [[ ! -f "${icns}" ]]; then 106 | echo 'Could not find app icon.' >&2 107 | exit 1 108 | fi 109 | 110 | sips --setProperty format png "${icns}" --out "${output_file}" >/dev/null 111 | -------------------------------------------------------------------------------- /cask-analytics: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'date' 4 | require 'json' 5 | require 'open-uri' 6 | require 'open3' 7 | require 'optparse' 8 | require 'pathname' 9 | 10 | # Options 11 | ARGV.push('--help') if ARGV.empty? 12 | 13 | options = {} 14 | OptionParser.new do |parser| 15 | parser.banner = <<~BANNER 16 | Show analytics information for casks in the main taps. 17 | 18 | Usage: 19 | #{File.basename($PROGRAM_NAME)} [options] 20 | 21 | Options: 22 | BANNER 23 | 24 | parser.on( 25 | '-a', '--no-age', 26 | 'Do not show when cask was added (faster output).' 27 | ) 28 | end.parse!(into: options) 29 | 30 | # Helpers 31 | def shallow?(repo) 32 | Open3.capture2( 33 | 'git', '-C', repo.to_path, 34 | 'rev-parse', '--is-shallow-repository' 35 | ).first.strip == 'true' 36 | end 37 | 38 | # Run 39 | HBC_TAPS = Pathname.new(Open3.capture2('brew', '--repository', 'homebrew/cask').first).dirname 40 | 41 | ARGV.each do |cask_name| 42 | cask_path = HBC_TAPS.glob("homebrew-cask*/Casks/#{cask_name[0]}/#{cask_name}.rb").first 43 | 44 | abort 'Did not find any cask locally named ' + cask_name if cask_path.nil? 45 | 46 | puts cask_name 47 | 48 | analytics_dir = Pathname.new('/tmp').join('cask-analytics') 49 | analytics_dir.mkpath 50 | 51 | %w[30 90 365].each do |days| 52 | json_file = analytics_dir.join("#{days}d.json") 53 | 54 | unless json_file.exist? 55 | json_file.write(URI.parse( 56 | "https://formulae.brew.sh/api/analytics/cask-install/#{days}d.json" 57 | ).read) 58 | end 59 | 60 | analytics = JSON.parse(json_file.read)['items'] 61 | cask_info = analytics.select { |hash| hash['cask'] == cask_name }.first 62 | 63 | print "#{days} days: " 64 | 65 | if cask_info.nil? 66 | puts 'n/a' 67 | else 68 | puts "#{cask_info['count']} (##{cask_info['number']})" 69 | end 70 | end 71 | 72 | cask_tap_dir = cask_path.dirname.dirname 73 | 74 | unless options[:'no-age'] 75 | if shallow?(cask_tap_dir) 76 | system('git', '-C', cask_tap_dir.to_path, 'fetch', '--unshallow') 77 | end 78 | 79 | cask_added_date = Date.parse( 80 | Open3.capture2( 81 | 'git', '-C', cask_tap_dir.to_path, 82 | 'log', '--diff-filter=A', 83 | '--follow', '--max-count=1', 84 | '--format=%aI', cask_path.to_path 85 | ).first.strip 86 | ) 87 | 88 | cask_added_formatted = cask_added_date.strftime('%Y, %B %d') 89 | cask_age = (Date.today - cask_added_date).to_i.to_s 90 | 91 | puts "Age: #{cask_age} days (added #{cask_added_formatted})" 92 | end 93 | 94 | puts # Empty line to separate multiple casks 95 | end 96 | -------------------------------------------------------------------------------- /cask-repair: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | readonly program="$(basename "${0}")" 4 | declare -rx MACOS_VERSION='11' # Latest macOS version, so commands like `fetch` are not dependent on the contributor’s OS 5 | readonly submit_pr_to='homebrew:master' 6 | readonly caskroom_origin_remote_regex='(https://|(ssh://)?git@)github.com[/:]Homebrew/homebrew-cask' 7 | readonly caskroom_taps=(cask cask-versions cask-fonts cask-drivers) 8 | readonly caskroom_taps_dir="$(brew --repository)/Library/Taps/homebrew" 9 | readonly user_agent=(--user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10) https://brew.sh') 10 | readonly hub_config="${HOME}/.config/hub" 11 | readonly github_username="${GITHUB_USER:-$(awk '/user:/{print $(NF)}' "${hub_config}" 2>/dev/null | head -1)}" 12 | readonly cask_repair_remote_name="${github_username}" 13 | readonly cask_repair_branch_prefix='cask_repair_update' 14 | readonly submission_error_log="$(mktemp)" 15 | 16 | show_home='false' # By default, do not open the cask's homepage 17 | show_appcast='false' # By default, do not open the cask's appcast 18 | warning_messages=() 19 | has_errors='' 20 | 21 | function color_message { 22 | local color="${1}" 23 | local message="${2}" 24 | local -r all_colors=('black' 'red' 'green' 'yellow' 'blue' 'magenta' 'cyan' 'white') 25 | 26 | for i in "${!all_colors[@]}"; do 27 | if [[ "${all_colors[${i}]}" == "${color}" ]]; then 28 | local -r color_index="${i}" 29 | echo -e "$(tput setaf "${i}")${message}$(tput sgr0)" 30 | break 31 | fi 32 | done 33 | 34 | if [[ -z "${color_index}" ]]; then 35 | echo "${FUNCNAME[0]}: '${color}' is not a valid color." 36 | exit 1 37 | fi 38 | } 39 | 40 | function failure_message { 41 | color_message 'red' "${1}" >&2 42 | } 43 | 44 | function success_message { 45 | color_message 'green' "${1}" 46 | } 47 | 48 | function warning_message { 49 | color_message 'yellow' "${1}" 50 | } 51 | 52 | function push_failure_message { 53 | warning_message 'There were errors while pushing:' 54 | echo "${1}" 55 | abort 'Please fix the errors and try again. If the issue persists, open a bug report on the repo for this script (https://github.com/vitorgalvao/tiny-scripts).' 56 | } 57 | 58 | function require_hub { 59 | if ! hash 'hub' 2> /dev/null; then 60 | warning_message '`hub` was not found. Installing it…' 61 | brew install hub 62 | fi 63 | 64 | if [[ -z "${github_username}" ]] || [[ -z "${GITHUB_TOKEN}" && ! $(grep 'oauth_token:' "${hub_config}" 2>/dev/null) ]]; then 65 | abort '`hub` is not configured.\nTo do it, run `(cd $(brew --repository) && hub issue)`. Your Github password will be required, but is never stored.' 66 | fi 67 | } 68 | 69 | function usage { 70 | echo " 71 | Usage: 72 | ${program} [options] 73 | 74 | Options: 75 | -o, --open-home Open the homepage for the given cask. 76 | -a, --open-appcast Open the appcast for the given cask. 77 | -v, --cask-version Give a version directly, instead of being prompted for it. 78 | -s, --cask-sha Give a sha256 directly, skipping the download fetch. 79 | -u, --cask-url Give a URL directly, instead of being prompted for it. 80 | -e, --edit-cask Opens cask for editing before trying first download. 81 | -c , --closes-issue Adds 'Closes #.' to the pull request. 82 | -m , --message Adds '' to the pull request. 83 | -r, --reword Open commit message editor before committing. 84 | -b, --blind-submit Submit cask without asking for confirmation, if there are no errors. 85 | -f, --fail-on-error If there are any errors with the submission, abort. 86 | -w, --fail-on-warning If there are any warnings or errors with the submission, abort. 87 | -i, --install-cask Installs your updated cask after submission. 88 | -d, --delete-branches Deletes all local and remote branches named like ${cask_repair_branch_prefix}-. 89 | -h, --help Show this help. 90 | " | sed -E 's/^ {4}//' 91 | } 92 | 93 | function current_origin { 94 | git remote get-url origin 95 | } 96 | 97 | function current_tap { 98 | basename "$(current_origin)" '.git' 99 | } 100 | 101 | function ensure_caskroom_repos { 102 | local current_caskroom_taps 103 | 104 | current_caskroom_taps=($(HOMEBREW_NO_AUTO_UPDATE=1 brew tap | grep '^homebrew/cask' | sed 's|^homebrew/|homebrew-|')) 105 | 106 | for repo in "${caskroom_taps[@]}"; do 107 | if grep --silent "${repo}" <<< "${current_caskroom_taps[@]}"; then 108 | continue 109 | else 110 | warning_message "\`homebrew/${repo}\` not tapped. Tapping…" 111 | HOMEBREW_NO_AUTO_UPDATE=1 brew tap "homebrew/${repo}" 112 | fi 113 | done 114 | } 115 | 116 | function cd_to_cask_tap { 117 | local cask_file cask_file_location 118 | 119 | cask_file="${1}" 120 | 121 | cask_file_location="$(find "${caskroom_taps_dir}" -path "*/Casks/${cask_file}")" 122 | [[ -z "${cask_file_location}" ]] && abort "No such cask was found in any official repo (${cask_name})." 123 | cd "$(dirname "${cask_file_location}")" || abort "Failed to change to directory of ${cask_file}." 124 | } 125 | 126 | function require_correct_origin { 127 | local origin_remote 128 | 129 | origin_remote="$(current_origin)" 130 | 131 | grep --silent --ignore-case --extended-regexp "^${caskroom_origin_remote_regex}" <<< "${origin_remote}" || abort "\`origin\` is pointing to an incorrect remote (${origin_remote}). Its beginning must match ${caskroom_origin_remote_regex}." 132 | } 133 | 134 | function ensure_cask_repair_remote { 135 | if ! git remote | grep --silent "${cask_repair_remote_name}"; then 136 | warning_message "A \`${cask_repair_remote_name}\` remote does not exist. Creating it now…" 137 | 138 | hub fork 139 | fi 140 | } 141 | 142 | function http_status_code { 143 | local url follow_redirects 144 | 145 | url="${1}" 146 | [[ "${2}" == 'follow_redirects' ]] && follow_redirects='--location' || follow_redirects='--no-location' 147 | 148 | curl --silent --head "${follow_redirects}" "${user_agent[@]}" --write-out '%{http_code}' "${url}" --output '/dev/null' 149 | } 150 | 151 | function has_interpolation { 152 | [[ "${1}" =~ \#{version.*} ]] 153 | } 154 | 155 | function is_version_latest { 156 | local cask_file="${1}" 157 | 158 | [[ "$(brew cask _stanza version "${cask_file}")" == 'latest' ]] 159 | } 160 | 161 | function has_block_url { 162 | local cask_file="${1}" 163 | 164 | grep --silent 'url do' "${cask_file}" 165 | } 166 | 167 | function has_language_stanza { 168 | local cask_file="${1}" 169 | 170 | brew cask _stanza language "${cask_file}" 2>/dev/null 171 | } 172 | 173 | function modify_stanza { 174 | local stanza_to_modify new_stanza_value cask_file stanza_match_regex last_stanza_match stanza_start ending_comma 175 | 176 | stanza_to_modify="${1}" 177 | new_stanza_value="${2}" 178 | cask_file="${3}" 179 | 180 | stanza_match_regex="^\s*${stanza_to_modify} " 181 | last_stanza_match="$(grep "${stanza_match_regex}" "${cask_file}" | tail -1)" 182 | stanza_start="$(/usr/bin/perl -pe "s/(${stanza_match_regex}).*/\1/" <<< "${last_stanza_match}")" 183 | if grep --quiet ',$' <<< "${last_stanza_match}"; then 184 | ending_comma=',' 185 | fi 186 | 187 | /usr/bin/perl -0777 -i -e' 188 | $last_stanza_match = shift(@ARGV); 189 | $stanza_start = shift(@ARGV); 190 | $new_stanza_value = shift(@ARGV); 191 | $ending_comma = shift(@ARGV); 192 | print <> =~ s|\Q$last_stanza_match\E|$stanza_start$new_stanza_value$ending_comma|r; 193 | ' "${last_stanza_match}" "${stanza_start}" "${new_stanza_value}" "${ending_comma}" "${cask_file}" 194 | } 195 | 196 | function modify_url { 197 | local url cask_file 198 | 199 | url="${1}" 200 | cask_file="${2}" 201 | 202 | modify_stanza 'url' "\"${url}\"" "${cask_file}" 203 | } 204 | 205 | function appcast_url { 206 | local cask_file="${1}" 207 | 208 | brew cask _stanza appcast "${cask_file}" 209 | } 210 | 211 | function has_appcast { 212 | local cask_file="${1}" 213 | 214 | [[ -n "$(appcast_url "${cask_file}" 2>/dev/null)" ]] 215 | } 216 | 217 | function sha_change { 218 | local package_sha cask_file given_sha 219 | 220 | cask_file="${1}" 221 | given_sha="${2}" 222 | 223 | # If there is no checksum despite the cask being versioned, assume it is on purpose 224 | if [[ "$(brew cask _stanza sha256 "${cask_file}")" == ':no_check' ]] && ! is_version_latest "${cask_file}"; then 225 | return 226 | fi 227 | 228 | # If a sha256 was given, use that instead of fetching 229 | if [[ -n "${given_sha}" ]]; then 230 | modify_stanza 'sha256' "\"${given_sha}\"" "${cask_file}" 231 | return 232 | fi 233 | 234 | # Set sha256 as :no_check temporarily, to prevent mismatch errors when fetching 235 | modify_stanza 'sha256' ':no_check' "${cask_file}" 236 | 237 | if ! brew fetch --force "${cask_file}"; then 238 | clean 239 | abort "There was an error fetching ${cask_file}. Please check your connection and try again." 240 | fi 241 | package_sha="$(HOMEBREW_NO_COLOR=1 brew fetch "${cask_file}" 2>/dev/null | tail -1 | sed 's/SHA256: //')" 242 | 243 | modify_stanza 'sha256' "\"${package_sha}\"" "${cask_file}" 244 | } 245 | 246 | function delete_created_branches { 247 | local local_branches remote_branches 248 | 249 | for dir in "${caskroom_taps_dir}/homebrew-cask"*; do 250 | cd "${dir}" || abort "Failed to delete branches. ${dir} does not exist." 251 | 252 | if git remote | grep --silent "${cask_repair_remote_name}"; then # Proceed only if the correct remote exists 253 | # Delete local branches 254 | local_branches=$(git branch --all | grep --extended-regexp "^ *${cask_repair_branch_prefix}-.+$" | /usr/bin/perl -pe 's|^ *||;s|\n| |') 255 | [[ -n "${local_branches}" ]] && git branch -D ${local_branches} 256 | 257 | # Delete remote branches 258 | git fetch --prune "${cask_repair_remote_name}" 259 | remote_branches=$(git branch --all | grep --extended-regexp "remotes/${cask_repair_remote_name}/${cask_repair_branch_prefix}-.+$" | /usr/bin/perl -pe 's|.*/||;s|\n| |') 260 | [[ -n "${remote_branches}" ]] && git push "${cask_repair_remote_name}" --delete ${remote_branches} 261 | fi 262 | 263 | cd .. 264 | done 265 | } 266 | 267 | function edit_cask { 268 | local cask_file found_editor 269 | 270 | cask_file="${1}" 271 | 272 | echo 'Opening cask in default editor. If it is a GUI editor, you will need to completely quit it (⌘Q) before the script can continue.' 273 | 274 | for text_editor in {"${HOMEBREW_EDITOR}","${EDITOR}","${GIT_EDITOR}"}; do 275 | if [[ -n "${text_editor}" ]]; then 276 | eval "${text_editor}" "${cask_file}" 277 | found_editor='true' 278 | break 279 | fi 280 | done 281 | 282 | [[ -n "${found_editor}" ]] || open -W "${cask_file}" 283 | } 284 | 285 | function add_warning { 286 | local message severity color 287 | 288 | severity="${1}" 289 | message="$(sed '/./,$!d' <<< "${2}")" # Remove leading blank lines, so audit errors related to ruby still show 290 | 291 | if [[ "${severity}" == 'warning' ]]; then 292 | color="$(tput setaf 3)•$(tput sgr0)" 293 | else 294 | color="$(tput setaf 1)•$(tput sgr0)" 295 | has_errors='true' 296 | fi 297 | 298 | warning_messages+=("${color} ${message}") 299 | } 300 | 301 | function show_warnings { 302 | if [[ "${#warning_messages[@]}" -gt 0 ]]; then 303 | printf '%s\n' "${warning_messages[@]}" >&2 304 | divide 305 | fi 306 | } 307 | 308 | function clear_warnings { 309 | warning_messages=() 310 | unset has_errors 311 | } 312 | 313 | function lock { 314 | local lock_file action 315 | readonly lock_file='/tmp/cask-repair.lock' 316 | readonly action="${1}" 317 | 318 | if [[ "${action}" == 'create' ]]; then 319 | touch "${lock_file}" 320 | elif [[ "${action}" == 'exists?' ]]; then 321 | [[ -f "${lock_file}" ]] && return 0 || return 1 322 | elif [[ "${action}" == 'remove' ]]; then 323 | [[ -f "${lock_file}" ]] && rm "${lock_file}" 324 | fi 325 | } 326 | 327 | function clean { 328 | local current_branch 329 | 330 | lock 'remove' 331 | 332 | [[ "$(dirname "$(dirname "${PWD}")")" == "${caskroom_taps_dir}" ]] || return # Do not try to clean if not in a tap dir (e.g. if script was manually aborted too fast) 333 | 334 | # current_branch="$(git branch --show-current)" # Only available from git 2.22 (newer than the deault in Mojave) 335 | current_branch="$(git rev-parse --abbrev-ref HEAD)" 336 | 337 | git reset HEAD --hard --quiet 338 | git checkout master --quiet 339 | git branch -D "${current_branch}" --quiet 340 | [[ -f "${submission_error_log}" ]] && rm "${submission_error_log}" 341 | unset given_cask_version given_cask_url cask_updated 342 | } 343 | 344 | function skip { 345 | clean 346 | echo -e "${1}" 347 | } 348 | 349 | function abort { 350 | clean 351 | failure_message "\n${1}\n" 352 | exit 1 353 | } 354 | 355 | trap 'abort "You aborted."' SIGINT 356 | 357 | function divide { 358 | command -v 'hr' &>/dev/null && hr - || echo '--------------------' 359 | } 360 | 361 | # Options 362 | args=() 363 | while [[ "${1}" ]]; do 364 | case "${1}" in 365 | -h | --help) 366 | usage 367 | exit 0 368 | ;; 369 | -o | --open-home) 370 | show_home='true' 371 | ;; 372 | -a | --open-appcast) 373 | show_appcast='true' 374 | ;; 375 | -v | --cask-version) 376 | given_cask_version="${2}" 377 | shift 378 | ;; 379 | -s | --cask-sha) 380 | given_cask_sha="${2}" 381 | shift 382 | ;; 383 | -u | --cask-url) 384 | given_cask_url="${2}" 385 | shift 386 | ;; 387 | -e | --edit-cask) 388 | edit_on_start='true' 389 | ;; 390 | -c | --closes-issue) 391 | issue_to_close="${2}" 392 | shift 393 | ;; 394 | -m | --message) 395 | extra_message="${2}" 396 | shift 397 | ;; 398 | -r | --reword) 399 | reword_commit='true' 400 | ;; 401 | -b | --blind-submit) 402 | updated_blindly='true' 403 | ;; 404 | -f | --fail-on-error) 405 | abort_on_error='true' 406 | ;; 407 | -w | --fail-on-warning) 408 | abort_on_error='true' 409 | abort_on_warning='true' 410 | ;; 411 | -i | --install-cask) 412 | install_now='true' 413 | ;; 414 | -d | --delete-branches) 415 | can_run_without_arguments='true' 416 | delete_created_branches='true' 417 | ;; 418 | --) 419 | shift 420 | args+=("${@}") 421 | break 422 | ;; 423 | -*) 424 | echo "Unrecognised option: ${1}" 425 | exit 1 426 | ;; 427 | *) 428 | args+=("${1}") 429 | ;; 430 | esac 431 | shift 432 | done 433 | set -- "${args[@]}" 434 | 435 | # DEPRECATION MESSAGE 436 | warning_message '`cask-repair` is deprecated. I will accept PRs to fix bugs, but spending more time on it is difficult to justify. I recommend using `brew bump-cask-pr`. It doesn’t do as much in niche cases, but it does more in other slightly more relevant cases. It covers the vast majority of bump needs better. It ships with Homebrew so there’s nothing to install. Do `brew bump-cask-pr --help` to see how to use it.' 437 | 438 | # Exit if no argument or more than one argument was given 439 | if [[ -z "${1}" && "${can_run_without_arguments}" != 'true' ]]; then 440 | usage 441 | exit 1 442 | fi 443 | 444 | if [[ "${delete_created_branches}" == 'true' ]]; then 445 | delete_created_branches 446 | exit 0 447 | fi 448 | 449 | # Only allow one instance at a time 450 | if lock 'exists?'; then 451 | # We want this to be different from abort, so as to not remove the lock file 452 | failure_message "Only one ${program} instance can be run at once." 453 | exit 1 454 | else 455 | lock 'create' 456 | fi 457 | 458 | require_hub 459 | ensure_caskroom_repos 460 | 461 | if [[ -z "${HOMEBREW_NO_AUTO_UPDATE}" ]]; then 462 | brew update 463 | echo -n 'Updating taps… ' 464 | else 465 | warning_message "You have set 'HOMEBREW_NO_AUTO_UPDATE'. If ${program} fails, unset it and retry your command before submitting a bug report." 466 | fi 467 | 468 | for cask in "${@}"; do 469 | # Clean the cask's name, and check if it is valid 470 | cask_name="${cask%.rb}" # Remove '.rb' extension, if present 471 | cask_file="./${cask_name}.rb" 472 | cask_branch="${cask_repair_branch_prefix}-${cask_name}" 473 | 474 | cd_to_cask_tap "${cask_name}.rb" 475 | require_correct_origin 476 | ensure_cask_repair_remote 477 | 478 | has_language_stanza "${cask_file}" && abort "${cask_name} has a language stanza. It cannot be updated via this script. Try update_multilangual_casks: https://github.com/Homebrew/homebrew-cask/blob/master/developer/bin/update_multilangual_casks" 479 | 480 | git rev-parse --verify "${cask_branch}" &>/dev/null && git checkout "${cask_branch}" master --quiet || git checkout -b "${cask_branch}" master --quiet # Create branch or checkout if it already exists 481 | 482 | # Open home and appcast 483 | [[ "${show_home}" == 'true' ]] && brew home "${cask_file}" 484 | 485 | if has_appcast "${cask_file}"; then 486 | cask_appcast_url="$(appcast_url "${cask_file}")" 487 | 488 | if [[ "${show_appcast}" == 'true' ]]; then 489 | [[ "${cask_appcast_url}" =~ ^https://github.com.*releases.atom$ ]] && open "${cask_appcast_url%.atom}" || open "${cask_appcast_url}" # if appcast is from github releases, open the page instead of the feed 490 | fi 491 | fi 492 | 493 | # Show cask's current state 494 | divide 495 | cat "${cask_file}" 496 | divide 497 | 498 | # Save old cask version 499 | old_cask_version="$(brew cask _stanza version "${cask_file}")" 500 | 501 | # Set cask version 502 | if [[ -z "${given_cask_version}" ]]; then 503 | read -rp $'Type the new version (or leave blank to use current one, or use `s` to skip)\n> ' given_cask_version # Ask for cask version, if not given previously 504 | 505 | if [[ "${given_cask_version}" == 's' ]]; then 506 | skip 'Skipping…' 507 | continue 508 | fi 509 | 510 | [[ -z "${given_cask_version}" ]] && given_cask_version="${old_cask_version}" 511 | fi 512 | 513 | if [[ "${given_cask_version}" == ':latest' || "${given_cask_version}" == 'latest' ]]; then # Allow both ':latest' and 'latest' to be given 514 | modify_stanza 'version' ':latest' "${cask_file}" 515 | else 516 | modify_stanza 'version' "\"${given_cask_version}\"" "${cask_file}" 517 | fi 518 | 519 | if [[ -n "${given_cask_url}" ]]; then 520 | if has_block_url "${cask_file}"; then 521 | warning_message 'Cask has block url, so it can only be modified manually (choose `[e]dit` when prompted).' 522 | else 523 | modify_url "${given_cask_url}" "${cask_file}" 524 | fi 525 | else 526 | # If url does not use interpolation, is not block, and not updating blindly, ask for it 527 | cask_bare_url=$(grep "url ['\"].*['\"]" "${cask_file}" | sed -E "s|.*url ['\"](.*)['\"].*|\1|") 528 | 529 | if ! has_interpolation "${cask_bare_url}" && ! has_block_url "${cask_file}" && [[ -z "${updated_blindly}" ]]; then 530 | read -rp $'Paste the new URL (or leave blank to use the current one)\n> ' given_cask_url 531 | 532 | [[ -n "${given_cask_url}" ]] && modify_url "${given_cask_url}" "${cask_file}" 533 | fi 534 | 535 | cask_url=$(brew cask _stanza url "${cask_file}") 536 | 537 | # Check if the URL sends a 200 HTTP code, else abort 538 | cask_url_status=$(http_status_code "${cask_url}" 'follow_redirects') 539 | 540 | [[ "${cask_url}" =~ (github.com|bitbucket.org) ]] && cask_url_status='200' # If the download URL is from github or bitbucket, fake the status code 541 | 542 | if [[ "${cask_url_status}" != '200' ]] && [[ -z "${updated_blindly}" ]]; then 543 | [[ -z "${cask_url_status}" ]] && add_warning warning 'you need to use a valid URL' || add_warning warning "url is probably incorrect, as a non-200 (OK) HTTP response code was returned (${cask_url_status})" 544 | fi 545 | fi 546 | 547 | [[ "${edit_on_start}" == 'true' ]] && edit_cask "${cask_file}" 548 | 549 | if is_version_latest "${cask_file}"; then 550 | modify_stanza 'sha256' ':no_check' "${cask_file}" 551 | else 552 | sha_change "${cask_file}" "${given_cask_sha}" 553 | fi 554 | 555 | # Check if everything is alright, else abort 556 | [[ -z "${cask_updated}" ]] && cask_updated='false' 557 | until [[ "${cask_updated}" =~ ^[yne]$ ]]; do 558 | # fix style errors and check for style and audit errors 559 | style_message=$(brew style --fix "${cask_file}" 2>/dev/null) 560 | style_result="${?}" 561 | [[ "${style_result}" -ne 0 ]] && add_warning error "${style_message}" 562 | 563 | audit_message=$(brew audit "${cask_file}" 2>/dev/null) 564 | audit_result="${?}" 565 | [[ "${audit_result}" -ne 0 ]] && add_warning error "${audit_message}" 566 | 567 | git --no-pager diff 568 | divide 569 | show_warnings 570 | [[ -n "${abort_on_error}" && "${has_errors}" == 'true' ]] && abort 'The submission has errors and you elected to abort on those cases.' 571 | [[ -n "${abort_on_warning}" && "${#warning_messages[@]}" -gt 0 ]] && abort 'The submission has warnings and you elected to abort on those cases.' 572 | 573 | if [[ -n "${updated_blindly}" && "${#warning_messages[@]}" -eq 0 ]]; then 574 | cask_updated='y' 575 | else 576 | read -rn1 -p 'Is everything correct? ([y]es / [n]o / [e]dit) ' cask_updated 577 | echo # Add an empty line 578 | fi 579 | 580 | if [[ "${cask_updated}" == 'y' ]]; then 581 | if [[ "${style_result}" -ne 0 || "${audit_result}" -ne 0 ]]; then 582 | cask_updated='false' 583 | else 584 | break 585 | fi 586 | elif [[ "${cask_updated}" == 'e' ]]; then 587 | edit_cask "${cask_file}" 588 | if ! is_version_latest "${cask_file}"; then # Recheck sha256 values if version isn't :latest 589 | sha_change "${cask_file}" 590 | fi 591 | cask_updated='false' 592 | clear_warnings 593 | elif [[ "${cask_updated}" == 'n' ]]; then 594 | abort 'You decided to abort.' 595 | fi 596 | done 597 | 598 | # Skip if no changes were made, submit otherwise 599 | if git diff-index --quiet HEAD --; then 600 | skip 'No changes made to the cask. Skipping…' 601 | continue 602 | else 603 | echo 'Submitting…' 604 | fi 605 | 606 | # Grab version as it ended up in the cask 607 | cask_version="$(brew cask _stanza version "${cask_file}")" 608 | 609 | # Commit, push, submit pull request, clean 610 | [[ "${old_cask_version}" == "${cask_version}" ]] && commit_message="Update ${cask_name}" || commit_message="Update ${cask_name} from ${old_cask_version} to ${cask_version}" 611 | 612 | if [[ -n "${reword_commit}" ]]; then 613 | git commit "${cask_file}" --message "${commit_message}" --edit --quiet 614 | commit_message="$(git log --format=%B -n 1 HEAD | head -n 1)" 615 | else 616 | git commit "${cask_file}" --message "${commit_message}" --quiet 617 | fi 618 | 619 | pr_message="${commit_message}\n\n**Important:** *Do not tick a checkbox if you haven’t performed its action.* Honesty is indispensable for a smooth review process.\n\nAfter making all changes to a cask, verify:\n\n- [ ] The submission is for [a stable version](https://github.com/Homebrew/homebrew-cask/blob/master/doc/development/adding_a_cask.md#stable-versions) or [documented exception](https://github.com/Homebrew/homebrew-cask/blob/master/doc/development/adding_a_cask.md#but-there-is-no-stable-version).\n- [x] \`brew audit --cask {{cask_file}}\` is error-free.\n- [x] \`brew style --fix {{cask_file}}\` reports no offenses." 620 | [[ -n "${issue_to_close}" ]] && pr_message+="\n\nCloses #${issue_to_close}." 621 | [[ -n "${extra_message}" ]] && pr_message+="\n\n${extra_message}" 622 | submit_pr_from="${github_username}:${cask_branch}" 623 | 624 | git push --force "${cask_repair_remote_name}" "${cask_branch}" --quiet 2> "${submission_error_log}" 625 | 626 | if [[ "${?}" -ne 0 ]]; then 627 | # Fix common push errors 628 | if grep --quiet 'shallow update not allowed' "${submission_error_log}"; then 629 | echo 'Push failed due to shallow repo. Unshallowing…' 630 | HOMEBREW_NO_AUTO_UPDATE=1 brew tap --full "homebrew/$(current_tap)" 631 | git push --force "${cask_repair_remote_name}" "${cask_branch}" --quiet 2> "${submission_error_log}" 632 | 633 | [[ "${?}" -ne 0 ]] && push_failure_message "$(< "${submission_error_log}")" 634 | else 635 | push_failure_message "$(< "${submission_error_log}")" 636 | fi 637 | fi 638 | 639 | pr_link=$(hub pull-request -b "${submit_pr_to}" -h "${submit_pr_from}" -m "$(echo -e "${pr_message}")") 640 | 641 | if [[ -n "${pr_link}" ]]; then 642 | if [[ -n "${install_now}" ]]; then 643 | success_message 'Updating cask locally…' 644 | brew reinstall "${cask_file}" 645 | else 646 | echo -e "\nYou can upgrade the cask right now from your personal branch:\n brew reinstall https://raw.githubusercontent.com/${github_username}/$(current_tap)/${cask_branch}/Casks/${cask_name}.rb" 647 | fi 648 | 649 | clean 650 | success_message "\nSubmitted (${pr_link})\n" 651 | else 652 | abort 'There was an error submitting the pull request. Please open a bug report on the repo for this script (https://github.com/vitorgalvao/tiny-scripts).' 653 | fi 654 | done 655 | -------------------------------------------------------------------------------- /gfv: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | readonly program="$(basename "${0}")" 4 | readonly cap_fps='30' 5 | readonly tmp_dir="$(mktemp -d)" 6 | trap 'rm -rf "${tmp_dir}"' EXIT 7 | 8 | function depends_on { 9 | local -r all_deps=("${@}") 10 | local missing_deps=() 11 | 12 | for dep in "${all_deps[@]}"; do 13 | hash "${dep}" 2> /dev/null || missing_deps+=("${dep}") 14 | done 15 | 16 | if [[ "${#missing_deps[@]}" -gt 0 ]]; then 17 | echo 'Missing required tools:' >&2 18 | printf ' %s\n' "${missing_deps[@]}" >&2 19 | exit 1 20 | fi 21 | } 22 | 23 | function get_output_path { 24 | local -r ext="${1}" 25 | local -r input_path="${2}" 26 | local -r init_output_path="${3}" 27 | 28 | if [[ -n "${init_output_path}" ]]; then 29 | [[ "${init_output_path##*.}" == "${ext##*.}" ]] && echo "${init_output_path}" || echo "${init_output_path}${ext}" 30 | else 31 | echo "$(pwd -P)/$(basename "${input_path%.*}${ext}")" 32 | fi 33 | } 34 | 35 | function try_overwrite { 36 | local -r force="${1}" 37 | local -r input_path="${2}" 38 | 39 | if [[ "${force}" == 'true' ]]; then 40 | mkdir -p "$(dirname "${input_path}")" 41 | return 0 42 | fi 43 | 44 | if [[ ! -d "$(dirname "${input_path}")" ]]; then 45 | echo "Cannot create '${input_path}'. Parent directory does not exist." >&2 46 | exit 1 47 | fi 48 | 49 | if [[ -e "${input_path}" ]]; then 50 | echo "Cannot write to '${input_path}'. Already exists." >&2 51 | exit 1 52 | fi 53 | } 54 | 55 | function usage { 56 | echo " 57 | Usage: 58 | ${program} [options] 59 | 60 | Options: 61 | -f , --fps Frames per second. Default is auto-detected from video, capped at ${cap_fps}. 62 | -w , --width Resize the gif proportionally. 63 | -o , --output-file File to output to. Default is with same name on current directory. 64 | -O, --overwrite Create intermediary directories and overwrite output. 65 | -g, --gifski Use gifski for the conversion. 66 | -h, --help Show this help. 67 | " | sed -E 's/^ {4}//' 68 | } 69 | 70 | # Options 71 | args=() 72 | while [[ "${1}" ]]; do 73 | case "${1}" in 74 | -h | --help) 75 | usage 76 | exit 0 77 | ;; 78 | -f | --fps) 79 | readonly chosen_fps="${2}" 80 | shift 81 | ;; 82 | -w | --width) 83 | readonly width="${2}" 84 | shift 85 | ;; 86 | -o | --output-file) 87 | readonly given_output_path="${2}" 88 | shift 89 | ;; 90 | -O | --overwrite) 91 | readonly overwrite='true' 92 | ;; 93 | -g | --gifski) 94 | depends_on 'gifski' 95 | readonly use_gifski='true' 96 | ;; 97 | --) 98 | shift 99 | args+=("${@}") 100 | break 101 | ;; 102 | -*) 103 | echo "Unrecognised option: ${1}" 104 | exit 1 105 | ;; 106 | *) 107 | args+=("${1}") 108 | ;; 109 | esac 110 | shift 111 | done 112 | set -- "${args[@]}" 113 | 114 | readonly input_file="${1}" 115 | readonly output_file="$(get_output_path '.gif' "${input_file}" "${given_output_path}")" 116 | try_overwrite "${overwrite:-false}" "${output_file}" 117 | 118 | if [[ "${#}" -ne 1 || ! -f "${input_file}" ]]; then 119 | usage 120 | exit 1 121 | fi 122 | 123 | depends_on 'ffmpeg' 124 | 125 | # Set FPS 126 | if [[ -z "${chosen_fps}" ]]; then 127 | depends_on 'ffprobe' 128 | 129 | readonly video_fps="$(ffprobe -loglevel error -select_streams v:0 -of default=noprint_wrappers=1:nokey=1 -show_entries stream=r_frame_rate "${input_file}" | bc)" 130 | readonly fps="$([[ "${video_fps}" -gt "${cap_fps}" ]] && echo "${cap_fps}" || echo "${video_fps}")" 131 | else 132 | readonly fps="${chosen_fps}" 133 | fi 134 | 135 | # Make the animated gif 136 | if [[ -z "${use_gifski}" ]]; then 137 | [[ -z "${width}" ]] && width='-1' # Make width same as original video, if none given 138 | 139 | readonly palette="${tmp_dir}/palette.png" 140 | ffmpeg -i "${input_file}" -filter_complex "fps=${fps},scale=${width}:-1:flags=lanczos,palettegen" "${palette}" 141 | ffmpeg -i "${input_file}" -i "${palette}" -filter_complex "fps=${fps},scale=${width}:-1:flags=lanczos[x];[x][1:v]paletteuse" "${output_file}" 142 | else 143 | readonly frames_dir="${tmp_dir}/frames" 144 | mkdir -p "${frames_dir}" 145 | 146 | echo 'Extracting images from video…' 147 | ffmpeg -loglevel quiet -i "${input_file}" -filter_complex "fps=${fps}" "${frames_dir}/output_mage%9d.png" 148 | 149 | options=() 150 | [[ -n "${width}" ]] && options+=('--width' "${width}") 151 | options+=('--fps' "${fps}") 152 | 153 | gifski "${frames_dir}/"* "${options[@]}" --output "${output_file}" 154 | fi 155 | -------------------------------------------------------------------------------- /linux-usb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | IFS=$'\n' 4 | 5 | # Helpers 6 | function error { 7 | echo "${1}" >&2 8 | exit 1 9 | } 10 | 11 | function usage { 12 | echo " 13 | Create bootable Linux USB sticks from ISOs on macOS. 14 | 15 | Usage: 16 | ${program} [options] 17 | 18 | Options: 19 | -h, --help Show this help. 20 | " | sed -E 's/^ {4}//' 21 | } 22 | 23 | # Checks 24 | if [[ "${1}" =~ ^(-h|--help)$ ]] 25 | then 26 | usage 27 | exit 0 28 | fi 29 | 30 | readonly linux_iso="${1}" 31 | 32 | if [[ -z "${linux_iso}" || "$(file --mime-type --brief "${linux_iso}")" != 'application/x-iso9660-image' ]] 33 | then 34 | usage 35 | exit 1 36 | fi 37 | 38 | # Get disk to save to 39 | readonly external_disks=($(diskutil list | grep '(external, physical)' | awk '{ print $1 }')) 40 | 41 | [[ "${#external_disks[@]}" -lt 1 ]] && error 'Found no external disks. Connect one and run the script again.' 42 | 43 | disks_with_info=() 44 | disk_order='0' 45 | 46 | for disk in "${external_disks[@]}"; do 47 | disk_order="$(bc <<< "${disk_order} + 1")" 48 | disk_size="$(diskutil info "${disk}" | grep 'Disk Size' | awk '{ print $3" "$4 }')" 49 | disks_with_info+=("[${disk_order}] ${disk} ${disk_size}") 50 | done 51 | 52 | while [[ -z "${disk_to_write_number}" ]] || [[ "${disk_to_write_number}" -lt 1 ]] || [[ "${disk_to_write_number}" -gt "${#disks_with_info[@]}" ]]; do 53 | echo 'Pick a disk do write to:' 54 | printf '%s\n' "${disks_with_info[@]}" 55 | echo 56 | read -r -p '> ' disk_to_write_number 57 | done 58 | 59 | readonly disk_to_write="$(sed "${disk_to_write_number}q;d" <<< "${disks_with_info[@]}" | awk '{ print $2 }')" 60 | 61 | # Convert the Linux iso 62 | readonly linux_dmg="$(mktemp).dmg" 63 | hdiutil convert -quiet "${linux_iso}" -format UDRW -o "${linux_dmg}" 64 | 65 | # Write to the disk 66 | diskutil unmountDisk "${disk_to_write}" 67 | echo 'Linux will now be saved to your USB flash drive. This should take a while. You may need to enter your password (for the write permissions).' 68 | sudo dd if="${linux_dmg}" of="${disk_to_write}" status=progress 69 | 70 | # eject disk 71 | diskutil eject "${disk_to_write}" 72 | 73 | echo 'Done. You now have a bootable Linux USB flash drive.' 74 | -------------------------------------------------------------------------------- /lossless-compress: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | readonly program="$(basename "${0}")" 4 | readonly formats=('jpg' 'png' 'gif') 5 | 6 | function depends_on { 7 | local -r all_deps=("${@}") 8 | local missing_deps=() 9 | 10 | for dep in "${all_deps[@]}"; do 11 | hash "${dep}" 2> /dev/null || missing_deps+=("${dep}") 12 | done 13 | 14 | if [[ "${#missing_deps[@]}" -gt 0 ]]; then 15 | echo 'Missing required tools:' >&2 16 | printf ' %s\n' "${missing_deps[@]}" >&2 17 | exit 1 18 | fi 19 | } 20 | 21 | function usage { 22 | echo " 23 | Losslessly compress files. Supported formats: 24 | ${formats[*]} 25 | 26 | Usage: 27 | ${program} 28 | 29 | Options: 30 | -h, --help Show this help. 31 | " | sed -E 's/^ {4}//' 32 | } 33 | 34 | if [[ "${#}" -lt 1 ]]; then 35 | usage 36 | exit 1 37 | fi 38 | 39 | if [[ "${1}" =~ ^(-h|--help)$ ]]; then 40 | usage 41 | exit 0 42 | fi 43 | 44 | depends_on 'jpegtran' 'oxipng' 'gifsicle' 45 | 46 | for image_file in "${@}"; do 47 | echo "Compressing ${image_file}…" 48 | 49 | file_type="$(file --mime-type --brief "${image_file}" | cut -d'/' -f2)" 50 | 51 | if [[ "${file_type}" == 'jpeg' ]]; then 52 | jpegtran -copy none -optimize -progressive -outfile "${image_file}" "${image_file}" 53 | elif [[ "${file_type}" == 'png' ]]; then 54 | oxipng --quiet "${image_file}" 55 | elif [[ "${file_type}" == 'gif' ]]; then 56 | gifsicle --optimize=3 --batch "${image_file}" 57 | else 58 | echo "'${file_type}' is not a supported image type." 59 | fi 60 | done 61 | -------------------------------------------------------------------------------- /makeicns: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | readonly program="$(basename "${0}")" 4 | 5 | function usage { 6 | echo " 7 | Generate an icns icon from a png. 8 | 9 | Usage: 10 | ${program} [options] 11 | 12 | Options: 13 | -i, --input File to convert. 14 | -o, --output File to save. 15 | -h, --help Show this message. 16 | " | sed -E 's/^ {4}//' 17 | 18 | exit "${1}" 19 | } 20 | 21 | # Options 22 | args=() 23 | while [[ "${1}" ]] 24 | do 25 | case "${1}" in 26 | -h | --help) 27 | usage 0 28 | ;; 29 | -i | --input) 30 | readonly input="$(realpath "${2}")" 31 | shift 32 | ;; 33 | -o | --output) 34 | readonly output="${2%.icns}.icns" # Add extension if missing 35 | shift 36 | ;; 37 | --) 38 | shift 39 | args+=("${@}") 40 | break 41 | ;; 42 | -*) 43 | echo "Unrecognised option: ${1}" 44 | exit 1 45 | ;; 46 | *) 47 | args+=("${1}") 48 | ;; 49 | esac 50 | shift 51 | done 52 | set -- "${args[@]}" 53 | 54 | # Checks 55 | [[ -f "${input}" && -n "${output}" ]] || usage 1 56 | 57 | # Main 58 | readonly iconset="$(mktemp -d)" 59 | 60 | if [[ "$(/usr/bin/file --mime-type --brief "${input}")" != 'image/png' ]] 61 | then 62 | echo 'Image needs to be a png.' >&2 63 | exit 1 64 | fi 65 | 66 | for size in {16,32,64,128,256,512} 67 | do 68 | /usr/bin/sips --resampleHeightWidth "${size}" "${size}" "${input}" --out "${iconset}/icon_${size}x${size}.png" &> /dev/null 69 | /usr/bin/sips --resampleHeightWidth "$((size * 2))" "$((size * 2))" "${input}" --out "${iconset}/icon_${size}x${size}@2x.png" &> /dev/null 70 | done 71 | 72 | /bin/mv "${iconset}" "${iconset}.iconset" 73 | /usr/bin/iconutil --convert icns "${iconset}.iconset" --output "${output}" 74 | -------------------------------------------------------------------------------- /manpages/_rebuild_man_pages: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "${0}")" || exit 1 4 | 5 | for md_file in *md; do 6 | file_no_extension="${md_file%.*}" 7 | sed "s/’/'/g;s/[“”]/\"/g;s/[—–]/-/g;s/…/.../g" "${md_file}" | ronn --manual "${file_no_extension}" --organization 'Vítor Galvão' --roff > "${file_no_extension}.1" 8 | done 9 | -------------------------------------------------------------------------------- /manpages/progressbar.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "PROGRESSBAR" "1" "May 2020" "Vítor Galvão" "progressbar" 5 | . 6 | .SH "NAME" 7 | \fBprogressbar\fR \- overlay a progress bar on videos and gifs 8 | . 9 | .SH "SYNOPSIS" 10 | \fBprogressbar [options]\fR 11 | . 12 | .SH "DESCRIPTION" 13 | Convert a sequence of images or a video file to a gif or video with a progress bar\. 14 | . 15 | .P 16 | Initially built to make short snippets of looping instructionals clearer as to where they start\. 17 | . 18 | .SH "OPTIONS" 19 | . 20 | .TP 21 | \fB\-c, \-\-bar\-color\fR \fIcolor\fR 22 | Set the bar\'s color\. Default: \fB#f12b24\fR\. 23 | . 24 | .TP 25 | \fB\-s, \-\-bar\-height\fR \fInumber\fR 26 | Set the bar\'s height as a percent of the total height\. Default: \fB1\fR\. 27 | . 28 | .TP 29 | \fB\-p, \-\-bar\-position\fR \fItop|bottom\fR 30 | Default: \fBbottom\fR\. 31 | . 32 | .TP 33 | \fB\-d, \-\-delay\fR \fInumber\fR 34 | Delay between each frame, in seconds\. Ignored when input is a video\. Default: \fB1\.5\fR\. 35 | . 36 | .TP 37 | \fB\-o, \-\-output\-file\fR \fIfile\fR 38 | File to output to\. Give it a \.mov extension to save as a video, or any other to save as gif\. Default: \fBoutput\.gif\fR in the current directory\. 39 | . 40 | .TP 41 | \fB\-g\fR, \fB\-\-gifski\fR 42 | Use \fBgifski\fR for the conversion\. 43 | . 44 | .TP 45 | \fB\-h, \-\-help\fR 46 | Show help\. 47 | . 48 | .SH "AUTHORS" 49 | Vítor Galvão (https://vitorgalvao\.com) 50 | -------------------------------------------------------------------------------- /manpages/progressbar.md: -------------------------------------------------------------------------------- 1 | # progressbar(1) - overlay a progress bar on videos and gifs 2 | 3 | ## SYNOPSIS 4 | 5 | `progressbar [options]` 6 | 7 | ## DESCRIPTION 8 | 9 | Convert a sequence of images or a video file to a gif or video with a progress bar. 10 | 11 | Initially built to make short snippets of looping instructionals clearer as to where they start. 12 | 13 | ## OPTIONS 14 | 15 | * `-c, --bar-color` : 16 | Set the bar’s color. Default: `#f12b24`. 17 | 18 | * `-s, --bar-height` : 19 | Set the bar’s height as a percent of the total height. Default: `1`. 20 | 21 | * `-p, --bar-position` : 22 | Default: `bottom`. 23 | 24 | * `-d, --delay` : 25 | Delay between each frame, in seconds. Ignored when input is a video. Default: `1.5`. 26 | 27 | * `-o, --output-file` : 28 | File to output to. Give it a .mov extension to save as a video, or any other to save as gif. Default: `output.gif` in the current directory. 29 | 30 | * `-g`, `--gifski`: 31 | Use `gifski` for the conversion. 32 | 33 | * `-h, --help`: 34 | Show help. 35 | 36 | ## AUTHORS 37 | 38 | Vítor Galvão (https://vitorgalvao.com) 39 | -------------------------------------------------------------------------------- /pingpong: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | readonly program="$(basename "${0}")" 4 | 5 | function depends_on { 6 | local -r all_deps=("${@}") 7 | local missing_deps=() 8 | 9 | for dep in "${all_deps[@]}"; do 10 | hash "${dep}" 2> /dev/null || missing_deps+=("${dep}") 11 | done 12 | 13 | if [[ "${#missing_deps[@]}" -gt 0 ]]; then 14 | echo 'Missing required tools:' >&2 15 | printf ' %s\n' "${missing_deps[@]}" >&2 16 | exit 1 17 | fi 18 | } 19 | 20 | function get_output_path_ext { 21 | local -r ext="${1}" 22 | local -r input_path="${2}" 23 | local output_path="${3}" 24 | 25 | if [[ -n "${output_path}" ]]; then 26 | [[ "${output_path##*.}" == "${ext}" ]] && echo "${output_path}" || echo "${output_path}${ext}" 27 | else 28 | output_path="${input_path%.*}${ext}" 29 | 30 | while [[ -e "${output_path}" ]]; do 31 | output_path="${output_path%.*}_$(date -u +'%Y.%m.%d.%H%M%S')${ext}" 32 | done 33 | 34 | echo "${output_path}" 35 | fi 36 | } 37 | 38 | function usage { 39 | echo " 40 | Stitch a video with its reversed version, for continuous loops 41 | 42 | Usage: 43 | ${program} [options] 44 | 45 | Options: 46 | -o, --output-file File to output to. Default is saving next to the input file with same name and date. 47 | -h, --help Show this help. 48 | " | sed -E 's/^ {4}//' 49 | } 50 | 51 | args=() 52 | while [[ "${1}" ]]; do 53 | case "${1}" in 54 | -h | --help) 55 | usage 56 | exit 0 57 | ;; 58 | -o | --output-file) 59 | readonly given_output_path="${2}" 60 | shift 61 | ;; 62 | --) 63 | shift 64 | args+=("${@}") 65 | break 66 | ;; 67 | -*) 68 | echo "Unrecognised option: ${1}" 69 | exit 1 70 | ;; 71 | *) 72 | args+=("${1}") 73 | ;; 74 | esac 75 | shift 76 | done 77 | set -- "${args[@]}" 78 | 79 | if [[ "${#}" -eq 0 ]]; then 80 | usage 81 | exit 1 82 | fi 83 | 84 | readonly input_file="${1}" 85 | readonly input_file_ext="$([[ "${input_file}" == *.* ]] && echo ".${input_file##*.}" || echo '')" 86 | readonly output_file="$(get_output_path_ext "${input_file_ext}" "${input_file}" "${given_output_path}")" 87 | 88 | depends_on 'ffmpeg' 89 | 90 | ffmpeg -i "${input_file}" -an -filter_complex "[0]reverse[r];[0][r]concat" "${output_file}" 91 | -------------------------------------------------------------------------------- /progressbar: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | readonly program="$(basename "${0}")" 4 | 5 | # Defaults 6 | bar_color='#f12b24' 7 | bar_height_percent='1' 8 | bar_pos='bottom' 9 | seconds_delay='1.5' 10 | output_file='output.gif' 11 | 12 | function message { 13 | echo "${1}" 14 | } 15 | 16 | function wrong_arguments { 17 | echo 'You need either give multiple images or one video file as arguments' >&2 18 | usage 19 | exit 1 20 | } 21 | 22 | function depends_on { 23 | local -r all_deps=("${@}") 24 | local missing_deps=() 25 | 26 | for dep in "${all_deps[@]}"; do 27 | hash "${dep}" 2> /dev/null || missing_deps+=("${dep}") 28 | done 29 | 30 | if [[ "${#missing_deps[@]}" -gt 0 ]]; then 31 | echo 'Missing required tools:' >&2 32 | printf ' %s\n' "${missing_deps[@]}" >&2 33 | exit 1 34 | fi 35 | } 36 | 37 | function usage { 38 | echo " 39 | Usage: 40 | ${program} [options] 41 | 42 | Options: 43 | -c, --bar-color Default: #f12b24. 44 | -s, --bar-height Bar’s height as a percent of the total height. Default: 1. 45 | -p, --bar-position [top|bottom] Default: bottom. 46 | -d, --delay Delay between each frame, in seconds. Default: 1.5. 47 | -o, --output-file File to output to. Default: output.gif in current directory. Saves to video when given the .mov extension. 48 | -g, --gifski Use gifski for the conversion. 49 | -h, --help Show this help. 50 | " | sed -E 's/^ {4}//' 51 | } 52 | 53 | # Options 54 | args=() 55 | while [[ "${1}" ]]; do 56 | case "${1}" in 57 | -h | --help) 58 | usage 59 | exit 0 60 | ;; 61 | -c | --bar-color) 62 | bar_color="${2}" 63 | shift 64 | ;; 65 | -s | --bar-height) 66 | bar_height_percent="${2}" 67 | shift 68 | ;; 69 | -p | --bar-position) 70 | bar_pos="${2}" 71 | shift 72 | ;; 73 | -d | --delay) 74 | seconds_delay="${2}" 75 | shift 76 | ;; 77 | -o | --output-file) 78 | output_file="${2}" 79 | shift 80 | ;; 81 | -g | --gifski) 82 | use_gifski='true' 83 | ;; 84 | --) 85 | shift 86 | args+=("${@}") 87 | break 88 | ;; 89 | -*) 90 | echo "Unrecognised option: ${1}" 91 | exit 1 92 | ;; 93 | *) 94 | args+=("${1}") 95 | ;; 96 | esac 97 | shift 98 | done 99 | set -- "${args[@]}" 100 | 101 | trap 'exit 1' SIGINT 102 | 103 | depends_on 'convert' 'identify' 'ffmpeg' 'ffprobe' 'gfv' 104 | 105 | # Determine if working from images or video 106 | [[ "${#}" -eq 0 ]] && wrong_arguments 107 | 108 | if [[ "${#}" -eq 1 ]]; then 109 | [[ "$(file --mime-type --brief "${1}")" != 'video'* ]] && wrong_arguments 110 | 111 | readonly background_video="${1}" 112 | else 113 | readonly background_video="$(mktemp).mov" 114 | readonly images=("${@}") 115 | 116 | for file in "${images[@]}"; do 117 | [[ "$(file --mime-type --brief "${file}")" != 'image'* ]] && wrong_arguments 118 | done 119 | 120 | readonly frame_delay="$(bc <<< "${seconds_delay} * 100")" 121 | 122 | message 'Generating intermediary video file…' 123 | convert -delay "${frame_delay}" "${images[@]}" "${background_video}" 124 | fi 125 | 126 | message 'Generating progress bar…' 127 | readonly total_frames="$(ffprobe -loglevel error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of default=nokey=1:noprint_wrappers=1 "${background_video}")" 128 | readonly total_steps="$(bc <<< "${total_frames} - 1")" # Remove one from the total since we will start counting steps from 0. This is for the logic of having no bar on the first step and to map to the array correctly. 129 | 130 | readonly canvas_width="$(ffprobe -loglevel error -count_frames -show_entries stream=width -of default=nokey=1:noprint_wrappers=1 "${background_video}")" 131 | readonly canvas_height="$(ffprobe -loglevel error -count_frames -show_entries stream=height -of default=nokey=1:noprint_wrappers=1 "${background_video}")" 132 | readonly bar_height_px="$(bc <<< "${canvas_height} * ${bar_height_percent} / 100")" 133 | 134 | [[ "${bar_pos}" == 'top' ]] && readonly bar_ystart='0' || readonly bar_ystart="$(bc <<< "${canvas_height} - ${bar_height_px}")" 135 | readonly bar_yend="$(bc <<< "${bar_ystart} + ${bar_height_px}")" 136 | 137 | readonly tmp_bar_graphics_dir="$(mktemp -d)" 138 | readonly tmp_bar_video="$(mktemp).mov" 139 | 140 | # Make bar graphics. 141 | for step_name in $(seq -w 0 "${total_steps}"); do 142 | [[ "${step_name}" =~ ^0+$ ]] && step_number='0' || step_number="$(sed -E 's/^0+//' <<< "${step_name}")" # Remove leading zeros 143 | 144 | if [[ "${step_number}" -eq 0 ]]; then 145 | bar_width='0' # First frame shold never have a bar. Without this we'd have to divide by zero. 146 | elif [[ "${step_number}" -eq "${total_steps}" ]]; then 147 | bar_width="${canvas_width}" # Last frame should always fill the full width. Without this we may get a fractional result slightly smaller. 148 | else 149 | bar_width="$(bc -l <<< "${canvas_width} / ${total_steps} * ${step_number}")" 150 | fi 151 | 152 | convert -size "${canvas_width}"x"${canvas_height}" canvas:transparent -fill "${bar_color}" -draw "rectangle 0,${bar_ystart} ${bar_width},${bar_yend}" "${tmp_bar_graphics_dir}/${step_name}.png" 153 | done 154 | 155 | # Make bar graphics into a video and superimpose on the original. 156 | ffmpeg -loglevel error -pattern_type glob -i "${tmp_bar_graphics_dir}/*.png" -vcodec png "${tmp_bar_video}" 157 | 158 | readonly tmp_superimposed_video="$(mktemp).mov" 159 | ffmpeg -i "${background_video}" -i "${tmp_bar_video}" -filter_complex overlay "${tmp_superimposed_video}" 160 | 161 | # Make gif 162 | message 'Creating gif…' 163 | 164 | if [[ "${output_file}" == *'.mov' ]]; then 165 | mv "${tmp_superimposed_video}" "${output_file}" 166 | else 167 | options=() 168 | [[ -n "${use_gifski}" ]] && options+=('--gifski') 169 | 170 | gfv "${options[@]}" "${tmp_superimposed_video}" --output-file "${output_file}" 171 | fi 172 | 173 | message "Saved to ${output_file}" 174 | -------------------------------------------------------------------------------- /seren: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | readonly program="$(basename "${0}")" 4 | 5 | IFS=$'\n' 6 | 7 | # Instructions on how to use the script 8 | # Shown with '-h', or when no arguments are given 9 | function usage { 10 | echo " 11 | Usage: 12 | ${program} [options] 13 | 14 | Options: 15 | -s , --starting-number Sets starting number for the sequence (defaults to 1). 16 | -l , --leading-zeros Sets the amount of leading zeros (defaults to what'll accomodate the number of files). 17 | -p , --prepend Sets name to prepend to the sequence number (defaults to nothing). 18 | -a , --append Sets name to append to the sequence number (defaults to nothing). 19 | -v, --verbose Be verbose. 20 | -h, --help Show this message. 21 | " | sed -E 's/^ {4}//' 22 | } 23 | 24 | if [[ "${#}" -eq 0 ]]; then 25 | usage 26 | exit 1 27 | fi 28 | 29 | # Options 30 | args=() 31 | while [[ "${1}" ]]; do 32 | case "${1}" in 33 | -h | --help) 34 | usage 35 | exit 0 36 | ;; 37 | -s | --starting-number) 38 | starting_num="${2}" 39 | shift 40 | ;; 41 | -l | --leading-zeros) 42 | sequence_length="$(bc <<< "${2} + 1")" 43 | shift 44 | ;; 45 | -p | --prepend) 46 | string_to_prepend="${2}" 47 | shift 48 | ;; 49 | -a | --append) 50 | string_to_append="${2}" 51 | shift 52 | ;; 53 | -v | --verbose) 54 | be_verbose="v" 55 | ;; 56 | --) 57 | shift 58 | args+=("${@}") 59 | break 60 | ;; 61 | -*) 62 | echo "Unrecognised option: ${1}" 63 | exit 1 64 | ;; 65 | *) 66 | args+=("${1}") 67 | ;; 68 | esac 69 | shift 70 | done 71 | set -- "${args[@]}" 72 | 73 | # Determines the starting number, depending on if one was picked 74 | num="$([[ -n "${starting_num}" ]] && bc <<< "${starting_num} - 1" || echo '0')" 75 | 76 | # If no sequence length is given, 77 | # it will be set accordingly to what the last number 78 | # in the sequence is (depending on the starting number) 79 | if [[ -z "${sequence_length}" ]]; then 80 | sequence_end="$(bc <<< "${#} + ${num}")" 81 | sequence_length="${#sequence_end}" 82 | fi 83 | 84 | # The actual renaming part 85 | for file_to_rename in "${@}"; do 86 | num="$(printf "%.${sequence_length}d" "$(bc <<< "${num} + 1")")" 87 | file_extension="$([[ "${file_to_rename}" = *.* ]] && echo ".${file_to_rename##*.}" || echo '')" 88 | 89 | mv -n"${be_verbose}" "${file_to_rename}" "$(dirname "${file_to_rename}")/${string_to_prepend}${num}${string_to_append}${file_extension}" 90 | done 91 | --------------------------------------------------------------------------------