Jump to content

Recommended Posts

OK, so what I want is to be able to type "app" with a name of an application installed on my system, to show the version of the app in the Alfred results ... much like how in Yosemite we can type an app name and have the version/purchased date/etc listed on the right pane of Spotlight.

 

I have had to go back to Mavericks for now as Yosemite feels so unfinished on my system.

 

I LIKE how in Alfred I can TAB to complete an application's name with a file filter, but I can not display results of a script with a file filter unless I use large text.

 

I have a workflow (temp) that will use the file filter and run an osa-script to output the application version to large text:

 

get version of application "{query}"

 

BUT, I'd like to be able to get autocomplete into a script filter if I could ... is this possible?

Share this post


Link to post

 

A 'little' ... except that the workflow only copies the info to the clipboard ... I would like to be able to see the version in the Alfred results, and be able to action the items separately ... I'll be adding other stuff specific to the APP extension and may need to copy those items separately.

Share this post


Link to post

I might be misunderstanding, but if you need to display the information in something that's a little more spacious than the Alfred viewport, then you could always consider using CocoaDialog to supplement, or you could use a slightly larger viewport by screwing with some automator applications.

 

For the latter: the Packal Updater's GUI is actually done just via a simple automator app that has as its only action "view webpage in pop up window" (or something to that extent). I used it to create a fake application interface, but it works well for just displaying any sort of text-based file. If you want to make the text prettier, then you'd just have the workflow dynamically create some HTML and include a stylesheet, and that can work without needing to launch any sort of web-server in the background. You can also set the size and the position of the viewport when you create it via automator.

 

For an easier solution, you could just use some applescript to display a dialog box that allows for text input, but just auto-fill it with the information from the workflow.

 

Another solution, similar to CocoaDialog, would be to use Pashua to construct a dialog box with whatever you need. Since Pashua's display is built solely from a script, you can create it on the fly to suit the workflow's output.

 

If you want an easy way to implement any of the above ideas (except for the Applescript solution), then you can always use the Bundler to take care of installing the helper applications for you. If you're creating the workflow for private use, then you can use the dev version of the bundler that has some nice wrappers for CocoaDialog and is also faster and more stable. You can see a bit more about how to use the newer version here: http://shawnrice.github.io/bundler-docs. Right now, that version is primarily waiting on the Python implementation to be finished. Once that is done, then you can use it for public workflows.

Share this post


Link to post

Just to clarify a little if I can:

appinfo.png

In the above screenshot, the first result listed in Alfred2 is just a file filter so that I can type "app" and START to type an application name, but use autofile to complete the app's name.  If I hit enter on the first result, I have it set to display the file version in large type.

 

BUT, what I want is to be able to show the SECOND result instead ... in the results of Alfred so that I can start to type an app's name, autofill if it's a long app like "Adobe Illustrator CC 2014" and see the file version / size / id / icon or whatever right there.

 

I'm just trying to simplify as much as possible by just using the results of Alfred

Share this post


Link to post

This might not be entirely what you're looking for, but here is a short, incomplete version in PHP (it's a bit slow, but you can either cache it or redo it in Bash). 

 

It's incomplete because

  (1) it doesn't have the filtering in it yet — add in the arguments.

  (2) it's slow (it's PHP wrapping around a slow bash command — rewrite it entirely in bash, or use something that reads plists better)

  (3) it scans only the directories you specify.

 

Code:

<?php

function array_prepend( &$value, $key, $prefix ) {
    $value = "{$prefix}/{$value}";
}

// Add all directories where apps might be... this might not be what you're going for.
$directories = [ "/Applications" ];
$apps = [];
foreach ( $directories as $dir ) :
    $tmp = array_diff( scandir( $dir ), [ '.', '..', '.DS_Store' ] );
    array_walk( $tmp, 'array_prepend', $dir );
    $apps = array_merge( $apps, $tmp );
endforeach;

$fields = [
    'CFBundleExecutable'         => 'Name',
    'CFBundleIdentifier'         => 'Bundle',
    'CFBundleShortVersionString' => 'Major',
    'CFBundleVersion'            => 'Minor'
];

$results = [];
$total = count( $apps );
$count = 1;
foreach ( $apps as $app ) :
    file_put_contents('php://stderr', "{$count}/{$total}" . PHP_EOL );
    if ( 'app' != pathinfo( $app, PATHINFO_EXTENSION ) ) {
        $count++;
        continue;
    }
    if ( ! file_exists( "{$app}/Contents/Info.plist" ) ) {
        $count++;
        continue;
    }

    $plist = "{$app}/Contents/Info.plist";

    foreach( $fields as $key => $field ) :
        $results[$app][$field] = exec(
            "defaults read '{$plist}' {$key}"
        );
    endforeach;
    $results[$app]['Size'] = trim( str_replace('total', '', exec( "du -hcs '{$app}' | grep total" ) ) );
    $count++;
endforeach;

$xml = new \XMLWriter();
$xml->openMemory();
$xml->setIndent(4);
$xml->startDocument('1.0', 'UTF-8' );
$xml->startElement( 'items' );

foreach( $results as $app => $field ) :
    $xml->startElement( 'item' );
    $xml->writeAttribute( 'arg', $field['Name'] );
    $xml->writeAttribute( 'uid', '' );
    $xml->writeAttribute( 'valid', 'yes' );
    $xml->writeAttribute( 'autocomplete', $field['Name'] );

    $xml->startElement( 'title' );
    $xml->text( "{$field['Name']} | {$field['Major']} | {$field['Size']}" );
    $xml->endElement();

    $xml->startElement( 'subtitle' );
    $xml->text( "{$field['Bundle']}" );
    $xml->endElement();

    $xml->startElement( 'icon' );
    $xml->writeAttribute( 'type', 'fileicon' );
    $xml->text( "{$app}" );
    $xml->endElement();

    $xml->endElement();
endforeach;
$xml->endDocument();

echo $xml->outputMemory();

Share this post


Link to post

Well, that is a script that scans through any directory that you set in an array and then reads the plists of each application to get the relevant data and then smushes it all into some Alfred XML that displays it as you want it to do.

 

I started to write "put it in a script filter and then..." and I decided to take care of the "and" and transformed the code into a (theoretically) working script filter.

 

(1) Create a new workflow

(2) Create a script filter type "php", argument required, whatever keyword you want

(3) Copy / Paste this into it:

<?php

// Grab the query from alfred
$arg = "{query}";
if ( empty( $arg ) ) {
    die( 'please provide at least one argument' );
}

// Set the data directory
$data = $_SERVER['alfred_workflow_data_dir'];

// Make the data directory
if ( ! file_exists( $data ) ) {
    mkdir( $data, 0775, true );
}

// If the data doesn't exist, then generate it and cache it
if ( ! file_exists( "{$data}/app.json" ) ) {
    $apps = process();
    file_put_contents( "{$data}/app.json", $apps );
}

// Run the filter
to_xml( filter( $arg ) );

// Loads the (recently) cached data and filters out anything that doesn't match the app name
// returns an array of matches
function filter( $arg ) {

    $apps    = json_decode( file_get_contents( "{$data}/app.json" ), true );
    $results = [];
    foreach( $apps as $path => $app ) :
        if ( false !== strpos( $app['name'], $arg ) ) {
            $results[$path] = $app;
        }
    endforeach;
    return $results;
}

// A callback function to re-appeand the path
function array_prepend( &$value, $key, $prefix ) {
    $value = "{$prefix}/{$value}";
}

// Cycles through the directories and reads the apps' plists to get all the relevant information
// returns an array
function process() {
    // Add all directories where apps might be... this might not be what you're going for.
    $directories = [ "/Applications" ];
    $apps = [];
    foreach ( $directories as $dir ) :
        $tmp = array_diff( scandir( $dir ), [ '.', '..', '.DS_Store' ] );
        array_walk( $tmp, 'array_prepend', $dir );
        $apps = array_merge( $apps, $tmp );
    endforeach;

    $fields = [
        'CFBundleExecutable'         => 'Name',
        'CFBundleIdentifier'         => 'Bundle',
        'CFBundleShortVersionString' => 'Major',
        'CFBundleVersion'            => 'Minor'
    ];

    $results = [];
    $total = count( $apps );
    $count = 1;
    foreach ( $apps as $app ) :
        file_put_contents('php://stderr', "{$count}/{$total}" . PHP_EOL );
        if ( 'app' != pathinfo( $app, PATHINFO_EXTENSION ) ) {
            $count++;
            continue;
        }
        if ( ! file_exists( "{$app}/Contents/Info.plist" ) ) {
            $count++;
            continue;
        }

        $plist = "{$app}/Contents/Info.plist";

        foreach( $fields as $key => $field ) :
            $results[$app][$field] = exec(
                "defaults read '{$plist}' {$key}"
            );
        endforeach;
        $results[$app]['Size'] = trim( str_replace('total', '', exec( "du -hcs '{$app}' | grep total" ) ) );
        $count++;
    endforeach;

    return $results;
}

// pushes the data into alfred readable xml
function to_xml( $results ) {
    $xml = new \XMLWriter();
    $xml->openMemory();
    $xml->setIndent(4);
    $xml->startDocument('1.0', 'UTF-8' );
    $xml->startElement( 'items' );

    foreach( $results as $app => $field ) :
        $xml->startElement( 'item' );
        $xml->writeAttribute( 'arg', $field['Name'] );
        $xml->writeAttribute( 'uid', '' );
        $xml->writeAttribute( 'valid', 'yes' );
        $xml->writeAttribute( 'autocomplete', $field['Name'] );

        $xml->startElement( 'title' );
        $xml->text( "{$field['Name']} | {$field['Major']} | {$field['Size']}" );
        $xml->endElement();

        $xml->startElement( 'subtitle' );
        $xml->text( "{$field['Bundle']}" );
        $xml->endElement();

        $xml->startElement( 'icon' );
        $xml->writeAttribute( 'type', 'fileicon' );
        $xml->text( "{$app}" );
        $xml->endElement();

        $xml->endElement();
    endforeach;
    $xml->endDocument();

    return $xml->outputMemory();
}

(4) Run it. The first run will take a bit, but after that it should be snappy. Granted, you'll have to kill the data each time you add / update an app.

(5) Add in some action to take after that.

 

There might be a bug or two in the updated version (I just transformed it into a working script filter).

 

But, really, it's just here to be a proof of concept of what I think you want to do. If you decide to re-code it in Ruby or Python, then you can use the objective-c bindings to get the information faster and in a way that you won't need to cache it. Or you can make it snappier in any language by reading the plists without having to resort to the rather slow `defaults read` command. You could always change it to use `/usr/libexec/PlistBuddy`, which would be faster (why didn't I think of using that one before?).

 

Alternately, if you want to speed this one up, then you could just apply the filter when scanning the directory and not cache the data.

 

So, maybe this will help point you in a direction that lets you get the job done the way you want.

Share this post


Link to post

Did all that ... and it doesn't do anything, also, looking at the workflow folder it doesn't seem to create any data files either. I get this in the debug:

[ERROR: alfred.workflow.input.scriptfilter] Code 254: Parse error: parse error in Command line code on line 1
[ERROR: alfred.workflow.input.scriptfilter] XML Parse Error 'The operation couldn’t be completed. (NSXMLParserErrorDomain error 4.)'. Row 1, Col 1: 'Document is empty' in XML:
Parse error: parse error in Command line code on line 1

Share this post


Link to post

Just getting back to this....

 

So, there was a syntax error or two in the code above. That's what I get for posting code without checking it.

 

So, here's a quick version posted on Dropbox (it probably won't be up there for too, too long).

 

It takes a similar approach as above by reading through all of the `Info.plist` files in any folder that ends with `.app` in order to get the information. This one replaces `defaults read` for a PHP library that can read plists (to speed things up).

 

There are three main drawbacks to it right now:

(1) To get the filesize of the app, it uses `du`, which is pretty slow. Granted, since all apps are actually directories, we have to get the size of the full directory each time.

(2) The recursive file scan is a bit slow.

(3) None of the files are actionable right now.

 

This one does not implement any caching, which means that the results are always up to date, but that also means that it isn't quite so fast.

 

Regardless, it's a proof of concept.

 

If you want to make use of this, then you should add in the actions (copy/large text, whatever else).

Share this post


Link to post

Hmm, ok this one isn't QUITE working right for me.  It's not finding everything or autocompleting ... apps like Calculator aren't even showing up.

So, for now, I am just going to use a file filter to get the autocomplete stuff working, but I'm adding a script to do an alfred search with results of the file filter. One more keypress than I want, but so far so good.

Share this post


Link to post

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...