Skip to content

Instantly share code, notes, and snippets.

@ttscoff
Last active March 4, 2024 12:00
  • Star 39 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save ttscoff/0be33bf2cace95ff26fd787da25df239 to your computer and use it in GitHub Desktop.
appinfo: A quick ruby script for Mac that returns some information for a specified Mac app in Terminal. Includes icon display if using imgcat or chafa are available.

appinfo

A script for getting details of installed Mac apps. Like Get Info but faster and cooler.

Save the script below as appinfo somewhere in your path. Make it executable with chmod a+x appinfo. Then just run appinfo APPNAME to get info on any installed app.

#!/usr/bin/env ruby
require 'date'
###################################################
# iii fff #
# aa aa pp pp pp pp nn nnn ff oooo #
# aa aaa ppp pp ppp pp iii nnn nn ffff oo oo #
# aa aaa pppppp pppppp iii nn nn ff oo oo #
# aaa aa pp pp iii nn nn ff oooo #
# pp pp #
###################################################
=begin appinfo
Shows keys from spotlight data for an app
usage: 'appinfo [app name]'
Keys for sizes are converted to human-readable numbers (e.g. 25.3MB)
Keys for dates are converted to localized short date format
=== Config
:show_icon: If you have imgcat or chafa installed, print out an icon
:keys: The keys to parse and their "pretty" form for printing Output in
the order listed
==== Default keys:
'location' => 'Location',
'kMDItemCFBundleIdentifier' => 'Bundle ID',
'kMDItemPhysicalSize' => 'Size',
'kMDItemVersion' => 'Version',
'kMDItemContentCreationDate' => 'Released',
'kMDItemAppStorePurchaseDate' => 'Purchased',
'kMDItemLastUsedDate' => 'Last Used',
'kMDItemAppStoreCategory' => 'Category',
'kMDItemCopyright' => 'Copyright'
=end
CONFIG = {
:show_icon => true,
:keys => {
'location' => 'Location',
'kMDItemCFBundleIdentifier' => 'Bundle ID',
'kMDItemAlternateNames' => 'Alternate Names',
'kMDItemPhysicalSize' => 'Size',
'kMDItemVersion' => 'Version',
'kMDItemContentCreationDate' => 'Released',
'kMDItemAppStorePurchaseDate' => 'Purchased',
'kMDItemLastUsedDate' => 'Last Used',
'kMDItemAppStoreCategory' => 'Category',
'kMDItemCopyright' => 'Copyright',
'kMDItemExecutableArchitectures' => 'Architecture'
}
}
def class_exists?(class_name)
klass = Module.const_get(class_name)
return klass.is_a?(Class)
rescue NameError
return false
end
if class_exists? 'Encoding'
Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external')
Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal')
end
class Array
def longest_element
group_by(&:size).max.last[0].length
end
end
class String
def to_human(fmt=false)
n = self.to_i
count = 0
formats = %w(B KB MB GB TB PB EB ZB YB)
while (fmt || n >= 1024) && count < 8
n /= 1024.0
count += 1
break if fmt && formats[count][0].upcase =~ /#{fmt[0].upcase}/
end
format("%.2f",n) + formats[count]
end
end
def find_app(app)
location = nil
narrow = ' -onlyin /System/Applications -onlyin /Applications -onlyin /Applications/Setapp -onlyin /Applications/Utilities -onlyin /Developer/Applications'
res = `mdfind#{narrow} 'kind:app filename:"#{app}"' | grep -E '\.app$' | head -n 1`.strip
unless res && res.length > 0
res = `mdfind 'kind:app filename:"#{app}"' | grep -E '\.app$' | head -n 1`.strip
end
if class_exists? 'Encoding'
res = res.force_encoding('utf-8')
end
return res && !res.empty? ? res.strip : false
end
def exec_available(cli)
if File.exist?(File.expand_path(cli))
File.executable?(File.expand_path(cli))
else
system "which #{cli}", out: File::NULL, err: File::NULL
end
end
def show_icon(app_path)
if CONFIG[:show_icon] && (exec_available('imgcat') || exec_available('chafa') || exec_available("kitty"))
app_icon = `defaults read "#{app_path}/Contents/Info" CFBundleIconFile`.strip.sub(/(\.icns)?$/, '.icns')
if exec_available('imgcat')
cmd = 'imgcat'
elsif exec_available('chafa')
cmd = 'chafa -s 15x15 -f iterm'
elsif exec_available('kitty')
cmd = 'kitty +kitten icat --align=left'
end
res = `mkdir -p ${TMPDIR}appinfo && sips -s format png --resampleHeightWidthMax 256 "#{app_path}/Contents/Resources/#{app_icon}" --out "${TMPDIR}appinfo/#{app_icon}.png"` # > /dev/null 2>&1
$stdout.puts `#{cmd} "${TMPDIR}appinfo/#{app_icon}.png" && rm "${TMPDIR}appinfo/#{app_icon}.png"`
end
end
def parse_info(info)
values = {}
if class_exists? 'Encoding'
info = info.force_encoding('utf-8')
end
info.gsub!(/(\S+)\s*=\s*\((.*?)\)/m) do
m = Regexp.last_match
val = m[2].strip.split(/\n/).delete_if { |i| i.strip.empty? }.map { |l|
l.strip.gsub(/"/, '').sub(/,$/, '').sub(/x86_64/, 'Intel').sub(/arm64/, 'Apple Silicon')
}.join(', ')
val += " (Unviversal Binary)" if val =~ /Intel/ && val =~ /Apple Silicon/
values[m[1]] = val
''
end
info.split(/\n/).delete_if(&:empty?).each do |line|
sp = line.split(/\s*=\s*/)
values[sp[0]] = sp[1].gsub(/"/, '')
end
values
end
def get_info(appname)
app = appname # .sub(/\.app$/,'')
found = find_app(app)
if found
keys = "-name " + CONFIG[:keys].keys.join(' -name ')
res = %x{mdls #{keys} "#{found}"}
result = parse_info(res)
result['location'] = found
return result
else
$stdout.puts %Q{App "#{app}" not found.}
Process.exit 1
end
end
def info(app)
appinfo = get_info(app)
if appinfo && appinfo.length > 0
show_icon(appinfo['location'])
longest_key = CONFIG[:keys].values.longest_element
CONFIG[:keys].each {|k,v|
key = v
val = appinfo[k]&.strip || 'None'
val = case k
when /Size$/
val.to_human
when /Date$/
if appinfo[k].strip =~ /^\d{4}-\d{2}-\d{2}/
Date.parse(val.strip).strftime('%D') rescue val
end
else
val
end
val = val =~ /\(null\)/ ? "\033[0;36;40mUnknown\033[0m" : "\033[1;37;40m#{val}\033[0m"
$stdout.puts "\033[0;32;40m%#{longest_key}s: %s" % [key, val]
}
end
end
def exit_help(code=0)
output = <<~ENDOUT
Shows keys from Spotlight data for an app
Usage:
#{File.basename(__FILE__)} [app name]
ENDOUT
puts output
Process.exit code.to_i
end
if ARGV.length == 0
exit_help(1)
elsif ARGV[0] =~ /^-?h(elp)?$/
exit_help
else
info(ARGV.join(" "))
end
@cocoonkid
Copy link

inappropriate ioctl for device
       Location: /Applications/Alfred 4.app
      Bundle ID: com.runningwithcrayons.Alfred
Alternate Names: Alfred 4.app
           Size: 14.55MB
        Version: 4.6
       Released: 10/15/21
      Purchased:
      Last Used: 10/26/21
       Category: Productivity
      Copyright: Copyright © 2019 Running with Crayons. All rights reserved.
   Architecture: Apple Silicon, Intel (Unviversal Binary)
Time: 0h:00m:03s

gen-sync/projects_assets/setup_macos via 💎 v2.6.3 on ☁️   took 3s
❯ which imgcat
imgcat: aliased to /Users/cocoonkid/.iterm2/imgcat

gen-sync/projects_assets/setup_macos via 💎 v2.6.3 on ☁️
❯ which chafa
/opt/homebrew/bin/chafa

Thank you for clearing it up @ryanfitzer
I couldn't make it work with imagers though. Neither in iterm2 nor alacritty.

But not really that important anyway :-)

@ttscoff
Copy link
Author

ttscoff commented Oct 28, 2021

Use this diff to show the app icon in the Kitty terminal emulator.

Added to the script, thanks!

@ttscoff
Copy link
Author

ttscoff commented Oct 28, 2021

I'm not sure why so many are having issues with imgcat, I can't replicate. which imgcat gives me ~/.iterm2/imgcat, where iTerm installed it. AFAIK I didn't do anything else to it. Definitely shows me an image without issue.

Curious about @cocoonkid 's error, inappropriate ioctl for device. Would be good to know exactly which shell command produced that error. The only part of the show_icon function that outputs to terminal is a $stdout.puts call that shows the STDOUT output of either imgcat or chafa, depending on which it finds available first (in that order). Would need to know which one it was attempting to run to solve that. I don't think the sips call would produce that error.

The other thing that might affect people with chafa is that I have it forced to --format iterm. The other options are kitty, sixels, and symbols, so feel free to play with that in line 116 if you need to.

@bocciaman
Copy link

bocciaman commented Dec 15, 2021

I got everything working after running brew install chafa.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment