Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial commit

  • Loading branch information...
commit 20fe9d1893794533e6178f4d05c79e167efb428f 0 parents
@glucero authored
Showing with 2,137 additions and 0 deletions.
  1. +3 −0  .gitignore
  2. +21 −0 LICENSE.txt
  3. +67 −0 README.md
  4. +134 −0 build_app
  5. +22 −0 source/bin/fusuma
  6. +14 −0 source/conf/configuration.json
  7. +67 −0 source/lib/fusuma.rb
  8. +65 −0 source/lib/fusuma/application.rb
  9. +48 −0 source/lib/fusuma/ax.rb
  10. +43 −0 source/lib/fusuma/ax_application.rb
  11. +44 −0 source/lib/fusuma/ax_window.rb
  12. +42 −0 source/lib/fusuma/configuration.rb
  13. +17 −0 source/lib/fusuma/container.rb
  14. +88 −0 source/lib/fusuma/keymap.rb
  15. +76 −0 source/lib/fusuma/layout.rb
  16. +57 −0 source/lib/fusuma/layouts/small_workspace.json
  17. +30 −0 source/lib/fusuma/logger.rb
  18. +48 −0 source/lib/fusuma/properties.rb
  19. +50 −0 source/lib/fusuma/scale.rb
  20. +33 −0 source/lib/fusuma/status_bar.rb
  21. +78 −0 source/lib/fusuma/status_bar_icon.rb
  22. +57 −0 source/lib/fusuma/status_bar_window.rb
  23. +78 −0 source/lib/fusuma/window.rb
  24. +88 −0 source/lib/fusuma/workspace.rb
  25. 0  source/log/.gitkeep
  26. +1 −0  source/vendor/hotkeys-0.1.2/CHANGES
  27. +3 −0  source/vendor/hotkeys-0.1.2/Gemfile
  28. +17 −0 source/vendor/hotkeys-0.1.2/Gemfile.lock
  29. +19 −0 source/vendor/hotkeys-0.1.2/LICENSE
  30. +51 −0 source/vendor/hotkeys-0.1.2/README.md
  31. +40 −0 source/vendor/hotkeys-0.1.2/Rakefile
  32. +41 −0 source/vendor/hotkeys-0.1.2/hotkeys.gemspec
  33. +89 −0 source/vendor/hotkeys-0.1.2/lib/hotkeys.rb
  34. +25 −0 source/vendor/hotkeys-0.1.2/lib/hotkeys/support.rb
  35. +259 −0 source/vendor/hotkeys-0.1.2/lib/hotkeys/support/bridgesupport/Events.bridgesupport
  36. BIN  source/vendor/hotkeys-0.1.2/lib/hotkeys/support/bundles/shortcut.bundle
  37. +104 −0 source/vendor/hotkeys-0.1.2/lib/hotkeys/support/bundles/shortcut.m
  38. +188 −0 source/vendor/hotkeys-0.1.2/lib/hotkeys/support/keys.rb
  39. +30 −0 source/vendor/hotkeys-0.1.2/lib/hotkeys/version.rb
3  .gitignore
@@ -0,0 +1,3 @@
+.DS_Store
+*.log
+build/*
21 LICENSE.txt
@@ -0,0 +1,21 @@
+
+Copyright (C) 2012 Gino Lucero
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
67 README.md
@@ -0,0 +1,67 @@
+# fusuma
+
+fusuma has been written in MacRuby, [and who doesn't like Ruby](http://www.youtube.com/watch?v=QkqxQROcTIU)? there are no nibs/xibs, no compiled binaries or anything else you get when you make a regular OSX project with Apple's tools.
+
+#### so shut down XCode, pop open Vim/Emacs/Sublime/TextMate/nano(?) and jump in!
+
+***
+
+### fusuma can
+
+ * keep track of unique windows and applications
+ * position and resize any window
+ * activate and set focus to any window
+ * automatically tile and organize your windows based on the configured layout's rules
+ * assign global hot keys for any of the above actions
+
+### fusuma can't
+
+ * remove window decorations from another application
+
+***
+
+### todo
+
+ * clean up and documentation (this is an ongoing todo item)
+ * a few more default layouts and more keymaps
+ * multi-layout/monitor support
+ * observer to notify fusuma when a window opens/closes/changes state
+ * fix sizing of fixed-ratio windows like terminals and editors with fixed width fonts like MacVim/Emacs (if this is even possible)
+ * finish packaging/configuration (yawn)
+
+***
+
+### wanna help/play?
+
+ * download and install the latest [MacRuby](http://macruby.org/)
+ * fork fusuma
+ * run './build_app' to build the latest version
+ * run 'open fusuma.app' (or double click it)
+ * hax (the most important step!)
+
+***
+
+### license
+
+ MIT License
+
+ Copyright (C) 2012 Gino Lucero
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
134 build_app
@@ -0,0 +1,134 @@
+#!/usr/bin/env ruby
+
+module Fusuma
+
+ require 'time'
+ require 'fileutils'
+
+ APP_NAME = 'fusuma'
+ VERSION = '1.0.0'
+
+ class Build
+
+ BUILD_TIME = Time.now.iso8601
+
+ ############################################################################
+ # the project and build heirarchy
+ PROJECT_ROOT = File.dirname(__FILE__)
+ PROJECT_LICENSE = File.join(PROJECT_ROOT, 'LICENSE.txt')
+ BUILD_ROOT = File.join(PROJECT_ROOT, 'build')
+ BUILD_DIR = File.join(BUILD_ROOT, BUILD_TIME)
+ OSX_APP_DIR = File.join(BUILD_DIR, "#{APP_NAME}.app")
+ CONTENTS_DIR = File.join(OSX_APP_DIR, 'Contents')
+ BUILD_STAMP_FILE = File.join(CONTENTS_DIR, 'build')
+ FRAMEWORKS_DIR = File.join(CONTENTS_DIR, 'Frameworks')
+ # MACRUBY_DIR = File.join(FRAMEWORKS_DIR, 'MacRuby.framework') # TODO: embed MacRuby
+ INFO_PLIST_FILE = File.join(CONTENTS_DIR, 'Info.plist')
+ MACOS_DIR = File.join(CONTENTS_DIR, 'MacOS')
+ EXECUTABLE_FILE = File.join(MACOS_DIR, "#{APP_NAME}")
+ PROFILE_DIR = File.join(CONTENTS_DIR, 'Profile')
+ RESOURCES_DIR = File.join(CONTENTS_DIR, 'Resources')
+ LICENSE_FILE = File.join(PROJECT_ROOT, 'LICENSE.txt')
+ APP_BIN_DIR = File.join(RESOURCES_DIR, 'bin')
+ APP_CONF_DIR = File.join(RESOURCES_DIR, 'conf')
+ # ICON_FILE = File.join(RESOURCES_DIR, 'fusuma.icns') # TODO: make icon
+ APP_LIB_DIR = File.join(RESOURCES_DIR, 'lib')
+ VERSION_FILE = File.join(APP_LIB_DIR, 'version.rb')
+ APP_LOG_DIR = File.join(RESOURCES_DIR, 'log')
+ APP_VENDOR_DIR = File.join(RESOURCES_DIR, 'vendor')
+ VERSION_PLIST_FILE = File.join(CONTENTS_DIR, 'version.plist')
+ # ICON_CHECK_FILE = File.join(OSX_APP_DIR, 'Icon?')
+ PROJECT_SOURCE_DIR = File.join(PROJECT_ROOT, 'source')
+ SOURCE_BIN_DIR = File.join(PROJECT_SOURCE_DIR, 'bin')
+ SOURCE_CONF_DIR = File.join(PROJECT_SOURCE_DIR, 'conf')
+ SOURCE_LIB_DIR = File.join(PROJECT_SOURCE_DIR, 'lib')
+ SOURCE_VENDOR_DIR = File.join(PROJECT_SOURCE_DIR, 'vendor')
+ ############################################################################
+ # files created during build time
+ BUILD_STAMP_CONTENTS = BUILD_TIME
+ LICENSE_CONTENTS = File.open(PROJECT_LICENSE).read
+ INFO_PLIST_CONTENTS = <<-TEXT
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>CFBundleExecutable</key>
+ <string>#{APP_NAME}</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+ <key>LSUIElement</key>
+ <true/>
+ </dict>
+</plist>
+ TEXT
+ EXECUTABLE_CONTENTS = <<-TEXT
+#!/bin/sh
+cd "`dirname $0`/../Resources"
+/usr/local/bin/macruby bin/#{APP_NAME}
+ TEXT
+ VERSION_PLIST_CONTENTS = <<-TEXT
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>CFBundleShortVersionString</key>
+ <string>#{VERSION}</string>
+ <key>CFBundleVersion</key>
+ <string>#{VERSION}</string>
+ <key>ProjectName</key>
+ <string>#{APP_NAME}</string>
+ </dict>
+</plist>
+ TEXT
+ VERSION_CONTENTS = <<-TEXT
+module Fusuma
+ VERSION = "#{VERSION}"
+end
+ TEXT
+ ############################################################################
+
+ def build_application
+ create_directories
+ copy_source
+
+ create_files
+ end
+
+ def create_directories
+ [FRAMEWORKS_DIR, MACOS_DIR, PROFILE_DIR, APP_LOG_DIR].each do |directory|
+ FileUtils.makedirs directory
+ end
+ end
+
+ def create_files
+ %w(BUILD_STAMP INFO_PLIST EXECUTABLE LICENSE VERSION_PLIST VERSION).each do |file|
+ values = %w(FILE CONTENTS).each
+ write_file { Build.const_get "#{file}_#{values.next}" }
+ end
+
+ File.chmod 0766, EXECUTABLE_FILE
+ end
+
+ def write_file
+ File.open(yield, 'w') { |f| f.write yield }
+ end
+
+ def copy_source
+ %w(BIN CONF LIB VENDOR).each do |dir|
+ values = %w(SOURCE APP).each
+ copy_dir { Build.const_get "#{values.next}_#{dir}_DIR" }
+ end
+ end
+
+ def copy_dir
+ FileUtils.copy_entry(yield, yield)
+ end
+
+ def initialize
+ build_application
+ end
+ end
+end
+
+Fusuma::Build.new
+
22 source/bin/fusuma
@@ -0,0 +1,22 @@
+$:.unshift File.join(File.dirname($0), '/../lib')
+
+framework 'ScriptingBridge'
+framework 'Carbon'
+framework 'Cocoa'
+
+require 'vendor/hotkeys-0.1.2/lib/hotkeys'
+
+require 'json'
+require 'ostruct'
+require 'logger'
+
+require 'version'
+require 'fusuma'
+
+begin
+ Fusuma::Fusuma.new
+rescue => error
+ puts error.message
+ puts error.backtrace.join("\n")
+end
+
14 source/conf/configuration.json
@@ -0,0 +1,14 @@
+{
+ "layouts": [
+ "small_workspace"
+ ],
+ "keymap": {
+ "master": "RETURN+COMMAND+SHIFT",
+ "remove_all_windows": "Q+COMMAND+SHIFT",
+ "rotate_layout_counterclockwise": "K+COMMAND+SHIFT",
+ "rotate_layout_clockwise": "J+COMMAND+SHIFT",
+ "activate_previous_window": "H+COMMAND+SHIFT",
+ "activate_next_window": "L+COMMAND+SHIFT"
+ }
+}
+
67 source/lib/fusuma.rb
@@ -0,0 +1,67 @@
+#### remove when we're done ####
+class Object
+
+ def filter_methods
+ (self.methods(true, true) - Object.methods(true, true)).sort
+ end
+end
+################################
+
+module Fusuma
+
+ require 'fusuma/logger'
+
+ require 'fusuma/properties'
+ require 'fusuma/scale'
+ require 'fusuma/container'
+
+ require 'fusuma/layout'
+ require 'fusuma/keymap'
+ require 'fusuma/configuration'
+
+ require 'fusuma/ax'
+ require 'fusuma/ax_application'
+ require 'fusuma/ax_window'
+
+ require 'fusuma/workspace'
+ require 'fusuma/application'
+ require 'fusuma/window'
+
+ require 'fusuma/status_bar'
+ require 'fusuma/status_bar_icon'
+ require 'fusuma/status_bar_window'
+
+ class Fusuma
+
+ include Logger
+ include Properties
+
+ def initialize
+ log.info 'Initializing Fusuma.'
+ @application = NSApplication.sharedApplication
+
+ NSApp.setActivationPolicy POLICY
+ @status_bar = StatusBar.new
+
+ @application.delegate = self
+ @application.run
+ rescue => error
+ log.error error.message
+ log.error error.backtrace.join("\n")
+ end
+
+ def applicationDidFinishLaunching(notification)
+ log.info 'Configuring Fusuma.'
+ configuration = Configuration.new
+
+ @layouts = configuration.layouts
+ @keymap = configuration.keymap(@layouts.first) # use only 1 layout right now
+ rescue => error
+ log.error error.message
+ log.error error.backtrace.join("\n")
+ end
+
+ end
+
+end
+
65 source/lib/fusuma/application.rb
@@ -0,0 +1,65 @@
+module Fusuma
+
+ class Application
+
+ include Properties
+ include Container
+
+ def initialize(application)
+ container = Workspace.find(application[BUNDLE])
+
+ if container.nil?
+ # There are some non-native applications that can't be controlled via
+ # the SystemEvents application list. For these applications, an
+ # AXUIElement reference is created for controlling windows, while the
+ # SBRunningApplication instance is used for controlling the application
+ # itself.
+ @container = AXUIElementCreateApplication(application[PID])
+ @running = application[RUNNING]
+
+ # Override the Application's control methods.
+ extend AX
+ extend AX::Application
+ else
+ @container = container
+ end
+ end
+
+ def self.active
+ # The focused application (the application listed in the menu bar).
+ Application.new Workspace.active
+ end
+
+ def self.all
+ # Returns an array of all applications launched by the user.
+ #
+ # System applications (loginwindow, Dock, Notification Center) and
+ # applications that are set to be automatically opened and backgrounded
+ # (Alfred, Growl, Caffeine) are not included in this list.
+ Workspace.launched_applications.map do |application|
+ Application.new application
+ end
+ end
+
+ def windows
+ # If we have a valid SBApplication container, we can rely on it returning
+ # an array of valid windows. If you try to get the windows array from an
+ # Application with no open windows, you get nil not an empty array.
+ container.windows.to_a.map { |window| Window.new(window, self) }
+ end
+
+ def focused_window
+ # SBApplication windows are returned in correct Z order, so we can just
+ # create a window with the first.
+ Window.new(container.windows.first, self)
+ end
+
+ def activate
+ # If we have a valid SBApplication, we can just set the application to
+ # 'Frontmost'.
+ container.setFrontmost(true)
+ end
+
+ end
+end
+
48 source/lib/fusuma/ax.rb
@@ -0,0 +1,48 @@
+module Fusuma
+
+ module AX
+
+ def title
+ copy(TITLE, &:value)
+ end
+
+ def create(type, values)
+ value = create_pointer(type, values)
+
+ yield AXValueCreate(*value)
+ end
+
+ def copy(attribute)
+ pointer = Pointer.new(:id)
+
+ AXUIElementCopyAttributeValue(container, attribute, pointer)
+ yield pointer
+ end
+
+ def set(attribute, value)
+ AXUIElementSetAttributeValue(container, attribute, value)
+ end
+
+ def create_pointer(type, values = nil)
+ type = type.to_s.capitalize
+ pointer = Pointer.new("{CG#{type}=dd}")
+
+ unless values.nil?
+ point = Kernel.const_get("NS#{type}")
+ pointer.assign point.new(*values)
+ end
+
+ Array[Properties.const_get("CG#{type.upcase}"), pointer]
+ end
+
+ def get(type, source)
+ type, pointer = create_pointer(type)
+
+ AXValueGetValue(source, type, pointer)
+ pointer.value
+ end
+
+ end
+
+end
+
43 source/lib/fusuma/ax_application.rb
@@ -0,0 +1,43 @@
+module Fusuma
+
+ module AX
+
+ module Application
+
+ include Properties
+
+ def windows
+ # If we're relying on AXUIElement creation we need to copy the
+ # application's 'window list' attribute to a pointer and create
+ # AXUIElementWindows.
+ copy(WINDOWS, &:value).to_a.map { |window| create window }
+ end
+
+ def focused_window
+ # AXUIElement creation requires us to copy the application's focused
+ # window attribute to a pointer and create an AXUIWindowElement with
+ # it.
+ create copy(FOCUSED, &:value)
+ end
+
+ def create(window)
+ window = Fusuma::Window.new(window, self)
+ window.extend AX
+ window.extend AX::Window
+ window
+ end
+
+ def activate
+ # If we're using an AXUIElement, we can use a function from the
+ # NSRunningApplication instance 'activateWithOptions' (some
+ # applications respond to this function by opening another instance of
+ # the application or another window).
+ @running.activateWithOptions(2)
+ end
+
+ end
+
+ end
+
+end
+
44 source/lib/fusuma/ax_window.rb
@@ -0,0 +1,44 @@
+module Fusuma
+
+ module AX
+
+ module Window
+
+ include Properties
+
+ def activate
+ set(MAIN, true)
+
+ @application.activate
+ end
+
+ def position
+ copy(POS) do |pointer|
+ get(:point, pointer.value)
+ end
+ end
+
+ def position=(position)
+ create(:point, position) do |pointer|
+ set(POS, pointer)
+ end
+ end
+
+ def dimensions
+ copy(DIM) do |pointer|
+ get(:size, pointer.value)
+ end
+ end
+
+ def dimensions=(dimensions)
+ create(:size, dimensions) do |pointer|
+ set(DIM, pointer)
+ end
+ end
+
+ end
+
+ end
+
+end
+
42 source/lib/fusuma/configuration.rb
@@ -0,0 +1,42 @@
+module Fusuma
+
+ class Configuration
+
+ include Properties
+
+ def layouts
+ # Build each of the listed Layouts in the configuration, then match each
+ # layout to each active desktop for the current space (layout_1 is paired
+ # with with desktop_1 and layout_2 to desktop_2, etc). Finally, rescale
+ # the layout to the matching desktop's dimensions.
+ read_configuration do |layouts, keymap|
+ @keymap = keymap
+ layouts = layouts.map { |l| Layout.load(l) }
+ layouts.zip(Workspace.desktops) do |layout, desktop|
+ layout.scale_to desktop unless desktop.nil?
+ end
+
+ layouts
+ end
+ end
+
+ def keymap(layout)
+ KeyMap.new(layout) do |keymap|
+ @keymap.each do |action, sequence|
+ keymap.add(action, sequence)
+ end
+
+ keymap.keys
+ end
+ end
+
+ def read_configuration
+ File.open(CONFIG, 'r') do |file|
+ file = JSON.parse(file.read)
+ yield file.values
+ end
+ end
+
+ end
+end
+
17 source/lib/fusuma/container.rb
@@ -0,0 +1,17 @@
+module Fusuma
+
+ module Container
+
+ attr_reader :container
+
+ def ==(target)
+ container.eql? target.container
+ end
+
+ def title
+ container.name
+ end
+
+ end
+
+end
88 source/lib/fusuma/keymap.rb
@@ -0,0 +1,88 @@
+module Fusuma
+
+ class KeyMap
+
+ include Logger
+
+ attr_reader :keys, :layout
+
+ def activate_next_window
+ # find the active window in the layout and activate the previous window
+ # without changing the layout or positioning of any windows
+ log.debug "Activating the next layout window."
+ index = layout.index(Application.active.focused_window)
+ layout[index - 1].activate
+ end
+
+ def activate_previous_window
+ # Reverse the layout, find the position of the currently focused window
+ # and activate the previous window. This is done because negative numbered
+ # array elements start from the end of the array - so we don't have to
+ # worry about the index being out of bounds.
+ log.debug "Activating the previous layout window."
+ reversed = layout.reverse
+ index = reversed.index(Application.active.focused_window)
+ reversed[index - 1].activate
+ end
+
+ def rotate_layout_clockwise
+ # make the last window the main(first) window and reorganize the windows
+ log.debug "Rotating the layout clockwise."
+ layout.prepend layout.last
+ apply_layout
+ end
+
+ def rotate_layout_counterclockwise
+ # make the main(first) window the last window and reorganize the windows
+ log.debug "Rotating the layout counter clockwise."
+ layout.append layout.first
+ apply_layout
+ end
+
+ def main_window
+ # make the active window the main(first) window (if it's not in the layout,
+ # add it) and reorganize the windows
+ log.debug "Setting the active window as layout's main window."
+ layout.prepend Window.active
+ apply_layout
+ end
+
+ def master
+ if layout.main.eql? Window.active
+ log.debug "The active window is already the main window."
+ remove_active_window
+ else
+ log.debug "This active window is not in the layout or is not the main window."
+ main_window
+ end
+ end
+
+ def remove_active_window
+ log.debug "Removing the active window from the layout."
+ layout.remove Window.active
+ apply_layout
+ end
+
+ def apply_layout
+ log.debug "Applying the window positioning and sizing rules of the layout."
+ layout.apply
+ end
+
+ def remove_all_windows
+ log.debug "Removing all windows from the layout."
+ layout.clear
+ end
+
+ def add(action, sequence)
+ keys.addHotString(sequence, &(-> { send(action.to_sym) }))
+ end
+
+ def initialize(layout)
+ @layout = layout
+ @keys = HotKeys.new
+
+ yield self
+ end
+
+ end
+end
76 source/lib/fusuma/layout.rb
@@ -0,0 +1,76 @@
+module Fusuma
+
+ class Layout < Array
+
+ include Properties
+
+ attr_accessor :frames, :scale
+
+ def self.all
+ layouts = Dir.glob(File.join(LAYOUTS, '**.json'))
+
+ layouts.map do |layout|
+ File.open(layout, 'r') do |file|
+ OpenStruct.new JSON.load(file.read)
+ end
+ end
+ end
+
+ def self.load(name)
+ config = Layout.all.find { |l| l.name.eql? name }
+
+ Layout.new(config.scale, config.frames)
+ end
+
+ def initialize(scale, frames)
+ @scale = Area(*scale.values.flatten)
+ @frames = frames
+ end
+
+ def scale_to(desktop)
+ scale = Scale.from_rect(@scale)
+
+ @scale = desktop # assign the desktop rect as this layout's scale
+ scale.scaler(desktop)
+
+ @frames.map! do |frame|
+ frame.map! { |layout| scale.convert(layout) }
+ end
+ end
+
+ def apply
+ zip(@frames[count - 1]) do |window, frame|
+ # reposition and resize each window depending on its associated layout
+ # frame's position/dimensions.
+ window.position = frame.origin.to_a
+ window.dimensions = frame.size.to_a
+ end
+ end
+
+ def remove(window)
+ reject! { |w| w.eql? window }
+ end
+
+ def main() first end
+
+ def append(window)
+ # Remove the window if it already exists, then add it to the end of the
+ # layout and remove any extra windows from the beginning of the layout.
+ remove window
+ push window
+
+ shift while count > @frames.count
+ end
+
+ def prepend(window)
+ # Remove the window if it already exists, then add it to the beginning of
+ # the layout and remove any extra windows from the end of the layout.
+ remove window
+ unshift window
+
+ pop while count > @frames.count
+ end
+
+ end
+
+end
57 source/lib/fusuma/layouts/small_workspace.json
@@ -0,0 +1,57 @@
+{
+ "name": "small_workspace",
+ "scale": {
+ "origin": [ 0, 0 ],
+ "size": [ 120, 120 ]
+ },
+ "frames": [
+ [
+ {
+ "position": [ 0, 0 ],
+ "dimensions": [ 120, 120 ]
+ }
+ ],
+ [
+ {
+ "position": [ 60, 0 ],
+ "dimensions": [ 60, 120 ]
+ },
+ {
+ "position": [ 0, 0 ],
+ "dimensions": [ 60, 120 ]
+ }
+ ],
+ [
+ {
+ "position": [ 60, 0 ],
+ "dimensions": [ 60, 120 ]
+ },
+ {
+ "position": [ 0, 60 ],
+ "dimensions": [ 60, 60 ]
+ },
+ {
+ "position": [ 0, 0 ],
+ "dimensions": [ 60, 60 ]
+ }
+ ],
+ [
+ {
+ "position": [ 60, 0 ],
+ "dimensions": [ 60, 120 ]
+ },
+ {
+ "position": [ 0, 80 ],
+ "dimensions": [ 60, 40 ]
+ },
+ {
+ "position": [ 0, 40 ],
+ "dimensions": [ 60, 40 ]
+ },
+ {
+ "position": [ 0, 0 ],
+ "dimensions": [ 60, 40 ]
+ }
+ ]
+ ]
+}
30 source/lib/fusuma/logger.rb
@@ -0,0 +1,30 @@
+module Fusuma
+
+ module Logger
+
+ def log
+ @log ||= logger
+ end
+
+ private
+
+ def log_formatter
+ ->(severity, datetime, program, message) do
+ datetime = datetime.strftime("%Y-%m-%dT%H:%M:%S")
+ "[#{datetime}] - #{severity} - (#{component_name}) #{message}\n"
+ end
+ end
+
+ def component_name
+ self.class.name[/\w+::(.+)/, 1]
+ end
+
+ def logger
+ logger = Kernel::Logger.new(File.join('log', 'fusuma.log'), 3, 5 * 1024 * 1024)
+ logger.formatter = log_formatter
+ logger
+ end
+ end
+
+end
+
48 source/lib/fusuma/properties.rb
@@ -0,0 +1,48 @@
+module Fusuma
+
+ module Properties
+
+ NAME = 'NSApplicationName'
+ PID = 'NSApplicationProcessIdentifier'
+ BUNDLE = 'NSApplicationBundleIdentifier'
+ RUNNING = 'NSWorkspaceApplicationKey'
+
+ EVENTS = 'com.apple.SystemEvents'
+
+ FOCUSED = NSAccessibilityFocusedWindowAttribute
+ WINDOWS = NSAccessibilityWindowsAttribute
+ POS = NSAccessibilityPositionAttribute
+ DIM = NSAccessibilitySizeAttribute
+ MAIN = NSAccessibilityMainAttribute
+ TITLE = NSAccessibilityTitleAttribute
+ RAISE = NSAccessibilityRaiseAction
+
+ STATUS = NSVariableStatusItemLength
+ CENTER = NSCenterTextAlignment
+ BREAK = NSLineBreakByTruncatingTail
+ FONTSIZE = NSFontAttributeName
+ FGCOLOR = NSForegroundColorAttributeName
+ PGSTYLE = NSParagraphStyleAttributeName
+ MASK = NSBorderlessWindowMask
+ BACKING = NSBackingStoreBuffered
+ LEVEL = NSStatusWindowLevel
+ POLICY = NSApplicationActivationPolicyAccessory
+
+ CGSIZE = KAXValueCGSizeType
+ CGPOINT = KAXValueCGPointType
+
+ ACTION = 'actT'.unpack('N').first
+ POSITION = 'posn'.unpack('N').first
+ DIMENSIONS = 'ptsz'.unpack('N').first
+
+ LAYOUTS = File.expand_path(File.join('lib', 'fusuma', 'layouts'))
+ CONFIG = File.join('conf', 'configuration.json')
+
+ def Area(x, y, w, h)
+ NSRect.new(NSPoint.new(x, y), NSSize.new(w, h))
+ end
+
+ end
+
+end
+
50 source/lib/fusuma/scale.rb
@@ -0,0 +1,50 @@
+module Fusuma
+
+ class Scale
+
+ include Properties
+
+ attr_reader :x, :y, :w, :h
+
+ def initialize(x, y, w, h)
+ @x, @y = x, y
+ @w, @h = w, h
+ end
+
+ def self.from_rect(rect)
+ # Conversion of an NSRect to a fusuma Scale
+ Scale.new *rect.to_a.map { |coord| coord.to_a }.flatten
+ end
+
+ def rescale(value, from_range, to_range)
+ # rescale the value to a new min/max range based on the value's current
+ # min/max range
+ a = value - from_range.min
+ b = to_range.max - to_range.min
+ c = from_range.max - from_range.min
+
+ a.to_f * b.to_f / c.to_f + to_range.min
+ end
+
+ def flip_rect
+ @y = (@y > 0) ? @y : (@y * -1) + Workspace.desktops.first.origin.y
+
+ Area(x, y, w, h)
+ end
+
+ def scaler(scale)
+ # an NSRect for a new layout scaler
+ @scale = Scale.from_rect(scale)
+ end
+
+ def convert(layout)
+ layout = layout.values.map do |a, b|
+ [rescale(a, (x..w), (@scale.x..@scale.w)),
+ rescale(b, (y..h), (@scale.y..@scale.h))]
+ end
+
+ Area(*layout.flatten)
+ end
+
+ end
+end
33 source/lib/fusuma/status_bar.rb
@@ -0,0 +1,33 @@
+module Fusuma
+
+ class StatusBar
+
+ include Logger
+ include Properties
+
+ def initialize
+ log.info 'Initializing StatusBar.'
+ @status_bar = NSStatusBar.systemStatusBar.statusItemWithLength(STATUS)
+ @status_bar.view = StatusBarIcon.new(self)
+ @status_bar.highlightMode = true
+ end
+
+ def show(frame)
+ @window = StatusBarWindow.alloc.init(frame, self)
+ @window.show
+ @status_bar.view.show
+ end
+
+ def hide(frame)
+ @window.hide
+ @status_bar.view.hide
+ @window = frame
+ end
+
+ def toggle(frame = nil)
+ @window ? hide(frame) : show(frame)
+ end
+
+ end
+
+end
78 source/lib/fusuma/status_bar_icon.rb
@@ -0,0 +1,78 @@
+module Fusuma
+
+ class StatusBarIcon < NSView
+
+ include Properties
+
+ ICON = 'f'
+ SIZE = 18.0
+
+ X = Y = 0.0
+ W = H = 20.0
+
+ def initialize(controller)
+ @selected = false
+ @controller = controller
+ initWithFrame(Area(X, Y, W, H))
+ end
+
+ def color
+ @selected ? NSColor.selectedMenuItemTextColor : NSColor.controlTextColor
+ end
+
+ def highlight(rect)
+ NSColor.selectedMenuItemColor.set
+ NSRectFill(rect)
+ end
+
+ def icon_frame
+ size = ICON.sizeWithAttributes(style)
+
+ width = (frame.size.width - size.width) / 2.0
+ height = (frame.size.height - size.height) / 2.0
+
+ Area(width, height, size.width, size.height)
+ end
+
+ def drawRect(rect)
+ highlight(rect) if @selected
+
+ ICON.drawInRect(icon_frame, withAttributes:style)
+ end
+
+ def style
+ { FONTSIZE => font_size,
+ FGCOLOR => color,
+ PGSTYLE => para_style }
+ end
+
+ def font_size
+ NSFont.menuBarFontOfSize(SIZE)
+ end
+
+ def para_style
+ paragraph = NSMutableParagraphStyle.alloc.init
+ paragraph.setParagraphStyle(NSParagraphStyle.defaultParagraphStyle)
+ paragraph.setAlignment(CENTER)
+ paragraph.setLineBreakMode(BREAK)
+ paragraph
+ end
+
+ def mouseDown(event)
+ @controller.toggle window.frame
+ end
+
+ def show
+ @selected = true
+ setNeedsDisplay true
+ end
+
+ def hide
+ @selected = false
+ setNeedsDisplay true
+ end
+
+ end
+
+end
+
57 source/lib/fusuma/status_bar_window.rb
@@ -0,0 +1,57 @@
+module Fusuma
+
+ class StatusBarWindow < NSWindow
+
+ include Properties
+
+ X = Y = 0.0
+ W, H = 50.0, 30.0
+
+ def init(frame, controller)
+ @controller = controller
+
+ window = create(Area(frame.origin.x, (frame.origin.y - H), W, H))
+ window.contentView.addSubview(create_button)
+ window
+ end
+
+ def create(frame)
+ window = initWithContentRect(frame, styleMask:MASK, backing:BACKING, defer:false)
+ window.level = LEVEL
+ window.hasShadow = true
+ window.delegate = self
+ window
+ end
+
+ def create_button
+ button = NSButton.alloc.initWithFrame(Area(X, Y, W, H))
+ button.bezelStyle = 11
+ button.title = 'Quit'
+ button.target = self
+ button.action = 'quit:'
+ button
+ end
+
+ def show
+ makeKeyAndOrderFront self
+ NSApplication.sharedApplication.activateIgnoringOtherApps true
+ end
+
+ def hide
+ orderOut self
+ end
+
+ def canBecomeKeyWindow
+ true
+ end
+
+ def resignKeyWindow
+ @controller.toggle
+ end
+
+ def quit(sender)
+ NSApp.terminate nil
+ end
+ end
+
+end
78 source/lib/fusuma/window.rb
@@ -0,0 +1,78 @@
+module Fusuma
+
+ class Window
+
+ include Properties
+ include Container
+
+ def initialize(window, application)
+ @container = window
+ @application = application
+ end
+
+ def location
+ # The window's current desktop based on whether the window's origin (x,y)
+ # is within a desktop's visible range.
+ Workspace.desktops.each_with_index do |desktop, index|
+ x, y = position
+ x_range = (desktop.origin.x..desktop.size.width)
+ y_range = (desktop.origin.y..desktop.size.height)
+
+ if x_range.include?(x) && y_range.include?(y)
+ location = Struct.new(:index, :origin, :size)
+ return location.new(index, desktop.origin, desktop.size)
+ end
+ end
+ end
+
+ def self.all
+ # All open windows for all open applications that the user has launched.
+ Application.all.map(&:windows)
+ end
+
+ def self.active
+ # The active window of the active application.
+ Application.active.focused_window
+ end
+
+ def activate
+ # Raise the window's index to the top of the application's window list,
+ # then activate the application. (fancy way of saying 'focus the window')
+ container.elementArrayWithCode(ACTION).objectWithName(RAISE).perform
+
+ @application.activate
+ end
+
+ def position
+ # Returns Array[x, y]:
+ #
+ # > window.position
+ # => [0.0, 0.0]
+ container.propertyWithCode(POSITION).get
+ end
+
+ def position=(position)
+ # Set the window's position:
+ #
+ # > window.position = [0.0, 0.0]
+ container.propertyWithCode(POSITION).setTo position
+ end
+
+ def dimensions
+ # Returns Array[height, width]:
+ #
+ # > window.dimensions
+ # => [100.0, 100.0]
+ container.propertyWithCode(DIMENSIONS).get
+ end
+
+ def dimensions=(dimensions)
+ # Set the window's dimensions:
+ #
+ # > window.dimensions = [100.0, 100.0]
+ container.propertyWithCode(DIMENSIONS).setTo dimensions
+ end
+
+ end
+end
+
88 source/lib/fusuma/workspace.rb
@@ -0,0 +1,88 @@
+module Fusuma
+
+ module Workspace
+
+ extend self
+ include Properties
+
+ def menubar
+ # The OSX menu bar's height in pixels. It seems to always be 22 pixels no
+ # matter what resolution I'm using, but it doesn't hurt to ask the system.
+ NSMenu.menuBarHeight
+ end
+
+ def desktops
+ # Returns the position and dimensions (as an NSRect) of each active
+ # desktop for the current OSX space.
+ #
+ # > Workspace.desktops.first
+ # => #<NSRect origin=#<NSPoint x=4.0 y=0.0> size=#<NSSize width=1436.0 height=878.0>>
+
+ # screens = NSScreen.screens.map(&:frame)
+ screens = NSScreen.screens.map(&:visibleFrame) # ಠ_ಠ
+
+ # Cocoa sees desktops stacked on top of each other starting with the main
+ # desktop (the desktop that has the OSX menu bar) and moving down regardless
+ # of the actual desktop setup.
+ #
+ # For example, if your main monitor's resolution is 1440x900 and the
+ # secondary monitor's resolution is 1440x900 and you have the secondary
+ # monitor stacked on top of the primary - the secondary monitor's origin
+ # will be [0,900] while the origin of a maximized window on the secondary
+ # monitor will be [0,-900].
+ #
+ # Yes, this is stupid.
+ screens.map!.each_with_index do |screen, index|
+ if index.zero?
+ screen
+ else
+ scale = Scale.from_rect(screen)
+ scale.flip_rect
+ end
+ end
+ end
+
+ def find(bundle)
+ # You can create each SBApplication instance individually:
+ # > SBApplication.applicationWithBundleIdentifier('com.apple.finder')
+ # => <FinderApplication @0x1837140: application "Finder" (254)>
+ # > SBApplication.applicationWithProcessIdentifier(254)
+ # => <FinderApplication @0x1624920: application "Finder" (254)>
+ #
+ # Most of the actions and objects available to an SBApplication instance
+ # require the application to have an OSX scripting definition, which in turn
+ # means the application is required to be a native OSX application.
+ #
+ # Instead of creating each SBApplication individually, we can bypass the
+ # scripting definition requirement by creating an SBApplication instance
+ # of System Events (the scripting bridge application itself) and use its
+ # list of all running applications.
+ system_events = SBApplication.applicationWithBundleIdentifier(EVENTS)
+
+ system_events.applicationProcesses.find do |application|
+ application.bundleIdentifier.eql? bundle
+ end
+ end
+
+ def launched_applications
+ # A list of applications launched by the user.
+ #
+ # NSApplicationPath (ex. '/System/Library/CoreServices/Finder.app')
+ # NSWorkspaceApplicationKey (an NSRunningApplication instance)
+ # NSApplicationBundleIdentifier (ex. 'com.apple.finder')
+ # NSApplicationProcessSerialNumberLow (ex 45067)
+ # NSApplicationProcessIdentifier (ex. 254)
+ # NSApplicationProcessSerialNumberHigh (ex. 0)
+ # NSApplicationName (ex. 'Finder')
+ NSWorkspace.sharedWorkspace.launchedApplications
+ end
+
+ def active
+ # This returns the same properties as the Workspace.launched_applications
+ # list, but only the currently focused application.
+ NSWorkspace.sharedWorkspace.activeApplication
+ end
+
+ end
+end
+
0  source/log/.gitkeep
No changes.
1  source/vendor/hotkeys-0.1.2/CHANGES
@@ -0,0 +1 @@
+None yet!
3  source/vendor/hotkeys-0.1.2/Gemfile
@@ -0,0 +1,3 @@
+source :rubygems
+
+gemspec
17 source/vendor/hotkeys-0.1.2/Gemfile.lock
@@ -0,0 +1,17 @@
+PATH
+ remote: .
+ specs:
+ hotkeys (0.1.0)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ rake (0.9.1)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ bundler (~> 1.0.0)
+ hotkeys!
+ rake
19 source/vendor/hotkeys-0.1.2/LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2011 by Robert Lowe <rob[!]iblargz.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
51 source/vendor/hotkeys-0.1.2/README.md
@@ -0,0 +1,51 @@
+HotKeys
+=====
+
+A simple gem which allows you to bind global hot keys with macruby, optionally can also specify
+which application needs to be frontmost (inspired by Keyboard Maestro)
+
+1. `gem install hotkeys`
+2. `macruby examples/simple.rb`
+
+Usage example:
+`macruby examples/simple.rb`
+
+ require 'rubygems'
+ require 'hotkeys'
+
+ # Delegate method called when the app finished loading
+ def applicationDidFinishLaunching(notification)
+ @hotkeys = HotKeys.new
+
+ # Will only trigger if Safari is frontmost application, second option can be left blank
+ # for truly global shortcut
+ @hotkeys.addHotString("Space+OPTION","com.apple.safari") do
+ puts "LOL MACRUBY RUNS"
+ end
+
+ # Seriously for a second; For some reason a global hotkey or at least
+ # the way I've got it to working blocks the keystroke. Which I guess is normal?
+ #
+ # Maybe I'll hack around that with system events for a bypass option, it wouldn't
+ # be elegate, but this is new territory anywho
+ #
+ @hotkeys.addHotString("S+COMMAND") do
+ puts "NO SAVING FOR YOU, LOL" # Trolled myself with this for longer than I should of
+ end
+ end
+
+ # We are delegating the application to self so the script will know when
+ # it finished loading
+ NSApplication.sharedApplication.delegate = self
+ NSApplication.sharedApplication.run
+
+Todo
+=====
+ * Clean up shortcut.m
+
+
+Known Bugs
+=====
+* Cannot unbind keys, (anyone awesome at objective+c?)
+
+# Copyright (C) 2011 by Robert Lowe <rob[!]iblargz.com> - MIT
40 source/vendor/hotkeys-0.1.2/Rakefile
@@ -0,0 +1,40 @@
+require 'rake/clean'
+require 'rake/testtask'
+require 'fileutils'
+require 'date'
+
+require 'lib/hotkeys/version.rb'
+
+# task :default => :test
+# task :spec => :test
+
+# PACKAGING ============================================================
+
+if defined?(Gem)
+ # Load the gemspec using the same limitations as github
+ def spec
+ require 'rubygems' unless defined? Gem::Specification
+ @spec ||= eval(File.read('hotkeys.gemspec'))
+ end
+
+ def package(ext='')
+ "pkg/hotkeys-#{spec.version}" + ext
+ end
+
+ desc 'Build packages'
+ task :package => %w[.gem].map {|e| package(e)}
+
+ desc 'Build and install as local gem'
+ task :install => package('.gem') do
+ `gem install #{package('.gem')}`
+ end
+
+ directory 'pkg/'
+ CLOBBER.include('pkg')
+
+ file package('.gem') => %w[pkg/ hotkeys.gemspec] + spec.files do |f|
+ `gem build hotkeys.gemspec`
+ mv File.basename(f.name), f.name
+ end
+
+end
41 source/vendor/hotkeys-0.1.2/hotkeys.gemspec
@@ -0,0 +1,41 @@
+require 'lib/hotkeys/version'
+
+Gem::Specification.new do |s|
+ s.specification_version = 2 if s.respond_to? :specification_version=
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+
+ s.name = %q{hotkeys}
+ s.version = "#{Hotkeys::Version::STRING}"
+ s.authors = ["Rob Lowe"]
+ s.date = %q{2011-06-08}
+ s.description = %q{A simple gem which allows you to bind global hot keys with macruby}
+ s.email = %q{rob@iblargz.com}
+ s.extra_rdoc_files = [
+ "LICENSE",
+ "CHANGES",
+ "README.md"
+ ]
+ s.files = [
+ "Gemfile",
+ "Gemfile.lock",
+ "LICENSE",
+ "README.md",
+ "Rakefile",
+ "hotkeys.gemspec",
+ "lib/hotkeys.rb",
+ "lib/hotkeys/version.rb",
+ "lib/hotkeys/support.rb",
+ "lib/hotkeys/support/bridgesupport/Events.bridgesupport",
+ "lib/hotkeys/support/bundles/shortcut.bundle",
+ "lib/hotkeys/support/bundles/shortcut.m",
+ "lib/hotkeys/support/keys.rb"
+ ]
+ s.homepage = %q{http://github.com/RobertLowe/hotkeys}
+ s.licenses = ["MIT"]
+ s.require_paths = ["lib"]
+ s.rubygems_version = %q{1.4.2}
+ s.summary = %q{A simple gem which allows you to bind global hot keys with macruby}
+
+ s.add_development_dependency(%q<rake>, [">= 0"])
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
+end
89 source/vendor/hotkeys-0.1.2/lib/hotkeys.rb
@@ -0,0 +1,89 @@
+# Copyright (C) 2011 by Robert Lowe <rob[!]iblargz.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+unless RUBY_ENGINE =~ /macruby/
+ raise NotImplementedError, "Hotkeys only runs on macruby! ;)"
+end
+
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+framework 'AppKit'
+framework 'Carbon'
+framework 'Cocoa'
+framework 'ScriptingBridge'
+
+# required here, beause we subclass this sucker right away.
+require File.dirname(__FILE__) + '/hotkeys/support/bundles/shortcut'
+
+class HotKeys < Shortcut
+ autoload :Version, 'hotkeys/version'
+ autoload :Support, 'hotkeys/support'
+
+ def initialize(delegation=nil)
+ self.delegate = delegation || self
+ end
+
+ def hotkeyWasPressed(key)
+ # Matching int
+ configuration = @configurations.detect {|configuration| configuration[:key] == key }
+ str = configuration[:str]
+
+ # Sibling bindings
+ configurations = @configurations.select {|config| config[:str] == str}
+
+ # Run em
+ configurations.each do |configuration|
+ block = configuration[:block]
+ if configuration[:bundle_identifier] == frontmost_bundler_identifier
+ block.call
+ elsif configuration[:bundle_identifier].nil?
+ block.call
+ end
+ end if configurations
+ end
+
+ def addHotString(str, options = {}, &block)
+ args = HotKeys::Support::Keys.parse(str)
+ @configurations ||= []
+ key = self.send(:"addShortcut:withKeyModifier", *args)
+ @configurations << {
+ :key => key.to_s,
+ :str => str,
+ :block => block,
+ :bundle_identifier => options.delete(:bundle_identifier)
+ }
+ end
+
+private
+
+ def systemevents
+ @systemevents ||= SBApplication.applicationWithBundleIdentifier("com.apple.systemevents")
+ end
+
+ def frontmost
+ @frontmost = systemevents.processes.select {|process| process.frontmost == true }
+ @frontmost.first
+ end
+
+ def frontmost_bundler_identifier
+ frontmost.bundleIdentifier if frontmost
+ end
+
+end
+
25 source/vendor/hotkeys-0.1.2/lib/hotkeys/support.rb
@@ -0,0 +1,25 @@
+# Copyright (C) 2011 by Robert Lowe <rob[!]iblargz.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+class HotKeys
+ module Support
+ autoload :Keys, 'hotkeys/support/keys'
+ end
+end
259 source/vendor/hotkeys-0.1.2/lib/hotkeys/support/bridgesupport/Events.bridgesupport
@@ -0,0 +1,259 @@
+<?xml version='1.0'?>
+<!DOCTYPE signatures SYSTEM "file://localhost/System/Library/DTDs/BridgeSupport.dtd">
+<signatures version='0.9'>
+<depends_on path='/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/CommonPanels.framework'/>
+<depends_on path='/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/Help.framework'/>
+<depends_on path='/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework'/>
+<depends_on path='/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/ImageCapture.framework'/>
+<depends_on path='/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/Ink.framework'/>
+<depends_on path='/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/OpenScripting.framework'/>
+<depends_on path='/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/Print.framework'/>
+<depends_on path='/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/SecurityHI.framework'/>
+<depends_on path='/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/SpeechRecognition.framework'/>
+<depends_on path='/System/Library/Frameworks/ApplicationServices.framework'/>
+<enum name='activMask' value='256'/>
+<enum name='activateEvt' value='8'/>
+<enum name='activeFlag' value='1'/>
+<enum name='activeFlagBit' value='0'/>
+<enum name='adbAddrMask' value='16711680'/>
+<enum name='alphaLock' value='1024'/>
+<enum name='alphaLockBit' value='10'/>
+<enum name='app1Evt' value='12'/>
+<enum name='app1Mask' value='4096'/>
+<enum name='app2Evt' value='13'/>
+<enum name='app2Mask' value='8192'/>
+<enum name='app3Evt' value='14'/>
+<enum name='app3Mask' value='16384'/>
+<enum name='app4Evt' value='15'/>
+<enum name='app4Mask' value='32768'/>
+<enum name='autoKey' value='5'/>
+<enum name='autoKeyMask' value='32'/>
+<enum name='btnState' value='128'/>
+<enum name='btnStateBit' value='7'/>
+<enum name='charCodeMask' value='255'/>
+<enum name='cmdKey' value='256'/>
+<enum name='cmdKeyBit' value='8'/>
+<enum name='controlKey' value='4096'/>
+<enum name='controlKeyBit' value='12'/>
+<enum name='diskEvt' value='7'/>
+<enum name='diskMask' value='128'/>
+<enum name='driverEvt' value='11'/>
+<enum name='driverMask' value='2048'/>
+<enum name='everyEvent' value='65535'/>
+<enum name='highLevelEventMask' value='1024'/>
+<enum name='kAppleLogoCharCode' value='20'/>
+<enum name='kAppleLogoUnicode' value='63743'/>
+<enum name='kBackspaceCharCode' value='8'/>
+<enum name='kBellCharCode' value='7'/>
+<enum name='kBulletCharCode' value='165'/>
+<enum name='kBulletUnicode' value='8226'/>
+<enum name='kCheckCharCode' value='18'/>
+<enum name='kCheckUnicode' value='10003'/>
+<enum name='kClearCharCode' value='27'/>
+<enum name='kCommandCharCode' value='17'/>
+<enum name='kCommandUnicode' value='8984'/>
+<enum name='kControlUnicode' value='8963'/>
+<enum name='kDeleteCharCode' value='127'/>
+<enum name='kDiamondCharCode' value='19'/>
+<enum name='kDiamondUnicode' value='9670'/>
+<enum name='kDownArrowCharCode' value='31'/>
+<enum name='kEndCharCode' value='4'/>
+<enum name='kEnterCharCode' value='3'/>
+<enum name='kEscapeCharCode' value='27'/>
+<enum name='kFormFeedCharCode' value='12'/>
+<enum name='kFunctionKeyCharCode' value='16'/>
+<enum name='kHelpCharCode' value='5'/>
+<enum name='kHighLevelEvent' value='23'/>
+<enum name='kHomeCharCode' value='1'/>
+<enum name='kLeftArrowCharCode' value='28'/>
+<enum name='kLineFeedCharCode' value='10'/>
+<enum name='kNonBreakingSpaceCharCode' value='202'/>
+<enum name='kNullCharCode' value='0'/>
+<enum name='kOptionUnicode' value='8997'/>
+<enum name='kPageDownCharCode' value='12'/>
+<enum name='kPageUpCharCode' value='11'/>
+<enum name='kPencilLeftUnicode' value='63490'/>
+<enum name='kPencilUnicode' value='9998'/>
+<enum name='kReturnCharCode' value='13'/>
+<enum name='kRightArrowCharCode' value='29'/>
+<enum name='kShiftUnicode' value='8679'/>
+<enum name='kSpaceCharCode' value='32'/>
+<enum name='kTabCharCode' value='9'/>
+<enum name='kUpArrowCharCode' value='30'/>
+<enum name='kVK_ANSI_0' value='29'/>
+<enum name='kVK_ANSI_1' value='18'/>
+<enum name='kVK_ANSI_2' value='19'/>
+<enum name='kVK_ANSI_3' value='20'/>
+<enum name='kVK_ANSI_4' value='21'/>
+<enum name='kVK_ANSI_5' value='23'/>
+<enum name='kVK_ANSI_6' value='22'/>
+<enum name='kVK_ANSI_7' value='26'/>
+<enum name='kVK_ANSI_8' value='28'/>
+<enum name='kVK_ANSI_9' value='25'/>
+<enum name='kVK_ANSI_A' value='0'/>
+<enum name='kVK_ANSI_B' value='11'/>
+<enum name='kVK_ANSI_Backslash' value='42'/>
+<enum name='kVK_ANSI_C' value='8'/>
+<enum name='kVK_ANSI_Comma' value='43'/>
+<enum name='kVK_ANSI_D' value='2'/>
+<enum name='kVK_ANSI_E' value='14'/>
+<enum name='kVK_ANSI_Equal' value='24'/>
+<enum name='kVK_ANSI_F' value='3'/>
+<enum name='kVK_ANSI_G' value='5'/>
+<enum name='kVK_ANSI_Grave' value='50'/>
+<enum name='kVK_ANSI_H' value='4'/>
+<enum name='kVK_ANSI_I' value='34'/>
+<enum name='kVK_ANSI_J' value='38'/>
+<enum name='kVK_ANSI_K' value='40'/>
+<enum name='kVK_ANSI_Keypad0' value='82'/>
+<enum name='kVK_ANSI_Keypad1' value='83'/>
+<enum name='kVK_ANSI_Keypad2' value='84'/>
+<enum name='kVK_ANSI_Keypad3' value='85'/>
+<enum name='kVK_ANSI_Keypad4' value='86'/>
+<enum name='kVK_ANSI_Keypad5' value='87'/>
+<enum name='kVK_ANSI_Keypad6' value='88'/>
+<enum name='kVK_ANSI_Keypad7' value='89'/>
+<enum name='kVK_ANSI_Keypad8' value='91'/>
+<enum name='kVK_ANSI_Keypad9' value='92'/>
+<enum name='kVK_ANSI_KeypadClear' value='71'/>
+<enum name='kVK_ANSI_KeypadDecimal' value='65'/>
+<enum name='kVK_ANSI_KeypadDivide' value='75'/>
+<enum name='kVK_ANSI_KeypadEnter' value='76'/>
+<enum name='kVK_ANSI_KeypadEquals' value='81'/>
+<enum name='kVK_ANSI_KeypadMinus' value='78'/>
+<enum name='kVK_ANSI_KeypadMultiply' value='67'/>
+<enum name='kVK_ANSI_KeypadPlus' value='69'/>
+<enum name='kVK_ANSI_L' value='37'/>
+<enum name='kVK_ANSI_LeftBracket' value='33'/>
+<enum name='kVK_ANSI_M' value='46'/>
+<enum name='kVK_ANSI_Minus' value='27'/>
+<enum name='kVK_ANSI_N' value='45'/>
+<enum name='kVK_ANSI_O' value='31'/>
+<enum name='kVK_ANSI_P' value='35'/>
+<enum name='kVK_ANSI_Period' value='47'/>
+<enum name='kVK_ANSI_Q' value='12'/>
+<enum name='kVK_ANSI_Quote' value='39'/>
+<enum name='kVK_ANSI_R' value='15'/>
+<enum name='kVK_ANSI_RightBracket' value='30'/>
+<enum name='kVK_ANSI_S' value='1'/>
+<enum name='kVK_ANSI_Semicolon' value='41'/>
+<enum name='kVK_ANSI_Slash' value='44'/>
+<enum name='kVK_ANSI_T' value='17'/>
+<enum name='kVK_ANSI_U' value='32'/>
+<enum name='kVK_ANSI_V' value='9'/>
+<enum name='kVK_ANSI_W' value='13'/>
+<enum name='kVK_ANSI_X' value='7'/>
+<enum name='kVK_ANSI_Y' value='16'/>
+<enum name='kVK_ANSI_Z' value='6'/>
+<enum name='kVK_CapsLock' value='57'/>
+<enum name='kVK_Command' value='55'/>
+<enum name='kVK_Control' value='59'/>
+<enum name='kVK_Delete' value='51'/>
+<enum name='kVK_DownArrow' value='125'/>
+<enum name='kVK_End' value='119'/>
+<enum name='kVK_Escape' value='53'/>
+<enum name='kVK_F1' value='122'/>
+<enum name='kVK_F10' value='109'/>
+<enum name='kVK_F11' value='103'/>
+<enum name='kVK_F12' value='111'/>
+<enum name='kVK_F13' value='105'/>
+<enum name='kVK_F14' value='107'/>
+<enum name='kVK_F15' value='113'/>
+<enum name='kVK_F16' value='106'/>
+<enum name='kVK_F17' value='64'/>
+<enum name='kVK_F18' value='79'/>
+<enum name='kVK_F19' value='80'/>
+<enum name='kVK_F2' value='120'/>
+<enum name='kVK_F20' value='90'/>
+<enum name='kVK_F3' value='99'/>
+<enum name='kVK_F4' value='118'/>
+<enum name='kVK_F5' value='96'/>
+<enum name='kVK_F6' value='97'/>
+<enum name='kVK_F7' value='98'/>
+<enum name='kVK_F8' value='100'/>
+<enum name='kVK_F9' value='101'/>
+<enum name='kVK_ForwardDelete' value='117'/>
+<enum name='kVK_Function' value='63'/>
+<enum name='kVK_Help' value='114'/>
+<enum name='kVK_Home' value='115'/>
+<enum name='kVK_ISO_Section' value='10'/>
+<enum name='kVK_JIS_Eisu' value='102'/>
+<enum name='kVK_JIS_Kana' value='104'/>
+<enum name='kVK_JIS_KeypadComma' value='95'/>
+<enum name='kVK_JIS_Underscore' value='94'/>
+<enum name='kVK_JIS_Yen' value='93'/>
+<enum name='kVK_LeftArrow' value='123'/>
+<enum name='kVK_Mute' value='74'/>
+<enum name='kVK_Option' value='58'/>
+<enum name='kVK_PageDown' value='121'/>
+<enum name='kVK_PageUp' value='116'/>
+<enum name='kVK_Return' value='36'/>
+<enum name='kVK_RightArrow' value='124'/>
+<enum name='kVK_RightControl' value='62'/>
+<enum name='kVK_RightOption' value='61'/>
+<enum name='kVK_RightShift' value='60'/>
+<enum name='kVK_Shift' value='56'/>
+<enum name='kVK_Space' value='49'/>
+<enum name='kVK_Tab' value='48'/>
+<enum name='kVK_UpArrow' value='126'/>
+<enum name='kVK_VolumeDown' value='73'/>
+<enum name='kVK_VolumeUp' value='72'/>
+<enum name='kVerticalTabCharCode' value='11'/>
+<enum name='keyCodeMask' value='65280'/>
+<enum name='keyDown' value='3'/>
+<enum name='keyDownMask' value='8'/>
+<enum name='keyUp' value='4'/>
+<enum name='keyUpMask' value='16'/>
+<enum name='mDownMask' value='2'/>
+<enum name='mUpMask' value='4'/>
+<enum name='mouseDown' value='1'/>
+<enum name='mouseMovedMessage' value='250'/>
+<enum name='mouseUp' value='2'/>
+<enum name='networkEvt' value='10'/>
+<enum name='networkMask' value='1024'/>
+<enum name='nullEvent' value='0'/>
+<enum name='optionKey' value='2048'/>
+<enum name='optionKeyBit' value='11'/>
+<enum name='osEvt' value='15'/>
+<enum name='osEvtMessageMask' value='-16777216'/>
+<enum name='osMask' value='32768'/>
+<enum name='resumeFlag' value='1'/>
+<enum name='rightControlKey' value='32768'/>
+<enum name='rightControlKeyBit' value='15'/>
+<enum name='rightOptionKey' value='16384'/>
+<enum name='rightOptionKeyBit' value='14'/>
+<enum name='rightShiftKey' value='8192'/>
+<enum name='rightShiftKeyBit' value='13'/>
+<enum name='shiftKey' value='512'/>
+<enum name='shiftKeyBit' value='9'/>
+<enum name='suspendResumeMessage' value='1'/>
+<enum name='updateEvt' value='6'/>
+<enum name='updateMask' value='64'/>
+<function name='Button'>
+<retval type='B'/>
+</function>
+<function name='FlushEvents'>
+<arg type='S'/>
+<arg type='S'/>
+</function>
+<function name='GetKeys'>
+<arg type='[4{BigEndianUInt32=I}]'/>
+</function>
+<function name='IsCmdChar'>
+<arg type='^{EventRecord=SLI{Point=ss}S}'/>
+<arg type='s'/>
+<retval type='B'/>
+</function>
+<function name='LMGetKbdLast'>
+<retval type='C'/>
+</function>
+<function name='LMGetKbdType'>
+<retval type='C'/>
+</function>
+<function name='LMGetKeyRepThresh'>
+<retval type='s'/>
+</function>
+<function name='LMGetKeyThresh'>
+<retval type='s'/>
+</function>
+</signatures>
BIN  source/vendor/hotkeys-0.1.2/lib/hotkeys/support/bundles/shortcut.bundle
Binary file not shown
104 source/vendor/hotkeys-0.1.2/lib/hotkeys/support/bundles/shortcut.m
@@ -0,0 +1,104 @@
+// Copyright (C) 2011 by Robert Lowe <rob[!]iblargz.com>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+// shortcut.m
+//
+// Credits to chapados for putting some of this together some time ago
+// https://gist.github.com/114521
+//
+// I was only able to hack this a small bit to get it working with macruby
+// If anyone is able to kick my butt in Objective+C/MacRuby, please do it, I could learn from you!
+//
+// It's pretty ugly.
+//
+// TODO:
+// * Unregister binded keys!
+// * Handle signatures?
+//
+// compile:
+// gcc shortcut.m -o shortcut.bundle -g -framework Foundation -framework Carbon -framework Cocoa -dynamiclib -fobjc-gc -arch i386 -arch x86_64
+
+#import <Foundation/Foundation.h>
+#import <Cocoa/Cocoa.h>
+#import <Carbon/Carbon.h>
+
+@interface Shortcut : NSObject
+{
+ id delegate;
+}
+@property (assign) id delegate;
+- (int) addShortcut: (int)code withKeyModifier:(int)modifier;
+- (void) hotkeyWasPressed: (NSString*)hotKeyID;
+@end
+OSStatus myHotKeyHandler(EventHandlerCallRef nextHandler, EventRef anEvent, void *userData);
+
+
+@implementation Shortcut
+@synthesize delegate;
+
+OSStatus myHotKeyHandler(EventHandlerCallRef nextHandler, EventRef anEvent, void *userData)
+{
+ if ( userData != NULL ) {
+ id delegate = (id)userData;
+ if ( delegate ) {
+ EventHotKeyID myHotKeyID;
+ GetEventParameter(anEvent, kEventParamDirectObject, typeEventHotKeyID,NULL,
+ sizeof(myHotKeyID),NULL,&myHotKeyID);
+
+ NSString* ObjectiveFail = [NSString stringWithFormat:@"%i", myHotKeyID.id];
+
+ [delegate hotkeyWasPressed: ObjectiveFail]; // I don't why but macruby get crashy when I returned an int so, whatever, to_i that sh-t.
+ }
+ }
+ return noErr;
+}
+
+- (int) addShortcut: (int)code withKeyModifier:(int)modifier
+{
+ static UInt32 _id = 0;
+
+ EventHotKeyRef myHotKeyRef;
+ EventHotKeyID myHotKeyID;
+
+ EventTypeSpec eventType;
+ eventType.eventClass=kEventClassKeyboard;
+ eventType.eventKind=kEventHotKeyPressed;
+
+ EventTargetRef eventTarget = (EventTargetRef) GetEventMonitorTarget(); //GetApplicationEventTarget()?
+
+ myHotKeyID.signature='mhk1';
+ myHotKeyID.id=_id++;
+
+ if ( delegate == nil )
+ delegate = self;
+
+ InstallEventHandler(eventTarget, &myHotKeyHandler, 1, &eventType, (void *)delegate, NULL);
+ RegisterEventHotKey(code, modifier, myHotKeyID, eventTarget, 0, &myHotKeyRef);
+
+ return (int)myHotKeyID.id;
+}
+
+- (void) hotkeyWasPressed: (NSString*)hotKeyID {
+ NSLog(@"Shh, you shouldn't see me!");
+};
+
+@end
+
+void Init_shortcut(void) {}
188 source/vendor/hotkeys-0.1.2/lib/hotkeys/support/keys.rb
@@ -0,0 +1,188 @@
+# Copyright (C) 2011 by Robert Lowe <rob[!]iblargz.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+load_bridge_support_file File.dirname(__FILE__) + '/bridgesupport/Events.bridgesupport'
+
+class HotKeys
+ module Support
+ class Keys
+
+ # Returns an array with [keyCode, keyModifier]
+ #
+ # Examples:
+ # Keys.parse("A")
+ # => [0, 0]
+ # Keys.parse("A+COMMAND")
+ # => [0, 256]
+ # Keys.parse("A+COMMAND+OPTION")
+ # => [0, 2304]
+ # Keys.parse("B+COMMAND+OPTION+SHIFT")
+ # => [11, 2816]
+ #
+ # Note: A is literally 0 on the virtual keyboard, these virtual mappings are dumb-founding
+ # Why they couldn't simply be ASCII or Unicode; what the sh-t steve?
+ #
+ def self.parse(keyString)
+ keyModifiers = HotKeys::Support::Keys::Mappings.select {|key, value| key =~ /Key$/ }
+ keyCodes = HotKeys::Support::Keys::Mappings.select {|key, value| !(key =~ /Key$/) }
+
+ keyModifiersRegexPart = keyModifiers.map {|key, value| key.gsub(/Key$/, "")}.sort_by {|x| x.length}.reverse.join("|")
+ keyModifiers = keyModifiers.map {|key, value| {key.gsub(/Key$/, "").to_s.downcase => value}}.inject({}) {|retval,hash| retval.merge(hash) }
+ keyCodesRegexPart = keyCodes.map {|key, value| key }.sort_by {|x| x.length}.reverse.join("|")
+ keyCodes = keyCodes.map {|key, value| {key.to_s.downcase => value}}.inject({}) {|retval,hash| retval.merge(hash) }
+
+ keyRegexString = "(#{keyCodesRegexPart})[\+]?(#{keyModifiersRegexPart})?[\+]?(#{keyModifiersRegexPart})?[\+]?(#{keyModifiersRegexPart})?"
+ matchData = Regexp.new(keyRegexString, true).match(keyString)
+
+ if matchData && matchData[1] && keyCodes[matchData[1].downcase]
+ [
+ keyCodes[matchData[1].downcase],
+ ((matchData[2]) ? keyModifiers[matchData[2].downcase] : 0) +
+ ((matchData[3]) ? keyModifiers[matchData[3].downcase] : 0) +
+ ((matchData[4]) ? keyModifiers[matchData[4].downcase] : 0)
+ ]
+ else
+ raise "Cannot parse hotkey #{keyString}. /#{keyRegexString}/i"
+ end
+ end
+
+ # This doesn't map all the virtual keyboard keys, just the "useful" ones,
+ # submit an issue on github if you think it should include more
+ Mappings = {
+ :ControlKey => ControlKey,
+ :OptionKey => OptionKey,
+ :CommandKey => CmdKey,
+ :ShiftKey => ShiftKey,
+ :RightOptionKey => RightOptionKey,
+ :RightControlKey => RightControlKey,
+ :RightShiftKey => RightShiftKey,
+ :Num0 => KVK_ANSI_0,
+ :Num1 => KVK_ANSI_1,
+ :Num2 => KVK_ANSI_2,
+ :Num3 => KVK_ANSI_3,
+ :Num4 => KVK_ANSI_4,
+ :Num5 => KVK_ANSI_5,
+ :Num6 => KVK_ANSI_6,
+ :Num7 => KVK_ANSI_7,
+ :Num8 => KVK_ANSI_8,
+ :Num9 => KVK_ANSI_9,
+ :Keypad0 => KVK_ANSI_Keypad0,
+ :Keypad1 => KVK_ANSI_Keypad1,
+ :Keypad2 => KVK_ANSI_Keypad2,
+ :Keypad3 => KVK_ANSI_Keypad3,
+ :Keypad4 => KVK_ANSI_Keypad4,
+ :Keypad5 => KVK_ANSI_Keypad5,
+ :Keypad6 => KVK_ANSI_Keypad6,
+ :Keypad7 => KVK_ANSI_Keypad7,
+ :Keypad8 => KVK_ANSI_Keypad8,
+ :Keypad9 => KVK_ANSI_Keypad9,
+ :KeypadClear => KVK_ANSI_KeypadClear,
+ :KeypadDecimal => KVK_ANSI_KeypadDecimal,
+ :KeypadDivide => KVK_ANSI_KeypadDivide,
+ :KeypadEnter => KVK_ANSI_KeypadEnter,
+ :KeypadEquals => KVK_ANSI_KeypadEquals,
+ :KeypadMinus => KVK_ANSI_KeypadMinus,
+ :KeypadMultiply => KVK_ANSI_KeypadMultiply,
+ :KeypadPlus => KVK_ANSI_KeypadPlus,
+ :A => KVK_ANSI_A,
+ :B => KVK_ANSI_B,
+ :C => KVK_ANSI_C,
+ :D => KVK_ANSI_D,
+ :E => KVK_ANSI_E,
+ :F => KVK_ANSI_F,
+ :G => KVK_ANSI_G,
+ :H => KVK_ANSI_H,
+ :I => KVK_ANSI_I,
+ :J => KVK_ANSI_J,
+ :K => KVK_ANSI_K,
+ :L => KVK_ANSI_L,
+ :M => KVK_ANSI_M,
+ :N => KVK_ANSI_N,
+ :O => KVK_ANSI_O,
+ :P => KVK_ANSI_P,
+ :Q => KVK_ANSI_Q,
+ :R => KVK_ANSI_R,
+ :S => KVK_ANSI_S,
+ :T => KVK_ANSI_T,
+ :U => KVK_ANSI_U,
+ :V => KVK_ANSI_V,
+ :W => KVK_ANSI_W,
+ :X => KVK_ANSI_X,
+ :Y => KVK_ANSI_Y,
+ :Z => KVK_ANSI_Z,
+ :F1 => KVK_F1,
+ :F2 => KVK_F2,
+ :F3 => KVK_F3,
+ :F4 => KVK_F4,
+ :F5 => KVK_F5,
+ :F6 => KVK_F6,
+ :F7 => KVK_F7,
+ :F8 => KVK_F8,
+ :F9 => KVK_F9,
+ :F10 => KVK_F10,
+ :F11 => KVK_F11,
+ :F12 => KVK_F12,
+ :F13 => KVK_F13,
+ :F14 => KVK_F14,
+ :F15 => KVK_F15,
+ :F16 => KVK_F16,
+ :F17 => KVK_F17,
+ :F18 => KVK_F18,
+ :F19 => KVK_F19,
+ :F20 => KVK_F20,
+ :Space => KVK_Space,
+ :Tab => KVK_Tab,
+ :Return => KVK_Return,
+ :Minus => KVK_ANSI_Minus,
+ :Equal => KVK_ANSI_Equal,
+ :Backslash => KVK_ANSI_Backslash,
+ :Slash => KVK_ANSI_Slash,
+ :LeftBracket => KVK_ANSI_LeftBracket,
+ :RightBracket => KVK_ANSI_RightBracket,
+ :Comma => KVK_ANSI_Comma,
+ :Period => KVK_ANSI_Period,
+ :Semicolon => KVK_ANSI_Semicolon,
+ :Quote => KVK_ANSI_Quote,
+ :Function => KVK_Function,
+ :Home => KVK_Home,
+ :PageUp => KVK_PageUp,
+ :PageDown => KVK_PageDown,
+ :End => KVK_End,
+ :ForwardDelete => KVK_ForwardDelete,
+ :Delete => KVK_Delete,
+ :CapsLock => KVK_CapsLock,
+ :LeftArrow => KVK_LeftArrow,
+ :UpArrow => KVK_UpArrow,
+ :DownArrow => KVK_DownArrow,
+ :RightArrow => KVK_RightArrow,
+ :Escape => KVK_Escape,
+ :Grave => KVK_ANSI_Grave,
+ :Help => KVK_Help,
+ :VolumeDown => KVK_VolumeDown,
+ :VolumeUp => KVK_VolumeUp,
+ :Mute => KVK_Mute,
+ :KeyDown => KeyDown,
+ :KeyUp => KeyUp,
+ :MouseDown => MouseDown,
+ :MouseUp => MouseUp
+ }
+ end
+ end
+end
30 source/vendor/hotkeys-0.1.2/lib/hotkeys/version.rb
@@ -0,0 +1,30 @@
+# Copyright (C) 2011 by Robert Lowe <rob[!]iblargz.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+class Hotkeys
+ module Version
+ MAJOR = 0
+ MINOR = 1
+ PATCH = 2
+ BUILD = nil
+
+ STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.