JXA NSTask

Comipoli では JXA で標準出力から画像データを得る必要がある。
doShellScript が使えるから簡単だね、と思ったら…

こいつだと 1 ライン分(一行目)しか得られないようだ。
てか画像データを直接受け取っても JavaScript にはバイナリのデータ型が無い。
NSData として受け取る必要がある。

JXA で UNIX コマンドを使う方法は cookbook にあるんだけど。
Getting the Application Instance ? JXA-Cookbook/JXA-Cookbook Wiki ? GitHub

NSTask の launch は現在非推奨になっているんだなぁこれが。
NSTask – Foundation | Apple Developer Documentation

Swift では Process.run() という新しい手段ができている。
NSTask Sample for Swift ? GitHub

で、Objective-c, JXA では Process class は使えないと。
つまり Objective-c に NSProcess が追加されるまで launch を使うしかない。

ところで NSTask.launchPath にはフルパスを!と皆書いているけど。
/usr/bin/env を入れればいいと思うのは筆者だけ?
パスが通っていればどこにあってもいいように Python のシバンに使うよね。

てなことで、ちょっと長いけど。

#!/usr/bin/osascript

// osacompile -l JavaScript -o Comipoli.app comipoli.js

ObjC.import("Cocoa");

const PATH = "/Users/sasakima-nao/Documents/もっと GF.cbz";
const PICEXT = /\.(jpe?g|png|gif)$/i;

class ComipoliArchive {
    constructor() {
        this.status = 0;
        this.namelist = [];
    }
    getData(arr) {
        let task = $.NSTask.new;
        let pipe = $.NSPipe.pipe;
        task.standardOutput = pipe;
        task.launchPath = "/usr/bin/env"; // Deprecated
        task.arguments = arr;
        task.launch; // Deprecated
        //task.waitUntilExit; // No!
        let data = pipe.fileHandleForReading.readDataToEndOfFile;
        return data;
    }
    getString(arr) {
        let data = this.getData(arr);
        let str = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding);
        return str.js;
    }
    newArchive(path, is_unrar, is_7za) {
        this.path = path;
        if (/\.(cbz|zip)$/i.test(path)) {
            this.status = 0;
            this.namelist = [];
            let output = this.getString(["unzip", "-Z", path]);
            let list = output.split("\n");
            for (let line of list) {
                if (line.startsWith("-")) {
                    let name = line.slice(53);
                    if (PICEXT.test(name)) {
                        this.namelist.push(name);
                    }
                }
            }
            this.namelist.sort();
        }
        else {
            return false;
        }
        return true;
    }
    _zipEscape(str) {
        const ESCAPE = "[]*?!^-\\";
        let res = "";
        for (let s of str) {
            if (ESCAPE.includes(s)) res += "\\";
            res += s;
        }
        return res;
    }
    getItem(num) {
        let name = this._zipEscape(this.namelist[num]);
        let data = this.getData(["unzip", "-pj", this.path, name]);
        return $.NSImage.alloc.initWithData(data);
    }
    getLength() {
        return this.namelist.length;
    }
}

class ComipoliWindow {
    constructor(app) {
        ObjC.registerSubclass({
            name: "WinDelegate",
            protocols: ["NSWindowDelegate"],
            methods: {
                "windowDidResize:": {
                    types: ["void", ["id"]],
                    implementation: (aNotification)=> {
                        this.fitImage();
                    }
                },
                "windowWillClose:": {
                    types: ["void", ["id"]],
                    implementation: (notification)=> {
                        app.terminate(0);
                    }
                }
            }
        });
        // var
        this.num = 0;
        this.window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
            $.NSMakeRect(0, 0, 600, 500),
            $.NSTitledWindowMask
            | $.NSClosableWindowMask
            | $.NSMiniaturizableWindowMask
            | $.NSResizableWindowMask,
            $.NSBackingStoreBuffered,
            false
        );
        // NSImageView
        this.firstpage = $.NSImageView.new;
        this.firstpage.setImageScaling($.NSScaleToFit);
        this.secondpage = $.NSImageView.new;
        this.secondpage.setImageScaling($.NSScaleToFit);
        this.window.contentView.addSubview(this.firstpage);
        this.window.contentView.addSubview(this.secondpage);
        // set
        this.archive = new ComipoliArchive();
        this.archive.newArchive(PATH, false, false);
        this.nextPage();
        //   
        this.window.title = $("Title");
        this.window.orderFrontRegardless;
        this.window.delegate = $.WinDelegate.new;
        this.window.makeKeyAndOrderFront(this.window);
        this.fitImage();
    }
    nextPage() {
        let item = this.archive.getItem(this.num);
        this.firstpage.setImage(item);
        //
        item = this.archive.getItem(this.num + 1); 
        this.secondpage.setImage(item);
        this.num += 2;
    }
    fitImage() {
        let aw = this.window.contentView.frame.size.width;
        let ah = this.window.contentView.frame.size.height;
        let iw = this.firstpage.image.size.width;
        let ih = this.secondpage.image.size.height;
        // left
        let f = this.secondpage.frame;
        f.size = $.NSMakeSize(aw/2, ah);
        this.secondpage.frame = f;
        // right
        f = this.firstpage.frame;
        f.size = $.NSMakeSize(aw/2, ah);
        f.origin = $.CGPointMake(aw/2, 0);
        this.firstpage.frame = f;
    }
}

function run(argv) {
    const app = $.NSApplication.sharedApplication;
    const window = new ComipoliWindow(app);
    app.setActivationPolicy($.NSApplicationActivationPolicyRegular);
    ObjC.registerSubclass({
        name: "MenuAction",
        methods: {
            "nextPage:": {
                types: ["void", ["id"]],
                implementation: (sender)=> {
                    window.nextPage();
                }
            }
        }
    });
    app.mainMenu = function() {
        function nm(title, action, key, target) {
            let item = $.NSMenuItem.new;
            if (target) item.target = target;
            item.title = $(title);
            item.action = action;
            item.keyEquivalent = $(key);
            return item;
        }
        // main
        const mainMenu = $.NSMenu.new;
        // Menubar
        const itemApp  = $.NSMenuItem.new;
        const itemFile = $.NSMenuItem.new;
        mainMenu.addItem(itemApp);
        mainMenu.addItem(itemFile);
        // Drop Down Menu
        const menuApp  = $.NSMenu.alloc.initWithTitle($("Comipoli"));
        const menuFile  = $.NSMenu.alloc.initWithTitle($("File"));
        itemApp.submenu  = menuApp;
        itemFile.submenu = menuFile;
        // Action
        menuApp.addItem(nm("Quit", "terminate:", "q", null));
        //  
        let ac = $.MenuAction.new;
        menuFile.addItem(nm("Next Page", "nextPage:", "p", ac));
        //
        return mainMenu;
    }();
    app.run;
}

できた。

command+P でページめくりもできます。
意外に完成は早くなりそう。