よくあるツールなんだけど、なかなか希望に叶うものというと見つけにくく、どうせなら自分で書いたらいいかと思ったので書いてみた。やってみたら割とすぐ書けた。

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
		unless AXIsProcessTrusted()
			$stderr.puts "Require setting"
			system('open', '/System/Library/PreferencePanes/Security.prefPane')
			exit
		end

		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

	REPLACE_MAP = {
		"\r"   => "↵\n",
		"\e"   => "⎋",
		"\t"   => "⇥",
		"\x19" => "⇤",
		" "    => "␣",
		"\x7f" => "⌫",
		"\x03" => "⌤",
		"\xEF\x9C\xA8" => "⌦",
		"\xEF\x9C\x84" => "[F1]",
		"\xEF\x9C\x85" => "[F2]",
		"\xEF\x9C\x86" => "[F3]",
		"\xEF\x9C\x87" => "[F4]",
		"\xEF\x9C\x88" => "[F5]",
		"\xEF\x9C\x89" => "[F6]",
		"\xEF\x9C\x8A" => "[F7]",
		"\xEF\x9C\x8B" => "[F8]",
		"\xEF\x9C\x8C" => "[F9]",
		"\xEF\x9C\x8D" => "[F10]",
		"\xEF\x9C\x8E" => "[F11]",
		"\xEF\x9C\x8F" => "[F12]",

		"\xEF\x9C\x80" => "↑",
		"\xEF\x9C\x81" => "↓",
		"\xEF\x9C\x82" => "←",
		"\xEF\x9C\x83" => "→",
		"\xEF\x9C\xAC" => "⇞",
		"\xEF\x9C\xAD" => "⇟",
		"\xEF\x9C\xA9" => "↖",
		"\xEF\x9C\xAB" => "↘",
	}

	def readable(char)
		p char
		re = Regexp.new(REPLACE_MAP.keys.map {|i| Regexp.escape(i) }.join("|"))
		char.gsub(re, REPLACE_MAP)
	end
end

app = NSApplication.sharedApplication
app.delegate = AppDelegate.new
app.run
  1. トップ
  2. tech
  3. MacRuby でスクリーンキャスト用のキー履歴表示ツールを作る
▲ この日のエントリ