Programming」カテゴリーアーカイブ

Automator Clipboard

クイックアクションのネタもう一つ。
やってみたら上手くいったので。

macOS でフルパスが必要な時、Terminal.app にドロップしてからコピー。
が有名な方法だけど正直面倒臭い、クリップボードに一発でコピーしたい。
minipoli を作った作者だもん、もう誰も覚えていないだろうけど。

クリップボードを使ってみる ? Swiftをはじめよう!

macOS では NSPasteboard を使うようだ。
クイックアクションで簡単に作れそう。
NSRect を使わないなら JXA でイケるな、よし。

Automator 起動、クイックアクションを新規作成。
[指定された Finder 項目を取得] を配置
[JavaScript を実行] を配置

ObjC.import("Cocoa");
function run(argv) {
    let s = argv.join('\n');
    let clipboard = $.NSPasteboard.generalPasteboard;
    clipboard.clearContents;
    clipboard.setStringForType($(s), $.NSPasteboardTypeString);
}

ファイル名コピー.workflow

NSPasteboardItem は必要なかった。
後はフルパスを得たいファイルを選択してコンテキストメニューから

後はそのままテキストエディタ等に貼り付けできます。
join(‘ ‘) にすれば端末用の空白区切り、等に応用できます。

ちなみに GNOME(GTK+) 環境では同様な処理は不可能。
クリップボードは特殊なバッファではなく単なるポインタだから。
コピー元アプリを終了させるとバッファを保持できなくてペースト不可になる。
Gedit と gnome-terminal で試してみるとすぐ解ると思う。
Linux ではフルパスを使うことは皆無に近いから別にいいんだけど。

macOS Zip compress without ‘__MACOSX’

前回の続き。
よく考えたら zip コマンドなら __MACOSX は作られないんだよね。
だったらクイックアクションを作ってしまえばイイじゃん。

せっかくなのでプロンプトを出して圧縮名を決められるようにする。
デフォルトはディレクトリ名で、Fedora の file-roller と同じように。

Automator 起動、クイックアクションを新規作成。
[指定された Finder 項目を取得] を配置
[シェルスクリプトを実行] を配置

dpath=`dirname $1`
cd $dpath
dname=`basename $dpath`

res=`osascript -e "set result to display dialog \"Archive Name ?\" default answer \"$dname\"
text returned of result"`

for f in "$@"; do
	s=`basename $f`
	param+=" $s"
done

zip -r $res.zip $param

保存、日本語でもいいよ。

注意点はパラメーターはフルパスだということ。
$@ で zip に渡すと展開時にパスを再現してしまいます。
dirname, basename コマンドをフル活用しよう。

Finder で何か選択して実行、よし __MACOSX の無い zip のできあがり。
二本指タップから ZIP 圧縮するんだから同じだよね。
ただ、Automator のエディタはなんとかならんのか、編集し辛すぎ。

Python3 plistlib

plistlib — Mac OS X .plist ファイルの生成と解析 ? Python 3.7.2 ドキュメント

こんなモジュールが Python3 のデフォルトであったのか!
Info.plist の編集はどうやろうか迷ったけどコイツでいこう。

#!/usr/bin/env python3

import plistlib

PATH = 'Comipoli.app/Contents/Info.plist'

doctype = dict(
    CFBundleDocumentTypes = [
        dict(
            CFBundleTypeExtensions = ['cbz'],
            CFBundleTypeRole = 'Viewer',
            LSTypeIsPackage = False,
            NSPersistentStoreTypeKey = 'Binary'
        ),
        dict(
            CFBundleTypeExtensions = ['cbr'],
            CFBundleTypeRole = 'Viewer',
            LSTypeIsPackage = False,
            NSPersistentStoreTypeKey = 'Binary'
        ),
        dict(
            CFBundleTypeExtensions = ['cb7'],
            CFBundleTypeRole = 'Viewer',
            LSTypeIsPackage = False,
            NSPersistentStoreTypeKey = 'Binary'
        ),
        dict(
            CFBundleTypeExtensions = ['pdf'],
            CFBundleTypeRole = 'Viewer',
            LSTypeIsPackage = False,
            NSPersistentStoreTypeKey = 'Binary'
        )
    ]
)
plist = None
with open(PATH, 'rb') as fp:
    plist = plistlib.load(fp)

plist.update(doctype)

with open(PATH, 'wb') as fp:
    plistlib.dump(plist, fp)

make_plist.py

他のアプリの Info.plist を参考にこれだけ追加してみた。
アイコンを変更することもできるけどまあいいや。

ということで、ビルドスクリプトの最後でコレを実行。
/Applications に移動して pdf で二本指タップしてみる。

Comipoli.app が見事に登録されています。
選択すれば開くことができるしデフォルトアプリにすることもできる。

ところで、0.0.2 は app 化すると引数起動ができなかった。

(旧) Cocoaの日々: アプリ起動時に渡される引数の処理

App にすると application:openFiles: でしか引数を受け付けしないのね。
Window を作るのを applicationWillFinishLaunching: に変更。
application:openFiles: のほうが良さげなのでこっちで処理。
よし日本語でも問題なく引数付き起動できるようになった。

しかし PyObjC だけでここまで作れるとは自分でも思わなかった。
macOS でも Python3 は使うべき。

__getitem__ for in

Python で自作クラスに __getitem__ を定義すれば for 文が使えるようになる。
しかし場合によっては無限大になったり存在しない値を戻したりしかねない。

len から自前で最大値を取得して、という方法ではないようだ。
for 文はいったい何を基準で抜けているのか。

配列の範囲外を得ようとすると IndexError の例外を投げてくる。
ということはコレを利用して for 文を抜けているのだろうか。
下記の __getitem__ が無限になってしまうコードに入れて実験。

#!/usr/bin/env python3

class Test():
    def __getitem__(self, n):
        if n > 3:
            raise IndexError
        return n * 2

t = Test()

for n in t:
    print(n)

'''output
0
2
4
6
'''

なるほど。

Poppler で範囲外を指定すると None を戻すだけで例外にならず親でエラーになっていた。
cbz 等は self.namelist が IndexError を吐いて親に raise していただけ。
Python が用意してくれた例外に頼らず自身で raise していこう。

WebKit on PyGObject and PyObjC

WebKit バインドは PyObjC に含まれている。
Fedora の gir には WebKit2 が含まれている。
もしかしたら共通コードでイケるんでね?

#!/usr/bin/env python3

import objc
from AppKit import *
from WebKit import *

class WView(WKWebView):
    def init(self):
        config = WKWebViewConfiguration.new()
        objc.super(WView, self).initWithFrame_configuration_(NSZeroRect, config)
        return self

class Win(NSWindow):
    def init(self):
        frame = NSMakeRect(100, 400, 900, 600)
        objc.super(Win, self).initWithContentRect_styleMask_backing_defer_(
            frame,
            NSTitledWindowMask |
            NSClosableWindowMask |
            NSResizableWindowMask |
            NSMiniaturizableWindowMask,
            NSBackingStoreBuffered,
            False)
        self.setTitle_('Web')
        self.setDelegate_(self)
        # view
        self.webview = WView.new()
        self.webview.setFrameSize_(self.contentView().frame().size)
        self.contentView().addSubview_(self.webview)
        # url
        url = NSURL.URLWithString_('https://www.google.com/')
        req = NSURLRequest.requestWithURL_(url)
        self.webview.loadRequest_(req)
        return self

    def windowDidResize_(self, sender):
        self.webview.setFrameSize_(self.contentView().frame().size)

class Menu(NSMenu):
    def init(self):
        objc.super(Menu, self).init()
        item_app  = NSMenuItem.new()
        self.addItem_(item_app)
        menu_app = NSMenu.new()
        item_app.setSubmenu_(menu_app)
        item_quit = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('Quit', 'terminate:', 'q')
        menu_app.addItem_(item_quit)
        return self

NSApplication.sharedApplication()
window = Win.new()
window.makeKeyAndOrderFront_(window)
NSApp.setMainMenu_(Menu.new())
NSApp.activateIgnoringOtherApps_(True)
NSApp.run()

PyObjC

#!/usr/bin/env python3

import sys, gi
gi.require_version('Gtk', '3.0')
gi.require_version('WebKit2', '4.0')
from gi.repository import Gtk, GLib, WebKit2

class WebView(WebKit2.WebView):
    def __init__(self, ctx):
        WebKit2.WebView.__init__(self, web_context=ctx)

class Win(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        #
        ctx = WebKit2.WebContext()
        self.webkit = WebView(ctx)
        self.add(self.webkit)
        self.webkit.load_uri('https://www.google.com/')
        #
        self.resize(900, 600)
        self.show_all()

class App(Gtk.Application):
    __gtype_name__ = 'App'
    def __init__(self):
        GLib.set_prgname('App')
        Gtk.Application.__init__(self)

    def do_startup(self):
        Gtk.Application.do_startup(self)
        Win(self)
    
    def do_activate(self):
        self.props.active_window.present()

App().run(sys.argv)

PyGObject

全然違ったwwwww
Apple さん、なんで URL は文字列じゃダメなの?
GNOME さん、WebKit2.WebView() ではエラーになるんですけど?

実は我がアプリに本棚機能を付けようかと考えて。
WebKit からローカル HTML でやれば GNOME, macOS 共通にできるかなって。
こりゃ無理だ。