よくあるツールなんだけど、なかなか希望に叶うものというと見つけにくく、どうせなら自分で書いたらいいかと思ったので書いてみた。やってみたら割とすぐ書けた。
MacRuby のインストールが必要。1ファイルにしたかったので、XCode なしで使っている。あんまり XCode なしでの作例がないが普通に NSApplication.sharedApplication を取得したらいいだけだった。
NSEvent.addGlobalMonitorForEventsMatchingMask:handler: は「システム環境設定」→「セキュリティとプライバシー」→「アクセシビリティ」で macruby を許可しないと使えない。これができるということは、すなわちキーロガーが実装できるということなので、必要なときだけ許可するほうがいいと思う。
#!macruby framework "Cocoa" class MainView < NSView def init super @log = "" end def drawRect(rect) super NSColor.clearColor.set NSRectFill(bounds) font = NSFont.boldSystemFontOfSize(24) shadow = NSShadow.alloc.init shadow.setShadowColor(NSColor.blackColor) shadow.setShadowBlurRadius(2) shadow.setShadowOffset([0, 0]) attrs = NSMutableDictionary.alloc.initWithDictionary({ NSForegroundColorAttributeName => NSColor.whiteColor, NSFontAttributeName => font, NSShadowAttributeName => shadow, }) y = 0 @log.split(/\n/).reverse.each do |line| storage = NSTextStorage.alloc.initWithString(line, attributes: attrs) manager = NSLayoutManager.alloc.init container = NSTextContainer.alloc.init manager.addTextContainer(container) storage.addLayoutManager(manager) range = manager.glyphRangeForTextContainer(container) 10.times do manager.drawGlyphsForGlyphRange(range, atPoint: [0, y]) end rect = manager.boundingRectForGlyphRange([0, manager.numberOfGlyphs], inTextContainer: container) y+= rect.size.height end end def <<(log) @log << log @log = @log.split(/\n+/, -1).last(5).join("\n") end def clear @log.clear end end class AppDelegate attr_accessor :window def applicationDidFinishLaunching(a_notification) @enable = true prevKeyed = 0 NSEvent.addGlobalMonitorForEventsMatchingMask(NSKeyDownMask, handler: lambda {|e| return unless e.type == NSKeyDown mod = "" if e.modifierFlags & NSShiftKeyMask != 0 mod += "⇧" end if e.modifierFlags & NSControlKeyMask != 0 mod += "⌃" end if e.modifierFlags & NSAlternateKeyMask != 0 mod += "⌥" end if e.modifierFlags & NSCommandKeyMask != 0 mod += "⌘" end if (e.modifierFlags & NSControlKeyMask != 0) && (e.modifierFlags & NSCommandKeyMask != 0) && e.charactersIgnoringModifiers == 'l' @enable = !@enable @view.clear @view << (@enable ? "[enabled]" : "[disabled]") @view.needsDisplay = true return end if @enable if e.modifierFlags & (NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask) == 0 char = readable(e.characters) if Time.now.to_i - prevKeyed > 1 @view << "\n#{char}" else @view << char end else @view << "\n#{mod}#{readable(e.charactersIgnoringModifiers).upcase}\n" end @view.needsDisplay = true end prevKeyed = Time.now.to_i }) rect = [0, 0, 800, 500] @window = NSWindow.alloc.initWithContentRect(rect, styleMask: NSBorderlessWindowMask, backing: NSBackingStoreBuffered, defer: 0) @window.opaque = false @window.hasShadow = false @window.level = 1000 @window.movableByWindowBackground = true # @window.ignoresMouseEvents = true @window.makeKeyAndOrderFront(nil) @window.orderFrontRegardless @view = MainView.alloc.initWithFrame(rect) @view.init @view << "Initialized" @window.contentView = @view end def readable(char) p char char.gsub(/[\r\e\x7f]/, { "\r" => "↵\n", "\e" => "⎋", "\t" => "⇥", "\x7f" => "⌫", }) end end app = NSApplication.sharedApplication app.delegate = AppDelegate.new app.run