├── .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 |
--------------------------------------------------------------------------------