Keyboard shortcut directly to stay-open script handler possible?

Hi!
My first post here, and thank you for this wonderful resource of Applescript knowledge :slight_smile:

Now to my problem: I’m a translator, and I wrote a script/Automator workflow (with the help of many posts in this forum) which acts as a kind of glossary/dictionary:
I have a simple tab-delimited TXT file with glossary terms, such as:
carAuto
(for e.g. English to German)

So, when I select a word (or a longer string) in any app, I press a keyboard shortcut, which calls my script, which does the following:

  1. checks if the selected string exists in the glossary file
  2. if it does, it returns the translation (second column) (pastes it over the original selection)
  3. if the selected string is not in the glossary file, a dialog opens asking you to enter a new translation (which is then added to glossary file, and also pasted OVER the original string)
  4. quit script

The script works fine (I can provide the complete listing, if anyone is interested) - but I would like to improve it, such as that it runs as a stay-open script, doing the following:

  1. when started, the script would ask the user to select some options, such as:
  • name and location of glossary TXT file
  • which type of matching to use (“exact match” or e.g. “regex-type”)
  • whether to also show the results as a notification
  • etc…
  1. then, after the settings have been entered (in a custom dialog with some checkboxes, etc.), the script would be hidden and wait for input - as usual, by a keyboard shortcut (?)

(i.e. I want to set the options just once, when the script is started!)

So, I know this could be done by actually having TWO scripts:

  1. turn my existing glossary script into a main (stay-open) script, as described above, which would do the initial options setup via dialog as described above, and a separate handler for the rest of the stuff (searching and returning the result, etc. - which is the script as I have it now)
  2. second script, which is started via keyboard shortcut, which would just copy the selected text/string and send it to the first (stay-open) script handler, which will do its stuff and return the result…

But, I would like to have my glossary script available to others for download, and having this in two scripts, setting up the keyboard shortcut, etc., might be a bit complicated for non tech-savvy users… It would be better to have just ONE script / Automator workflow…

Is it possible to have the process as I described it above (initial setup of options which is done only once, when the stay-open script is started) in just ONE script?

I guess I would need a possibility to invoke only a certain part (handler) of the stay-open script via a keyboard shortcut…

But, from the information I managed to find so far, it does not seem that this is possible - i.e. to have a keyboard shortcut interact directly with a handler of a stay-open script…

Is there a way this could be done within a single script - or do I have to resort to having two scripts, as I described above?

Thanks in advance for any help…
Denis

Model: Mac Mini
AppleScript: 2.7
Browser: Firefox 68.0
Operating System: macOS 10.14

This is how you can build custom dialogs in the usual Script Editor, without xCode (there are not some handlers for handling actions, but this example is demonstrative and already does something):

use AppleScript version "2.5"
use scripting additions
use framework "Foundation"
use framework "AppKit"

property NSView : a reference to current application's NSView
property NSScreen : a reference to current application's NSScreen
property NSButton : a reference to current application's NSButton
property NSWindow : a reference to current application's NSWindow
property NSRectEdgeMaxX : a reference to current application's NSRectEdgeMaxX
property NSWindowController : a reference to current application's NSWindowController
property NSTitledWindowMask : a reference to current application's NSTitledWindowMask
property NSRoundedBezelStyle : a reference to current application's NSRoundedBezelStyle
property NSNormalWindowLevel : a reference to current application's NSNormalWindowLevel
property NSBackingStoreBuffered : a reference to current application's NSBackingStoreBuffered
property NSMomentaryLightButton : a reference to current application's NSMomentaryLightButton

set aScreen to NSScreen's mainScreen()
set aFrame to {{0, 0}, {600, 200}}
set aBacking to NSTitledWindowMask
set aDefer to NSBackingStoreBuffered

-- create window
set aWin to NSWindow's alloc()
(aWin's initWithContentRect:aFrame styleMask:aBacking backing:aDefer defer:false screen:aScreen)
aWin's setTitle:"You are welcome!  Please, set your preferences here."
aWin's setDelegate:me
aWin's setDisplaysWhenScreenProfileChanges:true
aWin's setHasShadow:true
aWin's setIgnoresMouseEvents:false
aWin's setLevel:(NSNormalWindowLevel)
aWin's setOpaque:false
aWin's setAlphaValue:1.0
aWin's setReleasedWhenClosed:true
aWin's |center|()
aWin's makeKeyAndOrderFront:(me)

-- Create NSWindowController
set wController to NSWindowController's alloc()
wController's initWithWindow:aWin
wController's showWindow:me

-- create custom view
set aNSV to NSView's alloc()'s initWithFrame:(current application's NSMakeRect(0, 0, 600, 200))
aNSV's setNeedsDisplay:true

-- add custom view to window
aWin's setContentView:aNSV

-- create control (here is a button)
set bButton to (NSButton's alloc()'s initWithFrame:(current application's NSMakeRect(150, 0, 300, 60)))
bButton's setTitle:"OK"
bButton's setButtonType:(NSMomentaryLightButton)
bButton's setBezelStyle:(NSRoundedBezelStyle)
bButton's setKeyEquivalent:(return)
bButton's setTarget:me
bButton's setAction:("clicked:")

-- add control (here is a button) to custom view
aNSV's addSubview:bButton

-- create other controls

-- add other controls to custom view

-- handlers to process control's actions

The example should be run on the main thread (with shortcut Command+Control-R of Script editor).
There is also a Dialog Toolkit, but I could not find complete documentation with clear examples.

Thanks for the great example of a custom dialog, but that was not my question :slight_smile:

Let me rephrase the original question:

Is it possible to have a stay-open script, in which a particular handler can be “invoked” (called) directly by a keyboard shortcut?

For example (pseudo-code):
[format]
on run

set options from a dialog

end run

on CalledFromShortcut

search for glossary term and return result

end CalledFromShortcut[/format]

In such a stay-open script, would it be possible to assign a keyboard shortcut to call the “CalledFromShortcut” handler from the running script directly, without running the whole script from the beginning (since it’s already open and running)?

Sorry, I hope this makes it a bit clearer :slight_smile:

I know that I can have such stay-open script handler called by another script (to which a keyboard shortcut can be assigned) - but I would like to avoid having TWO scripts…

on run
	-- set options from a dialog
	repeat
		NoShortcutHandler(Parametr1, parametr2...)
	end repeat
end run

on NoShortcutHandler(Parametr1, parametr2...)
	# search for glossary term and return result
end NoShortcutHandler
  1. Save script as application.
  2. Quitting translation - manually, using usual menu item Quit of application, when you need.
  3. Works fine as script too.

Show your сode?

Here is my Automator workflow (“workflow receives: NO INPUT” in “any application”)
This workflow is assigned a keyboard shortcut in System Preferences > Keyboard > Shortcuts.
It works by first selecting some text (in any app), and then pressing the assigned shortcut to search for the selected text in the glossary file:

Run AppleScript:


# AppGloss - glossary script

on run {input, parameters}
	
	# here we set the glossary file name and location:
	set myGlossaryPath to "~/Desktop/AppGloss.txt"
	
	# copy old clipboard!
	set oldClip to the clipboard
	delay 0.1 -- make sure clipboard was copied properly
	
	# now we copy the selection
	tell application "Finder" to set frontApp to (name of 1st process whose frontmost = true)
	tell application frontApp
		activate
		tell application "System Events" to keystroke "c" using {command down}
		delay 0.1 -- Without this, the clipboard may have stale data
	end tell
	
	set myOriginalSearchTerm to the clipboard
	
	# let's see the case of the first character ("alpha"):
	set alpha to the first character of the myOriginalSearchTerm
	
	set the_numAlpha to ASCII number alpha
	if the_numAlpha > 96 and the_numAlpha < 123 or (the_numAlpha > 223 and the_numAlpha < 439) then
		set SrcCase to "LC"
	else
		set SrcCase to "UC"
	end if
	
	# now turn mySearchTerm to lowercase
	set mySearchTerm to (do shell script "echo " & quoted form of myOriginalSearchTerm & " | tr '[A-Z]' '[a-z]' ")
	
	
	if mySearchTerm is not "" then
		set myCommand to "awk -F\\t " & "'tolower($1) == \"" & mySearchTerm & "\" {print $2}' " & myGlossaryPath
		# the above turns all searched text to lowercase, and searches for EXACT (case-sensitive) result
		
		set mySearchResult to do shell script myCommand
		
		if mySearchResult is "" then
			set the clipboard to oldClip -- for pasting previously copied translation
			set ResText to the text returned of (display dialog "Exact match not found!\nEnter translation for \"" & myOriginalSearchTerm & "\":" default answer "")
			if ResText is "" then
				display dialog "Please enter translation!\nAborting!"
				return
			else
				set new_line to myOriginalSearchTerm & tab & ResText
				set new_line to new_line as Unicode text
				do shell script "echo " & quoted form of new_line & ">> " & myGlossaryPath -- write new translation to glossary
			end if
			
			
		else
			set ResText to mySearchResult
		end if
	end if
		
	delay 0.01
	
		
	# here is the checkCase routine, to apply the same initial character case as in "mySearchTerm":
	# let's see the case of the first character ("beta"):
	set beta to the first character of the ResText
	
	set the_numBeta to ASCII number beta
	if the_numBeta > 96 and the_numBeta < 123 or (the_numBeta > 223 and the_numBeta < 439) then
		set TargCase to "LC"
	else
		set TargCase to "UC"
	end if
	
	
	# now the comparison of SrcCase and TargCase
	if SrcCase = TargCase then
		# nothing to do, they are equal!
		#display dialog "Case is the same!"
	else if SrcCase = "UC" then -- source is uppercase, we need to turn target to uppercase, too
		# turn ResText to UPPERCASE:
		set ResText to changeCaseOfText(ResText, "UC")
	else if SrcCase = "LC" then --- source is lowercase
		# turn ResText to lowercase:
		set ResText to changeCaseOfText(ResText, "LC")
		
	end if
	
	
	set the clipboard to ResText
	
	delay 0.02 -- Without this delay, may restore clipboard before pasting
	
	tell application frontApp
		activate
		tell application "System Events" to keystroke "v" using {command down}
	end tell
	
	
end run


# here is the case changer handler (changes only the case of the first character of the string):
on changeCaseOfText(theText, theCaseToSwitchTo)
	set theRest to (characters 2 thru -1 of theText) as string
	set InitChar to (character 1 of theText) as string
	
	if theCaseToSwitchTo contains "LC" then
		set ChangedChar to (do shell script "echo " & quoted form of InitChar & " | tr '[A-Z]' '[a-z]' ")
		set theAlteredText to ChangedChar & theRest
		
	else if theCaseToSwitchTo contains "UC" then
		set ChangedChar to (do shell script "echo " & quoted form of InitChar & " | tr '[a-z]' '[A-Z]' ")
		set theAlteredText to ChangedChar & theRest
	else
		return theText
	end if
	
	return theAlteredText
end changeCaseOfText


This Automator workflow works just fine, the way I want it, and it’s OK for my purposes - but I would like to add a possibility of setting some options when the script is invoked (only on the first run).

NOTE: this script is not for “automatic” translation (processing of all words in a text), which your solution might imply. The script is invoked only occasionally.

For an illustration, here is a youtube video tutorial for a similar script I wrote in Autohotkey for Windows (the video is in Croatian, but it clearly shows what happens and how the script is used):
https://www.youtube.com/watch?v=XYsKNBP82Po

I would like to more or less copy that functionality. I have the skeleton functionality, as listed in the code above, but I want to turn it into stay-open app, where the search is activated only when actually invoked by a special keyboard shortcut.

So, I would like to make it a stay-open script (make it an application), which:

  1. upon starting, would ask the user for the options (glossary name and location via Finder “Open file” dialog) and store these options in variables (see top of the script above, variable “myGlossaryPath”)
  2. stay open and wait for selected text to be sent to it, which is then processed (search in the glossary file) - and return the result (paste over the original selection) - and then the user continues working/typing text… until s/he again selects some text and calls the search via keyboard shortcut.

Your solution above is not what I want - it runs a loop, always with the same text selected :frowning:

I want the script to stay-open in the Dock and wait (doing noting) until called (by a keyboard shortcut?), and then do only the “search” part, without repeating the initial options routine.
When the searched text is found, it returns it, pasting the translation over the selection, and then again waits for the next input, doing nothing…

Model: Mac Mini
AppleScript: 2.7
Browser: Firefox 68.0
Operating System: macOS 10.14

Not the same text. It depends on how you build your code. See my code Translate PDF using “Infix PDF Editor” and “Google Translate”

NOTE 1: No need changing case of first letter. You can search matching item within ignoring case block

NOTE 2: How to check if the clipboard is set or not yet. Not just delay. Again, see my code above, to make your code more stable.

No.

Thanks, Shane.

Short and to the point :slight_smile:

I’ll just get on with my other idea: have one stay-open (main) script with the handler for searching, and another, shorter script, which is run via a keyboard shortcut, which will just pass the selected text to the “search” handler of the main script, and exit…

I know you said you had working code in an action, but two things strike me:

  • The code could be more efficient. You’d be better off with less of the shell script stuff.

  • If you save from Automator as a service, you don’t need the initial copy and later paste – services do that automatically.

Thanks Shane - yes, you are right - it would be better without calling shell stuff :slight_smile:

As you can probably see, I’m more or less a total newbie with Applescript (I get around Linux command tools a bit better…)

I tried to use pure AS, but using shell scripting was way easier - as I said, I’m a total newbie in AS.

As for your second remark, I did try that (without the initial copy & paste): however, the script did not work with applications which do not allow text editing (e.g. Preview with a PDF file open). Strange…

The version with explicit copy (& paste) works also with PDF files.

The purpose is when a translator has e.g. a dictionary in PDF format, s/he can easily and quickly “harvest” some interesting terms directly from the PDF file.

In the end I also changed the above code slightly, so that, when you have a PDF file with e.g. source and target terms in different columns, you can first select target (“target language”) term, press Command-C to copy it, then select the source (“source language”) term, and then press keyboard shortcut to call the script, and both terms are already selected (“target” term is already populated in the “enter translation” text entry field) - so you can just press “OK” (or Enter key on the keyboard), and thus quickly add the pair of terms to the glossary file for future use.

Like I said, the initial “input” variant with Automator didn’t work with PDF files: although I could select text in the PDF, I could not start the script - it would just beep, and do nothing.
This is the code I used initially (without explicit copy - in Automator the “receive selection as text” was selected):



# remove any leading or trailing spaces:
set input to text from first word to last word of (input as text)
# now set the (trimmed) contents of input to our search term:
set mySearchTerm to input


However, that did not work with PDF (non-editable) files.

Once I changed it to explicity “copy” (via clipboard) and set the Automator workflow input to “without input data” , it also started to work with PDF files…
Strange. I thought it would not matter, as long as I can select text in an app.

Anyway, it works now (albeit a bit slower than it could)…
When I get the time, I’ll probably try to change everything to pure AS to gain some speed…

Ah, I guess that’s not totally surprising, if a bit disappointing. In that case I’d put you main code in a stay-open applet, and run the calling script from FastScripts, which is a utility that lets you assign keyboard shortcuts to scripts (free for a small number of shortcuts, and only $15 for unlimited version).

So here’s some code to consider for the stay-open script. It uses one of my script libraries you’ll need to download, and which you can install in the applet itself if that’s more convenient. It might need some tweaking, but should get you started:

use AppleScript version "2.5" -- macOS 10.11 or later
use script "RegexAndStuffLib" version "1.0" -- From https://www.macosxautomation.com/applescript/apps/Script_Libs.html
use scripting additions

property theInfo : missing value
property theFile : missing value

on run
	set my theFile to choose file with prompt "Choose the translation file" of type {"txt"}
	set my theInfo to (read theFile as «class utf8»)
end run

-- for drag-and-drop
on open fileList
	set my theFile to item 1 of fileList
	set my theInfo to (read theFile as «class utf8»)
end open

on lookupTerm(theTerm)
	set lcTerm to lowercase from theTerm
	considering case
		set termIsLower to (lcTerm = theTerm)
	end considering
	-- search
	set escapedTerm to escape for regex lcTerm
	set theFind to regex search once theInfo search pattern "^" & escapedTerm & tab & "(.+)$" capture groups 1 without match case
	if theFind is missing value then
		set ResText to the text returned of (display dialog "Exact match not found!
Enter translation for \"" & theTerm & "\":" default answer "")
		if ResText is "" then
			display dialog "Please enter translation!
Aborting!"
			return ""
		else
			set newLine to linefeed & theTerm & tab & ResText
			write newLine to theFile as «class utf8» starting at eof
		end if
	else
		set ResText to theFind
	end if
	set lcResText to lowercase from ResText
	considering case
		set findIsLower to (lcResText = ResText)
	end considering
	if termIsLower = findIsLower then
		-- do nothing
	else if termIsLower then
		set ResText to lcResText
	else
		set ResText to uppercase from ResText
	end if
	return ResText
end lookupTerm

Your calling script will need to copy, pass the found value, and paste in the result.

(You could skip the library and do the above in AppleScriptObjC code to handle the regex and case stuff, but that’s a bit more complex.)

Wow, thanks Shane!

This is more or less all I needed!
I’ll try it out ASAP (and later probably do a few minor adjustments/additions) :slight_smile:

Thanks again and regards,
Denis

Fastscripts is an excellent app, which I purchased several years ago, although I believe the current price is $25 for the unlimited version.

Inflation :wink: Still great value.

Depending on your requirements, you might also want to do locale-aware case conversions.

I have more or less all the elements I need now (in order to avoid “do shell” invocation)…
I’ll definitely change some stuff around: namely, the case-sensitive issues do not refer to actual searching (I want the actual search to be case-insensitive), but to pasting the found term with the same initial case as the original.
So, e.g. if the searched term was “see”, the pasted result would be “pogledajte” (in Croatian).
However, if the term was “See” (note the uppercase initial character), the pasted result is “Pogledajte” (same initial case, the rest of the string is unchanged).

Namely, I have some terms/strings which are often repeated, sometimes at the beginning of the sentence, sometimes in the middle of sentence, e.g.: “open the Control Panel”…
Examples:
“Open the Control Panel to …blahblah”
and
“In order to set these settings, open the Control Panel and blahblah…”

So, if I have a translation for “open the Control Panel”, I select the text (“open the Control Panel”), and call the script via a keyboard shortcut, BUT: I want the translation to be pasted with the same initial case - so that I don’t have to go back and correct the initial case manually.
Doesn’t seem like a big deal, but it ultimately saves a lot of time on repetitive texts…
The version you so kindly provided turns all of the resulting text either to uppercase or lowercase, which is not what I need - but I can change that easily - just make that part operate only on the first character :slight_smile:

Also, some of my glossaries have several columns, like:
source termtarget termAuthorDomain

  • and your version returns everything afer the first TAB… I need only the second column returned.
    I can take care of that myself, I guess, with a little fiddling (I need to study your RegEx library a bit better…).

Thanks and regards,
Denis