Hello
It's been a while and I'm not sure you're still listenting to or interested in this.
Anyway I finally had some time to play with and come up with an implementation via system services.
There are three files to make the service, whose codes are listed below.
• main.m (wrapper executable)
• main.rb (service body)
• make (bash script to make .service from source)
# Recipe
1) Create the three plain text files (in utf8) by copy-pasting codes listed below and place them loose in a new directory, e.g., ~/Desktop/work.
2) Issue the following commands in Terminal, which yield a package named TextHighlightServices.service in ~/Desktop/work.
* You need to have gcc installed, which comes with Xcode.
cd ~/Desktop/work
chmod ug+x make
./make
3) Move the TextHighlightServices.service to ~/Library/Serivecs
4) Log out and log in to let the new service be recognised.
(This may not be necessary for .service, but I'd play for safety. Also you may use the following command in Terminal instead of re-login, if necessary:
# in 10.5
/System/Library/CoreServices/pbs
# in 10.6
/System/Library/CoreServices/pbs -flush
5) Done.
# Usage
If everything goes well, you'll see two services named "Text Highlight - Add" and "Text Highlight - Remove" in Services menu in TextEdit when you select some text in rich text mode. (In 10.6, services only appear in its applicable context, while in 10.5, they may yet appear in non-applicable context and be grayed-out.)
Invoke the serivces on selected rich text via menu or keyboard shortcuts and see if it works. Keyboard shortcuts are currently defined as Command-9 for highlight and Command-0 for unhighlight.
You may change the menu item names, keyboard shortcuts and highlight colour by changing the values in the #properties section at the beginning of make file. (Or you may directly edit Info.plist file in the package. But 'make' is instant and it'll be faster to re-make the package than to edit it manually)
# Notes
• Highlight and unhighlight are implemented as two services and you cannot assign one keyboard shortcut, e.g., Command-Y to both to toggle highlight state. (One 'toggle highlight service' is possible but you need to re-write the code as such)
• Keyboard shortcut is case-sensitive. E.g., J means Command-Shift-j.
• If keyboard shortcut is ignored, it is most likely that the key is already used for something else.
• This service is written to terminate itself if idle for ca. 20 seconds. It will be re-launched on demand. To keep the service running is not a problem, usually, but I noticed this rubycocoa code constantly uses ca 1.1% CPU and decided not to let it waste energy. If you rewrite the code in Objective-C proper, it would be 0.0% CPU usage in idle.
• Tested with 10.5.8 and 10.6.5.
# Files
main.m
------------------------------------------------------
//
// file
// main.m
//
// function
// wrapper executable to run Contents/Resources/main.rb
//
// compile
// gcc -framework RubyCocoa -o PROGNAME main.m
//
#import <RubyCocoa/RBRuntime.h>
int main(int argc, const char *argv[])
{
return RBApplicationMain("main.rb", argc, argv);
}
------------------------------------------------------
main.rb
------------------------------------------------------
#
# file
# main.rb
#
# function
# to provide text highlight services
#
require 'osx/cocoa'
include OSX
class Services < NSObject
attr_reader :last_invoked
def init()
@last_invoked = NSDate.date
self
end
# utility method
def highlight_pboard_colour(option, pboard, colr)
# int option : 0 = remove bg colour, 1 = add bg colour
# NSPastedBoard *pboard : pasteboard passed via service
# NSColor colr : background colour for highlight
raise ArgumentError, "invalid option: #{option}" unless [0,1].include? option
@last_invoked = NSDate.date
# read rtf data from clipboard
rtf = pboard.dataForType('public.rtf')
# make mutable attributed string from rtf data
docattr = OCObject.new
mas = NSMutableAttributedString.alloc.objc_send(
:initWithRTF, rtf,
:documentAttributes, docattr)
rase ArgumentError, "zero-length rtf is given" if mas.length == 0
# add or remove background colour attribute
r = NSMakeRange(0, mas.length)
if option == 1
mas.objc_send(
:addAttribute, NSBackgroundColorAttributeName,
:value, colr,
:range, r)
elsif option == 0
mas.objc_send(
:removeAttribute, NSBackgroundColorAttributeName,
:range, r)
end
mas.fixAttributesInRange(r)
# make rtf data from mutable attributed string
rtf = mas.objc_send(
:RTFFromRange, r,
:documentAttributes, docattr)
# write rtf data to the clipboard
pboard.objc_send(
:declareTypes, ['public.rtf'],
:owner, nil)
pboard.objc_send(
:setData, rtf,
:forType, 'public.rtf')
end
# service method 1 : highlight rtf
def highlight_userData_error(pboard, udata, error)
# NSPastedBoard *pboard : pasteboard passed via service
# NSString *udata : space delimited string of red, green, blue, alpha components to define background colour
# NSString **error
# set background colour from given user data
r, g, b, a = udata.to_s.split(/\s+/).map {|x| x.to_f}
colr = NSColor.objc_send(
:colorWithCalibratedRed, r,
:green, g,
:blue, b,
:alpha, a)
# add background colour to rtf
self.highlight_pboard_colour(1, pboard, colr)
end
# register ruby method as objc method
objc_method(:highlight_userData_error, 'v@:@@^@')
# service method 2 : un-highlight rtf
def unhighlight_userData_error(pboard, udata, error)
# NSPastedBoard *pboard : pasteboard passed via service
# NSString *udata : space delimited string of red, green, blue, alpha components to define background colour
# NSString **error
self.highlight_pboard_colour(0, pboard, nil)
end
# register ruby method as objc method
objc_method(:unhighlight_userData_error, 'v@:@@^@')
end
class AppDelegate < NSObject
def init()
@services = Services.alloc.init
@timer = NSTimer.objc_send(
:timerWithTimeInterval, 5.0, # periodic check interval [sec]
:target, self,
:selector, :idle,
:userInfo, nil,
:repeats, true)
@TTL = 20.0 # time to live in idle [sec]
self
end
def applicationDidFinishLaunching(notif)
NSApp.setServicesProvider(@services)
NSRunLoop.currentRunLoop.objc_send(
:addTimer, @timer,
:forMode, NSDefaultRunLoopMode)
end
def applicationShouldTerminate(sender)
@timer.invalidate
NSTerminateNow
end
def idle(timer)
if @services.last_invoked.timeIntervalSinceNow < -@TTL
NSApp.terminate(self)
end
end
end
def main
app = NSApplication.sharedApplication
delegate = AppDelegate.alloc.init
app.setDelegate(delegate)
app.run
end
if __FILE__ == $PROGRAM_NAME # = if this file is the primary ruby program currently executed
main
end
------------------------------------------------------
make
------------------------------------------------------
#!/bin/bash
#
# file
# make
#
# function
# to make PROGNAME.service package from main.m and main.rb
#
export LC_ALL=en_GB.UTF-8
# properties
PROGNAME="TextHighlightServices"
EXECPATH="${PROGNAME}.service/Contents/MacOS/${PROGNAME}"
BNDL_TYPE="APPL"
BNDL_SIGNATURE="????"
BNDL_VERSION="1.0"
BUILD_VERSION="1"
SRC_VERSION="10000"
HIGHLIGHT_MENU_ITEM="Text Highlight - Add"
HIGHLIGHT_KEY="9"
UNHIGHLIGHT_MENU_ITEM="Text Highlight - Remove"
UNHIGHLIGHT_KEY="0"
HIGHLIGHT_COLOUR="1.0 1.0 0.6 1.0" # R B G A in [0.0, 1.0]
# make package directories
rm -rf "${PROGNAME}".service
mkdir -p "${PROGNAME}".service/Contents/{MacOS,Resources}
# make PkgInfo
cat <<EOF > "${PROGNAME}.service/Contents/PkgInfo"
${BNDL_TYPE}${BNDL_SIGNATURE}
EOF
# make Info.plist
cat <<EOF > "${PROGNAME}".service/Contents/Info.plist
<?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>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${PROGNAME}</string>
<key>CFBundleIdentifier</key>
<string>bubo-bubo.${PROGNAME}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PROGNAME}</string>
<key>CFBundlePackageType</key>
<string>${BNDL_TYPE}</string>
<key>CFBundleShortVersionString</key>
<string>${BNDL_VERSION}</string>
<key>CFBundleSignature</key>
<string>${BNDL_SIGNATURE}</string>
<key>CFBundleVersion</key>
<string>${BNDL_VERSION}</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSServices</key>
<array>
<dict>
<key>NSPortName</key>
<string>${PROGNAME}</string>
<key>NSMessage</key>
<string>highlight</string>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>${HIGHLIGHT_MENU_ITEM}</string>
</dict>
<key>NSKeyEquivalent</key>
<dict>
<key>default</key>
<string>${HIGHLIGHT_KEY}</string>
</dict>
<key>NSSendTypes</key>
<array>
<string>NSRTFPboardType</string>
</array>
<key>NSReturnTypes</key>
<array>
<string>NSRTFPboardType</string>
</array>
<key>NSTimeout</key>
<string>10000</string>
<key>NSUserData</key>
<string>${HIGHLIGHT_COLOUR}</string>
<key>NSRequiredContext</key>
<dict>
<key>NSApplicationIdentifier</key>
<array>
<string>com.apple.TextEdit</string>
</array>
</dict>
</dict>
<dict>
<key>NSPortName</key>
<string>${PROGNAME}</string>
<key>NSMessage</key>
<string>unhighlight</string>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>${UNHIGHLIGHT_MENU_ITEM}</string>
</dict>
<key>NSKeyEquivalent</key>
<dict>
<key>default</key>
<string>${UNHIGHLIGHT_KEY}</string>
</dict>
<key>NSSendTypes</key>
<array>
<string>NSRTFPboardType</string>
</array>
<key>NSReturnTypes</key>
<array>
<string>NSRTFPboardType</string>
</array>
<key>NSTimeout</key>
<string>10000</string>
<key>NSRequiredContext</key>
<dict>
<key>NSApplicationIdentifier</key>
<array>
<string>com.apple.TextEdit</string>
</array>
</dict>
</dict>
</array>
<key>NSUIElement</key>
<string>1</string>
</dict>
</plist>
EOF
# make version.plist
cat <<EOF > "${PROGNAME}".service/Contents/version.plist
<?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>BuildVersion</key>
<string>${BUILD_VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${BNDL_VERSION}</string>
<key>CFBundleVersion</key>
<string>${BNDL_VERSION}</string>
<key>ProjectName</key>
<string>${PROGNAME}</string>
<key>SourceVersion</key>
<string>${SRC_VERSION}</string>
</dict>
</plist>
EOF
# make ServicesMenu.strings
mkdir -p "${PROGNAME}".service/Contents/Resources/English.lproj
cat <<EOF > "${PROGNAME}".service/Contents/Resources/English.lproj/ServicesMenu.strings
/* Services menu item to highlight or unhighlight the selected text */
"${HIGHLIGHT_MENU_ITEM}" = "${HIGHLIGHT_MENU_ITEM}";
"${UNHIGHLIGHT_MENU_ITEM}" = "${UNHIGHLIGHT_MENU_ITEM}";
EOF
# copy *.rb in Contents/Resoures
ditto *.rb "${PROGNAME}".service/Contents/Resources/
# compile wrapper executable
gcc -framework RubyCocoa -o "${EXECPATH}" main.m
------------------------------------------------------
It is fun to play with rubycocoa.
And glad if this helps.
Good luck,
H
cf.
http://developer.apple.com/DOCUMENTATION/Cocoa/Conceptual/SysServices/SysService s.pdf
Message was edited by: Hiroto ( fixed 'make' to './make' in Recipe 2, sorry)